Back to Technology

Embedded Systems Series Part 3: RTOS Fundamentals (FreeRTOS/Zephyr)

January 25, 2026 Wasil Zafar 55 min read

Master real-time operating systems—task management, scheduling, synchronization primitives, and multi-threaded embedded development.

Table of Contents

  1. Introduction to RTOS
  2. FreeRTOS Deep Dive
  3. Zephyr RTOS Overview
  4. Task Management
  5. Scheduling Algorithms
  6. Synchronization Primitives
  7. Inter-Task Communication
  8. Memory Management
  9. Timing & Deadlines
  10. Conclusion & Next Steps

Introduction to RTOS

Series Navigation: This is Part 3 of the 12-part Embedded Systems Series. Review Part 2: STM32 & ARM Development first.

A Real-Time Operating System (RTOS) provides deterministic, time-bounded response to events. Unlike general-purpose OSes (Windows, Linux), an RTOS guarantees that tasks complete within specified deadlines—critical for motor control, medical devices, and automotive systems.

When to Use an RTOS vs. Bare-Metal

RTOS vs. Bare-Metal Decision Matrix

Complexity Timing Resources
FactorBare-MetalRTOS
ComplexitySimple state machineMultiple concurrent tasks
TimingSuper loops, manual timingPreemptive scheduling
MemoryMinimal overhead2-10KB+ kernel
DebuggingSimplerTask visualization tools
ScalabilityHard to add featuresEasy to add tasks
PowerFine-grained controlTickless idle modes
Rule of Thumb: Use RTOS when you have 3+ independent concurrent activities with different timing requirements. Stick with bare-metal for simple, single-purpose devices.

FreeRTOS Deep Dive

FreeRTOS is the most popular RTOS, running on 40%+ of embedded devices. It's open-source (MIT license), supports 35+ architectures, and integrates with AWS IoT.

FreeRTOS Project Setup

// FreeRTOS configuration (FreeRTOSConfig.h)
#define configUSE_PREEMPTION            1
#define configUSE_PORT_OPTIMISED_TASK_SELECTION 1
#define configUSE_TICKLESS_IDLE         0
#define configCPU_CLOCK_HZ              84000000  // STM32F4 @ 84MHz
#define configTICK_RATE_HZ              1000      // 1ms tick
#define configMAX_PRIORITIES            5
#define configMINIMAL_STACK_SIZE        128       // words (512 bytes)
#define configTOTAL_HEAP_SIZE           (15 * 1024)
#define configMAX_TASK_NAME_LEN         16

// Feature enables
#define configUSE_MUTEXES               1
#define configUSE_COUNTING_SEMAPHORES   1
#define configUSE_QUEUE_SETS            1
#define configUSE_TASK_NOTIFICATIONS    1
#define configUSE_TRACE_FACILITY        1
#define configUSE_STATS_FORMATTING_FUNCTIONS 1

// Hook functions for debugging
#define configUSE_MALLOC_FAILED_HOOK    1
#define configCHECK_FOR_STACK_OVERFLOW  2

Creating Your First Tasks

#include "FreeRTOS.h"
#include "task.h"

// Task function prototype
void vLedTask(void *pvParameters);
void vSensorTask(void *pvParameters);

int main(void) {
    // Hardware initialization
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    
    // Create tasks
    xTaskCreate(
        vLedTask,           // Task function
        "LED",              // Task name (debugging)
        128,                // Stack size (words)
        NULL,               // Parameters
        1,                  // Priority (1 = low)
        NULL                // Task handle (optional)
    );
    
    xTaskCreate(
        vSensorTask,
        "Sensor",
        256,                // Larger stack for complex tasks
        (void*)42,          // Pass parameter
        2,                  // Higher priority
        NULL
    );
    
    // Start the scheduler (never returns)
    vTaskStartScheduler();
    
    // Should never reach here
    while(1);
}

// LED blink task
void vLedTask(void *pvParameters) {
    const TickType_t xDelay = pdMS_TO_TICKS(500);  // 500ms delay
    
    for(;;) {
        HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
        vTaskDelay(xDelay);  // Block, allowing other tasks to run
    }
}

// Sensor reading task
void vSensorTask(void *pvParameters) {
    int param = (int)pvParameters;  // 42
    
    for(;;) {
        // Read sensor
        uint16_t value = read_adc();
        
        // Process data
        if (value > THRESHOLD) {
            // Alert condition
        }
        
        vTaskDelay(pdMS_TO_TICKS(100));  // 10Hz sampling
    }
}

Zephyr RTOS Overview

Zephyr is a modern, scalable RTOS backed by the Linux Foundation. It's gaining traction for IoT devices with its excellent Bluetooth/networking stack and device tree configuration.

FreeRTOS vs. Zephyr Comparison

