Python Raspberry Pi GPIO — Deep Dive

GPIO Under the Hood

On the Raspberry Pi, GPIO is memory-mapped I/O. The BCM283x/BCM2711 SoC exposes GPIO registers at specific physical memory addresses. When you set a pin HIGH, the library writes a bit to a register mapped into the process’s address space via /dev/gpiomem or /dev/mem.

The legacy RPi.GPIO library accesses these registers directly. The newer gpiozero library abstracts this through pin factories — swappable backends that can use RPi.GPIO, pigpio, RPIO, or even mock pins for testing.

Pin Factories in gpiozero

# Use pigpio for remote GPIO control or better PWM
import os
os.environ['GPIOZERO_PIN_FACTORY'] = 'pigpio'

from gpiozero import LED
led = LED(17)

Pin factory choices affect capabilities:

FactoryPWMRemoteTiming Precision
RPi.GPIOSoftwareNo~1 ms
pigpioHardware + SoftwareYes~5 μs
lgpioSoftwareNo~1 ms
nativeNoneNoN/A

The pigpio factory runs a daemon (pigpiod) that provides hardware-timed PWM and can be accessed over the network, enabling GPIO control from a different machine.

Interrupt-Driven Input

Polling pins in a loop wastes CPU and misses fast events. Interrupt-driven input reacts to pin state changes immediately:

from gpiozero import Button
import time

button = Button(2, bounce_time=0.05)

press_count = 0

def on_press():
    global press_count
    press_count += 1
    print(f"Press #{press_count} at {time.monotonic():.3f}")

button.when_pressed = on_press

# Main loop can do other work
while True:
    time.sleep(1)
    print(f"Total presses: {press_count}")

Under the hood, gpiozero uses epoll on the GPIO sysfs interface (or pigpio callbacks) to receive kernel-level interrupts without busy-waiting.

Debouncing

Mechanical buttons produce electrical noise — a single press can trigger dozens of false state changes within milliseconds. Gpiozero’s bounce_time parameter filters these:

# Ignore state changes within 50ms of each other
button = Button(2, bounce_time=0.05)

For critical applications, hardware debouncing (a capacitor across the button) is more reliable than software debouncing.

I2C Communication

I2C (Inter-Integrated Circuit) connects multiple sensors and devices over just two wires (SDA and SCL). The Raspberry Pi has hardware I2C on GPIO 2 (SDA) and GPIO 3 (SCL).

Enabling and Detecting Devices

# Enable I2C in raspi-config or add to /boot/config.txt:
# dtparam=i2c_arm=on

# Detect connected devices
sudo i2cdetect -y 1

Reading Sensors with smbus2

import smbus2
import struct

bus = smbus2.SMBus(1)  # I2C bus 1

# Read from BMP280 pressure sensor at address 0x76
BMP280_ADDR = 0x76
REG_TEMP = 0xFA

# Read 3 bytes of temperature data
data = bus.read_i2c_block_data(BMP280_ADDR, REG_TEMP, 3)
raw_temp = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4)

# Apply compensation formula (simplified)
temp_celsius = raw_temp / 5120.0
print(f"Temperature: {temp_celsius:.2f}°C")

Using Adafruit CircuitPython Libraries on Pi

The Blinka compatibility layer lets you use CircuitPython libraries on Raspberry Pi:

pip install adafruit-blinka adafruit-circuitpython-bme280
import board
import adafruit_bme280.advanced as adafruit_bme280

i2c = board.I2C()
bme280 = adafruit_bme280.Adafruit_BME280_I2C(i2c)

bme280.overscan_temperature = adafruit_bme280.OVERSCAN_X16
bme280.overscan_humidity = adafruit_bme280.OVERSCAN_X1

print(f"Temp: {bme280.temperature:.1f}°C")
print(f"Humidity: {bme280.humidity:.1f}%")
print(f"Pressure: {bme280.pressure:.1f} hPa")

This approach gives you access to hundreds of well-tested sensor drivers without writing raw I2C code.

SPI Communication

SPI (Serial Peripheral Interface) is faster than I2C but requires more wires. It is used for displays, SD cards, and ADCs. The Pi has two SPI buses:

import spidev

spi = spidev.SpiDev()
spi.open(0, 0)  # bus 0, chip select 0
spi.max_speed_hz = 1_000_000

# Read from MCP3008 ADC (10-bit, 8 channels)
def read_adc(channel):
    cmd = [1, (8 + channel) << 4, 0]
    reply = spi.xfer2(cmd)
    value = ((reply[1] & 3) << 8) + reply[2]
    return value

# Read analog sensor on channel 0
voltage = read_adc(0) * 3.3 / 1023
print(f"Voltage: {voltage:.3f}V")

