GPIO With Python on Raspberry Pi 5: gpiozero, lgpio, and Why RPi.GPIO Stopped Working

Your old RPi.GPIO script worked on the Pi 4. On the Pi 5, it throws RuntimeError: Cannot determine SOC peripheral base address. The cause is hardware, not your code. The Pi 5 moved GPIO control off the main BCM2712 SoC and onto a separate I/O controller called RP1. Libraries that poke memory-mapped registers directly can no longer find the pins. This guide shows what changed, which library to use, and how to port your projects with tested code.

Quick Takeaways

  • RPi.GPIO and pigpio do not work on the Pi 5 because they depend on the old BCM283x register layout. RP1 replaced it.
  • gpiozero is the best default for most projects. It runs on top of lgpio on Pi 5 and keeps a simple, high-level API.
  • lgpio gives low-level control, hardware PWM timing options, and direct access to the gpiochip character device.
  • All GPIO pins still run at 3.3V logic. Never connect 5V signals directly to a pin.
Library Pi 5 Support Level Best For Maintained
RPi.GPIO No Low Legacy Pi 1-4 scripts Effectively dormant
pigpio No Low Legacy PWM/servo on Pi 1-4 Dormant
gpiozero Yes High Beginners, prototypes, education Yes
lgpio Yes Low Precise control, custom drivers Yes
libgpiod (Python bindings) Yes Low Kernel-native, portable code Yes
rpi-lgpio Yes Drop-in shim Quick RPi.GPIO compatibility Yes

Why RPi.GPIO Stopped Working on the Raspberry Pi 5

On earlier boards, the SoC contained the GPIO block. RPi.GPIO mapped /dev/mem and wrote straight to registers at a known peripheral base address. It was fast and simple, and it bypassed the kernel.

The Pi 5 changes this design. Raspberry Pi built RP1, a custom south-bridge chip, and connected it to the BCM2712 over a PCIe 2.0 x4 link. RP1 now handles:

  • The 40-pin header GPIO
  • I2C, SPI, UART, and PWM
  • USB 3.0 and Ethernet MAC
  • Camera and display interfaces (MIPI)

Because RP1 sits behind PCIe, its registers live at different addresses and use a different peripheral layout. The old address-detection logic in RPi.GPIO fails. Accessing RP1 also goes through the kernel’s gpiochip driver, so userspace code should use the character device interface (/dev/gpiochip4 on Pi 5) instead of raw memory.

What the Error Looks Like

RuntimeError: Cannot determine SOC peripheral base address

On Pi 5, this error means the library is probing for BCM283x hardware that no longer owns the pins. No patch to your script fixes it. You must switch libraries or use a compatibility shim.

The gpiochip Number Changed

On the Pi 5, the header pins live on gpiochip4. Earlier models used gpiochip0. Check with:

gpiodetect

Expected output on a Pi 5:

gpiochip0 [gpio-brcmstb@107d508500] (32 lines)
gpiochip1 [gpio-brcmstb@107d508520] (4 lines)
gpiochip2 [gpio-brcmstb@107d517c00] (17 lines)
gpiochip3 [gpio-brcmstb@107d517c20] (6 lines)
gpiochip4 [pinctrl-rp1] (54 lines)

The pinctrl-rp1 entry is your 40-pin header. Newer Raspberry Pi OS releases also create a /dev/gpiochip0 alias that points to it, which helps portability. Verify on your image before hardcoding.

Pinout Reference: 40-Pin Header on Pi 5

The physical header layout matches the Pi 4. Your HATs and jumper wiring carry over. Only the software stack changed.

Function BCM GPIO Physical Pin Notes
3.3V power – 1, 17 Max ~50mA shared budget guidance
5V power – 2, 4 Direct from USB-C input
GND – 6, 9, 14, 20, 25, 30, 34, 39 Common ground
I2C1 SDA GPIO2 3 Onboard 1.8kΩ pull-up
I2C1 SCL GPIO3 5 Onboard 1.8kΩ pull-up
UART TX GPIO14 8 Serial console optional
UART RX GPIO15 10 Serial console optional
SPI0 MOSI GPIO10 19 SPI data out
SPI0 MISO GPIO9 21 SPI data in
SPI0 SCLK GPIO11 23 SPI clock
PWM0 GPIO12 32 Hardware PWM channel
PWM1 GPIO13 33 Hardware PWM channel

