用 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_indexcurrent_pos,避免中断嵌套修改。
  • 若 HT 和 IDLE 几乎同时,由于 IDLE 优先级更高,会先执行 IDLE 处理,此时 rx_index 更新为当前 DMA 位置,随后 HT 处理时,current_pos 可能已变化,但通过比较 rx_indexcurrent_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 实时计算数据位置,配合状态标志,可以有效解决半满中断与空闲中断的竞争问题。该方法已在实际项目中验证,适用于高速、不定长通信场景。