- Asked
- A reviewer approved #5156 and said he would rather these boards moved
to
tockloader local-board; the answer on the thread was that it would be looked into for a later pull request. This page is that look. Both halves of that exchange are quoted below, and under them the second ask that came once this change was open. - Route
- Build the kernel binary, let tockloader keep a flash image file, install
applications into it as
.tabarchives, and hand the flat image topicotool,openocdorprobe-rs. No ELF is spliced, so nothing recomputes a program header.makenever takes an application:make initrecords the board,tockloader install --local-boardadds the application, andmake flashwrites the image — the shapeesp32-c3-devkitM-1already uses. - Status
- runs on silicon A kernel and an application assembled this way were put onto a Pico 2 W every way the rules offer — over SWD, through the bootloader and with probe-rs — and the process is live in the kernel's own process list — Section 3. Four costs come with it, and one of them is a tool swap on the four RP2040 boards — Section 5. The change itself is #5160 — five boards, two commits.
the ask On the pull request that fixes it the short way
I would rather see this move to use `tockloader local-board`, but if this improves things then fine
Quoted with its backticks because that is how it was written. The approval and the alternative arrived in the same sentence, so nothing here is a dispute — it is the second half being taken seriously.
the answer Given on the thread the same day
I can look into that for a future pr. This fixes the immediate breakage.
What follows is that look, run rather than reasoned about.
the second ask On this pull request, once it was open
If we're going to switch this, let's go with somewhat of an established workflow:
- `make init` configures the local-board for tockloader
- `make install` flashes the local image file to the board
We don't need all of the comments in the makefiles.
Also, why change from elf2uf2-rs?
Taken as written. That workflow is
esp32-c3-devkitM-1's, and adopting it removes the
APP variable rather than redefining it — which retires
the cost a reviewer would have stopped on. The elf2uf2-rs question is
answered in Section 5: it reads only ELF, and what
this route produces is a flat image.
1What the route is
The rule in the tree today makes a copy of the kernel ELF, gives its
.apps section file content, and writes one application's TBF into
it. The local-board route never opens an ELF. tockloader keeps a
file that stands in for the board's flash; the kernel binary is written at the
base of it and applications are installed into it at the application region,
exactly as they would be onto a real board.
Four commands, and what the file contains afterwards
| Step | Command | Result |
|---|---|---|
| declare | tockloader local-board set raspberry_pi_pico_2 --binary-path <image> --arch cortex-m33 --app-address 0x10040000 --flash-address 0x10000000 |
Records the board once — this is all
make init does. The addresses are the board's own
layout.ld. |
| kernel | tockloader flash --local-board --address 0x10000000 raspberry_pi_pico_2.bin |
262,144 bytes, byte-for-byte the .bin the ordinary
build already produces. |
| application | tockloader install --local-board console.tab |
Reports the application at 0x10040080 inside a
1,024-byte region beginning at 0x10040000. |
| load | openocd … program <image> 0x10000000 verify
or picotool uf2 convert <image> -t bin -o 0x10000000 |
make flash-openocd and make flash.
Both take a flat binary; neither is given an ELF. |
The resulting image is 263,169 bytes: the kernel binary
unchanged — compared byte for byte, not assumed — then
the 1,024-byte TBF at offset 0x40000, then a single terminator
byte. The kernel binary is already exactly 0x40000 long on
both chips, so the application lands where the linker script puts the
application region with no padding arithmetic anywhere.
tock/tock
e2ec78ff5 with tockloader 1.18.1. Flash begins at
0x10000000 on every RP2 board, and the application region is at
0x10040000 on four of the five.A fifth board is carried along without being edited.
raspberry_pi_pico_w's Makefile is one line —
include ../raspberry_pi_pico/Makefile — so it inherits
whatever that file says. Splicing never had to care: objcopy wrote
into the .apps section at whatever address the board's own ELF put
it. Naming the address in the Makefile does have to care, and that board's
layout.ld gives its kernel 576 K rather than 256 K, so
its prog region begins at 0x10090000. Configured with
0x10040000 it would install applications on top of its own kernel.
It sets APP_ADDRESS for itself, and make init there
prints the higher address.
2Why the defect cannot occur here
#4770 is not really
about the stack. It is about what objcopy does when a section
acquires file content: it lays the program headers out again, and on the way
through, the (NOLOAD) stack segment in SRAM comes back claiming
file content it does not have. The section stays NOBITS the whole
time, which is why nothing that reads the section table can see it.
That sequence needs an ELF to happen to. On this route the only
objcopy in the chain is the kernel's own ELF-to-binary step, which
the build already performs and which nothing here changes. There is no second
copy, no section given contents, and no program header to recompute.
The three failure modes recorded on the #4770 page
were checked against this route rather than assumed away. Two of them belong to
the UF2 converters and cannot arise, because what they are handed is not an ELF.
The third belonged to openocd, which used to write flash correctly,
skip the SRAM segment with no flash bank found, and then fail its
own verify_image against memory it had declined to write. The
programming run in Section 3 emitted no such warning and
verified clean, which is the observation rather than the argument.
3On silicon
The image was programmed onto a Pico 2 W over a Debug Probe,
the board reset, and the console read. Two applications were used in turn:
console, which prints a line and exits, and blink,
which loops — so that the second run leaves a live process for the kernel
to be asked about.
Programmed, booted, and asked
| Stage | What came back | Reading |
|---|---|---|
| programming | ** Programming Finished **** Verified OK ** |
flat image accepted |
| boot | Initialization complete. Enter main loopHello world! |
the application ran |
| through the bootloader | The same image as a UF2 — 549,376 bytes, 1,073 blocks
— copied onto the mounted RP2350 volume.
The board left BOOTSEL by itself and booted. |
drag-and-drop works |
| through probe-rs | probe-rs download --binary-format bin --base-address,
then probe-rs reset. Finished in 17.69 s. |
same flag form as the rule |
| process list | PID 0 Unique blink Quanta 0
Syscalls 141 Restarts 0 Grants 1/4
Yielded |
the kernel enumerates it |
The third row is the one that matters. A line of
output could come from anywhere; a process the kernel has admitted to its
own table and parked in Yielded was loaded and started from
that image. The system-call count is whatever it had reached when the
question was asked — the application loops — so it says the
process is running, not that the number reproduces. The
Hello world! line above it is from the first run, whose
application prints once and terminates.
e2ec78ff5. All three ways of
putting the image on the board were run — SWD, the bootloader, and
probe-rs. SWD and the bootloader were then run again after the rework,
under the target names the change actually ships —
make flash-openocd and make flash — and both
behaved as they had. The process list is the board's own process console answering
list over the UART. Transcripts are quoted, not
summarised.And two applications at once, which the route it replaces cannot do at
all. --update-section .apps= takes one file. Two were
installed into a 274,433-byte image — leak_claim at
0x10040080 and leak_observe at 0x10041080
— and the kernel listed both, loaded over SWD and again through the
bootloader. One tockloader install --local-board carries both,
its signature being [tab ...]: run with the two archives it reports
a layout of leak_claim 4,096 bytes at 0x10040000 and
leak_observe 8,192 bytes at 0x10041000, and the file it
leaves is 274,433 bytes. That was read off the rule until 2026-09-09, when the
call was made directly and the number came back the same.
The two have to want different addresses:
these are built for adjacent slots, and two applications compiled for the same
one are refused with Could not meet fixed address requirements.
That requirement comes from an optional TBF header, not from this route
— process_binary.rs:231 only checks a fixed address when the
application declares one.
And the empty case, which the rework created and nothing had tested.
Collapsing program into flash means flash
converts the image rather than the ELF, so with no application installed the
board is handed a dense 262,144 bytes where the ELF was sparse. Run against an
erased application region it boots and the process console answers
list with its header row and nothing under it — which is two
observations, not one: the kernel came up from the dense image, and an erased
region really is empty rather than quietly holding something the loader would
find.
The byte does the work, not the erase it triggers. That distinction
looked unresolvable: the route writes the terminator through a programming path
that erases the sector first, so the two always arrive together. They come apart
on the bench, because NOR programming only clears bits and the terminator is
0x00 — nothing needs erasing to write it. With a ghost
standing on the board, flash fillb 0x10041000 0x00 1 wrote that one
byte and nothing else: a dump either side of it differs at exactly one
position, the TBF version field going 02 to 00,
with the header's size, checksum and name still intact. On the next reset the
kernel listed one process where it had listed two. One byte, no erase, ghost
gone.
The route does more than dodge the ELF defect — it closes a hazard the current one has. That took two measurements, and the first is about the kernel rather than about either route.
The loader stops at the first invalid header. It walks TBF headers from the start of the application region, and when one does not parse it gives up rather than scanning on. That was checked instead of assumed: a valid application placed one sector further along, with erased flash between, is never reached and never appears in the process list. So there is exactly one address at which an old application can come back — the slot immediately after the new image's last one.
tockloader writes a single zero byte at that slot. Splicing writes nothing there. That is the entire difference between the two routes on this point, and it is worth exactly one byte.
Two images one byte apart, flashed over the same board state
| Image | Length | Process list after reset |
|---|---|---|
| ends exactly on the old header | 266,240 bytes | blink, then ghost —
old application comes back |
| the same, plus tockloader's terminator | 266,241 bytes | blink alone —
slot invalidated |
Both arms start from an identical board state, written
with an erase beforehand: one application in a 4 kB slot and a second,
valid one beginning on the next sector boundary. The images differ by one
byte and nothing else. Without it the kernel loads an application that is
not in the image it was given. openocd reports the
mechanism itself — the byte falls inside the old application's sector,
which forces that sector to be erased. On this flash the byte and the erase
it triggers cannot be told apart; together they make the slot
unreadable.
leak_claim and, from a second-slot
build, leak_observe. The two images are tockloader's own output
and that same file with its last byte removed, so the only difference between
the arms is the byte under test.The ordinary case, for completeness. The A/B above is contrived on purpose — it puts an application exactly where the next one would start, because that is the only place a stale header can survive. The everyday version was run too: an 8,192-byte application flashed, then a 1,024-byte one over it with no erase in between. Only the second appears in the process list. That is the terminator doing its job without anyone arranging for it to matter.
What this retires. An earlier version of this page recommended padding the image across the whole application region. That rested on a wrong model of the loader — that it might scan past a gap — and it is unnecessary: the terminator already covers the only slot that can matter. Flashing a kernel on its own does leave applications in place, but that is Tock's ordinary behaviour and not a fault to be fixed here.
4The other half, byte for byte
The bootloader route was run — Section 3 — but a board booting is not the same as knowing what was written to it. So the UF2 was also taken apart and compared against the bytes that had just run over SWD.
The UF2 disassembled and compared
| Property | Value | Why it is checked |
|---|---|---|
| blocks | 1,029 | Every one carries the two UF2 magic words and the trailing one, or the reassembly would have stopped. |
| family | 0xe48bff59 |
RP2350 Arm-S. The converter inferred it; a wrong family is refused by the bootloader and looks like nothing happening. |
| payload | 263,424 bytes | Reassembled from the block headers, not from the file order. |
| against the image | identical | The first 263,169 bytes equal the image programmed in Section 3; the remainder is zero padding to a block boundary. |
This is what carries the silicon result across to the
program target: the UF2 is not like the image that
booted, it is that image plus padding. The same conversion with
--family rp2040 produces family 0xe48bff56 and
places the TBF at the same address — but see
Section 7 for what that does and does not show.
5What it would cost
The four lines in #5156 buy a working make program and change
nothing else. This route is better shaped, and it is not free.
The heaviest cost is the converter swap on the four RP2040 boards, so it was measured rather than estimated. It looks like a question about a chip — and there is no RP2040 to ask, nor an emulator: mainline QEMU has no such machine, and Tock's own emulated boards are RISC-V and x86. But it is really a question about two programs, and both are here.
Both converters, one kernel, compared address by address
| From | elf2uf2-rs | picotool |
|---|---|---|
| the kernel ELF | 400 blocks, 102,400 bytes | 400 blocks, 102,400 bytes |
| family | 0xe48bff56 |
0xe48bff56 |
| bytes they disagree on | 0 | |
| the flash image | cannot — reads ELF only | 1,033 blocks, TBF at the application address |
Not one byte differs across the 102,400 addresses
both write from the same kernel. Converting the flash image, picotool writes
a further 159,744 bytes that elf2uf2-rs never had the chance to —
every one of them zero, filling unused flash between the end of the kernel
and the end of the kernel region, and stopping before the application
address. So the question "does an RP2040 boot from this?" reduces to "does
it boot today?", which is known. What it costs is size, and more
than the earlier shape did: make flash now converts the image
rather than the ELF, so on this board its UF2 goes from 204,800 to 524,288
bytes with no application installed at all. The 400 blocks both files write
are byte-identical and the 624 added ones are entirely zero — the
image is dense where the ELF was sparse.
raspberry_pi_pico at
e2ec78ff5 and compared by reassembling both files from their
block headers into address maps. An emulator would have proved that the
emulator models the chip; this proves the two files agree.Four costs, each one measured rather than anticipated
| Cost | What was observed | Weight |
|---|---|---|
| Four boards would change converter | Three Makefiles between them, since
raspberry_pi_pico_w includes one of the others.
elf2uf2-rs reads only ELF. Wrapping the flat image
back into one — the technique the ESP32-C3 board uses —
produces a file with no program headers at all, and the converter
rejects it with Unrecognized ABI 97.
picotool takes the binary directly, and produces the
same kernel bytes — Figure 5. |
measured, not blind |
| The kernel UF2 gets bigger | One flash target now covers both cases, so it
converts the image. With no application installed that is 524,288
bytes against 204,800 on the Pico and 147,456 on the Pico 2
— every added block zero, and every block they share
identical. Installing an application adds five blocks, not a second
file. |
user-visible |
| The board setting is machine-wide | One board at a time, in a single file under the user's
application-support directory. make init for one board
replaces the setting for another, and make flash
depends on a step run earlier. |
already upstream |
| tockloader becomes required | For a route that needs none of it today. It is what installs
an application now — there is no APP variable to
redefine, because there is no longer one at all. Without it
make flash stops at write-kernel:
tockloader: command not found,
Error 127, exit 2 — loudly, which is worth
saying because one recipe already in the tree swallows the same 127
and exits 0. |
normal for Tock |
The first two are the ones a reviewer would stop on.
The one that used to head this list is gone: an earlier shape of this
change kept APP and made it mean a .tab rather than
a .tbf, and adopting the established workflow deletes the
variable instead of redefining it. The Pico 2 carries no converter
swap either — it already uses picotool — which is
why that board is the one worth moving first.
esp32-c3-devkitM-1 does exactly this today, and this change now
follows it. That is what makes the exposure acceptable, not what makes it
harmless: an earlier shape ran local-board set on every build so
no rule could inherit another board's addresses, and a separate
make init gives that up.6Choosing between this and #5156
Both fix #4770 on the
same five boards — four Makefiles in #5156's case, because
raspberry_pi_pico_w includes another board's. They are not alternatives in the sense of one being right
— they are different sizes of change with different evidence behind them,
and the order they land in costs nothing either way.
What each one is, and what each one has behind it
| #5156 | #5160, this one | |
|---|---|---|
| size | One objcopy line per rule. Four Makefiles,
thirteen added lines. |
Nine files, +159 −126. Five Makefiles and four READMEs;
program, program-openocd,
program-probe and flash-app all
disappear. |
| what a user notices | Nothing. | make init once per board, then
tockloader install --local-board instead of
APP=; elf2uf2-rs is no longer needed and
tockloader is. |
| the ghost application | Not addressed. The splice still writes nothing at the slot after the last application. | Fixed — Figure 3. |
| RP2040 evidence | hardware the reporter ran it on his own board | none file-level only; no RP2040 here and no emulator |
| review standing | Approved by two maintainers. | Opened 2026-09-09, and reworked the same day to the workflow the reviewer asked for. |
The asymmetry that matters is the fourth row. This page's weakest point is exactly where #5156 is strongest. Merging #5156 first costs this pull request nothing — it deletes those lines anyway — and it keeps a fix with hardware behind it on the four boards nobody here can test.
#5158 went first, and the collision cost one line. That pull request
was two commits: one renamed a make target in two RP2 READMEs, and
one said in the ADC syscall document that samples are left-justified. It merged
on 2026-09-09, and this change was rebased onto it the same day. Both conflicts
were the same shape — #5158's corrected make program against
the tockloader install lines that replace the whole block —
and both resolved by keeping the replacement. The ADC commit is untouched.
This is what the queue's dependency arrows are for: the hold was recorded before
either landed, and the resolution was a rebase rather than a surprise.
7What this does not establish
This list was longer, and every entry that came off it came off by going
back to the board rather than by arguing about it: a UF2 written through the
bootloader, two applications installed at once, an A/B rebuilt from real
applications instead of a patched copy, and — after the rework introduced
it — a kernel-only image, which the collapse of program into
flash made a new and untested case. What follows is what nothing
here could close.
- Nothing ran on an RP2040. There is no such board here and no
emulator for it — mainline QEMU has no RP2040 machine, and Tock's own
emulated boards are RISC-V and x86. The image assembles, begins with that
board's own kernel binary byte for byte, and converts to the right family with
a valid TBF header at the application address — but nothing was booted
from it.
make initandmake flashdo run end to end onraspberry_pi_pico, with tockloader 1.18.1 and picotool doing the work, so what is untested is the boot and not the rules. Figure 5 argues it would behave as it does today; it does not observe it. - probe-rs ran, but not against the chip the rule names. The
invocation in Figure 2 used
--chip RP235x, because that is the board here.flash-probeon the RP2040 boards passes--chip rp2040, and that argument was never exercised. The flags around it were. - The stale-slot A/B was run over SWD only. A UF2 was written through the bootloader in Section 3, but not as an arm of that comparison. The bootloader erases a sector before writing into it, so the same reasoning should hold; it was not observed.