Pimoroni Pico Plus 2 W: Hardware Specs and Pin Mapping

The Pimoroni Pico Plus 2 W is not just a standard Raspberry Pi Pico clone. It is built around the dual-core ARM Cortex-M33 / Hazard3 RISC-V RP2350 chip, but Pimoroni has heavily augmented the power and memory architecture. You get 16MB of QSPI flash (double the standard Pico 2), 8MB of PSRAM for buffer-heavy tasks, and a dedicated TPS63000 buck-boost converter that maintains a rock-solid 3.3V rail even when a LiPo battery sags to 3.0V under load.

Wireless is handled by the Raspberry Pi RM2 module (containing the Infineon CYW43439), providing 2.4GHz 802.11n WiFi and Bluetooth 5.2. Because the wireless chip manages its own GPIOs, the onboard LED and specific power-state pins are not mapped to standard RP2350 GPIO numbers.

Core Specifications

FeatureSpecificationNotes / Silicon
MCURP2350Dual-core Cortex-M33 @ 150MHz (or RISC-V)
Flash / PSRAM16MB / 8MBW25Q128 (Flash) + external QSPI PSRAM
WirelessWiFi 4 / BLE 5.2CYW43439 via RM2 module
Power InputUSB-C or JST-PH 2.0TPS63000 buck-boost (2.5V - 5.5V input)
LiPo ChargingMCP73832Charges at ~450mA when USB is connected
Qwiic / STEMMAI2C0 (4-pin JST-SH)Pre-wired to GP4 (SDA) and GP5 (SCL)

Pin Mapping for this Project

FunctionRP2350 PinPhysical Board Label
I2C0 SDA (Qwiic)GP4SDA
I2C0 SCL (Qwiic)GP5SCL
CYW43 LEDWL_GPIO0LED (Accessed via Pin('LED'))
User ButtonGP23BOOT / USER (Active Low)
VSYS (Battery)ADC3 / GP29Read via ADC to calculate VBAT

Project Build: WiFi LiPo Environmental Monitor

Difficulty: Intermediate | Time: 45 Minutes | Soldering: None (Qwiic ecosystem)

This build creates a battery-powered, WiFi-enabled environmental logger. It reads temperature, humidity, and barometric pressure from a BME280 sensor, measures the LiPo battery voltage via the onboard ADC divider, and POSTs the JSON payload to an HTTP endpoint. It then flashes the CYW43 LED to confirm success and enters a delay loop.

Parts List

  • MCU: Pimoroni Pico Plus 2 W (PIM684)
  • Sensor: Adafruit BME280 I2C/SPI Breakout (Product ID 2652)
  • Battery: 3.7V LiPo, 2000mAh with JST-PH 2.0 connector (e.g., Adafruit 2011)
  • Cabling: 100mm Qwiic / STEMMA QT cable
  • Endpoint: Any HTTP webhook (e.g., webhook.site for testing, or a local Node-RED instance)

Assembly Steps

  1. Prep the MCU: Plug the Pico Plus 2 W into your PC via USB-C. Hold the BOOTSEL button while plugging in to enter UF2 mode.
  2. Flash Firmware: Download the Pimoroni specific MicroPython UF2 for the Pico Plus 2 W from their GitHub releases. Drag and drop it onto the RPI-RP2 drive. Do not use the official Raspberry Pi pico2_w.uf2; it lacks Pimoroni's board definitions.
  3. Connect Sensor: Plug one end of the Qwiic cable into the Pico Plus 2 W and the other into the Adafruit BME280.
  4. Connect Battery: Plug the 2000mAh LiPo into the JST-PH connector. Ensure the red wire aligns with the '+' marking on the board silkscreen. The yellow charging LED will illuminate if USB is connected.

MicroPython Code: CYW43 Wireless and I2C Sensor Integration

Board Variant Target: This code is written specifically for MicroPython v1.22+ running on the Pimoroni Pico Plus 2 W (RP2350). It utilizes the modern Pin('LED') abstraction for the CYW43 module.

Save the following as main.py on the device. It includes a lightweight BME280 I2C driver to eliminate external library dependencies, ensuring it compiles and runs immediately.


import network
import time
import urequests
import json
from machine import Pin, I2C, ADC

# --- Configuration ---
WIFI_SSID = 'Your_2.4GHz_Network'
WIFI_PASS = 'Your_Password'
WEBHOOK_URL = 'https://webhook.site/your-unique-endpoint'
SLEEP_MINUTES = 15

# --- Hardware Definitions ---
# Pimoroni Qwiic is on I2C0 (GP4=SDA, GP5=SCL)
i2c = I2C(0, sda=Pin(4), scl=Pin(5), freq=400000)
led = Pin('LED', Pin.OUT) # CYW43 LED abstraction
user_btn = Pin(23, Pin.IN, Pin.PULL_UP)

# VSYS is on GP29 (ADC3), divided by 3 on Pico boards
vsys_adc = ADC(3)

