2

I'm quite new to Arduino and the programming of the board, but I wanted to have a go at making a traffic light based on an example I found online.

The example supplied a simple layout which I roughly copied with the components I had.

enter image description here

I then created the following code to mimic the traffic lights of my local area.

int red = 13;   int yellow = 12;   int green = 11;

void setup(){
    pinMode(red,OUTPUT);
    pinMode(yellow,OUTPUT);
    pinMode(green,OUTPUT);
}

void loop(){
    changeLights();
}

void changeLights(){
    // red on, other off for 3 seconds
    digitalWrite(green,LOW); 
    digitalWrite(yellow,LOW);
    digitalWrite(red,HIGH); 
    delay(3000);

    // red on, amber on,green off for 3 seconds 
    digitalWrite(green,LOW);
    digitalWrite(yellow,HIGH); 
    digitalWrite(red,HIGH); 
    delay(3000);

    // red off, amber off,green on for 3 seconds 
    digitalWrite(green,HIGH);
    digitalWrite(yellow,LOW); 
    digitalWrite(red,LOW); 
    delay(3000);

    // red off, amber on,green on for 3 seconds 
    digitalWrite(green,HIGH);
    digitalWrite(yellow,HIGH); 
    digitalWrite(red,LOW); 
    delay(3000);
}

What I'm wanting to achieve next is a button which resets the code back to the top of void changeLights. I assume I would be able to do this using a push-to-make switch, but I'm not sure how to incorporate it into my circuit or if it is even possible to reset using this.

  • Jumping to the top of changeLights() is not the best idea. Wouldn't you like to have an interupt service routine that clears all leds and then returns to the loop() function? – ott-- Mar 3 '16 at 8:48
  • I'm not sure what the best practice is, But now is the best time to learn. Point me in the right direction please. – Terry Mar 3 '16 at 8:49
  • 1
    The reset button already does more or less what you want. If you do not want to reset the whole program, start by removing the call to delay() as in the blink without delay Arduino example. Then you will be able to poll a button in parallel. BTW, you will drive the motorists crazy if green is lit for only three seconds. – Edgar Bonet Mar 3 '16 at 9:49
4

As currently written, your changeLights() function, which is repeatedly called over and over again by the Arduino loop() function, runs a timed sequence of lights.

  • Red light is on for 3000 ms
  • Amber is on for 3000 ms
  • Green is on for 3000 ms

This sequence could also be implemented using a state machine, but as it is right now, it's just a simple sequence of operations.

So now you have a new feature request (heh, this happens all the time in pro software development):

  • hardware will include a momentary switch to ground
  • when the switch is pressed, the changeLights() sequence will skip to the next Red light
  • we might also want to do other kinds of sequence change, not just reset

The simplest way to code this would be to use digitalRead() to test whether the switch is pressed. You could test just after each of the delay(3000) calls. Technically this would work, but it would only sense the switch state just at the instant that digitalRead() is called. So for most of your 9-second sequence, it will ignore the switch, and only briefly test. The user will have to hold the button for 3 seconds to be sure the program senses the switch press.

There are some better ways to make this work. The delay(3000) actually makes the program do nothing else but watch the clock for the full delay interval, it doesn't do anything else during this time. So instead of delay(3000) you could use a for loop, and test whether the button has been pressed.

// Spend 3 seconds waiting; exit the function if BUTTON1 is pressed.
for(int count = 300; count > 0; count--) {
    delay(10); // test the button every 10 ms
    if (digitalRead(BUTTON1) == LOW) {
        return; // skip out of the changeLights() function
    }
}

