用 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 中断处理
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 中断处理
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是否相等,可跳过重复处理。
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. 主循环处理帧
while (1) {
if (frame_ready) {
frame_ready = 0;
// 解析完整帧,此时 rx_index 已指向帧尾
parse_frame();
}
}
完整代码示例(基于 STM32F1xx 标准库)
#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 实时计算数据位置,配合状态标志,可以有效解决半满中断与空闲中断的竞争问题。该方法已在实际项目中验证,适用于高速、不定长通信场景。