Tock on the RP2350

A dependency graph of one contributor's work on the Raspberry Pi Pico 2 and Pico 2 W.

I'm thoroughly confused. I guess the tests were added to the chip crate in one PR, used in a different PR for testing, but reverted from the second PR's main.rs changes before the second PR was merged. [...] It doesn't seem like there is a clear testing/bring up strategy for the rp2x PIO. — bradjc, reviewing #5126

That was fair, and this page is the answer to it. The work is a stack: one long chain of commits where each depends on the one before, and the early links are also open as small reviewable pull requests of their own. GitHub cannot express a stack when the branches live in a fork, so the same commit appears in two places and reads as the same work submitted twice.

Below is that stack, drawn. Click a pull request to light up exactly the commits it carries. Every node also shows how it is checked, which is the other half of the question: a pure code move is proved by the binary coming out with identical sections, logic is covered by a host test that fails without the change, and anything touching hardware is run on a board.

What Tock can drive on this chip

The list of blocks is not kept here. Every chip crate has a resets.rs holding one bit per resettable hardware block, so the chip's own source says what it has — 23 blocks on the RP2350, 21 on the RP2040. A cell is filled when a driver module for that block exists in the crate. That is presence, not completeness: it says a driver is there, not that every feature of the block is reachable.

  • 6driven upstream of 23 RP2350 blocks
  • 10driven on this fork the bench kernel, everything merged
  • 14driven on the RP2040 the older chip, better covered
  • a driver module covers it
  • the block is there, nothing drives it
  • this chip does not have the block
Hardware blockRP2040RP2350fork covered by
  • Pins
  • Buses
  • Analogue
  • Time
  • Moving data
  • Clocks
  • Chip services

Click a block to trace it down to its registers. The marks a block with a defect found here.

Read at upstream/master (a7b14ffaf) and bench/stepper-pico2w. Not shown because they are not blocks the reset controller names: chip, clocks, deferred_calls, interrupts, lib, mod, resets, ticks, watchdog, xosc.

From a system call to a register

Every rung comes from the tree. The driver number from the one enum that assigns them, the capsule from the board's own with_driver, the kernel interface from what that capsule imports, the chip driver from the crate implementing it, and the registers from its register_structs!. A rung with no answer is a fact about the tree rather than a gap here.

Analogue in ADC

Turns a voltage on a pin into a number. The joystick, the potentiometer and the on-chip temperature sensor all arrive through it.

  1. System call0x00005 capsules_core::adc · reachable on raspberry_pi_pico, raspberry_pi_pico_2_w, raspberry_pi_pico_w
  2. Capsulecapsules/core/src/adc.rs
  3. Kernel interfacekernel::hil::adc::Adc
  4. Chip driverchips/rp2350/src/adc.rs 57 lines
  5. Chip driverchips/rp2xxx/src/adc.rs 253 lines
  6. Peripheral base0x400A0000
  7. RegistersAdcRegisters0x000cs0x004result0x008fcs0x00Cfifo0x010div0x014intr0x018inte0x01Cintf0x020ints
  8. Inside a register
  9. On the RP2040chips/rp2040/src/adc.rs 48 lines

The board in your hand

The forty pins, and what each one is doing on a kernel you can actually flash. The header's shape is the board's form factor; every role on it is read from that board's own source, so a pin the kernel has taken shows as taken and a pin an application can drive shows the number it drives it by. The Pico 2 W's map is the bench kernel: the display, the stepper and the radio are all on it.

  • an application can drive it
  • the kernel has taken it
  • the radio has it
  • an analogue input
  • named by the board, withheld from applications
  • the board does not name it
  1. 1GP0UART0 TX, the console out
  2. 2GP1UART0 RX, the console in
  3. 3GNDground
  4. 4GP2SPI0 SCK, the display's clock
  5. 5GP3SPI0 TX, the display's data
  6. 6GP4an app can drive it as gpio 4
  7. 7GP5an app can drive it as gpio 5
  8. 8GNDground
  9. 9GP6an app can drive it as gpio 6
  10. 10GP7an app can drive it as gpio 7
  11. 11GP8an app can drive it as gpio 8
  12. 12GP9an app can drive it as gpio 9
  13. 13GNDground
  14. 14GP10an app can drive it as gpio 10
  15. 15GP11an app can drive it as gpio 11
  16. 16GP12an app can drive it as gpio 12
  17. 17GP13an app can drive it as gpio 13
  18. 18GNDground
  19. 19GP14an app can drive it as gpio 14
  20. 20GP15an app can drive it as gpio 15
  1. 40VBUSpower
  2. 39VSYSpower
  3. 38GNDground
  4. 373V3_ENboard control
  5. 363V3(OUT)power
  6. 35ADC_VREFboard control
  7. 34GP28analogue pad, prepared for the ADC
  8. 33AGNDground
  9. 32GP27analogue pad, prepared for the ADC
  10. 31GP26analogue pad, prepared for the ADC
  11. 30RUNboard control
  12. 29GP22an app can drive it as gpio 22
  13. 28GNDground
  14. 27GP21stepper phase
  15. 26GP20stepper phase
  16. 25GP19stepper phase
  17. 24GP18stepper phase
  18. 23GNDground
  19. 22GP17SPI chip select
  20. 21GP16an app can drive it as gpio 16

Not brought out to the header

Four of the chip's GPIOs are not on the forty pins. On a Pico 2 W they are the radio, which is why an application cannot have them and why the board has no fourth ADC channel.

  • GP23radio power
  • GP24radio gSPI, clocked by PIO
  • GP25radio chip select
  • GP29radio gSPI, clocked by PIO

What a process can actually call

Not what the kernel supports — what an application on each board can reach, read from every with_driver arm on it. A board can answer some driver numbers in its own file and pass the rest to a base platform in another crate, so both hops are followed — reading only the first reports one driver on a board that exposes ten.

  • 11Pico WRP2040, upstream
  • 5Pico 2RP2350, upstream
  • 9Pico 2 WRP2350, this fork