FreeRTOS Zephyr
AspectFreeRTOSZephyr
Learning CurveEasierSteeper (cmake, devicetree)
Footprint6-10KB8-20KB+
NetworkingFreeRTOS+TCP or lwIPBuilt-in (excellent BLE)
ConfigurationHeader fileKconfig + Devicetree
VendorsUniversal supportNordic, Intel, NXP focus
CertificationIEC 61508, MISRAGrowing safety portfolio
// Zephyr task (thread) example
#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>

#define LED0_NODE DT_ALIAS(led0)
static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios);

// Define thread stack
K_THREAD_STACK_DEFINE(blink_stack, 512);
struct k_thread blink_thread_data;

void blink_thread(void *p1, void *p2, void *p3) {
    gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE);
    
    while (1) {
        gpio_pin_toggle_dt(&led);
        k_msleep(1000);  // Sleep 1 second
    }
}

int main(void) {
    // Create thread
    k_thread_create(&blink_thread_data, blink_stack,
                    K_THREAD_STACK_SIZEOF(blink_stack),
                    blink_thread, NULL, NULL, NULL,
                    5,     // Priority (lower = higher priority in Zephyr)
                    0,     // Options
                    K_NO_WAIT);  // Start immediately
    
    return 0;  // Main can return in Zephyr
}

Task Management

Task States

FreeRTOS tasks cycle through four states:

  • Running: Currently executing on CPU
  • Ready: Able to run, waiting for CPU time
  • Blocked: Waiting for event (delay, semaphore, queue)
  • Suspended: Explicitly paused by vTaskSuspend()
// Task control functions
TaskHandle_t xSensorHandle = NULL;

// Create with handle
xTaskCreate(vSensorTask, "Sensor", 256, NULL, 2, &xSensorHandle);

// Suspend task (from another task or ISR)
vTaskSuspend(xSensorHandle);

// Resume task
vTaskResume(xSensorHandle);

// Delete task (NULL = self)
vTaskDelete(xSensorHandle);
// vTaskDelete(NULL);  // Delete calling task

// Get task state
eTaskState state = eTaskGetState(xSensorHandle);
// Running, Ready, Blocked, Suspended, Deleted

// Get high water mark (minimum free stack)
UBaseType_t stackLeft = uxTaskGetStackHighWaterMark(xSensorHandle);

Scheduling Algorithms

Priority-Based Preemptive Scheduling

FreeRTOS uses fixed-priority preemptive scheduling—the highest priority ready task always runs. When priorities are equal, tasks share time via round-robin.

Priority Assignment Strategy:
  • Highest: Hardware interrupt handlers (ISR)
  • High: Safety-critical, deadline-driven tasks
  • Medium: Communication, sensor processing
  • Low: Logging, display updates, background work
  • Idle: System cleanup, power management
// Priority management
#define PRIORITY_CRITICAL   (configMAX_PRIORITIES - 1)  // 4
#define PRIORITY_HIGH       3
#define PRIORITY_MEDIUM     2
#define PRIORITY_LOW        1
#define PRIORITY_IDLE       0  // tskIDLE_PRIORITY

// Change priority at runtime
vTaskPrioritySet(xSensorHandle, PRIORITY_HIGH);

// Get current priority
UBaseType_t prio = uxTaskPriorityGet(xSensorHandle);

// Yield to other same-priority tasks
taskYIELD();

Synchronization Primitives

Semaphores

Semaphores signal events between tasks or from ISRs to tasks.

#include "semphr.h"

// Binary semaphore (signal events)
SemaphoreHandle_t xButtonSemaphore;

void setup(void) {
    xButtonSemaphore = xSemaphoreCreateBinary();
}

