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.
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 |
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
- Run
i2cdetect -y 1: This is your ground truth. If the sensor shows up as77in 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. - Verify I2C is Enabled in
/boot/firmware/config.txt: On Raspberry Pi OS Bookworm, ensure the linedtparam=i2c_arm=onis present and not commented out. A recent OS update occasionally resets this during major kernel upgrades. - 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
0x77to0x76. 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:
- 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.
- 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.
- 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_throttledto 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.






