Skip to content
Home » Software Design » Firmware Architecture Patterns — Super-Loop, Event-Driven, and RTOS

Firmware Architecture Patterns — Super-Loop, Event-Driven, and RTOS

Firmware Architecture Patterns featured image with purple background, Architecture badge, Fw icon, Super-Loop to RTOS subtitle in the Software Design in C series by NerdyElectronics
Software Design Principles in C
Part 29 of 31View Full Path →

KEY TAKEAWAYS

  • Start with the simplest architecture that meets your requirements
  • Super-loop for simple devices, time-triggered for mixed-rate tasks
  • Event-driven for reactive and battery-powered systems
  • RTOS when you need true prioritization and concurrent tasks
  • You can combine patterns — e.g., time-triggered with an event queue for async events

What Is Firmware Architecture?

Firmware architecture is the high-level structure that determines how your embedded application runs: how tasks are scheduled, how events are handled, and how modules communicate. Choosing the right architecture is one of the most impactful decisions in an embedded project — it affects performance, power consumption, complexity, and maintainability.There are three main patterns, each suited to different complexity levels.

Pattern 1: Super-Loop (Bare-Metal Polling)

The simplest architecture. A single while(1) loop runs forever, polling inputs and calling processing functions in sequence.
int main(void) {
    system_init();
    sensor_init();
    display_init();
    comm_init();

    while (1) {
        sensor_read_all();
        process_data();
        display_update();
        comm_check_messages();
    }
}

Strengths

  • Simplest possible architecture — easy to understand and debug
  • No scheduler overhead, no context switching
  • Deterministic execution order
  • Ideal for small, single-purpose devices

Weaknesses

  • Every function must return quickly — one slow function blocks everything
  • No prioritization — a critical alarm waits behind a display update
  • Wastes CPU cycles polling when nothing has changed
  • Becomes unwieldy as the number of tasks grows

Best For

Simple devices: a thermostat, LED controller, basic sensor reader — anything with 3-5 tasks and no strict timing requirements.

Pattern 2: Time-Triggered (Timed Super-Loop)

An improvement on the super-loop: tasks run at different rates based on a timer tick.
volatile uint32_t tick_ms = 0;

void SysTick_Handler(void) {
    tick_ms++;
}

int main(void) {
    system_init();
    uint32_t last_10ms  = 0;
    uint32_t last_100ms = 0;
    uint32_t last_1s    = 0;

    while (1) {
        uint32_t now = tick_ms;

        if (now - last_10ms >= 10) {
            last_10ms = now;
            sensor_read();          // Fast: 100 Hz
            safety_check();
        }

        if (now - last_100ms >= 100) {
            last_100ms = now;
            filter_update();        // Medium: 10 Hz
            control_loop();
        }

        if (now - last_1s >= 1000) {
            last_1s = now;
            display_update();       // Slow: 1 Hz
            log_write();
            comm_heartbeat();
        }
    }
}

Strengths

  • Tasks run at predictable rates
  • Fast tasks run frequently; slow tasks do not waste CPU
  • Still simple — no RTOS overhead
  • Easy to add or remove tasks at each rate

Weaknesses

  • Still cooperative — a slow task can delay faster tasks
  • All tasks share one stack — no isolation
  • Must ensure worst-case execution of fast tasks fits within the tick period

Best For

Medium-complexity embedded systems: data loggers, motor controllers, sensor hubs with mixed-rate tasks.

Pattern 3: Event-Driven (with Event Queue)

Instead of polling, modules post events to a central queue. The main loop processes events one at a time.
typedef enum {
    EVT_NONE,
    EVT_BUTTON_PRESS,
    EVT_SENSOR_READY,
    EVT_TIMER_TICK,
    EVT_UART_RX,
    EVT_ALARM,
} EventType;

typedef struct {
    EventType type;
    uint32_t  data;
} Event;

#define QUEUE_SIZE 32
static Event queue[QUEUE_SIZE];
static int head = 0, tail = 0;

void event_post(EventType type, uint32_t data) {
    queue[head].type = type;
    queue[head].data = data;
    head = (head + 1) % QUEUE_SIZE;
}

