Appendix E: Studio
Up to this point every example in the book has been built and flashed
the same way: make build/<name>_flash.uf2, hold BOOTSEL, drag-drop.
That is the truthful, low-level path: the Makefile is the build
system, picotool (or the BOOTSEL drive) is the flasher, and you
read the resulting UF2 with your eyes if you ever need to debug what
went onto the chip. It is also the path you reach for when you want
to know exactly what the toolchain is doing.
Studio is the same engine wrapped in something a new user can drive
without reading a Makefile. You pick a target, toggle peripherals,
click Build, click Flash. Under the hood it runs the same
arm-none-eabi-as and arm-none-eabi-ld invocations, against the
same sources in src/, with the same linker scripts in link/.
There is no parallel build; no second copy of uart.S lives inside
Studio. The catalog points at ../src/uart.S and that is the file
that gets assembled.
This appendix walks through using Studio for the workflows the preceding chapters established by hand: building a flash blinky, building an SRAM blinky, picking a custom set of peripherals, shipping a bootloader chain, and pushing per-slot updates.
When to use Studio (and when not to)
Studio is the right tool when:
- You're new to the SDK and want to see things build before you understand why.
- You're prototyping and want to flip peripherals on and off without editing example sources.
- You're shipping a bootloader chain (chapter material is in
docs/bootloader.md). The TOML field[bootloader] tsbl = "ab"is dramatically less typing than the fullmake build/firmware_<name>.uf2recipe. - You're doing A/B field updates on a board that already has the bootloader installed.
The Makefile is still the right tool when:
- You want to know exactly which
as/ldflags were used. - You're writing CI:
make test-t1,make test-tools,make test-t2is the canonical entry point. - You need to script the build of something Studio doesn't model yet: a custom linker script, a one-off footer, a hand-rolled UF2.
- You are debugging the build itself.
The CLI half of Studio (rpasm, the binary, not the GUI) lives in
between. It's scriptable and exposes every operation the GUI does,
so you can pin it into CI alongside make once you trust it.
Building blinky
The fastest way through the GUI is to use the example dropdown. From the SDK root:
$ go run ./studio/cmd/rpasm-studio
A window opens with two mode tabs at the top: Examples and
Custom Project. Examples mode is selected by default, with
blinky (the canonical src/main.S) preselected. The Target
dropdown shows rp2350-arm; Layout defaults to sram (faster
iteration cycle, doesn't wear flash). Click Build. The Output
panel scrolls through the assemble/link steps and ends with three
paths: ELF, BIN, UF2. The Flash button (which was greyed out
as "Flash (build first)") activates.
Put the Pico 2 in BOOTSEL (hold the button, plug in the cable),
click Flash, and the on-board LED starts blinking. Same result
as make build/blinky.uf2 followed by drag-drop; about 20 fewer
keystrokes, and the build log is sitting right there to read.
For a flash-resident build, flip the Layout dropdown to flash,
rebuild, reflash. Whatever was at 0x10000000 is overwritten with
the new image; the SRAM build no longer survives a power cycle but
the flash one does.
Custom Project mode
Examples mode picks one source file from examples/ and links it
against every default-on module from the catalog. That's enough to
get started but doesn't let you say "I want UART and PIO but not
DMA". Custom Project mode does.
Switch to the Custom Project tab. The left pane changes: where
Examples mode showed a preview of the picked .S file, Custom mode
shows a Source path input and a grid of Features checkboxes
organised by category. Each checkbox is one module in the catalog
(STARTUP, UART, GPIO, …); they start at their catalog default
(most are default-on).
Type or paste a source path: ../src/main.S for the standard blinky,
or any of your own .S files. Uncheck the modules you don't need,
typically USB, PIO, SHA256, TRNG, etc., when the app doesn't
touch those peripherals. Just below the Source path input, a row of
"Selected modules" badges shows the live set that the next Build
will pull in. Click Build, then Flash, same as before.
The catalog itself is regular files at studio/catalog/. To add a
module you author a small TOML descriptor like
studio/catalog/peripherals/myperiph.toml:
symbol = "MYPERIPH"
name = "My Peripheral"
category = "peripherals"
order = 145
default = false
description = "What this module does."
sources = ["../src/myperiph.S"]
It shows up in the GUI's Features grid on the next launch. There's no plugin system, no rebuild of Studio; the catalog is just data.
Saving a project
The settings the GUI captures (target, layout, the source path, the
feature toggles) round-trip through a .rpasm.toml file via the
Save and Load buttons in the project row. Save once, and the
same configuration is one click away next time, or shareable as a
text file. The CLI consumes the same format
(rpasm build path/to/project.rpasm.toml), so a project file
authored in the GUI is what your CI runs against in the Makefile.
Loading a project also populates the path resolution: the GUI walks
up to find go.mod + catalog/, and Browse... opens a native
file dialog (zenity on Linux, the OS picker elsewhere) when you'd
rather click than type.
The bootloader workflow
If you've read docs/bootloader.md you
know the shape of the chain: bootrom → SSBL → TSBL → app slot. From
the Makefile, building that whole stack into one UF2 takes a
make bootloader && make build/blinky_app.elf && make build/firmware_blinky.uf2
incantation. From Studio it's one TOML field.
Add to your project TOML:
[bootloader]
tsbl = "ab" # "bypass" for single-slot, "ab" for A/B + rollback
Studio sees the field and switches the build engine into chain mode:
- App links at slot A's base (
0x10008000) usinglink/app_at_0x10008000.ld. - SSBL is assembled from
src/ssbl/ssbl.S+src/crc32.S, linked withlink/ssbl.ld. - The chosen TSBL flavor (
src/tsbl/tsbl_ab.Sortsbl_bypass.S) is assembled and linked withlink/tsbl.ld. - CRC32 + SHA-256 footers are computed for TSBL and app.
- The pieces are stitched into
firmware_<name>.uf2at their canonical addresses.
The Memory tab now has a third section, "Bootloader chain", that
breaks the firmware down by stage (SSBL / TSBL-<flavor> / Slot A /
Slot B) with each one's used and capacity in bytes. Slot B reads
as 0 B used after a full firmware flash: the chain UF2 only
populates slot A, and TSBL-ab will see only A as valid and boot it.
Flash the firmware UF2 with Flash as normal. The board boots through the whole chain.
If the app needs to talk back to the TSBL (to confirm a successful
boot for the A/B rollback path, or to request a reboot into DFU mode),
enable the Boot API module in the Features grid. It pulls in
src/boot_api.S, which exposes boot_confirm(),
boot_request_dfu(), and boot_request_bootsel() to your app code.
Per-slot field updates
Once the bootloader chain is in place, you rarely re-flash the whole thing. The usual update pattern is "the device is running, push a new app to the other slot, let TSBL-ab pick it up on the next boot, and roll back if it's buggy."
Studio's per-slot flag does exactly that:
$ rpasm flash --slot b path/to/project.rpasm.toml
This rebuilds the app at slot B's base (0x10080000) using the
slot-B linker script, packs a slot-only UF2 with just the app and
its footer, and writes only 0x10080000/0x100F7F00. SSBL and TSBL
are untouched. The footer is written with seq=2 so TSBL-ab's
higher-sequence-wins selector picks slot B over slot A on the next
boot.
If the new app calls boot_confirm(), the rollback marker in
WATCHDOG_SCRATCH[6] is cleared and slot B sticks. If it doesn't
(if it crashes, hangs, or triggers a watchdog reset before
confirming) the marker survives the warm reset, TSBL-ab sees it on
the way back up, and rolls back to slot A. There's a worked example
of the watchdog-driven rollback in examples/blinky_buggy_demo.S
and the rollback demo firmware (make build/firmware_rollback_demo.uf2).
The --slot a flag does the symmetric thing for slot A. Either
way, the bootloader chain is the part that isn't in the UF2 you
flash, which is what makes the update cheap and recoverable.
Inspecting a board
rpasm bootinfo (or the GUI's Query slots button in the tools
row) puts the connected BOOTSEL board through an EXIT_XIP + READ
sequence, parses both slot footers, and reports them:
$ rpasm bootinfo
slot A @ 0x10008000: status=good seq=1 payload=1188 B crc=0xcbf6f7d1
slot B @ 0x10080000: status=good seq=2 payload=1188 B crc=0xa5e10efa
This is the field-debugging tool. If a customer reports their board
boots the old slot after an update, bootinfo is how you confirm
whether the update wrote where it claimed.
The CLI under the GUI
The GUI is a Gio window over the same engine rpasm exposes from
the command line. Anything in the GUI maps to one or more CLI
calls:
| GUI action | CLI equivalent |
|---|---|
| Build button | rpasm build project.toml |
| Flash button | rpasm flash project.toml |
Flash with [bootloader] set |
rpasm flash project.toml (auto picks firmware UF2) |
| Reset to BOOTSEL | rpasm reboot --bootsel |
| Query slots | rpasm bootinfo |
| Memory tab content | rpasm build tail (printed after the build summary) |
| Validate without building | rpasm validate project.toml |
| Toolchain + udev + board status | rpasm doctor |
If you have an existing Makefile-driven flow and want to add Studio to it incrementally, the CLI is the surface to bind against. The GUI is purely the user-facing front; everything is scriptable.
When something doesn't work
The first thing to check is rpasm doctor:
$ rpasm doctor
root: /home/.../rp-asm/studio
modules: 26
targets: 1
[target rp2350-arm]
as: /usr/bin/arm-none-eabi-as
...
[board]
udev: /etc/udev/rules.d/99-rpasmboot.rules present
boards: none in BOOTSEL right now ...
Three classes of problem this surfaces:
-
Missing toolchain.
as/ld/objcopypaths fail to resolve to a real binary. Installgcc-arm-none-eabi(or equivalent on your distro), make sure the prefix incatalog/targets/rp2350-arm.tomlmatches what you installed. -
Missing udev rule. On Linux, PICOBOOT USB access without the rule fails with
EACCES;rpasm flash --method rpasmbootwould fall back to drive-copy and you'd wonder why everything is slower. The doctor prints the one-linesudo ... && sudo udevadm ...command. -
No board. "Hold BOOTSEL while plugging in the board" is nearly always the answer. If
lsusbshows a2e8a:000fand doctor still says no boards, something is wrong with the udev rule installation.
For protocol-level problems with rpasmboot itself, the diagnostic
log lines mirror to the launching terminal; every line the GUI
shows in its Output panel also prints on stderr. So
go run ./studio/cmd/rpasm-studio 2>&1 | tee studio.log captures
everything for paste-into-issue debugging without needing a
selectable widget.
Where to go from here
This appendix is the narrative tour. For the field-by-field
reference (every project TOML key, every CLI flag, every internal
package) see docs/studio.md. The bootloader
chain itself (memory map, footer layout, status state machine,
watchdog ABI) is in docs/bootloader.md.
The peripheral modules referenced by the catalog have their own
datasheets under docs/{uart,usb,pio,...}.md.
Studio is meant to dissolve into the background once you know what it's doing. The Makefile, the linker scripts, the SDK sources: none of those go away. Studio is just a cleaner front-end onto them.