DriverPico WPico 2Pico 2 W number
  • alarm0x00000
  • console0x00001
  • led0x00002
  • gpio0x00004
  • adc0x00005
  • stepper0x00011
  • ipc0x10000
  • spi_controller0x20001
  • i2c_master0x20003
  • wifi0x30008
  • temperature0x60000
  • date_time0x90007

The stack

One column per pull request, 14 of them, holding all 29 commits. Depth down a column is stack order: an arrow runs from each commit to the one that needs it first. A dashed box is a commit that belongs to two pull requests, and a dashed orange arrow is a dependency that crosses between columns — together those are the whole reason the same work appears twice on GitHub. The word at the bottom left of a box is how that change is verified.

Click a pull request to light up exactly the commits it carries. Click a node for detail.

#5086merged#5104closed#5109merged#5112merged#5126approved#5140approved#5141in review#5150closed#5154closed#5156in review#5157in review#5158merged#5160closed#5165closedDeclare all 520 kB ofSRAMbuild only#5086Remove the boot-from-RAMlayoutunchecked#5104Fix the boot-from-RAMaddresseson silicon#5109Move PL022 SPI into ashared crateidentical sections#5112Add SPI to the RP2350on silicon#5112rp2xxx READMEunchecked#5112Retire the PIO examplesmoduleunchecked#5126 #5141Share the GPIO pad enumsidentical sections#5140Add RP2350 pad controlson silicon#5140 #5141Move the PIO driver intorp2xxxidentical sections#5141Add PIO to the RP2350on silicon#5141Add DMA to the RP2350host test#5141Move the gSPI driver intorp2xxxidentical sections#5141Fix the openocd configpathbuild only#5141Split Pico 2 into libraryand binaryidentical sections#5141Add the Pico 2 W boardon silicon#5141Hold DMA and PIO0 inperipheralson silicon#5141Bring up the CYW43439radioon silicon#5141Fix the RX FIFO joinhost test#5150 #5157Stop add_programpanickinghost test#5150 #5157Test the PIO arithmetichost test#5150 #5157Service every PIOinterrupt linehost test#5150 #5157Scope the irq flag to theblockhost test#5150 #5157Strip .stack from theflashed ELFon silicon#5154 #5156Name a make target thatexistsbuild only#5158Say ADC samples areleft-justifiedunchecked#5158Flash image fromtockloader, not a spliceon silicon#5160Same route on RP2040,picotool onlybuild only#5160Clear NVIC pending afterthe reseton silicon#5165

How each change is verified

  • host test6A test in the tree that fails if the change is reverted.
  • on silicon10Run on a real board and observed, not inferred.
  • identical sections5A pure move: the linked binary comes out with the same text, data and bss, so the change provably alters no behaviour.
  • build only4Checked by the thing building or linking, and nothing further.
  • unchecked4No automated check reaches this.

Nothing selected

Click any node in the graph.

The queue

The fork moves at whatever speed the work goes; upstream gets a trickle. Nothing is proposed until it is finished, demonstrated and small enough to review in one sitting, and only a few are in flight at a time — the constraint upstream is review throughput, not how fast patches can be written. Everything below exists as working code on the fork. The column it sits in is a decision about when to ask someone to read it, not about whether it is done.

Read from the working clones, so it is what exists rather than what was last written down. A branch is matched to its pull request by which commits they share, not by name.

On the fork7 — finished, not proposedIn review3 — waiting on a maintainerApproved2 — waiting to be mergedMerged4 — upstreamClosed5 — withdrawnrp2-adctock · 3 commitsasync/alarmlibtock-rs · 15 commits waitingpico2-platformlibtock-rs · 3 commits waitingstepperlibtock-rs · 3 commitsplatform-pico2libtock-rs · 2 commitsunittest-fakeslibtock-rs · 2 commitspico2-getting-startedbook · 10 commitschips: rp2040: five PIOfixes, and the driver's…#5157 · 5 commitsrp2: strip .stack from theELF that make program…#5156 · 1 commitboards: add Raspberry PiPico 2 W, with the…#5141 · 11 commitschips: rp2xxx, rp2350:share the pad control…#5140 · 2 commitschips: rp2040: PIOcleanups#5126 · 1 commitdocs: rp2: two places thedocumentation disagrees…#5158 · 2 commitschips: rp2350: add SPIdriver#5112 · 3 commitsboards: give theboot-from-RAM layout…#5109 · 1 commitboards: declare all 520 kBof SRAM on Pico 2#5086 · 1 commitboards: rp2350: clearstale NVIC pending after…#5165 · 1 commitrp2: build the flashedimage with tockloader…#5160 · 2 commitsrp2: strip .stack from theELF that make program…#5154 · 1 commitchips: rp2040: five PIOfixes, and the driver's…#5150 · 5 commitsboards: remove deadboot-from-RAM layout#5104 · 1 commit

What the columns mean

A unit of work moves left to right. The first column is not a backlog being worked through — everything in it is finished, demonstrated and pushed. It is waiting to be asked for, a few at a time, because the constraint upstream is review throughput. A dashed arrow is one piece of work that cannot go until another lands.

Nothing selected

Click any card above.