bool event_get(Event *out) {
    if (head == tail) return false;
    *out = queue[tail];
    tail = (tail + 1) % QUEUE_SIZE;
    return true;
}

int main(void) {
    system_init();

    while (1) {
        Event evt;
        if (event_get(&evt)) {
            switch (evt.type) {
            case EVT_BUTTON_PRESS:  handle_button(evt.data);  break;
            case EVT_SENSOR_READY:  handle_sensor(evt.data);  break;
            case EVT_TIMER_TICK:    handle_tick();             break;
            case EVT_UART_RX:       handle_uart(evt.data);    break;
            case EVT_ALARM:         handle_alarm(evt.data);    break;
            default: break;
            }
        } else {
            enter_low_power();  // Sleep until next interrupt
        }
    }
}

Strengths

  • CPU sleeps when idle — great for battery-powered devices
  • Events from ISRs are safely deferred to the main loop
  • Clean separation between “detecting events” (ISRs) and “handling events” (main loop)
  • Easy to add new event types without modifying existing handlers

Weaknesses

  • Queue can overflow if events arrive faster than they are processed
  • No prioritization — events are processed FIFO
  • More complex than a simple super-loop

Best For

Battery-powered devices, reactive systems (user interfaces, wireless nodes), and systems where most time is spent idle.

Pattern 4: RTOS-Based (Preemptive Multitasking)

A Real-Time Operating System runs multiple tasks concurrently with prioritized preemption. Higher-priority tasks can interrupt lower-priority ones.
// FreeRTOS example
void sensor_task(void *params) {
    while (1) {
        float temp = sensor_read();
        xQueueSend(data_queue, &temp, portMAX_DELAY);
        vTaskDelay(pdMS_TO_TICKS(10));  // 100 Hz
    }
}

void display_task(void *params) {
    while (1) {
        float temp;
        xQueueReceive(data_queue, &temp, portMAX_DELAY);
        display_show_temperature(temp);
    }
}

void alarm_task(void *params) {
    while (1) {
        ulTaskNotifyTake(pdTRUE, portMAX_DELAY);  // Wait for notification
        activate_alarm();
        vTaskDelay(pdMS_TO_TICKS(5000));
        deactivate_alarm();
    }
}

int main(void) {
    system_init();
    xTaskCreate(sensor_task,  "Sensor",  256, NULL, 3, NULL);  // High priority
    xTaskCreate(alarm_task,   "Alarm",   256, NULL, 2, NULL);  // Medium priority
    xTaskCreate(display_task, "Display", 512, NULL, 1, NULL);  // Low priority
    vTaskStartScheduler();
    while (1) {}  // Should never reach here
}

Strengths

  • True prioritization — critical tasks preempt non-critical ones
  • Each task has its own stack — isolation and cleaner code
  • Built-in synchronization (queues, semaphores, mutexes)
  • Scales well to complex applications with many concurrent activities

Weaknesses

  • RAM overhead — each task needs its own stack (256-1024 bytes typical)
  • Complexity — priority inversion, deadlocks, race conditions
  • Harder to debug — non-deterministic execution order
  • Overkill for simple applications

Best For

Complex embedded systems: industrial controllers, medical devices, automotive ECUs, IoT gateways — anything with multiple concurrent responsibilities and strict timing requirements.

Choosing the Right Pattern

CriteriaSuper-LoopTime-TriggeredEvent-DrivenRTOS
ComplexityLowLow-MediumMediumHigh
RAM overheadMinimalMinimalQueue bufferPer-task stacks
Task priorityNoneBy rateFIFOFull preemptive
Power efficiencyPoor (polling)ModerateGood (sleep)Good (idle task)
Best task count1-55-105-155-20+

Key Takeaways

  • Start with the simplest architecture that meets your requirements
  • Super-loop for simple devices, time-triggered for mixed-rate tasks
  • Event-driven for reactive and battery-powered systems
  • RTOS when you need true prioritization and concurrent tasks
  • You can combine patterns — e.g., time-triggered with an event queue for async events

Leave a Reply

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