队列(Queue)是 FreeRTOS 中最基础的任务间通信机制。信号量、互斥锁等机制在底层都是基于队列实现的。
按复制 vs 按引用
FreeRTOS 的队列采用**按复制(copy by value)**方式:数据逐字节复制到队列内部存储,而不是只存指针。
这比按引用排队更安全、更易用:
| 优势 | 说明 |
|---|---|
| 栈变量可以直接发送 | 即使发送函数返回,栈变量已销毁,队列里的数据不受影响 |
| 发送后立即可重用变量 | 数据已经被复制走了,发送方可以立刻修改原变量 |
| 发送方和接收方完全解耦 | 不用关心谁”拥有”数据、谁负责释放内存 |
| 支持内存保护 | 跨内存保护边界也能安全传递 |
| 大数据可以传指针 | 复制排队不妨碍你传指针——把指针本身复制进队列即可 |
传指针的注意事项
- 被指向的内存生命周期必须覆盖队列传递的整个过程(不能是栈上局部变量)
- 必须明确谁负责释放这块内存
- 确保发送方和接收方不会同时修改指向的内容
创建队列
QueueHandle_t xQueueCreate(
UBaseType_t uxQueueLength, // 队列能容纳的最大项数
UBaseType_t uxItemSize // 每项的大小(字节)
);
返回值:非 NULL 为队列句柄;NULL 表示堆内存不足创建失败。
// 静态版本:用户提供内存
QueueHandle_t xQueueCreateStatic(
UBaseType_t uxQueueLength,
UBaseType_t uxItemSize,
uint8_t *pucQueueStorage, // 数据存储区
StaticQueue_t *pxQueueBuffer // 队列结构体内存
);
每个队列需要两块 RAM:
- 队列控制块(Queue_t 结构体)
- 数据存储区(大小 =
uxQueueLength * uxItemSize字节)
xQueueCreate() 从堆上动态分配这两块内存。
发送数据
// 发送到队尾(FIFO,最常用)
BaseType_t xQueueSendToBack(
QueueHandle_t xQueue,
const void *pvItemToQueue, // 指向要发送的数据
TickType_t xTicksToWait // 队列满时等待多久
);
// 发送到队头(LIFO,"插队",用于紧急消息)
BaseType_t xQueueSendToFront(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
// xQueueSend() 等价于 xQueueSendToBack()
参数 xTicksToWait:
0:不等待,队列满则立即返回errQUEUE_FULLpdMS_TO_TICKS(100):最多等 100msportMAX_DELAY:死等(需要INCLUDE_vTaskSuspend = 1)
返回值:pdPASS 发送成功;errQUEUE_FULL 队列已满。
接收数据
BaseType_t xQueueReceive(
QueueHandle_t xQueue,
void *pvBuffer, // 接收缓冲区
TickType_t xTicksToWait // 队列空时等待多久
);
接收后数据从队列中移除。如果只想查看不移除,用 xQueuePeek()。
读写阻塞行为
队列可以有多个发送者和多个接收者。
多任务阻塞等待
当多个任务阻塞在同一个队列上等数据时,数据到达后唤醒的顺序:
- 优先级最高的等待任务先被唤醒
- 优先级相同则等待时间最长的先唤醒(FIFO)
Task A(优先级3)、Task B(优先级2)、Task C(优先级2) 都在等队列数据
│
数据到达!
│
Task A 先被唤醒(优先级最高)
Task A 处理完后,下一个数据到达 → Task B 被唤醒(比 C 等得久)
多任务阻塞写(队列满时)
同理,队列满时多个任务阻塞等待发送空间,有空间后按优先级+等待时间唤醒。
查询队列状态
// 查询队列中当前的消息数量
UBaseType_t uxQueueMessagesWaiting(QueueHandle_t xQueue);
// 查询队列剩余空间
UBaseType_t uxQueueSpacesAvailable(QueueHandle_t xQueue);
// 重置队列为空状态
BaseType_t xQueueReset(QueueHandle_t xQueue);
邮箱模式(长度为1的队列)
当队列长度设为 1 时,它就变成了”邮箱”:
| 特性 | 普通队列 | 邮箱(长度=1) |
|---|---|---|
| 长度 | 任意 N | 固定为 1 |
| 读取方式 | 出队(数据消失) | Peek(数据保留)/ 覆盖写入 |
| 写入方式 | 入队到队尾/队头 | Overwrite(覆盖旧值) |
| 数据所有权 | 单次消费 | 广播,所有人可读 |
| 典型用途 | 任务间传递消息 | 共享最新状态值 |
// 覆盖写入(不管邮箱是否有数据,直接覆盖)
xQueueOverwrite(xMailbox, &newValue);
// 读取但不删除(可以反复读同一个值)
xQueuePeek(xMailbox, &value, 0);
典型场景:温度传感器周期性更新当前温度值,多个显示/控制任务随时读取最新温度,不需要消费掉数据。
多源消息的区分
当多个任务/中断向同一个队列发消息时,接收方需要知道消息来源。用结构体:
typedef struct {
uint8_t source; // 消息来源标识
uint8_t cmd; // 命令码
uint32_t data; // 数据
} Message_t;
// 发送
Message_t msg = {.source = SENSOR_A, .cmd = CMD_DATA, .data = 42};
xQueueSend(xQueue, &msg, 0);
// 接收后判断
Message_t rx;
xQueueReceive(xQueue, &rx, portMAX_DELAY);
switch(rx.source) {
case SENSOR_A: /* 处理传感器A */ break;
case BUTTON_ISR: /* 处理按键中断 */ break;
}
队列集(Queue Set)
队列集允许一个任务同时阻塞等待多个队列中的任意一个有数据。
QueueSetHandle_t xQueueSet = xQueueCreateSet(10); // 队列集能容纳的事件数
xQueueAddToSet(xQueue1, xQueueSet);
xQueueAddToSet(xQueue2, xQueueSet);
// 等待任意队列有数据
QueueSetMemberHandle_t xActivated = xQueueSelectFromSet(xQueueSet, portMAX_DELAY);
if(xActivated == xQueue1) {
xQueueReceive(xQueue1, &data, 0);
} else if(xActivated == xQueue2) {
xQueueReceive(xQueue2, &data, 0);
}
不推荐使用队列集
官方手册建议优先考虑用一个队列传不同类型的结构体消息,比队列集更高效。队列集仅在无法修改发送端代码(如第三方库)时使用。
典型使用模式
任务A生产 → 任务B消费(单播):
传感器采集 → [q1, q2, q3...] → 数据处理任务(每条处理一次)
多任务广播状态(邮箱):
温度传感器 → [25.3°C] → 任务A读、任务B读、任务C读(都读同一个值,不删除)
紧急消息插队:
紧急停机指令 → xQueueSendToFront() → 跳过排队普通数据,立即处理
下一步:04-中断处理