(Just saw Edgar Bonet's comment about the blink without delay Ardino example, which is another valid way to do the same thing. You could also use the millis() function and calculate the time when the next state transition should happen.)

Since this is an educational example, it's worth looking into how you would implement this as a state machine. (The traffic lights controller is often used as an introduction to state machines.) There's two types of state machines, Mealy and Moore, the difference is that one kind the outputs are determined only by the internal state, and the other kind, the outputs are a function of the internal state and the inputs.

A simple traffic light controller state machine might look like this:

  • state 0: initialize; next state=1
  • state 1: Red light on, timer=5 seconds, next state=2
  • state 2: Green light on, timer=3 seconds, next state=3
  • state 3: Amber light on, timer=1 seconds, next state=1

Use a timing countdown like in the example above, and when the countdown reaches 0, advance to the next state.

const int S0_INIT = 0; // state 0: initialize
const int S1_RED = 1; // state 1: Red light on
const int S2_GREEN = 2; // state 2: Green light on
const int S3_AMBER = 3; // state 3: Amber light on
int state = S0_INIT;
int stateTimer = 0;
// void setup() not shown; configures input and output pins by pinMode(...)
void loop()
{
    if (stateTimer < 0) { stateTimer = 0; state = S0_INIT; return; }
    if (stateTimer > 0) {
        stateTimer = stateTimer - 1;
        delay(10); // 10 ms interval
        return;
    }
    switch(state) {
    case S0_INIT: // state 0: initialize; next state=1
        stateTimer = 0;
        state = S1_RED;
        break;
    case S1_RED: // state 1: Red light on, timer=5 seconds, next state=2
        // digitalWrite(... etc.) Red light on
        stateTimer = 500;
        state = S2_GREEN;
        break;
    case S2_GREEN: // state 2: Green light on, timer=3 seconds, next state=3
        // digitalWrite(... etc.) Green light on
        stateTimer = 300;
        state = S3_AMBER;
        break;
    case S3_AMBER: // state 3: Amber light on, timer=1 seconds, next state=1
        // digitalWrite(... etc.) Amber light on
        stateTimer = 100;
        state = S1_RED;
        break;
    }
}

A more advanced traffic light controller (that responds to input) might look like this:

  • state 0: initialize; next state=1
  • state 1: Red light on, next state=2 (unless SWITCH1=HIGH, next state=4)
  • state 2: Green light on, next state=3 (unless SWITCH1=HIGH, next state=4)
  • state 3: Amber light on, next state=1 (unless SWITCH1=HIGH, next state=4)
  • state 4: Red light on, next state=5 (unless SWITCH1=LOW, next state=2)
  • state 5: no lights on, next state=4 (unless SWITCH1=LOW, next state=2)

An external input SWITCH1 switches this traffic controller between Red-Amber-Green sequence (states 1-2-3) and flashing-Red sequence (states 4-5). Adding constants for S4_BLINKING_RED=4 and S5_BLINKING_RED=5 and replacing the switch(state):

    switch(state) {
    case S0_INIT:
        stateTimer = 0;
        state = S1_RED;
        break;
    case S1_RED:
        // digitalWrite(... etc.) Red light on
        stateTimer = 500;
        state = S2_GREEN;
        if (digitalRead(SWITCH1) == HIGH) {
            state = S4_BLINKING_RED;
        }
        break;
    case S2_GREEN:
        // digitalWrite(... etc.) Green light on
        stateTimer = 300;
        state = S3_AMBER;
        if (digitalRead(SWITCH1) == HIGH) {
            state = S4_BLINKING_RED;
        }
        break;
    case S3_AMBER:
        // digitalWrite(... etc.) Amber light on
        stateTimer = 100;
        state = S1_RED;
        if (digitalRead(SWITCH1) == HIGH) {
            state = S4_BLINKING_RED;
        }
        break;
    case S4_BLINKING_RED:
        // digitalWrite(... etc.) Red light on
        stateTimer = 50;
        state = S5_BLINKING_RED;
        if (digitalRead(SWITCH1) == LOW) {
            state = S2_GREEN;
        }
        break;
    case S5_BLINKING_RED:
        // digitalWrite(... etc.) no lights on
        stateTimer = 50;
        state = S4_BLINKING_RED;
        if (digitalRead(SWITCH1) == LOW) {
            state = S2_GREEN;
        }
        break;
    }

(Remember to define the SWITCH1 pin, and configure pinMode(SWITCH1 ) in setup().)

Of course a real traffic light controller has more lights, say Northbound-Red, Northbound-Amber, Northbound-Green, Westbound-Red, Westbound-Amber, Westbound-Green, etc. Not to mention left turn arrows and maybe even "vehicle present" sensors. For a smart traffic light controller like that, the state is named by what traffic flow is authorized during that state:

  • state 1: northbound-southbound traffic green, 5 seconds, next state 2
  • state 2: northbound-southbound traffic amber, 2 seconds, next state 3
  • state 3: all lights red, 0.5 second, next state 4 (unless no vehicles waiting west-east lanes, then next state 6)
  • state 4: westbound-eastbound traffic green, 5 seconds, next state 5
  • state 5: westbound-eastbound traffic amber, 2 seconds, next state 6
  • state 6: all lights red, 0.5 second, next state 7 (unless no vehicles waiting N-W nor S-E left turn bays, then next state 9)
  • state 7: north-to-west left turn and south-to-east left turn green, 3 seconds, next state 8
  • state 8: north-to-west left turn and south-to-east left turn amber, 2 seconds, next state 9
  • state 9: all lights red, 0.5 second, next state 10 (unless no vehicles waiting W-S nor E-N left turn bays, then next state 12)
  • state 10: west-to-south left turn and east-to-north left turn green, 3 seconds, next state 11
  • state 11: west-to-south left turn and east-to-north left turn amber, 2 seconds, next state 1
  • state 12: all lights red, 0.5 second, next state 1 (unless no vehicles waiting north-south lanes, then next state 3)

The power of using state machines is that they all use the same basic "engine", just with more states, more outputs, more inputs. If the traffic engineer wants to adjust the timing to improve the flow of traffic, that only requires adjusting some of the data, which is much lower risk than rewriting code. ... and of course state machines apply to more than just traffic lights, it's a very generally useful design technique.

| improve this answer | |
  • Agreed, a state machine allow for much more complex situations, while still being readable. – Nick Gammon Mar 4 '16 at 5:02
3

MarkU provided a pretty thorough answer with all the whys and hows of state machines. This is definitely the way to go, and I encourage you to read that answer very carefully.

I wrote a comment to your question where I suggest dropping delay() in order for your loop to never block and your program to be responsive to user input. Now I am writing this answer only to show you how to combine both ideas, i.e. implement a state machine without delays. I do not think this code needs any explanation besides those in MarkU's answer and in the Blink without delay Arduino example.

// Pinout.
const int red = 13, yellow = 12, green = 11, reset = 10;

void setup() {
    pinMode(red,    OUTPUT);
    pinMode(yellow, OUTPUT);
    pinMode(green,  OUTPUT);
    pinMode(reset,  INPUT_PULLUP);  // use internal pull-up
}

void loop() {
    static enum { INIT, RED, RED_AMBER, GREEN, GREEN_AMBER } state = INIT;
    static uint32_t duration;
    static uint32_t last_change;
    uint32_t now = millis();

    // Sense the reset button.
    if (digitalRead(reset) == LOW)
        state = INIT;

    // Wait at this state for the specified duration.
    if (state != INIT && now - last_change < duration)
        return;

    // Switch to next state.
    switch (state) {
        case INIT:
        case GREEN_AMBER:
            state = RED;
            duration = 3000;
            digitalWrite(green,  LOW);
            digitalWrite(yellow, LOW);
            digitalWrite(red,    HIGH);
            break;
        case RED:
            state = RED_AMBER;
            duration = 3000;
            digitalWrite(green,  LOW);
            digitalWrite(yellow, HIGH);
            digitalWrite(red,    HIGH);
            break;
        case RED_AMBER:
            state = GREEN;
            duration = 3000;
            digitalWrite(green,  HIGH);
            digitalWrite(yellow, LOW);
            digitalWrite(red,    LOW);
            break;
        case GREEN:
            state = GREEN_AMBER;
            duration = 3000;
            digitalWrite(green,  HIGH);
            digitalWrite(yellow, HIGH);
            digitalWrite(red,    LOW);
            break;
    }
    last_change = now;
}

I would normally have declared duration as a static const. I kept it non-const only to follow MarkU's steps and make it easy to have different durations for different states (really, the green light should last longer...).

| improve this answer | |
  • Very good! I would make pinMode(reset, INPUT_PULLUP); to save having to have an external pull-up resistor. – Nick Gammon Mar 4 '16 at 5:01
  • @NickGammon: Good point! I edited the answer to follow your suggestion. – Edgar Bonet Mar 4 '16 at 8:31
0

The best way to handle this problem is using a state machine, as shown in all the other answers. In fact, this IS the classical problem to learn how to use and implement state machines.

I just wanted to add the "fast" solution to your problem, which will enable you to do what you want without changing your code too much:

void changeLights(){
    unsigned long startTime;

    // red on, other off for 3 seconds
    digitalWrite(green,LOW); 
    digitalWrite(yellow,LOW);
    digitalWrite(red,HIGH); 
    startTime = millis();
    while ((millis() - startTime) < 3000)
    { // Wait for 3 seconds while checking for the button
        if (digitalRead(buttonPin) == HIGH)
            return; // Just exit this function; the loop will start again
    }

    [ repeat for the other colors ]
}
| improve this answer | |

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.