7.8 LED Traffic Light
Three LEDs, three resistors, and a program that walks through a fixed sequence of steps without ever stopping to wait. This project builds a miniature European traffic light (green, blinking green, orange, red, then red and orange together) and introduces the finite state machine, the pattern almost every real embedded program is built on.
In this documentation you will learn:
- What a finite state machine is, and how three variables are enough to build one
- How to run a timed sequence with
time.ticks_ms()instead oftime.sleep() - How to make one LED blink inside a state while the timer of that state keeps running
- How to follow the progress of a program in the console
Hardware required
- 1x Soldered NULA MINI board
- 1x Breadboard
- 1x Green LED
- 1x Orange or yellow LED
- 1x Red LED
- 3x 330 Ω resistors
- 7x Jumper wires
- 1x USB-C cable
Putting the components together
Everything in this build sits on the f–j side of the breadboard. Nothing crosses the centre channel, and the same four-part chain is repeated three times, six rows further along each time.
1. Insert the board
Push the NULA MINI into one end of the breadboard so it occupies rows 25 to 30. The board body covers columns f to i, so on this side only column j is still reachable.

2. Build the ground rail
A short blue jumper takes j30 (the GND pin of the board) down to the blue − rail. Everything in this circuit will find its way back to ground through that rail.

3. Run the green signal wire
The green jumper runs from j27 (the IO4 pin) out to j21, clear of the board.

4. Add the resistor for the green LED
A 330 Ω resistor bridges h21 to h19, from the row the signal wire arrives in to the row the LED will sit in.
h21 and j21 are the same row, so the resistor and the jumper are connected even though they sit in different hole columns. Only the row number matters here; use whichever holes the legs reach comfortably.
5. Press in the green LED
The green LED goes in with its long leg in i19, the row the resistor arrives in, and its short leg in i17.

6. Ground the green LED
A blue jumper takes j17 down to the blue − rail, completing the first chain.

7. Run the orange signal wire
Now the same chain again, six rows further along. The orange jumper runs from j26 (the IO3 pin) out to j15.

8. Add the resistor for the orange LED
The second 330 Ω resistor bridges h15 to h13.

9. Press in the orange LED
Long leg in i13, short leg in i11.

10. Ground the orange LED
A blue jumper takes j11 down to the blue − rail.

11. Run the red signal wire
The red jumper runs from j25 (the IO2 pin) out to j9. It is a long wire and it takes the scenic route off the board and back; nothing is connected underneath, so the loop is harmless.

12. Add the resistor for the red LED
The third 330 Ω resistor bridges h9 to h7.

13. Press in the red LED
Long leg in i7, short leg in i5.

14. Ground the red LED
The last blue jumper takes j5 down to the blue − rail. The circuit is now complete.

15. Connect the USB cable
Plug the USB-C cable into the board and into your computer.

