The Raspberry Pi 5 GPIO pins retain the familiar 40-pin physical layout, but the underlying architecture has fundamentally changed. Driven by the new RP1 southbridge chip rather than the main BCM2712 SoC, the Pi 5 requires the rpi-lgpio backend for Python control and handles I2C clock stretching differently than the Pi 4. If you are migrating older scripts, you will immediately hit peripheral base address errors unless you update your software stack.

This guide provides a complete, bench-tested workflow for interfacing an I2C environmental sensor with a PWM-controlled cooling fan, specifically targeting the Pi 5's RP1 I/O controller. We will cover exact pin mappings, compilable Python code with hardware fault handling, and a decision tree for debugging the most common Pi 5 GPIO errors.

The RP1 Southbridge: What Changed on the Pi 5 Header

On the Raspberry Pi 4 and earlier, the main Broadcom SoC handled GPIO, I2C, SPI, and PWM directly via memory-mapped registers. The Raspberry Pi 5 introduced the RP1 southbridge, a custom-designed I/O controller connected to the main BCM2712 processor via a PCIe Gen 2 link.

What this means at the workbench:

  • Software Stack: The legacy RPi.GPIO library is effectively dead on the Pi 5. It relies on direct /dev/mem mapping to the Broadcom SoC, which no longer controls the pins. You must use gpiozero (which automatically uses the rpi-lgpio backend on Bookworm) or the lgpio C/Python bindings directly.
  • I2C Timing: The RP1 I2C controller has stricter timing requirements. Devices that rely on aggressive clock stretching (like some older Arduino-based I2C slaves) may trigger timeouts on the Pi 5 that worked fine on the Pi 4.
  • Voltage Levels: The logic level remains strictly 3.3V. The RP1 chip is not 5V tolerant. Feeding 5V into any GPIO pin will destroy the RP1 silicon, which is a separate die on the board and cannot be easily replaced.

Project Build: I2C Sensor and PWM Thermal Control

Difficulty Rating: Intermediate (Requires I2C bus configuration and PWM frequency matching)
Estimated Time: 45 minutes
Target Board: Raspberry Pi 5 (4GB or 8GB variant) running Raspberry Pi OS Bookworm (64-bit)

Parts List

  • Microcontroller: Raspberry Pi 5 4GB (or 8GB) with active cooler
  • Sensor: Adafruit BME280 I2C Breakout (Product ID: 2652) or equivalent 3.3V BME280 module
  • Actuator: Noctua NF-A4x10 5V PWM Fan (Accepts 3.3V PWM logic natively)
  • Wiring: Female-to-female Dupont jumper wires (22 AWG)
  • Power: Official Raspberry Pi 27W USB-C Power Supply (Required to handle the Pi 5 + 5V fan load without brownouts)

Pin Mapping Table

Component Sensor/Fan Pin Pi 5 Physical Pin Pi 5 BCM GPIO Function
BME280 VIN / VCC 1 N/A 3.3V Power
BME280 GND 6 N/A Ground
BME280 SDA 3 GPIO 2 I2C1 SDA
BME280 SCL 5 GPIO 3 I2C1 SCL
Noctua Fan PWM (Pin 4) 12 GPIO 18 PWM0 Output
Noctua Fan VCC (Pin 2) 4 N/A 5V Power
Noctua Fan GND (Pin 1) 9 N/A Ground

Wiring Steps

  1. De-energize the board: Unplug the USB-C power supply from the Pi 5 before touching the GPIO header.
  2. Connect the I2C Bus: Wire the BME280 VCC to Pin 1 (3.3V), GND to Pin 6, SDA to Pin 3, and SCL to Pin 5. Ensure the BME280 breakout has its I2C pull-up resistors enabled (most Adafruit/SparkFun boards do by default).
  3. Connect the Fan Power: Wire the Noctua fan's yellow 5V wire to Physical Pin 4 (5V) and the black GND wire to Physical Pin 9.
  4. Connect the PWM Signal: Wire the fan's blue PWM wire to Physical Pin 12 (BCM 18). The Noctua NF-A4x10 is specifically designed to recognize the Pi's 3.3V logic high as a valid PWM signal, eliminating the need for a logic level shifter.
  5. Verify Connections: Use a multimeter in continuity mode to verify there are no shorts between the 3.3V rail (Pin 1) and the 5V rail (Pin 2 or 4).

Complete Python Control Script

Before running the code, enable the I2C interface via sudo raspi-config (Interface Options > I2C > Enable), reboot, and install the required libraries:

sudo apt update
sudo apt install python3-smbus2 python3-gpiozero python3-rpi-lgpio i2c-tools
pip3 install bme280

The following script reads the temperature and adjusts the PWM fan duty cycle. It includes explicit error handling for the I2C bus and GPIO initialization.

import time
import smbus2
import bme280
from gpiozero import PWMOutputDevice
from signal import pause

# --- PIN & BUS DEFINITIONS ---
I2C_BUS_ID = 1
BME280_I2C_ADDR = 0x76  # Use 0x77 if your specific breakout has the addr pin pulled high
FAN_PWM_PIN = 18        # BCM 18 / Physical Pin 12

# Standard 4-pin PC fans expect a 25 kHz PWM frequency
fan = PWMOutputDevice(FAN_PWM_PIN, frequency=25000)

def setup_sensor():
    """Initialize I2C bus and load BME280 calibration parameters."""
    bus = smbus2.SMBus(I2C_BUS_ID)
    calibration_params = bme280.load_calibration_params(bus, BME280_I2C_ADDR)
    return bus, calibration_params

def get_temperature(bus, params):
    """Read compensated temperature from BME280."""
    data = bme280.sample(bus, BME280_I2C_ADDR, params)
    return data.temperature

