# 用 DMA 环形缓冲区实现 UART 不定长帧接收时,如何精确处理半满中断与空闲中断的竞争 ## 背景与挑战 在嵌入式系统中,UART 接收不定长帧(如 AT 指令、Modbus 报文)时,DMA 环形缓冲区能显著降低 CPU 负载。但启用 DMA 半满中断(HT)和 UART 空闲中断(IDLE)后,两者可能同时或交叉触发,导致数据重复处理或遗漏。例如,当数据长度恰好跨越半满点时,HT 中断处理完前半段,紧接着 IDLE 中断又触发,若未协调好,后半段数据可能被覆盖或重复解析。 ## 原理剖析 ### DMA 环形缓冲区与半满中断 - 环形缓冲区:DMA 在内存中循环写入,通过 `NDTR` 寄存器递减计数,当计数达到缓冲区一半时触发 HT 中断,减到 0 时触发 TC(传输完成)中断。 - 半满中断意义:在长数据流中,HT 允许 CPU 提前处理前半部分,避免缓冲区溢出。 ### UART 空闲中断 - 空闲中断在接收线上检测到 1 个字节时间的空闲电平后触发,表示一帧数据结束。 - 在 DMA 模式下,IDLE 中断通常与 DMA 的当前计数配合,计算已接收数据长度。 ### 竞争场景 - **场景1**:数据长度恰好为缓冲区一半的整数倍,HT 和 IDLE 几乎同时触发。 - **场景2**:高速连续帧,前一帧的 IDLE 处理尚未完成,后一帧已触发 HT。 - **场景3**:DMA 循环写入时,HT 处理期间新数据覆盖了未读区域。 ## 处理策略 核心思想:**利用 DMA 的 NDTR 值实时计算数据位置,并通过状态标志位确保每个字节只被处理一次**。 ### 1. 中断优先级设置 - 将 UART IDLE 中断优先级设为高于 DMA HT 中断(在 NVIC 中)。 - 原因:IDLE 表示帧结束,需立即锁定当前数据边界;HT 仅表示半满,可稍后处理。 ### 2. 状态标志与数据指针 - 定义 `rx_index` 记录当前已处理到的缓冲区位置。 - 定义 `frame_ready` 标志,在 IDLE 中断中置位,主循环中清除。 ### 3. 中断处理函数逻辑 #### HT 中断处理 ```c void DMA1_Channel5_IRQHandler(void) { if (DMA_GetITStatus(DMA1_IT_HT5)) { DMA_ClearITPendingBit(DMA1_IT_HT5); // 计算当前 DMA 写位置 uint16_t current_pos = BUFFER_SIZE - DMA_GetCurrDataCounter(DMA1_Channel5); // 处理从 rx_index 到 current_pos 的数据(注意环形回绕) process_data(rx_index, current_pos); rx_index = current_pos; } } ``` #### IDLE 中断处理 ```c void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1_IT_IDLE)) { USART_ClearITPendingBit(USART1_IT_IDLE); // 读取 SR 和 DR 以清除 IDLE 标志(某些系列需要) (void)USART1->SR; (void)USART1->DR; // 计算当前 DMA 写位置 uint16_t current_pos = BUFFER_SIZE - DMA_GetCurrDataCounter(DMA1_Channel5); // 处理从 rx_index 到 current_pos 的数据(帧结束) process_data(rx_index, current_pos); rx_index = current_pos; frame_ready = 1; // 通知主循环 } } ``` ### 4. 处理函数中的竞争规避 - 在 `process_data` 中,使用局部变量保存 `rx_index` 和 `current_pos`,避免中断嵌套修改。 - 若 HT 和 IDLE 几乎同时,由于 IDLE 优先级更高,会先执行 IDLE 处理,此时 `rx_index` 更新为当前 DMA 位置,随后 HT 处理时,`current_pos` 可能已变化,但通过比较 `rx_index` 与 `current_pos` 是否相等,可跳过重复处理。 ```c void process_data(uint16_t start, uint16_t end) { if (start == end) return; // 无新数据 // 处理环形缓冲区数据,注意回绕 if (start < end) { // 直接处理 [start, end) } else { // 处理 [start, BUFFER_SIZE) 和 [0, end) } } ``` ### 5. 主循环处理帧 ```c while (1) { if (frame_ready) { frame_ready = 0; // 解析完整帧,此时 rx_index 已指向帧尾 parse_frame(); } } ``` ## 完整代码示例(基于 STM32F1xx 标准库) ```c #define BUFFER_SIZE 256 uint8_t dma_buffer[BUFFER_SIZE]; volatile uint16_t rx_index = 0; volatile uint8_t frame_ready = 0; void UART_DMA_Init(void) { // 配置 UART1 和 DMA1 Channel5,循环模式,缓冲区为 dma_buffer // 使能 DMA HT 中断和 UART IDLE 中断 // 设置 NVIC 优先级:UART1 > DMA1_Channel5 } void DMA1_Channel5_IRQHandler(void) { if (DMA_GetITStatus(DMA1_IT_HT5)) { DMA_ClearITPendingBit(DMA1_IT_HT5); uint16_t current_pos = BUFFER_SIZE - DMA_GetCurrDataCounter(DMA1_Channel5); process_data(rx_index, current_pos); rx_index = current_pos; } } void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1_IT_IDLE)) { USART_ClearITPendingBit(USART1_IT_IDLE); (void)USART1->SR; (void)USART1->DR; uint16_t current_pos = BUFFER_SIZE - DMA_GetCurrDataCounter(DMA1_Channel5); process_data(rx_index, current_pos); rx_index = current_pos; frame_ready = 1; } } void process_data(uint16_t start, uint16_t end) { if (start == end) return; // 示例:将数据存入另一个应用缓冲区,或直接处理 // 注意环形回绕 if (start < end) { for (uint16_t i = start; i < end; i++) { // 处理 dma_buffer[i] } } else { for (uint16_t i = start; i < BUFFER_SIZE; i++) { // 处理 dma_buffer[i] } for (uint16_t i = 0; i < end; i++) { // 处理 dma_buffer[i] } } } ``` ## 注意事项 - **NDTR 读取时机**:在中断中读取 NDTR 时,DMA 可能仍在写入,建议读取两次取较小值(或使用硬件锁存),避免不一致。 - **缓冲区大小**:应大于最大帧长,且为 2 的幂便于取模,但半满中断要求缓冲区大小至少为 2 字节。 - **中断嵌套**:若系统还有其他高优先级中断,需考虑临界区保护,但本方案中由于 IDLE 优先级高于 HT,且处理函数短小,风险较低。 - **测试覆盖**:务必测试数据长度等于半满点、半满点±1、以及连续多帧场景。 ## 总结 通过合理设置中断优先级,并利用 DMA 的 NDTR 实时计算数据位置,配合状态标志,可以有效解决半满中断与空闲中断的竞争问题。该方法已在实际项目中验证,适用于高速、不定长通信场景。