Your Product Already Has a State Machine

Every embedded product moves through modes: off, booting, idle, pairing, running, charging, fault, sleep. The logic that governs those transitions exists whether or not anyone wrote it down. When it is not designed, it emerges as a pile of boolean flags scattered across the codebase, and you end up with code that reads like if (isPaired && !isCharging && hasError == 0 && motorRunning). Six flags make sixty-four combinations. Nobody has tested sixty-four combinations. The ones nobody thought about are the field bugs.

The symptoms are recognizable. The device sometimes ends up in a mode the specification does not describe. A bug fix in one feature breaks another. Test coverage is meaningless because the team cannot enumerate what should be tested. All of that is the cost of an undesigned state machine, and it grows superlinearly with feature count.

An explicit state machine replaces implicit combinations with an enumerated, finite set. The device is in exactly one state. Transitions happen only on defined events. Anything else is an error you can detect rather than a behavior you discover from a customer.

Before One Line of Code: The Diagram

Draw it. On paper, in a diagramming tool, or in PlantUML checked into the repository next to the source. The diagram is the design review artifact, and the arguments it provokes are the cheap ones.

List the states first and keep the list honest. A typical connected product has six to twelve top-level states; if you have thirty, you are probably encoding data as states. Then list the events: button presses, timer expirations, sensor thresholds, BLE connect and disconnect, command received, charger attached, fault detected. Then draw the transitions and, critically, answer for every state and every event pair what happens, including "nothing, ignore it." That grid is where the requirements gaps live: what happens if the charger is unplugged mid-firmware-update, if the user long-presses during pairing, if the sensor faults while the motor is running.

Mark entry and exit actions on each state rather than scattering them through transitions. Turn the motor off on exit from Running, once, instead of in each of the five arrows leaving it. And define the initial state and the fault state explicitly; the recovery behavior tied to the fault state belongs on the diagram, not in someone's head.

This diagram is also the best specification handoff you will produce. It is reviewable by people who do not read C, which means the product owner catches the missing case before it is implemented. It slots directly into the behavioral section of a product requirements document, and it makes the effort estimate in firmware development cost far more accurate, because states and transitions are countable in a way that "the app talks to the device" is not.

Implementation 1: Switch-Case

A state enum, a state variable, and a switch on it inside a function that takes an event. Two nested switches, or one switch on state containing a switch on event. It is a handful of lines, needs no framework, compiles to a jump table, and any embedded engineer reads it immediately.

Use it when you have up to roughly eight states and a dozen events. That covers a large share of real products: a sensor node, a battery-powered tool, a simple appliance controller. Keep the discipline that the state variable is written in exactly one place, inside the transition function, and never assigned from a driver or an interrupt.

It stops scaling when the switch runs past a few hundred lines, when the same event needs identical handling in eight states, or when you cannot see the machine's shape by reading the code. That is the signal to move, not a reason to keep adding cases.

Implementation 2: Transition Tables

Encode the machine as data: an array of rows, each holding current state, event, guard function, action function, and next state. The engine is a loop over that table, typically fifteen lines, and it never changes. Adding behavior means adding a row.

The advantages are substantial for medium machines. The table is the diagram, in a form a reviewer can check line by line against the drawing. You can validate completeness at build time and detect unreachable states. In products with certification obligations, the table is directly traceable evidence that implemented behavior matches the specification, which is what an auditor working through FDA design controls asks for.

The costs are a small indirection overhead, a steeper first read for a new engineer, and function pointers in flash, which some safety coding standards restrict. Table-driven machines fit comfortably in a task-based design, and how that task interacts with the rest of the system depends on the architecture chosen in RTOS vs bare metal.

Implementation 3: Hierarchical State Machines

Flat machines suffer from transition explosion. If a critical fault must move the device to Fault from any of fifteen states, you draw fifteen arrows and maintain them forever. Hierarchical machines, the UML statechart model, solve this by nesting: child states inherit their parent's transitions, so the fault arrow is drawn once on the enclosing superstate.

Hierarchy also gives you orthogonal regions, letting charging state and connectivity state evolve independently rather than multiplying into a combined state space, and history states, letting a device return to whichever substate it left when an interruption clears. Use hierarchy when you have more than about fifteen states, when many transitions are duplicated across states, or when the product genuinely has concurrent modes. A BLE peripheral with pairing, bonding, data transfer, and OTA phases, all of which must handle disconnect and low-battery, is a natural fit, and the phases themselves come straight out of BLE firmware development. Frameworks exist, and code generators can produce C from a statechart, but the overhead is real: heavier runtime, harder single-stepping, and a tool dependency in your build. Do not reach for it on a product with nine states.

Three Rules That Apply to All Three

Interrupts post events; they do not change state. An ISR pushes an event into a queue and returns. The state machine runs in a task or the main loop and is the only thing that touches the state variable. Violating this is how you get transitions that only fail once a month on one unit, the kind of bug that costs days with a logic analyzer and a debugger to catch.

Never block inside a state handler. No delay loops, no waiting on a peripheral. Start the operation, arm a timeout, and return; handle completion as another event. Blocking inside a handler is what makes a device unresponsive and what eventually trips the watchdog.

Log every transition. A trace of from-state, event, to-state, and timestamp, over UART during development and into a small ring buffer in production, is the single highest-value diagnostic in embedded work. When a unit comes back from the field, the last forty transitions usually tell you the whole story, and it is worth explaining that value to a non-technical stakeholder using the framing in what firmware actually is.

Add one more habit: make the machine testable off-target. If the state machine is pure logic with hardware behind function pointers or a thin abstraction layer, you can run the entire behavioral test suite on a build machine, exercise every transition in seconds, and catch regressions before flashing anything.

Design the Logic Before You Write It

Projects House designs product behavior as an explicit, reviewed state model before implementation, then builds firmware traceable back to it. Describe your product's modes and the situations that worry you through our contact form and we will map the machine with you.