Introduction to RTOS
Embedded Systems Mastery
Fundamentals & Architecture
Microcontrollers, memory, interruptsSTM32 & ARM Cortex-M Development
ARM architecture, peripherals, HALRTOS Fundamentals
FreeRTOS, Zephyr, task managementCommunication Protocols Deep Dive
UART, SPI, I2C, CAN, USBEmbedded Linux Fundamentals
Linux kernel, userspace, filesystemU-Boot Bootloader Mastery
Boot process, configuration, customizationLinux Device Drivers
Character, block, network driversLinux Kernel Customization
Kernel configuration, modules, debuggingAndroid System Architecture
Android layers, services, frameworkAndroid HAL & Native Development
HAL interfaces, NDK, JNIAndroid BSP & Kernel
BSP development, kernel integrationDebugging & Optimization
JTAG, GDB, profiling, optimizationA 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
| Factor | Bare-Metal | RTOS |
|---|---|---|
| Complexity | Simple state machine | Multiple concurrent tasks |
| Timing | Super loops, manual timing | Preemptive scheduling |
| Memory | Minimal overhead | 2-10KB+ kernel |
| Debugging | Simpler | Task visualization tools |
| Scalability | Hard to add features | Easy to add tasks |
| Power | Fine-grained control | Tickless idle modes |
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
| Aspect | FreeRTOS | Zephyr |
|---|---|---|
| Learning Curve | Easier | Steeper (cmake, devicetree) |
| Footprint | 6-10KB | 8-20KB+ |
| Networking | FreeRTOS+TCP or lwIP | Built-in (excellent BLE) |
| Configuration | Header file | Kconfig + Devicetree |
| Vendors | Universal support | Nordic, Intel, NXP focus |
| Certification | IEC 61508, MISRA | Growing 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.
- 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
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:
- 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.
- 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.