Table of Contents
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 singlewhile(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
| Criteria | Super-Loop | Time-Triggered | Event-Driven | RTOS |
|---|---|---|---|---|
| Complexity | Low | Low-Medium | Medium | High |
| RAM overhead | Minimal | Minimal | Queue buffer | Per-task stacks |
| Task priority | None | By rate | FIFO | Full preemptive |
| Power efficiency | Poor (polling) | Moderate | Good (sleep) | Good (idle task) |
| Best task count | 1-5 | 5-10 | 5-15 | 5-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

Vivek Bhageria — Lead Firmware R&D Engineer, 12+ years. Ex-Bosch (automotive powertrain), MusicTribe (real-time audio), medical devices. M.Tech BITS Pilani. I write at NerdyElectronics — practical, register-level embedded systems for engineers who want to understand what’s actually happening under the hood.





