一、问题背景与现象
在STM32开发中,使用HAL库的HAL_UARTEx_ReceiveToIdle_DMA配合空闲中断接收不定长数据是常见方案。但在高波特率(如921600)或连续快速发送场景下,常出现丢包:接收缓冲区中数据不完整,或两次空闲中断之间数据被覆盖。
典型现象:
- 发送端连续发送多帧,接收端偶尔丢失整帧或帧尾若干字节;
- 调试发现
HAL_UARTEx_RxEventCallback回调中Size参数小于实际发送长度; - 使用逻辑分析仪确认线上数据完整,问题出在软件层。
二、根因分析
HAL库的DMA空闲中断接收流程如下:
-
HAL_UARTEx_ReceiveToIdle_DMA启动DMA,将数据搬运到用户缓冲区; - 总线空闲时触发IDLE中断,HAL在中断中计算已接收长度并调用回调;
- 回调中若未及时重新启动接收,后续数据会丢失。
丢包的核心原因有三点:
-
回调中处理耗时过长:在
HAL_UARTEx_RxEventCallback中做解析、打印等操作,期间DMA未重新武装,新数据到达即丢失; - 单缓冲覆盖:HAL默认使用单缓冲区,下一次接收会覆盖上一次未处理完的数据;
- IDLE中断与DMA半满/全满事件竞争:高波特率下,IDLE中断响应延迟导致长度计算偏差。
三、丢包复现实验
3.1 硬件与配置
- MCU:STM32F407,主频168MHz;
- UART2:波特率921600,DMA1 Stream5 Channel4;
- 发送端:PC串口助手连续发送100帧,每帧64字节,间隔1ms。
3.2 复现代码(有缺陷版本)
#define RX_BUF_SIZE 256
uint8_t rx_buf[RX_BUF_SIZE];
void StartReceive(void)
{
HAL_UARTEx_ReceiveToIdle_DMA(&huart2, rx_buf, RX_BUF_SIZE);
}
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if (huart->Instance == USART2) {
// 模拟耗时处理:解析+打印
ProcessData(rx_buf, Size); // 耗时约500us
HAL_UARTEx_ReceiveToIdle_DMA(&huart2, rx_buf, RX_BUF_SIZE);
}
}
3.3 复现结果
运行后统计:100帧中约丢失7~12帧,且丢失帧多发生在处理耗时较长的时刻。将ProcessData改为空函数后丢包率降为0,验证了“回调耗时导致DMA未及时重启”的结论。
四、环形缓冲修复方案
4.1 设计思路
- 使用双缓冲+DMA循环模式或环形缓冲区,将“接收”与“处理”解耦;
- 回调中只做数据搬运(拷贝到环形缓冲),不做业务处理;
- 主循环从环形缓冲取数据解析,彻底避免回调阻塞。
4.2 环形缓冲实现
#define RING_SIZE 1024
typedef struct {
uint8_t buf[RING_SIZE];
volatile uint16_t head; // 写指针(DMA回调更新)
volatile uint16_t tail; // 读指针(主循环更新)
} RingBuffer_t;
static RingBuffer_t g_ring;
static inline uint16_t Ring_Used(void)
{
return (g_ring.head - g_ring.tail) & (RING_SIZE - 1);
}
static inline uint16_t Ring_Free(void)
{
return RING_SIZE - 1 - Ring_Used();
}
// 由DMA空闲回调调用,仅做拷贝
void Ring_Write(const uint8_t *data, uint16_t len)
{
for (uint16_t i = 0; i < len; i++) {
uint16_t next = (g_ring.head + 1) & (RING_SIZE - 1);
if (next == g_ring.tail) break; // 满则丢弃,可加计数
g_ring.buf[g_ring.head] = data[i];
g_ring.head = next;
}
}
// 主循环调用
uint16_t Ring_Read(uint8_t *out, uint16_t max_len)
{
uint16_t n = 0;
while (n < max_len && g_ring.tail != g_ring.head) {
out[n++] = g_ring.buf[g_ring.tail];
g_ring.tail = (g_ring.tail + 1) & (RING_SIZE - 1);
}
return n;
}
4.3 接收回调改造
#define DMA_RX_SIZE 128
static uint8_t dma_rx_buf[DMA_RX_SIZE];
void StartReceive(void)
{
HAL_UARTEx_ReceiveToIdle_DMA(&huart2, dma_rx_buf, DMA_RX_SIZE);
__HAL_DMA_DISABLE_IT(&hdma_usart2_rx, DMA_IT_HT); // 关闭半满中断,减少干扰
}
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if (huart->Instance == USART2) {
Ring_Write(dma_rx_buf, Size); // 仅拷贝,微秒级
HAL_UARTEx_ReceiveToIdle_DMA(&huart2, dma_rx_buf, DMA_RX_SIZE);
__HAL_DMA_DISABLE_IT(&hdma_usart2_rx, DMA_IT_HT);
}
}
4.4 主循环处理
uint8_t frame[256];
while (1) {
uint16_t len = Ring_Read(frame, sizeof(frame));
if (len > 0) {
ProcessData(frame, len); // 业务处理不再阻塞接收
}
}
五、配置步骤要点
- CubeMX中UART2开启DMA接收,模式选Normal(配合每次重启)或Circular(配合半满/全满计算);
- NVIC中使能USART2全局中断,DMA流中断优先级建议低于串口;
- 关闭DMA半满中断
DMA_IT_HT,避免与IDLE事件重复触发; - 环形缓冲大小取2的幂,用位与代替取模提升效率;
-
head/tail用volatile修饰,单写单读场景无需加锁。
六、注意事项
- 回调中严禁调用
printf、HAL_Delay等阻塞函数; - 若使用Circular模式,需在回调中根据
Size与上次位置计算增量,逻辑更复杂,推荐Normal+重启; - 环形缓冲满时应记录溢出计数,便于现场定位;
- 高波特率下建议将DMA缓冲区设为32~64字节,缩短单次搬运时间;
- 验证时用逻辑分析仪对比线上数据与环形缓冲内容,确认零丢包。
七、总结
HAL库DMA+空闲中断丢包的本质是“接收与处理未解耦”。通过回调只做环形缓冲写入、主循环异步处理,可将回调耗时压缩到微秒级,从根本上消除丢包。该方案已在多个工业串口通信项目中验证,921600波特率连续收发零丢失。
暂无评论,来抢沙发~