FreeRTOS Queue Management Overview

Queue Characteristics

A queue can hold a limited number of data units of a defined length. The maximum number of units that a queue can hold is referred to as the queue’s “depth”; this depth and the size of each unit must be set when the queue is created.

Typically, queues are used as FIFO (First In, First Out), meaning data is written to the tail of the queue and read from the head; however, it is also possible to write to the head of the queue.

Next, let’s look at the queue processing process:

FreeRTOS Queue Management Overview

img

Queue API Functions

To call the following functions, you need to #include "queue.h"

「1. Queue Creation」

Attribute API Interface Actual Execution Function
Dynamic xQueueCreate() xQueueGenericCreate()
Static xQueueCreateStatic() xQueueGenericCreateStatic()

「2. Queue Sending」

Enqueue Method API Interface Actual Execution Function Other
Enqueue from the tail xQueueSend() xQueueGenericSend()
Same as above xQueueSendToBack() Same as above
Same as above xQueueOverwrite() Same as above Only for queues with a message count of 1
Enqueue from the head xQueueSendToFront() Same as above
Enqueue from the tail (used in interrupts) xQueueSendFromISR() xQueueGenericSendFromISR()
Same as above xQueueSendToBackFromISR() Same as above
Same as above xQueueOverwriteFromISR() Same as above Only for queues with a message count of 1
Enqueue from the head (used in interrupts) xQueueSendToFrontFromISR() Same as above

「3. Queue Receiving」

Dequeue Method API Interface Actual Execution Function
Dequeue and delete xQueueReceive() xQueueGenericReceive()
Dequeue without deleting xQueuePeek() Same as above
Dequeue and delete (used in interrupts) xQueueReceiveFromISR() xQueueReceiveFromISR()
Dequeue without deleting (used in interrupts) xQueuePeekFromISR() xQueuePeekFromISR()

「4. Queue Reset and Delete」

Functionality API Interface Actual Execution Function
Reset and clear memory xQueueReset() xQueueGenericReset()
Delete and free memory vQueueDelete() vQueueDelete()

Common Queue Function Analysis

「1. xQueueCreate() API Function」

QueueHandle_t  xQueueCreate( UBaseType_t  uxQueueLength,
                             UBaseType_t  uxItemSize );

Input parameters:

  • 「uxQueueLength」: The length of the queue items; that is, the maximum number of messages that the queue can store, also known as the queue depth.
  • 「uxItemSize」: The size of the information; that is, the size of each message’s data in bytes.

Return parameters (this return value should be saved as a handle to operate on this queue):

  • 「NULL」: Indicates that there was not enough heap space allocated for the queue, causing the creation to fail.
  • 「Non-NULL」: Indicates that the queue was created successfully.

「2. xQueueSendToBack() and xQueueSendToFront() API Functions」

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

xQueueSendToFront() and xQueueSendToBack() function parameters and return values.

Input parameters:

  • 「xQueue」: The handle of the target queue. This handle is the return value when calling xQueueCreate() to create this queue.
  • 「pvItemToQueue」: A pointer to the data being sent. It points to the data unit that will be copied into the target queue.
  • 「xTicksToWait」: Blocking timeout. If the queue is full when sending, this time is the maximum wait time for the task to be in a blocked state waiting for the queue space to become valid; if xTicksToWait is set to 0, both xQueueSendToFront() and xQueueSendToBack() will return immediately; if xTicksToWait is set to portMAX_DELAY, then the blocking wait will have no timeout limit.

Return parameters (there are two possible return values):

  • 「pdTRUE」: Data was successfully sent to the queue.
  • 「errQUEUE_FULL」: Unable to write data due to the queue being full.

「3. xQueueReceive() and xQueuePeek() API Functions」

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

xQueueReceive() and xQueuePeek() function parameters and return values.

Input parameters:

  • 「xQueue」: The handle of the queue being read. This handle is the return value when calling xQueueCreate() to create this queue.
  • 「pvBuffer」: The pointer to the receiving buffer. It points to a memory area used to receive data copied from the queue.
  • 「xTicksToWait」: Blocking timeout. If the queue is empty when receiving, this time is the maximum wait time for the task to be in a blocked state waiting for valid queue data; if xTicksToWait is set to 0, both xQueueReceive() and xQueuePeek() will return immediately; if xTicksToWait is set to portMAX_DELAY, then the blocking wait will have no timeout limit.

