Queue Management

[中文]

A queue is a First In First Out (FIFO) mechanism provided by FreeRTOS for safely passing data between tasks or between tasks and interrupts. It supports blocking operations, allowing the sender to wait for space, the receiver to wait for data, and provides dedicated interrupt-safe functions. It is commonly used to implement inter-task communication or the producer-consumer model.

This document only covers some commonly used APIs in queue management, for more information, please refer to the FreeRTOS official documentation.

Note

The producer-consumer model is a common concurrent programming pattern, where the producer is responsible for generating data and putting it into the buffer, and the consumer takes data from the buffer for processing. The two are decoupled through a shared buffer, do not directly depend on each other, and are suitable for handling data collection and processing decoupling scenarios.

When calling queue-related APIs in ESP-IDF, you need to include the following header file in the file:

#include "freertos/queue.h"

Dynamically Create Queue

API Prototype

QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength,
                            UBaseType_t uxItemSize );

xQueueCreate() is used to create a queue for inter-task communication, allocate memory and initialize the queue structure, enabling tasks to safely send and receive data.

Parameter Explanation

Input Parameter

Parameter Function

Parameter Description

uxQueueLength

The maximum number of items that the queue can store at one time.

Determines the queue capacity, limiting how many data can be stored at once.

uxItemSize

The size (in bytes) required to store each item in the queue.

The queue stores each item by copying, so this parameter value is the number of bytes that each enqueued item will copy. Each item in the queue must have the same size.

Return Value Description

Return Value

Description

Returns the handle of the created queue

The queue is successfully created, returning a valid handle for subsequent operations.

NULL

Memory allocation failed, queue creation failed.

Example 1: Dynamically Create Queue

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

// Define a QueueHandle_t type variable to store the handle returned after the queue is successfully created
QueueHandle_t queue = NULL;
#define QUEUE_LENGTH 5  // Define queue length
#define ITEM_SIZE    4   // Define item size (4 bytes per item)