In review now — 4

  • tock · pico2w-typed11 commits#5141in review
    The Pico 2 W board and the radio.
  • tock · rp2-pio-tests5 commits#5157in review
    Five PIO fixes and the driver's first host tests. Four defects demonstrated by tests that fail on unmodified upstream; one of them panics the kernel.**Open as #5157**, in review, the same five commits. #5150 was the first attempt and was closed 2026-09-07 for a body written by AI under a checkbox promising it would not be, which is the one part of that close not worth contesting. **#5150 cannot be reopened, and that was tested rather than reasoned about** -- it refuses in the browser and through the API with `Could not open the pull request`, as #5154 does, with `viewerCanUpdate` and `viewerDidAuthor` true on both. #5150 was never force-pushed and refuses identically, which is what ruled out the force-push blamed for #5154; the only property the two share is `isDraft: true`. It is moot in any case, because this work is already open as its successor. Two earlier notes here argued that #5150 was reopenable where #5154 was not, and a third called the question untested; all three were written without reading the test that had already been run. #5157 carries a rewritten body of its own, 2,233 characters against a house median of 1,062 measured 2026-09-10 across the last 200 merged pull requests, so roughly twice the house length rather than the 5,714 this note used to claim for a body that is in fact 2,203.
  • tock · rp2-make-program-fix1 commit#5156in review
    Fixes #4770: `make program` cannot flash an application on any RP2 board, because splicing one in gives the (NOLOAD) `.stack` segment file content for SRAM and both UF2 converters refuse the result. One `objcopy -R .stack` per rule, five rules, four boards.**Open as #5156**, head `223d878c0`. **Both approvals were dismissed on 2026-09-10** by a force-push that corrected the Makefile comments and the commit message: they said the `--set-section-flags` step caused the bad `p_filesz`, and it does not -- a bare `objcopy` does the same thing, and the cause is the empty `.relocate` section. bradjc and ppannuto need re-asking; lschuermann re-approved #5158 after exactly this. The first attempt, #5154, was closed the same day it was opened -- for a placeholder description, not for the change -- and could not be reopened afterwards because the branch had been force-pushed. Its evidence is the reporter's own RP2040 run plus a `program-openocd` A/B on RP2350 silicon. Four Makefiles, thirteen added lines, no Rust. Touches `boards/raspberry_pi_pico_2/Makefile`, which #5141 also edits -- different regions of the file, so it merges cleanly either order.
  • tock · rp2-pio-prep1 commit#5126approved
    PIO cleanups.

