Home Learning State Machines in Arduino Projects Explained (With Examples)

State Machines in Arduino Projects Explained (With Examples)

by shedboy71
[lebox id="1"]

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

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 enum for 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

 

 

Share
[lebox id="2"]

You may also like