FreeRTOS vs Bare Metal: When to Use an RTOS in Your Embedded Project

Introduction

Every embedded systems engineer faces a fundamental architectural decision early in a project: should the firmware run on bare metal or on a real-time operating system (RTOS)? For decades, bare metal super-loops dominated the microcontroller landscape. A simple while(1) loop with interrupt service routines (ISRs) was enough for most applications. But as devices grew more connected, feature-rich, and time-sensitive, RTOS adoption surged—and FreeRTOS, the open-source lightweight kernel now maintained by Amazon Web Services, became the de facto choice for millions of embedded projects worldwide.

This article provides a thorough, practical comparison of bare metal programming and FreeRTOS-based development. We will examine architectures, performance characteristics, memory footprints, debugging complexity, and real-world use cases to help you decide which approach best serves your project. By the end, you will have a clear decision framework for choosing between bare metal and FreeRTOS.

Understanding the Two Architectures

Bare Metal: The Super-Loop Model

Bare metal programming runs firmware directly on the microcontroller without an intermediary operating system layer. The classic architecture is the super-loop:

void main(void) {
    SystemInit();
    while (1) {
        ReadSensors();
        ProcessData();
        UpdateDisplay();
        HandleCommunication();
    }
}

All tasks execute sequentially inside a single infinite loop. Interrupt service routines handle time-critical events, setting flags or filling buffers for the main loop to process later. The developer has full, direct control over every CPU cycle and every byte of memory. There is no scheduler, no context switching overhead, and no kernel-induced latency.

This model excels in simplicity. A junior engineer can read a bare metal codebase and trace execution from main() through every function call without needing to understand task priorities, semaphores, or inter-task communication. Debugging is straightforward because there is only one thread of execution. Tools like breakpoints and watchpoints behave predictably because no scheduler can preempt your debug session.

FreeRTOS: The Preemptive Multitasking Kernel

FreeRTOS introduces a lightweight, preemptive kernel that allows multiple tasks to run “concurrently” on a single-core microcontroller. Each task is assigned a priority, and the scheduler ensures the highest-priority ready task always runs. Tasks voluntarily yield the CPU by blocking on delays, queues, semaphores, or event groups—or the scheduler preempts a lower-priority task when a higher-priority one becomes ready.

Key FreeRTOS components include:

  • Task Scheduler: Preemptive or cooperative scheduling with configurable tick frequency (typically 1 kHz). Supports unlimited tasks (limited by RAM), each with its own stack.
  • Queues: Thread-safe FIFO buffers for passing data between tasks and ISRs. Supports blocking sends and receives with optional timeouts.
  • Semaphores and Mutexes: Binary and counting semaphores for task synchronization. Mutexes include priority inheritance to mitigate priority inversion.
  • Software Timers: One-shot or auto-reload timers that execute callback functions in a dedicated timer task.
  • Event Groups: Bitmask-based synchronization primitive for waiting on multiple events simultaneously.
  • Task Notifications: Lightweight, fast mechanism for unblocking a single task—often faster than semaphores.
  • Stream and Message Buffers: Lock-free, optimized buffers designed for ISR-to-task and task-to-task communication.

FreeRTOS is designed for resource-constrained microcontrollers. A minimal kernel can fit in under 10 KB of flash and a few hundred bytes of RAM, making it viable even on low-cost ARM Cortex-M0 devices.

When Bare Metal Wins

Ultra-Low Power, Simple Devices

If your device wakes up every few seconds, reads a sensor via I2C, transmits a BLE advertisement packet, and goes back to sleep, a full RTOS is overkill. The scheduling overhead and periodic tick interrupt can prevent the microcontroller from entering its deepest sleep states. Bare metal code can use tickless operation naturally—wake from deep sleep via an RTC alarm or external interrupt, do the work, and return to sleep immediately.

Consider a BLE temperature beacon that transmits once per minute. On bare metal, the device sleeps in STOP or STANDBY mode consuming microamps. Adding FreeRTOS introduces a tick interrupt that wakes the CPU 1000 times per second, dramatically increasing average current draw unless you implement tickless idle mode—which adds its own complexity and may still not match bare metal efficiency for extremely sparse wake-up patterns.

Extremely Tight Memory Constraints

Microcontrollers with 2 KB of RAM and 16 KB of flash cannot comfortably host FreeRTOS. Even a minimal FreeRTOS configuration requires:

  • Kernel code: ~6–10 KB of flash
  • Kernel RAM (system stack, TCBs, queues, timers): ~1–3 KB minimum for 3–4 tasks
  • Per-task stack: typically 128–512 bytes each, depending on call depth and local variables

