Programming a Raspberry Pi for hardware I/O is no longer just about writing a quick Python script. With the release of the Raspberry Pi 5 and the shift to Raspberry Pi OS Bookworm, you are now dealing with the RP1 southbridge chip architecture and PEP 668 Python environment restrictions. If you are programming a Raspberry Pi 5 to read I2C sensors like the Bosch BME280, a script that worked flawlessly on a Pi 3 in 2019 will likely throw environment errors or I2C bus faults today.
This guide targets the Raspberry Pi 5 (8GB variant) running 64-bit Bookworm. We will wire a BME280 environmental sensor, build a robust Python logging script with proper error handling, and debug the exact I2C errors that halt bench builds.
Hardware BOM, Pin Mapping, and Sensor Specs
Before writing code, we need to lock in the physical layer. The BME280 operates strictly at 3.3V logic. Feeding it 5V from the Pi's 5V rail will instantly destroy the sensor's internal CMOS. Always use the 3.3V pin.
Parts List
- Board: Raspberry Pi 5 (8GB RAM) - ~$80
- Sensor: Adafruit BME280 I2C/SPI Breakout (Product ID 2652) or generic equivalent - ~$15-$20
- Wiring: 28 AWG Silicone Female-to-Female Jumper Wires
- Storage: SanDisk Extreme 32GB microSD (A1 rated for logging write cycles)
- Power: Official Raspberry Pi 27W USB-C Power Supply (Crucial for Pi 5 USB/IO stability)
Pin Mapping Table
The Raspberry Pi 5 routes its primary user-accessible I2C bus through the RP1 chip to BCM GPIO 2 and 3. Here is the exact physical wiring map:
| Physical Pin | BCM GPIO | Pi 5 Function | BME280 Breakout Pin |
|---|---|---|---|
| Pin 1 | N/A | 3.3V Power | VIN / VCC |
| Pin 3 | GPIO 2 | I2C SDA1 | SDI / SDA |
| Pin 5 | GPIO 3 | I2C SCL1 | SCK / SCL |
| Pin 6 | N/A | Ground | GND |
BME280 Hardware Specification Sheet
Understanding the datasheet limits prevents bus-lockups. The Bosch BME280 Datasheet dictates the following operational boundaries:
| Parameter | Value | Engineering Note |
|---|---|---|
| I2C Address | 0x76 or 0x77 | Adafruit uses 0x77; most generic Amazon/eBay clones default to 0x76. |
| VDD Range | 1.71V to 3.6V | Must connect to Pi 3.3V rail. 5V will cause catastrophic failure. |
| I2C Clock (SCL) | Max 400 kHz | Pi 5 default I2C baudrate is 100kHz; safe for standard wiring. |
| Standby Current | 0.1 µA | Draws ~3.6 µA at 1Hz sampling; negligible for Pi power budget. |
Pi 5 Environment Prep & I2C Enablement
The biggest hurdle when programming a Raspberry Pi today is the OS-level Python environment. Raspberry Pi OS Bookworm enforces PEP 668, meaning running sudo pip install will break your system dependencies. You must use a virtual environment.
- Enable I2C: Open terminal and run
sudo raspi-config. Navigate to Interface Options > I2C and enable it. Reboot. - Verify Hardware: Run
sudo i2cdetect -y 1. You should see76or77in the grid. If the grid is empty, check your physical wiring. - Create Virtual Environment: In your project folder, run:
python3 -m venv env
source env/bin/activate - Install Dependencies: With the venv active, install the I2C bus library and the sensor driver:
pip install smbus2 RPi.bme280
i2cdetect shows all addresses occupied (a grid full of UU or --), your SDA and SCL lines are swapped, or the sensor is holding the bus low because it's unpowered. Never hot-swap I2C sensors while the Pi is powered.
The Python Logging Script (Pi 5 Target)
Below is the complete, compilable Python script. It initializes the I2C bus, reads the BME280, and logs temperature, humidity, and pressure to a CSV file. Crucially, it includes explicit error handling for the most common I2C hardware faults.
#!/usr/bin/env python3
"""
BME280 I2C Environmental Logger
Target: Raspberry Pi 5 (8GB) / Raspberry Pi OS Bookworm 64-bit
Dependencies: pip install smbus2 RPi.bme280
"""
import time
import csv
import logging
import smbus2
import bme280
from datetime import datetime
# --- PIN & BUS DEFINITIONS ---
I2C_BUS_ID = 1 # /dev/i2c-1 is the primary user bus on Pi 5 RP1 southbridge
BME280_ADDR = 0x76 # Change to 0x77 if using official Adafruit breakout
LOG_FILE = "env_log.csv"
SAMPLE_INTERVAL = 10 # Seconds between reads
# --- LOGGING SETUP ---
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s | %(levelname)s | %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
def init_csv():
"""Create CSV header if file doesn't exist."""
try:
with open(LOG_FILE, mode='x', newline='') as f:
writer = csv.writer(f)
writer.writerow(['Timestamp', 'Temp_C', 'Pressure_hPa', 'Humidity_%'])
except FileExistsError:
pass
def main():
init_csv()
logging.info(f"Initializing I2C bus {I2C_BUS_ID} at address {hex(BME280_ADDR)}")
try:
bus = smbus2.SMBus(I2C_BUS_ID)
# Load calibration parameters from the sensor's internal ROM
calibration_params = bme280.load_calibration_params(bus, BME280_ADDR)
except FileNotFoundError:
logging.critical(f"I2C bus /dev/i2c-{I2C_BUS_ID} not found. Is I2C enabled in raspi-config?")
return
except Exception as e:
logging.critical(f"Failed to load calibration data: {e}")
return
logging.info("Sensor calibrated. Starting logging loop...")
try:
while True:
try:
# Read compensated data
data = bme280.sample(bus, BME280_ADDR, calibration_params)
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
# Log to console
logging.info(f"T: {data.temperature:.2f}C | P: {data.pressure:.1f}hPa | H: {data.humidity:.1f}%")
# Append to CSV
with open(LOG_FILE, mode='a', newline='') as f:
writer = csv.writer(f)
writer.writerow([
timestamp,
round(data.temperature, 2),
round(data.pressure, 1),
round(data.humidity, 1)
])
time.sleep(SAMPLE_INTERVAL)
except OSError as e:
# Catch specific I2C hardware faults
if e.errno == 121:
logging.error("OSError: [Errno 121] Remote I/O error. Check physical wiring and pull-ups.")
elif e.errno == 110:
logging.error("OSError: [Errno 110] Connection timed out. Sensor may be locked up; power cycle required.")
else:
logging.error(f"I2C Bus Error: {e}")
time.sleep(5) # Back off before retrying
except KeyboardInterrupt:
logging.info("Logging stopped by user.")
finally:
if 'bus' in locals():
bus.close()
if __name__ == '__main__':
main()
Debugging I2C Faults: Exact Errors & Fixes
When programming a Raspberry Pi for I2C, the physical layer is usually where builds fail. The Pi 5's RP1 chip handles I2C routing differently than the BCM2711 on the Pi 4, making bus capacitance and pull-up resistor values more critical.
The First Three Things to Check When It Fails
- Run the Bus Detective: Execute
sudo i2cdetect -y 1. If your sensor address doesn't appear, the Pi cannot physically see the chip. No amount of Python debugging will fix a hardware disconnect. - Measure VCC at the Breakout: Use a multimeter to probe the VIN and GND pins directly on the sensor breakout board. You must read between 3.2V and 3.4V. If you read 0V, your jumper wire is bad. If you read 5V, you are on the wrong Pi power rail and the sensor is likely dead.
- Verify the Python Environment: If your script fails to import
smbus2, ensure your terminal prompt shows(env). Bookworm's PEP 668 enforcement will silently block global package installs, leading to missing module errors.
Ranked Causes for Exact Error Strings
OSError: [Errno 121] Remote I/O errorThis is the most common I2C fault on Raspberry Pi. It means the Pi sent a clock signal, but the sensor did not acknowledge (NACK) or pulled the SDA line low.
| Rank | Root Cause | Fix / Action |
|---|---|---|
| 1 | Loose Dupont/Jumper connection on SDA or SCL. | Replace cheap Dupont wires with 28 AWG silicone crimped wires. Wiggle the breadboard to reproduce. |
| 2 | Incorrect I2C Address hardcoded in script. | Generic BME280s are usually 0x76. Adafruit is 0x77. Update the BME280_ADDR variable. |
| 3 | Missing Pull-up Resistors on I2C lines. | The Pi 5 RP1 has internal ~1.8kΩ pull-ups, but long wires add capacitance. Add external 4.7kΩ pull-ups to 3.3V if wires exceed 12 inches. |
error: externally-managed-environmentTriggered when running
pip install outside a virtual environment on Raspberry Pi OS Bookworm.
Fix: This is a feature, not a bug, designed to protect the OS Python packages. Create a virtual environment using python3 -m venv env, activate it with source env/bin/activate, and run your pip installs inside it.
Pi 4 vs Pi 5 I2C Architecture Comparison
Understanding the hardware shift helps explain why older tutorials fail on newer boards.
| Feature | Raspberry Pi 4 (BCM2711) | Raspberry Pi 5 (RP1 Southbridge) |
|---|---|---|
| I2C Controller | Integrated directly into main SoC | Handled by external RP1 I/O controller |
| Device Tree Path | /soc/i2c@7e804000 | /axi/pcie@120000/rp1/i2c@70000 |
| Default Pull-ups | 1.8kΩ to 3.3V | 1.8kΩ to 3.3V (via RP1) |
| Bus Speed Limit | 400 kHz (Fast Mode) | 400 kHz (Fast Mode) |
Scaling the Build: Simplify or Extend
Once your baseline logger is running reliably, you will likely want to adapt the project for a specific deployment. Here is how to pivot the architecture based on your end goal.
How to Simplify the Build
If loose jumper wires and breadboards are causing persistent Errno 121 faults, eliminate the physical layer variables entirely. Swap the BME280 breakout and wires for a pre-integrated I2C HAT like the Pimoroni Enviro+ for Raspberry Pi or the Adafruit BME280 STEMMA QT variant. The STEMMA QT connector uses a keyed JST-SH cable that physically prevents reversed polarity and guarantees solid connections, reducing I2C bus noise and capacitance issues to near zero.
How to Extend the Build
To turn this local CSV logger into a smart home node, integrate MQTT.
- Install the MQTT library in your venv:
pip install paho-mqtt. - Import
paho.mqtt.clientand initialize a client instance before yourwhileloop. - Inside the loop, replace or supplement the CSV write with:
client.publish("homeassistant/sensor/bme280/temperature", payload=data.temperature, qos=1) - Configure an MQTT Auto-Discovery payload so Home Assistant automatically recognizes the Pi 5 as a new environmental sensor entity without manual YAML configuration.






