一、问题背景与现象
在STM32开发中,使用HAL库的HAL_UARTEx_ReceiveToIdle_DMA配合空闲中断接收不定长串口数据是一种常见方案。但许多开发者会遇到:
- 高波特率(如115200以上)连续发送时偶发丢包
- 数据帧间隔极短时,空闲中断来不及处理,下一帧已覆盖缓冲区
- 主循环处理耗时较长时,DMA目标缓冲区被新数据覆盖
二、原理与丢包根因分析
2.1 DMA+空闲中断工作流程
- 调用
HAL_UARTEx_ReceiveToIdle_DMA启动DMA接收,数据写入用户缓冲区 - 总线空闲时触发USART空闲中断,HAL库在中断中计算已接收长度并调用回调
- 回调中通常再次启动接收
2.2 丢包根因
- 中断响应延迟:空闲中断到回调执行期间,若新数据到达且DMA已重新启动,可能覆盖未处理数据
- 单缓冲区覆盖:HAL库默认使用单一目标缓冲区,回调中若处理时间过长,DMA继续写入导致旧数据被覆盖
- HAL库内部状态机竞争:在回调中直接重启接收,若前一帧未处理完,新帧数据会覆盖
- DMA半传输/传输完成中断与空闲中断的时序问题:高负载下中断嵌套或优先级配置不当导致空闲中断丢失
三、环形缓冲改造方案
3.1 设计思路
- 使用DMA循环模式(Circular)持续接收,数据写入固定大小的DMA缓冲区
- 利用DMA半传输/传输完成中断与空闲中断共同维护写指针
- 用户层通过环形缓冲区读取数据,实现生产者-消费者解耦
- 空闲中断仅更新写指针并通知,不直接处理数据
3.2 配置步骤
- CubeMX中配置USART1为异步模式,波特率115200,开启DMA接收(Circular模式),使能USART全局中断
- 在NVIC中使能DMA通道中断与USART中断,设置合适优先级(DMA高于USART)
- 定义环形缓冲区结构体与DMA缓冲区
- 实现空闲中断回调与DMA中断回调,更新写指针
- 用户层实现环形缓冲区读取函数
四、完整代码示例
4.1 环形缓冲区定义
#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;
uint8_t dma_buf[256]; // DMA循环缓冲区
volatile uint16_t dma_last_pos = 0;
4.2 初始化与启动
void UART_DMA_Init(void) {
uart_ring.head = 0;
uart_ring.tail = 0;
dma_last_pos = 0;
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, dma_buf, sizeof(dma_buf));
__HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT); // 可选:关闭半传输中断
}
4.3 空闲中断与DMA中断回调
// 空闲中断回调(HAL_UARTEx_RxEventCallback)
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) {
if (huart->Instance == USART1) {
uint16_t pos = sizeof(dma_buf) - __HAL_DMA_GET_COUNTER(huart->hdmarx);
// 将DMA缓冲区中从dma_last_pos到pos的数据写入环形缓冲区
uint16_t len = (pos >= dma_last_pos) ? (pos - dma_last_pos) : (sizeof(dma_buf) - dma_last_pos + pos);
for (uint16_t i = 0; i < len; i++) {
uint16_t idx = (dma_last_pos + i) % sizeof(dma_buf);
uint16_t next = (uart_ring.head + 1) % RING_BUF_SIZE;
if (next != uart_ring.tail) { // 防止溢出
uart_ring.buffer[uart_ring.head] = dma_buf[idx];
uart_ring.head = next;
}
}
dma_last_pos = pos;
// 注意:循环模式下无需重启接收,但需清除空闲标志
__HAL_UART_CLEAR_IDLEFLAG(huart);
}
}
// DMA传输完成中断回调(可选,用于处理半传输/完成事件)
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart) {
// 可在此更新写指针,逻辑同上,略
}
4.4 用户层读取函数
uint16_t RingBuffer_Read(uint8_t *data, uint16_t max_len) {
uint16_t len = 0;
while (len < max_len && uart_ring.tail != uart_ring.head) {
data[len++] = uart_ring.buffer[uart_ring.tail];
uart_ring.tail = (uart_ring.tail + 1) % RING_BUF_SIZE;
}
return len;
}
五、注意事项
-
DMA循环模式下,
HAL_UARTEx_ReceiveToIdle_DMA只需调用一次,无需在回调中重启,否则会破坏循环 - 空闲中断回调中必须清除空闲标志,否则会反复进入中断
- 环形缓冲区大小应大于单帧最大长度,并留有余量;读写指针操作需保证原子性(单生产者单消费者可免锁)
- DMA中断优先级应高于USART中断,确保写指针及时更新
- 若数据量极大,可考虑双缓冲或DMA链表模式,但环形缓冲已能满足多数场景
- 调试时可用GPIO翻转或计数统计验证丢包是否消除
通过上述改造,将数据接收与处理解耦,可有效解决HAL库默认方案在高负载下的丢包问题,提升串口通信的可靠性。