[ 附件影像 RECORD ]
FreeRTOS学习笔记 · 第十一讲:4.1freertosQueue消息队列

FreeRTOS学习笔记 · 第十一讲:4.1freertosQueue消息队列

[ SCAN_URL ]
[ 归档时间 ]:2026-03-28 19:52 [ 课题责任人 ]:文止墨 [ 档案分类 ]:嵌入式, 文章, 编程

概念介绍

在 freertos 里,我们可能会用到多个任务或 ISR,任务和 ISR 统称为进程。当任务与任务之间,或者任务与 ISR 之间需要进行通讯同步时,所使用的机制就称为进程间通信 (IPC,Inter-process communication)

在这里视频提了一嘴双缓冲区的机制,这个机制常常被用来处理生产者与消费者速度不匹配的问题和确保数据的输入和处理不会互相干扰,不过除了双缓冲之外,还有一种环缓冲也可以起到这种作用。

双缓冲区有两个长度一致的内存空间,当生产者填满了其中一个时,就会通知消费者处理该空间的数据,并开始向另一个空间填充数据。

环形缓冲区只有一个内存空间,生产者使用一个写指针来向空间不断写入数据,同时指针后移,直到触底后返回起点;消费者也有一个读指针执行相同的操作。

只要设计得当,这两种缓冲区都可以通过无锁通信。

进程间通信与同步机制 (IPC) 核心组件

队列 (Queue)

队列本质上是一个数据缓冲区。

  • 主要用途:用于在进程(任务/中断)之间传递少量的数据。因此在很多语境下,它也被称为消息队列 (Message Queue)

信号量 (Semaphore)

信号量主要用于同步和资源管理,主要分为以下两种类型:

  • 二值信号量 (Binary Semaphore)
  • 相当于一个标志位(只有 0 和 1 两个状态)。
  • 主要用于进程间同步(例如:中断触发后,释放一个二值信号量来唤醒处理任务)。
  • 计数信号量 (Counting Semaphore)
  • 内部包含一个计数值(大于 1)。
  • 一般用于共享资源的管理(例如:系统中有 3 个串口可用,就可以创建一个初始值为 3 的计数信号量,任务申请到一个就减一,用完释放就加一)。

互斥量 (Mutex)

互斥量是一种特殊的二值信号量,专门用于保护临界资源,确保同一时刻只有一个任务能访问该资源。

  • 分类
    • 普通互斥量 (Mutex)
    • 递归互斥量 (Recursive Mutex):允许同一个任务多次获取同一个互斥量而不会造成死锁(获取多少次就需要释放多少次)。
  • 核心特性 (重点):互斥量具有优先级继承机制 (Priority Inheritance Mechanism)
  • 解决的痛点:这种机制可以有效地减轻或解决在多任务抢占式系统中常见的优先级翻转问题 (Priority Inversion)

事件组 (Event Group)

与信号量(通常是一对一)不同,事件组可以处理多对多的复杂逻辑关系。它内部由一组标志位(通常是 16 位或 32 位)组成,每一位代表一个独立的事件。

  • 主要应用场景
    • 多事件触发:一个任务需要等待多个条件同时满足(“逻辑与 AND”)才能运行。例如:必须等到“电机达到指定转速”且“温度处于安全范围”这两个事件都发生时,才开始下一步操作。
    • 事件广播:一个事件发生时,可以同时唤醒多个正在等待该事件的任务。
    • 任务同步:实现多个任务在某个执行点上的同步等待(大家都到达集合点后,再一起往下走)。

任务通知(Task Notification)

是 FreeRTOS 中一种极速、轻量级的通信机制。它不需要创建独立的 IPC 对象,而是直接利用目标任务“任务控制块 (TCB)”中自带的 32 位变量进行信号或数据的传递。

  • 核心优势速度最快(解除阻塞速度比其他机制快约 45%),且不消耗额外内存(RAM)。
  • 应用场景:极其灵活,可用于模拟二值信号量计数信号量事件组,或作为传递单一数据的轻量级邮箱
  • 适用限制:专用于有明确接收目标的通信(一对一或多对一),无法实现事件广播,且发送方无法阻塞等待。

流缓冲区 (Stream Buffer) 与 消息缓冲区 (Message Buffer)