On an STM8S with 2 KB RAM, those numbers are prohibitive. Bare metal keeps everything in your control, and when every byte counts, that control matters.

Hard Real-Time with Sub-Microsecond Jitter

FreeRTOS’s critical sections disable interrupts briefly. On ARM Cortex-M, entering a critical section sets the BASEPRI register to mask interrupts below a certain priority. This is fast—typically tens of CPU cycles—but for some applications, even that is too much.

Motor control loops running at 100 kHz or high-speed digital protocols requiring cycle-accurate timing may find FreeRTOS’s interrupt latency unacceptable. Bare metal lets you run time-critical code directly in ISRs or use DMA + timer-triggered peripherals with zero kernel interference.

Regulatory and Certification Requirements

Safety-critical systems governed by IEC 61508, ISO 26262, or DO-178C require rigorous verification. While FreeRTOS offers a Safety Critical (SafeRTOS) variant, certified bare metal code is often simpler to verify because there is no scheduler to analyze. Every execution path is deterministic and testable without worrying about task interactions, priority inversions, or deadlocks.

When FreeRTOS Shines

Multi-Protocol Connectivity Stacks

Modern IoT devices frequently combine Wi-Fi, Bluetooth Low Energy, Thread, Ethernet, and cellular connectivity simultaneously. Each protocol stack expects to run in its own context with its own timing requirements. Attempting to manage TCP/IP, TLS handshakes, BLE GATT operations, and MQTT keep-alive pings inside a single super-loop quickly becomes unmanageable.

FreeRTOS excels here. You can dedicate a task to each protocol stack, assign appropriate priorities, and let the scheduler handle concurrency. The TCP/IP stack (FreeRTOS+TCP) integrates natively, and TLS libraries like mbedTLS or wolfSSL work smoothly with FreeRTOS’s task and timer abstractions. This is why virtually every ESP32 application uses FreeRTOS—Espressif’s SDK is built on it.

Complex User Interfaces

A graphical display with touch input, animations, and multiple screens demands responsive, non-blocking code. On bare metal, you must carefully interleave UI updates with background processing to avoid missed touch events or display tearing. FreeRTOS lets you run the GUI in a dedicated task, often with a higher priority than background data processing, ensuring smooth 60 FPS rendering while sensor fusion or data logging runs in lower-priority tasks.

Combined with lightweight GUI libraries like LVGL, FreeRTOS enables desktop-like responsiveness on microcontrollers with as little as 128 KB of RAM. LVGL itself integrates with FreeRTOS through a tick hook and mutual exclusion on the display buffer.

Modular, Team-Based Development

When five engineers work on different subsystems—power management, wireless communication, sensor acquisition, and firmware update—bare metal code becomes a bottleneck. Every developer modifies the super-loop or adds interrupt handlers, creating merge conflicts and subtle timing bugs.

FreeRTOS encourages modular design. Each developer owns one or more tasks with well-defined interfaces via queues. Integration becomes a matter of connecting queues rather than weaving code into a monolithic loop. The configASSERT() macro and stack overflow detection hooks catch bugs early. This isolation accelerates development and makes code reviews more focused.

Complex Timing and Sequencing

Bare metal code managing a multi-step process—say, a GSM modem AT command sequence with timeouts, retries, and error recovery—often devolves into deeply nested state machines that are hard to read and harder to maintain. FreeRTOS’s blocking APIs (xQueueReceive(), ulTaskNotifyTake()) with timeouts let you write linear, readable code for sequential operations.

void ModemTask(void *pvParameters) {
    while (1) {
        SendATCommand("AT+CREG?\r\n");
        if (xQueueReceive(rxQueue, &response, pdMS_TO_TICKS(5000))) {
            ParseNetworkRegistration(response);
        } else {
            HandleTimeout();
        }
        vTaskDelay(pdMS_TO_TICKS(60000)); // Check every 60s
    }
}

This linear style is far more maintainable than a state machine with dozens of states and transitions.

Head-to-Head Comparison

Criterion Bare Metal FreeRTOS
Flash overhead 0 bytes 6–12 KB
RAM overhead 0 bytes (only your data) 1–5 KB for kernel + task stacks
Interrupt latency Minimal (ISR entry only) +critical section time (typically <1 µs)
Code complexity (simple project) Low—single loop, easy to trace Higher—task management overhead
Code complexity (complex project) Very high—nested state machines Lower—modular tasks, clean separation
Power efficiency Excellent—tickless by nature Good—tickless idle mode available
Debugging Simple—single thread, predictable Harder—concurrency bugs, task-aware debugger needed
Team scalability Poor—merge conflicts, coupling Good—task isolation, queue interfaces
Portability Low—tightly coupled to hardware Higher—hardware abstraction through tasks
Learning curve Low—any C programmer can start Moderate—concurrency concepts needed

