20  Software Architecture Patterns

20.1 Learning Objectives

By the end of this chapter, you will be able to:

  • Implement Bare-Metal Architecture: Write efficient single-loop firmware for simple applications
  • Design State Machines: Organize firmware using states and transitions for predictable behavior
  • Build Event-Driven Systems: Construct interrupt-driven and callback-based code for power-efficient responsiveness
  • Apply RTOS Concepts: Configure real-time operating systems for complex multi-tasking scenarios

Software architecture is how you organize your firmware code to make it work efficiently and reliably. Think of it like organizing your room – you can dump everything in a pile (bare-metal loop), sort things into labeled boxes (state machine), respond when someone calls your name (event-driven), or hire a butler to manage everything (RTOS). Each approach works for different situations.

“There are four ways to organize firmware, and each is like a different management style!” said Max the Microcontroller. “Bare-metal is the simplest – one big loop that checks everything in order, like a single worker doing tasks from a to-do list. Simple but predictable.”

Sammy the Sensor asked about the others. Max continued, “A state machine organizes code into states – Sleeping, Sensing, Transmitting, Error. The device switches between states based on events. It is like a traffic light that cycles through red, yellow, and green.” Lila the LED liked that one. “I use state machines all the time! My states are Off, Blinking, and Solid. Clean and easy to debug.”

“Event-driven is my favorite,” said Bella the Battery. “Instead of constantly checking sensors in a loop, the code sleeps until something happens – an interrupt from Sammy, a timer alarm, or a network message. No wasted energy polling things that have not changed!” Max added the final option. “And when your project gets really complex – reading multiple sensors, managing Wi-Fi, updating a display, all simultaneously – you use an RTOS. It is like hiring a project manager that juggles multiple tasks, giving each one a fair share of processor time.”

Key Concepts

  • Design Model: Abstract representation of system behaviour, user interaction, and data flow used to guide implementation decisions.
  • Calm Technology: Design philosophy creating technology that informs without demanding attention, residing at the periphery of user awareness.
  • Design Thinking: Human-centred innovation process: empathise, define, ideate, prototype, and test in iterative cycles.
  • Reference Architecture: Documented, validated design pattern for a class of IoT systems, reducing design time and avoiding known pitfalls.
  • Facet Model: Framework decomposing IoT system design into orthogonal dimensions (physical, cyber, social) for structured analysis.
  • Context Awareness: System capability to sense and respond to its physical, social, and operational environment.
  • System-of-Systems: IoT architecture where multiple autonomous systems interact to produce emergent capabilities no single system provides alone.

20.2 Prerequisites

Before diving into this chapter, you should be familiar with:

In 60 Seconds

This chapter covers software architecture patterns, explaining the core concepts, practical design decisions, and common pitfalls that IoT practitioners need to build effective, reliable connected systems.


20.3 Bare-Metal Architecture

Description: Direct programming without operating system, executing in a single infinite loop.

Structure:

void setup() {
  // Initialize hardware
  initHardware();
  initSensors();
  initCommunication();
}

void loop() {
  // Main program logic
  readSensors();
  processData();
  transmitData();
  checkCommands();
  delay(1000);
}

Advantages:

  • Simple and predictable
  • Minimal overhead
  • Full control over execution
  • Easy to understand

Disadvantages:

  • Blocking operations problematic
  • Difficult to manage complex timing
  • Poor scalability

Best For:

  • Simple applications
  • Learning and experimentation
  • Resource-constrained devices

20.4 State Machine Architecture

Description: Organizing code around states and transitions, common in embedded systems.

State Diagram: State machine diagram showing transitions between IDLE, SENSING, TRANSMITTING, and SLEEPING states with numbered steps and directional arrows

Implementation:

enum State {
  IDLE,
  SENSING,
  TRANSMITTING,
  SLEEPING
};

State currentState = IDLE;

void loop() {
  switch(currentState) {
    case IDLE:
      if (shouldSense()) {
        currentState = SENSING;
      }
      break;

    case SENSING:
      readSensors();
      currentState = TRANSMITTING;
      break;

    case TRANSMITTING:
      if (sendData()) {
        currentState = SLEEPING;
      }
      break;

    case SLEEPING:
      enterSleep(SLEEP_TIME);
      currentState = IDLE;
      break;
  }
}

