As Arduino projects grow beyond simple sketches, code based on delay() statements and deeply nested if conditions quickly becomes hard to read, hard to debug, and unreliable. This is where state machines become essential.
A state machine is a programming model where your system:
- Exists in one state at a time
- Reacts to events or conditions
- Transitions cleanly between states
State machines are fundamental in embedded systems, industrial controllers, robotics, and consumer electronics. Arduino does not provide a built-in state machine framework, but its structure (setup() and loop()) is perfectly suited to implementing them.
This tutorial explains:
- What state machines are (conceptually and practically)
- Why they matter in Arduino projects
- Several implementation patterns
- Many code examples, from simple to advanced
- Common mistakes and best practices
What Is a State Machine?
A state machine consists of:
- States: distinct modes of operation
- Events/conditions: triggers for change
- Transitions: rules for moving between states
- Actions: behavior executed in each state
Simple Example
IDLE → BUTTON_PRESSED → RUNNING → DONE → IDLE
At any moment, the system is in exactly one state.
Why Use State Machines?
Problems with Delay-Based Code
digitalWrite(ledPin, HIGH);
delay(5000);
digitalWrite(ledPin, LOW);
delay(5000);
Problems:
- Blocks the CPU
- Cannot respond to inputs during delay
- Does not scale
Problems with Nested Logic
if (conditionA) {
if (conditionB) {
if (conditionC) {
// logic explosion
}
}
}
State machines:
- Eliminate blocking delays
- Improve readability
- Make behavior predictable
- Scale cleanly
Simple State Machine
Using an Enum for States
enum State {
IDLE,
RUNNING,
STOPPED
};
State currentState = IDLE;
Enums make code readable and safe.
Basic State Machine Structure
void loop() {
switch (currentState) {
case IDLE:
// behavior
break;
case RUNNING:
// behavior
break;
case STOPPED:
// behavior
break;
}
}
Each case represents one state.
Example 1: LED Controller State Machine
Requirements
- Button starts LED blinking
- Button stops blinking
- No delays
Code
const int ledPin = 13;
const int buttonPin = 2;
enum State {
OFF,
BLINK_ON,
BLINK_OFF
};
State state = OFF;
unsigned long lastChange = 0;
const unsigned long interval = 500;
void setup() {
pinMode(ledPin, OUTPUT);
pinMode(buttonPin, INPUT_PULLUP);
}
void loop() {
unsigned long now = millis();
switch (state) {
case OFF:
digitalWrite(ledPin, LOW);
if (digitalRead(buttonPin) == LOW) {
state = BLINK_ON;
lastChange = now;
}
break;
case BLINK_ON:
digitalWrite(ledPin, HIGH);
if (now - lastChange >= interval) {
state = BLINK_OFF;
lastChange = now;
}
break;
case BLINK_OFF:
digitalWrite(ledPin, LOW);
if (now - lastChange >= interval) {
state = BLINK_ON;
lastChange = now;
}
if (digitalRead(buttonPin) == LOW) {
state = OFF;
}
break;
}
}
Key Takeaways
- No
delay() - Immediate button response
- Time-based transitions using
millis()
State Machines and Events
Event-Driven Thinking
Instead of:
do everything every loop
Think:
IF event occurs → change state
Common Arduino events:
- Button pressed
- Timer expired
- Sensor threshold crossed
- Communication received
Example 2: Traffic Light State Machine
States
- RED
- GREEN
- YELLOW
Code
enum LightState {
RED,
GREEN,
YELLOW
};
LightState light = RED;
unsigned long lastTime = 0;
void setup() {
pinMode(10, OUTPUT); // red
pinMode(11, OUTPUT); // yellow
pinMode(12, OUTPUT); // green
}
void loop() {
unsigned long now = millis();
switch (light) {
case RED:
digitalWrite(10, HIGH);
digitalWrite(11, LOW);
digitalWrite(12, LOW);
if (now - lastTime >= 5000) {
light = GREEN;
lastTime = now;
}
break;
case GREEN:
digitalWrite(10, LOW);
digitalWrite(12, HIGH);
if (now - lastTime >= 5000) {
light = YELLOW;
lastTime = now;
}
break;
case YELLOW:
digitalWrite(12, LOW);
digitalWrite(11, HIGH);
if (now - lastTime >= 2000) {
light = RED;
lastTime = now;
}
break;
}
}
Finite vs Infinite State Machines
Finite State Machine (FSM)
- Known, limited number of states
- Most Arduino projects use FSMs
Infinite or Dynamic States
- Generated at runtime
- Rare and usually unnecessary on Arduino
Stick to finite state machines for clarity.
Example 3: Sensor-Based State Transitions
Use Case
- Idle until temperature rises
- Activate cooling
- Return to idle
enum SystemState {
IDLE,
COOLING
};
SystemState systemState = IDLE;
const int tempPin = A0;
void loop() {
int temp = analogRead(tempPin);
switch (systemState) {
case IDLE:
if (temp > 600) {
systemState = COOLING;
}
break;
case COOLING:
digitalWrite(9, HIGH);
if (temp < 500) {
digitalWrite(9, LOW);
systemState = IDLE;
}
break;
}
}
Hierarchical State Machines (Advanced Pattern)
Instead of many states, use sub-states.
enum MainState { SYSTEM_OFF, SYSTEM_ON };
enum SubState { WAITING, ACTIVE };
MainState mainState = SYSTEM_ON;
SubState subState = WAITING;
This avoids state explosion in complex systems.
State Entry and Exit Actions
Sometimes you want code to run once when entering a state.
Pattern
State previousState;
if (currentState != previousState) {
// entry action
previousState = currentState;
}
Example
if (state != lastState) {
Serial.print("Entering state: ");
Serial.println(state);
lastState = state;
}
Common State Machine Mistakes
Mistake 1: Too Much Logic in One State
Fix: break into smaller states.
Mistake 2: Forgetting Default Case
Always include:
default:
state = SAFE_STATE;
break;
Mistake 3: Mixing Delays with State Machines
Avoid:
delay(1000);
Use timers instead.
Debugging State Machines
Print State Transitions
void printState(State s) {
Serial.println((int)s);
}
LED Indicators per State
Use LEDs to visually confirm behavior.
When NOT to Use a State Machine
- Very small, one-shot sketches
- Single blocking operation with no inputs
- Disposable test code
Otherwise, use one.
State Machine vs Scheduler
- State machine: controls behavior flow
- Scheduler: controls task timing
They often work together, not against each other.
Best Practices Summary
- Use
enumfor states - One responsibility per state
- Use
millis()for timing - Avoid blocking code
- Log transitions during development
- Reset to safe state on errors
Final Thoughts
State machines are not an “advanced trick” — they are a core embedded systems skill. Once you start using them in Arduino projects, your code becomes:
- Easier to reason about
- More responsive
- More reliable
- Much easier to expand