一种经过高度优化的通信机制,主要为了解决传统队列 (Queue) 在特定场景下效率不够高的问题。

  • 显著特点与适用场景
    • 严格的 1对1 限制专门针对只有一个写入者 (Single Writer) 和一个读取者 (Single Reader) 的场景设计。因为明确了只有单收单发,它在底层省去了很多复杂的互斥锁机制,因此执行效率比普通队列快得多。
    • 跨核高效传输:除了在单核的任务/中断间通信,它还非常适合用于 AMP(非对称多处理)架构的多核 CPU 中,作为两个不同内核之间传递数据的高速通道。
  • 两者的细微区别(延伸)
    • 流缓冲区 (Stream Buffer):处理的是连续的字节流,没有数据边界(类似环形缓冲区读取方式)。写入10个字节,读取时可以分两次读,每次读5个。
    • 消息缓冲区 (Message Buffer):基于流缓冲区构建,但处理的是离散的消息。写入一条 10 字节的消息,读取时必须一次性把这 10 个字节作为一条完整消息读出来。

队列的特点和基本操作

队列的创建与储存

队列创建时被分配固定个数的存储单元,每个存储单元存储固定大小的数据,进程间传递的数据就保存在队列的存储单元里。

函数xQueueCreate()以动态分配内存的方式创建队列,队列需要用的存储空间由FreeRTOS从堆空间自动分配。也可以用xQueueCreateStatic()使用静态方式创建队列。

freertos 创建 IPC 组件时都有动态和静态两种方式,后面我们将只介绍动态方法。

C
#define xQueueCreate( uxQueueLength, uxItemSize )     xQueueGenericCreate( ( uxQueueLength ), ( uxItemSize ), ( queueQUEUE_TYPE_BASE ) )

可以看到,xQueueCreate 其实并不是一个真正的 C 语言函数,而是一个预处理宏 (Macro)

xQueueGenericCreate时创建队列、互斥量、信号量的通用函数。这也说明了互斥量和信号量本质也是一种特殊的队列。

C
QueueHandle_t xQueueGenericCreate( 
    const UBaseType_t uxQueueLength, 
    const UBaseType_t uxItemSize, 
    const uint8_t ucQueueType 
);

调用函数 xQueueCreate() 的示例如下:

C
Queue_KeysHandle = xQueueCreate (5, sizeof(uint16_t));

这行代码创建了一个具有5个存储单元的队列,每个单元占用 sizeof(uint16_t) 个字节,也就是2个字节。

队列的存储单元可以设置任意大小,可以存储任意数据类型 ;队列存储数据采用数据复制的方式。因此当数据项较大时,可以传递数据的指针,通过指针再去读取原始数据;此时要注意不能随意销毁数据,防止读取到的是一个未定义的值。

向队列写入数据

一个任务或ISR向队列写入数据称为发送消息。队列是一个共享的存储区域,可以被多个进程写入,也可以被多个进程读取。

写入的方法有两种:

  • xQueueSendToBack():向队列后端写入数据(FIFO模式)
  • xQueueSendToFront():向队列前端写入数据(LIFO模式)

它们都是宏函数,调用了函数 xQueueGenericSend(),当要写入的队列已满时,会返回一个错误值。

除此之外,还有第三种写入方式xQueneOverwrite(),它会覆盖队列原来的数据,所以只能用于队列长度为一的队列(通常称呼这种队列叫“邮箱”)。

向队列中读取数据

可以在任务或ISR里读取队列的数据,称为接收消息。总是从队列头读取数据,读出后删除这个单元的数据,后面的数据前移。

函数 xQueueReceive() 的函数原型如下:

C
BaseType_t xQueueReceive( QueueHandle_t xQueue, void * const pvBuffer, TickType_t xTicksToWait );
  • xQueue 是所操作的队列句柄;
  • pvBuffer 是从队列读出数据保存的缓冲区;
  • xTicksToWait 是阻塞方式等待节拍数;填 0:不阻塞(轮询模式);填 portMAX_DELAY:死等。任务 C 进入睡眠状态,交出 CPU,直到有人发了数据才会被系统强行唤醒;填具体数值就等待具体数值的时间。
  • 函数的返回值是 pdTRUEpdFALSE

这里的前移只是逻辑上的移动,实际上在内存中数据依然保持不变。

如果每次读取都要把后面的数据在内存中物理拷贝前移,那将是对 CPU 极大的浪费。FreeRTOS 队列底层实际上维护着一个读指针(Head)和一个写指针(Tail)。

xQueueReceive 读走数据后,原来的内存数据并不会被清空或移动,而是读指针向后滑动了一格。系统逻辑上认为这块内存“空出来了”,等待写指针绕回来时将其覆盖。

还有一种读取方式:xQueuePeek,它只会读取内容而不删除,因此无论在短时间内调用多少次该函数,读取到的值都不会变,直到有别的进程向其中写入新的数据。该函数适用于前面说过的邮箱、亦或是某种 消息路由器 。

什么是消息路由器?什么时候会用到它?

消息路由器 (Message Router) 是一种专门用于解耦系统通信的中心化任务(Task)。

