# 用DMA环形缓冲区实现UART不定长接收:帧尾超时与半满中断竞争的精准处理 ## 1. 背景与挑战 在嵌入式系统中,UART接收不定长数据帧是常见需求。传统中断逐字节接收在高速率下会频繁打断CPU,而DMA+环形缓冲区可大幅降低中断负载。但引入DMA后,两个异步事件——**帧尾超时**(IDLE中断或定时器)与**半满中断**(DMA传输过半)——可能同时或交错发生,若处理不当,会导致数据覆盖、重复读取或帧错位。 ## 2. 原理剖析:竞争的本质 ### 2.1 DMA环形缓冲区机制 - 缓冲区大小设为2的幂次(如256字节),DMA在内存中循环写入。 - 通过`NDTR`寄存器或`DMA_GetCurrDataCounter()`获取当前剩余计数,从而计算写指针位置。 - 半满中断在传输达到缓冲区一半时触发,用于及时搬移数据,防止溢出。 ### 2.2 帧尾超时机制 - 常用方式:UART空闲中断(IDLE)或定时器超时(如10ms无新数据)。 - 目的:判断一帧数据接收完毕,触发处理回调。 ### 2.3 竞争场景 - **场景A**:DMA半满中断触发时,恰好收到帧尾(IDLE),两个中断几乎同时挂起。 - **场景B**:半满中断处理中,IDLE到来,但半满处理尚未完成,数据可能被覆盖。 - **场景C**:IDLE处理时,DMA继续写入,导致读指针追赶写指针。 核心矛盾:**两个事件都试图更新读指针,但缺乏原子性保护**。 ## 3. 解决方案:状态机+双标志位 ### 3.1 设计思路 - 引入`rx_state`状态机(IDLE、RECEIVING、FRAME_READY)。 - 使用两个标志位:`half_flag`(半满待处理)和`idle_flag`(帧尾待处理)。 - 中断中只置标志位,主循环或低优先级任务中统一处理,避免嵌套竞争。 ### 3.2 状态转移 - **IDLE**:等待起始字节,收到首个字节后进入RECEIVING。 - **RECEIVING**:持续接收,若半满触发则置`half_flag`,若IDLE触发则置`idle_flag`并进入FRAME_READY。 - **FRAME_READY**:主循环处理完整帧,然后回到IDLE。 ### 3.3 竞争消解规则 - 在中断服务函数中,仅更新标志位,不直接操作缓冲区读指针。 - 主循环中,先检查`idle_flag`,若置位则处理整帧(此时半满标志可能已置位,但需先完成帧处理)。 - 若`half_flag`置位且`idle_flag`未置位,则搬移半满数据到用户缓冲区,并更新读指针。 - 关键:**处理半满时,必须禁用UART的IDLE中断或使用临界区,防止IDLE打断**。 ## 4. 完整代码示例(基于STM32 HAL库) ```c // 环形缓冲区定义 #define BUF_SIZE 256 #define HALF_SIZE (BUF_SIZE/2) uint8_t dma_rx_buf[BUF_SIZE]; volatile uint16_t read_index = 0; volatile uint8_t rx_state = 0; // 0:IDLE, 1:RECEIVING, 2:FRAME_READY volatile uint8_t half_flag = 0; volatile uint8_t idle_flag = 0; // 用户帧缓冲区(假设最大帧长128) uint8_t frame_buf[128]; uint16_t frame_len = 0; // 初始化DMA和UART(略) // UART IDLE中断回调(在HAL_UART_IRQHandler中调用) void HAL_UART_IdleCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { // 计算当前写指针 uint16_t write_index = BUF_SIZE - __HAL_DMA_GET_COUNTER(huart->hdmarx); // 置帧尾标志 idle_flag = 1; // 保存当前帧长度(假设从read_index到write_index) frame_len = (write_index >= read_index) ? (write_index - read_index) : (BUF_SIZE - read_index + write_index); // 状态机转移 rx_state = 2; // FRAME_READY // 注意:此时不立即处理,等待主循环 } } // DMA半满中断回调(在HAL_DMA_IRQHandler中调用) void HAL_DMA_HalfCpltCallback(DMA_HandleTypeDef *hdma) { if (hdma->Instance == DMA1_Channel5) { // 假设UART1_RX用DMA1_CH5 half_flag = 1; } } // 主循环中的处理函数 void UART_Process(void) { // 处理帧尾(优先级高) if (idle_flag) { idle_flag = 0; // 复制数据到用户缓冲区(注意环形回绕) for (uint16_t i = 0; i < frame_len; i++) { frame_buf[i] = dma_rx_buf[(read_index + i) % BUF_SIZE]; } // 更新读指针 read_index = (read_index + frame_len) % BUF_SIZE; // 调用用户回调 User_Frame_Handler(frame_buf, frame_len); // 状态复位 rx_state = 0; // 重新使能IDLE中断(若之前禁用) __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); } // 处理半满(仅当无帧尾待处理) else if (half_flag) { half_flag = 0; // 临界区:暂时禁用IDLE中断,防止竞争 __HAL_UART_DISABLE_IT(&huart1, UART_IT_IDLE); // 计算当前写指针 uint16_t write_index = BUF_SIZE - __HAL_DMA_GET_COUNTER(huart1.hdmarx); // 计算可读数据长度(从read_index到write_index) uint16_t avail = (write_index >= read_index) ? (write_index - read_index) : (BUF_SIZE - read_index + write_index); // 如果可读长度大于半满,则只搬移半满部分(但此处我们搬移全部可用,但注意不要超过半满) // 实际上半满中断表示已写入半满,所以可用长度至少为半满,但可能更多(若IDLE同时发生) // 为简化,我们搬移所有可用数据到临时缓冲(但需防止溢出) // 此处仅演示,实际可搬移到更大的用户缓冲 uint8_t temp[128]; uint16_t copy_len = (avail > 128) ? 128 : avail; for (uint16_t i = 0; i < copy_len; i++) { temp[i] = dma_rx_buf[(read_index + i) % BUF_SIZE]; } // 更新读指针 read_index = (read_index + copy_len) % BUF_SIZE; // 重新使能IDLE中断 __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); // 处理temp数据(例如存入队列) // ... } } // 用户回调示例 void User_Frame_Handler(uint8_t *data, uint16_t len) { // 处理完整帧 } ``` ## 5. 配置步骤 1. **初始化DMA**:配置为循环模式,数据宽度字节,内存地址自增,外设地址固定。 2. **使能DMA中断**:开启半满中断(`HAL_DMA_Start_IT`)和传输完成中断(可选)。 3. **配置UART**:使能IDLE中断(`__HAL_UART_ENABLE_IT(&huart, UART_IT_IDLE)`),并设置DMA接收。 4. **编写回调**:如上代码,在`HAL_UART_IdleCpltCallback`和`HAL_DMA_HalfCpltCallback`中置标志。 5. **主循环调用**:周期性调用`UART_Process()`。 ## 6. 注意事项 - **临界区保护**:访问`read_index`、`frame_len`等共享变量时,建议使用临界区或关中断,防止主循环与中断冲突。 - **缓冲区大小**:需大于最大帧长,且为2的幂次,便于取模运算。 - **半满中断的触发时机**:DMA半满中断在传输半满时触发,但若帧长小于半满,则不会触发,此时IDLE是唯一信号。 - **超时时间选择**:若使用定时器超时,需根据波特率和帧间隔调整,避免误判。 - **性能考量**:主循环处理频率需足够高,否则半满标志可能堆积,导致缓冲区溢出。 ## 7. 总结 通过状态机与双标志位,我们成功将DMA半满中断与UART IDLE中断的竞争转化为顺序处理,避免了数据竞争和丢帧。此方案在STM32F1/F4/H7等系列上均适用,只需调整DMA和UART句柄。实际项目中,还可结合空闲定时器(如TIM)实现更精确的超时控制。掌握这一策略,你的嵌入式通信层将更加健壮。