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


Chapter 1. From Contracts To Tables

The last post ended on a promise: the architectural choice extends to state machines themselves — and the form that reads cleanest for humans, auditors, and AI is the table 📐.

This post is that form, written down.

A state machine is the smallest unit of structure in firmware. It is one of the very few places where the design and the code are the same thing — what the diagram says is what the device does. Every firmware product has dozens of them. Some are tiny — a button debouncer with three states. Some are large — a connected sensor whose lifecycle has fifteen states and forty transitions across boot, normal operation, fault, recovery, and shutdown.

The shape of those state machines decides a lot. It decides how quickly a new engineer understands the device. It decides what an auditor can certify. It decides whether the AI agent extending the code proposes a transition that already lives in the model or invents one the runtime cannot reach.

The form I keep coming back to, across many products, is the flat table — five columns, one row per transition. A small data structure the runtime walks at every event. Boring to look at. Calm to extend. Hard to get wrong.

First principle. State machines are one of the few places where the design and the code are the same artifact. The shape of that artifact compounds.


Chapter 2. The FSM That Lived In Three Files

A few products back, I joined a team mid-flight to help land a difficult device into production 🛠️.

The product was a connected door lock. Battery-powered. BLE-paired. Cloud-managed. The firmware was thoughtful — the team was strong and the codebase was clean for its age. The lifecycle FSM was the part that gave me pause. It lived in three files. A header declared the states as an enum. A C file held a big switch on the current state with nested switch calls on the event inside. A second C file held the action functions the transitions called. The guards were inline if statements inside the switch arms.

Reading the FSM end-to-end took a notebook and two passes. The team had grown the device's behavior over two years, and every behavior was somewhere in the three files. The product worked. The team shipped it.

The 2am surprise came in the field. A small population of devices began entering an irrecoverable state after a specific sequence — provision, factory reset, re-provision, low-battery event, BLE disconnect. The path through the FSM crossed a transition that had been added late and a guard that had been written for a different event. The two together opened a door into a state with no exit.

The fix was three lines. Finding the fix took two senior engineers about four days of reading the three files in tandem and drawing the implicit graph on a whiteboard. Half of that time was reading. The other half was being sure they had read every branch 🕯️.

The FSM was correct in the engineer's head. It was incomplete on paper. That gap is the cost of an FSM written across three files instead of as a table.

First principle. A state machine spread across three files is whole in the head and incomplete on paper. The paper is what the next reader inherits.


Chapter 3. The Five Columns Of A Flat FSM

The form I have come to prefer is small enough to write on an index card 🗂️.

state          event          guard               action               next_state
─────────────  ─────────────  ──────────────────  ───────────────────  ─────────────
IDLE           PROVISION_REQ  bond_table_empty()  pair_and_provision   PROVISIONED
PROVISIONED    UNLOCK_REQ     credential_valid()  unlock_motor         UNLOCKED
PROVISIONED    UNLOCK_REQ     credential_invalid  log_denied           PROVISIONED
UNLOCKED       AUTO_TIMEOUT   —                   relock_motor         PROVISIONED
UNLOCKED       MOTOR_FAULT    —                   enter_safe_state     FAULTED
PROVISIONED    LOW_BATTERY    battery_critical    persist_state        LOW_POWER
LOW_POWER      EXT_POWER_OK   —                   resume_from_persist  PROVISIONED
FAULTED        OPERATOR_RESET fault_clearable     clear_fault          IDLE
ANY            FACTORY_RESET  —                   wipe_secrets         IDLE
*              *              —                   log_unknown          *

Five columns. One row per transition. The five columns are: state (the current state), event (what came in), guard (the boolean precondition), action (the function called on the transition), and next_state (where the FSM lands).

The runtime is six lines of code. Walk the table. For each row whose state matches the current state and whose event matches the current event and whose guard returns true, call the action and assign the next state. The walk is deterministic. The order of rows is the priority. The fall-through row at the bottom catches anything the table did not enumerate.

The whole FSM lives in one artifact a reader can hold in their hand. The engineer reads it in two minutes. The auditor confirms it in an afternoon. The AI agent extends it by adding a row 📨.

First principle. A flat (state, event, guard, action, next_state) table is the form of state machine a human, an auditor, and an AI agent can all read on the first pass.


Chapter 4. What The Table Form Earns You

The same FSM, written as a table, earns four things that the switch-based form has to be argued for case by case 🎁.

Completeness becomes visible. Every (state, event) pair the device can encounter is either a row in the table or covered by a fall-through. The auditor's question — "what does the device do in state X when event Y arrives?" — has a one-row answer.

