Firmware development for the Raspberry Pi Pico (original, RP2040 — dual Cortex-M0+ @ 133 MHz, 264 KB SRAM, 2 MB QSPI flash) — its 26 exposed GPIO, 4-channel 12-bit ADC, 16-channel PWM, 2× UART/I2C/SPI, PIO state machines, USB 1.1 CDC, VSYS/VBUS/SMPS power wiring, and the PlatformIO + arduino-pico (earlephilhower) setup around them. Use when working on this board, any Pico H or Pico clone, or a bare RP2040: project setup, platformio.ini, the earlephilhower-vs-Mbed core choice, pin mapping, USB...
Scanned 9/2/2026
Install to Claude Code
npx -y skills add alexex1993/mcu-skills --skill rp2040-pico --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Rp2040 Pico?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/alexex1993-rp2040-pico)More formats (shields.io, HTML) on the badges page.
---
name: rp2040-pico
description: Firmware development for the Raspberry Pi Pico (original, RP2040 — dual Cortex-M0+ @ 133 MHz, 264 KB SRAM, 2 MB QSPI flash) — its 26 exposed GPIO, 4-channel 12-bit ADC, 16-channel PWM, 2× UART/I2C/SPI, PIO state machines, USB 1.1 CDC, VSYS/VBUS/SMPS power wiring, and the PlatformIO + arduino-pico (earlephilhower) setup around them. Use when working on this board, any Pico H or Pico clone, or a bare RP2040: project setup, platformio.ini, the earlephilhower-vs-Mbed core choice, pin mapping, USB CDC serial, analogRead/analogWrite behaviour, Wire/SPI defaults, battery power, EEPROM emulation, BOOTSEL/UF2 flashing, or debugging why something on the board does not work.
---
# Raspberry Pi Pico (RP2040)
Board-specific firmware knowledge. The Pico's failure modes are quiet: a
wrong `platformio.ini` line silently links a different Arduino core, `Serial`
goes to a port that does not exist yet, and an ADC pin doubles as a power
rail — so read the reference files rather than guessing.
- `reference/board-hardware.md` — the complete board reference: 40-pin map
with alt functions, the four hidden GPIOs, power tree, clocks, memory map,
relevant errata **plus** a development guide (Part II: §7 toolchain and
the core-choice table, §8 peripheral cookbook, §9 flashing, §10
symptom → cause → fix table).
- `reference/recipes.md` — copy-paste code: `platformio.ini`, `board.h`
excerpts, USB CDC + Serial1, ADC with the VSYS ×3 math, PWM and the slice
rule, Wire1 remapping, EEPROM with `commit()`, interrupts, core 1,
BOOTSEL tricks.
- `template/` — a **project that builds clean**, in two variants, plus a
scaffold script. See `template/README.md`.
## Orientation
| | |
|---|---|
| MCU | RP2040 — 2× ARM Cortex-M0+ @ **133 MHz** (arduino-pico default; SDK default 125 MHz), 40 nm QFN-56 |
| Memory | 264 KB SRAM (6 banks), **no internal flash** — 2 MB W25Q16JV QSPI, XIP from `0x10000000` + 16 KB cache. Max sketch 2,093,056 B (last 4 KB = emulated EEPROM) |
| LED / button | LED = GPIO25, **active-HIGH**, not on the header · BOOTSEL button, sampled **at power-up only** |
| USB | micro-B, FS device/host; `Serial` = **USB CDC**; 1200-baud touch reboots into BOOTSEL; enumerates as VID:PID `2E8A:000A` ("Raspberry Pi" / "Pico") once the sketch is running |
| ADC | 12-bit 500 ksps: A0=GPIO26, A1=27, A2=28, **A3=GPIO29 = VSYS/3 (internal)** + channel 4 = die temp; `analogRead` is **10-bit by default**; ENOB 8.7 bits |
| PWM | every GPIO, 16 ch (8 slices × 2); `analogWrite` default 8-bit @ 1 kHz; **GPIO 2n & 2n+1 share one slice** |
| Bus defaults | Wire = 4/5 · **Wire1 = 26/27 (= A0/A1!)** · SPI = 17/18/19/16 · Serial1 = 0/1 · Serial2 = 8/9 |
| Power | VBUS 5 V → diode → VSYS **1.8-5.5 V** → RT6150 buck-boost → 3.3 V; 3V3 pin out < 300 mA; 3V3_EN low = board "dead" |
| Debug | 3-pin SWD header (SWCLK/GND/SWDIO); boot ROM is mask ROM — **unbrickable** |
| Toolchain | PlatformIO + `raspberrypi` 1.19.0 + arduino-pico 5.6.0 (`board_build.core = earlephilhower`) |
## Rules that prevent the expensive mistakes
Each of these produces a failure that looks like something else.
1. **`board_build.core = earlephilhower` in `platformio.ini`.** Plain
`framework = arduino` silently builds against the Arduino **Mbed** core:
`#include <EEPROM.h>` fails to compile, `Wire1`/`Serial2` don't exist,
`analogWrite` is 500 Hz with no frequency API — and none of the usual
Pico tutorial code works. The error reads like a broken dependency, not
a missing ini line.
2. **`Serial` is USB CDC, not UART0.** A USB-serial adapter wired to
GPIO0/1 sees nothing from `Serial.print` — those pins are `Serial1`.
The CDC port exists only once the host opens it: bytes printed before
that are dropped, and `while (!Serial)` blocks startup until a monitor
connects. Never gate bring-up on the port being open.
3. **Upload usually needs no button — until it does.** `pio run -t upload`
touches the CDC port at 1200 baud and a *running* sketch reboots itself
into BOOTSEL for picotool. If the firmware has crashed or its USB is
dead, the upload dies with `Cannot find BOOTSEL disk` — hold BOOTSEL
while replugging USB, then re-run. That recovery is always available;
the boot ROM is mask ROM and cannot be overwritten.
4. **BOOTSEL only works at power-up.** Pressing it while the board runs
does nothing (it grounds the flash CS line). To get the `RPI-RP2`
drive: hold BOOTSEL, plug/replug USB, release. A running sketch can
re-enter it from code (`reset_usb_boot(0, 0)` — recipe 13).
5. **`analogRead` returns 10-bit values (0-1023) by default** on a 12-bit
ADC. Symptom: "stuck at 1023 at full scale" or calibration off by 4×.
`analogReadResolution(12)` before sampling.
6. **A3/GPIO29 is not a free pin — it measures VSYS/3.** Multiply by 3 for
the input voltage. GPIO23 (SMPS power-save), GPIO24 (VBUS sense), GPIO25
(LED), GPIO29 are real GPIOs in code but **not on the 40-pin header** —
wiring plans that use them compile and "work" while nothing appears on
the breadboard.
7. **GPIO pairs share a PWM slice.** Slice = pin/2: 0&1, 2&3 … 28&29 share
one counter and therefore one frequency — the last `analogWriteFreq()`
on either pin retimes both, and a stepper/servo pair on one slice
glitches in a way that looks like a power problem.
8. **Wire1 defaults to GPIO26/27 — the same pins as A0/A1.** I2C bus
corruption that starts as soon as analogRead is called is this, not
electrical noise. `Wire1.setSDA()/setSCL()` to another I2C1 pair
(2/3, 6/7, 10/11, 14/15, 18/19, 22/23) before `begin()`.
9. **GPIO26-29 are not 5 V tolerant and carry a reverse diode to 3V3.**
Above ~3.6 V they damage the chip; any voltage on them while the board
is unpowered back-powers the 3.3 V rail (a Pico that "ghosts" with no
USB connected is being fed through an analog pin). GPIO0-25 tolerate
applied voltage while unpowered.
10. **The ADC's accuracy budget is worse than its bit count.** Reference =
filtered 3.3 V SMPS rail, ~30 mV inherent offset (150 µA through the
200 Ω filter), ENOB 8.7 bits, DNL spikes at codes 512/1536/2560/3584.
Naive absolute measurements are ±2-3 %. Average; cancel offset with a
grounded channel; drive GPIO23 HIGH during sampling to quiet the SMPS
(recipe 5), or fit an LM4040 3.0 V on ADC_VREF (range drops to 3.0 V).
11. **EEPROM lives in flash and only persists on `commit()`.** `begin(n)`
maps a shadow, `put()` stages (update semantics), `commit()` burns one
4 KB sector erase (100 k cycles). A sketch that never commits loses
everything on reboot — silently, with no error.
12. **Flash writes stall both cores.** Code executes from the same QSPI
chip (XIP), so `EEPROM.commit()` and filesystem writes pause the whole
processor mid-stream. Keep them out of timing-critical and ISR-adjacent
windows; the symptom is a periodic hiccup that looks like a scheduler
bug.
13. **USB may not enumerate on a busy hub** (erratum E5, B0/B1 silicon —
B2 fixes it): next to a chatty device on the same transaction
translator, the port never leaves RESET. LED works, no COM port. Plug
in directly or move ports before reinstalling drivers.
14. **Power wiring has three traps.** Feed VSYS (pin 39, 1.8-5.5 V) for
battery/external power — a single Li-ion works. USB *host* mode needs
5 V supplied to the VBUS pin (pin 40), not just VSYS. And a board that
looks completely dead with USB connected usually has 3V3_EN (pin 37)
shorted low — that pin disables the entire SMPS. The 3V3 pin (36) is an
output (< 300 mA); never back-feed it. RUN (pin 30) shorted to GND is
the reset button.
15. **LittleFS/FatFS silently gets zero bytes unless you ask for space.**
The build always reserves the last 4 KB of flash for `EEPROM.h`
(`maximum_sketch_size = flash_size - 4096 - filesystem_size`,
`board_build.filesystem_size` default **`0MB`**), and any littlefs/fatfs
region is carved out of flash *below* that, sized by that same option.
`LittleFS.begin()` on the stock template returns `false` (or mounts an
empty 0-byte volume) — not a build error, not a crash — because nothing
was reserved. Set `board_build.filesystem_size = 1MB` (or `2MB`, `4MB`
…) in `platformio.ini` before using `LittleFS`, `FatFS` or `SDFS`'s
flash-backed mode, and re-flash with `pio run -t uploadfs` once for the
initial image.
## When the task is analog measurement
The ADC is ratiometric to a filtered SMPS rail, not to a reference. What
that means in numbers: full scale is 3.3 V ± the SMPS tolerance (±1-2 %);
there is a built-in ~30 mV offset because the ADC's ~150 µA supply current
flows through the 200 Ω filter resistor (it varies ±20 µA with sampling);
effective resolution is 8.7 bits, with DNL spikes at four codes roughly
every 1024 counts (512, 1536, 2560, 3584 — erratum E11). So a single raw
12-bit reading implies far more accuracy than it has.
Work in that order: average many samples (free, fixes noise + DNL); tie a
spare ADC channel to AGND and subtract its reading (cancels the offset);
drive GPIO23 HIGH while sampling to force the SMPS into PWM mode, LOW after
(cuts ripple, costs light-load efficiency); and only then consider the
LM4040 3.0 V shunt on ADC_VREF — that also shrinks the input range to
3.0 V. For battery monitoring, `analogRead(A3) × 3` is well inside the
ADC's envelope — relative changes are trustworthy even when absolutes
aren't.
## When the task is powering the board
The chain is VBUS → Schottky D1 → VSYS → RT6150 buck-boost → 3V3. The
buck-boost accepts 1.8-5.5 V at VSYS, which makes power the most flexible
part of the board: USB, one Li-ion cell (with protection!), 3×AA, or any
2.3-5.5 V supply with an ORing diode/P-FET into VSYS. The traps: USB host
mode must see 5 V on the **VBUS pin** (VSYS power alone leaves the host
port dead); USB-only setups may bridge VBUS to VSYS to remove the diode
drop; and never feed the 3V3 pin — it's an output, and > 3.3 V there goes
straight into the RP2040 and flash. VSYS current draw: ~10 mA idle in
BOOTSEL, ~1.3 mA sleep, ~0.8 mA dormant, 90+ mA loaded.
## Starting a new project
Do not hand-assemble one. `template/` builds clean; scaffold from it:
```sh
~/.claude/skills/rp2040-pico/template/variants/new-project.sh <target-dir> [--full|--minimal]
cd <target-dir> && pio run -t upload -t monitor
```
- `--minimal` — classic Blink on GPIO25. **58,172 B** flash / 8,732 B RAM.
Flash this first on an unfamiliar board: if the LED does not blink after
upload, the problem is the cable, the BOOTSEL dance or rule 1, not the
code. (The floor is TinyUSB CDC — a bare blink costs ~58 KB flash here,
vs ~4 KB flash / ~40 KB RAM on the Mbed core.)
- `--full` (default) — non-blocking heartbeat, USB-CDC report, A0 at 12
bits, die temperature, VSYS via A3, VBUS sense, EEPROM boot counter.
61,112 B flash / 9,036 B RAM.
Both build as-is with platform-raspberrypi 1.19.0 + arduino-pico 5.6.0
(verified). Nothing is generated and no paths are embedded, so copying the
tree by hand works identically. `template/README.md` maps files to
subsystems.
When the user already has a project, prefer adding
`board_build.core = earlephilhower` to their `platformio.ini` over
rewriting their code — that one line is usually the whole fix.
## Flashing
Over the micro-USB cable:
```sh
pio run -t upload -t monitor
```
Normally nothing is pressed. PlatformIO opens the CDC port at 1200 baud,
the running sketch reboots into BOOTSEL, picotool flashes and reboots the
board — the LED blinks again within a second of `SUCCESS`. The board
enumerates as a CDC port (`/dev/cu.usbmodem*`, `COMx`); the monitor's baud
rate is ignored.
When the running firmware cannot respond (crashed, USB disabled, power-only
cable), the keystroke sequence is: hold **BOOTSEL** → plug/replug USB →
release → a 128 MB `RPI-RP2` drive appears → either re-run `pio run -t
upload` (picotool picks up the BOOTSEL device), or drag
`.pio/build/pico/firmware.uf2` onto the drive — the board reboots itself
when the copy completes. `pio run -t erase` copies a `flash_nuke.uf2` that
erases all 2 MB (including EEPROM) — the clean-slate recovery. A
power-only USB cable is the most common cause of "no RPI-RP2 drive".
The alternative route is SWD over the 3-pin bottom header with a second
Pico as Picoprobe (`upload_protocol = picoprobe`) — needed only when flash
itself is wedged; the mask-ROM bootloader survives everything.
## Reporting
State honestly what was verified on hardware and what came from the
datasheet. In this skill: both template variants and the recipes marked
compile-verified build clean against platform-raspberrypi 1.19.0 +
arduino-pico 5.6.0; the `--minimal` blink is the user project this skill
was extracted from. **No analog measurement, power draw or timing figure
was measured on hardware by the author** — the ADC accuracy envelope, VSYS
divider, SMPS behaviour and current figures are datasheet-derived
(Pico datasheet §3-4, RP2040 datasheet errata), and core-behaviour claims
(read resolution defaults, Wire1 pins, upload touch) come from the
arduino-pico 5.6.0 sources. Anything over 133 MHz F_CPU a user requests is
overclocking — label it as outside spec.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!