01 · Why an RTOS?¶
Almost every microcontroller program starts life as a superloop: one
loop() that reads inputs, does work, and writes outputs, forever. It's a
fine architecture — until the firmware has to do several timing-sensitive
things at once. This module explains where the superloop breaks down, what
"real-time" actually means (hint: it's not "fast"), how an RTOS scheduler
solves the problem, and — just as important — when you shouldn't reach for
one. It ends with how to set up a free, no-hardware environment for the whole
level.
The bare-metal superloop and where it cracks¶
Here is a perfectly reasonable bare-metal sketch: blink an LED at 2 Hz and print a sensor reading every second.
void setup() {
Serial.begin(115200);
pinMode(2, OUTPUT);
}
void loop() {
digitalWrite(2, HIGH);
delay(250);
digitalWrite(2, LOW);
delay(250);
Serial.println(analogRead(34)); // runs every ~500 ms, not every 1000 ms
delay(500); // ...so patch it with another delay
}
The timings are already entangled: change the blink rate and the print rate
changes too. Experienced Arduino programmers fix this with millis()-based
cooperative scheduling (covered in depth in the
Embedded Systems Mastery Path),
which works well — but it has structural limits:
- One slow job stalls everything. If parsing a command or writing to flash takes 80 ms, every other "task" in the loop is late by 80 ms. The loop is only as responsive as its slowest step.
- No priorities. The loop gives every job equal standing. You can't say "the motor-control update at 1 kHz always wins over the logging."
- State machines everywhere. Each job that must not block gets rewritten as a state machine with its own timestamps and flags. At five jobs it's manageable; at fifteen it's a maintenance nightmare.
- Worst-case timing is a guess. The time between two runs of any job is the sum of everything else in the loop — which changes with every code edit.
What "real-time" actually means¶
A real-time system is not a fast system. Real-time means deterministic: the system guarantees it responds within a known deadline, every time — and you can reason about that bound.
- A 16 MHz Arduino toggling a pin within 10 µs of an interrupt, always, is real-time.
- A 3 GHz server that usually answers in 1 ms but occasionally takes 2 s (garbage collection, page fault...) is fast but not real-time.
Two flavors you'll hear about:
- Hard real-time — missing a deadline is a system failure (airbag trigger, motor commutation, pacemaker pulse).
- Soft real-time — missing a deadline degrades quality but isn't fatal (dropped audio frame, delayed screen refresh).
An RTOS is an operating system whose scheduler is built for determinism: the highest-priority ready task runs, immediately, with a small and bounded switching cost — rather than "whenever the loop gets around to it."
Preemptive vs. cooperative scheduling¶
- Cooperative: a task runs until it voluntarily gives up the CPU. The
millis()superloop is cooperative scheduling by hand. Cheap and predictable — but one misbehaving task freezes the system, and response time depends on every task's good manners. - Preemptive: the scheduler can interrupt (preempt) a running task the moment a higher-priority task becomes ready. The slow flash-write no longer delays the motor update — the motor task simply preempts it, runs, and the flash write resumes where it left off.
FreeRTOS is preemptive by default (cooperative mode exists as a config option). Preemption is what buys you the core RTOS promise: the latency of your critical task no longer depends on the rest of your code.
Superloop (cooperative): RTOS (preemptive):
|--blink--|--LOG 80ms--|--motor-- |--motor--| (motor READY → runs NOW)
^ preempts
motor is 80 ms late |--log......log--| log resumes after
What FreeRTOS gives you¶
FreeRTOS is the most widely deployed RTOS kernel in the world — billions of devices, from sensors to spacecraft. The relevant facts:
- Tiny: the kernel is roughly 6-12 kB of flash and a few kB of RAM in a typical build — three C files plus a port layer.
- Free and permissive: MIT-licensed, no royalties, commercial use allowed. (A safety-certified derivative, SAFERTOS, exists for regulated industries — Level 4 territory.)
- Ubiquitous: it's the kernel inside the ESP32 Arduino core, ESP-IDF, AWS IoT device SDKs, and countless vendor SDKs. Learn it once, use it everywhere.
- A small, learnable API: tasks, queues, semaphores, mutexes, timers, notifications, event groups — that's the entire Level 1 syllabus, and it's 90 % of what production firmware uses.
When NOT to use an RTOS¶
An RTOS is a tool, not a badge of professionalism. Skip it when:
- The superloop is genuinely enough. One or two activities, simple
timing —
millis()scheduling is simpler to write, test, and debug. - RAM is critically scarce. Every task needs its own stack (hundreds of bytes to kilobytes each). On a 2 kB-RAM part, tasks are a luxury.
- Your timing is purely interrupt-driven. If ISRs plus a trivial background loop already meet the deadlines, a scheduler adds nothing.
- The team doesn't know it yet and the deadline is tomorrow. Concurrency bugs (races, deadlocks, starvation) are a real tax; this course exists to make you pay it once, cheaply, in a simulator.
Rule of thumb: when you find yourself building your third hand-rolled state machine with its own timing bookkeeping, or you need priorities, it's RTOS time.
Your lab for this level: ESP32 + Wokwi (no hardware)¶
The ESP32 Arduino core runs on FreeRTOS out of the box — setup() and
loop() themselves execute inside a FreeRTOS task, and the entire FreeRTOS
API is available with no extra includes or installs. That makes it the
easiest way in the world to run real RTOS code:
- Open wokwi.com in a browser.
- Create a new ESP32 project.
- Paste any sketch from this course into
sketch.inoand press the play button. Serial output appears in the built-in monitor.
Everything works identically on a real ESP32 board via the Arduino IDE.
Pure-C alternatives (no Arduino)
Prefer studying FreeRTOS as plain C on your PC? Two good options, both covered further in Level 2:
- The official POSIX/Linux simulation port compiles the real kernel into a native Linux/macOS program — full API, gdb debugging, no hardware.
- QEMU emulates real boards (e.g.
qemu-system-arm -machine mps2-an385runs official FreeRTOS Cortex-M demos).
The API you learn here is identical in all three environments.
Cheat sheet¶
| Concept | Meaning |
|---|---|
| Superloop | One big loop() doing everything; response time = sum of all jobs |
| Real-time | Deterministic response within a deadline — not "fast" |
| Hard / soft real-time | Missed deadline = failure / = degraded quality |
| Cooperative scheduling | Tasks yield voluntarily; one bad task stalls all |
| Preemptive scheduling | Scheduler interrupts lower-priority work the instant higher-priority work is ready |
| FreeRTOS | MIT-licensed, ~10 kB kernel; the world's most deployed RTOS; built into the ESP32 Arduino core |
| When to skip an RTOS | Simple timing, tiny RAM, pure ISR designs, or no team experience + no time |
How It Actually Works¶
A "superloop" and an RTOS run on the same Cortex/Xtensa core with the same
interrupt hardware — the difference is entirely in software. FreeRTOS's
scheduler is just a C function, vTaskSwitchContext(), invoked from two
places: the SysTick/tick-timer ISR (or on ESP32, a hardware timer driving
xPortSysTickHandler) and portYIELD(). Every task gets a Task Control
Block (TCB) — a plain struct holding its saved stack pointer, priority,
state, and the list nodes that place it on one of the kernel's state lists
(Ready list per priority, a Delayed list, blocked lists for queues/semaphores).
There is no free lunch: an RTOS spends real CPU cycles on every tick interrupt
(walking the delayed-task list, checking timeouts) and on every context
switch (saving ~16-32 registers to one task's stack, loading another's) —
typically low single-digit microseconds on a 240 MHz Xtensa core. That
overhead is the literal cost of the promise "the highest-priority ready work
always runs within a bounded, tiny latency," which a superloop cannot make
because one delay(5000) blocks everything, including a button ISR's
handler if it was written to poll a flag the loop never reaches in time.
Exercise¶
- Take the entangled blink-plus-print sketch from the top of this page and list every timing bug it has (there are at least three: blink duty cycle, print period, and serial-command latency if you added one).
- Sketch (on paper) how you'd fix it with
millis()— count how many state variables you need. - Write down two activities from a device you own (e.g. a smart kettle: heater control + display update + button handling) and classify each as hard or soft real-time, with the deadline you'd assign.
- Set up the Wokwi ESP32 project described above and run the default blink sketch — you'll build on this project in every following module.