State machines, a short tutorial

A common response to “I want my code to do A., then B., then C., etc.,” from the knowledgeable is “Use a state machine.” Many who are new to coding/programming have never heard of a state machine, although almost everyone is familiar with the concept. There are different ways to realize a state machine, like the very similar Sequential Function Chart, a construct used in Programmable Logic Controllers, but this discussion will deal only with the C++ switch/case construct - which is described briefly on the Arduino reference page. There are also numerous examples on the web and elsewhere but I wanted to offer an Arduino-focused example to eliminate the distraction of translating a generic non-Arduino explanation to what is seen in the IDE.

The example will be a simple garage door opener. Push a button and the door goes up and stops. Push again, the door goes down and stops. Right there are four ‘states’ – 1. Closed, waiting for open command; 2. Opening, waiting for top limit; 3. Open, waiting for down command; 4. Closing, waiting for bottom limit. Notice there are three elements for each state:

  1. A name, usually describing what the state does. In reality it's an alias for the underlying numeric value the switch statement evaluates to select a case. It can also be, and usually is, a target for another state to go to – the way Closed above goes to Opening. The example uses an enum to give the states their names.

  2. One or more actions that happen in this state, like driving an LED on or off or calling a function.

  3. Some condition or combination of conditions which, when met, advance us to the next state.

The listing is pretty liberally commented so there won’t be a lot of discussion here. Also, to reduce clutter in the listing, there’s no switch debouncing included in this first effort. It’s not needed for the sketch to demonstrate the state machine principle.

You can observe the effects of the default: label by uncommenting doorState = 6;, the last statement in setup(). Since only four names are given in the enum statement, and they are not given explicit values, the compiler assigns the values zero through three. If switchState is 6 on the first pass through the code it won’t match any of the cases given so, the default is selected, a message is displayed, and the state is then set to doorIsDown. Thereafter the sequence will proceed normally. It should be obvious from this that you can start the sequence at any point by loading an arbitrary, legal, value to doorState.

Hardware:

The examples use an UNO but are easily adapted to other processors if needed.

The usual breadboard and hookup/jumper wires for setting up experiments.

Two normally open pushbuttons for digital input – like the tactile switches that come with Arduino kits.

Two LEDs with associated dropping resistors for visual indicator outputs. You can omit the LEDs if you’re satisfied with watching the action on the serial monitor.

Construct the circuit and load and run the sketch – see the listing for pin numbers and schematic. Don’t worry about the second switch right now, it’ll be added later.

Pressing and releasing the switch should cause the LEDs to alternately light for a time, signifying the motor running to open or close the door. A timer is used rather than a physical input to simplify the circuit - although it’s not uncommon for a real application to use a timer state as the condition to advance.

As part of the attempt to simplify I made a small modification to the timer comparison. The usual Arduino timer uses the construction

if( millis() – yourTimer >= yourPreset){
// Time is up, do something
}

To avoid having to think about subtraction I put this into a #define and gave it the name accumulatedMillis –
#define accumulatedMillis millis() – yourTimer

Now every time the compiler sees accumulatedMillis it will substitute millis() – yourTimer in its place so the comparison becomes
if(accumulatedMillis >= yourPreset).

Notice that the condition to get out of the closed state is the switch being operated. Once we’re in doorOpening the switch state isn’t checked since it doesn’t matter anymore. Likewise, when the door is either open or closed we don’t check the timer to see if it’s time to stop the motor.

Remember, this is only an illustration, don’t build a real garage door opener based on this sketch! Real-world door openers have safety features built in which don’t exist in this demo.

//
// The sketch simulates a simple motor-driven door opener/closer
// such as might be used on a garage, chicken coop, window blinds,
// etc. Starting from a known position a switch is pressed, the
// door opens, then stops.  When the switch is pressed again the
// door closes, then stops.
//
// A timer is used to simulate the delay while the door is being
// driven to its new position and LEDs simulate the up/down
// forward/reverse action of the the motor.

// The sketch uses the following I/O

const unsigned char switchInput = 10;   //  Arduino pin 10 --| |--SW-- GND 
const unsigned char openLED = 9;  //  +5V--/\/\/- 330Ω -->|-- Arduino pin 9
const unsigned char closeLED = 7; //  +5V--/\/\/- 330Ω -->|-- Arduino pin 7

#define motorRun LOW
#define motorStop HIGH
#define accumulatedMillis millis() - timerMillis

const unsigned long motorTimerPreset = 2000;  // two seconds
unsigned long timerMillis;  // For counting time increments

// The door has four possible states it can be in
// Let's give the states descriptive names
enum {doorIsDown, doorIsUp, doorOpening, doorClosing};
unsigned char doorState;  // What the door is doing at any given moment.

void setup() {
  Serial.begin(115200);
  pinMode(switchInput, INPUT_PULLUP);
  pinMode(openLED, OUTPUT);
  digitalWrite(openLED, HIGH);
  pinMode(closeLED, OUTPUT);
  digitalWrite(closeLED, HIGH);
  //  doorState = 6;
}

