The Raspberry Pico 2, built on the RP2350 chip, fundamentally changes the math for embedded data logging. Unlike its predecessor, the RP2350 offers dual Arm Cortex-M33 (or RISC-V) cores running at 150MHz, 520KB of SRAM, and a hardware security enclave. When you need to poll high-speed I2C sensors while simultaneously writing to an SPI SD card, the original Pico’s single-core bottleneck and 264KB SRAM limit often result in dropped samples. The Pico 2 solves this by letting you dedicate Core 0 to sensor acquisition and display rendering, while Core 1 handles blocking SPI file I/O.
This guide walks through building a dual-core environmental logger. We will cover the exact hardware variants, the critical RP2350 pin allocations, and the MicroPython firmware required to run it. More importantly, we will cover the specific hardware and software faults that cause this build to fail on the bench, and exactly how to fix them.
Project Specs and Raspberry Pico 2 Pin Mapping
Before wiring anything, we need to lock in the peripheral allocation. The RP2350 has more GPIOs (48 total, with 30 exposed on the standard Pico 2 board) and more flexible peripheral mapping than the RP2040. However, not all pins are equal. For instance, GP0 and GP1 share routing with the USB interface; using them for high-frequency I2C can cause USB enumeration glitches if you are powering the board via USB while coding.
Required Parts List
- MCU: Raspberry Pi Pico 2 (RP2350) with pre-soldered headers (Part # SC1103 or equivalent).
- Sensor: BME280 Breakout Board (I2C variant, 3.3V logic, ensure it has onboard voltage regulation and pull-ups if buying generic).
- Display: SSD1306 128x64 OLED (I2C interface, 0x3C default address).
- Storage: MicroSD Card SPI Breakout (Must be a 3.3V logic level module with a proper LDO and level shifters, not the raw 5V Arduino modules).
- Passives: 2x 4.7kΩ resistors (for I2C pull-ups), 1x 100nF ceramic capacitor (for SD card VCC decoupling).
RP2350 Pin Allocation Table
This table defines the exact wiring for this project. Keep this reference on your bench.
| Board Pin | RP2350 GPIO | Function | Peripheral | Wiring Notes & Constraints |
|---|---|---|---|---|
| Pin 4 | GP4 | I2C SDA | I2C0 | Connect to BME280 & OLED SDA. Add 4.7kΩ pull-up to 3.3V. |
| Pin 5 | GP5 | I2C SCL | I2C0 | Connect to BME280 & OLED SCL. Add 4.7kΩ pull-up to 3.3V. |
| Pin 9 | GP6 | SPI SCK | SPI0 | Connect to MicroSD SCK. Keep trace under 5cm. |
| Pin 10 | GP7 | SPI MOSI | SPI0 | Connect to MicroSD MOSI (DI). |
| Pin 11 | GP8 | SPI MISO | SPI0 | Connect to MicroSD MISO (DO). |
| Pin 12 | GP9 | SPI CS | GPIO | Connect to MicroSD CS. Active LOW. |
| Pin 36 | 3V3(OUT) | Power | Internal LDO | Max 300mA draw. Powers all sensors. |
| Pin 38 | GND | Ground | Common | Star-ground all sensor modules to this pin. |
Hardware Assembly and Power Decoupling
Wiring a dual-bus system requires strict attention to ground returns and power decoupling. The SPI bus toggling at 10MHz+ will inject noise into the 3.3V rail, which can corrupt sensitive I2C ADC readings on the BME280.
- Place the Pico 2 across the center trench of a standard 830-point solderless breadboard.
- Wire the I2C Bus: Connect GP4 to the SDA rail and GP5 to the SCL rail. Run 3.3V and GND to the same rails. Plug in the BME280 and SSD1306 OLED to these shared rails.
- Install Pull-ups: Insert the two 4.7kΩ resistors between the 3.3V rail and the SDA/SCL rails respectively.
- Wire the SPI Bus: Connect GP6 (SCK), GP7 (MOSI), GP8 (MISO), and GP9 (CS) directly to the MicroSD breakout. Do not share these pins with the I2C devices.
- Decouple the SD Card: Solder or plug a 100nF ceramic capacitor directly across the VCC and GND pins on the MicroSD breakout board. SD cards draw spikes of up to 150mA during write operations; without this capacitor, the voltage dip will brownout the RP2350 or corrupt the FAT filesystem.
- Verify Voltages: Before plugging in USB, use a multimeter to check for shorts between the 3.3V and GND rails. You should read an open circuit (or high resistance due to the pull-ups), not 0Ω.
Complete MicroPython Firmware (Dual-Core)
This code targets the Raspberry Pi Pico 2 (RP2350) running MicroPython v1.24.0 or newer. It utilizes the _thread module to split tasks. Core 0 handles the I2C sensor polling and OLED rendering at 2Hz. Core 1 handles the blocking SPI SD card writes at 0.5Hz. A thread lock protects the shared data dictionary.
Note: Ensure ssd1306.py and bme280.py are uploaded to your Pico 2's root directory or lib folder before running.
import machine
import ssd1306
import bme280
import sdcard
import uos
import _thread
import time
import gc
# --- PIN DEFINITIONS ---
I2C_SDA = machine.Pin(4)
I2C_SCL = machine.Pin(5)
SPI_SCK = machine.Pin(6)
SPI_MOSI = machine.Pin(7)
SPI_MISO = machine.Pin(8)
SPI_CS = machine.Pin(9)
# --- SHARED STATE & LOCK ---
sensor_data = {"temp": 0.0, "hum": 0.0, "press": 0.0}
data_lock = _thread.allocate_lock()
log_counter = 0
# --- HARDWARE INIT ---
def init_i2c():
i2c = machine.I2C(0, sda=I2C_SDA, scl=I2C_SCL, freq=400000)
if not i2c.scan():
raise RuntimeError("I2C Scan failed. Check wiring and pull-ups.")
return i2c
def init_spi_sd():
spi = machine.SPI(0, baudrate=1000000, polarity=0, phase=0,
sck=SPI_SCK, mosi=SPI_MOSI, miso=SPI_MISO)
cs = SPI_CS
try:
sd = sdcard.SDCard(spi, cs)
uos.mount(sd, '/sd')
print("SD Card mounted successfully.")
return True
except OSError as e:
print(f"SD Mount Failed: {e}")
return False
# --- CORE 1: SPI DATA LOGGING ---
def core1_logger():
global log_counter
sd_mounted = init_spi_sd()
while True:
time.sleep(2) # Log every 2 seconds
if sd_mounted:
data_lock.acquire()
t = sensor_data["temp"]
h = sensor_data["hum"]
p = sensor_data["press"]
data_lock.release()
try:
with open('/sd/log.csv', 'a') as f:
f.write(f"{log_counter},{t},{h},{p}\n")
log_counter += 1
except OSError as e:
print(f"Core 1 Write Error: {e}")
sd_mounted = False # Disable further writes if card fails
# Force garbage collection to prevent SRAM fragmentation
gc.collect()
# --- CORE 0: I2C SENSOR & DISPLAY ---
def main():
i2c = init_i2c()
# Assuming BME280 is at 0x76, SSD1306 at 0x3C
bme = bme280.BME280(i2c=i2c, address=0x76)
oled = ssd1306.SSD1306_I2C(128, 64, i2c, addr=0x3C)
# Start Core 1
_thread.start_new_thread(core1_logger, ())
oled.fill(0)
oled.text("Pico 2 Logger", 0, 0)
oled.show()
time.sleep(1)
while True:
try:
# Read BME280 (returns tuple of strings in some libs, handle accordingly)
raw_t, raw_p, raw_h = bme.values
t_val = float(raw_t[:-1]) # Strip 'C' or 'F'
h_val = float(raw_h[:-1])
p_val = float(raw_p[:-3]) # Strip 'hPa'
data_lock.acquire()
sensor_data["temp"] = t_val
sensor_data["hum"] = h_val
sensor_data["press"] = p_val
data_lock.release()
# Update OLED
oled.fill(0)
oled.text(f"T: {t_val:.1f} C", 0, 10)
oled.text(f"H: {h_val:.1f} %", 0, 25)
oled.text(f"P: {p_val:.0f} hPa", 0, 40)
oled.text(f"Logs: {log_counter}", 0, 55)
oled.show()
except OSError as e:
print(f"Core 0 I2C Error: {e}")
oled.fill(0)
oled.text("I2C BUS ERROR", 0, 20)
oled.show()
time.sleep(0.5) # 2Hz update rate
if __name__ == "__main__":
main()
Debugging: First Three Things to Check When It Fails
Embedded debugging is rarely about the code; it is almost always about physics. If your Pico 2 boots but immediately throws errors, do not rewrite the firmware. Check these three physical layer issues first.
1. The Exact Error: OSError: [Errno 5] EIO
This is the most common error on the RP2350 I2C bus. It means the peripheral sent a NAK (Not Acknowledged) or the bus timed out waiting for the SDA line to go high.
- Cause A (Most Likely): Missing or insufficient I2C pull-up resistors. The internal RP2350 pull-ups are ~60kΩ. At 400kHz, the RC time constant with the bus capacitance prevents the line from reaching the 3.3V logic-high threshold before the clock edge. Fix: Add external 4.7kΩ resistors to 3.3V.
- Cause B: SDA and SCL swapped. The RP2040 could sometimes mask this if devices were forgiving; the RP2350 I2C state machine is stricter. Fix: Swap GP4 and GP5 wires.
- Cause C: Address mismatch. Generic BME280 breakouts often default to 0x76, but some clone boards hardwire 0x77. Fix: Run
i2c.scan()in the REPL and update theaddress=parameter in the code.
2. The Exact Error: OSError: [Errno 2] ENOENT on SD Mount
This occurs when the uos.mount() function fails to find a valid FAT filesystem on the SPI bus.
- Cause A: 5V SD Module on a 3.3V MCU. If you are using a cheap Arduino SD module without a proper logic level shifter, the RP2350's 3.3V MOSI signal won't cross the 5V module's threshold. Fix: Use a dedicated 3.3V SPI SD breakout (like those based on the Waveshare or Adafruit designs).
- Cause B: SDHC/SDXC Incompatibility. Older MicroPython
sdcard.pydrivers struggle with 64GB+ SDXC cards formatted as exFAT. Fix: Use a 16GB or 32GB microSD card formatted strictly as FAT32.
3. Silent Reboots or Core 1 Lockups
If the Pico 2 randomly disconnects from USB or Core 1 stops logging while Core 0 keeps updating the screen, you are experiencing a brownout.
- Cause: The SD card write spike (up to 150mA) combined with the OLED screen draw exceeds the Pico 2's onboard 3.3V LDO capacity, or causes a voltage dip on the USB VBUS line that resets the RP2350 brownout detector (BOR). Fix: Ensure the 100nF decoupling capacitor is soldered directly to the SD breakout. If the issue persists, power the Pico 2 via the VSYS pin with an external 5V/2A supply rather than relying on USB VBUS.
Extending or Simplifying the Build
Depending on your deployment environment, you may need to alter the hardware footprint.
How to Simplify (Bench Testing)
If you are just validating the RP2350 dual-core architecture and don't want to deal with SPI filesystem formatting, drop the SD card entirely. Comment out the init_spi_sd() call and the file write block in Core 1. Replace it with a simple print() statement to the REPL. This isolates the I2C bus and proves the thread-locking logic works without SPI variables interfering.
How to Extend (Networked IoT)
To push this data to the cloud, upgrade to the Raspberry Pi Pico 2 W (which integrates the Infineon CYW43439 WiFi/BLE chip). You will need to modify Core 1 to handle the WiFi stack. Because the CYW43439 uses a proprietary SDIO interface that consumes specific RP2350 pins and DMA channels, you must avoid using SPI0 for an SD card simultaneously without careful DMA arbitration. For networked logging, it is often cleaner to use the standard Pico 2 and wire a W5500 Ethernet SPI HAT to SPI1 (GP10-GP13), leaving the WiFi stack off the MCU entirely and relying on hardware TCP/IP offload.
Reference Note: For deep-dive electrical characteristics of the RP2350, including the exact brownout detection thresholds and internal pull-up tolerances, refer to the official RP2350 Datasheet. For MicroPython-specific thread locking and hardware APIs, consult the MicroPython RP2 Quick Reference.






