Building an Arduino traffic light controller is a rite of passage for embedded hobbyists, but most tutorials stop at blocking delay() loops that freeze your microcontroller. If you want to add pedestrian buttons, intersection sensors, or WiFi telemetry later, you need a non-blocking state machine from day one. This guide gives you the exact hardware specs, a professional pin mapping, and fully compilable C++ code targeting the Arduino Uno R3 (and the newer R4 Minima), along with a decision path to select the right components for your specific scale.

Project Verdict & Component Decision Path

Before buying parts, decide the physical scale and electrical load of your model. Driving a few 5mm LEDs on a desk requires different hardware than driving high-intensity 10mm modules or 12V automotive bulbs for a garden railway.

Decision Tree: Which LEDs and Drivers to Pick
  • IF you are building a desk-sized learning model → Pick: Standard 5mm kit LEDs with 220Ω resistors.
  • IF you need high visibility across a room (or for a diorama) → Pick: 10mm diffused LEDs (Red 620nm, Yellow 590nm, Green 520nm) with calculated current-limiting resistors.
  • IF you are driving >20mA per color, multiple intersections, or 12V bulbs → Pick: Logic-level MOSFETs (IRLZ44N) or a ULN2003A Darlington transistor array to protect the Arduino GPIO pins.

Default Recommendation: For 90% of hobbyists, the Arduino Uno R3 paired with three 10mm diffused LEDs and through-hole resistors provides the best balance of visibility, simplicity, and safety.

Parts List & Spec Sheet

Here is the exact bill of materials for the default 10mm desk model. Pricing reflects typical 2026 hobbyist market rates.

Component Exact Variant / Part Number Qty Est. Price
Microcontroller Arduino Uno R3 (ATmega328P) or R4 Minima 1 $24.00 - $28.00
Red LED Kingbright WP7113SRD (10mm, 620nm, Vf 2.0V) 1 $0.35
Yellow LED Kingbright WP7113SYD (10mm, 590nm, Vf 2.1V) 1 $0.35
Green LED Kingbright WP7113SGD (10mm, 520nm, Vf 3.2V) 1 $0.45
Resistors (Red/Yel) 150Ω 1/4W Carbon Film (5% tolerance) 2 $0.10
Resistor (Green) 100Ω 1/4W Carbon Film (5% tolerance) 1 $0.05
Breadboard 830 tie-point solderless breadboard 1 $6.00
Wiring 22 AWG solid-core jumper wires 6 $0.50

Electrical Math Note: The Arduino Uno digital pins can safely source up to 20mA continuously (40mA absolute max). For the Red LED (Vf 2.0V), R = (5V - 2.0V) / 0.020A = 150Ω. For the Green LED (Vf 3.2V), R = (5V - 3.2V) / 0.020A = 90Ω. We use 100Ω for green as it is the closest standard E12 value, yielding a safe ~18mA. See Adafruit's LED guide for deeper dive into forward voltage characteristics.

Pin Mapping & Wiring Steps

Keep your wiring organized. Using sequential digital pins makes debugging significantly easier when you are probing with a multimeter.

Component Arduino Pin Wire Color (Suggested) Notes
Red LED Anode (+) D8 Red Via 150Ω resistor
Yellow LED Anode (+) D9 Yellow Via 150Ω resistor
Green LED Anode (+) D10 Green Via 100Ω resistor
All LED Cathodes (-) GND Black Shared ground rail

Wiring Procedure

  1. Place the LEDs: Insert the 10mm LEDs into the breadboard. Note that the flat edge of the LED plastic flange indicates the cathode (negative) side.
  2. Install Resistors: Insert one leg of the appropriate resistor into the same row as the LED anode (long leg, round side). Insert the other leg into an empty row.
  3. Run Signal Wires: Connect 22 AWG jumper wires from the empty resistor rows to Arduino pins D8, D9, and D10.
  4. Establish Ground: Connect all LED cathodes (flat edge) to the breadboard's negative ground rail. Run a single wire from this rail to any of the Arduino's GND pins.
  5. Verify: Before plugging in the USB cable, use your multimeter's continuity mode to ensure no solder bridges or loose strands are shorting the 5V rail to GND.

Compilable State-Machine Code

This code targets the Arduino Uno R3 (AVR) and Uno R4 Minima (ARM). It uses a non-blocking state machine driven by millis(). This means the microcontroller is never frozen in a delay(), allowing you to poll sensors or read serial commands in the main loop simultaneously.

// Arduino Traffic Light Controller - Non-Blocking State Machine
// Target Board: Arduino Uno R3 / R4 Minima
// Author: ElectricalFlux

// --- PIN DEFINITIONS ---
const int PIN_RED = 8;
const int PIN_YELLOW = 9;
const int PIN_GREEN = 10;

// --- TIMING CONSTANTS (milliseconds) ---
const unsigned long TIME_GREEN = 5000;   // 5 seconds
const unsigned long TIME_YELLOW = 2000;  // 2 seconds
const unsigned long TIME_RED = 5000;     // 5 seconds

// --- STATE MACHINE ENUM ---
enum TrafficState {
  STATE_GREEN,
  STATE_YELLOW,
  STATE_RED
};