Electrical Limits to Respect

  • Logic level: 3.3V. Inputs are not 5V tolerant.
  • Recommended per-pin current: 8mA typical, 16mA absolute maximum guidance for drive strength settings.
  • Total current across all GPIO: stay under roughly 50mA unless you buffer the load.
  • Drive LEDs through a 330Ω resistor. At 3.3V, this limits current to about 4-5mA for a standard red LED.
  • Drive motors, relays, and solenoids through a MOSFET or transistor stage with a flyback diode.

Setting Up Your Environment

Use a current Raspberry Pi OS (Bookworm or newer). The required libraries ship in the default image, but confirm them.

sudo apt update
sudo apt install -y python3-gpiozero python3-lgpio gpiod python3-libgpiod

On Bookworm and later, Python enforces externally managed environments. If you need packages from PyPI, use a virtual environment that can see system packages:

python3 -m venv --system-site-packages ~/gpio-env
source ~/gpio-env/bin/activate
pip install gpiozero lgpio

Check the installed versions:

python3 -c "import gpiozero, lgpio; print(gpiozero.__version__)"

Option 1: gpiozero (Recommended for Most Projects)

gpiozero abstracts pins into devices like LED, Button, and Servo. On Pi 5, it selects lgpio as the default pin factory automatically. You write device-level code, and the library handles the pin plumbing.

Blink an LED

Wiring: GPIO17 (pin 11) to a 330Ω resistor, then to the LED anode. LED cathode to GND (pin 9).

from gpiozero import LED
from time import sleep

led = LED(17)  # BCM numbering: GPIO17 is physical pin 11

while True:
    led.on()    # Drive pin HIGH (3.3V)
    sleep(0.5)
    led.off()   # Drive pin LOW (0V)
    sleep(0.5)

Even simpler, use the built-in blink method:

from gpiozero import LED
from signal import pause

led = LED(17)
led.blink(on_time=0.5, off_time=0.5)  # Runs in a background thread
pause()                               # Keep the script alive

Read a Button With Interrupts

Wiring: GPIO27 (pin 13) to one side of a momentary switch. Other side to GND. gpiozero enables the internal pull-up when you set pull_up=True.

from gpiozero import Button
from signal import pause

button = Button(27, pull_up=True, bounce_time=0.05)  # 50ms software debounce

def pressed():
    print("Button pressed")

def released():
    print("Button released")

button.when_pressed = pressed    # Callback fires on falling edge
button.when_released = released  # Callback fires on rising edge
pause()

PWM Brightness Control

from gpiozero import PWMLED
from time import sleep

led = PWMLED(18, frequency=1000)  # 1kHz PWM on GPIO18

for duty in range(0, 101, 5):
    led.value = duty / 100        # value is 0.0 to 1.0
    sleep(0.05)

Servo Control

Standard hobby servos need a 50Hz signal with a 1-2ms pulse. Power the servo from an external 5V supply and share GND with the Pi.

from gpiozero import Servo
from time import sleep

# Pulse widths tuned for common SG90 servos
servo = Servo(12, min_pulse_width=0.5/1000, max_pulse_width=2.4/1000)

servo.min()   # Rotate to one extreme
sleep(1)
servo.mid()   # Center
sleep(1)
servo.max()   # Rotate to the other extreme
sleep(1)
servo.detach()  # Stop pulses to prevent jitter

The software PWM used here can jitter under CPU load. For stable servo control, use a hardware PWM channel (see the PWM section below).

Forcing the Pin Factory

Set the pin factory explicitly if you need to debug or test:

export GPIOZERO_PIN_FACTORY=lgpio
python3 blink.py

Or in code:

from gpiozero import Device, LED
from gpiozero.pins.lgpio import LGPIOFactory

Device.pin_factory = LGPIOFactory()
led = LED(17)

Option 2: lgpio (Low-Level Control)

