引言

在STM32等MCU上,UART接收数据通常采用中断或DMA方式。对于不定长数据,DMA+环形缓冲区是兼顾实时性与效率的优选方案。然而,DMA的半满中断(HT)和全满中断(TC)触发时机与数据帧边界往往不对齐,导致数据被截断或粘包。本文将深入探讨如何利用半满中断与环形缓冲区特性,实现精准的帧边界识别与数据提取。

DMA环形缓冲区基础

原理概述

DMA环形缓冲区(Circular Buffer)是一种固定大小的内存区域,DMA控制器在传输数据时自动回绕,实现连续写入。配合半满中断(Half Transfer)和全满中断(Transfer Complete),CPU可在数据达到一半或写满时介入处理,避免数据覆盖。

  • 半满中断:当DMA传输计数达到缓冲区大小的一半时触发。
  • 全满中断:当DMA传输计数达到缓冲区大小时触发(在循环模式下,即写满一圈)。

为什么需要半满中断?

对于不定长数据,若仅使用全满中断,则必须等待缓冲区写满才能处理,延迟大且可能丢失数据。半满中断允许CPU在缓冲区半满时提前处理,降低延迟,同时配合环形特性,可处理任意长度数据。

帧边界检测策略

核心问题

DMA只负责搬运数据,不关心帧结构。当一帧数据跨越半满或全满边界时,若简单按中断点切割,会导致帧被拆分。因此,我们需要一种机制来识别帧的起始和结束。

常用方法

  • 空闲中断(IDLE):UART的IDLE中断在总线空闲时触发,可标记一帧结束。这是最常用的帧边界检测手段。
  • 帧头帧尾匹配:在数据流中查找特定字节(如0xAA, 0x55)作为帧头,或使用长度字段。
  • 超时判断:通过定时器,若超过一定时间未收到新数据,则认为帧结束。

本文重点结合半满中断与IDLE中断,实现精准帧边界。

实现方案

硬件配置(以STM32F4为例)

  1. UART配置:使能DMA接收,开启IDLE中断。
  2. DMA配置:设置为循环模式(Circular),缓冲区大小设为2的幂次(如256字节),使能半满和全满中断。
  3. 中断优先级:DMA中断优先级高于UART IDLE中断,确保数据不丢失。

软件设计

数据结构

#define BUF_SIZE 256  // 必须是2的幂
#define BUF_MASK (BUF_SIZE-1)

uint8_t dma_buf[BUF_SIZE];
volatile uint16_t last_pos = 0;  // 上次处理位置
volatile uint8_t frame_ready = 0; // 帧完成标志

DMA中断处理

void DMA1_Stream5_IRQHandler(void) {
    uint16_t cur_pos;
    if (DMA_GetITStatus(DMA1_Stream5, DMA_IT_HT)) {
        DMA_ClearITPendingBit(DMA1_Stream5, DMA_IT_HT);
        cur_pos = BUF_SIZE/2; // 半满位置
        process_data(cur_pos); // 处理从last_pos到cur_pos的数据
        last_pos = cur_pos;
    }
    if (DMA_GetITStatus(DMA1_Stream5, DMA_IT_TC)) {
        DMA_ClearITPendingBit(DMA1_Stream5, DMA_IT_TC);
        cur_pos = 0; // 回绕到0
        process_data(cur_pos); // 处理从last_pos到缓冲区末尾的数据
        last_pos = cur_pos;
    }
}

IDLE中断处理

void USART1_IRQHandler(void) {
    if (USART_GetITStatus(USART1, USART_IT_IDLE)) {
        USART_ClearITPendingBit(USART1, USART_IT_IDLE);
        // 读取当前DMA剩余计数,计算实际数据位置
        uint16_t remaining = DMA_GetCurrDataCounter(DMA1_Stream5);
        uint16_t cur_pos = BUF_SIZE - remaining;
        // 处理从last_pos到cur_pos的数据(可能包含完整帧)
        process_data(cur_pos);
        last_pos = cur_pos;
        frame_ready = 1; // 通知主循环处理帧
    }
}

数据处理函数

void process_data(uint16_t end_pos) {
    // 从last_pos到end_pos的数据,可能包含多个帧或半帧
    // 需要根据帧协议解析,这里以帧头0xAA 0x55为例
    static uint8_t in_frame = 0;
    static uint8_t frame_len = 0;
    static uint8_t frame_data[256];
    static uint16_t frame_idx = 0;

    uint16_t i = last_pos;
    while (i != end_pos) {
        uint8_t byte = dma_buf[i];
        if (!in_frame) {
            // 查找帧头
            if (byte == 0xAA) {
                in_frame = 1;
                frame_idx = 0;
                frame_data[frame_idx++] = byte;
            }
        } else {
            frame_data[frame_idx++] = byte;
            // 假设帧格式:AA 55 LEN DATA... CHECK
            if (frame_idx == 2) {
                frame_len = byte; // 长度字段
            } else if (frame_idx == frame_len + 3) { // 帧头2字节+长度1字节+数据+校验
                // 帧完整,处理帧
                handle_frame(frame_data, frame_idx);
                in_frame = 0;
            }
        }
        i = (i + 1) & BUF_MASK; // 环形回绕
    }
}

注意事项

  • 缓冲区大小:必须大于最大帧长,且为2的幂,便于取模运算。
  • 临界区保护:在中断中修改last_pos时,主循环读取需关中断,防止数据竞争。
  • 半满与全满中断处理:在循环模式下,全满中断意味着DMA计数器回绕到0,此时last_pos可能大于cur_pos,需注意环形处理。
  • IDLE中断时机:IDLE中断在总线空闲时触发,但可能发生在DMA中断处理期间,需确保顺序正确。
  • 性能优化:若帧率较高,可将数据处理放在主循环,中断只置标志位。

完整示例代码

以下是一个简化的STM32F4工程核心代码,展示如何整合以上逻辑。

// main.c
int main(void) {
    // 初始化UART、DMA、中断等
    while (1) {
        if (frame_ready) {
            frame_ready = 0;
            // 处理完整帧,此时last_pos已更新
            // 可调用解析函数
        }
    }
}

// 中断服务函数已在上面给出,此处省略

总结

通过DMA环形缓冲区结合半满中断和IDLE中断,我们能够高效、精准地处理UART不定长接收。关键在于理解DMA中断的触发点,并利用环形缓冲区的回绕特性,配合帧协议解析,确保帧边界不丢失。此方案在STM32等主流MCU上广泛适用,可大幅提升通信可靠性。