void loop() {
  switch (doorState) {

    case doorIsDown: // Nothing happening, waiting for switchInput
      Serial.println("door down");
      if (digitalRead(switchInput) == LOW) { // switchInput  pressed
        timerMillis = millis(); // reset the timer
        doorState = doorOpening; // Advance to the next state
        break;
      }
      else {   // Switch not pressed
        break; // State remains the same, continue with rest of the program
      }

    case doorOpening:
      Serial.println("door opening");
      digitalWrite(openLED, motorRun);
      //
      // The compare below would be replaced by a test of a limit
      // switch, or other sensor, in a real application.
      if (accumulatedMillis >= motorTimerPreset) { // Door up
        digitalWrite( openLED, motorStop); // Stop the motor
        doorState = doorIsUp; // The door is now open
        break;
      }
      else {
        break;
      }

    case doorIsUp:
      Serial.println("door up");
      if (digitalRead(switchInput) == LOW) { // switchInput pressed
        timerMillis = millis(); // reset the timer
        doorState = doorClosing; // Advance to the next state
        break;
      }
      else { // switchInput was not pressed
        break;
      }

    case doorClosing:
      Serial.println("door closing");
      digitalWrite(closeLED, motorRun); // Down LED on
      if (accumulatedMillis >= motorTimerPreset) {
        digitalWrite(closeLED, motorStop); // Stop the motor
        doorState = doorIsDown;  // Back to start point
        break;
      }
      else {
        break;
      }
    default:
      Serial.println("\n We hit the default");
      delay(3000);
      doorState = doorIsDown;
      break;
  }
}
4 Likes

It’s not immediately apparent but, the sketch above has a fault, namely that if the switch is held closed the door will cycle continuously. The code checks to see if the switch is closed, not when the switch becomes closed. Real doors don’t do this so let’s fix it. The next sketch adds the Bounce2.h library to handle switch conditioning. Again, this is to avoid cluttering the listing with the switch debounce code. Anyway, now the state machine is triggered by the switchClosed transition, not the state of it being closed, so even if the switch is held closed the door will cycle only once. The rest of the code is unchanged.

// Switch/case tutorial part #2
// Using switch transitions to trigger the code
// Precludes continuous operation if a switch is
// held down.

#include <Bounce2.h>  // To handle switch closure

// Instantiate a Bounce object
//Bounce sitchInputdebouncer = Bounce();
Bounce debouncedSwitch = Bounce();

const unsigned char switchInput = 10;   //  Arduino pin 10 --| |--SW-- GND 
const unsigned char openContactor = 9;  //  +5--/\/\/- 330Ω -->|--DIO9
const unsigned char closeContactor = 7; //  +5--/\/\/- 330Ω -->|--DIO7

#define motorRun LOW
#define motorStop HIGH
#define accumulatedMillis millis() - timerMillis

const unsigned long motorTimerPreset = 2000;  // two seconds
unsigned long timerMillis;  // For counting time increments

// The door has four possible states it can be in
// Let's give the states descriptive names
enum {doorIsDown, doorIsUp, doorOpening, doorClosing};
unsigned char doorState = doorIsDown;  // What the door is doing at any given moment.

// New variables for switch actuation
bool switchClosed;  // On for one scan when switch is pressed.
bool switchClosedSetup;

void setup() {
  Serial.begin(115200);

  // After setting up the button, setup the Bounce instance :
  debouncedSwitch.attach(switchInput);
  debouncedSwitch.interval(5); // interval in ms

  pinMode(switchInput, INPUT_PULLUP);// +5V--| |--SW--DIO10
  pinMode(openContactor, OUTPUT);
  digitalWrite(openContactor, HIGH);
  pinMode(closeContactor, OUTPUT);
  digitalWrite(closeContactor, HIGH);
}

void loop() {

  // Update the Bounce instance :
  debouncedSwitch.update();
  // Get the updated value :
  unsigned char switchValue = debouncedSwitch.read();

  // switchClosed will be TRUE for one program scan when the
  // pushbutton is pressed and debounced.
  switchClosed = (!switchValue and switchClosedSetup); //
  switchClosedSetup = switchValue;

  switch (doorState) {

    case doorIsDown: // Nothing happening, waiting for switchInput
      Serial.println("door down");
      if (switchClosed) {
        timerMillis = millis(); // reset the timer
        doorState = doorOpening; // Advance to the next state
        break;
      }
      else {
        break; // Continue with rest of the program
      }

    case doorOpening:
      Serial.println("door opening");
      digitalWrite(openContactor, motorRun);
      //
      // Remember, accumulatedMillis is the same as millis() - timerMillis
      if (accumulatedMillis >= motorTimerPreset) { // Door up?
        digitalWrite( openContactor, motorStop); // Stop the motor
        doorState = doorIsUp;
        break;
      }
      else break;

    case doorIsUp:
      Serial.println("door up");
      if (switchClosed) { // User requests door close
        timerMillis = millis(); // reset the timer
        doorState = doorClosing; // Advance to the next state
        break;
      }
      else { // Continue with rest of program
        break;
      }

    case doorClosing:
      Serial.println("door closing");
      digitalWrite(closeContactor, motorRun); // Down contactor on
      if (accumulatedMillis >= motorTimerPreset) {
        digitalWrite(closeContactor, motorStop); // Stop the motor
        doorState = doorIsDown;  // Back to start point
        break;
      }
      else {
        break;
      }
    default:
      doorState = doorIsDown;
      break;
  }
}
2 Likes

