STM32F4 DMA双缓冲环形队列接收不定长串口数据:半传输中断与传输完成中断的竞态条件精确处理
👁 1 阅读 · 2026-08-27 · 嵌入式
在STM32F4系列嵌入式开发中,使用DMA双缓冲环形队列接收不定长串口数据是常见需求,但半传输中断与传输完成中断的竞态条件常导致数据丢失或重复处理。本文深入分析竞态产生的根源,提供基于状态机与临界区保护的精确处理方案,并给出完整代码示例,帮助开发者构建稳定高效的串口接收框架。
# 引言
在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中断的竞态问题。该方案已在多个项目中验证,稳定可靠。开发者可根据实际需求调整缓冲区数量和队列大小,以适应不同数据速率。