ESP32 ESP-IDF FreeRTOS Queue Tutorial

In this ESP32 ESP-IDF FreeRTOS Queue tutorial, we will learn to create FreeRTOS Queues with ESP32 ESP-IDF. Previously we learned how to create FreeRTOS tasks with ESP32 ESP-IDF. However, creating separate tasks does not sufficient for a complete RTOS-based application because these independent tasks are smaller programs having their own stack, memory, and execution jobs. It follows that we require a mechanism for these independent tasks to communicate with one another so that they may exchange data with each other.

The FreeRTOS Kernel offers a mechanism for inter-task data transfer, just as other contemporary RTOS Kernels. Message Queues are what these are called used for this purpose. They serve as the fundamental building block for all FreeRTOS tasks synchronization and communication techniques. They are used to send and receive messages between tasks.

Before we move ahead, make sure you have the latest version of VS Code installed on your system with the ESP-IDF extension configured.

FreeRTOS Queues Introduction

A message queue is a type of FIFO buffer that stores fixed-size data items. Additionally, once a queue has been initialized, the amount of items it may hold is fixed. Generally, Tasks read from the front end of the buffer and write data to the end of the buffer. But writing from the beginning is another option. The buffer can be used by several writers and readers simultaneously.

The buffer, however, may only be used by one writer or reader tasks at a time, and other processes are blocked. As a result, blocking is possible for both writes and reads to the buffer.

For example, task1 sends data to the buffer. task2 reads this data from the buffer and deletes its value from the buffer. Each data block in the FreeRTOS queue can be of any data type or size.

Tasks Blocking on Queue Reads

FreeRTOS task can be Blocked on reads is likely in the following situations:

  1. If multiple tasks are willing to read data from the message queue, the highest priority task gets to read data first and lowest priority one read data at the end. Meanwhile, other tasks stay blocked. We can also set the maximum blocking time of a task while dispatching a read request. But various FreeRTOS tasks can also have distinct blocking times.
  2. The other potential case is when a queue is empty. In such a case, all read requests go to a blocking state. Once data become available from the message queue (when another task places data into the ready queue), all readers are transferred to the ready state and the highest priority task gets to read from Queue first.

Tasks Blocking on Queue Writes

Similar to reading from a queue, A writer task can also optionally define a block time when writing to a queue. For example, a writer’s task wants to write data to a queue that is already full. In that case, the block time is the maximum time the task should be kept in the Blocked state to wait for space to become available in the queue

Multiple writer tasks can write in a single Queue. Therefore, there is a possibility that an already filled queue will have more than one task blocked on it waiting to complete a write operation. When this is the case, only one task will be unblocked when space on the queue becomes available.

The task that is unblocked will always be the highest priority task that was waiting for space. If the blocked tasks have equal priority, then it will be the task that has been waiting for space the longest that is unblocked.

This simulation shows the sequence of reads/writes:

FreeRTOS queue reading writing sequence
Source: FreeRTOS

ESP32 ESP-IDF FreeRTOS Queue Example Code

Let us work on the code that we used previously when creating two tasks, and incorporate Queue in it. Refer to the article FreeRTOS Tasks with ESP-IDF before moving ahead. The creation of tasks is already discussed in that article. Here, we will show you how to use Queue.

To use the functionalities of the Queue library provided by FreeRTOS, let us first includes its header file:

#include freertos/queue.h

Next, create a Queue Handle as shown below:

QueueHandle_t queue;

First Task Function

Then we will define the Demo_Task function. This will enable us to send data to the buffer.

void Demo_Task(void *arg)
{
    char txBuffer[50];
    queue = xQueueCreate(5, sizeof(txBuffer)); 
    if (queue == 0)
    {
     printf("Failed to create queue= %p\n", queue);
    }

    sprintf(txBuffer, "Hello from Demo_Task 1");
    xQueueSend(queue, (void*)txBuffer, (TickType_t)0);

    sprintf(txBuffer, "Hello from Demo_Task 2");
    xQueueSend(queue, (void*)txBuffer, (TickType_t)0); 

    sprintf(txBuffer, "Hello from Demo_Task 3");
    xQueueSend(queue, (void*)txBuffer, (TickType_t)0);  

    while(1){
        vTaskDelay(1000/ portTICK_RATE_MS);
    }
}

Firstly, we will create an array of characters to store data. It is called ‘txBuffer.’

char txBuffer[50];

Create Queue

Next, we will create the Queue using xQueueCreate() function. The first argument is the character number on the Queue buffer whereas the second parameter is the size of each character on the buffer. In our case, it is as follows:

queue = xQueueCreate(5, sizeof(txBuffer)); 

As the size of txBuffer is set to 50, each block will have a limit of 50 characters.

Moreover, we will add an if statement to check whether the Queue is created successfully or not.

