A hardware guy picks an STM32H743 in March because the RF front end needs three SPI buses and a spare timer for the PLL lock strobe. Reasonable call. The part goes in the schematic, the board gets laid out, and firmware starts in July.
By July, the flash layout is fixed. So is the boot path, the update strategy, the interrupt latency you can actually hit, and whether Linux was ever an option. Four months before anyone opened an editor.
That’s the part nobody tells you when they hand you a book on software architecture. Most of your firmware architecture was decided by a part number. Your job is figuring out which decisions are still yours, and not wasting them.
This is article one of twelve. The question it answers: how do people who’ve shipped this stuff actually pick an embedded firmware architecture?
1. What Is Embedded Firmware Architecture?
Everyone uses these words interchangeably. They shouldn’t. The thing that separates them is how much it costs to change your mind.
| Level | Scope | Cost to reverse | Example |
|---|---|---|---|
| Coding | Statements, functions | An afternoon | for loop vs. memcpy |
| Design | A module, a driver | A sprint | Ring buffer vs. double buffer in the UART driver |
| Architecture | Component boundaries and their contracts | A release cycle, plus regression risk | Blocking driver API vs. callback vs. event post |
| System architecture | Silicon, mechanicals, cloud, provisioning, service | A hardware spin and re-certification | MCU + external secure element vs. TrustZone |
| Firmware architecture | What lives inside the MCU, on silicon someone already chose | Weeks to months | Superloop vs. RTOS. Single-bank vs. A/B flash. |
Two things fall out of that.
The first is that firmware architecture sits downstream of system architecture, which is why you need to be in the room at schematic review. The decisions that hurt in year two are boring ones. No spare flash for a second image. No free GPIO for a boot-mode strap. A sensor sitting on a bus you can’t reset independently of everything else on it. All of those cost nothing to fix at review and can’t be fixed at all afterward. If someone handed you the silicon and then asked you to design the architecture, a chunk of it was already assigned.
The second: architecture is whatever the code can’t easily undo later. That gives you a definition that survives production.
Firmware architecture is the set of decisions about component boundaries, control flow, timing, memory ownership, and failure behaviour that later code can’t easily undo.
What’s missing from that list is language, coding style, folder layout, whether you use CubeMX. Those get argued about in design reviews as if they were architecture. They’re not. What is on the list, and gets treated as an implementation detail almost everywhere: memory ownership and failure behaviour. Get those wrong and the codebase stops being maintainable somewhere around the second engineer.
2. The Ladder Every Product Climbs
Architectures aren’t a menu where you pick the one you like. They’re a ladder, and products get pushed up it. What matters is knowing what does the pushing.
| Architecture | Best for | Core problem |
|---|---|---|
| Superloop | Simple systems | Poor scalability |
| Interrupt-driven | Timing-sensitive work | Complexity |
| Event-driven | Asynchronous systems | Event management |
| RTOS | Concurrent applications | Synchronisation |
| Embedded Linux | Complex compute / networking | Resource overhead |
| Hybrid | Complex products | Architectural complexity |
That’s the summary version. Now the useful version.
Bare metal
Register writes. No abstraction. Often no main() loop worth the name. Bring-up code, bootloaders and production test fixtures live here forever and that’s fine.
It’s not a product architecture. It’s still a skill you need, because when the RTOS hangs before the scheduler starts, bare metal is what you’re debugging.
Superloop
for (;;) {
sensor_poll();
control_update();
display_refresh();
comms_service();
}People are too quick to dismiss this. A superloop on a 5 ms period with four cooperative tasks is easier to reason about than four RTOS tasks with three mutexes wedged between them. I’d take the first one on a product I have to support for eight years.
What pushes you off it: worst-case loop latency is the sum of every task’s worst case. Not the average, the sum. The day display_refresh() picks up an SPI transaction that blocks for 8 ms, your control loop jitter is 8 ms. And jitter is what kills control systems, not throughput.
Watch for this tell: you start sprinkling if (millis() - last > N) guards to stagger work across passes. Congratulations, you wrote a scheduler with no priority model. Time to climb.
Interrupt-driven
Timing-critical work moves into ISRs, the loop handles the rest. First architecture with real concurrency, so also the first with real concurrency bugs.
Numbers to anchor on. A Cortex-M4 takes 12 cycles to enter an ISR and about 6 to tail-chain between pending ones, both from zero wait state memory. An M7 is 12 typical and 14 worst case, and ST is explicit that the 12 only holds with code in ITCM and data in DTCM. At 480 MHz, 12 cycles is 25 ns, which is nothing. It’s also hardware latency to the first instruction, not the cost of your handler. Compiler prologue, the body, and 18 words of lazy FPU stacking if you touch floating point all sit on top of it.
Notice what that M7 figure is conditional on. Move the data off TCM and you start paying. An ISR walking a buffer in AXI SRAM misses the D-cache once per 32-byte line, and AXI SRAM runs at half the core clock, so each fill costs tens of cycles rather than one. Do that across a few hundred bytes and the handler you calculated at 25 ns shows up on the scope in the microseconds. Move the hot data to DTCM and it goes away.
One buffer, one line, is a different problem wearing the same costume. That’s the ordinary DMA case, and the fix there is an MPU region marked non-cacheable or explicit clean and invalidate. That one is coherency. The latency problem is the loop.
Don’t take the datasheet number. Toggle a GPIO on entry and exit, measure it on your board, at your clock config, with cache on. And look at the 99.99th percentile, not the mean. The mean is a comfortable lie.
What pushes you off it: shared state. Every variable touched by both an ISR and the main loop needs volatile, a critical section or an atomic. The third engineer on the project will miss one. Not might. Will.
Event-driven
ISRs stop doing work and start posting events. The loop becomes a dispatcher, handlers run to completion, nothing blocks.
You get a single-threaded mental model with interrupt-grade responsiveness, and most of your mutex requirements evaporate because nothing preempts a handler. It pairs with state machines so naturally that a lot of teams who think they need an RTOS actually needed this.
Queue depth is an architecture decision, not something you tune later. Size it against the worst-case burst. What happens when the radio, the sensor and the button all fire inside the same millisecond? And decide up front what overflow does: drop oldest, drop newest, or fault. Dropping events quietly is how you get a field bug nobody can reproduce.
What pushes you off it: blocking. The moment a handler has to wait on a TLS handshake or a filesystem write or USB enumeration, run-to-completion breaks. Then you either chop the operation into a state machine or you bring in threads.
RTOS
Threads, priorities, blocking APIs, preemption. You get to write sequential code for things that are genuinely sequential, which is a real win for protocol stacks. I’m not anti-RTOS. I’m anti-RTOS-by-default, which is a different complaint.
The bill comes in RAM. A FreeRTOS TCB is 80–120 bytes depending on config, and useful task stacks start at 512 B to 1 KB, climbing to 1–2 KB the moment printf or a TLS stack shows up in the call tree. Twenty tasks at that upper figure is 20–40 KB before your application owns a byte. On an H743 with 1 MB, who cares. On an L0 with 20 KB, that’s the entire budget.
Watch the units when you size those stacks. xTaskCreate takes a depth in words, not bytes, so on Cortex-M whatever number you pass gets multiplied by four. CubeMX shipped exactly that bug for years, taking words in the GUI and handing them to osThreadNew, which expects bytes.
The other cost is a bug class that doesn’t exist one rung down. Priority inversion. Deadlock. A stack overflow that quietly stomps the neighbouring task’s data and surfaces three seconds later in code that has nothing to do with it. Article 05 deals with the mitigations.
What pushes you off it: you need a filesystem, sockets, a display framework, process isolation, a package ecosystem, and you have the compute to pay for them.
Embedded Linux
Different universe. MMU, Cortex-A, external DRAM, boot times in seconds instead of microseconds, and a userspace that makes networking and storage close to free.
The thing people underestimate is what you give up: determinism. A Linux userspace process will not reliably service a 50 µs deadline. PREEMPT_RT narrows the gap. It doesn’t close it, and anyone telling you otherwise hasn’t measured it under load. So if your product has a web UI and a motor commutation loop, Linux alone is the wrong answer, which is exactly why the last rung exists.
Hybrid
Two cores, two architectures. On an MP1, one or two A7 cores run Linux for connectivity and UI while an M4 runs the deterministic control loop. On an H745, two M-class cores split the same way without Linux in the picture. OpenAMP/RPMsg carries messages across.
This is the right answer for more products every year, and it’s the hardest one to get right, because you now own two boot sequences, two update paths, two failure domains, and a shared-memory contract between them. Cache coherency across that shared region is yours to solve. The silicon won’t do it for you. MPU config and explicit clean/invalidate discipline are architecture here, not tuning.
The rule
Climb when something pushes you. Not before.
Every rung up buys capability and spends determinism, RAM, boot time and debuggability. Firmware running an RTOS with three tasks and no blocking calls didn’t need an RTOS. It needed a superloop and someone willing to say so in the design review.
3. The Decision Framework
Twelve inputs. Each one has a threshold that actually flips the answer, which is the only reason it’s on the list.
CPU capability. Not MHz. Instructions per deadline. A control loop needing 20,000 cycles of DSP work every 1 ms costs you 20 MIPS before headroom. And check the FPU. Take an M4F and turn the FPU off: soft float costs roughly 10× to 30× per operation, about 26 cycles for a multiply against single digits in hardware. That’s per operation, so a real workload lands nearer 5× to 10× once loop overhead and integer work are counted, and it will still blow a deadline your spreadsheet said was comfortable. On an M0+ there is no FPU to turn on in the first place.
RAM. The hard gate on RTOS viability. Under ~32 KB an RTOS is usually a bad trade. Above ~128 KB it’s affordable. Above ~1 MB with external DRAM and an MMU, Linux joins the conversation.
Flash. This one decides your update strategy and almost nobody checks it in time. If the application eats more than roughly 45% of flash, A/B dual-bank updates are off the table and you’re stuck with something riskier for the life of the product. Check it before layout freezes. I mean it.
Timing. The single most decisive input. Find the tightest hard deadline and be honest about which ones are hard. Hard means missing it damages hardware, breaks a standard, or loses data you can’t get back. A user noticing something is slow is not a hard deadline, no matter how loudly product says it is.
Interrupt rate. Aggregate load, not peak count. 10 kHz of interrupts at 3 µs each is 3% CPU. 100 kHz of the same is 30%, plus cache thrash, and now your worst-case task latency isn’t something you can work out on paper.
Peripheral count and coupling. Twelve peripherals that never interact is a superloop. Four that must be sequenced under a shared timing constraint is a state machine.
Concurrency. Ask a sharper question than “do we need multitasking”: how many operations must block at the same time? Blocking is what costs you threads. Concurrent non-blocking work is what event loops do for free.
Safety. 61508 and 26262 change the architecture, not just the paperwork: partitioning, MPU-enforced isolation, an RTOS you can defend, traceability from requirement to test. 62304 is a lifecycle standard and gets there indirectly, through the Class A/B/C classification and the segregation it demands between software items. Retrofitting any of it is a rewrite, and everyone who’s tried will tell you the same thing.
Security. Secure boot means a root of trust in immutable memory and a signature check before every jump. Boot chain, key storage and flash layout are structural. Article 08 goes deep. The CRA has already turned this from a nice differentiator into a condition of selling in Europe at all. Reporting obligations bite on 11 September 2026. The design and maintenance requirements follow on 11 December 2027.
Networking. TCP/IP with TLS realistically wants 80–160 KB of flash before your code. lwIP on its own is 40–80 KB; mbedTLS adds about 27 KB for a PSK-only build and climbs past 200 KB at defaults. The 20–50 KB RAM figure people quote holds only after you cut MBEDTLS_SSL_MAX_CONTENT_LEN down from its 16 KB default, and you can only do that if you own both ends of the connection. At defaults you’re near 32 KB per session before lwIP’s pbuf pools. Add cert storage and a time source to validate against. This input alone shoves a lot of designs two rungs up.
Update mechanism. Not a phase-two feature. Ever. It dictates flash partitioning, bootloader design, rollback storage, version metadata, and whether a failed update bricks a unit inside somebody’s wall. Article 12 is the whole thing, and it’s the one most people wish they’d read first.
Product lifetime and team size. A ten-year product has to survive silicon EOL, toolchain rot, and complete turnover of the team that built it. That’s article 10. On team size, Conway was right and arguing about it is a waste of a meeting: your module boundaries will end up matching your team boundaries. So draw them there deliberately.
The tree
Run it per subsystem, not per product. Real products are mixed: motor loop interrupt-driven, protocol stack event-driven, logging on a low-priority RTOS task. “Our architecture is FreeRTOS” describes a dependency, not an architecture. That’s also what makes the first branch usable, because a sub-10 µs deadline is an answer for that path, not for the whole system.
Write the answers down. Three sentences per decision: what we picked, what we rejected, what would make us revisit. Ten minutes of work, and it’s the highest-return document in a long-lived firmware project. Year four, someone asks why the flash is partitioned that way. Either there’s an answer or there’s a rewrite.
4. Layers
Anything above a superloop organises into layers. This stack is the reference for the rest of the series.
Three rules make the diagram worth more than the paper it’s on.
Dependencies point down. A driver never includes an application header. When a lower layer needs to tell an upper layer something, it does it through a callback or an event the upper layer registered. Inversion of control, not an upward #include.
Don’t skip layers casually. There’s one good reason to skip: measured performance on a hot path. A DMA-driven ADC feeding a control loop shouldn’t route through three abstractions per sample. So skip. But do it deliberately, comment it with the measurement that justified it, and keep it inside one module.
Vendor types stop at the boundary. This is the rule that gets broken most and costs most.
/* application/thermostat.c — ANTI-PATTERN */
#include "stm32h7xx_hal.h"
extern UART_HandleTypeDef huart3;
void thermostat_report(float celsius)
{
char buf[32];
int n = snprintf(buf, sizeof buf, "T=%.2f\r\n", (double)celsius);
HAL_UART_Transmit(&huart3, (uint8_t *)buf, (uint16_t)n, 100);
}It compiles. It ships. It’s still broken, and here’s the bill.
Application code now depends on the vendor HAL, so it won’t compile on a host and you can’t unit test it without hardware on a desk. It reaches four layers down to grab a global handle, so the UART instance is nailed down at compile time in product logic. It blocks inside that logic until the transfer completes or 100 ms elapses, whichever comes first, which means your control loop deadline is now hostage to a serial port. And moving telemetry to USB CDC or a radio means editing application source, so changing a transport triggers a re-review of product behaviour.
Four problems, one #include.
Push a contract down instead and let the platform satisfy it:
/* services/telemetry.h */
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
typedef struct {
bool (*write)(void *ctx, const uint8_t *data, size_t len); /* non-blocking */
void *ctx;
} telemetry_sink_t;
/* application/thermostat.c — no vendor headers, no heap, no blocking */
static uint32_t telemetry_drops;
void thermostat_report(const telemetry_sink_t *sink, float celsius)
{
uint8_t buf[32];
size_t n = fmt_temperature(buf, sizeof buf, celsius);
if (!sink->write(sink->ctx, buf, n)) {
telemetry_drops++; /* sink full: dropping is fine, silence isn't */
}
}No heap, no exceptions, no RTTI. A function pointer and one indirect call. On target the sink wraps a DMA-backed UART with a ring buffer. On the host test runner it appends to an array and the test asserts on the bytes.
That one change is most of what article 09 means by designing for testability. And it was an architecture decision, not a coding one.
5. Quality Attributes
You compare architectures on properties, not features. Nine matter. What turns a design review into an actual review is refusing to accept any of them without a measurable proxy.
| Attribute | Proxy | Article |
|---|---|---|
| Portability | Files containing vendor headers | 04 |
| Testability | % of modules that build and run on the host | 09 |
| Maintainability | Time for a new engineer to ship a safe change | 03 |
| Scalability | RAM and latency cost of adding the next feature | 02 |
| Reliability | Field MTBF, watchdog resets per 1,000 device-days | 07 |
| Determinism | 99.99th-percentile jitter on a GPIO, on a scope | 05 |
| Observability | Mean time to diagnose a field fault from logs alone | 11 |
| Security | Attack surface: every input, every writable region, every key | 08 |
| Recoverability | Survives power loss mid-update? Test it 1,000 times. | 12 |
Here’s the part that gets skipped. These fight each other. Portability costs determinism, because abstraction adds indirection. Testability costs footprint. Security costs boot time and flash. Observability costs bandwidth and power. You cannot max all nine and anyone claiming otherwise is selling a framework.
So rank them. In writing. Before you start. Three products, same layer diagram, same RTOS:
Battery-powered LoRaWAN sensor on a ten-year coin cell — reliability, recoverability, observability. Determinism doesn’t register, because nothing has a hard deadline. Portability’s nearly worthless because that silicon isn’t changing.
Medical infusion pump — determinism, reliability, testability. Not because they’re nicer engineering, but because an auditor is going to ask for evidence on exactly those three.
Consumer connected product on an 18-month cycle — scalability, observability, security. The architecture that wins is the one that absorbs whatever the roadmap does next and lets you diagnose a field regression from telemetry on a Friday afternoon.
Three genuinely different architectures out of the same diagram. The ranking is the architecture. Put it at the top of the design doc and use it to end arguments.
6. Four Ways the Decision Goes Wrong
Article 03 covers structural anti-patterns properly. These four are different. They’re failures in how the decision got made, and they happen before any code exists.
Architecture by résumé. “We used Zephyr on the last one.” Sure, and the last one had 2 MB of flash, three people fluent in Kconfig and Device Tree, and nothing tighter than a 10 ms deadline. If none of that’s true now, familiarity is a cost you’re paying, not a saving you’re making.
Picking an RTOS for “multitasking” without measuring a deadline. The most common over-climb there is. Three tasks, none of which block, then two mutexes bolted on when the shared state bites. That’s a superloop with priority inversion added at full price.
Multitasking isn’t a requirement. A missed deadline is a requirement.
Deferring the update mechanism to phase two. Flash partitioning is architecture. Ship single-bank at 90% occupancy and you’ve permanently chosen a no-rollback update path with a brick window in it. Adding A/B in year two means a new part, a board spin, or a recall. Pick one.
Promoting the vendor example to production. Vendor examples are optimised to give you a working demo in ten minutes. Global handles, blocking calls, application logic sitting in main() between the USER CODE BEGIN markers. Fine to learn from, unsafe to build on. The specific trap is that CubeMX regenerates over those files, so your architectural intent ends up living inside a code generator’s output and gets silently reverted the day a colleague adds a peripheral pin.
Same failure underneath all four: the decision was made by default instead of on purpose. Defaults aren’t neutral. They encode somebody else’s constraints, from somebody else’s project.
Next
All of the above is reasoning. Which rung, which attributes, keeping vendor types out of application code.
None of it is a shipping architecture yet. For that you need concrete module boundaries, a memory ownership model, a defined startup and shutdown sequence, a fault path, and a build a new engineer can reproduce on day one.
Article 02: Production-Grade Firmware Architecture — where this turns into something you can build against. Coming next.
Series Index
Phase 1 — Architecture
01. Embedded Firmware Architecture Fundamentals (you are here)
02. Production-Grade Firmware Architecture
03. Firmware Architecture Anti-Patterns
Phase 2 — Implementation
04. HAL vs BSP vs Drivers vs Middleware
05. RTOS Architecture
06. State Machine Architecture
Phase 3 — Failure & Resilience
07. Error Handling & Recovery
08. Firmware Security Architecture
Phase 4 — Verification
09. Designing Firmware for Testability
Phase 5 — Product Lifecycle
10. Designing Firmware for 10-Year Products
11. Field Diagnostics & Observability
12. OTA & Safe Firmware Updates