Finished, not proposed yet — 7 branches, 38 commits

  • tock · rp2-adc3 commits
    Moves the SAR ADC driver into the shared rp2xxx crate, adds the RP2350's, and wires it up on the Pico 2 so driver 0x00005 answers. Three commits, the same shape as #5112, which merged.Ready. Unblocks the whole analogue tier of the breadboard kit — joystick, potentiometer, light sensor, thermistor — none of which needs new wiring.
    Evidence for a pull request — 3 commits

    Cut from upstream/master at 0cc36ca6c, 2 commits behind · 8 files changed, 431 insertions(+), 238 deletions(-)

    What it adds
    The RP2350 has no ADC driver at all — nor I2C nor PWM; the port is thinner than the RP2040's across the board. This moves the SAR ADC into the crate the two chips share and adds the RP2350's half: base address, channel set, and the interrupt routing.
    Why the shape is safe
    It is the shape that already merged. #5112 moved the PL022 SPI into `rp2xxx` and added the RP2350's the same way, in the same crate, with the same argument. No new driver number, no new API surface, no question about whether the feature is wanted.
    What actually differs between the chips
    Register offsets are identical. The base moves to 0x400a0000, CS.AINSEL is four bits rather than three and CS.RROBIN nine rather than five, and the channel count depends on the package — QFN-60 bonds four inputs with the temperature sensor on channel 4, QFN-80 bonds eight with it on channel 8. The channel set is a trait the chip crate implements, so the package question is answered where it belongs.
    Two things changed rather than moved
    The driver used to decide the temperature sensor's channel with `if *channel as u32 == 4`, a literal that is right for the RP2040 and a QFN-60 RP2350 and wrong for a QFN-80 one. And a field written on every sample and never read anywhere has gone.
    Sizes
    Not identical, and could not be: moving code across a crate boundary changes the partition the optimiser starts from. .text on the three RP2040 boards that build it goes 95392 to 95244, 98616 to 98696, and 91664 to 91588. Behaviour unchanged — same registers, same write order, same interrupt handling.
    The board half, and why it is in the same branch
    A chip driver with no consumer invites the question of what it is for, so the third commit wires it on the upstream `raspberry_pi_pico_2`: mux, four channels, syscall driver. GPIO26-28 reach the header as ADC0-ADC2 and GPIO29 measures VSYS through a divider. Those four leave the GPIO syscall list, commented out exactly as `nano_rp2040_connect` already does it, because a pin cannot usefully be both — an application driving GPIO26 as an output while another samples channel 0 is a short through the pad driver, and neither driver can see the other to refuse.
    A pad trap the chip driver cannot fix
    Out of reset an RP2350 pad has its isolation latch set and its pull-down enabled. The pull-down is the one that bites: across an analogue source it forms the lower leg of a divider, and the reading that produces is the one diagnosed as a bad sensor. **That degraded case was not reproduced here** -- what was measured is the configured case below, which reaches both rails. The pad belongs to GPIO, not to the converter, so the board configures it: `set_function(NULL)` clears the isolation latch, `PullNone` removes the pull, `deactivate_pads` switches the digital input buffer off. The same three steps `adc_gpio_init` takes in the C SDK. `nano_rp2040_connect` does none of them today.
    Read on silicon, and the pad sequence is measured rather than argued
    A joystick on a Pico 2 W reads 0 to 65408 on one axis and 0 to 65520 on the other, out of 65535 — both rails, both axes. That is the check that matters for the pad configuration: 65408 and 65520 against a full scale of 65535 are both rails. A pad left with its reset pull-down would sit inside a divider and could not reach them, though that degraded case was not reproduced here. “The pads are configured” and “the range reaches the rails” are different claims and only the second is checkable. Left-justification is confirmed in the same run: every raw reading is a multiple of sixteen.
    What that covers, and what it does not
    The chip driver, the ADC FIFO interrupt routing and the three-step pad sequence are exercised on silicon. The `raspberry_pi_pico_2` board file in this branch is not — the measurement is on a Pico 2 W, whose board file is #5141 and not upstream, and there is no plain Pico 2 here to run it on. The two boards share the chip half and differ only in which channels they expose. Builds, fmt and clippy clean, and .text on raspberry_pi_pico_2 goes 65092 to 66708 by `size -A`, so the driver is demonstrably in the binary rather than optimised out.

    Commits

    1. boards: raspberry_pi_pico_2: expose the ADC to userspace
    2. chips: rp2350: add the SAR ADC
    3. chips: rp2040: move the SAR ADC driver into the shared rp2xxx crate
  • libtock-rs · async/alarm15 commits
    Futures over the alarm and console drivers, a single-task executor, select, and fakes that model cancellation. Meant to become two pull requests, but it is not two branches yet: the two unittest commits sit inside this one and have to be lifted out first.Hard order, not a preference. The async tests call fake::Alarm::new_deferred and fake::Console::new_deferred in three places, and those constructors are exactly what the unittest commits add — so either the unittest fixes land first and the async work builds on them, or the async pull request carries both commits itself. It cannot go first and cannot go alone. The branch still carries both commits inside it, and which way that resolves depends on how unittest-fakes is received: rebase to drop the duplicates if it merges, ship as-is if it does not. Deliberately not decided in advance, because guessing means doing the work twice.
    Evidence for a pull request — 15 commits

    Cut from upstream/master at 469d9a037 · 22 files changed, 2080 insertions(+), 22 deletions(-)

    What it adds
    A Future and executor layer over Tock's syscalls: futures for the alarm and console drivers, a single-task executor with `block_on`, `join` and `select`, and unittest fakes that model alarm expiration and an outstanding console receive.
    How it was verified
    `make test` green. On a Pico 2 W running the Pico 2 W kernel, every 500 ms await lands within a few hundred microseconds of its deadline, including the two immediately after a cancelled five-second sleep — 500250 and 500249 ticks. That discriminates working cancellation from both a leaked upcall, which would return instantly, and a timer left armed, which would take five million.
    It is not one pull request
    Intended as two, and it is not two branches yet: the two unittest commits sit inside this one and have to be lifted out first. The order is forced rather than preferred — the async tests call `fake::Alarm::new_deferred` and `fake::Console::new_deferred` in three places and those constructors are what the unittest commits add.
    Known gap
    The console half of concurrency is untested, and cannot be tested until the UART defect above is fixed. The failure is in the kernel, not in the futures.

    Commits

    1. console: record why the abort's return value carries no information
    2. examples: exercise select on hardware in async_probe
    3. async: add join and select
    4. unittest: model an outstanding receive in fake::Console
    5. unittest: model alarm expiration so cancellation can be asserted
    6. examples: add async_probe, an on-device self-test for the async stack
    7. make: build and lint the async examples in the test gate
    8. examples: add async_sleep, the first app built on the async stack
    9. syscalls_tests: clean up the async_call test fixtures
    10. syscalls_tests: test async_call directly
    11. platform: extract the async soundness argument into one audited place
    12. console: add a Future-returning read
    13. alarm: document the invariant the waker swap leans on
    14. alarm: stop the alarm when a Sleep is cancelled
    15. alarm: add a Future-returning sleep and a block_on executor
  • libtock-rs · pico2-platform3 commits
    The original three commits, now superseded: what is ready went to platform-pico2, and what is left is the Pico 2 W row.Waits for its board. The W platform row names raspberry_pi_pico_2_w, which is #5141 and not upstream, so this cannot go until that lands.
    Evidence for a pull request — 3 commits

    Cut from upstream/master at 469d9a037 · 3 files changed, 11 insertions(+), 3 deletions(-)

    What it adds
    Build platform entries for the Raspberry Pi Pico 2 and Pico 2 W, and a build error that named neither the platform nor the file to edit.
    Where the addresses came from
    Read off a linked kernel with nm rather than off the linker script, because the application region is what the kernel leaves rather than what the board reserves. For the Pico 2 W: _sapps 0x10090000, _eapps 0x100d0000, _sappmem 0x20005c04, _eappmem 0x20082000. The RAM row starts at 0x20020000 and +392K lands exactly on _eappmem.
    Verified
    Applications built through these rows load, run and print on a Pico 2 W. Both boards' numbers were reproduced independently in two sessions against separately built kernels that came out to identical text and bss.
    It should be split before it goes
    Two of its three commits are ready: the Pico 2 row and the error-message fix. The third adds a `raspberry_pi_pico_2_w` row, and that board is not upstream yet — it is an open pull request in the kernel repository. A reviewer meeting a platform entry for a board they cannot find would be right to ask, and the answer is another repository's unmerged work. Send the Pico 2 row now; the W row follows the board.

    Commits

    1. runner: say which platform has no architecture mapping
    2. Add the Raspberry Pi Pico 2 W as a build platform
    3. Add the Raspberry Pi Pico 2 as a build platform
  • libtock-rs · stepper3 commits
    The userspace half of the stepper: `libtock_stepper` and an example that turns a revolution each way.**The kernel half landed in `main` on 2026-09-13**, so this no longer waits on it. It remains the only half still on its own branch.
    Evidence for a pull request — 3 commits

    Cut from upstream/master at 469d9a037 · 6 files changed, 364 insertions(+)

    What it is
    `libtock_stepper` — exists, step_forward, step_reverse, stop — wired into the umbrella crate, plus an example turning one revolution each way. The interval is a newtype in microseconds so a caller cannot silently pass milliseconds.
    Gate
    Full gate green, seven tests under both cargo test and miri. The fake lives in the crate's own tests rather than in libtock_unittest, since there is no upstream capsule to model yet.
    A limitation worth knowing
    From the blocking API the owner cannot reach `stop`: `step_forward` blocks in a yield loop for the whole run, so the only process allowed to stop the motor is inside a call that will not return until it finishes. Stop is reachable from an upcall handler or from the async version. That is an argument for the async stepper rather than a defect in either half.
    Verified on silicon
    Drove the capsule on a Pico 2 W: 4096 steps forward, 4096 reverse, first flash, turning a real 28BYJ-48.

    Commits

    1. stepper: document Reserve for a non-owner stop
    2. stepper: document half-steps, BUSY, and what a stop reports
    3. stepper: add a userspace driver for the stepper capsule
  • libtock-rs · platform-pico22 commits
    The Raspberry Pi Pico 2 build platform row, plus a build error that named neither the platform nor the file to edit.Ready. Split out from pico2-platform so that it carries nothing depending on an unmerged board.
    Evidence for a pull request — 2 commits

    Cut from upstream/master at 469d9a037 · 3 files changed, 9 insertions(+), 3 deletions(-)

    What it adds
    A build platform row for the Raspberry Pi Pico 2, and a build error that previously named neither the platform nor the file to edit — registering a platform takes three files that do not know about each other, and missing the third failed with "Failed to determine ELF's architecture".
    Where the addresses came from
    Read off a linked kernel with nm rather than off the linker script, because the application region is what the kernel leaves rather than what the board reserves. Re-checked against a kernel built from current master: _sapps 0x10040000, _eapps 0x10080000, _eappmem 0x20082000, so the row's flash 0x10040000 + 256K and RAM 0x20020000 + 392K both land exactly. Applications built through this row load, run and print on real hardware.
    Gate
    Full gate green at ce33db6, the same suite: cross-target example builds, workspace tests, fmt, clippy including riscv32imac, and miri under strict provenance.
    Why the Pico 2 W row is not here
    It names a board that is not upstream. Only boards/raspberry_pi_pico_2 exists at master; the W board is #5141 and still open. That row waits for its board rather than asking a reviewer to accept a platform entry they cannot resolve.

    Commits

    1. runner: say which platform has no architecture mapping
    2. Add the Raspberry Pi Pico 2 as a build platform
  • libtock-rs · unittest-fakes2 commits
    Two fixes to the test fakes: model alarm expiration so cancellation can be asserted, and model an outstanding receive in the console fake.The first thing proposed anywhere in libtock-rs, and deliberately the smallest — it is independently useful, hard to argue with, and the async series cannot go until it does. **Agreed as the first probe on 2026-08-31 and still unsent as of 09-09.** Zero pull requests have ever been opened in that repository from here, so this one is about establishing a relationship as much as landing a fix.
    Evidence for a pull request — 2 commits

    Cut from upstream/master at 469d9a037 · 4 files changed, 254 insertions(+), 20 deletions(-)

    What it fixes
    Two gaps in libtock-rs's test fakes. `unittest/src/fake/alarm/mod.rs` did not model expiration, so a test could not assert that cancelling an armed alarm actually stopped it; the console fake in `unittest/src/fake/console/` did not model an outstanding receive. Command 3 was genuinely unimplemented in both, which is a fidelity bug in the fakes independent of anything built on them.
    Why it is first
    The smallest and least arguable thing in the queue, and it blocks the async series: those tests call `fake::Alarm::new_deferred` and `fake::Console::new_deferred` in three places, and these commits are what add those constructors.
    Gate
    Full gate green at f85f019: examples built for thumbv7em and riscv32imc, the workspace test run, fmt, three clippy passes including riscv32imac, and workspace-wide miri under strict provenance. 101 cargo tests and 90 under miri in libtock_unittest alone.
    Provenance
    Extracted from the async branch rather than cherry-picked: the originals also touched the async crate's own tests, and those stayed behind. Four files, all under `unittest/`, nothing outside it. Builds and tests standalone on master — 95 unit tests and 2 doc tests pass.
    How the fakes were validated, in three parts
    The alarm fake models command 3 clearing the deadline, which is what was measured on a Pico 2 W: an armed five-second sleep dropped, then a 500 ms sleep timing 500261 ticks — discriminating a working stop from both a leaked upcall, which would return instantly, and a timer left armed, which would take five million. The console fake answering success to an abort either way matches the capsule source and an instrumented kernel trace. The console fake's model of a receive that stays outstanding is unvalidated, and stays that way until the UART defect is fixed, because no environment currently holds a console read open.

    Commits

    1. unittest: model an outstanding receive in fake::Console and implement abort
    2. unittest: model alarm expiration and implement stop
  • book · pico2-getting-started10 commits
    A getting-started page for the Pico 2. The book has no Pico coverage at all.Written and green; waiting on its pull request description.
    Evidence for a pull request — 10 commits

    Cut from origin/master at 760025b0d · 3 files changed, 240 insertions(+)

    What it adds
    A getting-started page for the Raspberry Pi Pico 2. The book currently has no Pico coverage at all.
    Verified
    The documented route was walked end to end on real hardware, and five things that were wrong got fixed in the process.
    Caveat to disclose
    The BOOTSEL flashing route is unverified on macOS, because the board is never connected to the development machine here. The page tells the reader to check where the volume mounted before naming it, so the literal path is illustrative rather than load-bearing, and the bootrom behaviour underneath is documented and host-independent.

    Commits

    1. setup: the by-hand UF2 command ran in the wrong directory
    2. setup: the headless mount commands failed if you actually ran them
    3. setup: a headless Linux box mounts nothing, and the page assumed it did
    4. setup: the console echoes, so say what actually proves the input wire
    5. setup: cite the page that actually makes the no-reflash promise
    6. setup: tighten the Pico 2 page for density and reading order
    7. setup: cut the Pico 2 page's applications section
    8. setup: refresh the Pico 2 page's size line
    9. setup: link the Pico 2 page's first mention of TBF
    10. setup: add a getting started page for the Raspberry Pi Pico 2

