Build a traffic light

Program a four-phase traffic light with three LEDs and a ten-second cycle. Learn state tables and timing with millis().

Getting started with ESP32 and ArduinoBeginner60 min

What you will learn

  • Connect red, yellow and green LEDs to GPIO25, GPIO26 and GPIO33 with individual resistors.
  • Describe four phases in a table that defines every output and duration.
  • Explain how millis() determines when to advance to the next phase.
  • Check the red, red-and-yellow, green, yellow sequence.
  • Calculate the cycle period and predict the effect of changing a phase duration.

Before you start

Courses 01 and 02 provide the required uploading and LED wiring skills. Experience with millis() from course 03 is helpful.

Reference setup

Board in Arduino IDE
ESP32 Dev Module
Arduino-ESP32
3.3.12
Serial Monitor
115200 baud

This is a reference profile, not an identification of the pictured board. Adapt the GPIO mapping and verify the circuit before using ESP32-C3/S2/S3 or a differently labelled board.

What we will build

Build a demonstration traffic light: red for four seconds, red and yellow together for one, green for four, then yellow alone for one. The sequence repeats. Each phase is a state with defined outputs and timing, a useful approach for later timers or automation.

Learning objectives

  • Connect three LEDs, each with its own resistor.
  • Describe device behaviour in a state table.
  • Separate output selection from phase timing.
  • Use millis() to change state without a several-second pause.
  • Calculate the cycle period and compare it with observations.

Prerequisites

Use the uploading and LED wiring skills from courses 01 and 02. Experience with millis() from course 03 helps; we explain it here.

Equipment

ComponentQuantitySpecification and notes
Development board with a classic ESP32-WROOM-32 module1Match the reference GPIO labels; check physical header positions on the actual board.
USB data and power cable1Use the connector fitted to your board; the pictured kit lists Micro-USB.
Solderless breadboard1The kit lists 830 tie points. Check whether the power rails are split.
Red LED1Anode A and cathode K; identify polarity on the actual part.
Yellow LED1Anode A and cathode K.
Green LED1Anode A and cathode K.
220 Ω resistor3One current-limiting resistor per LED branch; use the kit’s 220 Ω resistors.
Jumper wires7Use male-to-male or female-to-male leads to suit the board headers.

Approximate quantity.

Use a classic ESP32/ESP32-WROOM-32. Follow GPIO25, GPIO26 and GPIO33 labels, not physical pin positions. ESP32-C3, S2 and S3 require different wiring.

Four states of one device

A state defines an output combination. Red and yellow together form a separate phase, giving four states with three LEDs.

PhaseRedYellowGreenDuration
RedOnOffOff4000 ms
Red and yellowOnOnOff1000 ms
GreenOffOffOn4000 ms
YellowOffOnOff1000 ms

The durations total 10,000 ms, or 10 seconds. Each phase specifies all three outputs. Yellow is deliberately switched off when green begins. Merely turning on the next LED without clearing previous outputs would leave an unwanted combination.

millis() lets us check elapsed time while the main loop continues running. Arduino Blink Without Delay demonstrates this principle with one LED; here we apply it to phases with different durations.

Wiring and assembly

Wiring diagram

The drawing shows electrical connections by GPIO label, not the board’s physical header layout. Power the board through USB.

Wiring diagram — electrical connections listed in the table below
Wiring diagramEnlargeSVGPNG
Connections · Wiring diagram
FromToConnection
ESP32.GPIO25R1.1Control the RED branch.
R1.2D1.AConnect the 220 Ω resistor to the LED anode.
D1.KESP32.GNDConnect the cathode to common ground.
ESP32.GPIO26R2.1Control the YELLOW branch.
R2.2D2.AConnect the 220 Ω resistor to the LED anode.
D2.KESP32.GNDConnect the cathode to common ground.
ESP32.GPIO33R3.1Control the GREEN branch.
R3.2D3.AConnect the 220 Ω resistor to the LED anode.
D3.KESP32.GNDConnect the cathode to common ground.
  1. Disconnect USB. Arrange the LEDs in red, yellow, green order so their individual circuits are easy to follow.
  2. Connect board GND to the shared breadboard ground connection. Check whether a supply rail is split in the middle; join its sections if you use both.
  3. Connect GPIO25 through a 220 Ω resistor to the red LED anode.
  4. Connect GPIO26 through another 220 Ω resistor to the yellow LED anode.
  5. Connect GPIO33 through a third 220 Ω resistor to the green LED anode.
  6. Connect every cathode to shared GND. Check polarity and make sure each LED's leads occupy different, unconnected breadboard contact groups.

Each LED gets a resistor because each is a separate current branch. A resistor has no polarity, but must remain in series with its LED. All three cathodes may share ground. Power the board through USB; these GPIO outputs operate small demonstration circuits and must not be connected to 5 V.

