What this is. Opinion + Experience + Fact (35% opinion · 30% experience · 35% fact). Written in collaboration with AI — I discuss, I do not outsource.

Most firmware doesn't start as a state machine. It starts as a main loop and a few flags — and it works, until it doesn't. This is the case for admitting the machine was there all along: why scattered flags turn into your hardest bugs, what changes when you name your states, and how deciding transitions in one place makes the worst failures lose their hiding spots.

Every embedded engineer has met this bug. A device that behaves perfectly on the bench does something inexplicable in the field — connects while it should be sleeping, retries a command it already finished, wedges in a state the code "can't" reach. You trace it for a day and find no single broken line. The failure lived in a combination of conditions no one designed and no one thought could happen at once.

That is almost always the signature of hidden state — behaviour spread across a handful of boolean flags instead of expressed as one explicit machine. The firmware is a state machine; it's just pretending not to be. Let me walk through why that pretence gets expensive, and what changes when you stop keeping it.


1. Every flag is a hidden state

A flag looks like a shortcut. isBooted, isConnected, isRetrying — each one is a small, honest fact about the device. The trouble is that a boolean isn't really a shortcut; it's a state that never got a name. And states don't stay separate. Add a few flags and the number of combinations the code can be in multiplies — a handful of booleans encode dozens of combined states, most of which were never meant to happen.

As features land, the guard conditions grow to match: if (isConnected && !isRetrying && hasConfig && !isSleeping). Each new flag roughly doubles the space the logic has to account for, until no one can hold in their head what the device will actually do in response to a given event. The nastiest bugs aren't in one line — they live in a combination the code quietly allows but no one ever intended.

▸ First principle. Each flag is an unnamed state, and together they multiply faster than anyone can track — hidden state does not stay simple.

2. Name the states, and the chaos shrinks

The fix is almost embarrassingly plain: write down what the device actually is at any moment. Booting. Idle. Connecting. Working. Recovering. A short, honest list of the modes it genuinely occupies. Then draw the transitions — which event moves it from one named state to the next, and only those.

The moment you do this, the illegal combinations that caused the field bug become impossible by design rather than by luck: if the device can only be in one named state at a time, "connected while sleeping" isn't a combination the code can reach. And the diagram does double duty — it tells a new engineer, and future-you, exactly how the thing behaves, on one page, without archaeology through scattered if statements.

The same device, as flags versus as named states On the left, a tangle of boolean flags (isBooted, isConnected, isRetrying, hasConfig) feeding a knot of overlapping if-conditions. On the right, five named states — Booting, Idle, Connecting, Working, Recovering — connected by labelled event transitions in a clean ring. Hidden state vs. named states FLAGS · a state you forgot to name isBooted isConnected isRetrying hasConfig 4 flags → 16 combinations, most never meant to happen. if (isConnected && !isRetrying && hasConfig && isBooted) … The bug lives in a combination no one designed. NAMED STATES · one at a time Booting Idle Connecting Working Recovering Each event moves the device along exactly one edge.

▸ First principle. A state you name is a state you can reason about, test, and defend — and illegal combinations become impossible by design, not by luck.

3. Decide transitions in one place

Naming the states is half of it. The other half is deciding where transitions happen. The failure mode is scattering that decision across a dozen interrupt handlers and flag-checks, each nudging the device toward a new mode with no single owner. Instead, feed events into one place that owns the transition: an event arrives, the machine looks at its current state, and moves — or refuses to.

This is exactly what UML statecharts formalize — states, events, transitions, and guards as first-class things rather than emergent behaviour of scattered booleans. Run the machine cooperatively in a single thread and an entire class of bugs — the races and deadlocks that come with juggling shared flags across preemptive tasks — simply never has a chance to occur. A small machine you can hand-code in a switch; a large one earns a statechart library or a code generator. Either way, there is one place to look when behaviour surprises you.

▸ First principle. Decide transitions in one place, and the hardest bugs lose their hiding spots.

4. Make it boring and reusable

None of this is exotic; it's just discipline applied before the flags accumulate rather than after. Twenty years of embedded work taught me to make that discipline boring and reusable instead of re-deriving it on every project. It's the spine of EmbedIQ, the open-source layer I've been building: each task runs as a single thread with a small table-based state machine and one message queue — explicit states, one place for transitions, no nested tangle. The pattern in this post, made a default you don't have to remember.

▸ First principle. The cheapest state machine is the one you designed on purpose, once, and reused — not the one your flags built for you by accident.

Sources

The flag-accumulation antipattern and finite state machines / UML statecharts as the event-driven alternative are long-standing, widely documented embedded practice — see Promwad, "Event-Driven Architecture in Embedded Firmware"; Embedded.com, "Programming embedded systems: what is a state machine?"; Miro Samek's state-machine.com (FSM / statechart fundamentals); and Barr Group, "How to Code a State Machine in C or C++." This piece describes method and experience; it carries no measured statistics.

FAQ

Why do boolean flags cause bugs in firmware?
Because each flag is really an unnamed piece of state, and independent flags combine: a handful of booleans encode dozens of combined states, most of which were never intended. As guard conditions grow, the device can reach combinations no one designed — and those unintended combinations are where the hardest field bugs live.

What does it mean to model a device as a state machine?
You enumerate the modes the device genuinely occupies (for example Booting, Idle, Connecting, Working, Recovering), allow it to be in only one at a time, and define which event moves it from one state to the next. Behaviour becomes explicit and reviewable instead of emerging from scattered flags.

Why decide transitions in one place?
When every interrupt and function nudges the device's mode independently, no one owns the behaviour and races appear. Feeding events to a single place that owns the transition — the essence of UML statecharts — gives you one location to reason about and test, and running it cooperatively in one thread removes a whole class of race and deadlock bugs.

When is a full statechart library worth it versus a hand-coded switch?
Small machines are fine to hand-code as a switch on the current state. As the number of states, nested behaviours, and guards grows, a statechart library or a code generator earns its keep by keeping the transitions rigorous and in one place rather than sprawling back into flags.

This is an embedded field note. The short version lives on my LinkedIn; the longer story, and the open-source layer that makes this pattern a default, live on my profile. — Ritesh | ritzylab.com