FreeRTOS 通信任务设计(2)----UART+DMA 环形缓冲 + 空闲中断+ 流缓冲区--- 高效接收方案详解

🎬 渡水无言个人主页渡水无言

专栏传送门 : 《linux专栏》《嵌入式linux驱动开发》《linux系统移植专栏》

专栏传送门 : 《freertos专栏》 《STM32 HAL库专栏》《linux裸机开发专栏

专栏传送门《产品测评专栏

⭐️流水不争先,争的是滔滔不绝

📚博主简介:第二十届中国研究生电子设计竞赛全国二等奖 |国家奖学金 | 省级三好学生

| 省级优秀毕业生获得者 | csdn新星杯TOP18 | 半导纵横专栏博主 | 211在读研究生

在这里主要分享自己学习的linux嵌入式领域知识;有分享错误或者不足的地方欢迎大佬指导,也欢迎各位大佬互相三连

目录

前言

一、核心技术原理总览

二、DMA环形区详解

三、空闲中断

四、流缓冲区

五、示例代码

总结


前言

在嵌入式串口通信中,高效、低 CPU 占用的不定长数据接收 是核心痛点。传统逐字节中断方案 CPU 负载高、易丢包,轮询方案实时性差。本文基于UART+DMA 环形缓冲 + 空闲中断+ 流缓冲区的方案来详细解决这些问题,从原理、代码到实战全流程覆盖,适合 FreeRTOS 通信任务、机器人上位机通信等场景复用。


一、核心技术原理总览

在串口通信中,数据以字节为最小单位连续传输,MCU 无法预知数据的发送起止时刻。从物理层来看,一帧数据本质上就是一段连续的字节流,如果没有额外的机制,系统不仅无法高效接收数据,更无法可靠地判断哪些数据已经完整接收或可以处理。

UART+DMA 环形缓冲 + 空闲中断+ 流缓冲区完美解决这些问题:
DMA 会将串口收到的数据循环写入大小为 256 字节的固定缓冲区,串口持续接收数据时,DMA 会自动完成数据搬运,CPU 无需逐字节干预。当上位机连续发送数据时,DMA 会自动缓存数据;

当发送暂停超过设定时长。

STM32 会触发串口空闲中断,通知 CPU 处理缓冲区中的完整数据帧。

**流缓冲区:**在中断和任务之间搭建一个安全通道,中断只负责快速存入数据,通信任务阻塞等待读取,实现中断与任务解耦,保证线程安全、不丢数据。

二、DMA环形区详解

DMA(Direct Memory Access,直接存储器访问)是 STM32 的硬件外设,是一种让外设直接把数据写入内容的机制。

让CPU 从 "搬运工" 变 "管理者":仅在数据接收完成后处理数据,无需逐字节干预。

有无 DMA 的核心差异

假设上位机发送 10 字节数据:11 22 33 44 55 66 77 88 99 AA

DMA 支持两种工作模式:

模式 特点 适用场景
普通模式 接收满指定长度后停止,需手动重启 DMA 固定长度数据(如 ADC 采样、SPI)
环形模式 写满缓冲区后自动回到开头,无限循环接收,永不停止 UART 不定长数据接收(本方案)

环形 DMA 工作原理直观示意 # 缓冲区结构(以8字节为例)

buf[0] buf[1] buf[2] buf[3] buf[4] buf[5] buf[6] buf[7]

写入流程:

写入开始 → → → → → → → → 写满后回到 buf[0] 循环写入。

这时候又会出现一个问题了:

如何区分新 / 旧数据?

这时候可以设置两个变量curr_pos和last_pos。

curr_pos:DMA 当前写入位置。

last_pos:上一次处理的位置。

两者之间的区域就是"新数据"。

当curr_pos > last_pos,新数据就是一段连续区间。

当curr_pos <last_pos,说明 DMA 绕回开头,新数据分为两段:[last_pos, 缓冲区末尾] + [buf[0], curr_pos)。

三、空闲中断

空闲中断(IDLE Interrupt)是 UART 的硬件特性:当串口线路上超过 1 个字符的传输时间无新数据时,自动触发中断

