FreeRTOS is a game changer when developing embeded applications with even modest complexity. By breaking actions into seperate tasks it becomes trivial to maintain tight timing control across multiple subsystems and assigning a priority to these processes in the scheduler. It also enables event driven behavior across tasks, blocking tasks until there is something to process, or feeding data from multiple sources into a central processor via a queue.
The ESP32 is my MCU of choice. It is cheap, powerful and has integrated WiFi/BLE. I also choose to develop most applications using the Arduino framework. It is significantly faster and easier to understand than vanila C and time is precious. Although the native ESP toolchain, esp-idf, is C based with FreeRTOS integrated from scratch.
Thankfully the Arduino stack is built on top of esp-idf, this means we get the best of both worlds, we can use Arduino and FreeRTOS in the same project.
FreeRTOS refresher
Need an intro to FreeRTOS? This is a great resource. It covers all fo the exciting things:
- Tasks
- Semaphores
- Queues
etc…
Practical example
How does this work in a practical context? Want to do more than flash an LED?
As a proof of concept I wanted to update a display based on data changed in another task. For this is used the TTGO T-Display and printed a counter value to the display
This project has two tasks:
- Display management
- Counter incrementer
Behavior between the tasks is moderated by a binary semaphore. This is essentially a batton passed between tasks. It blocks the display from updating, until another process has performed an action that would require the display to be updated. In this example, incrementing the counter triggers the display to be updatedTask structure
All tasks have a few requirements: - Must loop forver, no return
- must be type void with argument (void * params)
Display management
1 | void displayTask(void * params){ |
2 | for( ; ; ) |
3 | { |
4 | // Block task until another task gives the semaphore dispSemaphore |
5 | xSemaphoreTake(dispSemaphore, portMAX_DELAY); |
6 | |
7 | // Update the display |
8 | tft.fillScreen(0x0000); |
9 | tft.setCursor(0, 0, 2); |
10 | tft.setTextColor(TFT_WHITE,TFT_BLACK); tft.setTextSize(1); |
11 | tft.println("Hello World!"); |
12 | tft.setTextColor(TFT_YELLOW); tft.setTextFont(7); |
13 | tft.println(counter); |
14 | |
15 | // Sleep task for 500 mSec so we dont spam display |
16 | vTaskDelay(500/ portTICK_PERIOD_MS); |
17 | } |
18 | } |
Notably, you could skip the vTaskDelay. When using FreeRTOS, this replaces the delay()
function in Arduino native which blocks other processes from running and causes the watchdog to panic. The code also uses a global value counter
this could also be managed with a queue and object struct instead
Incrementer
1 | void incrementer(void * params){ |
2 | while(1){ |
3 | counter++; |
4 | // Print information to console |
5 | ESP_LOGI( TAG, "counter: %i", counter); |
6 | // Update the display |
7 | xSemaphoreGive(dispSemaphore); |
8 | vTaskDelay( 5000 / portTICK_PERIOD_MS); |
9 | } |
10 | } |
Here the task runs every five seconds, giving the semaphore once the global value is updated
Initialisation
I am relatively strict for Arduino projects. Hardware and communication initialisation happens in void setup()
and application behavior is managed in void loop()
however this causes a few problems with FreeRTOS. As tasks run continuously once initialised so you only want to call vTaskCreate()
once for each task.
1 | void loop() { |
2 | xTaskCreate( &displayTask, "display Manager", 1024 * 36, NULL , 20, NULL); |
3 | xTaskCreate( &incrementer, "summer", 2046, NULL , 2, NULL); |
4 | while(1){ |
5 | // Spin main loop forever |
6 | } |
7 | } |
Semaphore
The semaphore must also be defined globally and initalised. I initialised the semaphore in the void setup()
function
1 | xSemaphoreHandle dispSemaphore; |
1 | dispSemaphore = xSemaphoreCreateBinary(); |
Profit
This framework is enough to start working with event driven systems on an ESP32. Running multiple processes in ‘parallel’ so that sensors, outputs and displays are updated at the appropriate intervals.