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 |
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 |
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.
- 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.
- 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. - 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.
- 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.