注意:空闲中断并不等同于 "协议帧结束"。空闲中断并不了解任何协议格式,也不关心数据内容,它只基于串口物理层的行为作出判断。换句话说,空闲中断表达的真实语义是:"串口在刚刚接收了一串字节之后,暂时没有新的字节到来"。

复制代码
时间轴 →
[11] → [22] → [33] → [44] → [55] (连续字节)
          ↓
空闲时间 > 1 个字符传输时间
          ↓
触发 IDLE 空闲中断

收完55后无新数据,UART 触发空闲中断,通知 CPU 处理已接收的11 22 33 44 55

四、流缓冲区

在 RTOS 系统中,中断和任务存在一个天然矛盾:中断需要快速执行,不能做复杂处理;而协议解析任务逻辑复杂、耗时不可控。如果在中断中直接解析数据,会导致系统实时性下降,甚至出现数据覆盖问题。

FreeRTOS 流缓冲区(StreamBuffer)(面向字节流的通信机制)正是为了解决这个矛盾而设计的,它是一个由内核管理的 FIFO 缓冲区,专门用于在中断和任务之间传递字节流数据,核心特点如下:

与队列有所不同,其并不关心"消息边界",把数据视为一条连续的字节流,和串口、DMA 的数据特性完全匹配。

形象理解:流缓冲区是一根 "水管"。

中断端:不断向水管里 "灌水"(写入字节),不用关心任务什么时候读取。

任务端:在需要的时候从水管里 "接水"(读取字节),不用关心数据什么时候写入。

只要水管里有数据,双方就能顺利工作,互不影响。

在我们的项目中,流缓冲区的使用流程如下:

(通过xStreamBufferSendFromISR()函数写入流缓冲区)

  1. 空闲中断触发后,将 DMA 新接收的数据写入流缓冲区。
  2. 通信任务(如CommTask)阻塞在流缓冲区通过xStreamBufferReceive()读取接口上,不占用 CPU。
  3. 流缓冲区有数据时,自动唤醒通信任务,开始协议解析。

注意:流缓冲区不保证一次读取能拿到完整的一帧数据,它只负责传递字节流,如何拼成完整的协议帧,依然需要由协议解析状态机来完成。

五、示例代码

cpp 复制代码
/**
 ****************************************************************************************************
 * @brief        STM32 UART1 DMA环形缓冲 + 空闲中断 + FreeRTOS流缓冲区 示例代码
 * @platform     STM32 + FreeRTOS + HAL库
 * @function     解决串口粘包/丢包,实现低负载、高可靠不定长数据接收
 ****************************************************************************************************
 */

#include "usart.h"
#include "FreeRTOS.h"
#include "stream_buffer.h"
#include "task.h"

/* 配置参数 */
#define UART1_RX_DMA_SIZE       256     /* DMA环形缓冲大小 */
#define UART1_RX_SB_SIZE        512     /* 流缓冲区大小 */
#define UART1_RX_TRIG_LEVEL     1       /* 触发唤醒阈值 */

/* 全局句柄与缓冲 */
static uint8_t uart1_rx_dma_buf[UART1_RX_DMA_SIZE];   /* DMA环形接收缓冲区 */
static StreamBufferHandle_t uart1_rx_stream = NULL;   /* 流缓冲区句柄 */
extern UART_HandleTypeDef huart1;                     /* UART句柄 */

/**
 * @brief  串口DMA+空闲中断初始化
 */
void UART_DMA_IDLE_Init(void)
{
    /* 创建流缓冲区 */
    uart1_rx_stream = xStreamBufferCreate(UART1_RX_SB_SIZE, UART1_RX_TRIG_LEVEL);
    
    /* 启动UART1 DMA循环接收 */
    HAL_UART_Receive_DMA(&huart1, uart1_rx_dma_buf, UART1_RX_DMA_SIZE);
    
    /* 使能空闲中断 */
    __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
}

/**
 * @brief  UART1 空闲中断回调函数(核心:DMA → 流缓冲区)
 * @note   运行在中断上下文
 */
