To get you started with how to program your Pico in CircuitPython, especially for those who may have started out with the official MicroPython setup, we've 'ported' the Getting Started with MicroPython on Pico book examples to CircuitPython. The book is awesome, please download/purchase it to support Raspberry Pi Press!

Traffic lights and pedestrian crossings are something most folks encounter on a regular basis, but perhaps never give much consideration, especially to the fact that they use microcontrollers. At their simplest, each of the light colors, red, amber and green, turn on and off for a predetermined period of time. More typically, they are far more complicated, involving things like inter-system communication, and tracking traffic flow and pedestrian crossings. While building a typical traffic management system is an incredibly advanced project, it's easy to build a simple simulator using a microcontroller and a few components.

This example will show you how to use your Raspberry Pi Pico board to control multiple LEDs using different timings, and how to monitor for a button press while other code is running using polling.

Parts Used

scattered pile of multi colored unlit LEDs
Need some indicators? We are big fans of these diffused LEDs. They are fairly bright, so they can be seen in daytime, and from any angle. They go easily into a breadboard and will add...
$4.95
In Stock
Angled shot of 25 Through-Hole Resistors - 1.0K ohm 5% 1/4W.
ΩMG! You're not going to be able to resist these handy resistor packs! Well, axially, they do all of the resisting for you!This is a 25 Pack of...
$0.75
In Stock

Wiring the Traffic Light

For this example, you'll need your Pico board, a red LED, an amber LED and a green LED, three resistors, and a number of male-to-male jumper wires. A 220Ω-1.0KΩ resistor will work; a 220Ω resistor is shown in the diagram.

The first step is to wire up the LEDs. Connect the LEDs to your Pico board as shown below.

  • Board GND to breadboard ground rail (using a jumper wire)
  • Board GP11 to 220Ω resistor
  • Red LED+ to 220Ω resistor (connected to GP11)
  • Red LED- to breadboard ground rail (using a jumper wire)
  • Board GP14 to 220Ω resistor
  • Amber LED+ to 220Ω resistor (connected to GP14)
  • Amber LED- to breadboard ground rail (using a jumper wire)
  • Board GP13 to 220Ω resistor 
  • Green LED+ to 220Ω resistor (connected to GP13)
  • Green LED- to breadboard ground rail (using a jumper wire)

Programming the Traffic Light

Now that you've wired up your traffic light, you can begin programming it.

Update your code.py to the following and save.

# SPDX-FileCopyrightText: 2021 Kattni Rembor for Adafruit Industries
#
# SPDX-License-Identifier: MIT

"""Traffic light simulator example for Pico. Turns on red, amber and green LEDs in traffic
light-like sequence.

REQUIRED HARDWARE:
* Red LED on pin GP11.
* Amber LED on pin GP14.
* Green LED on pin GP13.
"""
import time
import board
import digitalio

red_led = digitalio.DigitalInOut(board.GP11)
red_led.direction = digitalio.Direction.OUTPUT
amber_led = digitalio.DigitalInOut(board.GP14)
amber_led.direction = digitalio.Direction.OUTPUT
green_led = digitalio.DigitalInOut(board.GP13)
green_led.direction = digitalio.Direction.OUTPUT

while True:
    red_led.value = True
    time.sleep(5)
    amber_led.value = True
    time.sleep(2)
    red_led.value = False
    amber_led.value = False
    green_led.value = True
    time.sleep(5)
    green_led.value = False
    amber_led.value = True
    time.sleep(3)
    amber_led.value = False

The red LED lights up, followed by the amber LED. Both turn off as the green LED lights up. Green turns off, and amber lights up. Amber turns off, and the cycle begins again with the red LED.

Now, a detailed look at the code. First, you import the necessary modules.

import time
import board
import digitalio

Next, you set up the LEDs. As with the single red LED, each of the three LEDs requires assigning a variable to a digitalio object, and setting the pin direction to output.

red_led = digitalio.DigitalInOut(board.GP11)
red_led.direction = digitalio.Direction.OUTPUT
amber_led = digitalio.DigitalInOut(board.GP14)
amber_led.direction = digitalio.Direction.OUTPUT
green_led = digitalio.DigitalInOut(board.GP13)
green_led.direction = digitalio.Direction.OUTPUT

Variables should be descriptive and make it simple to figure out what they apply to. Previously, with only one LED, it was reasonable to call the variable led. Since you are now working with three different colors of LEDs, you should be more specific with your variables, so you know what you're working with in your code. Therefore, the variable names were updated to red_led, amber_led, and green_led.

Next you begin an infinite loop, and use time.sleep() to control the timing of the LEDs.

while True:
    red_led.value = True
    time.sleep(5)
    amber_led.value = True
    time.sleep(2)
    red_led.value = False
    amber_led.value = False
    green_led.value = True
    time.sleep(5)
    green_led.value = False
    amber_led.value = True
    time.sleep(3)
    amber_led.value = False

This timing sequence is based on actual traffic lights in the United Kingdom, shortened significantly for this example. Five seconds is hardly enough time for actual traffic flow!

