In this lesson:
- the bare-metal super-loop and where it breaks down
- what an RTOS adds: tasks, priorities, blocking that yields the CPU
- the costs: RAM (per-task stacks), complexity, concurrency bugs
- when you need an RTOS, and when you don't
- the middle ground (super-loop + interrupts + state machines)
This module builds on Embedded Fundamentals (interrupts, timers, the boot sequence).
Bare-metal: the super-loop
The simplest firmware is bare-metal: no operating system, just a while(1) super-loop plus interrupt handlers.
int main(void) {
init();
while (1) {
read_sensor(); // each runs in turn, top to bottom, forever
update_display();
handle_buttons();
log_over_uart();
}
}It's wonderful when it fits: tiny, no kernel overhead, fully deterministic, easy to reason about. But it strains as you add concurrent activities with different rates and priorities:
- Everything runs at the pace of the slowest step. A slow
update_display()delays the sensor read behind it. - Blocking stalls the whole system. A
log_over_uart()that waits for the UART freezes everything else. - Prioritizing is manual and fragile. Want the sensor serviced more often than the display? You hand-roll timers, counters, and flags, and it tangles fast.
- Adding features perturbs timing. Each new item in the loop shifts everyone else's timing.
You can push the super-loop far with interrupts and state machines, but eventually the juggling becomes unmanageable.
RTOS: tasks with a scheduler
A Real-Time Operating System lets you write each activity as an independent task (a function with its own stack), and a scheduler decides which task runs when, giving each task the illusion of its own CPU.
// Each activity is its own task; the scheduler interleaves them by priority.
void sensor_task(void *arg) { for (;;) { read_sensor(); vTaskDelay(1); } }
void display_task(void *arg) { for (;;) { update_display(); vTaskDelay(20);} }
int main(void) {
xTaskCreate(sensor_task, "sensor", 256, NULL, 3, NULL); // higher priority
xTaskCreate(display_task, "display", 256, NULL, 1, NULL); // lower priority
vTaskStartScheduler(); // never returns; the scheduler takes over
}The wins:
- Independent timing & priorities. A high-priority task preempts a low one, so the sensor isn't held hostage by the display.
- Blocking is fine, it yields. When a task waits (for a delay, a queue, a UART), the scheduler runs other tasks.
vTaskDelay(20)isn't a busy-wait; the CPU does useful work meanwhile. - Modularity. Each task is self-contained; adding one doesn't re-time the others (within priority/CPU limits).
What it costs
An RTOS is not free:
- RAM, each task needs its own stack, plus kernel data (task control blocks, queues). On a device with a few KB of RAM, a handful of tasks can be a tight fit.
- Complexity & concurrency bugs, the moment tasks share data, you have races, and you need mutexes, queues, and care about priority inversion and deadlocks (the rest of this module exists because of this).
- Scheduler overhead, context switches and the tick interrupt cost cycles (small, but nonzero).
- Learning curve & footprint, the kernel itself takes flash and must be configured.
super-loop: everything serialized, one stack, no kernel β simple but rigid
RTOS: tasks interleaved by priority, N stacks + kernel β flexible but heavierWhen you need one (and when you don't)
Reach for an RTOS when:
- you have multiple independent activities with different rates/priorities,
- you need blocking I/O without freezing everything,
- a high-priority event must preempt slow background work,
- the super-loop's hand-rolled timing has become a tangle.
Stay bare-metal when:
- the job is simple (a few tasks a super-loop handles cleanly),
- you're extremely RAM-constrained (no room for per-task stacks + kernel),
- you need the simplest possible timing story for certification/audit.
The middle ground handles a surprising amount: a super-loop driven by interrupts (fast events in ISRs) feeding ring buffers, with logic as state machines (the DSA/FSM lesson). Many shipping products never need an RTOS. Reach for one when the concurrency genuinely warrants it, not reflexively.
Gotchas
- An RTOS doesn't make you real-time. It gives you tools (priorities, preemption) to design for deadlines, but a badly prioritized or CPU-overloaded task set still misses them. Real-time is a property you engineer (later lessons), not a checkbox.
- Per-task stack RAM adds up. Every task pays for its worst-case stack (ISR nesting included). Undersize it and you overflow into another task's memory, a nasty, no-MMU corruption (the stack lesson).
- Concurrency bugs are the new failure class. Shared data needs protection; you've traded the super-loop's simple determinism for races, priority inversion, and deadlocks. Budget for that complexity.
- Blocking changes meaning. In a super-loop, blocking stalls everything; in an RTOS, a blocking call yields to other tasks, but blocking in an ISR is still forbidden, and over-blocking can still starve lower-priority tasks.
TL;DR
- Bare-metal firmware is a
while(1)super-loop plus ISRs: tiny, deterministic, no overhead, but everything runs at the slowest step's pace, blocking stalls all, and prioritizing is manual and fragile. - An RTOS runs each activity as an independent task scheduled by priority, so high-priority work preempts low, and blocking yields the CPU to other tasks instead of freezing the system.
- The costs are real: per-task stack RAM + kernel footprint, scheduler overhead, and a whole new class of concurrency bugs (mutexes, priority inversion, deadlocks, the rest of this module).
- Use an RTOS for multiple independent activities with different rates/priorities and blocking I/O; stay bare-metal for simple or severely RAM-constrained jobs, and remember a super-loop + interrupts + state machines covers a lot.
- An RTOS provides the tools for real-time but doesn't guarantee it, meeting deadlines is something you still design and verify.