Tutorial · Beginner · 17 min read

Finite-State Machines for Robot Behavior

Design a finite-state machine for robot behavior with explicit transitions, timeouts, recovery states, non-blocking updates, and testable safety rules.

Published

Robot programs often begin as a loop full of if statements: follow the line unless an obstacle is close, turn until the path clears, stop when the battery is low, and somehow recover when a sensor times out. As behaviors grow, the same condition can trigger contradictory commands.

A finite-state machine (FSM) makes the active behavior explicit. At any instant the robot is in one state; events and guards determine when it transitions to another. The pattern is small enough for an Arduino and structured enough to test.

Separate state, event, and action

A state describes what the robot is doing over time: FOLLOW_LINE, AVOID, SEARCH, or FAULT. An event is something observed: an obstacle enters the stop zone, the line is lost, a timeout expires, or the operator presses stop. An action is the output: set wheel speeds, clear an integrator, or raise an alert.

Avoid creating states for instantaneous facts. OBSTACLE_DETECTED is usually an event or guard; AVOIDING_OBSTACLE is a state because it persists while the robot executes a behavior.

Start with a transition table before code:

Current state Guard or event Next state Entry action
Idle Start requested and sensors valid Follow Reset controller
Follow Obstacle inside stop threshold Avoid Stop forward drive
Follow Line missing for 200 ms Search Remember last side
Avoid Path clear for 500 ms Follow Reset controller
Any active state Sensor timeout Fault Stop motors
Fault Operator reset and health valid Idle Clear fault

The table exposes missing cases and makes safety priority visible.

Keep the update loop non-blocking

A state should perform a small update and return. A two-second delay() inside AVOID prevents the program from seeing a stop request or sensor failure during those two seconds.

enum class State { Idle, Follow, Avoid, Search, Fault };

State state = State::Idle;
unsigned long enteredAt = 0;

void transitionTo(State next) {
  state = next;
  enteredAt = millis();
}

void loop() {
  Sensors s = readSensors();

  if (!s.healthy && state != State::Fault) transitionTo(State::Fault);

  switch (state) {
    case State::Idle:   updateIdle(s); break;
    case State::Follow: updateFollow(s); break;
    case State::Avoid:  updateAvoid(s); break;
    case State::Search: updateSearch(s); break;
    case State::Fault:  updateFault(s); break;
  }
}

Each update reads the already captured sensor snapshot and writes one coherent actuator command. This avoids different states reading changing values at different times inside one cycle.

Put transitions at clear boundaries

Choose one ownership rule. A simple embedded design lets each state request its outgoing transitions, while a top-level safety check can override any active state.

void updateFollow(const Sensors& s) {
  if (s.obstacleMm < stopThresholdMm) {
    setWheels(0, 0);
    transitionTo(State::Avoid);
    return;
  }

  if (s.lineMissingForMs > 200) {
    transitionTo(State::Search);
    return;
  }

  followLine(s.lineError);
}

Return immediately after a transition so the old state cannot also command the actuators. On entry to a control state, reset history that should not leak across behaviors, such as a PID integral or derivative sample.

Use exit actions sparingly. Most cleanup can happen in the centralized transitionTo() function or the next state’s entry path. Separate onEntry, onUpdate, and onExit functions become useful only when several states genuinely need them.

Use timeouts and hysteresis as guards

Physical events are noisy. If AVOID exits the instant distance rises above the same threshold that entered it, measurement noise can cause rapid state chatter. Use two thresholds or a persistence time: enter below 250 mm, but leave only after distance stays above 350 mm for 500 ms.

Every state that expects progress needs a timeout. If the robot enters SEARCH but never finds the line, it should not spin forever. Transition to FAULT or a bounded recovery after a tested interval.

Base timers on time since entry or time since last progress. Use subtraction such as now - enteredAt >= timeout, which remains correct across unsigned timer rollover when types are consistent.

Give safety transitions priority

Define what can interrupt everything: emergency stop, invalid critical sensor data, motor-driver fault, or a watchdog timeout. Evaluate these conditions before behavior-specific transitions and make the safe output explicit.

A FAULT state should latch when automatic recovery could restart dangerous motion. Requiring an operator reset after the cause clears is often safer than jumping directly back into FOLLOW.

Do not let two independent state machines write the same motor commands without an arbitration rule. If navigation and battery management both affect motion, one owner should select the final command and apply safety limits.

Test transitions without running motors

Move decisions into functions that accept a sensor snapshot and time, then return a next state or command. Feed a sequence of synthetic snapshots:

  1. Valid line and clear path stays in FOLLOW.
  2. One noisy close sample does not trigger if persistence is required.
  3. A sustained close obstacle enters AVOID and commands stop first.
  4. A clear path held for the exit interval returns to FOLLOW.
  5. A sensor timeout reaches FAULT from every active state.

Log state changes as old → new, the triggering event, and timestamp. Continuous per-loop logs hide transitions in noise; transition logs explain why behavior changed.

Know when an FSM stops being the simplest model

An FSM works well when the robot has a modest number of mutually exclusive modes. It becomes difficult when states multiply to represent combinations such as “navigate while monitoring battery while retrying localization.” The transition count can grow faster than the behaviors themselves.

Behavior trees compose reusable actions with sequence, fallback, and recovery structure, which is why navigation systems such as Nav2 use them for complex tasks. Do not introduce a behavior-tree framework for four states. Move when the transition table becomes genuinely hard to review or behaviors need hierarchical reuse.

Diagnose common design failures

  • The robot ignores stop while turning: a state contains a blocking loop or delay.
  • Motors receive two commands per cycle: transition code continues executing after changing state.
  • The machine chatters: noisy guards lack hysteresis or persistence.
  • Recovery never ends: the state has no progress condition or timeout.
  • A fault instantly restarts motion: safety faults are not latched or reset conditions are too weak.
  • Testing requires the whole robot: decisions and hardware access are mixed instead of using sensor snapshots and command outputs.

The Obstacle Avoidance Simulator provides a useful behavior sequence to model: cruise, steer around a threat, arrive, or stop when no safe motion remains. The Maze Solver Simulator shows another separation between sensing, map update, planning, and one-cell motion.

Further reading

References