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:
-
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.
-
One or more actions that happen in this state, like driving an LED on or off or calling a function.
-
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;
}
}