Reachability becomes a graph problem. A 30-line script walks the table and prints which states are reachable from IDLE and which are dead. The "state with no exit" bug from the door lock is a static-analysis pass against the table, not a 2am phone call.

Tests become data. Every row in the table is a test case. The test harness runs through the rows, drives each event, and asserts the resulting state. Adding a row adds a test. Removing a row removes a test. The coverage report is the table itself.

AI agents extend it cleanly. The agent reads the table, proposes a new row for a new event, and respects the existing structure. The proposal is a diff a human can review in seconds. The agent does not invent a new state or a new event on the side, because the form makes the cost of doing so visible.

These four properties are why the form keeps winning across products. None of them are exotic. All of them are denied to an FSM that lives in three files 🪞.

First principle. A flat FSM table turns completeness, reachability, tests, and AI extension into properties of the data structure itself.


Chapter 5. Where Tables Earn Their Limits

There is a class of system where the table form is not the right fit, and it is worth naming honestly so the form does not become a dogma 🎯.

A device whose FSM has three states and four transitions does not earn a table. A small struct or a clean switch is the right scale. A device whose state machine is dominated by continuous signal processing — a motor controller running a control loop at 10 kHz — needs a different decomposition; the FSM is the outer skeleton and the inner loop is its own form.

A heavily hierarchical FSM with parallel regions and deep nested states — the kind UML statecharts capture — can be flattened into a table, but the flattening is not always the cleanest form for the team to maintain. In those cases the table is a generated artifact from a higher-level description rather than the source of truth itself.

The table form earns its place when the FSM is the device's lifecycle and when the audience is multiple — the team, the auditor, the agent. That is most connected products in 2026. It is not all of them.

First principle. The flat table is the right form for the lifecycle FSM of most connected products. It is one tool, not the only one.


Chapter 6. The Convention That Compounds Across Products

The third reason to write FSMs as tables is the one that pays back the most over a career 📚.

When every product on a team uses the same FSM convention, the team's collective intuition transfers across products. The engineer who learns to read the door-lock table reads the thermostat table on the first try. The agent trained on the message-driven contracts from the last post reads the FSM tables in the same way. The auditor who certified product N walks into product N+1 and asks the same questions of the same artifact.

The convention itself is small. Five columns, one row per transition, fall-through at the bottom, runtime in six lines. A team can adopt it in a week and write the lifecycle FSMs of the next product against it 📦.

The flat FSM table sits next to the typed message contracts (from the last post) and the bounded event taxonomy (from the post before) as the third member of the small kit that makes the layer above the RTOS legible. Contracts say who talks to whom. The event taxonomy says what gets recorded. The FSM table says how each module behaves. Together, the three are a small, learnable architecture the team and the agent can both build inside.

First principle. Conventions compound. The FSM table is the third member of the small kit — contracts, events, tables — that makes the application layer legible across a team and across products.


Chapter 7. What I Would Convert This Sprint

If I were on a firmware team this week, here is the smallest move I would make 📌.

Pick the FSM in the product whose bug surface is the largest — usually the device lifecycle FSM. Read the three files (or the one big switch) end to end. On paper, write the five-column table that covers what the current code does. Note the rows that live in the code today without being written down anywhere, and the rows that the table forces you to think about for the first time.

Decide whether to ship the table now or in the next refactor window. Most teams ship it at the next refactor — the value is the table itself, not the runtime change. The act of writing the table surfaces the missing rows. The missing rows are the bugs the team will catch before they become 2am phone calls.

Three or four hours per FSM. One FSM per sprint. Six months later, every lifecycle FSM in the codebase is a table, and the team's mental model of every product matches the model the runtime is walking 🕊️.

The form is small. The convention is portable. The payback shows up the first time a 2am bug is a row that was missing from a table the team had in front of them, instead of a path through three files no one had read in two years.

If you wrote your product's lifecycle FSM as a flat table this week — which row would surprise you that the current code does not enumerate?

Next: tables, contracts, observability — these compound. There is one cost they compound against, and it is the largest hidden cost in firmware: debugging.

First principle. The form of the FSM is what the team inherits. The table form is what the team can hand on to the next reader, the next product, and the next agent.


Labeled: Opinion + Experience + Fact (45% opinion · 35% experience · 15% fact · 5% fiction)

Sources:

(Written in collaboration with AI — I discuss, I do not outsource.)

New to this labeling? Read the framework → 20+ Years of Ideas. Articulation Is the Craft.

— Ritesh | ritzylab.com

#EmbeddedSystems #Firmware #StateMachines #Architecture #FirstPrinciples