If you need to log temperature, humidity, and barometric pressure on a budget, the Raspberry Pi Pico paired with a Bosch BME280 sensor over I2C is the most reliable, cost-effective setup on the bench. The RP2040 chip handles I2C polling effortlessly, and the BME280 provides industrial-grade compensation algorithms that outperform cheap DHT11/22 modules by a wide margin. This guide gives you the exact hardware picks, a bulletproof wiring schematic, and production-ready MicroPython code with the error handling needed to survive real-world I2C bus noise.
Board and Interface Selection: The Decision Path
Before wiring, you must choose the correct Pico variant and communication protocol. The RP2040 supports both I2C and SPI natively, and the Pico family has splintered into several SKUs. Use this decision matrix to lock in your hardware.
| Decision Criteria | Option A | Option B | Verdict |
|---|---|---|---|
| Wireless Needs | Local logging / OLED display only | MQTT / WiFi data pushing required | If local: Standard Pico. If WiFi: Pico W. |
| Header Soldering | Willing to solder 40 pins (Saves $1) | Want pre-soldered headers (Saves 15 mins) | Pick the Pico H variant (pre-soldered). |
| Bus Protocol | I2C (2 wires, address-based) | SPI (4 wires, chip-select based) | I2C for simple wiring; SPI only if >2 sensors. |
| Wire Run Length | Under 30cm (1 foot) | Over 30cm (1 foot) | Under 30cm: 400kHz. Over 30cm: Drop to 100kHz. |
Hardware Spec Sheet and Pin Mapping
The RP2040 has two independent I2C controllers (I2C0 and I2C1). We will use I2C0 on its default pins to keep the routing clean. Note that while the RP2040 has internal pull-up resistors, they are weak (~50kΩ). The BME280 requires strong pull-ups for stable 400kHz operation.
Parts List
- MCU: Raspberry Pi Pico H with pre-soldered headers ($5.00)
- Sensor: Adafruit BME280 I2C/SPI Breakout (Product ID 2652) ($15.00) — Includes required 4.7kΩ pull-ups onboard.
- Display (Optional): 0.96" 128x64 SSD1306 I2C OLED ($10.00)
- Wiring: 26 AWG silicone jumper wires (pre-crimped with Dupont connectors)
Pin Mapping Table (I2C0)
| Pico H Pin | GPIO / Function | BME280 Breakout Pin | Notes |
|---|---|---|---|
| Pin 6 | GP4 (I2C0 SDA) | SDI / SDA | Data line. Keep under 30cm. |
| Pin 7 | GP5 (I2C0 SCL) | SCK / SCL | Clock line. Keep parallel to SDA. |
| Pin 36 | 3V3(OUT) | VIN / VCC | Do NOT use 5V (VSYS). BME280 is strictly 3.3V logic. |
| Pin 38 | GND | GND | Common ground reference. |
Step-by-Step Wiring Procedure
- De-energize the bus: Ensure the Pico is unplugged from your PC or USB power supply before making connections.
- Seat the Pico: Press the Raspberry Pi Pico H firmly into the center trench of a standard 830-point breadboard, ensuring the castellated edges (or header pins) are fully seated in the F-row and J-row.
- Route Power: Connect a red jumper from Pico Pin 36 (3V3) to the breadboard's positive (+) rail. Connect a black jumper from Pico Pin 38 (GND) to the negative (-) rail.
- Wire the Sensor: Plug the BME280 breakout into the breadboard. Run a red wire from the (+) rail to
VIN, and a black wire from the (-) rail toGND. - Connect I2C Lines: Run a blue wire from Pico Pin 6 (GP4) to the BME280
SDApin. Run a yellow wire from Pico Pin 7 (GP5) to the BME280SCLpin. - Verify Pull-ups: If using a generic, unbranded BME280 clone from an online marketplace, use your multimeter in continuity/resistance mode to verify 4.7kΩ resistance between the SDA/SCL lines and VCC. If missing, solder two 4.7kΩ through-hole resistors to the breakout board.
Complete MicroPython Code (Target: Pico H)
This code targets the standard Raspberry Pi Pico (or Pico H) running MicroPython v1.22 or newer. It includes a raw I2C chip-ID verification step before initializing the heavy compensation library, which isolates hardware faults from software faults.
bme280.py library. Download robert-hh's BME280 MicroPython module and save bme280.py to the root directory of your Pico's flash storage via Thonny or mpremote before running main.py.
# main.py - Raspberry Pi Pico BME280 I2C Logger
# Target Board: Raspberry Pi Pico H (RP2040)
# Target Firmware: MicroPython v1.22+
from machine import Pin, I2C
import time
import struct
# --- Pin Definitions ---
I2C_SDA_PIN = 4 # GP4 (Physical Pin 6)
I2C_SCL_PIN = 5 # GP5 (Physical Pin 7)
BME280_I2C_ADDR = 0x76 # Default Adafruit addr. Clones often use 0x77.
# --- Hardware Initialization ---
i2c = I2C(0, sda=Pin(I2C_SDA_PIN), scl=Pin(I2C_SCL_PIN), freq=400000)
def verify_hardware():
"""Checks I2C bus and verifies BME280 Chip ID before loading library."""
devices = i2c.scan()
if not devices:
raise OSError("I2C_SCAN_EMPTY: No devices found on bus.")
if BME280_I2C_ADDR not in devices:
# Fallback check for alternate address
if 0x77 in devices:
return 0x77
raise OSError(f"BME280_NOT_FOUND: Scanned {devices}, expected {hex(BME280_I2C_ADDR)}.")
# Read Chip ID register (0xD0). BME280 should return 0x60.
chip_id = i2c.readfrom_mem(BME280_I2C_ADDR, 0xD0, 1)[0]
if chip_id != 0x60:
raise ValueError(f"WRONG_CHIP: Expected 0x60, got {hex(chip_id)}. Is this a BMP280?")
return BME280_I2C_ADDR
try:
print("[INFO] Verifying I2C hardware...")
active_addr = verify_hardware()
print(f"[SUCCESS] BME280 found at {hex(active_addr)}. Chip ID verified.")
# Import library only after hardware is proven healthy
import bme280
sensor = bme280.BME280(i2c=i2c, address=active_addr)
print("[INFO] Starting environmental logging loop...")
while True:
# bme280.values returns a tuple: (temp_str, pressure_str, humidity_str)
temp, pres, hum = sensor.values
print(f"Temp: {temp} | Press: {pres} | Hum: {hum}")
time.sleep(2.0)
except OSError as e:
print(f"[FATAL I2C ERROR] {e}")
print("Action: Check SDA/SCL routing, pull-up resistors, and 3.3V power.")
except ValueError as e:
print(f"[FATAL SENSOR ERROR] {e}")
print("Action: You may have wired a BMP280 (no humidity) instead of a BME280.")
except Exception as e:
print(f"[UNEXPECTED ERROR] {type(e).__name__}: {e}")
Debugging I2C Failures: "OSError: [Errno 121] EIO"
When working with the RP2040's I2C peripheral, the MicroPython machine.I2C driver will throw specific OS errors when the bus fails. The two most common exact error strings you will encounter are:
OSError: [Errno 121] EIO(I/O Error: The sensor NACK'd the address or data byte).OSError: [Errno 19] ENODEV(No Device: The bus timed out waiting for an ACK).
The First Three Things to Check When It Fails
Before rewriting your code or swapping the sensor, execute this physical checklist:
- Verify SDA/SCL Swap: The RP2040 allows I2C pin multiplexing, but the default mapping for I2C0 is GP4 (SDA) and GP5 (SCL). If you swapped these on the breadboard, the bus will fail to initialize. Check physical pins 6 and 7.
- Measure Pull-Up Resistance: Put your multimeter in resistance mode (power off). Probe between SDA and 3.3V, then SCL and 3.3V. You must read ~4.7kΩ. If you read >10kΩ or OL (open line), the bus lacks pull-ups and the RP2040's open-drain outputs cannot pull the line high fast enough for 400kHz.
- Check Logic Levels: Ensure the BME280
VINis connected to the Pico's3V3(OUT)(Pin 36), NOTVSYS(Pin 39) or USB 5V. Feeding 5V into the BME280's VCC will fry the internal compensation memory within seconds.
Ranked Causes for Persistent EIO Errors
| Rank | Root Cause | Fix / Measurement Threshold |
|---|---|---|
| 1 | Missing or weak I2C pull-up resistors. | Solder 4.7kΩ resistors to SDA/SCL. Verify with DMM. |
| 2 | Wire run exceeds 30cm, causing capacitive loading. | Shorten wires, or drop freq in code to 100000 (100kHz). |
| 3 | Address mismatch (Clone boards often default to 0x77). | Run i2c.scan() in REPL. Update BME280_I2C_ADDR variable. |
| 4 | SDA line stuck low (Bus lockup from interrupted transaction). | Power cycle the Pico completely. (RP2040 I2C0 lacks auto-recovery). |
Extending and Simplifying the Build
Once the baseline logger is stable, you will likely want to adapt it for a specific deployment. Here is how to scale the design without introducing instability.
How to Simplify (Cost & Space Reduction)
If you only need temperature and humidity (no barometric pressure), swap the BME280 for an SHT31-D or AHT20. The AHT20 costs roughly $2.50 compared to the BME280's $15.00. To simplify the code, remove the pressure compensation logic and read the 6-byte raw data packet directly via i2c.readfrom_mem(), eliminating the need for the external bme280.py library file entirely.
How to Extend (Multi-Sensor Arrays)
The I2C bus is address-limited. You can only run two BME280s on a single bus (one at 0x76, one at 0x77 by pulling the CSB pin high). To extend beyond two sensors:
- Use I2C1: The RP2040 has a second I2C controller. Map I2C1 to GP2 (SDA) and GP3 (SCL) to run a second, independent bus.
- Add an I2C Multiplexer: Use a TCA9548A I2C multiplexer breakout ($4). This allows you to route up to 8 identical BME280 sensors (all at 0x76) by switching the multiplexer's internal channels via software.
- Switch to SPI: If you need high-speed polling of 3+ sensors, abandon I2C. Wire the BME280s in SPI mode, using a single shared MISO/MOSI/SCK bus and individual GPIO Chip Select (CS) pins for each sensor.
For 90% of makers building a local weather station or greenhouse monitor, the Raspberry Pi Pico H over I2C remains the undisputed default. Stick to the 4.7kΩ pull-ups, keep your wire runs short, and let the RP2040's hardware I2C peripheral handle the heavy lifting.






