When you examine the architecture of Raspberry Pi 5, the biggest shock for veteran hardware hackers isn't the faster Cortex-A76 CPU—it's the RP1 southbridge. Unlike previous generations where the main SoC handled every peripheral directly, the Pi 5 offloads all GPIO, I2C, SPI, USB, and Ethernet traffic to a custom-designed RP1 chip connected via a PCIe 2.0 lane. This architectural shift solves legacy bugs (like I2C clock stretching) but introduces new hardware interfacing quirks that will brick your weekend project if you aren't prepared.

This guide breaks down the Pi 5's RP1 architecture, provides a complete hardware-interfacing project using an I2C ADC and a PWM fan, and details exactly how to debug the infamous I2C bus errors that plague makers migrating from older Pi models.

The Shift to RP1: How Raspberry Pi 5 Architecture Changes Hardware Interfacing

In the Pi 4 (BCM2711), the GPIO pins were physically wired directly to the main processor's silicon. This meant bit-banging protocols like WS2812B LEDs was easy, but the I2C controller was notoriously bad at handling "clock stretching"—a feature where a slow sensor holds the clock line low to buy processing time. The Pi 4 would frequently drop the bus or lock up.

The Raspberry Pi RP1 datasheet reveals a completely different topology. The BCM2712 SoC handles compute and memory, while the RP1 acts as a massive I/O expander. Because the RP1 has its own dedicated ARM Cortex-M33 cores and localized SRAM, peripheral timing is vastly more deterministic. However, the electrical characteristics of the RP1's GPIO pads differ slightly from the legacy BCM2835/2711 pads, particularly regarding internal pull-up resistors on I2C lines.

Table 1: Pi 4 vs. Pi 5 Architecture & Peripheral Spec Sheet
Feature Raspberry Pi 4 (BCM2711) Raspberry Pi 5 (BCM2712 + RP1)
Peripheral Routing Direct to Main SoC silicon Routed via RP1 Southbridge (PCIe 2.0 x1)
I2C Clock Stretching Buggy / Unreliable (Hardware bug) Fully supported in RP1 hardware
GPIO Voltage Logic 3.3V (Direct SoC VDD) 3.3V (RP1 VIO bank, separately powered)
Hardware PWM Channels 2 Channels (Shared with Audio) 4+ Dedicated RP1 PWM slices (No audio conflict)
Default I2C Pull-ups ~1.8kΩ internal (Often sufficient) Weak/Disabled by default (External 4.7kΩ required)

Project Build: I2C Sensor and PWM Fan Control on the RP1 Southbridge

To demonstrate the RP1's I2C and PWM capabilities, we will build a thermal management system. We'll read an analog thermistor via an ADS1115 16-bit ADC over I2C, and map that temperature to a 5V PWM cooling fan. This project specifically targets the Raspberry Pi 5 (8GB variant) running Raspberry Pi OS (Bookworm or later), utilizing the modern gpiozero library which relies on the lgpio backend required for the RP1 architecture.

Parts List & Pin Mapping

  • Board: Raspberry Pi 5 (8GB) with active cooler
  • ADC: Adafruit ADS1115 16-Bit ADC Breakout (Product ID: 1085)
  • Fan: 5V 40x40mm PWM Cooling Fan (4-pin)
  • Passives: 2x 4.7kΩ pull-up resistors, 10kΩ NTC thermistor, 10kΩ fixed resistor (voltage divider)
  • Power: 27W USB-C PD Power Supply (Official Pi 5)
Table 2: Pi 5 GPIO Pin Mapping
Pi 5 Pin (BCM) Function Target Module Pin Notes
GPIO 2 (SDA1) I2C Data ADS1115 SDA Add 4.7kΩ pull-up to 3.3V
GPIO 3 (SCL1) I2C Clock ADS1115 SCL Add 4.7kΩ pull-up to 3.3V
GPIO 18 Hardware PWM0 Fan PWM Wire (Blue) RP1 dedicated PWM slice
Pin 1 (3.3V) VCC ADS1115 VDD Do NOT use 5V for I2C logic
Pin 6 (GND) Ground ADS1115 GND / Fan GND Common ground required
Wiring Warning: The Pi 5 GPIO header remains 3.3V logic. If you connect a 5V I2C device without a logic level converter (like the BSS138), you will back-feed 5V into the RP1 southbridge VIO bank, potentially destroying the I2C pad. Keep the ADS1115 powered at 3.3V for this build.

