前言
任务之间怎么传数据?答案是队列。它是 FreeRTOS 最核心的通信机制------信号量、互斥量底层都是队列实现的。
目录
[六、其他常用队列 API](#六、其他常用队列 API)
一、队列是什么
队列是任务间传递数据的桥梁:一个任务往队列里写数据,另一个任务从队列里读数据,双方不用关心对方什么时候处理。
cpp
任务A ──写入──> [队列缓冲区] ──读出──> 任务B
队列的特性:
-
先进先出(FIFO):先写入的先被读出;
-
数据拷贝 :写入时数据被复制进队列,读出时再复制出来------队列里存的是数据的副本;
-
有长度限制:队列满了再写会失败(或阻塞),空了再读也会失败(或阻塞)。
二、传输数据的两种方法
FreeRTOS 队列传输数据有两种方式:
| 方式 | 做法 | 适用 |
|---|---|---|
| 直接传数据 | 把数据本体拷进队列 | 小数据(结构体、数值) |
| 传指针 | 只把数据的地址拷进队列 | 大块数据(数组、字符串、缓冲区) |
⚠️ 传指针要注意:指针指向的内存必须长期有效(全局/静态/堆内存),不能传函数局部变量------函数返回后内存就没了,队列里存的是悬空指针。
三、队列常规操作
使用队列的标准流程:创建 → 写 → 读 → 删除。
创建队列
cpp
QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength, // 队列长度
UBaseType_t uxItemSize); // 每个元素大小(字节)
QueueHandle_t xQueue; // 队列句柄
xQueue = xQueueCreate(5, sizeof(int)); // 长度为 5、元素为 int 的队列
写队列
cpp
BaseType_t xQueueSend(QueueHandle_t xQueue, // 队列句柄
const void *pvItemToQueue, // 要发送的数据指针
TickType_t xTicksToWait); // 阻塞等待时间(0=不等待)
读队列
cpp
BaseType_t xQueueReceive(QueueHandle_t xQueue,
void *pvBuffer, // 接收缓冲区
TickType_t xTicksToWait);
四、队列的阻塞访问
队列操作支持阻塞------这是它最强大的地方:
-
写队列时队列满 → 任务进入阻塞态,等队列有空位(最多等
xTicksToWait时间); -
读队列时队列空 → 任务进入阻塞态,等有数据(最多等
xTicksToWait时间); -
超时还没等到 → 返回
pdFALSE(失败)。
cpp
int num;
// 阻塞等待 1000ms 内的数据,没有就返回 pdFALSE
if (xQueueReceive(xQueue, &num, pdMS_TO_TICKS(1000)) == pdPASS)
{
printf("Received: %d\n", num);
}
阻塞期间任务不占 CPU------调度器会去运行其他就绪任务,这就是 RTOS 高效的来源。
五、示例:队列的基本使用
本示例创建 1 个队列、2 个发送任务、1 个接收任务:发送任务优先级 1,分别往队列写入 100、200;接收任务优先级 2,读队列并打印。
cpp
QueueHandle_t xQueue;
void vSenderTask(void *pvParameters) // 两个发送任务共用
{
int num = *(int *)pvParameters;
while (1)
{
xQueueSend(xQueue, &num, portMAX_DELAY); // 无限等待
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void vReceiverTask(void *pvParameters)
{
int num;
while (1)
{
xQueueReceive(xQueue, &num, portMAX_DELAY);
printf("Received: %d\n", num);
}
}
int main(void)
{
static int num1 = 100, num2 = 200;
xQueue = xQueueCreate(5, sizeof(int));
xTaskCreate(vSenderTask, "sender1", 128, &num1, 1, NULL);
xTaskCreate(vSenderTask, "sender2", 128, &num2, 1, NULL);
xTaskCreate(vReceiverTask, "receiver", 128, NULL, 2, NULL);
vTaskStartScheduler();
}
运行结果:接收任务(优先级 2)先运行,阻塞等数据;发送任务写入 100、200,接收任务依次打印。两个发送任务轮流发送,接收任务按 FIFO 读出。
六、其他常用队列 API
| 函数 | 功能 | 说明 |
|---|---|---|
xQueuePeek() |
偷看 :读出但不移除数据 | 多任务共享读同一数据时用 |
xQueueOverwrite() |
覆盖:直接覆盖队首数据 | 仅限队列长度为 1 时使用,不会阻塞 |
xQueueReset() |
复位队列 | 清空所有数据 |
uxQueueMessagesWaiting() |
查询队列中有多少数据 | 调试用 |
uxQueueSpacesAvailable() |
查询队列剩余空间 | 调试用 |
vQueueDelete() |
删除队列 | 释放内存 |
xQueueSendFromISR() |
在中断里写队列 | 中断中使用,详见后文 |
覆盖与偷看的区别
-
覆盖(xQueueOverwrite) :队列长度必须为 1,队列满时新数据直接覆盖旧数据------不会阻塞,适合"只保留最新值"的场景(如最新的传感器读数);
-
偷看(xQueuePeek) :从队列复制出数据但不移除,数据留给后面的人读。队列空时偷看会阻塞;一旦有数据,以后每次偷看都成功。
七、邮箱(覆盖模式)
队列长度为 1 + 覆盖写入 = 邮箱:只存最新的一份数据,新数据来了直接顶掉旧的。常用于"最新状态通知"(如最新温度、最新配置)。
八、队列集
队列集(Queue Set):把多个队列"打包"成一个集合,一个任务可以同时等待多个队列:
cpp
QueueSetHandle_t xQueueSet = xQueueCreateSet(10); // 创建队列集
xQueueAddToSet(xQueue1, xQueueSet); // 把队列加入集合
xQueueAddToSet(xQueue2, xQueueSet);
xQueueSetMemberHandle_t xMember = xQueueSelectFromSet(xQueueSet, portMAX_DELAY);
// 返回哪个队列有数据,再针对该队列读取
适用场景:一个任务要监听多个数据源(串口、按键、传感器),用队列集就不用开多个任务。
九、总结
-
队列 = 任务间传数据的先进先出缓冲区,数据拷贝进出;
-
阻塞访问是核心:满/空时任务自动阻塞,不浪费 CPU;
-
小数据直接传,大块数据传指针(注意指针生命周期);
-
队列长度 1 + 覆盖 = 邮箱(存最新值);
-
队列集让一个任务同时等多个队列。