If your Raspberry Pi Pico is throwing I2C bus errors when reading sensors like the BME280, the direct answer is almost always one of two things: you are relying on the RP2040's weak internal 50kΩ pull-up resistors instead of external 4.7kΩ resistors, or your Thonny IDE backend has locked up the USB serial port. You need external pull-ups for any I2C bus running at 400kHz, and you need to hard-reset the Pico's power cycle to clear the bus state.

This guide walks through the exact hardware setup, provides a fault-tolerant MicroPython script to verify your bus, and gives you a definitive troubleshooting path for the most common Pico I2C crashes. We are targeting the Raspberry Pi Pico W running MicroPython v1.22.1+, though the electrical principles apply to the entire RP2040 and RP2350 family.

The Verdict: Which Raspberry Pi Pico Variant to Choose

Before wiring up sensors, you need the right board. The Pico ecosystem has expanded significantly, and picking the wrong variant leads to unnecessary hardware hacks. Use this decision tree to select your board, terminating in a concrete default pick for 90% of sensor projects.

Requirement If Yes... If No...
Do you need WiFi or Bluetooth (BLE) to log sensor data to MQTT or a dashboard? Choose Pico W (RP2040 + CYW43439) Proceed to next question
Do you need >2MB flash, 520KB SRAM, or dual-core 150MHz (e.g., for DSP or audio)? Choose Pico 2 (RP2350) Proceed to next question
Is your budget strictly under $4 per unit for a high-volume offline deployment? Choose Pico (Standard RP2040) Choose Pico W
Concrete Pick: For almost all bench prototyping and environmental monitoring builds, buy the Raspberry Pi Pico W. The CYW43439 wireless chip adds only ~$2 to the BOM cost but saves you from needing a separate ESP32 bridge just to push BME280 telemetry to Home Assistant.

Parts List & Pin Mapping

Do not buy 5V-tolerant I2C sensors for the Pico. The RP2040 and RP2350 GPIO pads are strictly 3.3V tolerant; feeding 5V into GP4 or GP5 will permanently fry the IO bank. Here is the exact bill of materials and wiring map for a robust I2C build.

Bill of Materials

  • Microcontroller: Raspberry Pi Pico W (with pre-soldered headers)
  • Sensor: BME280 Breakout (Adafruit 2652 or any generic 3.3V variant with onboard voltage regulator)
  • Resistors: 2x 4.7kΩ through-hole resistors (for I2C pull-ups)
  • Wiring: 22 AWG solid core jumper wires
  • Power: 5V/2A USB-C power supply (do not rely on laptop USB ports for stable 3.3V rail regulation)

Pin Mapping Table

Pico W Pin GPIO Number Function BME280 Pin Notes
Pin 6 GP4 I2C0 SDA SDI/SDA Connect 4.7kΩ pull-up to 3.3V
Pin 7 GP5 I2C0 SCL SCK/SCL Connect 4.7kΩ pull-up to 3.3V
Pin 36 3V3 OUT Power VCC / VIN Max draw 300mA from this rail
Pin 38 GND Ground GND Common ground is mandatory
Callout Tip: The Internal Pull-Up Trap. The RP2040 datasheet specifies internal pull-ups at roughly 50kΩ to 60kΩ. The I2C specification requires a maximum rise time of 300ns for a 400kHz bus, which mathematically requires a ~4.7kΩ resistor assuming standard bus capacitance. If you try to use machine.Pin.PULL_UP in your code instead of physical resistors, the bus will fail at 400kHz. Always use physical 4.7kΩ resistors on the breadboard.

Complete MicroPython Build & Code

This script targets the Raspberry Pi Pico W. It does not require external libraries (like bme280.py) to prove the bus is working. Instead, it scans the bus and reads the BME280's hardware Chip ID register (0xD0). If the bus is wired correctly, this register will always return 0x60. This is the ultimate sanity check before you bother loading a heavy sensor driver.

import machine
import time

# --- PIN DEFINITIONS ---
SDA_PIN = 4
SCL_PIN = 5
I2C_FREQ = 400000  # 400kHz Fast Mode
BME280_ADDR = 0x76 # Default for Adafruit/some generics; try 0x77 if this fails
CHIP_ID_REG = 0xD0
EXPECTED_CHIP_ID = 0x60

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

def scan_i2c_bus():
    """Scans the bus and returns a list of hex addresses."""
    devices = i2c.scan()
    if not devices:
        print("[FATAL] No I2C devices found. Check wiring and pull-ups.")
        return []
    print(f"[INFO] Found {len(devices)} device(s): {[hex(d) for d in devices]}")
    return devices

def verify_bme280_chip_id(addr):
    """Reads the Chip ID register to verify sensor identity and bus health."""
    try:
        # Read 1 byte from register 0xD0
        chip_id = i2c.readfrom_mem(addr, CHIP_ID_REG, 1)
        id_val = chip_id[0]
        print(f"[INFO] Register 0xD0 returned: {hex(id_val)}")
        
        if id_val == EXPECTED_CHIP_ID:
            print("[SUCCESS] BME280 Chip ID verified! Bus is healthy.")
            return True
        else:
            print(f"[WARNING] Unexpected Chip ID. Expected {hex(EXPECTED_CHIP_ID)}, got {hex(id_val)}.")
            return False
            
    except OSError as e:
        handle_i2c_error(e, addr)
        return False

