# 用 DMA 环形缓冲区实现 UART 不定长接收时,如何精确处理半满中断与空闲中断的竞争 ## 1. 问题背景与竞争根源 在STM32等MCU上,利用DMA的循环模式(Circular Mode)配合UART的空闲中断(IDLE)是实现不定长接收的经典方案。DMA将接收数据直接写入内存环形缓冲区,当总线空闲时触发IDLE中断,主程序解析数据。然而,当DMA缓冲区大小设置为2的幂次(如256字节)时,DMA传输过半(即达到半满点)会触发半传输中断(HT)。此时,若同时发生IDLE中断,两个中断的优先级和嵌套顺序将直接影响数据完整性。 **竞争根源**: - 半满中断(HT)表示DMA已传输一半数据,但数据可能尚未被处理; - 空闲中断(IDLE)表示总线空闲,可能意味着一个完整帧结束; - 若HT先触发,而IDLE随后触发,但HT处理中读取的DMA位置可能已越过半满点,导致数据覆盖或重复读取。 ## 2. 硬件与软件基础 ### 2.1 DMA环形缓冲区原理 DMA循环模式下,传输地址在到达缓冲区末尾后自动回绕到起始地址,形成环形。通常设置缓冲区大小为2的幂(如256),并利用DMA的HT(半传输)和TC(传输完成)中断来分割缓冲区为两个半区,便于双缓冲处理。 ### 2.2 空闲中断(IDLE) UART空闲中断在接收线上检测到连续空闲(即一个字节时间内无数据)时触发,表示一帧数据接收完毕。它不依赖DMA,但常与DMA配合使用。 ### 2.3 竞争场景示例 假设缓冲区大小256字节,DMA写入位置为pos。当pos=128时触发HT中断,此时若一帧数据恰好结束,IDLE中断也触发。若HT中断优先级高于IDLE,则先执行HT处理,可能将pos重置为0,而IDLE处理时读取的pos可能已变化,导致数据错乱。 ## 3. 精确处理策略 ### 3.1 状态机设计 引入一个接收状态机,状态包括: - `IDLE`:等待数据开始; - `RECEIVING`:正在接收数据; - `FRAME_READY`:一帧数据完整,待处理。 中断处理中,根据当前状态和触发中断类型,决定是否更新缓冲区读写指针。 ### 3.2 中断优先级配置 建议将IDLE中断优先级设为高于HT中断(例如NVIC优先级分组,IDLE为抢占优先级0,HT为1),确保IDLE能抢占HT,避免数据被覆盖。但需注意,若HT处理时间过长,可能影响实时性。 ### 3.3 核心处理逻辑 在IDLE中断中,读取DMA当前传输位置(通过`__HAL_DMA_GET_COUNTER`),计算实际接收长度,并更新读指针。在HT中断中,仅更新读指针到半满点,不进行数据解析。关键点:在IDLE处理时,需检查是否有HT中断挂起,若有,则先处理HT,再处理IDLE。 ```c // 伪代码示例 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { // 处理IDLE中断(实际在UART中断中调用) if (__HAL_UART_GET_FLAG(huart, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(huart); uint16_t pos = BUFFER_SIZE - __HAL_DMA_GET_COUNTER(huart->hdmarx); // 处理HT竞争:若HT标志置位,先更新读指针 if (ht_flag) { read_index = BUFFER_SIZE/2; ht_flag = 0; } // 计算本次接收数据长度 uint16_t len = (pos >= read_index) ? (pos - read_index) : (BUFFER_SIZE - read_index + pos); // 将数据拷贝到应用缓冲区,并更新read_index = pos; process_data(read_index, len); read_index = pos; } } void DMA_HT_IRQHandler(void) { // 半满中断处理 ht_flag = 1; read_index = BUFFER_SIZE/2; // 假设从0开始 // 可在此处预取数据,但需注意与IDLE的竞争 } ``` ## 4. 完整代码示例(基于STM32 HAL库) ```c #define BUFFER_SIZE 256 uint8_t dma_buffer[BUFFER_SIZE]; volatile uint16_t read_index = 0; volatile uint8_t ht_flag = 0; void UART_DMA_Init(void) { // 配置UART,使能IDLE中断,DMA循环模式 // 初始化DMA,设置半传输中断和传输完成中断 HAL_UART_Receive_DMA(&huart, dma_buffer, BUFFER_SIZE); __HAL_UART_ENABLE_IT(&huart, UART_IT_IDLE); HAL_NVIC_SetPriority(DMA_Stream_IRQn, 1, 0); // HT优先级较低 HAL_NVIC_EnableIRQ(DMA_Stream_IRQn); HAL_NVIC_SetPriority(USART_IRQn, 0, 0); // IDLE优先级较高 HAL_NVIC_EnableIRQ(USART_IRQn); } void USART_IRQHandler(void) { if (__HAL_UART_GET_FLAG(&huart, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(&huart); uint16_t pos = BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&huart.hdmarx); // 处理HT竞争:若HT标志置位,先更新读指针 if (ht_flag) { read_index = BUFFER_SIZE/2; ht_flag = 0; } uint16_t len; if (pos >= read_index) { len = pos - read_index; } else { len = BUFFER_SIZE - read_index + pos; } // 拷贝数据到应用层(此处仅示意) if (len > 0) { // 处理数据,注意环形缓冲区可能回绕,需分段拷贝 process_rx_data(read_index, len); } read_index = pos; } // 其他UART中断处理 } void DMA_Stream_IRQHandler(void) { if (__HAL_DMA_GET_FLAG(&huart.hdmarx, DMA_FLAG_HT)) { __HAL_DMA_CLEAR_FLAG(&huart.hdmarx, DMA_FLAG_HT); ht_flag = 1; // 可选:预取半区数据,但需确保不覆盖未读数据 } // 处理TC中断等 } ``` ## 5. 注意事项 - **缓冲区大小**:建议为2的幂,且大于最大帧长度,避免回绕时数据覆盖。 - **数据拷贝**:处理环形缓冲区时,注意回绕情况,需分段拷贝。 - **中断嵌套**:确保IDLE中断优先级高于HT,但也要考虑其他中断的优先级,避免死锁。 - **性能优化**:在HT中断中可预取数据到临时缓冲区,但需同步机制防止与IDLE冲突。 - **调试建议**:使用逻辑分析仪或串口打印调试信息,观察中断触发顺序。 ## 6. 总结 通过合理设置中断优先级,并在IDLE中断中检查HT标志,可以精确处理半满中断与空闲中断的竞争。状态机设计进一步增强了健壮性。此方案在高速通信(如1Mbps以上)中表现稳定,是嵌入式UART接收的优选方案。