Skip to main content

Raspberry Pi 5 GPIO Migration: Moving from RPi.GPIO to lgpio for Python Scripts

 If you have recently attempted to migrate your legacy Python automation scripts to a Raspberry Pi 5, you have likely encountered the immediate, script-breaking failure of RPi.GPIO. The standard error—RuntimeError: This module can only be run on a Raspberry Pi!—is misleading. You are on a Raspberry Pi, but the underlying hardware architecture has shifted fundamentally, rendering direct memory access libraries obsolete.

This guide details the architectural root cause of this failure and provides a production-grade implementation using the lgpio library to restore GPIO functionality on Raspberry Pi 5 running Raspberry Pi OS (Bookworm).

The Root Cause: The RP1 Southbridge

The failure of RPi.GPIO on the Raspberry Pi 5 is not a software bug; it is an architectural incompatibility.

On Raspberry Pi generations 1 through 4, the GPIO pins were directly controlled by the main Broadcom SoC (e.g., BCM2835, BCM2711). Libraries like RPi.GPIO worked by memory-mapping /dev/mem or /dev/gpiomem and manipulating specific registers at known offsets to toggle pins. This was fast but brittle, relying on specific hardware addresses.

The Raspberry Pi 5 decouples I/O from the main processor (BCM2712). Most I/O, including the 40-pin GPIO header, is now managed by a separate, dedicated I/O controller chip called the RP1. The BCM2712 communicates with the RP1 via a PCIe link.

Because the GPIO registers no longer reside at the memory addresses RPi.GPIO expects—and because the kernel now strictly mediates access to the RP1—direct register manipulation fails. We must move to the Linux Kernel GPIO character device API (/dev/gpiochip*), which is properly abstracted by the lgpio library.

The Fix: Implementation with lgpio

While there are compatibility shims available (like rpi-lgpio), the most robust path forward for long-term maintenance is rewriting the GPIO handling logic using the native lgpio library. This library interacts directly with the Linux GPIO kernel driver, ensuring compatibility with the RP1 chip and future hardware revisions.

1. Environment Preparation

On a standard Raspberry Pi OS (Bookworm) installation for Pi 5, lgpio is often pre-installed. However, to ensure you have the library and system dependencies, run:

sudo apt update
sudo apt install python3-lgpio

Note: If you are working within a Python virtual environment (PEP 668 compliance), you may need to install the library via pip with system site packages enabled or install it strictly inside the venv.

2. The Migration Code

Below is a robust Python script demonstrating how to control an output device (LED) and read an input device (Button) using lgpio.

Key Changes from RPi.GPIO:

  1. Handles: You must open a connection to the GPIO chip (gpiochip_open) and store the handle.
  2. Claims: You must explicitly "claim" a line as input or output. This prevents resource conflicts between processes.
  3. Cleanup: Explicitly closing the chip handle is critical to release kernel resources.
import lgpio
import time
import signal
import sys

# Configuration
# On RPi 5, the primary GPIO header is typically on chip 0.
# The pin numbers below correspond to BCM numbering (e.g., GPIO 17, GPIO 27).
LED_PIN = 17
BUTTON_PIN = 27
CHIP_NUM = 0

def signal_handler(sig, frame):
    """
    Gracefully handle Ctrl+C to ensure resources are freed.
    """
    print("\nExiting safely...")
    raise KeyboardInterrupt

def main():
    h = None
    try:
        # 1. Open the GPIO Chip
        # Returns a handle (integer) representing the open device file
        h = lgpio.gpiochip_open(CHIP_NUM)
        print(f"GPIO Chip {CHIP_NUM} opened successfully. Handle: {h}")

        # 2. Claim Output Pin (LED)
        # lgpio.gpio_claim_output(handle, gpio_pin, level)
        # We initialize it to 0 (LOW)
        lgpio.gpio_claim_output(h, LED_PIN, 0)
        print(f"Claimed GPIO {LED_PIN} as OUTPUT.")

        # 3. Claim Input Pin (Button)
        # lgpio.gpio_claim_input(handle, gpio_pin, flags)
        # LGPIO_PULL_UP ensures the pin reads 1 when floating (button open)
        # and 0 when pressed (assuming button connects to Ground).
        lgpio.gpio_claim_input(h, BUTTON_PIN, lgpio.LGPIO_PULL_UP)
        print(f"Claimed GPIO {BUTTON_PIN} as INPUT with Pull-Up.")

        print("System Ready. Press the button attached to GPIO 27.")

        while True:
            # 4. Read Input
            # Returns 0 (Low) or 1 (High)
            button_state = lgpio.gpio_read(h, BUTTON_PIN)

            if button_state == 0:
                # Button pressed (connected to Ground)
                print("Button Pressed! LED ON.")
                lgpio.gpio_write(h, LED_PIN, 1)
            else:
                # Button released (pulled up)
                lgpio.gpio_write(h, LED_PIN, 0)

            # Debounce/Sleep to reduce CPU usage
            time.sleep(0.05)

    except KeyboardInterrupt:
        pass
    except Exception as e:
        print(f"An unexpected error occurred: {e}")
    finally:
        # 5. Resource Cleanup
        # Crucial: Release the hold on GPIO lines so other processes can use them.
        if h is not None:
            lgpio.gpio_free(h, LED_PIN)
            lgpio.gpio_free(h, BUTTON_PIN)
            lgpio.gpiochip_close(h)
            print("GPIO resources released and chip closed.")

if __name__ == "__main__":
    # Register the signal handler for SIGINT (Ctrl+C)
    signal.signal(signal.SIGINT, signal_handler)
    main()

Technical Breakdown

The gpiochip_open(0) Handle

In RPi.GPIO, the setup was global (GPIO.setmode). In lgpio, we treat the GPIO controller as a file resource. gpiochip_open(0) returns a file descriptor. On the Raspberry Pi 5, the OS maps the RP1's GPIO bank to the first logical GPIO chip. This handle (h) is required for all subsequent calls, allowing your script to potentially talk to multiple GPIO chips (e.g., IO expanders) simultaneously without ambiguity.

Claiming vs. Setting Direction

RPi.GPIO allowed you to simply set a direction (GPIO.setup). If another process was using that pin, it might silently override it or cause a conflict. lgpio enforces a "Claim" mechanism. gpio_claim_output tells the Linux kernel: "I need exclusive control over this line." If the kernel or another process has locked this pin, the Python script will throw an exception immediately, making debugging much easier than troubleshooting phantom voltage issues.

The Loss of "Board" Numbering

You will notice the code uses 17 and 27. These are the Broadcom (BCM) GPIO numbers. lgpio works strictly with GPIO line offsets. The concept of "Board" numbering (physical pin numbers 1-40) is an abstraction layer that lgpio does not natively support. To maintain technical rigor, you should refer to the Raspberry Pi 5 Pinout and map your physical connections to the BCM/GPIO numbers.

Conclusion

The transition to Raspberry Pi 5 requires Python developers to stop thinking about "poking memory addresses" and start thinking about "requesting resources from the kernel." While the loss of RPi.GPIO is a friction point, the move to lgpio and the Linux GPIO character device API results in code that is safer, more portable, and aligned with modern Linux architectural standards.