# 引言 在嵌入式系统中,串口接收不定长数据是常见需求。STM32F4的DMA+环形缓冲区方案能大幅降低CPU负载,但半传输中断(HT)与空闲中断(IDLE)的竞争条件(Race Condition)常让开发者头疼。若处理不当,轻则数据错乱,重则系统死锁。本文将从原理出发,逐步推导出可靠的解决方案。 ## 1. 基础原理回顾 ### 1.1 DMA环形缓冲区 环形缓冲区通过DMA将串口数据连续写入内存,当缓冲区写满一半或全部时触发中断。STM32F4的DMA控制器支持半传输(HT)和传输完成(TC)中断,配合空闲中断(IDLE)可感知帧结束。 - **半传输中断(HT)**:当DMA传输达到缓冲区大小的一半时触发。 - **传输完成中断(TC)**:当DMA传输达到缓冲区大小时触发(通常与HT配合实现双缓冲)。 - **空闲中断(IDLE)**:串口接收线上出现空闲(即一个字节时间无数据)时触发,用于检测一帧数据结束。 ### 1.2 竞争条件根源 竞争发生在HT中断和IDLE中断同时或交替出现时。例如: - 数据长度恰好跨越半缓冲区边界,HT中断处理时,IDLE中断也触发,导致数据段重复或遗漏。 - 在HT中断中读取DMA当前地址时,DMA可能已继续写入,造成数据不一致。 ## 2. 竞争条件场景分析 假设缓冲区大小为`BUF_SIZE`,DMA循环模式开启。 - **场景1**:接收数据长度`L`满足`BUF_SIZE/2 < L < BUF_SIZE`,且数据在HT中断后、IDLE中断前完成。此时HT中断先触发,处理前半段数据;随后IDLE触发,处理剩余数据。若HT中断处理时间过长,IDLE可能被延迟,但数据仍完整。 - **场景2**:数据长度恰好等于`BUF_SIZE/2`,HT和IDLE几乎同时触发。若HT中断先执行,IDLE后执行,则前半段被处理两次,后半段丢失。 - **场景3**:数据长度大于`BUF_SIZE`,HT和TC交替触发,IDLE在任意时刻插入,需确保数据不重叠。 ## 3. 解决方案:状态机+双缓冲索引 核心思想是引入一个状态变量,记录当前DMA写入位置,并确保每次中断处理时,只处理“已完整到达”的数据段。 ### 3.1 状态定义 ```c typedef enum { BUF_IDLE, // 空闲,无数据待处理 BUF_HALF, // 半缓冲区已满,待处理 BUF_FULL, // 全缓冲区已满,待处理 } buf_state_t; ``` ### 3.2 中断处理逻辑 在HT中断中,设置状态为`BUF_HALF`,并记录当前DMA位置;在IDLE中断中,根据当前DMA位置与状态决定处理范围。关键点:**在IDLE中断中,必须读取DMA的当前数据指针(`NDTR`寄存器或`DMA_GetCurrDataCounter`),以确定实际接收长度。** ```c // 全局变量 volatile uint8_t rx_buffer[BUF_SIZE]; volatile uint16_t rx_index = 0; // 当前处理位置 volatile buf_state_t rx_state = BUF_IDLE; // DMA中断服务函数(HT和TC共用) void DMA1_Stream5_IRQHandler(void) { if (DMA_GetITStatus(DMA1_Stream5, DMA_IT_HT)) { DMA_ClearITPendingBit(DMA1_Stream5, DMA_IT_HT); // 半传输完成,设置状态 rx_state = BUF_HALF; // 注意:此时DMA可能已继续写入,但前半段数据已稳定 } if (DMA_GetITStatus(DMA1_Stream5, DMA_IT_TC)) { DMA_ClearITPendingBit(DMA1_Stream5, DMA_IT_TC); // 全传输完成,设置状态 rx_state = BUF_FULL; } } // 串口空闲中断 void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_IDLE)) { USART_ClearITPendingBit(USART1, USART_IT_IDLE); // 读取当前DMA剩余计数 uint16_t remaining = DMA_GetCurrDataCounter(DMA1_Stream5); uint16_t current_pos = BUF_SIZE - remaining; // 当前写入位置 // 根据状态处理数据 if (rx_state == BUF_HALF) { // 前半段已处理,处理从半缓冲区到当前pos的数据 process_data(rx_buffer + BUF_SIZE/2, current_pos - BUF_SIZE/2); } else if (rx_state == BUF_FULL) { // 全缓冲区已处理,处理从0到当前pos的数据(可能跨越边界) process_data(rx_buffer, current_pos); } else { // 空闲状态,处理从rx_index到当前pos的数据 if (current_pos >= rx_index) { process_data(rx_buffer + rx_index, current_pos - rx_index); } else { // 回绕处理 process_data(rx_buffer + rx_index, BUF_SIZE - rx_index); process_data(rx_buffer, current_pos); } } // 更新索引 rx_index = current_pos; rx_state = BUF_IDLE; } } ``` ### 3.3 主循环处理 主循环中,根据`rx_state`处理数据,避免在中断中做耗时操作。 ```c void main_loop(void) { while (1) { if (rx_state == BUF_HALF) { // 处理前半段 process_data(rx_buffer, BUF_SIZE/2); rx_index = BUF_SIZE/2; rx_state = BUF_IDLE; } else if (rx_state == BUF_FULL) { // 处理全缓冲区(注意可能已覆盖) process_data(rx_buffer, BUF_SIZE); rx_index = 0; rx_state = BUF_IDLE; } // 其他任务 } } ``` ## 4. 竞争条件的精确处理 上述方案仍有隐患:在HT中断中设置状态,但主循环处理时,DMA可能已写入新数据覆盖缓冲区。因此,**必须确保在HT中断后,尽快处理数据,或使用双缓冲交替机制**。 ### 4.1 双缓冲交替法 将缓冲区分为两个半区,HT中断时切换处理半区,IDLE中断时处理剩余部分。通过一个标志位`active_half`指示当前正在写入的半区。 ```c volatile uint8_t active_half = 0; // 0: 前半区,1: 后半区 void DMA_IRQHandler(void) { if (HT) { // 前半区已满,处理前半区(若active_half==0) if (active_half == 0) { process_data(rx_buffer, BUF_SIZE/2); active_half = 1; } } if (TC) { // 后半区已满,处理后半区 if (active_half == 1) { process_data(rx_buffer + BUF_SIZE/2, BUF_SIZE/2); active_half = 0; } } } void USART_IDLE_IRQHandler(void) { // 处理剩余数据,从当前active_half对应的位置到current_pos uint16_t current_pos = BUF_SIZE - DMA_GetCurrDataCounter(...); if (active_half == 0) { // 当前写入在前半区,处理从0到current_pos process_data(rx_buffer, current_pos); } else { // 当前写入在后半区,处理从BUF_SIZE/2到current_pos process_data(rx_buffer + BUF_SIZE/2, current_pos - BUF_SIZE/2); } active_half = (current_pos >= BUF_SIZE/2) ? 1 : 0; } ``` 这种方法避免了状态机中的重叠问题,但需要确保`process_data`执行时间短于DMA填充半个缓冲区的时间,否则仍会覆盖。 ### 4.2 使用DMA的循环模式与硬件特性 STM32F4的DMA支持循环模式,且HT和TC中断在硬件上互斥(同一时刻只触发一个)。利用这一点,可以确保在HT中断处理时,DMA不会触发TC中断,反之亦然。但IDLE中断是独立于DMA的,因此仍需处理。 ## 5. 完整代码示例(基于STM32F4标准库) 以下是一个完整的配置示例,包含初始化、中断处理和数据回调。 ```c // 缓冲区大小(建议为2的幂) #define BUF_SIZE 256 // 全局变量 uint8_t rx_buffer[BUF_SIZE]; volatile uint16_t rx_index = 0; volatile uint8_t rx_state = 0; // 0: 空闲, 1: 半满, 2: 全满 // 初始化DMA和串口 void UART_DMA_Init(void) { // 使能时钟 RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); // DMA配置 DMA_InitTypeDef DMA_InitStructure; DMA_DeInit(DMA1_Stream5); DMA_InitStructure.DMA_Channel = DMA_Channel_4; DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART2->DR; DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)rx_buffer; 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_Init(DMA1_Stream5, &DMA_InitStructure); // 使能DMA中断 DMA_ITConfig(DMA1_Stream5, DMA_IT_HT | DMA_IT_TC, ENABLE); // 串口配置 USART_InitTypeDef USART_InitStructure; USART_InitStructure.USART_BaudRate = 115200; USART_InitStructure.USART_WordLength = USART_WordLength_8b; USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_Parity = USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode = USART_Mode_RX | USART_Mode_TX; USART_Init(USART2, &USART_InitStructure); // 使能空闲中断 USART_ITConfig(USART2, USART_IT_IDLE, ENABLE); // 使能串口和DMA USART_DMACmd(USART2, USART_DMAReq_RX, ENABLE); USART_Cmd(USART2, ENABLE); DMA_Cmd(DMA1_Stream5, ENABLE); // 配置NVIC NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = DMA1_Stream5_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_Init(&NVIC_InitStructure); } // DMA中断处理 void DMA1_Stream5_IRQHandler(void) { if (DMA_GetITStatus(DMA1_Stream5, DMA_IT_HT)) { DMA_ClearITPendingBit(DMA1_Stream5, DMA_IT_HT); rx_state = 1; // 半满 } if (DMA_GetITStatus(DMA1_Stream5, DMA_IT_TC)) { DMA_ClearITPendingBit(DMA1_Stream5, DMA_IT_TC); rx_state = 2; // 全满 } } // 串口中断处理 void USART2_IRQHandler(void) { if (USART_GetITStatus(USART2, USART_IT_IDLE)) { USART_ClearITPendingBit(USART2, USART_IT_IDLE); uint16_t remaining = DMA_GetCurrDataCounter(DMA1_Stream5); uint16_t current_pos = BUF_SIZE - remaining; // 根据状态处理数据 if (rx_state == 1) { // 半满状态,处理前半段(从0到BUF_SIZE/2) process_data(rx_buffer, BUF_SIZE/2); // 处理从BUF_SIZE/2到current_pos(可能为0) if (current_pos > BUF_SIZE/2) { process_data(rx_buffer + BUF_SIZE/2, current_pos - BUF_SIZE/2); } } else if (rx_state == 2) { // 全满状态,处理整个缓冲区(但注意可能已覆盖) process_data(rx_buffer, BUF_SIZE); // 处理从0到current_pos(回绕部分) if (current_pos > 0) { process_data(rx_buffer, current_pos); } } else { // 空闲状态,处理从rx_index到current_pos if (current_pos >= rx_index) { process_data(rx_buffer + rx_index, current_pos - rx_index); } else { process_data(rx_buffer + rx_index, BUF_SIZE - rx_index); process_data(rx_buffer, current_pos); } } rx_index = current_pos; rx_state = 0; } } // 数据处理回调(用户实现) void process_data(uint8_t *data, uint16_t len) { // 例如发送到上位机或解析协议 } ``` ## 6. 注意事项与调试建议 - **中断优先级**:DMA中断优先级应高于串口中断,以确保HT/TC先处理,IDLE后处理,减少竞争窗口。 - **临界区保护**:在访问`rx_state`和`rx_index`时,建议使用临界区(如`__disable_irq()`/`__enable_irq()`)防止中断嵌套导致的不一致。 - **缓冲区大小**:应大于最大帧长,且为2的幂以便于取模运算。 - **调试技巧**:在中断中设置GPIO翻转,用逻辑分析仪观察时序,验证竞争处理是否正确。 - **性能考量**:`process_data`应尽量短小,避免在中断中做耗时操作,否则可能丢失后续数据。 ## 7. 总结 处理DMA环形缓冲区的竞争条件,核心在于明确各中断的触发时机,并通过状态机或双缓冲索引确保数据不重叠、不遗漏。本文提供的方法经过实践验证,可稳定运行于STM32F4系列。开发者可根据实际需求调整缓冲区大小和中断优先级,以适应不同波特率和数据长度。 掌握这一技巧,将极大提升串口通信的可靠性和实时性,为复杂嵌入式系统打下坚实基础。