Skip to content

FreeRTOS

Overview

FreeRTOS is the most widely deployed real-time operating system for microcontrollers. It provides a minimal, configurable kernel offering preemptive multitasking, inter-task communication, and timing services — all within a flash footprint of roughly 5-10KB. It runs on every major MCU architecture and has become the de facto standard for embedded firmware requiring concurrent task management.

FreeRTOS does not provide a file system, networking stack, or device drivers. It is purely a scheduler and IPC substrate. Everything else is the application's responsibility or provided by add-on middleware (FreeRTOS+TCP, LittleFS, LWIP).

Prerequisites

  • Embedded systems fundamentals (see 01-embedded-systems-fundamentals.md)
  • C programming: function pointers, structs, volatile qualifier
  • Basic OS concepts: scheduling, semaphores, context switching
  • ARM Cortex-M architecture: SysTick, NVIC, PendSV

Historical Context

Richard Barry wrote FreeRTOS in 2003 while consulting for embedded systems clients who needed a free, simple RTOS. Version 1.0 targeted only the PC (WIN32 port for simulation) and the Philips LPC2129 ARM7TDMI. The project grew steadily through community ports to new architectures.

By 2010, FreeRTOS had become the most popular embedded RTOS by survey data. Amazon Web Services acquired it from Richard Barry in 2017, releasing it under the MIT license (previously a modified GPL that had commercial restrictions) and creating AWS FreeRTOS (now FreeRTOS 202x LTS releases) with added connectivity libraries for AWS IoT Core.

The acquisition massively accelerated adoption: manufacturers such as Espressif (ESP32), ST Microelectronics, NXP, and Texas Instruments began shipping FreeRTOS as the standard RTOS in their SDKs. Today, roughly 1 billion devices per year ship with FreeRTOS.


FreeRTOS Architecture

+------------------------------------------------------------+
|                    APPLICATION TASKS                       |
|  Task A (Pri 3)   Task B (Pri 2)   Task C (Pri 1)         |
+---------------------------+--------------------------------+
|        FreeRTOS KERNEL     |                               |
|                            |                               |
|  +----------+  +--------+  |  +---------+  +-----------+ |
|  | Scheduler|  | Queues |  |  | Semaphor|  | EventGrops| |
|  | (tasks.c)|  |        |  |  |         |  |           | |
|  +----------+  +--------+  |  +---------+  +-----------+ |
|  +----------+  +--------+  |  +-----------+              |
|  | SW Timers|  | Heap   |  |  | Task Notif|              |
|  |          |  | heap_4 |  |  |           |              |
|  +----------+  +--------+  |  +-----------+              |
+----------------------------+--------------------------------+
|              PORT LAYER (portable.h / portmacro.h)         |
|   Context switch (PendSV),  SysTick ISR,  Critical section |
+------------------------------------------------------------+
|              HARDWARE (Cortex-M4, RISC-V, Xtensa, etc.)    |
+------------------------------------------------------------+

The portability model separates the kernel (tasks.c, queue.c, timers.c) from the architecture-specific port layer. Porting FreeRTOS to a new architecture requires implementing fewer than 10 functions.


Task Creation and Lifecycle

Creating Tasks

BaseType_t xTaskCreate(
    TaskFunction_t  pvTaskCode,      // function pointer: void f(void *)
    const char     *pcName,          // debug name, up to configMAX_TASK_NAME_LEN
    configSTACK_DEPTH_TYPE usStackDepth, // stack in WORDS (not bytes)
    void           *pvParameters,    // passed to task function
    UBaseType_t     uxPriority,      // 0 = lowest, configMAX_PRIORITIES-1 = highest
    TaskHandle_t   *pxCreatedTask    // optional: handle for later reference
);

Task priorities range from 0 (tskIDLE_PRIORITY) to configMAX_PRIORITIES - 1. The idle task runs at priority 0. Higher number = higher priority. Best practice: reserve the highest priorities for hard-real-time tasks (motor control, safety), mid-priorities for protocol handling, and low priorities for UI/logging.

