DMA 全深度解析(3):环形缓冲区(Ring Buffer)的实现
本文介绍了一种基于STM32F4+FreeRTOS+HAL的UART-DMA串口采集系统设计方案。通过环形缓冲区(RingBuffer)实现异步数据采集与同步处理的解耦,采用"中间件+BSP+APP"三层架构设计,包含DMA CIRCULAR模式持续写入、中断回调更新指针、队列通知同步等核心机制。系统支持半满/全满中断和空闲中断互补触发,结合状态机实现帧协议解析,有效解决了串口
前言
在嵌入式开发中,串口通信是连接外设与主控的核心桥梁 ——GPS 定位数据、蓝牙透传指令、传感器输出等场景都离不开 UART。但串口数据 “异步到达、同步处理” 的特性,很容易引发数据错乱、丢包问题:DMA 持续往内存搬数据,CPU 却可能因协议解析、校验等操作无法实时读取。
环形缓冲区(Ring Buffer)是解决这一矛盾的经典方案,本文基于 STM32F4 + FreeRTOS + HAL,从架构设计到代码实现,完整搭建一套 “UART-DMA + 环形缓冲区” 的高可靠串口采集系统,兼顾数据完整性与处理效率。
一、核心设计思路
1.1 环形缓冲区的核心价值
环形缓冲区通过 “头尾指针 + 自动回绕” 机制,实现读写分离、互不干扰:
- 写指针(head):DMA 持续写入数据,head 不断前进;
- 读指针(tail):CPU 处理数据,tail 追赶 head;
- 环形复用:指针到达缓冲区末尾后自动回绕,无需频繁申请内存;
- 安全隔离:头尾指针间的距离保证读写操作不会冲突。
1.2 架构设计
为保证代码可复用、易维护,系统采用 “中间件 + BSP + APP” 分层设计
(1)我们先来了解嵌入式主要架构:

(2)了解环形缓冲区的数据结构
本质就是环形队列的实现
数据填充依赖write指针移动填充,当write的下一个指针与read指针重合,说明当前队列已满;
数据解析依赖read指针移动解包,当read下一个指针与write指针重合,说明当前队列已空;

(3)我们来了解环形缓冲区的主要简单架构设计:
我们看到架构,中断服务函数中会判断触发中断的类型,根据不同的类型来移动写指针。
比如触发DMA半满中断,写指针移到缓冲区1/2大小的位置;
比如触发DMA全满中断,写指针移动到缓冲区0位置;
触发串口空闲中断,写指针根据传输的字节数移动写指针;
是不是发现其实和AB-buffer有异曲同工之妙。

(4)性能分析

(5)项目中常用架构
“中间层维护了一个路由表(或订阅关系),当解析线程识别出数据属于哪一类协议(或发给哪个设备ID)时,就通过消息队列/通知接口,将数据推送给对应的消费者 APP。”

