Search Note: If you landed here after typing raspberry pie pico into your search engine, don't worry—you're in the right place. The official hardware is the Raspberry Pi Pico (powered by the RP2040 chip), and this guide covers exactly how to get it talking to I2C sensors without throwing bus errors.

Building a local weather station is the rite of passage for embedded makers. But when you wire up a BME280 environmental sensor and an SSD1306 OLED display to a Raspberry Pi Pico, the I2C bus can quickly become a bottleneck of NACK errors and frozen displays. This guide walks you through a robust, production-style build using MicroPython, focusing heavily on the electrical realities of the I2C bus that most tutorials ignore.

Project Spec Sheet & Parts List

This build targets the Raspberry Pi Pico H (the variant with pre-soldered headers, saving you from SMD soldering). We are using MicroPython v1.23 (stable as of early 2026), which includes native RP2040 I2C hardware acceleration.

Difficulty Rating: Intermediate (Requires basic I2C theory and MicroPython file management)
Time to Complete: 45 minutes (wiring) + 30 minutes (coding & debugging)

Exact Bill of Materials (BOM)

  • MCU: Raspberry Pi Pico H (RP2040, 264KB SRAM, pre-soldered headers). Do not use the Pico W for this specific baseline code unless you plan to add the WiFi MQTT extension later.
  • Sensor: BME280 Breakout Board (3.3V logic variant with onboard voltage regulator and I2C pull-ups, e.g., Adafruit 2652 or generic clones with the 3V3 pin clearly marked).
  • Display: SSD1306 0.96" 128x64 OLED (I2C interface, 4-pin VCC/GND/SCL/SDA).
  • Passives: 2x 2.2kΩ resistors (for I2C pull-ups, critical for 400kHz Fast Mode).
  • Wiring: 22 AWG solid core jumper wires or silicone ribbon cable.

I2C Bus Planning & Electrical Characteristics

Before plugging in a single wire, you must understand the electrical limits of your I2C bus. The RP2040's internal I2C pull-up resistors are approximately 50kΩ. This is far too weak to pull the bus high quickly enough at 400kHz (Fast Mode), resulting in rounded signal edges and data corruption. We run this bus at 400kHz and use external 2.2kΩ pull-ups to ensure crisp square waves.

Table 1: I2C Device Characteristics & Address Map
Device Default I2C Address Max Clock Speed Setup/Hold Time Bus Capacitance Add
RP2040 (Master) N/A 1 MHz (FM+) 250ns / 100ns ~10 pF per pin
BME280 Sensor 0x76 (or 0x77) 400 kHz 250ns / 100ns ~12 pF
SSD1306 OLED 0x3C (or 0x3D) 400 kHz 100ns / 50ns ~15 pF
Total Bus Limit - - - 400 pF Max

Note: Keep your I2C wire runs under 30cm (12 inches) to stay well below the 400pF capacitance limit. For longer runs, drop the clock speed to 100kHz.

Pin Mapping & Wiring Steps

We are mapping both devices to the Pico's I2C0 bus. The RP2040 allows flexible pin mapping, but sticking to the default I2C0 pins simplifies troubleshooting.

Pinout Table

Pico Pin (GPIO) Function Connects To
Pin 1 (GP0)I2C0 SDABME280 SDA & OLED SDA
Pin 2 (GP1)I2C0 SCLBME280 SCL & OLED SCL
Pin 36 (3V3)Power OutBME280 VIN & OLED VCC
Pin 38 (GND)GroundBME280 GND & OLED GND

Numbered Wiring Procedure

  1. De-energize the bus: Ensure the Pico is unplugged from your PC before wiring.
  2. Wire Power and Ground: Connect Pin 36 (3V3) to the positive rail and Pin 38 (GND) to the negative rail. Warning: Do not connect the BME280 VCC to 5V (VBUS). The RP2040 GPIO pins are strictly 3.3V tolerant. A 5V logic high will permanently damage the SDA/SCL input buffers.
  3. Wire Data Lines: Connect GP0 to the SDA lines of both modules. Connect GP1 to the SCL lines.
  4. Install Pull-ups: Insert a 2.2kΩ resistor between the 3V3 rail and the SDA line. Insert a second 2.2kΩ resistor between the 3V3 rail and the SCL line. (If using an Adafruit BME280 breakout, it has 10kΩ pull-ups onboard; adding 2.2kΩ external pull-ups puts the parallel resistance at ~1.8kΩ, which is perfect for 400kHz).
  5. Verify Connections: Use a multimeter in continuity mode to ensure SDA is not shorted to SCL, and neither is shorted to 3V3 or GND.

Complete MicroPython Code

This code targets the Raspberry Pi Pico / Pico H. It requires the ssd1306.py and bme280.py MicroPython libraries to be uploaded to the Pico's root directory via Thonny IDE. The script includes explicit error handling for I2C initialization and memory management via the garbage collector.

import machine
import ssd1306
import bme280
import time
import gc

# --- Pin Definitions & I2C Config ---
I2C_SDA = 0  # GP0
I2C_SCL = 1  # GP1
I2C_FREQ = 400000  # 400kHz Fast Mode

