Real-time firmware: FreeRTOS vs Arduino loop
When you outgrow setup()/loop(), and what RTOS tasks give you in exchange for the complexity. The transition every serious embedded developer makes — and when not to.
Arduino's loop() works for a robot with one job: read sensor, compute control, write motor. Add a wireless link, blinking LEDs, and a watchdog, and the loop becomes a tangled mess. An RTOS (Real-Time Operating System) splits the work into tasks that the kernel multiplexes onto the CPU. The transition is the biggest "wait, this is so much cleaner" moment in embedded firmware.
The Arduino-loop limit
Standard Arduino structure:
void loop() {
read_sensors(); // 5 ms
compute_control();// 2 ms
write_motors(); // 1 ms
handle_serial(); // up to 50 ms (blocking!)
}
One iteration takes 8 ms in the best case; 60 ms when serial is busy. Control loop frequency varies from 12 Hz to 125 Hz. Anything that needs precise timing breaks.
The classical workarounds are non-blocking everything (state machines), millis() scheduling, interrupts. They work; they don't scale beyond a few responsibilities.
What an RTOS gives you
An RTOS provides:
- Tasks: independent units of work, each with its own stack and priority.
- Scheduler: at each tick (typically 1 ms), runs the highest-priority ready task.
- Synchronization primitives: queues, mutexes, semaphores, event groups.
- Timing primitives:
vTaskDelay, periodic timers, deadlines.
You think in terms of "the control task runs at 1 kHz; the comm task runs when there's data; the LED task blinks at 1 Hz." The kernel handles the multiplexing.
The FreeRTOS pattern
void control_task(void* pv) {
TickType_t last = xTaskGetTickCount();
while (1) {
read_sensors();
compute_control();
write_motors();
vTaskDelayUntil(&last, pdMS_TO_TICKS(1)); // 1 kHz
}
}
void comm_task(void* pv) {
while (1) {
char buf[64];
int n = serial_read_blocking(buf, sizeof(buf)); // sleeps task
process_command(buf, n);
}
}
void led_task(void* pv) {
while (1) {
digitalWrite(LED, HIGH);
vTaskDelay(pdMS_TO_TICKS(500));
digitalWrite(LED, LOW);
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void setup() {
xTaskCreate(control_task, "ctrl", 2048, NULL, 5, NULL);
xTaskCreate(comm_task, "comm", 2048, NULL, 3, NULL);
xTaskCreate(led_task, "led", 1024, NULL, 1, NULL);
vTaskStartScheduler();
}
void loop() { /* unused */ }
Each task is independent. The control task wakes every millisecond; the comm task sleeps until serial data; the LED task blinks. Priority 5 > 3 > 1, so control preempts comm preempts LED.
Sync primitives
Queues (FIFO message passing)
Pass data between tasks safely. Sender doesn't wait for receiver; receiver blocks until message arrives.
QueueHandle_t cmd_queue = xQueueCreate(10, sizeof(Command));
// Producer task
xQueueSend(cmd_queue, &new_cmd, portMAX_DELAY);
// Consumer task
Command cmd;
xQueueReceive(cmd_queue, &cmd, portMAX_DELAY);
Mutexes (mutual exclusion)
Protect shared resources from concurrent access:
SemaphoreHandle_t mutex = xSemaphoreCreateMutex();
xSemaphoreTake(mutex, portMAX_DELAY);
shared_counter++;
xSemaphoreGive(mutex);
Mostly avoidable in practice — prefer queues over shared state.
Semaphores (binary signals)
One task signals; another waits.
SemaphoreHandle_t sem = xSemaphoreCreateBinary();
// Producer
xSemaphoreGive(sem);
// Consumer
xSemaphoreTake(sem, portMAX_DELAY);
The RTOS landscape
| RTOS | Targets | Notes |
|---|---|---|
| FreeRTOS | Cortex-M, ESP32, RISC-V | Most popular. Default RTOS for ESP32 (FreeRTOS-aware Arduino core). |
| Zephyr | Wide | Linux Foundation; richer than FreeRTOS; used in Nordic IoT. |
| NuttX | Wide | POSIX-compatible. Powers PX4 flight stack. |
| Mbed OS | ARM Cortex-M | ARM-blessed; declining usage. |
| QNX, VxWorks | Industrial | Commercial; aerospace + automotive. |
For ESP32, FreeRTOS comes built-in via the Arduino core. For STM32, link FreeRTOS via STM32CubeMX or PlatformIO. For Nordic, Zephyr is the natural choice.
When to switch from Arduino loop
- You have multiple things happening at different rates.
- You need precise timing on one thing and relaxed timing on others.
- You have blocking operations (TLS handshakes, DNS, large file reads).
- The loop has grown to 200+ lines and is hard to read.
When NOT to switch
- You have one job (blink an LED, read a sensor, send via serial).
- The Arduino IDE's simplicity is part of why your team can use it.
- You're prototyping; switch later if needed.
The RTOS adds complexity. Don't add it prematurely. But once your firmware is non-trivial, RTOS is the path of less suffering.
Common gotchas
- Stack overflow: each task has its own stack (512 B – 4 KB typical). Large local arrays or deep call chains overflow silently — symptom: random reboots. Increase stack size.
- Priority inversion: a high-priority task waits for a low-priority task that holds a mutex. RTOS may not handle this correctly. Use priority-inheritance mutexes if available.
- Watchdog timeouts: the kernel handles tasks; if your task gets stuck, the system watchdog should reset. Configure it.
- Heap fragmentation: dynamic allocation in long-running RTOS tasks fragments the heap. Use static allocation; pre-allocate buffers.
- Blocking inside ISRs: never call
xSemaphoreTakefrom an interrupt; use the FromISR variants.
Production patterns
- Hard real-time control task: highest priority; minimal compute; runs deterministically.
- Soft real-time tasks: lower priority; can be preempted.
- Background tasks: lowest priority; logs, telemetry, blinking lights.
- Communication queues: between tasks; never share buffers directly.
- Idle hook: ESP32's idle task can sleep the CPU for power savings.
The PX4 example
PX4 (autopilot for drones) runs ~30 tasks on a Pixhawk:
- Sensor drivers (each at 200–1000 Hz).
- Attitude / position estimator (250 Hz).
- Attitude / position controllers (250 Hz).
- RC reader, MAVLink comm, logger, parameter server.
All on a single STM32. The RTOS architecture (NuttX in PX4's case) is what makes this tractable.
Exercise
Take an Arduino loop with three things (read sensor, control motor, blink LED). Convert to FreeRTOS. Each becomes a task. Compare timing precision: the Arduino-loop motor control jitters when other things happen; the FreeRTOS control task runs deterministically at 1 kHz. The before/after demonstrates exactly what the RTOS buys.
Next
PCB basics — when breadboards stop scaling and you spin your first custom board.
Comments
Sign in to post a comment.