Unlike the fixed-function pins on an Arduino Uno, the RP2040 chip inside the Raspberry Pi Pico features a highly flexible pin multiplexing system. Almost any of the 26 multifunction GPIO pins can be routed to I2C, SPI, or UART peripherals via the internal IO bank. Because of this, a standard raspberry pi pico pin diagram is less of a fixed map and more of a routing matrix. This flexibility is powerful, but it introduces unique debugging challenges—especially when dealing with I2C bus capacitance, logic-level mismatches, and internal pull-up resistor limitations.

This guide provides a data-dense GPIO multiplexing table, a complete I2C sensor build using the Pico W, and a deep-dive into debugging the most common RP2040 I2C bus failures.

Raspberry Pi Pico Pin Diagram & GPIO Multiplexing Matrix

The RP2040 exposes 30 GPIO pins (GP0-GP29), but GP23-GP25 are typically reserved for internal board functions (like the onboard LED and power supply control) on standard Pico variants. The remaining 26 pins are broken out to the headers. Below is the multiplexing matrix for the primary GPIO pins, showing which peripheral functions can be mapped to each pin.

Bench Note: The RP2040 operates strictly at 3.3V logic. Feeding a 5V I2C or SPI signal into any GPIO pin will permanently damage the silicon. Always use a logic level shifter (like the TXS0108E) if interfacing with 5V Arduino modules.
Pin GPIO I2C Default SPI Default UART Default PWM Channel ADC
1GP0I2C0 SDASPI0 RXUART0 TX0A-
2GP1I2C0 SCLSPI0 CSnUART0 RX0B-
3GND-----
4GP2I2C1 SDASPI0 SCKUART0 CTS1A-
5GP3I2C1 SCLSPI0 TXUART0 RTS1B-
6GP4I2C0 SDASPI0 RXUART1 TX2A-
7GP5I2C0 SCLSPI0 CSnUART1 RX2B-
31GP26I2C1 SDASPI1 SCKUART1 CTS5AADC0
32GP27I2C1 SCLSPI1 TXUART1 RTS5BADC1
34GP28-SPI1 RXUART0 TX6AADC2

Source: Official Raspberry Pi Pico Datasheet, Section 1.4.3.

Project Build: I2C Environmental Sensor on Pico W

To demonstrate practical pin mapping and bus debugging, we will wire a Bosch BME280 environmental sensor to the Pico W. This build targets the Raspberry Pi Pico W (RP2040 with 2MB flash and Infineon CYW43439 WiFi/BT module), but the GPIO mapping applies identically to the standard Pico and Pico H.

Parts List & Specifications

  • Microcontroller: Raspberry Pi Pico W (RP2040, 3.3V logic)
  • Sensor: Bosch BME280 Breakout Board (Adafruit 2652 or generic 3.3V variant)
  • Resistors: 2x 4.7kΩ (1/4W, for I2C pull-up)
  • Wiring: 22 AWG solid core jumper wires
Safety & Hardware Warning: Many cheap BME280 modules sold online are actually mislabeled BMP280s (which lack humidity sensing) or are 5V-tolerant modules with onboard regulators that output 5V on the SDA/SCL lines. Verify your breakout board has a 3.3V voltage regulator and logic level shifters before connecting it to the Pico.

Wiring Steps

  1. Connect the BME280 VCC to the Pico W 3V3(OUT) (Pin 36). Do not use VBUS (5V) unless your specific breakout board explicitly requires it and has an onboard LDO.
  2. Connect BME280 GND to Pico W GND (Pin 38).
  3. Connect BME280 SDA to Pico W GP4 (Pin 6). This maps to I2C0 SDA.
  4. Connect BME280 SCL to Pico W GP5 (Pin 7). This maps to I2C0 SCL.
  5. Critical Step: Solder or breadboard a 4.7kΩ pull-up resistor from GP4 to 3V3, and another 4.7kΩ from GP5 to 3V3. The RP2040's internal pull-ups are ~50kΩ, which is too weak to overcome bus capacitance at 400kHz I2C speeds.

Compilable MicroPython Code with I2C Error Handling

The following MicroPython script initializes the I2C bus, scans for the sensor, and reads the BME280's Chip ID register (0xD0) to verify communication. It includes robust error handling to catch bus timeouts and NACK (Not Acknowledged) errors.

# Target Board: Raspberry Pi Pico W (RP2040)
# Environment: MicroPython v1.22+ 
# Author: ElectricalFlux

from machine import Pin, I2C
import time

# --- Pin Definitions ---
SDA_PIN = 4   # GP4 (I2C0 SDA)
SCL_PIN = 5   # GP5 (I2C0 SCL)
I2C_FREQ = 400_000  # 400kHz Fast Mode

# BME280 Default I2C Addresses
BME_ADDR_PRIMARY = 0x76
BME_ADDR_SECONDARY = 0x77
BME_CHIP_ID_REG = 0xD0
EXPECTED_CHIP_ID = 0x60

def init_i2c():
    print(f'Initializing I2C0 on GP{SDA_PIN} (SDA) and GP{SCL_PIN} (SCL)...')
    i2c = I2C(0, sda=Pin(SDA_PIN), scl=Pin(SCL_PIN), freq=I2C_FREQ)
    return i2c