First, you turn on the red LED, and wait 5 seconds. Then you turn on the amber LED for 2 seconds, to signal the light is about to change. The red LED stays on because you have not yet told it to turn off. Next, you turn off both the red and amber LEDs. Then you turn on the green LED for 5 seconds, followed by turning off the green LED. Finally you turn on the amber LED for 3 seconds, and then turn it off. At that point, the red LED turns on again because the loop has completed and restarted.

This example allows for traffic, but doesn't take pedestrians into account. The next example does exactly that.

Traffic Light and Pedestrian Crossing

In the real world, traffic lights are designed not only for road vehicles, but also to allow pedestrians to safely cross streets. Now, it's time to take a pedestrian into consideration. Adding a couple of components can turn your traffic light into a pedestrian crossing.

Wiring the Pedestrian Crossing

This example requires your current hardware setup, plus a button switch, a piezo buzzer, and a few more male-to-male jumper wires.

Connect the button switch and piezo to your current setup as shown below. The button is shown with jumper wires connected to two legs on the same side. Each pair of legs is shaped a bit like a staple. You should be able to identify the separate pairs and ensure you're connected to the opposite legs, even though you have the wires on the same side. The direction of the piezo doesn't matter.

  • Board 3V3 to breadboard power rail (using jumper wire)
  • Board GP16 to leg of button (using jumper wire)
  • Opposite leg of button to breadboard power rail (using jumper wire)
  • Board GP12 to leg of piezo buzzer (using jumper wire)
  • Other leg of piezo buzzer to breadboard ground rail (using jumper wire)

Programming the Traffic Light and Pedestrian Crossing

Update your code.py to the following and save.

# SPDX-FileCopyrightText: 2021 Kattni Rembor for Adafruit Industries
#
# SPDX-License-Identifier: MIT

"""Traffic light with pedestrian crossing simulator example for Pico. Turns on red, amber and green
LEDs in traffic light-like sequence. When button is pressed, upon light sequence completion, the
red LED turns on and the buzzer beeps to indicate pedestrian crossing is active.

REQUIRED HARDWARE:
* Red LED on pin GP11.
* Amber LED on pin GP14.
* Green LED on pin GP13.
* Button switch on pin GP16.
* Piezo buzzer on pin GP13.
"""
import time
import board
import digitalio
import pwmio

red_led = digitalio.DigitalInOut(board.GP11)
red_led.direction = digitalio.Direction.OUTPUT
amber_led = digitalio.DigitalInOut(board.GP14)
amber_led.direction = digitalio.Direction.OUTPUT
green_led = digitalio.DigitalInOut(board.GP13)
green_led.direction = digitalio.Direction.OUTPUT
button = digitalio.DigitalInOut(board.GP16)
button.switch_to_input(pull=digitalio.Pull.DOWN)
buzzer = pwmio.PWMOut(board.GP12, frequency=660, duty_cycle=0, variable_frequency=True)

button_pressed = False


def waiting_for_button(duration):
    global button_pressed  # pylint: disable=global-statement
    end = time.monotonic() + duration
    while time.monotonic() < end:
        if button.value:
            button_pressed = True


while True:
    if button_pressed:
        red_led.value = True
        for _ in range(10):
            buzzer.duty_cycle = 2 ** 15
            waiting_for_button(0.2)
            buzzer.duty_cycle = 0
            waiting_for_button(0.2)
        button_pressed = False
    red_led.value = True
    waiting_for_button(5)
    amber_led.value = True
    waiting_for_button(2)
    red_led.value = False
    amber_led.value = False
    green_led.value = True
    waiting_for_button(5)
    green_led.value = False
    amber_led.value = True
    waiting_for_button(3)
    amber_led.value = False

The light sequence is the same as before. This time, however, you can press the button, and when the light sequence completes, the red LED will stay on and the piezo will play a series of beeps. Then the light sequence will begin again. You can repeat this as often as you like.

Now, a detailed look at the code. First, you import the necessary modules. This example uses the same three as the previous example, but also requires a new module: pwmio.

import time
import board
import digitalio
import pwmio

Then you set up all of the hardware components. The LED setup is the same as before, but you are adding in the button and the buzzer.

red_led = digitalio.DigitalInOut(board.GP11)
red_led.direction = digitalio.Direction.OUTPUT
amber_led = digitalio.DigitalInOut(board.GP14)
amber_led.direction = digitalio.Direction.OUTPUT
green_led = digitalio.DigitalInOut(board.GP13)
green_led.direction = digitalio.Direction.OUTPUT
button = digitalio.DigitalInOut(board.GP16)
button.switch_to_input(pull=digitalio.Pull.DOWN)
buzzer = pwmio.PWMOut(board.GP12, frequency=660, duty_cycle=0, variable_frequency=True)

The button is set up as it as in previous examples, however the pin it is connected to on your Pico board has changed, so make sure you use the current pin.

