Bitronix Lab Topic

Embedded RTOS

When a single while(1) loop is no longer enough — give your firmware a real-time operating system and let tasks, queues and semaphores do the heavy lifting.

FreeRTOSZephyrTasksQueuesMutexes
Bitronix Lab logo Innovating ideas, empowering future

Embedded RTOS: Multi-Tasking on a Microcontroller

Why an RTOS?

Bare-metal super-loops work — until they don't. The moment you have a UART that needs servicing every 1 ms, a button that must respond instantly, a sensor sampled at 100 Hz and a Wi-Fi stack, you need true concurrency. That is what an RTOS gives you.

RTOS = Real-Time Operating System. Not "fast" — predictable. A task with a deadline always runs by its deadline.

Core concepts

  • Task — an independent thread of execution with its own stack and priority.
  • Scheduler — picks which task runs next, based on priority and state.
  • Tick — periodic timer (typically 1 ms) that drives time-keeping.
  • Queue — thread-safe FIFO for passing data between tasks.
  • Semaphore — signalling primitive (binary or counting).
  • Mutex — exclusive lock with priority inheritance.
  • Software timer — schedule a callback to run later.

The scheduler in 30 seconds

FreeRTOS, Zephyr, ThreadX and friends use preemptive priority-based scheduling:

  1. Every task is in one of: Running, Ready, Blocked, Suspended.
  2. The highest-priority Ready task always runs.
  3. If two tasks share a priority, they round-robin on each tick.
  4. A task blocks when waiting for a queue, semaphore, mutex or delay.

Tasks in FreeRTOS

A FreeRTOS task is just an infinite-loop function with its own stack:

#include "FreeRTOS.h"
#include "task.h"

static void blink_task(void *arg) {
    (void)arg;
    for (;;) {
        gpio_toggle(LED_PIN);
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

static void sensor_task(void *arg) {
    (void)arg;
    for (;;) {
        int16_t t = read_temperature();
        publish(t);
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

int main(void) {
    board_init();

    xTaskCreate(blink_task,  "blink",  256, NULL, 1, NULL);
    xTaskCreate(sensor_task, "sensor", 512, NULL, 3, NULL);

    vTaskStartScheduler();   // never returns
    for (;;) { }
}

Synchronization — queues, semaphores, mutexes

Queue

QueueHandle_t q = xQueueCreate(8, sizeof(int16_t));

/* producer */
int16_t reading = read_adc();
xQueueSend(q, &reading, portMAX_DELAY);

/* consumer */
int16_t v;
if (xQueueReceive(q, &v, pdMS_TO_TICKS(100))) {
    process(v);
}

Mutex

SemaphoreHandle_t spi_lock = xSemaphoreCreateMutex();

void log_to_flash(const char *line) {
    if (xSemaphoreTake(spi_lock, pdMS_TO_TICKS(50))) {
        spi_write(line);
        xSemaphoreGive(spi_lock);
    }
}

ISRs & the RTOS — the golden rule

Inside an interrupt service routine, you may only call the ...FromISR() versions of RTOS APIs:

void EXTI0_IRQHandler(void) {
    BaseType_t higher_prio_woken = pdFALSE;
    xSemaphoreGiveFromISR(button_sem, &higher_prio_woken);
    portYIELD_FROM_ISR(higher_prio_woken);
    EXTI->PR = (1U << 0);   // clear flag
}

Never block inside an ISR. No vTaskDelay, no xQueueSend, no printf. Signal a task and let it do the work.

Pitfalls & tips

  • Stack sizing — start with configCHECK_FOR_STACK_OVERFLOW = 2, then tune with high-water-mark APIs.
  • Priority inversion — always use a mutex (with priority inheritance), not a binary semaphore, for shared resources.
  • Tickless idle — enable it to slash power consumption when idle.
  • Task storms — don't spawn one task per feature; consolidate where you can.
  • Determinism > throughput — measure worst-case, not average.

An RTOS does not make your firmware "fancier" — it makes it predictable. That is what shipping products is built on.