Getting the raspberry pi pin config right is the difference between a reliable embedded system and a frustrating afternoon of chasing ghost voltages. Whether you are routing I2C data lines or driving a hardware PWM signal, the Pi's 40-pin header requires exact mapping, correct pull-up states, and modern Python libraries. In this guide, we will build an environmental monitoring node with active PWM cooling, map the exact pins, write the control code, and debug the most common I2C failure modes you will encounter on the bench.
Project Spec Sheet & Parts List
Difficulty Rating: Intermediate (Requires basic I2C theory and Linux terminal navigation)
Target Board Variant: Raspberry Pi 4 Model B (4GB RAM) running Raspberry Pi OS (Bookworm). Note: Pi 5 uses the RP1 southbridge chip, which alters GPIO memory mapping; see the FAQ for Pi 5 specifics.
Estimated Build Time: 45 minutes
Required Hardware
- Microcontroller: Raspberry Pi 4 Model B (4GB)
- Sensor: BME280 I2C Temperature/Humidity/Pressure breakout (Adafruit 2652 or generic 3.3V variant with onboard pull-ups)
- Actuator: Noctua NF-A4x10 5V PWM Fan (Accepts 3.3V PWM logic natively)
- Wiring: 22 AWG silicone Dupont jumper wires (Female-to-Female)
- Power: Official Raspberry Pi 27W USB-C Power Supply
Raspberry Pi Pin Config Mapping
The Pi uses the Broadcom (BCM) GPIO numbering system in software, but physical board pin numbers for wiring. Below is the exact mapping for this build. We are using I2C Bus 1 for the sensor and Hardware PWM0 for the fan.
| Component | Breakout Pin | Pi Physical Pin | BCM GPIO / Function | Wire Color (Suggested) |
|---|---|---|---|---|
| BME280 | VCC / VIN | Pin 1 | 3.3V Power | Red |
| BME280 | GND | Pin 6 | Ground | Black |
| BME280 | SCL | Pin 5 | GPIO 3 (SCL1) | Yellow |
| BME280 | SDA | Pin 3 | GPIO 2 (SDA1) | Orange |
| Noctua Fan | PWM (Pin 4) | Pin 12 | GPIO 18 (PWM0) | Blue |
| Noctua Fan | VCC (Pin 1/2) | Pin 2 or 4 | 5V Power | Red (Stripe) |
| Noctua Fan | GND (Pin 1/2) | Pin 9 | Ground | Black (Stripe) |
Wiring Steps & Python Implementation
Before writing code, ensure your physical connections are secure. I2C is highly sensitive to loose Dupont connections, which cause capacitance spikes and bus lockups.
1. Physical Wiring & OS Prep
- Power down the Pi and disconnect the USB-C cable.
- Wire the BME280 and Noctua fan according to the pin mapping table above.
- Boot the Pi and open the terminal. Enable the I2C interface by running
sudo raspi-config, navigating to Interface Options > I2C, and selecting Yes. - Install the required Python libraries for I2C communication and GPIO control:
sudo apt update && sudo apt install python3-smbus i2c-tools python3-gpiozero. - Install the BME280 wrapper:
pip3 install RPi.bme280.
2. Complete Control Script
This script reads the BME280 over I2C and dynamically adjusts the fan speed via hardware PWM based on the temperature threshold. It includes robust error handling for bus failures.
import time
import sys
import smbus2
import bme280
from gpiozero import PWMOutputDevice
# --- PIN & BUS DEFINITIONS ---
I2C_BUS_ID = 1
BME280_I2C_ADDR = 0x76 # Check with `i2cdetect -y 1` (some are 0x77)
FAN_PWM_PIN = 18 # BCM GPIO 18 (Physical Pin 12)
# --- THRESHOLDS ---
TEMP_IDLE = 45.0 # Below this, fan is off
TEMP_MAX = 65.0 # At or above this, fan is 100%
def setup_hardware():
"""Initialize I2C bus and PWM device with error handling."""
try:
bus = smbus2.SMBus(I2C_BUS_ID)
calibration_params = bme280.load_calibration_params(bus, BME280_I2C_ADDR)
print("[OK] BME280 initialized on I2C Bus 1.")
except OSError as e:
print(f"[FATAL] I2C Initialization Failed: {e}")
print("Check wiring, ensure I2C is enabled in raspi-config, and verify address.")
sys.exit(1)
# Initialize PWM fan (gpiozero handles hardware PWM on GPIO 18 automatically)
fan = PWMOutputDevice(FAN_PWM_PIN, frequency=25000) # 25kHz is standard for PC fans
fan.value = 0 # Start at 0% duty cycle
print(f"[OK] PWM Fan initialized on GPIO {FAN_PWM_PIN}.")
return bus, calibration_params, fan
def calculate_fan_duty_cycle(temp_c):
"""Map temperature to a 0.0 - 1.0 PWM duty cycle."""
if temp_c <= TEMP_IDLE:
return 0.0
elif temp_c >= TEMP_MAX:
return 1.0
else:
# Linear interpolation between idle and max
return (temp_c - TEMP_IDLE) / (TEMP_MAX - TEMP_IDLE)
def main_loop():
bus, params, fan = setup_hardware()
try:
while True:
try:
# Read sensor data
data = bme280.sample(bus, BME280_I2C_ADDR, params)
temp_c = data.temperature
# Calculate and apply fan speed
duty_cycle = calculate_fan_duty_cycle(temp_c)
fan.value = duty_cycle
print(f"Temp: {temp_c:.2f}C | Humidity: {data.humidity:.1f}% | Fan PWM: {duty_cycle*100:.0f}%")
except OSError as e:
# Catches transient I2C bus lockups or disconnected wires during runtime
print(f"[WARN] I2C Read Error: {e}. Retrying in 5s...")
fan.value = 1.0 # Fail-safe: spin fan to 100% on sensor loss
time.sleep(5)
continue
time.sleep(2)
except KeyboardInterrupt:
print("\n[INFO] Shutting down gracefully...")
finally:
fan.off()
bus.close()
print("[INFO] Fan stopped and I2C bus closed.")
if __name__ == "__main__":
main_loop()
Debugging: "Remote I/O error" and Pin Config Failures
When working with the Pi's I2C bus, you will inevitably encounter bus lockups or configuration mismatches. The most common failure when running the script above is:
OSError: [Errno 121] Remote I/O error
Ranked Causes and Fixes
- I2C Interface Disabled in OS: The Pi OS does not enable I2C by default. Run
sudo raspi-config, go to Interface Options, and enable I2C. Reboot. - Wrong I2C Bus Selected: The Pi 4 has multiple I2C buses. Bus 0 is reserved for the HAT EEPROM. Your code must use
I2C_BUS_ID = 1. If your code tries to query Bus 0, it will throw Errno 121. - Incorrect I2C Address: The BME280 usually defaults to
0x76, but Adafruit breakouts often default to0x77. Runi2cdetect -y 1in the terminal. If you see77in the grid, update your Python variable. - Missing Pull-Up Resistors: While the Pi has 1.8kΩ internal pull-ups on SDA1/SCL1, long wire runs (>30cm) or cheap sensor breakouts without onboard pull-ups will cause signal degradation, resulting in intermittent Errno 121 crashes. Add external 4.7kΩ pull-ups to 3.3V if wires are long.
The First Three Things to Check When It Fails
If your script crashes immediately upon execution, perform this exact triage sequence:
- Verify Hardware Visibility: Run
i2cdetect -y 1. If the grid is entirely empty (only dashes), your hardware is not communicating. Check VCC/GND wiring. - Verify Voltage Levels: Use a multimeter to measure DC voltage between Pin 1 (3.3V) and Pin 6 (GND). It must read between 3.25V and 3.35V. If it reads 5V, you are plugged into Pin 2, and you have likely fried the sensor's logic level.
- Check Library Deprecations: If you are using the older
RPi.GPIOlibrary instead ofgpiozero, it will fail on newer OS kernels and completely fail on Pi 5. Always usegpiozerofor modern PWM control.
Extending and Simplifying the Build
Depending on your end goal, you may want to strip this project down to its bare essentials or scale it up into a full IoT node.
How to Simplify
If you only need data logging and do not require active thermal management, remove the Noctua fan and the gpiozero dependencies entirely. Strip the script down to just the smbus2 and bme280 read loop, and append the output to a local CSV file using Python's built-in csv module. This reduces the hardware footprint to just four jumper wires and lowers the Pi's idle power draw by eliminating the 5V fan rail.
How to Extend
To turn this into a smart home node, integrate the Raspberry Pi OS MQTT protocols via the paho-mqtt Python library. Publish the temperature and humidity payloads to a local Mosquitto broker, allowing Home Assistant to ingest the data without polling the Pi. You can also add an SSD1306 128x64 OLED display to the exact same I2C Bus 1 (the SSD1306 typically uses address 0x3C, avoiding conflicts with the BME280) to display real-time metrics locally without needing a monitor attached to the Pi's HDMI port.
Raspberry Pi Pin Config FAQ
How do I verify my current raspberry pi pin config from the command line?
The fastest way to visualize your active pin mapping without looking up a datasheet is using the pinout command. If you have gpiozero installed (which comes pre-installed on Raspberry Pi OS), simply type pinout in the terminal. It will render an ASCII-art diagram of the 40-pin header, color-coding the power, ground, and GPIO pins, and displaying the Broadcom (BCM) numbers alongside the physical pin numbers. This is invaluable when you are SSH'd into a headless Pi and need to verify which physical pin corresponds to BCM GPIO 18.
Why does my raspberry pi pin config fail with a peripheral base address error on Pi 5?
If you port code from a Pi 4 to a Raspberry Pi 5 and encounter RuntimeError: Cannot determine SOC peripheral base address, it is because the Pi 5 abandoned the Broadcom BCM2711 SoC GPIO architecture in favor of the custom RP1 southbridge chip. Older libraries like RPi.GPIO attempt to read memory addresses directly from /dev/mem based on the old Broadcom layout, which no longer exists on the Pi 5. To fix this, you must migrate your code to gpiozero or lgpio, which utilize the modern Linux libgpiod character device interface rather than raw memory mapping. For detailed migration steps, refer to the gpiozero official documentation.
Can I remap I2C pins in my raspberry pi pin config using device tree overlays?
Yes. While Pins 3 and 5 (I2C Bus 1) are the hardware defaults with built-in 1.8kΩ pull-up resistors, you can map I2C to alternative GPIO pins if your physical layout requires it. This is done by editing the /boot/firmware/config.txt file and adding a device tree overlay. For example, adding dtparam=i2c_gpio=on enables a software (bit-banged) I2C bus, which defaults to GPIO 23 (SDA) and GPIO 24 (SCL). Note that bit-banged I2C is significantly slower and more CPU-intensive than the hardware I2C controller, so it should only be used for low-speed sensors like basic temperature probes, not high-throughput devices like OLED displays or ADCs.