def handle_i2c_error(e, addr):
    """Catches and diagnoses specific MicroPython I2C OSErrors."""
    err_code = e.args[0] if e.args else None
    if err_code == 110:
        print(f"[ERROR] OSError: [Errno 110] ETIMEDOUT on address {hex(addr)}.")
        print("-> Cause: SCL line held low (clock stretching failure) or missing pull-ups.")
    elif err_code == 5:
        print(f"[ERROR] OSError: [Errno 5] EIO on address {hex(addr)}.")
        print("-> Cause: Device NACKed. Wrong address (try 0x77) or sensor is dead.")
    else:
        print(f"[ERROR] Unhandled I2C OSError: {e}")

# --- MAIN EXECUTION ---
print("Starting I2C Diagnostic Routine...")
devices = scan_i2c_bus()

if BME280_ADDR in devices:
    verify_bme280_chip_id(BME280_ADDR)
elif 0x77 in devices:
    print("[INFO] Address 0x76 not found, but 0x77 is present. Switching...")
    verify_bme280_chip_id(0x77)
else:
    print("[HALT] BME280 not detected on expected addresses.")

Troubleshooting: Fixing "OSError: [Errno 110] ETIMEDOUT"

When the RP2040 I2C peripheral times out waiting for an ACK or a clock release, MicroPython throws OSError: [Errno 110] ETIMEDOUT. This is a hardware-level bus lockup. Here are the ranked causes and fixes, from most likely to least likely.

  1. Missing or Incorrect Pull-Up Resistors (80% of cases): As noted, the Pico's internal pull-ups are too weak. If you omitted the 4.7kΩ physical resistors on SDA and SCL, the signal edges are too slow, and the Pico's I2C state machine times out before the bus reaches the logic HIGH threshold. Fix: Solder or breadboard 4.7kΩ resistors from GP4 to 3.3V, and GP5 to 3.3V.
  2. Bus Capacitance Overload (10% of cases): If you have daisy-chained more than 4 sensors or are using long, unshielded ribbon cables (>30cm), the parasitic capacitance exceeds the 400pF I2C limit. The 4.7kΩ resistor can't charge the line fast enough. Fix: Drop the I2C frequency in code to 100000 (100kHz) or reduce cable length.
  3. Slave Clock Stretching Failure (5% of cases): Some poorly designed generic BME280 clones hold the SCL line low indefinitely if they experience an internal brownout. Fix: Power cycle the sensor independently of the Pico, or add a 100µF decoupling capacitor across the sensor's VCC and GND pins.
  4. Thonny Backend Lockup (5% of cases): If you soft-reset the Pico while an I2C transaction is mid-flight, the RP2040's I2C hardware block can enter a zombie state. Fix: Unplug the USB cable entirely for 5 seconds to drain the capacitors, then reconnect.

First Three Checks When the Bus Fails

When your sensor returns garbage data or throws an EIO / ETIMEDOUT error, do not immediately rewrite your code. Grab your multimeter and perform these three physical checks in order.

1. Probe the Bus Voltages (DC Mode)

Set your digital multimeter to DC Volts. Place the black probe on the Pico's GND pin. Place the red probe on GP4 (SDA), then GP5 (SCL). You must read between 3.28V and 3.31V. If you read 0V, your sensor is internally shorting the bus to ground (dead sensor). If you read ~1.5V, your pull-up resistors are missing, and the line is floating in an undefined state.

2. Run the Raw I2C Scan in the REPL

Open the Thonny Shell (REPL) and type exactly this:

import machine
i2c = machine.I2C(0, sda=machine.Pin(4), scl=machine.Pin(5))
print(i2c.scan())

If it returns an empty list [], you have a physical wiring or power issue. If it returns [118] (which is 0x76 in decimal), your hardware is perfect, and the bug is in your sensor driver library.

3. Verify the Thonny Interpreter State

Thonny's MicroPython backend frequently caches stale I2C states. Go to Tools > Options > Interpreter. Ensure you are using "MicroPython (Raspberry Pi Pico)" and not a generic serial port. Click Stop/Restart backend. If the Pico's onboard LED flashes rapidly, the USB CDC connection has re-initialized cleanly.

Extending and Simplifying the Build

Once your BME280 is reliably returning a Chip ID, you have two paths forward depending on your project goals.

Path A: Extend with an I2C OLED Display

The most common extension is adding a 0.96" SSD1306 I2C OLED to display local readings. Because I2C is a multi-drop bus, you simply wire the OLED's SDA/SCL in parallel with the BME280. Warning: Cheap SSD1306 modules often lack onboard pull-up resistors. If you add an OLED and your BME280 suddenly starts throwing ETIMEDOUT errors, the OLED is dragging down the bus capacitance. Add a second set of 4.7kΩ pull-ups near the OLED, or drop the bus speed to 100kHz.

Path B: Simplify by Switching to SPI

If you are building a data logger that samples at high frequencies (e.g., >10Hz), I2C is the wrong protocol. I2C requires address bytes and ACK bits for every transaction, adding massive overhead. The Fix: Switch your BME280 to SPI mode. Most breakouts have an SPI pad you can bridge. Wire GP19 (MOSI), GP16 (MISO), GP18 (SCK), and GP17 (CS) to the sensor. SPI on the RP2040 can easily sustain 10MHz+ with zero pull-up resistor headaches and no address collisions. Use SPI when raw throughput matters; stick to I2C when you want to minimize wire count.

For deeper electrical specifications on the RP2040 GPIO pad states and I2C timing diagrams, consult the official Raspberry Pi RP2040 Datasheet. For MicroPython-specific I2C method signatures and exception handling, refer to the MicroPython machine.I2C documentation.