Two traffic lights at a crossroads

Two Arduinos talk over a wire: traffic light A controls the crossroads, B listens and reports when it has stopped, and with millis() both notice a fault.

Arduino Electronics
Two traffic lights at a crossroads

Last time each person built a traffic light for cars and pedestrians. At a real crossroads, traffic lights don’t work on their own: while one road has green, the other must have red. So the traffic lights have to agree with each other.

Today we will connect two Arduinos with a wire. Two people who sit next to each other build one crossroads together. Traffic light A controls the crossroads and tells traffic light B when it may go, and traffic light B listens to these messages.

We will also learn a new command, millis(). With it the Arduino can measure time without stopping to watch what is happening around it. That is exactly what traffic light B needs: when traffic light A breaks down, B must notice it by itself.

At the end we will add a second wire, going the other way. Through it B tells traffic light A that it has stopped, so A no longer has to guess.

Our crossroads

Traffic light A controls one road, and traffic light B controls the crossing road

Traffic light A controls the cars and pedestrians on one road, and traffic light B those on the crossing road. As last time, the pedestrians walk alongside their cars and cross the side street. When A has green, its pedestrians cross traffic light B’s road, so B must have red at that time. And the other way round.

The most important rules of the crossroads:

  • when one traffic light isn’t showing red, the other must show red;
  • between them there is always at least one second when both show red. In that second, a car that entered the crossroads on yellow has time to leave it.

Why not just start both traffic lights at the same time?

We could start both traffic lights in the same second and give traffic light B a program shifted by half a cycle. It would work for a while, but not for long:

  • Nobody can press two reset buttons in exactly the same millisecond.
  • Each Arduino has its own clock, and two clocks never run exactly the same. The difference is tiny, but it adds up: after an hour the traffic lights could be several seconds apart.
  • When one Arduino resets, for example while we upload a program to it, it starts from the beginning. The other carries on as if nothing had happened.

That is why the traffic lights must keep talking to each other.

Who is in charge?

At our crossroads traffic light A is in charge. It measures all the times, and over the wire it sends traffic light B only two messages: go and stop. B doesn’t decide by itself when to go. It only decides how to show the message with its lights.

Why can't both traffic lights be in charge?

Imagine that both traffic lights have the same rule: "When the other one has red, I may switch on green." After switching on, both have red. Both look at the other one, both see red, and both switch on green!

Programmers call a mistake like this a race condition: the result depends on who is faster. The simplest cure is to have only one in charge. That is why a real crossroads has one controller that runs all of its traffic lights.

A message through a wire

A wire can carry only two states: HIGH (about 5 V) and LOW (0 V). That is enough for our two messages:

Wire stateMessage from AWhat B does
HIGHgored + yellow, then green
LOWstopgreen blinks, yellow, then red

Why does LOW mean stop? Think about everything that can happen: someone pulls out the wire, someone pulls out traffic light A’s USB cable, or A resets while we upload a program to it. In all these cases there is no voltage on the wire, so B reads LOW and stops. Every fault on the link means stop, and never go. Engineers say that such a link is fail-safe.

How the two traffic lights work together

TimeTraffic light AWireTraffic light B
0–4 sredLOWgreen blinks (2 s), yellow (1 s), red
4–5 sred + yellowLOWred
5–9 sgreen, pedestrians A walkLOWred
9–11 sgreen blinksLOWred
11–12 syellowLOWred
12–13 sredLOWred
13–14 sredHIGHred + yellow
14–18 sredHIGHgreen, pedestrians B walk
⇒back to the start, wire LOW again

The whole cycle lasts 18 s. Each road has the same states as last time: red + yellow 1 s, green 4 s, blinking 2 s and yellow 1 s. Between seconds 3 and 4, and between seconds 12 and 13, both traffic lights show red.

We can draw the same table as a timing diagram. Time runs from left to right, and each row shows one traffic light, one group of pedestrians or one wire. The higher the line in a traffic light’s row, the further that traffic light is in its cycle. The grey columns are the seconds when everyone is stopped.

Timing diagram: traffic light A, traffic light B and the link wire during one cycle

Traffic light A can’t see traffic light B’s lights. So after the stop message it waits 4 s: 2 s for the blinking, 1 s for the yellow and 1 more second when everyone has red. Only then does it switch on its red + yellow. In Exercise 6 we will fix this: B will report by itself when it has stopped.

Materials

Each person gets:

  • everything from the last lesson: an Arduino Uno, a USB cable, a large breadboard, the 3D-printed traffic light, LEDs for the pedestrians, the buzzer, resistors and jumper wires
  • 2 × 470 Ω resistor (yellow-violet-brown)
  • 2 × 10 kΩ resistor (brown-black-orange)

Each crossroads (two people) also gets:

  • three longer jumper wires (30–50 cm) in three colours: purple for the link, orange for the confirmation (Exercise 6) and black for GND

At the start, each person wires the traffic light exactly as at the end of the last lesson (Exercises 1, 4 and 5) and uploads its whole program. The wiring rules are the same as last time. When the traffic light works as it did last time, you are ready.

Exercise 1: The clock inside the Arduino

While delay() is running, the Arduino does nothing else: it just waits. Traffic light B mustn’t wait like that, because it has to watch the wire all the time. We need a different way of measuring time.

Every Arduino has a built-in stopwatch. It starts when the Arduino is powered on or reset, and it counts milliseconds. The command millis() tells us how far the stopwatch has counted so far.

How much time has passed?

// C++
void setup() {
  Serial.begin(9600);
}

void loop() {
  Serial.println(millis());
  delay(1000);
}

Open the Serial Monitor (9600 baud). Each new number is about 1000 bigger than the one before. Press the reset button on the Arduino: the counting starts from the beginning.

