Physical Computing

Raspberry Pi Projects

Project 6 — GPIO and Physical Computing

What you will build Four hands-on circuits: a blinking LED, a button input, a temperature sensor, and a relay switch
Difficulty Beginner — no electronics experience required
Time to complete 2 – 3 hours across all four mini-projects
Hardware Any Raspberry Pi with GPIO pins (Pi 4, Pi 3, Pi Zero 2W all work)

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.

Safety first — read before connecting anything:
  • 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.

GPIO header — left column = odd pins, right column = even pins
13.3V Power
5V Power2
3GPIO 2 (SDA)
5V Power4
5GPIO 3 (SCL)
Ground6
7GPIO 4
GPIO 14 (TXD)8
9Ground
GPIO 15 (RXD)10
11GPIO 17
GPIO 18 (PWM)12
13GPIO 27
Ground14
15GPIO 22
GPIO 2316
173.3V Power
GPIO 2418
19GPIO 10 (MOSI)
Ground20
21GPIO 9 (MISO)
GPIO 2522
23GPIO 11 (SCLK)
GPIO 8 (CE0)24
25Ground
GPIO 7 (CE1)26
27GPIO 0 (ID_SD)
GPIO 1 (ID_SC)28
29GPIO 5
Ground30
31GPIO 6
GPIO 12 (PWM)32
33GPIO 13 (PWM)
Ground34
35GPIO 19 (PWM)
GPIO 1636
37GPIO 26
GPIO 2038
39Ground
GPIO 2140
3.3V
5V
Ground
GPIO
I²C
SPI
UART
Physical pin 1 is always the 3.3V pin — it is at the end of the header closest to the SD card slot. An easy way to find it: look for the small square pad on the PCB next to pin 1, or the white dot printed on some Pi models. A printable pinout reference is available at pinout.xyz.

What You Will Need

🧪Half-size breadboardNo soldering required. Components press in and connect via the internal rails.
🌈Jumper wiresMale-to-female (for Pi header to breadboard) and male-to-male. Get a mixed pack.
💡LEDs (×5)Standard 5mm LEDs in assorted colours. Red, green, yellow work well.
330Ω resistors (×5)Orange-Orange-Brown colour bands. Current limiters for LEDs.
🔘Tactile push button (×2)Standard 6mm momentary push button. Very cheap — often in starter kits.
🌡DHT22 sensorTemperature + humidity sensor. Also works with DHT11 (less accurate). Includes a pull-up resistor.
🔁1-channel relay module5V relay module with built-in transistor driver. Safe to connect directly to GPIO pins.
🔌10kΩ resistors (×2)Brown-Black-Orange. Used as pull-up or pull-down resistors for button inputs.
📦Starter kit (alternative)A Pi starter kit from Amazon or Pimoroni typically contains all of the above for ~£15–20.

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
All four projects are self-contained Python scripts. Run them with python3 script_name.py from the terminal. Stop any running script with Ctrl+C.
💡
Mini-Project 1 — Blinking LED Output · gpiozero · 5 minutes

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).

You will need
  • 1× LED (any colour)
  • 1× 330Ω resistor
  • Jumper wires (male-to-female)
  • Breadboard

Wiring

Wiring diagram
GPIO 17 (pin 11) → 330Ω resistor → LED long leg (anode +)
Ground (pin 6) → LED short leg (cathode −)
330Ω resistor colour bands: 330Ω Orange – Orange – Brown – Gold
The LED has a flat side on its base — that is the cathode (negative). The longer leg is the anode (positive). Getting these backwards won't damage the Pi, but the LED won't light up.

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)
🔘
Mini-Project 2 — Push Button Input Input · pull-up resistor · event callbacks · 10 minutes

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.

You will need
  • 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

Button wiring
GPIO 2 (pin 3) → one leg of the push button
Ground (pin 6) → other leg of the push button
GPIO 17 (pin 11) → 330Ω → LED anode, LED cathode → Ground (same as project 1)

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()
🌡
Mini-Project 3 — Temperature & Humidity Sensor (DHT22) Sensor input · Adafruit library · CSV logging · 15 minutes

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.

You will need
  • 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)

DHT22 module → Pi
VCC pin on module → 3.3V (pin 1) or 5V (pin 2) — check your module's datasheet
GND pin on module → Ground (pin 6)
DATA pin on module → GPIO 4 (pin 7)
If using a bare DHT22 chip (4 legs), pin 1 is VCC, pin 2 is DATA, pin 3 is NC (not connected), pin 4 is GND. You also need a 10kΩ resistor between VCC and DATA (pull-up). The pre-made module board includes this resistor — it is the easiest option.

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.

