队列(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:

  1. 队列控制块(Queue_t 结构体)
  2. 数据存储区(大小 = 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_FULL
  • pdMS_TO_TICKS(100):最多等 100ms
  • portMAX_DELAY:死等(需要 INCLUDE_vTaskSuspend = 1

返回值:pdPASS 发送成功;errQUEUE_FULL 队列已满。

接收数据

BaseType_t xQueueReceive(
    QueueHandle_t xQueue,
    void *pvBuffer,       // 接收缓冲区
    TickType_t xTicksToWait  // 队列空时等待多久
);

接收后数据从队列中移除。如果只想查看不移除,用 xQueuePeek()

读写阻塞行为

队列可以有多个发送者多个接收者

多任务阻塞等待

当多个任务阻塞在同一个队列上等数据时,数据到达后唤醒的顺序:

  1. 优先级最高的等待任务先被唤醒
  2. 优先级相同则等待时间最长的先唤醒(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-中断处理

相关:06-资源管理08-任务通知返回目录