Project Overview & Difficulty Rating

Building a reliable environmental data logger requires more than just copying a tutorial; it requires understanding the physical layer of the I2C bus. This guide walks through building a BME280-based temperature, humidity, and pressure logger using Raspberry Pi Picos. We will focus heavily on the physical wiring, bus verification, and the exact debugging steps required when the I2C bus inevitably throws an error.

Difficulty: 2/5 (Beginner-Intermediate)
Time to Build: 45 minutes
Target Board Variant: This code and wiring specifically target the Raspberry Pi Pico W (RP2040) running MicroPython v1.22 or newer. The standard Pico (non-W) will work identically for the I2C portion, but lacks the Wi-Fi hardware for remote logging extensions.

Hardware Spec Sheet & Parts List

The BME280 is a 3.3V logic device. Feeding it 5V will permanently destroy the internal MEMS structures. Always verify your breakout board has an onboard voltage regulator if you plan to use a 5V source, but since the Pico outputs 3.3V natively, we will wire it directly to the 3V3(OUT) pin.

Component Exact Variant / Model Approx. Price (2026)
Microcontroller Raspberry Pi Pico W (with pre-soldered headers) $6.00
Sensor Adafruit BME280 I2C/SPI Breakout (Product ID: 2652) or generic 3.3V BME280 module $10.00 - $15.00
Prototyping Half-size solderless breadboard (400 tie-points) $5.00
Wiring 24 AWG solid core jumper wires (Dupont male-to-male) $6.00 / pack

Pin Mapping & Wiring Steps

The RP2040 chip inside Raspberry Pi Picos features two dedicated I2C controllers (I2C0 and I2C1) that can be mapped to multiple GPIO pins. For this build, we are using I2C0 on its default pins: GP4 (SDA) and GP5 (SCL).

Pico W Pin RP2040 GPIO BME280 Breakout Pin Wire Color (Suggested)
Pin 4 (GP2) - - -
Pin 6 (GP4) GPIO 4 SDI / SDA Blue
Pin 7 (GP5) GPIO 5 SCK / SCL Yellow
Pin 36 (3V3 OUT) - VIN / VCC Red
Pin 38 (GND) - GND Black
Bench Tip: The internal pull-up resistors on the RP2040 are roughly 50kΩ to 60kΩ. This is too weak for reliable I2C communication at 400kHz over standard jumper wires. If your BME280 breakout board does not have onboard 4.7kΩ pull-up resistors, you must add them externally between SDA/VCC and SCL/VCC. The Adafruit 2652 breakout includes them; cheap generic eBay modules often do not.
  1. De-energize the board: Ensure the Pico W is unplugged from your PC before wiring.
  2. Connect Power: Route the red jumper from Pico Pin 36 (3V3 OUT) to the BME280 VIN. Route the black jumper from Pico Pin 38 (GND) to the BME280 GND. Do not use Pin 40 (VBUS/5V) for the BME280 VCC.
  3. Connect Data: Route the blue jumper from Pico Pin 6 (GP4) to BME280 SDA. Route the yellow jumper from Pico Pin 7 (GP5) to BME280 SCL.
  4. Verify Connections: Use a multimeter in continuity mode to ensure SDA and SCL are not shorted to each other or to ground.

MicroPython Code: Bus Verification & Error Handling

Before integrating a heavy third-party library like bme280.py, you must verify the physical I2C layer. The following MicroPython script initializes the bus, scans for devices, and reads the BME280 Chip ID register (0xD0). If this script runs successfully, your wiring is flawless, and you can safely add the full sensor driver.

import machine
import utime

# --- PIN DEFINITIONS ---
SDA_PIN = 4
SCL_PIN = 5
I2C_FREQ = 400000  # 400kHz Fast Mode
BME280_ADDR = 0x76 # Default address (0x77 if SDO pin is tied high)

# Initialize I2C0 bus
i2c = machine.I2C(0, sda=machine.Pin(SDA_PIN), scl=machine.Pin(SCL_PIN), freq=I2C_FREQ)

def verify_i2c_sensor():
    """Scans the I2C bus and verifies the BME280 Chip ID."""
    print(f"Scanning I2C bus at {I2C_FREQ//1000}kHz...")
    
    try:
        devices = i2c.scan()
        if not devices:
            print("FATAL: No I2C devices found. Check power, ground, and pull-up resistors.")
            return False
            
        print(f"Found devices: {[hex(d) for d in devices]}")
        
        if BME280_ADDR not in devices:
            print(f"ERROR: BME280 not at expected address 0x{BME280_ADDR:02x}.")
            return False
        
        # Read the Chip ID register (0xD0)
        # The BME280 should return 0x60
        chip_id = i2c.readfrom_mem(BME280_ADDR, 0xD0, 1)
        
        if chip_id[0] == 0x60:
            print(f"SUCCESS: BME280 verified. Chip ID: 0x{chip_id[0]:02x}")
            return True
        else:
            print(f"WARNING: Device found, but Chip ID is 0x{chip_id[0]:02x} (Expected 0x60). Wrong sensor?")
            return False
            
    except OSError as e:
        # Catch hardware-level I2C faults
        print(f"HARDWARE ERROR: {e}")
        print("Bus collision, NACK, or timeout occurred. Check SDA/SCL routing.")
        return False

if __name__ == "__main__":
    if verify_i2c_sensor():
        print("Physical layer verified. Safe to import and run full bme280.py driver.")
    else:
        print("Halting. Fix hardware faults before proceeding.")

