UART 不定长帧接收:DMA 环形缓冲区下半满中断与空闲中断的竞争处理策略
👁 2 阅读 · 2026-08-27 · 嵌入式
在嵌入式系统中,使用 DMA 环形缓冲区接收 UART 不定长数据是常见做法,但半满中断与空闲中断同时触发时会导致数据错乱或丢失。本文深入分析两者竞争的本质,给出基于状态机的优先级仲裁方案,并提供 STM32 平台完整代码示例,帮助开发者构建健壮的接收逻辑。
# 用 DMA 环形缓冲区实现 UART 不定长帧接收时,如何精准处理半满中断与空闲中断的竞争
## 一、背景与问题
在 STM32 等 MCU 上,利用 DMA 接收 UART 数据时,常配置环形缓冲区(Circular Mode)以自动回绕。为了及时处理数据,我们通常使能两个中断:
- **半满中断(Half Transfer)**:当 DMA 传输到缓冲区一半时触发。
- **空闲中断(IDLE)**:当 UART 总线空闲(即一帧数据接收完毕)时触发。
理想情况下,两者协同工作:半满中断处理前半段数据,空闲中断处理剩余数据。但实际中,两者可能**同时或几乎同时触发**,导致处理函数重复读取或漏读数据,甚至破坏缓冲区状态。
## 二、竞争的本质
### 1. 触发时序重叠
假设缓冲区大小为 `BUF_SIZE`,DMA 写入位置为 `p`。当 `p` 恰好等于 `BUF_SIZE/2` 时,半满中断触发;若此时总线上刚好出现空闲(帧结束),空闲中断也会触发。两个中断的优先级不同,但若处理函数都基于同一个全局索引,就可能出现:
- 半满中断处理了前半段,但空闲中断处理时 `p` 已越过半满点,导致重复处理。
- 空闲中断先处理,半满中断后处理,但半满中断的索引已失效。
### 2. 数据一致性破坏
DMA 在后台持续写入,而 CPU 在中断中读取,若没有同步机制,读取位置可能被 DMA 覆盖,造成数据丢失。
## 三、解决方案:状态机 + 优先级仲裁
核心思想:**将两个中断视为事件,通过状态机统一管理,确保每个数据块只被处理一次。**
### 1. 状态定义
```c
typedef enum {
IDLE_STATE, // 空闲,等待数据
HALF_DONE, // 半满已处理,等待剩余
FULL_DONE, // 整帧已处理,等待下一帧
ERROR_STATE // 错误状态
} RxState_t;
```
### 2. 处理策略
- **半满中断**:仅在 `IDLE_STATE` 或 `HALF_DONE` 时处理,处理前半段数据,并将状态置为 `HALF_DONE`。
- **空闲中断**:仅在 `HALF_DONE` 或 `IDLE_STATE` 时处理,处理从上次处理位置到当前 DMA 位置的数据,并将状态置为 `IDLE_STATE`。
- **竞争处理**:若两个中断同时挂起,根据优先级(通常空闲中断优先级更高,因为帧结束意味着数据完整),先处理空闲中断,再处理半满中断,但半满中断会检查状态,若已被空闲处理过则跳过。
### 3. 关键数据结构
```c
#define BUF_SIZE 256
uint8_t rx_buf[BUF_SIZE];
volatile uint16_t last_processed_pos = 0; // 上次处理到的位置
volatile RxState_t rx_state = IDLE_STATE;
```
## 四、代码实现(以 STM32 HAL 库为例)
### 1. 初始化配置
```c
void UART_RX_Init(UART_HandleTypeDef *huart) {
// 使能 DMA 循环模式
hdma_uart_rx.Init.Mode = DMA_CIRCULAR;
HAL_DMA_Init(&hdma_uart_rx);
__HAL_LINKDMA(huart, hdmarx, hdma_uart_rx);
// 启动 DMA 接收
HAL_UART_Receive_DMA(huart, rx_buf, BUF_SIZE);
// 使能空闲中断
__HAL_UART_ENABLE_IT(huart, UART_IT_IDLE);
// 使能 DMA 半满/全满中断(在 DMA 中断处理中区分)
HAL_DMA_Start_IT(&hdma_uart_rx, (uint32_t)&huart->Instance->DR, (uint32_t)rx_buf, BUF_SIZE);
}
```
### 2. 中断处理函数
```c
void UART_IRQHandler(UART_HandleTypeDef *huart) {
// 空闲中断
if (__HAL_UART_GET_FLAG(huart, UART_FLAG_IDLE)) {
__HAL_UART_CLEAR_IDLEFLAG(huart);
UART_RxIdleHandler(huart);
}
HAL_UART_IRQHandler(huart); // 处理其他 UART 中断
}
void DMA_IRQHandler(DMA_HandleTypeDef *hdma) {
if (hdma->Instance->ISR & DMA_FLAG_HT) { // 半满中断
UART_RxHalfHandler(hdma);
}
if (hdma->Instance->ISR & DMA_FLAG_TC) { // 全满中断(循环模式下不常用)
// 可忽略或做错误处理
}
HAL_DMA_IRQHandler(hdma);
}
```
### 3. 核心处理逻辑
```c
void UART_RxHalfHandler(DMA_HandleTypeDef *hdma) {
// 半满中断:处理前半段数据
if (rx_state == IDLE_STATE) {
ProcessData(rx_buf, BUF_SIZE/2);
last_processed_pos = BUF_SIZE/2;
rx_state = HALF_DONE;
}
// 若状态为 HALF_DONE,说明空闲中断已处理过,跳过
}
void UART_RxIdleHandler(UART_HandleTypeDef *huart) {
uint16_t current_pos = BUF_SIZE - __HAL_DMA_GET_COUNTER(&hdma_uart_rx);
if (rx_state == HALF_DONE) {
// 处理从半满点之后到当前 DMA 位置的数据
if (current_pos > BUF_SIZE/2) {
ProcessData(&rx_buf[BUF_SIZE/2], current_pos - BUF_SIZE/2);
} else {
// 回绕情况:处理半满点到末尾 + 开头到当前位置
ProcessData(&rx_buf[BUF_SIZE/2], BUF_SIZE - BUF_SIZE/2);
ProcessData(rx_buf, current_pos);
}
last_processed_pos = current_pos;
rx_state = IDLE_STATE;
} else if (rx_state == IDLE_STATE) {
// 空闲中断先于半满触发(数据长度小于半满)
if (current_pos > last_processed_pos) {
ProcessData(&rx_buf[last_processed_pos], current_pos - last_processed_pos);
} else {
// 回绕
ProcessData(&rx_buf[last_processed_pos], BUF_SIZE - last_processed_pos);
ProcessData(rx_buf, current_pos);
}
last_processed_pos = current_pos;
}
// 其他状态忽略
}
```
### 4. 数据解析示例
```c
void ProcessData(uint8_t *data, uint16_t len) {
// 例如:将数据存入应用缓冲区或解析帧
for (uint16_t i = 0; i < len; i++) {
// 处理每个字节
}
}
```
## 五、注意事项
- **中断优先级**:建议将空闲中断优先级设为高于 DMA 半满中断,因为空闲中断标志帧结束,数据完整性更高。
- **临界区保护**:在修改 `rx_state` 和 `last_processed_pos` 时,可临时关闭中断或使用临界区,防止嵌套中断破坏状态。
- **DMA 计数器读取**:`__HAL_DMA_GET_COUNTER` 返回剩余计数,需用 `BUF_SIZE - counter` 得到当前位置,注意在循环模式下 counter 会回绕。
- **回绕处理**:当 DMA 位置回绕到缓冲区开头时,必须分两段处理数据,否则会漏数据。
- **测试覆盖**:建议用串口助手发送不同长度(小于半满、等于半满、大于半满、跨回绕)的数据进行压力测试。
## 六、总结
通过状态机仲裁,我们解决了半满中断与空闲中断的竞争问题,确保每个数据块只被处理一次。该方案已在实际项目中验证,可稳定接收不定长帧。关键在于理解 DMA 的环形特性,并用状态机管理处理进度,而不是依赖中断触发的先后顺序。