Yet another Finite State Machine introduction

Hello everyone,

On this gray day, considering the number of questions that boil down to programming a finite automaton (or a state machine), I thought I would translate my small tutorial I also have in French to explain how to approach this type of code in a structured manner. Of course, there are libraries that do this more or less for you, but understanding how it works can often allow you to do without libraries that might weigh down your code.

The general idea is to write a program that controls a "system" that must react by triggering "actions" that modify the system, for example, based on "events" that occur, and the reaction may depend on the current state of your system.

Here's a real-life example to better understand:
Take a light bulb connected to a push-button switch. You press the button, what should happen? It's simple: if the bulb was off, then it should turn on (action = power the bulb), and if it was on, then it should turn off (action = cut off power to the bulb).

So, we introduce the concept of the system's state here.
We will have two states: lightOff and lightOn, which we will depict like this


and there is only one possible event, which is clicking the button.

When an event is detected, it triggers an action that may lead to a state change. In this scenario, if the system is in the "off" state and detects a button click, it will transition to the "on" state. During this transition, a specific action is taken โ€“ turning the light on. Similarly, when the system is in the "on" state, a click event will lead to a transition to the "off" state, accompanied by the action of turning the light off.

Therefore, we can represent our system as follows:

Very often, we encounter such systems in timers. We can thus introduce a new type of event related to the time spent in a particular state. In the case of a timer, if the system is in the "on" state and the set time has elapsed, it is necessary to turn off the light. Here, we observe an event (timeout), an action (turning off the light), and a state transition (moving from "on" to "off").

Therefore, we can represent our system with an additional transition as follows:

Since the purpose of this tutorial is not to delve into the theory of finite automata, I'll let you explore Wikipedia for more information and will focus on examples that are commonly encountered when working with Arduinos.

To make this type of coding easier, you need to understand a few elements of the C++ language.


A practical tool for the state machine programmer: enumerations

In C++, an enumeration is a type, named or not, that groups in a list named constants

There is an associated value for each element in the list which will be distinct from the others โ€” by default the first will be 0, the second 1, the third 2, etc. For example, you can write the following code:

enum {monday, tuesday, wednesday, thursday, friday, saturday, sunday} day;

This declares a variable day that can take on one of the values from the list (monday, tuesday, wednesday, thursday, friday, saturday, sunday).

click if you want to read more about enums

So, if you run the following code on an Arduino:

enum {monday, tuesday, wednesday, thursday, friday, saturday, sunday} day;

void setup() {
  Serial.begin(115200);
  day = thursday;
  Serial.println(day);
}

void loop() {}

You will see "3" in the Serial console (set at 115200 baud).

If you want to assign specific values to certain elements in the list, it's possible. The default numbering starts at 0 and increments by 1, unless the program explicitly assigns a value to enumerated constants (in which case it takes that value, and the following ones increment by one compared to the preceding one in the declaration order). For example:

enum {monday=1, tuesday, wednesday, thursday, friday, saturday, sunday} day;

This declares that Monday is 1, so Tuesday is 2, and so on. If you use this definition in the above code, instead of printing "3," you will see "4." (You could also say:

enum {monday, tuesday, wednesday, thursday=12, friday, saturday, sunday} day;

In this case, Monday would be 0, Tuesday 1, Wednesday 2, Thursday 12, Friday 13 (lucky number), Saturday 14, etc.)

Note that since C++11 (available in the latest versions of the IDE), it is possible to define the underlying type for enumeration variables. By default, it is an integer (type int) that takes up two bytes of memory on an UNO. If you have only a few states (less than 255) and their values don't matter (below 255), you can add the byte type to the declaration and save one byte. You might also want to use unsigned long, for example, if you need large predefined values like reading an IR remote code.

In this code, the variable day will be an int on two bytes:

enum {monday, tuesday, wednesday, thursday, friday, saturday, sunday} day;

And in this code, the variable day will be a byte, so on a single byte:

enum :byte {monday, tuesday, wednesday, thursday, friday, saturday, sunday} day;

Why am I mentioning this? Because an enum is very convenient for listing the states of our system so that the programmer can easily understand it.

