一、问题背景与现象
在STM32开发中,使用HAL库的HAL_UARTEx_ReceiveToIdle_DMA配合空闲中断(IDLE)接收不定长数据是一种常见方案。但许多开发者反馈:在波特率较高(如921600)或数据帧较长时,偶尔会丢失整包数据或部分字节。现象通常为:
- 空闲中断触发后,
Size参数偶尔为0或小于实际长度。 - 连续多帧数据时,第二帧开始丢失。
- 调试时单步运行正常,全速运行丢包。
二、丢包根因分析
2.1 HAL库的DMA接收机制
HAL_UARTEx_ReceiveToIdle_DMA内部启动DMA循环模式,并在UART空闲中断中调用HAL_UARTEx_RxEventCallback。关键问题在于:空闲中断触发时,DMA可能尚未完成当前数据块的搬运,导致Size计算基于DMA剩余计数器(NDTR),而NDTR的更新与CPU读取存在竞争。
2.2 中断处理时序缺陷
空闲中断服务函数中,HAL库会先清除IDLE标志,再读取NDTR计算长度。若此时DMA仍在搬运最后几个字节,NDTR尚未更新到最终值,计算出的长度偏小,造成尾部数据丢失。此外,若在回调中重新启动接收,可能因DMA未完全停止而覆盖缓冲区。
2.3 缓冲区管理问题
多数示例使用单缓冲区直接处理数据,若处理耗时超过下一帧到达时间,新数据会覆盖旧数据。且HAL库的HAL_UARTEx_RxEventCallback在DMA循环模式下可能被多次调用,缺乏环形缓冲区的解耦。
三、环形缓冲区改造方案
3.1 设计思路
- 使用DMA循环模式,将UART数据持续搬运至固定大小的DMA缓冲区。
- 空闲中断触发时,根据DMA当前写指针与上次读指针的差值,将新数据拷贝至软件环形缓冲区。
- 主循环从环形缓冲区读取并处理,实现生产者-消费者解耦。
- 关键:在空闲中断中先暂停DMA(或使用
__HAL_DMA_DISABLE),确保NDTR稳定后再计算长度,拷贝完成后重新使能。
3.2 环形缓冲区实现
#define RING_BUF_SIZE 1024
typedef struct {
uint8_t buffer[RING_BUF_SIZE];
volatile uint16_t head;
volatile uint16_t tail;
} RingBuffer_t;
RingBuffer_t uart_ring;
void RingBuffer_Init(RingBuffer_t *rb) {
rb->head = 0;
rb->tail = 0;
}
uint16_t RingBuffer_Write(RingBuffer_t *rb, const uint8_t *data, uint16_t len) {
uint16_t i;
for (i = 0; i < len; i++) {
uint16_t next = (rb->head + 1) % RING_BUF_SIZE;
if (next == rb->tail) break; // 满
rb->buffer[rb->head] = data[i];
rb->head = next;
}
return i;
}
uint16_t RingBuffer_Read(RingBuffer_t *rb, uint8_t *data, uint16_t len) {
uint16_t i;
for (i = 0; i < len; i++) {
if (rb->tail == rb->head) break; // 空
data[i] = rb->buffer[rb->tail];
rb->tail = (rb->tail + 1) % RING_BUF_SIZE;
}
return i;
}
3.3 DMA与空闲中断配置
#define DMA_BUF_SIZE 256
uint8_t dma_buf[DMA_BUF_SIZE];
volatile uint16_t last_pos = 0;
void UART_Init(void) {
// 启动DMA循环接收
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, dma_buf, DMA_BUF_SIZE);
__HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT); // 关闭半传输中断
}
// 空闲中断回调
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) {
if (huart->Instance == USART1) {
// 暂停DMA,确保NDTR稳定
__HAL_DMA_DISABLE(huart->hdmarx);
uint16_t curr_pos = DMA_BUF_SIZE - __HAL_DMA_GET_COUNTER(huart->hdmarx);
uint16_t len;
if (curr_pos >= last_pos) {
len = curr_pos - last_pos;
RingBuffer_Write(&uart_ring, &dma_buf[last_pos], len);
} else {
// 回绕
len = DMA_BUF_SIZE - last_pos;
RingBuffer_Write(&uart_ring, &dma_buf[last_pos], len);
len = curr_pos;
RingBuffer_Write(&uart_ring, &dma_buf[0], len);
}
last_pos = curr_pos;
// 重新使能DMA
__HAL_DMA_ENABLE(huart->hdmarx);
// 重新启动接收(HAL库需要)
HAL_UARTEx_ReceiveToIdle_DMA(huart, dma_buf, DMA_BUF_SIZE);
}
}
3.4 主循环处理
uint8_t process_buf[128];
while (1) {
uint16_t n = RingBuffer_Read(&uart_ring, process_buf, sizeof(process_buf));
if (n > 0) {
// 处理数据
ParseData(process_buf, n);
}
}
四、注意事项
-
DMA暂停与恢复:在空闲中断中必须暂停DMA再读取NDTR,否则可能读到中间值。恢复后需重新调用
HAL_UARTEx_ReceiveToIdle_DMA,否则HAL库状态机不更新。 - 环形缓冲区大小:应大于最大帧长的2倍,避免生产过快覆盖。
- 中断优先级:UART空闲中断优先级应高于DMA传输完成中断,确保及时响应。
-
临界区保护:若主循环与中断同时操作环形缓冲区,需对
head/tail使用原子操作或关中断。 -
HAL库版本差异:不同版本
HAL_UARTEx_RxEventCallback的调用时机略有不同,建议实测验证。
五、总结
通过暂停DMA稳定NDTR、引入环形缓冲区解耦生产消费,可彻底解决HAL库DMA空闲中断接收的丢包问题。该方案已在实际项目中验证,在1Mbps波特率下连续接收无丢包,适用于高速、不定长串口通信场景。
暂无评论,来抢沙发~