If you arrived here searching for "raspberry pi picp" pinouts or datasheets, you are looking for the Raspberry Pi Pico (built on the RP2040 chip). "PiCP" is a common search typo, but the hardware and I2C rules remain exactly the same. To build a reliable I2C environmental sensor node on the Pico, use the RP2040-based Pico W, wire SDA to GPIO4 (Pin 6) and SCL to GPIO5 (Pin 7), and ensure 4.7kΩ pull-up resistors are present on the bus.

This guide targets the Raspberry Pi Pico W running MicroPython v1.22.2. We will wire an AHT20 temperature and humidity sensor, write a self-contained I2C script with raw byte-level error handling, and systematically debug the most common I2C bus failures.

Build Profile:
⏱️ Time: 25 minutes | 💰 Cost: ~$11 USD | 🧠 Difficulty: 2/5 (Intermediate Beginner)

I2C Sensor Selection and Component Specifications

Before wiring, you need to select the right sensor for your environment. The I2C bus on the RP2040 is highly capable, but sensor choice dictates your accuracy, power draw, and code complexity. Below is a data-dense comparison of the most common 3.3V I2C environmental sensors used in Pico builds in 2026.

Sensor Module I2C Address Temp Accuracy Humidity Accuracy Typical Price (2026) Best Use Case
AHT20 (Adafruit 4566) 0x38 ±0.3°C ±2% RH $4.50 Indoor HVAC, general room monitoring
BME280 (Adafruit 2652) 0x77 / 0x76 ±1.0°C ±3% RH $9.95 Weather stations (includes barometric pressure)
SHT40 (Adafruit 4885) 0x44 ±0.2°C ±1.8% RH $6.50 High-precision incubators, server rooms
HTU21D (Generic Breakout) 0x40 ±0.3°C ±2% RH $3.00 Budget builds, legacy codebases

Exact Parts List for This Build

  • Microcontroller: Raspberry Pi Pico W (with pre-soldered headers) - ~$6.00
  • Sensor: Adafruit AHT20 Temperature & Humidity Sensor Breakout (Product ID: 4566) - ~$4.50
  • Resistors: 2x 4.7kΩ through-hole resistors (for I2C pull-ups, if your breakout lacks them. Note: The Adafruit 4566 includes onboard pull-ups, so these are optional but good for bench testing).
  • Wiring: 4x male-to-male jumper wires, half-size breadboard.

Pin Mapping and Wiring Procedure

The RP2040 chip allows I2C multiplexing across several pins, but sticking to the default I2C0 bus prevents software configuration headaches. According to the official Pico W datasheet, GPIO4 and GPIO5 are the default pins for I2C0.

Pico W Pin (Physical) GPIO Number Function AHT20 Breakout Pin
Pin 36 N/A 3V3 OUT (Power) VIN / VCC
Pin 38 N/A GND (Ground) GND
Pin 6 GPIO 4 I2C0 SDA (Data) SDA
Pin 7 GPIO 5 I2C0 SCL (Clock) SCL

Numbered Wiring Steps

  1. De-energize the board: Ensure the Pico W is unplugged from your PC or USB power supply before wiring.
  2. Connect Power: Run a jumper from Pico Pin 36 (3V3) to the AHT20 VIN pin. Warning: Do not use Pin 40 (VBUS/5V). The AHT20 is a 3.3V logic device; feeding it 5V will permanently destroy the sensor's internal CMOS.
  3. Connect Ground: Run a jumper from Pico Pin 38 (GND) to the AHT20 GND pin.
  4. Connect I2C Data (SDA): Wire Pico Pin 6 (GPIO4) to the sensor SDA pin.
  5. Connect I2C Clock (SCL): Wire Pico Pin 7 (GPIO5) to the sensor SCL pin.
  6. Verify Pull-ups: If using a generic, unbranded breakout board, insert a 4.7kΩ resistor between SDA and 3V3, and another between SCL and 3V3. The I2C bus uses open-drain architecture; without pull-ups, the bus will float and fail to register logic HIGHs.

Complete MicroPython Code with Error Handling

Many tutorials rely on external .py libraries that hide the I2C mechanics. For robust debugging, we will write a self-contained script that communicates directly with the AHT20 via raw I2C byte commands. This code targets MicroPython v1.22+ on the Pico W.

Save the following as main.py on your Pico W. It includes an automatic I2C bus scanner that triggers if the sensor fails to respond, saving you from blind troubleshooting.


import machine
import time
import sys

# --- PIN DEFINITIONS ---
SDA_PIN = 4
SCL_PIN = 5
I2C_FREQ = 400000  # 400kHz Fast Mode
SENSOR_ADDR = 0x38  # AHT20 default I2C address

# Initialize I2C0 bus
i2c = machine.I2C(0, sda=machine.Pin(SDA_PIN), scl=machine.Pin(SCL_PIN), freq=I2C_FREQ)

def scan_i2c_bus():
    """Fallback function to identify connected devices if main sensor fails."""
    print("[DEBUG] Scanning I2C bus...")
    devices = i2c.scan()
    if not devices:
        print("[ERROR] No I2C devices found. Check wiring, pull-ups, and 3.3V power.")
    else:
        for dev in devices:
            print(f"[DEBUG] Found device at hex address: {hex(dev)}")

