Bare-Metal Super-Loop vs RTOS
Embedded → RTOS: Tasks & Scheduling
The while(1) that quietly stops scaling
Almost every embedded project starts the same way: a setup() that configures peripherals, then a while(1)— the “super-loop” — that polls sensors, checks button state, and drives outputs, all in one flat sequence, forever. Simple, deterministic, easy to trace with a debugger — for a huge number of embedded products, that's genuinely all you need.
The trouble starts with a second job on a different timing requirement. Say the loop now also has to service a radio stack needing attention every few milliseconds, while a sensor read takes 200ms. In a super-loop, that 200ms read blocks everything else — the radio stack misses its window, packets drop. A state machine can step through the slow operation non-blockingly, but every new concurrent job multiplies that hand-rolled scheduling logic's complexity. This is the wall an RTOS exists to knock down.
What an RTOS actually adds
An RTOS (Real-Time Operating System) is a small, lightweight operating system built for microcontrollers. It gives three things a super-loop doesn't have on its own:
| Piece | What it gives you |
|---|---|
| A scheduler | decides, on a recurring timer tick, which of several independent tasks gets the CPU next, based on priority and readiness |
| Tasks | each with its own stack and program counter — write "read the sensor" and "service the radio" as separate, independently-blocking functions instead of interleaving them by hand |
| Real-time guarantees | "real-time" doesn't mean "fast" — a task's worst-case execution timing can be reasoned about and bounded before runtime, which matters when a missed deadline is a real failure (a motor control loop, a safety interlock) |
Under the hood this is still bare-metal C running on the same microcontroller — an RTOS is a library you link in and a scheduler that runs off a hardware timer interrupt, not separate silicon. The 200ms sensor read becomes its own task that calls vTaskDelay()or blocks on I/O completion, and while it's blocked the scheduler simply runs the radio task instead.
The decision that actually matters: when NOT to reach for an RTOS
The honest answer to “when would you notuse an RTOS” isn't “always use one.” An RTOS adds real cost: extra flash/RAM for the kernel and each task's own stack, execution order that's harder to trace with a plain breakpoint debugger, and a genuinely new class of bugs (race conditions, priority inversion, deadlock) that a single-threaded super-loop simply can't have.
// Reach for a super-loop when: // - one, or a small number of, timing requirements — no real concurrency // - tight RAM/flash budget (a few KB of RAM, an 8-bit MCU) // - the whole product is small enough that hand-rolled state machines stay readable // Reach for an RTOS when: // - several independent jobs with DIFFERENT timing requirements // (e.g. a 1ms motor control loop + a 100ms UI update + a UART command parser) // - you need priority: some work must always preempt other work // - the product will keep growing new concurrent features over its lifetime
ifchecks and it stays readable, you probably don't need an RTOS yet. The moment you're manually tracking “has enough time passed since I last did X” for three or more independent things at once, you're already reimplementing a scheduler badly — the signal to switch to a real one.Try it — one slow task starves the whole loop
Run a “blink LED every 50ms” task alongside a “slow sensor read” task under both models. Under a super-loop, the slow read blocks the LED task until it finishes. Under an RTOS, the scheduler preempts the slow task and the LED keeps its 50ms cadence.
Practice
delay() calls in it. Time how late the LED blink drifts when the sensor read takes 200ms. Then re-implement it as two FreeRTOS tasks using vTaskDelay() instead of delay(), and measure the same drift — it should stay essentially zero regardless of how long the sensor task takes.Completion saves automatically after you read through to the end.
Next lesson →