Analysis of FreeRTOS Queues

1. Introduction to Queues

A queue is a mechanism for data exchange between tasks, tasks and interrupts, and interrupts and tasks. Based on queues, FreeRTOS implements various functionalities such as queue sets and semaphores. All communication and synchronization mechanisms in FreeRTOS are based on queues. Queues have the following characteristics:

1.1. Data Storage

Queues can store a limited number of fixed-size data items. They typically use a FIFO (First In, First Out) buffering mechanism, where new data is always written to the tail of the queue, and data is always read from the head of the queue. However, FreeRTOS queues also support writing data to the head of the queue and can specify whether to overwrite previously stored data at the head.

1.2. Multi-task Access

Queues are kernel objects with their own independent permissions and do not belong to or are assigned to any task. All tasks or interrupts can write to and read from the same queue. It is common for multiple entities to write to a queue, but less common for multiple entities to read from it.

1.3. Queue Read Blocking

When a task reads a message from a queue, it can specify a blocking timeout. If the queue is empty when the task attempts to read from it, the task will remain blocked until valid data is available in the queue. When another task or interrupt writes a message to the queue, the blocked task will automatically transition from the blocked state to the ready state. If the waiting time exceeds the specified blocking time, the task will automatically transition from the blocked state to the ready state, even if there is no valid data in the queue. Since multiple tasks can read from a single queue, it is possible for multiple tasks to be in a blocked state waiting for valid data. In this case, once valid data is available, only one task will be unblocked, determined by the order of blocking and task priority.

1.4. Queue Write Blocking

Similar to reading from a queue, tasks can specify a blocking timeout when writing to a queue. This time is the maximum duration a task will remain blocked waiting for valid space in the queue when the queue is full. Since multiple tasks can write to a queue, it is possible for multiple tasks to be in a blocked state waiting for valid space. In this case, once space becomes available, the task with the highest priority and longest wait time will be unblocked first.

2. Queue Operations

Here is a brief introduction to the process of queue operations:

2.1. Creating a Queue

Analysis of FreeRTOS Queues

Create a queue for communication between Task A and Task B, with a length of 5. The newly created queue is empty.

2.2. Writing Messages to the Queue

Analysis of FreeRTOS Queues

Task A writes a private variable to the tail of the queue. Since the queue was empty before writing, the newly written message is both the head and the tail of the queue.

Analysis of FreeRTOS Queues

Task A changes the value of the private variable and writes the new value to the queue. Now the queue contains two values written by Task A, where the first written value is at the head of the queue, and the newly written value is at the tail. At this point, there are still 3 free positions in the queue.

2.3. Reading Messages from the Queue

Analysis of FreeRTOS Queues

Task B first reads from the queue, obtaining the message that Task A first wrote to the queue. The message written by Task A the second time becomes the head of the queue.

Analysis of FreeRTOS Queues

3. FreeRTOS Queue Related API Functions

3.1. Queue Structure

