Skip to content

FreeRTOS — Real-Time Operating System for Embedded IoT Guide

DodaTech Updated 2026-06-24 6 min read

In this tutorial, you'll learn about FreeRTOS. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

FreeRTOS is a real-time operating system (RTOS) kernel for embedded devices that provides deterministic task scheduling, inter-task communication, and synchronization primitives, enabling reliable multitasking on resource-constrained IoT microcontrollers.

Why FreeRTOS Matters

Bare-metal (main loop) firmware becomes unmanageable as complexity grows. A sensor that must read temperature every 10 seconds, publish MQTT messages, handle OTA updates, and debounce a button requires careful timing in a loop. Miss one delay and everything breaks. FreeRTOS allows you to write each concern as a separate task with its own timing. The kernel preemptively schedules tasks based on priority, ensuring critical operations (like reading a safety sensor) always run before non-critical ones (like updating a display). FreeRTOS runs on everything from tiny Cortex-M0 chips to ESP32 dual-core processors. Amazon's IoT devices use FreeRTOS as the foundation for AWS IoT connectivity. ESP32 ships with FreeRTOS as its core OS.

Plain-Language Explanation

Think of bare-metal firmware as a single chef in a kitchen. To make a three-course meal, the chef must chop vegetables, stir the soup, check the oven, and plate the starter in strict sequence. If the chef spends too long chopping, the soup burns.

FreeRTOS is like having multiple chefs. One chef constantly stirs the soup (sensor reading task). Another watches the oven (MQTT publishing task). A third preps vegetables (button input task). They communicate by leaving notes on a board (queues) and ringing a bell when something is ready (semaphores). Each chef focuses on their job, and the head chef (kernel) decides who works at any moment.

graph TD
    subgraph "FreeRTOS Tasks"
        T1[Task: Sensor Read
Priority 2] --> Q[Queue: Sensor Data] T2[Task: MQTT Publish
Priority 1] --> Q T3[Task: Button Input
Priority 3] --> Sem[Semaphore: Alert] T4[Task: LED Blink
Priority 1] --> Sem T5[Task: OTA Update
Priority 1] end Kernel[FreeRTOS Kernel
Preemptive Scheduler] --> T1 Kernel --> T2 Kernel --> T3 Kernel --> T4 Kernel --> T5 style Kernel fill:#e67e22,color:#fff style T1 fill:#27ae60,color:#fff style T3 fill:#e74c3c,color:#fff

Tasks

A task is a C function with its own stack. Each task has a priority (0 is lowest, configMAX_PRIORITIES-1 is highest):

void vSensorTask(void *pvParameters) {
    const TickType_t xDelay = pdMS_TO_TICKS(10000);

    for (;;) {
        float temperature = read_temperature_sensor();
        float humidity = read_humidity_sensor();
        // Send data to MQTT task via queue
        xQueueSend(xSensorQueue, &(SensorReading){temperature, humidity}, 0);
        vTaskDelay(xDelay);
    }
}

void vMQTTTask(void *pvParameters) {
    SensorReading reading;
    for (;;) {
        if (xQueueReceive(xSensorQueue, &reading, portMAX_DELAY) == pdPASS) {
            char payload[64];
            snprintf(payload, sizeof(payload),
                     "{\"temp\":%.1f,\"hum\":%.1f}",
                     reading.temperature, reading.humidity);
            mqtt_publish("sensors/environment", payload);
        }
    }
}

void setup() {
    xSensorQueue = xQueueCreate(10, sizeof(SensorReading));

    xTaskCreate(vSensorTask, "Sensor", 2048, NULL, 2, NULL);
    xTaskCreate(vMQTTTask, "MQTT", 4096, NULL, 1, NULL);

    vTaskStartScheduler();  // Start FreeRTOS scheduler
}

void loop() {
    // Never reached - FreeRTOS runs tasks
}

Queues

Queues pass data between tasks safely. The sending task puts data in, the receiving task takes it out. Queues are thread-safe and support multiple senders and receivers:

QueueHandle_t xSensorQueue;

typedef struct {
    float temperature;
    float humidity;
    uint32_t timestamp;
} SensorReading;