The whole circuit at a glance
| Part | Connection | What it does |
|---|---|---|
| Jumper | j30 (GND) → blue − rail | Makes the rail the shared ground |
| Jumper | j27 (IO4) → j21 | Green signal off the board |
| 330 Ω | h21 → h19 | Limits current, green channel |
| Green LED | i19 (long leg) → i17 | Green light |
| Jumper | j17 → blue − rail | Green LED back to ground |
| Jumper | j26 (IO3) → j15 | Orange signal off the board |
| 330 Ω | h15 → h13 | Limits current, orange channel |
| Orange LED | i13 (long leg) → i11 | Orange light |
| Jumper | j11 → blue − rail | Orange LED back to ground |
| Jumper | j25 (IO2) → j9 | Red signal off the board |
| 330 Ω | h9 → h7 | Limits current, red channel |
| Red LED | i7 (long leg) → i5 | Red light |
| Jumper | j5 → blue − rail | Red LED back to ground |
Read the board from the far end back and the LEDs come out red, orange, green, a real traffic light lying on its side.
How a state machine works
A finite state machine is a program that is always in exactly one of a small, fixed set of situations, and moves between them on some trigger. Here the situations are the five phases of the light, and the trigger is the clock.
The whole memory of the machine is three variables:
| Variable | Meaning |
|---|---|
state | Which phase we are in right now |
last_change | The moment we entered that phase |
state_duration | How long we mean to stay in it |
Every phase then does the same two things, and only those two: set the LEDs the way this phase wants them, and check whether its time is up. If it is, name the next phase, remember the current moment as the new starting point, and set the next duration. That pattern, repeated five times, is the entire program.
| Phase | LEDs lit | Duration |
|---|---|---|
GREEN | green | 5 s |
GREEN_BLINK | green, flashing every 0.4 s | 3 s |
ORANGE | orange | 2 s |
RED | red | 5 s |
RED_ORANGE | red and orange | 2 s |
One full lap is 17 seconds, and then it starts again. The red-and-orange phase is the warning used on many European roads that green is about to come.
Naming the five states
An Arduino sketch would use an enum for this. Python has no enum in MicroPython, so the script simply gives five names five numbers:
GREEN = 0
GREEN_BLINK = 1
ORANGE = 2
RED = 3
RED_ORANGE = 4
That is all an enum really was. Without these names the five phases would be the bare numbers 0 to 4, and a mistake would be very easy to make and very hard to spot. With them you write GREEN and RED, and the code reads almost like a description of the real thing.
if / elif that follows is the MicroPython stand-in for a switch statement, which Python does not have either. It behaves the same way: only the block belonging to the current state runs, and the rest are skipped.Why ticks_ms() and not sleep()
time.sleep() stops the program dead. That is fine when a program has one thing to do, but look at the blinking green phase: the green LED has to flash on and off every 0.4 seconds while the three-second timer for the phase itself keeps running. Two clocks, at the same time, in the same block of code. A sleep cannot do that, because whichever one you wait on, the other stops.
time.ticks_ms() returns the number of milliseconds since the board was powered on. Instead of waiting, the program just looks at the clock and asks has enough time passed?:
now = time.ticks_ms()
if time.ticks_diff(now, last_change) >= state_duration:
# move to the next state
The loop keeps spinning and the answer is False almost every time. When it finally turns True, the phase changes. Nothing is ever blocked, so any number of timers can run side by side.
time.ticks_diff() rather than subtracting the two numbers. The tick counter wraps back around to a small value after a while, and a plain subtraction gives a large negative answer when it does, at which point the traffic light freezes in whatever phase it happened to be in. ticks_diff() knows about the wraparound. This matters more here than anywhere else in the kit, because this is a program meant to run for days.The two lines that keep the blink honest
When the program leaves GREEN for GREEN_BLINK it also does this:
green_on = True
last_blink = now
Without them, green_on and last_blink would still hold whatever the previous lap left behind. last_blink is the one that bites: it would be a whole lap old, so ticks_diff() would already be far beyond BLINK_INTERVAL and the first flip would happen on the very first pass of the phase instead of 0.4 s into it. green_on then decides which way that early flip goes, so the green LED can drop out the instant the blinking starts rather than staying lit for its first 0.4 s. Two lines to make the phase begin from a known state, a small thing that is easy to leave out and annoying to track down afterwards.
Notice too that the GREEN_BLINK block never writes to the green LED at the top the way the other phases do. It only turns the other two off. That is deliberate: while this phase is running the blinking code owns the green LED, and setting it high on every pass would cancel the blink out.
The flip itself is one line:
green_on = not green_on
green_led.value(1 if green_on else 0)
not flips True into False and back. The 1 if green_on else 0 after it converts that True or False into the 1 or 0 the pin expects, all on one line, the same conditional expression used in 6.2 to pick between two words.Code
from machine import Pin
import time
# These are the variables to which we pass the numbers of pins that we had connected the three LEDs to.
# The NULA board has a pin naming logic as follows: IO4, where 4 is the number that we give to the variable.
#
# Remember that every one of these LEDs needs its own 330 Ohm resistor in series with it. That resistor limits how
# much current flows, and without it an LED draws more than it is built for and can be damaged.
LED_GREEN = 4
LED_ORANGE = 3
LED_RED = 2
# These give names to a fixed set of values. Without them we would have to remember that state 0 means green and
# state 3 means red, and a mistake there would be very easy to make and very hard to spot.
# Each of these names is one state that our traffic light can be in, and it can only ever be in one at a time.
GREEN = 0
GREEN_BLINK = 1
ORANGE = 2
RED = 3
RED_ORANGE = 4
# Here we create our three Pin objects. Pin.OUT tells the board that these pins should write a value instead of
# reading one, since all three of them drive an LED.
green_led = Pin(LED_GREEN, Pin.OUT)
orange_led = Pin(LED_ORANGE, Pin.OUT)
red_led = Pin(LED_RED, Pin.OUT)
# This variable holds the state we are in right now. Together with the two below it, it is the whole memory of our
# state machine: "last_change" remembers the moment we entered the current state, and "state_duration" holds how long
# we mean to stay in it. That is all a finite state machine needs: where am I, since when, and for how long.
state = GREEN
last_change = time.ticks_ms()
state_duration = 5000
# These three variables are only used by the blinking green state. "green_on" remembers whether the green LED is
# currently lit, "BLINK_INTERVAL" is how long it stays that way before flipping, and "last_blink" remembers when it
# last flipped. Feel free to experiment with the interval to make the blinking faster or slower.
green_on = True
BLINK_INTERVAL = 400
last_blink = time.ticks_ms()
# Print out the initial message so we know that the program started successfully.
print("Traffic Light Example started!")
print("State: GREEN")
while True:
# We read the clock once at the top of the loop and use that one value everywhere below, which keeps all the
# comparisons in this pass consistent with each other.
now = time.ticks_ms()
# Below, each state gets its own block, and only the block belonging to the current state runs. Every block does
# the same two things: it sets the LEDs the way this state wants them, and then it checks whether its time is up.
if state == GREEN:
# Green on, everything else off.
green_led.value(1)
orange_led.value(0)
red_led.value(0)
# After five seconds, move on to the blinking green state and give it three seconds.
if time.ticks_diff(now, last_change) >= state_duration:
state = GREEN_BLINK
last_change = now
state_duration = 3000
# Here we set the blinking up before we hand over to it. Without these two lines the blink would carry
# on from wherever it left off the previous time around the cycle, so the green LED could enter this
# state switched off and the first flash would come at the wrong moment.
green_on = True
last_blink = now
print("State: GREEN_BLINK")
elif state == GREEN_BLINK:
# Here we leave the green LED alone, because the blinking below is what decides whether it is on or off.
orange_led.value(0)
red_led.value(0)
# This is the blinking itself. Every time the interval has passed we flip green_on to its opposite value with
# the "not" operator and write the new value to the pin. Because this runs on the clock rather than on a
# pause, the state machine above keeps working the whole time the LED is blinking.
if time.ticks_diff(now, last_blink) >= BLINK_INTERVAL:
green_on = not green_on
green_led.value(1 if green_on else 0)
last_blink = now
# After three seconds of blinking, move on to orange and give it two seconds.
if time.ticks_diff(now, last_change) >= state_duration:
state = ORANGE
last_change = now
state_duration = 2000
print("State: ORANGE")
elif state == ORANGE:
# Orange on, everything else off.
green_led.value(0)
orange_led.value(1)
red_led.value(0)
# After two seconds, move on to red and give it five seconds.
if time.ticks_diff(now, last_change) >= state_duration:
state = RED
last_change = now
state_duration = 5000
print("State: RED")
elif state == RED:
# Red on, everything else off.
green_led.value(0)
orange_led.value(0)
red_led.value(1)
# After five seconds, move on to red together with orange and give it two seconds.
if time.ticks_diff(now, last_change) >= state_duration:
state = RED_ORANGE
last_change = now
state_duration = 2000
print("State: RED_ORANGE")
elif state == RED_ORANGE:
# Red and orange lit at the same time, which on many European traffic lights is the warning that green is
# coming.
green_led.value(0)
orange_led.value(1)
red_led.value(1)
# After two seconds we are back at the start, and the whole cycle begins again.
if time.ticks_diff(now, last_change) >= state_duration:
state = GREEN
last_change = now
state_duration = 5000
print("State: GREEN")
# A very short pause leaves the processor a moment to handle its own background work.
time.sleep_ms(10)
What you should see
The three LEDs cycle continuously, and the whole lap takes 17 seconds.
Four of the five phases are steady, and the one in the middle is not: during the blinking green the LED flashes four times, on for 0.4 s and off for 0.4 s. For half of those three seconds every LED on the board is dark, which is worth knowing before you decide something has stopped working. The only phase with two LEDs lit at once is the red-and-orange one at the end.
In the console
Unlike most of the projects in this kit, this script talks. Every phase change is announced as it happens, so you can follow the state machine even when you are not watching the LEDs:
Traffic Light Example started!
State: GREEN
State: GREEN_BLINK
State: ORANGE
State: RED
State: RED_ORANGE
State: GREEN
and then those same five lines over and over.
The gaps between the lines are the durations themselves: five seconds after State: RED appears, State: RED_ORANGE follows it. That makes the console a useful test on its own: if these lines keep scrolling at the right pace but the LEDs are doing nothing, the program is fine and the fault is in the wiring.
If one colour never lights, that LED's own chain is at fault, and the other two prove the board and the code are fine. Check its polarity first, then that its resistor really does bridge two different rows. If nothing lights at all, check the j30 ground wire: without it, none of the three chains has a way back.
Full example
Check out the full example code on the link below:
7.8_LED_Traffic_Light.py
Traffic light simulation using the NULA MINI and three LEDs. Demonstrates finite state machines, non-blocking timing with ticks_ms(), and driving several outputs from one timed sequence.