The steps to use an RTOS message queue to exchange data between tasks. In this post, we post data from the low level hardware (UART peripheral) to a task that handles a logic part of our firmware. |
This is a direct follow up from the previous FreeRTOS post, where semaphores and triggers were used to collect incoming UART data without burning CPU cycles.
Now, we add the mechanism to give that data to the logic part of the application, while keeping the UART / low-level part lean.
We'll use message queues for that. One of the common RTOS mechanisms to give away data and continue with core business.
From the Previous Post: use FreeRTOS and Triggers to get UART data in the background
You'll need to read the previous post. It explains how to use the PSoC smart buffered Serial Communication Blocks, with a trigger and RTOS, to get packages of data. Without polling. Without looping over the serial read...
The mechanism sets up the firmware to lay low until the agreed number of bytes have been received on the serial port. The controller can spend its powers to do other things, or to go lower power.
What do we gain:
- the hardware layer stays lean and reactive. There's very little latency between getting bits at the serial port and accepting them.
This part is trigger driven. We get a trigger when an pre-defined number of bytes arrives. We act on that with a little logic as possible: put the payload on a queue.
Then go back to being ready for new serial data. - That physical data is queued and handed over to the business logic.
Two gains actually: separation between physical implementation and business logic, and a buffer queue that can take a number of data feeds in case the controller is more busy then usual on something else.
Using a Message Queue to send data to a task
In this post I focus on a few of the advantages:
- keep the UART handling as lean and reactive as possible
- isolation between hardware (UART) and logic
- buffering to deal with peak loads without losing serial data
The first one is fairly easy. When you have a queue set up, the UART handler just has to put the received data on it, and continue with its business to handle incoming data.
This is executed right after the UART trigger woke op our FreeRTOS task:
/* Handle received data */ if(telemetry_queue) { if( xQueueSend( telemetry_queue, ( void * ) rxBuffer, ( TickType_t ) 10 ) != pdPASS ) { /* Failed to post the message, even after 10 ticks. */ CY_ASSERT_L3(!0); } }
This is only 2 lines of active code.
- Check if the queue exists (in my code not really needed, because I create it before any logic runs)
- Copy the just received serial data onto the buffer and return to deal with UART data coming in (if any)
The rest is just debugging code that I used to assert that the job was not skipping any packages (queue full).
At this point. The UART part of our code forgets about that data. It is pushed onto the queue. It is now a job for the business logic to eat that queue data.
The mechanism of reading the data off the queue is similar to a trigger. Our receiving task will not consume any CPU, unless a message was pushed onto the queue it is waiting for.
void telemetry_receive_task(void *pvParameters) { for( ;; ) { if(xQueueReceive(telemetry_queue, message, portMAX_DELAY )) { cyhal_gpio_toggle(CYBSP_USER_LED); // // process the telemetry data. // uint32_t checksum = 0; // for (int i = 0; i < sizeof(message); i++) { // checksum += message[i]; // } //// CY_ASSERT_L3((checksum > 0U)); } } // end for }
Again very simple. The task is a loop that does not continuously burn clock ticks.
It has a guard condition: a message should be available on the queue it's watching.
Once a message arrives on the queue, the task wakes up and takes that message.
In our scenario here, that is the same data that was received on the UART port from previous post.
We can then process that message.
In my example here, I toggle a LED . It may sound silly, but when you start developing a mechanism like this, nothing beats a visual clue when you try to see real time reactivity.
The code in comments is there to simulate a real action on the received data.
This is the point in your firmware where you do the handling of that telemetry data you got from the UART port.
The only part of the code that I haven't shown is the creation of the queue. That is simple too:
void initTelemetryQueue() { telemetry_queue = xQueueCreate( 10, TELEMETRY_MESSAGE_SIZE ); }
One line of code. And that reflects the complexity of using queues:
- one line of code to create a message queue
- one line of code to push a message onto it
- one line of code to wait for messages without polling away clock ticks, and retrieve the data.
Maybe by describing the mechanism, what the gains are and how simple it is to do that, I take away some of the mystery of RTOS.
What did I gain by using an RTOS?
This functionality can perfectly be achieved in several other ways. Many of them without an RTOS. I did get some advantages by default though:
If you have another framework or if you are a natural better programmer than the rest, you can do this yourself. Or if your project is trivial. |
The ModusToolbox project is attached.
It's configured for: UART 5.0 and 5.1 (the Debug USB COM port), 9600 / 8 / 1 / N
If you want to use other pins, check uart_task.h. Theres's an example for pins 10.0 and 10.1 that you can use as inspiration...
Top Comments