In this article, we will learn about freeRTOS Queue by going through a project with Arduino: control a seven segment display to show a number from 0 to 9. In this simple project, we will talk about task to task communication and how to use Queue to transfer data from one task to another.
In freeRTOS applications, there are usually two or more tasks that are assigned different priorities. The kernel switch between tasks and choose which one to run based on a scheduling algorithm. At any moment in time, there can be only one task that can run. By rapidly switching between tasks, it emulates a multi-tasking system. Each task can be thought of an independent program that does a specific thing.
In a bare metal program, controlling a seven segment to count from 0 to 9 repeatedly is a trivial job. It can be done by initialising the port that control the seven segment display and a variable to hold the number to be displayed on the seven segment, then go into an infinite loop. In each loop, call a function to display a number on the seven segment, then increase the number and add a delay function at the end of the loop. The pseudo code of the program looks something like this
How can this project be translated to a multi-tasking system? Of course it does not make sense to implement a complex multi-tasking system for a trivial project which can be done easily using a few lines of code. However, it makes a lot of sense to use a simple system to learn new concepts. In a multi-tasking program, we will first need to decide to spit the job into tasks. For this project, we can think of using two tasks: task 1 for generating a number from 0 to 9 over and over, task 2 for displaying a number on a seven segment. Both tasks have the same priority of 1. Each task does not know about the present of each other and run independently. To make it work as a system, we need two tasks to communicate with each other. Specifically, we need task 1 to send the current number it is generating to task 2, so that task 2 can take that number and show it on the seven segment display. Sending data between tasks can be achieved by using Queue. One task sends data to the queue and another task receives data from the queue.
freeRTOS Queue APIs
Queue is a data structure that can hold a number of elements or items. Similar to a queue at a supermarket where people are waiting to checkout. Whoever comes first to the queue gets the first position, then another person arrives and fills in the second position, and so on. When the clerk starts serving, the first person goes out of the queue, then the second person advances one step and stay in the first person’s position, and so does everyone in the queue. A queue can be characterised by the number of people it can hold. If a queue is full then nobody can get into it. There are basic functions to work with Queue: creating a queue, sending (or writing) an item to a queue, and receiving (or reading) an item from queue.
Creating a queue
In freeRTOS, a queue is created by calling xQueueCreate() function which takes two parameters: the first parameter is the number of items that it can hold and the second parameter is the size of each item. A real life analogy is a queue at a supermarket that can have at most 10 people waiting, and each position in the queue is enough for one person to stand. The xQueueCreate() function returns NULL if the queue can not be created and returns a variable of type QueueHandle_t if queue is created successfully (analogy: give queue a name so that we can refer to it). For our project to control a seven segment, we can create a queue named queue_1 to store the numbers that task_1 sends to task_2. Since task_1 is generating an integer number from 0 to 9, and task_2 is getting the number to display on the seven segment, we will define a queue that can hold, for example, 3 integer numbers. Since the queue can be accessed by both task_1 and task_2, we will make it a global variable. The code to create the queue looks like this
Send data to a queue
To send data to a queue, a tasks call xQueueSend() which takes 3 parameters. The first parameter is, as you can reasonably guess, what queue to send data to. It is queue_1 variable of type QueueHandle_t that was created previously. In task_1, we want to send a number from 0 to 9 to the queue, so we will create a variable numberToSend to store the value and pass in the address of the variable (&numberToSend). The third parameter xTicksToWait is the amount of time in number of ticks that the task should remain in blocked state to wait for queue become available. If xTicksToWait is set to portMAX_DELAY then task_1 will wait indefinitely until the queue is not full.
Task state transition is described as follow. Initially task_1 runs and it calls xQueueSend() to send an integer to queue. If xTicksToWait is specified and the queue is full, task_1 will enter blocked state to wait for xTicks. When task_2 reads the queue and takes away one integer to make an empty space in the queue before xTicks, there is an event occurred (queue is not full event). Task_1 registered to this event and becomes unblock. It enters ready state and is available for the kernel to select to run. Because we have two tasks of the same priority, at the next kernel tick, task_1 is selected and run. The xQueueSend() function runs and return pdPASS.
Task_1 code to generate an integer and send it to the queue can be written as follow
What happens in the main loop of task_1 is it sends an integer number to the queue, then increases the number and resets number if it is equal 10. After that, vTaskDelay() is called to put task_1 to blocked state for 1 second. After 1 second, task_1 unblocks and can be selected to run again which will send another integer to the queue for task_2 to read.
Receiving data from a queue
To receive data from the queue, task_2 calls xQueueReceive() function. The xQueueReceive() API takes 3 input parameters. The first parameter is what queue it should read from. We should pass in the variable queue_1 of type QueueHandle_t defined earlier so that task_2 knows it should read from queue_1. The second parameter is a pointer that points to the location of a buffer that the data received from the queue should be copied to. Task_2 should allocate enough memory to store the value that it receives from the queue. Since queue_1 contains integer numbers, we can declare a variable numToDisplay of type integer and pass in the address of it (&numToDisplay). The third parameter is xTicksToWait. Similar to xQueueSend(), xTicksToWait is the maximum time that task_2 can wait in blocked state until there is an item in queue for it to receive. If xTicksToWait is portMAX_DELAY then task_2 will wait in blocked state forever until there is data on the queue. Function xQueueReceive() returns pdPASS if the data can be received successfully from the queue and return errQUEUE_EMPTY if queue is empty.
Task_2 code to read from the queue and display a number on the seven segment can be written as:
In this article, you have learnt about task to task communication in freeRTOS using Queue. There are other ways to send and receive data between tasks which we will discuss in other articles. Thanks for reading.