If you are building an I2C environmental sensor node and need wireless telemetry, buy the Raspberry Pi Pico W (specifically the RP2040-based Pico W with pre-soldered headers, typically $8.50). The base Pico lacks WiFi, and the newer Pico 2 (RP2350) currently has limited MicroPython library support for edge-case I2C clock stretching compared to the mature RP2040 ecosystem. (Note: While frequently misspelled as raspberri pi pico in search queries, the official hardware is the Raspberry Pi Pico, and this guide targets the exact silicon and firmware behaviors of the RP2040).

This guide walks through wiring a BME280 sensor to the Pico W, provides production-ready MicroPython code with explicit error handling, and gives you a definitive decision tree for debugging the dreaded OSError: [Errno 5] EIO I2C failure.

Board Selection Decision Tree: Pico vs. Pico W vs. Pico 2

Before wiring, confirm you have the right silicon. The RP2040 and RP2350 handle I2C bus capacitance and clock stretching slightly differently. Use this decision matrix to lock in your board variant.

CriteriaPico (Base)Pico W (RP2040)Pico 2 (RP2350)
Wireless TelemetryNo (Requires external ESP-01)Yes (CYW43439 onboard)Yes (on Pico 2 W variant)
MicroPython I2C MaturityHighHighMedium (Edge cases in clock stretching)
Price (Approx. 2026)$4.00$6.00 - $8.50$5.00 - $9.00
Best Use CaseOffline data loggingIoT sensor nodes, MQTTHigh-compute local DSP
Concrete Pick: For 95% of I2C sensor projects requiring network access, choose the Raspberry Pi Pico W with pre-soldered headers (Adafruit 5526 or SparkFun DEV-20164). Pre-soldered headers save you 15 minutes of flux-core soldering and ensure reliable breadboard contact for I2C lines, which are highly sensitive to intermittent connections.

Hardware BOM and Pin Mapping

Do not use generic, unbranded BME280 breakouts if you are debugging I2C for the first time. Cheap clones often lack onboard pull-up resistors and use 5V logic level shifters that introduce bus capacitance. We are using the Adafruit variant for guaranteed 3.3V native logic.

Parts List

  • MCU: Raspberry Pi Pico W (RP2040) with headers
  • Sensor: Adafruit BME280 I2C/SPI Breakout (Product ID: 2652) - $14.95
  • Resistors: 2x 4.7kΩ 1/4W metal film resistors (for external I2C pull-ups)
  • Wire: 22 AWG solid core hook-up wire (keep I2C runs under 30cm / 12 inches)

Pin Mapping Table

Pico W PinGPIO NumberBME280 Breakout PinFunction
Pin 6GP4SDI / SDAI2C Data Line
Pin 7GP5SCK / SCLI2C Clock Line
Pin 363V3 OUTVIN / 3Vo3.3V Power
Pin 38GNDGNDCommon Ground

Step-by-Step Wiring and Pull-Up Resistor Rules

I2C is an open-drain protocol. The MCU and sensor can only pull the line LOW; they rely on resistors to pull the line HIGH. If your pull-up resistance is too high, the signal rises too slowly, causing bit errors. If it is too low, the MCU cannot pull the line fully to ground.

  1. Power the Board: Connect Pico W 3V3 (Pin 36) to the BME280 VIN pin. Connect Pico W GND (Pin 38) to BME280 GND. Warning: Never connect 5V to the Pico W GPIO pins; the RP2040 is strictly 3.3V tolerant and 5V will permanently destroy the GPIO bank.
  2. Route Data and Clock: Connect GP4 to SDA, and GP5 to SCL.
  3. Calculate Pull-Up Resistance: The Adafruit BME280 breakout includes 10kΩ internal pull-up resistors. For short wires (<10cm) at 100kHz, this is fine. However, for 400kHz Fast Mode or longer wires, 10kΩ is too weak due to wire capacitance. We add external 4.7kΩ resistors from SDA to 3V3 and SCL to 3V3.
  4. The Math: 10kΩ (internal) in parallel with 4.7kΩ (external) yields an equivalent resistance of ~3.2kΩ. This sits perfectly in the ideal 2.2kΩ - 4.7kΩ sweet spot for 3.3V I2C buses, ensuring sharp square wave edges without exceeding the RP2040's 3mA sink current limit per pin.

Complete MicroPython Firmware with Error Handling

This code targets the Raspberry Pi Pico W (RP2040) running MicroPython v1.22+. It explicitly defines pins, sets a conservative 100kHz frequency to prevent EIO errors on marginal wiring, and includes a robust try/except block to catch and categorize I2C bus faults.


import machine
import time

# --- Pin Definitions for Raspberry Pi Pico W ---
I2C_SDA_PIN = 4  # GP4
I2C_SCL_PIN = 5  # GP5
I2C_BUS_ID = 0
I2C_FREQ = 100000  # 100kHz Standard Mode (Use 400000 only with 3.2k pull-ups)

# BME280 I2C Address (Adafruit uses 0x77, some generic boards use 0x76)
BME280_ADDR = 0x77
CHIP_ID_REG = 0xD0
EXPECTED_CHIP_ID = 0x60

