The Raspberry Pi Pico 2.0 (powered by the RP2350 chip) is a massive leap over the original RP2040. With dual Arm Cortex-M33 cores running at 150MHz, 520KB of SRAM, and a hardware security architecture, it is built for concurrent processing. However, moving from single-core to dual-core embedded programming introduces new failure modes, particularly around shared I2C bus access and thread synchronization.

This guide walks through building a robust, dual-core I2C environmental data logger using the Pico 2.0 and a BME280 sensor. Core 0 handles the hardware I2C polling and bus recovery, while Core 1 formats the CSV data and manages the USB serial output. We will also tackle the most common I2C failure in MicroPython and provide exact recovery routines.

Project Spec Sheet & Exact Parts List

Difficulty Rating: Intermediate (Requires basic MicroPython and multithreading knowledge)
Estimated Build Time: 45 minutes
Target Board Variant: Raspberry Pi Pico 2 (Base model, RP2350 Arm Cortex-M33 variant, not the RISC-V Hazard3 variant for maximum MicroPython library compatibility in 2026).
Component Exact Model / Variant Notes & Bench Tips
Microcontroller Raspberry Pi Pico 2 (RP2350) Ensure you download the micropython-rp2350-arm UF2 file, not the RISC-V build.
Sensor BME280 (I2C Breakout) Must be a 3.3V logic variant. 5V modules without level shifters will damage the RP2350 GPIOs.
Wiring 24 AWG Solid Core Jumper Wires Keep I2C runs under 30cm to avoid capacitance-induced clock stretching.
Resistors 4.7kΩ (x2) Required for I2C pull-ups if your BME280 breakout lacks them.

Pin Mapping & Hardware Setup

The RP2350 maintains the same 40-pin footprint as the original Pico, but the internal multiplexing is more flexible. We are using I2C0 on GPIO 4 and GPIO 5.

Pico 2.0 Pin GPIO Number BME280 Pin Wire Color
Pin 6 (GP4)GPIO 4 (SDA)SDI / SDABlue
Pin 7 (GP5)GPIO 5 (SCL)SCK / SCLYellow
Pin 36 (3V3)3.3V PowerVIN / VCCRed
Pin 38 (GND)GroundGNDBlack

Wiring Steps:

  1. De-energize the board: Unplug the USB-C cable before wiring. The RP2350 is highly sensitive to electrostatic discharge (ESD) on unconfigured pins.
  2. Connect Power and Ground: Route 3.3V and GND to the BME280 breakout. Never connect a 5V VCC line to this sensor when using the Pico 2.0.
  3. Wire the I2C Bus: Connect GP4 to SDA and GP5 to SCL.
  4. Verify Pull-ups: Use a multimeter in continuity/resistance mode to check between SDA/SCL and 3.3V. If you do not read approximately 4.7kΩ, your breakout board lacks internal pull-ups and you must add external 4.7kΩ resistors. The RP2350 internal pull-ups (~50kΩ) are too weak for reliable 400kHz I2C communication.

Dual-Core MicroPython Implementation

This firmware targets the Raspberry Pi Pico 2 (RP2350 Arm variant). It utilizes the _thread module to split tasks. Core 0 acts as the hardware abstraction layer (HAL), polling the sensor and handling hardware watchdog resets. Core 1 acts as the application layer, formatting the data and printing to the USB serial monitor.

Note: Ensure you have the bme280 MicroPython library installed on your Pico 2.0 filesystem (e.g., via Thonny or mpremote).


import machine
import utime
import _thread
import bme280

# --- PIN DEFINITIONS ---
I2C_SDA = machine.Pin(4)
I2C_SCL = machine.Pin(5)
LED_PIN = machine.Pin(25, machine.Pin.OUT) # Pico 2 onboard LED

# --- HARDWARE SETUP ---
# Initialize I2C0 at 400kHz
i2c = machine.I2C(0, sda=I2C_SDA, scl=I2C_SCL, freq=400000)

# Initialize Hardware Watchdog Timer (8 second timeout)
# RP2350 WDT requires explicit feeding to prevent system reset
wdt = machine.WDT(timeout=8000)

# Thread synchronization lock
sensor_lock = _thread.allocate_lock()
shared_data = {'temp': 0.0, 'hum': 0.0, 'press': 0.0, 'ready': False}

# --- I2C BUS RECOVERY ROUTINE ---
def recover_i2c_bus():
    """Bit-bangs SCL to release a stuck SDA line from a locked slave."""
    print('[Core 0] Attempting I2C bus recovery...')
    scl_pin = machine.Pin(5, machine.Pin.OUT)
    for _ in range(9):
        scl_pin.value(0)
        utime.sleep_ms(1)
        scl_pin.value(1)
        utime.sleep_ms(1)
    # Reinitialize I2C peripheral
    global i2c
    i2c = machine.I2C(0, sda=machine.Pin(4), scl=machine.Pin(5), freq=400000)

# --- CORE 0: HARDWARE POLLING ---
def core0_sensor_task():
    bme = None
    while True:
        try:
            if bme is None:
                bme = bme280.BME280(i2c=i2c)
            
            temp = bme.temperature
            hum = bme.humidity
            press = bme.pressure
            
            with sensor_lock:
                shared_data['temp'] = temp
                shared_data['hum'] = hum
                shared_data['press'] = press
                shared_data['ready'] = True
                
            LED_PIN.toggle()
            wdt.feed() # Feed the watchdog
            utime.sleep(2)
            
        except OSError as e:
            print(f'[Core 0] I2C Error: {e}')
            bme = None
            recover_i2c_bus()
            utime.sleep(1)

