Table of contents
RTOS vs Bare-Metal Firmware: When a Scheduler Pays Off
RTOS vs bare-metal firmware: how superloops and preemptive schedulers handle timing, what scheduling theory guarantees, and the priority-inversion trap.
The firmware reads a sensor every millisecond, updates a display, writes logs to flash and talks to a radio, all in one loop. It worked until the flash write grew to four milliseconds; now the sensor is read late every time a log is written. The RTOS vs bare metal decision usually shows up at exactly this moment: when one long job starts delaying a short, urgent one. This article explains how each approach handles timing, what real-time scheduling research can and can’t guarantee, and the failure modes that come with a scheduler, including the priority inversion that reset a spacecraft on Mars.
Key Takeaways
- In a bare-metal superloop, the worst-case latency of any job is roughly the time of one full pass through the loop. A single long job delays everything else.
- A preemptive RTOS lets a high-priority task interrupt a low-priority one, so the worst-case latency of an urgent task no longer depends on the length of unrelated work.
- Liu and Layland (1973) proved that rate-monotonic priorities meet all deadlines if total CPU utilization stays under n(2^(1/n) − 1), about 78% for three tasks and approaching 69% for many. Exact response-time analysis often admits more.
- Lehoczky, Sha and Ding (1989) found that randomly generated task sets were typically schedulable up to about 88% utilization under rate-monotonic priorities, so the bound is pessimistic, not a ceiling.
- Shared resources introduce priority inversion. Mars Pathfinder reset repeatedly in 1997 until priority inheritance, already available in its kernel, was turned on.
The problem: a loop is only as fast as its slowest job
A bare-metal program usually has the shape of a superloop: initialize the hardware, then repeat forever: check inputs, run each job, maybe sleep. Interrupt handlers catch events that can’t wait and set flags for the loop to process. Nothing else runs. It’s the simplest structure there is, and for many products it’s the right one.
The trouble is that every job waits for every other job. If the loop calls a 0.2 ms sensor routine, a 1 ms display update and a 4 ms flash write in turn, the sensor routine can run at most once every 5.2 ms, however urgent it is. Firmware engineers then start splitting long jobs into state machines that do a slice of work per pass, moving logic into interrupt handlers, or adding special cases (“skip the display if the sensor is due”). Each fix works. Together they make the timing hard to reason about, which is the thing the superloop was supposed to avoid.
An RTOS (real-time operating system) changes the structure. Each job becomes a task with a priority. The kernel runs the highest-priority task that’s ready, and when a higher-priority task becomes ready (because a timer expired or an interrupt released it), the kernel preempts the running task, saves its context and switches. The flash write still takes 4 ms, but the sensor task can interrupt it.
What research says about scheduling
The rate-monotonic bound
The foundation of fixed-priority scheduling is a 1973 paper by C. L. Liu and James Layland in the Journal of the ACM. They considered periodic tasks, each with a period, a worst-case execution time and a deadline equal to its period, running on one processor with preemption. They showed that assigning priorities by rate (shorter period means higher priority) is optimal among fixed-priority schemes, and that such a task set always meets its deadlines if total utilization U (the sum of execution time divided by period over all tasks) satisfies:
U ≤ n(2^(1/n) − 1)
For one task the bound is 100%, for two about 82.8%, for three about 78.0%, and as n grows it approaches ln 2, about 69.3%. The test is sufficient, not necessary: a task set above the bound may still be schedulable, but the bound doesn’t prove it.
How pessimistic the bound is
Lehoczky, Sha and Ding gave an exact characterization of rate-monotonic schedulability at the 1989 Real-Time Systems Symposium and studied average behavior. For large sets of randomly generated tasks, the utilization at which deadlines started to be missed was typically around 88%, well above the 69% worst case. Real firmware isn’t a random task set, so this isn’t a design target, but it explains why many systems run comfortably above the Liu and Layland bound.
Response-time analysis
The practical tool for an actual design is response-time analysis, described by Audsley, Burns, Richardson, Tindell and Wellings in a 1993 Software Engineering Journal paper. The worst-case response time R of a task is its own execution time C plus the interference from every higher-priority task j released while it’s running:
R = C + Σ ⌈R / Tj⌉ · Cj
The equation is solved by iteration, starting from R = C plus one execution of each higher-priority task, until R stops changing. If R is no longer than the task’s deadline, the task is schedulable. The same framework extends to blocking from shared resources, release jitter and deadlines shorter than periods.
A worked example
Take the three jobs from the opening as tasks, with illustrative numbers:
- Sensor: period 1 ms, execution 0.2 ms (U = 0.20)
- Display: period 5 ms, execution 1 ms (U = 0.20)
- Logging: period 20 ms, execution 4 ms (U = 0.20)
Total utilization is 60%, under the three-task bound of 78%, so rate-monotonic priorities are guaranteed to work. Response-time analysis gives the actual worst cases:
- Sensor: R = 0.2 ms (nothing preempts it).
- Display: start at 1.2 ms; the sensor is released twice in that window, so R = 1 + 2 × 0.2 = 1.4 ms, which is stable. Deadline 5 ms: met.
- Logging: start at 5.2 ms. Iterating: 4 + 6 × 0.2 + 2 × 1 = 7.2 ms, then 4 + 8 × 0.2 + 2 × 1 = 7.6 ms, then 7.6 ms again. Deadline 20 ms: met.
In the superloop, the sensor’s worst-case latency was 5.2 ms against a 1 ms requirement. Under the RTOS it’s 0.2 ms plus kernel overhead. The kernel overhead (context switches, tick handling) has to be measured on the target and added to each execution time; on small microcontrollers it’s typically in the range of microseconds per switch, but that’s a figure to measure, not assume.
Priority inversion and Mars Pathfinder
Preemption creates a new problem when tasks share resources. If a low-priority task holds a mutex that a high-priority task needs, the high-priority task must wait. That wait is expected and bounded by the length of the critical section. But if a medium-priority task preempts the low-priority one while it holds the mutex, the high-priority task waits for the medium task too, for as long as it runs. The high-priority task is effectively running at the lowest priority.
Sha, Rajkumar and Lehoczky analyzed this in a 1990 paper in IEEE Transactions on Computers and proposed priority inheritance protocols: while a task holds a resource that a higher-priority task is waiting for, it runs at the waiting task’s priority, so medium-priority work can’t get in the way. Their priority ceiling protocol goes further, bounding blocking to at most one critical section and preventing deadlock between those resources.
The best-known real case came in July 1997. A few days after Mars Pathfinder landed, the spacecraft began resetting. As Mike Jones recounted from a talk by Wind River’s David Wilner, a high-priority bus-management task shared a mutex with a low-priority meteorological task; a medium-priority communications task could preempt the meteorological task while it held the mutex, the bus task missed its schedule, and a watchdog reset the system. The VxWorks mutexes supported priority inheritance through an initialization flag, but it was off. Engineers uploaded a patch that turned it on, and the resets stopped.
The design lesson is less about Mars than about defaults: most RTOS kernels offer priority inheritance on mutexes, and some make it optional or treat binary semaphores differently from mutexes. Use the primitive that has it for anything shared between priorities.
RTOS vs bare metal compared
| Property | Bare-metal superloop | Preemptive RTOS |
|---|---|---|
| Worst-case latency of an urgent job | About one full loop pass (unless moved into an interrupt) | Its own execution plus higher-priority interference and blocking |
| Timing analysis | Simple while all jobs are short; ad hoc once jobs are split | Formal: utilization bound and response-time analysis |
| Memory | Smallest; one stack | Kernel code plus a stack and control block per task |
| CPU overhead | None beyond the loop | Context switches and tick or timer handling |
| Concurrency bugs | Mostly between main loop and interrupts | Also between tasks: races, deadlock, priority inversion |
| Power management | Explicit sleep in the loop | Idle task or tickless idle, depending on the kernel |
| Middleware (networking, USB, file systems) | Often written for polling or callbacks | Often assumes threads and blocking calls |
| Best fit | Few jobs, all short; strict size or certification limits | Mixed long and urgent work; several protocols or stacks |
What to actually do
Write the task table first
List every job with its period or triggering event, worst-case execution time and deadline. Measure execution times on the target with a GPIO toggle or cycle counter rather than estimating. This table is useful whichever way you go.
Stay bare metal if every job is short
If the longest job is shorter than the tightest deadline, a superloop with interrupts for the truly urgent events is often the simplest correct design. Many sensor nodes, simple controllers and bootloaders fall here.
Move to an RTOS when long and urgent work collide
If you find yourself splitting long jobs into slices just to keep a short job on time, or a stack you need (TCP/IP, USB, a file system) expects blocking calls, a preemptive scheduler is usually less complex than the workarounds.
Assign priorities by deadline and check them
Rate-monotonic (or deadline-monotonic, when deadlines are shorter than periods) priorities are a good default. Run response-time analysis on the task table, with measured kernel overhead added, and keep the margin.
Use mutexes with priority inheritance
Protect shared resources with the kernel’s mutex type that supports priority inheritance, keep critical sections short, and avoid sharing a resource between a very high and a very low priority task when a message queue would do.
What not to do
Don’t use an RTOS to avoid thinking about timing; it moves the problem into priorities and blocking. And don’t size stacks by guessing: measure the high-water mark under worst-case conditions, since a stack overflow in one task corrupts another.
How to put it to work
If you have an hour
Build the task table for your current firmware: period, measured execution time and deadline for each job. Compute total utilization and the worst-case latency of your most urgent job in the current design.
If you have a sprint
If the latency doesn’t meet the deadline, prototype the urgent job as a high-priority task under an RTOS, or as an interrupt-driven state machine in the superloop, and compare measured latency, code size and RAM.
If you want it to last the product’s lifetime
Keep the task table and response-time analysis with the firmware and rerun them when a job changes. Add stack high-water marks and missed-deadline counters to the diagnostics you can read from the field.
The question to ask yourself: “If the longest job in this firmware doubled in length, which deadline would be missed first, and would I know?”
Frequently asked questions
What is the difference between an RTOS and bare-metal firmware?
Bare-metal firmware runs directly on the hardware, usually as a single loop plus interrupt handlers. An RTOS adds a scheduler that runs several tasks with priorities and can preempt a running task when a more urgent one becomes ready, along with primitives such as queues, semaphores and mutexes.
When should I use an RTOS?
When long-running work and short, urgent work have to coexist and splitting the long work by hand becomes complex, or when the software stacks you depend on assume threads and blocking calls. If every job is short and the system is small, bare metal is often simpler.
What is the rate-monotonic utilization bound?
It’s the result from Liu and Layland (1973): with priorities assigned by period, a set of n periodic tasks always meets its deadlines if total utilization is at most n(2^(1/n) − 1), about 78% for three tasks and approaching 69% for many. Task sets above the bound can still be schedulable; response-time analysis checks them exactly.
What is priority inversion?
It’s when a high-priority task waits for a low-priority task that holds a shared resource, and a medium-priority task preempts the low-priority one, so the high-priority task is delayed by unrelated work. Priority inheritance, in which the resource holder temporarily takes the waiting task’s priority, prevents the unbounded part of the delay.
Does an RTOS make firmware slower?
It adds overhead for context switches and timer handling, and memory for each task’s stack and control block. In return, urgent work no longer waits for long jobs. Whether the trade is worth it depends on the timing requirements; the overhead should be measured on the target.
Related reading: polling vs interrupts vs DMA, measuring microcontroller power consumption and running inference on a Cortex-M.
Sources
- Liu, C. L., & Layland, J. W. (1973). “Scheduling Algorithms for Multiprogramming in a Hard-Real-Time Environment.” Journal of the ACM, 20(1), 46–61. https://doi.org/10.1145/321738.321743
- Lehoczky, J., Sha, L., & Ding, Y. (1989). “The Rate Monotonic Scheduling Algorithm: Exact Characterization and Average Case Behavior.” Proceedings of the Real-Time Systems Symposium, 166–171. https://doi.org/10.1109/REAL.1989.63567
- Sha, L., Rajkumar, R., & Lehoczky, J. P. (1990). “Priority Inheritance Protocols: An Approach to Real-Time Synchronization.” IEEE Transactions on Computers, 39(9), 1175–1185. https://doi.org/10.1109/12.57058
- Audsley, N., Burns, A., Richardson, M., Tindell, K., & Wellings, A. J. (1993). “Applying New Scheduling Theory to Static Priority Pre-emptive Scheduling.” Software Engineering Journal, 8(5), 284. https://doi.org/10.1049/sej.1993.0034
- Jones, M. (1997). “What Really Happened on Mars?” Account of David Wilner’s talk at the IEEE Real-Time Systems Symposium. https://www.cs.cornell.edu/courses/cs614/1999sp/papers/pathfinder.html



