The Raspberry Pi 5 brings significant PCIe and USB 3.0 bandwidth upgrades, but its Broadcom BCM2712 SoC handles low-level GPIO and I2C communication much like its predecessors. When using a Raspberry Pi for environmental monitoring, the I2C bus remains the most reliable protocol for digital sensors, provided you respect its physical layer limitations. This guide walks through building a robust temperature, humidity, and pressure logger using the Pi 5 and a BME280 sensor, complete with production-grade error handling and I2C debugging workflows.

Project Overview & Bill of Materials

Difficulty: Intermediate (Requires basic Linux CLI and Python knowledge)
Time to Complete: 45 minutes
Target Board: Raspberry Pi 5 (4GB or 8GB variant) running Raspberry Pi OS Bookworm (64-bit)

The Raspberry Pi 5 requires a strict 5V/5A USB-C PD power supply to maintain stable logic levels on the 3.3V rail, which directly feeds the I2C pull-up resistors. Using an underpowered supply will cause brownouts that manifest as random I2C bus lockups.

ComponentExact Variant / ModelEstimated Cost (2026)
Single Board ComputerRaspberry Pi 5 (4GB RAM)$60.00
Power SupplyOfficial Raspberry Pi 27W USB-C PD (5V/5A)$12.00
Environmental SensorAdafruit BME280 I2C/SPI Breakout (Product ID: 2652)$19.95
WiringPremium Female/Female Silicone Jumper Wires (20cm)$4.00
StorageSanDisk Extreme 32GB microSD (A1 rated)$9.00

Hardware Wiring & Pin Mapping

The Raspberry Pi 5's 40-pin header maintains backward compatibility with the Pi 4's I2C pinout. Hardware I2C bus 1 is exposed on pins 3 and 5. The BME280 operates strictly at 3.3V logic; never connect it to a 5V I2C bus without a level shifter, or you will destroy the sensor's internal ASIC.

Raspberry Pi 5 PinBCM GPIOFunctionBME280 PinWire Color
Pin 1N/A3.3V PowerVIN (or 3Vo)Red
Pin 6N/AGroundGNDBlack
Pin 3GPIO 2I2C SDASDABlue
Pin 5GPIO 3I2C SCLSCLYellow
Pro-Tip on I2C Capacitance: The Adafruit BME280 (PID 2652) includes onboard 10kΩ pull-up resistors. However, the I2C specification limits total bus capacitance to 400pF. Keep your silicone jumper wires under 18 inches (45cm). If you need longer runs, use twisted-pair CAT5 cable and add a dedicated I2C bus extender like the PCA9615.
  1. De-energize the Pi: Unplug the USB-C power supply before touching the GPIO header.
  2. Connect Power and Ground: Attach the red wire to Pin 1 (3.3V) and black to Pin 6 (GND).
  3. Connect Data Lines: Attach blue to Pin 3 (SDA) and yellow to Pin 5 (SCL).
  4. Verify Connections: Use a multimeter in continuity mode to verify GND-to-GND before applying power.
  5. Boot the Pi: Plug in the 27W power supply and SSH into the Pi or open a terminal.

Python Implementation with Error Handling

Before writing code, ensure the I2C interface is enabled via sudo raspi-config (Interface Options -> I2C -> Enable) and install the required Adafruit Blinka and BME280 libraries:

sudo apt update
sudo apt install python3-pip i2c-tools
pip3 install --break-system-packages adafruit-circuitpython-bme280

The following Python script targets the Raspberry Pi 5 running Bookworm. It includes explicit pin/bus definitions and robust try/except blocks to catch physical layer disconnects and addressing errors without crashing the daemon.

import time
import board
import adafruit_bme280

# Pin/Bus Definitions for Raspberry Pi 5
# Hardware I2C bus 1 is the default on the 40-pin header (Pins 3 & 5)
I2C_BUS = board.I2C()  # Uses board.SCL and board.SDA
# Adafruit BME280 default address is 0x77. Generic clones often use 0x76.
SENSOR_ADDRESS = 0x77

def main():
    try:
        # Initialize I2C bus and Sensor object
        i2c = I2C_BUS
        bme280 = adafruit_bme280.Adafruit_BME280_I2C(i2c, address=SENSOR_ADDRESS)
        
        # Set local sea level pressure for accurate altitude calculation (hPa)
        bme280.sea_level_pressure = 1013.25 

        print('BME280 Sensor initialized successfully on I2C Bus 1.')
        
        while True:
            temp_c = bme280.temperature
            humidity = bme280.relative_humidity
            pressure = bme280.pressure
            
            print(f'Temp: {temp_c:.2f} C | Humidity: {humidity:.2f} % | Pressure: {pressure:.2f} hPa')
            time.sleep(2.0)
            
    except ValueError as e:
        # Catches missing sensor, wrong address, or unpowered chip
        print(f'[FATAL] Sensor not found on I2C bus. Exact error: {e}')
        print('Action: Check wiring, run i2cdetect -y 1, and verify SENSOR_ADDRESS.')
    except OSError as e:
        # Catches bus lockups, NACKs, or physical disconnects during runtime
        print(f'[ERROR] I2C Bus communication failed. Exact error: {e}')
        print('Action: Check for loose Dupont wires, bus capacitance, or logic level mismatches.')
    except KeyboardInterrupt:
        print('\nMonitoring stopped by user.')