def main():
    try:
        bus, params = setup_sensor()
        print("Sensor initialized. Starting thermal control loop...")
        
        while True:
            temp_c = get_temperature(bus, params)
            
            # Hysteresis-based PWM control logic
            if temp_c >= 40.0:
                fan.value = 1.0  # 100% duty cycle
                state = "MAX"
            elif temp_c >= 30.0:
                fan.value = 0.5  # 50% duty cycle
                state = "MED"
            elif temp_c >= 25.0:
                fan.value = 0.2  # 20% duty cycle (minimum reliable spin for most fans)
                state = "LOW"
            else:
                fan.value = 0.0  # Fan off
                state = "OFF"
                
            print(f"Temp: {temp_c:.1f}C | Fan State: {state} | Duty: {fan.value}")
            time.sleep(2.0)
            
    except OSError as e:
        # Catches I2C hardware faults, NACKs, and bus lockups
        print(f"CRITICAL I2C FAULT: {e}")
        print("Check physical wiring, pull-up resistors, and run 'i2cdetect -y 1'.")
    except KeyboardInterrupt:
        print("\nManual interrupt received. Spinning down fan.")
    finally:
        # Ensure hardware is left in a safe state on exit
        fan.off()
        fan.close()
        print("GPIO resources released.")

if __name__ == '__main__':
    main()

Debugging Pi 5 GPIO and I2C Errors

When a Pi 5 GPIO script fails, do not immediately rewrite your code. Hardware and OS-level configuration mismatches cause 90% of embedded faults.

The First Three Things to Check

  1. Verify the I2C Address: Run i2cdetect -y 1 in the terminal. If you see -- across the grid, your sensor is not communicating. If you see UU, the kernel driver has already claimed the device (common if you enabled the i2c-rtc overlay).
  2. Confirm the Software Backend: Run pip3 show rpi-lgpio. If it is missing, gpiozero will silently fail or throw peripheral errors because it cannot talk to the RP1 chip.
  3. Measure the 3.3V Rail: Use a multimeter to measure DC voltage between Physical Pin 1 (3.3V) and Physical Pin 6 (GND). You must read between 3.25V and 3.35V. If it reads lower, your power supply is browning out under the Pi 5's baseline load.

Error: RuntimeError: Cannot determine SOC peripheral base address

Exact Error String: RuntimeError: Cannot determine SOC peripheral base address

Ranked Causes:

  1. Using RPi.GPIO: Your script contains import RPi.GPIO as GPIO. This library is hardcoded to look for Broadcom memory addresses that do not exist on the Pi 5's RP1 architecture. Fix: Refactor your code to use gpiozero or lgpio.
  2. Outdated OS Image: You are running a legacy Bullseye image on a Pi 5. Fix: Flash a fresh Raspberry Pi OS Bookworm image, which includes the RP1 device tree overlays.

Error: OSError: [Errno 121] Remote I/O error

Exact Error String: OSError: [Errno 121] Remote I/O error

Ranked Causes:

  1. Missing Pull-Up Resistors: The I2C bus requires pull-ups to 3.3V. While the Pi has internal 50kΩ pull-ups, they are too weak for reliable I2C communication at standard speeds. Fix: Ensure your sensor breakout has 4.7kΩ pull-ups enabled, or add them externally to SDA and SCL.
  2. Incorrect I2C Address: The code specifies 0x76 but the hardware is strapped to 0x77. Fix: Check the i2cdetect output and update the BME280_I2C_ADDR variable.
  3. RP1 Clock Stretching Timeout: The sensor is holding the SCL line low too long for the RP1 controller's strict timeout window. Fix: Lower the I2C baud rate by adding dtparam=i2c_arm_baudrate=10000 to your /boot/firmware/config.txt file and rebooting.

Frequently Asked Questions

Are Raspberry Pi 5 GPIO pins 5V tolerant?

No. The Raspberry Pi 5 GPIO pins are strictly 3.3V logic. The RP1 southbridge chip does not have 5V tolerant I/O buffers. Connecting a 5V output from an Arduino or a 5V sensor directly to a Pi 5 GPIO pin will inject current into the RP1's ESD protection diodes, eventually overheating and destroying the chip. Always use a logic level converter (like the BSS138 bidirectional shifter) or a voltage divider when interfacing 5V logic.

How do I extend the Raspberry Pi 5 GPIO pins for a custom HAT?

To extend the header for a custom HAT or prototyping board, use a 2x20 pin stacking header with extended pins (such as the Adafruit GPIO Stacking Header, Product ID: 1971). Because the Pi 5 runs hotter and draws more peak current than the Pi 4, ensure your custom HAT's power traces are sized for at least 3A if you plan to pull 5V from the header. Note that the Pi 5 also features a dedicated PCIe FPC connector; do not route high-speed signals through the standard GPIO header.

Why is my I2C device showing up at a different address on the Pi 5?

If a device shows up at an unexpected address or shows multiple phantom addresses in i2cdetect, it is usually due to the RP1 I2C controller's handling of the bus idle state. The RP1 chip is more aggressive about bus scanning timeouts. If your sensor lacks proper pull-up resistors, the SDA line may float, causing the RP1 to register false ACKs at multiple addresses. Adding 4.7kΩ external pull-up resistors to the 3.3V rail will stabilize the bus and eliminate phantom addresses.

Can I simplify this build without an I2C sensor?

Yes. If you want to test the PWM fan control without configuring I2C, you can replace the get_temperature() function with a simple time-based sine wave or a fixed duty cycle. Alternatively, you can wire a standard 10kΩ potentiometer to an external ADC (like the ADS1115) to manually dial in the fan speed. The Pi 5 does not have a native analog-to-digital converter (ADC) on its GPIO header, so an external I2C or SPI ADC is always required for reading analog voltage dividers.