Debugging: "OSError: [Errno 121] EIO" and I2C Failures

When working with Raspberry Pi Picos and I2C sensors, you will eventually hit a bus fault. The most common error string thrown by MicroPython on the RP2040 is:

OSError: [Errno 121] EIO (or OSError: [Errno 110] ETIMEDOUT on newer builds)

This error means the Pico sent a clock pulse and an address, but the sensor responded with a NACK (Not Acknowledged) by holding the SDA line high, or the bus timed out waiting for a response.

The First Three Things to Check When It Fails

  1. Verify VCC Voltage (Not VBUS): Measure the voltage at the BME280 VIN pin with a multimeter. It must read 3.3V. If you accidentally wired it to Pin 40 (VBUS), you are feeding it 5V from the USB line. The sensor is likely dead, and you must replace it.
  2. Run i2c.scan() in the REPL: Open the Thonny shell and type i2c.scan(). If it returns an empty list [], the Pico cannot see the device at all. This points to a broken ground wire, missing pull-up resistors, or a swapped SDA/SCL pair.
  3. Swap SDA and SCL: It is incredibly common to misread the pinout diagram. Swap the blue and yellow wires. I2C will not work if the data and clock lines are reversed, and the RP2040 will throw an EIO error rather than a polite "wrong pin" warning.

Ranked Causes for I2C EIO Errors

  • Cause 1 (60%): Missing or insufficient pull-up resistors on the SDA/SCL lines. (Fix: Add 4.7kΩ resistors to 3.3V).
  • Cause 2 (20%): SDA and SCL wires swapped at the breadboard. (Fix: Swap wires).
  • Cause 3 (15%): Poor breadboard contact or cold solder joint on the Pico headers. (Fix: Reflow headers or move to a different breadboard row).
  • Cause 4 (5%): I2C address mismatch. The BME280 SDO pin is floating or tied high, changing the address to 0x77. (Fix: Update BME280_ADDR in code).

Extending and Simplifying the Build

Once the physical layer is verified, you have two paths forward depending on your project constraints.

How to Extend the Build (Remote IoT Logger)

To turn this into a remote IoT node, leverage the Wi-Fi capabilities of the Pico W. 1. Download the bme280.py driver and umqtt.simple.py library to your Pico. 2. Connect to your local 2.4GHz Wi-Fi network using the network module. 3. Publish the compensated temperature and humidity JSON payloads to an MQTT broker (like Mosquitto or Adafruit IO) every 60 seconds. 4. Implement machine.deepsleep() between reads to drop current consumption from ~45mA to microamps, allowing months of runtime on a 18650 Li-ion cell.

How to Simplify the Build (Offline Data Logger)

If Wi-Fi is unnecessary or you are operating in a Faraday cage/remote field location, drop the Pico W and use the standard Raspberry Pi Pico ($4). Instead of MQTT, wire an SPI microSD card module (like the Adafruit 254) to the Pico's SPI0 bus. Write the raw CSV data directly to the FAT32 filesystem using MicroPython's uos and standard file I/O operations. This eliminates network stack overhead and reduces code complexity significantly.

Raspberry Pi Picos FAQ

Can I use standard Raspberry Pi Picos for Wi-Fi data logging?

No. The standard Raspberry Pi Pico (the one without the metal RF shield on the top right) uses the exact same RP2040 dual-core Cortex-M0+ processor, but it completely lacks the Infineon CYW43439 wireless chip. If your project requires MQTT, HTTP requests, or Bluetooth Low Energy (BLE), you must purchase the Pico W or the newer Pico 2 W. For strictly offline, wired, or local-storage projects, the standard Pico is perfectly adequate and saves you $2 per unit.

Why do my Raspberry Pi Picos show up as two different COM ports in Thonny?

This is a common point of confusion for beginners. When you plug in the Pico while holding the BOOTSEL button, it mounts as a USB Mass Storage Device (a virtual flash drive named RPI-RP2) so you can drag-and-drop .uf2 firmware files. This uses one USB interface. Once MicroPython is installed and you plug it in normally, it enumerates as a USB Serial Device (CDC/ACM) to provide the REPL (Read-Eval-Print Loop) console. Thonny interacts with the Serial port for coding, and the Mass Storage port for low-level firmware flashing.

How do I recover a bricked Raspberry Pi Pico W that won't mount as a drive?

If your MicroPython code crashes the board on boot (e.g., an infinite loop blocking the USB stack) or the filesystem is corrupted, the Pico might seem dead. To recover it: 1. Unplug the USB cable. 2. Press and hold the white BOOTSEL button on the Pico. 3. Plug the USB cable back into your PC while still holding the button. 4. Release the button. The Pico will bypass the internal flash and boot into the ROM-resident USB bootloader, appearing as the RPI-RP2 drive. You can then drag a fresh flash_nuke.uf2 to wipe the board, followed by the official MicroPython .uf2 file from the MicroPython download page.

What is the maximum I2C bus speed for Raspberry Pi Picos?

According to the Raspberry Pi Pico Datasheet, the RP2040 I2C controllers support Standard-mode (100kHz), Fast-mode (400kHz), and Fast-mode Plus (1MHz). While you can initialize the bus at freq=1000000 in MicroPython, 1MHz is rarely practical on a solderless breadboard. The parasitic capacitance of long jumper wires and breadboard contacts will round off the square clock waves, causing bit errors at 1MHz. Stick to 400kHz for reliable bench prototyping, and only use 1MHz if you have a custom PCB with short, impedance-controlled traces.