if __name__ == '__main__':
    main()

Debugging I2C Failures: Exact Errors and Fixes

When using a Raspberry Pi for I2C, the Linux kernel abstracts the hardware, meaning Python throws generic OS-level errors when the physical layer fails. Here are the exact error strings you will encounter and how to fix them.

Error 1: ValueError: No I2C device at address: 0x77

Ranked Causes:

  1. Wrong I2C Address: You are using a third-party BME280 clone that defaults to 0x76 instead of the Adafruit default of 0x77.
  2. Sensor is Unpowered: The 3.3V rail is not reaching the VIN pin due to a broken jumper wire.
  3. Counterfeit Chip: The sensor is actually a BMP280 (no humidity) masquerading as a BME280, failing the internal ID register check.

Error 2: OSError: [Errno 121] Remote I/O error

Ranked Causes:

  1. Bus Lockup / Capacitance: Wires are too long, causing the SDA/SCL rise times to exceed the I2C specification, resulting in a NACK (Not Acknowledged) from the sensor.
  2. SDA/SCL Swapped: The data and clock lines are reversed.
  3. Thermal Throttling: The Pi 5 has throttled its core clock, disrupting the I2C timing bit-bang (rare on hardware I2C, common on software I2C).
The First 3 Things to Check When I2C Fails:
  1. Verify the I2C Matrix: Run i2cdetect -y 1 in the terminal. You should see a 77 or 76 in the grid. If the grid is entirely empty, the bus is disabled or unpowered. If you see UU, the kernel driver has already claimed the device.
  2. Check config.txt: Open /boot/firmware/config.txt and ensure the line dtparam=i2c_arm=on is present and uncommented.
  3. Multimeter Continuity Test: With the Pi powered off, set your multimeter to continuity. Probe the sensor's GND pin and the Pi's metal USB shield (which is tied to system ground). A beep confirms your ground reference is solid.

Extending and Simplifying the Build

How to Extend: To turn this into a remote IoT node, integrate the paho-mqtt Python library to publish the sensor dictionary to a local Mosquitto broker. You can also add a 0.96-inch SSD1306 OLED display on the same I2C bus (address 0x3C) for local readouts without needing a network connection.

How to Simplify: The Raspberry Pi 5 is a powerhouse, but if your only goal is headless sensor logging, it is overkill. Downgrade the compute module to a Raspberry Pi Zero 2 W. It uses the exact same 40-pin I2C mapping, costs roughly $15, and draws less than 200mA, making it viable for 18650 lithium-ion battery packs with a simple TP4056 charging module.

Frequently Asked Questions

Is using a Raspberry Pi overkill for simple sensor reading?

Yes, if you are only reading one sensor and doing nothing else. A Raspberry Pi runs a full Linux kernel, requires a complex bootloader, and draws roughly 2.5W to 5W at idle. For simple, low-power sensor polling, an ESP32-C3 or Arduino Nano is vastly superior, drawing milliamps and booting in milliseconds. However, using a Raspberry Pi is justified if you need local database storage (SQLite), a web dashboard (Flask/Dash), or complex edge-computing tasks like FFT analysis on the sensor data.

What are the best practices when using a Raspberry Pi in headless mode?

When running without a monitor, always configure SSH with key-based authentication and disable password logins. More importantly, configure a static IP via dhcpcd.conf or your router's DHCP reservation so you don't lose track of the node. Finally, use systemd to wrap your Python script into a service with Restart=always and RestartSec=10 to ensure the script recovers automatically if the I2C bus throws a fatal OSError during a power flicker.

How do I prevent SD card corruption when using a Raspberry Pi for 24/7 logging?

MicroSD cards fail when subjected to continuous write cycles, such as logging sensor data every second to a local text file. To prevent this, mount the root filesystem as read-only using the overlayfs feature in raspi-config (Performance Options -> Overlay File System). Alternatively, write your sensor logs to a RAM disk (tmpfs) and use a cron job to batch-upload the data to an external server or USB thumb drive once an hour. For production deployments, bypass the SD card entirely and boot the Pi 5 from an NVMe SSD via the PCIe HAT.