Never going upstream — 12

Bench harnesses and teaching material. Listed so that nobody browsing the fork has to guess which branches are waiting to be proposed.

  • tock · learning/series188 commits
    The merged tree the course cites. The nine chapters moved to the site repository and are published at /read/; what is left here is the kernel work they were written against, kept because every chapter pins one commit on it and no other tree resolves those citations — upstream resolves 7 of the 14, #5140 six, #5141 seven, and the bench kernel, which contains all of it, also seven, because each later branch shifts the same file further along.Not a queue item. Retires itself when the chapters stop citing line numbers, or when #5140 and #5141 land and the citations are re-pinned to upstream.
  • tock · main92 commits
    **The trunk.** Renamed from `rp2350-distro` and made the fork's default branch on 2026-09-13, so landing on the repository shows our work rather than upstream's. `master` stays pinned to upstream as the clean reference that rebases and A/B baselines are cut from. Our own flavour of Tock for the RP2350, and the branch all work lands on. **Head `fab074b7c`, 76 commits ahead of upstream after all ten of 2026-09-13's branches merged in.** Kept current with a merge rather than a rebase so the hand-resolved conflicts survive.Not a staging area and not intended for upstream as a branch; pull requests are cut from the feature branches instead. Merged current with upstream on 2026-09-12, after which all six RP2 boards build and the 19 PIO tests pass. One of its eleven merges needed manual three-way resolution -- git merges the moved PIO driver clean and silently reverts five fixes and deletes the test module -- which is why this is merged rather than rebased.
  • tock · bench/stepper-pico2w23 commits
    The Pico 2 W branch, the stepper capsule and a board wiring for GPIO 18-21, merged so the motor can actually be driven. A vehicle, not a contribution: the capsule is cut from master and the board only exists on #5141, so there is nowhere upstream the two currently meet.
  • tock · pico2w-radio-bench16 commits
    Every commit titled NOT FOR UPSTREAM: it starts the radio from the board so a scan can be driven without an app.
  • tock · bench/uart-abort-pico2w15 commits
    The vehicle for the 2026-09-13 silicon A/B: `pico2w-typed` with `rp2-uart-abort-fix` merged and nothing else, so the only difference on the RP2350 path is where `rx_status` returns to Idle. Both kernels build to text=323116.Not a contribution -- the board only exists on our branches, so there is nowhere upstream the two meet. Kept so the measurement can be repeated rather than only cited, the same reason `bench/uart-fix-at-pinned-rev` is kept. Transcripts at `learning/bench/uart-abort-silicon/`.
  • tock · rp2-pad-controls2 commits
    Shared pad enums and the RP2350 pad controls. **SUPERSEDED: `main` already shares them** -- both gpio.rs files reference `rp2xxx::pads` nine times each, with the casing corrected to `Drive12mA` where this branch still spells it `Drive12ma`.Merging it on 2026-09-13 broke all six RP2 boards with 19 errors: the conflicted files were resolved to `main`'s superset, but `chips/rp2350/src/gpio.rs` merged **cleanly** carrying the old casing. The merge was dropped. **A clean merge is not a safe merge** -- the conflict was visible and the incompatibility was not.
  • tock · bench/reclaim-leak-demo1 commit
    Reproduces the GPIO reclaim leak on a board.
  • tock · bench/uart-fix-at-pinned-rev1 commit
    The UART abort fix applied at the revision libtock-rs pins, which is where the A/B behind rp2-uart-abort-fix was actually run — the reproduction application does not load on a master kernel. Kept so the measurement can be repeated rather than only cited.
  • tock · rp2350-spi-bench1 commit
    A bench harness that drives the SPI loopback.
  • libtock-rs · hw/pico2w-async59 commits
    The two branches above merged together, as a vehicle for running on hardware — and, for now, the only home of `examples/console_read_busy.rs`, the QEMU reproduction of the UART defect.The branch itself is not for upstream, but the reproduction on it is meant to be run by other people, so it needs a home that is. Where it goes depends on whether it travels with a defect report or stands alone as a libtock-rs example.
  • libtock-rs · bench/reclaim-leak5 commits
    The two apps that demonstrate the reclaim leak.
  • libtock-rs · kit-examples2 commits
    Loopback and pin-walk apps written to exercise the bench.

