14  Firmware Architecture Patterns

Use software architecture to expose timing, state, failure, and handoff risk before prototype code becomes production structure

prototyping
firmware
software-architecture
embedded-software
validation
Keywords

IoT software architecture, firmware architecture pattern, state machine prototype, event-driven firmware, RTOS prototype evidence

In 60 Seconds

Architecture is how a software prototype makes responsibilities visible. A useful IoT architecture prototype shows which code owns hardware access, state transitions, timing, communication, storage, update behavior, and failure recovery. Choose the lightest pattern that exposes the current risk, then record what must be kept, rewritten, or checked again before production.

14.1 Start With the Story

A firmware prototype often begins as one file because that is the quickest way to see a sensor value. The architecture story starts when the same file must handle state, timing, retries, storage, updates, and diagnostics without hiding where faults belong. Architecture is not ceremony; it is a set of trust lines that lets reviewers see what each part owns.

Use this chapter to draw those lines only as far as the evidence requires. Start with the runtime boundary, test normal and fault paths, then decide whether the structure should be kept, simplified, or replaced.

14.2 Architecture Sets Trust Lines

Firmware architecture is not only code organization. It decides which behavior can be measured, repeated, and trusted when the prototype leaves the bench.

Evidence route for a greenhouse ventilation firmware prototype moving from the architecture question through driver, state, gateway, storage, update, and diagnostic boundaries, then pattern choice, normal and fault runs, review decision, and handoff.
Firmware architecture should route the greenhouse ventilation prototype question through explicit driver, state, gateway, storage, update, and diagnostic boundaries before pattern choice, run evidence, review decision, and handoff.

The central design question is ownership. One part of the firmware should own sensor reads, another should own product state, another should own communication, and another should expose evidence when timing or recovery fails.

For a greenhouse ventilation prototype, this means the SHT31 or BME280 driver reports readings and validity flags, the application state machine decides whether the fan relay should run, the MQTT layer publishes a status document, and the diagnostic layer records loop time, publish failures, and stale readings. If those jobs are mixed into one long function, the dashboard may update once while the team still cannot explain which code owns a sensor fault, a gateway outage, or a manual override.

The architecture does not need to be large to be useful. A small super loop can be a good first choice when it has bounded driver calls and clear state labels. A state machine is better when operating modes matter. An event queue helps when door interrupts, timer ticks, gateway messages, and fan-current alerts arrive at different times. A FreeRTOS, Zephyr, or ESP-IDF task model is worth considering only when independent responsibilities have real timing or blocking pressure.

That choice should be reversible during prototyping. If a one-board slice shows that publish backoff is harmless, the team can keep the simpler loop. If the same slice shows missed fan decisions or stale payloads, the record justifies adding a queue or task boundary before the structure spreads into more features.

Record the architecture as evidence, not as a decorative diagram. Name the board or harness, pattern, state owner, communication owner, storage owner, blocking paths, and one fault run. That record tells the next engineer whether to keep the pattern, simplify it, or check the timing again when the MCU, radio, broker, payload, or update path changes.

14.3 Separate Three Hard Problems

Most prototype firmware becomes fragile when state, time, and faults are mixed into one loop. Separate those concerns before the demo becomes the pilot code.

Start by drawing the smallest runtime boundary that can answer the question. For a cold-room sensor node on an ESP32-C3, the first boundary might be a driver module for I2C temperature and door inputs, a pure state function that classifies normal, door_open, sensor_fault, and gateway_offline, and a publisher that formats MQTT JSON for Mosquitto, EMQX, AWS IoT Core, or Azure IoT Hub. The prototype then proves whether each boundary has a visible input, output, timeout, and error signal.