if (queue == 0){
printf("Failed to create queue= %p\n", queue);
}

Next, we define the string ‘Hello from Demo_Task 1’ that we want to send to the Queue using the sprintf() function.

sprintf(txBuffer, "Hello from Demo_Task 1");

Write Data to Queue

To send this buffer to Queue we will use xQueueSend() function. The first argument of this function is the Queue that will be used. In our case, it is ‘queue.’ The second argument is the pointer of the data which we are sending. In our case, it is ‘(void*)txBuffer.’ The third argument is blocking time. As we do not want to add blocks we will set it to zero.

xQueueSend(queue, (void*)txBuffer, (TickType_t)0); 

Let’s add more strings to the txBuffer and send them to the Queue as follows:

    sprintf(txBuffer, "Hello from Demo_Task 1");
    xQueueSend(queue, (void*)txBuffer, (TickType_t)0);

    sprintf(txBuffer, "Hello from Demo_Task 2");
    xQueueSend(queue, (void*)txBuffer, (TickType_t)0); 

    sprintf(txBuffer, "Hello from Demo_Task 3");
    xQueueSend(queue, (void*)txBuffer, (TickType_t)0);  

Second Task Function

In the second task function, Demo_Task2() we will obtain the data from the Queue buffer.

void Demo_Task2(void *arg)
{
    char rxBuffer[50];
    while(1){
     if( xQueueReceive(queue, &(rxBuffer), (TickType_t)5))
     {
      printf("Received data from queue == %s/n", rxBuffer);
      vTaskDelay(1000/ portTICK_RATE_MS);

     }
    }
}

Firstly, we will create an array of characters to store data which will be received from the Queue buffer. It is called ‘rxBuffer.’

char rxBuffer[50];

Read Data from Queue

Inside the continuous while loop, we will use the xQueueReceive() function so that we can read the buffer constantly. Its first argument is the Queue which will be checked from the xQueueSend() function. In our case, it is a queue. The second argument is the pointer of the data which we are receiving. In our case, it is ‘&(rxBuffer). The third argument is the blocking time. We will add blocking time while receiving data from Queue buffer so that we do not lose any data.

xQueueReceive(queue, &(rxBuffer), (TickType_t)5)

Moreover, we will add a print statement to check the data received on the serial monitor.

  if( xQueueReceive(queue, &(rxBuffer), (TickType_t)5))
     {
      printf("Received data from queue == %s\n", rxBuffer);
      vTaskDelay(1000/ portTICK_RATE_MS);

     }

Complete Code

This is the complete code that we are using for which we explained the parts relating to FreeRTOS Queue.

#include <stdio.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include "freertos/queue.h"

TaskHandle_t myTaskHandle = NULL;
TaskHandle_t myTaskHandle2 = NULL;
QueueHandle_t queue;

void Demo_Task(void *arg)
{
    char txBuffer[50];
    queue = xQueueCreate(5, sizeof(txBuffer)); 
    if (queue == 0)
    {
     printf("Failed to create queue= %p\n", queue);
    }

    sprintf(txBuffer, "Hello from Demo_Task 1");
    xQueueSend(queue, (void*)txBuffer, (TickType_t)0);

    sprintf(txBuffer, "Hello from Demo_Task 2");
    xQueueSend(queue, (void*)txBuffer, (TickType_t)0); 

    sprintf(txBuffer, "Hello from Demo_Task 3");
    xQueueSend(queue, (void*)txBuffer, (TickType_t)0);  

    while(1){
        vTaskDelay(1000/ portTICK_RATE_MS);
    }
}

void Demo_Task2(void *arg)
{
    char rxBuffer[50];
    while(1){
     if( xQueueReceive(queue, &(rxBuffer), (TickType_t)5))
     {
      printf("Received data from queue == %s\n", rxBuffer);
      vTaskDelay(1000/ portTICK_RATE_MS);

     }
    }
}

void app_main(void)
{
   xTaskCreate(Demo_Task, "Demo_Task", 4096, NULL, 10, &myTaskHandle);
   xTaskCreatePinnedToCore(Demo_Task2, "Demo_Task2", 4096, NULL,10, &myTaskHandle2, 1);
 }

Demonstration

To flash your chip, type the following command in the serial terminal. Remember to replace the COM port with the one through which your board is connected.

idf.py -p COMX flash monitor

After the code flashes successfully, this output gets printed in the serial terminal. As you can see from the output of the serial monitor that the first task writes three strings to a queue and the second task reads them as soon as any entry in the queue become available.

ESP32 FreeRTOS Queue with ESP-IDF Example1 demo

This is how we can read and write data to Queues using FreeRTOS and perform inter-task communication to share data between independent tasks. Queues provide a secure and efficient way to share data between tasks.

You may also like to read:

3 thoughts on “ESP32 ESP-IDF FreeRTOS Queue Tutorial”

Leave a Comment