def read_aht20():
    """Reads temperature and humidity via raw I2C byte manipulation."""
    try:
        # 1. Initialize sensor (send 0xBE, 0x08, 0x00)
        i2c.writeto(SENSOR_ADDR, b'\xBE\x08\x00')
        time.sleep_ms(10)
        
        # 2. Trigger measurement (send 0xAC, 0x33, 0x00)
        i2c.writeto(SENSOR_ADDR, b'\xAC\x33\x00')
        time.sleep_ms(80) # Wait for measurement to complete
        
        # 3. Read 7 bytes of data
        data = i2c.readfrom(SENSOR_ADDR, 7)
        
        # 4. Check busy bit (Bit 7 of status byte)
        timeout = 0
        while (data[0] & 0x80) != 0:
            time.sleep_ms(10)
            data = i2c.readfrom(SENSOR_ADDR, 7)
            timeout += 1
            if timeout > 20:
                raise RuntimeError("Sensor busy timeout")
                
        # 5. Parse raw bytes into physical values
        raw_hum = ((data[1] << 12) | (data[2] << 4) | (data[3] >> 4))
        raw_temp = (((data[3] & 0x0F) << 16) | (data[4] << 8) | data[5])
        
        humidity = (raw_hum / 1048576.0) * 100.0
        temperature = (raw_temp / 1048576.0) * 200.0 - 50.0
        
        return temperature, humidity
        
    except OSError as e:
        # Catch the exact MicroPython I2C NACK error
        print(f"[CRITICAL] I2C Bus Failure: {e}")
        scan_i2c_bus()
        sys.exit(1)
    except Exception as e:
        print(f"[ERROR] Unexpected failure: {e}")
        sys.exit(1)

# --- MAIN EXECUTION LOOP ---
print(f"Booting I2C Node on Pico W | SDA:GPIO{SDA_PIN} SCL:GPIO{SCL_PIN}")
scan_i2c_bus() # Initial verification

while True:
    temp_c, hum_rh = read_aht20()
    print(f"Temp: {temp_c:.2f} C | Humidity: {hum_rh:.2f} %")
    time.sleep(2)

Debugging: "OSError: [Errno 5] EIO" and Bus Failures

When working with I2C on the RP2040, the most common roadblock is the bus locking up or refusing to acknowledge the sensor. If your script crashes, you will likely see this exact error string in the Thonny or PuTTY REPL:

OSError: [Errno 5] EIO

In MicroPython, EIO (Error Input/Output) on an I2C call means the Pico sent the address byte, but the sensor did not pull the SDA line low to send an ACK (Acknowledge) bit.

The First Three Things to Check

Before rewriting your code or blaming a dead sensor, check these three physical layer issues:

  1. Measure the Pull-up Voltage: Set your multimeter to DC Voltage. Probe the SDA and SCL lines relative to GND while the bus is idle. You must read ~3.3V. If you read 0V or a floating value (like 0.8V), your pull-up resistors are missing or broken.
  2. Verify SDA/SCL Swap: It is incredibly easy to swap GPIO4 and GPIO5 on the breadboard. Double-check that SDA is on Pin 6 and SCL is on Pin 7. The I2C protocol is not auto-reversing.
  3. Confirm the Logic Level: If you accidentally powered the AHT20 from the 5V VBUS pin (Pin 40), the sensor's internal protection diodes may have clamped the I2C lines to 5V, confusing the 3.3V RP2040 GPIOs. Always use Pin 36 (3V3).

Ranked Causes for EIO Errors

Rank Root Cause Diagnostic Step Fix
1 Missing Pull-up Resistors Multimeter reads < 3.0V on idle SDA/SCL Add 4.7kΩ resistors to 3.3V
2 Incorrect I2C Address Run i2c.scan(); returns empty list Check sensor datasheet; AHT20 is 0x38, not 0x40
3 Bus Capacitance Too High Works on short wires, fails on >1 meter cables Drop I2C freq to 100kHz or use an I2C bus extender (PCA9600)
4 SDA/SCL Shorted to Ground Multimeter continuity test beeps between SDA and GND Inspect breadboard for stray wire strands or solder bridges
Pro-Tip for RP2040 I2C Lockups: If the Pico completely locks up and i2c.scan() throws an error even after a soft reset, the I2C peripheral state machine is jammed. You must perform a hard power cycle (unplug the USB cable entirely for 5 seconds) or call machine.reset() in your REPL to clear the hardware registers.

Extending and Simplifying the Build

Once you have verified the raw I2C communication is stable, you can adapt this hardware node for production or simplify it for rapid prototyping.

How to Extend the Build

  • Add MQTT over WiFi: Because we specified the Pico W variant, you have access to the CYW43439 WiFi chip. Import the network and umqtt.simple libraries to push the temp_c and hum_rh variables to a Home Assistant MQTT broker every 60 seconds.
  • Implement Deep Sleep: For battery-powered deployments, replace the time.sleep(2) loop with machine.lightsleep(). The RP2040 doesn't have a true hardware deep sleep like the ESP32, but dropping the CPU clock and disabling the WiFi radio between reads will reduce idle current from ~60mA down to ~1.5mA, extending a 2000mAh LiPo pack from 1 day to over 3 weeks.

How to Simplify the Build

  • Switch to Qwiic / STEMMA QT: If you hate breadboards and jumper wires, buy the Pico W with a Qwiic connector soldered on (or use an Adafruit Qwiic SHIM). This allows you to daisy-chain I2C sensors using keyed 4-pin JST cables, entirely eliminating wiring errors and guaranteeing the presence of pull-up resistors on the breakout boards.
  • Use an I2C Multiplexer: If you need to read three AHT20 sensors (which all share the hardcoded 0x38 address), you cannot put them on the same bus. Instead of bit-banging software I2C on random GPIOs, use a TCA9548A I2C Multiplexer. It sits on the main bus at 0x70 and allows you to route the Pico's I2C0 signals to 8 separate downstream channels, letting you read multiple identical sensors without address conflicts.

For deeper reading on the I2C standard and timing requirements, refer to the NXP I2C-bus specification (UM10204), and for MicroPython-specific hardware APIs, consult the official machine.I2C documentation.