lgpio is the library gpiozero uses underneath. It wraps the Linux gpiochip interface and offers direct, C-style functions. Use it when you need tighter control over timing, bulk reads, or custom protocols.

Basic Output

import lgpio
import time

CHIP = 4        # gpiochip4 is the RP1 header on Pi 5
LED_PIN = 17    # BCM GPIO17

h = lgpio.gpiochip_open(CHIP)          # Open the GPIO chip handle
lgpio.gpio_claim_output(h, LED_PIN)    # Claim the pin as an output

try:
    for _ in range(10):
        lgpio.gpio_write(h, LED_PIN, 1)   # HIGH
        time.sleep(0.5)
        lgpio.gpio_write(h, LED_PIN, 0)   # LOW
        time.sleep(0.5)
finally:
    lgpio.gpio_free(h, LED_PIN)           # Release the pin
    lgpio.gpiochip_close(h)               # Close the chip handle

Input With Pull-Up and Edge Alerts

import lgpio
import time

CHIP = 4
BTN_PIN = 27

h = lgpio.gpiochip_open(CHIP)
# Claim as input with internal pull-up, with a 10ms glitch filter
lgpio.gpio_claim_input(h, BTN_PIN, lgpio.SET_PULL_UP)
lgpio.gpio_set_debounce_micros(h, BTN_PIN, 10000)

# Register for falling-edge alerts (button press connects pin to GND)
lgpio.gpio_claim_alert(h, BTN_PIN, lgpio.FALLING_EDGE, lgpio.SET_PULL_UP)

def on_press(chip, gpio, level, timestamp):
    print(f"GPIO{gpio} edge at {timestamp} ns, level={level}")

cb = lgpio.callback(h, BTN_PIN, lgpio.FALLING_EDGE, on_press)

try:
    while True:
        time.sleep(1)
except KeyboardInterrupt:
    pass
finally:
    cb.cancel()
    lgpio.gpiochip_close(h)

Hardware-Timed PWM With lgpio

import lgpio
import time

CHIP = 4
PIN = 18
h = lgpio.gpiochip_open(CHIP)
lgpio.gpio_claim_output(h, PIN)

# tx_pwm(handle, gpio, frequency_Hz, duty_cycle_percent)
lgpio.tx_pwm(h, PIN, 1000, 25)   # 1kHz, 25% duty
time.sleep(2)
lgpio.tx_pwm(h, PIN, 1000, 75)   # 1kHz, 75% duty
time.sleep(2)
lgpio.tx_pwm(h, PIN, 0, 0)       # Stop PWM

lgpio.gpiochip_close(h)

Note: tx_pwm produces PWM generated by the library’s timing thread, not the RP1 PWM block. Treat it as software-assisted PWM. For jitter-free output, use the kernel PWM overlay below.

Hardware PWM on the Pi 5

For servos, motor controllers, and audio-grade signals, use the kernel PWM driver. Enable it in /boot/firmware/config.txt:

# Enables 2 PWM channels on GPIO12 and GPIO13
dtoverlay=pwm-2chan

Reboot, then control PWM through sysfs:

# Export channel 0 (GPIO12)
echo 0 | sudo tee /sys/class/pwm/pwmchip0/export

# Period in nanoseconds: 20,000,000 ns = 50Hz
echo 20000000 | sudo tee /sys/class/pwm/pwmchip0/pwm0/period

# Duty cycle: 1,500,000 ns = 1.5ms (servo center)
echo 1500000 | sudo tee /sys/class/pwm/pwmchip0/pwm0/duty_cycle

# Start output
echo 1 | sudo tee /sys/class/pwm/pwmchip0/pwm0/enable

The pwmchip index can differ. On some OS versions the Pi 5 PWM controller appears as pwmchip2. Run ls /sys/class/pwm/ and use the chip that exists.

Option 3: rpi-lgpio (Drop-In RPi.GPIO Replacement)

You have a large legacy codebase and cannot rewrite it today. rpi-lgpio provides the RPi.GPIO API on top of lgpio. Remove the old package and install the shim:

sudo apt remove python3-rpi.gpio
sudo apt install python3-rpi-lgpio

