Logo
FreeRTOS Demystified: How Embedded Multitasking Works

FreeRTOS Demystified: How Embedded Multitasking Works

Sony Sunny7 min read

Embedded systems are everywhere — from smart thermostats to automotive electronics — and many require precise control over multiple tasks running simultaneously.

FreeRTOS is a lightweight, open-source real-time operating system (RTOS) designed to help developers achieve efficient multitasking on microcontrollers with minimal overhead.

This post breaks down how FreeRTOS works and adds a hands-on setup guide for popular microcontrollers, including a Blinky example you can compile today.


⚙️ What Is FreeRTOS?

FreeRTOS is a deterministic kernel that manages multiple independent tasks (threads), each with its own stack, priority, and state.
It focuses on predictable timing, small memory footprint, and simple APIs—ideal for resource-constrained MCUs.


🧩 Why Multitasking Matters

A single, sequential while(1) loop can cause jitter and missed deadlines. FreeRTOS lets you split logic into independent tasks and schedule them predictably based on priority and timing.


🔁 Scheduling & Context Switching (Quick Recap)

  • Preemptive scheduling: high-priority tasks preempt lower ones.
  • Cooperative: tasks yield explicitly.
  • Context switch saves/restores CPU registers and stack for each task—done in microseconds.

📨 Communication Primitives (Quick Recap)

  • Queues for passing data.
  • Semaphores for signaling.
  • Mutexes for protecting shared resources.
    Use these to avoid races and blocking in high-priority code.

🛠️ Set Up FreeRTOS on Microcontrollers (Step-by-Step)

Below are universal steps plus platform-specific notes for STM32, ESP32, and Arduino-class boards.

1) Get the Code

  • Option A (vendor-integrated):
    • STM32CubeIDE: enable FreeRTOS in CubeMX middleware.
      • Downloads the correct FreeRTOS kernel
      • Picks the right port layer (for STM32F4 → portable/GCC/ARM_CMx)
      • Generates the full FreeRTOSConfig.h
    • ESP-IDF: FreeRTOS is built in (no extra setup).
    • Arduino:
      • For AVR (Uno/Nano): install FreeRTOS by Richard Barry (Arduino port)
      • For SAMD (MKR/Zero) or RP2040 (Pico): install Arduino_FreeRTOS or FreeRTOS-RP2040 (both use the same API).
  • Option B (source):
    • Download from the official FreeRTOS repo. Add Source/ and the correct portable/ port to your project. Refer to the References section below.

2) Choose the Port Layer

Pick the correct port for your MCU/CPU:

  • ARM Cortex-M: portable/GCC/ARM_CMx is selected automatically if you are using cubeMX middleware.
  • ESP32: ESP-IDF already ships with an integrated FreeRTOS SMP (Symmetric Multi-Processing) port.
  • AVR/SAMD: use the FreeRTOS by Richard Barry library → it automatically includes the AVR or SAMD port.

3) Create/Configure FreeRTOSConfig.h

This file tunes the kernel. Start minimal:

// FreeRTOSConfig.h (Cortex-M example)
#define configUSE_PREEMPTION                    1
#define configCPU_CLOCK_HZ                      (SystemCoreClock)
#define configTICK_RATE_HZ                      ((TickType_t)1000)    // 1ms tick
#define configMAX_PRIORITIES                    5
#define configMINIMAL_STACK_SIZE                ((unsigned short)128)  // tune per MCU
#define configTOTAL_HEAP_SIZE                   ((size_t)(16*1024))    // tune per project
#define configUSE_MUTEXES                       1
#define configUSE_COUNTING_SEMAPHORES           1
#define configUSE_TIMERS                        1
#define configTIMER_TASK_PRIORITY               2
#define configTIMER_QUEUE_LENGTH                10
#define configTIMER_TASK_STACK_DEPTH            256
// Debug
#define configCHECK_FOR_STACK_OVERFLOW          2
#define configUSE_MALLOC_FAILED_HOOK            1
 

Timing tips

  • configTICK_RATE_HZ: 1 kHz (1 ms tick) is common; reduce to 100–250 Hz to save power if you can.
  • Interrupt priorities (Cortex-M): set configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY and keep ISRs that call FreeRTOS APIs at/under that priority. Never call FreeRTOS APIs from very high-priority ISRs.

Select a Heap Scheme

  • FreeRTOS provides heap allocators in portable/MemMang/:
  • heap_4.c: general-purpose, coalescing (good default)
  • heap_5.c: like heap_4 but supports multiple regions
  • heap_1.c: very simple, no free
  • Include exactly one in your build and set configTOTAL_HEAP_SIZE.

