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.