Your existing code runs unchanged:

import RPi.GPIO as GPIO
import time

GPIO.setmode(GPIO.BCM)
GPIO.setup(17, GPIO.OUT)

for _ in range(5):
    GPIO.output(17, GPIO.HIGH)
    time.sleep(0.5)
    GPIO.output(17, GPIO.LOW)
    time.sleep(0.5)

GPIO.cleanup()

The shim covers most of the API. It does not cover everything. Test edge detection, PWM, and GPIO.wait_for_edge() behaviors before you ship.

Comparing the Approaches

Criteria gpiozero lgpio rpi-lgpio libgpiod
Code length Shortest Medium Same as legacy Longest
Learning curve Easy Moderate None for old users Steep
Interrupt support Callbacks Callbacks Edge detect Event loop
Timing precision Good Better Good Best (kernel)
Device abstractions LED, Button, Servo, sensors None None None
Migration effort Rewrite Rewrite Near zero Rewrite

Migrating an RPi.GPIO Script to gpiozero

Take this typical legacy snippet:

import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BCM)
GPIO.setup(17, GPIO.OUT)
GPIO.setup(27, GPIO.IN, pull_up_down=GPIO.PUD_UP)

try:
    while True:
        if GPIO.input(27) == GPIO.LOW:
            GPIO.output(17, GPIO.HIGH)
        else:
            GPIO.output(17, GPIO.LOW)
finally:
    GPIO.cleanup()

The gpiozero version removes setup and cleanup:

from gpiozero import LED, Button
from signal import pause

led = LED(17)
button = Button(27)  # pull_up=True is the default

button.when_pressed = led.on    # Press: LED on
button.when_released = led.off  # Release: LED off
pause()

Four things changed:

  • No setmode(). gpiozero uses BCM numbering by default.
  • No cleanup(). Pins release automatically on exit.
  • Button inverts logic so “pressed” is True, even with a pull-up.
  • Callbacks replace polling, which cuts CPU use.

Communication Protocols on the Pi 5

GPIO is only part of the story. Most sensors speak a bus protocol.

I2C

Enable I2C with sudo raspi-config under Interface Options, then scan the bus:

sudo apt install -y i2c-tools
i2cdetect -y 1

Read a BME280 temperature sensor at address 0x76 using smbus2:

from smbus2 import SMBus

I2C_BUS = 1
BME280_ADDR = 0x76
CHIP_ID_REG = 0xD0   # Chip ID register, returns 0x60 for BME280

with SMBus(I2C_BUS) as bus:
    chip_id = bus.read_byte_data(BME280_ADDR, CHIP_ID_REG)
    print(f"Chip ID: {hex(chip_id)}")  # Expect 0x60

SPI

Enable SPI in raspi-config, then use spidev:

import spidev

spi = spidev.SpiDev()
spi.open(0, 0)                # Bus 0, chip-select 0
spi.max_speed_hz = 1_000_000  # 1MHz clock
spi.mode = 0                  # CPOL=0, CPHA=0

response = spi.xfer2([0x01, 0x80, 0x00])  # Example MCP3008 channel 0 read
value = ((response[1] & 0x03) << 8) | response[2]
print(f"ADC value: {value}")
spi.close()

UART

The primary UART appears at /dev/ttyAMA0 on Pi 5 headers, and the dedicated debug port is a separate 3-pin connector. Open the port with pyserial:

import serial

ser = serial.Serial("/dev/ttyAMA0", baudrate=115200, timeout=1)
ser.write(b"AT\r\n")
print(ser.readline())
ser.close()

Real-World Project: Temperature-Triggered Fan Controller

This build reads a DS18B20 one-wire temperature sensor and switches a fan through an N-channel MOSFET. It shows input, output, and safe load driving together.

Parts List

  • Raspberry Pi 5
  • DS18B20 waterproof temperature sensor
  • 4.7kΩ pull-up resistor
  • 2N7000 or IRLZ44N logic-level MOSFET
  • 1N4007 flyback diode
  • 5V DC fan (under 500mA)
  • 10kΩ gate pulldown resistor

