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!
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
- Using millis() for timing. A beginners guide
- Several things at the same time
- Flashing multiple LEDs at the same time
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.