void app_main(void)
{
    // Create queue
    queue = xQueueCreate(QUEUE_LENGTH, ITEM_SIZE);

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

The above code execution result is as follows:

Create SUCCESS

This example demonstrates the use of xQueueCreate() to create a queue with a length of 5 and a size of 4 bytes per item, by judging whether the returned handle is NULL to confirm whether the queue is successfully created.

When calling this API, please note:

  • Set the queue length and element size reasonably, and note that all element sizes must be consistent and stored by copying.

  • Must check the return value to prevent failure to create due to insufficient memory. It is recommended to create the queue during system initialization to ensure that the queue is ready when the task is running, and to ensure that the system has enough heap space.

  • The queue itself is thread-safe and can be safely used for inter-task communication.

Delete Queue

API Prototype

void vQueueDelete( QueueHandle_t xQueue );

vQueueDelete() is used to delete a created queue and release the memory resources occupied by the queue. After the call, the queue handle will become invalid and cannot be used again.

Note

This API is only applicable to dynamically created queues.

Parameter Explanation

Input Parameter

Parameter Function

Parameter Description

xQueue

Handle of the queue to be deleted

For example, a valid handle returned by xQueueCreate.

After deleting the queue, the value of the queue handle itself will not automatically become NULL or other special values, it still maintains the original numerical value. But this handle has become invalid and cannot be used for any queue operations, otherwise it will cause undefined behavior.

Therefore, after calling vQueueDelete(), it is recommended to manually set the handle to NULL to prevent misuse.

Send Data

API Prototype

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

xQueueSend() is used to send data to the end of the queue. If the queue is full, you can choose to block and wait until there is space or timeout to ensure the safe delivery of data to the receiver.

Parameter Explanation

Input Parameter

Parameter Function

Parameter Description

xQueue

Handle of the target queue for data transmission

For example, a valid handle returned by xQueueCreate, used to specify the queue for data sending.

pvItemToQueue

Pointer to the data to be sent

The data will be directly copied into the created queue, ensuring that the data size does not exceed the queue element size.

xTicksToWait

Maximum waiting time

When the queue is full, the maximum time that the caller blocks and waits, defined in tick cycles; returns immediately when it is 0.

Note

When a task tries to operate on a queue (such as sending or receiving data), if the conditions are not met (the queue is full or empty), the task will pause execution and enter a waiting state until the conditions are met or timeout.

In the queue, this mechanism ensures that tasks do not busy wait or waste CPU resources, but effectively wait for the queue to become available, improving system efficiency and responsiveness.

Return Value Description

Return Value

Description

pdTRUE

Send successful, data has been added to the queue.

errQUEUE_FULL

Send failed, the queue is full and did not wait or wait timeout.

Example 2: Send data to the queue

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

// Define a QueueHandle_t type variable to store the handle returned after successful queue creation
QueueHandle_t queue = NULL;
#define QUEUE_LENGTH 5  // Define queue length
#define ITEM_SIZE    4   // Define item size (4 bytes per item)

void app_main(void)
{
    // Create a queue
    queue = xQueueCreate(QUEUE_LENGTH, ITEM_SIZE);

    int num = 1;    // Data to be sent
    BaseType_t xReturn; // Used to receive the return value

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

    while (1) {
        xReturn = xQueueSend(queue, (void *)&num, 10);
        // Determine whether the data is sent successfully through the return value
        if (xReturn == pdTRUE) {
            printf("%d: Item Send SUCCESS\n", num);
        }
        else {
            printf("Item Send FALSE\n");
        }
        num++;
    }
}

The execution result of the above code is as follows:

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

This example mainly demonstrates the basic usage of xQueueSend().

By creating a fixed-length queue and continuously sending integers, it demonstrates how to use this function to write data into the queue and determine whether the sending is successful through the return value.

Since no task is set to receive data in the example, the queue space will not be released, and the queue will gradually fill up with continuous sending. When the queue is full, calling xQueueSend() again will block and wait for up to 10 ticks (specified by the parameter). If there is still no space after timeout, the function returns errQUEUE_FULL, indicating that the sending failed.

In addition, the second parameter passed in must be the data address, usually forced to be converted to void *. The queue implements transmission by copying data copies, not passing pointers, ensuring data safety and consistency.

Receiving data

API prototype

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

xQueueReceive() is used to receive data from the head of the queue. If the queue is empty, you can choose to block and wait until there is data or timeout, ensuring the safe acquisition of data in the queue.

Parameter explanation

Input parameter

Parameter function

Parameter description

xQueue

The handle of the target queue for receiving data

For example, a valid handle returned by xQueueCreate is used to specify the queue for receiving data.

pvBuffer

Pointer to the buffer used to store the received data

The data will be copied to this buffer, ensuring that the size of the buffer is not less than the size of the queue element.

xTicksToWait

Maximum waiting time

The maximum time the caller blocks and waits when the queue is empty, defined in tick cycles; returns immediately when it is 0.

Return Value Description

Return Value

Description

pdTRUE

Reception successful, data has been copied to the specified buffer.

pdFALSE

Reception failed, the queue is empty and no wait or wait timeout.

Example 3: Sending and Receiving Data

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

// Define QueueHandle_t type variable to store the handle returned after successful queue creation
QueueHandle_t queue = NULL;
#define QUEUE_LENGTH 5  // Define queue length
#define ITEM_SIZE    4   // Define item size (4 bytes per item)

static void vSendTask( void *pvParameters )
{
    int num = 1;    // Data to be sent
    BaseType_t xReturn; // Used to receive return value

    while (1) {
        xReturn = xQueueSend(queue, (void *)&num, 10);
        // Determine whether the data is sent successfully through the return value
        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;   // Used to receive data
    BaseType_t xReturn; // Used to receive return value

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

        // Determine whether the data is received successfully through the return value
        if (xReturn == pdTRUE) {
            printf("Item Receive: %d \n", receiver);
        }
        else {
            printf("Item Receive FALSE\n");
        }
        vTaskDelay(1);
    }
}

void app_main(void)
{
    // Create queue
    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);
    }
}

The execution result of the above code is as follows:

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

This example demonstrates the basic process of data transfer between two tasks using xQueueSend() and xQueueReceive().

The send task writes an integer to the queue each time, and the receive task reads and prints the data from the queue. Both tasks run at the same frequency and determine whether the operation is successful based on the return value.

From the execution results, it can be seen that the data has been successfully written and received, indicating that the queue creation and basic communication functions are normal. Since the two tasks run at the same frequency, have the same scheduling rhythm and priority, the queue always remains available, and there are no cases of full or empty queues, so there are no send or receive failures.

In the ESP-IDF environment, if two tasks involve the same queue for data read and write, it is recommended to bind them to the same CPU core to reduce the uncertainty and performance loss brought by cross-core scheduling and ensure more stable and reliable communication behavior.

Data Writing

API Prototype

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

xQueueOverwrite() is used to write data into the queue. If the queue is full, it will overwrite the old data. It is only suitable for queues with a length of 1 and is often used in scenarios where the latest data is always maintained.

Note

In cases where the queue length is greater than 1, xQueueOverwrite() should not be used. Instead, the standard xQueueSend() should be used. Avoid directly overwriting the header data, skipping the normal first-in-first-out rule, and causing damage to the queue structure. This practice may lead to data consistency issues, especially when multiple tasks access the same queue at the same time, it can easily cause logical errors or data confusion.

Parameter Explanation

Input Parameter

Parameter Function

Parameter Description

xQueue

Handle of the target queue for data writing

