USB CDC serprog firmware for RP2040/RP2350 boards (for example Raspberry Pi Pico), optimized for SPI flash read/write with flashrom.
This project turns your board into a USB SPI programmer that flashrom can talk to through the serprog protocol, and also exposes a simultaneous UART console bridge.
- USB CDC
serproginterface (works withflashrom -p serprog:...) - USB CDC
uart-consolebridge (USB <-> target UART TX/RX) - Hardware SPI backend (
spi0) for good throughput - Configurable SPI speed from host (
S_SPI_FREQ) - Optional safe-disconnect control pins:
FLASH_ACTIVE_ENfor external gating (isolation and/or target power switch)
- Build helper script that fetches required dependencies
Requirements:
gitcmake- C/C++ toolchain for Pico SDK builds
Run:
./build.shDiagnostic console is disabled by default. To enable it for a build:
SPI_DEBUG_CONSOLE=1 ./build.shThis script:
- clones
pico-sdkinto./pico-sdk(if missing) - clones
tinyusbinto./tinyusb(if missing) - initializes Pico SDK submodules
- builds firmware into
./build
Output files:
build/rpi-spi.uf2build/rpi-spi.elf
- Hold
BOOTSELwhile plugging in the board. - A USB mass storage drive appears (usually
RPI-RP2). - Copy UF2:
cp build/rpi-spi.uf2 /media/$USER/RPI-RP2/After reboot, the board appears as USB CDC serial devices.
Default pinout (spi0):
GPIO1-> flashCS#GPIO2-> flashCLKGPIO3-> flashDI / MOSIGPIO0-> flashDO / MISO3V3(or external regulator output) -> flashVCCGND-> targetGND
UART bridge pinout (default, RP2040 Zero friendly):
GPIO8-> target UARTRX(this board drives TX on GPIO8)GPIO9-> target UARTTX(this board reads RX on GPIO9)
Optional control pins:
GPIO20->FLASH_ACTIVE_EN(external gating enable)
Targeted use case for these pins:
- In-circuit programming of a flash chip that is still connected to its normal host SoC.
- Without isolation, the host SoC can keep driving
CS/CLK/MOSI/MISO(or back-power parts of the board), which causes bus contention, unstable reads/writes, or possible damage. FLASH_ACTIVE_ENcan drive one external control path that enables your isolation and/or target-power switch chain.- Current firmware behavior ties
FLASH_ACTIVE_ENtoS_PIN_STATE:S_PIN_STATE=1->FLASH_ACTIVE_ENassertedS_PIN_STATE=0->FLASH_ACTIVE_ENdeasserted
- Default behavior is inactive at boot (use a pull-down so it stays low until firmware explicitly asserts it).
How to use FLASH_ACTIVE_EN in hardware:
- Target-power gating:
- Connect
FLASH_ACTIVE_ENto theENpin of a load switch/regulator feeding flashVCC. FLASH_ACTIVE_EN=0: target flash power path off (safe idle/disconnected state).FLASH_ACTIVE_EN=1: target flash power path on for read/write/verify operations.
- Connect
- SPI-line isolation:
- Connect
FLASH_ACTIVE_ENto theOE/ENof analog switches or bus buffers inserted inCS/SCK/MOSI/MISO. FLASH_ACTIVE_EN=0: SPI path open/high-Z between programmer and target host domain.FLASH_ACTIVE_EN=1: SPI path connected so programmer can access the flash.
- Connect
- Combined use:
- You can fan out
FLASH_ACTIVE_ENto both power-gating and SPI-isolation control inputs (if voltage domains are compatible). - This gives one control state for "programming active" and one for "electrically isolated/safe idle".
- You can fan out
Important:
- The flash must be powered (
VCC+GND) to respond on SPI. - Share ground between programmer and target.
- Confirm voltage compatibility before connecting (many SPI flashes are 3.3V-only).
- Do not drive
CS/CLK/MOSI/MISOinto an unpowered target flash (can back-power through IO protection paths). - For in-circuit programming, isolation hardware is strongly recommended.
Find your serial device:
- Linux: usually
/dev/ttyACM0(or/dev/ttyACM1, ...) - macOS: usually
/dev/cu.usbmodem* - Windows:
COMx
Read:
flashrom -p serprog:dev=/dev/ttyACM0,spispeed=12M -r backup.binWrite:
flashrom -p serprog:dev=/dev/ttyACM0,spispeed=12M -w image.binVerify:
flashrom -p serprog:dev=/dev/ttyACM0,spispeed=12M -v image.binWith default build settings, firmware exposes two USB serial interfaces:
serprogport: forflashromuart-consoleport: raw UART bridge toGPIO8/GPIO9
When built with SPI_DEBUG_CONSOLE=1, a third interface is added:
diag-consoleport: interactive diagnostics
This repo includes 99-rpi-spi.rules for USB VID:PID 2e8a:000a.
Install it:
sudo cp 99-rpi-spi.rules /etc/udev/rules.d/
sudo udevadm control --reload-rules
sudo udevadm triggerUnplug/replug the board after installing the rule.
The rule sets device mode to 0660, assigns group dialout, and adds uaccess.
If needed, add your user to dialout and re-login:
sudo usermod -aG dialout "$USER"On Linux you can identify ports by symlink name:
ls -l /dev/serial/by-id/Look for entries containing serprog and uart-console.
If built with SPI_DEBUG_CONSOLE=1, you will also see diag-console.
If built with SPI_DEBUG_CONSOLE=1, connect to the diagnostic console:
screen /dev/ttyACM1 115200If ttyACM2 is not the diagnostic console, try the other enumerated ports by name.
Connect to UART bridge console:
screen /dev/ttyACM1 115200Use the uart-console by-id symlink for stable naming when possible.
Exit screen:
Ctrl-a, thenk, theny
help: show command listinfo: show firmware/build/board identifiers and configured pin mapstatus: show current SPI state and optional control-pin statecheck: run default diagnostic suitecheck force: run extended diagnostics including CS-effect check
- contention: detects if another domain appears to drive
CS/SCK/MOSIwhile this firmware tri-states drivers - control pins: validates that
FLASH_ACTIVE_ENcan toggle (if configured) - flash detect: JEDEC-ID probe at conservative speed
- stability: repeated JEDEC reads for flaky connections/noise
- speed margin: low/mid/high speed probe comparison
- optional CS-effect (force mode): checks if CS changes target response as expected
- likely disconnected pin hints with confidence:
likely_cs_disconnected(high): CS-effect test shows identical behavior regardless of CS statelikely_miso_disconnected(medium): JEDEC bytes stay stuck at0xFF/0x00likely_clk_mosi_path_issue(low): no JEDEC response plus no observed bus activity
FLASH_ACTIVE_EN is optional. If not connected or disabled at build time, diagnostics will skip related checks and continue.
check prints a final report with:
Overall: PASS | WARN | FAIL- per-check evidence and likely cause
- confidence tags in likely-cause lines for pin-disconnect inference (
high/medium/low) Recommended Actionsin priority orderNext Validationsteps
Typical recommended actions include:
- lower
flashromspeed (spispeed=1M, then4M, then12M) - reseat clip, shorten wires, improve ground
- verify target flash power and voltage
- hold target SoC in reset or add SPI isolation hardware
- verify CS/SCK/MOSI/MISO mapping
- continuity-check suspected lines from programmer header to flash pins when a
likely_*_disconnectedfinding appears
flashromalways owns theserprogport.- UART bridge runs on
uart-consolecontinuously, including during SPI flashing. - diagnostics run on
diag-consolewhen built withSPI_DEBUG_CONSOLE=1. - diagnostics are intended to run when
flashromis not actively performing SPI operations.
Start conservative, then increase:
- try
spispeed=12Mfirst - then
24M,32M, etc. if stable
If you see read/verify errors:
- shorten wires
- improve grounding
- reduce SPI speed
- add proper bus isolation for in-circuit use
Note: firmware returns the nearest supported SPI clock to host requests.
If your hardware includes isolation and/or target power control tied to GPIO20:
S_PIN_STATE = 1: SPI drivers active,FLASH_ACTIVE_ENassertedS_PIN_STATE = 0: SPI pins tri-stated, CS released,FLASH_ACTIVE_ENdeasserted
This allows keeping clips connected while electrically disconnecting the programmer side.
You can override defaults at build time:
cmake -S . -B build \
-DPICO_SDK_PATH=$PWD/pico-sdk \
-DPICO_TINYUSB_PATH=$PWD/tinyusb \
-DCMAKE_C_FLAGS='-DSP_PIN_CS=5 -DSP_PIN_SCK=6 -DSP_PIN_MOSI=7 -DSP_PIN_MISO=4 -DSP_PIN_FLASH_ACTIVE_EN=10'
cmake --build build -j"$(nproc)"Useful flags:
SP_DEFAULT_SPI_HZ(default startup SPI speed)SP_PIN_UART_TX/SP_PIN_UART_RX(UART bridge pins)SP_DEFAULT_UART_BAUD(default UART bridge speed; host can change by serial port settings)SP_PIN_FLASH_ACTIVE_EN=-1to disable flash-active pin featureSP_PIN_FLASH_ACTIVE_EN_ACTIVE_HIGH=0|1
In tusb_config.h:
#define TUD_OPT_RP2040_USB_DEVICE_ENUMERATION_FIX 0What this workaround targets:
- RP2040 B0/B1 can fail to enumerate on some host/hub combinations after USB bus reset.
- In Pico SDK this is implemented as a "brute force workaround for USB device enumeration issue" (
pico_fix/rp2040_usb_device_enumeration). - The fix waits for reset (
SE0) to end, then forces a validLS_Jstate for ~1 ms so the controller can enter the connected state reliably.
When to enable:
- You see intermittent "device not recognized" / missing CDC port after reconnect, reboot, or flashing.
- Enumeration is less reliable when connected through certain USB hubs/docks.
Tradeoff:
- Current default (
0) favors max throughput/lowest overhead in stable setups. - Set to
1if you need better plug/replug enumeration robustness, then rebuild.
- Device not found by
flashrom:- confirm board enumerates as CDC serial device
- confirm
dev=path orCOMport is correct - verify no other process has the serial port open
flashromcan connect but read/write fails:- check wiring order (
CS/SCK/MOSI/MISO) - check target voltage and ground
- lower
spispeed
- check wiring order (
- In-circuit target behaves strangely:
- add/verify bus isolation hardware
- ensure target host SoC is not actively driving the SPI bus
Related projects reviewed during development: