FreeRTOS 队列集:让一个任务等待多个消息来源
1. 队列集解决什么问题?
假设一个任务既要接收队列中的数据,又要等待某个信号量。如果先阻塞等待队列,即使信号量已经到来,它也不能马上去处理。
队列集(Queue Set)让任务同时等待多个队列或信号量:任意一个成员有数据或可获取的信号量时,等待任务就有机会被唤醒。
可以把它想成多个信箱共用的"通知台":
-
队列、信号量是各个信箱,存放数据或事件令牌。
-
队列集记录"哪个信箱有东西"。
-
接收任务先到通知台领通知,再到对应信箱取一次内容。
队列集适合"多个来源、统一处理"的场景。它等待的是任意一个成员可读或可获取,不是等所有成员都准备好,也不是向多个任务广播消息。
各队列可以保存不同类型的数据,但这不是必须使用队列集的理由;也可以设计带类型标记的消息结构体,用一个普通队列统一传递。
2. 工作原理:先选成员,再取数据
使用流程如下:
创建队列集和成员 → 将空成员加入队列集
↓
生产者向队列发送数据,或释放信号量
↓
队列集记录对应成员的句柄
↓
接收任务 Select → 得到成员句柄 → Receive / Take 一次
句柄 可以理解为对象的"编号"。xQueueSelectFromSet() 返回哪个对象需要处理,实际数据仍保存在原来的队列里。
例如队列 A 成功收到两条数据,信号量 B 成功释放一次,队列集就可以记录三条通知:A、A、B。任务每次选出一个成员,再从该成员取走一条数据或一个令牌。
因此必须记住:成功选择一次,就对应读取一次或获取一次。 不要选出队列后,直接用循环把它读空。
3. 配置与容量计算
在 FreeRTOSConfig.h 中启用队列集:
#define configUSE_QUEUE_SETS 1
本文使用动态创建,还需要启用动态内存分配支持,即 configSUPPORT_DYNAMIC_ALLOCATION 为 1,并提供相应的 FreeRTOS 堆实现。
常用类型与头文件:
#include "FreeRTOS.h"
#include "queue.h"
#include "semphr.h"
QueueSetHandle_t set; /* 队列集句柄 */
QueueSetMemberHandle_t readyMember; /* 被选中的成员句柄 */
队列集容量怎么算?
xQueueCreateSet(uxEventQueueLength) 的参数表示最多能保存多少条成员就绪通知,单位是条,不是字节。
为保证所有成员同时存满时,通知仍能被完整记录,应按下面的方法分配容量:
| 成员类型 | 计入队列集的容量 |
|---|---|
| 普通队列 | 队列长度,即最多容纳的数据条数 |
| 二值信号量 | 1 |
| 计数信号量 | 最大计数值 |
例如:长度为 3 的队列+长度为 2 的队列+一个二值信号量,需要容量 3 + 2 + 1 = 6。
容量过小可能造成通知无法入集,出现"成员中有数据,接收任务却等不到相应通知"的问题;启用断言时,也可能触发内核断言。
4. 五个核心 API
4.1 创建队列集:xQueueCreateSet()
QueueSetHandle_t xQueueCreateSet(UBaseType_t uxEventQueueLength);
传入通知容量;成功返回队列集句柄,失败返回 NULL,通常需要检查 FreeRTOS 堆空间。
set = xQueueCreateSet(4); /* 例如:长度为 3 的队列+一个二值信号量 */
4.2 添加成员:xQueueAddToSet()
BaseType_t xQueueAddToSet(QueueSetMemberHandle_t member,
QueueSetHandle_t set);
第一个参数是队列或信号量句柄,第二个参数是队列集句柄。成功返回 pdPASS,失败返回 pdFAIL。
添加时有两个关键条件:成员必须为空,且一个成员只能属于一个队列集。 对信号量来说,"空"就是计数为 0。
建议在生产任务和相关中断开始发送数据之前,完成所有成员的添加。
4.3 等待并选择成员:xQueueSelectFromSet()
QueueSetMemberHandle_t xQueueSelectFromSet(QueueSetHandle_t set,
TickType_t waitTicks);
返回有待处理通知的成员句柄 ;超时仍没有通知则返回 NULL。等待时间以系统节拍 Tick 为单位:
waitTicks |
含义 |
|---|---|
0 |
立即检查,不等待 |
pdMS_TO_TICKS(100) |
最多等待约 100 ms,精度取决于节拍周期 |
portMAX_DELAY |
当 INCLUDE_vTaskSuspend 为 1 时,可无限期等待 |
等待期间任务进入阻塞态,不会占用 CPU 反复检查。被唤醒后何时真正运行,仍由任务优先级和调度规则决定。
4.4 移除成员:xQueueRemoveFromSet()
BaseType_t xQueueRemoveFromSet(QueueSetMemberHandle_t member,
QueueSetHandle_t set);
成员必须属于指定队列集,并且已经为空;成功返回 pdPASS,否则返回 pdFAIL。
移除只是解除成员关系,不会删除队列或信号量。实际移除前,应先协调生产者停止写入,按正常的"选择→读取"流程处理完剩余通知,避免并发操作造成状态不一致。
4.5 中断中选择成员:xQueueSelectFromSetFromISR()
QueueSetMemberHandle_t xQueueSelectFromSetFromISR(QueueSetHandle_t set);
这是中断版本,不能阻塞 ,没有等待时间参数;有通知时返回成员句柄,否则返回 NULL。它同样只选成员,不取数据;后续操作也必须使用适用于 ISR 的 API。
更常见的设计是:中断产生消息或释放信号量,任务阻塞等待队列集并处理事件。
5. 简短示例:同时等待数据和事件
设定一个长度为 3、存放 uint32_t 的队列,以及一个二值信号量。队列集容量为 3 + 1 = 4。
下面是初始化的核心调用片段,省略错误处理和任务创建。每个创建结果都必须检查是否为 NULL,每次添加都必须检查是否为 pdPASS;全部成功后再启动生产者和接收任务。
#include <stdint.h>
static QueueHandle_t dataQueue;
static SemaphoreHandle_t eventSem;
static QueueSetHandle_t eventSet;
/* 放在初始化流程中执行,按上述要求逐项检查返回值 */
dataQueue = xQueueCreate(3, sizeof(uint32_t));
eventSem = xSemaphoreCreateBinary(); /* 初始为空,适合直接加入 */
eventSet = xQueueCreateSet(4);
xQueueAddToSet(dataQueue, eventSet);
xQueueAddToSet(eventSem, eventSet);
生产者仍然向原来的成员写入,不直接向队列集发送数据:
uint32_t value = 123;
xQueueSend(dataQueue, &value, 0); /* 入队失败时需按业务处理 */
xSemaphoreGive(eventSem); /* 已经为满时,本次释放不会再增加令牌 */
接收任务统一等待,按照返回的句柄判断消息来源:
void ReceiverTask(void *argument)
{
QueueSetMemberHandle_t member;
uint32_t value;
(void)argument;
for (;;) {
member = xQueueSelectFromSet(eventSet, portMAX_DELAY);
if (member == dataQueue) {
if (xQueueReceive(dataQueue, &value, 0) == pdPASS) {
/* 处理收到的 value */
}
} else if (member == eventSem) {
if (xSemaphoreTake(eventSem, 0) == pdPASS) {
/* 处理事件 */
}
}
/* 返回 NULL 表示本次等待未取得通知,继续下一轮 */
}
}
这里 Receive 和 Take 的等待时间为 0,因为任务已经通过队列集等到了对应通知。前提是所有消费者都遵守队列集的读取规则,不能有其他代码绕过选择步骤抢先取走内容。
6. 最容易出错的地方
-
把选择当成接收。
Select只拿到成员句柄,之后还要Receive或Take。 -
绕过队列集直接读取成员。 这会留下尚未消费的通知,使通知与成员内容不匹配。成员加入队列集后,每次读取或获取前都应先成功选中该成员。
-
选择一次,读取多次。 一次选择只对应一次读取或获取;处理下一条时重新选择。
-
成员还没加入就先发送。 添加时成员必须为空;二值信号量不要先 Give 再加入。
-
把互斥量当作普通事件来源。 等待队列集的任务不能因此让互斥量持有者获得优先级继承,初学者应优先用普通队列、二值信号量或计数信号量作为成员。
-
中断中调用任务版 API。 ISR 中发送队列、释放信号量要使用对应的
...FromISR()版本,并满足端口规定的中断优先级限制。
7. 本章函数与宏汇总
| 函数 / 宏 | 中文含义 | 使用方法与要点 |
|---|---|---|
xQueueCreateSet(n) |
创建队列集 | n 为通知容量;按成员队列长度、信号量最大计数之和分配;失败返回 NULL |
xQueueAddToSet(member, set) |
将成员加入队列集 | 成员必须为空且未属于其他队列集;成功返回 pdPASS |
xQueueSelectFromSet(set, ticks) |
在任务中等待并选择就绪成员 | 返回成员句柄,超时返回 NULL;之后仍需读取一次或获取一次 |
xQueueSelectFromSetFromISR(set) |
在中断中选择就绪成员 | 不阻塞;返回成员句柄或 NULL |
xQueueRemoveFromSet(member, set) |
从队列集中移除成员 | 成员必须属于该集且为空;成功返回 pdPASS,不删除成员 |
xQueueCreate(length, itemSize) |
创建普通队列 | 指定最大条数和每条数据的字节数;失败返回 NULL |
xQueueSend(queue, &data, ticks) |
向队列尾部发送数据 | 数据复制到成员队列;成功返回 pdPASS,队列集自动记录通知 |
xQueueReceive(queue, &data, 0) |
从队列取出一条数据 | 先通过队列集选中该队列,再读取一次;成功返回 pdPASS |
xSemaphoreCreateBinary() |
创建二值信号量 | 初始计数为 0;失败返回 NULL |
xSemaphoreGive(sem) |
释放信号量 | 成功释放后会通知其所属队列集;二值信号量已满时释放失败 |
xSemaphoreTake(sem, 0) |
获取一个信号量令牌 | 先通过队列集选中该信号量,再获取一次;成功返回 pdPASS |
configUSE_QUEUE_SETS |
队列集功能开关 | 在 FreeRTOSConfig.h 中设为 1 |
configSUPPORT_DYNAMIC_ALLOCATION |
动态内存分配功能开关 | 本文的动态创建方式要求设为 1 |
pdMS_TO_TICKS(ms) |
毫秒转换为系统节拍 | 用于填写等待时间,注意节拍分辨率 |
portMAX_DELAY |
最大等待节拍值 | INCLUDE_vTaskSuspend == 1 时,用于上述等待 API 可表示无限期等待 |
INCLUDE_vTaskSuspend |
任务挂起 API 配置开关 | 也影响上述 portMAX_DELAY 是否具有无限期等待语义 |