Wiring

Component Connection
DS18B20 VDD (red) 3.3V (pin 1)
DS18B20 GND (black) GND (pin 6)
DS18B20 DATA (yellow) GPIO4 (pin 7)
4.7kΩ resistor Between DATA and 3.3V
MOSFET gate GPIO17 (pin 11)
10kΩ resistor Between gate and GND
MOSFET source GND
MOSFET drain Fan negative terminal
Fan positive 5V (pin 2)
1N4007 diode Cathode to 5V, anode to MOSFET drain

Enable the one-wire interface by adding this line to /boot/firmware/config.txt, then reboot:

dtoverlay=w1-gpio,gpiopin=4

Firmware

import glob
import time
from gpiozero import LED

FAN_PIN = 17
ON_TEMP_C = 45.0    # Turn fan on above this temperature
OFF_TEMP_C = 40.0   # Turn fan off below this temperature (hysteresis)

fan = LED(FAN_PIN)  # LED class works for any simple digital output

def read_temp_c():
    # DS18B20 appears under /sys/bus/w1/devices/28-xxxxxxxxxxxx/
    device = glob.glob("/sys/bus/w1/devices/28-*/w1_slave")[0]
    with open(device) as f:
        lines = f.read().splitlines()
    if not lines[0].endswith("YES"):          # CRC check failed
        raise RuntimeError("Sensor CRC error")
    raw = lines[1].split("t=")[1]
    return int(raw) / 1000.0                  # Millidegrees to degrees C

while True:
    try:
        temp = read_temp_c()
        print(f"Temperature: {temp:.1f} C")
        if temp >= ON_TEMP_C:
            fan.on()
        elif temp <= OFF_TEMP_C:
            fan.off()                         # Hysteresis prevents rapid cycling
    except (IndexError, RuntimeError) as e:
        print(f"Sensor error: {e}")
        fan.on()                              # Fail safe: run the fan
    time.sleep(2)

The 5°C hysteresis band stops the fan from chattering on and off near the threshold.

Troubleshooting Common GPIO Errors on Pi 5

Symptom Likely Cause Fix
Cannot determine SOC peripheral base address Using legacy RPi.GPIO Install rpi-lgpio or migrate to gpiozero
lgpio.error: 'GPIO busy' Another process owns the pin Stop the other script, or run gpioinfo to find the owner
PermissionError on /dev/gpiochip* User not in gpio group sudo usermod -aG gpio $USER, then log out and in
pigpio daemon fails to start pigpio unsupported on RP1 Switch to lgpio or kernel PWM
LED stays dim or off Wrong pin numbering Use BCM numbers, not physical pin numbers
Button reads random values Floating input Enable the internal pull-up or add a 10kΩ resistor
Servo jitters Software PWM under load Use hardware PWM via dtoverlay=pwm-2chan
externally-managed-environment Bookworm pip restriction Use apt packages or a venv with --system-site-packages

Inspect Pin State From the Shell

gpioinfo gpiochip4 | head -30     # List line names, directions, and consumers
pinctrl get 17                     # Show the function and level of GPIO17
pinctrl set 17 op dh               # Set GPIO17 as output, drive high

pinctrl ships with Raspberry Pi OS and works on RP1. It is the fastest way to check whether the problem is hardware or software.

Best Practices for Reliable GPIO Code

  • Protect the pins. Add a 330Ω to 1kΩ series resistor on inputs exposed to the outside world.
  • Level shift 5V signals. Use a BSS138 bidirectional level shifter for I2C at 5V, or a 74LVC245 for parallel buses.
  • Isolate inductive loads. Use optocouplers such as the PC817 with relays or motors, and add flyback diodes.
  • Debounce in software. Use bounce_time in gpiozero or gpio_set_debounce_micros in lgpio.
  • Prefer callbacks over polling. Interrupt-style code frees the CPU and reduces latency.
  • Clean up on exit. Wrap hardware code in try/finally so pins return to a safe state.

FAQ

Does RPi.GPIO work on the Raspberry Pi 5?