TrafficState currentState = STATE_GREEN;
unsigned long stateStartTime = 0;

void setup() {
  // Initialize Serial for debugging
  Serial.begin(115200);
  while (!Serial) { ; } // Wait for serial port (required for some native USB boards)
  Serial.println("Traffic Light Controller Initialized.");

  // Configure GPIO pins
  pinMode(PIN_RED, OUTPUT);
  pinMode(PIN_YELLOW, OUTPUT);
  pinMode(PIN_GREEN, OUTPUT);

  // Set initial state
  setLights(HIGH, LOW, LOW);
  stateStartTime = millis();
}

void loop() {
  unsigned long currentMillis = millis();
  unsigned long timeInState = currentMillis - stateStartTime;

  // State Machine Logic
  switch (currentState) {
    case STATE_GREEN:
      if (timeInState >= TIME_GREEN) {
        transitionTo(STATE_YELLOW);
      }
      break;

    case STATE_YELLOW:
      if (timeInState >= TIME_YELLOW) {
        transitionTo(STATE_RED);
      }
      break;

    case STATE_RED:
      if (timeInState >= TIME_RED) {
        transitionTo(STATE_GREEN);
      }
      break;
  }
  
  // You can add non-blocking sensor reads or button polls here
}

// --- HELPER FUNCTIONS ---
void transitionTo(TrafficState nextState) {
  switch (nextState) {
    case STATE_GREEN:
      setLights(HIGH, LOW, LOW);
      Serial.println("State: GREEN");
      break;
    case STATE_YELLOW:
      setLights(HIGH, HIGH, LOW); // Red + Yellow for UK/EU style, or just Yellow for US
      // For US style (just Yellow): setLights(LOW, HIGH, LOW);
      delay(5); // Tiny debounce/settling delay for serial print
      setLights(LOW, HIGH, LOW); // Force US style Yellow
      Serial.println("State: YELLOW");
      break;
    case STATE_RED:
      setLights(LOW, LOW, HIGH);
      Serial.println("State: RED");
      break;
  }
  currentState = nextState;
  stateStartTime = millis();
}

void setLights(bool red, bool yellow, bool green) {
  digitalWrite(PIN_RED, red ? HIGH : LOW);
  digitalWrite(PIN_YELLOW, yellow ? HIGH : LOW);
  digitalWrite(PIN_GREEN, green ? HIGH : LOW);
}

Debugging: First Three Checks & Common Errors

When the circuit fails to operate as expected, do not start rewriting code immediately. Hardware faults account for 80% of beginner embedded failures. Perform these three checks first:

  1. Check LED Polarity: If an LED stays dark, rotate it 180 degrees. The flat edge of the plastic lens must connect to the ground rail.
  2. Verify Resistor Presence: If the LED burned out instantly or the Arduino reset itself, you likely wired the LED directly to 5V without a resistor, drawing excessive current and triggering the board's polyfuse or damaging the ATmega328P IO pin.
  3. Confirm Baud Rate: If the Serial Monitor displays gibberish (e.g., ÿÿÿ), your IDE Serial Monitor is set to 9600 baud while the code specifies 115200. Match them in the bottom right corner of the IDE.
Common Upload Error: avrdude: stk500_recv(): programmer is not responding

If you see this exact string in the Arduino IDE output window, the PC cannot talk to the bootloader. Ranked causes and fixes:

  1. Wrong COM Port (90% of cases): Go to Tools > Port and select the correct COM port (Windows) or /dev/ttyACM0 / /dev/cu.usbmodem* (Mac/Linux).
  2. Pins 0 and 1 Shorted (5% of cases): If you wired anything to D0 (RX) or D1 (TX), disconnect them. The USB-serial chip uses these pins to flash the board; external components will corrupt the upload handshake.
  3. Faulty USB Cable (5% of cases): Many cheap micro-USB cables are "charge only" and lack the internal D+/D- data wires. Swap to a known-good data cable.

Extending or Simplifying the Build

Depending on your end goal, you may need to adjust the complexity of this project.

How to Simplify (For Young Beginners)

If you are teaching a child or a beginner who finds state machines confusing, strip the code down to a blocking sequence. Replace the loop() contents with sequential digitalWrite() and delay() commands. While this freezes the processor, it maps directly to human logic (Turn Red On -> Wait -> Turn Red Off) and eliminates the need to understand millis() rollover math.

How to Extend (Adding a Pedestrian Crosswalk)

To add a pedestrian push button without breaking the non-blocking loop:

  1. Wire a momentary tactile switch between D2 and GND.
  2. In setup(), add pinMode(2, INPUT_PULLUP);. This uses the internal 20kΩ pull-up resistor, eliminating the need for an external resistor.
  3. In the main loop(), read the button state: bool buttonPressed = (digitalRead(2) == LOW);.
  4. If buttonPressed is true, and the current state is STATE_GREEN, force an immediate call to transitionTo(STATE_YELLOW).

For multi-intersection modeling, do not wire a second set of lights directly to the remaining Uno pins. The ATmega328P has strict current limits per port bank. Instead, use an I2C port expander like the MCP23017 to drive the second intersection, keeping the primary microcontroller safe from overcurrent conditions.