队列核心API实战:创建/删除/发送/接收消息

队列这东西,说白了就是RTOS里的消息快递站。你发一个数据包进去,别人从另一头取出来。我刚开始用FreeRTOS那会儿,总觉得队列不就是个数组嘛,有啥好学的?结果第一次做多任务通信,数据乱得一塌糊涂,才老老实实回来啃API。

今天咱们就把队列的五个核心操作讲透:创建、删除、发送、接收,还有阻塞与非阻塞模式。嗯,这些搞明白了,你写多任务代码心里就有底了。

队列的创建与删除

先说说创建。每个RTOS都有自己的队列创建函数,但套路都差不多。拿FreeRTOS举例:

// 创建一个能存放5个uint32_t类型数据的队列
QueueHandle_t xQueue;
xQueue = xQueueCreate(5, sizeof(uint32_t));

if (xQueue == NULL) {
    // 创建失败,内存不够了
    printf("队列创建失败!\n");
}

这里有两个参数:队列长度和每个数据项的大小。我个人习惯把数据项大小写成 sizeof(),而不是直接写数字。为啥?代码移植性好,你换个数据类型不用改两处。

小技巧:队列创建时,RTOS会在内部申请一块连续内存。如果你用动态创建,记得检查返回值是否为NULL。我在项目中遇到过,某次内存池配小了,队列创建静默失败,查了两天才找到原因。

删除队列就简单了:

vQueueDelete(xQueue);
xQueue = NULL;  // 我习惯顺手置空,防止野指针

删除后,队列里还没取走的数据就全丢了。所以删除前,最好确保没有任务还在等这个队列的消息。

发送消息:阻塞与非阻塞

发送消息,核心函数就两个:

// 非阻塞发送
BaseType_t xQueueSend(xQueue, &data, 0);

// 阻塞发送(等待10个tick)
BaseType_t xQueueSend(xQueue, &data, pdMS_TO_TICKS(10));

第三个参数是等待时间。填0就是非阻塞——队列满了?直接返回失败,不等待。填个正数就是阻塞模式——队列满了,任务就挂起,等别人取走数据腾出位置,或者等到超时。

关键区别:
  • 非阻塞:立即返回,适合对实时性要求高的场景
  • 阻塞:等待直到成功或超时,适合必须把数据发出去的场景

我曾经在一个电机控制项目里,用非阻塞发送日志数据。结果队列满了,日志丢了,调试时死活找不到问题。后来改成阻塞发送,虽然偶尔会卡一下任务,但数据一条不丢。你想想看,关键数据丢了,比慢一点更可怕。

接收消息:阻塞与非阻塞

接收的套路和发送对称:

uint32_t receivedData;

// 非阻塞接收
if (xQueueReceive(xQueue, &receivedData, 0) == pdPASS) {
    // 成功收到数据
} else {
    // 队列为空,立即返回
}

// 阻塞接收(一直等到天荒地老)
xQueueReceive(xQueue, &receivedData, portMAX_DELAY);

注意 portMAX_DELAY 这个宏。它表示「无限等待」。我一般只在初始化阶段用这个,因为那时候任务还没开始跑,等多久都行。正常运行的任务,我建议给个超时时间,比如100ms。万一对方任务挂了,你这边也不至于死等。

避坑指南:我曾经在中断服务函数里调用了阻塞接收,结果系统直接死机。记住:中断里只能用非阻塞API!因为中断上下文不能挂起任务。

知识体系总览

下面这张图,把队列的核心操作串起来了。你看一眼就明白:

队列核心API知识体系 队列 (Queue) 创建 xQueueCreate() 删除 vQueueDelete() 发送消息 xQueueSend() 接收消息 xQueueReceive() 非阻塞模式 超时时间 = 0 立即返回,适合中断/高实时场景 阻塞模式 超时时间 > 0 等待直到成功/超时,保证数据不丢 核心原则: 中断中用非阻塞,任务中用阻塞(给超时) 创建检查返回值,删除后置空指针

实战小例子:按键消息处理

光说不练假把式。我写个简单例子,你感受一下:

// 定义队列句柄
QueueHandle_t xKeyQueue;

// 按键中断服务函数
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    uint8_t keyCode = (uint8_t)GPIO_Pin;
    
    // 中断里用FromISR版本,非阻塞
    xQueueSendFromISR(xKeyQueue, &keyCode, &xHigherPriorityTaskWoken);
    
    // 如果唤醒了一个更高优先级的任务,就切换
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

// 按键处理任务
void vKeyTask(void *pvParameters) {
    uint8_t keyValue;
    
    while(1) {
        // 阻塞等待按键消息,超时100ms
        if (xQueueReceive(xKeyQueue, &keyValue, pdMS_TO_TICKS(100)) == pdPASS) {
            // 处理按键
            printf("按键按下: %d\n", keyValue);
        } else {
            // 超时了,可以做点别的事
            // 比如喂狗、检查状态
        }
    }
}

这个模式很常见。中断负责快速收数据,任务负责慢慢处理。队列就像个缓冲池,把两者解耦了。

记住三点:
  1. 中断里只能用 FromISR 后缀的API
  2. 任务里阻塞接收,别用 portMAX_DELAY 死等
  3. 队列大小要合理,太大浪费内存,太小容易丢数据

嗯,队列的核心API就这些。你回去拿开发板试试,先创建个队列,然后两个任务一个发一个收,看看阻塞和非阻塞的区别。实践出真知,光看文档是学不会的。


专注资料整理