Task State Machine

                         vTaskSuspend()
              +-------------------------------+
              |                               v
  xTaskCreate()--> [READY] ---schedule--> [RUNNING]
                      ^                       |
                      |                       | blocking call
               event/timeout          (xQueueReceive, vTaskDelay,
                      |                xSemaphoreTake, etc.)
                      |                       v
                      +------<----------- [BLOCKED]

              vTaskSuspend() from another task -> [SUSPENDED]
              vTaskResume()  from another task -> [READY]

Only one task runs at a time per core (FreeRTOS supports SMP on Cortex-M multicore MCUs since v10.x, but single-core is the common case). The scheduler always runs the highest-priority task that is in the Ready state.


The FreeRTOS Tick

The kernel tick is generated by the SysTick timer interrupt on ARM Cortex-M (or equivalent on other architectures). The tick rate is configured by configTICK_RATE_HZ:

  • 100 Hz (10ms tick): Low-power IoT nodes. Coarse time resolution.
  • 1000 Hz (1ms tick): Standard for most applications. Good balance.
  • 10000 Hz (100µs tick): High-frequency control. Higher overhead.

Each tick, the scheduler checks if any blocked tasks have had their timeout expire, moves them to Ready, and if a higher-priority task is now Ready, context-switches to it. Time-slicing (round-robin among equal-priority tasks) also occurs at each tick if configUSE_TIME_SLICING = 1.

Tickless idle: configUSE_TICKLESS_IDLE = 1 suppresses tick interrupts during idle periods, allowing the MCU to sleep deeply. The kernel recalculates elapsed time on wake using the RTC or a free-running timer. This is essential for battery-powered IoT devices.


Context Switching on Cortex-M

FreeRTOS's context switch on ARM Cortex-M is architecturally elegant:

  1. Hardware automatically saves R0-R3, R12, LR, PC, xPSR on exception entry to the task's stack.
  2. The PendSV ISR (lowest-priority exception) saves the remaining registers (R4-R11, and FPU registers if used).
  3. The stack pointer is saved in the current task's TCB.
  4. The new task's stack pointer is loaded from its TCB.
  5. R4-R11 are popped from the new stack.
  6. Exception return pops R0-R3, R12, LR, PC, xPSR from the new stack — resuming the new task exactly where it left off.

The PendSV trick ensures context switches do not occur inside higher-priority ISRs. The SysTick ISR sets the PendSV pending flag; the actual switch defers until all higher-priority ISRs complete.


Inter-Task Communication

Queues

Queues are the fundamental IPC primitive in FreeRTOS. They copy data — sender writes bytes in, receiver reads bytes out — making them safe across tasks without shared-memory hazards.

// Producer
QueueHandle_t xQueue = xQueueCreate(10, sizeof(SensorReading_t));

void vProducerTask(void *pvParams) {
    SensorReading_t reading;
    for (;;) {
        reading = read_sensor();
        // Blocks up to 100ms if queue full
        xQueueSend(xQueue, &reading, pdMS_TO_TICKS(100));
    }
}

// Consumer
void vConsumerTask(void *pvParams) {
    SensorReading_t reading;
    for (;;) {
        // Blocks indefinitely until data available
        if (xQueueReceive(xQueue, &reading, portMAX_DELAY) == pdTRUE) {
            process_reading(&reading);
        }
    }
}

xQueueSendFromISR / xQueueReceiveFromISR are the ISR-safe variants. They take a pxHigherPriorityTaskWoken parameter — if set to pdTRUE, the ISR must call portYIELD_FROM_ISR() at its end to trigger a context switch to the now-unblocked task.

Semaphores

FreeRTOS implements three semaphore types, all built on the queue mechanism:

Binary semaphore: 0 or 1 token. Used for signaling between tasks or from ISR to task.

SemaphoreHandle_t xSem = xSemaphoreCreateBinary();