// ISR signals the semaphore
void EXTI15_10_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    
    HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_13);
    
    xSemaphoreGiveFromISR(xButtonSemaphore, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

// Task waits for semaphore
void vButtonTask(void *pvParameters) {
    for(;;) {
        // Block until button pressed (or timeout)
        if (xSemaphoreTake(xButtonSemaphore, portMAX_DELAY) == pdTRUE) {
            // Handle button press
            HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
        }
    }
}

// Counting semaphore (resource pool)
SemaphoreHandle_t xPoolSemaphore = xSemaphoreCreateCounting(5, 5);
// Max 5, initial 5

Mutexes (Mutual Exclusion)

Mutexes protect shared resources from concurrent access. Unlike semaphores, mutexes support priority inheritance to prevent priority inversion.

SemaphoreHandle_t xUartMutex;

void setup(void) {
    xUartMutex = xSemaphoreCreateMutex();
}

// Thread-safe UART write
void safe_uart_print(const char *msg) {
    // Acquire mutex (block if held by another task)
    if (xSemaphoreTake(xUartMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
        HAL_UART_Transmit(&huart2, (uint8_t*)msg, strlen(msg), HAL_MAX_DELAY);
        xSemaphoreGive(xUartMutex);  // Release
    } else {
        // Timeout - handle error
    }
}

Priority Inversion Problem

Classic Bug Solution: Priority Inheritance

Scenario: Low-priority task holds mutex. High-priority task blocks waiting. Medium-priority task preempts low ? high-priority task starves.

Solution: FreeRTOS mutexes implement priority inheritance—low-priority task temporarily inherits high priority while holding the mutex.

Inter-Task Communication

Queues

Queues pass data between tasks safely. They're the primary mechanism for producer-consumer patterns.

#include "queue.h"

typedef struct {
    uint16_t sensor_id;
    float value;
    uint32_t timestamp;
} SensorData_t;

QueueHandle_t xSensorQueue;

void setup(void) {
    // Queue of 10 SensorData_t items
    xSensorQueue = xQueueCreate(10, sizeof(SensorData_t));
}

// Producer task
void vSensorTask(void *pvParameters) {
    SensorData_t data;
    
    for(;;) {
        data.sensor_id = 1;
        data.value = read_temperature();
        data.timestamp = xTaskGetTickCount();
        
        // Send to queue (block if full for 100ms)
        if (xQueueSend(xSensorQueue, &data, pdMS_TO_TICKS(100)) != pdPASS) {
            // Queue full - handle overflow
        }
        
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

// Consumer task
void vProcessingTask(void *pvParameters) {
    SensorData_t received;
    
    for(;;) {
        // Block until data available
        if (xQueueReceive(xSensorQueue, &received, portMAX_DELAY) == pdPASS) {
            printf("Sensor %d: %.2f at %lu\n", 
                   received.sensor_id, received.value, received.timestamp);
        }
    }
}

Task Notifications (Lightweight)

Task notifications are faster than semaphores for simple signaling—no separate object needed.

TaskHandle_t xReceiverHandle;

// ISR sends notification
void UART_RxCallback(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    vTaskNotifyGiveFromISR(xReceiverHandle, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

// Task waits for notification
void vReceiverTask(void *pvParameters) {
    for(;;) {
        ulTaskNotifyTake(pdTRUE, portMAX_DELAY);  // Clear on exit
        process_uart_data();
    }
}

Memory Management

FreeRTOS provides 5 heap implementations:

FreeRTOS Heap Options:
  • heap_1: Allocate only (no free) - simplest, deterministic
  • heap_2: Best fit, no coalescence - fragmentation risk
  • heap_3: Wraps standard malloc/free - thread-safe wrapper
  • heap_4: Best fit with coalescence - recommended
  • heap_5: heap_4 + non-contiguous regions
// Dynamic allocation
void *ptr = pvPortMalloc(256);
if (ptr != NULL) {
    // Use memory
    vPortFree(ptr);
}

// Check available heap
size_t freeHeap = xPortGetFreeHeapSize();
size_t minEverFreeHeap = xPortGetMinimumEverFreeHeapSize();

// Malloc failed hook (debugging)
void vApplicationMallocFailedHook(void) {
    taskDISABLE_INTERRUPTS();
    for(;;);  // Halt on allocation failure
}

Timing & Deadlines

Software Timers

#include "timers.h"

TimerHandle_t xHeartbeatTimer;

void vHeartbeatCallback(TimerHandle_t xTimer) {
    HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
}

void setup(void) {
    // Create periodic timer (1 second)
    xHeartbeatTimer = xTimerCreate(
        "Heartbeat",
        pdMS_TO_TICKS(1000),
        pdTRUE,             // Auto-reload (periodic)
        NULL,               // Timer ID
        vHeartbeatCallback
    );
    
    xTimerStart(xHeartbeatTimer, 0);
}

// One-shot timer
xTimerCreate("Timeout", pdMS_TO_TICKS(5000), pdFALSE, NULL, vTimeoutCallback);

Deadline-Driven Design

// Periodic task with deadline monitoring
void vCriticalTask(void *pvParameters) {
    TickType_t xLastWakeTime = xTaskGetTickCount();
    const TickType_t xPeriod = pdMS_TO_TICKS(10);  // 10ms period
    
    for(;;) {
        TickType_t xStart = xTaskGetTickCount();
        
        // Do critical work
        process_motor_control();
        
        // Check if we missed deadline
        TickType_t xElapsed = xTaskGetTickCount() - xStart;
        if (xElapsed > pdMS_TO_TICKS(8)) {  // 80% threshold
            log_warning("Near deadline miss!");
        }
        
        // Sleep until next period (precise timing)
        vTaskDelayUntil(&xLastWakeTime, xPeriod);
    }
}

Conclusion & What's Next

You've learned the core RTOS concepts—task creation, scheduling, synchronization primitives, queues, and timing. FreeRTOS and Zephyr are production-ready choices for embedded multi-tasking.

Key Takeaways:
  • Use RTOS for 3+ concurrent activities with timing requirements
  • FreeRTOS: Universal, lightweight, AWS IoT integration
  • Zephyr: Modern, excellent networking, Linux Foundation backed
  • Mutexes prevent priority inversion; semaphores signal events
  • Queues are the safest way to pass data between tasks

In Part 4, we'll deep dive into communication protocols—UART, SPI, I2C, CAN, and USB—the backbone of embedded connectivity.

Next Steps

Technology