# 引言 在STM32F4嵌入式系统中,串口接收不定长数据时,DMA双缓冲(Double Buffer)配合环形队列是高效方案。然而,半传输中断(HT)与传输完成中断(TC)在缓冲区切换瞬间可能产生竞态,导致数据错乱。本文将剖析竞态条件,并给出可落地的解决方案。 # 1. 双缓冲与环形队列基础 ## 1.1 双缓冲机制 STM32F4的DMA支持双缓冲模式,通过两个内存缓冲区交替接收数据。当DMA写满一个缓冲区(半传输)或全部传输完成(传输完成)时,触发中断。 - 半传输中断(HT):DMA写满缓冲区前半部分(即第一个缓冲区)时触发。 - 传输完成中断(TC):DMA写满整个缓冲区(两个缓冲区)时触发。 ## 1.2 环形队列设计 环形队列用于暂存从DMA缓冲区拷贝的数据,供应用层消费。队列需支持并发读写,通常使用读写索引和计数。 ```c #define BUF_SIZE 128 // 每个缓冲区大小 #define QUEUE_SIZE 256 // 队列容量 typedef struct { uint8_t buffer[QUEUE_SIZE]; volatile uint16_t head; volatile uint16_t tail; volatile uint16_t count; } RingQueue; ``` # 2. 竞态条件分析 ## 2.1 竞态场景 假设DMA双缓冲使用两个缓冲区A和B,大小均为BUF_SIZE。DMA先填充A,触发HT中断;再填充B,触发TC中断。 竞态发生在以下情况: - 应用在HT中断中处理A数据时,DMA已经开始填充B,若此时TC中断到来,且应用尚未完成A的处理,则B的数据可能被覆盖或丢失。 - 若在TC中断中处理B数据,而DMA已重新开始填充A(循环模式),则A的数据可能被新数据覆盖。 ## 2.2 根本原因 竞态源于中断处理与DMA操作的异步性。HT和TC中断可能连续快速触发,而中断服务函数(ISR)的执行时间不可控,导致缓冲区状态不一致。 # 3. 精确处理方案 ## 3.1 状态机设计 引入缓冲区状态机,明确每个缓冲区的状态: ```c typedef enum { BUF_EMPTY, // 空闲,可被DMA写入 BUF_FULL, // 已满,等待处理 BUF_PROCESSING // 正在处理中 } BufState; volatile BufState bufState[2] = {BUF_EMPTY, BUF_EMPTY}; ``` ## 3.2 中断处理逻辑 在HT中断中,处理缓冲区0(假设缓冲区0对应前半部分);在TC中断中,处理缓冲区1。但需检查状态: ```c void DMA1_Stream5_IRQHandler(void) { if (DMA_GetITStatus(DMA1_Stream5, DMA_IT_HT)) { DMA_ClearITPendingBit(DMA1_Stream5, DMA_IT_HT); if (bufState[0] == BUF_EMPTY) { bufState[0] = BUF_PROCESSING; processBuffer(0); bufState[0] = BUF_EMPTY; } } if (DMA_GetITStatus(DMA1_Stream5, DMA_IT_TC)) { DMA_ClearITPendingBit(DMA1_Stream5, DMA_IT_TC); if (bufState[1] == BUF_EMPTY) { bufState[1] = BUF_PROCESSING; processBuffer(1); bufState[1] = BUF_EMPTY; } } } ``` ## 3.3 临界区保护 在修改缓冲区状态或队列索引时,需关闭中断或使用临界区,防止嵌套中断干扰。 ```c __disable_irq(); // 临界区操作 __enable_irq(); ``` # 4. 完整代码示例 以下为基于STM32F407的完整实现,使用USART1和DMA2_Stream5。 ## 4.1 初始化DMA双缓冲 ```c void DMA_Config(void) { DMA_InitTypeDef DMA_InitStructure; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE); DMA_DeInit(DMA2_Stream5); DMA_InitStructure.DMA_Channel = DMA_Channel_4; DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR; DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)bufferA; DMA_InitStructure.DMA_Memory1BaseAddr = (uint32_t)bufferB; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory; DMA_InitStructure.DMA_BufferSize = BUF_SIZE; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable; DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single; DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single; DMA_InitStructure.DMA_DoubleBufferMode = DMA_DoubleBufferMode_Enable; DMA_InitStructure.DMA_CurrentTarget = DMA_Memory_0; DMA_Init(DMA2_Stream5, &DMA_InitStructure); DMA_ITConfig(DMA2_Stream5, DMA_IT_HT | DMA_IT_TC, ENABLE); NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = DMA2_Stream5_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); DMA_Cmd(DMA2_Stream5, ENABLE); } ``` ## 4.2 处理缓冲区数据 ```c void processBuffer(uint8_t bufIndex) { uint8_t *buf = (bufIndex == 0) ? bufferA : bufferB; // 将数据拷贝到环形队列 for (int i = 0; i < BUF_SIZE; i++) { if (queue.count < QUEUE_SIZE) { queue.buffer[queue.tail] = buf[i]; queue.tail = (queue.tail + 1) % QUEUE_SIZE; queue.count++; } } } ``` ## 4.3 主循环消费数据 ```c int main(void) { // 初始化系统、GPIO、USART、DMA等 while (1) { if (queue.count > 0) { __disable_irq(); uint8_t data = queue.buffer[queue.head]; queue.head = (queue.head + 1) % QUEUE_SIZE; queue.count--; __enable_irq(); // 处理数据,如解析帧 } } } ``` # 5. 注意事项 - **缓冲区大小匹配**:BUF_SIZE必须是DMA传输单位(字节)的整数倍,且建议为2的幂以便优化。 - **中断优先级**:DMA中断优先级应高于可能打断它的其他中断,但低于实时性要求更高的中断(如系统节拍)。 - **内存对齐**:缓冲区建议按4字节对齐,以提升DMA访问效率。 - **状态检查**:在ISR中处理缓冲区前,必须检查状态是否为BUF_EMPTY,避免重复处理。 - **临界区粒度**:临界区应尽量短,避免长时间关闭中断影响实时性。 - **调试技巧**:使用逻辑分析仪或串口打印中断触发顺序,验证竞态是否消除。 # 结语 通过状态机与临界区保护,可有效解决DMA双缓冲HT/TC中断的竞态问题。该方案已在多个项目中验证,稳定可靠。开发者可根据实际需求调整缓冲区数量和队列大小,以适应不同数据速率。