In the above example of the timer, we saw that we had two states. Thus, we could declare:

enum {lightOff, lightOn} currentState;

This way, we define a variable currentState that can take on the value of lightOff or lightOn.


Another practical tool for the state machine programmer: the switch/case statement.

I'll let you read the programming documentation on the switch/case statement. Its interest lies in the fact that often in our state machines, we need to say "if the current state is this, then do this; otherwise, if the current state is that, then do something else, etc."

If you have many possible states, all these nested tests make the code difficult to read, and the switch/case statement simplifies all of that. By cleverly combining this with our enum, for example, we can write:

enum {lightOff, lightOn} currentState;
...

switch (currentState) {
  case lightOff:
    // do something

    break;

  case lightOn:
    // do something else

    break;
}

Let's look at a Practical Implementation:

Let's build a case somewhat similar to that of the timer but a bit more complex to have many states to manage.

Step 1: Set up your breadboard and connect the Arduino.

You will need:

  • 4 LEDs of different colors (red, orange, yellow, green)
  • 4 resistors of 200ฮฉ or 220ฮฉ for example (suitable for current limiting with your LEDs)
  • A momentary button
  • An Arduino UNO or similar
  • Wires to connect everything

Here is the setup:

Connect:

  • Pin 4 โžœ Button โžœ GND (wire in diagonal across the button ensures the correct pin connections)
  • Pin 8 โžœ 220 ฮฉ โžœ (anode) Red LED (cathode) โžœ GND
  • Pin 9โžœ 220 ฮฉ โžœ (anode) Orange LED (cathode) โžœ GND
  • Pin 10โžœ 220 ฮฉ โžœ (anode) Yellow LED (cathode) โžœ GND
  • Pin 11โžœ 220 ฮฉ โžœ (anode) Green LED (cathode) โžœ GND

Now, starting from here, we will perform a couple exercises


Exercise #1

In this exercise, we want to start with all lights off, and the button should sequentially turn on the LEDs,

  • First press: the green LED lights up.
  • Second press: the green LED stays on, and the yellow LED lights up.
  • Third press: the orange LED lights up in addition.
  • Fourth press: the red LED lights up in addition.
  • Fifth press: everything turns off.

This strongly resembles a state machine, which we could describe as follows:

The states:

  • All off (REST)
  • Green LED on (G)
  • Green and Yellow LEDs on (GY)
  • Green, Yellow, and Orange LEDs on (GYO)
  • Green, Yellow, Orange, and Red LEDs on (GYOR)

Initial state = REST

Possible action = click on the button

And here is the diagram of possible transitions:

so how are we going to code this ?


In order to focus on the core topic, I will use a button library. There are many available and I like Toggle from @dlloyd. He introduced his library here in the forum and his GitHub offers extra information (and source code).

The library is pretty simple to use. You define a Toggle instance, in the setup you call begin passing the pin number and in the loop you poll the button to update its state and you have simple functions you can call to detect if an event happen. The most useful one for us is onPress() which returns true once when the button has been pressed. The library manages bouncing for us.


So back to our code.

We need to declare all the pins used for the LEDs, instantiate the button, and code the state machine using a union for the different states names, we will have a neat switch/case, as mentioned above.

Here is a Wokwi for testing

click to see the code
// Button management library
#include <Toggle.h> //  https://github.com/Dlloydev/Toggle
const byte buttonPin = 4; // Our button is on pin 4
Toggle button;

// Pins used for LEDs
const byte pinRedLed = 8;
const byte pinOrangeLed = 9;
const byte pinYellowLed = 10;
const byte pinGreenLed = 11;

// The list of possible states of our system
// along with a currentState variable taking one of these values
enum {REST, STATE_G, STATE_GY, STATE_GYO, STATE_GYOR} currentState;

void turnLedsOff() {
  digitalWrite(pinGreenLed, LOW);
  digitalWrite(pinYellowLed, LOW);
  digitalWrite(pinOrangeLed, LOW);
  digitalWrite(pinRedLed, LOW);
}

