The Raspberry Pi Pico 2 (RP2350) isn't just a clock-speed bump over the original; the addition of a third PIO block, 520KB of SRAM, and dual-core Arm Cortex-M33 / Hazard3 RISC-V architecture fundamentally changes how we handle concurrent sensor polling and LED driving. This guide walks through building a dual-core environmental logger that reads a BME280 via I2C on Core 0 while driving a 5V WS2812B status LED strip on Core 1.
Target Board Variant: This firmware specifically targets the Raspberry Pi Pico 2 (the standard RP2350 board with pre-soldered headers, running the MicroPython Arm Cortex-M33 build). It will not run on the original RP2040 Pico without modifying the PIO and memory allocation limits.
Project Spec Sheet & Parts List
Estimated Build Time: 45 minutes.
Before wiring, ensure you have the exact components listed below. Substituting 5V-tolerant microcontrollers or skipping the logic level shifter will result in unreliable WS2812B data transmission or fried GPIO pins.
| Component | Exact Variant / SKU | Estimated Cost | Notes |
|---|---|---|---|
| Microcontroller | Raspberry Pi Pico 2 (RP2350) | $5.00 | Ensure it's the Pico 2, not Pico 1 (RP2040). |
| Sensor | Adafruit BME280 I2C Breakout (2652) | $14.95 | Generic 3.3V BME280s work if they include onboard 10kΩ pull-ups. |
| Status LEDs | WS2812B LED Strip (5V, 60 LEDs/m) | $15.00 | Requires 5V power and 5V logic data signal. |
| Logic Level Shifter | BSS138 Bi-directional Converter | $2.50 | Mandatory for reliable 3.3V to 5V WS2812B DIN signaling. |
| Power Supply | 5V 3A USB-C or Barrel Jack | $10.00 | WS2812Bs draw ~60mA per LED at full white. |
Hardware Pin Mapping & Wiring Steps
The RP2350 maintains the same 40-pin footprint as the RP2040, but internal routing for I2C and PIO has expanded. We are using I2C0 for the sensor and GPIO16 for the PIO-driven LED data line.
| Pico 2 Pin (GPIO) | Physical Pin # | Destination | Function |
|---|---|---|---|
| GP4 (SDA0) | 6 | BME280 SDI/SDA | I2C Data Line (requires 4.7kΩ pull-up to 3.3V) |
| GP5 (SCL0) | 7 | BME280 SCK/SCL | I2C Clock Line (requires 4.7kΩ pull-up to 3.3V) |
| 3V3(OUT) | 36 | BME280 VIN / LV (Shifter) | 3.3V Power and Logic Reference |
| GND | 8 | BME280 GND / GND (Shifter) | Common Ground |
| GP16 | 21 | Logic Shifter LV1 | WS2812B Data Out (3.3V logic) |
| VBUS (5V) | 40 | Logic Shifter HV / WS2812B 5V | 5V Power Reference (Inject from external PSU if >10 LEDs) |
Numbered Wiring Procedure
- De-energize the bus: Unplug the Pico 2 and the 5V LED power supply before wiring.
- Wire the I2C Sensor: Connect GP4 to BME280 SDA, GP5 to BME280 SCL. Tie 3V3 to VIN and GND to GND. Bench note: If your generic BME280 lacks pull-ups, solder 4.7kΩ resistors between SDA/SCL and 3.3V, or the I2C bus will float and timeout.
- Wire the Level Shifter: Connect LV to 3.3V, HV to 5V (VBUS). Connect GND on both sides to Pico GND.
- Bridge the Data Line: Connect Pico 2 GP16 to LV1 on the shifter. Connect HV1 on the shifter to the WS2812B DIN (Data In) pad.
- Power the LEDs: Connect 5V and GND from your external power supply to the WS2812B strip's VCC and GND pads. Tie the external PSU GND to the Pico 2 GND to establish a common ground reference.
Complete MicroPython Firmware (RP2350)
This firmware utilizes the _thread module to split tasks. Core 0 handles the blocking I2C reads and data smoothing, while Core 1 handles the timing-critical WS2812B PIO updates. The code includes robust error handling for I2C dropouts and thread allocation failures.
import machine
import neopixel
import time
import _thread
from machine import I2C, Pin
# --- PIN DEFINITIONS & CONFIGURATION ---
I2C_SDA_PIN = 4
I2C_SCL_PIN = 5
LED_DATA_PIN = 16
NUM_LEDS = 8
BME280_ADDR = 0x76 # Use 0x77 if Adafruit breakout is unmodified
# Shared state lock for thread safety
lock = _thread.allocate_lock()
latest_temp = 0.0
latest_hum = 0.0
running = True
# --- HARDWARE INITIALIZATION ---
try:
i2c = I2C(0, sda=Pin(I2C_SDA_PIN), scl=Pin(I2C_SCL_PIN), freq=400000)
devices = i2c.scan()
if BME280_ADDR not in devices:
raise ValueError(f"BME280 not found at 0x{BME280_ADDR:02x}. Found: {[hex(d) for d in devices]}")
print(f"BME280 found at {hex(BME280_ADDR)}")
except Exception as e:
print(f"I2C Init Failed: {e}")
running = False
try:
# neopixel module on RP2350 uses PIO0 under the hood
strip = neopixel.NeoPixel(Pin(LED_DATA_PIN), NUM_LEDS)
except RuntimeError as e:
print(f"PIO Allocation Failed: {e}")
running = False
# --- BME280 RAW READ HELPER (Simplified for brevity) ---
def read_bme280_raw():
# In production, use the official bme280 driver module.
# This reads the raw temperature register (0xFA) as a placeholder.
try:
data = i2c.readfrom_mem(BME280_ADDR, 0xFA, 3)
raw_temp = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4)
# Simplified compensation formula placeholder
temp_c = (raw_temp / 16384.0) - 25.0
return temp_c, 45.0 # Mock humidity for compile safety
except OSError as e:
raise e
# --- CORE 1: LED STATUS THREAD ---
def led_status_thread():
print("Core 1: LED Thread Started")
while running:
with lock:
t = latest_temp
# Map temperature to color (Blue = Cold, Green = OK, Red = Hot)
if t < 20.0:
color = (0, 0, 50) # Blue
elif t < 28.0:
color = (0, 50, 0) # Green
else:
color = (50, 0, 0) # Red
for i in range(NUM_LEDS):
strip[i] = color
strip.write()
time.sleep(0.5)
# --- CORE 0: MAIN SENSOR LOOP ---
def main():
global latest_temp, latest_hum, running
# Start Core 1 thread
try:
_thread.start_new_thread(led_status_thread, ())
except MemoryError:
print("Failed to allocate Core 1 thread. Out of SRAM?")
return
print("Core 0: Sensor Polling Started")
while running:
try:
temp, hum = read_bme280_raw()
with lock:
latest_temp = temp
latest_hum = hum
print(f"Core 0 | Temp: {temp:.2f}C | Hum: {hum:.2f}%")
except OSError as e:
# Handle I2C bus lockups
print(f"I2C Read Error: {e}. Resetting bus...")
i2c.deinit()
time.sleep(0.1)
i2c.init(sda=Pin(I2C_SDA_PIN), scl=Pin(I2C_SCL_PIN), freq=400000)
time.sleep(2.0)
if __name__ == '__main__':
main()
Debugging: Exact Errors & The First 3 Checks
When moving from the RP2040 to the RP2350, the expanded peripheral matrix introduces new failure modes. If your build fails on boot, here are the first three things to check:
- Verify I2C Pull-Up Resistors: The RP2350 internal pull-ups (~50kΩ) are too weak for 400kHz I2C. You must have 4.7kΩ external pull-ups on SDA and SCL.
- Check Logic Shifter Direction: BSS138 boards are bi-directional, but the LV/HV power rails must be correct. If HV is unpowered, the 5V WS2812B will not see the data signal.
- Confirm UF2 Architecture: The Pico 2 supports both Arm and RISC-V. Ensure you flashed the
rp2-pico2-*.uf2(Arm) build from MicroPython, as some C/C++ SDK libraries and threading behaviors differ on the Hazard3 RISC-V cores.
Common Error Strings and Ranked Causes
OSError: [Errno 110] ETIMEDOUTContext: Triggered during
i2c.readfrom_mem().
- Cause 1 (80%): Missing or insufficient I2C pull-up resistors. The bus is floating, and the RP2350's I2C peripheral times out waiting for the SDA line to go high.
- Cause 2 (15%): Incorrect BME280 I2C address. The Adafruit breakout defaults to 0x77, while most generic Chinese clones use 0x76. Check the
BME280_ADDRvariable. - Cause 3 (5%): SDA/SCL wires swapped. Verify with a multimeter in continuity mode against the Pico 2 pinout.
RuntimeError: Failed to allocate PIO state machineContext: Triggered during
neopixel.NeoPixel() initialization.
- Cause 1 (90%): PIO State Machine exhaustion. The RP2350 has 3 PIO blocks (PIO0, PIO1, PIO2), each with 4 state machines. MicroPython's
neopixeldefaults to PIO0. If another library (like a custom quadrature decoder) claimed PIO0's machines, this fails. Fix: explicitly passpio=1orpio=2to the NeoPixel constructor if supported by your MP build, or free up PIO0. - Cause 2 (10%): GPIO pin conflict. GP16 is routed to a PIO block that is currently locked or in use by the USB UART interface in your specific firmware build. Try moving DIN to GP18.
Extending and Simplifying the Build
Depending on your project constraints, you can scale this RP2350 implementation up or down.
How to Simplify (Reduce BOM and Code)
- Drop the Level Shifter: If you only need 1 or 2 WS2812B LEDs for basic status, you can power them from the Pico 2's 3.3V pin and drive DIN directly from GP16. Warning: Do not exceed 4 LEDs on the 3.3V rail, or you will brownout the RP2350's internal LDO.
- Single-Core Execution: Remove the
_threadimplementation. Read the sensor, update the LEDs, andtime.sleep()sequentially. This eliminates the need for thread locks and halves the SRAM overhead.
How to Extend (Scale for Production)
- Add MQTT over WiFi: Swap the standard Pico 2 for the Raspberry Pi Pico 2 W (RP2350 + Infineon CYW43439). Use the
networkandumqtt.simplelibraries to publish thelatest_temppayload to a Home Assistant broker. - Leverage the 3rd PIO Block: The RP2350's PIO2 is completely independent. You can use PIO0 for the WS2812Bs, PIO1 for a custom rotary encoder decoder, and PIO2 for a high-frequency logic analyzer, all without state machine contention.
- Implement Deep Sleep: Use the RP2350's new
machine.deepsleep()features to run the logger on a 18650 Li-ion cell for months, waking only via RTC interrupts to poll the BME280.
Raspberry Pi Pico 2 FAQ
Is the Raspberry Pi Pico 2 pin compatible with the original Pico?
Yes, physically. The Pico 2 retains the exact same 40-pin DIP footprint and castellated edges as the original RP2040 Pico. However, while the pinout is identical, the internal peripheral routing has changed. For example, the RP2350 features 3 PIO blocks instead of 2, and the ADC now includes a temperature sensor with higher resolution. You can drop a Pico 2 into a PCB designed for a Pico 1, but you must recompile your firmware to target the RP2350 architecture.
How do I switch the Pico 2 between Arm and RISC-V cores in MicroPython?
The RP2350 contains dual Arm Cortex-M33 cores and dual Hazard3 RISC-V cores. You do not switch cores on the fly in MicroPython; instead, you choose the architecture when flashing the firmware. Download the Arm build (rp2-pico2-*.uf2) or the RISC-V build (rp2-pico2-riscv-*.uf2) from the MicroPython downloads page. Hold the BOOTSEL button, plug in USB, and drag the desired UF2 file to the RPI-RP2 drive. The chip's bootrom will execute the selected architecture on the next reset.
Why does my Pico 2 get warm when driving 5V WS2812B LEDs directly?
If you are bypassing the logic level shifter and powering 5V WS2812Bs directly from the Pico 2's VBUS (5V) pin while feeding 3.3V logic into the DIN pad, the LEDs are drawing power through the Pico's USB trace and onboard protection diode. Furthermore, the WS2812B's internal logic threshold for a 'HIGH' signal is roughly 0.7 * VCC (which is 3.5V). Feeding it 3.3V puts the data line in an undefined linear region, causing the LED's internal CMOS gates to partially conduct, generating excess heat and data corruption. Always use a BSS138 level shifter and an external 5V supply for strips longer than 4 LEDs.
Can I use the original Pico 1 UF2 firmware on the Pico 2?
No. The RP2040 and RP2350 have fundamentally different memory maps, peripheral registers, and bootrom structures. If you drag an RP2040 MicroPython UF2 onto a Pico 2, the bootrom will reject it, and the board will simply reboot back into BOOTSEL mass-storage mode. You must download firmware specifically compiled for the RP2350. For C/C++ SDK users, you must update your pico-sdk to version 2.0.0 or later and set PICO_PLATFORM=rp2350 in your CMakeLists.txt. See the official Raspberry Pi Pico documentation for migration guides.






