Bench note

State machines that survive the prototype

Why state machines at all

You can write firmware as a pile of if-statements and flags. It works until you add a third sensor, a timeout, or an interrupt that fires during another interrupt. Then you're debugging race conditions at 3 a.m. because motor_running and last_button_press disagree about what the system is doing.

A finite state machine (FSM) gives you one source of truth: a single variable that says "we are in state IDLE" or "we are in state RUNNING." Transitions are explicit. You can log them. You can draw them. You can reason about them without a debugger.

This isn't theory. It's how you ship a robot that doesn't freeze when someone presses two buttons at once.

Structure that scales

Start with an enum for states and a switch statement in your main loop. Each case handles exactly one state. Transitions happen only at the end of a case, and they're always logged:

typedef enum {
    STATE_IDLE,
    STATE_HOMING,
    STATE_RUNNING,
    STATE_ERROR
} system_state_t;

system_state_t current_state = STATE_IDLE;

void state_machine_tick(void) {
    switch (current_state) {
        case STATE_IDLE:
            if (button_pressed()) {
                log_transition("IDLE -> HOMING");
                current_state = STATE_HOMING;
                start_homing_sequence();
            }
            break;
        case STATE_HOMING:
            if (limit_switch_triggered()) {
                log_transition("HOMING -> RUNNING");
                current_state = STATE_RUNNING;
                zero_position();
            }
            break;
        // ...
    }
}

No hidden state. No "well, usually this flag means we're done homing." The state variable is the contract.

When to transition

Transitions belong at the end of work, not the beginning. If you're reading a sensor in STATE_HOMING, don't immediately jump to STATE_RUNNING when the limit switch trips. Finish the current iteration—store the reading, update your position estimate—then transition. Otherwise you'll leave half-finished work in registers or buffers, and your next state will inherit garbage.

This pairs well with interrupt-driven sensor reads. The ISR sets a flag; the state machine checks that flag and decides whether to transition. The ISR doesn't change state directly. Keep decision-making in one place.

Logging every move

Log every transition with a timestamp. Not "when you remember." Every single one. This costs you a UART call and maybe 40 bytes of flash per transition. In return, you get a complete record of what your firmware was thinking when it locked up or skipped a step.

void log_transition(const char* msg) {
    uart_printf("[%lu] %s\n", millis(), msg);
}

When you're tuning a closed-loop controller and the motor stutters once every fifty cycles, that log will show you the state machine briefly entered STATE_ERROR because a sensor read timed out. Without the log, you're guessing.

States for errors, not flags

Don't use a boolean error_flag alongside your state machine. Make STATE_ERROR a real state with its own case. Define what the system does in that state: does it retry? Does it wait for a reset command? Does it disable the motor and log the fault?

Explicit error states force you to decide recovery behavior up front, not during a panicked debug session. They also show up in your logs, so you know when the system gave up, not just that it eventually did.

Draw it once

Before you write the code, draw the state diagram on paper. Circles for states, arrows for transitions, labels for conditions. If you can't draw it cleanly, your logic is too tangled to code. The diagram is also documentation you'll reference in six months when you add a new feature and need to remember why STATE_HOMING can't transition directly to STATE_IDLE.

This is the same discipline that makes versioned hardware survive revisions. Write down the design before you solder it.

Not everything needs one

If your firmware blinks an LED on a timer, you don't need a state machine. If it coordinates three sensors, two motors, and a timeout, you do. The threshold is "can I still reason about all possible sequences of events without a diagram?" If no, FSM. If yes, maybe wait one feature.

State machines are structure you impose when complexity arrives. Impose it early, and the complexity stays manageable. Wait too long, and you're refactoring a mess into a state machine while the hardware is already screwed to the enclosure.

← Full log