BME280 on Pi Zero: Log Temperature, Humidity and Pressure to CSV

A Pi Zero with a small environmental sensor makes a compact weather station or room monitor that you can leave running for weeks. The Bosch BME280 measures temperature, relative humidity and barometric pressure in one tiny package and talks over I2C, which needs only two signal wires.

In this tutorial you will enable I2C on Raspberry Pi OS Bookworm, wire a BME280 breakout, confirm it is detected, install a Python driver inside a virtual environment and write a logger that appends a timestamped reading to a CSV file every minute. The CSV opens directly in Excel, LibreOffice or Python's pandas for plotting.

I2C in Brief

I2C is a two-wire bus: SDA carries data and SCL carries the clock generated by the master, here the Pi. Many devices can share the same two wires because each has a 7-bit address. The BME280 uses address 0x76 or 0x77, selected by the level on its SDO pin; most cheap breakouts default to 0x76 and many Adafruit-style boards to 0x77.

Both lines are open-drain and need pull-up resistors. The Pi has 1.8 kilohm pull-ups to 3.3 V on GPIO2 and GPIO3, and most breakout boards add their own, so no extra resistors are needed for a short connection.

Wiring and Enabling I2C

BME280 pinPi Zero pin (physical)BCM name
VIN / VCCPin 13.3 V
GNDPin 6GND
SDAPin 3GPIO2 (SDA1)
SCLPin 5GPIO3 (SCL1)

Power the sensor from 3.3 V, not 5 V. Some breakouts have a regulator and accept 5 V, but feeding 3.3 V is always safe and keeps the I2C pull-ups at the Pi's logic level.

Enable I2C, check the sensor address and create a Python virtual environment
# Enable the I2C interface without opening the menu (0 means enable)
sudo raspi-config nonint do_i2c 0

# Install the i2c-tools utilities and the venv module
sudo apt install -y i2c-tools python3-venv

# Scan bus 1; you should see 76 or 77 in the grid
i2cdetect -y 1

# Bookworm blocks system-wide pip installs, so use a virtual environment.
# --system-site-packages also lets it use apt-installed libraries such as gpiozero
python3 -m venv --system-site-packages ~/envs/sensors
source ~/envs/sensors/bin/activate
pip install RPi.bme280 smbus2
Expected i2cdetect output with the sensor at 0x76:

     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
70: -- -- -- -- -- -- 76 --

If the grid is empty, check the wiring before going further. Software cannot fix a sensor the bus cannot see.

The CSV Logger

The RPi.bme280 package reads the factory calibration constants stored in the sensor and applies Bosch's compensation formulas for you. Save the program below as bme280_logger.py and run it with the virtual environment's Python.

bme280_logger.py: append one reading per minute to a CSV file
#!/usr/bin/env python3
"""Log BME280 temperature, humidity and pressure to a CSV file.

Run:  ~/envs/sensors/bin/python bme280_logger.py
"""
import csv
import os
import time
from datetime import datetime

import bme280   # provided by the RPi.bme280 package
import smbus2

I2C_BUS = 1            # the 40-pin header uses I2C bus 1
ADDRESS = 0x76         # change to 0x77 if i2cdetect showed 77
INTERVAL_S = 60        # seconds between readings
CSV_PATH = os.path.expanduser('~/bme280_log.csv')

bus = smbus2.SMBus(I2C_BUS)
# Read the sensor's calibration data once at startup
calibration = bme280.load_calibration_params(bus, ADDRESS)

def read_sensor():
    """Take one compensated sample and return rounded values."""
    data = bme280.sample(bus, ADDRESS, calibration)
    return (round(data.temperature, 2),   # degrees Celsius
            round(data.humidity, 1),      # percent relative humidity
            round(data.pressure, 2))      # hPa

def main():
    new_file = not os.path.exists(CSV_PATH)
    # newline='' prevents blank lines on some platforms; buffering=1 = line buffered
    with open(CSV_PATH, 'a', newline='', buffering=1) as f:
        writer = csv.writer(f)
        if new_file:
            writer.writerow(['timestamp', 'temperature_c', 'humidity_pct', 'pressure_hpa'])
        while True:
            try:
                temp, hum, pres = read_sensor()
                stamp = datetime.now().isoformat(timespec='seconds')
                writer.writerow([stamp, temp, hum, pres])
                print(stamp, temp, 'C', hum, '%', pres, 'hPa')
            except OSError as err:
                # A loose wire gives an I/O error; log it and keep running
                print('Read failed:', err)
            time.sleep(INTERVAL_S)

if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        print('Stopped')
    finally:
        bus.close()

How the Logger Works

  • Calibration first: each BME280 is trimmed at the factory and stores its constants in internal memory. load_calibration_params reads them once; every later sample is corrected with them.
  • Append mode: opening the file with 'a' means a restart continues the same file. The header row is written only if the file did not exist.
  • Line buffering: with buffering=1, each row is passed to the operating system as soon as it is written, so a power cut loses at most the current reading rather than a large buffer.
  • ISO timestamps: 2025-01-15T14:30:00 sorts correctly as text and is parsed automatically by spreadsheets and pandas.
  • Error handling: an OSError from a loose connection is reported but does not crash the logger.

Pressure in hPa is the station pressure at your altitude, not the sea-level value that weather reports quote. To compare with a weather service you must correct for altitude. Also note that temperature reads a little high if the sensor sits right above the Pi, because the board warms the air around it. Mount the sensor on short wires away from the Zero for room readings.

To look at the data quickly without leaving the terminal, tail -n 5 ~/bme280_log.csv shows the latest rows and wc -l ~/bme280_log.csv counts them. At one reading per minute the file grows by 1440 rows a day, which is well under 100 kilobytes, so a 16 GB card will hold years of data. If you need a faster rate, reduce INTERVAL_S, but remember that the BME280 heats itself slightly when sampled continuously, so one reading every few seconds is a sensible lower limit for temperature work.

Troubleshooting

  • No device in i2cdetect: SDA and SCL swapped, I2C not enabled, or the header pins not soldered.
  • ModuleNotFoundError: bme280: you ran the system python3 instead of the virtual environment's Python.
  • Humidity always zero or an error about chip ID: the board is a BMP280.
  • Remote I/O error: wrong address in the script. Use the number shown by i2cdetect.
  • Logger stops when you close SSH: run it as a systemd service, as shown in the systemd tutorial in this series.

Summary

You enabled I2C with raspi-config, wired a BME280 to GPIO2 and GPIO3, verified it with i2cdetect, and built a robust CSV logger inside a Bookworm virtual environment. The same structure, a sensor read function, an append-only file and a fixed interval, works for almost any I2C sensor, and the CSV is ready for plotting or for publishing to an MQTT broker in a later project.