RTOS Fundamentals
A Real-Time Operating System provides deterministic task scheduling with bounded response times. Unlike a general-purpose OS (Linux, Windows), an RTOS guarantees that the highest-priority ready task always runs — not eventually, but within a known number of microseconds.
Why It Matters
When an embedded system grows beyond a simple super loop — handling sensors, communication, display, and control simultaneously — an RTOS provides structure. Tasks, priorities, and synchronization primitives replace spaghetti state machines. FreeRTOS is the most widely used embedded RTOS, running on everything from Cortex-M0 to ESP32.
How It Works
Task States
A task (also called a thread) is a function with its own stack that runs as if it has its own CPU. The RTOS scheduler multiplexes tasks onto the real CPU.
┌──────────┐
create --> │ Ready │<-------+
└────┬─────┘ |
| (scheduler |
| picks it) |
┌────v─────┐ |
│ Running │ |
└──┬───┬───┘ |
(blocks | | (preempted |
on wait) | | by higher |
┌──v┐ | priority) |
│ │ +-----------+
│ B │
│ l │ (event arrives/
│ o │ timeout expires)
│ c │ +------------> Ready
│ k │
│ e │
│ d │
└───┘
Suspended: removed from scheduling entirely
(vTaskSuspend / vTaskResume)
- Ready: can run, waiting for CPU time
- Running: currently executing (only one task at a time on single-core MCU)
- Blocked: waiting for an event — semaphore, queue, delay, timer
- Suspended: explicitly paused, will not run until resumed
Scheduler Types
| Type | Behavior | Trade-off |
|---|---|---|
| Preemptive | Higher-priority task immediately takes CPU from lower-priority | Responsive, but needs careful synchronization |
| Cooperative | Task runs until it explicitly yields | Simpler, but one task can starve others |
| Time-sliced | Equal-priority tasks share CPU in round-robin with configurable tick | Fair, combined with preemptive in FreeRTOS |
FreeRTOS default: preemptive + time-sliced for same-priority tasks. The SysTick interrupt fires every tick (typically 1 ms) and triggers the scheduler.
Priority Inversion and Priority Inheritance
Priority inversion: a high-priority task is blocked because a low-priority task holds a resource, and a medium-priority task runs instead — effectively inverting the priorities.
Time -->
High: [run]...[blocked on mutex]................[run]
Med: ........[run][run][run][run][run][run]....
Low: [lock mutex]..............................[unlock][...]
Problem: High waits for Low, but Med keeps preempting Low,
so Low never releases the mutex.
Solution: priority inheritance. When High blocks on a mutex held by Low, the RTOS temporarily raises Low’s priority to match High’s. Now Med cannot preempt Low, and Low quickly finishes and releases the mutex.
FreeRTOS provides xSemaphoreCreateMutex() with priority inheritance built in. Always use mutexes (not binary semaphores) when protecting shared resources between tasks of different priorities.
Synchronization Primitives
| Primitive | Purpose | ISR-safe variant |
|---|---|---|
| Binary semaphore | Signal from ISR to task (event notification) | xSemaphoreGiveFromISR() |
| Counting semaphore | Count available resources or events | xSemaphoreGiveFromISR() |
| Mutex | Mutual exclusion with priority inheritance | No — never lock a mutex in an ISR |
| Queue | Thread-safe FIFO for passing data between tasks or from ISR to task | xQueueSendFromISR() |
| Event groups | Wait for combination of flags (AND/OR) | xEventGroupSetBitsFromISR() |
| Task notification | Lightweight semaphore/event/mailbox per task | vTaskNotifyGiveFromISR() |
FreeRTOS API Examples
// Task creation
void sensor_task(void *params) {
while (1) {
int16_t temp = read_temperature();
xQueueSend(temp_queue, &temp, portMAX_DELAY);
vTaskDelay(pdMS_TO_TICKS(100)); // sleep 100ms, let other tasks run
}
}
void display_task(void *params) {
int16_t temp;
while (1) {
xQueueReceive(temp_queue, &temp, portMAX_DELAY); // block until data
update_lcd(temp);
}
}
int main(void) {
temp_queue = xQueueCreate(10, sizeof(int16_t));
xTaskCreate(sensor_task, "sensor", 256, NULL, 2, NULL);
xTaskCreate(display_task, "display", 256, NULL, 1, NULL);
// name stack params pri handle
vTaskStartScheduler(); // never returns
}// Semaphore: signal from ISR to task
SemaphoreHandle_t uart_sem = xSemaphoreCreateBinary();
void USART1_IRQHandler(void) {
if (USART1->SR & USART_SR_RXNE) {
rx_byte = USART1->DR;
BaseType_t woken = pdFALSE;
xSemaphoreGiveFromISR(uart_sem, &woken);
portYIELD_FROM_ISR(woken); // context switch if higher-pri task woke
}
}
void uart_task(void *params) {
while (1) {
xSemaphoreTake(uart_sem, portMAX_DELAY); // block until ISR signals
process_byte(rx_byte);
}
}// Mutex: protect shared I2C bus
SemaphoreHandle_t i2c_mutex = xSemaphoreCreateMutex();
void read_sensor_a(void) {
xSemaphoreTake(i2c_mutex, portMAX_DELAY);
i2c_read(SENSOR_A_ADDR, data, len);
xSemaphoreGive(i2c_mutex);
}
void read_sensor_b(void) {
xSemaphoreTake(i2c_mutex, portMAX_DELAY);
i2c_read(SENSOR_B_ADDR, data, len);
xSemaphoreGive(i2c_mutex);
}Memory Considerations
Each task needs its own stack (typically 128-1024 words). FreeRTOS offers static and dynamic allocation:
xTaskCreate()— allocates stack from FreeRTOS heapxTaskCreateStatic()— you provide a pre-allocated buffer
Monitor stack usage with uxTaskGetStackHighWaterMark() to detect overflows. Stack overflow is the most common RTOS crash cause. FreeRTOS can hook a callback on overflow detection (configCHECK_FOR_STACK_OVERFLOW).
Related
- Bare Metal vs RTOS — when to use an RTOS vs. super loop
- Interrupts and Timers — SysTick drives the scheduler, ISR-to-task signaling
- Concurrency and Synchronization — same concepts (mutex, semaphore) at OS level
- Scheduling — preemptive vs. cooperative scheduling theory
- Processes and Threads — RTOS tasks are lightweight threads
- Memory Management — heap allocation and stack sizing