🔁
Mini-Project 4 — Controlling a Relay Output · real-world switching · mains safety · 15 minutes

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.

You will need
  • 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
Mains electricity is dangerous. For your first test, switch a low-voltage load (a buzzer, a 5V motor, a battery lamp) instead of mains voltage. If you do eventually switch mains devices, ensure all connections are fully insulated, the relay module is rated for the load, and you are confident in what you are doing. When in doubt, use a smart plug (Tasmota, TP-Link Kasa) instead.

Wiring (relay module)

Relay module → Pi
VCC on module → 5V (pin 2) — relay coils need 5V
GND on module → Ground (pin 6)
IN on module → GPIO 27 (pin 13) — control signal (3.3V compatible)

The relay module's output terminals (COM, NO, NC) connect to your load:

TerminalMeaningUse
COMCommonAlways connected — one side of your load goes here
NONormally OpenCircuit is open (off) when relay is off; closed (on) when relay is on
NCNormally ClosedCircuit 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

⚠ LED does not light up
Work through the checklist: (1) Is the LED inserted the right way around? The longer leg (anode) goes toward the GPIO pin side of the circuit. (2) Is the 330Ω resistor connected? Without it the LED may actually burn out instantly. (3) Is your GPIO number correct — are you using BCM numbering? GPIO 17 is physical pin 11. (4) Run a quick test: python3 -c "from gpiozero import LED; LED(17).on(); input('Press Enter to stop')". (5) Try a different LED — they do fail.
⚠ Button press is not detected or fires many times
(1) Ensure 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).
⚠ DHT22 gives "RuntimeError: Timed out waiting for PulseIn message"
The DHT22 is a slow sensor — it only takes readings every 2 seconds and occasionally misses one. This is normal: catch the RuntimeError and retry (as in the example code). Also check: (1) Power — the module needs stable 3.3V or 5V. (2) Wiring — the DATA pin must go to a GPIO pin, not a power pin. (3) If errors are frequent, the cable may be too long (keep it under 20cm) or there is interference. Try a different GPIO pin.
⚠ Relay clicks but the connected device doesn't switch on
(1) Check whether your relay module is active-high or active-low (most cheap modules are active-low). If it clicks the wrong way, swap 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).
⚠ "gpiozero.exc.GPIOPinInUse" error
A previous script is still running and has claimed the pin. Find it with 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.
⚠ Pi appears to reboot or freeze when relay activates
The relay coil is drawing too much current from the Pi's 5V pin, causing a voltage drop that resets the Pi. (1) Use a powered USB hub or external 5V supply to power the relay module, with only GND and the IN signal connected to the Pi. (2) Add a flyback diode across the relay coil if using a bare relay (not a module). Module boards usually include this protection.
⚠ systemd service starts but GPIO script fails immediately
Check the service logs: 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:

IdeaComponents neededKey concept
Traffic light sequence3× LEDs (red, amber, green)Timed output sequencing
Motion-triggered cameraPIR sensor + Pi Camera moduleInterrupt-driven input
Distance alarmHC-SR04 ultrasonic sensor + buzzerEcho timing, PWM audio
Servo motor controlSG90 servo motorPWM duty cycle, gpiozero Servo class
OLED status displaySSD1306 I²C OLED displayI²C protocol, Pillow image library
Soil moisture monitorCapacitive soil sensor + ADC chip (MCP3008)SPI protocol, analogue-to-digital
RGB LED stripWS2812B LED stripNeoPixel library, addressable LEDs

Quick Reference

TaskCode / Command
Import LEDfrom gpiozero import LED
Import Buttonfrom gpiozero import Button
Import PWM LEDfrom gpiozero import PWMLED
Import relay / outputfrom gpiozero import OutputDevice
Turn LED onled.on()
Turn LED offled.off()
Toggle LEDled.toggle()
Blink LEDled.blink(on_time=0.5, off_time=0.5)
Set PWM brightnessled.value = 0.5  (0.0–1.0)
Read button statebutton.is_pressed  (True/False)
Button event callbackbutton.when_pressed = my_function
Wait for buttonbutton.wait_for_press()
Keep script runningfrom signal import pause; pause()
View GPIO pin mappinout (terminal command)
Check GPIO group membershipgroups | grep gpio
Kill a running scriptkill $(pgrep -f script_name.py)
Check service logssudo journalctl -u service-name -f