Project Overview & Difficulty Rating
If you want to DIY with Raspberry Pi to build a reliable environmental logging station, the BME280 sensor via the I2C bus is the gold standard. Unlike cheap DHT11 sensors that rely on fragile bit-banged GPIO timing, the BME280 uses the hardware I2C controller, yielding precise temperature, pressure, and humidity data without blocking the CPU.
Time to Build: 45 minutes
Estimated Cost: $105 - $115 USD
Target Board: Raspberry Pi 5 (8GB variant) running Raspberry Pi OS (Bookworm or newer). Note: The Pi 5 uses the RP1 southbridge chip, which changes how I2C and GPIO are addressed compared to the Pi 4.
In this guide, we will wire the sensor, write a robust Python script using smbus2 to read compensated data, and log it to a local SQLite database. We will also cover the exact debugging steps for the most common I2C failure modes.
Hardware Spec Sheet & Parts List
Sourcing the right variants is critical. Do not buy the bare silver BME280 chip; you need a breakout board with built-in 3.3V voltage regulation and I2C pull-up resistors.
| Component | Exact Variant / Model | Estimated Price (2026) | Notes |
|---|---|---|---|
| Microcontroller | Raspberry Pi 5 (8GB RAM) | $80.00 | 4GB works, but 8GB handles local DB/Flask better. |
| Sensor | BME280 Breakout (3.3V/5V tolerant) | $6.00 - $9.00 | Must have 4 pins (VIN, GND, SCL, SDA). Avoid 6-pin SPI versions. |
| Storage | 32GB microSD (SanDisk Extreme) | $14.00 | High endurance rated for continuous SQLite writes. |
| Power Supply | 27W USB-C PD Power Supply | $12.00 | Official Pi 27W PSU prevents peripheral brownouts. |
| Wiring | F-to-F Jumper Wires (4x) | $3.00 | Keep I2C runs under 30cm to avoid capacitance issues. |
Pin Mapping & Wiring Steps
The Raspberry Pi 5 routes its primary I2C bus through the RP1 southbridge. The physical pins remain identical to older models, but the underlying architecture handles the clock stretching and ACK/NACK bits differently. Always de-energize the Pi before wiring.
| Pi 5 Physical Pin | BCM GPIO | Function | BME280 Breakout Pin | Wire Color (Standard) |
|---|---|---|---|---|
| Pin 1 | N/A | 3.3V Power | VIN / VCC | Red |
| Pin 3 | GPIO 2 | I2C1 SDA | SDA | Yellow |
| Pin 5 | GPIO 3 | I2C1 SCL | SCL | Orange |
| Pin 6 | N/A | Ground | GND | Black |
0x76. If your board has an SDO (Serial Data Out) pin, tying it to GND sets the address to 0x76, while tying it to VCC sets it to 0x77. We use 0x76 in this build.
- Flash the OS: Use Raspberry Pi Imager to install Raspberry Pi OS (64-bit, Bookworm) onto your microSD card. Ensure SSH and your WiFi credentials are pre-configured in the advanced settings.
- Enable I2C: Boot the Pi, open a terminal, and run
sudo raspi-config. Navigate to Interface Options > I2C and enable it. Reboot the Pi. - Wire the Sensor: Connect the four jumper wires between the Pi's 40-pin header and the BME280 breakout exactly as mapped in the table above.
- Verify Connection: Install the I2C tools via
sudo apt install i2c-tools. Runi2cdetect -y 1. You should see76in the grid output. If you see--everywhere, check your wiring before proceeding to code.
Python Code: BME280 I2C Reader with SQLite Logging
This script targets the Raspberry Pi 5. It uses smbus2 for low-level I2C communication and the bme280 library to handle the complex Bosch compensation algorithms (which require reading 32 bytes of calibration registers from the sensor's EEPROM).
First, install the required Python packages:
pip3 install smbus2 RPi.bme280
Create a file named env_logger.py and paste the following complete, compilable code:
import smbus2
import bme280
import sqlite3
import time
import sys
import os
# --- PIN & BUS DEFINITIONS (BCM Numbering) ---
I2C_PORT = 1
I2C_SDA_BCM = 2 # Routes to Physical Pin 3
I2C_SCL_BCM = 3 # Routes to Physical Pin 5
BME280_ADDR = 0x76
DB_FILE = 'environmental_data.db'
def setup_database():
"""Initialize SQLite database and create readings table."""
conn = sqlite3.connect(DB_FILE)
c = conn.cursor()
c.execute('''CREATE TABLE IF NOT EXISTS readings
(id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
temperature REAL,
pressure REAL,
humidity REAL)''')
conn.commit()
return conn
def main():
print(f"Initializing I2C bus {I2C_PORT} (SDA:BCM{I2C_SDA_BCM}, SCL:BCM{I2C_SCL_BCM})...")
conn = setup_database()
bus = smbus2.SMBus(I2C_PORT)
# 1. Verify Sensor Connection & Load Calibration
try:
# Reading the Chip ID register (0xD0) should return 0x60 for BME280
chip_id = bus.read_byte_data(BME280_ADDR, 0xD0)
if chip_id != 0x60:
print(f"WARNING: Unexpected Chip ID {hex(chip_id)}. Expected 0x60.")
calib = bme280.load_calibration_params(bus, BME280_ADDR)
print("Calibration parameters loaded successfully.")
except OSError as e:
print(f"FATAL: Cannot reach BME280 at {hex(BME280_ADDR)}.")
print(f"Exact Error: {e}")
print("Check wiring, ensure I2C is enabled, and verify pull-up resistors.")
sys.exit(1)
except Exception as e:
print(f"FATAL: Unexpected error during calibration: {e}")
sys.exit(1)
print("Sensor initialized. Logging to SQLite every 60 seconds. Press Ctrl+C to stop.")
# 2. Main Logging Loop
try:
while True:
try:
data = bme280.sample(bus, BME280_ADDR, calib)
c = conn.cursor()
c.execute("""INSERT INTO readings (temperature, pressure, humidity)
VALUES (?, ?, ?)""",
(round(data.temperature, 2),
round(data.pressure, 2),
round(data.humidity, 2)))
conn.commit()
print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] Logged: "
f"{data.temperature:.2f}C | {data.pressure:.2f}hPa | {data.humidity:.2f}%")
except OSError as e:
print(f"[ERROR] I2C Bus read failure: {e}. Retrying in 60s...")
time.sleep(60)
except KeyboardInterrupt:
print("\nLogging stopped by user.")
finally:
conn.close()
bus.close()
print("Database and I2C bus closed cleanly.")
if __name__ == '__main__':
main()
Debugging: Fixing I2C Read Failures
When working with I2C on the Pi 5's RP1 architecture, the most common failure results in this exact error string:
OSError: [Errno 121] Remote I/O error
This error means the Pi's I2C controller sent a clock pulse and address, but received no ACKnowledge (ACK) bit back from the sensor. Here are the ranked causes and fixes:
- Wiring/Pin Swap (Most Likely): SDA and SCL are swapped, or the ground wire is loose. Fix: Verify physical pins 3 (SDA) and 5 (SCL) with a multimeter for continuity to the breakout board.
- I2C Kernel Module Not Loaded: The OS hasn't loaded the I2C driver. Fix: Run
ls /dev/i2c*. If/dev/i2c-1is missing, rerunsudo raspi-configand reboot. - Address Mismatch: Your specific breakout board has the SDO pin pulled high, making the address
0x77instead of0x76. Fix: Runi2cdetect -y 1and update theBME280_ADDRvariable in the code to match the hex value shown in the grid. - Missing Pull-Up Resistors: I2C requires pull-up resistors on SDA and SCL. While the Pi has onboard 1.8kΩ pull-ups, long wires (>30cm) introduce capacitance that overwhelms them. Fix: Add external 4.7kΩ pull-up resistors between SDA/SCL and 3.3V.
1. Run
i2cdetect -y 1 to see if the hardware layer can even see the device.2. Measure the voltage between the breakout board's VCC and GND pins with a multimeter (must read 3.2V - 3.4V).
3. Check for physical solder bridges on the BME280 breakout header pins.
Extending and Simplifying the Build
Depending on your end goal, you can modify this architecture to fit your skill level or project scope.
How to Simplify
If raw smbus2 and manual calibration loading feel too complex, switch to the Adafruit CircuitPython BME280 library. It abstracts the I2C bus and calibration into simple property calls (sensor.temperature). However, note that CircuitPython libraries sometimes have higher overhead and slower I2C polling rates compared to the native smbus2 approach shown above.
How to Extend
To turn this local logger into an IoT dashboard:
- Add MQTT: Install
paho-mqttand publish thedata.temperaturepayload to a local Mosquitto broker. This allows Home Assistant to auto-discover the sensor via MQTT Discovery. - Add Grafana: Instead of SQLite, use the
influxdb-clientPython package to push time-series data to an InfluxDB instance, then visualize the pressure drops (indicating incoming storms) on a Grafana dashboard. - Add a Display: Wire an SSD1306 128x64 OLED display to the same I2C bus (it will use address
0x3C) to show real-time readings without needing a monitor attached to the Pi.
FAQ: DIY with Raspberry Pi
Why does my DIY with Raspberry Pi 5 I2C project fail when the exact same code worked on a Pi 4?
The Raspberry Pi 5 replaced the legacy Broadcom SoC peripheral routing with a dedicated RP1 southbridge chip. While the I2C bus addresses (/dev/i2c-1) remain the same for user-space applications, the underlying GPIO memory mapping changed entirely. If your older code used the legacy RPi.GPIO library to toggle pins or bit-bang I2C, it will throw a RuntimeError on the Pi 5. You must migrate to rpi-lgpio, gpiozero (with the lgpio backend), or use hardware I2C via smbus2 as demonstrated in this guide.
Can I use 5V logic sensors for DIY with Raspberry Pi projects?
No. The Raspberry Pi (all models, including the Pi 5) uses strictly 3.3V logic on its GPIO and I2C pins. Connecting a 5V sensor directly to the Pi's SDA/SCL pins will backfeed 5V into the RP1 southbridge, permanently destroying the I2C controller or the entire chip. If you must use a 5V sensor (like some ultrasonic rangers or older LCDs), you must use a bidirectional logic level shifter (like the Texas Instruments TXS0108E or a cheap BSS138 MOSFET-based breakout) between the Pi and the sensor.
What is the best power supply for DIY with Raspberry Pi 5 projects?
For any project involving peripherals (sensors, OLEDs, USB devices), you must use a 27W USB-C PD (Power Delivery) power supply. The Pi 5 can draw up to 5A at 5V (25W) to support full peripheral current limits. If you use a standard 15W phone charger, the Pi's firmware will detect the insufficient power negotiation and throttle the USB and GPIO current limits to 600mA, which can cause brownouts and I2C bus resets when your sensor or display spikes in power draw.
How do I protect the SQLite database from corrupting during a power loss?
SQLite is highly resilient, but sudden power loss during a write operation can corrupt the journal file. To mitigate this in a DIY build, enable WAL (Write-Ahead Logging) mode by executing PRAGMA journal_mode=WAL; immediately after your sqlite3.connect() call. WAL mode separates the read and write operations, drastically reducing the chance of database corruption if the Pi loses power unexpectedly.






