在 freertos 里,我们可能会用到多个任务或 ISR,任务和 ISR 统称为进程。当任务与任务之间,或者任务与 ISR 之间需要进行通讯或同步时,所使用的机制就称为进程间通信 (IPC,Inter-process communication)。
在这里视频提了一嘴双缓冲区的机制,这个机制常常被用来处理生产者与消费者速度不匹配的问题和确保数据的输入和处理不会互相干扰,不过除了双缓冲之外,还有一种环缓冲也可以起到这种作用。
双缓冲区有两个长度一致的内存空间,当生产者填满了其中一个时,就会通知消费者处理该空间的数据,并开始向另一个空间填充数据。
环形缓冲区只有一个内存空间,生产者使用一个写指针来向空间不断写入数据,同时指针后移,直到触底后返回起点;消费者也有一个读指针执行相同的操作。
只要设计得当,这两种缓冲区都可以通过无锁通信。
队列本质上是一个数据缓冲区。
信号量主要用于同步和资源管理,主要分为以下两种类型:
互斥量是一种特殊的二值信号量,专门用于保护临界资源,确保同一时刻只有一个任务能访问该资源。
与信号量(通常是一对一)不同,事件组可以处理多对多的复杂逻辑关系。它内部由一组标志位(通常是 16 位或 32 位)组成,每一位代表一个独立的事件。
是 FreeRTOS 中一种极速、轻量级的通信机制。它不需要创建独立的 IPC 对象,而是直接利用目标任务“任务控制块 (TCB)”中自带的 32 位变量进行信号或数据的传递。
一种经过高度优化的通信机制,主要为了解决传统队列 (Queue) 在特定场景下效率不够高的问题。
队列创建时被分配固定个数的存储单元,每个存储单元存储固定大小的数据,进程间传递的数据就保存在队列的存储单元里。
函数xQueueCreate()以动态分配内存的方式创建队列,队列需要用的存储空间由FreeRTOS从堆空间自动分配。也可以用xQueueCreateStatic()使用静态方式创建队列。
freertos 创建 IPC 组件时都有动态和静态两种方式,后面我们将只介绍动态方法。
#define xQueueCreate( uxQueueLength, uxItemSize ) xQueueGenericCreate( ( uxQueueLength ), ( uxItemSize ), ( queueQUEUE_TYPE_BASE ) )可以看到,xQueueCreate 其实并不是一个真正的 C 语言函数,而是一个预处理宏 (Macro)。
xQueueGenericCreate时创建队列、互斥量、信号量的通用函数。这也说明了互斥量和信号量本质也是一种特殊的队列。
QueueHandle_t xQueueGenericCreate(
const UBaseType_t uxQueueLength,
const UBaseType_t uxItemSize,
const uint8_t ucQueueType
);调用函数 xQueueCreate() 的示例如下:
Queue_KeysHandle = xQueueCreate (5, sizeof(uint16_t));这行代码创建了一个具有5个存储单元的队列,每个单元占用 sizeof(uint16_t) 个字节,也就是2个字节。
队列的存储单元可以设置任意大小,可以存储任意数据类型 ;队列存储数据采用数据复制的方式。因此当数据项较大时,可以传递数据的指针,通过指针再去读取原始数据;此时要注意不能随意销毁数据,防止读取到的是一个未定义的值。
一个任务或ISR向队列写入数据称为发送消息。队列是一个共享的存储区域,可以被多个进程写入,也可以被多个进程读取。
写入的方法有两种:
xQueueSendToBack():向队列后端写入数据(FIFO模式)xQueueSendToFront():向队列前端写入数据(LIFO模式)它们都是宏函数,调用了函数 xQueueGenericSend(),当要写入的队列已满时,会返回一个错误值。
除此之外,还有第三种写入方式xQueneOverwrite(),它会覆盖队列原来的数据,所以只能用于队列长度为一的队列(通常称呼这种队列叫“邮箱”)。
可以在任务或ISR里读取队列的数据,称为接收消息。总是从队列头读取数据,读出后删除这个单元的数据,后面的数据前移。
函数 xQueueReceive() 的函数原型如下:
BaseType_t xQueueReceive( QueueHandle_t xQueue, void * const pvBuffer, TickType_t xTicksToWait );xQueue 是所操作的队列句柄;pvBuffer 是从队列读出数据保存的缓冲区;xTicksToWait 是阻塞方式等待节拍数;填 0:不阻塞(轮询模式);填 portMAX_DELAY:死等。任务 C 进入睡眠状态,交出 CPU,直到有人发了数据才会被系统强行唤醒;填具体数值就等待具体数值的时间。pdTRUE 或 pdFALSE。这里的前移只是逻辑上的移动,实际上在内存中数据依然保持不变。
如果每次读取都要把后面的数据在内存中物理拷贝前移,那将是对 CPU 极大的浪费。FreeRTOS 队列底层实际上维护着一个读指针(Head)和一个写指针(Tail)。
当
xQueueReceive读走数据后,原来的内存数据并不会被清空或移动,而是读指针向后滑动了一格。系统逻辑上认为这块内存“空出来了”,等待写指针绕回来时将其覆盖。
还有一种读取方式:xQueuePeek,它只会读取内容而不删除,因此无论在短时间内调用多少次该函数,读取到的值都不会变,直到有别的进程向其中写入新的数据。该函数适用于前面说过的邮箱、亦或是某种 消息路由器 。
消息路由器 (Message Router) 是一种专门用于解耦系统通信的中心化任务(Task)。
它的唯一职责是:接收来自四面八方的所有混合消息,根据消息的“头部特征(如命令ID、设备ID)”,将消息分发给对应的、真正负责处理的业务任务。
应用场景:功能集中型系统。
比如智能家居的控制系统,它可能会从多个渠道接受消息,如果你让每个业务都去轮询这些消息接口,很显然会造成性能的极大浪费。所以让一个路由器去接受消息,在转发给对应的任务。
再比如软硬件分离,按键中断等硬件输入可以全部交由路由处理,软件层只需要读取对应硬件的状态即可
除此之外,路由还可以检查消息是由谁发送的,如果发现消息来源非法,那么就丢弃该消息。
缺点:增加内存开销、增加响应时间、整个系统的运转全部依靠路由任务(单点故障)。
因此应该使用诸如冗余、软件看门狗、关键信息不经过路由发送等方式保证系统稳定性。
无论是读数据还是写数据,这些函数都有带有FromISR后缀的中断函数版本,如xQueueReceiveFromISR。
xQueueReset该函数没有对应的中断版本。
将队列复位为空队列。这也是一种“逻辑上”的操作,系统会将读写指针归位并把缓冲区信息数量归零。
vQueueDelete该函数没有对应的中断版本。
删除队列,同样用于销毁信号量和互斥量。
如果你的队列最初是用静态方法创建的,底层控制块里会有一个 ucStaticallyAllocated 标志位。vQueueDelete 检测到这个标志后,不会去调用 vPortFree,仅仅是在内核层面上注销这个队列对象的存在。
为了防止发生Use-After-Free(释放后使用)的严重漏洞,请在调用删除函数后,紧接着将句柄赋值为 NULL:
vQueueDelete( myQueueHandle );
myQueueHandle = NULL; // 极度重要的防错习惯pcQueueGetName该函数没有对应的中断版本。
获取队列的名称。默认该函数被裁剪。
这些名称并不是直接在控制块中保存的,而是系统会维护一个注册列表,将传入的句柄和表对比,从而得到名字。
在使用这个函数之前,必须使用
vQueueAddToRegistry去将名字与句柄绑定。注册进去的,以及读取出来的,仅仅是一个指针,FreeRTOS 底层并不会把那个字符串复制一遍存起来。 绝对不能使用局部变量的数组来给队列命名。如果你传了一个局部字符串的指针,函数退出后该内存被回收,日后
pcQueueGetName读到的将是乱码甚至引发内存段错误。必须使用全局常量字符串(如 const char *str = “name”; )。
vQueueSetQueueNumber该函数没有对应的中断版本。
为队列设置一个编号,默认该函数被裁剪。
该编号仅对用户可见,方便用户调试。
该编号会保存在 控制块结构体 的 uxQueueNumber 成员变量中。
uxQueueSpacesAvailable该函数没有对应的中断版本。1
查询指定队列中目前还有多少个空闲项。该函数是原子的。
并不能将该函数的返回值作为是否向队列中写入数据且无需等待的判断依据。因为查询和写入这两个步骤之间并不是原子的,可能会有一个更高优先级的任务随时抢占队列空位。因此无论什么时候都应该设置一个阻塞时间。
该队列的正确用途2应该是系统健康检测和当需要一次性写入多个数据时,查询空位。
uxQueueMessagesWaiting该函数对应的中断版本为
uxQueueMessagesWaitingFromISR。
查询指定队列中目前存有几个有效的数据项(即有几条消息正在排队等待被读取)。该函数是原子的。
基于同样的原因,不能用它来作为判断是否接受的条件。
该函数的正确用途是:
当检测到缓冲区快满时说明消费者数量不足,可以多创建几个消费者线程实现动态负载;
批量打包数据,只有当数据多余一定数量时才唤醒处理线程一次性处理;
用于 UI 显示当前有多少消息未处理。
xQueueIsQueueEmptyFromISR、xQueueIsQueueFullFromISR这两个函数没有对应的任务版本。但你可以使用
uxQueueSpacesAvailable和uxQueueMessagesWaiting来达到相同的效果。
查询某个队列目前是否为空/为满。
xQueueSend该函数对应的中断版本为
xQueueSendFromISR。
早期版本的函数,和xQueueSendToBack完全一样。