前言

在嵌入式开发中,串口通信是连接外设与主控的核心桥梁 ——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的“消息通知接口”。

假设这是一个物联网网关:

  1. 左边(驱动):收到串口数据 01 04 00 01 02 58(一堆乱码)。

  2. 中间(解析)

    • 识别到 01是温湿度传感器模块。

    • 解析出数值 25℃

    • 查内部路由表:发现消费者A订阅了 01号设备

  3. 右边(转发):中间层把 {"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. 环形缓冲区:绝对计数指针 + 取余复用,牺牲 1 个单元区分空 / 满,简化数据量计算;
  2. DMA CIRCULAR:无需手动重启 DMA,持续写入缓冲区,降低 CPU 开销;
  3. 三层架构:中间件解耦硬件 / OS,BSP 封装驱动,APP 聚焦业务,代码可复用性高;
  4. 状态机解析:帧协议解析鲁棒性强,适配不定长、多帧连续场景。

4.2 扩展方向

  • 扩容缓冲区:将CIRCULAR_BUFFER_SIZE改为 256/512,适配高波特率(如 1Mbps)大数据量场景;
  • 低功耗优化:用HAL_UARTEx_ReceiveToIdle_IT替代 DMA,减少外设功耗;
  • 多任务安全:为缓冲区读写加互斥锁,适配多任务读写场景;
  • 协议扩展:支持变长帧、多命令类型、ACK/NAK 握手机制。

嵌入式技术是硬功夫,也是细活儿。尽管我已经反复推敲,但受限于个人水平,文中难免存在疏漏或理解偏差。

       

如果各位读者发现文中有知识点错误、逻辑漏洞,或者涉及到版权/侵权问题,恳请不吝赐教或直接联系我。技术的进步在于分享与纠错,我希望这篇博客不仅能帮到你,也能在大家的反馈中不断进化。

 

感谢阅读,我们下期见。

    

 

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