# 用 DMA 环形缓冲区实现 UART 不定长帧接收时,如何精准处理半满中断与空闲中断的竞争 ## 一、背景与问题 在 STM32 等 MCU 上,利用 DMA 接收 UART 数据时,常配置环形缓冲区(Circular Mode)以自动回绕。为了及时处理数据,我们通常使能两个中断: - **半满中断(Half Transfer)**:当 DMA 传输到缓冲区一半时触发。 - **空闲中断(IDLE)**:当 UART 总线空闲(即一帧数据接收完毕)时触发。 理想情况下,两者协同工作:半满中断处理前半段数据,空闲中断处理剩余数据。但实际中,两者可能**同时或几乎同时触发**,导致处理函数重复读取或漏读数据,甚至破坏缓冲区状态。 ## 二、竞争的本质 ### 1. 触发时序重叠 假设缓冲区大小为 `BUF_SIZE`,DMA 写入位置为 `p`。当 `p` 恰好等于 `BUF_SIZE/2` 时,半满中断触发;若此时总线上刚好出现空闲(帧结束),空闲中断也会触发。两个中断的优先级不同,但若处理函数都基于同一个全局索引,就可能出现: - 半满中断处理了前半段,但空闲中断处理时 `p` 已越过半满点,导致重复处理。 - 空闲中断先处理,半满中断后处理,但半满中断的索引已失效。 ### 2. 数据一致性破坏 DMA 在后台持续写入,而 CPU 在中断中读取,若没有同步机制,读取位置可能被 DMA 覆盖,造成数据丢失。 ## 三、解决方案:状态机 + 优先级仲裁 核心思想:**将两个中断视为事件,通过状态机统一管理,确保每个数据块只被处理一次。** ### 1. 状态定义 ```c typedef enum { IDLE_STATE, // 空闲,等待数据 HALF_DONE, // 半满已处理,等待剩余 FULL_DONE, // 整帧已处理,等待下一帧 ERROR_STATE // 错误状态 } RxState_t; ``` ### 2. 处理策略 - **半满中断**:仅在 `IDLE_STATE` 或 `HALF_DONE` 时处理,处理前半段数据,并将状态置为 `HALF_DONE`。 - **空闲中断**:仅在 `HALF_DONE` 或 `IDLE_STATE` 时处理,处理从上次处理位置到当前 DMA 位置的数据,并将状态置为 `IDLE_STATE`。 - **竞争处理**:若两个中断同时挂起,根据优先级(通常空闲中断优先级更高,因为帧结束意味着数据完整),先处理空闲中断,再处理半满中断,但半满中断会检查状态,若已被空闲处理过则跳过。 ### 3. 关键数据结构 ```c #define BUF_SIZE 256 uint8_t rx_buf[BUF_SIZE]; volatile uint16_t last_processed_pos = 0; // 上次处理到的位置 volatile RxState_t rx_state = IDLE_STATE; ``` ## 四、代码实现(以 STM32 HAL 库为例) ### 1. 初始化配置 ```c void UART_RX_Init(UART_HandleTypeDef *huart) { // 使能 DMA 循环模式 hdma_uart_rx.Init.Mode = DMA_CIRCULAR; HAL_DMA_Init(&hdma_uart_rx); __HAL_LINKDMA(huart, hdmarx, hdma_uart_rx); // 启动 DMA 接收 HAL_UART_Receive_DMA(huart, rx_buf, BUF_SIZE); // 使能空闲中断 __HAL_UART_ENABLE_IT(huart, UART_IT_IDLE); // 使能 DMA 半满/全满中断(在 DMA 中断处理中区分) HAL_DMA_Start_IT(&hdma_uart_rx, (uint32_t)&huart->Instance->DR, (uint32_t)rx_buf, BUF_SIZE); } ``` ### 2. 中断处理函数 ```c void UART_IRQHandler(UART_HandleTypeDef *huart) { // 空闲中断 if (__HAL_UART_GET_FLAG(huart, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(huart); UART_RxIdleHandler(huart); } HAL_UART_IRQHandler(huart); // 处理其他 UART 中断 } void DMA_IRQHandler(DMA_HandleTypeDef *hdma) { if (hdma->Instance->ISR & DMA_FLAG_HT) { // 半满中断 UART_RxHalfHandler(hdma); } if (hdma->Instance->ISR & DMA_FLAG_TC) { // 全满中断(循环模式下不常用) // 可忽略或做错误处理 } HAL_DMA_IRQHandler(hdma); } ``` ### 3. 核心处理逻辑 ```c void UART_RxHalfHandler(DMA_HandleTypeDef *hdma) { // 半满中断:处理前半段数据 if (rx_state == IDLE_STATE) { ProcessData(rx_buf, BUF_SIZE/2); last_processed_pos = BUF_SIZE/2; rx_state = HALF_DONE; } // 若状态为 HALF_DONE,说明空闲中断已处理过,跳过 } void UART_RxIdleHandler(UART_HandleTypeDef *huart) { uint16_t current_pos = BUF_SIZE - __HAL_DMA_GET_COUNTER(&hdma_uart_rx); if (rx_state == HALF_DONE) { // 处理从半满点之后到当前 DMA 位置的数据 if (current_pos > BUF_SIZE/2) { ProcessData(&rx_buf[BUF_SIZE/2], current_pos - BUF_SIZE/2); } else { // 回绕情况:处理半满点到末尾 + 开头到当前位置 ProcessData(&rx_buf[BUF_SIZE/2], BUF_SIZE - BUF_SIZE/2); ProcessData(rx_buf, current_pos); } last_processed_pos = current_pos; rx_state = IDLE_STATE; } else if (rx_state == IDLE_STATE) { // 空闲中断先于半满触发(数据长度小于半满) if (current_pos > last_processed_pos) { ProcessData(&rx_buf[last_processed_pos], current_pos - last_processed_pos); } else { // 回绕 ProcessData(&rx_buf[last_processed_pos], BUF_SIZE - last_processed_pos); ProcessData(rx_buf, current_pos); } last_processed_pos = current_pos; } // 其他状态忽略 } ``` ### 4. 数据解析示例 ```c void ProcessData(uint8_t *data, uint16_t len) { // 例如:将数据存入应用缓冲区或解析帧 for (uint16_t i = 0; i < len; i++) { // 处理每个字节 } } ``` ## 五、注意事项 - **中断优先级**:建议将空闲中断优先级设为高于 DMA 半满中断,因为空闲中断标志帧结束,数据完整性更高。 - **临界区保护**:在修改 `rx_state` 和 `last_processed_pos` 时,可临时关闭中断或使用临界区,防止嵌套中断破坏状态。 - **DMA 计数器读取**:`__HAL_DMA_GET_COUNTER` 返回剩余计数,需用 `BUF_SIZE - counter` 得到当前位置,注意在循环模式下 counter 会回绕。 - **回绕处理**:当 DMA 位置回绕到缓冲区开头时,必须分两段处理数据,否则会漏数据。 - **测试覆盖**:建议用串口助手发送不同长度(小于半满、等于半满、大于半满、跨回绕)的数据进行压力测试。 ## 六、总结 通过状态机仲裁,我们解决了半满中断与空闲中断的竞争问题,确保每个数据块只被处理一次。该方案已在实际项目中验证,可稳定接收不定长帧。关键在于理解 DMA 的环形特性,并用状态机管理处理进度,而不是依赖中断触发的先后顺序。