Building a traffic light simulator is a rite of passage for embedded systems hobbyists, but most tutorials stop at a basic delay() loop. A real-world Arduino stop light requires non-blocking timing so it can instantly respond to external interrupts—like a pedestrian crossing button. This guide walks you through building a state-machine-driven stop light using an Arduino Uno R3, complete with hardware calculations, debounced button logic, and a debugging framework for when the upload fails.

Project Overview & Difficulty Rating

Target Board: Arduino Uno R3 (ATmega328P microcontroller)
Difficulty: 2/5 (Beginner-Intermediate)
Estimated Time: 45 minutes
Estimated Cost: $12 - $15 USD

This project targets the standard Arduino Uno R3. While the code is compatible with the Nano v3 and Mega 2560, the pin mapping and physical layout instructions below are specific to the Uno R3's DIP-28 ATmega328P architecture.

Hardware Specifications & Pin Mapping

Before plugging in components, we need to calculate the correct current-limiting resistors. The ATmega328P GPIO pins have an absolute maximum current rating of 40mA, but the recommended continuous operating current is 20mA. Using Ohm's Law (R = (Vs - Vf) / I), we calculate the resistors based on a 5V source.

Component Specification Forward Voltage (Vf) Target Current Calculated Resistor Assigned GPIO
Red LED 5mm Diffused 2.0V 20mA 150Ω (Use 220Ω) D8
Yellow LED 5mm Diffused 2.1V 20mA 145Ω (Use 220Ω) D9
Green LED 5mm Diffused 3.3V 20mA 85Ω (Use 100Ω) D10
Pushbutton 6x6x5mm Tactile N/A N/A Internal Pull-up D2

Note: Standardizing on 220Ω 1/4W carbon film resistors for the red and yellow LEDs simplifies your bill of materials, as the slight drop in current (to ~13mA) is visually indistinguishable for indicator applications. For a deep dive on LED resistor selection, refer to the SparkFun LED Tutorial.

Exact Parts List

  • 1x Arduino Uno R3 (with USB-B cable)
  • 1x Half-size or Full-size solderless breadboard
  • 1x Red 5mm LED, 1x Yellow 5mm LED, 1x Green 5mm LED
  • 2x 220Ω 1/4W resistors (Red/Yellow), 1x 100Ω or 220Ω resistor (Green)
  • 1x 6x6x5mm through-hole tactile pushbutton
  • ~10x Male-to-male jumper wires (22 AWG solid core)

Step-by-Step Wiring Procedure

  1. Establish Power Rails: Connect the Arduino 5V pin to the red breadboard rail, and either GND pin to the blue breadboard rail.
  2. Wire the LEDs: Insert the three LEDs into the breadboard. The short leg (cathode) and the flat edge on the bulb rim indicate the negative side. Connect all three cathodes to the blue GND rail via jumper wires.
  3. Install Current Limiting: Bridge a resistor from the anode (long leg) of each LED to an empty row. Connect D8 to the Red resistor, D9 to Yellow, and D10 to Green.
  4. Wire the Pedestrian Button: Place the tactile switch across the breadboard's center trench. Connect one diagonal leg to the blue GND rail, and the opposite diagonal leg to D2.
Callout Tip: We are wiring the button to GND and using the microcontroller's internal INPUT_PULLUP resistor. This eliminates the need for an external 10kΩ pull-down resistor and prevents the pin from floating, which is a common cause of phantom button presses in beginner builds.

Complete C++ Firmware & State Machine Logic

To ensure the pedestrian button is responsive even while the red light is active for 10 seconds, we cannot use the blocking delay() function. Instead, we use a non-blocking state machine driven by millis(). This architecture is a foundational pattern for responsive embedded systems.

// Arduino Stop Light with Pedestrian Override
// Target: Arduino Uno R3 (ATmega328P)

const int RED_PIN = 8;
const int YELLOW_PIN = 9;
const int GREEN_PIN = 10;
const int BUTTON_PIN = 2;

// Timing constants (in milliseconds)
const unsigned long GREEN_DURATION = 5000;  // 5 seconds
const unsigned long YELLOW_DURATION = 2000; // 2 seconds
const unsigned long RED_DURATION = 5000;    // 5 seconds (standard)
const unsigned long RED_PEDESTRIAN = 10000; // 10 seconds (button pressed)

enum LightState { STATE_GREEN, STATE_YELLOW, STATE_RED };
LightState currentState = STATE_GREEN;

unsigned long previousMillis = 0;
unsigned long currentDuration = GREEN_DURATION;

// Button debouncing variables
int buttonState = HIGH;
int lastButtonState = HIGH;
unsigned long lastDebounceTime = 0;
const unsigned long debounceDelay = 50;

void setup() {
  pinMode(RED_PIN, OUTPUT);
  pinMode(YELLOW_PIN, OUTPUT);
  pinMode(GREEN_PIN, OUTPUT);
  pinMode(BUTTON_PIN, INPUT_PULLUP); // Uses internal 20k pull-up
  
  // Initialize to Green
  digitalWrite(GREEN_PIN, HIGH);
  digitalWrite(YELLOW_PIN, LOW);
  digitalWrite(RED_PIN, LOW);
  previousMillis = millis();
}