Return parameters (there are two possible return values):

  • 「pdTRUE」: Successfully read data from the queue.
  • 「pdFALSE」: No data was read due to the queue being empty during the read operation.

Handling Large Data Units

If the size of the data units stored in the queue is large, it is better to use the queue to pass pointers to the data rather than copying the data byte by byte into or out of the queue. Passing pointers is more efficient in terms of processing speed and memory space utilization; however, when using queues to pass pointers, it is essential to carefully ensure the following two points:

「1. Ownership of the Memory Space Pointed to by the Pointer Must Be Clear」

When tasks share memory through pointers, it must be fundamentally ensured that no two tasks modify the data in shared memory simultaneously, or otherwise invalidate the shared memory data or create consistency issues. In principle, shared memory should only be accessed by the sending task before its pointer is sent to the queue; after the shared memory pointer is read from the queue, its content should only be accessed by the receiving task.

「2. The Memory Space Pointed to by the Pointer Must Be Valid」

If the memory space pointed to by the pointer is dynamically allocated, only one task should be responsible for freeing that memory. Once that memory space is freed, no task should access that space again.

Avoid using pointers to access space allocated on the task stack. Because when the stack frame changes, the data on the stack will no longer be valid.

Example Testing

/* Standard includes. */
#include <stdio.h>
#include <string.h>

/* Scheduler includes. */
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"

/* Library includes. */
#include "stm32f10x_it.h"

/* Private app includes. */
#include "bsp_time.h"
#include "bsp_uart.h"
#include "bsp_gpio.h"

/* Task priorities. */
#define mainCREATOR_TASK_PRIORITY           ( tskIDLE_PRIORITY + 3 )

/*----------------------------- End -----------------------------*/

/*
 * User Private Task.
 */
static void prvUser_Task( void *pvParameters );

/*
 * Configure the clocks, GPIO and other peripherals as required by the demo.
 */
static void prvSetupHardware( void );

/*----------------------------- End -----------------------------*/

/************************************************
函数名称 : main
功    能 : 主函数入口
参    数 : 无
返 回 值 : 无
*************************************************/
int main( void )
{
#ifdef DEBUG
  debug();
#endif

 prvSetupHardware();

 /* Start the tasks defined within this file/specific to this demo. */
 xTaskCreate( prvUser_Task, "prvUser_Task", configMINIMAL_STACK_SIZE, NULL, mainCREATOR_TASK_PRIORITY, NULL );
 
 /* Start the scheduler. */
 vTaskStartScheduler();
 
 /* Will only get here if there was not enough heap space to create the
 idle task. */
 return 0;
}
/*----------------------------- End -----------------------------*/

extern QueueHandle_t xQueueUartTx;
void vSender1_Task( void *pvParameters )
{ 
 UartTx_Buff_TypeDef uart_data;
 
 strcpy((char *)uart_data.TxBuffer,"TaskA Running\r\n");

 uart_data.TxCounter = strlen("TaskA running\r\n");
 printf("Task A was created successfully....\r\n");
 uart_data.COMx = EVAL_COM1;
  
 while(1){
  /* 延时 800个tick */
  vTaskDelay(800);
  xQueueSend(xQueueUartTx, (void *)&uart_data, portMAX_DELAY);
 }
}

void vSender2_Task( void *pvParameters )
{ 
 UartTx_Buff_TypeDef uart_data;

 strcpy((char *)uart_data.TxBuffer,"TaskB Running\r\n");

 uart_data.TxCounter = strlen("TaskB running\r\n");
 printf("Task B was created successfully....\r\n");
 uart_data.COMx = EVAL_COM1;
  
 while(1){
  /* 延时 800个tick */
  vTaskDelay(800);
  xQueueSend(xQueueUartTx, (void *)&uart_data, portMAX_DELAY);
 }
}

static void prvUser_Task( void *pvParameters )
{ 
 /* User-defined private tasks */
 xTaskCreate( vSender1_Task, "vSender1_Task", configMINIMAL_STACK_SIZE + 300, NULL, tskIDLE_PRIORITY+2, NULL );
 xTaskCreate( vSender2_Task, "vSender2_Task", configMINIMAL_STACK_SIZE + 300, NULL, tskIDLE_PRIORITY+2, NULL );
 
 printf("delete user task\n");
 vTaskDelete(NULL);  // 删除自己
}

