07-消息队列与队列集.md 6.3 KB


tags: [source-summary] type: source source: "尚硅谷嵌入式技术之FreeRTOS实时操作系统 V1.0.3 — 第10、14章 + 配套代码09/14" author: "尚硅谷研究院" date: 2026-07-12 url: ""

created: 2026-07-12

消息队列与队列集

用生活理解:队列就像公司内部的"传话器"——task1 把消息放进队列,task2 从队列取出消息。就像两个人用一个信箱通信:一个人往里塞信,一个人取信看。队列集就像你把多个信箱挂在一起,派一个人守着,哪个信箱有信来了他就取。

队列基础

队列 = 先进先出(FIFO)的消息缓冲区,线程安全。

创建队列

QueueHandle_t queue1;
// 参数1:队列长度(能存几个消息)
// 参数2:每个消息的大小(单位字节)
queue1 = xQueueCreate(2, sizeof(uint8_t));
// 创建一个能存2个消息的队列,每个消息1字节
// 就像做了一个只能放2封信的信箱,每封信只能写1个字

发送消息(入队)

uint8_t key = KEY1_PRESS;
xQueueSend(queue1, &key, portMAX_DELAY);
// 第3个参数 portMAX_DELAY = "队列满了就死等"
// 也可以设 0 = "满了就放弃" 或者 100 = "等100个tick"

阻塞 / 非阻塞:就像排队吃饭——队列满了你可以等(设超时),也可以直接走人(超时=0 不等待)。接收同理:队列空了可以等数据来,也可以直接放弃。

接收消息(出队)

uint8_t receive;
xQueueReceive(queue1, &receive, portMAX_DELAY);
// 同样 portMAX_DELAY = "队列空了就死等"

Receive vs PeekxQueueReceive() = 剪切(从队列中删除数据),xQueuePeek() = 复制(数据保留在队列中,只看不取)。就像看信箱——Receive 是取走信,Peek 是只看看不拿。

Send to frontxQueueSendToFront() = 插队(把消息放到队首),xQueueSendToBack() = 正常排队(默认)。

小数据 vs 大数据

// ======== 小数据:直接传值(比如按键值) ========
uint8_t key = KEY1_PRESS;
xQueueSend(queue1, &key, portMAX_DELAY);
// 把key的值复制一份放进队列,原始key怎么变都不影响

// ======== 大数据:传地址(比如字符串) ========
char *big_data = "这是一段很长的数据...";
xQueueSend(big_queue, &big_data, portMAX_DELAY);
// 不复制整个字符串,只复制地址(4字节),省内存!
// 接收方通过地址直接访问字符串

为什么要传地址? 如果数据有 1KB,每次复制一份就太浪费内存了。传地址就像把文件路径告诉对方,而不是把整个文件复印一份送过去。


示例 1:消息队列实验(项目 09)

情景

  • task1:按键扫描,KEY1/KEY2 → 键值入队;KEY3 → 大数据地址入队
  • task2:从队列接收小数据(键值)
  • task3:从队列接收大数据地址,访问数据

核心代码

#include "queue.h"  // 队列相关API的头文件

QueueHandle_t queue1;     // 小数据队列
QueueHandle_t big_queue;  // 大数据队列

void freertos_start(void)
{
    // 创建小数据队列:长度2,每项1字节
    queue1 = xQueueCreate(2, sizeof(uint8_t));
    // 创建大数据队列:长度1,每项4字节(指针大小)
    big_queue = xQueueCreate(1, sizeof(char *));

    xTaskCreate(/* ...start_task... */);
    vTaskStartScheduler();
}

/* ========== task1:发送消息 ========== */
void task1(void *pvParameters)
{
    char *big_data = "这里是需要传递的大数据内容...";
    uint8_t key = 0;
    while (1)
    {
        key = Key_Detect();
        if (key == KEY1_PRESS || key == KEY2_PRESS)
        {
            // 发送键值到小数据队列
            xQueueSend(queue1, &key, portMAX_DELAY);
        }
        else if (key == KEY3_PRESS)
        {
            // 发送大数据地址
            xQueueSend(big_queue, &big_data, portMAX_DELAY);
        }
        vTaskDelay(500);
    }
}

/* ========== task2:接收小数据 ========== */
void task2(void *pvParameters)
{
    uint8_t receive;
    while (1)
    {
        xQueueReceive(queue1, &receive, portMAX_DELAY);
        printf("收到键值: %d\r\n", receive);
    }
}

/* ========== task3:接收大数据 ========== */
void task3(void *pvParameters)
{
    char *receive_buff;
    while (1)
    {
        xQueueReceive(big_queue, &receive_buff, portMAX_DELAY);
        printf("收到大数据: %s\r\n", receive_buff);
    }
}

示例 2:队列集(项目 14)

为什么需要队列集?

假设你管理两个信箱(队列)和一个通知铃(信号量)。不用队列集时,你必须轮询检查每个信箱——"1号有信吗?2号有信吗?铃响了吗?"很麻烦。

痛点导向:当需要从多个队列读取数据时,如果先阻塞在队列 A 的读取上,但队列 B 先到了数据,就会卡住——永远等不到 B 的数据。队列集让任务一次性阻塞在所有队列上,哪个先到就处理哪个。

队列集 = 统一管理的"消息中心"。你把队列和信号量都注册进去,然后只需要等一个函数返回,它就告诉你"是哪个有消息了"。

核心代码

// ======== 创建队列集并注册 ========
QueueSetHandle_t queue_set_handle;

// 创建一个能容纳2个成员的队列集
queue_set_handle = xQueueCreateSet(2);

// 把队列和信号量注册到队列集
xQueueAddToSet(queue1, queue_set_handle);    // 注册队列
xQueueAddToSet(sem_handle, queue_set_handle); // 注册信号量

// ======== 接收方:哪个有消息就处理哪个 ========
void task2(void *pvParameters)
{
    QueueSetMemberHandle_t member_handle;

    while (1)
    {
        // 阻塞等待:队列集中任何一个成员有消息
        member_handle = xQueueSelectFromSet(
            queue_set_handle, portMAX_DELAY);

        if (member_handle == queue1)
        {
            // 是队列有消息了
            uint8_t val;
            xQueueReceive(queue1, &val, 0);
            printf("队列收到: %d\r\n", val);
        }
        else if (member_handle == sem_handle)
        {
            // 是信号量被释放了
            xSemaphoreTake(sem_handle, 0);
            printf("信号量收到!\r\n");
        }
    }
}

配套代码

  • code/09_消息队列/
  • code/14_队列集/

后续章节:[[08-信号量与互斥信号量]]