What is new in the program?

  • millis() – Returns the number of milliseconds since the Arduino was switched on or reset. 1000 ms is one second. We can’t stop the stopwatch or set it back to zero from the program: it always counts on.
How much time has passed since switching on if millis() returns 90000?
90 000 ms = 90 s, or a minute and a half.

Two lights, two rhythms

Now we want the yellow for cars to blink every 0.5 s and the green for pedestrians every 0.3 s. Try to imagine how you would write this with delay(). While we wait half a second for the yellow, we can’t change the green!

With millis() we do it differently, as when we glance at a clock from time to time. We don’t wait; we only check: is it time for a change yet? If not, we carry on.

Plan before you program:
repeat forever:
if 500 ms have passed since the last change of the yellow:
remember the current time
change the yellow
if 300 ms have passed since the last change of the pedestrian green:
remember the current time
change the pedestrian green
// C++
const int carYellow = 9;
const int pedestrianGreen = 6;

unsigned long lastYellow = 0;       // when we last changed the yellow
unsigned long lastPedestrian = 0;   // when we last changed the pedestrian green
bool yellowOn = false;
bool pedestrianOn = false;

void setup() {
  pinMode(carYellow, OUTPUT);
  pinMode(pedestrianGreen, OUTPUT);
}

void loop() {
  if (millis() - lastYellow >= 500) {
    lastYellow = millis();
    yellowOn = !yellowOn;
    digitalWrite(carYellow, yellowOn);
  }

  if (millis() - lastPedestrian >= 300) {
    lastPedestrian = millis();
    pedestrianOn = !pedestrianOn;
    digitalWrite(pedestrianGreen, pedestrianOn);
  }
}

Each light blinks in its own rhythm, as if two different programs were running them.

What is new in the program?

  • unsigned long lastYellow = 0; – A long is a whole number that can be very big, and unsigned means without a sign, so no negative numbers. Such a variable can hold numbers up to about 4.29 billion. We always store the time from millis() in an unsigned long. On many Arduinos an int only goes up to 32 767, and in milliseconds that is just 33 seconds.
  • millis() - lastYellow – The current time minus the time of the last change. That is the number of milliseconds that have passed since the last change.
  • if (millis() - lastYellow >= 500) – If at least 500 ms have passed, it is time for a change. If not, the program doesn’t wait, but carries on straight away.
  • lastYellow = millis(); – We remember the moment of the change. From it we count the next 500 ms.
  • yellowOn = !yellowOn; – As with ledMode in lesson 2.1: true becomes false, and false becomes true.
  • digitalWrite(carYellow, yellowOn); – For the Arduino, true is the same as HIGH (the number 1), and false the same as LOW (the number 0).
  • There isn’t a single delay() in loop(). So loop() runs thousands of times a second and each time just glances at the clock. In between it can do other things: this program looks after two lights, and traffic light B will also look after the wire.

What happens when the stopwatch is full?

After about 49 days and 17 hours, millis() reaches the biggest number an unsigned long can hold and carries on counting from zero, like the hand of a clock that goes from 12 to 1.

That is why we always write millis() - lastYellow >= 500, with a subtraction. The subtraction gives the correct gap even when the counting goes past zero, just as 2 hours pass from 11 o'clock to 1 o'clock, even though 1 is less than 11. If we had written millis() >= lastYellow + 500, the traffic light would go briefly crazy at that moment: the yellow would flash quickly a few times, and traffic light B could start blinking yellow for no reason.

Experiment: change 300 to 1000, then to 100. Each light changes its rhythm, and the other doesn’t notice the change at all.

Exercise 2: Two Arduinos, one wire

Two people who sit next to each other agree which of them is A and which is B.

The link between traffic light A and traffic light B

The schematic shows board A. Board B is wired exactly the same. J1 and J2 are connectors: the places where the long wires leave board A. The labels B: D2 and B: GND show which pin of Arduino B each long wire goes to.

Reading a schematic: wires that cross

On a schematic, lines sometimes have to cross, because there is no other way to draw them. So we look out for dots:

  • Connected wires A dot where lines meet means that the wires are connected.
  • Wires not connected Lines that cross without a dot are not connected: one wire just passes over the other.

Look at our schematic: the wire to B: D2 crosses the vertical GND wire, but there is no dot at that point. So the link and GND are not connected. If they were, the link would always be LOW and B would never go.

Rules for connecting two Arduinos

  1. While we are wiring, both Arduinos are unplugged from their USB cables.
  2. Between the two boards we connect only the message wires and GND. We never connect the 5 V pin.
  3. We wire both boards the same way: for each message wire, each board has its own 470 Ω resistor and its own 10 kΩ resistor. That way we can later swap roles without rewiring.
  4. Before we plug in the USB cables, the workshop leader checks the circuit.

Wiring

On both boards we add the link in row 29. As last time, the wire from the Arduino goes into the left half of the row, and the resistor crosses the centre gap:

PartArduino pinWire from the Arduino470 Ω resistorRight side
LinkD2a29e29–f2910 kΩ resistor from i29 to the right – rail; long link wire in h29

Then two long wires connect the boards:

WireBoard ABoard B
Link (purple)h29h29
GND (black)right – railright – rail
Wiring the two Arduinos

What does each part do?

  • Common GND. A voltage is always the difference between two points, like a height: when we say that something is 5 m high, we have to know whether we measure from the floor or from the roof. When A sends 5 V, it means “5 V more than my GND”. B can understand the message only if it measures the voltage from the same GND. Without this wire the message means nothing.
  • 10 kΩ resistor: pull-down. In the lesson Arduino - step by step a pull-up resistor pulled the transistor’s base towards +5 V. This resistor pulls the link towards GND, so it is called a pull-down. When nobody is sending HIGH, for example when the wire is pulled out, the link is reliably LOW. Without it the pin would be floating: it would pick up interference and read HIGH or LOW at random. This resistor is what makes the link fail-safe: when there is no message, B reads stop.
  • 470 Ω resistor: protection. What if we upload the program for A to both Arduinos by mistake? Then both pins send a voltage, and when one sends HIGH and the other LOW, current would flow straight from one pin into the other. A short circuit like that can damage the pins. By Ohm’s law (I = U / R), with two 470 Ω resistors the current is at most 5 V / 940 Ω ≈ 5 mA, which doesn’t harm the pins. When everything works as it should, only a very small current flows through the resistors and they don’t disturb the message.

