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.
| 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)
| 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 |
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.
- Run
i2cdetect -y 1: If the grid is entirely empty or showsUUon every address, your bus is physically locked up or lacks pull-up resistors. - 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.
- 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:
- 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.txtusingdtparam=i2c_baudrate=10000, or use shorter wires. - 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.
- 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-upgradeto 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++.






