Raspberry Pi 4 GPIO Basics with gpiozero: LEDs, Buttons and a Buzzer

The 40-pin header is what turns a Raspberry Pi 4 from a small PC into a controller for real hardware. Each general-purpose input/output (GPIO) pin can be set high (3.3 V) or low (0 V) from software, or read to see whether an external signal is high or low.

The easiest and officially recommended Python library for this is gpiozero. It is preinstalled on Raspberry Pi OS Bookworm and on Bookworm it talks to the hardware through the lgpio backend. In this tutorial you will learn the pin numbering, the electrical limits, and then build a pedestrian crossing: red, amber and green LEDs, a push button that requests the crossing, and a buzzer that beeps while it is safe to cross.

Pin Numbering and Electrical Limits

There are two ways to name a pin. Physical numbering counts the header pins 1 to 40, with pin 1 at the end nearest the microSD card side marked by a square pad. BCM numbering uses the SoC's GPIO number, for example GPIO17 is on physical pin 11. gpiozero uses BCM numbers by default, so LED(17) means GPIO17. Run the command pinout in a terminal to print a map of the header for your exact board.

  3V3  (1)  (2)  5V
 GPIO2 (3)  (4)  5V
 GPIO3 (5)  (6)  GND
 GPIO4 (7)  (8)  GPIO14
   GND (9)  (10) GPIO15
GPIO17 (11) (12) GPIO18
GPIO27 (13) (14) GND
GPIO22 (15) (16) GPIO23
  3V3 (17)  (18) GPIO24
 ...  first 18 of 40 pins shown ...
  • GPIO pins are 3.3 V only. Never feed 5 V into a pin.
  • Keep the current from a single pin to a few milliamps. An LED with a 330 ohm resistor draws roughly (3.3 - 2.0) / 330, about 4 mA, which is safe.
  • Never connect an output pin directly to GND or 3.3 V without a resistor; a software mistake can short it.
  • GPIO2 and GPIO3 have fixed 1.8 k pull-ups for I2C. Avoid them for buttons in this project.

Components and Wiring

ComponentGPIO (BCM)Physical pinWiring
Red LEDGPIO1711Pin to 330 ohm to LED anode, cathode to GND
Amber LEDGPIO2713Same as red
Green LEDGPIO2215Same as red
Push buttonGPIO2316One leg to GPIO23, other leg to GND
Active buzzer (3.3 V type)GPIO2418Plus to GPIO24, minus to GND
Ground railGND14Common ground for all parts

The button needs no external resistor: gpiozero's Button class enables the SoC's internal pull-up by default, so the pin reads high when released and low when pressed. Use an active buzzer, which beeps when given DC; a passive buzzer needs a PWM tone. If your buzzer draws more than about 15 mA, drive it through an NPN transistor such as a BC547 or 2N2222 with a 1 k base resistor.

The Pedestrian Crossing Program

crossing.py - traffic lights with a request button and buzzer using gpiozero
#!/usr/bin/env python3
# crossing.py - pedestrian crossing on a Raspberry Pi 4 using gpiozero
# Wiring (BCM numbers): red=17, amber=27, green=22, button=23, buzzer=24
from gpiozero import TrafficLights, Button, Buzzer
from signal import pause
from threading import Event, Thread
from time import sleep

# TrafficLights groups three LEDs: red, amber, green
lights = TrafficLights(red=17, amber=27, green=22)
# Button uses the internal pull-up; bounce_time ignores contact bounce
button = Button(23, bounce_time=0.05)
buzzer = Buzzer(24)

request = Event()   # set when a pedestrian presses the button

def on_press():
    """Called by gpiozero in a background thread on every press."""
    print('Crossing requested')
    request.set()

button.when_pressed = on_press

def controller():
    """Main traffic sequence, runs forever in its own thread."""
    while True:
        # Traffic flows: green for vehicles
        lights.green.on()
        request.wait()          # block here until someone presses the button
        sleep(2)                # small delay before stopping traffic

        # Green -> amber -> red for vehicles
        lights.green.off()
        lights.amber.on()
        sleep(2)
        lights.amber.off()
        lights.red.on()

        # Pedestrians cross: buzzer beeps 0.2 s on, 0.2 s off, 15 times (6 s)
        buzzer.beep(on_time=0.2, off_time=0.2, n=15, background=False)

        # Warning phase: flash amber before giving traffic green again
        lights.red.off()
        lights.amber.blink(on_time=0.3, off_time=0.3, n=6, background=False)

        request.clear()         # ignore presses made during the crossing

Thread(target=controller, daemon=True).start()
print('Running. Press Ctrl+C to stop.')
try:
    pause()                     # keep the main thread alive
except KeyboardInterrupt:
    print('Stopping')
finally:
    lights.off()
    buzzer.off()

Run it with python3 crossing.py. The green LED stays on until you press the button; then the sequence runs and returns to green.

How the Code Works

  • TrafficLights is a composite device that bundles three LED objects. You can still reach each one as lights.red, lights.amber and lights.green.
  • Button(23, bounce_time=0.05) ignores edges that arrive within 50 ms of the previous one. Mechanical switches bounce, and without this one press can register several times.
  • when_pressed registers a callback. gpiozero calls it from its own thread, so the callback only sets a threading.Event and returns quickly.
  • request.wait() blocks the controller thread until the event is set, so the program uses no CPU while waiting.
  • beep() and blink() with background=False block until the pattern finishes, which keeps the sequence simple to read.
  • pause() from the signal module keeps the main thread alive so callbacks keep working, and the finally block switches everything off on exit.

To run the program automatically at boot, create a systemd service or add it to the crontab with @reboot. A later article on web servers shows a complete systemd unit file you can adapt.

Troubleshooting

  • LED never lights: the LED is backwards. The longer leg (anode) goes towards the GPIO pin through the resistor.
  • Button triggers by itself: a floating input. Make sure the button goes to GND, not 3.3 V, since the default pull-up is used.
  • GPIO busy error: another program, such as an earlier copy of the script, still holds the pin. Stop it and run again.
  • Old code uses RPi.GPIO with GPIO.setmode(): it still works on a Pi 4, but gpiozero is shorter and safer, and the same code also runs on newer boards.
  • Buzzer is silent: you probably have a passive buzzer. Use TonalBuzzer with it instead, or swap for an active type.

Summary

You learned the difference between BCM and physical numbering, the 3.3 V and current limits of Pi 4 GPIO, and how gpiozero turns LEDs, buttons and buzzers into simple Python objects with callbacks and blocking patterns. The same ideas carry into the next article, where PWM is used to control a servo's angle and a DC motor's speed.