void runStateMachine() {
  button.poll(); // update the state of the button

  switch (currentState) {
    case REST:                            // we are at rest and if we get a click, turn on the green LED
      if (button.onPress()) {             // this is our event detection
        digitalWrite(pinGreenLed, HIGH);  // execute the associated action. Green LED powered
        currentState = STATE_G;           // note the new state of our system
      }
      break;

    case STATE_G:                         // green LED was on and if we get a click, turn on the yellow LED
      if (button.onPress()) {             // this is our event detection
        digitalWrite(pinYellowLed, HIGH); // execute the associated action. Yellow LED powered
        currentState = STATE_GY;          // note the new state of our system
      }
      break;

    case STATE_GY:                        // green and yellow were on, if we get a click, turn on the orange LED
      if (button.onPress()) {             // this is our event detection
        digitalWrite(pinOrangeLed, HIGH); // execute the associated action. Orange LED powered
        currentState = STATE_GYO;         // note the new state of our system
      }
      break;

    case STATE_GYO:                       // green, orange, and yellow were on, if we get a click, turn on the red LED
      if (button.onPress()) {             // this is our event detection
        digitalWrite(pinRedLed, HIGH);    // execute the associated action. Red LED powered
        currentState = STATE_GYOR;        // note the new state of our system
      }
      break;

    case STATE_GYOR:                      // everything was on, if we get a click a click, return to rest
      if (button.onPress()) {             // this is our event detection
        turnLedsOff();                    // execute the associated action. return to the initial state
        currentState = REST;
      }
      break;
  }
}

// ------------------------------------------------------
// We initialize our system in the setup
// ------------------------------------------------------
void setup() {
  // configure
  pinMode(pinRedLed, OUTPUT);
  pinMode(pinOrangeLed, OUTPUT);
  pinMode(pinYellowLed, OUTPUT);
  pinMode(pinGreenLed, OUTPUT);

  button.begin(buttonPin);

  // define Initial conditions
  turnLedsOff();
  currentState = REST;

}

void loop() {
  // we update ou system based on possible events
  runStateMachine();

  // Here, we can do other things as long as it doesn't take too long and is non blocking
}

All the intelligence of the machine is in the runStateMachine() function, which is straightforward to read thanks to the switch/case and the use of easily readable state codes as declared in the enum and the convenient Toggle library.

In simple terms, within the switch/case, for each state we check if the button has been pressed and if it is we know that we need to move to the next state. By looking at the diagram, we know what action to take and what the next state is. So, it's just a matter of coding that. It's quite simple! :blush:


Exercise #2

In this exercise, we are asked to ensure we don't waste energy. We should not keep the light on for too long, so we are required to add a timer. The specification says: "If the light is on for more than 15 seconds without any user action, then turn everything off."

Now that we are experienced, we immediately see that this involves a new type of event that we will need to consider in our state machine: the passage of time.

Our machine is therefore getting a little more complicated. We have a new event to consider, the "timeout" event which will generate new transitions: a transition from all states except "all off" to the "all off" state.

On a diagram, the new transitions look like this:

(Of course, they are added to the existing transitions.)

How are we going to handle this?

Certainly, we cannot use delay(15000) in our code; otherwise, the buttons would no longer be operational. We must not block the code.

We won't reinvent the wheel for this; we'll use a classic technique.

You have all read the tutorial (if not, you should) "Blink Without Delay," which is one of the standard examples for time management.

For extra information and examples look at

Once you grasp this concept, we will apply it.


So, we will need a variable chrono that will store the "time" of the last user action and a constant for how long we need to wait before registering a timeout event

// Introducing time as an additional event
unsigned long chrono; // Note: unsigned long, like millis()
const unsigned long TimeOut = 15000ul; // 15 seconds (ul at the end for unsigned long, a good habit to adopt)

We need to rearm our timer every time the user presses a button, since the specifications say 15 seconds after the last action. Therefore, we will add a line of code in our state machine function to reset our "stopwatch":

chrono = millis(); // We just had an action, so we reset our stopwatch

last we should we test for time out.

