freertos-en-arduino-esp32-integracion

How to Use FreeRTOS with Arduino and ESP32

  • 5 min

FreeRTOS in Arduino and ESP32 is the integration of the FreeRTOS kernel into the environment we use to program the board.

We already know what an RTOS is and why preemptive multitasking helps us overcome the “Super Loop”. Now it’s time to put this into practice on our board, because running FreeRTOS on an Arduino Uno is not the same as running it on an ESP32.

In this post, we will see how the operating system integrates into the Arduino environment and what prior configuration we need before writing our first line of code.

The ESP32 Case: Native from the Factory

If you use an ESP32 with the official Arduino core, I have good news: you don’t need to install FreeRTOS separately.

The Arduino development environment for ESP32 is built on top of the official Espressif SDK (ESP-IDF). And it turns out the heart of ESP-IDF is FreeRTOS.

This means that when you write code for ESP32 in Arduino, you are already using FreeRTOS, even if you don’t realize it.

The Secret of loop() on ESP32

Even though we write the same void loop(), underneath it doesn’t work the same as on an Arduino Uno.

In the Arduino core for ESP32, there is hidden code (in the core’s main.cpp) that does something like this:

// Simplified pseudocode of the Arduino-ESP32 core
void app_main() {
    initArduino();
    xTaskCreateUniversal(loopTask, "loopTask", 8192, NULL, 1, NULL, ARDUINO_RUNNING_CORE);
}

void loopTask(void *pvParameters) {
    setup();

    for(;;) {
        loop();
        // On single-core variants, the core periodically yields the CPU.
    }
}
Copied!

Your beloved void loop() runs inside a FreeRTOS task called loopTask, with priority 1. The core depends on the model and the Arduino core configuration.

That’s why, on the ESP32, we can mix “Arduino-style” code with FreeRTOS calls (xTaskCreate, vTaskDelay, etc.) without needing any strange #include directives or complex configurations. It’s native.

The Arduino AVR Case (Uno, Nano, Mega)

If you use an 8-bit microcontroller like the ATmega328P (Arduino Uno), the story changes. Here FreeRTOS is not integrated. The microcontroller is much more limited in RAM and speed.

To use it here, we must install it as an external library.

  1. Go to the Library Manager in the IDE.
  2. Search for “FreeRTOS”.
  3. Install a compatible port, such as Arduino_FreeRTOS_Library.

And in our code, we must explicitly include it:

#include <Arduino_FreeRTOS.h>

void setup() {
  // Create tasks here...
}
Copied!

In Arduino_FreeRTOS_Library, the Scheduler starts automatically when setup() ends. Other FreeRTOS ports might require an explicit call, so it’s a good idea to follow the documentation for your specific platform.

An Arduino Uno only has 2 KB of SRAM. The kernel and task stacks consume a significant portion, so the margin is very small. It’s fine for learning, but for projects with a higher workload, it’s usually better to use a 32-bit microcontroller like ESP32, STM32, or SAMD.

Configuration: FreeRTOSConfig.h

FreeRTOS is extremely modular. It can be configured to be very lightweight or to have many features. This is all controlled from a header file called FreeRTOSConfig.h.

In this file, constants are defined using #define that activate or deactivate parts of the Kernel.

If you use ESP32 from the Arduino core, the configuration is part of the precompiled libraries and cannot be changed like another file in the sketch. To customize it, you need to recompile those libraries or integrate Arduino as an ESP-IDF component.

However, it’s important to know some key parameters that define the behavior of our board:

configTICK_RATE_HZ

Defines the frequency of the Tick interrupt (the system’s heartbeat).

  • On ESP32, it is usually configured to 1000 Hz.
  • This means the Scheduler wakes up every 1 ms.
  • Therefore, the minimum time resolution for vTaskDelay is 1 ms.

configMAX_PRIORITIES

The maximum number of available priority levels.

  • On ESP32 it is usually 25.
  • Remember: the higher the number, the higher the task priority. Interrupt priorities use a different scale.

configMINIMAL_STACK_SIZE

The minimum recommended stack size for an empty task.

  • It’s important to avoid wasting RAM.

configUSE_PREEMPTION

  • Set to 1. It confirms that the system is preemptive and not cooperative.

Core 0 or Core 1? (Introduction to Dual Core)

The classic ESP32 has two cores: Protocol CPU (PRO_CPU / Core 0) and Application CPU (APP_CPU / Core 1). Not all ESP32 family members are multi-core, so check the model you are using.

By default in Arduino:

  • Core 0: Runs several system tasks; the WiFi driver is usually pinned here.
  • Core 1: This is the usual core for loopTask on the classic ESP32 with the standard Arduino configuration.

When we create our own tasks, we can decide whether we want the Scheduler to decide where to place them, or if we want to “pin” them to a specific core. But we’ll see that in the “Multicore” section at the end of the course.