# --- CORE 1: DATA FORMATTING & OUTPUT ---
def core1_serial_task():
    print('[Core 1] Serial logging started.')
    print('Timestamp,Temp_C,Humidity_%,Pressure_hPa')
    while True:
        with sensor_lock:
            if shared_data['ready']:
                t = shared_data['temp']
                h = shared_data['hum']
                p = shared_data['press']
                shared_data['ready'] = False
                
                # Format and print CSV
                timestamp = utime.ticks_ms()
                print(f'{timestamp},{t},{h},{p}')
        utime.sleep_ms(500)

# --- MAIN EXECUTION ---
if __name__ == '__main__':
    # Scan I2C bus before starting threads
    devices = i2c.scan()
    if not devices:
        raise RuntimeError('No I2C devices found. Check wiring and pull-ups.')
    
    # Launch Core 1
    _thread.start_new_thread(core1_serial_task, ())
    
    # Run Core 0 task on main thread
    core0_sensor_task()

Debugging I2C Lockups: Fixing 'OSError: [Errno 5] EIO'

When working with I2C on the RP2350, the most frequent showstopper is the bus lockup. If your serial monitor spits out the exact error string OSError: [Errno 5] EIO, your I2C peripheral has lost synchronization with the slave device.

Ranked Causes of Errno 5

  1. SDA Line Stuck Low: The BME280 was interrupted mid-byte (often by a loose wire or a software reset) and is holding the SDA line low, preventing the RP2350 from generating a START condition.
  2. Missing or Weak Pull-up Resistors: The I2C spec requires open-drain lines pulled high. Relying solely on the Pico 2.0 internal pull-ups will cause signal rise times to fail at 400kHz.
  3. Logic Level Mismatch: Using a 5V sensor module without a bidirectional logic level shifter. The RP2350 GPIOs are strictly 3.3V tolerant.

The First Three Things to Check When It Fails

Before rewriting your code, grab your multimeter and verify these three physical layer conditions:

  1. Check the Pull-up Voltage: Measure DC voltage between SDA and GND, and SCL and GND. Both should read a stable 3.2V to 3.3V when the bus is idle. If it reads ~1.5V or fluctuates wildly, your pull-ups are missing or the wrong value.
  2. Verify the I2C Address: Run a basic i2c.scan() script. The BME280 should return [118] (0x76) or [119] (0x77). If it returns an empty list [], the hardware connection is broken.
  3. Inspect for Ground Loops: Ensure the GND pin on the Pico 2.0 and the GND pin on the sensor breakout are tied together directly. A floating ground reference will corrupt the I2C clock edges.

For deeper architectural details on the RP2350 I2C peripheral state machine, refer to the official Raspberry Pi Pico hardware documentation.

Extending and Simplifying the Build

How to Simplify (Single-Core Mode):
If you do not need concurrent USB serial streaming and sensor polling, strip out the _thread module entirely. Move the bme280 read logic directly into a single while True: loop and use utime.sleep(2). This eliminates the need for sensor_lock, reduces SRAM overhead by roughly 4KB (the default thread stack size), and makes debugging tracebacks significantly easier.

How to Extend (Wireless MQTT Logging):
To push this data to a home automation dashboard, swap the base Pico 2 for the Pico 2 W. You can assign Core 0 to handle the BME280 and the local OLED display, while Core 1 manages the WiFi radio and MQTT publishing via the umqtt.simple library. The RP2350's 520KB SRAM easily accommodates the TLS buffers required for secure MQTT over WiFi without triggering out-of-memory panics.

Raspberry Pi Pico 2.0 FAQ

Is the Pico 2.0 pinout identical to the original Pico?

Physically, yes. The Pico 2.0 retains the exact same 40-pin DIP footprint and castellated pads as the original RP2040 Pico, making it a drop-in replacement for existing carrier boards. However, logically, the RP2350 exposes more ADC channels (up to 4 dedicated ADC pins plus a temperature sensor) and introduces the HSTX (High-Speed Serial Transmit) peripheral, which allows for DVI video output without consuming PIO state machines.

Should I choose the Arm or RISC-V core variant for MicroPython?

For MicroPython development in 2026, you should select the Arm Cortex-M33 variant. While the RP2350 uniquely supports dual RISC-V Hazard3 cores, the MicroPython ecosystem, third-party C-modules, and compiled .mpy files are overwhelmingly optimized for the Arm Thumb instruction set. The RISC-V variant is excellent for bare-metal C/C++ development using the Pico SDK, but Arm remains the path of least resistance for Python-based embedded projects.

Why does my Pico 2.0 get warm when running dual-core code?

It is entirely normal for the RP2350 silicon to reach 35°C to 45°C (warm to the touch) when both Cortex-M33 cores are active at 150MHz. The dual-core architecture draws significantly more dynamic current than the single-core idle states of the RP2040. As long as the board is not too hot to keep your finger on (which would indicate >60°C and a potential short circuit or linear regulator failure), the temperature is within the manufacturer's specified operating envelope. If thermals are a concern in an enclosed 3D-printed case, drop the system clock to 125MHz using machine.freq(125000000).