static void prvSetupHardware( void )
{
 /* Start with the clocks in their expected state. */
 RCC_DeInit();

 /* Enable HSE (high speed external clock). */
 RCC_HSEConfig( RCC_HSE_ON );

 /* Wait till HSE is ready. */
 while( RCC_GetFlagStatus( RCC_FLAG_HSERDY ) == RESET )
 {
 }

 /* 2 wait states required on the flash. */
 *( ( unsigned long * ) 0x40022000 ) = 0x02;

 /* HCLK = SYSCLK */
 RCC_HCLKConfig( RCC_SYSCLK_Div1 );

 /* PCLK2 = HCLK */
 RCC_PCLK2Config( RCC_HCLK_Div1 );

 /* PCLK1 = HCLK/2 */
 RCC_PCLK1Config( RCC_HCLK_Div2 );

 /* PLLCLK = 8MHz * 9 = 72 MHz. */
 RCC_PLLConfig( RCC_PLLSource_HSE_Div1, RCC_PLLMul_9 );

 /* Enable PLL. */
 RCC_PLLCmd( ENABLE );

 /* Wait till PLL is ready. */
 while(RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET)
 {
 }

 /* Select PLL as system clock source. */
 RCC_SYSCLKConfig( RCC_SYSCLKSource_PLLCLK );

 /* Wait till PLL is used as system clock source. */
 while( RCC_GetSYSCLKSource() != 0x08 )
 {
 }

 /* Configure HCLK clock as SysTick clock source. */
 SysTick_CLKSourceConfig( SysTick_CLKSource_HCLK );
 
 /*
  * STM32中断优先级分组为 4,即 4bit都用来表示抢占优先级,范围为:0~15
  * 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
  * 都统一用这个优先级分组,千万不要再分组,切忌。
  */
 NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
 
 /* Other peripheral configuration */
 // vSetupTimer();
 vSetupUSART();
 vSetupParPort();
}

/*----------------------------- End -----------------------------*/

From the overall project, we mainly created two tasks as senders for the queue; why go through the extra step of using the queue to transmit print data instead of directly calling the printf function to output? In RTOS applications, we know that each task is essentially independent, which can lead to a situation where Task A is printing output data while Task B also wants to print data. In that case, the output data may overlap or be lost or garbled; to test this, the examples of Task A and Task B are designed to be identical, including priority; moreover, we added direct printing like we did before without RTOS to see what would happen, and then we will attach the receiving task of the queue:

#include "bsp_uart.h"

/* Scheduler includes. */
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"

#define BAUDRATE_1  115200;   // Baud rate settings supported: 115200,19200,9600,38400,57600,1200,2400,4800
#define BAUDRATE_2  115200;   // Baud rate settings supported: 115200,19200,9600,38400,57600,1200,2400,4800

QueueHandle_t xQueueUartTx;