Where the open branches collide

Derived by comparing the file list of every open pull request against every other, so it cannot drift from what the branches do.

  • #5126 and #5141 both change chips/rp2040/src/pio.rs, chips/rp2040/src/pio_gspi.rs, chips/rp2040/src/pio_pwm.rs and 1 more — whichever lands first, the other rebases.
  • #5126 and #5157 both change chips/rp2040/src/pio.rs, chips/rp2040/src/pio_gspi.rs — whichever lands first, the other rebases.
  • #5140 and #5141 both change chips/rp2040/src/gpio.rs, chips/rp2350/src/gpio.rs, chips/rp2xxx/src/lib.rs and 1 more — whichever lands first, the other rebases.
  • #5141 and #5156 both change boards/raspberry_pi_pico_2/Makefile — whichever lands first, the other rebases.
  • #5141 and #5157 both change boards/raspberry_pi_pico_w/src/pio_gspi_component.rs, chips/rp2040/src/chip.rs, chips/rp2040/src/pio.rs and 1 more — whichever lands first, the other rebases.

Pull requests

4 merged, 5 open.

#5165closed

boards: rp2350: clear stale NVIC pending after the peripheral reset

+7 / −01 files2026-09-13
#5160closed

rp2: build the flashed image with tockloader instead of splicing an ELF

+159 / −1269 files2026-09-13
#5158merged

docs: rp2: two places the documentation disagrees with the code

+7 / −33 files2026-09-09
#5157in review

chips: rp2040: five PIO fixes, and the driver's first host tests

+502 / −2634 files2026-09-08
#5156in review

rp2: strip .stack from the ELF that make program flashes

+13 / −04 files2026-09-08
#5154closed

rp2: strip .stack from the ELF that make program flashes

+21 / −04 files2026-09-07
#5150closed

chips: rp2040: five PIO fixes, and the driver's first host tests

+502 / −2634 files2026-09-07
#5141in review

boards: add Raspberry Pi Pico 2 W, with the CYW43439 radio working

+4423 / −255038 files2026-09-03
#5140approved

chips: rp2xxx, rp2350: share the pad control enums, add the RP2350 pad controls

+51 / −194 files2026-09-03
#5126approved

chips: rp2040: PIO cleanups

+136 / −2804 files2026-08-29
#5112merged

chips: rp2350: add SPI driver

+778 / −63715 files2026-09-01
#5109merged

boards: give the boot-from-RAM layout addresses that link