void UART1_Idle_Callback(void)
{
    static uint16_t last_pos = 0;
    uint16_t curr_pos = UART1_RX_DMA_SIZE - __HAL_DMA_GET_COUNTER(huart1.hdmarx);
    
    if (curr_pos == last_pos) return;
    
    BaseType_t hpw = pdFALSE;
    
    /* 情况1:DMA未回绕,直接读取一段 */
    if (curr_pos > last_pos)
    {
        uint16_t len = curr_pos - last_pos;
        xStreamBufferSendFromISR(uart1_rx_stream, &uart1_rx_dma_buf[last_pos], len, &hpw);
    }
    /* 情况2:DMA回绕,分两段读取 */
    else
    {
        uint16_t len1 = UART1_RX_DMA_SIZE - last_pos;
        xStreamBufferSendFromISR(uart1_rx_stream, &uart1_rx_dma_buf[last_pos], len1, &hpw);
        
        if (curr_pos > 0)
        {
            xStreamBufferSendFromISR(uart1_rx_stream, uart1_rx_dma_buf, curr_pos, &hpw);
        }
    }
    
    last_pos = curr_pos;
    portYIELD_FROM_ISR(hpw);
}

/**
 * @brief  通信任务:从流缓冲区读取数据
 */
void Comm_Task(void *arg)
{
    uint8_t buf[64];
    
    while (1)
    {
        /* 阻塞等待数据 */
        size_t len = xStreamBufferReceive(uart1_rx_stream, buf, sizeof(buf), portMAX_DELAY);
        
        if (len > 0)
        {
            // 在这里处理接收到的数据:协议解析/业务逻辑
        }
    }
}

UART_DMA_IDLE_Init函数

完成三件事:创建流缓冲区 → 启动 DMA 环形接收 → 打开空闲中断,是整个方案的初始化入口。
UART1_Idle_Callback(最核心)

空闲中断触发后,自动计算 DMA 指针,判断是否回绕,并将新数据安全送入流缓冲区,自动唤醒任务。

✅ 解决粘包:靠空闲中断分割数据段。

✅ 解决丢包:靠环形缓冲 + 流缓冲区缓存。
Comm_Task

以阻塞方式从流缓冲区读取数据,不占用 CPU,收到数据后再进行解析,实现中断与任务完全解耦。

总结

我们可以把整个串口接收流程拆成四层来理解,各层职责清晰:

DMA 层:负责将串口收到的字节持续写入物理内存,全程不需要 CPU 干预;

空闲中断层:判断 "一批数据已经接收完成,可以转交处理",是上层任务的触发信号;

流缓冲区层:作为 "安全通道",在中断上下文与任务上下文之间传递数据,避免中断阻塞和数据丢失;

CommTask 任务层:在任务上下文对流缓冲区中的数据进行协议解析和业务处理。

相关推荐
学习噢学个屁2 小时前
基于51单片机心率仪—体温心率血氧蓝牙
c语言·单片机·嵌入式硬件·51单片机
W.W.H.2 小时前
嵌入式常见面试题——硬件与中断篇
经验分享·单片机·嵌入式硬件
灰子学技术2 小时前
Envoy 中 UDP 网络通信实现分析
网络·单片机·嵌入式硬件·网络协议·udp
济6173 小时前
FreeRTOS 通信任务设计(1)---STM32 串口 DMA + 协议帧解析 + CRC 校验全流程详解
stm32·嵌入式·freertos
三佛科技-187366133973 小时前
便携式一字美甲灯方案开发
单片机·嵌入式硬件
飞睿科技3 小时前
从 Mesh 到无线视频,ESP32-E22 的场景落地指南,飞睿科技乐鑫代理商
单片机·嵌入式硬件
Tomhex3 小时前
stm32将JTAG/SWD接口误设GPIO模式后无法调试
stm32
PegasusYu3 小时前
STM32 I2C访问配置霍尔磁角度传感器MT6701
stm32·编码器·i2c·stm32cubeide·mt6701·角度·磁角度传感器
清风6666663 小时前
基于单片机的智能门控制系统设计与故障报警实现
数据库·单片机·mongodb·毕业设计·课程设计·期末大作业