# 引言 在 STM32 等单片机上,UART 接收不定长数据时,DMA 环形缓冲区 + 空闲中断(IDLE)是常用组合。但引入半满中断(Half Transfer)后,两者可能同时触发,导致数据处理顺序混乱。本文从原理出发,分析竞争场景,并给出可落地的解决方案。 # 1. 基础原理与竞争根源 ## 1.1 DMA 环形缓冲区工作模式 - 环形缓冲区:DMA 在内存中循环写入,通过 `NDTR`(剩余传输计数)或回调判断读写位置。 - 半满中断:当 DMA 传输达到缓冲区一半时触发,用于及时搬运数据,防止覆盖。 - 空闲中断:UART 接收线空闲时触发,表示一帧数据结束。 ## 1.2 竞争场景分析 假设缓冲区大小为 2N,DMA 写入位置为 `write_index`,应用读取位置为 `read_index`。 - **场景 A**:数据量恰好达到半满(N 字节)时,半满中断触发,同时 UART 空闲(如数据包长度恰为 N),空闲中断也触发。若中断处理顺序不当,可能重复处理或漏处理数据。 - **场景 B**:半满中断处理中,空闲中断到来,但半满处理尚未完成,导致空闲中断的数据被半满逻辑覆盖。 - **场景 C**:空闲中断先触发,但半满中断在空闲处理期间触发,造成数据错位。 核心问题:两个中断共享同一缓冲区索引,且无同步机制。 # 2. 精准处理策略 ## 2.1 设计原则 - 使用状态机跟踪 DMA 当前处于“半满”还是“全满”阶段。 - 利用标志位记录待处理事件,统一在安全点处理数据。 - 优先处理空闲中断(因为一帧结束,数据完整),但需避免重复。 ## 2.2 状态机定义 ```c typedef enum { DMA_STATE_HALF = 0, // 当前 DMA 写指针位于前半区 DMA_STATE_FULL = 1 // 当前 DMA 写指针位于后半区 } dma_state_t; ``` - 当半满中断触发时,切换状态,并设置 `half_flag`。 - 当全满中断(Transfer Complete)触发时,切换状态,并设置 `full_flag`。 - 空闲中断触发时,设置 `idle_flag`。 ## 2.3 中断处理逻辑 ```c volatile uint8_t half_flag = 0; volatile uint8_t full_flag = 0; volatile uint8_t idle_flag = 0; volatile dma_state_t dma_state = DMA_STATE_HALF; void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart) { half_flag = 1; dma_state = DMA_STATE_FULL; // 下次写入将进入后半区 } void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { full_flag = 1; dma_state = DMA_STATE_HALF; } void HAL_UART_IdleCpltCallback(UART_HandleTypeDef *huart) { idle_flag = 1; } ``` ## 2.4 主循环处理(或低优先级任务) ```c void ProcessUartData(void) { // 先处理空闲事件(一帧结束,数据最完整) if (idle_flag) { idle_flag = 0; // 计算当前有效数据长度 uint16_t len = GetValidDataLength(); if (len > 0) { ProcessFrame(len); } // 重置状态,防止重复处理 ClearFlags(); } // 再处理半满/全满事件(用于长数据流) if (half_flag) { half_flag = 0; // 处理前半区数据 ProcessBuffer(0, BUFFER_SIZE/2); } if (full_flag) { full_flag = 0; // 处理后半区数据 ProcessBuffer(BUFFER_SIZE/2, BUFFER_SIZE/2); } } ``` ## 2.5 关键函数实现 ```c uint16_t GetValidDataLength(void) { // 根据 DMA 当前写指针和状态计算 uint16_t write_pos = BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&hdma_uart_rx); if (dma_state == DMA_STATE_FULL) { // 写指针在后半区,有效数据从 read_index 到 write_pos return write_pos - read_index; } else { // 写指针在前半区,有效数据分为两段 return (BUFFER_SIZE - read_index) + write_pos; } } void ProcessBuffer(uint16_t start, uint16_t len) { // 从缓冲区 start 位置读取 len 字节处理 // 更新 read_index read_index = (start + len) % BUFFER_SIZE; } ``` # 3. 完整代码示例(基于 STM32 HAL) ```c // 缓冲区定义 #define BUFFER_SIZE 256 uint8_t rx_buffer[BUFFER_SIZE]; volatile uint16_t read_index = 0; // 初始化 DMA 接收 void UART_RX_Init(void) { HAL_UART_Receive_DMA(&huart1, rx_buffer, BUFFER_SIZE); __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); } // 中断回调(在 UART 中断中调用) void HAL_UART_IRQHandler(UART_HandleTypeDef *huart) { // 处理空闲中断 if (__HAL_UART_GET_FLAG(huart, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(huart); HAL_UART_IdleCpltCallback(huart); } HAL_UART_IRQHandler(huart); // 调用 HAL 默认处理(会触发 DMA 回调) } // 主循环 while (1) { ProcessUartData(); // 其他任务 } ``` # 4. 注意事项与优化 - **中断优先级**:建议将 UART 中断优先级设为高于 DMA 中断,但两者都应低于系统节拍。 - **临界区保护**:在修改 `read_index` 或标志位时,可短暂关闭中断或使用临界区,防止竞态。 - **数据覆盖风险**:若处理速度跟不上 DMA 写入,需增加缓冲区大小或采用双缓冲。 - **空闲中断误触发**:在连续数据流中,空闲中断可能不触发,此时依赖半满/全满中断保证数据不丢失。 - **测试建议**:使用串口调试助手发送不同长度数据(如 N-1、N、N+1 字节)验证逻辑。 # 结语 通过状态机与标志位分离事件处理,可有效消除半满中断与空闲中断的竞争。该方法已在实际项目验证,稳定可靠。掌握此策略,可显著提升 UART 接收的健壮性。