Electronics Design AU
Embedded Systems

Bare-Metal vs RTOS: Which Should You Use for Your Firmware?

Last updated 26 June 2026 · 6 min read

Direct Answer

Bare-metal firmware runs a single main loop with interrupt handlers and no operating system layer; it is simpler and lower overhead, making it the right choice for single-purpose tasks with clear, simple concurrency. An RTOS adds a task scheduler, queues, and semaphores, which is worthwhile when a system has three or more independent activities with different timing requirements that would be complex to coordinate manually — the overhead is a few kilobytes of RAM and some added scheduling latency.

Detailed Explanation

The choice between bare-metal and RTOS is a firmware architecture decision, not a capability question — both approaches can produce reliable, production-grade embedded software. The right answer depends on how many independent activities your application has, how tightly their timing requirements interact, and how complex the coordination between them is.

What is bare-metal firmware?

Bare-metal firmware runs directly on the hardware with no operating system layer. The typical structure is a superloop — an infinite while(1) loop in main() that polls for work and calls handler functions — combined with interrupt service routines (ISRs) that respond to hardware events asynchronously.

int main(void) {
    hardware_init();
    while (1) {
        if (uart_data_ready()) process_uart();
        if (sensor_timer_elapsed()) read_sensor();
        if (button_pressed()) handle_button();
        enter_low_power_if_idle();
    }
}

The superloop is simple, transparent, and uses zero RAM beyond what the application needs. Timing is managed by the programmer explicitly — poll intervals, flags set in ISRs, and careful ordering of operations.

What is RTOS-based firmware?

RTOS firmware replaces the superloop with a task scheduler. Each independent activity becomes a task — a function that runs in its own infinite loop, with its own stack, blocked when it has nothing to do.

void sensor_task(void *params) {
    for (;;) {
        ulTaskNotifyTake(pdTRUE, portMAX_DELAY);  // blocked until notified
        sensor_value = read_sensor_i2c();
        xQueueSend(data_queue, &sensor_value, 0);
    }
}

The RTOS kernel decides which task runs, suspends blocked tasks so they consume zero CPU, and wakes them when their event arrives. Tasks communicate via thread-safe primitives: queues, semaphores, mutexes, and event groups.

When bare-metal is the right choice

IndicatorWhy bare-metal suits it
One or two main activitiesA superloop handles them cleanly without scheduling overhead
Very small MCU (< 16 kB RAM)RTOS RAM overhead may not leave enough room for the application
All timing driven by a single timer or interruptNo concurrency coordination required
Team unfamiliar with RTOS conceptsBare-metal is easier to reason about without RTOS debugging experience
Safety-critical code needing formal analysisBare-metal with deterministic ISRs is often easier to certify than an RTOS

A battery-powered temperature logger that wakes on a timer, reads a sensor, writes to flash, and sleeps again is a textbook bare-metal application — it does exactly one thing in a predictable sequence.

When an RTOS is the right choice

IndicatorWhy RTOS suits it
Three or more independent activities with different timingManually coordinating them in a superloop becomes complex and fragile
Activities that block (wait for network, flash write, UI input)RTOS blocking is efficient; polling wastes CPU and complicates timing
Wireless protocol stack (BLE, Wi-Fi)Most BLE/Wi-Fi stacks (ESP-IDF, Zephyr) are RTOS-native and assume task-based concurrency
Multiple developers on one firmware codebaseRTOS tasks partition ownership cleanly
Power management with multiple wake sourcesRTOS idle hooks integrate cleanly with sleep modes

A BLE-connected device reading three sensors, maintaining a cloud connection, processing user commands, and logging data to flash simultaneously is a strong RTOS candidate — the concurrency and blocking behaviour would be painful to manage in a superloop.

The RAM cost of an RTOS

FreeRTOS on ARM Cortex-M4 with a minimal configuration adds roughly 5–10 kB of flash for the kernel and requires approximately 300–500 bytes of RAM for the kernel itself plus the size of each task's stack. A realistic system with four tasks and 512-byte stacks each adds about 4–6 kB of RAM overhead on top of application data. On an STM32 with 64 kB or more of SRAM, this is comfortable. On a Cortex-M0+ with 8 kB, it may leave too little room — RAM budget is one reason MCU selection and firmware architecture should be decided together.

