If you are looking for reliable Arduino traffic light code that goes beyond basic beginner tutorials, you need a non-blocking state machine. The code provided below targets the Arduino Uno R3 (ATmega328P) and uses millis() instead of delay(). This ensures your microcontroller remains free to process pedestrian crosswalk buttons, ultrasonic vehicle sensors, or serial debugging commands without freezing during a 30-second red light phase.

Project Overview & Difficulty Rating

ParameterSpecification
Target BoardArduino Uno R3 (Rev3) or Nano v3 (5V logic variants)
DifficultyBeginner-Intermediate (Wiring is simple; state-machine logic requires focus)
Build Time35-45 minutes
Core ConceptNon-blocking timing, finite state machines (FSM), current-limiting resistors

Most online examples use delay(5000) to hold a light red for five seconds. While functional for a single LED, delay() halts the CPU. If you later add a push-button for a pedestrian crossing, the system will ignore the button press until the delay finishes. By using a time-tracking state machine, we evaluate the clock on every loop iteration, allowing instant interrupts and sensor polling.

Hardware Spec Sheet & Pin Mapping

Before writing a single line of code, we must size the current-limiting resistors correctly. Modern 5mm diffused LEDs are incredibly efficient; driving them at the absolute maximum 20mA often results in blinding brightness and unnecessary heat. We will target 10-14mA for optimal visibility and longevity.

LED & Resistor Specifications

LED ColorTypical Forward Voltage (Vf)Target CurrentCalculated Resistor (at 5V)Standard BOM Resistor
Red (5mm Diffused)2.0V13.6 mA220 Ω220 Ω (1/4W)
Yellow (5mm Diffused)2.1V13.1 mA220 Ω220 Ω (1/4W)
Green (5mm Diffused)3.2V8.1 mA225 Ω220 Ω (1/4W)
Bench Tip: Notice we use 220 Ω for all three colors. While the green LED calculates to 225 Ω, using a standard 220 Ω resistor drops the current to a perfectly safe 8.1mA. Standardizing your Bill of Materials (BOM) to a single resistor value prevents wiring mistakes on the breadboard and keeps your component bins organized.

Exact Pin Mapping Table

Arduino Digital PinComponentRecommended Wire ColorFunction
D8Red LED Anode (+)RedStop signal
D9Yellow LED Anode (+)Yellow/OrangeCaution/Transition signal
D10Green LED Anode (+)GreenGo signal
GNDBreadboard Negative RailBlackCommon ground return path

Step-by-Step Wiring Procedure

  1. Establish Power Rails: Connect a black jumper wire from the Arduino Uno GND pin to the blue negative rail on your breadboard. Do not skip this; a missing common ground is the #1 cause of 'ghosting' (faintly glowing LEDs).
  2. Place the LEDs: Insert the Red, Yellow, and Green LEDs into the breadboard. Ensure the longer leg (anode) and shorter leg (cathode) are in separate, unconnected rows. Maintain at least two empty rows between each LED for resistor placement.
  3. Install Current Limiting Resistors: Bend the leads of three 220 Ω resistors. Insert one lead into the same row as the LED's short leg (cathode), and the other lead into the blue negative ground rail.
  4. Route Signal Wires: Connect red, yellow, and green jumper wires from Arduino digital pins D8, D9, and D10 directly to the long legs (anodes) of the respective LEDs.
  5. Verify with a Multimeter: Before plugging in the USB cable, set your multimeter to continuity mode. Place one probe on the Arduino GND pin and the other on the breadboard ground rail. You should hear a beep, confirming a solid ground path.

The Complete Arduino Traffic Light Code

The following C++ code implements a finite state machine (FSM). It cycles through four states: Green, Yellow, Red, and Red-Yellow (the latter is standard in UK/EU sequences before returning to Green; if you are in the US, you can easily comment out the Red-Yellow phase). The code includes explicit pin definitions and serial debugging output.

// ==========================================
// Arduino Traffic Light Code - Non-Blocking FSM
// Target: Arduino Uno R3 (ATmega328P)
// ==========================================

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

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

LightState currentState = STATE_GREEN;
unsigned long previousMillis = 0;
unsigned long currentInterval = 0;

void setup() {
  // Initialize Serial for debugging
  Serial.begin(9600);
  Serial.println(F("[SYS] Traffic Light Controller Initialized"));

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

  // Fail-safe: Start with all lights off to prevent shorts on boot
  digitalWrite(PIN_RED, LOW);
  digitalWrite(PIN_YELLOW, LOW);
  digitalWrite(PIN_GREEN, LOW);

  // Set initial state timings (in milliseconds)
  updateStateTiming();
}

void loop() {
  unsigned long currentMillis = millis();

  // Non-blocking timer check
  if (currentMillis - previousMillis >= currentInterval) {
    previousMillis = currentMillis;
    advanceState();
    updateStateTiming();
  }

  // You can add pedestrian button polling or sensor reads here
  // without interrupting the light sequence.
}

