用 DMA 环形缓冲区实现 UART 不定长接收时,如何精确区分帧尾与噪声

一、问题背景与挑战

在嵌入式系统中,UART 通信常需接收不定长数据帧。使用 DMA + 环形缓冲区(Ring Buffer)可大幅降低 CPU 负载,但随之而来的难题是:如何从连续的数据流中准确识别一帧的结束?尤其当总线存在噪声(如毛刺、随机字节)时,误判帧尾会导致数据错乱、协议解析失败。

传统方法如:

  • 固定长度接收:不适用于可变帧长。
  • 逐字节中断接收:CPU 负担重,且无法利用 DMA。
  • 空闲中断(IDLE):可检测总线空闲,但噪声可能打断空闲状态。

因此,需要一套综合策略,结合硬件特性与软件状态机,精确区分有效帧尾与噪声。

二、DMA 环形缓冲区原理

2.1 环形缓冲区结构

环形缓冲区(Ring Buffer)是一块固定大小的内存,通过读写指针循环使用。DMA 将 UART 接收数据连续写入缓冲区,CPU 通过读指针按帧处理。

#define BUF_SIZE 256
uint8_t rx_buf[BUF_SIZE];
volatile uint16_t head = 0; // DMA 写指针(由硬件更新)
uint16_t tail = 0;         // CPU 读指针

2.2 DMA 配置要点

  • 使用 DMA 循环模式(Circular Mode),使 DMA 自动回绕。
  • 使能 UART 空闲中断(IDLE),用于指示总线空闲。
  • 设置 DMA 传输完成中断(半传输/全传输),用于处理缓冲区溢出。

三、帧尾与噪声的区分策略

3.1 帧格式设计

首先,协议层应定义明确的帧格式,例如:

帧头(1字节) + 长度(1字节) + 数据(N字节) + 校验(1字节)

帧头固定为 0xAA,长度字段表示数据长度,校验采用累加和或 CRC。这样,即使噪声混入,也能通过校验剔除。

3.2 空闲中断 + 帧超时

空闲中断:当总线空闲超过一个字节时间时触发,可认为当前帧结束。但噪声可能产生短时低电平,导致空闲中断提前触发。

帧超时:在收到帧头后,启动软件定时器(如 SysTick),若在超时时间内未收到完整帧,则丢弃并重新等待帧头。

3.3 状态机解析

采用状态机处理每个字节,状态包括:WAIT_HEADERWAIT_LENGTHWAIT_DATAWAIT_CHECK

  • WAIT_HEADER:只匹配帧头,非帧头字节视为噪声丢弃。
  • WAIT_LENGTH:读取长度,并校验合法性(如 0~255)。
  • WAIT_DATA:累积数据,直到达到长度。
  • WAIT_CHECK:校验和验证,通过则帧有效,否则丢弃。

3.4 噪声抑制技巧

  • 最小帧间隔:定义帧间最小间隔(如 5ms),若两次空闲中断间隔过短,则忽略后一次。
  • 字节超时:在接收数据过程中,若两个字节间隔超过设定值(如 10ms),则视为帧超时,重置状态机。
  • 硬件滤波:若 MCU 支持,可启用 UART 的噪声滤波或数字滤波功能。

四、完整代码示例(STM32 平台)

以下代码基于 STM32 HAL 库,实现 DMA 环形缓冲区 + 空闲中断 + 状态机解析。

// 定义帧结构
#define FRAME_HEADER 0xAA
#define MAX_DATA_LEN 255

typedef enum {
    WAIT_HEADER,
    WAIT_LENGTH,
    WAIT_DATA,
    WAIT_CHECK
} FrameState;

// 全局变量
uint8_t rx_buf[BUF_SIZE];
volatile uint16_t head = 0;
uint16_t tail = 0;
FrameState state = WAIT_HEADER;
uint8_t frame_len = 0;
uint8_t frame_data[MAX_DATA_LEN];
uint8_t check_sum = 0;
uint8_t frame_ready = 0; // 置1表示一帧完整接收

// UART 空闲中断回调(HAL 库)
void HAL_UART_IdleCb(UART_HandleTypeDef *huart) {
    if (huart->Instance == USART1) {
        __HAL_UART_CLEAR_IDLEFLAG(&huart1);
        // 更新 head 指针(DMA 当前计数值)
        head = BUF_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
        // 处理新数据
        ProcessRxData();
    }
}

// 处理接收数据
void ProcessRxData(void) {
    while (tail != head) {
        uint8_t byte = rx_buf[tail];
        tail = (tail + 1) % BUF_SIZE;
        
        switch (state) {
            case WAIT_HEADER:
                if (byte == FRAME_HEADER) {
                    state = WAIT_LENGTH;
                }
                // 否则丢弃(噪声)
                break;
            case WAIT_LENGTH:
                frame_len = byte;
                if (frame_len > 0 && frame_len <= MAX_DATA_LEN) {
                    state = WAIT_DATA;
                    check_sum = FRAME_HEADER + frame_len;
                    frame_data_len = 0;
                } else {
                    state = WAIT_HEADER; // 长度非法,重新等待帧头
                }
                break;
            case WAIT_DATA:
                frame_data[frame_data_len++] = byte;
                check_sum += byte;
                if (frame_data_len == frame_len) {
                    state = WAIT_CHECK;
                }
                break;
            case WAIT_CHECK:
                if (byte == (uint8_t)check_sum) {
                    frame_ready = 1; // 帧有效
                    // 可在此处通知主循环处理帧
                }
                state = WAIT_HEADER;
                break;
        }
    }
}

// 主循环中检查帧就绪
int main(void) {
    // 初始化...
    HAL_UART_Receive_DMA(&huart1, rx_buf, BUF_SIZE); // 启动 DMA 循环接收
    while (1) {
        if (frame_ready) {
            frame_ready = 0;
            // 处理 frame_data 和 frame_len
        }
        // 其他任务
    }
}

关键点说明

  • head 指针更新:在空闲中断中,通过 DMA 剩余计数计算实际写入位置。
  • 状态机:严格按帧格式解析,非帧头字节直接丢弃,有效抑制噪声。
  • 校验和:累加帧头、长度和数据,确保数据完整性。

五、注意事项

  1. 缓冲区溢出:若 DMA 写入速度大于 CPU 处理速度,可能导致覆盖。需监控 head 与 tail 距离,必要时丢弃旧数据或增大缓冲区。
  2. 空闲中断误触发:噪声可能造成短暂空闲,但状态机在等待帧头时会忽略,在接收数据中则可能提前结束。可通过帧超时机制(如使用定时器)辅助判断。
  3. DMA 配置:务必使用循环模式,并正确设置数据宽度(字节)。
  4. 中断优先级:空闲中断应设为较高优先级,避免数据丢失。
  5. 测试与调试:使用逻辑分析仪或串口助手模拟噪声,验证状态机的鲁棒性。

六、总结

通过 DMA 环形缓冲区 + 空闲中断 + 状态机 + 校验和的组合,可以有效区分帧尾与噪声。关键在于协议设计(帧头、长度、校验)和软件状态机的严谨实现。此方案在 STM32 等主流 MCU 上广泛适用,既能保证接收效率,又能提升通信可靠性。

实际项目中,还可根据需求扩展:如增加 CRC32 校验、支持多帧缓存、动态调整超时时间等。希望本文能为你提供实用的参考。