在嵌入式开发中,串口通信是最基础也最常用的外设接口之一。但传统的串口收发方式存在一个痛点:当短时间内需要发送大量数据时,要么会阻塞主程序运行,要么会导致数据丢失。我在开发STM32项目时,就经常遇到这样的困扰。
这个开源项目就是为了解决这个问题而设计的。它实现了一个基于DMA和队列机制的串口通信模块,具有以下核心特点:
这个模块特别适合需要频繁进行串口通信的场景,比如:
传统串口发送方式有两种:
这两种方式在面对突发的大量数据发送时都会有问题。而DMA+队列的方案则完美解决了这些问题:
队列是这个模块的核心,设计时考虑了以下几点:
实际测试表明,在STM32F103@72MHz下,使用这个模块后,连续调用发送函数的耗时从原来的ms级降低到μs级,性能提升显著。
模块使用以下几个核心变量:
c复制// DMA缓冲区
uint8_t Serial_TxBuffer[SERIAL_TX_BUFFER_SIZE]; // DMA发送缓冲区
uint8_t Serial_RxBuffer[SERIAL_RX_BUFFER_SIZE]; // DMA接收缓冲区
// 发送队列
char Serial_TxQueue[SERIAL_TX_QUEUE_SIZE][SERIAL_TX_BUFFER_SIZE]; // 发送队列
uint8_t Serial_TxQueue_Head = 0; // 队列头指针
uint8_t Serial_TxQueue_Tail = 0; // 队列尾指针
// 状态标志
volatile uint8_t Serial_TxBusy = 0; // 发送忙标志
uint8_t Serial_RxFlag = 0; // 接收完成标志
以Serial_SendString()函数为例,其工作流程如下:
参数检查:
DMA状态判断:
队列管理:
DMA发送:
模块使用DMA传输完成中断来自动处理队列中的后续数据:
c复制void DMA1_Channel4_IRQHandler(void)
{
if(DMA_GetITStatus(DMA1_IT_TC4))
{
DMA_ClearITPendingBit(DMA1_IT_TC4);
// 检查队列中是否有待发送数据
if(!Serial_TxQueue_IsEmpty())
{
// 从队列头部取出数据
char* nextMsg = Serial_TxQueue[Serial_TxQueue_Head];
uint16_t length = strlen(nextMsg);
// 更新队列头指针
Serial_TxQueue_Head = (Serial_TxQueue_Head + 1) % SERIAL_TX_QUEUE_SIZE;
// 启动下一次DMA发送
memcpy(Serial_TxBuffer, nextMsg, length);
DMA1_Channel4->CNDTR = length;
DMA1_Channel4->CMAR = (uint32_t)Serial_TxBuffer;
DMA_Cmd(DMA1_Channel4, ENABLE);
}
else
{
// 队列为空,清除忙标志
Serial_TxBusy = 0;
}
}
}
添加文件到工程:
#include "Serial_1.h"初始化串口:
c复制Serial_Init(); // 系统启动时调用一次
发送数据:
c复制// 发送字符串
Serial_SendString("Hello World!\n");
// 格式化输出
Serial_Printf("Temperature: %.1f°C\n", tempValue);
// 发送数组
uint8_t data[] = {0x01, 0x02, 0x03};
Serial_SendArray(data, sizeof(data));
接收数据处理:
c复制if(Serial_RxFlag) // 检查是否有新数据
{
// 处理Serial_RxPacket中的数据
Serial_RxFlag = 0; // 清除标志
}
移植到其他平台时需要注意:
DMA配置:
无DMA的平台:
引脚配置:
对于资源紧张的MCU,可以调整以下参数:
c复制#define SERIAL_TX_BUFFER_SIZE 128 // 减小发送缓冲区大小
#define SERIAL_TX_QUEUE_SIZE 5 // 减少队列深度
默认情况下,队列满时会丢弃新数据。可以根据需求修改为:
覆盖最旧数据:
c复制// 在Serial_SendString()中修改队列满的处理
if(Serial_TxQueue_IsFull()) {
// 覆盖最旧的数据
Serial_TxQueue_Head = (Serial_TxQueue_Head + 1) % SERIAL_TX_QUEUE_SIZE;
}
阻塞等待:
c复制while(Serial_TxQueue_IsFull()) {
// 等待队列有空位
}
模块支持多个串口同时使用,建议:
可能原因及解决方法:
DMA配置错误:
缓冲区溢出:
排查步骤:
常见问题:
在我的开源电动滑板项目中,这个串口模块用于:
遥控器通信:
调试输出:
固件升级:
在一个温湿度监测系统中,模块被用来:
可以扩展模块支持硬件流控制(RTS/CTS):
建议在接收端添加协议解析层:
对于频繁大量数据传输:
在没有DMA的平台上,可以用中断实现类似功能:
发送逻辑:
接收逻辑:
Keil/IAR:
Arduino:
RTOS环境:
验证模块的可靠性:
连续发送测试:
极限长度测试:
队列深度测试:
吞吐量:
CPU占用率:
延迟测试:
对于重要数据,可以实现:
当前实现主要针对字符串,可以扩展:
添加运行统计信息:
优势:
劣势:
优势:
劣势:
利用IO口调试:
打印调试信息:
边界测试:
在开发过程中,通过以下优化显著提升了性能:
memcpy替代循环拷贝:
DMA参数直接配置:
volatile关键字使用:
在STM32F103C8T6上的资源占用:
Flash:
RAM:
CPU:
动态配置:
错误恢复:
功耗优化:
欢迎社区贡献:
移植适配:
功能扩展:
文档完善:
常见问题:
发送缓冲区小于实际数据长度
队列深度不足
错误现象:
正确做法:
风险:
解决方案:
在STM32F103C8T6@72MHz环境下的测试结果:
| 测试项 | 传统方式 | DMA+队列 | 提升 |
|---|---|---|---|
| 发送100字节耗时 | 1.2ms | 15μs | 80倍 |
| 连续发送1KB数据总耗时 | 12.5ms | 0.8ms | 15倍 |
| CPU占用率(持续发送) | 85% | <1% | - |
| 最大连续发送速率 | 800KB/s | 1.2MB/s | 50% |
c复制/**
* @brief 初始化串口模块
* @param 无
* @retval 无
*/
void Serial_Init(void);
c复制/**
* @brief 发送单个字节
* @param Byte: 要发送的字节(0-255)
*/
void Serial_SendByte(uint8_t Byte);
/**
* @brief 发送字节数组
* @param Array: 数组首地址
* @param Length: 数组长度
*/
void Serial_SendArray(uint8_t *Array, uint16_t Length);
/**
* @brief 发送字符串
* @param String: 以'\0'结尾的字符串
*/
void Serial_SendString(char *String);
/**
* @brief 发送数字
* @param Number: 要发送的数字
* @param Length: 显示的数字位数(不足补0)
*/
void Serial_SendNumber(uint32_t Number, uint8_t Length);
/**
* @brief 格式化输出
* @param format: 格式化字符串
* @param ...: 可变参数
*/
void Serial_Printf(char *format, ...);
c复制/**
* @brief 接收数据包(外部变量)
* @note 当Serial_RxFlag=1时有效
*/
extern uint8_t Serial_RxPacket[SERIAL_RX_BUFFER_SIZE];
/**
* @brief 接收完成标志
* @note 1=有新数据,0=无新数据
*/
extern uint8_t Serial_RxFlag;
这个串口模块经过多个项目的实际验证,确实能显著提升STM32的串口通信效率和可靠性。它的优势在于:
在实际使用中,建议根据具体需求调整缓冲区大小和队列深度。对于更复杂的应用场景,可以考虑在此基础上添加协议解析、流控制等功能。