One-Wire Protocol

The DS18B20 temperature sensor uses the one-wire protocol over a single GPIO pin. The kernel handles the protocol:

import glob
import time

# Enable one-wire in /boot/config.txt:
# dtoverlay=w1-gpio

base_dir = '/sys/bus/w1/devices/'
device_folder = glob.glob(base_dir + '28*')[0]
device_file = device_folder + '/w1_slave'

def read_temp():
    with open(device_file, 'r') as f:
        lines = f.readlines()
    
    if 'YES' not in lines[0]:
        return None
    
    pos = lines[1].find('t=')
    if pos != -1:
        temp_string = lines[1][pos + 2:]
        return float(temp_string) / 1000.0

while True:
    temp = read_temp()
    if temp is not None:
        print(f"Temperature: {temp:.1f}°C")
    time.sleep(1)

Device Tree Overlays

The Raspberry Pi uses device tree overlays to configure hardware interfaces. These are loaded at boot from /boot/config.txt:

# Enable I2C
dtparam=i2c_arm=on

# Enable SPI
dtparam=spi=on

# One-wire on GPIO4
dtoverlay=w1-gpio

# Hardware PWM on GPIO18
dtoverlay=pwm

# I2S audio
dtoverlay=hifiberry-dac

# Additional I2C bus on GPIO 23/24
dtoverlay=i2c-gpio,bus=3,i2c_gpio_sda=23,i2c_gpio_scl=24

Custom overlays can define pin functions, pull resistors, and clock settings at the kernel level, avoiding configuration in Python entirely.

Production GPIO Patterns

Graceful Cleanup

Always clean up GPIO resources when your program exits:

from gpiozero import LED, Button
import signal
import sys

led = LED(17)
button = Button(2)

def cleanup(signum, frame):
    led.off()
    led.close()
    button.close()
    sys.exit(0)

signal.signal(signal.SIGTERM, cleanup)
signal.signal(signal.SIGINT, cleanup)

Gpiozero handles cleanup automatically in most cases, but explicit cleanup is important for systemd services that receive SIGTERM.

Running as a systemd Service

# /etc/systemd/system/gpio-monitor.service
[Unit]
Description=GPIO Monitor Service
After=multi-user.target

[Service]
Type=simple
ExecStart=/usr/bin/python3 /opt/gpio-monitor/main.py
Restart=always
RestartSec=5
User=pi

[Install]
WantedBy=multi-user.target

Thread Safety

GPIO operations from multiple threads require care. The pigpio library is thread-safe. RPi.GPIO is not — protect shared pin access with locks:

import threading
from gpiozero import LED

led = LED(17)
led_lock = threading.Lock()

def safe_led_on():
    with led_lock:
        led.on()

Performance Limits

GPIO toggle speed depends on the access method:

MethodMax Toggle Frequency
gpiozero (RPi.GPIO)~70 kHz
pigpio library~150 kHz
Direct register access (C)~25 MHz
Hardware PWMConfigurable to MHz

For bit-banging protocols or high-speed signaling, Python is too slow. Use hardware peripherals (I2C, SPI, UART, PWM) or offload timing-critical work to a microcontroller connected via serial.

Raspberry Pi 5 Changes

The Raspberry Pi 5 uses a new RP1 I/O controller chip instead of the BCM SoC’s built-in GPIO. This means:

  • /dev/gpiomem is replaced by /dev/gpiochip4
  • The lgpio library is the recommended low-level interface
  • RPi.GPIO does not work on Pi 5 without patches
  • gpiozero works through the lgpio pin factory

If you are writing code that needs to run on both Pi 4 and Pi 5, gpiozero’s abstraction layer handles the differences transparently.

One thing to remember: GPIO programming is ultimately about bridging the gap between software abstractions and electrical reality — understanding both sides (Python APIs and hardware constraints) is what separates projects that work from projects that work reliably.

pythonraspberry-pigpiohardware

See Also

  • Python Behavior Trees Robotics How robots make decisions using a tree-shaped rulebook that keeps them organized, like a flowchart that tells a robot what to do in every situation.
  • Python Bluetooth Ble How Python connects to fitness trackers, smart locks, and wireless sensors using the invisible radio signals all around you.
  • Python Circuitpython Hardware Why CircuitPython makes wiring up LEDs, sensors, and motors as easy as plugging in a USB drive.
  • Python Computer Vision Autonomous How self-driving cars use cameras and Python to see the road, spot pedestrians, read signs, and understand traffic — like giving a car human eyes and a brain.
  • Python Home Assistant Automation How Python turns your home into a smart home that reacts to you automatically, like a helpful invisible butler.