Use instrumentation before adding architecture weight. Measure the longest sensor read, publish attempt, flash write, display update, and watchdog window. If the loop still responds within the required window, keep it simple. If publish backoff delays fan safety logic, move communication into a state transition, queue, or task. If a door interrupt triggers heavy work, turn it into a short event capture and process the decision in normal context.

  • State ownership: Name the valid modes, transitions, stale-data states, and rejected commands before writing handlers.
  • Time ownership: Identify blocking reads, retry delays, sensor warm-up, watchdog windows, and radio duty-cycle limits.
  • Fault ownership: Decide which module reports sensor failure, queue overflow, bad configuration, failed publish, brownout restart, or update rollback.

A super loop can be enough when those owners are visible. An event queue or RTOS is worth the extra complexity when the owners must run independently or meet competing deadlines.

Keep shared state boring. Prefer snapshots, typed events, atomic flags, or one owning task instead of global structures that drivers, callbacks, and publishers all mutate. In FreeRTOS, record task names, priorities, queue lengths, stack high-water marks, and the timeout used when one task waits for another. In Zephyr, record the work queue, message queue, timer, and device-tree assumptions. In a host harness, record which simulated events stand in for hardware behavior and which timing claims still require the target board.

14.4 Prototype Architecture Ages Fast

Early firmware often works because the device is observed, reset manually, and tested on one network. Production firmware must survive missing packets, long sleep periods, full queues, corrupted settings, partial updates, and restarts without a developer watching the serial monitor.

The architecture should therefore make hidden coupling visible. If a sensor driver can block communication, if an MQTT retry can starve actuator safety logic, or if a configuration write can corrupt calibration during brownout, the prototype has answered an architecture question before the fault reaches a field pilot.

Most hidden coupling comes from time, memory, or ownership. A TLS handshake may block long enough for a display to show stale values. A flash erase may pause a low-cost MCU while an alarm transition is due. An interrupt may update a shared flag while the main loop is formatting a payload. A queue may drop events faster than the review log records them. The architecture is trustworthy only when those behaviors have an owner and an observation point.

Embedded operating-system boundaries are part of that evidence. On a phone, laptop, or Linux gateway, the kernel usually protects itself from ordinary application code and mediates hardware access through drivers and services. Many MCU firmwares are flatter: application code, driver code, and runtime code may share one address space, and a bad pointer or runaway callback can stop the whole device. Cortex-M memory-protection units help when the runtime actually configures them, but the prototype record should not assume that protection exists. It should say whether the firmware has a protected process boundary, MPU regions, or a single trusted image, and which code is allowed to bypass the abstraction for direct peripheral control.

Interrupt handling is the sharpest version of that boundary. The direct handler should do the smallest urgent work: capture the event, clear or read the device status, copy a byte or timestamp, and enqueue a fact for normal code. Heavier processing belongs in a deferred context such as an event queue, work queue, bottom-half callback, or RTOS task. That deferred work still needs a contract: new interrupts may preempt it, queues may fill, and shared state can be touched from both handler and background paths unless ownership is explicit.

Event-driven and threaded designs expose different costs. An event-driven callback model can keep RAM low and make every external event visible, but cross-event state must live in a named state object or queue, and the callback must not block behind a send, storage write, or long calculation. A threaded model can make blocking flows easier to read because each responsibility has its own stack and wait points, but the record must include task priority, stack headroom, queue depth, wake source, and the condition that makes a task runnable. In FreeRTOS-style systems, higher-priority tasks can preempt lower-priority work and interrupts can unblock tasks; in Mbed OS or vendor SDKs, richer peripheral APIs can make code cleaner while still leaving implementation details and direct-register escape hatches to review.

On an ESP32 or STM32 target, useful under-the-hood evidence includes maximum loop duration, watchdog reset reason, heap-watermark trend, queue depth, stack high-water mark, publish retry count, and the specific fault that forced a state transition. On a Nordic nRF52840 or other low-power node, include wake reason, radio association time, sample-to-publish duration, and current profile. On a Linux gateway, include service restart behavior, systemd unit state, persistent queue size, and what happens when DNS or the broker is unavailable.