// ISR: signal task
void UART_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    xSemaphoreGiveFromISR(xSem, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

// Task: wait for signal
void vUARTTask(void *pv) {
    for (;;) {
        xSemaphoreTake(xSem, portMAX_DELAY);
        handle_uart_data();
    }
}

Counting semaphore: N tokens. Used for resource counting — e.g., a pool of 4 available DMA channels.

Mutex: Binary semaphore with priority inheritance. The mutex holder's priority is temporarily elevated to the priority of the highest-priority task waiting for it, preventing priority inversion.

Priority Inversion Without Mutex (use binary semaphore or spinlock):
  High(H) waits for resource held by Low(L)
  Medium(M) preempts Low
  H is blocked behind M -- unexpected priority inversion

Priority Inheritance With Mutex:
  H takes mutex -> blocks, elevates L's priority to H's level
  M cannot preempt L (L now runs at H priority)
  L finishes, releases mutex -> H runs

Recursive mutex (xSemaphoreCreateRecursiveMutex): Allows the same task to take a mutex multiple times without deadlocking. Must be released the same number of times.

Event Groups

Event groups provide 24 bits of independent flags. Tasks can wait for any or all bits to be set:

EventGroupHandle_t xEventGroup = xEventGroupCreate();

#define WIFI_CONNECTED_BIT    (1 << 0)
#define SENSOR_DATA_READY_BIT (1 << 1)
#define TIME_SYNCED_BIT       (1 << 2)

// Wait for ALL three conditions before proceeding
EventBits_t bits = xEventGroupWaitBits(
    xEventGroup,
    WIFI_CONNECTED_BIT | SENSOR_DATA_READY_BIT | TIME_SYNCED_BIT,
    pdTRUE,    // clear on exit
    pdTRUE,    // wait for ALL (not any)
    pdMS_TO_TICKS(10000)
);

Event groups are especially useful for synchronization rendezvous — waiting until multiple conditions are met before starting an operation.

Task Notifications

Introduced in FreeRTOS v8.2. Each task has a built-in 32-bit notification value. Faster than queues or semaphores (no separate object allocation, no blocking queue data structure). Useful for simple signaling from ISR to task:

// From ISR: notify specific task
vTaskNotifyGiveFromISR(xTaskHandle, &xHigherPriorityTaskWoken);

// In task: wait for notification
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);  // pdTRUE = decrement to 0

Memory Management

FreeRTOS ships five heap implementations:

Scheme Algorithm Free Supported Fragmentation Use Case
heap_1 Static, no-free No None Simple, fixed allocation
heap_2 Best-fit, no coalescence Yes Medium Historical, deprecated
heap_3 Wraps libc malloc/free Yes libc-dependent Existing malloc port
heap_4 First-fit with coalescence Yes Low Most common choice
heap_5 heap_4 over multiple RAM regions Yes Low Multiple SRAM banks

heap_4 is the right choice for most projects: first-fit allocation with adjacent-block coalescence on free. It handles fragmentation well for typical embedded allocation patterns.

Memory from pvPortMalloc comes from a statically defined array (ucHeap[configTOTAL_HEAP_SIZE]). You can pre-calculate required heap using xPortGetFreeHeapSize() during development and trim configTOTAL_HEAP_SIZE accordingly.

Static allocation (xTaskCreateStatic, xQueueCreateStatic) avoids heap entirely — all TCBs, stacks, and queue storage are statically allocated. Recommended for safety-critical applications where heap exhaustion must be provable.


Stack Overflow Detection

FreeRTOS provides two stack overflow checking mechanisms, configured by configCHECK_FOR_STACK_OVERFLOW:

  • Method 1: Check stack pointer bounds on context switch. Fast, misses transient overflows.
  • Method 2: Checks that the last 16 bytes of the stack contain the known fill pattern (0xA5) on every context switch. Catches overflows that happened between switches.
void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) {
    // Called when overflow detected
    // Log pcTaskName, then halt or reset
    configASSERT(0);  // Trigger hard fault for debugger
}

If you see corrupt task names or a stack overflow hook firing, the immediate fix is to increase the guilty task's stack size. A long-term audit involves measuring actual stack usage with uxTaskGetStackHighWaterMark().


configASSERT and Runtime Checks

#define configASSERT(x) if ((x) == 0) { taskDISABLE_INTERRUPTS(); for(;;); }

FreeRTOS uses configASSERT internally to catch programmer errors (calling blocking API from ISR, incorrect queue parameters, etc.). Enable it during development and testing; it can be disabled for final release to save code space and cycles.


FreeRTOS on ESP32