Common Pitfalls and Best Practices

Bare Metal Pitfalls

  • Blocking in the super-loop: A single blocking call—waiting for a UART byte, polling a sensor until ready—freezes the entire system. Always use non-blocking patterns or ISR-driven state machines.
  • ISR bloat: Long-running ISRs delay all other interrupts. Keep ISRs short; defer heavy processing to the main loop.
  • Timing drift: Super-loop execution time varies with code paths. Use hardware timers for periodic tasks, never assume the loop iteration time is constant.
  • Shared data corruption: Data shared between ISRs and the main loop must be accessed atomically. Disable interrupts briefly, use volatile carefully (it is not a synchronization primitive), or use lock-free single-producer-single-consumer buffers.

FreeRTOS Pitfalls

  • Stack overflow: The most common FreeRTOS bug. Enable configCHECK_FOR_STACK_OVERFLOW (option 2 is more thorough) and use uxTaskGetStackHighWaterMark() during development. Increase stack sizes generously during prototyping, then optimize.
  • Priority inversion: A high-priority task blocked by a low-priority task holding a mutex, while a medium-priority task preempts the low-priority one. Use mutexes (not binary semaphores) for mutual exclusion—FreeRTOS mutexes include priority inheritance by default.
  • Deadlock: Two tasks each holding one mutex and waiting for the other. Establish a consistent lock ordering; if task A uses mutexes 1 and 2, task B must also acquire them in order 1 then 2.
  • Starvation: Low-priority tasks never running because higher-priority tasks never block. Ensure every task has a blocking point—even if it is just a short vTaskDelay(1).
  • ISR API misuse: FreeRTOS API functions called from ISRs must use the FromISR variants (e.g., xQueueSendFromISR(), not xQueueSend()). Using the wrong variant causes unpredictable behavior.

The Hybrid Approach: Best of Both Worlds

Many successful projects use a hybrid architecture. Critical, high-speed control loops (motor commutation, digital power conversion) run bare metal in high-priority ISRs or DMA-driven peripherals, while supervisory functions (user interface, connectivity, logging) run as FreeRTOS tasks at lower priorities. This preserves deterministic timing for the control loop while benefiting from FreeRTOS’s concurrency model for everything else.

STMicroelectronics’ STM32Cube ecosystem supports this pattern natively. You can configure a high-priority timer-triggered DMA chain for sensor acquisition that writes directly to a memory buffer, while a FreeRTOS task at a lower priority picks up the buffer and processes it. FreeRTOS is unaware of the bare metal “fast path”—it simply sees a task that unblocks when data is ready.

Decision Framework

Use the following flowchart to guide your decision:

  1. Does your MCU have < 8 KB RAM or < 32 KB flash? → Bare metal.
  2. Do you need TCP/IP, Wi-Fi, BLE, or a graphical UI? → FreeRTOS (or the SDK’s bundled RTOS).
  3. Is the project a simple sensor/actuator with one main function? → Bare metal.
  4. Are 3+ engineers working on different subsystems? → FreeRTOS.
  5. Does the project require functional safety certification? → Evaluate SafeRTOS vs. certified bare metal based on complexity.
  6. Is there a hard real-time loop faster than 10 kHz? → Hybrid: bare metal fast path + FreeRTOS for supervision.
  7. Otherwise → FreeRTOS. It scales down well enough and gives you room to grow.

Conclusion

Bare metal and FreeRTOS are not competing philosophies—they are tools suited to different problems. Bare metal gives you unmatched simplicity, minimal overhead, and deterministic behavior for simple devices. FreeRTOS provides the concurrency, modularity, and rich synchronization primitives that complex connected devices demand.

The question is not “which is better?” but “which is right for this project?”. Start with your requirements: memory budget, timing constraints, team size, connectivity needs, and certification obligations. Match those constraints to the decision framework above, and you will choose the right architecture—not for ideological reasons, but for engineering ones.

Call to Action

At InnovChip, we design and develop embedded systems across the full spectrum—from ultra-low-power bare metal sensor nodes to multi-radio FreeRTOS-based IoT gateways. Our engineering team has deep experience with STM32, ESP32, nRF52, and other popular MCU platforms. Whether you need architecture consulting, firmware development, or hardware design, we can help you make the right technical decisions and deliver production-ready firmware on schedule.

Contact InnovChip for Your Embedded Project

Leave a Reply

Your email address will not be published. Required fields are marked *