Architecture also shapes update safety. If configuration migration, OTA download, bootloader confirmation, and rollback policy are hidden inside unrelated application code, a partial update can leave the prototype in an unclear state. Separate the update boundary early enough that later OTA work can check image slot, firmware version, schema migration, rollback trigger, and safe fallback behavior without untangling every driver and publisher.

14.5 Learning Objectives

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

  • Choose an architecture pattern from evidence needs rather than implementation habit.
  • Separate driver, application state, communication, storage, update, and observability responsibilities.
  • Compare super loop, state machine, event queue, and RTOS task models as prototype evidence tools.
  • Review timing, blocking, fault recovery, and shared-state risks before a demo becomes an architecture.
  • Write an architecture review record that supports later library, testing, OTA, and best-practice decisions.

14.6 Architecture Choice Is Evidence Choice

The language chapter decided which runtime carries each software risk. Architecture decides how responsibilities are separated inside that runtime. The prototype should make one question reviewable: what structure best exposes the current timing, state, fault, or handoff risk?

Start with the evidence question:

State evidenceCan reviewers see each mode, transition, timeout, stale-data state, and recovery path?
Timing evidenceCan the prototype show which operations block, which deadlines matter, and which work can wait?
Boundary evidenceAre hardware drivers, application decisions, communication, storage, and update behavior separated enough to test?
Failure evidenceDoes the structure show what happens when a sensor, network, clock, queue, storage, or update path fails?
Avoid Pattern Prestige

An RTOS is not automatically better than a small state machine. A super loop is not automatically careless. A useful prototype chooses the simplest architecture that can expose the deciding risk and records when that choice must be revisited.

14.7 Responsibility Boundaries

Architecture starts by naming ownership. If every function reads hardware, changes state, formats payloads, and publishes messages, the prototype may work once but will be hard to review.

Layered responsibility boundary diagram for IoT prototype software with drivers, application state, communication, storage, update, and observability.
Figure 14.1: Responsibility boundaries in IoT firmware

Use these boundaries when reviewing prototype code:

Driver boundaryOwns hardware reads, actuator writes, bus transactions, and sensor status. It should not decide business rules.
Application stateOwns modes, transitions, thresholds, stale readings, alarms, and local decisions. It should not hide hardware errors.
Communication contractOwns payload shape, topic or endpoint, retry policy, backoff, and offline behavior. It should expose failed publish attempts.
Storage boundaryOwns local queue, configuration, calibration, counters, and persistence rules. It should make loss and corruption cases visible.
Update boundaryOwns version, migration, rollback expectation, and safe behavior during partial update or restart.
Observability boundaryOwns logs, counters, trace points, debug pins, status output, and review artifacts that explain what happened.

Before accepting a firmware architecture, record one reviewer-visible answer for each boundary:

  • Which module owns the boundary and which other modules may call it.
  • The normal input, output, timeout, and error signal for the boundary.
  • The fault case that proves the boundary does not silently hide stale data, failed I/O, full storage, bad configuration, or update interruption.
  • The observation that will appear in logs, counters, debug output, or test artifacts when the boundary is exercised.
  • The handoff decision: keep the boundary as-is, rewrite it before pilot use, or check it again after the next hardware or protocol change.

14.8 Pattern Selection Map

Architecture patterns are not levels to climb. They are different ways to make evidence visible.

Evidence map matching super loop, state machine, event queue, RTOS task model, host harness, and hybrid structure to the state, timing, boundary, and failure evidence each choice must expose.
Figure 14.2: Software architecture pattern evidence map
Super loopUseful when one ordered sequence can prove the risk: read, decide, publish, sleep. Record worst blocking points and timeout expectations.
State machineUseful when modes and transitions matter: idle, sensing, publishing, offline, fault, recovery, and update. Record allowed transitions and stale-state behavior.
Event queueUseful when interrupts, timers, buttons, packets, or sensor-ready signals should hand off work without doing heavy processing inside handlers.
RTOS task modelUseful when independent responsibilities have different periods, priorities, or blocking behavior. Record task ownership, queue contracts, stack evidence, and shared-state rules.
Host harnessUseful when the architecture decision can be stressed with simulated events, payloads, and fault sequences before every run reaches hardware.
Hybrid structureUseful when a small state machine owns behavior while events, tasks, or harnesses provide inputs. Record the boundary so the design stays reviewable.
Prefer Bounded Work