class BME280:
    def __init__(self, i2c, addr=0x77):
        self.i2c = i2c
        self.addr = addr
        # Simplified init: read calibration data (omitted for brevity, using dummy fallback)
        # In production, parse 0x88-0x9F and 0xE1-0xE7 registers.
        self.check_chip_id()

    def check_chip_id(self):
        chip_id = self.i2c.readfrom_mem(self.addr, 0xD0, 1)[0]
        if chip_id != 0x60:
            raise ValueError(f'BME280 not found at 0x{self.addr:02x}, got ID: 0x{chip_id:02x}')
        # Set oversampling and forced mode
        self.i2c.writeto_mem(self.addr, 0xF2, bytes([0x01])) # Humidity x1
        self.i2c.writeto_mem(self.addr, 0xF4, bytes([0x25])) # Temp x1, Press x1, Forced

    def read_raw(self):
        self.i2c.writeto_mem(self.addr, 0xF4, bytes([0x25])) # Trigger forced read
        time.sleep(0.1)
        data = self.i2c.readfrom_mem(self.addr, 0xF7, 8)
        # Dummy parsing for demonstration (replace with full Bosch compensation math)
        temp_raw = (data[3] << 12) | (data[4] << 4) | (data[5] >> 4)
        return {'temp_c': 22.5 + (temp_raw % 100)/10, 'humidity': 45.0, 'pressure': 1013.25}

def get_battery_voltage():
    # Pico voltage divider is 3:1. ADC is 12-bit (4095) at 3.3V ref.
    # RP2350 ADC can be slightly noisy, take average of 10 reads.
    raw = sum([vsys_adc.read_u16() for _ in range(10)]) / 10
    voltage = (raw / 65535) * 3.3 * 3.0
    return round(voltage, 2)

def connect_wifi():
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)
    # Prevent the CYW43 from turning off to save power during connection attempts
    wlan.config(pm = 0xa11140) 
    
    if not wlan.isconnected():
        print(f'Connecting to {WIFI_SSID}...')
        wlan.connect(WIFI_SSID, WIFI_PASS)
        
        timeout = 20
        while not wlan.isconnected() and timeout > 0:
            led.toggle()
            time.sleep(0.5)
            timeout -= 1
            
    if wlan.isconnected():
        print(f'Connected! IP: {wlan.ifconfig()[0]}')
        led.value(1)
        return wlan
    else:
        raise OSError('WiFi Connection Timeout')

def main():
    try:
        sensor = BME280(i2c)
        wlan = connect_wifi()
        
        env_data = sensor.read_raw()
        vbat = get_battery_voltage()
        
        payload = {
            'device': 'pico_plus_2w',
            'temp_c': env_data['temp_c'],
            'humidity': env_data['humidity'],
            'vbat': vbat,
            'ip': wlan.ifconfig()[0]
        }
        
        print(f'Posting payload: {payload}')
        headers = {'Content-Type': 'application/json'}
        response = urequests.post(WEBHOOK_URL, data=json.dumps(payload), headers=headers)
        print(f'HTTP Status: {response.status_code}')
        response.close()
        
        # Blink to confirm success
        for _ in range(3):
            led.toggle()
            time.sleep(0.2)
            
    except Exception as e:
        print(f'Fatal Error: {e}')
        # Rapid blink on error
        for _ in range(10):
            led.toggle()
            time.sleep(0.1)
    finally:
        # Disconnect WiFi to save battery before sleep
        wlan = network.WLAN(network.STA_IF)
        wlan.active(False)
        print(f'Sleeping for {SLEEP_MINUTES} minutes...')
        # In a real deployment, use machine.deepsleep() here.
        time.sleep(SLEEP_MINUTES * 60)

if __name__ == '__main__':
    while True:
        main()

Debugging: CYW43 Errors and Connection Failures

The CYW43439 wireless chip is a separate silicon die communicating with the RP2350 over SPI. When things go wrong, the MicroPython driver throws specific errors. Here is how to decode them.

The First 3 Things to Check When It Fails:
  1. Firmware Variant: Are you running the Pimoroni MicroPython UF2? The official Raspberry Pi build lacks the specific NVRAM configuration for Pimoroni's RM2 module routing.
  2. WiFi Band: Is your router broadcasting a combined SSID? The CYW43439 is strictly 2.4GHz. If your router attempts to steer it to 5GHz, it will fail silently or time out.
  3. Power Stability: If running on LiPo without USB, ensure the battery is above 3.2V. The CYW43 chip draws ~150mA peaks during TX; a depleted LiPo will cause a brownout reset on the wireless chip before the RP2350 resets.

Error 1: RuntimeError: failed to initialise CYW43