+43 / −276 files2026-09-03
#5104closed

boards: remove dead boot-from-RAM layout

+0 / −305 files2026-08-24
#5086merged

boards: declare all 520 kB of SRAM on Pico 2

+1 / −11 files2026-08-20

Defects found

Each demonstrated before it was written down — by a test that fails without the fix, or by an instrumented kernel on a board. The UART pair needs no board at all: make qemu-example EXAMPLE=console_read_busy against libtock-rs's own pinned kernel prints read -> 0 bytes, Err(BUSY) on an affected build, where a correct one would leave the read outstanding.

  • fix proposedPIO: the RX FIFO join never happened #5150Joining the FIFOs silently did nothing, so a program relying on the depth dropped words.
  • fix proposedPIO: add_program panicked on half its own range #5150A valid load address panicked the kernel instead of returning an error.
  • fix proposedPIO: three of four interrupt lines unserviced #5150Only one state machine's interrupt was ever handled.
  • fix proposedPIO: an interrupt flag scoped to the wrong thing #5150The flag belongs to the block; treating it as per-state-machine mis-attributes interrupts.
  • fix written, not sentUART: an aborted receive tears down every other receive rp2-uart-abort-fixThe abort completion calls the client back before marking the receiver idle, so the multiplexer's restart is refused and it ends every device's receive instead. The same code is in three chip drivers, and eleven boards pair a process console with the userspace console on one multiplexer. Reproducible under QEMU with no hardware, on hifive1, at the eighteen-month-old revision libtock-rs already pins — and no application can avoid it. Removing the delay before the read, and then issuing the read before any other system call, both still fail: the process console's prompt prints before the application's first line, so the receive is already armed before the application's first instruction. “The app read too early” is not an available explanation. What the reproduction still lacks is the opposite control, a kernel with no process console on that multiplexer, which is a board change rather than an application one.
  • fix written, not sentUART: a failed receive hands back the wrong static buffer rp2-uart-abort-fixA virtual device propagates the multiplexer's error with `?`, and that error carries the multiplexer's buffer rather than the caller's. Two static buffers change owners and the multiplexer's slot is left empty for the life of the board.
  • fix written, not sentUART: the teardown drops a buffer it cannot deliver rp2-uart-abort-fixWhen a restart fails the mux takes every device's buffer, but only returns it to devices still in the Receiving state — so a device that had aborted a read loses its buffer permanently. Found while fixing the two above.
  • fix written, not sentAny process can panic a Pico 2 kernel through GPIO rp2350-gpio-irq`IO_IRQ_BANK0` is declared in `interrupts.rs` and referenced nowhere else, so `service_interrupt` returns false for it and the chip panics with `unhandled interrupt 21`. Enabling a GPIO interrupt is command 7 of the GPIO syscall driver, so **any application on the upstream raspberry_pi_pico_2 can take the kernel down**, on any of the twenty-four pins that board exposes. Reproduced on that board with buttons on GP14 and GP15: it panics before the change and reports every press and release after, four lines.
  • fix written, not sentA Pico 2 panics at boot on an interrupt nothing claims rp2350-stale-nvic-pendingIRQ 14 is USBCTRL_IRQ and this chip crate has no USB driver, so nothing services it and the first pass through `service_pending_interrupts` panics. The pending bit is a leftover, not a live interrupt: read over SWD in the panic handler, NVIC ISPR bit 14 is set while the USB block's MAIN_CTRL, INTR, INTE and INTS are all zero. The bootrom runs that controller with its interrupt enables set, so a source still asserting when `Chip::init()` clears pending re-latches at once, and the peripheral reset afterwards silences it without taking the latched bit back.
  • not filedA stopped process never gets its GPIO reclaimedThe pin stays driven forever. A sibling capsule already has the fix, which makes this a consistency bug. Five of forty-five capsules are affected.
  • not filed`make program` cannot flash an app on the Pico 2The objcopy step gives the stack segment a file size it should not have; picotool then refuses the image and leaves a zero-byte UF2 behind.

Findings

Pico problems worked through far enough to be useful to whoever fixes them — some reported by other people, some our own changes written up so a reviewer can check them. Each page cites the tree at a commit, quotes tool output rather than summarising it, and ends with what it does not establish — which for the USB one is most of it. All of them, with how to read one.

  • open#4770 — make program builds an ELF that no UF2 tool will takeSplicing an app into the kernel makes objcopy lay the segments out again, and the (NOLOAD) stack segment comes back claiming 5,376 bytes of file content for RAM. The reported cause is not what is happening, and the workaround in the issue strips a section that is only safe to strip by accident. Six candidate fixes measured against a kernel built from upstream; the one that works is a single objcopy line, and it is A/B verified on all four RP2 boards by running their real make targets.Reported by potto216. The issue on GitHub.
  • closed#5160 — a flashing route with no ELF to get wrongThe reviewer who approved the four-line fix for #4770 asked for something else in the same sentence: that these boards stop splicing an application into the kernel ELF and use tockloader local-board instead. That route was run end to end -- assembled by tockloader, loaded three ways (SWD, the bootloader, probe-rs), with two applications co-resident and live in the kernel's own process list. It cannot hit the defect because it never opens an ELF. He then asked for it in the shape esp32-c3-devkitM-1 already uses, which retired the cost this page had led with: make no longer takes an APP at all. Four costs remain, and the tool swap on the RP2040 boards is the one a reviewer would stop on. Written up under #5156 until that change had a number of its own; /findings/5156/ redirects.Opened by bigmark222. The pull request on GitHub.
  • closed#5165 — a pending interrupt survives the reset that silenced itIRQ 14 is USBCTRL_IRQ, and this chip crate has no USB driver at all, so nothing can claim it. The pending bit is a latch rather than a live interrupt: read over SWD in the panic handler, NVIC ISPR bit 14 is set while the USB block reads byte-for-byte identical to a boot that did not panic. The bootrom hands over a controller that is enabled, addressed and permitted to interrupt, so `Chip::init()`'s clear cannot stick and the peripheral reset afterwards silences the source without taking the latched bit back. Erasing the first flash sector reproduces it on demand, and one statement removes it -- measured A/B/A on silicon.Opened by bigmark222. The pull request on GitHub.
  • open#5157 — a PIO interrupt flag delivered to the wrong clientA PIO block raises eight IRQ flags that belong to the block, and the driver delivered flag n to state machine n's client -- an association the hardware does not have and the datasheet invites by naming those bits SM0-SM3. A flag whose state machine has no client is never cleared, so the peripheral keeps asserting and the kernel spins. Invisible in the tree because the only user sits on the diagonal. Four more defects alongside it, and the misrouting plus its fix were both run on silicon.Opened by bigmark222. The pull request on GitHub.
  • open#5153 — EP0 IN is armed at bus reset and never taken backThe RP2040 USB driver hands EP0's IN buffer to the controller during bus reset, with a length of 64 and a PID of DATA0 and nothing queued to send. A SETUP packet does not take it back, so the first control read can be answered out of a buffer nobody filled. Two of the three claims in the issue's own AI analysis hold up; the third points at a delay already in the code. Not tested -- there is no RP2040 board here, and rp2350 has no USB driver to stand in for one.Reported by potto216. The issue on GitHub.