The buzzer set up is different than any previous examples. You can use PWM with variable frequency to play tones using piezo buzzers. To use PWM with CircuitPython, you use the pwmio module. You create the buzzer variable and assign it to a pwmio.PWMOut object. This object can take up to four arguments: pin, frequency, duty_cycle and variable_frequency. To play tones, you'll need to provide all four. pin is the pin to which you wired the piezo. frequency is the frequency of the tone - in this case 660Hz. duty_cycle is set to 0 when the object is created, or your piezo will play the tone constantly as long as a loop is present in your code. Finally, variable_frequency is set to True to allow for changing the frequency later in the code.

Then, you create a variable called button_pressed and set it to False initially, as the button is not initially pressed.

button_pressed = False

In this example, you need a way to monitor for button presses while controlling the timing of events in the code, such as lighting up and turning off the LEDs. In CircuitPython, this is done using polling, wherein the program is repeatedly reading a value until it changes.

In all the previous examples, you've used time.sleep() to control the timing of events. This is an excellent method for many use cases. However, during sleep(), the code is essentially paused. Therefore, the board cannot accept any other inputs or perform any other functions for that period of time. This type of code is referred to as being blocking. In many cases, this is not an issue, but in this case, you are waiting for a button to be pressed, so you'll need to do things a little differently.

This is where another function of the time module comes in: monotonic(). At any given point in time, time.monotonic() is equal to the number seconds since your board was last power-cycled. (The soft-reboot that occurs with the auto-reload when you save changes to your CircuitPython code, or enter and exit the REPL, does not start it over.) When it is called, it returns a number with a decimal, which is called a float. If, for example, you assign time.monotonic() to a variable, and then call monotonic() again to assign into a different variable, each variable is equal to the number of seconds that time.monotonic() was equal to at the time the variables were assigned. You can then subtract the first variable from the second to obtain the amount of time that passed. Here is a simple example.

For this example, you'll create a waiting_for_button() function that will "act as" the sleep() function did in previous examples.

def waiting_for_button(duration):
    global button_pressed  # pylint: disable=global-statement
    end = time.monotonic() + duration
    while time.monotonic() < end:
        if button.value:
            button_pressed = True

The waiting_for_button() function requires a duration in seconds. Variables created outside of functions are considered global variables. However, if you want to change a global variable within a function, you need to use the global keyword. As this function changes the state of the button_pressed variable, the first thing within the function is global button_pressed. Next, you create the end variable and assign it to time.monotonic() + duration which is the current time plus the provided duration. Then, you create a loop that checks the current value of time.monotonic() and checks to see if it is less than the end time. As long as that is valid, it looks for the button to be pressed, in which case button.value is True, and sets the button_pressed variable to True.  Because a duration in seconds is provided every time this function is called, the code spends that duration checking for a button press. As the code is able to register a button press at any other time as well, the code is no longer blocking, and is able to register a button press at any time!

The last part of the code is an infinite loop. The first thing inside the loop is an if block that is looking for the button press.

while True:
    if button_pressed:
        red_led.value = True
        for _ in range(10):
            buzzer.duty_cycle = 2 ** 15
            waiting_for_button(0.2)
            buzzer.duty_cycle = 0
            waiting_for_button(0.2)
        button_pressed = False

The if block checks to see if the button_pressed variable is True, and if it is, executes the code within the block. If it is True, it turns on the red LED. Then, the for _ in range(10): tells the code to loop 10 times over the four lines of code indented below it. This section sets the duty_cycle of the piezo buzzer to half which causes it to play a tone, pauses for 0.2 seconds, then sets the duty_cycle to 0 to stop playing the tone, and pauses again for 0.2 seconds. This effectively causes a series of 10 beeps. And finally, it resets the button_pressed variable to False, so the code is ready again to wait for another button press.

The last part of the infinite loop looks similar to the simple traffic light example, but instead of using time.sleep(), it is using the waiting_for_button() function you created earlier in this example.

[...]
    red_led.value = True
    waiting_for_button(5)
    amber_led.value = True
    waiting_for_button(2)
    red_led.value = False
    amber_led.value = False
    green_led.value = True
    waiting_for_button(5)
    green_led.value = False
    amber_led.value = True
    waiting_for_button(3)
    amber_led.value = False

The timing and LED color sequence are the same. Now, press the button. If the program is currently in the middle of the loop, nothing will happen until it reaches the end of the loop and begins again. When the loop starts over following a button press, the LED will turn red, and the buzzer will beep, indicating it is safe for the pedestrian to cross! After that section of code is completed, the LED sequence will begin again with the red LED lighting up for 5 seconds. This is similar to how actual pedestrian crossings work - the light remains red after the crossing indicates it is no longer safe to ensure folks in the middle of crossing make it to the other side safely.

Press the button again to begin the sequence again. You can do this as many times as you like, the code will continue to run. That's what goes into creating a traffic light with pedestrian crossing!

This guide was first published on Jan 21, 2021. It was last updated on Mar 18, 2024.

This page (Traffic Light and Pedestrian Crossing) was last updated on Mar 18, 2024.

Text editor powered by tinymce.