它的唯一职责是:接收来自四面八方的所有混合消息,根据消息的“头部特征(如命令ID、设备ID)”,将消息分发给对应的、真正负责处理的业务任务。

应用场景:功能集中型系统。

比如智能家居的控制系统,它可能会从多个渠道接受消息,如果你让每个业务都去轮询这些消息接口,很显然会造成性能的极大浪费。所以让一个路由器去接受消息,在转发给对应的任务。

再比如软硬件分离,按键中断等硬件输入可以全部交由路由处理,软件层只需要读取对应硬件的状态即可

除此之外,路由还可以检查消息是由谁发送的,如果发现消息来源非法,那么就丢弃该消息。

缺点:增加内存开销、增加响应时间、整个系统的运转全部依靠路由任务(单点故障)。

因此应该使用诸如冗余、软件看门狗、关键信息不经过路由发送等方式保证系统稳定性。

无论是读数据还是写数据,这些函数都有带有FromISR后缀的中断函数版本,如xQueueReceiveFromISR

其它队列状态操作函数

xQueueReset

该函数没有对应的中断版本。

将队列复位为空队列。这也是一种“逻辑上”的操作,系统会将读写指针归位并把缓冲区信息数量归零。

vQueueDelete

该函数没有对应的中断版本。

删除队列,同样用于销毁信号量和互斥量。

如果你的队列最初是用静态方法创建的,底层控制块里会有一个 ucStaticallyAllocated 标志位。vQueueDelete 检测到这个标志后,不会去调用 vPortFree,仅仅是在内核层面上注销这个队列对象的存在。

为了防止发生Use-After-Free(释放后使用)的严重漏洞,请在调用删除函数后,紧接着将句柄赋值为 NULL

C
vQueueDelete( myQueueHandle );
myQueueHandle = NULL; // 极度重要的防错习惯

pcQueueGetName

该函数没有对应的中断版本。

获取队列的名称。默认该函数被裁剪。

这些名称并不是直接在控制块中保存的,而是系统会维护一个注册列表,将传入的句柄和表对比,从而得到名字。

在使用这个函数之前,必须使用vQueueAddToRegistry去将名字与句柄绑定。

注册进去的,以及读取出来的,仅仅是一个指针,FreeRTOS 底层并不会把那个字符串复制一遍存起来。 绝对不能使用局部变量的数组来给队列命名。如果你传了一个局部字符串的指针,函数退出后该内存被回收,日后 pcQueueGetName 读到的将是乱码甚至引发内存段错误。必须使用全局常量字符串(如 const char *str = “name”; )

vQueueSetQueueNumber

该函数没有对应的中断版本。

为队列设置一个编号,默认该函数被裁剪。

该编号仅对用户可见,方便用户调试。

该编号会保存在 控制块结构体 的 uxQueueNumber 成员变量中。

uxQueueSpacesAvailable

该函数没有对应的中断版本。1

查询指定队列中目前还有多少个空闲项。该函数是原子的。

并不能将该函数的返回值作为是否向队列中写入数据且无需等待的判断依据。因为查询和写入这两个步骤之间并不是原子的,可能会有一个更高优先级的任务随时抢占队列空位。因此无论什么时候都应该设置一个阻塞时间。

该队列的正确用途2应该是系统健康检测和当需要一次性写入多个数据时,查询空位。

uxQueueMessagesWaiting

该函数对应的中断版本为uxQueueMessagesWaitingFromISR

查询指定队列中目前存有几个有效的数据项(即有几条消息正在排队等待被读取)。该函数是原子的。

基于同样的原因,不能用它来作为判断是否接受的条件。

该函数的正确用途是:

当检测到缓冲区快满时说明消费者数量不足,可以多创建几个消费者线程实现动态负载;

批量打包数据,只有当数据多余一定数量时才唤醒处理线程一次性处理;

用于 UI 显示当前有多少消息未处理。

xQueueIsQueueEmptyFromISRxQueueIsQueueFullFromISR

这两个函数没有对应的任务版本。但你可以使用uxQueueSpacesAvailableuxQueueMessagesWaiting来达到相同的效果。

查询某个队列目前是否为空/为满。

其它队列写入函数

xQueueSend

该函数对应的中断版本为xQueueSendFromISR

早期版本的函数,和xQueueSendToBack完全一样。

  1. 写完笔记回头看这些红字莫名感到恐怖是怎么回事ww ↩︎
  2. 所谓的正确用途是指推荐用途,并没有绝对正确一说哦 ↩︎

[ 发起通讯连接 / INITIATE COMM-LINK ]

[SYS]: 您的回传节点(邮箱)将被严格保密。带有 * 的字段为必填项。


> 终止读取并返回主控制台 <