For example, a valid handle returned by xQueueCreate, used to specify the queue for data writing.

pvItemToQueue

Pointer to the data to be written

The data will be directly copied to the created queue, ensuring that the data size does not exceed the size of the queue element.

Return Value Description

Return Value

Description

pdPASS

Write successful, data has been added to the queue.

Example 4: Latest Data Overwrite

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

// Define QueueHandle_t type variable, used to store the handle returned after successful queue creation
QueueHandle_t queue = NULL;
#define QUEUE_LENGTH 1  // Define queue length
#define ITEM_SIZE    4   // Define item size (4 bytes per item)

static void vSendTask( void *pvParameters )
{
    int num = 1;    // Data to be sent
    BaseType_t xReturn; // For receiving return value

    while (1) {
        xReturn = xQueueOverwrite(queue, (void *)&num);
        // Determine if the data is sent successfully through the return value
        if (xReturn == pdTRUE) {
            printf("Item Send: %d \n", num);
            num++;
        }
        vTaskDelay(1);
    }
}

static void vReceiveTask( void *pvParameters )
{
    int receiver = 0;   // For receiving data
    BaseType_t xReturn; // For receiving return value

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

        // Determine if the data is received successfully through the return value
        if (xReturn == pdTRUE) {
            printf("Item Receive: %d \n", receiver);
        }
        else {
            printf("Item Receive FALSE\n");
        }
        vTaskDelay(5);
    }
}

void app_main(void)
{
    // Create a queue
    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);
    }
}

The execution result of the above code is as follows:

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

This example demonstrates the behavior characteristics of data sending using xQueueOverwrite() when the queue length is 1.

The sending task continuously writes integers into the queue, and the receiving task periodically reads data from the queue. Since xQueueOverwrite() will overwrite old data when the queue is full, the sending task can write continuously without waiting for reception, and the new data will always replace the old data retained in the queue.

The receiving task reads the queue once every 5 ticks, so it can only read the data that has not been overwritten again after the last overwrite in the queue.

From the execution results, it can be seen that the receiving task only successfully received part of the data, and most of the intermediate values could not be obtained because they had been overwritten before receiving.

This clearly reflects the overwrite feature of xQueueOverwrite(), which is suitable for scenarios that only care about the latest data, such as sensor readings or status refresh.

View data

API prototype

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

xQueuePeek() is used to view the data at the head of the queue, but it does not remove the data from the queue. It is suitable for reading the current head element without changing the queue content.

Parameter Explanation

Input Parameter

Parameter Function

Parameter Description

xQueue

The handle of the queue to be viewed

For example, a valid handle returned by xQueueCreate.

pvBuffer

Pointer to the buffer used to store the received data

The data will be copied to this buffer, ensure that the size of this buffer is not less than the size of the queue element.

xTicksToWait

Maximum waiting time

The maximum time the caller blocks and waits when the queue is empty, defined in tick cycles; returns immediately when it is 0.

Return Value Explanation

Return Value

Explanation

pdTRUE

Received successfully, data has been copied to the specified buffer.

pdFALSE

Failed to receive, the queue is empty and did not wait or the wait timed out.

Example 5: Viewing Data

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

// Define QueueHandle_t type variable, used to store the handle returned after the queue is created successfully
QueueHandle_t queue = NULL;
#define QUEUE_LENGTH 5  // Define queue length
#define ITEM_SIZE    4   // Define item size (4 bytes per item)

static void vSendTask( void *pvParameters )
{
    int num = 1;    // Data to be sent
    BaseType_t xReturn; // Used to receive return value

    while (1) {
        xReturn = xQueueSend(queue, (void *)&num, 10);
        // Determine whether the data is sent successfully through the return value
        if (xReturn == pdTRUE) {
            printf("Item Send: %d \n", num);
            num++;
        }
        vTaskDelay(1);
    }
}

static void vReceiveTask( void *pvParameters )
{
    int receiver = 0;   // Used to receive data
    BaseType_t xReturn; // Used to receive return value

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

        // Determine whether the data is received successfully through the return value
        if (xReturn == pdTRUE) {
            printf("Item Peek: %d \n", receiver);
        }
        else {
            printf("Item Peek FALSE\n");
        }
        vTaskDelay(1);
    }
}

void app_main(void)
{
    // Create queue
    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);
    }
}

The execution result of the above code is as follows:

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

This example demonstrates the function of using xQueuePeek() to view the data at the head of the queue.

The send task continuously writes increasing integers to the queue, while the receive task uses xQueuePeek() to read the first element of the queue, but does not remove it.

The execution result shows that although the send task keeps writing new data, the receive task always reads the first data item in the queue (i.e., the value written for the first time), indicating that xQueuePeek() is only used to view data and will not change the queue content, which is suitable for obtaining the current head information of the queue without affecting the queue status.