Before the traffic lights, let’s check that the link works. On every Arduino there is a small LED labelled L. It is connected to pin D13, and in the program it is called LED_BUILTIN.

The program for Arduino A sends messages: two seconds HIGH, two seconds LOW. It switches its own L on together with the message.

// C++
const int linkWire = 2;

void setup() {
  pinMode(linkWire, OUTPUT);
  pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
  digitalWrite(linkWire, HIGH);
  digitalWrite(LED_BUILTIN, HIGH);
  delay(2000);
  digitalWrite(linkWire, LOW);
  digitalWrite(LED_BUILTIN, LOW);
  delay(2000);
}

The program for Arduino B reads the wire and shows on its own L what it has read.

// C++
const int linkWire = 2;

void setup() {
  pinMode(linkWire, INPUT);
  pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
  int message = digitalRead(linkWire);
  digitalWrite(LED_BUILTIN, message);
}

Upload both programs. L on A and L on B must switch on and off together.

Now pull the long link wire out of h29 on board B. L on B must go out and stay out, even though L on A keeps blinking. This tells us that the pull-down resistor works. Put the wire back.

What is new in the program?

  • pinMode(linkWire, INPUT); – The pin is an input: the Arduino only measures the voltage on it and doesn’t send any.
  • digitalRead(linkWire) – Reads the pin. It returns HIGH if the voltage on the pin is close to 5 V, and LOW if it is close to 0 V. It is the partner of digitalWrite(): one Arduino writes, and the other reads.
  • int message = digitalRead(linkWire); – We remember the state we read in the variable message.
  • digitalWrite(LED_BUILTIN, message); – L shows what B has read: it is on when the message is HIGH.
  • LED_BUILTIN – The name of the pin that the built-in LED L is connected to. We don’t have to remember that it is pin 13.

Exercise 3: Traffic light A is in charge

Exercises 3 and 4 are done at the same time: person A writes the program for traffic light A, and person B the program for traffic light B.

Person A starts from the whole program from the last lesson. The function lights() stays exactly the same. In loop() we add the states from the crossroads table and the messages for B.

Plan before you program:
repeat forever:
red, wait 4 s (B finishes its green and yellow)
red + yellow, wait 1 s
green, wait 4 s
green blinks, 2 s
yellow, wait 1 s
red, wait 1 s (everyone has stopped)
send B: go
red, wait 5 s (B is driving)
send B: stop

Below the other constants, add:

// C++
const int linkWire = 2;        // wire to traffic light B

At the end of setup(), add:

// C++
  pinMode(linkWire, OUTPUT);
  digitalWrite(linkWire, LOW);     // at the start, B stays stopped

Replace loop() with this:

// C++
void loop() {
  lights(HIGH, LOW, LOW, 4000);     // red: B finishes its green and yellow
  lights(HIGH, HIGH, LOW, 1000);    // red + yellow
  lights(LOW, LOW, HIGH, 4000);     // green
  lights(LOW, LOW, BLINK, 2000);    // green blinks
  lights(LOW, HIGH, LOW, 1000);     // yellow
  lights(HIGH, LOW, LOW, 1000);     // red: everyone has stopped

  digitalWrite(linkWire, HIGH);         // message for B: go
  lights(HIGH, LOW, LOW, 5000);     // red: B has red + yellow, then green
  digitalWrite(linkWire, LOW);          // message for B: stop
}

What is new in the program?

  • pinMode(linkWire, OUTPUT); – A sends messages, so its link pin is an output, like the pins for the LEDs.
  • digitalWrite(linkWire, LOW); – Right at the start we send stop. Until A decides otherwise, B stays stopped.
  • digitalWrite(linkWire, HIGH); – The go message. With the same command we use to switch on an LED, we now send a message to another Arduino.
  • lights(HIGH, LOW, LOW, 5000); – While B is driving, A has red. A also decides how long B has green: 5 s of the go message, of which 1 s is red + yellow and 4 s are green.
  • Why does loop() start with 4 s of red? Imagine that A resets while B has green. At that moment the link pin is left without voltage, so B reads stop and starts blinking. A starts from the beginning: first setup(), then loop(). If loop() started with red + yellow, A would have green after just 1 s, while B would still be blinking: both roads would be driving! This way A always holds red for 4 s first and gives B enough time to stop.
  • A may use delay(). Traffic light A doesn’t listen to anyone; it only talks. While it waits, it can’t miss a message.

If the test program from Exercise 2 is still running on B, L on B lights up exactly when B should have red + yellow and green. Compare that with the lights of traffic light A.

How long does B have green if, in the call lights(HIGH, LOW, LOW, 5000), we change 5000 to 8000?
7 s. The go message then lasts 8 s, and for the first second of it B has red + yellow. We didn’t have to change traffic light B’s program, because A decides how long B’s green lasts.

Exercise 4: Traffic light B listens

Person B also starts from the whole program from the last lesson. The function lights() stays the same, but B no longer measures how long red and green last. It waits for a message.

Plan before you program:
repeat forever:
red
wait while the wire says stop
red + yellow, wait 1 s
if the wire still says go:
green
wait while the wire says go
green blinks, 2 s
yellow, wait 1 s

Below the other constants, add:

