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
| Component | GPIO (BCM) | Physical pin | Wiring |
|---|---|---|---|
| Red LED | GPIO17 | 11 | Pin to 330 ohm to LED anode, cathode to GND |
| Amber LED | GPIO27 | 13 | Same as red |
| Green LED | GPIO22 | 15 | Same as red |
| Push button | GPIO23 | 16 | One leg to GPIO23, other leg to GND |
| Active buzzer (3.3 V type) | GPIO24 | 18 | Plus to GPIO24, minus to GND |
| Ground rail | GND | 14 | Common 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
#!/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
LEDobjects. You can still reach each one aslights.red,lights.amberandlights.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.Eventand 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
signalmodule keeps the main thread alive so callbacks keep working, and thefinallyblock 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
TonalBuzzerwith 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.