队列管理

[English]

队列是 FreeRTOS 提供的一种先进先出(FIFO)机制,用于在任务之间或任务与中断之间安全地传递数据。 它支持阻塞操作,允许发送方等待空间、接收方等待数据,并提供专用的中断安全函数。 常用于实现任务间通信或生产者-消费者模型。

本文档仅涵盖队列管理中部分常用 API,更多内容请参考 FreeRTOS 官方文档

备注

生产者-消费者模型是一种常见的并发编程模式,其中生产者负责生成数据并放入缓冲区,消费者从缓冲区中取出数据进行处理。 两者通过共享的缓冲区进行解耦,互不直接依赖,适合处理数据采集与处理解耦的场景。

在 ESP-IDF 中调用队列相关 API 时,需要在文件中包含以下头文件:

#include "freertos/queue.h"

动态创建队列

API 原型

QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength,
                            UBaseType_t uxItemSize );

xQueueCreate() 用于创建一个用于任务间通信的队列,分配内存并初始化队列结构,使任务能够安全地发送和接收数据。

参数解释

传入参数

参数功能

参数说明

uxQueueLength

队列一次可存储的最大项目数。

决定队列容量,限制一次能存多少个数据。

uxItemSize

存储队列中每个项目所需的大小(以字节为单位)。

队列以复制方式存储每个项目,因此该参数值是每个入队项目将复制的字节数。队列中的每个项目必须具有相同的大小。

返回值说明

返回值

说明

返回所创建队列的句柄

队列创建成功,返回有效句柄用于后续操作。

NULL

内存分配失败,队列创建失败。

示例 1:动态创建队列

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"

// 定义 QueueHandle_t 类型变量,用于存储队列创建成功后返回的句柄
QueueHandle_t queue = NULL;
#define QUEUE_LENGTH 5  // 定义队列长度
#define ITEM_SIZE    4   // 定义项目大小(4 字节每项)

void app_main(void)
{
    // 创建队列
    queue = xQueueCreate(QUEUE_LENGTH, ITEM_SIZE);

    if (queue == NULL) {
        printf("Create FALSE\n");
    }
    else {
        printf("Create SUCCESS\n");
    }
}

以上代码执行结果如下:

Create SUCCESS

该示例展示了使用 xQueueCreate() 创建一个长度为 5、每项大小为 4 字节的队列,通过判断返回句柄是否为 NULL 来确认队列是否成功创建。

调用该 API 时需要注意:

  • 合理设置队列长度和元素大小,注意所有元素大小必须一致且通过复制存储。

  • 必须检查返回值防止内存不足导致创建失败,建议在系统初始化阶段创建队列以确保任务运行时队列已准备好,同时保证系统有足够堆空间。

  • 队列本身线程安全,可安全用于任务间通信。

删除队列

API 原型

void vQueueDelete( QueueHandle_t xQueue );

vQueueDelete() 用于删除一个已经创建的队列,释放该队列占用的内存资源。调用后,该队列句柄将失效,不能再使用。

备注

该 API 仅适用于动态创建的队列。

参数解释

传入参数

参数功能

参数说明

xQueue

需要删除的队列的句柄

例如由 xQueueCreate 返回的有效句柄。

删除队列后,队列句柄本身的值不会自动变成 NULL 或其他特殊值,它仍然保持原来的数值。 但该句柄已失效,不能再用于任何队列操作,否则会导致未定义行为。

因此,调用 vQueueDelete() 后,建议手动将句柄置为 NULL,防止误用。

发送数据

API 原型

BaseType_t xQueueSend( QueueHandle_t xQueue,
                       const void * pvItemToQueue,
                       TickType_t xTicksToWait );

xQueueSend() 用于向队列末尾发送数据,如果队列已满,可以选择阻塞等待直到有空间或超时,确保数据安全传递给接收方。

参数解释

传入参数

参数功能

参数说明

xQueue

数据传输的目标队列的句柄

例如由 xQueueCreate 返回的有效句柄,用于指定数据发送的队列。

pvItemToQueue

指向待发送数据的指针

数据将直接复制到已创建的队列中,确保数据大小不超过队列元素大小。

xTicksToWait

最长等待时间

当队列已满时,调用者阻塞等待的最大时间,以滴答周期(tick)为单位定义;为 0 时立即返回。

备注

