Project Overview & Difficulty Rating
Interfacing environmental sensors via I2C is a rite of passage for embedded Linux builders. While the Raspberry Pi makes hardware access accessible via Python, the underlying I2C bus is notoriously unforgiving of physical layer mistakes. This guide walks through wiring, reading, and—most importantly—debugging a BME280 temperature, humidity, and pressure sensor using Python on a Raspberry Pi.
Difficulty: 2/5 (Solderless, but requires I2C bus debugging knowledge)
Time to Complete: 45 minutes
Hardware BOM & Pin Mapping
Do not buy generic, unbranded BME280 breakouts from bulk marketplaces if you are a beginner. Many cheap clones omit the necessary I2C pull-up resistors, leading to immediate bus failures. We are using the Adafruit variant for guaranteed hardware compliance.
| Component | Exact Variant / Part Number | Est. Cost |
|---|---|---|
| Microcontroller | Raspberry Pi 5 (8GB) | $80.00 |
| Sensor | Adafruit BME280 I2C/SPI Breakout (Product ID: 2652) | $14.95 |
| Wiring | Silicone Female-to-Female Jumper Wires (200mm) | $3.95 |
Pin Mapping Table
The BME280 operates strictly on 3.3V logic. Connecting the VIN pin to the Pi's 5V rail will permanently destroy the sensor's internal ASIC.
| BME280 Pin | Raspberry Pi 5 Pin | GPIO / Function |
|---|---|---|
| VIN | Pin 1 | 3.3V Power |
| GND | Pin 6 | Ground |
| SCK / SCL | Pin 5 | GPIO 3 (I2C1 SCL) |
| SDI / SDA | Pin 3 | GPIO 2 (I2C1 SDA) |
Step-by-Step Wiring & I2C Setup
Before writing any Python, you must enable the I2C interface at the OS level and verify the physical connection.
- Wire the hardware: Connect the four pins exactly as mapped in the table above. Keep the jumper wires under 30cm (12 inches) to avoid excessive bus capacitance.
- Enable I2C: Open a terminal and run
sudo raspi-config. Navigate to Interface Options > I2C and select Yes to enable the ARM I2C interface. Reboot the Pi. - Install OS-level tools: Run
sudo apt update && sudo apt install i2c-tools python3-pip. - Verify hardware address: Run
i2cdetect -y 1. You should see77in the grid output. If the grid is empty, stop here and check your physical wiring. - Install Python libraries: We use Adafruit's Blinka layer and the specific BME280 CircuitPython library. Run:
pip3 install adafruit-blinka adafruit-circuitpython-bme280
Complete Python Code with Error Handling
Hardware I2C on Linux is not deterministic. Buses lock up, wires vibrate loose, and sensors brown out. A production-ready script must catch OSError exceptions rather than crashing the entire application.
import time
import board
import busio
import adafruit_bme280
# Explicitly define I2C pins using the Blinka board module
# This maps to Pi 5's RP1 I2C1 (GPIO 2 / GPIO 3)
i2c = busio.I2C(board.SCL, board.SDA)
# Initialize sensor with explicit address (Adafruit breakout defaults to 0x77)
try:
sensor = adafruit_bme280.Adafruit_BME280_I2C(i2c, address=0x77)
# Optional: Configure IIR filter and oversampling for stable readings
sensor.iir_filter = adafruit_bme280.IIR_FILTER_X16
sensor.oversampling_temperature = adafruit_bme280.OVERSAMPLING_X16
except ValueError as e:
print(f"[FATAL] Sensor not found at 0x77. Check wiring and i2cdetect. Error: {e}")
exit(1)
print("Sensor initialized. Logging data...")
while True:
try:
temp_c = sensor.temperature
humidity = sensor.humidity
pressure = sensor.pressure
print(f"Temp: {temp_c:.2f} C | Hum: {humidity:.1f} % | Pres: {pressure:.1f} hPa")
except OSError as e:
# Catch I2C bus lockups or transient communication failures
print(f"[WARN] I2C Bus Error: {e}. Retrying in 5 seconds...")
time.sleep(5)
continue
except Exception as e:
print(f"[ERROR] Unexpected failure: {e}")
time.sleep(2)
continue
time.sleep(2)
Debugging: Fixing "OSError: [Errno 121] Remote I/O error"
If you are building I2C projects on a Raspberry Pi, you will eventually encounter this exact error string:
OSError: [Errno 121] Remote I/O error
This error means the Linux kernel's I2C driver sent a clock pulse, but the sensor did not acknowledge (ACK) the transaction. The bus timed out.
The First Three Things to Check
- Run
i2cdetect -y 1again: If the sensor vanished from the matrix, the sensor has crashed or lost power. Power cycle the Pi. - Measure the 3.3V rail under load: Use a multimeter on the sensor's VIN and GND pins. If it reads below 3.1V, the Pi's 3.3V regulator is sagging, causing the sensor's internal logic to brown out.
- Check for clock stretching bugs: The Pi's hardware I2C controller has a known silicon bug where it fails to handle 'clock stretching' (when a sensor holds the SCL line low to buy processing time). If your sensor uses clock stretching, you must use software I2C (bitbanging) instead of hardware I2C.
Ranked Causes for Errno 121
- Cause 1 (60%): Missing or weak pull-up resistors. I2C is an open-drain protocol. It requires resistors pulling SDA and SCL to 3.3V. The Adafruit BME280 has 10kΩ pull-ups onboard. If you wire multiple generic sensors together without checking their pull-up status, the parallel resistance drops too low, or if absent, the lines float, causing garbage data and Errno 121.
- Cause 2 (25%): Excessive bus capacitance. Long wires act as capacitors. Standard I2C limits bus capacitance to 400pF. If your wires are over 50cm, the rising edge of the SCL clock becomes a slow ramp, and the Pi misses the bit. Fix: Use shorter wires or add a dedicated I2C bus extender like the PCA9615.
- Cause 3 (15%): Address collision or misconfiguration. You are polling address
0x77but the sensor's SDO pin is tied to GND, making its actual address0x76. The Pi sends data to 0x77, gets no ACK, and throws the Remote I/O error.
For deeper kernel-level I2C documentation, refer to the official Raspberry Pi hardware configuration docs.
Extending and Simplifying the Build
Depending on your end goal, you should adapt the architecture of this script.
How to Simplify
If you just need a quick data logger for a school project or a short-term test, strip out the infinite while loop and the OLED/display logic. Modify the script to append a single line to a local CSV file using Python's built-in csv module, run it via a simple cron job every 5 minutes, and exit. This eliminates the need to manage daemon memory leaks or bus lockups over long uptimes.
How to Extend
For a permanent smart-home node, you need the script to survive reboots and publish to an MQTT broker (like Mosquitto or Home Assistant).
- Add MQTT: Install
paho-mqttvia pip. Inside thetryblock, format the sensor data as a JSON payload and publish it to a topic likehomeassistant/sensor/bme280/state. - Create a systemd service: Do not use
rc.localor.bashrcfor auto-starting hardware scripts in 2026. Create a file at/etc/systemd/system/bme280-logger.service:
[Unit]
Description=BME280 I2C Logger
After=network.target i2c.service
[Service]
ExecStart=/usr/bin/python3 /home/pi/bme280_logger.py
WorkingDirectory=/home/pi
Restart=always
User=pi
Environment=PYTHONUNBUFFERED=1
[Install]
WantedBy=multi-user.target
Enable it with sudo systemctl enable --now bme280-logger. The Restart=always directive ensures that if an unhandled kernel panic drops the I2C bus, systemd will automatically restart your Python script.
Frequently Asked Questions (FAQ)
Is Python fast enough for real-time hardware control on a Raspberry Pi?
No. CPython (the standard Python interpreter) is garbage-collected and subject to the Global Interpreter Lock (GIL) and OS-level scheduling jitter. You can easily read an I2C sensor every 10ms, but you cannot reliably bit-bang a protocol or trigger a GPIO pin with microsecond precision. For hard real-time motor control or sub-millisecond timing, you must write a C/C++ daemon, or use a microcontroller (like a Pi Pico) alongside the Pi to handle the low-level timing.
How do I run a Python Raspberry Pi script automatically on boot in 2026?
The modern, robust standard is systemd (as shown in the Extend section above). Older tutorials often suggest adding your script to /etc/rc.local or the user's .bashrc. Avoid these. rc.local runs before networking is fully initialized (which breaks MQTT connections), and .bashrc only runs if you physically log in via SSH or attach a monitor. systemd handles dependencies, logging (journalctl -u bme280-logger), and automatic restarts.
Why does my Python Raspberry Pi GPIO script fail with "Permission denied"?
If you are using legacy libraries like RPi.GPIO on newer 64-bit Raspberry Pi OS (Bookworm), you will hit permission errors because the underlying /dev/mem access model was deprecated for security reasons. The modern approach is to use the lgpio library or Adafruit's Blinka (which wraps lgpio), which utilizes the standard Linux character device interface (/dev/gpiochipX). Ensure your user is in the gpio group by running sudo usermod -a -G gpio pi.
Can I use MicroPython on a standard Raspberry Pi instead of CPython?
No. MicroPython is designed for bare-metal microcontrollers without an operating system, such as the Raspberry Pi Pico (RP2040) or ESP32. The standard Raspberry Pi 4 and 5 are Single Board Computers (SBCs) running a full Linux kernel. You must use standard CPython (or alternative implementations like PyPy) on Linux SBCs. If you want the lightweight, hardware-close feel of MicroPython, add a $4 Pi Pico to your project and have the Pi 5 communicate with it via UART or USB serial.