Complete Python Control Code (Target: Pi 5 / Bookworm OS)

The following script uses smbus2 for raw I2C register manipulation and gpiozero for PWM control. It includes robust error handling specifically designed to catch the I2C bus faults common in poorly wired RP1 setups.

import smbus2
import time
from gpiozero import PWMOutputDevice
import sys

# --- PIN & CONFIG DEFINITIONS ---
FAN_PIN = 18          # BCM 18 (Hardware PWM0 on Pi 5)
I2C_BUS = 1           # /dev/i2c-1
ADC_ADDR = 0x48       # Default ADS1115 address (ADDR pin to GND)

# ADS1115 Register Pointers
CONFIG_REG = 0x01
CONVERSION_REG = 0x00

# Initialize PWM Fan (Frequency 25kHz is standard for PC fans)
fan = PWMOutputDevice(FAN_PIN, frequency=25000, initial_value=0)

# Initialize I2C Bus
try:
    bus = smbus2.SMBus(I2C_BUS)
except FileNotFoundError:
    print("CRITICAL: I2C bus not found. Run 'sudo raspi-config' and enable I2C.")
    sys.exit(1)

def read_adc_voltage():
    """Reads Channel 0 from ADS1115 and returns voltage."""
    # Config: OS=1, MUX=000 (AIN0/GND), PGA=010 (+/-2.048V), MODE=1, DR=100
    config = 0x8583 
    
    # Write config register
    config_bytes = [(config >> 8) & 0xFF, config & 0xFF]
    bus.write_i2c_block_data(ADC_ADDR, CONFIG_REG, config_bytes)
    
    # Wait for conversion (approx 8ms for 128 SPS)
    time.sleep(0.01) 
    
    # Read conversion register
    data = bus.read_i2c_block_data(ADC_ADDR, CONVERSION_REG, 2)
    raw_adc = (data[0] << 8) | data[1]
    
    # Handle two's complement for negative voltages
    if raw_adc > 0x7FFF:
        raw_adc -= 0x10000
        
    # Convert to voltage (PGA set to 2.048V, 16-bit resolution)
    voltage = raw_adc * (2.048 / 32768.0)
    return voltage

def voltage_to_temp_c(voltage):
    """Simple Steinhart-Hart approximation for 10k NTC thermistor."""
    if voltage <= 0.01:
        return 99.0 # Prevent divide by zero / disconnected sensor
    # Assuming 3.3V reference, 10k fixed resistor, 10k NTC to GND
    resistance = 10000 * ((3.3 / voltage) - 1)
    # Simplified Beta parameter equation (Beta = 3950, R0=10k at 25C)
    import math
    temp_k = 1.0 / ((1.0 / 298.15) + (1.0 / 3950) * math.log(resistance / 10000))
    return temp_k - 273.15

def main_loop():
    print("Starting Pi 5 RP1 Thermal Controller...")
    try:
        while True:
            try:
                v = read_adc_voltage()
                temp_c = voltage_to_temp_c(v)
                
                # Map temperature (30C to 60C) to Fan Duty Cycle (0.2 to 1.0)
                if temp_c < 30.0:
                    duty = 0.0
                elif temp_c > 60.0:
                    duty = 1.0
                else:
                    duty = 0.2 + ((temp_c - 30.0) / 30.0) * 0.8
                    
                fan.value = duty
                print(f"Temp: {temp_c:5.1f}C | Voltage: {v:4.2f}V | Fan PWM: {duty*100:4.1f}%")
                
            except OSError as e:
                # Catching the specific I2C bus error
                if e.errno == 121:
                    print(f"I2C FAULT: {e}. Check pull-ups and wiring.")
                else:
                    print(f"I2C Unknown Error: {e}")
            
            time.sleep(1.0)
            
    except KeyboardInterrupt:
        print("\nShutting down safely...")
    finally:
        fan.off()
        bus.close()