static void prvUartTx_Task( void *pvParameters )
{
 UartTx_Buff_TypeDef uart_data;
 
 for( ; ; ){
  if(xQueueReceive(xQueueUartTx, (void *)&uart_data, portMAX_DELAY) == pdPASS){
   if(uart_data.TxBuffer > 0){
    printf("len = %d\n",uart_data.TxCounter);
    USART_SendString(uart_data.COMx, uart_data.TxBuffer, uart_data.TxCounter);
   }
  }
 }
}
/************************************************
函数名称 : vSetupUSART
功    能 : UART初始化接口
参    数 : 无
返 回 值 : 无
*************************************************/
void vSetupUSART( void )
{
 UART1_Config();
// UART2_Config();
 
 /* Create the queue used by the Usart task. */
 xQueueUartTx = xQueueCreate((unsigned portBASE_TYPE)UART_QUEUE_TX_LENGTH, sizeof(UartTx_Buff_TypeDef)); // Handle of the target queue
// xQueueUart1_Rx = xQueueCreate((unsigned portBASE_TYPE)UART_QUEUE_RX_LENGTH, sizeof(UartRx_Buff_TypeDef)); // Handle of the target queue
 xQueueUart1_Rx = xQueueCreate((unsigned portBASE_TYPE)20, sizeof(uint8_t)); // Handle of the target queue

 xTaskCreate( prvUartTx_Task, "prvUartTx_Task", 300, NULL, tskIDLE_PRIORITY + 3, NULL );
 xTaskCreate( prvUart1_Rx_Task, "prvUart1_Rx_Task", 500, NULL, tskIDLE_PRIORITY + 3, NULL );
}
/************************************************
函数名称 : UART1_Config
功    能 : UART1端口配置
参    数 : 无
返 回 值 : 无
*************************************************/
void UART1_Config(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;
    USART_InitTypeDef USART_InitStructure;

    /* config GPIOA clock */
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
 
    /* config USART1 clock */
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);

    /* USART1 GPIO config */
    /* Configure USART1 Tx (PA.09) as alternate function push-pull */
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    /* Configure USART1 Rx (PA.10) as input floating */
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    /* Enable the USART1 Interrupt */
    NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 7;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

    /* USART1 mode config */
    USART_InitStructure.USART_BaudRate = BAUDRATE_1;
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;
    USART_InitStructure.USART_StopBits = USART_StopBits_1;
    USART_InitStructure.USART_Parity = USART_Parity_No ;
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
    USART_Init(EVAL_COM1, &USART_InitStructure);

 USART_ITConfig(EVAL_COM1, USART_IT_RXNE, ENABLE);
    USART_Cmd(EVAL_COM1, ENABLE);
}
/************************************************
函数名称 : USART_SendByte
功    能 : 串口字符发送
参    数 : c ---- 发送的数据
返 回 值 : 无
*************************************************/
void USART_SendByte( USART_TypeDef* USARTx, uint8_t c )
{     
 USART_SendData(USARTx, c);
 
 while (USART_GetFlagStatus(USARTx, USART_FLAG_TXE) == RESET);
}
/************************************************
函数名称 : USART_SendString
功    能 : 串口字符串发送
参    数 : USARTx ---- 串口
   pData ---- 字符串
   Length ---- 长度
返 回 值 : 无
*************************************************/
void USART_SendString( USART_TypeDef* USARTx, const uint8_t *pData, uint16_t Length )
{
    while(Length--)
    {
        USART_SendByte(USARTx, *pData);
        pData++;
    }
}
/************************************************
函数名称 : USART_Printf
功    能 : 串口打印输出
参    数 : USARTx ---- 串口
   String ---- 字符串
返 回 值 : 无
*************************************************/
void USART_Printf( USART_TypeDef* USARTx, char *String )
{
    do
    {
        USART_SendByte(USARTx, *String);
        String++;
    }while((*String) != '\0');
}
/************************************************
函数名称 : fputc
功    能 : 重定向 c库函数 printf到 DEBUG_UART
参    数 : ch
返 回 值 : 无
*************************************************/
int fputc(int ch, FILE *f)
{
    /* 发送一个字节数据到 DEBUG_UART */
    USART_SendData(DEBUG_UART, (uint8_t) ch);

    /* 等待发送完毕 */
    while (USART_GetFlagStatus(DEBUG_UART, USART_FLAG_TXE) == RESET);

    return (ch);
}
/************************************************
函数名称 : fgetc
功    能 : 重定向 c库函数 scanf到 DEBUG_UART
参    数 : f ---- 文件
返 回 值 : 无
*************************************************/
int fgetc(FILE *f)
{
    /* 等待 DEBUG_UART输入数据 */
    while (USART_GetFlagStatus(DEBUG_UART, USART_FLAG_RXNE) == RESET);

    return (int)USART_ReceiveData(DEBUG_UART);
}

/*---------------------------- END OF FILE ----------------------------*/

In fact, compared to the previous STM32 Notes on USART[1], the changes are not significant; we just separated the processing and encapsulated it in the form of tasks.

Finally, let’s take a look at the test results:

FreeRTOS Queue Management Overview

img

From the results, we can see that if we directly use printf for output, because two tasks of the same priority are outputting data at the same time, the result shows data overlap (which is not the result we want). In contrast, using the queue to output data allows for normal printing (even though their task priorities are the same and the execution flow is the same, it distributes the data correctly for printing; further testing revealed that tasks with the same priority are processed in the order they were created; there are also other small tests that yield various results, which can be explored further). The main point here is that to avoid multiple tasks accessing shared data (or the same hardware) in RTOS, it is best to use queues or mutexes as needed to handle the data.

In a multitasking system, there is a potential risk. When a task is using a resource and has not completely finished accessing that resource, it may be preempted, leaving the resource in an inconsistent or incomplete state. If another task or interrupt accesses that resource at that time, it could lead to data corruption or other similar errors, as seen in the initial printing errors.

Reference

[1]

STM32 Notes on USART: https://blog.csdn.net/qq_42992084/article/details/104098531

Leave a Comment