How I Used FreeRTOS Queues for Safe Task Communication in a Real Project

January 07, 2025

Introduction

If you're diving into embedded systems or working with real-time tasks, you probably know that managing resources efficiently is key to keeping your system running smoothly. But let’s be honest—sometimes, sharing resources between tasks can be overwhelming. When not done properly, it can lead to tricky issues like memory corruption or tasks stepping on each other's toes.

In this article, I want to share with you how FreeRTOS queues can help you avoid these pitfalls by allowing tasks to communicate safely and efficiently. I’ll walk you through a project I've worked before, a monitoring school transportation system using RFID tags to identify students and verify their payment status and track bus position. The system required several tasks, including managing RFID tag readers, sending data to a remote server, and alerting users with a buzzer and LEDs based on the payment status.

Project Overview

The goal of the project was to create a real-time system for monitoring school buses, integrating multiple components such as RFID reader, a GPRS/GPS modem for server communication and location tracking, and an alert system for notifying drivers and students of the payment status.

School Bus Monitoring System Diagram

Key Firmware Components:

  • RFID Reader: Identifies students as they board the bus.
  • SIM7000G Modem (GPRS/GPS): Responsible for sending telemetry and position data to a server and downloading the latest student payment status.
  • Payment and Alert System: Verifies payment status and provides feedback via a buzzer and LED.
  • UI Feedback Module: Manages LED indicators to display network and GPS status.
  • Server Communication: Periodically syncs with a remote server to update user payment statuses and flag overdue payments.

This system posed several challenges, such as managing data from different peripherals, ensuring timely verification of payments, and avoiding blocking operations, all while keeping the system scalable.

FreeRTOS Architecture

FreeRTOS allowed us to structure the project into distinct tasks that communicated via queues, ensuring that no single task would block the others. This approach allowed us to prioritize tasks, maintain real-time performance, and achieve concurrency across components. The main architecture is divided into three components:

  1. FreeRTOS Tasks: The system was divided into separate tasks, each responsible for managing different aspects of the system (blue).
  2. Queues: Used to pass data between tasks, such as sending RFID tag data from the reader to the payment verification task (green).
  3. Support Modules: These include peripherals such as RFID readers, modems, and the alert system (Red).

FreeRTOS Task Breakdown

  • TagReader Task: This task is responsible for reading RFID tags when students enter the bus.
  • Modem Controller Task: Manages GPS and the GPRS connection, syncs with the server to retrieve updated user lists, and reports telemetry data and bus positions.
  • Device Controller Task: This task verify the corresponding user’s payment status. If the student’s payment is up-to-date, the system signals success through a short buzzer beep and a green LED. If a payment is overdue, a long buzzer alert and a red LED notify both the student and the driver.
  • UI Task (LED Status): Monitors network and GPS status, controlling LED indicators.

Firmware FreeRTOS Architecture

How Queues Help Avoid Resource Conflicts

I know, you might say that we can use mutexes or semaphores for sharing resources between tasks. However, since this project is mostly event-based, I prefer to use queues.

Using queues in FreeRTOS is a simple yet powerful way to prevent errors when sharing data between tasks. Instead of having tasks access shared memory directly, which could lead to memory corruption or race conditions, tasks send data to each other through a queue. This ensures that each task gets the data it needs at the right time, without interrupting the others.

Here’s how I used queues in this project:

Sharing RFID Tags

  • The TagReader Task reads an RFID tag and places the data into the tagPaymentQueue, which is processed by the Device Controller Task. Then the Device Controller Task for payment validation and alert generation (buzzer and led activation).
  • The same TagReader Tasksenda tag into the tagTelemetryQueue, which the Modem Controller Task uses for telemetry reporting.

The beauty of this approach is that the TagReader Task doesn’t have to wait for the validation process to finish—it can immediately move on to read the next tag. This is key to maintaining performance and ensuring that the system can handle multiple tags without delays.

Sharing Modem Controller Status Payment Validator to UI Task

The Modem Controller Task shares network and GPS status with the UI Task via the uiQeueue allowing the UI to update LED indicators when is required.

Again, this is a non-blocking operation and event-based approach, eliminates the need for the UI to constantly poll the modem controller for status updates.

Full System Architecture with Queues

Why Queues Are So Effective

I’ve learned that queues are a lifesaver when it comes to inter-task communication. Here’s why:

  • No Blocking: Tasks can communicate without waiting for each other to finish, allowing them to run in parallel and improving the system’s responsiveness.
  • Prevents Memory Corruption: Instead of directly accessing shared memory, tasks send data through queues, ensuring that no task accidentally corrupts another task’s data.
  • Scalable Design: As the project grows, you can easily add more tasks and new modules without disrupting the existing ones. Queues allow tasks to interact safely and asynchronously.

In this project, queues are what made it possible for each task to operate independently while still sharing necessary data. It’s a crucial design decision that allows the system to handle multiple operations without breaking down.

Final Tips for Effective Task Management and Resource Sharing

Before we wrap up, here are some additional tips to keep in mind as you work with FreeRTOS tasks and queues:

Manage Queue Size Appropriately

It’s important to configure the queue size based on the amount of data your tasks need to share. Too small of a queue, and you might lose data if it overflows. Too large, and you could end up using unnecessary memory. Always adjust the queue size to fit your system’s needs, balancing between memory usage and task communication speed.

Set Task Priorities Based on Application Needs

Each task in your system has a different level of importance, and FreeRTOS allows you to set priorities for each task. For example, the TagRead Controller Task handles critical operations and we don't want to lose some RFID tags values, so we want to give it a higher priority than the UI Feedback Task. Tailor the priorities according to your application's real-time requirements to ensure the most important tasks get executed first.

Design Tasks Around Domain or Features

When organizing tasks, it’s essential to group them based on domains or features of your system. In my case, I created tasks based on distinct modules, each handling its own specialized function:

  • TagReader Task: Dedicated solely to reading RFID tags. This keeps the task focused on one job, making it easier to maintain and debug.
  • Modem Controller Task: Responsible only for GSM/GPRS, GPS, and internet connection management. By isolating this functionality, I ensure that the modem task doesn’t get bogged down by unnecessary concerns.
  • Payment Validator Task: This task is all about validating the user’s payment status, which involves checking the database. It has a specific role, so it doesn’t have to worry about other parts of the system.
  • UI Feedback Task: This one handles everything related to user feedback—updating the LED indicator. By dedicating a task to UI feedback, it remains responsive and doesn’t interfere with the critical logic of the system.

Designing tasks with a clear domain in mind makes it easier to maintain, test, and scale your system. Each task knows exactly what it’s responsible for and doesn’t take on unnecessary duties, keeping the system organized and manageable.

Conclusion

Resource sharing can be challenging, especially when tasks need to access the same data. Without the right mechanisms, memory corruption or race conditions can easily occur. However, by using FreeRTOS queues, I was able to keep tasks organized and efficient, ensuring smooth communication and safe resource sharing in this real-time embedded system.

I hope you found this insight helpful! Feel free to take the lessons learned here and apply them to your own projects. And if you run into any challenges, remember—queues are your friend.


Profile picture

Written by Marco Ciau who is apassionate about providing solutions by using software. I thoroughly enjoy learning new things and am always eager to embrace new challenges. You can follow me on Twitter.