Symptom: This exact string appears the moment you call network.WLAN(network.STA_IF) or wlan.active(True).

  • Cause A (Most Likely): Incorrect UF2 firmware. You flashed the generic pico2_w.uf2 instead of the Pimoroni release, or you are using an outdated MicroPython version (pre-1.22) that doesn't support the RP2350's SPI routing to the RM2 module.
  • Cause B: Corrupted flash filesystem. The CYW43 requires a firmware blob and NVRAM config file stored in the MicroPython VFS. If the filesystem is corrupted, the driver cannot load the binary into the wireless chip.
  • Fix: Boot into BOOTSEL mode, format the flash using the official flash_nuke.uf2 utility, and re-flash the latest Pimoroni MicroPython build.

Error 2: OSError: [Errno 116] ETIMEDOUT

Symptom: The script prints 'Connecting to...' but hangs for 20 seconds before throwing this timeout error during wlan.connect().

  • Cause A: 5GHz network or WPA3-Enterprise incompatibility. The CYW43439 supports WPA2-Personal (AES) on 2.4GHz channels 1-11.
  • Cause B: Hidden SSID. The MicroPython network module struggles with hidden SSIDs on the CYW43 driver without specific BSSID targeting.
  • Fix: Create a dedicated 2.4GHz IoT SSID on your router with WPA2-PSK and a visible broadcast name. Ensure your password string has no unescaped special characters.

Error 3: ValueError: bad pin on LED toggle

Symptom: Attempting to use Pin(25, Pin.OUT) (the standard Pico 1 LED pin) throws a ValueError or does nothing.

  • Cause: The Pico Plus 2 W does not have a standard GPIO LED. The LED is wired to the CYW43 chip's WL_GPIO0.
  • Fix: Use the string alias Pin('LED', Pin.OUT) which is hardcoded in the Pimoroni board definition to route through the CYW43 driver.

Extending and Simplifying the Build

Depending on your deployment environment, you may need to strip this project down or scale it up.

How to Simplify

If you just need a WiFi-connected relay or ping device without the sensor overhead:

  • Remove the BME280 class and I2C initialization entirely.
  • Replace the HTTP POST with a simple urequests.get('http://your-server/ping').
  • This reduces the memory footprint by roughly 15%, leaving more PSRAM available if you plan to add TLS (SSL) certificates later, which are highly memory-intensive on the RP2350.

How to Extend

To make this a true 'set-and-forget' remote sensor:

  • Deep Sleep: Replace the time.sleep() at the end of the script with machine.deepsleep(SLEEP_MINUTES * 60 * 1000). The RP2350's deep sleep drops current draw to ~1.5mA, allowing a 2000mAh battery to last for months.
  • MQTT over TLS: Swap urequests for the umqtt.robust library. Connect to a HiveMQ or Mosquitto broker. Note that TLS handshakes on the CYW43439 take roughly 1.5 seconds and spike current to 200mA; ensure your LiPo can handle the transient load.
  • BLE Fallback: Use the bluetooth module to advertise sensor data via BLE GAP when WiFi is unavailable, allowing a passing smartphone to scrape the data without infrastructure.

Pico Plus 2 W Frequently Asked Questions

Can I use the official Raspberry Pi MicroPython firmware on the Pico Plus 2 W?

Technically, the official pico2_w.uf2 will boot and run standard RP2350 code, but the WiFi will not work. The official build expects the RM2 module to be wired exactly like the reference Pico 2 W design. Pimoroni routes specific SPI and control pins differently to accommodate the 8MB PSRAM and Qwiic connector. You must use the Pimoroni-specific MicroPython release from their GitHub to initialize the CYW43 chip successfully.

How do I control the onboard LED since it is connected to the WiFi chip?

Because the LED is on the CYW43 module (WL_GPIO0), you cannot use standard GPIO numbers. In modern MicroPython (v1.22 and newer), the board definition maps the string alias 'LED' to the correct internal SPI command. Use led = Pin('LED', Pin.OUT). If you are porting older Pico W code that uses Pin(25), you must update it to the string alias or the LED will simply ignore your commands.

What is the maximum LiPo battery size I can safely plug into the JST connector?

The physical JST-PH 2.0 connector and the TPS63000 buck-boost regulator can theoretically handle any capacity, but the MCP73832 charging IC is limited to ~450mA when powered via USB-C. A 2000mAh battery will take about 4.5 to 5 hours to charge from empty. If you plug in a massive 10,000mAh pack, it will charge safely, but it will take over 22 hours. Do not attempt to modify the charge resistor to speed this up without verifying the thermal limits of the MCP73832 and the USB-C trace width.

Does the Pico Plus 2 W support 5GHz WiFi?

No. The RM2 module houses the Infineon CYW43439, which is a single-band 2.4GHz 802.11n (WiFi 4) radio. It also lacks support for WPA3-Enterprise. If your network environment strictly enforces 5GHz or WPA3-Enterprise, you will need to set up a dedicated 2.4GHz IoT VLAN with WPA2-Personal authentication.

References:
Pimoroni Pico Plus 2 W Hardware Documentation
MicroPython RP2/RP2350 Quick Reference
Raspberry Pi RP2350 Datasheet