FreeRTOS — Real-Time Operating System for Embedded IoT Guide
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
Stack overflow: Task stack too small causes corruption. Use
uxTaskGetStackHighWaterMark()to monitor remaining stack, and enableconfigCHECK_FOR_STACK_OVERFLOWin config.Blocking in ISRs: FreeRTOS ISRs must not block. Use
FromISRAPI variants (xQueueSendFromISR,xSemaphoreGiveFromISR) and defer processing to tasks.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.
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.
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
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.
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.
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.
Why use xQueueSendFromISR instead of xQueueSend in an interrupt? ISRs must not block. The
FromISRvariant avoids blocking and uses a parameter to request a context switch if a higher-priority task is woken.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
- ESP32
- MicroPython
- MQTT
- IoT Overview
- Firmware OTA Updates
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro