UART DMA 环形缓冲区:半满/全满中断边界竞态的精确处理策略
👁 1 阅读 · 2026-08-27 · 嵌入式
在嵌入式开发中,使用 DMA 环形缓冲区接收 UART 不定长帧是提升效率的常用手段,但半满与全满中断的边界竞态常导致数据丢失或重复处理。本文深入剖析该竞态的产生机理,并给出基于状态机与指针快照的精确处理方案,附完整代码示例与注意事项,帮助开发者彻底解决这一棘手问题。
# 用 DMA 环形缓冲区实现 UART 不定长帧接收:半满与全满中断的边界竞态精确处理
## 一、背景与问题定义
在 STM32 等 MCU 上,利用 DMA 的循环模式接收 UART 数据,可大幅降低 CPU 负载。通常配置 DMA 传输长度(如 256 字节),并开启半传输中断(HT)和传输完成中断(TC)。当缓冲区半满或全满时,CPU 在中断中搬运数据到应用缓冲区,形成环形缓冲。
然而,**边界竞态**发生在以下场景:
- 数据恰好落在半满点(如第 128 字节)时,HT 中断触发,但此时 DMA 可能已写入第 129 字节。
- 数据跨越半满点后,TC 中断触发,但 CPU 尚未处理完 HT 中断的数据,导致重复读取或漏读。
- 更隐蔽的是,当数据长度恰好等于半满或全满时,中断标志可能同时或先后置位,若处理不当,会丢失边界字节。
## 二、竞态根源分析
DMA 的 HT 和 TC 中断基于传输计数(DMA_CNDTR)判断。在循环模式下,CNDTR 从初值递减到半值或零时触发中断。但中断响应有延迟,且 CPU 读取 CNDTR 时可能已变化,导致判断不准确。
核心矛盾:**中断触发点与数据实际写入点不同步**。例如:
- 配置 DMA 长度为 256,半满点为 128。当 CNDTR 从 128 减到 127 时,HT 中断置位,但此时 DMA 已写入第 129 个字节(索引 128)。若 CPU 在中断中只处理前 128 字节,则第 129 字节会被下一次处理覆盖。
- 同理,TC 中断在 CNDTR 从 1 减到 0 时触发,但 DMA 已写入第 256 字节,且可能已回绕到缓冲区开头写入新数据。
## 三、解决方案:基于状态机的双缓冲处理
### 3.1 设计思路
采用**双缓冲索引 + 状态机**,避免依赖中断标志的精确性,而是通过记录 DMA 当前写位置(CNDTR 反推)和上次处理位置,计算可安全处理的数据量。
核心变量:
- `dma_buffer`:DMA 循环缓冲区,大小 `BUF_SIZE`(2 的幂,便于取模)。
- `last_pos`:上次处理到的位置(字节索引)。
- `state`:当前状态(IDLE、HALF、FULL)。
### 3.2 中断处理逻辑
在 HT 和 TC 中断中,不直接处理数据,而是设置标志位,并在主循环或中断尾部调用统一处理函数。
```c
#define BUF_SIZE 256
#define HALF_SIZE (BUF_SIZE/2)
volatile uint8_t dma_buffer[BUF_SIZE];
volatile uint32_t last_pos = 0;
volatile uint8_t state = 0; // 0:IDLE, 1:HALF, 2:FULL
void DMA_IRQHandler(void) {
if (DMA_GetITStatus(DMA1_IT_HT)) {
DMA_ClearITPendingBit(DMA1_IT_HT);
state = 1; // 标记半满
}
if (DMA_GetITStatus(DMA1_IT_TC)) {
DMA_ClearITPendingBit(DMA1_IT_TC);
state = 2; // 标记全满
}
}
void Process_DMA_Buffer(void) {
uint32_t current_pos = BUF_SIZE - DMA_GetCurrDataCounter(DMA1_Channel4);
uint32_t available = 0;
if (state == 1) {
// 半满:处理从 last_pos 到 HALF_SIZE 的数据
available = HALF_SIZE - last_pos;
if (available > 0) {
HandleData(&dma_buffer[last_pos], available);
last_pos = HALF_SIZE;
}
state = 0;
} else if (state == 2) {
// 全满:处理从 last_pos 到 BUF_SIZE,再处理从 0 到 current_pos(可能已回绕)
available = BUF_SIZE - last_pos;
if (available > 0) {
HandleData(&dma_buffer[last_pos], available);
}
// 处理回绕部分(从 0 到 current_pos)
if (current_pos > 0) {
HandleData(&dma_buffer[0], current_pos);
}
last_pos = current_pos;
state = 0;
}
}
```
### 3.3 消除竞态的关键点
- **使用 current_pos 而非中断标志**:通过 `DMA_GetCurrDataCounter()` 获取当前写位置,确保处理的数据量准确。
- **状态机防止重复处理**:每次中断只处理一次,且 `last_pos` 更新后,下次中断基于新位置计算。
- **处理回绕**:全满时,数据可能已回绕到缓冲区开头,需分段处理。
## 四、完整代码示例(STM32F1 系列)
```c
// 初始化 DMA
void UART_DMA_Init(void) {
DMA_InitTypeDef DMA_InitStructure;
USART_InitTypeDef USART_InitStructure;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
// USART 配置(略)
DMA_DeInit(DMA1_Channel4);
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)dma_buffer;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
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_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel4, &DMA_InitStructure);
// 开启 HT 和 TC 中断
DMA_ITConfig(DMA1_IT_HT4, ENABLE);
DMA_ITConfig(DMA1_IT_TC4, ENABLE);
// 使能 DMA 和 USART
DMA_Cmd(DMA1_Channel4, ENABLE);
USART_DMACmd(USART1, USART_DMAReq_RX, ENABLE);
// 配置 NVIC(略)
}
// 数据处理函数(用户实现)
void HandleData(uint8_t *data, uint32_t len) {
// 例如:解析帧、存储到环形队列等
}
// 主循环中调用
int main(void) {
// 初始化...
while (1) {
if (state != 0) {
Process_DMA_Buffer();
}
// 其他任务
}
}
```
## 五、注意事项
- **缓冲区大小应为 2 的幂**,便于取模运算,提高效率。
- **中断优先级**:确保 DMA 中断优先级高于其他可能长时间阻塞的中断,避免数据覆盖。
- **临界区保护**:在更新 `last_pos` 和 `state` 时,可暂时关闭中断或使用临界区,防止主循环与中断冲突。
- **测试边界情况**:重点测试数据长度恰好为 128、256、129 等边界值,验证无数据丢失。
- **性能考量**:若数据速率极高,可在中断中直接处理,但需保证处理时间短于 DMA 写入一个字节的时间。
## 六、总结
通过状态机与 DMA 当前计数器的结合,我们精确地解决了半满/全满中断的边界竞态。该方法不依赖中断标志的精确时序,而是基于实际写位置计算可处理数据量,从根本上避免了重复或遗漏。此模式可广泛应用于各种需要 DMA 接收不定长帧的嵌入式场景,如串口通信、传感器数据采集等。