An Arduino traffic light, with a crossing button that works
- 192parts on the bench
- 25boards running now
- 1.00xreal time, on every board
The traffic light is the project almost every Arduino course reaches for in its second week, because it is three LEDs and a handful of delays and it still looks like something. The version above goes one step further than most: it is a pelican crossing, with a pedestrian lamp, a request button, and the rule that green never ends sooner than it should. It is running on a simulated Arduino Uno right now, on the same sketch you can read further down this page.
Press the button on the breadboard and watch what happens. The blue WAIT lamp lights the instant the contact closes, green holds until it has had its minimum time, then amber, then red with the white walk lamp, then the flashing phase, and back to green. Open it in the editor to change any of the timings and run it again.
What you need
- An Arduino Uno
- Five LEDs: red, yellow, green, white and blue
- Five 220 ohm resistors, one per LED
- One pushbutton
- A 74HC04 hex inverter, for the WAIT lamp
- A breadboard and jumper wires
Wiring
How the sequence is built
A crossing has one resting state, green for the traffic, and it leaves that state only when somebody asks. The sketch writes that down as a loop with four phases: green until a request (but never less than three seconds and never more than nine), amber for a second and a half, red with the walk lamp for four seconds, then twelve half-second flashes of amber and the walk lamp together, which is what a real pelican crossing does at the end of its walk phase.
Each phase is a call to a small lights() helper that sets red, amber and green in one go. Writing the three lamps together, every time, is what stops the most common traffic light bug, where a phase forgets to turn off the lamp before it and two colors end up lit at once.
void loop() {
lights(0, 0, 1); // green
waitForRequest();
lights(0, 1, 0); // amber
delay(AMBER_MS);
lights(1, 0, 0); // red, and walk
digitalWrite(WALK, HIGH);
delay(WALK_MS);
for (int i = 0; i < 12; i++) { // the flashing phase
int on = (i & 1) ? LOW : HIGH;
digitalWrite(WALK, on);
lights(0, on == HIGH, 0);
delay(500);
}
digitalWrite(WALK, LOW);
}Why the button is on an interrupt
The first crossing sketch most people write checks the button with digitalRead() somewhere in loop(). It works when you test it by holding the button down, and it fails the moment somebody taps it, because for most of the cycle the board is inside a delay() and not looking at the pin at all. A tap during the red phase is simply never seen.
This sketch puts the button on pin 2, which is the Uno’s INT0 pin, and attaches an interrupt to its falling edge. The handler does one thing: it sets a flag called requested. Whatever the main loop is doing, including sitting in a delay, the press is caught. The loop then reads the flag with interrupts briefly held off, so it never reads a half-written value, and clears it at the end of the cycle.
The flag is declared volatile, which tells the compiler that something outside the normal flow of the program can change it. Leave that word out and an optimizing compiler is allowed to read the variable once and never look again.
volatile bool requested = false;
static void onPress() { requested = true; }
void setup() {
pinMode(BUTTON, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(BUTTON), onPress, FALLING);
}The lamp that needs no code
The blue WAIT lamp is not driven by the Uno at all. It hangs off one gate of a 74HC04 inverter wired straight to the button line: the line sits high through the pull-up, the button pulls it low, the inverter turns that low into a high, and the lamp lights. It reacts in nanoseconds, before the sketch has had a chance to notice anything.
It is in the circuit as a reminder that not every job belongs to the microcontroller. It also shows something about the simulator: the 74HC04 here is a real 14-pin chip with its supply on pins 14 and 7, and if you delete either supply wire in the editor the lamp stops working, exactly as it would on a desk.
Try it in the editor
Open the circuit in the editor and change MIN_GREEN from 3000 to 500. Press the button the moment green comes on and the crossing now stops the traffic almost at once, which is exactly why real crossings have a minimum.
Then break it on purpose. Replace the interrupt with a plain digitalRead() of pin 2 inside waitForRequest() and tap the button during the red phase. The press is lost. Put the interrupt back and it is caught every time.
Finally, drag a logic analyzer out of the parts bin and clip its four channels to pins 8, 9, 10 and 11. The whole sequence appears as a timing diagram, with the flashing phase as a neat row of pulses, which is a far quicker way to check the timings than watching lamps.
Common mistakes
No resistor on an LED. Every lamp here has its own 220 ohm resistor, which holds the current to about 14 milliamps from a 5 volt pin. An LED wired straight to a pin tries to take far more than the pin should give.
Reading the button only between delays, as described above. If a press is sometimes ignored, this is almost always why.
Expecting the button to read HIGH when pressed. With INPUT_PULLUP the pin idles high and a press pulls it to ground, so pressed is LOW. The interrupt is on the falling edge for the same reason.
Two colors lit at once, because one phase turned a lamp on and the next one never turned it off. Setting all three together in one function removes the whole class of bug.
Questions
Can I build a simple traffic light without the button?
Yes. Delete the button and the waitForRequest() call and give green a fixed delay() of its own. The result is the classic three-LED sequence. The button version is worth the extra few lines because it is how real crossings behave.
Why pin 2 for the button?
On an Uno, only pins 2 and 3 have their own external interrupts, INT0 and INT1. Any pin can be read with digitalRead(), but only those two can call a function the moment they change.
Does this run on a real Arduino Uno unchanged?
The sketch uses only standard Arduino calls: pinMode, digitalWrite, delay, millis and attachInterrupt. It is the same code you would upload from the Arduino IDE.
How do I make it a four-way junction?
Add a second set of three lamps on three more pins and give each phase two calls, one for each road. A junction also has a red-and-amber phase before green, which a pelican crossing does not.
Build this for real
Open the editor, change a value and watch the number move with it. Nothing to install, and no account needed.