Hardware Tick & Startup

  • Cortex-M: the port uses SysTick or a timer for the RTOS tick. Ensure SysTick_Handler or port-specific tick handler is linked (CubeMX handles this).
  • ESP32: tick and SMP scheduling are built into ESP-IDF.
  • Arduino: library sets up the tick; avoid long blocking in loop().

Build Integration

  • STM32CubeIDE: CubeMX generates FreeRTOS files and includes automatically.
  • CMake (generic):
# CMakeLists.txt (snippet)
add_subdirectory(FreeRTOS-Kernel)  # if you vendored the kernel as a submodule
target_link_libraries(my_firmware PRIVATE freertos_kernel)
target_include_directories(my_firmware PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/FreeRTOS-Kernel/include)
  • ESP-IDF: FreeRTOS is part of the SDK—no extra linking.

Sanity Checks

  • Enable stack overflow and malloc failed hooks.
  • Start with few tasks, small stacks (256–512 bytes), and measure high-water marks with uxTaskGetStackHighWaterMark().

💡 Blinky: The Smallest Useful FreeRTOS App

Below are three versions of Blinky so you can run it on your favorite platform.


A) STM32 (HAL) — Two FreeRTOS Tasks on STM32 (C + HAL)

#include "main.h"
#include "cmsis_os.h"
#include "FreeRTOS.h"
#include "task.h"
 
/* --- Configure your LED pins here --- */
#define LED1_PIN GPIO_PIN_5      // Example: Nucleo onboard LED (PA5)
#define LED1_PORT GPIOA
 
#define LED2_PIN GPIO_PIN_6      // Second LED (connect manually if needed)
#define LED2_PORT GPIOA
 
extern UART_HandleTypeDef huart2;  // UART2 handle from CubeMX
 
/* --- Task Handles (optional) --- */
TaskHandle_t BlinkLED1Handle = NULL;
TaskHandle_t BlinkLED2Handle = NULL;
 
/* --- Task 1: Blink LED1 --- */
void TaskBlinkLED1(void *pvParameters)
{
  (void) pvParameters;
  for (;;)
  {
    HAL_GPIO_TogglePin(LED1_PORT, LED1_PIN);
    vTaskDelay(pdMS_TO_TICKS(500));  // Blink every 500ms
  }
}
 
/* --- Task 2: Blink LED2 and print --- */
void TaskBlinkLED2(void *pvParameters)
{
  (void) pvParameters;
  const char *taskName = pcTaskGetName(NULL);
 
  for (;;)
  {
    HAL_GPIO_TogglePin(LED2_PORT, LED2_PIN);
 
    char msg[50];
    snprintf(msg, sizeof(msg), "Running: %s\r\n", taskName);
    HAL_UART_Transmit(&huart2, (uint8_t *)msg, strlen(msg), HAL_MAX_DELAY);
 
    vTaskDelay(pdMS_TO_TICKS(1000));  // Blink every 1 second
  }
}
 
/* --- Main Setup --- */
int main(void)
{
  HAL_Init();
  SystemClock_Config();
 
  /* --- GPIO & UART init (CubeMX-generated normally) --- */
  MX_GPIO_Init();
  MX_USART2_UART_Init();
 
  /* --- Create Tasks --- */
  xTaskCreate(TaskBlinkLED1, "LED1", 128, NULL, 1, &BlinkLED1Handle);
  xTaskCreate(TaskBlinkLED2, "LED2", 256, NULL, 1, &BlinkLED2Handle);
 
  /* --- Start Scheduler --- */
  vTaskStartScheduler();
 
  /* --- Should never reach here --- */
  for (;;);
}
 
 

Notes (STM32):

  • If using CubeMX, enable FreeRTOS middleware and it will autogenerate kernel files.
  • Make sure the SysTick is owned by FreeRTOS (Cube handles this).
  • Tune stack sizes; 256–512 bytes is typical for simple tasks.

B) ESP32 (ESP-IDF) — GPIO 2 (built-in LED on many boards)

// blinky_freertos_esp32.c
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
 
#define LED_PIN GPIO_NUM_2
 
void vTaskBlink(void *pvParameters) {
  (void) pvParameters;
  bool on = false;
  for (;;) {
    gpio_set_level(LED_PIN, on);
    on = !on;
    vTaskDelay(pdMS_TO_TICKS(500));
  }
}
 