def verify_sensor(i2c):
    devices = i2c.scan()
    if not devices:
        print('ERROR: No I2C devices found on bus. Check wiring and pull-ups.')
        return None
    
    print(f'Found I2C devices at: {[hex(d) for d in devices]}')
    
    target_addr = None
    if BME_ADDR_PRIMARY in devices:
        target_addr = BME_ADDR_PRIMARY
    elif BME_ADDR_SECONDARY in devices:
        target_addr = BME_ADDR_SECONDARY
    else:
        print(f'ERROR: BME280 not found at {hex(BME_ADDR_PRIMARY)} or {hex(BME_ADDR_SECONDARY)}.')
        return None

    try:
        # Read 1 byte from the Chip ID register
        chip_id = i2c.readfrom_mem(target_addr, BME_CHIP_ID_REG, 1)[0]
        if chip_id == EXPECTED_CHIP_ID:
            print(f'SUCCESS: BME280 verified at {hex(target_addr)} (Chip ID: {hex(chip_id)})')
            return target_addr
        else:
            print(f'WARNING: Device found, but Chip ID is {hex(chip_id)}. Expected {hex(EXPECTED_CHIP_ID)}.')
            return None
            
    except OSError as e:
        # Catches I2C NACK and Timeout errors
        print(f'I2C Communication Failed: {e}')
        return None

if __name__ == '__main__':
    i2c_bus = init_i2c()
    sensor_addr = verify_sensor(i2c_bus)
    
    if sensor_addr:
        print('Sensor ready for data logging.')
    else:
        print('Halting execution. Debug hardware connections.')

Reference: MicroPython machine.I2C Documentation

Debugging: Fixing I2C 'OSError: [Errno 121] EIO' on RP2040

When working with the RP2040's I2C peripheral, the most common failure mode is the bus hanging or throwing an exception. If your console outputs the exact error string OSError: [Errno 121] EIO (or occasionally OSError: [Errno 5] EIO depending on your MicroPython build), it means the I2C controller sent a byte but did not receive an ACKnowledge (ACK) bit from the target device, or the SCL line is being held low.

Here are the first three things to check when this error occurs, ranked from most likely to least likely:

1. Missing or Inadequate External Pull-Up Resistors

The RP2040 datasheet notes that internal GPIO pull-ups are enabled by default in some MicroPython I2C implementations, but they are roughly 50kΩ to 60kΩ. The I2C specification requires a maximum pull-up resistance of ~4.7kΩ for 100kHz (Standard Mode) and ~2.2kΩ for 400kHz (Fast Mode) to ensure the signal rise time meets the tr threshold. Fix: Add physical 4.7kΩ resistors between SDA/SCL and 3.3V.

2. I2C Address Mismatch (0x76 vs 0x77)

The BME280's I2C address is determined by the state of its SDO (Serial Data Out) pin. If SDO is tied to GND, the address is 0x76. If tied to VCC, it is 0x77. Many breakout boards have a jumper pad on the bottom to change this. Fix: Run i2c.scan() in the MicroPython REPL. If it returns [0x77] but your code is hardcoded to 0x76, the sensor will NACK the transaction, throwing the EIO error.

3. Bus Capacitance and Wire Length

If you are using long jumper wires (over 30cm) or a breadboard with high parasitic capacitance, the 4.7kΩ pull-ups might still be too weak to pull the line high before the next clock cycle. Fix: Lower the I2C frequency in your code from 400,000 to 100,000 Hz, or drop the pull-up resistors to 2.2kΩ.

Extending and Simplifying the Build

Once you have stable I2C communication, you can scale this project up or down based on your deployment needs.

How to Simplify

If you are struggling with I2C bus capacitance or just need a visual output, swap the BME280 for an SSD1306 128x64 I2C OLED display. The SSD1306 is highly forgiving on bus timing, rarely requires external pull-ups due to its internal circuitry, and uses the exact same machine.I2C initialization. This is the best 'sanity check' peripheral to verify your Pico's GPIO pins are functioning correctly before debugging complex sensors.

How to Extend

To turn this into a production-ready IoT node, leverage the Pico W's WiFi capabilities and the RP2040's dual I2C buses:

  • Secondary Bus: Move the BME280 to I2C1 (GP2/GP3) and place the SSD1306 OLED on I2C0 (GP4/GP5). This prevents display refresh delays from blocking sensor polling.
  • PIO Integration: For advanced users, bypass the standard I2C hardware blocks entirely and use the RP2040's Programmable I/O (PIO) state machines. PIO allows you to implement custom I2C protocols or bit-bang multiple I2C buses simultaneously without CPU intervention, a feature detailed in the Raspberry Pi Pico Python SDK guide.
  • MQTT Telemetry: Use the umqtt.simple library to publish the sensor data to a local Mosquitto broker or Home Assistant instance, utilizing the Pico W's 2.4GHz WiFi radio.

Understanding the raspberry pi pico pin diagram is only the first step. True embedded reliability comes from respecting the electrical characteristics of the bus—specifically logic levels, pull-up sizing, and bus capacitance. By following the multiplexing matrix and debugging steps above, you can eliminate I2C ghost bugs and build robust RP2040 projects.