Advantages:

  • Clear logic flow
  • Easier to debug
  • Predictable behavior
  • Power management friendly

Disadvantages:

  • Can become complex with many states
  • Requires careful design

Best For:

  • Battery-powered devices
  • Applications with distinct modes
  • Safety-critical systems

20.5 Event-Driven Architecture

Description: Responding to events (interrupts, messages, timers) rather than polling.

Implementation:

volatile bool dataReady = false;

void setup() {
  attachInterrupt(digitalPinToInterrupt(SENSOR_PIN),
                  sensorISR, RISING);
}

void sensorISR() {
  dataReady = true;
}

void loop() {
  if (dataReady) {
    processData();
    dataReady = false;
  }
  // MCU can sleep when no events
  lowPowerMode();
}

Event Queue Pattern:

struct Event {
  uint8_t type;
  uint32_t data;
};

Queue<Event> eventQueue;

void buttonISR() {
  Event e = {EVENT_BUTTON, millis()};
  eventQueue.push(e);
}

void timerISR() {
  Event e = {EVENT_TIMER, 0};
  eventQueue.push(e);
}

void loop() {
  if (!eventQueue.isEmpty()) {
    Event e = eventQueue.pop();
    handleEvent(e);
  } else {
    enterLowPowerMode();
  }
}

Advantages:

  • Responsive to external events
  • Power-efficient (sleep when idle)
  • Scales well with complexity

Disadvantages:

  • Interrupt handling complexity
  • Potential race conditions
  • Debugging can be challenging

Best For:

  • Interactive devices
  • Low-power applications
  • Complex systems with multiple inputs

20.6 RTOS-Based Architecture

Description: Using Real-Time Operating System for task scheduling and resource management.

FreeRTOS Example:

#include <Arduino.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

void sensorTask(void *parameter) {
  while(1) {
    readSensors();
    vTaskDelay(1000 / portTICK_PERIOD_MS);
  }
}

void communicationTask(void *parameter) {
  while(1) {
    sendData();
    vTaskDelay(5000 / portTICK_PERIOD_MS);
  }
}

void setup() {
  xTaskCreate(sensorTask, "Sensor", 2048, NULL, 1, NULL);
  xTaskCreate(communicationTask, "Comm", 2048, NULL, 1, NULL);
}

void loop() {
  // Empty - tasks handle everything
}

RTOS Primitives:

Primitive Purpose Example Use
Task Independent execution unit Sensor reading, communication
Queue Inter-task data passing Sensor data to network task
Semaphore Resource synchronization SPI bus sharing
Mutex Mutual exclusion Shared data protection
Timer Periodic callbacks Heartbeat, watchdog

Advantages:

  • Multiple concurrent tasks
  • Priority-based scheduling
  • Resource protection (mutexes, semaphores)
  • Professional development paradigm

Disadvantages:

  • Memory overhead
  • Complexity
  • Steeper learning curve

Best For:

  • Complex multi-function devices
  • Professional embedded development
  • Systems requiring strict timing

RTOS vs Bare-Metal Memory Trade-off: For ESP32 with 520KB SRAM:

FreeRTOS overhead: \[\text{Memory}_{\text{RTOS}} = 15\text{KB kernel} + (N_{\text{tasks}} \times 4\text{KB stack}) + 8\text{KB heap}\]

4-task application: \[\text{Total} = 15 + (4 \times 4) + 8 = 39\text{KB} \text{ (7.5% of RAM)}\]

Benefit: Deterministic task switching in \(10\mu s\) vs bare-metal polling delays of \(1-100ms\). For sensor fusion requiring <5ms response, RTOS reduces worst-case latency by 95% at cost of 39KB memory. Worth it for timing-critical applications, overkill for simple data loggers.

Interactive Calculator:

20.6.1 Worked Example: FreeRTOS Sensor-to-Cloud Pipeline on ESP32

Scenario: Build an air quality monitor that reads a PM2.5 sensor every 10 seconds, processes rolling averages, and publishes to MQTT every 60 seconds. The display must update independently every 2 seconds. A bare-metal loop cannot handle all three timing requirements cleanly.

Why RTOS: Three tasks with different periods (2s display, 10s sensor, 60s MQTT) would require complex timer juggling in bare-metal. FreeRTOS lets each task run at its own pace with clean separation.

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"

// Shared data protected by mutex
typedef struct {
    float pm25, pm25_avg, temperature, humidity;
    uint32_t timestamp;
} SensorData;

