Embedded
RTOS — Real-Time Operating System
A superloop works fine until a device has to do several genuinely independent, timing-sensitive things at once. An RTOS is what steps in at that point — not for extra features, but to guarantee that the things which absolutely must happen on time actually do, no matter what else the device is doing.
- What it is
- A scheduler-based kernel that runs multiple independent tasks, always giving the CPU to the highest-priority task that's ready to run.
- Mental model
- Not "an OS" in the desktop sense — a referee that instantly hands the CPU to whichever task matters most right now, every time that changes.
- Key numbers
- Each task has its own stack and priority; a higher-priority task preempts a lower one immediately, not at the next convenient moment.
- Reach for it when
- Multiple independent, timing-sensitive activities need to run concurrently and a superloop has become unmanageable.
Why not just a bigger superloop?
A bare-metal "superloop" (one big while(1) calling each
subsystem in turn) works well for simple devices, but it has one
structural weakness: everything runs at the same implicit priority,
in whatever order it was written. As soon as one device has several
genuinely independent timing requirements — a motor control loop that
needs a hard 1 ms deadline, a UI that can tolerate 100 ms, a
background upload that can wait entirely — a superloop forces the
programmer to manually interleave all of it by hand. An RTOS takes
that interleaving decision out of application code entirely and puts
it in a scheduler that makes it correctly, every time, based on
priority.
Tasks and task states
A task (sometimes "thread") is an independent unit of execution with its own stack, its own saved register context, and its own priority — from the programmer's point of view, each task can be written as if it had the CPU entirely to itself.
The scheduler: preemptive priority scheduling
The dominant RTOS scheduling model is simple to state: the highest-priority task that's ready to run, runs — always, immediately. If a low-priority task is running and a higher-priority task becomes ready (a semaphore it was waiting on is given, a timer expires), the scheduler preempts the running task on the spot, not at some future convenient checkpoint.
Equal-priority tasks are typically time-sliced (round-robin) so none of them starves the others — but between different priorities, it's never a fair split, by design.
Semaphores, mutexes, and queues
- Semaphore
- A counting signal used to notify another task an event happened — the standard way an interrupt hands work off to a task: the ISR gives a semaphore and returns immediately, a task blocks waiting to take it, and the real work happens in task context instead of inside the ISR.
- Mutex
- Mutual exclusion — protects a shared resource (a peripheral, a data structure) so only one task touches it at a time. Unlike a semaphore, a mutex has an owner and is specifically for protecting a resource, not general signaling.
- Queue
- A FIFO buffer for passing data between tasks (or from an ISR to a task) safely, without either side needing to know exactly when the other will read or write it.
Priority inversion
A classic RTOS bug class: a high-priority task blocks waiting for a mutex currently held by a low-priority task. If a medium-priority task then preempts the low-priority one (which has every right to, by priority rules), the high-priority task ends up waiting indefinitely — blocked not by the low-priority task directly, but by a medium-priority task that has nothing to do with the resource at all. The standard fix is priority inheritance: while holding a mutex a higher-priority task is waiting on, the low-priority task temporarily runs at the higher task's priority, so it can't be preempted by anything in between and finishes (and releases the mutex) as fast as possible.
Stacks, and why sizing them matters
Every task has its own stack, sized when the task is created — too small and a deep call chain (or interrupt nesting on top of it) silently corrupts adjacent memory, often manifesting as a bug that looks completely unrelated to the actual stack overflow. Most RTOSes provide stack high-water-mark checking specifically because guessing the right size up front is genuinely hard, and getting it wrong fails silently rather than loudly.
Bare-metal vs. RTOS vs. Linux
| Bare-metal | RTOS | Linux | |
|---|---|---|---|
| Scheduling goal | Whatever the programmer wrote, in order | Deterministic — highest priority ready task always runs | Fair — good average throughput, not deadline guarantees |
| Memory footprint | Minimal — no kernel at all | Small — kilobytes, not megabytes | Large — needs an MMU, typically megabytes |
| Determinism | Fully deterministic (you control everything) | Deterministic by design | Not deterministic — general-purpose, best-effort |
| Typical use | Very simple, single-purpose devices | Motor control, industrial, medical, automotive | Gateways, anything needing a full OS/networking/filesystem stack |
Real-world applications
- Motor and robotics control loops — a hard real-time task running a PID loop at a fixed rate, isolated by priority from lower-priority housekeeping work.
- Medical devices — RTOSes with certifiable, deterministic behavior (and often formal safety certification) are close to a hard requirement in regulated medical firmware.
- Automotive ECUs — many run a certified RTOS (e.g. AUTOSAR-compliant) specifically for the deterministic timing guarantees safety-critical functions need.
- Communication stacks — running MQTT, TLS, or a full network stack as one or more RTOS tasks, isolated from time-critical control tasks by priority.
- Real RTOS choices — FreeRTOS (by far the most widely deployed), Zephyr, ThreadX, and various vendor-specific or certified options for safety-critical work.
Gotchas
- Marking every task "high priority" defeats the entire purpose — priority only means something in relation to other priorities; if everything is high priority, nothing effectively is.
- Blocking calls (anything that can wait/sleep) inside an interrupt handler are a hard rule violation on virtually every RTOS — ISRs must stay short and non-blocking, deferring real work to a task exactly as described under Semaphores above.
- A stack overflow on one task can silently corrupt an adjacent task's memory rather than crashing cleanly — a bug that "makes no sense" in a completely unrelated task is worth checking stack usage for before anything else.
- Forgetting to protect a resource shared between an ISR and a task (or between two tasks) reproduces exactly the race-condition class covered in Interrupts — an RTOS gives you the tools to prevent it, but doesn't prevent it automatically.