def init_i2c():
    sda = machine.Pin(I2C_SDA_PIN)
    scl = machine.Pin(I2C_SCL_PIN)
    i2c = machine.I2C(I2C_BUS_ID, sda=sda, scl=scl, freq=I2C_FREQ)
    return i2c

def verify_sensor(i2c):
    devices = i2c.scan()
    if not devices:
        raise RuntimeError('I2C scan empty. Check SDA/SCL wiring and 3.3V power.')
    
    if BME280_ADDR not in devices:
        raise RuntimeError(f'BME280 not found at 0x{BME280_ADDR:02X}. Found: {[hex(d) for d in devices]}')
    
    # Read Chip ID register to confirm sensor is alive and not locked up
    chip_id = i2c.readfrom_mem(BME280_ADDR, CHIP_ID_REG, 1)[0]
    if chip_id != EXPECTED_CHIP_ID:
        raise RuntimeError(f'Invalid Chip ID: 0x{chip_id:02X}. Expected 0x60. Sensor may be counterfeit or damaged.')
    
    print('BME280 verified and responding correctly.')

# --- Main Execution ---
i2c_bus = init_i2c()

try:
    verify_sensor(i2c_bus)
except OSError as e:
    error_str = str(e)
    if '[Errno 5]' in error_str:
        print('CRITICAL FAULT: OSError: [Errno 5] EIO caught.')
        print('Action: I2C bus collision, missing pull-ups, or sensor locked in clock stretch.')
    elif '[Errno 110]' in error_str:
        print('CRITICAL FAULT: ETIMEDOUT. SCL line is being held LOW by the sensor.')
    else:
        print(f'Unexpected I2C OS Error: {error_str}')
except RuntimeError as e:
    print(f'Configuration Error: {e}')
except Exception as e:
    print(f'Unhandled Exception: {e}')

Debugging 'OSError: [Errno 5] EIO' on I2C

When the RP2040's I2C peripheral fails to receive an ACKnowledge (ACK) bit from the target device, MicroPython throws OSError: [Errno 5] EIO. This is the most common I2C failure on the bench. Do not blindly reboot; follow this ranked diagnostic path.

The First 3 Things to Check When It Fails

  1. Multimeter Continuity & Voltage: De-energize the board. Check continuity from the Pico W GND pin to the BME280 GND pin. Then, power it on and measure DC voltage between 3V3 and GND at the sensor breakout, not the Pico. It must read between 3.25V and 3.35V. A reading of 2.8V indicates a brownout causing the sensor to drop off the bus.
  2. Pull-Up Resistor Presence: With the board powered, measure DC voltage on the SDA and SCL lines relative to GND. Both should read ~3.3V when idle. If they read 0V or float around 1.5V, your pull-up resistors are missing, broken, or the wrong value.
  3. Address Mismatch: Run i2c.scan() in the REPL. If it returns [118], your sensor is at 0x76, not 0x77. Update the BME280_ADDR variable in the code.

Ranked Causes for EIO (If the 3 checks above pass)

RankCauseFix / Measurement Threshold
1Bus Capacitance > 400pFShorten wires. I2C spec limits bus capacitance to 400pF. Long ribbon cables act as capacitors, rounding off square waves into triangles. Keep I2C wires under 30cm.
2Sensor Locked in Clock StretchingThe BME280 holds SCL LOW if it needs more time to process. The RP2040 I2C hardware sometimes times out. Fix: Power cycle the sensor (toggle its 3.3V VIN pin via a MOSFET) or drop frequency to 50kHz.
3SDA and SCL SwappedUnlike UART, I2C is not cross-wired. SDA must go to SDA, SCL to SCL. Swap them and re-run i2c.scan().
4Counterfeit Sensor ICCheap clones fail the Chip ID check (Register 0xD0). If i2c.scan() sees the address but readfrom_mem throws EIO, the clone doesn't support standard register mapping. Buy name-brand breakouts.

Extending and Simplifying the Build

Once your I2C bus is stable and returning a valid Chip ID, you have two paths forward depending on your project constraints.

How to Extend (Adding Wireless Telemetry)

To push this data to a home automation server, integrate the umqtt.simple library. Because the Pico W's CYW43439 WiFi chip shares the SPI bus internally and draws up to 150mA during transmit bursts, ensure your 3.3V voltage regulator can supply at least 300mA. Add a 100µF decoupling capacitor across the 3V3 and GND rails near the Pico W to prevent WiFi transmit brownouts from resetting the I2C peripheral.

How to Simplify (Minimalist Breadboard)

If you are building a quick prototype and want to eliminate the external 4.7kΩ pull-up resistors, you can rely solely on the BME280 breakout's internal 10kΩ pull-ups, but you must enforce two rules: 1. Limit the I2C frequency to 50000 (50kHz) in the machine.I2C initialization. 2. Keep the jumper wires under 10cm (4 inches) to minimize parasitic capacitance. This trades bus speed for a cleaner, less cluttered breadboard.

Final Recommendation: Do not leave I2C bus stability to chance. For any deployment outside a controlled lab environment, hardwire the 4.7kΩ external pull-ups, stick to 100kHz standard mode, and use the try/except error handling block provided above. This guarantees your RP2040 will catch and log an EIO fault rather than silently hanging in a readfrom_mem loop.