systemref

State Machines in Firmware

H. Maqsood Jun 8, 2026 2 min read state-machine firmware embedded control-flow c/c++
A state machine replaces ad-hoc conditional logic with a finite, auditable set of states and transitions — essential in interrupt-driven firmware where uncontrolled branching causes undefined behavior.

Firmware logic written as nested conditionals degrades quickly. A flag checked here, an interrupt modifying it there, behavior that depends on the sequence of events rather than the current state — this is the failure mode. A state machine imposes structure: the system is always in one known state, and only defined inputs can move it.

The model

typedef enum { IDLE, ACTIVE, FAULT, RESETTING } State;
typedef enum { CMD_START, CMD_STOP, ERR_DETECTED, CMD_RESET } Event;

State transition(State current, Event event) {
  switch (current) {
    case IDLE:
      if (event == CMD_START)    return ACTIVE;
      break;
    case ACTIVE:
      if (event == CMD_STOP)     return IDLE;
      if (event == ERR_DETECTED) return FAULT;
      break;
    case FAULT:
      if (event == CMD_RESET)    return RESETTING;
      break;
    case RESETTING:
      return IDLE;
  }
  return current; // unhandled event — hold state
}

Unhandled events return the current state. Nothing silently mutates. Every possible transition is visible in one function.


Entry and exit actions

State transitions usually carry side effects — activating a relay, setting a GPIO, starting a timer. Separate these from the transition table. Entry and exit actions run on state change, not on every event.

void on_exit(State s) {
  if (s == ACTIVE) gpio_set(PIN_MOTOR, 0);
}

void on_enter(State s) {
  if (s == ACTIVE)    gpio_set(PIN_MOTOR, 1);
  if (s == FAULT)     gpio_set(PIN_ALARM, 1);
  if (s == RESETTING) start_reset_sequence();
}

// In the event loop:
State next = transition(current, event);
if (next != current) {
  on_exit(current);
  on_enter(next);
  current = next;
}

This ensures on_enter fires exactly once per state entry, regardless of how many events are processed in that state.


Encoding the table as data

For larger machines, a transition table is cleaner than a switch statement:

typedef struct {
  State from;
  Event event;
  State to;
} TransitionRule;

static const TransitionRule rules[] = {
  { IDLE,      CMD_START,    ACTIVE     },
  { ACTIVE,    CMD_STOP,     IDLE       },
  { ACTIVE,    ERR_DETECTED, FAULT      },
  { FAULT,     CMD_RESET,    RESETTING  },
  { RESETTING, CMD_START,    IDLE       },
};

State transition(State current, Event event) {
  for (size_t i = 0; i < sizeof(rules)/sizeof(rules[0]); i++) {
    if (rules[i].from == current && rules[i].event == event)
      return rules[i].to;
  }
  return current;
}

The full state model is now a static, readable data structure. Adding a state or transition is a one-line edit.


When to use it

Use state machines in firmware when:

  • A component has discrete operational modes with defined transitions between them
  • Safety or reliability requirements demand auditability — every transition must be traceable
  • You find yourself writing if (started && !stopped && !in_fault && mode == 2)

Avoid flat state machines when state count exceeds ~10–12. Beyond that, use a hierarchical state machine (HSM): substates that inherit transitions from their parent, reducing the combinatorial explosion of large flat tables. The UML statechart model is the standard reference.