UART 不定长接收:DMA 环形缓冲区中半满中断与空闲中断的竞争消除策略
👁 1 阅读 · 2026-08-27 · 嵌入式
在嵌入式开发中,使用 DMA 环形缓冲区接收 UART 不定长数据是兼顾实时性与 CPU 效率的经典方案。然而,半满中断与空闲中断的竞争常导致数据错乱或丢失。本文深入剖析竞争产生的根源,并给出基于状态机与标志位管理的精准处理策略,附完整代码示例与注意事项,助你彻底解决这一痛点。
# 引言
在 STM32 等单片机上,UART 接收不定长数据时,DMA 环形缓冲区 + 空闲中断(IDLE)是常用组合。但引入半满中断(Half Transfer)后,两者可能同时触发,导致数据处理顺序混乱。本文从原理出发,分析竞争场景,并给出可落地的解决方案。
# 1. 基础原理与竞争根源
## 1.1 DMA 环形缓冲区工作模式
- 环形缓冲区:DMA 在内存中循环写入,通过 `NDTR`(剩余传输计数)或回调判断读写位置。
- 半满中断:当 DMA 传输达到缓冲区一半时触发,用于及时搬运数据,防止覆盖。
- 空闲中断:UART 接收线空闲时触发,表示一帧数据结束。
## 1.2 竞争场景分析
假设缓冲区大小为 2N,DMA 写入位置为 `write_index`,应用读取位置为 `read_index`。
- **场景 A**:数据量恰好达到半满(N 字节)时,半满中断触发,同时 UART 空闲(如数据包长度恰为 N),空闲中断也触发。若中断处理顺序不当,可能重复处理或漏处理数据。
- **场景 B**:半满中断处理中,空闲中断到来,但半满处理尚未完成,导致空闲中断的数据被半满逻辑覆盖。
- **场景 C**:空闲中断先触发,但半满中断在空闲处理期间触发,造成数据错位。
核心问题:两个中断共享同一缓冲区索引,且无同步机制。
# 2. 精准处理策略
## 2.1 设计原则
- 使用状态机跟踪 DMA 当前处于“半满”还是“全满”阶段。
- 利用标志位记录待处理事件,统一在安全点处理数据。
- 优先处理空闲中断(因为一帧结束,数据完整),但需避免重复。
## 2.2 状态机定义
```c
typedef enum {
DMA_STATE_HALF = 0, // 当前 DMA 写指针位于前半区
DMA_STATE_FULL = 1 // 当前 DMA 写指针位于后半区
} dma_state_t;
```
- 当半满中断触发时,切换状态,并设置 `half_flag`。
- 当全满中断(Transfer Complete)触发时,切换状态,并设置 `full_flag`。
- 空闲中断触发时,设置 `idle_flag`。
## 2.3 中断处理逻辑
```c
volatile uint8_t half_flag = 0;
volatile uint8_t full_flag = 0;
volatile uint8_t idle_flag = 0;
volatile dma_state_t dma_state = DMA_STATE_HALF;
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart) {
half_flag = 1;
dma_state = DMA_STATE_FULL; // 下次写入将进入后半区
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
full_flag = 1;
dma_state = DMA_STATE_HALF;
}
void HAL_UART_IdleCpltCallback(UART_HandleTypeDef *huart) {
idle_flag = 1;
}
```
## 2.4 主循环处理(或低优先级任务)
```c
void ProcessUartData(void) {
// 先处理空闲事件(一帧结束,数据最完整)
if (idle_flag) {
idle_flag = 0;
// 计算当前有效数据长度
uint16_t len = GetValidDataLength();
if (len > 0) {
ProcessFrame(len);
}
// 重置状态,防止重复处理
ClearFlags();
}
// 再处理半满/全满事件(用于长数据流)
if (half_flag) {
half_flag = 0;
// 处理前半区数据
ProcessBuffer(0, BUFFER_SIZE/2);
}
if (full_flag) {
full_flag = 0;
// 处理后半区数据
ProcessBuffer(BUFFER_SIZE/2, BUFFER_SIZE/2);
}
}
```
## 2.5 关键函数实现
```c
uint16_t GetValidDataLength(void) {
// 根据 DMA 当前写指针和状态计算
uint16_t write_pos = BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&hdma_uart_rx);
if (dma_state == DMA_STATE_FULL) {
// 写指针在后半区,有效数据从 read_index 到 write_pos
return write_pos - read_index;
} else {
// 写指针在前半区,有效数据分为两段
return (BUFFER_SIZE - read_index) + write_pos;
}
}
void ProcessBuffer(uint16_t start, uint16_t len) {
// 从缓冲区 start 位置读取 len 字节处理
// 更新 read_index
read_index = (start + len) % BUFFER_SIZE;
}
```
# 3. 完整代码示例(基于 STM32 HAL)
```c
// 缓冲区定义
#define BUFFER_SIZE 256
uint8_t rx_buffer[BUFFER_SIZE];
volatile uint16_t read_index = 0;
// 初始化 DMA 接收
void UART_RX_Init(void) {
HAL_UART_Receive_DMA(&huart1, rx_buffer, BUFFER_SIZE);
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
}
// 中断回调(在 UART 中断中调用)
void HAL_UART_IRQHandler(UART_HandleTypeDef *huart) {
// 处理空闲中断
if (__HAL_UART_GET_FLAG(huart, UART_FLAG_IDLE)) {
__HAL_UART_CLEAR_IDLEFLAG(huart);
HAL_UART_IdleCpltCallback(huart);
}
HAL_UART_IRQHandler(huart); // 调用 HAL 默认处理(会触发 DMA 回调)
}
// 主循环
while (1) {
ProcessUartData();
// 其他任务
}
```
# 4. 注意事项与优化
- **中断优先级**:建议将 UART 中断优先级设为高于 DMA 中断,但两者都应低于系统节拍。
- **临界区保护**:在修改 `read_index` 或标志位时,可短暂关闭中断或使用临界区,防止竞态。
- **数据覆盖风险**:若处理速度跟不上 DMA 写入,需增加缓冲区大小或采用双缓冲。
- **空闲中断误触发**:在连续数据流中,空闲中断可能不触发,此时依赖半满/全满中断保证数据不丢失。
- **测试建议**:使用串口调试助手发送不同长度数据(如 N-1、N、N+1 字节)验证逻辑。
# 结语
通过状态机与标志位分离事件处理,可有效消除半满中断与空闲中断的竞争。该方法已在实际项目验证,稳定可靠。掌握此策略,可显著提升 UART 接收的健壮性。