Whatever pattern you choose, every sensor read, bus transaction, network publish, storage write, update check, and wait path should have a bounded behavior or a documented reason why it does not yet need one.

Use small examples to prove the boundary before copying a full framework.

enum class NodeMode { Booting, Sampling, Publishing, Sleeping, Fault };

NodeMode nextMode(NodeMode mode, bool sampleReady, bool publishOk, bool fault) {
  if (fault) return NodeMode::Fault;
  switch (mode) {
    case NodeMode::Booting: return NodeMode::Sampling;
    case NodeMode::Sampling: return sampleReady ? NodeMode::Publishing : NodeMode::Sampling;
    case NodeMode::Publishing: return publishOk ? NodeMode::Sleeping : NodeMode::Fault;
    case NodeMode::Sleeping: return NodeMode::Sampling;
    case NodeMode::Fault: return NodeMode::Fault;
  }
  return NodeMode::Fault;
}

Pair the state machine with a driver boundary and an event handoff:

  • Driver interface: return typed readings with units and validity flags, not raw global variables.
  • Pure processing: convert ADC, calibration, filtering, threshold checks, and payload formatting into deterministic functions.
  • Event queue: keep interrupts and callbacks short; enqueue or flag work, then process it in normal context.
  • Bounded retry: limit publish attempts, log each attempt, and convert blocking retry loops into state transitions when responsiveness matters.
  • Configuration record: validate sample interval, pins, thresholds, and site settings before a run becomes evidence.

Review rule: procedural loops, state machines, objects, queues, functional transforms, and RTOS tasks are useful only when each one has a named job and a testable boundary.

14.9 Timing and Shared-State Review

Most architecture bugs hide in time and shared state. The prototype should show which work can block, which work must be quick, and which data can be shared safely.

Timing review diagram showing fast interrupt handoff, bounded state work, slower communication, storage, and update paths, with shared state guarded by queues or snapshots.
Figure 14.3: Timing and shared-state review

Review these risks:

Blocking pathNetwork, storage, bus, and update calls must not freeze state review or hide stale output. Record timeout behavior.
Interrupt handoffInterrupts and callbacks should capture minimal facts and hand work to a loop, queue, or task.
Queue pressureEvents can arrive faster than they are handled. Record queue size, drop policy, coalescing rule, or backpressure behavior.
Shared dataUse snapshots, queues, atomic flags, or explicit guards when more than one path reads or writes the same data.
Priority inversionIn task models, a low-priority owner can block urgent work. Record resource ownership and any timeout or guard rule.
Recovery timingFault detection, watchdog reset, reconnect attempts, and update rollback need visible timing evidence, not only happy-path logs.

14.10 Greenhouse Ventilation Controller

A team is prototyping firmware for a greenhouse ventilation controller. The device reads temperature, humidity, and door state, drives a fan relay, reports status to a gateway, and must recover safely if a sensor or gateway is unavailable.

14.10.1 Stage 1: Super Loop for First Evidence

The first slice uses a simple ordered loop: read inputs, decide fan state, publish status, record log, wait. This proves the drivers, payload fields, and basic control rule. The team records where blocking can occur and keeps the loop intentionally small.

14.10.2 Stage 2: State Machine for Behavior

The next slice names states: idle, ventilating, sensor_fault, gateway_offline, and manual_override. Each transition has a reason, timeout expectation, and recovery rule. The team now has evidence for behavior, not just a sequence of function calls.

