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.
- 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
- 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.
- 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.
- Run Signal Wires: Connect 22 AWG jumper wires from the empty resistor rows to Arduino pins D8, D9, and D10.
- 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.
- 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:
- 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.
- 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.
- Confirm Baud Rate: If the Serial Monitor displays gibberish (e.g.,
ÿÿÿ), your IDE Serial Monitor is set to 9600 baud while the code specifies115200. Match them in the bottom right corner of the IDE.
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:
- 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). - 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.
- 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:
- Wire a momentary tactile switch between D2 and GND.
- In
setup(), addpinMode(2, INPUT_PULLUP);. This uses the internal 20kΩ pull-up resistor, eliminating the need for an external resistor. - In the main
loop(), read the button state:bool buttonPressed = (digitalRead(2) == LOW);. - If
buttonPressedis true, and the current state isSTATE_GREEN, force an immediate call totransitionTo(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.