Practical Examples

Bare-metal suits a smart plug controller: one main loop samples the current sensor at 50 Hz, integrates power usage, and responds to button presses. Two ISRs handle the mains zero-cross detection and a UART for configuration. The entire firmware is 12 kB of flash and 2 kB of RAM — an RTOS would consume more resources than the application logic itself.

RTOS suits a connected industrial monitor: a Modbus RTU reader, a 4G cellular modem driver, a local SD card logger, and a watchdog heartbeat task all run concurrently. The Modbus task blocks waiting for RS-485 responses; the cellular task blocks on HTTP responses. Without an RTOS, ensuring both can block without starving each other requires a complex state machine that the RTOS scheduler handles for free.

Design Considerations

  • Don't add an RTOS pre-emptively: start with bare-metal and add an RTOS when you encounter a specific concurrency problem that's genuinely hard to solve without one. Retrofitting an RTOS later is manageable; removing one is less common. The community discussion FreeRTOS vs bare-metal for a simple sensor node explores exactly this decision — when a working super-loop is the right answer, and what actually changes the calculation.
  • ISRs remain outside the RTOS: whether you use an RTOS or not, time-critical hardware responses still belong in ISRs at interrupt priority — not in RTOS tasks. The RTOS manages tasks, not interrupts.
  • RTOS-aware tooling: FreeRTOS integrates with SEGGER SystemView, Tracealyzer, and J-Link RTT for task-level profiling. These tools are essential for diagnosing priority inversion, stack overflow, and missed deadlines in production firmware.
  • Firmware architecture review: choosing the right architecture upfront saves weeks of painful refactoring. Zeus Design's embedded software team regularly advises on and implements both bare-metal and RTOS-based firmware designs across STM32, ESP32, and nRF platforms.

Common Mistakes

  • Solving a complexity problem with an RTOS when the real problem is poor code structure: if your bare-metal superloop is unmanageable, the root cause is often tangled logic, not missing task scheduling. Refactoring to a cleaner state-machine-based superloop may be all that's needed.
  • Calling blocking RTOS functions from ISRs: ISRs must use ISR-safe RTOS API calls (e.g. xQueueSendFromISR in FreeRTOS). Calling the standard blocking versions from an ISR corrupts the RTOS scheduler state.
  • Underestimating stack sizes: each RTOS task needs its own stack. Stack overflows in RTOS systems are a common source of hard-to-reproduce crashes. Enable FreeRTOS stack overflow detection during development and measure high-water marks before finalising stack sizes.
  • Treating RTOS priorities as arbitrary: assigning priorities without thinking about which tasks have the tightest timing requirements leads to priority inversion and missed deadlines. Map priority levels to timing requirements explicitly.

Frequently Asked Questions

Can you mix bare-metal and RTOS approaches in the same project?
Yes, and it is common. Time-critical ISRs always run outside the RTOS scheduler, at interrupt priority — they never sleep or block. The RTOS manages application-level tasks (sensor reads, comms, logging), while ISRs handle hardware events with the shortest possible response time and signal tasks via queues or semaphores.
Is an RTOS harder to debug than bare-metal?
Often yes, initially. Debugging a deadlock caused by two tasks competing for the same mutex, or a priority-inversion problem, requires understanding the RTOS state at the time of the bug. Most RTOS-aware debuggers (Ozone, J-Link, SystemView) can display task states, stack usage, and scheduling history, which makes RTOS debugging tractable once you have the right tools. Bare-metal bugs — usually ISR timing and shared state — can be just as subtle.
Does using FreeRTOS lock me into ARM?
No. FreeRTOS has been ported to dozens of processor architectures including Cortex-M, Cortex-A, AVR, PIC32, RISC-V, and Xtensa (ESP32). Zephyr supports an even wider range. Switching RTOS or switching MCU families while keeping the same RTOS is feasible — the application code built on RTOS primitives (tasks, queues, semaphores) is broadly portable.

References

Related Questions

Related Forum Discussions