FreeRTOS 队列集

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 是否具有无限期等待语义
相关推荐
硅基手札3 小时前
操作系统设计思想系列 06 · FreeRTOS 深度 — 极简可移植范本
架构·操作系统·freertos·实时操作系统·嵌入式开发·设计思想
天吾cc5 天前
FreeRTOS 列表和列表项
freertos
qq_401700415 天前
RTOS址与值 void*任务传参的坑 走void*万能转的天坑
freertos
天吾cc6 天前
FreeRTOS任务创建和删除
freertos
天吾cc6 天前
FreeRTOS中断管理
freertos
天吾cc6 天前
FreeRTOS任务挂起和恢复
freertos
中文英文-我选中文10 天前
8.2 学习探索
freertos
qq_4017004115 天前
FreeRTOS 堆内存管理源码剖析:释放一块内存,空闲块数量反而变少了?
单片机·freertos
List<String> error_P17 天前
使用FreeRTOS搭建工程项目(一)
freertos