14.10.3 Stage 3: Event Queue for Inputs

Door interrupts, timer ticks, fan-current alerts, and gateway messages are turned into events. Handlers only enqueue facts. The main loop processes events and updates state, so the architecture remains testable.

14.10.4 Stage 4: Task Model Only if Needed

If gateway communication, display update, and sensor sampling become independent blocking responsibilities, the team may move to a task model. That decision requires evidence: queue contracts, stack or memory headroom, shared-state rules, and fault behavior.

prototype=greenhouse-vent-controller
architecture_question=can the firmware expose control state and fault recovery clearly?
current_pattern=state machine with event queue
driver_boundary=temp_humidity, door, fan_relay, gateway_status
states=idle, ventilating, sensor_fault, gateway_offline, manual_override
events=timer_tick, sensor_ready, door_changed, publish_failed, override_command
normal_run=read sensors, update state, drive fan, publish status
fault_run=sensor missing, gateway unavailable, relay feedback mismatch
shared_state_rule=event queue owns input facts; state machine owns decisions
handoff=keep states, events, payload samples, and fault cases; repeat timing check on target

14.11 Architecture Review Record

Leave a record that another engineer can replay or challenge. A diagram alone is not enough; the record must say which pattern was chosen, what it proved, and what remains unproven.

Review record for a greenhouse ventilation firmware prototype with a state-machine and event-queue structure, driver, state, gateway, storage, update, and log boundaries, normal and fault run evidence, shared-state ownership, keep, rewrite, recheck, and next-chapter handoff decisions.
Figure 14.4: Software architecture review record

Use this template:

prototype=
architecture_question=
selected_pattern=
responsibility_boundaries=
state_or_event_model=
normal_run_evidence=
fault_run_evidence=
blocking_paths=
shared_state_rule=
timing_evidence=
observability_evidence=
keep=
rewrite=
repeat_check_when=
next_chapter_handoff=
review_owner=
review_date=

14.12 Knowledge Check

Architecture Evidence
Match Pattern to Evidence Need

Order the Architecture Review

14.13 Common Failure Patterns

One loop grows into everythingDrivers, state, communication, storage, and update code become tangled. Split by responsibility before reuse.
State is implicitThe device behaves differently after hidden flags or counters change. Name states and transitions so reviewers can challenge them.
Interrupts do too muchHandlers read sensors, publish messages, or write storage. Capture a fact and hand work to a safe context.
Tasks share data casuallyMultiple paths update the same structure without a snapshot, queue, or guard. Record the shared-state rule.
Architecture hides failureThe diagram shows the happy path but not sensor absence, publish failure, full storage, reboot, or partial update.
Pattern is chosen too earlyThe team adopts a heavy structure before proving it needs concurrency. Start with the evidence question, not the pattern name.

14.14 Summary

  • Architecture patterns are evidence tools for timing, state, failure, and handoff risk.
  • Use responsibility boundaries before choosing a pattern.
  • Super loops, state machines, event queues, RTOS tasks, host harnesses, and hybrids all have valid prototype roles.
  • Review blocking paths, interrupt handoff, queue pressure, shared state, priority risk, and recovery timing.
  • Record what to keep, rewrite, check again, and hand off to the library, testing, OTA, and best-practice chapters.

14.15 Key Takeaway

A useful software architecture separates hardware access, application logic, communication, configuration, and diagnostics so prototypes can evolve without rewrites.

14.16 What’s Next

Manage dependenciesLibraries & Version Control covers library choice and reuse boundaries.
Verify behaviorTesting & Debugging turns architecture evidence into repeatable checks.
Plan updatesOver-the-Air Updates covers update and recovery evidence for deployed prototypes.
Keep qualitySoftware Best Practices connects architecture decisions to durable firmware work.
Review setupSoftware Development Environments records build, upload, and observation context.
Review language fitChoosing Languages for IoT Software Prototypes records runtime constraints and handoff boundaries.