rp2040-pico · diff

git:20260822.73b0d0f to git:20260823.c7a4adf

12 added, 1 removed. Audit A to A.

---
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 |
+ | 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.