a.中间层到底在干什么?
中间层叫 “协议解析转发 APP”。
-
它不仅仅是一个“转发器”,它首先是一个“翻译官”。
-
它从左边(BSP驱动)拿到的是原始的、可能是二进制或乱序的数据流。
-
它的第一步工作是“解析”(解包),把数据流翻译成有意义的业务数据(比如:温度=25℃,设备ID=01)。
b. “注册”和“路由”是怎么发生的?
图中两条线:
-
“提供订阅注册接口”:这说明消费者(A、B、C)启动时,会告诉中间层:“我要关心什么数据”。
-
例如:消费者A注册“只关心温湿度”,消费者B注册“只关心电机转速”。
-
-
“提供消息通知接口”:这是中间层给消费者的回传通道。
c. 转发逻辑(判断依据)
当中间层的解析线程解包完成后,它会得到数据包中的关键信息(通常是协议头里的 ID 或类型字段)。
-
如果解析出的 ID 匹配 消费者A 注册的列表 → 调用消费者A的“消息通知接口”。
-
如果解析出的 ID 匹配 消费者B 注册的列表 → 调用消费者B的“消息通知接口”。
假设这是一个物联网网关:
左边(驱动):收到串口数据
01 04 00 01 02 58(一堆乱码)。中间(解析):
识别到
01是温湿度传感器模块。解析出数值
25℃。查内部路由表:发现消费者A订阅了
01号设备。右边(转发):中间层把
{"device": "01", "temp": 25}打包,通过接口发送给消费者A。
1.3 数据流与同步机制
数据流路径
UART1 RX ──DMA2_Stream2──▶ 环形缓冲区(DMA CIRCULAR模式持续写入)
│
├── DMA半满中断/全满中断/串口空闲中断 ──▶ 更新head指针
│
└── 队列通知 ──▶ 驱动线程 ──▶ 解析线程 ──▶ 协议解析+校验
同步机制(队列通知)
系统通过 FreeRTOS 队列实现 “中断→线程→应用” 的安全同步:
| 队列 | 发送方 | 接收方 | 作用 |
|---|---|---|---|
| queue_uart_irq_thread | DMA/UART 中断(ISR) | UART 驱动线程 | 通知 “数据就绪” |
| queue_irq_rec_A | UART 驱动线程 | 数据解析线程 | 转发通知,唤醒解析逻辑 |
1.4 环形缓冲区 vs AB 缓冲区(双缓冲)
| 对比维度 | 环形缓冲区(循环 FIFO) | AB 缓冲区(双缓冲 / 乒乓缓冲) |
|---|---|---|
| 内存结构 | 一块连续内存,逻辑首尾相连循环 | 两块独立固定内存A 区、B 区,不循环 |
| 核心原理 | 读写指针绕圈追赶,FIFO 先进先出 | 一块接收、一块处理,满了直接切换 |
| 数据类型 | 适配不定长字节流、串口 / 网络流式数据 | 适配固定长度帧、整块 DMA 批量数据 |
| 指针管理 | 维护读指针、写指针,需判空、判满 | 无循环指针,仅切换缓冲区标志位 |
| 溢出风险 | 可做水位控制(半满 / 全满),抗突发数据能力强 | 缓冲块满就必须立刻切换,处理不及时易丢包 |
| 中断依赖 | 强依赖 空闲中断、半满中断、全满中断 唤醒任务解包 | 一般只依赖 缓冲区满中断 做乒乓切换 |
| 解包能力 | 天然支持断包、粘包重组,适合串口协议、MQTT、Modem 等复杂解包 | 只适合整帧整块处理,不适合粘包不定长流 |
| 内存利用率 | 高,剩余空间可循环复用 | 较低,两块分区固定划分,总有一块空闲等待 |
| 实现复杂度 | 较高,需处理指针绕回、空满判断、临界区保护 | 极低,逻辑简单,只需切换标志 |
| 任务调度 | 支持异步唤醒,可缓存大量数据,任务延时处理无压力 | 必须立刻切换处理,实时性要求更高 |
| 典型应用 | 串口 DMA 空闲接收、网络数据流、日志缓存、协议解析 | 屏幕 LCD 刷屏、ADC 连续采样、定长 CAN 帧传输 |
二、核心代码实现
2.1 中间件层:环形缓冲区基础操作
核心是 “绝对计数指针 + 取余复用” 设计,head/tail 持续递增不回绕,通过% 缓冲区大小得到实际数组下标,head - tail可直接得到数据量。
头文件(mid_circular_buffer.h)
#define CIRCULAR_BUFFER_SIZE 10 // 演示用,实际建议设为128/256/512
// 返回状态枚举
typedef enum {
BUF_OK = 0x00U, // 操作成功
BUF_NO = 0x01U, // 条件不满足
BUF_EMPTY = 0x02U, // 缓冲区为空
BUF_FULL = 0x03U, // 缓冲区已满
BUF_ERR = 0x04U // 参数错误(NULL)
} Circular_Buffer_Status;
// 环形缓冲区结构体
typedef struct {
uint8_t data[CIRCULAR_BUFFER_SIZE]; // 数据存储区
uint32_t head; // 写指针(生产者)
uint32_t tail; // 读指针(消费者)
} circular_buffer_t;
核心操作函数(mid_circular_buffer.c)
// 创建空缓冲区(动态分配)
circular_buffer_t* createEmplyCircularBuffer_t(void)
{
circular_buffer_t *P_buffer_temp = (circular_buffer_t *)malloc(sizeof(circular_buffer_t));
if (NULL == P_buffer_temp) return NULL;
P_buffer_temp->head = 0;
P_buffer_temp->tail = 0;
memset(P_buffer_temp->data, 0, CIRCULAR_BUFFER_SIZE);
return P_buffer_temp;
}
// 判断缓冲区是否为空
Circular_Buffer_Status buffer_is_empty(circular_buffer_t *buffer)
{
if (NULL == buffer) return BUF_ERR;
return (buffer->head == buffer->tail) ? BUF_OK : BUF_NO;
}
// 判断缓冲区是否已满(牺牲1个单元区分空/满)
Circular_Buffer_Status buffer_is_full(circular_buffer_t *buffer)
{
if (NULL == buffer) return BUF_ERR;
return ((buffer->head + 1) % CIRCULAR_BUFFER_SIZE == buffer->tail % CIRCULAR_BUFFER_SIZE) ? BUF_OK : BUF_NO;
}
// 写入1字节
Circular_Buffer_Status insert_data(circular_buffer_t *buffer, uint8_t data)
{
if (NULL == buffer) return BUF_ERR;
if (BUF_OK == buffer_is_full(buffer)) return BUF_FULL;
buffer->data[buffer->head % CIRCULAR_BUFFER_SIZE] = data;
buffer->head++;
return BUF_OK;
}
// 读取1字节
Circular_Buffer_Status get_data(circular_buffer_t *buffer, uint8_t *data)
{
if (NULL == buffer) return BUF_ERR;
if (BUF_OK == buffer_is_empty(buffer)) return BUF_EMPTY;
*data = buffer->data[buffer->tail % CIRCULAR_BUFFER_SIZE];
buffer->tail++;
return BUF_OK;
}
2.2 BSP 层:UART-DMA 驱动与中断处理
负责硬件初始化、中断回调(更新 head 指针)、队列通知转发,核心是 “DMA CIRCULAR 模式”+“三种中断互补”。
驱动线程初始化
#define SENT_QUEUE_THREAD 0xA2A2A2A2 // ISR→驱动线程通知码
#define SENT_QUEUE_APP 0xB2B2B2B2 // 驱动→解析线程通知码
static circular_buffer_t* g_circular_buffer_irq_thread;
static QueueHandle_t queue_uart_irq_thread = NULL;
extern QueueHandle_t queue_irq_rec_A;
// UART驱动线程
void uart_driver_function(void *argument)
{
// 1. 创建环形缓冲区
circular_buffer_t* p_circular_buffer = createEmplyCircularBuffer_t();
if (NULL == p_circular_buffer) {
log_e("缓冲区创建失败!");
vTaskDelete(NULL);
}
g_circular_buffer_irq_thread = p_circular_buffer;
// 2. 启动DMA接收(CIRCULAR模式,直接写入缓冲区)
if (HAL_OK != HAL_UARTEx_ReceiveToIdle_DMA(&huart1, p_circular_buffer->data, CIRCULAR_BUFFER_SIZE)) {
log_e("DMA接收初始化失败!");
vTaskDelete(NULL);
}
// 3. 创建ISR→线程的队列
queue_uart_irq_thread = xQueueCreate(1, 4);
uint32_t thread_receive = 0;
for (;;) {
// 4. 阻塞等待ISR通知
xQueueReceive(queue_uart_irq_thread, &thread_receive, portMAX_DELAY);
if (SENT_QUEUE_THREAD == thread_receive) {
// 5. 转发通知给APP层
uint32_t thread_sent_app = SENT_QUEUE_APP;
xQueueOverwrite(queue_irq_rec_A, &thread_sent_app);
}
osDelay(10);
}
}
中断回调(更新 head 指针 + 队列通知)
DMA 半满、全满中断保证连续数据不丢失,串口空闲中断保证不定长帧及时处理:
// DMA半满中断回调(ISR上下文)
void dma_half_irq_callback(uint32_t number_of_data)
{
uint32_t head_pos = get_head_pos(g_circular_buffer_irq_thread);
uint32_t current_data_pos = CIRCULAR_BUFFER_SIZE / 2;
uint32_t pos_in_buffer = head_pos % (CIRCULAR_BUFFER_SIZE / 2);
// 计算head需要前移的步数
uint32_t move_pos = (current_data_pos < pos_in_buffer) ?
(current_data_pos + CIRCULAR_BUFFER_SIZE) - pos_in_buffer :
current_data_pos - pos_in_buffer;
head_pos_increment(g_circular_buffer_irq_thread, move_pos);
// ISR中发送队列(非阻塞)
uint32_t send_to_thread = SENT_QUEUE_THREAD;
xQueueGenericSendFromISR(queue_uart_irq_thread, &send_to_thread, NULL, queueOVERWRITE);
}
// DMA全满中断、UART空闲中断回调逻辑类似,仅目标位置不同
2.3 APP 层:帧协议解析(状态机)
解析自定义帧协议:0xFE(帧头) + 数据域 + 0xFF(帧尾) + 校验和,通过状态机实现高可靠解析。
#define FRAME_NOT_DETECTED 0x01 // 等待帧头
#define FRAME_HEAD 0x02 // 已收帧头
#define FRAME_HEAD_FLAG 0xFE // 帧头标识
#define FRAME_END_FLAG 0xFF // 帧尾标识
QueueHandle_t queue_irq_rec_A = NULL;
// 数据解析线程
void uart_Rec_A_function(void *argument)
{
queue_irq_rec_A = xQueueCreate(1, 4);
circular_buffer_t* g_buffer = get_ponterbuffer(); // 获取缓冲区指针
uint32_t receive_data = 0;
static uint32_t temp_data_array[20]; // 帧缓存
static uint8_t count = 0; // 数据计数
static uint32_t status = FRAME_NOT_DETECTED; // 状态机
for (;;) {
// 阻塞等待驱动线程通知
xQueueReceive(queue_irq_rec_A, &receive_data, portMAX_DELAY);
// 循环读取缓冲区所有数据
while (BUF_NO == buffer_is_empty(g_buffer)) {
uint8_t temp_data = 0;
get_data(g_buffer, &temp_data);
// 状态机解析
switch (status) {
case FRAME_NOT_DETECTED:
if (FRAME_HEAD_FLAG == temp_data) status = FRAME_HEAD;
break;
case FRAME_HEAD:
if (FRAME_END_FLAG == temp_data) {
// 校验和验证
uint16_t sum = 0;
for (uint8_t i = 0; i < count - 1; i++) sum += temp_data_array[i];
if (sum == temp_data_array[count - 1]) {
// 校验通过,输出有效数据
for (uint8_t i = 0; i < count - 1; i++)
log_i("有效数据:%d", temp_data_array[i]);
} else {
log_w("校验失败,数据异常");
}
count = 0;
status = FRAME_NOT_DETECTED;
} else {
// 存储中间数据
temp_data_array[count++] = temp_data;
log_i("接收数据:%d", temp_data);
}
break;
default:
break;
}
}
osDelay(1);
}
}
2.4 CubeMX 配置要点
| 配置项 | 取值 / 说明 |
|---|---|
| UART 实例 | USART1(PA9 TX/PA10 RX) |
| 波特率 | 115200 |
| DMA 通道 | DMA2_Stream2_Channel4(USART1_RX) |
| DMA 模式 | CIRCULAR(环形模式,持续接收) |
| 数据宽度 | BYTE(匹配 uint8_t 缓冲区) |
| 中断优先级 | DMA/UART 中断优先级 ≥ configMAX_SYSCALL_INTERRUPT_PRIORITY |
| 使能中断 | UART 空闲中断、DMA 半满 / 全满中断 |