当任务尝试操作队列(如发送或接收数据)时,如果条件不满足(队列满或空),任务会暂停执行,进入等待状态,直到满足条件或超时。

在队列中,这种机制保证任务不会忙等待或浪费 CPU 资源,而是有效等待队列变为可用,提升系统效率和响应能力。

返回值说明

返回值

说明

pdTRUE

发送成功,数据已加入队列。

errQUEUE_FULL

发送失败,队列已满且未等待或等待超时。

示例 2:发送数据至队列

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"

// 定义 QueueHandle_t 类型变量,用于存储队列创建成功后返回的句柄
QueueHandle_t queue = NULL;
#define QUEUE_LENGTH 5  // 定义队列长度
#define ITEM_SIZE    4   // 定义项目大小(4 字节每项)

void app_main(void)
{
    // 创建队列
    queue = xQueueCreate(QUEUE_LENGTH, ITEM_SIZE);

    int num = 1;    // 待发送数据
    BaseType_t xReturn; // 用于接收返回值

    if (queue == NULL) {
        printf("Create FALSE\n");
    }
    else {
        printf("Create SUCCESS\n");
    }

    while (1) {
        xReturn = xQueueSend(queue, (void *)&num, 10);
        // 通过返回值判断数据是否发送成功
        if (xReturn == pdTRUE) {
            printf("%d: Item Send SUCCESS\n", num);
        }
        else {
            printf("Item Send FALSE\n");
        }
        num++;
    }
}

以上代码执行结果如下:

Create SUCCESS
1: Item Send SUCCESS
2: Item Send SUCCESS
3: Item Send SUCCESS
4: Item Send SUCCESS
5: Item Send SUCCESS
Item Send FALSE
Item Send FALSE

该示例主要展示了 xQueueSend() 的基本用法。

通过创建固定长度的队列并持续发送整数,演示如何使用该函数将数据写入队列,并通过返回值判断发送是否成功。

由于示例中未设置任务接收数据,队列空间不会释放,随着连续发送,队列逐渐填满。 当队列满后,再调用 xQueueSend() 会阻塞等待最多 10 个 tick(由参数指定)。 若超时仍无空位,函数返回 errQUEUE_FULL,表示发送失败。

此外,传入的第二个参数必须是数据地址,通常需强制转换为 void *。 队列通过复制数据副本实现传输,而非传递指针,保证了数据安全和一致性。

接收数据

API 原型

BaseType_t xQueueReceive( QueueHandle_t xQueue,
                          void * const pvBuffer,
                          TickType_t xTicksToWait );

xQueueReceive() 用于从队列头部接收数据,如果队列为空,可以选择阻塞等待直到有数据或超时,确保安全获取队列中的数据。

参数解释

传入参数

参数功能

参数说明

xQueue

接收数据的目标队列的句柄

例如由 xQueueCreate 返回的有效句柄,用于指定接收数据的队列。

pvBuffer

指向用于存储接收数据的缓冲区的指针

数据会复制到该缓冲区,确保该缓冲区大小不小于队列元素大小。

xTicksToWait

最长等待时间

当队列为空时,调用者阻塞等待的最大时间,以滴答周期(tick)为单位定义;为 0 时立即返回。

返回值说明

返回值

说明

pdTRUE

接收成功,数据已复制到指定的缓存区。

pdFALSE

接收失败,队列为空且未等待或等待超时。

示例 3:发送接收数据

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"

// 定义 QueueHandle_t 类型变量,用于存储队列创建成功后返回的句柄
QueueHandle_t queue = NULL;
#define QUEUE_LENGTH 5  // 定义队列长度
#define ITEM_SIZE    4   // 定义项目大小(4 字节每项)

static void vSendTask( void *pvParameters )
{
    int num = 1;    // 待发送数据
    BaseType_t xReturn; // 用于接收返回值

    while (1) {
        xReturn = xQueueSend(queue, (void *)&num, 10);
        // 通过返回值判断数据是否发送成功
        if (xReturn == pdTRUE) {
            printf("Item Send: %d \n", num);
            num++;
        }
        else {
            printf("Item Send FALSE\n");
        }
        vTaskDelay(1);
    }
}

static void vReceiveTask( void *pvParameters )
{
    int receiver = 0;   // 用于接收数据
    BaseType_t xReturn; // 用于接收返回值

    while (1) {
        xReturn = xQueueReceive(queue, (void *)&receiver, 10);

        // 通过返回值判断数据是否接收成功
        if (xReturn == pdTRUE) {
            printf("Item Receive: %d \n", receiver);
        }
        else {
            printf("Item Receive FALSE\n");
        }
        vTaskDelay(1);
    }
}