No. The original RPi.GPIO package fails on Pi 5 because GPIO moved to the RP1 chip. Install rpi-lgpio for a drop-in replacement of the same API, or migrate to gpiozero for a modern, maintained interface.

Which Python GPIO library is best for the Raspberry Pi 5?

gpiozero is the best choice for most users. It runs on lgpio under the hood and has simple classes for LEDs, buttons, servos, and sensors. Choose lgpio directly when you need low-level control, or libgpiod for kernel-native portability.

Does pigpio work on the Raspberry Pi 5?

No. pigpio relies on the old BCM283x DMA and register layout, which the Pi 5 does not expose. Replace it with lgpio for GPIO and alerts, or kernel PWM (dtoverlay=pwm-2chan) for hardware PWM output.

Why do I get a “GPIO busy” error with lgpio or gpiozero?

Another process or an earlier script still holds the pin. Run gpioinfo to see which line has a consumer, stop that process, and rerun your code. In gpiozero, call device.close() or let the script exit cleanly to release pins.

Hot this week

The State of Robotics in 2026: 10 Biggest Developments

The 10 biggest robotics developments of 2026: whole-body VLA models, humanoid safety, ROS 2 Lyrical Luth, and Jetson Thor. Get the data and code.

EU Machinery Regulation 2027: What Robot Builders Need to Know

Building robots for the EU? Regulation (EU) 2023/1230 applies from 20 Jan 2027. Get the cybersecurity, AI, and CE marking checklist now.

ISO 10218:2025 Explained: The New Industrial Robot Safety Standard

ISO 10218:2025 rewrites industrial robot safety: Class I/II robots, built-in cobot limits, cybersecurity. Get the checklist and ROS2 code. Read now.

NVIDIA Jetson Orin Nano, AGX Orin, and Thor: Which One for Your Robot?

Jetson Orin Nano vs AGX Orin vs Thor: compare TOPS, memory bandwidth, power, and price to pick the right robot compute. Read the guide.

ROS 2 Distributions Explained: Humble, Jazzy, Kilted, and Lyrical (Which to Use)

Compare ROS 2 Humble, Jazzy, Kilted, and Lyrical by EOL date, platform support, and features. Pick the right distro for your robot. Read the guide.

Topics

The State of Robotics in 2026: 10 Biggest Developments

The 10 biggest robotics developments of 2026: whole-body VLA models, humanoid safety, ROS 2 Lyrical Luth, and Jetson Thor. Get the data and code.

EU Machinery Regulation 2027: What Robot Builders Need to Know

Building robots for the EU? Regulation (EU) 2023/1230 applies from 20 Jan 2027. Get the cybersecurity, AI, and CE marking checklist now.

ISO 10218:2025 Explained: The New Industrial Robot Safety Standard

ISO 10218:2025 rewrites industrial robot safety: Class I/II robots, built-in cobot limits, cybersecurity. Get the checklist and ROS2 code. Read now.

NVIDIA Jetson Orin Nano, AGX Orin, and Thor: Which One for Your Robot?

Jetson Orin Nano vs AGX Orin vs Thor: compare TOPS, memory bandwidth, power, and price to pick the right robot compute. Read the guide.

ROS 2 Distributions Explained: Humble, Jazzy, Kilted, and Lyrical (Which to Use)

Compare ROS 2 Humble, Jazzy, Kilted, and Lyrical by EOL date, platform support, and features. Pick the right distro for your robot. Read the guide.

Build a Low-Cost AI Robot Arm With SO-101 and LeRobot

Build an SO-101 robot arm under $250, calibrate it, record demos, and train an ACT policy with LeRobot. Follow the full guide and start building.

Robot Foundation Models: GR00T, pi, Gemini Robotics, and Open Alternatives Compared

Compare robot foundation models: NVIDIA GR00T, Physical Intelligence π, Gemini Robotics 2, and open VLAs. Get latency math, code, and a pick guide.

How Much Does a Humanoid Robot Cost? Prices, Subscriptions, and Hidden Costs

Humanoid robot cost in 2026: prices from $4,900, $499/mo subscriptions, and hidden fees. See the full TCO breakdown and compare models now.

Related Articles

Popular Categories