void app_main(void) {
  gpio_config_t io = {
    .pin_bit_mask = 1ULL << LED_PIN,
    .mode = GPIO_MODE_OUTPUT,
    .pull_up_en = 0,
    .pull_down_en = 0,
    .intr_type = GPIO_INTR_DISABLE
  };
  gpio_config(&io);
 
  xTaskCreate(vTaskBlink, "blink", 2048, NULL, 1, NULL);
}
 

Notes (ESP32):

  • Build with idf.py build flash monitor.
  • Task stacks are larger on ESP32 due to newlib/printf; 2048 is a safe starting point.

C) Arduino-Class Boards (AVR/SAMD/RP2040) — LED on LED_BUILTIN

// blinky_freertos_arduino.ino
#include <Arduino_FreeRTOS.h>
 
// LED pin
#define LED_PIN LED_BUILTIN
 
// Handle for tasks (optional)
TaskHandle_t blink1Handle = NULL;
TaskHandle_t blink2Handle = NULL;
 
void TaskBlink1(void *pvParameters) {
  (void) pvParameters;
  pinMode(LED_PIN, OUTPUT);
 
  for (;;) {
    digitalWrite(LED_PIN, HIGH);
    vTaskDelay(pdMS_TO_TICKS(500));  // 500 ms
    digitalWrite(LED_PIN, LOW);
    vTaskDelay(pdMS_TO_TICKS(500));
  }
}
 
void TaskBlinkAndPrint(void *pvParameters) {
  (void) pvParameters;
  pinMode(LED_PIN, OUTPUT);
 
  for (;;) {
    digitalWrite(LED_PIN, !digitalRead(LED_PIN));
    Serial.print("Running: ");
    Serial.println(pcTaskGetName(NULL));
    vTaskDelay(pdMS_TO_TICKS(1000));
  }
}
 
void setup() {
  Serial.begin(115200);
  while (!Serial) ; // wait for serial port (needed on SAMD/RP2040)
 
  xTaskCreate(
    TaskBlink1,
    "Blink1",
    128,     // stack words (not bytes)
    NULL,
    1,       // priority
    &blink1Handle
  );
 
  xTaskCreate(
    TaskBlinkAndPrint,
    "BlinkPrint",
    256,
    NULL,
    1,
    &blink2Handle
  );
 
  vTaskStartScheduler();  // never returns
}
 
void loop() {
  // not used when scheduler is running
}
 

Notes (Arduino):

  • Install Arduino_FreeRTOS (if not included in your core).
  • Keep stacks small; avoid heavy printf.
  • Some cores start the scheduler for you; others require vTaskStartScheduler().

🧱 Extend Blinky with a Second Task (Optional)

Add a heartbeat logger to demonstrate multitasking:

void vTaskHeartbeat(void *pvParameters) {
  (void) pvParameters;
  for (;;) {
    // e.g., toggle a second LED or print a dot
    // printf(".\n");  // avoid in tiny MCUs
    vTaskDelay(pdMS_TO_TICKS(1000));
  }
}
 
int main(void) {
  // ...hardware init...
  xTaskCreate(vTaskBlink,     "Blink",     256, NULL, 1, NULL);
  xTaskCreate(vTaskHeartbeat, "Heartbeat", 256, NULL, 1, NULL);
  vTaskStartScheduler();
  while (1) {}
}
 

Common Pitfalls & Pro Tips

  • Stack overflows: enable configCHECK_FOR_STACK_OVERFLOW=2 and implement vApplicationStackOverflowHook().
  • Heap exhaustion: set configTOTAL_HEAP_SIZE realistically; implement vApplicationMallocFailedHook().
  • ISRs using FreeRTOS: only call xxxFromISR() APIs; end with portYIELD_FROM_ISR() as required.
  • Priorities: don’t make everything priority 3+. Reserve high priorities for hard real-time loops.
  • Timing: use vTaskDelayUntil() for periodic tasks to avoid drift.

🧠 Summary

You now have:

  • A solid conceptual model of tasks, priorities, delays, and IPC in FreeRTOS.
  • A repeatable setup flow for STM32, ESP32, and Arduino-class boards.
  • A Blinky you can build and extend into a multi-task demo.
  • FreeRTOS gives you determinism without bloat—perfect for professional embedded systems that must be responsive and maintainable.

References

GitHub-ready code — Takeaway!
GitHub-ready code — Takeaway!
💬

Comments

Loading comments…

Leave a Comment