typedef struct QueueDefinition /* The old naming convention is used to prevent breaking kernel aware debuggers. */{    int8_t * pcHead;           /**< Points to the beginning of the queue storage area. */    int8_t * pcWriteTo;        /**< Points to the free next place in the storage area. */    union    {        QueuePointers_t xQueue;     /**< Data required exclusively when this structure is used as a queue. */        SemaphoreData_t xSemaphore; /**< Data required exclusively when this structure is used as a semaphore. */    } u;    List_t xTasksWaitingToSend;             /**< List of tasks that are blocked waiting to post onto this queue.  Stored in priority order. */    List_t xTasksWaitingToReceive;          /**< List of tasks that are blocked waiting to read from this queue.  Stored in priority order. */    volatile UBaseType_t uxMessagesWaiting; /**< The number of items currently in the queue. */    UBaseType_t uxLength;                   /**< The length of the queue defined as the number of items it will hold, not the number of bytes. */    UBaseType_t uxItemSize;                 /**< The size of each items that the queue will hold. */    volatile int8_t cRxLock;                /**< Stores the number of items received from the queue (removed from the queue) while the queue was locked.  Set to queueUNLOCKED when the queue is not locked. */    volatile int8_t cTxLock;                /**< Stores the number of items transmitted to the queue (added to the queue) while the queue was locked.  Set to queueUNLOCKED when the queue is not locked. */    #if ( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )        uint8_t ucStaticallyAllocated; /**< Set to pdTRUE if the memory used by the queue was statically allocated to ensure no attempt is made to free the memory. */    #endif    #if ( configUSE_QUEUE_SETS == 1 )        struct QueueDefinition * pxQueueSetContainer;    #endif    #if ( configUSE_TRACE_FACILITY == 1 )        UBaseType_t uxQueueNumber;        uint8_t ucQueueType;    #endif} xQUEUE;

pcHead: Starting address of the storage area

pcWriteTo: Next write position

// Semaphores are implemented using queues, the following union members are used for queues and semaphores

xQueue: Used when the union is for a queue

xSemaphore: Used when the union is for a semaphore

xTasksWaitingToSend: List of tasks blocked waiting to write

xTasksWaitingToReceive: List of tasks blocked waiting to read

uxMessagesWaiting: Number of non-free items

uxLength: Length of the queue

uxItemSize: Size of queue items

cRxLock: Read lock counter

cTxLock: Write lock counter

ucStaticallyAllocated: Static creation flag, only available when both static and dynamic memory management are enabled,

this variable exists

pxQueueSetContainer: Pointer to the queue set containing this queue, only used when queue sets are enabled

The following two parameters are for debugging purposes:

uxQueueNumber: Number of queues, for debugging.

ucQueueType: Type of queue, 0: queue or queue set. 1: mutex semaphore. 2: counting semaphore.

3: binary semaphore. 4: recursive semaphore

3.2. Create Queue API

The dynamic creation API is defined as follows:

#define xQueueCreate( uxQueueLength, uxItemSize )    xQueueGenericCreate( ( uxQueueLength ), ( uxItemSize ), ( queueQUEUE_TYPE_BASE ) )

Parameters:

uxQueueLength: Length of the queue

uxItemSize: Size of queue items

Return value: NULL if queue creation fails. Other values: queue creation successful, returns the starting address of the queue

The static creation API is defined as follows:

#define xQueueCreateStatic( uxQueueLength, uxItemSize, pucQueueStorage, pxQueueBuffer )    xQueueGenericCreateStatic( ( uxQueueLength ), ( uxItemSize ), ( pucQueueStorage ), ( pxQueueBuffer ), ( queueQUEUE_TYPE_BASE ) )

uxQueueLength: Length of the queue.

uxItemSize: Size of queue items

pucQueueStorage: Starting address of the queue storage area

pxQueueBuffer: Static queue structure.

Return value: NULL if queue creation fails. Other values: queue creation successful, returns the starting address of the queue

3.3. Queue Write API

Analysis of FreeRTOS Queues

In tasks, the queue write API functions are xQueueSend(), xQueueSendToBack(), xQueueSendToFront(), and xQueueOverwrite(). These four API functions are actually macro definitions that call the xQueueGenericSend() function, specifying different write positions. Their function definition is as follows::

BaseType_t xQueueGenericSend( QueueHandle_t xQueue,                              const void * const pvItemToQueue,                              TickType_t xTicksToWait,                              const BaseType_t xCopyPosition ) PRIVILEGED_FUNCTION;

xQueue: The queue to write to

pvItemToQueue: The message to write

xTicksToWait: Blocking timeout

xCopyPosition: Write position.

/* For internal use only. */#define queueSEND_TO_BACK                     ( ( BaseType_t ) 0 ) // Write to the back of the queue#define queueSEND_TO_FRONT                    ( ( BaseType_t ) 1 ) // Write to the front of the queue#define queueOVERWRITE                        ( ( BaseType_t ) 2 ) // Overwrite the queue

In interrupts, the last four API function interfaces are called as shown above. Essentially, they also call the xQueueGenericSendFromISR() function through macro definitions.

3.4. Queue Read API

Analysis of FreeRTOS Queues

xQueueReceive() function is defined as follows:

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

xQueue: The queue to read from

pvBuffer: Buffer for reading the information

xTicksToWait: Blocking timeout

xQueuePeek() function differs fromxQueueReceive() in that it does not remove the read message after successfully reading it. This means that the same content can still be read from the queue next time. It is defined as follows:

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

xQueue: The queue to read from

pvBuffer: Buffer for reading the information

xTicksToWait: Blocking timeout.

The functions xQueueReceiveFromISR() and xQueuePeekFromISR() are used in interrupts to read from the queue.

4. Queue Code Testing

Add the following code in main.c:

#include "stm32h5xx_hal.h"#include "sys_freq.h"#include "uart.h"#include "I2c.h"
#include "FreeRTOS.h"#include "task.h"#include "queue.h"
TaskHandle_t    osTaskPeriod500_Handler;TaskHandle_t    osTaskEvent_Handler;
QueueHandle_t   TempQueue;
void osTask_Period500ms_cb(void){    uint8_t count = 0;    while(1)    {        if(count % 2 == 0)        {            xQueueSend(TempQueue, &amp;count, portMAX_DELAY);        }        printf("osTask_Period Running, count:%d\r\n", count);        if(count++ &gt;= 255)        {            count = 0;        }        vTaskDelay(500);    }
}
void osTask_Event_cb(void){    uint8_t queueData = 0;
    while(1)    {        xQueueReceive(TempQueue, &amp;queueData, portMAX_DELAY);        printf("osTask_Event Running, Queue recvData:%d\r\n", queueData);    }
}
int main(void){    HAL_StatusTypeDef retval = HAL_OK;    HAL_Init();    sys_clock_init();    uart_init(115200);
    TempQueue = xQueueCreate(2, sizeof(uint8_t));    xTaskCreate(osTask_Event_cb, "osTaskEvent", 1024, NULL, 7, &amp;osTaskEvent_Handler);    xTaskCreate(osTask_Period500ms_cb, "osTaskPeriod", 1024, NULL, 6, &amp;osTaskPeriod500_Handler);
    vTaskStartScheduler();    while (1)    {
    }
}

Log output via serial port:

Analysis of FreeRTOS Queues

When the count value is even, the 500ms periodic task sends the current count value to the queue, and the event task prints the received count value after receiving the queue data. From the above, it can be concluded that when there is data in the queue, the event task has a higher priority than the periodic task. Therefore, it will immediately preempt the CPU to execute the event task.

Leave a Comment