The Raspberry Pi Pico 2 (RP2350) is a massive leap over the original RP2040, offering dual ARM Cortex-M33 and RISC-V cores, 520KB of SRAM, and significantly improved GPIO pad characteristics. However, when you start daisy-chaining environmental sensors like the Bosch BME688, you quickly hit the physical limits of the I2C bus: address collisions and capacitance thresholds.
This guide walks through building a robust, multiplexed I2C sensor hub using the Pi Pico 2 and a TCA9548A I2C multiplexer. We will cover exact wiring, provide complete MicroPython firmware with hardware-level error handling, and deep-dive into debugging the infamous OSError: [Errno 5] EIO bus fault.
RP2350 vs RP2040: I2C and Hardware Upgrades
Before wiring the board, it is critical to understand what the Pi Pico 2 brings to the bench. The RP2350 silicon features upgraded GPIO pads with configurable Schmitt triggers and better drive strength, which directly impacts I2C signal integrity on noisy breadboards.
| Feature | Original Pico (RP2040) | Pi Pico 2 (RP2350) | Impact on I2C Projects |
|---|---|---|---|
| Core Architecture | Dual ARM Cortex-M0+ @ 133MHz | Dual ARM Cortex-M33 / RISC-V @ 150MHz | Faster I2C interrupt servicing; less likely to miss clock-stretch events. |
| SRAM | 264 KB | 520 KB | Ample buffer space for logging high-frequency sensor arrays without dropping packets. |
| GPIO I2C Pads | Standard CMOS, fixed hysteresis | Configurable Schmitt trigger, improved slew rate | Rejects high-frequency noise on long I2C traces, reducing phantom EIO errors. |
| I2C Controllers | 2x I2C (I2C0, I2C1) | 2x I2C (I2C0, I2C1) | Unchanged. Still requires a multiplexer for >2 identical sensors. |
| On-board SMPS | RT6154B (lower efficiency at light loads) | RT6154B (optimized quiescent current) | Cleaner 3V3(OUT) rail, reducing VCC ripple that triggers sensor brownouts. |
Hardware BOM and Pin Mapping
This build targets the standard Raspberry Pi Pico 2 with RP2350 (Micro-USB variant, Part # SC1260). Do not use the Pico 2 W (CYW43439) for this specific baseline tutorial, as the wireless module shares pins and alters the physical footprint on standard breadboards, though the I2C logic remains identical.
Parts List
- MCU: Raspberry Pi Pico 2 (RP2350, Micro-USB, Part # SC1260)
- Multiplexer: Adafruit TCA9548A I2C Multiplexer Breakout (Product ID 2717)
- Sensors: 2x Adafruit BME688 Environmental Sensor (Product ID 3660) or Pimoroni equivalent
- Passives: 2x 4.7kΩ through-hole resistors (for main bus pull-ups, if breakouts lack them)
- Wiring: 22 AWG silicone jumper wires (keep lengths under 10cm to minimize capacitance)
Pi Pico 2 Pin Mapping
| Pi Pico 2 Pin | GPIO | Function | Destination |
|---|---|---|---|
| Pin 6 | GP4 | I2C0 SDA | TCA9548A SDI (Main Bus Data) |
| Pin 7 | GP5 | I2C0 SCL | TCA9548A SCL (Main Bus Clock) |
| Pin 4 | GP2 | GPIO OUT | TCA9548A RST (Active Low Reset) |
| Pin 36 | 3V3(OUT) | Power (3.3V) | TCA9548A VIN & BME688 VCC |
| Pin 38 | GND | Ground | Common Ground Rail |
Step-by-Step Wiring and Assembly
Signal integrity on I2C is governed by bus capacitance. The I2C specification limits total bus capacitance to 400pF. A standard solderless breadboard adds roughly 20pF to 50pF per row, and jumper wires add ~1.5pF per centimeter. Keep your main bus traces as short as possible.
- Seat the Boards: Place the Pi Pico 2 across the center trench of the breadboard. Place the TCA9548A on the same side, 4 rows away. Place the two BME688 breakouts on the opposite side of the trench.
- Wire Power and Ground: Connect Pico 2 Pin 36 (3V3) to the red power rail, and Pin 38 (GND) to the blue ground rail. Distribute power to the VIN/VCC and GND pins on the TCA9548A and both BME688 sensors.
- Establish the Main I2C Bus: Run a wire from Pico 2 GP4 to the TCA9548A SDI pin. Run a wire from GP5 to the TCA9548A SCL pin.
- Install Pull-Up Resistors: Insert a 4.7kΩ resistor between the red power rail and the SDI line. Insert a second 4.7kΩ resistor between the red power rail and the SCL line. (Check your BME688 breakout datasheet; many modern boards include 10kΩ on-board pull-ups. If they do, you can omit the main bus resistors, but 4.7kΩ is safer for 400kHz fast-mode).
- Wire the Multiplexer Channels: Connect TCA9548A Channel 0 SDA/SCL to the first BME688. Connect Channel 1 SDA/SCL to the second BME688.
- Wire the Reset Line: Connect Pico 2 GP2 to the TCA9548A RST pin.
Complete MicroPython Firmware and Code
The following MicroPython script initializes the I2C bus, manages the TCA9548A multiplexer channels, and reads the Chip ID from the BME688 sensors to verify communication. It includes robust error handling to catch and report bus faults without crashing the REPL.
Target: Raspberry Pi Pico 2 (RP2350) running MicroPython v1.23.0 or newer.
import machine
import time
# --- Pin Definitions for Pi Pico 2 ---
I2C_SDA_PIN = 4
I2C_SCL_PIN = 5
MUX_RESET_PIN = 2
# --- I2C Addresses ---
TCA9548A_ADDR = 0x70
BME688_ADDR = 0x77 # Default Adafruit BME688 address (SDO tied to GND)
BME688_CHIP_ID_REG = 0xD0
EXPECTED_CHIP_ID = 0x61
def init_hardware():
"""Initialize GPIO and I2C peripherals."""
# Hold Mux in reset initially, then release
rst_pin = machine.Pin(MUX_RESET_PIN, machine.Pin.OUT, value=0)
time.sleep_ms(10)
rst_pin.value(1) # Release reset (Active Low)
# Initialize I2C0 at 100kHz (Standard Mode)
# RP2350 handles 400kHz well, but 100kHz is safer for long breadboard traces
i2c = machine.I2C(0, sda=machine.Pin(I2C_SDA_PIN), scl=machine.Pin(I2C_SCL_PIN), freq=100000)
return i2c
def select_mux_channel(i2c, channel):
"""Switch the TCA9548A to the specified channel (0-7)."""
if not (0 <= channel <= 7):
raise ValueError("Channel must be between 0 and 7")
try:
# Write a single byte with the bit corresponding to the channel set high
i2c.writeto(TCA9548A_ADDR, bytes([1 << channel]))
except OSError as e:
print(f"[FATAL] Mux switching failed on channel {channel}: {e}")
raise
def read_bme688_chip_id(i2c, channel):
"""Read the BME688 Chip ID to verify I2C communication."""
select_mux_channel(i2c, channel)
time.sleep_ms(5) # Allow bus to settle after mux switching
try:
# Read 1 byte from the Chip ID register
result = i2c.readfrom_mem(BME688_ADDR, BME688_CHIP_ID_REG, 1)
chip_id = result[0]
if chip_id == EXPECTED_CHIP_ID:
print(f"[OK] Channel {channel}: BME688 detected (ID: 0x{chip_id:02X})")
return True
else:
print(f"[WARN] Channel {channel}: Unknown device ID 0x{chip_id:02X}")
return False
except OSError as e:
if e.errno == 5: # EIO - Bus error / NACK
print(f"[ERR] Channel {channel}: OSError [Errno 5] EIO - Device not responding or bus fault.")
else:
print(f"[ERR] Channel {channel}: Unexpected I2C Error: {e}")
return False
def main():
print("Initializing Pi Pico 2 I2C Hub...")
i2c = init_hardware()
# Scan main bus to ensure Mux is present
devices = i2c.scan()
if TCA9548A_ADDR not in devices:
print(f"[FATAL] TCA9548A not found on main bus. Found: {[hex(d) for d in devices]}")
return
print("TCA9548A Multiplexer online. Polling sensors...\n")
while True:
for ch in range(2): # We have 2 sensors on channels 0 and 1
read_bme688_chip_id(i2c, ch)
time.sleep(2)
if __name__ == "__main__":
main()
Debugging "OSError: [Errno 5] EIO" on the RP2350
In MicroPython, OSError: [Errno 5] EIO is the universal I2C bus failure code. It means the master (Pi Pico 2) sent a clock pulse but did not receive an ACKnowledge (ACK) bit from the slave, or the SDA line was held low by a slave in a stuck state. Because the RP2350's I2C peripheral is hardware-driven, an EIO error almost always points to a physical layer issue, not a software bug.
- Pull-Up Continuity: Use a multimeter in continuity mode. With power OFF, check resistance between SDA and 3V3, and SCL and 3V3. You should read ~4.7kΩ. If it reads OL (infinite), your pull-ups are missing or the breadboard contact is dead.
- Mux Reset Pin State: Measure the voltage on the TCA9548A RST pin. It MUST be 3.3V during operation. If it is floating around 1.2V, the multiplexer is in an undefined state and will drop channels.
- Address Conflicts: Ensure the SDO pin on one BME688 is tied to GND (0x77) and the other to VCC (0x76). If both are 0x77, they will collide when the mux is bypassed or if you accidentally open both channels.
Ranked Causes of EIO Errors
| Rank | Cause | Symptom | Fix |
|---|---|---|---|
| 1 | Bus Capacitance > 400pF | Works at 100kHz, throws EIO at 400kHz. Scope shows rounded, shark-fin SDA edges. | Drop I2C frequency to 50kHz in code, or use 2.2kΩ pull-up resistors to charge the capacitance faster. |
| 2 | Missing / Weak Pull-Ups | SDA/SCL idle at 1.5V instead of 3.3V. Intermittent EIO errors. | Add external 4.7kΩ resistors to the 3V3 rail. Do not rely solely on internal MCU pull-ups (they are ~50kΩ and too weak). |
| 3 | Clock Stretching Timeout | EIO occurs randomly during heavy sensor reads (e.g., BME688 gas heater phase). | The BME688 holds SCL low while heating. Ensure your MicroPython I2C timeout parameter is set high enough (default is usually adequate, but increase if polling rapidly). |
| 4 | Ground Loop / VCC Sag | EIO triggers only when a secondary load (like an LED or motor) turns on. | Wire sensor GND directly to the Pico 2 GND pin, not through a long breadboard rail. Add a 100nF decoupling capacitor across the sensor VCC/GND pins. |
Scaling the Hub: Extend or Simplify
Once your dual-BME688 hub is stable, you will likely want to adapt the architecture for your specific enclosure or deployment environment. Here is how to scale the design up or down.
How to Simplify the Build
If you only need two sensors and they are different models (e.g., one BME688 and one SCD41 CO2 sensor), delete the TCA9548A multiplexer entirely. The Pi Pico 2 has two native, independent I2C controllers.
- Wire Sensor A to I2C0 (GP4/GP5).
- Wire Sensor B to I2C1 (GP6/GP7).
How to Extend the Build
To scale up to 8 or 16 sensors, leverage the RP2350's dual-core architecture.
- Hardware: Chain two TCA9548A boards by setting the A0/A1/A2 address jumpers on the second board to 0x71. Wire their SDI/SCL pins in parallel with the first board.
- Software: Assign Core 0 (ARM) to handle the I2C polling and sensor math. Use MicroPython's
_threadmodule to assign Core 1 to handle UART logging or WiFi transmission (if using a Pico 2 W). This prevents the I2C bus from timing out while the MCU waits for network ACKs.
By respecting the physical limits of the I2C bus and leveraging the RP2350's upgraded GPIO pads, the Pi Pico 2 becomes an exceptionally reliable platform for dense environmental sensor arrays. Keep your traces short, verify your pull-ups, and let the hardware I2C controllers do the heavy lifting.






