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 是否具有无限期等待语义
相关推荐
zhangzhangkeji3 天前
FreeRTos笔记(12c):从事件链表里删除节点 xTaskRemoveFromEventList、
freertos
La Pulga4 天前
【FreeRTOS源码学习笔记】任务延时列表
笔记·mcu·学习·嵌入式·freertos·实时操作系统
橘色的喵4 天前
coact:让嵌入式事件处理更可控
freertos·rt-thread·事件驱动·coact
人邮异步社区5 天前
学习FreeRTOS推荐哪本书?
freertos·rtos·嵌入式固件工程师
La Pulga7 天前
【FreeRTOS源码学习笔记】空闲任务与阻塞延时
笔记·mcu·学习·嵌入式·freertos·实时操作系统
La Pulga8 天前
【FreeRTOS源码学习笔记】任务切换
单片机·mcu·学习·嵌入式·freertos·实时操作系统
La Pulga10 天前
【FreeRTOS源码学习笔记】链表的实现
数据结构·笔记·stm32·mcu·链表·嵌入式·freertos
La Pulga10 天前
【FreeRTOS源码学习笔记】任务创建
笔记·stm32·mcu·学习·嵌入式·freertos·实时操作系统
天吾cc15 天前
FreeRTOS 事件标志组
freertos
编码追梦人16 天前
基于 ESP32-S3 的便携式多参数环境监护仪设计:从硬件选型到 FreeRTOS 多任务落地
数据库·单片机·嵌入式硬件·mongodb·esp32·freertos·低功耗设计