// Send from ISR (interrupt service routine)
void vGPIOInterruptHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    SensorReading reading = {25.0, 60.0, xTaskGetTickCountFromISR()};
    xQueueSendFromISR(xSensorQueue, &reading, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

Semaphores and Mutexes

Semaphores signal events between tasks. Binary semaphores are like flags — one task raises it, another waits for it. Mutexes protect shared resources:

SemaphoreHandle_t xSerialMutex;
SemaphoreHandle_t xButtonSemaphore;

void vButtonTask(void *pvParameters) {
    for (;;) {
        // Wait for button press semaphore from ISR
        if (xSemaphoreTake(xButtonSemaphore, portMAX_DELAY) == pdPASS) {
            toggle_alarm();
        }
    }
}

void vLogTask(void *pvParameters) {
    for (;;) {
        if (xSemaphoreTake(xSerialMutex, portMAX_DELAY) == pdPASS) {
            printf("Sensor data logged\n");
            xSemaphoreGive(xSerialMutex);
        }
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

Software Timers

Timers run a callback after a delay, without blocking a task:

TimerHandle_t xWatchdogTimer;

void vWatchdogCallback(TimerHandle_t xTimer) {
    // Reset external watchdog hardware
    gpio_set_level(WATCHDOG_PIN, 1);
    gpio_set_level(WATCHDOG_PIN, 0);
}

void setup() {
    xWatchdogTimer = xTimerCreate(
        "Watchdog",          // Name
        pdMS_TO_TICKS(1000), // Period (1 second)
        pdTRUE,              // Auto-reload
        (void *)0,           // Timer ID
        vWatchdogCallback    // Callback function
    );
    xTimerStart(xWatchdogTimer, 0);
}

Memory Management

FreeRTOS offers five heap allocation schemes. Heap 4 is most common — it merges adjacent free blocks to reduce fragmentation:

// FreeRTOS memory allocation strategies
// heap_1.c: Simple alloc, no free (never deletes)
// heap_2.c: Best-fit with merge (deprecated)
// heap_3.c: Wraps malloc()/free() (thread-safe)
// heap_4.c: Best-fit with coalescence (RECOMMENDED)
// heap_5.c: Same as heap_4 but supports non-contiguous memory

// Config in FreeRTOSConfig.h
#define configTOTAL_HEAP_SIZE       ((size_t)(64 * 1024))  // 64KB
#define configMINIMAL_STACK_SIZE    ((unsigned short)128)
#define configMAX_TASK_NAME_LEN     (16)

ESP32 Dual-Core with FreeRTOS

ESP32 runs FreeRTOS on both cores. You can pin tasks to specific cores:

// Run on core 0 (protocol processor)
xTaskCreatePinnedToCore(
    vWiFiTask, "WiFi", 4096, NULL, 1, NULL, 0
);

// Run on core 1 (application processor)
xTaskCreatePinnedToCore(
    vSensorTask, "Sensor", 2048, NULL, 2, NULL, 1
);

Common Mistakes

  1. Stack overflow: Task stack too small causes corruption. Use uxTaskGetStackHighWaterMark() to monitor remaining stack, and enable configCHECK_FOR_STACK_OVERFLOW in config.

  2. Blocking in ISRs: FreeRTOS ISRs must not block. Use FromISR API variants (xQueueSendFromISR, xSemaphoreGiveFromISR) and defer processing to tasks.

  3. Priority inversion: Low-priority task holds a mutex needed by high-priority task, while a medium-priority task preempts the low one. Use mutexes with priority inheritance.

  4. Starving idle task: If no task yields or delays, the idle task never runs. Many kernel maintenance operations (memory cleanup) happen in the idle hook.

  5. Not handling watchdog: A hard fault or infinite loop in any task freezes the system. Implement a watchdog timer task that resets the MCU if tasks stop checking in.

Practice Questions

  1. What is the difference between a task and a function? A task is an infinite loop with its own stack and priority, scheduled by the kernel. A function returns to the caller. A task never returns; it yields or delays.

  2. How does a queue differ from a Semaphore? A queue transfers data between tasks (copying values in and out). A Semaphore signals events or guards shared resources without transferring data.

  3. What is priority inversion and how does FreeRTOS handle it? Priority inversion occurs when a low-priority task blocks a high-priority task via a shared mutex. FreeRTOS mutexes with priority inheritance temporarily raise the low task's priority to resolve this.

  4. Why use xQueueSendFromISR instead of xQueueSend in an interrupt? ISRs must not block. The FromISR variant avoids blocking and uses a parameter to request a context switch if a higher-priority task is woken.

  5. What does uxTaskGetStackHighWaterMark return? It returns the minimum unused stack bytes since the task started. A low value indicates the task stack is too small and may overflow.

Mini Project

Build a FreeRTOS multi-sensor manager simulator in Python:

import threading, time, queue, random, enum
from dataclasses import dataclass

@dataclass
class SensorReading:
    temperature: float
    humidity: float
    timestamp: float

class FreeRTOSTask(threading.Thread):
    def __init__(self, name: str, priority: int, period_ms: int):
        super().__init__(daemon=True)
        self.name = name
        self.priority = priority
        self.period_ms = period_ms
        self.killed = threading.Event()

    def run(self):
        while not self.killed.is_set():
            self.task_function()
            time.sleep(self.period_ms / 1000.0)

    def task_function(self):
        raise NotImplementedError

class SensorTask(FreeRTOSTask):
    def __init__(self, data_queue: queue.Queue):
        super().__init__("Sensor", priority=2, period_ms=2000)
        self.data_queue = data_queue

    def task_function(self):
        temp = round(random.uniform(18.0, 30.0), 1)
        hum = round(random.uniform(40.0, 70.0), 1)
        reading = SensorReading(temp, hum, time.time())
        self.data_queue.put(reading)
        print(f"[{self.name}] Read: {temp}°C, {hum}%")

class MQTTTask(FreeRTOSTask):
    def __init__(self, data_queue: queue.Queue):
        super().__init__("MQTT", priority=1, period_ms=500)
        self.data_queue = data_queue

    def task_function(self):
        try:
            reading = self.data_queue.get_nowait()
            print(f"[{self.name}] Published: {reading.temperature}°C")
        except queue.Empty:
            pass

sensor_queue = queue.Queue(maxsize=10)
tasks = [SensorTask(sensor_queue), MQTTTask(sensor_queue)]

for t in tasks:
    t.start()

time.sleep(10)
for t in tasks:
    t.killed.set()

Expected output:

[Sensor] Read: 24.3°C, 55.2%
[MQTT] Published: 24.3°C
[Sensor] Read: 27.8°C, 52.1%
[MQTT] Published: 27.8°C

Cross-References

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro