Project Overview & Difficulty Rating
Getting reliable environmental data into a home automation stack or a custom datalogger starts with a solid sensor and a clean I2C bus. The Bosch BME280 is the benchmark for hobbyist and light-commercial temperature, humidity, and barometric pressure sensing. Unlike cheaper DHT11 or DHT22 modules that rely on fragile single-wire timing protocols, the BME280 uses the I2C bus, making it vastly more stable for continuous polling on a Linux-based system.
Parts List & Hardware Spec Sheet
Do not buy unbranded 'BME280' clones from random marketplaces if you can avoid it. Many cheap clones actually ship with BMP280 chips (which lack the humidity sensor) but are labeled as BME280. Stick to reputable breakout boards with proper voltage regulation and logic-level shifting.
| Component | Exact Model / Variant | Est. Price | Notes |
|---|---|---|---|
| Microcontroller | Raspberry Pi 5 (4GB or 8GB) | $60 - $80 | Requires active cooling (e.g., Active Cooler) for sustained loads. |
| Sensor Breakout | Adafruit BME280 (Product ID 2652) | $12.50 | Includes 3.3V LDO and I2C pull-ups. Default address: 0x77. |
| Alternative Sensor | SparkFun BME280 (SEN-13676) | $14.95 | Default address: 0x76. Requires jumper pad modification to change. |
| Wiring | Premium Female/Female Jumpers | $4.00 | Use 28 AWG silicone wires for flexible, reliable breadboard connections. |
Step-by-Step Wiring & Pin Mapping
The Raspberry Pi 5 exposes two hardware I2C buses on the primary 40-pin header. We will use I2C1 (the default bus mapped to /dev/i2c-1 in the OS). The Pi 5 GPIO operates strictly at 3.3V logic. The Adafruit and SparkFun breakouts handle this natively, but if you are using a raw BME280 chip on a bare PCB, ensure it has a 3.3V regulator.
Pin Mapping Table
| BME280 Breakout Pin | Raspberry Pi 5 Function | Pi 5 GPIO Number | Physical Pin # | Recommended Wire Color |
|---|---|---|---|---|
| VIN / VCC | 3.3V Power | N/A | Pin 1 | Red |
| GND | Ground | N/A | Pin 6 | Black |
| SCK / SCL | I2C1 Clock | GPIO 3 (SCL) | Pin 5 | Yellow |
| SDI / SDA | I2C1 Data | GPIO 2 (SDA) | Pin 3 | Blue |
- Power down the Raspberry Pi 5 completely and disconnect the USB-C power supply.
- Connect the Red jumper from the BME280 VIN pin to Physical Pin 1 (3.3V) on the Pi.
- Connect the Black jumper from the BME280 GND pin to Physical Pin 6 (Ground) on the Pi.
- Connect the Yellow jumper from the BME280 SCL pin to Physical Pin 5 (GPIO 3) on the Pi.
- Connect the Blue jumper from the BME280 SDA pin to Physical Pin 3 (GPIO 2) on the Pi.
- Double-check that SDA and SCL are not swapped. Swapping them won't fry the board, but it will cause immediate I/O errors in software.
How to Install the BME280 Software Stack on Raspberry Pi
With the hardware seated, we need to enable the I2C bus in the OS and install the Python libraries. Raspberry Pi OS 'Bookworm' enforces PEP 668, meaning you should no longer use sudo pip install globally. We will use a virtual environment.
- Boot the Pi and open a terminal.
- Enable the I2C interface by running:
sudo raspi-config - Navigate to Interface Options -> I2C -> Select Yes to enable it.
- Install the system-level I2C tools for debugging:
sudo apt update && sudo apt install -y i2c-tools python3-venv - Create and activate a Python virtual environment:
mkdir ~/bme280_project && cd ~/bme280_project
python3 -m venv venv
source venv/bin/activate - Install the required Python packages inside the virtual environment:
pip install smbus2 RPi.bme280
Before writing code, verify the hardware is visible on the bus. Run i2cdetect -y 1. You should see a 76 or 77 in the grid output. If the grid is empty, your wiring is incorrect or the sensor is dead.
Complete Python Code with Error Handling
Below is the production-ready polling script. It includes explicit pin/port definitions, calibration loading, and robust exception handling for the most common I2C failure modes.
import smbus2
import bme280
import time
import sys
# --- Pin and Port Definitions ---
I2C_PORT = 1
# Adafruit boards typically use 0x77, SparkFun/generic often use 0x76.
# Change this if i2cdetect shows a different address.
BME280_I2C_ADDRESS = 0x77
def initialize_sensor():
try:
bus = smbus2.SMBus(I2C_PORT)
# Load factory calibration data from the sensor's NVM
calibration_params = bme280.load_calibration_params(bus, BME280_I2C_ADDRESS)
print(f'Successfully initialized BME280 at I2C address 0x{BME280_I2C_ADDRESS:02x}')
return bus, calibration_params
except FileNotFoundError:
print('FATAL: I2C interface /dev/i2c-1 not found.')
print('Fix: Run sudo raspi-config and enable the I2C interface.')
sys.exit(1)
except OSError as e:
if e.errno == 121:
print(f'FATAL: Remote I/O error (Errno 121) at address 0x{BME280_I2C_ADDRESS:02x}.')
print('Fix: Check SDA/SCL wiring, ensure 3.3V power is stable, and verify address.')
else:
print(f'FATAL: Unexpected OS Error: {e}')
sys.exit(1)
def main():
bus, params = initialize_sensor()
try:
print('Starting environmental polling (Ctrl+C to stop)...')
while True:
# Read and compensate sensor data
data = bme280.sample(bus, BME280_I2C_ADDRESS, params)
# Format output
temp_c = data.temperature
temp_f = (temp_c * 9/5) + 32
pressure_hpa = data.pressure
humidity_pct = data.humidity
print(f'Temp: {temp_c:.2f}C ({temp_f:.2f}F) | '
f'Pressure: {pressure_hpa:.2f} hPa | '
f'Humidity: {humidity_pct:.2f}%')
time.sleep(5) # Poll every 5 seconds
except KeyboardInterrupt:
print('\nPolling stopped by user. Exiting cleanly.')
except Exception as e:
print(f'\nUnexpected runtime error: {e}')
sys.exit(1)
if __name__ == '__main__':
main()
Debugging: 'OSError: [Errno 121] Remote I/O error'
If you run the script and immediately hit a crash, you are likely staring at this exact terminal output:
OSError: [Errno 121] Remote I/O error
This is the Linux kernel's generic way of saying 'I tried to talk to an I2C device at the requested address, and nothing acknowledged the clock signal.' It is the most common hurdle when you install hardware on a Raspberry Pi.
The First Three Things to Check When It Fails
- Run the Bus Sweep: Execute
i2cdetect -y 1in the terminal. If the output grid is entirely dashes (--), the Pi cannot see the sensor at all. If you see a number (like76), your code is querying the wrong address. - Verify SDA and SCL Orientation: Use a multimeter in continuity mode (with power disconnected) to verify that the SDA pin on the breakout physically traces to Physical Pin 3 on the Pi, and SCL traces to Physical Pin 5. Silkscreen labels on cheap clone boards are frequently printed backward.
- Check Power Delivery: Measure the voltage between the breakout's VCC and GND pins with a multimeter. It must read between 3.2V and 3.4V. If it reads 0V, your jumper wire is broken or the Pi's 3.3V rail is damaged.
Ranked Causes for Errno 121
- Cause 1 (60%): Incorrect I2C Address in Code. The code requests 0x77, but the physical board has the address pad bridged to 0x76. Update the
BME280_I2C_ADDRESSvariable to matchi2cdetect. - Cause 2 (25%): Swapped SDA/SCL Lines. I2C is not bidirectional on a single wire. Clock must go to Clock, Data to Data.
- Cause 3 (10%): Missing Pull-up Resistors. While the Pi 5 has onboard pull-ups for I2C1, long wire runs (>30cm) or adding multiple devices can degrade the signal edge. Add a 4.7kΩ pull-up resistor from SDA to 3.3V, and another from SCL to 3.3V.
- Cause 4 (5%): Dead Sensor or Bad Solder Joints. Reflow the header pins on the BME280 breakout board. Factory header solder joints on budget boards are notoriously cold and crack under wire tension.
Extending and Simplifying the Build
How to Simplify: If wiring individual jumpers feels tedious or fragile for a permanent installation, ditch the breakout board and buy an Enviro+ Environmental HAT (Pimoroni). It plugs directly over the 40-pin header, requires zero wiring, and includes a BME280 alongside air quality sensors. You will need to swap the I2C address in the code, but the physical setup time drops to 10 seconds.
How to Extend: To turn this from a terminal script into a smart home node, integrate the paho-mqtt library. Wrap the bme280.sample() output into a JSON payload and publish it to an MQTT broker (like Mosquitto or Home Assistant). From there, you can trigger automations—such as turning on a dehumidifier relay when humidity_pct exceeds 65%.
Frequently Asked Questions
Can I install a BME280 on Raspberry Pi using SPI instead of I2C?
Yes. The BME280 chip natively supports SPI, I2C, and I2S. However, using SPI requires wiring four data lines (MOSI, MISO, SCLK, CS) plus power and ground, totaling six wires. Unless you are running the sensor over a meter of cable (where I2C capacitance limits kick in) or need extreme sampling rates that I2C cannot handle, I2C is heavily preferred for environmental sensors because it only requires two shared data lines and allows you to daisy-chain other sensors on the same bus.
Why does my sensor show 0x76 when I install on Raspberry Pi, but tutorials say 0x77?
The Bosch BME280 datasheet specifies that the base I2C address is determined by the state of the SDO (Serial Data Out) pin on the chip. If SDO is tied to GND, the address is 0x76. If SDO is tied to VCC, the address is 0x77. Adafruit designs their boards to default to 0x77, while SparkFun and most generic AliExpress clones default to 0x76. Always trust the output of i2cdetect -y 1 over the tutorial you are reading.
How do I auto-start the script on boot after I install on Raspberry Pi?
The most robust method in modern Raspberry Pi OS is to create a systemd service. Create a file at /etc/systemd/system/bme280.service. Define the ExecStart path pointing to your virtual environment's Python executable (e.g., /home/pi/bme280_project/venv/bin/python /home/pi/bme280_project/main.py). Then run sudo systemctl enable bme280.service and sudo systemctl start bme280.service. This ensures the script restarts automatically if it crashes or if the Pi loses power.
Do I need external pull-up resistors when I install on Raspberry Pi 5?
For a single BME280 breakout board with wires under 30cm, no. The Raspberry Pi 5 has internal pull-up resistors enabled on GPIO 2 and GPIO 3 when the I2C1 bus is active via the device tree. Furthermore, quality breakout boards (like the Adafruit 2652) include 4.7kΩ or 10kΩ onboard pull-ups. You only need to add external pull-ups to the 3.3V rail if you are chaining more than three I2C devices on the same bus, which increases the total bus capacitance and degrades the square-wave signal edges.
For more details on I2C bus capacitance limits and Pi 5 GPIO specifications, refer to the official Raspberry Pi hardware documentation and the Bosch Sensortec BME280 datasheet.