void app_main(void)
{
    // 创建队列
    queue = xQueueCreate(QUEUE_LENGTH, ITEM_SIZE);

    if (queue != NULL) {
        xTaskCreatePinnedToCore(vSendTask, "SendTask", 20480, NULL, 2, NULL, 0);
        xTaskCreatePinnedToCore(vReceiveTask, "ReceiveTask", 4096, NULL, 2, NULL, 0);
    }
}

以上代码执行结果如下:

Item Send: 1
Item Receive: 1
Item Send: 2
Item Receive: 2
Item Send: 3
Item Receive: 3
Item Send: 4
Item Receive: 4
Item Send: 5
Item Receive: 5
Item Send: 6
Item Receive: 6

该示例展示了使用 xQueueSend()xQueueReceive() 在两个任务之间进行数据传输的基本过程。

发送任务每次将整数写入队列,接收任务从队列中读取并打印数据,两个任务以相同频率运行,并通过返回值判断操作是否成功。

从执行结果可以看出数据被成功写入并接收,说明队列创建和基本通信功能正常。 由于两个任务运行频率一致、调度节奏同步且优先级相同,队列始终保持在可用状态,未出现满队列或空队列的情况,因此没有发生发送失败或接收失败。

在 ESP-IDF 环境中,若两个任务涉及同一个队列进行数据读写,建议将它们绑定在同一个 CPU 核上,以减少跨核调度带来的不确定性和性能损耗,确保通信行为更稳定可靠。

写入数据

API 原型

BaseType_t xQueueOverwrite( QueueHandle_t xQueue,
                            const void * pvItemToQueue );

xQueueOverwrite() 用于将数据写入队列中,如果队列已满,会覆盖掉旧数据,仅适用于队列长度为 1 的情况,常用于始终保持最新数据的场景。

备注

在队列长度大于 1 的情况下,不应使用 xQueueOverwrite(),应使用标准的 xQueueSend()。 避免直接覆盖头部数据,跳过正常的先进先出规则,导致破坏队列结构。 这种做法可能导致数据一致性问题,尤其是在多个任务同时访问同一队列时,容易引发逻辑错误或数据混乱。

参数解释

传入参数

参数功能

参数说明

xQueue

数据写入的目标队列的句柄

例如由 xQueueCreate 返回的有效句柄,用于指定数据写入的队列。

pvItemToQueue

指向待写入数据的指针

数据将直接复制到已创建的队列中,确保数据大小不超过队列元素大小。

返回值说明

返回值

说明

pdPASS

写入成功,数据已加入队列。

示例 4:最新数据覆盖

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"

// 定义 QueueHandle_t 类型变量,用于存储队列创建成功后返回的句柄
QueueHandle_t queue = NULL;
#define QUEUE_LENGTH 1  // 定义队列长度
#define ITEM_SIZE    4   // 定义项目大小(4 字节每项)

static void vSendTask( void *pvParameters )
{
    int num = 1;    // 待发送数据
    BaseType_t xReturn; // 用于接收返回值

    while (1) {
        xReturn = xQueueOverwrite(queue, (void *)&num);
        // 通过返回值判断数据是否发送成功
        if (xReturn == pdTRUE) {
            printf("Item Send: %d \n", num);
            num++;
        }
        vTaskDelay(1);
    }
}

static void vReceiveTask( void *pvParameters )
{
    int receiver = 0;   // 用于接收数据
    BaseType_t xReturn; // 用于接收返回值

    while (1) {
        xReturn = xQueueReceive(queue, (void *)&receiver, 10);

        // 通过返回值判断数据是否接收成功
        if (xReturn == pdTRUE) {
            printf("Item Receive: %d \n", receiver);
        }
        else {
            printf("Item Receive FALSE\n");
        }
        vTaskDelay(5);
    }
}

void app_main(void)
{
    // 创建队列
    queue = xQueueCreate(QUEUE_LENGTH, ITEM_SIZE);

    if (queue != NULL) {
        xTaskCreatePinnedToCore(vSendTask, "SendTask", 20480, NULL, 2, NULL, 0);
        xTaskCreatePinnedToCore(vReceiveTask, "ReceiveTask", 4096, NULL, 2, NULL, 0);
    }
}

