Ground Embedded KO Subscribe

Notes / Data Structures & Algorithms

State machine: the basic tool of firmware design

Organise a complex sequence of behaviour as states and transitions. When flags multiply and the if statements start to tangle, it is time to switch to a state machine.

GROUND EMBEDDED · NOTEData Structures & Algorithms 05State machinesBUFFERhead →tail →write · read

A rice cooker is in one of a few states: standby, cooking, steaming, keeping warm. When an event such as a button press or a temperature arrives, it moves to the next state. Most firmware can be expressed this way.

IDLEwaitingStart buttonRUNrunningFault detectedERRORstopped
State machine. The current state and the incoming event decide the next state.

The basic form with switch

typedef enum { IDLE, RUN, FAULT } state_t;
static state_t st = IDLE;

void motor_task(void) {
    switch (st) {
    case IDLE:  if (start_pressed()) { pwm_on(); st = RUN; } break;
    case RUN:   if (overcurrent()) { pwm_off(); st = FAULT; }
                else if (stop_pressed()) { pwm_off(); st = IDLE; } break;
    case FAULT: if (reset_pressed()) st = IDLE; break;
    }
}

What makes a good state machine

  • Draw the state diagram first. Circles are states and arrows are events and actions.
  • For every event in every state, "what happens" has to be defined. Undefined combinations become bugs.
  • Separate what is done once on entering a state (entry) from what is done while staying in it.
  • Always provide a way out of the fault state.

When the states multiply, manage the per-state handlers in a table of function pointers, or extend to a hierarchical state machine with parent states.

Why a state and not flags

Three flags such as is_running, is_error and is_paused make eight combinations. Among them the code ends up allowing combinations that must never exist, such as "running and in error". Keep the state in a single variable and only one can be true at a time.

Design with a table first

Taking the motor control of a vending machine as an example, fill in a table before writing any code.

Current state Event Next state Action
Idle Start button Accelerating Turn the driver on
Accelerating Target speed reached Constant speed None
Accelerating Overcurrent Error Turn the driver off
Constant speed Stop button Decelerating Target speed 0
Constant speed Overcurrent Error Turn the driver off
Decelerating Speed 0 Idle Turn the driver off
Error Reset button Idle Clear the error

A blank is itself a decision: "that event is ignored in that state". Filling in the table exposes missed cases such as "what if the start button is pressed while decelerating?".

Implementing it with switch

typedef enum { ST_IDLE, ST_ACCEL, ST_RUN, ST_DECEL, ST_ERROR } state_t;
static state_t st = ST_IDLE;

void fsm_step(void) {                        // called every 1 ms
    switch (st) {
    case ST_IDLE:
        if (btn_start()) { driver_on(); st = ST_ACCEL; }
        break;
    case ST_ACCEL:
        if (overcurrent())      { driver_off(); st = ST_ERROR; }
        else if (speed_ok())    { st = ST_RUN; }
        break;
    case ST_RUN:
        if (overcurrent())      { driver_off(); st = ST_ERROR; }
        else if (btn_stop())    { set_target(0); st = ST_DECEL; }
        break;
    case ST_DECEL:
        if (speed_zero())       { driver_off(); st = ST_IDLE; }
        break;
    case ST_ERROR:
        if (btn_reset())        { clear_fault(); st = ST_IDLE; }
        break;
    default:
        driver_off(); st = ST_ERROR;         // towards the safe side if the value is corrupted
        break;
    }
}
RoadmapSee where this note sits in Data Structures & Algorithms→

Do not stop while waiting

Wait with delay_ms(500) and neither buttons nor communication are handled meanwhile. Record the time the state was entered and check the elapsed time on every pass.

case ST_WAIT:
    if ((uint32_t)(now_ms() - entered_ms) >= 500u) st = ST_NEXT;
    break;

With unsigned subtraction the difference is calculated correctly even when the tick counter wraps round.

Things to do once on entry

Work that should happen only at the moment the state changes (starting a timer, sending a message) is handled at the transition, or by remembering the previous state and running only when it differs.

if (st != prev) { on_enter(st); entered_ms = now_ms(); prev = st; }

Problems that come up in practice

  • Too many states: give independent concerns (communication link state and motor state) separate state machines. Merged into one, they multiply by their combinations.
  • Changing the state directly in an interrupt: the state changes while the main loop is in the middle of deciding. Interrupts only record events; transitions happen in one place.
  • No way out of the error state: for every state, check in the table "how do you leave here?".
  • Hard to debug: log the previous and next state on every change and most field problems are solved.
FREE PDFGet the 3 free ebooksIf you want to keep reading this topic as one book: BLDC motor control, serial communication and sensors at book length. PDFs are in Korean.Get them free →