void loop() {
  unsigned long currentMillis = millis();
  
  // 1. Handle Button Debounce (Non-blocking)
  int reading = digitalRead(BUTTON_PIN);
  if (reading != lastButtonState) {
    lastDebounceTime = currentMillis;
  }
  if ((currentMillis - lastDebounceTime) > debounceDelay) {
    if (reading != buttonState) {
      buttonState = reading;
      // Button pressed (pulled to GND, so LOW means pressed)
      if (buttonState == LOW && currentState == STATE_GREEN) {
        // Force immediate transition to Yellow, then Red with extended time
        currentState = STATE_YELLOW;
        currentDuration = YELLOW_DURATION;
        previousMillis = currentMillis;
      }
    }
  }
  lastButtonState = reading;

  // 2. Handle Light State Transitions
  if (currentMillis - previousMillis >= currentDuration) {
    previousMillis = currentMillis;
    
    switch (currentState) {
      case STATE_GREEN:
        currentState = STATE_YELLOW;
        currentDuration = YELLOW_DURATION;
        updateLights(LOW, HIGH, LOW);
        break;
        
      case STATE_YELLOW:
        currentState = STATE_RED;
        // Check if pedestrian requested crossing
        currentDuration = (buttonState == LOW) ? RED_PEDESTRIAN : RED_DURATION;
        updateLights(HIGH, LOW, LOW);
        break;
        
      case STATE_RED:
        currentState = STATE_GREEN;
        currentDuration = GREEN_DURATION;
        updateLights(LOW, LOW, HIGH);
        break;
    }
  }
}

void updateLights(int red, int yellow, int green) {
  digitalWrite(RED_PIN, red);
  digitalWrite(YELLOW_PIN, yellow);
  digitalWrite(GREEN_PIN, green);
}

For more on handling mechanical switch noise, review the official Arduino Debounce Documentation.

Debugging: First Three Things to Check When It Fails

Embedded debugging is 20% code and 80% hardware verification. If your Arduino stop light isn't behaving, run through this diagnostic tree before rewriting your firmware.

The "First Three" Hardware Checks

  1. LED Polarity & Grounding: If an LED stays completely dark, flip it 180 degrees. If it glows very dimly, check your breadboard ground rail. A common mistake is forgetting to jumper the blue ground rail to the Arduino GND pin, breaking the circuit return path.
  2. Button Pull-Up Configuration: If the pedestrian override triggers randomly without you touching the button, your pin is floating. Verify that pinMode(BUTTON_PIN, INPUT_PULLUP); is in your setup, and ensure the button is wired to GND, not 5V.
  3. USB Power Sag: If the lights flicker when the red and yellow LEDs are on simultaneously, your USB port might be current-limiting. A standard USB 2.0 port supplies 500mA, which is plenty, but a degraded cable or unpowered hub can cause brownouts. Try a different USB-A to USB-B cable.

Resolving the Sync Error

If the Arduino IDE fails to upload the code and throws this exact error string in the console:

avrdude: stk500_getsync() attempt 1 of 10: not in sync: resp=0x00

This means the PC cannot communicate with the ATmega16U2 USB-to-Serial chip, or the chip cannot talk to the ATmega328P. Ranked causes and fixes:

  • Cause 1 (Most Likely): Wrong COM port selected. Go to Tools > Port and select the port that disappears when you unplug the Uno.
  • Cause 2: Corrupted Bootloader. If you previously used the board as an ISP programmer, you may have wiped the bootloader. You will need a second Arduino to burn the bootloader via ICSP headers.
  • Cause 3: Dead ATmega16U2 chip. If the board gets hot near the USB port and the "L" LED doesn't blink on reset, the USB interface chip is fried. Time to buy a new board.

Scaling the Build: Extensions and Simplifications

Once the base Arduino stop light is operational, you can adapt the project to fit your specific learning goals or physical constraints.

How to Simplify the Build

If you are teaching absolute beginners and the state-machine concept is causing confusion, you can strip the code down to a blocking architecture. Replace the millis() logic with standard delay() calls. The trade-off: The pedestrian button will only register if pressed exactly during the green phase, and the system will be entirely blind to inputs while the red or yellow delays are executing. This is acceptable for a simple visual demo, but fails as a real-world control system.

How to Extend the Build

To turn this breadboard prototype into a permanent installation or a high-power model:

  • Drive High-Power LEDs: Standard 5mm LEDs draw 20mA. If you want to use 12V automotive LED modules (which draw 500mA+), you cannot wire them directly to the GPIO pins. Use a logic-level N-channel MOSFET like the IRLZ44N. Wire the Arduino GPIO to the MOSFET gate, the 12V LED to the drain, and the source to ground.
  • Add a Countdown Display: Integrate a TM1637 4-digit 7-segment display via I2C. You can add a countdown timer that decrements every second during the STATE_RED phase, giving pedestrians a visual cue for when the light will change.
  • Intersection Logic: Link two Arduino boards via UART (TX/RX pins) to create a 4-way intersection. Board A can act as the master controller, sending serial bytes to Board B to ensure that when North/South is Green, East/West is strictly held at Red.