A motor spins at a precise speed. Every millisecond an ADC sample has to be read and pushed through a control loop. A radio interrupts to deliver a packet that must be acknowledged before the sender retries. A display refreshes, buttons bounce, a status LED blinks.

On a bare-metal microcontroller, all of that usually lives in one main loop. It works fine until one job takes a little too long and the control loop misses its slot. That missed slot is the moment engineers start asking about a real-time operating system. An RTOS will not make your firmware faster. It makes the timing of your firmware predictable, which is a different and often more useful thing.

Real-time means predictable, not fast

The word "real-time" trips people up. It does not mean fast. It means the system responds within a bounded time, every time. A modest microcontroller running a tight control loop can be perfectly real-time; a desktop processor running a general-purpose OS usually is not, because nothing in it promises your audio callback runs within two milliseconds.

Deadlines come in flavours:

  • Hard: missing it means failure. Motor commutation, airbag triggers, a safety node that stops sending its heartbeat.
  • Firm: a late result is worthless, but nothing breaks. A dropped frame, a sensor sample that gets thrown away.
  • Soft: late is annoying but tolerable. A UI refresh, a status LED, a log flush.

Two words come up whenever you measure this: latency (how long from event to response) and jitter (how much that time varies). An RTOS earns its keep by keeping both bounded, not by shaving the average.

Tasks, priorities, and scheduling

A task — some kernels call it a thread — is a piece of work with its own stack and its own state: running, ready, or blocked. The scheduler decides which ready task gets the CPU.

In a preemptive priority-based scheduler, the highest-priority ready task always runs. The moment a higher-priority task becomes ready, say a UART interrupt releases a semaphore, the kernel saves the current task's registers and switches. That is a context switch, and it costs cycles, so you do not want thousands of them per millisecond.

Equal-priority tasks normally take turns, either by time slicing or by running until they block. The tick is a periodic timer interrupt that drives timekeeping and timeouts, commonly around 1 kHz, though tickless designs shut it off to save power when nothing is due.

Priorities are a design decision

Do not rank priorities by how much you like each feature. If a low-priority task holds a resource a high-priority task needs, the high-priority task waits — priority inversion. The usual fix is a mutex with priority inheritance, which temporarily lifts the holder's priority; some kernels offer a priority-ceiling protocol instead. The better fix is to keep critical sections short and move data through queues rather than shared globals.

What an RTOS actually gives you

  • A scheduler with priorities, preemption and deterministic wake-up
  • Communication primitives: queues, semaphores, mutexes, event flags
  • Timing services: delays, timeouts on blocking calls, software timers
  • A portability layer, so the same task code runs on a different vendor's part
  • Middleware that assumes a kernel: TCP/IP stacks, USB device stacks, filesystems, BLE and Thread stacks
  • Isolation: each task has its own stack, so a corrupted variable is easier to localise

When a bare-metal main loop is the better answer

Plenty of good products never use an RTOS. The superloop is simple: initialise everything, then loop forever calling functions in a fixed order, with timers and interrupts setting flags for the loop to consume.

It wins on memory — no kernel, one stack, no task control blocks — and on a different kind of predictability: what you read is what runs. There are no races and no deadlocks, and a breakpoint does not disturb a scheduler that does not exist.

The catch is that there is no safety net. One blocking call — a flash write, a busy-wait on a sensor, a long print to a slow UART — and every other job in the loop waits behind it. If your device does two or three things and the timing math has margin, stay bare metal. Complexity you do not need is a bug you do not have.

The real costs

  • RAM: every task needs its own stack, typically a few hundred bytes to several kilobytes, plus kernel structures.
  • CPU: context switches, tick interrupts and locking all take cycles.
  • New failure modes: races, deadlocks and priority inversion become possible.
  • Debugging: halting at a breakpoint freezes the scheduler, so timing bugs often get worse under a debugger. Trace hardware and GPIO toggles help more.
  • Licensing and certification: the kernel becomes a supply-chain item. Safety-certified builds carry paperwork and cost, and some commercial kernels are not free.

Signals you probably need one

  1. You have three or more independent activities with different periods and deadlines.
  2. At least one of them must block — waiting for a packet, a DMA completion, a button press — without stalling the rest.
  3. You have to promise a worst-case response time and cannot prove it with a single loop.
  4. You are adding middleware that assumes a kernel: TCP/IP, USB, a filesystem, a BLE stack.
  5. Your main loop has become a pile of flags and nested state machines that nobody wants to touch.

Start bare metal, move when the timing math says so

Write down the timing requirement for each job: period, deadline, and how much jitter it tolerates. Then build the bare-metal version and measure it. Toggle a GPIO at the start and end of each job and look at the scope; that tells you the worst-case execution time far more honestly than a stopwatch. Add those times together, subtract the interrupt load, and compare against your shortest deadline. Comfortable margin? Keep the loop.

If you do move, do it in stages. Start with two tasks: one high-priority for control or time-critical I/O, one low-priority for housekeeping. Pass data through queues instead of globals. Give every blocking call a timeout so a stalled task fails visibly rather than hanging. Turn on stack high-water-mark reporting, then leave each task a few hundred bytes of headroom beyond the measured peak. FreeRTOS, Zephyr, ThreadX and similar kernels all run happily on Cortex-M class parts, and the concepts transfer if you switch later.

The honest answer to "do I need an RTOS?" is usually "not yet, but know when". Keep the bare-metal loop until the timing math stops working, and adopt a kernel because a deadline demands it — not because it sounds more professional.

Photo: ThisIsEngineering / Pexels