# Device Addresses (Verify with i2c.scan() if these fail)
BME_ADDR = 0x76
OLED_ADDR = 0x3C
OLED_WIDTH = 128
OLED_HEIGHT = 64

def init_i2c():
    try:
        i2c = machine.I2C(0, sda=machine.Pin(I2C_SDA), scl=machine.Pin(I2C_SCL), freq=I2C_FREQ)
        devices = i2c.scan()
        if not devices:
            raise RuntimeError("I2C scan returned empty. Check wiring and pull-ups.")
        print(f"I2C Devices found: {[hex(d) for d in devices]}")
        return i2c
    except Exception as e:
        print(f"I2C Init Failed: {e}")
        return None

def main():
    i2c = init_i2c()
    if not i2c:
        return

    # Initialize OLED
    try:
        oled = ssd1306.SSD1306_I2C(OLED_WIDTH, OLED_HEIGHT, i2c, addr=OLED_ADDR)
    except OSError:
        print(f"OLED not found at {hex(OLED_ADDR)}. Check address.")
        return

    # Initialize BME280
    try:
        bme = bme280.BME280(i2c=i2c, address=BME_ADDR)
    except OSError:
        print(f"BME280 not found at {hex(BME_ADDR)}. Check address.")
        return

    print("System Initialized. Starting loop...")
    
    while True:
        # Force garbage collection to prevent memory allocation errors
        gc.collect()
        
        try:
            # Read sensor data
            temp_c = bme.temperature[:-1]  # Strip 'C' character
            hum = bme.humidity[:-1]        # Strip '%' character
            pres = bme.pressure[:-3]       # Strip 'hPa' characters
            
            # Update OLED
            oled.fill(0)  # Clear screen
            oled.text("Weather Station", 0, 0)
            oled.text(f"Temp: {temp_c} C", 0, 20)
            oled.text(f"Hum:  {hum} %", 0, 35)
            oled.text(f"Pres: {pres} hPa", 0, 50)
            oled.show()
            
            # Print to UART for serial monitor
            print(f"T:{temp_c}C | H:{hum}% | P:{pres}hPa")
            
        except OSError as e:
            print(f"Sensor read error: {e}. Re-initializing I2C...")
            i2c = init_i2c()
            if not i2c:
                break
                
        time.sleep(2)

if __name__ == "__main__":
    main()

Debugging: "OSError: [Errno 5] EIO"

The most common failure mode when running this code is the bus locking up or throwing an I/O error. If your Thonny console spits out the following exact error string:

OSError: [Errno 5] EIO

This is MicroPython's translation of an I2C NACK (Negative Acknowledge). The RP2040 sent a byte, but the slave device did not pull the SDA line low to acknowledge it. Here are the first three things to check when it fails, ranked by probability:

  1. Logic Level Mismatch (Most Common): You wired the BME280 VCC to Pin 40 (5V VBUS) instead of Pin 36 (3V3). The sensor is outputting 5V logic, which the Pico's 3.3V GPIO protection diodes are clamping, corrupting the signal. Fix: Move VCC to 3V3 immediately.
  2. Missing or Weak Pull-up Resistors: At 400kHz, the bus capacitance prevents the internal 50kΩ pull-ups from pulling the line high fast enough. The receiver reads a '0' when it should be a '1'. Fix: Verify your external 2.2kΩ resistors are physically connected between 3V3 and SDA/SCL.
  3. Swapped SDA/SCL Lines: I2C is not symmetric. If GP0 (SDA) is wired to the sensor's SCL pin, the master will clock data onto the sensor's clock line, resulting in an immediate NACK. Fix: Trace GP0 to SDA and GP1 to SCL with a multimeter.

Extending or Simplifying the Build

Once you have the baseline I2C weather station running reliably, you can adapt the project to fit your specific needs.

How to Simplify (Drop the OLED)

If you are deploying this inside a sealed enclosure where a screen is useless, remove the SSD1306 entirely. This reduces the I2C bus capacitance by ~15pF and frees up 1024 bytes of SRAM (the OLED frame buffer). Simply delete the ssd1306 import and the oled.show() calls, relying entirely on the UART print() statements to log data to your PC or a secondary microcontroller.

How to Extend (Add WiFi via Pico W)

To push this data to the cloud, swap the Pico H for a Raspberry Pi Pico W. The Pico W uses the exact same RP2040 pinout for GP0 and GP1, meaning your I2C wiring remains untouched. You will need to import the network and umqtt.simple libraries to connect to your local WiFi and publish the temp_c, hum, and pres variables to an MQTT broker like Mosquitto or Adafruit IO. Ensure you add a 100µF decoupling capacitor across the 5V and GND rails near the Pico W, as WiFi transmission spikes can cause brownouts that reset the I2C bus.

For deeper technical specifications on the RP2040's I2C peripheral timing, refer to the official Raspberry Pi Pico Datasheet. For sensor-specific oversampling configurations, consult the Bosch BME280 documentation, and for language-specific I2C methods, check the MicroPython RP2 Quick Reference.