As your Arduino projects grow beyond a few if statements, keeping your code maintainable becomes a real challenge. The Arduino finite state machine (FSM) design pattern is the single most effective technique for organising complex, event-driven embedded code. Whether you’re building a vending machine controller, a traffic light system, or a multi-mode robot, FSMs make your code predictable, testable, and easy to extend.
Table of Contents
- What Is a Finite State Machine?
- Why Use FSM on Arduino?
- Implementing a Basic FSM with switch-case
- FSM with Function Pointers
- Entry and Exit Actions
- Real-World Example: Vending Machine Controller
- Arduino FSM Libraries
- Frequently Asked Questions
What Is a Finite State Machine?
A Finite State Machine is a mathematical model of computation with four key components:
- States — A finite set of distinct conditions the system can be in. At any moment, the system is in exactly one state.
- Events (Inputs) — Triggers that can cause a state transition (button press, timer expiry, sensor threshold crossed).
- Transitions — Rules that define which state to move to when a specific event occurs in a specific state.
- Actions — Code that runs on entry to a state, during a state, or on exit from a state.
A classic example: a traffic light. It has three states (RED, YELLOW, GREEN), one event (timer expired), and transitions between them in a fixed sequence. The FSM ensures the light never jumps from RED directly to GREEN — the rules enforce valid sequences.
Why Use FSM on Arduino?
Before FSMs, most beginners write code that looks like this — nested if conditions that try to handle every combination of inputs and current state simultaneously:
// The "spaghetti code" approach (avoid this!)
void loop() {
if (buttonPressed && !motorRunning && !alarmActive) {
startMotor();
} else if (buttonPressed && motorRunning) {
stopMotor();
} else if (alarmActive && motorRunning) {
stopMotor();
soundAlarm();
} else if (alarmActive && !motorRunning) {
soundAlarm();
}
// ... 50 more conditions ...
}
This approach breaks down quickly because:
- Adding a new state requires touching every existing condition
- Testing all possible condition combinations is exponentially hard
- Timing bugs appear when the system briefly passes through invalid states
- Understanding the code’s intent becomes impossible
An FSM replaces this with a clear diagram and structured code where each state handles only its own concerns.
Implementing a Basic FSM with switch-case
The simplest Arduino FSM uses an enum for states and a switch statement in the loop to dispatch logic per state. Let’s model a simple door lock with a keypad:
// States
enum State {
STATE_LOCKED,
STATE_ENTERING_CODE,
STATE_UNLOCKED,
STATE_ALARM
};
// Events
enum Event {
EVT_NONE,
EVT_KEYPRESS,
EVT_CORRECT_CODE,
EVT_WRONG_CODE,
EVT_TIMEOUT,
EVT_LOCK_BUTTON
};
State currentState = STATE_LOCKED;
Event currentEvent = EVT_NONE;
unsigned long stateEnteredAt = 0;
void setState(State newState) {
currentState = newState;
stateEnteredAt = millis();
}
void loop() {
currentEvent = getEvent(); // Poll buttons/sensors for events
switch (currentState) {
case STATE_LOCKED:
// Display "Locked" on LCD
if (currentEvent == EVT_KEYPRESS) {
setState(STATE_ENTERING_CODE);
}
break;
case STATE_ENTERING_CODE:
// Collect keypad input
if (currentEvent == EVT_CORRECT_CODE) {
setState(STATE_UNLOCKED);
} else if (currentEvent == EVT_WRONG_CODE) {
setState(STATE_ALARM);
} else if (millis() - stateEnteredAt > 10000) {
// 10-second entry timeout
setState(STATE_LOCKED);
}
break;
case STATE_UNLOCKED:
// Activate solenoid, display "Open"
if (currentEvent == EVT_LOCK_BUTTON ||
millis() - stateEnteredAt > 5000) {
setState(STATE_LOCKED);
}
break;
case STATE_ALARM:
// Sound buzzer, flash LED
if (millis() - stateEnteredAt > 30000) {
setState(STATE_LOCKED); // Auto-reset after 30s
}
break;
}
}
Notice how each case in the switch statement only has to worry about what happens in that state. The code for STATE_UNLOCKED doesn’t need to know anything about STATE_ALARM. This separation is the core benefit of the FSM pattern.
FSM with Function Pointers
For more complex FSMs, the switch-case approach grows unwieldy. A table-driven FSM uses a 2D array of function pointers — one dimension for states, one for events — to look up and call the appropriate transition function directly:
#define NUM_STATES 4
#define NUM_EVENTS 6
// Forward declarations
void doNothing();
void startCodeEntry();
void unlockDoor();
void triggerAlarm();
void lockDoor();
void timeoutToLocked();
// Transition table: transitionTable[state][event] = function to call
typedef void (*TransitionFn)();
TransitionFn transitionTable[NUM_STATES][NUM_EVENTS] = {
// NONE KEYPRESS CORRECT WRONG TIMEOUT LOCK
/* LOCKED */ {doNothing, startCodeEntry, doNothing, doNothing, doNothing, doNothing},
/* ENTERING */ {doNothing, doNothing, unlockDoor, triggerAlarm, timeoutToLocked, doNothing},
/* UNLOCKED */ {doNothing, doNothing, doNothing, doNothing, lockDoor, lockDoor},
/* ALARM */ {doNothing, doNothing, doNothing, doNothing, lockDoor, doNothing},
};
void loop() {
Event event = getEvent();
// Call the transition function for the current state and event
transitionTable[currentState][event]();
}
This approach scales cleanly to dozens of states and events. Adding a new event means adding one column; adding a new state means adding one row. The logic remains contained in small, individually testable functions.
Entry and Exit Actions
Professional FSM implementations distinguish between three types of actions:
- Entry action: Runs once when entering a state (initialise a timer, turn on an LED, play a tone)
- Do/During action: Runs repeatedly while in the state (read sensors, update display)
- Exit action: Runs once when leaving a state (turn off LED, save data)
void enterUnlocked() {
digitalWrite(SOLENOID_PIN, HIGH); // Entry: open lock
lcd.print("OPEN");
stateEnteredAt = millis();
}
void duringUnlocked() {
// During: auto-lock after 5 seconds
if (millis() - stateEnteredAt > 5000) {
exitUnlocked();
enterLocked();
currentState = STATE_LOCKED;
}
}
void exitUnlocked() {
digitalWrite(SOLENOID_PIN, LOW); // Exit: close lock
}
Real-World Example: Vending Machine Controller
Here’s a simplified vending machine FSM that demonstrates a complete, practical implementation:
enum VendState {
VEND_IDLE,
VEND_COIN_INSERTED,
VEND_SELECTION_MADE,
VEND_DISPENSING,
VEND_RETURNING_CHANGE
};
VendState vendState = VEND_IDLE;
int balance = 0;
int itemCost = 0;
unsigned long dispenseStarted = 0;
void loop() {
switch (vendState) {
case VEND_IDLE:
showIdleScreen();
if (coinDetected()) {
balance += readCoinValue();
vendState = VEND_COIN_INSERTED;
}
break;
case VEND_COIN_INSERTED:
displayBalance(balance);
if (itemSelected()) {
itemCost = getItemCost(selectedItem());
if (balance >= itemCost) {
vendState = VEND_SELECTION_MADE;
} else {
displayInsufficient();
}
}
if (refundPressed()) {
vendState = VEND_RETURNING_CHANGE;
}
break;
case VEND_SELECTION_MADE:
balance -= itemCost;
activateMotor();
dispenseStarted = millis();
vendState = VEND_DISPENSING;
break;
case VEND_DISPENSING:
if (millis() - dispenseStarted > 2000) {
stopMotor();
if (balance > 0) {
vendState = VEND_RETURNING_CHANGE;
} else {
vendState = VEND_IDLE;
}
}
break;
case VEND_RETURNING_CHANGE:
returnCoins(balance);
balance = 0;
vendState = VEND_IDLE;
break;
}
}
Every state has a clear purpose, every transition is explicit, and the code reads almost like the design document itself.
Arduino FSM Libraries
Several libraries abstract the FSM boilerplate so you can focus on your state logic:
- Arduino-fsm — Simple, event-driven FSM with on-enter/on-state/on-exit callbacks. Install from Library Manager.
- StateMachine — Supports hierarchical state machines (a state within a state) for very complex systems.
- Automaton — Full reactive state machine framework with built-in debouncing, timers, and event queuing.
Example using the Arduino-fsm library:
#include <Fsm.h>
State stateLocked(&onLockEnter, &onLockDuring, NULL);
State stateUnlocked(&onUnlockEnter, &onUnlockDuring, &onUnlockExit);
Fsm fsm(&stateLocked);
#define EVT_CORRECT 1
#define EVT_TIMEOUT 2
void setup() {
fsm.add_transition(&stateLocked, &stateUnlocked, EVT_CORRECT, NULL);
fsm.add_timed_transition(&stateUnlocked, &stateLocked, 5000, NULL);
}
void loop() {
if (codeCorrect()) fsm.trigger(EVT_CORRECT);
fsm.run_machine();
}
Frequently Asked Questions
How many states can an Arduino FSM have?
There’s no hard limit from the FSM pattern itself — the practical limit is your Arduino’s memory. An enum state + small struct per state is just a few bytes. A 100-state machine on an Arduino Uno is feasible as long as the code for each state is kept compact. The Mega is better for large FSMs.
Can I use FSM with interrupts?
Yes. The common pattern is to have the ISR set a volatile event flag, and the main loop polls that flag and feeds it into the FSM as an event. Never trigger FSM transitions directly inside an ISR — ISRs must be short and interrupt-safe.
What’s the difference between FSM and a hierarchical state machine (HSM)?
An HSM (Hierarchical State Machine) allows states to be nested inside other states. A sub-state inherits default transitions from its parent. This avoids repeating common transitions in every state of a group. The StateSmith and QP frameworks implement HSMs for embedded systems.
Is an FSM suitable for real-time control loops?
FSMs are excellent for mode management and sequencing but aren’t typically used as the inner real-time control loop. A common architecture: outer FSM manages modes (IDLE, RUNNING, ERROR), and within the RUNNING state, a fast control loop (PID, stepper driver, etc.) executes independently.
How do I visualise my FSM during development?
Draw a state diagram before writing any code. Free tools: draw.io, Lucidchart, or even paper. Add state transition debug output: Serial.print("→ STATE_UNLOCKED") every time you change state. Some teams auto-generate state diagrams from code using PlantUML.
Build smarter Arduino projects — Browse our full selection of Arduino boards and development tools at Zbotic. From beginner kits to advanced Mega boards, we have everything for your next FSM-powered project.
Add comment