If you want to know how to create a traffic light system with Arduino, the direct answer is to wire three 5mm LEDs (Red, Yellow, Green) to digital GPIO pins 8, 9, and 10 through 330Ω current-limiting resistors, and drive them using a non-blocking millis() state machine. This approach prevents the CPU from locking up, allowing you to add pedestrian buttons or sensors later without rewriting your core logic.

Below is the complete bench-tested procedure, from calculating your resistor values to debugging the exact compiler and hardware errors that trip up most builders on their first attempt.

Component Selection and Decision Tree

Before plugging anything into a breadboard, you need to select the right components. The most common mistake in basic LED projects is grabbing whatever resistor is in the bin, which either dims the LED to uselessness or overdrives the Arduino's GPIO pin, permanently damaging the ATmega328P microcontroller.

Project Spec Sheet & Parts List

Component Exact Variant / Specification Estimated Cost (2026)
Microcontroller Arduino Uno R3 (ATmega328P) or genuine Uno R4 Minima $25.00 - $28.00
LEDs 5mm Through-hole (Red, Yellow, Green), 20mA max forward current $2.00 (pack)
Resistors 330Ω (Orange-Orange-Brown-Gold), 1/4W carbon film $1.00 (pack)
Prototyping Half-size solderless breadboard (400 tie-points) $5.00
Wiring 22 AWG solid-core jumper wires (pre-cut kit) $4.00

Resistor Decision Path

Which resistor should you use? Here is the decision tree based on Ohm's Law (R = (Vs - Vf) / I):

  • If using standard 5mm LEDs (Vf ≈ 2.0V, I = 20mA): R = (5V - 2V) / 0.02A = 150Ω minimum.
  • If using high-efficiency modern LEDs (Vf ≈ 3.2V, I = 20mA): R = (5V - 3.2V) / 0.02A = 90Ω minimum.
  • Decision: Standardize on 330Ω resistors for all three colors. While 150Ω is the mathematical minimum for red, pushing 20mA continuously through a GPIO pin generates excess heat and approaches the ATmega328P's absolute maximum rating (40mA per pin, 200mA total for all pins combined). Dropping the current to ~10mA with a 330Ω resistor yields virtually the same perceived brightness to the human eye while drastically extending the lifespan of both the LED and the microcontroller.

Pin Mapping and Breadboard Wiring Steps

This build targets the Arduino Uno R3. If you are using an Uno R4 Minima or a Nano v3, the pin numbers below remain identical, but the physical board layout will differ slightly.

Pin Mapping Table

Arduino GPIO Pin Component Wire Color (Recommended)
Pin 8 Red LED Anode (via 330Ω resistor) Red
Pin 9 Yellow LED Anode (via 330Ω resistor) Yellow
Pin 10 Green LED Anode (via 330Ω resistor) Green
GND Breadboard Negative Rail (Common Cathode) Black

Numbered Wiring Procedure

  1. Establish the Ground Rail: Run a black jumper wire from the Arduino GND pin (next to the 5V pin) to the blue negative rail on the left side of your breadboard.
  2. Place the Resistors: Insert one leg of a 330Ω resistor into GPIO Pin 8, and the other leg into an empty row (e.g., Row 10). Repeat for Pin 9 (Row 15) and Pin 10 (Row 20).
  3. Insert the LEDs: Identify the polarity. The longer leg is the Anode (+); the shorter leg with the flat edge on the bulb is the Cathode (-). Insert the Anode of the Red LED into the same row as the Pin 8 resistor (Row 10). Insert the Cathode into the blue ground rail.
  4. Repeat for Yellow and Green: Wire the Yellow LED to Row 15 and the Green LED to Row 20, ensuring all Cathodes share the common ground rail.
  5. Verify Connections: Gently tug on each wire. Loose breadboard contacts are the number one cause of flickering in DIY embedded projects.
Bench Tip: Never wire LEDs directly in parallel to a single GPIO pin without individual resistors. Due to slight manufacturing variances in forward voltage, the LED with the lowest Vf will hog the current, burn out, and then cascade the failure to the others. Always use one resistor per LED.

Complete Compilable Arduino Code

Most beginner tutorials use the delay() function to time traffic lights. This is a critical flaw. delay() halts the microcontroller entirely, meaning it cannot read sensors, listen to serial commands, or debounce buttons while waiting. According to the official Arduino BlinkWithoutDelay documentation, professional embedded firmware relies on non-blocking timing using millis().

The code below implements a finite state machine (FSM). It cycles through Green, Yellow, and Red states without blocking the CPU. Copy and paste this directly into the Arduino IDE (v2.3+).

/*
 * Non-Blocking Traffic Light State Machine
 * Target Board: Arduino Uno R3 (ATmega328P)
 * Author: ElectricalFlux Bench Team
 */

// --- Pin Definitions ---
const int PIN_RED = 8;
const int PIN_YELLOW = 9;
const int PIN_GREEN = 10;

// --- Timing Constants (in milliseconds) ---
const unsigned long TIME_GREEN = 10000;  // 10 seconds
const unsigned long TIME_YELLOW = 3000;  // 3 seconds
const unsigned long TIME_RED = 10000;    // 10 seconds

// --- State Machine Enum ---
enum LightState {
  STATE_GREEN,
  STATE_YELLOW,
  STATE_RED
};

LightState currentState = STATE_GREEN;
unsigned long previousMillis = 0;

void setup() {
  // Initialize serial for debugging
  Serial.begin(115200);
  while (!Serial) { ; } // Wait for serial port (required for some 32u4 boards, safe on Uno)
  
  // Configure GPIO pins as outputs
  pinMode(PIN_RED, OUTPUT);
  pinMode(PIN_YELLOW, OUTPUT);
  pinMode(PIN_GREEN, OUTPUT);
  
  // Set initial state
  setLights(HIGH, LOW, LOW);
  Serial.println("System Initialized: GREEN LIGHT");
  previousMillis = millis();
}

