4 min read
On this page

Real-Time Programming

Real-Time Constraints

A real-time system must produce correct results within a defined time bound. The classification depends on what happens when a deadline is missed.

Categories

Type Deadline Miss Consequence Example
Hard System failure, catastrophic Airbag deployment, ABS braking, pacemaker
Firm Result is worthless, no catastrophe Video frame rendering, radar sweep
Soft Degraded quality, still usable Audio streaming, UI responsiveness

Key Metrics

  • Worst-Case Execution Time (WCET): maximum time a task can take
  • Jitter: variation in execution timing between invocations
  • Latency: delay from event occurrence to response start
  • Determinism: predictability of timing behavior

RTOS Concepts

RTOS task state machine

A Real-Time Operating System provides deterministic scheduling, inter-task communication, and resource management.

Tasks (Threads)

Tasks are independent units of execution with their own stack. Each task has a priority and state:

           create
  Ready <------------ Blocked
    |                    ^
    | scheduler          | wait (semaphore,
    v  dispatches        |  queue, delay)
  Running ---------------+
    |
    | delete / complete
    v
  Terminated

Scheduling Algorithms

Algorithm Description Used In
Preemptive priority Highest-priority ready task runs immediately FreeRTOS, Zephyr
Round-robin Equal-priority tasks share time slices FreeRTOS (same priority)
Rate Monotonic (RMS) Shorter period = higher priority (static) Safety-critical systems
Earliest Deadline First Closest deadline runs next (dynamic) Research, some RTOS

Rate Monotonic Schedulability Test

For n periodic tasks with periods T_i and execution times C_i:

Utilization U = sum(C_i / T_i) <= n * (2^(1/n) - 1)

For n -> infinity: U <= ln(2) ~ 0.693

Practical: if total CPU utilization < 69.3%, RMS guarantees all deadlines are met.

FreeRTOS

The most widely deployed RTOS, running on billions of devices. Open-source, supports ARM Cortex-M, RISC-V, Xtensa, and more.

Task Creation

// FreeRTOS task example (C API, as FreeRTOS is C-based)
void sensor_task(void *pvParameters) {
    for (;;) {
        read_sensor();
        vTaskDelay(pdMS_TO_TICKS(100)); // Sleep 100ms, yield CPU
    }
}

xTaskCreate(sensor_task, "Sensor", 256, NULL, 2, NULL);
// Parameters: function, name, stack_size, params, priority, handle

Queues

Thread-safe FIFO for passing data between tasks or from ISRs to tasks:

  Producer Task  ---->  [ | | | | ]  ---->  Consumer Task
                         Queue (N items)

Queues block the sender when full and the receiver when empty (with optional timeout).

Semaphores

Binary semaphore: signaling mechanism (ISR signals task). Starts empty.

ISR:  xSemaphoreGiveFromISR(sem, &woken);   // Signal
Task: xSemaphoreTake(sem, portMAX_DELAY);   // Wait for signal

Counting semaphore: tracks multiple available resources.

Mutexes

Mutual exclusion for shared resource protection. Unlike semaphores, mutexes support priority inheritance to mitigate priority inversion.

Task A (low priority):    Take mutex -> access shared resource -> Give mutex
Task B (high priority):   Take mutex (blocks if A holds it)
                         A's priority is temporarily raised to B's level

Zephyr RTOS

A modern, scalable RTOS backed by the Linux Foundation. Features:

  • Device tree for hardware description (borrowed from Linux)
  • Kconfig-based build system
  • Extensive driver model and networking stack
  • Bluetooth, Wi-Fi, Thread, LoRaWAN support
  • Memory protection via MPU
  • Supports 500+ boards across ARM, RISC-V, x86, Xtensa

Embassy (Async Embedded Rust)

Embassy is an async runtime for embedded Rust, offering an alternative to traditional RTOS patterns using Rust's async/await.

// Embassy async embedded -- no heap allocation, no threads

PROCEDURE MAIN(spawner)
    p ← INIT_PERIPHERALS(defaults)

    // Spawn concurrent tasks -- no heap allocation, no threads
    SPAWN(BLINK_TASK, p.PA5)
    SPAWN(SENSOR_TASK)