We could add this test into all states (in then switch) this would be done by adding this test to all. states except REST

      else if (millis() - chrono >= TimeOut) { // timeout event
        turnLedsOff();
        currentState = REST;
      }

The function would then become

void runStateMachine() {
  button.poll(); // update the state of the button

  switch (currentState) {
    case REST:                            // we are at rest and if we get a click, turn on the green LED
      if (button.onPress()) {             // this is our event detection
        digitalWrite(pinGreenLed, HIGH);  // execute the associated action. Green LED powered
        chrono = millis();                // We just had an action, so we reset our stopwatch
        currentState = STATE_G;           // note the new state of our system
      }
      break;

    case STATE_G:                         // green LED was on and if we get a click, turn on the yellow LED
      if (button.onPress()) {             // this is our event detection
        digitalWrite(pinYellowLed, HIGH); // execute the associated action. Yellow LED powered
        chrono = millis();                // We just had an action, so we reset our stopwatch
        currentState = STATE_GY;          // note the new state of our system
      }
      else if (millis() - chrono >= TimeOut) { // timeout event
        turnLedsOff();
        currentState = REST;
      }
      break;

    case STATE_GY:                        // green and yellow were on, if we get a click, turn on the orange LED
      if (button.onPress()) {             // this is our event detection
        digitalWrite(pinOrangeLed, HIGH); // execute the associated action. Orange LED powered
        chrono = millis();                // We just had an action, so we reset our stopwatch
        currentState = STATE_GYO;         // note the new state of our system
      }
      else if (millis() - chrono >= TimeOut) { // timeout event
        turnLedsOff();
        currentState = REST;
      }
      break;

    case STATE_GYO:                       // green, orange, and yellow were on, if we get a click, turn on the red LED
      if (button.onPress()) {             // this is our event detection
        digitalWrite(pinRedLed, HIGH);    // execute the associated action. Red LED powered
        chrono = millis();                // We just had an action, so we reset our stopwatch
        currentState = STATE_GYOR;        // note the new state of our system
      }
      else if (millis() - chrono >= TimeOut) { // timeout event
        turnLedsOff();
        currentState = REST;
      }
      break;

    case STATE_GYOR:                      // everything was on, if we get a click a click, return to rest
      if (button.onPress()) {             // this is our event detection
        turnLedsOff();                    // execute the associated action. return to the initial state
        chrono = millis();                // We just had an action, so we reset our stopwatch
        currentState = REST;
      }
      else if (millis() - chrono >= TimeOut) { // timeout event
        turnLedsOff();
        currentState = REST;
      }
      break;
  }
}

The positive aspect of doing this is that we maintain only one switch/case where all the relevant events are tested against each state (so we have one if for every arrow leaving a state)

The negative aspect is that we repeated a lot of code. Not everything needs to be in the switch/case, as long as the events are tested you are fine. So a better code would be to first test for the clicks and then if the state is not REST, test for timeout

here is the wokwi with this second approach

click to see the code
// Button management library
#include <Toggle.h> // // https://github.com/Dlloydev/Toggle
const byte buttonPin = 4; // Our button is on pin 4
Toggle button;

// Pins used for LEDs
const byte pinRedLed = 8;
const byte pinOrangeLed = 9;
const byte pinYellowLed = 10;
const byte pinGreenLed = 11;

// Introducing time as an additional event
unsigned long chrono; // Note: unsigned long, like millis()
const unsigned long TimeOut = 15000ul; // 15 seconds (ul at the end for unsigned long, a good habit to adopt)

// The list of possible states of our system
// along with a currentState variable taking one of these values
enum {REST, STATE_G, STATE_GY, STATE_GYO, STATE_GYOR} currentState;

void turnLedsOff() {
  digitalWrite(pinGreenLed, LOW);
  digitalWrite(pinYellowLed, LOW);
  digitalWrite(pinOrangeLed, LOW);
  digitalWrite(pinRedLed, LOW);
}

