Physical Computing
Raspberry Pi Projects
Project 6 — GPIO and Physical Computing
What is GPIO?
The 40-pin header on the side of your Raspberry Pi is the GPIO header — General Purpose Input/Output. These pins are your Pi's connection to the physical world. You can use them to switch an LED on and off, read when a button is pressed, communicate with sensors, drive motors, and much more.
Unlike the ports on a PC, GPIO pins are fully programmable in Python (or any language). With a few lines of code, you can make hardware respond to your logic — and that's where the magic starts.
- GPIO pins operate at 3.3 V. Connecting a 5 V signal to a GPIO input will damage your Pi.
- GPIO pins can only supply a few milliamps (8 mA per pin, 50 mA total). Never connect a motor, relay coil, or buzzer directly — use a transistor or relay module.
- Always double-check your wiring before powering on.
- Never connect or disconnect components while the Pi is running (for beginners — the Pi is not hot-swap safe).
The GPIO Pin Map
The Pi's 40-pin header has three numbering schemes. The one you will use in code is BCM numbering — the numbers printed on the chip (GPIO 17, GPIO 27, etc.), not the physical pin positions. The gpiozero library uses BCM by default.
What You Will Need
Setting Up Python
We will use the gpiozero library — the official, beginner-friendly Python GPIO library that ships with Raspberry Pi OS. It wraps common components in simple classes:
pi@raspberrypi:~$ sudo apt update pi@raspberrypi:~$ sudo apt install -y python3-gpiozero python3-pip
For the DHT22 sensor we also need Adafruit's library:
pi@raspberrypi:~$ pip3 install adafruit-circuitpython-dht pi@raspberrypi:~$ sudo apt install -y libgpiod2
python3 script_name.py from the terminal. Stop any running script with Ctrl+C.
The classic "Hello, World!" of physical computing. We will make an LED blink on and off, then make it fade smoothly using PWM (Pulse Width Modulation).
- 1× LED (any colour)
- 1× 330Ω resistor
- Jumper wires (male-to-female)
- Breadboard
Wiring
Code
#!/usr/bin/env python3 # blink_led.py — blink an LED on GPIO 17 from gpiozero import LED from time import sleep led = LED(17) # BCM pin 17 = physical pin 11 print("Blinking... press Ctrl+C to stop") while True: led.on() sleep(1) led.off() sleep(1)
Run it: python3 blink_led.py. You should see the LED blink every second.
Bonus — smooth fading with PWM
Connect the LED to GPIO 18 instead (the PWM-capable pin 12) and try this:
# fade_led.py — fade an LED in and out using PWM from gpiozero import PWMLED from time import sleep led = PWMLED(18) # PWM-capable pin (GPIO 18) while True: for brightness in [x / 10 for x in range(0, 11)]: led.value = brightness sleep(0.1) for brightness in [x / 10 for x in range(10, -1, -1)]: led.value = brightness sleep(0.1)
Read a push button and use it to toggle an LED. We will also learn about bounce — the electrical noise that buttons produce — and how to handle it.
- 1× LED + 330Ω resistor (from project 1)
- 1× tactile push button
- 1× 10kΩ resistor (or use the Pi's internal pull-up — see below)
- Jumper wires and breadboard
Wiring
We use the Pi's internal pull-up resistor in the code (no external 10kΩ needed unless you prefer it). When the button is not pressed, GPIO 2 reads HIGH; when pressed, it connects to Ground and reads LOW.
Code
# button_led.py — toggle LED with a button press from gpiozero import LED, Button from signal import pause led = LED(17) button = Button(2, pull_up=True, bounce_time=0.05) # pull_up=True → use internal pull-up resistor # bounce_time → ignore signals for 50ms after press (debounce) button.when_pressed = led.toggle # toggle LED on each press print("Press the button to toggle the LED. Ctrl+C to quit.") pause() # keep the script running, waiting for button events
Bonus — detect long press vs short press
# long_press.py — different actions for short and long presses from gpiozero import LED, Button from signal import pause led = LED(17) button = Button(2, hold_time=2) # "held" fires after 2 seconds def short_press(): led.toggle() print("Short press — LED toggled") def long_press(): led.blink(on_time=0.1, off_time=0.1, n=5) print("Long press — LED flashed 5 times") button.when_pressed = short_press button.when_held = long_press pause()
Read temperature and humidity from a DHT22 sensor and log the readings to a CSV file. This is a foundation for a weather station, a greenhouse monitor, or a Home Assistant sensor.
- 1× DHT22 sensor module (3-pin module with built-in pull-up resistor) or bare DHT22 chip + 10kΩ pull-up resistor
- Jumper wires and breadboard
Wiring (3-pin DHT22 module)
Code
#!/usr/bin/env python3 # dht22_logger.py — read DHT22 and log to CSV import board import adafruit_dht import csv import time from datetime import datetime SENSOR_PIN = board.D4 # GPIO 4 OUTPUT_FILE = "temperature_log.csv" INTERVAL = 10 # seconds between readings sensor = adafruit_dht.DHT22(SENSOR_PIN) with open(OUTPUT_FILE, "w", newline="") as f: writer = csv.writer(f) writer.writerow(["timestamp", "temperature_c", "humidity_pct"]) print(f"Logging to {OUTPUT_FILE} every {INTERVAL}s. Ctrl+C to stop.") while True: try: temp = sensor.temperature # degrees Celsius humi = sensor.humidity # percentage now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") print(f"{now} {temp:.1f}°C {humi:.1f}%") with open(OUTPUT_FILE, "a", newline="") as f: csv.writer(f).writerow([now, temp, humi]) except RuntimeError as e: # DHT sensors occasionally miss a reading — just retry print(f"Read error (will retry): {e}") time.sleep(INTERVAL)
Expose the sensor to Home Assistant (MQTT)
If you completed Project 5 and installed the Mosquitto MQTT add-on, you can publish readings directly to Home Assistant:
pi@raspberrypi:~$ pip3 install paho-mqtt
# Add to the bottom of dht22_logger.py, inside the try block: import paho.mqtt.publish as publish publish.single("home/sensor/temperature", f"{temp:.1f}", hostname="192.168.1.xx") # your Pi's IP publish.single("home/sensor/humidity", f"{humi:.1f}", hostname="192.168.1.xx")
In Home Assistant, add an MQTT Sensor integration pointing to the same topic and the readings will appear as entities on your dashboard.
A relay is an electrically operated switch. With a relay module and a Pi GPIO pin, you can switch mains-voltage devices (a lamp, a fan, a heater) on and off under software control — the foundation of home automation hardware projects.
- 1× 5V single-channel relay module (with built-in transistor driver — these are safe to connect directly to GPIO)
- Jumper wires
- A low-voltage test load: a 5V buzzer or a battery-powered lamp to test safely before using mains
Wiring (relay module)
The relay module's output terminals (COM, NO, NC) connect to your load:
| Terminal | Meaning | Use |
|---|---|---|
| COM | Common | Always connected — one side of your load goes here |
| NO | Normally Open | Circuit is open (off) when relay is off; closed (on) when relay is on |
| NC | Normally Closed | Circuit is closed (on) when relay is off; useful for fail-safe applications |
Code
# relay_control.py — switch a relay on and off from gpiozero import OutputDevice from time import sleep # Most relay modules are ACTIVE LOW — they trigger when GPIO goes LOW. # Set active_high=False if your relay activates on low signal. relay = OutputDevice(27, active_high=False, initial_value=False) print("Relay ON for 5 seconds, then OFF") relay.on() sleep(5) relay.off() print("Done")
Timed relay (e.g. a grow light scheduler)
# timed_relay.py — turn a relay on at 07:00 and off at 22:00 from gpiozero import OutputDevice from datetime import datetime from time import sleep relay = OutputDevice(27, active_high=False, initial_value=False) ON_HOUR = 7 OFF_HOUR = 22 print(f"Relay scheduler running. ON at {ON_HOUR:02d}:00, OFF at {OFF_HOUR:02d}:00.") while True: hour = datetime.now().hour if ON_HOUR <= hour < OFF_HOUR: relay.on() else: relay.off() sleep(60) # check every minute
Running Scripts Automatically on Boot
To have a GPIO script start when the Pi boots, use a systemd service. Here is a template for the DHT22 logger:
pi@raspberrypi:~$ sudo nano /etc/systemd/system/dht22-logger.service
[Unit] Description=DHT22 Temperature Logger After=network.target [Service] ExecStart=/usr/bin/python3 /home/pi/dht22_logger.py Restart=always User=pi WorkingDirectory=/home/pi [Install] WantedBy=multi-user.target
pi@raspberrypi:~$ sudo systemctl enable dht22-logger pi@raspberrypi:~$ sudo systemctl start dht22-logger pi@raspberrypi:~$ sudo systemctl status dht22-logger
Troubleshooting Common Problems
python3 -c "from gpiozero import LED; LED(17).on(); input('Press Enter to stop')". (5) Try a different LED — they do fail.bounce_time=0.05 is set in the Button constructor — without debouncing, one physical press can register dozens of times. (2) Check the wiring — the button must connect GPIO to Ground when pressed. (3) If using an external pull-down resistor, make sure it goes between the GPIO pin and Ground (10kΩ). (4) Confirm the button is a momentary type — some buttons are latching (they stay pressed until clicked again).active_high=False to active_high=True in the code. (2) Ensure the load wires go to COM and NO (Normally Open) on the relay — COM and NC would give the opposite behaviour. (3) The relay may not be getting enough current from the Pi. Try powering the relay module from the 5V pin (not 3.3V).ps aux | grep python and kill it: kill <PID>. Or reboot. To avoid this in production scripts, use gpiozero's device cleanup: the library automatically releases pins when the script exits cleanly (Ctrl+C). If your script crashes, pins may stay allocated until reboot.sudo journalctl -u dht22-logger -n 50. Common issues: (1) Wrong path to the Python script or Python interpreter — use which python3 to confirm the path. (2) The script imports a library that isn't installed for the system Python (installed with pip3 as user but not as root) — install with sudo pip3 install. (3) GPIO permissions — add your user to the gpio group: sudo usermod -aG gpio pi.Going Further
These four projects are just a starting point. Here are natural next steps to explore:
| Idea | Components needed | Key concept |
|---|---|---|
| Traffic light sequence | 3× LEDs (red, amber, green) | Timed output sequencing |
| Motion-triggered camera | PIR sensor + Pi Camera module | Interrupt-driven input |
| Distance alarm | HC-SR04 ultrasonic sensor + buzzer | Echo timing, PWM audio |
| Servo motor control | SG90 servo motor | PWM duty cycle, gpiozero Servo class |
| OLED status display | SSD1306 I²C OLED display | I²C protocol, Pillow image library |
| Soil moisture monitor | Capacitive soil sensor + ADC chip (MCP3008) | SPI protocol, analogue-to-digital |
| RGB LED strip | WS2812B LED strip | NeoPixel library, addressable LEDs |
Quick Reference
| Task | Code / Command |
|---|---|
| Import LED | from gpiozero import LED |
| Import Button | from gpiozero import Button |
| Import PWM LED | from gpiozero import PWMLED |
| Import relay / output | from gpiozero import OutputDevice |
| Turn LED on | led.on() |
| Turn LED off | led.off() |
| Toggle LED | led.toggle() |
| Blink LED | led.blink(on_time=0.5, off_time=0.5) |
| Set PWM brightness | led.value = 0.5 (0.0–1.0) |
| Read button state | button.is_pressed (True/False) |
| Button event callback | button.when_pressed = my_function |
| Wait for button | button.wait_for_press() |
| Keep script running | from signal import pause; pause() |
| View GPIO pin map | pinout (terminal command) |
| Check GPIO group membership | groups | grep gpio |
| Kill a running script | kill $(pgrep -f script_name.py) |
| Check service logs | sudo journalctl -u service-name -f |