ASYNC TASK BLINK_TASK(pin)
    led ← OUTPUT_PIN(pin, level ← LOW, speed ← LOW)
    LOOP
        TOGGLE(led)
        AWAIT TIMER(500 ms)   // Non-blocking delay

ASYNC TASK SENSOR_TASK()
    LOOP
        // Read sensor, process data
        AWAIT TIMER(100 ms)

Embassy advantages over traditional RTOS:

  • Zero-allocation async tasks (each task is a state machine)
  • Compile-time task safety (no stack overflow from undersized stacks)
  • Rust ownership model prevents data races without runtime checks
  • HAL drivers with native async support (UART, SPI, I2C with DMA)

Interrupt Handling

Interrupt Service Routine (ISR)

ISRs execute in response to hardware events. They must be:

  • Short: do minimal work, defer processing to tasks
  • Non-blocking: never wait, sleep, or call blocking functions
  • Reentrant-safe: use only ISR-safe APIs

Interrupt Priority and Nesting

ARM Cortex-M NVIC supports priority levels (lower number = higher priority on Cortex-M):

Priority 0 (highest) ──> System critical (fault handlers)
Priority 1            ──> Time-critical (motor control)
Priority 2            ──> Communication (UART RX)
Priority 3            ──> Background (sensor polling)
  ...
Priority 15 (lowest)  ──> Non-critical

Nested interrupts: a higher-priority interrupt can preempt a lower-priority ISR. The Cortex-M NVIC supports this natively via tail-chaining and late-arrival optimization.

Interrupt Latency

Time from interrupt assertion to first ISR instruction:

Component Cycles (Cortex-M4)
Recognition 1
Stacking (push registers) 12
Vector fetch Varies
Total minimum ~12-15 cycles

Tail-chaining eliminates unstacking/restacking when switching between ISRs.

ISR Pattern: Defer to Task

GLOBAL DATA_READY ← ATOMIC_BOOL(FALSE)

ISR EXTI0()
    // In ISR: minimal work
    CLEAR_INTERRUPT_FLAG()
    ATOMIC_STORE(DATA_READY, TRUE, Release)

// In main loop or task:
LOOP
    IF ATOMIC_SWAP(DATA_READY, FALSE, Acquire) THEN
        PROCESS_DATA()   // Heavy work done outside ISR
    WAIT_FOR_INTERRUPT()  // Sleep until next interrupt

Critical Sections

Code regions where interrupts are disabled to ensure atomic access to shared state.

// Disable interrupts for the duration of the block
CRITICAL_SECTION DO
    // Access shared state safely
    data ← BORROW_MUTABLE(SHARED_DATA)
    data ← data + 1
// Interrupts automatically restored

Critical sections must be kept as short as possible to minimize interrupt latency.

Mutex Alternatives in no_std Rust

// Global shared state protected by interrupt-disabling mutex
GLOBAL COUNTER ← MUTEX(0)

ISR TIM2()
    CRITICAL_SECTION DO
        count ← BORROW_MUTABLE(COUNTER)
        count ← count + 1

Priority Inversion

A high-priority task is blocked by a low-priority task holding a shared resource, while a medium-priority task preempts the low-priority task -- effectively inverting priorities.

High   ████░░░░░░░░░░████████   (blocked waiting for mutex)
Medium ░░░░████████████░░░░░░   (runs, preempts Low)
Low    ████░░░░░░░░░░░░████░░   (holds mutex, preempted by Medium)
                         ^ mutex released

Solutions

Solution Mechanism
Priority inheritance Low task temporarily gets high task's priority
Priority ceiling Mutex is assigned the priority of its highest user
Lock-free algorithms Atomics, wait-free data structures
Disable interrupts Simple but increases latency

Key Takeaways

  • Hard real-time systems have absolute deadlines; soft systems tolerate occasional misses.
  • RTOS primitives (tasks, queues, semaphores, mutexes) enable structured concurrent programming.
  • Embassy brings Rust's async/await to embedded, offering zero-allocation concurrency without a traditional RTOS.
  • ISRs must be short and non-blocking; defer heavy processing to tasks.
  • Priority inversion is a real hazard; use priority inheritance or lock-free designs.
  • Critical sections protect shared state but must be minimized to preserve system responsiveness.