Interfacing environmental sensors with a single-board computer is a foundational skill for home automation and edge computing. In this raspberry pi tutorial, we will wire a Bosch BME280 sensor to a Raspberry Pi 5 via the I2C bus, write production-grade Python code with robust error handling, and troubleshoot the most common I2C bus failures. The direct answer for the impatient: the Pi 5 communicates with the BME280 via the primary I2C1 bus (BCM GPIO 2/SDA and GPIO 3/SCL) using the adafruit-circuitpython-bme280 library, defaulting to I2C address 0x77.

Project Spec Sheet
Difficulty: Beginner-Intermediate
Time to Complete: 45 minutes
Estimated Cost: $85 - $95 USD
Target Board Variant: Raspberry Pi 5 (4GB or 8GB RAM) running Raspberry Pi OS (64-bit, Bookworm)

Hardware Spec Sheet & Parts List

Do not buy generic, unbranded sensor clones for mission-critical logging. Cheap clones often lack the required 0.1µF decoupling capacitors on the VCC line, leading to brownouts during Wi-Fi transmission spikes on the Pi. Here is the exact bill of materials for a reliable build.

Component Exact Variant / Model Number Approx. Price (2026) Why This Variant?
Microcontroller Raspberry Pi 5 (4GB RAM) $60.00 Dedicated I2C hardware controllers; 3.3V logic tolerant.
Sensor Breakout Adafruit BME280 (Product ID: 2652) $14.95 Includes 10kΩ I2C pull-ups and level shifting for 3V/5V safety.
Wiring 28 AWG Silicone Jumper Wires (F-F) $6.00 28 AWG prevents bending/breaking the Pi 5 header pins; silicone withstands heat.
Prototyping Standard 400-point Solderless Breadboard $5.00 Standard pitch for 0.1" breakout headers.

Pin Mapping & Wiring Steps

The Raspberry Pi 5 features multiple I2C buses, but the primary bus exposed on the main 40-pin header is I2C1. We will use this bus. Ensure your Pi is completely powered down and disconnected from the USB-C power supply before making physical connections.

Pi 5 Physical Pin BCM GPIO Name BME280 Breakout Pin Function
Pin 1 3V3 Power VIN (or 3Vo) 3.3V Power Supply
Pin 6 GND GND Common Ground
Pin 3 GPIO 2 (SDA1) SDA I2C Data Line
Pin 5 GPIO 3 (SCL1) SCL I2C Clock Line
⚠️ Callout Tip: Wire Length and Bus Capacitance
I2C is not designed for long cable runs. Keep your Dupont wires under 30cm (12 inches). If you exceed this, the parasitic capacitance of the wires will exceed the I2C standard 400pF limit, resulting in rounded clock edges and Remote I/O errors. If you must run wires further, use an I2C bus extender like the PCA9615.

Software Setup & Production Python Code

First, enable the I2C interface on your Pi 5. Open a terminal and run sudo raspi-config, navigate to Interface Options > I2C, and enable it. Reboot the Pi.

Next, install the Adafruit Blinka compatibility layer and the BME280 library. Blinka translates CircuitPython hardware APIs to Linux-based Python, making sensor code portable across microcontrollers and single-board computers.

sudo apt update
sudo apt install python3-pip python3-venv
mkdir ~/weather_station && cd ~/weather_station
python3 -m venv venv
source venv/bin/activate
pip3 install adafruit-blinka adafruit-circuitpython-bme280

Below is the complete, compilable Python script. It includes explicit pin definitions, continuous polling, and the necessary try/except blocks to handle I2C bus dropouts without crashing your logging daemon.

import time
import board
import busio
import adafruit_bme280

# Explicitly define I2C pins for Raspberry Pi 5 (BCM 2/SDA, BCM 3/SCL)
# Using busio allows us to explicitly target the hardware pins rather than relying on board defaults
i2c = busio.I2C(board.SCL, board.SDA)

# The default I2C address for the Adafruit BME280 is 0x77.
# If your board has the address jumper bridged, change this to 0x76.
SENSOR_ADDRESS = 0x77

def initialize_sensor():
    """Attempts to connect to the BME280 sensor with error handling."""
    try:
        sensor = adafruit_bme280.Adafruit_BME280_I2C(i2c, address=SENSOR_ADDRESS)
        sensor.sea_level_pressure = 1013.25  # Standard sea level pressure in hPa
        print('Successfully connected to BME280 sensor.')
        return sensor
    except ValueError as e:
        # This triggers if the sensor is not found at the specified I2C address
        print(f'FATAL: Sensor not found at address {hex(SENSOR_ADDRESS)}. Check wiring and i2cdetect. Error: {e}')
        return None

def main():
    sensor = initialize_sensor()
    if not sensor:
        return  # Exit if hardware is missing

    print('Starting environmental logging loop. Press Ctrl+C to stop.')
    
    while True:
        try:
            temp_c = sensor.temperature
            humidity = sensor.relative_humidity
            pressure_hpa = sensor.pressure
            
            # Calculate altitude based on pressure differential
            altitude_m = sensor.altitude
            
            print(f'[{time.strftime("%Y-%m-%d %H:%M:%S")}] Temp: {temp_c:.2f}C | '
                  f'Humidity: {humidity:.1f}% | Pressure: {pressure_hpa:.2f} hPa | '
                  f'Alt: {altitude_m:.1f}m')
            
            # The BME280 datasheet recommends a minimum 1-second delay between 
            # reads to prevent self-heating from skewing temperature data.
            time.sleep(2.0)
            
        except OSError as e:
            # Catches I2C bus dropouts (e.g., 'Remote I/O error')
            print(f'WARNING: I2C Bus Error ({e}). Attempting to reinitialize sensor in 5 seconds...')
            time.sleep(5)
            sensor = initialize_sensor()
            if not sensor:
                print('CRITICAL: Failed to recover sensor. Exiting.')
                break
        except KeyboardInterrupt:
            print('\nLogging stopped by user.')
            break