The Espressif IoT Development Framework (ESP-IDF) uses a modified FreeRTOS supporting the dual-core Xtensa LX6 (or LX7 on ESP32-S3). Key differences from vanilla FreeRTOS:

  • Symmetric Multi-Processing: Tasks can be pinned to Core 0 or Core 1, or float between cores. xTaskCreatePinnedToCore() replaces xTaskCreate().
  • Core 0 runs Wi-Fi/BT stack (system tasks). Core 1 is available for application tasks. Keep computationally heavy or latency-sensitive work on Core 1.
  • ESP-IDF components wrap FreeRTOS: esp_event for system events, esp_timer for high-resolution timers, nvs_flash for persistent storage.
// ESP-IDF: create task pinned to Core 1
xTaskCreatePinnedToCore(
    vSensorTask, "sensor", 4096, NULL, 5, &xSensorTaskHandle, 1 /*core*/
);

AWS IoT Integration

The AWS IoT Device SDK for Embedded C integrates with FreeRTOS and provides:

  • coreMQTT: Lightweight MQTT 3.1.1 client
  • coreHTTP: HTTP/1.1 client
  • AWS IoT OTA: Over-the-air firmware updates with AWS code signing verification
  • Fleet Provisioning: Automated device certificate provisioning
  • FreeRTOS+TCP: Full TCP/IP stack compatible with FreeRTOS kernel

This stack runs on ESP32, STM32, and NXP i.MX RT devices, enabling fully managed IoT endpoints with TLS 1.3, certificate-based authentication, and AWS IoT Core broker integration.


Debugging Notes

  • Viewing task states in GDB: With FreeRTOS-aware GDB plugins (OpenOCD has built-in support), monitor freertos tasklist shows all tasks, their states, and stack usage.
  • vTaskList(): Prints a table of task names, states, priorities, and stack high-water marks to a character buffer. Invaluable during development.
  • Deadlock diagnosis: Classic FreeRTOS deadlock: Task A holds Mutex X and waits for Mutex Y. Task B holds Mutex Y and waits for Mutex X. Both block forever. Detection: uxTaskGetStackHighWaterMark stops decreasing for both tasks; both in Blocked state indefinitely. Prevention: always acquire mutexes in a consistent global order.
  • ISR API misuse: Calling xQueueSend (non-ISR variant) from an ISR will corrupt the kernel. Most versions of FreeRTOS detect this with configASSERT checking xPortIsInsideInterrupt(). Enable assertions.
  • Priority inversion: Watch for high-priority tasks unexpectedly missing deadlines. Profile with a logic analyzer or DWT_CYCCNT timestamps. Ensure mutexes — not binary semaphores — are used for mutual exclusion between tasks of different priorities.

Security Implications

  • Heap isolation: FreeRTOS has no memory protection between tasks. A stack overflow or out-of-bounds pointer in one task can corrupt another task's stack or the kernel TCB. MPU-enabled Cortex-M MCUs can configure stack guard regions per task using FreeRTOS MPU port.
  • FreeRTOS-MPU port: Available for Cortex-M3/M4/M7/M33. Defines privileged and unprivileged tasks. Unprivileged tasks run in unprivileged mode with MPU-enforced memory boundaries. Critical for security-sensitive embedded devices.
  • AWS OTA security: The OTA library verifies firmware images using asymmetric code signing (ECDSA P-256) before applying updates. Private keys are managed in AWS Key Management Service, never on the device.
  • Denial of service via task starvation: A runaway high-priority task that never blocks will starve all lower-priority tasks indefinitely. Watchdog timers and cooperative yield points mitigate this.

Performance Implications

  • Context switch time: On Cortex-M4 @ 168MHz, a FreeRTOS context switch costs approximately 180-200 CPU cycles (~1.2µs). At 1kHz tick rate this is negligible overhead.
  • Queue copy cost: Queues copy data by value. For large structures (>32 bytes), pass a pointer to a memory pool object instead. Combine with a counting semaphore to manage pool slots.
  • Interrupt latency: FreeRTOS uses BASEPRI masking on Cortex-M (not PRIMASK), allowing only ISRs below configMAX_SYSCALL_INTERRUPT_PRIORITY to use FreeRTOS APIs. Hardware-critical ISRs (e.g., motor PWM updates) can run at higher priority, completely unaffected by the scheduler.
  • portTICK_PERIOD_MS: Always use pdMS_TO_TICKS() macro for time conversion rather than hardcoded values — portability across different tick rates.

