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

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.