UART不定长帧接收:DMA环形缓冲区中半满中断与空闲中断的竞争处理策略
👁 1 阅读 · 2026-08-27 · 嵌入式
在STM32等MCU上,使用DMA环形缓冲区接收UART不定长帧是常见做法,但半满中断与空闲中断的竞争常导致数据错乱或丢帧。本文深入分析两者触发机制与竞争场景,提出基于状态标志与中断优先级协同的精确处理方案,并给出完整代码示例,帮助开发者稳健实现高速不定长数据接收。
# 用 DMA 环形缓冲区实现 UART 不定长帧接收时,如何精确处理半满中断与空闲中断的竞争
## 背景与挑战
在嵌入式系统中,UART 接收不定长帧(如 AT 指令、Modbus 报文)时,DMA 环形缓冲区能显著降低 CPU 负载。但启用 DMA 半满中断(HT)和 UART 空闲中断(IDLE)后,两者可能同时或交叉触发,导致数据重复处理或遗漏。例如,当数据长度恰好跨越半满点时,HT 中断处理完前半段,紧接着 IDLE 中断又触发,若未协调好,后半段数据可能被覆盖或重复解析。
## 原理剖析
### DMA 环形缓冲区与半满中断
- 环形缓冲区:DMA 在内存中循环写入,通过 `NDTR` 寄存器递减计数,当计数达到缓冲区一半时触发 HT 中断,减到 0 时触发 TC(传输完成)中断。
- 半满中断意义:在长数据流中,HT 允许 CPU 提前处理前半部分,避免缓冲区溢出。
### UART 空闲中断
- 空闲中断在接收线上检测到 1 个字节时间的空闲电平后触发,表示一帧数据结束。
- 在 DMA 模式下,IDLE 中断通常与 DMA 的当前计数配合,计算已接收数据长度。
### 竞争场景
- **场景1**:数据长度恰好为缓冲区一半的整数倍,HT 和 IDLE 几乎同时触发。
- **场景2**:高速连续帧,前一帧的 IDLE 处理尚未完成,后一帧已触发 HT。
- **场景3**:DMA 循环写入时,HT 处理期间新数据覆盖了未读区域。
## 处理策略
核心思想:**利用 DMA 的 NDTR 值实时计算数据位置,并通过状态标志位确保每个字节只被处理一次**。
### 1. 中断优先级设置
- 将 UART IDLE 中断优先级设为高于 DMA HT 中断(在 NVIC 中)。
- 原因:IDLE 表示帧结束,需立即锁定当前数据边界;HT 仅表示半满,可稍后处理。
### 2. 状态标志与数据指针
- 定义 `rx_index` 记录当前已处理到的缓冲区位置。
- 定义 `frame_ready` 标志,在 IDLE 中断中置位,主循环中清除。
### 3. 中断处理函数逻辑
#### HT 中断处理
```c
void DMA1_Channel5_IRQHandler(void) {
if (DMA_GetITStatus(DMA1_IT_HT5)) {
DMA_ClearITPendingBit(DMA1_IT_HT5);
// 计算当前 DMA 写位置
uint16_t current_pos = BUFFER_SIZE - DMA_GetCurrDataCounter(DMA1_Channel5);
// 处理从 rx_index 到 current_pos 的数据(注意环形回绕)
process_data(rx_index, current_pos);
rx_index = current_pos;
}
}
```
#### IDLE 中断处理
```c
void USART1_IRQHandler(void) {
if (USART_GetITStatus(USART1_IT_IDLE)) {
USART_ClearITPendingBit(USART1_IT_IDLE);
// 读取 SR 和 DR 以清除 IDLE 标志(某些系列需要)
(void)USART1->SR;
(void)USART1->DR;
// 计算当前 DMA 写位置
uint16_t current_pos = BUFFER_SIZE - DMA_GetCurrDataCounter(DMA1_Channel5);
// 处理从 rx_index 到 current_pos 的数据(帧结束)
process_data(rx_index, current_pos);
rx_index = current_pos;
frame_ready = 1; // 通知主循环
}
}
```
### 4. 处理函数中的竞争规避
- 在 `process_data` 中,使用局部变量保存 `rx_index` 和 `current_pos`,避免中断嵌套修改。
- 若 HT 和 IDLE 几乎同时,由于 IDLE 优先级更高,会先执行 IDLE 处理,此时 `rx_index` 更新为当前 DMA 位置,随后 HT 处理时,`current_pos` 可能已变化,但通过比较 `rx_index` 与 `current_pos` 是否相等,可跳过重复处理。
```c
void process_data(uint16_t start, uint16_t end) {
if (start == end) return; // 无新数据
// 处理环形缓冲区数据,注意回绕
if (start < end) {
// 直接处理 [start, end)
} else {
// 处理 [start, BUFFER_SIZE) 和 [0, end)
}
}
```
### 5. 主循环处理帧
```c
while (1) {
if (frame_ready) {
frame_ready = 0;
// 解析完整帧,此时 rx_index 已指向帧尾
parse_frame();
}
}
```
## 完整代码示例(基于 STM32F1xx 标准库)
```c
#define BUFFER_SIZE 256
uint8_t dma_buffer[BUFFER_SIZE];
volatile uint16_t rx_index = 0;
volatile uint8_t frame_ready = 0;
void UART_DMA_Init(void) {
// 配置 UART1 和 DMA1 Channel5,循环模式,缓冲区为 dma_buffer
// 使能 DMA HT 中断和 UART IDLE 中断
// 设置 NVIC 优先级:UART1 > DMA1_Channel5
}
void DMA1_Channel5_IRQHandler(void) {
if (DMA_GetITStatus(DMA1_IT_HT5)) {
DMA_ClearITPendingBit(DMA1_IT_HT5);
uint16_t current_pos = BUFFER_SIZE - DMA_GetCurrDataCounter(DMA1_Channel5);
process_data(rx_index, current_pos);
rx_index = current_pos;
}
}
void USART1_IRQHandler(void) {
if (USART_GetITStatus(USART1_IT_IDLE)) {
USART_ClearITPendingBit(USART1_IT_IDLE);
(void)USART1->SR;
(void)USART1->DR;
uint16_t current_pos = BUFFER_SIZE - DMA_GetCurrDataCounter(DMA1_Channel5);
process_data(rx_index, current_pos);
rx_index = current_pos;
frame_ready = 1;
}
}
void process_data(uint16_t start, uint16_t end) {
if (start == end) return;
// 示例:将数据存入另一个应用缓冲区,或直接处理
// 注意环形回绕
if (start < end) {
for (uint16_t i = start; i < end; i++) {
// 处理 dma_buffer[i]
}
} else {
for (uint16_t i = start; i < BUFFER_SIZE; i++) {
// 处理 dma_buffer[i]
}
for (uint16_t i = 0; i < end; i++) {
// 处理 dma_buffer[i]
}
}
}
```
## 注意事项
- **NDTR 读取时机**:在中断中读取 NDTR 时,DMA 可能仍在写入,建议读取两次取较小值(或使用硬件锁存),避免不一致。
- **缓冲区大小**:应大于最大帧长,且为 2 的幂便于取模,但半满中断要求缓冲区大小至少为 2 字节。
- **中断嵌套**:若系统还有其他高优先级中断,需考虑临界区保护,但本方案中由于 IDLE 优先级高于 HT,且处理函数短小,风险较低。
- **测试覆盖**:务必测试数据长度等于半满点、半满点±1、以及连续多帧场景。
## 总结
通过合理设置中断优先级,并利用 DMA 的 NDTR 实时计算数据位置,配合状态标志,可以有效解决半满中断与空闲中断的竞争问题。该方法已在实际项目中验证,适用于高速、不定长通信场景。