// C++
const int linkWire = 2;        // wire from traffic light A

At the end of setup(), add:

// C++
  pinMode(linkWire, INPUT);

Replace loop() with this:

// C++
void loop() {
  lights(HIGH, LOW, LOW, 0);            // red
  while (digitalRead(linkWire) == LOW) {    // wait until A sends go
  }

  lights(HIGH, HIGH, LOW, 1000);        // red + yellow

  if (digitalRead(linkWire) == HIGH) {      // is A still saying go?
    lights(LOW, LOW, HIGH, 0);          // green
    while (digitalRead(linkWire) == HIGH) { // drive until A sends stop
    }
    lights(LOW, LOW, BLINK, 2000);      // green blinks
    lights(LOW, HIGH, LOW, 1000);       // yellow
  }
}

What is new in the program?

  • while (digitalRead(linkWire) == LOW) { } – A while loop repeats the commands between { } while the condition is true. Here the braces are empty, so the program just reads the wire again and again, thousands of times a second. As soon as A sends HIGH, the condition is no longer true and the program carries on.
  • for or while? We use a for loop when we know how many times to repeat something. We use a while loop when we are waiting for something to happen and don’t know how long it will take.
  • lights(HIGH, LOW, LOW, 0); – Duration 0: the function sets the lights and returns straight away, without waiting. How long the red lasts isn’t decided by B, but by A.
  • if (digitalRead(linkWire) == HIGH) – During red + yellow, B doesn’t look at the wire for one second, because the function lights() waits with delay(). What if A resets during exactly that second? Then B mustn’t switch on green. So before the green we check the wire once more. If it is LOW, B skips the whole green part, loop() starts from the beginning and B goes back to red.
  • Why may we wait during the blinking and the yellow? After the stop message, A holds red for 4 s anyway and doesn’t send a new message, so B doesn’t miss anything.
  • Pedestrians and sound work by themselves. The function lights() is the same as last time, so B’s pedestrians walk when B has steady green. We didn’t write a single new line for them.

Both traffic lights together

Upload program A to Arduino A and program B to Arduino B. Watch the lights of both traffic lights and compare them with the crossroads table. Check both rules: when one traffic light isn’t showing red, the other shows red, and between them there is always a second when both show red.

Why does B have red after switching on, even though nobody told it to?
Traffic light B’s loop() starts with red. Then B waits in the while loop until A sends go. If A isn’t running yet, the wire is LOW because of the pull-down resistor, so B stays on red.

Exercise 5: When traffic light A breaks down

Pull out the long link wire while B has red. What happens?

B has red and waits. And waits. A fault like this is safe, because nobody drives through on green on the wrong road. But the drivers on road B would wait forever in front of a traffic light that looks perfectly fine. Put the wire back.

Real traffic lights blink yellow when they are out of order. Then drivers know that the traffic light isn’t working and follow the road signs. We will teach our traffic light B to do the same:

  • In normal operation, B waits on red for 10 s (from second 3 to second 13 in the crossroads table).
  • If it waits longer than 15 s, something is wrong: B starts blinking yellow.
  • As soon as A sends go, B carries on normally.

Here we can’t use delay(): B must measure how long it has been waiting, blink yellow and watch the wire at the same time. For that we need millis(), exactly as in Exercise 1.

Plan before you program:
red
remember when we started waiting
while the wire says stop:
if we have been waiting longer than 15 s:
blink yellow
(the rest as before)

Changing the program

Below the other constants, person B adds:

// C++
const unsigned long MAX_WAIT = 15000;   // after 15 s without a message: fault

bool yellowOn = false;          // for blinking the yellow during a fault
unsigned long lastBlink = 0;    // when the yellow last changed

Below the function lights(), add a new function:

// C++
void blinkYellow() {
  if (millis() - lastBlink >= 500) {   // half a second has passed
    lastBlink = millis();
    yellowOn = !yellowOn;
    lights(LOW, yellowOn, LOW, 0);     // yellow on or off
  }
}

In loop(), change only the beginning, up to and including the first while loop:

