UART 不定长接收:DMA 环形缓冲区中帧尾与噪声的精确区分策略
👁 2 阅读 · 2026-08-27 · 嵌入式
在嵌入式开发中,使用 DMA 环形缓冲区实现 UART 不定长接收是常见的高效方案,但帧尾与噪声的区分常令人头疼。本文深入剖析 DMA 环形缓冲区的原理,提出基于空闲中断、帧超时、数据校验及状态机的综合判定策略,并给出 STM32 平台完整代码示例,帮助开发者构建鲁棒的通信协议。
# 用 DMA 环形缓冲区实现 UART 不定长接收时,如何精确区分帧尾与噪声
## 一、问题背景与挑战
在嵌入式系统中,UART 通信常需接收不定长数据帧。使用 DMA + 环形缓冲区(Ring Buffer)可大幅降低 CPU 负载,但随之而来的难题是:**如何从连续的数据流中准确识别一帧的结束**?尤其当总线存在噪声(如毛刺、随机字节)时,误判帧尾会导致数据错乱、协议解析失败。
传统方法如:
- 固定长度接收:不适用于可变帧长。
- 逐字节中断接收:CPU 负担重,且无法利用 DMA。
- 空闲中断(IDLE):可检测总线空闲,但噪声可能打断空闲状态。
因此,需要一套综合策略,结合硬件特性与软件状态机,精确区分有效帧尾与噪声。
## 二、DMA 环形缓冲区原理
### 2.1 环形缓冲区结构
环形缓冲区(Ring Buffer)是一块固定大小的内存,通过读写指针循环使用。DMA 将 UART 接收数据连续写入缓冲区,CPU 通过读指针按帧处理。
```c
#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 帧格式设计
首先,协议层应定义明确的帧格式,例如:
```c
帧头(1字节) + 长度(1字节) + 数据(N字节) + 校验(1字节)
```
帧头固定为 `0xAA`,长度字段表示数据长度,校验采用累加和或 CRC。这样,即使噪声混入,也能通过校验剔除。
### 3.2 空闲中断 + 帧超时
**空闲中断**:当总线空闲超过一个字节时间时触发,可认为当前帧结束。但噪声可能产生短时低电平,导致空闲中断提前触发。
**帧超时**:在收到帧头后,启动软件定时器(如 SysTick),若在超时时间内未收到完整帧,则丢弃并重新等待帧头。
### 3.3 状态机解析
采用状态机处理每个字节,状态包括:`WAIT_HEADER`、`WAIT_LENGTH`、`WAIT_DATA`、`WAIT_CHECK`。
- 在 `WAIT_HEADER`:只匹配帧头,非帧头字节视为噪声丢弃。
- 在 `WAIT_LENGTH`:读取长度,并校验合法性(如 0~255)。
- 在 `WAIT_DATA`:累积数据,直到达到长度。
- 在 `WAIT_CHECK`:校验和验证,通过则帧有效,否则丢弃。
### 3.4 噪声抑制技巧
- **最小帧间隔**:定义帧间最小间隔(如 5ms),若两次空闲中断间隔过短,则忽略后一次。
- **字节超时**:在接收数据过程中,若两个字节间隔超过设定值(如 10ms),则视为帧超时,重置状态机。
- **硬件滤波**:若 MCU 支持,可启用 UART 的噪声滤波或数字滤波功能。
## 四、完整代码示例(STM32 平台)
以下代码基于 STM32 HAL 库,实现 DMA 环形缓冲区 + 空闲中断 + 状态机解析。
```c
// 定义帧结构
#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 校验、支持多帧缓存、动态调整超时时间等。希望本文能为你提供实用的参考。