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

TypeBehaviorTrade-off
PreemptiveHigher-priority task immediately takes CPU from lower-priorityResponsive, but needs careful synchronization
CooperativeTask runs until it explicitly yieldsSimpler, but one task can starve others
Time-slicedEqual-priority tasks share CPU in round-robin with configurable tickFair, 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

PrimitivePurposeISR-safe variant
Binary semaphoreSignal from ISR to task (event notification)xSemaphoreGiveFromISR()
Counting semaphoreCount available resources or eventsxSemaphoreGiveFromISR()
MutexMutual exclusion with priority inheritanceNo — never lock a mutex in an ISR
QueueThread-safe FIFO for passing data between tasks or from ISR to taskxQueueSendFromISR()
Event groupsWait for combination of flags (AND/OR)xEventGroupSetBitsFromISR()
Task notificationLightweight semaphore/event/mailbox per taskvTaskNotifyGiveFromISR()

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 heap
  • xTaskCreateStatic() — 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).