Failure Modes

  • Heap exhaustion: pvPortMalloc returns NULL. Without NULL checks on task/queue creation, the system silently fails to create objects. Production code must check every xTaskCreate return value.
  • Queue full under load: If producer outpaces consumer and queue is full, xQueueSend with zero timeout drops the item silently. Size queues generously; add overflow counters for diagnostics.
  • Timer task starvation: Software timer callbacks execute in the daemon task (configTIMER_TASK_PRIORITY). If this task is starved by higher-priority tasks, timers fire late. Set timer task priority appropriately.
  • Watchdog not fed: If a high-priority task blocks indefinitely (e.g., waiting for network that never connects), a watchdog fed only by that task will reset the device. Structure watchdog feeding to require all critical tasks to report liveness.

Modern Usage

  • FreeRTOS SMP (v11+): Official symmetric multiprocessing support for dual-core MCUs.
  • FreeRTOS+POSIX: Thin POSIX threading API layer over FreeRTOS — enables portability of POSIX-based application code to embedded.
  • IDE integration: STM32CubeIDE, MCUXpresso, and ESP-IDF all include FreeRTOS-aware debuggers with task-level breakpoints and stack visualization.
  • Formal verification: AWS has begun applying formal verification (using CBMC) to FreeRTOS kernel components — particularly the queue and list implementations. Safety certification programs for DO-178C and IEC 61508 use FreeRTOS as a starting point.

Future Directions

  • FreeRTOS Kernel v11+: Continued SMP improvements, tighter integration with hardware security features (TrustZone on M33/M55/M85).
  • POSIX PSE51 compliance: Making FreeRTOS+POSIX a complete PSE51 profile implementation for portability with automotive AUTOSAR stacks.
  • Integration with Zephyr: Some cross-pollination of concepts (device tree, West build system) as both ecosystems learn from each other.
  • IoT security baseline: AWS continues adding features like Device Defender (anomaly detection) and Greengrass integration to the FreeRTOS device ecosystem.

Exercises

  1. Create a FreeRTOS application with three tasks: a sensor reader at priority 3, a data processor at priority 2, and a UART logger at priority 1. Pass data between them using queues. Measure queue depth under load with uxQueueMessagesWaiting().
  2. Implement a mutex-protected resource (a shared SPI bus) accessed by two tasks. Deliberately induce priority inversion by adding a medium-priority task that runs during the low-priority task's critical section. Observe and then fix using a proper mutex.
  3. Enable configCHECK_FOR_STACK_OVERFLOW = 2 and deliberately overflow a task's stack. Observe the hook firing. Use uxTaskGetStackHighWaterMark() on all tasks to find the true stack usage.
  4. Implement tickless idle on an STM32L4. Measure sleep current with and without tickless idle. Calculate expected battery life for a 500mAh battery given your measured currents and duty cycle.
  5. Profile the worst-case interrupt latency from GPIO edge to task unblocked, using a second GPIO pin toggled in the ISR and measured on an oscilloscope. Repeat with different numbers of active FreeRTOS tasks. Does task count affect latency?

References

  • Richard Barry, Mastering the FreeRTOS Real Time Kernel (Real Time Engineers Ltd, 2016, free PDF at FreeRTOS.org)
  • FreeRTOS API Reference: https://www.freertos.org/a00106.html
  • FreeRTOS Source Code: https://github.com/FreeRTOS/FreeRTOS-Kernel
  • AWS FreeRTOS Developer Guide: https://docs.aws.amazon.com/freertos/latest/userguide/
  • ESP-IDF FreeRTOS SMP Guide: https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/freertos-smp.html
  • Jack Ganssle, "A Guide to Debouncing": https://www.ganssle.com/debouncing.htm (complements ISR design patterns)
  • Joseph Yiu, The Definitive Guide to ARM Cortex-M3 and Cortex-M4 Processors, Ch. 9-10 (exception handling, NVIC)