void advanceState() {
  switch (currentState) {
    case STATE_GREEN:
      currentState = STATE_YELLOW;
      Serial.println(F("[STATE] Transitioning to YELLOW"));
      break;
    case STATE_YELLOW:
      currentState = STATE_RED;
      Serial.println(F("[STATE] Transitioning to RED"));
      break;
    case STATE_RED:
      currentState = STATE_RED_YELLOW;
      Serial.println(F("[STATE] Transitioning to RED-YELLOW"));
      break;
    case STATE_RED_YELLOW:
      currentState = STATE_GREEN;
      Serial.println(F("[STATE] Transitioning to GREEN"));
      break;
  }
  applyLights();
}

void updateStateTiming() {
  switch (currentState) {
    case STATE_GREEN:      currentInterval = 10000; break; // 10 seconds
    case STATE_YELLOW:     currentInterval = 3000;  break; // 3 seconds
    case STATE_RED:        currentInterval = 12000; break; // 12 seconds
    case STATE_RED_YELLOW: currentInterval = 2000;  break; // 2 seconds
  }
}

void applyLights() {
  // Turn all off first to prevent overlap glitches
  digitalWrite(PIN_RED, LOW);
  digitalWrite(PIN_YELLOW, LOW);
  digitalWrite(PIN_GREEN, LOW);

  switch (currentState) {
    case STATE_GREEN:
      digitalWrite(PIN_GREEN, HIGH);
      break;
    case STATE_YELLOW:
      digitalWrite(PIN_YELLOW, HIGH);
      break;
    case STATE_RED:
      digitalWrite(PIN_RED, HIGH);
      break;
    case STATE_RED_YELLOW:
      digitalWrite(PIN_RED, HIGH);
      digitalWrite(PIN_YELLOW, HIGH);
      break;
  }
}

Debugging: First Three Things to Check When It Fails

When embedded projects fail, the issue is rarely 'bad code' and almost always a mismatch between the physical hardware and the software's assumptions. If your sequence is broken, check these three ranked causes.

1. Compiler Error: Scope and Declaration Issues

Exact Error String: error: expected unqualified-id before 'if' or error: 'PIN_RED' was not declared in this scope.

Cause: This happens when beginners attempt to modify the code and accidentally place an if statement or a digitalWrite() call outside of the loop() or setup() functions, or they delete the const int pin definitions at the top of the sketch.

Fix: Ensure all executable logic resides strictly inside void loop() or a called function. Verify that PIN_RED, PIN_YELLOW, and PIN_GREEN are declared globally at the very top of the file before setup().

2. Hardware: 'Ghosting' or Faintly Glowing LEDs

Symptom: The red light is fully on, but the green and yellow LEDs are glowing at about 10% brightness instead of being completely off.

Cause: Missing or high-resistance common ground. If the breadboard's negative rail isn't properly tied to the Arduino GND, the microcontroller's internal protection diodes attempt to sink the current through the I/O pins, causing faint illumination and potential silicon damage.

Fix: Use your multimeter to measure the voltage between the Arduino 5V pin and the breadboard's negative rail. It should read exactly 4.8V to 5.1V. If it reads lower or fluctuates, replace the black ground jumper wire and ensure it is pushed fully into the breadboard contacts.

3. Hardware: Sequence Stuck on a Single Color

Symptom: The light turns green, but never advances to yellow. Serial monitor shows no state transitions.

Cause: The millis() overflow logic is failing, usually because previousMillis was not updated correctly, or a blocking function (like delay() or a poorly written sensor library) was added to the loop() that takes longer than the interval itself.

Fix: Open the Serial Monitor at 9600 baud. If you see [SYS] Traffic Light Controller Initialized but no [STATE] messages, check your loop() for hidden delays. If you added an ultrasonic sensor like the HC-SR04, ensure its ping() function has a strict timeout so it doesn't hang the processor waiting for an echo that never returns.

How to Extend or Simplify the Build

Depending on your end goal—whether it is a quick school project or a complex model railroad intersection—you may want to alter the architecture. Below is a decision matrix to help you choose the right approach.

ArchitectureBest Used When...ProsCons
Simplified (delay-based) You need a 5-minute demo for a science fair and don't care about buttons. Code is only 15 lines long; extremely easy to read for absolute beginners. CPU is locked; cannot read sensors or buttons during a light phase.
Base Build (millis FSM) You want a robust, realistic sequence that can handle basic serial debugging. Non-blocking; professional coding standard; easy to adjust timings dynamically. Requires understanding of state machines and unsigned long math.
Extended (Interrupts + RTOS) Building a 4-way intersection with vehicle detection, pedestrian buttons, and network telemetry. Handles complex, concurrent tasks flawlessly; highly scalable. Overkill for a single light; requires learning FreeRTOS or hardware interrupts.
Extension Idea: To add a pedestrian crosswalk button to the Base Build, wire a momentary push-button to Digital Pin 2 with an internal pull-up resistor (INPUT_PULLUP). In the loop(), check if Pin 2 reads LOW. If it does, set a boolean flag pedestrianRequested = true;. Then, modify the STATE_GREEN case in updateStateTiming() to cut the green interval from 10000ms down to 2000ms if that flag is true. This demonstrates the true power of non-blocking code.

For deeper reading on managing timing without blocking the main thread, review the official Arduino Programming Structure documentation. Additionally, if you want to dive deeper into the physics of why we use specific resistor values for different LED colors, All About Circuits provides an excellent chapter on LED characteristics and forward voltage drops.