void runStateMachine() {
  button.poll(); // update the state of the button

  // TEST FOR CLICK
  switch (currentState) {
    case REST:                            // we are at rest and if we get a click, turn on the green LED
      if (button.onPress()) {             // this is our event detection
        digitalWrite(pinGreenLed, HIGH);  // execute the associated action. Green LED powered
        chrono = millis();                // We just had an action, so we reset our stopwatch
        currentState = STATE_G;           // note the new state of our system
      }
      break;

    case STATE_G:                         // green LED was on and if we get a click, turn on the yellow LED
      if (button.onPress()) {             // this is our event detection
        digitalWrite(pinYellowLed, HIGH); // execute the associated action. Yellow LED powered
        chrono = millis();                // We just had an action, so we reset our stopwatch
        currentState = STATE_GY;          // note the new state of our system
      }
      break;

    case STATE_GY:                        // green and yellow were on, if we get a click, turn on the orange LED
      if (button.onPress()) {             // this is our event detection
        digitalWrite(pinOrangeLed, HIGH); // execute the associated action. Orange LED powered
        chrono = millis();                // We just had an action, so we reset our stopwatch
        currentState = STATE_GYO;         // note the new state of our system
      }
      break;

    case STATE_GYO:                       // green, orange, and yellow were on, if we get a click, turn on the red LED
      if (button.onPress()) {             // this is our event detection
        digitalWrite(pinRedLed, HIGH);    // execute the associated action. Red LED powered
        chrono = millis();                // We just had an action, so we reset our stopwatch
        currentState = STATE_GYOR;        // note the new state of our system
      }
      break;

    case STATE_GYOR:                      // everything was on, if we get a click a click, return to rest
      if (button.onPress()) {             // this is our event detection
        turnLedsOff();                    // execute the associated action. return to the initial state
        chrono = millis();                // We just had an action, so we reset our stopwatch
        currentState = REST;
      }
      break;
  }

  // TEST FOR TIMEOUT
  if ((currentState != REST) && (millis() - chrono >= TimeOut)) {     // timeout event
    turnLedsOff();
    currentState = REST;
  }
}

// ------------------------------------------------------
// We initialize our system in the setup
// ------------------------------------------------------
void setup() {
  // configure
  pinMode(pinRedLed, OUTPUT);
  pinMode(pinOrangeLed, OUTPUT);
  pinMode(pinYellowLed, OUTPUT);
  pinMode(pinGreenLed, OUTPUT);

  button.begin(buttonPin);

  // define Initial conditions
  turnLedsOff();
  currentState = REST;

}

void loop() {
  // we update ou system based on possible events
  runStateMachine();

  // Here, we can do other things as long as it doesn't take too long and is non blocking
}


as you can see now we have a more complex state machine with many transitions


but the code stayed simple and easy to read.

using state machines is a good way to first think about the system you want to manage, see what state can exist and what triggers transitions between those states and then it's easy to code and your code architecture is sound / easy to extend.


exercice 3
โžœ left to the reader

rewrite the code using the OneButton library.
In this library you use attach callback functions to the button for specific events and when it happens, the function is called. You can have a callback for a simpleClick for example, and this is a good place where you could have your state machine.

exercice 4
โžœ left to the reader
extend now your code to add double click. A double click should light on all LEDs if they are not all on, and turn them all off is they are all on.

This is an event easily tracked by the OneButton library and so you attach a callback to the doubleClick event in which you have another small state machine dealing with the various sates.

In a switch you can group cases that have a similar treatment so the function could be like


void doubleClickCallback() {
  switch (currentState) {
    case REST:                      // we are at rest and if we get a double click, turn all LEDs on
      turnLedsOn();                 // execute the associated action. return to the initial state
      currentState = STATE_GYOR;
      break;

    case STATE_G:                   // green LED was on and if we get a double click, turn all LEDs off
    case STATE_GY:                  // green and yellow were on, if we get a double click,  turn all LEDs off
    case STATE_GYO:                 // green, orange, and yellow were on, if we get a double click,  turn all LEDs offยท
    case STATE_GYOR:                // everything was on, if we get a click a double click,  turn all LEDs off
      turnLedsOff();                // execute the associated action. return to the initial state
      currentState = REST;
      break;
  }
}

have fun.

17 Likes