It’s time for a safety enhancement. Add the second switch to the circuit and make any necessary changes in your sketch to be able to read the switch. I used pin 11. The next sketch utilizes the second switch as an obstruction detector. If the obstruction switch is pressed while the door is going closed – because somebody’s bike, or the dog, didn’t get out of the way, the code changes direction and jumps to the opening state and returns the door to the open position. Real doors have various ways of doing this, like having a thru-beam photo detector aimed across the door opening. This illustrates the ability of the state machine to adjust by taking a different path if conditions warrant.

Hope this helps.

// Stop and raise the door if an obstruction is
// encountered while lowering.

#include <Bounce2.h>  // To handle switch closure

// Instantiate a Bounce object
// Bounce switchInputdebouncer = Bounce();
Bounce debouncedSwitch = Bounce();

const unsigned char switchInput = 10;  //        Arduino pin 10 --| |--SW-- GND 
const unsigned char obstructionSwitch = 11;  //  +5V--| |--SW--DIO11
const unsigned char openContactor = 9; //        +5--/\/\/- 330Ω -->|--DIO9
const unsigned char closeContactor = 7; //       +5--/\/\/- 330Ω -->|--DIO7

#define motorRun LOW
#define motorStop HIGH
#define accumulatedMillis millis() - timerMillis

const unsigned long motorTimerPreset = 2000;  // two seconds
unsigned long timerMillis;  // For counting time increments

// The door has four possible states it can be in
// Let's give the states descriptive names
enum {doorIsDown, doorIsUp, doorOpening, doorClosing};
unsigned char doorState = doorIsDown;  // What the door is doing at any given moment.

bool switchClosed;
bool switchClosedSetup;
int counter;

void setup() {
  Serial.begin(115200);

  // After setting up the button, setup the Bounce instance :
  debouncedSwitch.attach(switchInput);
  debouncedSwitch.interval(5); // interval in ms

  pinMode(switchInput, INPUT_PULLUP);
  pinMode(obstructionSwitch, INPUT_PULLUP);
  pinMode(openContactor, OUTPUT);
  digitalWrite(openContactor, HIGH);
  pinMode(closeContactor, OUTPUT);
  digitalWrite(closeContactor, HIGH);
}

void loop() {

  // Update the Bounce instance :
  debouncedSwitch.update();
  // Get the updated value :
  bool switchValue = debouncedSwitch.read();

  switchClosed = (!switchValue and switchClosedSetup); //
  switchClosedSetup = switchValue;

  switch (doorState) {

    case doorIsDown: // Nothing happening, waiting for switchInput
      Serial.println("door down");
      if (switchClosed) {
        // Notice at this point the switchInput is a don't care
        timerMillis = millis(); // reset the timer
        doorState = doorOpening; // Advance to the next state
        break;
      }
      else {
        break; // Continue with rest of the program
      }

    case doorOpening:
      Serial.println("door opening");
      digitalWrite(openContactor, motorRun);
      //
      if (accumulatedMillis >= motorTimerPreset) { // Door up?
        digitalWrite( openContactor, motorStop); // Stop the motor
        doorState = doorIsUp;
        break;
      }
       else {
        break; // Continue with rest of the program
      }

    case doorIsUp:
      Serial.println("door up");
      if (switchClosed) {
        // After this point the switchInput is a don't care
        timerMillis = millis(); // reset the timer
        doorState = doorClosing; // Advance to the next state
        break;
      }
      else { // switchInput was pressed
        break;
      }

    case doorClosing:
      Serial.println("door closing");
      if (digitalRead(obstructionSwitch) == 0) { // Have we met an obstruction?
        // Yes, abort the closing sequence and go to the opening sequence
        digitalWrite(closeContactor, motorStop);
        doorState = doorOpening;
        break;
      }
      digitalWrite(closeContactor, motorRun); // Down contactor on
      if (accumulatedMillis >= motorTimerPreset) {
        digitalWrite(closeContactor, motorStop); // Stop the motor
        doorState = doorIsDown;  // Back to start point
        break;
      }
      else {
        break;
      }
    default:
      doorState = doorIsDown;
      break;
  }
}
2 Likes