In a State
I was browsing some libraries intended to support programming a State Machine. One such library had a demo that was based upon a Pelican crossing. If you don’t know what one is, a Pelican crossing uses traffic lights to help pedestrians to cross busy roads. They are different to, but related to, managed pedestrian crossings at major road junctions that are primarily involved in traffic control, with pedestrians as an afterthought.
In the demo I saw, the combination of traffic lights and pedestrian information light sequences was presented as a set of states with a clear transition between them.
Always happy to borrow a good idea, this felt like an interesting project to explore the software options as it involves a sequence of timed states, optional LED and sound output and an interrupt to allow pedestrians to request the crossing lights to stop the traffic. The interrupt is only effected after a minimum time period with traffic freely flowing – but can be queued.
While the electronic side of this project is fairly straightforward, it is the C programming skills that we are looking to hone here. Tackling this task was also a good way of exploring one of the more effective software development strategies. The idea is to build the simplest program that actually does something and then to incrementally add functionality until the software finally meets the full specification. One of the reasons this is a successful strategy is highlighted by the reverse approach. Some (particularly new) programmers throw themselves into the task of writing code and only think about testing towards the end of the process. The trouble here is that the compiler is probably going to complain about a lot of things at once and some of the error messages may not make immediate sense. Plus, if (rather when) the program runs, any errors in the way it performs may not be easy to track back to the piece of code at fault.
Anyway – let’s look at the requirement. It turns out that there is an official specification for a Pelican crossing.
Official light sequences and times | ||
Pedestrian Control | Traffic Control | Timings |
Red Don’t cross | Green light | Minimum 6 to 60 seconds |
Red Don’t cross | Amber light | 3 seconds |
Red Don’t cross | Red light | 3 seconds |
Green to Cross | Red light | 4 to 7 seconds (sound signal) |
Flashing green | Flashing amber | 6 to 18 seconds |
Red Don’t Cross | Flashing amber | 2 seconds |
This gives us 6 distinct states where each would control the Arduino output for a specific period of time (with the green light to traffic lasting until a subsequent pedestrian request button push is detected).
We can build a model Pelican crossing on a solderless breadboard.
Parts List
2 red LEDs
2 green LEDs
1 yellow LED
5 330 ohm resistors
1 1K ohm resistor (later discarded – see text)
1 8 ohm mini speaker
1 push to make switch
Breadboard layout
There was nothing very complex to resolve when choosing the Arduino pins to use. I went for pin 2 for the pedestrian request button so I could use an external interrupt. I knew that Timer2 would be tied up managing the audible signal but that did not represent any sort of restriction. After that I just allocated digital pins in order but there is plenty of opportunity for you to make revisions or improvements.
My software strategy revolved around having a specific function to manage each state. This allows for the development and testing of each function individually.
Here is my initial and minimalist program version:
void state1(); void state2(); void state3(); void state4(); void state5(); void state6(); void (*states[])(void) = { state1, state2, state3, state4, state5, state6 }; void setup() { Serial.begin(115200); } void loop() { for(int i = 0; i < 6; i++){ states[i](); } } void state1() { if(Serial) {Serial.println("State 1");} delay(2000); } void state2() { if(Serial) {Serial.println("State 2");} delay(2000); } void state3() { if(Serial) {Serial.println("State 3");} delay(2000); } void state4() { if(Serial) {Serial.println("State 4");} delay(2000); } void state5() { if(Serial) {Serial.println("State 5");} delay(2000); } void state6() { if(Serial) {Serial.println("State 6");} delay(2000); }
I am sure that your first two questions are “Why are there function prototype declarations at the start?” and then “Why use an array of function pointers?”. Both good questions. The function prototype declarations are needed to allow the definition of the function pointer array as otherwise the compiler would not have known what the function names in that declaration were. I thought a function pointer array might be a good idea as it would make it easy to swap functions during program execution should the specification ultimately lead in that direction. One thing just led to the other. Give it a run though and check the function switching works.
We now have a test rig for the individual function development tasks but first it might be an idea to test the external components and make sure they are all correctly hooked up and working. To do that I added some global variables and changed the setup() function to read:
#define C6 1047 volatile bool pedRequest = false; const int RED_TRAFFIC = 8; const int AMBER_TRAFFIC = 7; const int GREEN_TRAFFIC = 6; const int DONT_CROSS = 5; const int CROSS = 4; const int PED_BUTTON = 2; const int PED_SOUNDER = 3; void setup() { Serial.begin(115200); pinMode(RED_TRAFFIC, OUTPUT); pinMode(AMBER_TRAFFIC, OUTPUT); pinMode(GREEN_TRAFFIC, OUTPUT); pinMode(DONT_CROSS, OUTPUT); pinMode(CROSS, OUTPUT); pinMode(PED_BUTTON, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(PED_BUTTON), intHandler, FALLING); }
added an ISR for the interrupt:
void intHandler() { pedRequest = true; }
Then added some temporary hardware test code to the state1() function.
void state1() { int leds[] = { RED_TRAFFIC, AMBER_TRAFFIC, GREEN_TRAFFIC, DONT_CROSS, CROSS}; if(Serial) {Serial.println("Testing LEDs");} for (int i = 0; i < 5; i++) { digitalWrite(leds[i], HIGH); delay(500); digitalWrite(leds[i], LOW); } if(Serial) {Serial.println("Testing Sound");} for(int i = 0; i < 5; i++) { tone(PED_SOUNDER, C6, 250); delay(500); } noTone(PED_SOUNDER); if(pedRequest) { pedRequest = false; if(Serial) { Serial.println("Pedestrian Button Pressed"); } } }
Running that code should clear up any hook-up errors and a test of the pedestrian request button should send a message to the Serial Monitor. It was this test that showed that my speaker was very quiet when used with a 1k ohm resistor in series so I felt perfectly OK in replacing it with a jumper wire but I was glad I had been cautious when first using this new component.
Before we go much further a decision needs to be made on how to manage the pelican crossing timings. The most obvious candidate to keep everything properly synchronised is the millis() function. The unsigned long value returned by millis() is managed by Timer0 but every 49.71 days the value will overflow and restart from zero. As we do not want any grannies or small children mowed down on our crossing as a result of timer errors we have to deal with timescales of that magnitude and longer.
We can manage the rollover by making use of unsigned integer arithmetic or we might consider what I would call the “Groundhog Day” approach and reset the millis() counter after every cycle through the Pelican states. The first approach makes some small demands upon our program testing skills while the latter has risks if your project makes any other use of the millis() function or if it includes any libraries that do.
I did consider using Timer1 as a dedicated clock for this program but then realised that millis() was a lot more interesting.
Timer0 is used to increment the relevant timer register once every 64 cycles of the Arduino board clock. This counter register is 8 bits with a maximum value of 255 and when it overflows the millis() counter is incremented by 1. There is a subtle issue here, as arithmeticians may have spotted, that it takes 1.024ms to increment millis() by 1. This leaves 24 microseconds not included in the millis() count of milliseconds. The fractional part is “carried” forward and used to increment the millis() counter by 2 instead of 1 every now and again. It is worth remembering that, and making sure that your code never tests for an absolute value as it may be that millis() could skip the particular integer value you were waiting for. Always use a test like >= (greater than or equal to) a particular value.
To explore the world of unsigned integer arithmetic, first add some lines to the state2() function so that it looks like:
void state2() { if(Serial) { Serial.println("State 2"); } unsigned long startMillis = millis(); digitalWrite(GREEN_TRAFFIC, LOW); digitalWrite(DONT_CROSS, HIGH); digitalWrite(AMBER_TRAFFIC, HIGH); do { }while((unsigned long)(millis() - startMillis) < 3000); }
If you upload and run the code then you should see that the state2() function turns on the traffic amber and pedestrian red lights and waits for three seconds before handing over to the next function. The delay is encompassed in the code that reads:
do {
}while((unsigned long)(millis() - startMillis) < 3000);
What’s with the expression ((unsigned long)(millis() - startMillis) < 3000) ?
Unsigned integer arithmetic
Casting the result of the comparison between the current program time in milliseconds and the start time of the function to an unsigned long deals nicely with the rollover issue. To test this, start a new program in the Arduino IDE and add the following code:
unsigned long milliMe; unsigned long stopVal = 2000ul; unsigned long startVal = 0xFFFFFFFF; unsigned int target = 1000; void setup() { Serial.begin(115200); startVal -= 10000; milliMe = startVal; }
Followed by:
void loop() { unsigned long lastMilli = milliMe; for(int i = 0; i < 32767; i++) { milliMe++; if((unsigned long)(milliMe - lastMilli) >= target) { Serial.print("Simulated mills() at: "); Serial.print(milliMe); Serial.print(", lastMilli: "); Serial.print(lastMilli); Serial.println(" difference >= target"); lastMilli = milliMe; } if(milliMe < startVal && milliMe > stopVal) { break; } } }
When uploaded and run, the program should send 12 lines of output to the Serial Monitor window. Here the milliMe value simulates a millis() overflow.
You should see that 999 – 4294967295 is indeed >= 1000 (in fact exactly 1000) if the arithmetic is managed with unsigned values.
Back to the Pelican crossing program. The state3() function is very similar to state2() with some changes to the LED settings but state4() adds a bit of interest as we have to manage a repeated sound signal during the time period that the state is current.
void state4() { if(Serial) { Serial.println("State 4"); } unsigned long startMillis = millis(); unsigned long lastMillis = startMillis; digitalWrite(RED_TRAFFIC, HIGH); digitalWrite(CROSS, HIGH); digitalWrite(DONT_CROSS, LOW); tone(PED_SOUNDER, C6, 250); while(true) { unsigned long currentMillis = millis(); if((unsigned long)(currentMillis - lastMillis) >= 500){ tone(PED_SOUNDER, C6, 250); lastMillis = currentMillis; } if((unsigned long)(currentMillis - startMillis) >= 7000) { break; // timed end of state 4 } } noTone(PED_SOUNDER); }
The process now continues with each state (embodied in a single function) being coded and tested in turn until the program is complete. I decided that in state5() I would maintain two independent sub-states – one for each of the lights to be flashed. The amber traffic light flashes on a half second cycle and the green pedestrian (hurry up) light flashes on a third of a second cycle.
Why not fill in the code for the remaining states running a test for each as you go on. If you get stuck or something does not work then refer to my final version, that follows in bitesized chunks.
#define C6 1047 #define BEEP 250 #define THRD_SEC 330 #define HALF_SEC 500 #define SECS_3 3000 #define SECS_2 2000 #define SECS_7 7000 #define SECS_6 6000 #define MAX_WAIT 10000 void state1(); void state2(); void state3(); void state4(); void state5(); void state6(); void (*states[])(void) = { state1, state2, state3, state4, state5, state6 }; volatile bool pedRequest = false; const int RED_TRAFFIC = 8; const int AMBER_TRAFFIC = 7; const int GREEN_TRAFFIC = 6; const int DONT_CROSS = 5; const int CROSS = 4; const int PED_BUTTON = 2; const int PED_SOUNDER = 3;
void setup() { pinMode(RED_TRAFFIC, OUTPUT); pinMode(AMBER_TRAFFIC, OUTPUT); pinMode(GREEN_TRAFFIC, OUTPUT); pinMode(DONT_CROSS, OUTPUT); pinMode(CROSS, OUTPUT); pinMode(PED_BUTTON, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(PED_BUTTON), intHandler, FALLING); } void loop() { for(int i = 0; i < 6; i++){ states[i](); } }
void intHandler() { pedRequest = true; } void state1() { pedRequest = false; unsigned long startMillis = millis(); digitalWrite(DONT_CROSS, HIGH); digitalWrite(GREEN_TRAFFIC, HIGH); digitalWrite(AMBER_TRAFFIC, LOW); while(true) { unsigned long currentMillis = millis(); if(pedRequest) { if((unsigned long)(currentMillis - startMillis) >= MAX_WAIT) { break; // pedestrian request and minimum time condition met } } } }
void state2() { unsigned long startMillis = millis(); digitalWrite(GREEN_TRAFFIC, LOW); digitalWrite(DONT_CROSS, HIGH); digitalWrite(AMBER_TRAFFIC, HIGH); do { }while((unsigned long)(millis() - startMillis) < SECS_3); }
void state3() { unsigned long startMillis = millis(); digitalWrite(RED_TRAFFIC, HIGH); digitalWrite(AMBER_TRAFFIC, LOW); digitalWrite(DONT_CROSS, HIGH); do { }while((unsigned long)(millis() - startMillis) < SECS_3); }
void state4() { unsigned long startMillis = millis(); unsigned long lastMillis = startMillis; digitalWrite(RED_TRAFFIC, HIGH); digitalWrite(CROSS, HIGH); digitalWrite(DONT_CROSS, LOW); tone(PED_SOUNDER, C6, BEEP); while(true) { unsigned long currentMillis = millis(); if((unsigned long)(currentMillis - lastMillis) >= HALF_SEC){ tone(PED_SOUNDER, C6, BEEP); lastMillis = currentMillis; } if((unsigned long)(currentMillis - startMillis) >= SECS_7) { break; // timed end of state 4 } } noTone(PED_SOUNDER); }
void state5() { unsigned long startMillis = millis(); unsigned long trafficMillis = startMillis; unsigned long pedMillis = startMillis; digitalWrite(AMBER_TRAFFIC, HIGH); digitalWrite(RED_TRAFFIC, LOW); digitalWrite(CROSS, HIGH); while(true) { unsigned long currentMillis = millis(); if((unsigned long)(currentMillis - trafficMillis) >= HALF_SEC){ digitalWrite(AMBER_TRAFFIC, digitalRead(AMBER_TRAFFIC) ^ 1); trafficMillis = currentMillis; } if((unsigned long)(currentMillis - pedMillis) >= THRD_SEC){ digitalWrite(CROSS, digitalRead(CROSS) ^ 1); pedMillis = currentMillis; } if((unsigned long)(currentMillis - startMillis) >= SECS_6) { break; // timed end of state 5 } } }
void state6() { unsigned long startMillis = millis(); unsigned long lastMillis = startMillis; digitalWrite(DONT_CROSS, HIGH); digitalWrite(CROSS, LOW); digitalWrite(AMBER_TRAFFIC, HIGH); while(true) { unsigned long currentMillis = millis(); if((unsigned long)(currentMillis - lastMillis) >= HALF_SEC){ digitalWrite(AMBER_TRAFFIC, digitalRead(AMBER_TRAFFIC) ^ 1); lastMillis = currentMillis; } if((unsigned long)(currentMillis - startMillis) >= SECS_2) { break; // timed end of state 6 } } }
If you wanted to create a full-sized Pelican crossing then you would probably need some brighter LEDs with their own driver chips and some more robust wiring but your Arduino and program would deliver on the project requirement.
You might like to consider a couple of additions as an exercise. You could add some functionality to allow an additional press of the pedestrian request button while state 5 is in progress to extend the time for that stage. Or you could consider adding an infra-red “crossing user” detector to do the same thing. My second challenge would be to add another state. When triggered, this state would suspend the Pelican crossing leaving the traffic amber light and the pedestrian crossing light flashing as a warning to all users that they should take extra care. When that state was toggled off (by some remote means) then the normal function of the pelican crossing should resume from a safe state for all users. My solution to that one at the end of the chapter.
Now for the Groundhog Day approach where the millis counter is reset every time the state cycle returns to the start. I have based my counter reset code on the millis() function itself. You can find that code by searching for the wiring.c file which will be located within the Arduino IDE installation folders.
We need to add an external reference to the timer0_millis volatile unsigned long somewhere at the head of our code before we can write a function to reset that value. The external variable modifier tells the compiler that the named variable is declared in a library and not in the current body of code.
extern volatile unsigned long timer0_millis; void resetMillis(){ uint8_t oldSREG = SREG; // copy the status Register cli(); // disable interrupts timer0_millis = 0; // zero the millis counter SREG = oldSREG; // reset the status Register }
Then place a call to the resetMillis() function at the top of the state1() function.
The millis() value should now never overflow while this program runs unless nobody wants to use our Pelican crossing for over 49 days*. The casts to unsigned long used in the elapsed time calculations would now be unnecessary. Even allowing for the performance advantage this is perhaps a technique worth knowing about but for many might only be considered a curiosity.
[* On the very day I wrote this I saw an item on the BBC web site about a red traffic light in Germany that had been showing red continuously for more than 27 years, so you never know.]
Implementing a suspended state
My mock-up of a crossing suspended state request was based upon a switch button press and not on some serial or other communications link. This made the changes relatively simple but did require that I freed up pin 3 which had been used by the speaker so that it was available to be used for external interrupt 1. Rather than repeat the whole code listing, the changes and additions are:
#define BOUNCE_TIME 50 volatile bool pedRequest = false; volatile bool suspRequest = false; unsigned long suspenseTime = millis(); // used to debounce button bool suspended = false; const int RED_TRAFFIC = 8; const int AMBER_TRAFFIC = 7; const int GREEN_TRAFFIC = 6; const int DONT_CROSS = 5; const int CROSS = 4; const int PED_BUTTON = 2; const int PED_SOUNDER = 9; const int REMOTE_SUSPEND = 3;
in setup()
pinMode(REMOTE_SUSPEND, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(REMOTE_SUSPEND), suspHandler, FALLING);
Added an ISR for the new interrupt:
void suspHandler() { unsigned long currentMillis = millis(); if((unsigned long)(currentMillis - suspenseTime) > BOUNCE_TIME) { suspRequest = true; suspenseTime = currentMillis; } }
Then a new state.
void state7() { unsigned long trafficMillis = millis(); unsigned long pedMillis = trafficMillis; digitalWrite(AMBER_TRAFFIC, HIGH); digitalWrite(RED_TRAFFIC, LOW); digitalWrite(CROSS, HIGH); digitalWrite(DONT_CROSS, LOW); digitalWrite(GREEN_TRAFFIC, LOW); while(true) { if(suspRequest) { return;} unsigned long currentMillis = millis(); if((unsigned long)(currentMillis - trafficMillis) >= HALF_SEC){ digitalWrite(AMBER_TRAFFIC, digitalRead(AMBER_TRAFFIC) ^ 1); trafficMillis = currentMillis; } if((unsigned long)(currentMillis - pedMillis) >= THRD_SEC){ digitalWrite(CROSS, digitalRead(CROSS) ^ 1); pedMillis = currentMillis; } } }
Then added a line to state1() at the top of the while() loop:
if(suspRequest) { return;}
Following that, changed the main loop() function to read:
void loop() { for(int i = 0; i < 6; i++){ if(suspRequest) { suspRequest = false; suspended = !suspended; if(suspended) { states[0] = state7; i = 0; } else { states[0] = state1; i = 1; // restart at pedestrian safe point } } states[i](); } }
Important point there. Always restart in a safe state. It could be your granny trying to cross the road or your fingers under the laser cutter.
My version made use of the ability to change the function the first pointer in the states[] array pointed to and took a small liberty with the int value (i) used by the for loop. Because I was using a button as a suspended state “toggle” I had to ensure that the press was de-bounced in the suspHandler() function although that had not been an issue for the pedestrian request button. Why not? Well the programmed delayed response of the state machine effectively de-bounced the button for us.
Extra points if your working solution is different and double points if you are happy to stand by your choices. Triple points if you spotted and corrected the LED light bug (in my code anyway) in state2() following the suspended state.
Review
There are two important messages that I hope this chapter delivers.
The first is, build programs up in stages. That way, each code step can be tested as it is made and any interaction with existing code checked for inadvertent bugs. This is the only effective way I have found for developing large programs and indeed more complex multi-program systems.
The second message is that if your project can be divided into a number of “states” then it is good practice to isolate each state in a separate function even if some or all such functions share some common code placed into (what shall we call them?) service functions. Given this design pattern, it greatly simplifies things if each main state function manages its own termination with successive states following on in sequence. Avoid having one state function directly calling another.