Learn

State machines on Arduino, with a game you can play

Sign up freeor see every lesson
  • 192parts on the bench
  • 25boards running now
  • 1.00xreal time, on every board
Arduino Uno: a four-state reaction gamelive0.000 s 0.00x
Hold the button
READY, ARMED, RUNNING, DONE: press the button to start, then again when the yellow lamp lights.

Sooner or later every sketch has to do things in order while still reacting to the world: wait for a button, then wait a random time, then light a lamp, then time how long you take to press again. Written with delay() and nested ifs, that turns into a program that is hard to follow and deaf to input half the time. A state machine is the standard way out, and it needs nothing more than an enum and one variable.

The circuit above is a reaction game on an Uno with a button on pin 2 and three lamps. The program has four states: READY (the red lamp on pin 13 is lit, waiting for you to press), ARMED (all dark, waiting a random 0.7 to 2 seconds), RUNNING (the yellow lamp on pin 12 is lit, the clock is running) and DONE (the green lamp on pin 11 shows the round is over). Press Run, press the button once, and wait for yellow.

Want the step-by-step version? The lesson "One variable that says where you are" walks through this with checkpoints.

Open the lesson

What the serial monitor tells you

The sketch prints every change of state, so the serial monitor is a log of the machine. One round we played read: "state: READY", "state: ARMED", "state: RUNNING", "reaction: 861 ms", "state: READY". Press during ARMED instead and you get "too early", straight to DONE, and back to READY 1.2 seconds later.

That log is the first reason to write code this way. When something goes wrong you know exactly which state the program was in and what moved it on, instead of guessing which of five nested ifs was true.

The shape of the code

Name the states with an enum, keep the current one in a variable, and give each state one block that says what it does and what moves it on. A helper, enter(), changes the state and notes the time, so every state knows how long it has been active without keeping its own timer.

Look at what is not there. There is no delay() anywhere. Every pass through loop() reads the button, checks the state, and returns within microseconds, so the button is read thousands of times a second in every state. Waiting is done by comparing millis() with the time the state was entered, the same pattern the blink without delay page uses for two LEDs.

firmware/uno/examples/cppstate (the machine, condensed from the program running above)
enum State { READY, ARMED, RUNNING, DONE };
State state = READY;
unsigned long enteredAt = 0;

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

// in loop(), after reading the button into justPressed:
if (state == READY) {
  if (justPressed) {
    waitMs = random(700, 2000);
    enter(ARMED);
  }
} else if (state == ARMED) {
  if (justPressed) {
    enter(DONE);                       // too early
  } else if (now - enteredAt >= waitMs) {
    enter(RUNNING);
  }
} else if (state == RUNNING) {
  if (justPressed) {
    Serial.println(now - enteredAt);   // the reaction time
    enter(DONE);
  }
} else {                               // DONE
  if (now - enteredAt >= 1200) enter(READY);
}

Events, not levels

The machine reacts to justPressed, which is true for exactly one pass through loop() when the button goes from up to down. If it reacted to "the button is down" instead, a single press held for a quarter of a second would be seen hundreds of times and walk the machine straight through READY, ARMED and into a false start. Detecting the edge, with a 25 millisecond debounce so contact bounce does not count as extra presses, is what makes one press one event.

This is the most common state machine bug on a real bench: a transition driven by a level where it needed an edge. If a state seems to be skipped, check what moves the machine on.

Draw it before you write it

Before writing a state machine, draw it: one circle per state and one arrow per transition, with the event that causes it written on the arrow. For this game that is four circles and five arrows: READY to ARMED on a press, ARMED to RUNNING when the random wait runs out, ARMED to DONE on a press (too early), RUNNING to DONE on a press, and DONE to READY after 1.2 seconds. If you cannot draw an arrow for something the program has to do, the design is missing a state or an event, and it is far cheaper to find that on paper.

The drawing also becomes your test list. Every arrow is a case to try once. In the simulator you can walk all five in under a minute: press once and wait, press twice quickly, press once and then again when yellow lights. Watching the serial log print each transition as you do it is the whole test. Two mistakes that show up this way are forgetting to call enter() (so enteredAt is stale and a timed state ends at once) and a state with no way out.

Compare it with a sketch that blocks

The Traffic light example in the gallery runs a pelican crossing the other way: a straight sequence of lights with delay() between them. It works, but only because the request button is on an interrupt; without that, a press during a delay() would be lost. Open it with the button below and read the two sketches side by side.

Rewriting the crossing as a state machine (GREEN, AMBER, WALK, FLASHING) removes the need for the interrupt, and adding a new rule, say "hold green for at least three seconds", becomes one extra condition in one block instead of a restructure of the whole loop.

Questions

Should I use switch or if and else if?

Either works. A switch on the state variable is common and makes the compiler warn you about an enum value you forgot to handle, as long as you remember a break at the end of every case. The example uses if and else if because it reads well in a short sketch.

How do I add a new state?

Add a name to the enum, add a block for it, and add the transitions into and out of it. Nothing else in the program needs to change, which is the point of the pattern.

Can a state machine run alongside other code?

Yes, as long as nothing in loop() blocks. Several machines can run side by side, each with its own state variable, because each one only does a little work per pass.

Where do I do something once when a state starts?

In the transition, just before or after calling enter(). The example picks the random wait at the moment it enters ARMED, so the wait is chosen once per round rather than on every pass.

Build this for real

Open the editor, change a value and watch the number move with it. Nothing to install, and no account needed.