SensorData latestData = {0};
SemaphoreHandle_t dataMutex;

Task 1 – Sensor Reading runs every 10 seconds, computing a rolling 5-minute average:

void sensorTask(void *param) {
    float readings[30]; int idx = 0, count = 0;
    while (1) {
        float pm25 = readPMS5003();
        readings[idx] = pm25;
        idx = (idx + 1) % 30;
        if (count < 30) count++;

        float sum = 0;
        for (int i = 0; i < count; i++) sum += readings[i];

        if (xSemaphoreTake(dataMutex, pdMS_TO_TICKS(100))) {
            latestData.pm25 = pm25;
            latestData.pm25_avg = sum / count;
            latestData.temperature = readBME280Temp();
            xSemaphoreGive(dataMutex);
        }
        vTaskDelay(pdMS_TO_TICKS(10000));
    }
}

Task 2 – Display refreshes every 2 seconds with colour-coded air quality:

void displayTask(void *param) {
    while (1) {
        SensorData local;
        if (xSemaphoreTake(dataMutex, pdMS_TO_TICKS(50))) {
            local = latestData;
            xSemaphoreGive(dataMutex);
        }
        oled.clearDisplay();
        oled.printf("PM2.5: %.0f ug/m3\n", local.pm25);
        // Color-code: RED > 75, YELLOW > 35, GREEN otherwise
        oled.display();
        vTaskDelay(pdMS_TO_TICKS(2000));
    }
}

Task 3 – MQTT publishes JSON every 60 seconds. The setup pins tasks to specific cores:

void setup() {
    dataMutex = xSemaphoreCreateMutex();
    // Sensor on core 1 (avoids Wi-Fi on core 0)
    xTaskCreatePinnedToCore(sensorTask,  "Sensor",  4096, NULL, 2, NULL, 1);
    xTaskCreatePinnedToCore(displayTask, "Display", 4096, NULL, 1, NULL, 1);
    xTaskCreatePinnedToCore(mqttTask,    "MQTT",    8192, NULL, 1, NULL, 0);
}

Key design decisions:

Decision Rationale
Mutex for shared data Prevents partial reads when sensor writes during display read
Copy-under-mutex pattern Minimizes mutex hold time to < 1ms (never block other tasks)
Sensor at priority 2 Sensor readings are time-sensitive; display/MQTT can wait
MQTT on core 0 Wi-Fi stack runs on core 0; keeping MQTT there avoids cross-core synchronization
4096 byte stack for sensor Sufficient for float arrays and I2C operations
8192 byte stack for MQTT Wi-Fi + TLS needs larger stack

Result: Clean separation of concerns. Each task runs at its own frequency without blocking others. Adding a new feature (e.g., buzzer alert when PM2.5 > 100) means adding one more task without modifying existing code.

20.7 Architecture Selection Flowchart

Decision tree flowchart for selecting firmware architecture based on project complexity, starting with a diamond decision node branching to multiple implementation options

20.8 Architecture Comparison

Factor Bare-Metal State Machine Event-Driven RTOS
Complexity Low Medium Medium High
Memory Overhead None Low Low High
Power Efficiency Variable Good Excellent Good
Responsiveness Poor Medium Excellent Good
Scalability Poor Medium Good Excellent
Debug Difficulty Low Low Medium High
Best RAM <16KB 16-64KB 16-64KB >64KB

20.9 Knowledge Check

Common Pitfalls

Creating interaction flows that make sense to engineers but contradict users’ existing mental models from smartphones and web applications produces steep learning curves and abandonment. Map every primary interaction to an existing familiar pattern before inventing new paradigms.

Icon-only interfaces that appear clean in design reviews fail when users cannot identify what an icon means without trying it. Pair icons with text labels in primary navigation and reserve icon-only presentation for secondary or expert-level interactions where meaning is established.

Interactions that change device state (locking a door, arming a sensor) without immediate visual or auditory feedback leave users uncertain whether their action was registered, often triggering repeated taps. Acknowledge every state change with a clear animation, LED change, or sound within 200 ms.

20.10 What’s Next

If you want to… Read this
Learn about physical feedback mechanisms Feedback and Status Indicators
Understand error and recovery design Error Handling and Recovery
Explore mobile companion app patterns Mobile App and Companion Design