if __name__ == "__main__":
    main_loop()

Debugging the Dreaded I2C "Remote I/O Error"

When migrating code to the Pi 5's RP1 architecture, the most common failure mode is the I2C bus throwing an exception. If your terminal outputs the exact error string OSError: [Errno 121] Remote I/O error, the Linux kernel's I2C driver is telling you that it sent a clock pulse but received no ACK (acknowledge) bit from the slave device.

The First Three Things to Check When It Fails:
  1. Run i2cdetect -y 1: If the grid is entirely empty or shows UU on every address, your bus is physically locked up or lacks pull-up resistors.
  2. Verify External Pull-ups: The RP1 silicon does not enable strong internal pull-ups on I2C pins by default like the BCM2835 did. You must have physical 4.7kΩ resistors pulling SDA and SCL to 3.3V.
  3. Check VCC Logic Levels: Ensure the ADS1115 VDD pin is connected to 3.3V (Pin 1), not 5V. A 5V sensor will not ACK a 3.3V clock signal from the RP1.

Ranked Causes and Fixes for Errno 121

If the first three checks don't clear the error, work down this ranked list of architectural and physical failure modes:

  1. Bus Capacitance Overload (Most Likely on Breadboards): The RP1 I2C pads have a strict capacitance limit. If you are using long jumper wires (>15cm) or a cheap breadboard with high parasitic capacitance, the 3.3V rise time will be too slow, and the RP1 will read it as a logic LOW. Fix: Lower the I2C bus speed to 10kHz in /boot/firmware/config.txt using dtparam=i2c_baudrate=10000, or use shorter wires.
  2. Address Collision or Lockup: Some cheap clone ADS1115 modules ship with the wrong I2C address or lock up if powered on without the host clock running. Fix: Power cycle the sensor while the Pi is already booted and running the I2C clock.
  3. RP1 Driver Bug (Kernel Level): Early releases of the Pi 5 Linux kernel had a bug where the RP1 I2C driver would fail to clear the TX FIFO buffer after a NACK, permanently stalling the bus until reboot. Fix: Run sudo apt update && sudo apt full-upgrade to ensure you are on the latest Bookworm kernel (6.6.x or newer).

Extending or Simplifying the Build

The beauty of the RP1 architecture is its scalability. Because the southbridge handles peripheral DMA independently of the main CPU, you can add heavy I/O loads without stuttering your main application.

  • To Simplify: If you don't need 16-bit precision, ditch the ADS1115 and use a cheap analog TMP36 sensor. However, remember that the Pi 5 (like all Pi models) lacks a built-in ADC. You will still need an I2C or SPI ADC chip, or you can use an RC (resistor-capacitor) charging circuit on a standard GPIO pin to estimate analog voltage, though this is highly inaccurate.
  • To Extend: Add a second I2C device, like an BME680 environmental sensor. Because the RP1 supports true hardware clock stretching, you can safely put the BME680 (which stretches the clock heavily during gas sensor heating) on the exact same I2C bus as the ADS1115 without crashing the Pi—a feat that was nearly impossible on the Pi 4.
  • High-Speed SPI Alternative: If you need to sample data faster than the 860 SPS limit of the ADS1115, swap to an MCP3008 SPI ADC. The RP1's SPI controllers can easily sustain 12MHz+ clock speeds via DMA, allowing for audio-rate sampling directly into Python or C++.
Project Difficulty: Intermediate (3/5)
Estimated Build Time: 45 Minutes
Core Takeaway: Respecting the RP1 southbridge's electrical requirements—specifically regarding pull-up resistors and logic levels—is the key to unlocking the Raspberry Pi 5's vastly superior peripheral architecture.