void loop() {
  unsigned long currentMillis = millis();
  unsigned long interval = 0;
  
  // Determine the required interval for the current state
  switch (currentState) {
    case STATE_GREEN:
      interval = TIME_GREEN;
      break;
    case STATE_YELLOW:
      interval = TIME_YELLOW;
      break;
    case STATE_RED:
      interval = TIME_RED;
      break;
  }
  
  // Non-blocking state transition check
  if (currentMillis - previousMillis >= interval) {
    previousMillis = currentMillis; // Save the last time we changed state
    
    // Transition to next state
    switch (currentState) {
      case STATE_GREEN:
        currentState = STATE_YELLOW;
        setLights(LOW, HIGH, LOW);
        Serial.println("State Changed: YELLOW LIGHT");
        break;
        
      case STATE_YELLOW:
        currentState = STATE_RED;
        setLights(HIGH, LOW, LOW);
        Serial.println("State Changed: RED LIGHT");
        break;
        
      case STATE_RED:
        currentState = STATE_GREEN;
        setLights(LOW, LOW, HIGH);
        Serial.println("State Changed: GREEN LIGHT");
        break;
    }
  }
  
  // The CPU is free here to handle other tasks (e.g., button reads, sensor polling)
}

// Helper function to set all lights safely
void setLights(int red, int yellow, int green) {
  digitalWrite(PIN_RED, red);
  digitalWrite(PIN_YELLOW, yellow);
  digitalWrite(PIN_GREEN, green);
}

Debugging: First Three Things to Check When It Fails

When your build doesn't work, don't start rewriting code. 90% of embedded failures are physical or configuration errors. Follow this exact diagnostic sequence.

1. The 'Port Not Available' or Upload Error

Exact Error String: Board at /dev/ttyACM0 is not available (Linux/Mac) or Serial port not found (Windows).

  • Cause A (Most Likely): You are using a cheap USB cable that only has power wires, not data wires. Fix: Swap to a known-good data cable from a smartphone or external hard drive.
  • Cause B: Missing CH340 driver (if using a $6 Arduino clone instead of a genuine Uno). Fix: Download and install the CH340 driver from the manufacturer's site, then restart the IDE.
  • Cause C: The port is locked by another program (like Cura or a 3D printer slicer). Fix: Close all other software that might poll serial ports.

2. The Compilation Syntax Error

Exact Error String: expected ';' before '}' token or expected unqualified-id before '{' token.

  • Cause A: Missing semicolon at the end of a variable declaration or inside the switch block. The compiler often flags the line *after* the actual mistake. Fix: Check the line immediately preceding the error highlight.
  • Cause B: Copy-pasting code from a web browser introduced hidden Unicode characters (like smart quotes instead of straight quotes). Fix: Delete the quotation marks around strings or pin definitions and re-type them manually.

3. Hardware Failure: LED is Dim, Flickering, or Dead

If the code uploads successfully but the hardware misbehaves, grab your multimeter.

  • Check 1 (Voltage at GPIO): Set multimeter to DC Volts. Put the black probe on the Arduino GND pin and the red probe on the active GPIO pin (e.g., Pin 8). You should read 4.8V to 5.1V. If you read 0V, the pin is dead or not configured as an OUTPUT in code. If you read 2.5V, the pin is oscillating or damaged.
  • Check 2 (Continuity): Power down the board. Set multimeter to Continuity (beep mode). Test from the LED cathode leg to the Arduino GND pin. If there is no beep, your breadboard ground rail is broken internally (common in cheap breadboards where the center gap splits the rails).
  • Check 3 (Polarity): LEDs are diodes; they only pass current in one direction. If it's dead, flip the LED 180 degrees. As noted in the Adafruit Guide to LEDs, the flat spot on the bulb casing always indicates the negative cathode side.

Extending or Simplifying the Build

Once the baseline system is running, you will likely want to adapt it for a specific use case, whether that's a quick science fair demo or a complex model railroad crossing.

How to Simplify (The 5-Minute Kid's Project)

If you are teaching a child or just need a quick visual indicator and don't care about CPU blocking, strip out the state machine. Replace the entire loop() with sequential delay() calls. It is bad practice for production firmware, but it reduces the code to 15 lines and removes the need to explain millis() math to a beginner.

How to Extend (Adding a Pedestrian Crosswalk Button)

To add a pedestrian button that forces the light to Red, wire a momentary tactile switch between GPIO Pin 2 and GND.
Implementation Steps:

  1. Enable the internal pull-up resistor in setup() using pinMode(2, INPUT_PULLUP);. This eliminates the need for an external 10kΩ pull-up resistor.
  2. In the loop(), read the pin: if (digitalRead(2) == LOW). (It reads LOW when pressed because the pull-up holds it HIGH normally).
  3. Implement a 50ms software debounce timer to prevent mechanical contact bounce from triggering multiple state changes.
  4. When a valid press is detected, override currentState to STATE_YELLOW and shorten the interval to transition quickly to STATE_RED, holding the Red state for 15 seconds before resuming the normal cycle.
Safety & Code Caveat: This guide covers low-voltage (5V DC) prototyping. If your end goal is to switch actual 120V/240V AC traffic lamps or high-wattage outdoor lighting, you must use mechanical relays or solid-state contactors rated for the load, and adhere to local electrical codes (NEC Article 409 for industrial control panels). Never wire mains AC directly to an Arduino breadboard.