以上代码执行结果如下:

Item Send: 1
Item Receive: 1
Item Send: 2
Item Send: 3
Item Send: 4
Item Send: 5
Item Receive: 5
Item Send: 6
Item Send: 7
Item Send: 8
Item Receive: 8

该示例展示了在队列长度为 1 的情况下,使用 xQueueOverwrite() 实现数据发送的行为特性。

发送任务持续将整数写入队列,接收任务周期性从队列中读取数据。 由于 xQueueOverwrite() 会在队列已满时覆盖旧数据,因此发送任务在未等待接收的情况下可以连续写入,新的数据总会替换旧数据保留在队列中。

接收任务每 5 个 tick 读取一次队列,因此只能读取到队列中最后一次被覆盖后仍未被再次覆盖的数据。

从执行结果可见,接收任务仅成功接收到部分数据,中间的大多数值由于在接收前已被覆盖,未能被获取。

这清晰体现了 xQueueOverwrite() 的覆盖特性,适用于仅关心最新数据的场景,例如传感器读数或状态刷新。

查看数据

API 原型

BaseType_t xQueuePeek( QueueHandle_t xQueue,
                       void * const pvBuffer,
                       TickType_t xTicksToWait );

xQueuePeek() 用于查看队列头部的数据,但不会将数据从队列中移除,适合在不改变队列内容的情况下读取当前队首元素。

参数解释

传入参数

参数功能

参数说明

xQueue

需要查看的队列的句柄

例如由 xQueueCreate 返回的有效句柄。

pvBuffer

指向用于存储接收数据的缓冲区的指针

数据会复制到该缓冲区,确保该缓冲区大小不小于队列元素大小。

xTicksToWait

最长等待时间

当队列为空时,调用者阻塞等待的最大时间,以滴答周期(tick)为单位定义;为 0 时立即返回。

返回值说明

返回值

说明

pdTRUE

接收成功,数据已复制到指定的缓存区。

pdFALSE

接收失败,队列为空且未等待或等待超时。

示例 5:查看数据

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"

// 定义 QueueHandle_t 类型变量,用于存储队列创建成功后返回的句柄
QueueHandle_t queue = NULL;
#define QUEUE_LENGTH 5  // 定义队列长度
#define ITEM_SIZE    4   // 定义项目大小(4 字节每项)

static void vSendTask( void *pvParameters )
{
    int num = 1;    // 待发送数据
    BaseType_t xReturn; // 用于接收返回值

    while (1) {
        xReturn = xQueueSend(queue, (void *)&num, 10);
        // 通过返回值判断数据是否发送成功
        if (xReturn == pdTRUE) {
            printf("Item Send: %d \n", num);
            num++;
        }
        vTaskDelay(1);
    }
}

static void vReceiveTask( void *pvParameters )
{
    int receiver = 0;   // 用于接收数据
    BaseType_t xReturn; // 用于接收返回值

    while (1) {
        xReturn = xQueuePeek(queue, (void *)&receiver, 10);

        // 通过返回值判断数据是否接收成功
        if (xReturn == pdTRUE) {
            printf("Item Peek: %d \n", receiver);
        }
        else {
            printf("Item Peek FALSE\n");
        }
        vTaskDelay(1);
    }
}

void app_main(void)
{
    // 创建队列
    queue = xQueueCreate(QUEUE_LENGTH, ITEM_SIZE);

    if (queue != NULL) {
        xTaskCreatePinnedToCore(vSendTask, "SendTask", 20480, NULL, 2, NULL, 0);
        xTaskCreatePinnedToCore(vReceiveTask, "ReceiveTask", 4096, NULL, 2, NULL, 0);
    }
}

以上代码执行结果如下:

Item Send: 1
Item Peek: 1
Item Send: 2
Item Peek: 1
Item Send: 3
Item Peek: 1
Item Send: 4
Item Peek: 1
Item Send: 5

该示例演示了使用 xQueuePeek() 查看队列头部数据的功能。

发送任务持续向队列写入递增的整数,而接收任务使用 xQueuePeek() 读取队列首个元素,但不将其移除。

执行结果显示,尽管发送任务不断写入新数据,接收任务读取的始终是队列中的第一个数据项(即第一次写入的值),说明 xQueuePeek() 仅用于查看数据而不会改变队列内容,适合在不影响队列状态的情况下获取当前队首信息。