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.

Project Difficulty: Intermediate | Time Required: 45 minutes | Cost: ~$28 USD

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.

FeatureOriginal Pico (RP2040)Pi Pico 2 (RP2350)Impact on I2C Projects
Core ArchitectureDual ARM Cortex-M0+ @ 133MHzDual ARM Cortex-M33 / RISC-V @ 150MHzFaster I2C interrupt servicing; less likely to miss clock-stretch events.
SRAM264 KB520 KBAmple buffer space for logging high-frequency sensor arrays without dropping packets.
GPIO I2C PadsStandard CMOS, fixed hysteresisConfigurable Schmitt trigger, improved slew rateRejects high-frequency noise on long I2C traces, reducing phantom EIO errors.
I2C Controllers2x I2C (I2C0, I2C1)2x I2C (I2C0, I2C1)Unchanged. Still requires a multiplexer for >2 identical sensors.
On-board SMPSRT6154B (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 PinGPIOFunctionDestination
Pin 6GP4I2C0 SDATCA9548A SDI (Main Bus Data)
Pin 7GP5I2C0 SCLTCA9548A SCL (Main Bus Clock)
Pin 4GP2GPIO OUTTCA9548A RST (Active Low Reset)
Pin 363V3(OUT)Power (3.3V)TCA9548A VIN & BME688 VCC
Pin 38GNDGroundCommon Ground Rail
Bench Tip: The TCA9548A RST pin is active-low. If you leave it floating, breadboard noise can trigger random resets, dropping your I2C channels. Always tie it to a GPIO (like GP2) and pull it HIGH in software, or physically tie it to 3V3 if you don't need software reset capabilities.

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.

  1. 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.
  2. 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.
  3. 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.
  4. 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).
  5. Wire the Multiplexer Channels: Connect TCA9548A Channel 0 SDA/SCL to the first BME688. Connect Channel 1 SDA/SCL to the second BME688.
  6. 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.

The First Three Things to Check When It Fails:
  1. 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.
  2. 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.
  3. 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

RankCauseSymptomFix
1Bus Capacitance > 400pFWorks 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.
2Missing / Weak Pull-UpsSDA/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).
3Clock Stretching TimeoutEIO 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).
4Ground Loop / VCC SagEIO 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).
This eliminates the multiplexer bottleneck, removes the need for channel-switching code, and physically separates the bus capacitance, entirely eradicating EIO errors related to bus overload.

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