Arduino setup

Use Arduino IDE 2.x with esp32 by Espressif Systems, target version 3.3.12. Select ESP32 Dev Module and the connected board's port. Follow Espressif's installation guide if the package is missing. No extra libraries are needed.

Download the Arduino example (.zip), extract it and open traffic_light/traffic_light.ino. The complete package stores it under arduino/. Keep folder and sketch names matched. Set Serial Monitor to 115200 baud. Compare GPIO assignments with your wiring.

Complete Arduino program

traffic_light.ino Arduino / C++
Download .ino

#include <Arduino.h>

constexpr uint8_t RED_PIN = 25;
constexpr uint8_t YELLOW_PIN = 26;
constexpr uint8_t GREEN_PIN = 33;
constexpr uint8_t PHASE_COUNT = 4;
const uint32_t PHASE_DURATION_MS[PHASE_COUNT] = {4000, 1000, 4000, 1000};
const char* PHASE_NAMES[PHASE_COUNT] = {"RED", "RED + YELLOW", "GREEN", "YELLOW"};

uint8_t phase = 0;
uint32_t phaseStartedMs = 0;

void showPhase() {
  // Set all outputs on every transition so no previous light remains on.
  digitalWrite(RED_PIN, (phase == 0 || phase == 1) ? HIGH : LOW);
  digitalWrite(YELLOW_PIN, (phase == 1 || phase == 3) ? HIGH : LOW);
  digitalWrite(GREEN_PIN, phase == 2 ? HIGH : LOW);
  Serial.print("Phase: ");
  Serial.println(PHASE_NAMES[phase]);
}

void setup() {
  Serial.begin(115200);
  pinMode(RED_PIN, OUTPUT);
  pinMode(YELLOW_PIN, OUTPUT);
  pinMode(GREEN_PIN, OUTPUT);
  phaseStartedMs = millis();
  showPhase();
}

void loop() {
  const uint32_t now = millis();

  if (now - phaseStartedMs >= PHASE_DURATION_MS[phase]) {
    phase = (phase + 1) % PHASE_COUNT;
    phaseStartedMs = now;
    showPhase();
  }
}

Upload the original first; save modifications separately.

Code walkthrough

The initial state is red. phase stores an index from 0 to 3; PHASE_DURATION_MS holds the durations. showPhase() sets all three outputs. pinMode() configures outputs and digitalWrite() sets their levels; see the Espressif GPIO documentation.

Every pass through loop() checks the time since the phase began. Until its duration expires, the state stays unchanged. Once it expires, the program chooses the next phase, applies its outputs and records its starting time. After the final phase, it returns to the first.

Elapsed time is the current timestamp minus the starting timestamp. Values use an unsigned integer type suitable for the millisecond counter. Outputs retain their levels between loop iterations. Serial Monitor reports each phase, beginning with Phase: RED.

Run and observe

Click Verify, then Upload. Watch the red LED as the program starts. Reset the device for a repeatable beginning. This table measures time from entry into the initial red phase.

Approximate timeExpected lights
0–4 sRed only.
4–5 sRed and yellow together.
5–9 sGreen only.
9–10 sYellow only.
From 10 sA new cycle begins with red.

Observe three complete cycles. Measure from the start of one red phase to the start of the next red phase: the same event repeats. Stopwatch readings include human reaction error, so small differences do not justify changing the program. Record compilation separately from physical board testing.

Troubleshooting

SymptomCheck and action
One LED never lightsCheck its polarity, resistor, wire and assigned GPIO.
Colours run in the wrong orderCheck the mapping: red 25, yellow 26, green 33.
Green and red stay on togetherRestore the original sketch and confirm every phase sets all three outputs.
All LEDs stay offCheck shared GND, split rails and successful upload of the new sketch.
Phases seem too shortDurations are milliseconds: four seconds is 4000, not 4.

Independent challenge

Extend only the green phase to six seconds. Calculate the new period before uploading. Observe two cycles and compare their durations with your prediction. Also calculate the total time that red and yellow are each lit during one cycle. Restore the four-second green phase afterwards.

Check your understanding

  1. Why can four phases use only three LEDs?
  2. Why must the green phase explicitly switch red and yellow off?
  3. Must the sum of individual LED on-times equal the cycle period?

Answer guidance

Change the third value in PHASE_DURATION_MS from 4000 to 6000 ms. Its index is 2 because indexing starts at zero. The new period is 4000 + 1000 + 6000 + 1000 = 12,000 ms. Red is lit for five seconds: four alone, one with yellow. Yellow is lit for two: one with red, one alone.

A phase represents an output combination, so it need not correspond to one LED. Explicitly setting every output clears previous states. In the original cycle, the on-times total 5 + 2 + 4 = 11 seconds although the cycle takes 10 seconds: two LEDs operate together for one second.

Downloads

Primary references