Written with AI assistance, disclosed on each page, which is also why the evidence is arranged to be checked rather than believed.

Run on hardware

A Raspberry Pi flashes the board and holds its serial line, so the machine that builds never touches the hardware.

  • A boot panic reproduced on demand, and removedErasing the first flash sector drops the bootrom into its own USB device, and resetting out of that live session panicked thirteen times in thirteen where the trigger had been unknown. The one-statement fix was then measured A/B/A on the same board: six panics, none, six again.
  • Pico 2 W scans WiFiFirmware up over PIO and DMA, MAC read from the radio's OTP, scan completed.
  • SPI loopbackA 32-byte pattern written and read back through the RP2350 SPI driver.
  • Boot from RAMThe layout from #5109, booting.
  • Two co-resident appsApplications load, run and print, which is what makes any userspace claim checkable.
  • Async sleep and cancellationA 500 ms await lands within a few hundred microseconds of its deadline, including immediately after a cancelled five-second sleep. That distinguishes working cancellation from both a leaked callback and a timer left armed.
  • PIO and DMA register readbackThe interrupt block's offset, the state machine count and the instruction memory size, read off the chip.

Test plan

  • #5152 rp2x PIO: a test plan

The course

Nine chapters on how the kernel works, written while learning it on this board and checked the same way this page is — every figure is something to drive rather than a picture to read, and every claim about the tree is verified against a commit the chapter names. It starts at what a register is and ends at grants.

Read it →

Its own gate runs 35 assertions on the cover and a suite per chapter. Sources are under learning/; read/ is what is served, and a check here fails if the two disagree.

Downstream

  • blockedAsync userspaceA Future and executor layer over Tock's syscalls in libtock-rs, validated on a Pico 2 W running the Pico 2 W kernel. The alarm half is done and measured. The console half is blocked on the UART defect being fixed, and that question is now closed rather than open: QEMU was the last candidate for an environment where a userspace console read stays outstanding, and it reproduces the defect too.
  • readyA Pico page for the Tock bookThe book has no Pico coverage at all. Written and pushed; the pull request is not open yet.
  • draftedNine chapters on how the kernel worksFrom what a register is through grants and the memory protection unit, written while learning the codebase.

Not done

  • Give the QEMU reproduction a home someone can reach`examples/console_read_busy.rs` currently exists only on a branch marked never-for-upstream, which is the wrong address for the one artefact here that a maintainer is meant to run themselves. It travels with the defect report, or goes up on its own as a libtock-rs example, but it cannot stay where it is.
  • Open the first libtock-rs pull request`unittest-fakes` was extracted, is gate-green and has been pushed since 2026-08-31. It is still unsent. **Zero pull requests have ever been opened in tock/libtock-rs from here** -- not zero merged, zero opened -- and the async series cannot go until this one does, because its tests call the constructors these two commits add. Two commits, the smallest ask available anywhere in the queue, and the only item whose value is opening a relationship rather than closing a defect. It is also the natural task for a second session, since nothing about it touches the RP2 work.
  • File the four unfiled defectsEach is demonstrated and none is filed. The constraint is review throughput, not the work.
  • Fix the reclaim leakThe sibling capsule already shows what the fix looks like.
  • Two RP2 Makefile bugs found while replacing the flash route`make flash-openocd` on nano_rp2040_connect exits 0 having flashed nothing. Its recipe references OPENOCD and OPENOCD_OPTIONS, which neither that Makefile nor boards/Makefile.common defines, so the line begins with `-c`; make reads the leading dash as ignore-errors, strips it, runs a command called `c` and swallows the result -- `c: command not found`, `Error 127 (ignored)`, exit 0. Established by expanding the recipe rather than by grepping for the variable, which had wrongly implicated two nRF boards that never use it. It is the only one of the eight flash-openocd recipes in the tree that breaks this way, and the only one of the eight not declared .PHONY. Separately, raspberry_pi_pico/Makefile has said it builds the Pico Explorer Base since 049db0bd4. Both are upstream, both are one line, and neither belongs in the local-board change.
  • The RP2350 has no USB driver at all`chips/rp2350/src/` has no `usb.rs`, so `USBCTRL_IRQ` has no owner. That is why a stale pending bit on it took the kernel down rather than being handled harmlessly the way it is on the RP2040, whose crate does have one. The boot panic that exposed this is fixed on `rp2350-stale-nvic-pending`, in the queue above, and **that fix does not close this**: any peripheral with no driver can still strand a pending bit, and a Pico 2 still cannot be a USB device at all.
  • A userspace driver for PIOThe RP2's most distinctive peripheral, and no process can reach it.
  • Hardware CI for the RP2 boardsThe project's testbed runs one board and never on pull requests. Named as a dependency in #5152 rather than promised.