// C++
void loop() {
  lights(HIGH, LOW, LOW, 0);                   // red
  unsigned long waitStart = millis();          // NEW: when we started waiting

  while (digitalRead(linkWire) == LOW) {           // wait until A sends go
    if (millis() - waitStart >= MAX_WAIT) {    // NEW
      blinkYellow();                           // A has been silent for too long
    }
  }

  // the rest of loop() stays the same

What is new in the program?

  • const unsigned long MAX_WAIT = 15000; – How long B may wait on red. We write the name in capital letters, like BLINK, because it doesn’t change while the program runs. The type is unsigned long because we compare it with the time from millis().
  • unsigned long waitStart = millis(); – We remember the moment when B switched on red. The variable is written inside loop(), so with every new red it gets a new time, and B has the full 15 s again.
  • millis() - waitStart >= MAX_WAIT – The same form as in Exercise 1: how much time has passed since we started waiting? If 15 s or more have passed, A has been silent for too long.
  • void blinkYellow() – The same idea as the yellow in Exercise 1. The important thing is that this function never waits. Each time it is called, it just glances at the clock and, if half a second has passed, changes the yellow. That is why the while loop still runs thousands of times a second, and B notices the go message as soon as it arrives.
  • lights(LOW, yellowOn, LOW, 0); – As in Exercise 1, true means HIGH and false means LOW. With one call we get the yellow either on or off.
  • During a fault the pedestrians wait. The function lights() lets pedestrians walk only when the cars have steady green, and during a fault they never have it.
  • What happens when A works again? After switching on, A starts its cycle, and B keeps blinking yellow in the meantime. Only when A sends go does B leave the while loop, switch on red + yellow and return to normal operation.

Where do we write variables?

Why do we write lastBlink and yellowOn above setup(), but waitStart inside loop()?

  • A variable above setup() is created once, when the program starts, and keeps its value for as long as the program runs. Every part of the program can use it: loop() and all our functions.
  • A variable inside loop(), or inside any other function, is created again on every pass and disappears at the end of the pass. Only the part of the program between the braces where it is written can use it.

The function blinkYellow() is called thousands of times a second, and each time it must know when the yellow last changed and whether it is on now. These values must be remembered between two calls, so we write those variables above setup(). The variable waitStart is needed only during one pass through loop(), and with every new red it gets a new time anyway.

Programmers call the first kind a global variable and the second kind a local variable.

Changing the durations in program A?

Traffic light B may wait on red for at most MAX_WAIT. If you make the cycle in program A so long that B waits longer than 15 s, B will blink yellow even though A is working. Then increase that number in program B too.

Experiments

Start each experiment while both traffic lights are working normally.

1. A broken wire
Pull out the long link wire while B has red. How long does it take before B starts blinking? Put the wire back and watch how B returns to normal operation.

2. Reset
Press the reset button on Arduino A while B has green. What does B do straight away? How long does A hold red after that?

3. A new program
While B has green, upload the program to Arduino A again. Compare what happens with experiment 2.

4. No power
Pull out Arduino A’s USB cable. What does B do? Put the cable back.

Why does B in experiment 1 start blinking before 15 s have passed since the wire was pulled out?
B doesn’t count from the moment the wire was pulled out, but from the moment it switched on red. When we pulled out the wire, B had already been waiting on red for a while, so less than 15 s were left.
Why are experiments 2, 3 and 4 the same for traffic light B?
In all three cases Arduino A stops sending a voltage on the link pin: during a reset and while a program is uploaded, the pin briefly becomes an input, and without power A sends nothing at all. The pull-down resistor then holds the link at LOW, so B reads stop. With every fault B sees the same message, and that message is safe.

Exercise 6: A message back

Traffic light A still only trusts that B has stopped. After the stop message it waits 4 s, because it knows that B needs that long for the blinking, the yellow and the second when everyone has red. What if someone makes the yellow longer in program B?

Try it: in program B, in the line with the yellow, change 1000 to 3000. Watch the crossroads after B gets the stop message. A switches on red + yellow while B still has yellow, and straight after that green, without the second when everyone has stopped. At a real crossroads that would be dangerous. Leave 3000 in place; we will need it for a check.

We will fix this with a second wire, going the other way. Through it B tells traffic light A: stopped. After the stop message, A no longer waits a fixed time, but waits for this confirmation.

State of the return wireMessage from BWhat A does
HIGHstopped: B has red and is waiting for the go messagemay carry on: 1 s red, then red + yellow
LOWnot stopped, or no messagewaits on red

Here too, HIGH means the thing that is safe only when the message really arrives. If the return wire is pulled out or B has no power, the pull-down resistor holds the wire at LOW and A waits. A fault never lets A go to green.

Compare the new timing diagram with the one from the start of the lesson. When everything works normally, the lights change at the same moments. The difference is in traffic light A’s row between seconds 0 and 3: A no longer counts 4 s, but waits for the confirmation. The confirmation arrives as soon as B switches on red, and then A counts 1 more second when everyone has stopped.

Timing diagram: traffic light A, traffic light B, the link wire and the confirmation wire during one cycle

What if the confirmation never arrives? B normally needs 3 s to stop. If A waits longer than 6 s, something is wrong and A starts blinking yellow, just like B in Exercise 5. B then gets no go message, so after 15 s it blinks yellow too. The whole crossroads then blinks yellow, like a real crossroads that is out of order.

Wiring

On both boards we add the confirmation in row 30, in the same way as the link in row 29:

PartArduino pinWire from the Arduino470 Ω resistorRight side
ConfirmationD4a30e30–f3010 kΩ resistor from i30 to the right – rail; long confirmation wire in g30

A third long wire connects the boards:

WireBoard ABoard B
Confirmation (orange)g30g30
The link and the confirmation between traffic light A and traffic light B

This schematic also shows board A, and board B is wired the same way. The confirmation leaves the board through the new connector J3, labelled B: D4. Watch the crossings here too: resistor R8 passes over the link wire without a dot, so it isn’t connected to it.

Wiring the confirmation

The pins now swap roles: on the return wire, B writes and A reads.

Changing program B

Plan before you program:
repeat forever:
red
tell A: stopped
wait while the wire says stop (and blink yellow if you wait too long)
tell A: not stopped
red + yellow, wait 1 s
(the rest as before)

Below the other constants, person B adds:

// C++
const int confirmWire = 4;     // wire to traffic light A: HIGH means "B has stopped"

At the end of setup(), add:

// C++
  pinMode(confirmWire, OUTPUT);
  digitalWrite(confirmWire, LOW);  // until we switch on red, we don't report stopped

In loop(), add the two lines marked NEW:

// C++
void loop() {
  lights(HIGH, LOW, LOW, 0);                   // red
  digitalWrite(confirmWire, HIGH);                 // NEW: tell A: stopped
  unsigned long waitStart = millis();          // when we started waiting

  while (digitalRead(linkWire) == LOW) {           // wait until A sends go
    if (millis() - waitStart >= MAX_WAIT) {
      blinkYellow();                           // A has been silent for too long
    }
  }

  digitalWrite(confirmWire, LOW);                  // NEW: we are moving, no longer stopped
  lights(HIGH, HIGH, LOW, 1000);               // red + yellow

  // the rest of loop() stays the same

What is new in program B?

  • pinMode(confirmWire, OUTPUT); – On the return wire B sends messages, so pin D4 is an output.
  • digitalWrite(confirmWire, HIGH); after switching on red – First we switch on red, and only then do we report stopped. We never report something that isn’t true yet.
  • digitalWrite(confirmWire, LOW); before red + yellow – First we take back the confirmation, and only then do we move. So at no moment does A think that B has stopped while B already has red + yellow.
  • Why does B report stopped even while it blinks yellow during a fault? During a fault, B is still waiting for the go message and will never switch on green without it. If it reported not stopped during a fault, after the fault both traffic lights would wait for each other: A would wait for the confirmation, and B for the go message. They would wait forever, even with the wires working again. Programmers call this a deadlock.

Changing program A

Plan before you program:
repeat forever:
red
wait until B reports stopped (and blink yellow if you wait too long)
red, wait 1 s (everyone has stopped)
red + yellow, wait 1 s
(the rest as before)

Below the other constants, person A adds:

// C++
const int confirmWire = 4;     // wire from traffic light B: HIGH means "B has stopped"
const unsigned long MAX_WAIT = 6000;    // after 6 s without confirmation: fault

bool yellowOn = false;          // for blinking the yellow during a fault
unsigned long lastBlink = 0;    // when the yellow last changed

Below the function lights(), add the function blinkYellow(). It is the same as in program B:

// C++
void blinkYellow() {
  if (millis() - lastBlink >= 500) {   // half a second has passed
    lastBlink = millis();
    yellowOn = !yellowOn;
    lights(LOW, yellowOn, LOW, 0);     // yellow on or off
  }
}

At the end of setup(), add:

// C++
  pinMode(confirmWire, INPUT);

In loop(), replace the first line, lights(HIGH, LOW, LOW, 4000);, with this:

// C++
  lights(HIGH, LOW, LOW, 0);                   // red
  unsigned long waitStart = millis();          // when we started waiting

  while (digitalRead(confirmWire) == LOW) {        // wait until B reports: stopped
    if (millis() - waitStart >= MAX_WAIT) {
      blinkYellow();                           // B has been silent for too long
    }
  }
  lights(HIGH, LOW, LOW, 1000);                // red: everyone has stopped

The rest of loop() stays the same.

What is new in program A?

  • pinMode(confirmWire, INPUT); and digitalRead(confirmWire) – The same as for traffic light B in Exercise 2, just the other way round: now A reads.
  • Do you recognise the while loop? It is the same loop that B uses in Exercise 5 to wait for the go message: red, remember when we started waiting, wait for the message, and blink yellow if you wait too long. Only the wire and the longest wait are different.
  • MAX_WAIT = 6000 – B normally reports stopped 3 s after the stop message. We give it twice as much time before we decide that something is wrong.
  • lights(HIGH, LOW, LOW, 1000); – B reports stopped the moment it switches on red. So A still has to count the second when everyone has stopped.
  • When is traffic light A safe now? Before, A guessed how long B needs to stop. Now it knows: however long B’s yellow lasts, A waits for its confirmation. When everything works normally, the cycle still lasts 18 s.
  • A now listens too, but only in the while loop, where it doesn’t use delay(). In the rest of the cycle it only talks, so there it may wait with delay().

Both traffic lights together

Upload the new programs to both Arduinos. If the new program runs on A and the old one on B, B never reports stopped, so after 6 s A starts blinking yellow.

1. A slower B
The yellow in program B still lasts 3 s. Watch the crossroads after the stop message: A now waits for B to stop and only then counts the second when everyone has stopped. The cycle is longer, but safer. Set the yellow back to 1000.

2. A broken return wire
Pull the orange wire out of g30 on board A while A has green. Watch both traffic lights for at least 40 seconds. Which traffic light starts blinking yellow first? Put the wire back and watch how the crossroads returns to normal operation.

Why does A start blinking first in experiment 2, and B only later?
After the stop message, A waits for the confirmation, but it doesn’t arrive because the wire is pulled out. After 6 s A starts blinking. At that moment B has already stopped and is waiting for the go message, which A doesn’t send during a fault. B starts blinking only when it has been waiting on red for 15 s.
Why does A return to normal operation straight away once the wire is back?
Even during a fault, B reports stopped, because it is waiting for the go message and won’t switch on green without it. As soon as the wire is back, A reads HIGH, leaves the while loop and carries on with its cycle. When A sends go, B also returns to normal operation.

Swapping roles

Both boards are wired the same way, so you can swap roles without rewiring. Person A now writes the program for B (Exercises 4, 5 and 6), and person B the program for A (Exercises 3 and 6). When you upload the new programs, the crossroads must work exactly as before.

The whole program: traffic light A

// C++
const int carRed = 10;
const int carYellow = 9;
const int carGreen = 8;
const int pedestrianRed = 7;
const int pedestrianGreen = 6;
const int buzzer = 5;
const int linkWire = 2;        // wire to traffic light B
const int confirmWire = 4;     // wire from traffic light B: HIGH means "B has stopped"

const int BLINK = 2;                // a third value next to LOW (0) and HIGH (1)
const unsigned long MAX_WAIT = 6000;    // after 6 s without confirmation: fault

bool pedestriansWalking = false;    // remembers whether the pedestrians had green
bool yellowOn = false;              // for blinking the yellow during a fault
unsigned long lastBlink = 0;        // when the yellow last changed

void lights(int red, int yellow, int green, int duration) {
  digitalWrite(carRed, red);
  digitalWrite(carYellow, yellow);

  // Pedestrians walk only when the cars have steady green.
  if (green == HIGH && red == LOW && yellow == LOW) {
    digitalWrite(pedestrianRed, LOW);
    digitalWrite(pedestrianGreen, HIGH);
    if (!pedestriansWalking) {     // they have just got green
      tone(buzzer, 800, 60);       // short higher sound: go
      pedestriansWalking = true;
    }
  } else {
    digitalWrite(pedestrianRed, HIGH);
    digitalWrite(pedestrianGreen, LOW);
    if (pedestriansWalking) {      // they have just got red
      tone(buzzer, 150, 150);      // short lower sound: stop
      pedestriansWalking = false;
    }
  }

  if (green == BLINK) {
    for (int t = 0; t < duration; t = t + 500) {
      digitalWrite(carGreen, LOW);
      delay(250);
      digitalWrite(carGreen, HIGH);
      delay(250);
    }
  } else {
    digitalWrite(carGreen, green);
    delay(duration);
  }
}

void blinkYellow() {
  if (millis() - lastBlink >= 500) {   // half a second has passed
    lastBlink = millis();
    yellowOn = !yellowOn;
    lights(LOW, yellowOn, LOW, 0);     // yellow on or off
  }
}

void setup() {
  pinMode(carRed, OUTPUT);
  pinMode(carYellow, OUTPUT);
  pinMode(carGreen, OUTPUT);
  pinMode(pedestrianRed, OUTPUT);
  pinMode(pedestrianGreen, OUTPUT);
  pinMode(buzzer, OUTPUT);
  pinMode(linkWire, OUTPUT);
  digitalWrite(linkWire, LOW);          // at the start, B stays stopped
  pinMode(confirmWire, INPUT);
}

void loop() {
  lights(HIGH, LOW, LOW, 0);                   // red
  unsigned long waitStart = millis();          // when we started waiting

  while (digitalRead(confirmWire) == LOW) {        // wait until B reports: stopped
    if (millis() - waitStart >= MAX_WAIT) {
      blinkYellow();                           // B has been silent for too long
    }
  }
  lights(HIGH, LOW, LOW, 1000);                // red: everyone has stopped

  lights(HIGH, HIGH, LOW, 1000);    // red + yellow
  lights(LOW, LOW, HIGH, 4000);     // green
  lights(LOW, LOW, BLINK, 2000);    // green blinks
  lights(LOW, HIGH, LOW, 1000);     // yellow
  lights(HIGH, LOW, LOW, 1000);     // red: everyone has stopped

  digitalWrite(linkWire, HIGH);         // message for B: go
  lights(HIGH, LOW, LOW, 5000);     // red: B has red + yellow, then green
  digitalWrite(linkWire, LOW);          // message for B: stop
}

The whole program: traffic light B

// C++
const int carRed = 10;
const int carYellow = 9;
const int carGreen = 8;
const int pedestrianRed = 7;
const int pedestrianGreen = 6;
const int buzzer = 5;
const int linkWire = 2;        // wire from traffic light A
const int confirmWire = 4;     // wire to traffic light A: HIGH means "B has stopped"

const int BLINK = 2;                // a third value next to LOW (0) and HIGH (1)
const unsigned long MAX_WAIT = 15000;   // after 15 s without a message: fault

bool pedestriansWalking = false;    // remembers whether the pedestrians had green
bool yellowOn = false;              // for blinking the yellow during a fault
unsigned long lastBlink = 0;        // when the yellow last changed

void lights(int red, int yellow, int green, int duration) {
  digitalWrite(carRed, red);
  digitalWrite(carYellow, yellow);

  // Pedestrians walk only when the cars have steady green.
  if (green == HIGH && red == LOW && yellow == LOW) {
    digitalWrite(pedestrianRed, LOW);
    digitalWrite(pedestrianGreen, HIGH);
    if (!pedestriansWalking) {     // they have just got green
      tone(buzzer, 800, 60);       // short higher sound: go
      pedestriansWalking = true;
    }
  } else {
    digitalWrite(pedestrianRed, HIGH);
    digitalWrite(pedestrianGreen, LOW);
    if (pedestriansWalking) {      // they have just got red
      tone(buzzer, 150, 150);      // short lower sound: stop
      pedestriansWalking = false;
    }
  }

  if (green == BLINK) {
    for (int t = 0; t < duration; t = t + 500) {
      digitalWrite(carGreen, LOW);
      delay(250);
      digitalWrite(carGreen, HIGH);
      delay(250);
    }
  } else {
    digitalWrite(carGreen, green);
    delay(duration);
  }
}

void blinkYellow() {
  if (millis() - lastBlink >= 500) {   // half a second has passed
    lastBlink = millis();
    yellowOn = !yellowOn;
    lights(LOW, yellowOn, LOW, 0);     // yellow on or off
  }
}

void setup() {
  pinMode(carRed, OUTPUT);
  pinMode(carYellow, OUTPUT);
  pinMode(carGreen, OUTPUT);
  pinMode(pedestrianRed, OUTPUT);
  pinMode(pedestrianGreen, OUTPUT);
  pinMode(buzzer, OUTPUT);
  pinMode(linkWire, INPUT);
  pinMode(confirmWire, OUTPUT);
  digitalWrite(confirmWire, LOW);       // until we switch on red, we don't report stopped
}

void loop() {
  lights(HIGH, LOW, LOW, 0);                   // red
  digitalWrite(confirmWire, HIGH);                 // tell A: stopped
  unsigned long waitStart = millis();          // when we started waiting

  while (digitalRead(linkWire) == LOW) {           // wait until A sends go
    if (millis() - waitStart >= MAX_WAIT) {
      blinkYellow();                           // A has been silent for too long
    }
  }

  digitalWrite(confirmWire, LOW);                  // we are moving, no longer stopped
  lights(HIGH, HIGH, LOW, 1000);               // red + yellow

  if (digitalRead(linkWire) == HIGH) {      // is A still saying go?
    lights(LOW, LOW, HIGH, 0);          // green
    while (digitalRead(linkWire) == HIGH) { // drive until A sends stop
    }
    lights(LOW, LOW, BLINK, 2000);      // green blinks
    lights(LOW, HIGH, LOW, 1000);       // yellow
  }
}

If something doesn’t work

  • In the link test, L on B doesn’t follow L on A ⇒ Is the black wire connected between the right – rails of both boards? Is the long link wire in h29 on both boards? Are the wires from D2 in a29, and the 470 Ω resistors in e29–f29?
  • L on B is on even when the link wire is pulled out ⇒ The 10 kΩ resistor on board B isn’t connected between i29 and the right – rail.
  • B has red all the time, and after 15 s it blinks yellow ⇒ The message isn’t reaching B. Repeat the link test from Exercise 2 and check that program A is uploaded to Arduino A.
  • B blinks yellow even though A is working ⇒ Did you make the durations in program A longer? Increase MAX_WAIT in program B.
  • After the stop message A doesn’t carry on, but blinks yellow after 6 s ⇒ The confirmation isn’t reaching A. Check the orange wire (g30 on both boards), the wires from D4 in a30, the 470 Ω resistors in e30–f30 and the 10 kΩ resistors from i30. Is the program from Exercise 6 uploaded to Arduino B?
  • A blinks yellow even though the confirmation arrives ⇒ Did you make the blinking or the yellow longer in program B? If B needs more than 6 s to stop, increase MAX_WAIT in program A.
  • Each traffic light runs on its own, or both blink yellow straight after switching on ⇒ Program A is uploaded to both Arduinos. The 470 Ω resistors protect the pins, but program B must be uploaded to Arduino B.
  • Both traffic lights have red all the time, and then they blink yellow ⇒ Program B is uploaded to both Arduinos, so nobody sends messages.
  • The program won’t upload ⇒ Each Arduino is connected to its own computer. Check that the Arduino Uno board and the right port are selected in the Arduino IDE, on the computer that the Arduino you are programming is connected to.

Extra challenges

  1. Give B a longer green than A, for example 6 s. Which program do you have to change? Do you also have to watch out for MAX_WAIT?
  2. Make A print in the Serial Monitor how many milliseconds it waited for B’s confirmation. Hint: straight after the while loop, calculate millis() - waitStart. How long does A usually wait? What happens to this number when you make the yellow longer in program B?

What did we learn?

  • millis() returns the number of milliseconds since the Arduino was switched on or reset.
  • We store the time from millis() in an unsigned long, because an int can be too small for it.
  • We check how much time has passed with a subtraction: millis() - last >= duration.
  • A program without delay() doesn’t wait, but just glances at the clock, so it can keep track of several things at the same time.
  • pinMode(pin, INPUT) and digitalRead() read whether a pin is HIGH or LOW.
  • A while loop repeats commands while a condition is true. We use it when we are waiting for something to happen.
  • Two Arduinos can talk over a wire: one writes with digitalWrite(), the other reads with digitalRead(). They must have a common GND.
  • A pull-down resistor holds a pin at LOW when nobody is sending a voltage.
  • A link is fail-safe when every fault means stop.
  • When two devices must work together, the simplest way is to have one in charge.
  • A message back (a confirmation) replaces guessing: A doesn’t calculate how long B needs to stop, but waits for B to report it.
  • If two devices wait for each other, they can wait forever. We prevent such a deadlock by carefully choosing what each of them reports.

A little quiz

1. What is the difference between delay(500) and the check millis() - last >= 500?
delay(500) stops the program for half a second, and during that time the Arduino does nothing else. The check with millis() doesn’t wait: it just looks whether half a second has passed and carries on straight away. That is why a program with millis() can keep track of several things at the same time.
2. Why do we store the time from millis() in an unsigned long, and not in an int?
On many Arduinos an int only goes up to 32 767, and millis() passes that number after just 33 seconds. An unsigned long holds numbers up to about 4.29 billion, which is more than 49 days in milliseconds.
3. Why does LOW on the wire mean stop, and not go?
Because with every fault the wire stays LOW: when the wire is pulled out, when A has no power and when A resets. If LOW meant go, every fault would let cars onto road B. This way every fault means stop.
4. What is the 10 kΩ resistor for, and what is the 470 Ω resistor for?
The 10 kΩ resistor is a pull-down: it holds the link at LOW when nobody is sending a voltage, so the pin isn’t floating. The 470 Ω resistor is protection: if both Arduinos send a voltage by mistake, one HIGH and the other LOW, the current through both resistors is at most about 5 mA and the pins aren’t damaged.
5. Why must the two Arduinos have a common GND?
A voltage is the difference between two points. When A sends 5 V, that is 5 V more than its GND. B can read the message correctly only if it measures from the same GND.
6. Why did A wait 4 s after the stop message in Exercise 3, while in Exercise 6 it waits for a confirmation?
In Exercise 3, A couldn’t see traffic light B’s lights, so it guessed: B needs 2 s for the blinking and 1 s for the yellow, and then there is 1 s when everyone has stopped. If B had a longer yellow, A would start too early. With the confirmation, A knows when B has stopped and waits as long as it takes. The same applies when A resets while B has green: at the start of loop(), A waits for the confirmation.
7. Why mustn't the function blinkYellow() use delay()?
While a delay() was running, B wouldn’t read the wire and couldn’t notice the go message straight away. A function without delay() just glances at the clock and returns straight away, so the while loop reads the wire thousands of times a second.
8. Why does B report stopped only after it switches on red, and take the confirmation back before it switches on red + yellow?
The confirmation may be HIGH only while it is true, that is, while B really has red and is waiting. So B first switches on red and then reports stopped, and before moving off it first takes back the confirmation and only then switches on red + yellow. That way A never gets a wrong message.
9. What would happen if B reported not stopped while it blinks yellow during a fault?
There would be a deadlock. A would wait for the stopped confirmation, and B for the go message. Neither would send what the other is waiting for, so both traffic lights would blink yellow forever, even with all the wires working. That is why B reports stopped even during a fault: it is still waiting for the go message and won’t switch on green without it.