三、运行验证
3.1 测试用例与日志
-
发送错误帧:
0xFE 01 02 03 05 0xFF(校验和应为 6,实际为 5)[I] 接收数据:1 [I] 接收数据:2 [I] 接收数据:3 [I] 接收数据:5 [W] 校验失败,数据异常 -
发送正确帧:
0xFE 01 02 03 06 0xFF(校验和 6)[I] 接收数据:1 [I] 接收数据:2 [I] 接收数据:3 [I] 接收数据:6 [I] 有效数据:1 [I] 有效数据:2 [I] 有效数据:3
3.2 核心特性验证
- 持续接收:DMA CIRCULAR 模式 + 环形缓冲区,head 指针持续递增,数组通过取余复用,实现 “有限内存、无限接收”;
- 中断互补:半满 / 全满中断保证连续数据不丢,空闲中断保证不定长帧及时处理;
- 线程安全:队列实现中断与线程的异步通知,避免竞态条件。
四、总结与扩展
4.1 核心设计亮点
- 环形缓冲区:绝对计数指针 + 取余复用,牺牲 1 个单元区分空 / 满,简化数据量计算;
- DMA CIRCULAR:无需手动重启 DMA,持续写入缓冲区,降低 CPU 开销;
- 三层架构:中间件解耦硬件 / OS,BSP 封装驱动,APP 聚焦业务,代码可复用性高;
- 状态机解析:帧协议解析鲁棒性强,适配不定长、多帧连续场景。
4.2 扩展方向
- 扩容缓冲区:将
CIRCULAR_BUFFER_SIZE改为 256/512,适配高波特率(如 1Mbps)大数据量场景; - 低功耗优化:用
HAL_UARTEx_ReceiveToIdle_IT替代 DMA,减少外设功耗; - 多任务安全:为缓冲区读写加互斥锁,适配多任务读写场景;
- 协议扩展:支持变长帧、多命令类型、ACK/NAK 握手机制。
嵌入式技术是硬功夫,也是细活儿。尽管我已经反复推敲,但受限于个人水平,文中难免存在疏漏或理解偏差。
如果各位读者发现文中有知识点错误、逻辑漏洞,或者涉及到版权/侵权问题,恳请不吝赐教或直接联系我。技术的进步在于分享与纠错,我希望这篇博客不仅能帮到你,也能在大家的反馈中不断进化。
感谢阅读,我们下期见。
更多推荐



所有评论(0)