The RP2040 chip inside the Raspberry Pi Pico offers highly flexible GPIO routing, but that flexibility is exactly what causes beginners to wire things incorrectly and chase phantom bugs. If you are looking for the default, safest Raspberry Pi Pico pins for an I2C sensor bus, use GPIO 4 (SDA) and GPIO 5 (SCL) for I2C0. These pins avoid the internal WiFi chip conflicts found on the Pico W and map cleanly to standard breadboard layouts.
Below is a complete, decision-forward guide to selecting your pins, wiring a BME280 environmental sensor, and debugging the most common I2C failure mode: the dreaded bus lockup.
The Quick Decision: Which Raspberry Pi Pico Pins to Use?
Do not guess your pinout. The RP2040 allows most peripherals to be mapped to multiple pins, but picking the wrong set leads to routing nightmares. Use this decision tree to lock in your hardware design before you touch a breadboard.
| If your project needs... | And your constraint is... | Then pick these exact GPIO pins | Peripheral Block |
|---|---|---|---|
| I2C Sensors (BME280, OLED) | Avoiding Pico W WiFi chip conflicts | GPIO 4 (SDA), GPIO 5 (SCL) | I2C0 |
| Secondary I2C Bus | Isolating a noisy motor controller | GPIO 6 (SDA), GPIO 7 (SCL) | I2C1 |
| SPI Display / SD Card | Standard hardware SPI routing | GPIO 16 (RX), 19 (TX), 18 (SCK), 17 (CS) | SPI0 |
| Analog Sensors (ADC) | True analog-to-digital conversion | GPIO 26, 27, 28 (ADC0-2) | ADC |
| UART Serial (GPS/Cell) | Default USB-to-Serial bridge mapping | GPIO 0 (TX), GPIO 1 (RX) | UART0 |
Parts List & Board Variant Specifications
This build targets the Raspberry Pi Pico W (with pre-soldered headers). While the RP2040 GPIO layout is identical to the original Pico, the Pico W is the current standard for IoT builds, and knowing its specific internal pin reservations is critical for modern embedded work.
- Microcontroller: Raspberry Pi Pico W (RP2040 + CYW43439) with headers — ~$6.00
- Sensor: Adafruit BME280 I2C/SPI Breakout (Product ID: 2652) — ~$19.95 (Includes onboard 3.3V regulator and 10kΩ pull-ups)
- Display (Optional): Generic SSD1306 128x64 I2C OLED (0.96") — ~$8.00
- Wiring: 22 AWG solid core jumper wires, 40-pin male-to-male
- Resistors: 4.7kΩ pull-up resistors (Only required if using bare sensor modules without integrated pull-ups)
Pin Mapping & Wiring the I2C Sensor Hub
When wiring multiple devices to the same I2C bus, they share the SDA and SCL lines. The BME280 and SSD1306 both operate at 3.3V logic, which perfectly matches the Pico's native GPIO voltage. Do not use 5V I2C devices without a level shifter; the RP2040 GPIOs are not 5V tolerant and will suffer permanent silicon damage.
| Raspberry Pi Pico W Pin | Function | BME280 Breakout Pin | SSD1306 OLED Pin |
|---|---|---|---|
| Pin 36 (3V3 OUT) | Power (3.3V) | VIN / VCC | VCC |
| Pin 38 (GND) | Ground | GND | GND |
| Pin 6 (GPIO 4) | I2C0 SDA | SDI / SDA | SDA |
| Pin 7 (GPIO 5) | I2C0 SCL | SCK / SCL | SCL |
Note on I2C Addresses: The BME280 default I2C address is usually 0x77 (Adafruit) or 0x76 (generic eBay modules). The SSD1306 is typically 0x3C. Always verify with an I2C scanner script before writing your main application logic.
Bare-Metal MicroPython Code with I2C Error Handling
Beginners often copy-paste heavy library code that fails silently or throws cryptic tracebacks. The code below is a complete, compilable MicroPython script that bypasses third-party libraries to perform a bare-metal register read on the BME280. It reads the Chip ID register (0xD0) to physically verify the sensor is on the bus and responding, complete with explicit error handling for I2C bus lockups.
Target Board: Raspberry Pi Pico / Pico W running MicroPython v1.22+.
import machine
import time
# ==========================================
# PIN DEFINITIONS (I2C0 Block)
# ==========================================
I2C0_SDA_PIN = 4
I2C0_SCL_PIN = 5
I2C_FREQ = 400000 # 400kHz Fast Mode
# Sensor Addresses (Check your specific breakout)
BME280_ADDR = 0x77 # Use 0x76 for generic SparkFun/eBay clones
BME280_CHIP_ID_REG = 0xD0
EXPECTED_CHIP_ID = 0x60
# ==========================================
# I2C INITIALIZATION
# ==========================================
sda_pin = machine.Pin(I2C0_SDA_PIN)
scl_pin = machine.Pin(I2C0_SCL_PIN)
i2c = machine.I2C(0, sda=sda_pin, scl=scl_pin, freq=I2C_FREQ)
def scan_bus():
print('Scanning I2C bus...')
devices = i2c.scan()
if not devices:
print('ERROR: No I2C devices found. Check wiring and pull-ups.')
else:
for addr in devices:
print(f'Found device at: {hex(addr)}')
def verify_bme280():
try:
# Read 1 byte from the Chip ID register
chip_id = i2c.readfrom_mem(BME280_ADDR, BME280_CHIP_ID_REG, 1)
if chip_id[0] == EXPECTED_CHIP_ID:
print(f'Success: BME280 verified at {hex(BME280_ADDR)}')
return True
else:
print(f'Warning: Device responded, but unexpected ID: {hex(chip_id[0])}')
return False
except OSError as e:
# Explicitly catch the I2C NACK / Bus Lockup error
if len(e.args) > 0 and e.args[0] == 121:
print('CRITICAL: OSError: [Errno 121] EIO')
print('The sensor NACKed the address or the bus is locked up.')
else:
print(f'Unhandled I2C OSError: {e}')
return False
except Exception as e:
print(f'Unexpected Error: {e}')
return False
# ==========================================
# MAIN EXECUTION LOOP
# ==========================================
scan_bus()
if verify_bme280():
print('Sensor is healthy. Proceeding to main data loop...')
while True:
# Insert full BME280 compensation math or library call here
time.sleep(2)
else:
print('Halt: Fix hardware wiring before proceeding.')
Debugging: "OSError: [Errno 121] EIO" and Bus Lockups
If your terminal spits out OSError: [Errno 121] EIO, your MicroPython environment is telling you that the I2C controller sent a byte, but the target device refused to acknowledge it (NACK), or the SDA line is being held low by a confused peripheral. This is the most common failure mode in embedded I2C.
The First Three Things to Check
- Are pull-up resistors present? I2C is an open-drain protocol. The Pico's internal pull-ups (usually ~50kΩ) are far too weak for reliable bus communication at 400kHz. You must have 4.7kΩ resistors pulling SDA and SCL to 3.3V. (Note: Adafruit and SparkFun breakouts include these; cheap bare modules do not).
- Did you swap SDA and SCL? The RP2040 pinout silkscreen on the bottom of the Pico can be confusing. Double-check that GPIO 4 is physically wired to the sensor's SDA, and GPIO 5 to SCL.
- Is the address correct? Run the
scan_bus()function from the code above. If the scanner returns an empty list, you have a hardware fault (power, ground, or missing pull-ups). If it returns0x76but your code targets0x77, update theBME280_ADDRvariable.
Ranked Causes for Persistent EIO Errors
| Rank | Cause | Verification & Fix |
|---|---|---|
| 1 | Missing or incorrect pull-up resistors | Measure resistance from SDA to 3.3V with a multimeter. Should read ~4.7kΩ. Solder external resistors if missing. |
| 2 | SDA/SCL crossed or broken jumper wire | Swap the two wires at the sensor end. Use a multimeter in continuity mode to verify the breadboard rails aren't internally split. |
| 3 | Pico W reserved pin conflict | Ensure you are not using GPIO 23, 24, 25, or 29. Move to GPIO 4/5. |
| 4 | Sensor brownout / power starvation | Measure the 3V3 pin under load. If it drops below 3.1V, power the Pico via the VSYS pin with a robust 5V supply, not a weak USB port. |
Extending or Simplifying the Build
Embedded design is about matching the complexity of the firmware to the reality of the hardware. Here is how to adjust this build based on your bench constraints.
How to Simplify (The "Just Make It Work" Path)
If you are struggling with I2C bus lockups and just need temperature data for a science project, abandon I2C and use a 1-Wire DS18B20 sensor. The DS18B20 requires only one data pin (e.g., GPIO 15), a 4.7kΩ pull-up, and is virtually immune to the bus-lockup issues that plague multi-device I2C setups. You will lose humidity and barometric pressure data, but you will gain absolute bus stability.
How to Extend (The Production IoT Path)
To scale this into a robust weather station:
- Add Hardware SPI for the Display: Move the SSD1306 OLED from the shared I2C bus to the SPI0 block (GPIO 16, 17, 18, 19). I2C is too slow for smooth display rendering, and sharing the bus with a display can cause timing delays that starve the BME280 of read cycles.
- Implement a Watchdog Timer: Use the RP2040's hardware watchdog (
machine.WDT) to automatically reboot the Pico if the I2C bus hard-locks during a lightning storm or ESD event. Relying solely on softwaretry/exceptblocks won't save you if the silicon I2C state machine itself freezes. - Use Interrupts for Rain Gauges: If adding a tipping-bucket rain gauge, wire it to GPIO 14 and use
machine.Pin.irq()to count tips. Never poll a mechanical switch in your mainwhileloop; you will miss tips while the CPU is busy writing to the OLED.
For authoritative reference on the RP2040's internal routing and peripheral multiplexing, always consult the official RP2040 Datasheet, specifically Chapter 1 (Table 2: GPIO Functions). For MicroPython-specific I2C implementation details and bus scanning methods, refer to the MicroPython machine.I2C documentation.