if __name__ == '__main__':
    main()

Debugging: The "Remote I/O Error" and I2C Failures

When working with I2C on Linux, you will inevitably encounter the dreaded OSError: [Errno 121] Remote I/O error or the initialization failure ValueError: No I2C device at address: 0x77. These errors mean the Linux kernel sent a clock pulse and data bit, but the sensor did not acknowledge (ACK) the transaction.

First Three Things to Check When It Fails

  1. Run i2cdetect -y 1: This is your ground truth. If the sensor shows up as 77 in the grid, your wiring and kernel drivers are fine, and the issue is in your Python library version. If the grid is empty, you have a physical layer or configuration problem.
  2. Verify I2C is Enabled in /boot/firmware/config.txt: On Raspberry Pi OS Bookworm, ensure the line dtparam=i2c_arm=on is present and not commented out. A recent OS update occasionally resets this during major kernel upgrades.
  3. Check the Address Jumper: Look closely at the BME280 breakout board. If a small blob of solder bridges the two pads labeled "I2C ADDR", the address shifts from 0x77 to 0x76. Update your Python code accordingly.

Ranked Causes for Intermittent "Remote I/O" Errors

If your script runs for an hour and then randomly throws the [Errno 121] error, the cause is rarely software. Rank your troubleshooting by these physical realities:

  1. Loose Dupont Wires (80% of cases): Solderless breadboards lose tension over time. A slight bump to the desk breaks the SDA line for a millisecond, causing the kernel I2C driver to timeout. Crimp proper JST connectors or solder the headers for permanent deployments.
  2. Missing Pull-Up Resistors (15% of cases): I2C is an open-drain protocol. It requires pull-up resistors to return the line to 3.3V. The Adafruit breakout includes these. If you are using a raw BME280 chip or a cheap clone without pull-ups, the signal will float, causing random bit-flips. Add 4.7kΩ resistors between SDA/SCL and 3.3V.
  3. Thermal Throttling / CPU Spikes (5% of cases): If your Pi 5 is under heavy load, the Linux kernel might delay the I2C clock stretching response beyond the BME280's timeout threshold. Use vcgencmd get_throttled to check for power or thermal throttling.

Extending and Simplifying the Build

Once you have stable I2C reads, you need to decide how this data fits into your broader ecosystem.

How to Extend the Build

To push this data to a Home Assistant dashboard, integrate the paho-mqtt library. Add an MQTT publish step inside the try block of the main loop. Use standardized discovery topics so Home Assistant auto-detects the sensor:

client.publish('homeassistant/sensor/weather_station/temp/state', payload=f'{temp_c:.2f}')

For local, offline viewing, wire a 128x64 SSD1306 OLED display to the same I2C bus. Because I2C supports multiple devices, you simply instantiate the display on address 0x3C while the BME280 remains on 0x77.

How to Simplify the Build

If you do not need a full Linux environment, a web server, or local database logging, a Raspberry Pi 5 is overkill. Simplify the build by swapping the Pi 5 for a Raspberry Pi Pico W ($6 USD). You can port the exact same Adafruit CircuitPython code over with zero changes to the sensor logic, dropping your hardware cost by $50 and your idle power consumption from 2.5W down to 0.1W.

Raspberry Pi Tutorial FAQ

Can I use this raspberry pi tutorial for a Raspberry Pi 4 or Pi Zero 2 W?

Yes. The physical pinout for the primary I2C1 bus (Pins 3 and 5) is identical across the Pi 3, Pi 4, Pi Zero 2 W, and Pi 5. The Python code and Blinka library will work without modification. The only difference is that the Pi 5 has a dedicated RP1 southbridge chip handling the I/O, which slightly improves I2C timing precision under heavy CPU loads compared to the Pi 4's BCM2711 SoC.

Why does my BME280 show up at 0x76 instead of 0x77 in this raspberry pi tutorial?

The Bosch BME280 silicon defaults to 0x76. However, most premium breakout boards (like Adafruit's) pull the SDO pin high via an onboard resistor to set it to 0x77, avoiding address collisions with the MPU6050 accelerometer (which is hard-coded to 0x76). If your board shows 0x76, simply change the SENSOR_ADDRESS variable in the Python script to 0x76.

How do I run this raspberry pi tutorial code automatically on boot?

Do not use the outdated rc.local method. The modern, robust approach on Raspberry Pi OS is to create a systemd service. Create a file at /etc/systemd/system/weather.service, point the ExecStart directive to your Python virtual environment executable (/home/pi/weather_station/venv/bin/python /home/pi/weather_station/main.py), and enable it with sudo systemctl enable --now weather.service. This ensures the script restarts automatically if the Pi reboots or the script crashes.

Is I2C better than SPI for this raspberry pi tutorial weather station?

For a single BME280 sensor, I2C is better because it only requires 2 data wires (plus power/ground), whereas SPI requires 4 data wires (MOSI, MISO, SCK, CS). However, if you plan to daisy-chain five environmental sensors across a long distance, SPI is superior. I2C degrades rapidly past 1 meter due to bus capacitance, while SPI can reliably push data further at the cost of needing a dedicated Chip Select (CS) wire for every single sensor.