How To Program an Arduino Finite State Machine
2023-08-14 | By Maker.io Staff
Image. Source: https://pixabay.com/photos/arduino-electronics-board-computer-631977/
A recent article investigated the theoretical concepts of finite state machines and how to model them using simple directed graphs. While the theoretical aspects are essential for understanding FSM concepts, you may be interested in how to incorporate this unique computational concept into your projects. This article will explain how you can implement any finite state machine in a variety of programs, such as an Arduino-powered project.
A State Machine Hello World Example
If you have ever written an Arduino sketch, you’re likely familiar with the simple blink example sketch used to test whether a development board functions as expected with the environment set up correctly. This first program similarly aims to explain FSM program concepts using as few lines of code as possible by expanding upon the simple single-LED blink script.
This first state machine should periodically cycle through three LEDs, turning each on for two and a half seconds while the others remain off. The following diagram illustrates this process:
This graph represents a simple SFM for turning on one of three LEDs
Remember that the graph’s nodes represent the states in the program, and the arrows indicate the possible transitions between states. Transitions may occur due to external events such as user inputs or automatically — for instance, periodically or at the end of the current state. In this example, all transitions occur periodically after about 2500 milliseconds.
Represent the FSM States in Arduino Programs
While there might be numerous methods to accomplish this task, enumerations (or enums, in short) are one fantastic way to represent the states of an FSM in programs. Enumerations offer excellent human-readable names to quickly identify states in the program code. The currently active state gets stored in a global variable. So, the state enum in the above hello-world example could look as follows:
enum States { RED, GREEN, BLUE }; // Set the initial (i.e., starting) state State state = States::RED;
You can switch between states by setting the state variable to a new value:
void nextState() { if (state == States::RED) state = States::GREEN; else if (state == States::GREEN) state = States::BLUE; else state = States::RED; }
In this straightforward example, the transitions occur based on a simple set of rules modeled using if-else blocks. When the machine is in the RED state, it transitions to GREEN. Similarly, when it is in the GREEN state, it moves to the BLUE state. Finally, it cycles back to RED when it finishes the BLUE state.
The program must then perform actions based on its current state. You can accomplish this in the sketch’s loop method, which may also contain code that’s similar for all states:
void loop() { // The following actions should always happen, // irrespective of the state digitalWrite(R_LED_PIN, LOW); digitalWrite(G_LED_PIN, LOW); digitalWrite(B_LED_PIN, LOW); // State-aware actions switch(state) { case States::RED: digitalWrite(R_LED_PIN, HIGH); break; case States::GREEN: digitalWrite(G_LED_PIN, HIGH); break; case States::BLUE: digitalWrite(B_LED_PIN, HIGH); break; } // Simulate some delay and then switch to the next state delay(2500); nextState(); }
The first three lines turn off all LEDs. These actions should take place in all states so they can exist outside of the subsequent switch block, which checks the machine’s current state and then turns on one of the three LEDs corresponding to the active state. The program then waits a short time before switching to the next state.
Switching States Based on User Input
More realistic machines must react to user inputs — for example, to display different content on a screen when users interact with the device. You can accomplish this using states, as outlined in the previous article, where a vending machine would display info content when you pressed a button.
The Arduino Interrupt Service Routine (ISR) is perfect for switching states as a consequence of user inputs, as illustrated by the following two examples:
void returnButtonInterruptHandler() { // Only switch to the return state machineState = MachineStates::RETURN; } void infoButtonInterruptHandler() { // Switch to the info state machineState = MachineStates::INFO; // Set a variable to keep track of when the machine // started displaying the info screen infoStateStartMillis = millis(); }
The seconds ISR additionally stores the time you pressed a button, which later allows the machine to switch back to its IDLE state after a few seconds.
Similarly, a machine can transition between states based on your specific input sequences. Take variable storage, for example. The program switches to an INFO or ERROR state whenever variables have specific values. When the values are correct, it doesn’t transition to a different state:
void dropSelectedItem(char a, char b) { if(a != '0' || a != '1' || a != '2') { machineState = MachineStates::INFO; infoStateStartMillis = millis(); } else { if(a == '2' && (b == '6' || b == '7' || b == '8' || b == '9')) { machineState = MachineStates::INFO; infoStateStartMillis = millis(); } } }
A transition to a subsequent state can also happen automatically when the machine finishes its current task, such as boot-up sequence completion:
void displayBootScreen() { // Ignore this function in all states except for the bootup state if (!machineState == MachineStates::BOOTUP) return; /* Send the manufacturer's logo to an attached display and perform additional boot-up tasks */ /* Register interrupts, pins, outputs, attached devices, etc. */ // Once finished booting, the machine should go into its idle state machineState = MachineStates::IDLE; }
Finally, the loop method only needs to check the current state and call functions or perform ad-hoc operations based on the state:
void loop() { // Perform actions based on the machine's state switch(machineState) { case MachineStates::BOOTUP: displayBootScreen(); break; case MachineStates::IDLE: displayIdleScreen(); break; case MachineStates::INFO: // Switch back to the idle state after five seconds if (infoStateStartMillis + 5000 > millis()) machineState = MachineStates::IDLE; else displayInfoScreen(); // Until then, display an info screen break; case MachineStates::DROP: dropSelectedItem(selectedItem[0], selectedItem[1]); machineState = MachineStates::IDLE; break; case MachineStates::RETURN: returnMoney(); machineState = MachineStates::IDLE; break; } }
The examples above omitted a few methods and variable definitions to keep the article more concise. You can download the complete code and the simple hello-world example program here.
Summary
Enumerations are a perfect construct for representing the states of a simple finite state machine in an Arduino sketch. While there may be other alternatives, enums allow programmers to define understandable human-readable names for all states in the machine, and you can transition between states by changing a single variable’s value, making this approach perfect for use in interrupt handling routines.
Aside from the enumeration encoding the states and the transitions, the machine must also perform actions based on its current state. This typically occurs in the program’s main loop function, where a specific function can be called based on the current state. You may use if-else or switch blocks to make the program react to its current state.
Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.
Visit TechForum