If you are migrating from a Raspberry Pi 4 to a Raspberry Pi 5, the Raspberry Pi GPIO header looks identical—40 pins, 0.1-inch pitch, 26 usable general-purpose I/O lines. But under the hood, the Pi 5’s new RP1 southbridge chip completely changes how those pins are addressed, powered, and protected. Legacy libraries like RPi.GPIO are dead on the Pi 5, and hardware PWM routing has shifted.
This guide cuts through the upgrade friction. We will build a closed-loop 12V PWM cooling fan controller driven by a BME280 environmental sensor, specifically targeting the Raspberry Pi 5 (8GB variant). You will get the exact pin mapping, the electrical limits of the RP1 chip, a fully compilable Python script using modern libraries, and a debugging matrix for the exact error strings that halt Pi 5 GPIO projects.
The Pi 5 GPIO Shift: RP1 Southbridge and 3.3V Logic
On the Pi 4 and earlier, the Broadcom SoC handled GPIO directly. On the Pi 5, the RP1 southbridge handles all peripheral I/O. This architectural shift means the GPIO base memory address changed, breaking older C-based Python wrappers. Furthermore, the RP1 chip enforces stricter current limits and relies heavily on the lgpio backend for user-space access.
The golden rule for Pi 5 GPIO remains: it is strictly 3.3V logic. Feeding 5V into any GPIO pin (including I2C SDA/SCL) will permanently destroy the RP1 silicon. If you are interfacing with 5V sensors or 12V loads like PC case fans, you must use logic-level MOSFETs or level shifters.
Parts List & Hardware Specifications
This build uses off-the-shelf maker components. Do not substitute the MOSFET with a standard bipolar junction transistor (like a TIP120); the voltage drop and heat dissipation will ruin the PWM efficiency.
- Microcontroller: Raspberry Pi 5 (8GB RAM) running Raspberry Pi OS (64-bit, Bookworm or newer)
- Sensor: BME280 I2C Temperature/Humidity/Pressure breakout (3.3V tolerant, default address 0x76 or 0x77)
- Actuator: 12V 4-Pin PWM PC Case Fan (e.g., Noctua NF-A12x25 PWM or be quiet! Silent Wings 4)
- Driver: IRLZ44N N-Channel Logic-Level MOSFET (Threshold voltage Vgs(th) < 2V, fully on at 3.3V)
- Passives: 100Ω gate resistor, 10kΩ gate-to-source pull-down resistor, 4.7kΩ I2C pull-up resistors (if not on breakout)
- Power: 12V 2A DC power supply (barrel jack or bench supply) for the fan
Pin Mapping & Electrical Limits
The table below details the specific Raspberry Pi GPIO pins used in this build. Note the Pi 5 RP1 specific current limits. While the absolute maximum per pin is 50mA, the RP1 documentation recommends keeping continuous draw under 8mA per pin to avoid brownouts and thermal throttling of the southbridge.
| BCM Pin | Physical Pin | Function in Build | Max Continuous Current | Logic Level | Pi 5 RP1 Specific Notes |
|---|---|---|---|---|---|
| 2 (SDA1) | 3 | I2C Data (BME280) | 8 mA (recommended) | 3.3V | Requires external 4.7kΩ pull-up to 3.3V if breakout lacks them. |
| 3 (SCL1) | 5 | I2C Clock (BME280) | 8 mA (recommended) | 3.3V | Shares I2C bus 1. Do not exceed 400kHz clock speed. |
| 18 (PWM0) | 12 | Fan PWM Control | 12 mA (absolute max) | 3.3V | Hardware PWM. Must use PWM0 or PWM1 channels for jitter-free fan control. |
| 17 | 11 | Manual Override Button | 8 mA (recommended) | 3.3V | Configure with internal pull-up via gpiozero to avoid floating state. |
| 3V3 | 1 / 17 | Sensor VCC | 50 mA (total rail) | 3.3V | Fused. If you short this, the Pi 5 will hard-reset via the PMIC. |
| GND | 6, 9, 14, 20 | Common Ground | N/A | 0V | Must be bonded to the 12V fan power supply ground. |
Wiring the MOSFET Fan Driver & I2C Bus
Driving a 12V inductive load (the fan motor) directly from a 3.3V GPIO pin will instantly fry the RP1 chip. We use the IRLZ44N MOSFET as a low-side switch. The 3.3V GPIO signal turns the MOSFET on, completing the 12V circuit through the fan.
- Gate Drive: Connect BCM 18 (Physical 12) to the MOSFET Gate via a 100Ω resistor. This resistor prevents high-frequency ringing and protects the GPIO pin from inductive kickback.
- Pull-Down: Connect a 10kΩ resistor between the MOSFET Gate and Source (Ground). This ensures the fan stays off while the Pi is booting and the GPIO pins are in a high-impedance state.
- Load: Connect the Fan's GND wire (usually black) to the MOSFET Drain. Connect the Fan's +12V wire (usually yellow/red) to your 12V power supply.
- PWM Wire: Connect the Fan's PWM control wire (usually blue) directly to BCM 18. Note: Some 4-pin fans require the PWM wire to be driven by the microcontroller, while others just need the ground switched. If your fan doesn't spin, switch to 2-pin mode by only switching the ground.
- I2C Sensor: Connect BME280 VCC to 3.3V, GND to GND, SDA to BCM 2, and SCL to BCM 3.
- Common Ground: Crucial step—bond the Raspberry Pi GND to the 12V Power Supply GND. Without a shared reference, the 3.3V gate signal is meaningless to the MOSFET.
Complete Python Control Script
This script targets the Raspberry Pi 5 using gpiozero (which natively uses the lgpio backend on Bookworm) and smbus2 for raw I2C communication. It reads the temperature and scales the fan PWM duty cycle proportionally.
Prerequisites: Run sudo apt update && sudo apt install python3-gpiozero python3-smbus2 and ensure I2C is enabled via sudo raspi-config.
#!/usr/bin/env python3
"""
Raspberry Pi 5 PWM Fan Controller & Environmental Monitor
Target Board: Raspberry Pi 5 (8GB)
"""
import time
import sys
from gpiozero import PWMLED, Button
from smbus2 import SMBus
# --- PIN & I2C DEFINITIONS (BCM Numbering) ---
FAN_PWM_PIN = 18 # Physical Pin 12 (Hardware PWM0)
BUTTON_PIN = 17 # Physical Pin 11
I2C_BUS_ID = 1 # /dev/i2c-1
BME280_ADDR = 0x76 # Change to 0x77 if using Adafruit breakout
# --- TEMPERATURE THRESHOLDS (Celsius) ---
TEMP_MIN = 30.0 # Below this, fan is off (0% duty)
TEMP_MAX = 50.0 # Above this, fan is max (100% duty)
# Initialize Hardware
# frequency=25000 is standard for 4-pin PC fans (25kHz PWM)
fan = PWMLED(FAN_PWM_PIN, frequency=25000)
override_btn = Button(BUTTON_PIN, pull_up=True, bounce_time=0.05)
def read_temperature():
"""Reads temperature from BME280 via I2C. Returns float or None on error."""
try:
with SMBus(I2C_BUS_ID) as bus:
# Trigger single measurement (osrs_t=1, osrs_p=0, osrs_h=0, mode=1)
bus.write_byte_data(BME280_ADDR, 0xF4, 0x25)
time.sleep(0.1) # Wait for measurement
# Read 3 bytes of temperature data (0xFA, 0xFB, 0xFC)
data = bus.read_i2c_block_data(BME280_ADDR, 0xFA, 3)
# Bitwise conversion (simplified for demo, assumes calibrated offset is negligible)
raw_temp = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4)
# Note: A production script MUST apply the BME280 calibration parameters
# from registers 0x88-0x9F. This raw calculation is for structural demonstration.
temp_c = (raw_temp / 16384.0) - 25.0
return round(temp_c, 2)
except OSError as e:
print(f"[I2C ERROR] {e}")
return None
def calculate_duty_cycle(temp):
"""Maps temperature to a 0.0 - 1.0 PWM duty cycle."""
if temp <= TEMP_MIN:
return 0.0
if temp >= TEMP_MAX:
return 1.0
return (temp - TEMP_MIN) / (TEMP_MAX - TEMP_MIN)
def main():
print(f"Starting Pi 5 Fan Controller on BCM {FAN_PWM_PIN}...")
print("Press GPIO 17 button to force 100% fan speed.")
try:
while True:
temp = read_temperature()
if override_btn.is_pressed:
fan.value = 1.0
status = "OVERRIDE"
elif temp is not None:
duty = calculate_duty_cycle(temp)
fan.value = duty
status = f"{duty*100:.0f}%"
else:
# Failsafe: If sensor drops out, run fan at 50% to prevent overheating
fan.value = 0.5
status = "FAILSAFE 50%"
print(f"Temp: {temp}°C | Fan: {status}")
time.sleep(2.0)
except KeyboardInterrupt:
print("\nShutting down gracefully...")
finally:
fan.off()
fan.close()
print("Fan stopped. GPIO released.")
if __name__ == "__main__":
main()
Debugging: Exact Error Strings & Ranked Causes
When working with the Raspberry Pi GPIO on the Pi 5, you will inevitably hit hardware or permission walls. Here are the exact error strings the Python interpreter will throw, ranked by likelihood, and how to fix them.
Error 1: RuntimeError: Cannot determine SOC peripheral base address
Cause: You are trying to use the legacy RPi.GPIO library on a Raspberry Pi 5. The library attempts to read /proc/cpuinfo or map /dev/mem to find the Broadcom base address, which no longer exists for GPIO on the RP1 chip.
Fix: Uninstall RPi.GPIO. Rewrite your script using gpiozero (which automatically detects the Pi 5 and uses the lgpio backend) or use the rpi-lgpio drop-in replacement package.
Error 2: OSError: [Errno 121] Remote I/O error
Cause: The I2C bus is failing to acknowledge the BME280 sensor. This is the most common hardware fault.
Ranked Fixes:
- Wrong Address: Run
i2cdetect -y 1in the terminal. If your sensor shows up at0x77instead of0x76, update theBME280_ADDRvariable in the script. - Missing Pull-ups: The RP1 I2C pins do not have strong internal pull-ups. Measure the voltage on SDA and SCL with a multimeter. If they are not at ~3.2V-3.3V, solder 4.7kΩ resistors between the SDA/SCL lines and the 3.3V pin.
- Ground Loop: Ensure the sensor GND is tied directly to the Pi GND, not just floating through a breadboard power rail that might have a bad contact.
Error 3: gpiozero.exc.PinPWMUnsupported
Cause: You assigned the fan to a pin that does not support hardware PWM, and the system is struggling to fall back to software PWM under load, or the backend explicitly blocks it.
Fix: Move the fan control wire to BCM 12, 13, 18, or 19. These are the dedicated hardware PWM pins on the Pi 5. BCM 18 (Physical 12) is the most reliable choice for 25kHz PC fan signals.
Simplifying or Extending the Build
Not every project needs a full proportional control loop, and some need much more. Here is how to adapt this architecture to your specific constraints.
How to Simplify (The Binary Thermostat)
If you don't have a 4-pin PWM fan and only have a standard 2-pin 12V DC fan, you can drop the PWM requirement entirely. Swap the PWMLED class for a standard LED class in gpiozero. Change the control logic to a simple hysteresis loop: turn the MOSFET fully ON (fan.on()) when temp > 45°C, and fully OFF (fan.off()) when temp < 40°C. This prevents the relay/MOSFET from rapid-cycling at the threshold boundary.
How to Extend (Home Assistant Integration)
To push this data to a smart home dashboard, integrate the paho-mqtt Python library. Inside the while True loop, format the temperature and fan duty cycle into a JSON payload and publish it to an MQTT broker (e.g., Mosquitto) running on your network. Home Assistant can then ingest the MQTT topic via the MQTT integration, allowing you to graph the thermal performance of your enclosure over time and set up mobile alerts if the failsafe triggers.
For further reading on Pi 5 pinouts and backend changes, consult the interactive diagrams at Pinout.xyz and the official gpiozero documentation.






