To use a Raspberry Pi for data logging, connect an I2C sensor (like the BME280) to GPIO pins 3 (SDA) and 5 (SCL), enable the I2C interface in raspi-config, and run a Python script using the adafruit-circuitpython-bme280 library to poll readings and append them to a CSV file. This guide targets the Raspberry Pi 5 (4GB variant) running Raspberry Pi OS (64-bit Bookworm), providing a reliable, mains-powered environmental logging baseline.
Difficulty: 2/5 (Beginner-Intermediate)
Time to complete: 45 minutes
Target Board: Raspberry Pi 5 4GB
Sensor: Adafruit BME280 I2C Breakout
Decision Tree: Which Pi and Storage for Your Logger?
Before buying parts, match your logging scenario to the right hardware. Data logging is an I/O-bound task, and picking the wrong board or storage medium will result in corrupted logs or dead batteries.
| Scenario | Power Source | Sample Rate | Recommended Board | Storage Medium |
|---|---|---|---|---|
| Continuous Environmental | Mains (USB-C) | 1-60 sec | Raspberry Pi 5 4GB | High Endurance microSD |
| Remote Weather Station | Solar/LiPo | 5-15 min | Raspberry Pi Zero 2 W | Standard 32GB microSD |
| High-Vibration Industrial | 24V DC DIN | 100ms+ | Compute Module 4 (Lite) | External NVMe SSD |
Parts List & Pin Mapping
Order these exact variants to ensure the code and wiring diagrams below work without modification. Generic "BME280" clones from random marketplaces often use the BMP280 chip (no humidity) or lack onboard I2C pull-up resistors.
Bill of Materials
- Board: Raspberry Pi 5 4GB (Official)
- Sensor: Adafruit BME280 I2C/SPI Breakout (Product ID 2652)
- Storage: SanDisk 64GB High Endurance UHS-I microSD (SDSQUNE-064G)
- Wiring: 4x Silicone female-to-female jumper wires (26 AWG)
- Power: Official Raspberry Pi 27W USB-C Power Supply
I2C Pin Mapping Table
The Raspberry Pi 5 exposes Hardware I2C1 on the primary 40-pin header. Do not use software I2C; it is prone to timing jitter and bus lockups.
| Pi 5 Pin # | GPIO / Function | BME280 Breakout Pin | Wire Color (Suggested) |
|---|---|---|---|
| 1 | 3V3 Power | VIN (or 3Vo) | Red |
| 3 | GPIO 2 (SDA1) | SDI (or SDA) | Yellow |
| 5 | GPIO 3 (SCL1) | SCK (or SCL) | Orange |
| 6 | Ground | GND | Black |
Step-by-Step Wiring & I2C Setup
Follow these steps to physically connect the sensor and enable the I2C bus at the OS level.
- De-energize the Pi: Unplug the USB-C power supply. Never wire I2C headers while the Pi is powered; a slipped jumper wire bridging 3.3V to SCL can permanently damage the Pi's SoC I2C controller.
- Connect the Wiring: Using the pin mapping table above, connect the four female-to-female jumper wires between the Pi 40-pin header and the BME280 breakout.
- Boot and SSH: Power on the Pi, connect via SSH (or open a terminal if using a monitor).
- Enable I2C: Run
sudo raspi-config. Navigate to Interface Options > I2C and select Yes to enable the ARM I2C interface. Reboot the Pi when prompted. - Verify the Bus: After reboot, install the I2C tools and scan the bus:
You should seesudo apt update && sudo apt install i2c-tools -y i2cdetect -y 177(or76) in the grid output. This confirms the Pi sees the BME280 at its default I2C address.
Complete Python Data Logging Script
This script uses the Adafruit CircuitPython ecosystem, which is the most robust way to interface with I2C sensors on modern Raspberry Pi OS. Install the dependencies first:
sudo apt install python3-pip python3-venv -y
python3 -m venv logger_env
source logger_env/bin/activate
pip3 install adafruit-circuitpython-bme280
Save the following code as data_logger.py. It includes explicit pin definitions, error handling for I2C dropouts, and CSV rotation logic.
import time
import csv
import board
import busio
import adafruit_bme280
from datetime import datetime
import os
import sys
# --- PIN DEFINITIONS & CONFIG ---
# Hardware I2C1 on Raspberry Pi 5
I2C_SDA = board.SDA # Physical Pin 3 (GPIO 2)
I2C_SCL = board.SCL # Physical Pin 5 (GPIO 3)
LOG_FILE = "environment_log.csv"
SAMPLE_INTERVAL = 60 # Seconds between readings
MAX_FILE_SIZE_MB = 50 # Rotate CSV when it hits this size
def setup_sensor():
"""Initialize I2C bus and BME280 sensor with address fallback."""
i2c = busio.I2C(I2C_SCL, I2C_SDA)
try:
# Default Adafruit BME280 address is 0x77
sensor = adafruit_bme280.Adafruit_BME280_I2C(i2c, address=0x77)
except ValueError:
# Fallback for generic clones wired with SDO tied to GND
sensor = adafruit_bme280.Adafruit_BME280_I2C(i2c, address=0x76)
# Configure sensor for continuous logging (lower self-heating)
sensor.mode = adafruit_bme280.MODE_NORMAL
sensor.overscan_temperature = adafruit_bme280.OVERSCAN_X1
sensor.overscan_pressure = adafruit_bme280.OVERSCAN_X1
sensor.overscan_humidity = adafruit_bme280.OVERSCAN_X1
return sensor
def check_and_rotate_log():
"""Rename old CSV if it exceeds max size to prevent infinite growth."""
if os.path.exists(LOG_FILE):
size_mb = os.path.getsize(LOG_FILE) / (1024 * 1024)
if size_mb >= MAX_FILE_SIZE_MB:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
os.rename(LOG_FILE, f"environment_log_{timestamp}.csv")
def log_data(sensor):
"""Read sensor and append to CSV."""
check_and_rotate_log()
file_exists = os.path.isfile(LOG_FILE)
try:
with open(LOG_FILE, mode='a', newline='') as file:
writer = csv.writer(file)
if not file_exists:
writer.writerow(["Timestamp", "Temp_C", "Pressure_hPa", "Humidity_%"])
timestamp = datetime.now().isoformat()
temp = round(sensor.temperature, 2)
pressure = round(sensor.pressure, 2)
humidity = round(sensor.humidity, 2)
writer.writerow([timestamp, temp, pressure, humidity])
print(f"[{timestamp}] Logged: {temp}C | {pressure}hPa | {humidity}%")
except OSError as e:
print(f"File write error: {e}")
if __name__ == "__main__":
try:
sensor = setup_sensor()
print("Sensor initialized. Starting logging loop...")
while True:
try:
log_data(sensor)
time.sleep(SAMPLE_INTERVAL)
except OSError as e:
# Catches I2C bus dropouts without crashing the script
print(f"I2C Read Error: {e}. Retrying in 10s...")
time.sleep(10)
except KeyboardInterrupt:
print("\nLogging stopped by user.")
sys.exit(0)
except Exception as e:
print(f"Fatal initialization error: {e}")
sys.exit(1)
Debugging: "Remote I/O error" and First Three Checks
When working with I2C on the Pi, you will inevitably encounter bus errors. The most common fatal error string thrown by Python's underlying I2C libraries is:
OSError: [Errno 121] Remote I/O erroror
ValueError: No I2C device at address: 0x77
If your script crashes or i2cdetect shows a blank grid, perform these first three checks before rewriting code:
- Run
i2cdetect -y 1: If the grid is empty, the OS cannot see the hardware. If it showsUU, the kernel has claimed the device (usually due to a conflicting/boot/firmware/config.txtoverlay). If it shows the correct hex address (77 or 76), the hardware is fine and your Python environment is misconfigured. - Measure VCC with a Multimeter: Put your multimeter in DC voltage mode. Probe the BME280 VIN pin and GND pin. You must read between 3.2V and 3.4V. If you read 0V, your jumper wire is dead or seated poorly. If you read 5V, you wired it to Pin 2 (5V) instead of Pin 1 (3.3V) and may have fried the sensor's logic level regulator.
- Clear the I2C Bus State: A crashed Python script can leave the SDA line pulled low, locking the bus. Reboot the Pi (
sudo reboot) to force the SoC to reset the I2C peripheral state.
Ranked Causes for Errno 121
If the first three checks pass but you still get [Errno 121] Remote I/O error intermittently during the logging loop, here are the ranked causes:
- Cause 1 (60%): Loose Dupont Wires. Standard cheap jumper wires lose tension over time. Vibration or thermal expansion breaks the SCL connection mid-read. Fix: Use silicone-jacketed wires or solder a 4-pin JST-SH connector.
- Cause 2 (25%): Missing Pull-Up Resistors. I2C requires pull-up resistors on SDA and SCL. The Adafruit 2652 breakout has them onboard. If you are using a bare $2 BME280 module from a bulk pack, it likely lacks them. Fix: Solder 4.7kΩ resistors between SDA-3.3V and SCL-3.3V.
- Cause 3 (15%): Sensor Self-Heating. Polling the BME280 too fast (e.g., every 1 second) causes the internal silicon to heat up, occasionally causing the internal ADC to lock up and drop off the bus. Fix: Keep sample intervals at 30 seconds or higher, and use
OVERSCAN_X1as shown in the code.
Extending and Simplifying the Build
Once the baseline logger is running, you can adapt it to fit specific deployment constraints.
How to Extend the Build
- Add an RTC (Real-Time Clock): If the Pi loses internet and reboots, its system clock will reset to the epoch, ruining your CSV timestamps. Wire a DS3231 RTC module to the same I2C bus (it uses address 0x68) and enable the
dtoverlay=i2c-rtc,ds3231inconfig.txt. - Add MQTT Telemetry: To view data live without SSH-ing into the Pi, add the
paho-mqttlibrary to the Python script. Publish thetemp,pressure, andhumidityvariables to a local Mosquitto broker or Home Assistant instance alongside the CSV write. - Switch to SQLite: If you plan to log at 1Hz (every second) for months, CSV files become slow to parse. Swap the
csvmodule for Python's built-insqlite3to write to a localized relational database, which handles concurrent reads (via a web dashboard) much better.
How to Simplify the Build
- Downsize to Pi Zero 2 W: If this is going in a weatherproof enclosure on a solar panel, swap the Pi 5 for a Pi Zero 2 W. It uses 80% less idle power. The GPIO pinout and I2C1 bus are identical, so the wiring and code require zero changes.
- Use Raspberry Pi OS Lite: Flash the "Lite" (headless) version of Raspberry Pi OS. It strips out the desktop environment, freeing up ~400MB of RAM and reducing background CPU interrupts that can occasionally delay I2C polling loops.
Sources & Further Reading:
Raspberry Pi Official Documentation: I2C Configuration
Adafruit Learning System: BME280 Breakout Guide






