Getting started with coding on a Raspberry Pi often means moving beyond simple terminal scripts and interacting with the physical world via GPIO. The I2C (Inter-Integrated Circuit) bus is the most common gateway for this, but it is also the most frequent source of hardware-level frustration. Unlike Arduino, where I2C is handled by a deterministic microcontroller, the Raspberry Pi runs a full Linux OS. This means your Python code must navigate kernel drivers, bus multiplexing, and hardware quirks specific to the Broadcom SoC.
This guide targets the Raspberry Pi 5 (4GB or 8GB variant) running Raspberry Pi OS (64-bit, Bookworm). We will wire a BME280 environmental sensor, write robust Python code using the smbus2 library, and systematically debug the infamous I2C errors that plague embedded Linux projects.
Project Spec Sheet and Hardware BOM
Before writing a single line of code, you need the right hardware. The Pi 5 operates its GPIO logic at 3.3V. Feeding 5V into the I2C pins will permanently damage the BCM2712 SoC. Always use 3.3V breakouts or a logic level converter.
| Component | Exact Variant / Model | Specs & Notes | Est. Price (2026) |
|---|---|---|---|
| Microcontroller | Raspberry Pi 5 (4GB) | BCM2712 SoC, 3.3V logic, I2C Bus 1 default | $60.00 |
| Sensor | Adafruit BME280 (PID 2652) | I2C/SPI, 3.3V-5V tolerant, onboard pull-ups | $14.95 |
| Wiring | 28 AWG Silicone Jumper Wires | Female-to-Female, 20cm length | $6.00 |
| OS / Storage | SanDisk Extreme 64GB microSD | A2 rating required for Pi 5 boot stability | $12.00 |
Pin Mapping Table
The Raspberry Pi 5 maintains the standard 40-pin header layout. We are using the primary hardware I2C bus (Bus 1). Use the following physical pin numbers (not BCM GPIO numbers) for wiring:
| Pi 5 Physical Pin | BCM GPIO | Function | BME280 Breakout Pin | Standard Wire Color |
|---|---|---|---|---|
| Pin 1 | N/A (Power) | 3.3V DC Power | VIN / VCC | Red |
| Pin 6 | N/A (Ground) | Ground | GND | Black |
| Pin 3 | GPIO 2 | I2C1 SDA (Data) | SDA | Blue |
| Pin 5 | GPIO 3 | I2C1 SCL (Clock) | SCL | Yellow |
Coding on a Raspberry Pi: Python I2C Implementation
When coding on a Raspberry Pi for hardware interaction, avoid heavy abstraction layers if you want to understand the underlying bus behavior. We will use smbus2, a pure-Python wrapper around the Linux i2c-dev kernel interface. It is lightweight, fast, and exposes the exact OS-level errors you need for debugging.
First, ensure your environment is prepped. Enable I2C via sudo raspi-config (Interface Options > I2C > Enable), reboot, and install the library:
sudo apt update
sudo apt install python3-smbus2 i2c-tools
Below is the complete, compilable Python script. It initializes the bus, verifies the sensor's Chip ID to confirm physical communication, and reads the temperature register with robust error handling.
#!/usr/bin/env python3
"""
Raspberry Pi 5 I2C BME280 Temperature Reader
Target Board: Raspberry Pi 5 (4GB/8GB)
OS: Raspberry Pi OS (64-bit, Bookworm)
Dependencies: smbus2 (pip install smbus2)
"""
import smbus2
import time
import sys
# --- PIN & BUS DEFINITIONS ---
# Raspberry Pi 5 primary hardware I2C bus
I2C_BUS_ID = 1
# Default I2C address for Adafruit BME280 (0x77 for some generic clones)
BME280_I2C_ADDR = 0x76
# BME280 Register Map (from Bosch Datasheet)
REG_CHIP_ID = 0xD0
REG_TEMP_DATA = 0xFA
REG_CTRL_MEAS = 0xF4
EXPECTED_CHIP_ID = 0x60
def initialize_sensor(bus):
"""Verifies connection and sets oversampling for temperature."""
# Read Chip ID to verify I2C communication
chip_id = bus.read_byte_data(BME280_I2C_ADDR, REG_CHIP_ID)
if chip_id != EXPECTED_CHIP_ID:
raise ValueError(f"Unexpected Chip ID: 0x{chip_id:02X}. Check wiring.")
# Configure: Temp oversampling x2 (010), Pressure x1 (001), Mode Normal (11)
# Binary: 01000111 -> Hex: 0x47
bus.write_byte_data(BME280_I2C_ADDR, REG_CTRL_MEAS, 0x47)
time.sleep(0.1) # Allow sensor to settle
def read_temperature_raw(bus):
"""Reads the 3-byte temperature data register."""
# Read 3 bytes starting from MSB register
data = bus.read_i2c_block_data(BME280_I2C_ADDR, REG_TEMP_DATA, 3)
# Combine bytes into a 20-bit integer (shift and mask)
raw_temp = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4)
return raw_temp
def main():
print(f"Initializing I2C Bus {I2C_BUS_ID} at address 0x{BME280_I2C_ADDR:02X}...")
try:
# Open the I2C bus
bus = smbus2.SMBus(I2C_BUS_ID)
initialize_sensor(bus)
print("Sensor initialized successfully. Reading data...")
while True:
try:
raw_temp = read_temperature_raw(bus)
# Simplified conversion for demonstration (real calc requires calibration registers)
# Approximation: raw_temp / 5120.0 * 100
temp_c = raw_temp / 5120.0
print(f"Raw ADC: {raw_temp} | Approx Temp: {temp_c:.2f} °C")
time.sleep(2.0)
except OSError as e:
# Catch transient I2C bus glitches without crashing the main loop
print(f"Transient Read Error: {e}. Retrying in 5s...")
time.sleep(5.0)
except OSError as e:
# Fatal setup errors (e.g., device not found, permission denied)
print(f"\n--- FATAL I2C SETUP ERROR ---")
print(f"Exception: {e}")
print("Check 'Debugging the Dreaded Remote I/O Error' section below.")
sys.exit(1)
except KeyboardInterrupt:
print("\nScript terminated by user.")
sys.exit(0)
if __name__ == "__main__":
main()
Debugging the Dreaded Remote I/O Error
If you run the script above and immediately hit a wall, you will likely see this exact error string in your terminal:
OSError: [Errno 121] Remote I/O error
In embedded Linux, Errno 121 (EREMOTEIO) means the kernel's I2C driver attempted to clock data out on the SDA/SCL lines, but the slave device did not acknowledge (ACK) the transaction. The bus timed out. Here are the first three things to check when this failure occurs, ranked from most to least likely:
1. The First Three Checks
- Verify Bus Visibility with
i2cdetect: Runi2cdetect -y 1in the terminal. If you see a matrix of dashes (--) instead of76or77, the Pi physically cannot see the sensor. This points to a wiring fault, swapped SDA/SCL lines, or a dead 3.3V rail. - Measure VCC at the Breakout: Do not trust the power supply label. Use your multimeter to measure DC voltage between the GND and VIN pins directly on the sensor breakout. You should read between 3.25V and 3.35V. If it reads 0V, you have a broken jumper wire or a blown Pi polyfuse.
- Check for I2C Address Conflicts: Some generic BME280 clones default to
0x77instead of0x76. Ifi2cdetectshows77, update theBME280_I2C_ADDRvariable in the Python script to0x77.
Pi 5 Specific Edge Case: Clock Stretching
The BCM2712 chip in the Raspberry Pi 5 has a known hardware quirk regarding I2C clock stretching. If your sensor holds the SCL line low to delay a response (common in older ADCs or certain multiplexers), the Pi 5's hardware I2C controller may prematurely time out, throwing Errno 121 even if the wiring is perfect. While the BME280 rarely stretches the clock aggressively, if you add devices like the CCS811 air quality sensor to the same bus, you will hit this bug.
dtparam=i2c_baudrate=30000 to your /boot/firmware/config.txt file and reboot. This gives the slave device more time to respond before the Pi's kernel driver abandons the transaction.
Debugging Decision Matrix
| Exact Error String | Root Cause | Resolution |
|---|---|---|
OSError: [Errno 121] Remote I/O error |
Slave NACK (No device at address, or clock stretch timeout) | Run i2cdetect, verify address, lower baud rate in config.txt |
OSError: [Errno 2] No such file or directory |
I2C kernel module not loaded or disabled in raspi-config | Run sudo raspi-config, enable I2C, reboot |
PermissionError: [Errno 13] Permission denied |
Running script without i2c group privileges |
Add user to group: sudo usermod -aG i2c $USER, log out/in |
ValueError: Unexpected Chip ID: 0xFF |
SDA/SCL swapped, or reading from wrong I2C bus ID | Swap Blue/Yellow wires, verify I2C_BUS_ID = 1 |
Extending and Simplifying the Build
Once you have stable I2C communication and clean temperature logs, you can adapt this project to fit your specific deployment needs.
How to Simplify (Headless Data Logging)
If you are deploying this in an enclosure and don't need real-time terminal output, strip out the print() statements and the time.sleep() loop. Instead, configure a systemd service to run the script once on boot, appending the raw hex data and a Unix timestamp to a local .csv file. This reduces CPU wake-states and is ideal for battery-backed Pi setups using a PiJuice HAT.
How to Extend (MQTT and Home Assistant)
To integrate this sensor into a smart home dashboard, extend the Python script using the paho-mqtt library.
- Install the broker client:
pip install paho-mqtt. - After calculating
temp_c, publish the payload to an MQTT topic:client.publish("homeassistant/sensor/office/temp", temp_c). - In Home Assistant, configure an MQTT sensor entity in your
configuration.yamlpointing to that exact topic. This bridges your raw embedded Linux code into a polished UI without needing heavy local frameworks like Node-RED.
Mastering I2C on the Pi requires respecting the boundary between software and silicon. By verifying your physical layer with a multimeter, understanding the kernel's error codes, and writing defensive Python, you transform Errno 121 from a project-killer into a routine debugging step.
References: For deeper register-level details, consult the Adafruit BME280 Learning System and the official Raspberry Pi Hardware Configuration Documentation.






