# 引言 在嵌入式系统开发中,UART不定长接收是高频需求。传统中断逐字节接收在高速率下会消耗大量CPU资源,而DMA+环形缓冲区方案能大幅降低负载。然而,当DMA配置为半满/全满中断时,边界竞态(Race Condition)成为隐蔽的“杀手”,稍有不慎就会导致数据错位或丢失。本文将以STM32 HAL库为例,深入解析竞态成因,并给出稳健的解决方案。 ## 一、DMA环形缓冲区与中断机制 ### 1.1 环形缓冲区原理 环形缓冲区(Ring Buffer)通常使用DMA将外设数据连续写入内存,缓冲区大小设为2的幂次(如256字节)。DMA传输完成后,通过半满(Half Transfer)和全满(Transfer Complete)中断通知CPU处理。缓冲区被逻辑上分为两个半区:前半区(0~127)和后半区(128~255)。 ### 1.2 中断触发条件 - **半满中断(HT)**:DMA传输计数达到缓冲区一半时触发。 - **全满中断(TC)**:DMA传输计数达到缓冲区大小时触发。 CPU在中断服务函数(ISR)中读取已填充的数据,并更新软件读索引。 ## 二、边界竞态的产生根源 竞态主要发生在以下场景: 1. **数据跨越半区边界**:当DMA写入位置恰好位于半区边界(如第127字节),且CPU正在处理前半区数据时,DMA可能已开始写入后半区,导致CPU误读未完成的数据。 2. **中断处理延迟**:ISR执行期间,DMA继续写入,若处理速度跟不上,可能覆盖未读数据。 3. **索引更新冲突**:软件读索引与DMA写索引(由硬件寄存器表示)的更新不同步,导致读写位置判断错误。 ## 三、精准处理策略 ### 3.1 硬件状态辅助判断 利用DMA的`NDTR`寄存器(剩余传输计数)和当前传输位置,可以精确计算DMA实际写入位置。在ISR中,通过读取`NDTR`值,结合缓冲区大小,可推算出当前写索引: ```c uint32_t get_dma_write_index(DMA_HandleTypeDef *hdma) { uint32_t ndtr = hdma->Instance->NDTR; return BUFFER_SIZE - ndtr; } ``` ### 3.2 软件状态机设计 定义缓冲区状态: ```c typedef enum { BUF_EMPTY, // 空 BUF_HALF, // 前半区有数据 BUF_FULL, // 后半区有数据 BUF_OVERFLOW // 溢出 } buf_state_t; ``` 在ISR中,根据中断类型和当前写索引更新状态,并决定处理哪个半区。 ### 3.3 关键代码实现 以下为完整示例(基于STM32 HAL库): ```c #define BUFFER_SIZE 256 #define HALF_SIZE (BUFFER_SIZE/2) uint8_t dma_rx_buf[BUFFER_SIZE]; volatile uint32_t read_index = 0; volatile buf_state_t state = BUF_EMPTY; // DMA中断回调(半满和全满共用) void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart) { process_half(HALF_SIZE); } void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { process_half(BUFFER_SIZE); } void process_half(uint32_t end_index) { uint32_t write_index = get_dma_write_index(&huart1); uint32_t start_index = (end_index >= HALF_SIZE) ? HALF_SIZE : 0; // 处理数据:从read_index到end_index(注意环形回绕) while (read_index != end_index) { process_byte(dma_rx_buf[read_index]); read_index = (read_index + 1) % BUFFER_SIZE; } // 更新状态 if (write_index == read_index) { state = BUF_EMPTY; } else if (write_index > read_index) { state = (write_index - read_index) > HALF_SIZE ? BUF_FULL : BUF_HALF; } else { state = (BUFFER_SIZE - read_index + write_index) > HALF_SIZE ? BUF_FULL : BUF_HALF; } } // 主循环中处理数据 void main_loop() { while (1) { if (state != BUF_EMPTY) { // 根据状态处理数据,例如解析帧 } } } ``` ### 3.4 边界竞态消除技巧 - **禁止中断嵌套**:在ISR中暂时关闭其他高优先级中断,或使用临界区保护。 - **使用双缓冲**:将缓冲区分为两个独立数组,每个半区一个,避免交叉。 - **延迟处理**:在ISR中仅拷贝数据到临时区,主循环中解析,减少ISR占用时间。 ## 四、注意事项 1. **NDTR寄存器访问**:读取NDTR时需注意字节序和位宽,STM32为32位寄存器,但仅低16位有效。 2. **内存屏障**:在读取DMA写入的数据前,需要`__DMB()`指令确保数据可见性。 3. **溢出保护**:当`read_index`与写索引相等时,可能为空或满,需通过状态标志区分。 4. **测试覆盖**:建议使用串口工具发送特定长度数据(如127、128、129字节)进行边界测试。 ## 五、总结 通过结合硬件NDTR寄存器精确计算写位置,并设计软件状态机管理缓冲区状态,可以有效消除半满/全满中断的边界竞态。该方法在STM32F4/F7/H7系列上验证稳定,可扩展至其他DMA外设。开发者应根据实际需求调整缓冲区大小和处理策略,确保系统实时性和可靠性。