README: which slot the 510 bytes belong to, and the way in without a reset

The Chips footnote said "the slot's last word" against a table whose subject is
the loader, where it means the *lower* slot's — the trampoline holding the
application's relocated reset vector, and a staging copy's own last word during
a self-update. The resident has all 512 bytes of the slot it runs in; 510 is
what an image must fit so a copy staged one slot down leaves that word alone.

And a section on entering from a running application, for the boards whose
adapter does not drive reset and which therefore have no edge to open a window
with. Deciding when to jump stays the application's business — a console
command, a held pin, an idle timeout — so there is no knock detector and no
header here, only the mechanics: the base as a --defsym symbol, and the three
things that must be true first (interrupts off, WDRF clear, and any peripheral
holding the link released, since a loader entered by a jump inherits the
application's registers rather than reset values).

Not the noipa indirect call run_app() uses, which is the obvious thing to copy
and the wrong one: that is a position-independence measure belonging to a loader
that runs the same image from either slot. An application is linked at a fixed
base, so a plain call to the symbol comes out `call 0x7e00` in four bytes where
the laundered form spends two ldi's and a helper call.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-01 23:12:34 +02:00
parent 2655d058f4
commit 8aa1721709

View File

@@ -49,8 +49,13 @@ loaders run 410 B smaller.
| ATmega644, 644A, 644P, 644PA | 64 KiB | 0xfe00 | USART0 | 372 B | 462 B | | ATmega644, 644A, 644P, 644PA | 64 KiB | 0xfe00 | USART0 | 372 B | 462 B |
| ATmega1284, 1284P | 128 KiB | 0x1fe00 | USART0 | 390 B | 480 B | | ATmega1284, 1284P | 128 KiB | 0x1fe00 | USART0 | 390 B | 480 B |
† No hardware boot section: the host patches the reset vector, and the budget † No hardware boot section: the host patches the reset vector, and an image's
is 510 bytes, since the slot's last word is the trampoline. budget is 510 bytes. The last word of the **lower** slot belongs to the host —
it is the trampoline holding the application's relocated reset vector, and it
is a staging copy's own last word during a self-update — so an image must fit
below it. The resident loader has all 512 bytes of the slot it runs in; 510 is
what an image must fit so that a copy of it staged one slot down leaves that
word alone.
The tightest fit in the whole space is therefore the 1284s' 480 of their The tightest fit in the whole space is therefore the 1284s' 480 of their
512: they alone carry the far-flash machinery (ELPM reads, RAMPZ page 512: they alone carry the far-flash machinery (ELPM reads, RAMPZ page
@@ -221,6 +226,59 @@ Re-timing a deployed loader is a self-update with a re-timed build. An autobaud
build counts poll iterations instead (`PUREBOOT_AUTOBAUD_POLLS`), there being build counts poll iterations instead (`PUREBOOT_AUTOBAUD_POLLS`), there being
no clock to turn into seconds. no clock to turn into seconds.
### Entering from a running application
A window opens at a reset and nowhere else, which assumes a reset edge exists —
a button, or DTR wired to it. On a board with neither, the way in has to come
from the application, by jumping to the loader base.
Deciding *when* to jump is the application's business and deliberately not
pureboot's: a console command, a held pin, a magic byte, an idle timeout — each
board's answer differs, and none of them belongs in a 512-byte loader. pureboot
offers no knock detector and no consumable header for this. What follows is the
handful of non-obvious mechanics.
Give the linker the base as a symbol rather than casting a literal to a
function pointer, so the address is stated once and in the same units the
loader is linked at:
```cmake
target_link_options(app PRIVATE "LINKER:--defsym=pureboot_loader=0x7e00")
```
```cpp
extern "C" [[noreturn]] void pureboot_loader();
// ... then, with interrupts off and the watchdog disabled:
pureboot_loader();
```
That is the whole jump: an ordinary call to an absolute symbol, which the
linker resolves and `-mrelax` shortens where it can — `call 0x7e00` in four
bytes, or an `rjmp` on a part small enough for one.
It is worth saying what *not* to copy here, because pureboot's own hand-over
(`run_app()`) looks different: it launders its target through a
`[[gnu::noipa]]` indirect call. That is a position-independence measure, and it
belongs to the loader alone — the same image runs from either slot, so it must
never bake an absolute address. An application is linked at a fixed base and
has no such problem; using the indirect form costs two `ldi`s and a helper call
to reach the same place a plain call reaches in four bytes.
Three things must be true before the jump:
- **Interrupts off and the watchdog disabled.** The loader is polled and
vector-less; an ISR landing in it vectors into the application's table.
- **WDRF clear.** pureboot hands straight back to the application on a watchdog
reset (above), and it only *peeks* MCUSR — so a jump arriving with WDRF still
set opens no window at all.
- **Release any peripheral holding the link.** A loader entered by a jump
inherits the application's registers rather than reset values: with `TXEN0`
still set the USART owns TxD, and a bit-banging loader then receives
perfectly and answers into a pin it does not control. Clearing `UCSR0B`
before jumping is the whole fix, and the symptom without it is a loader that
is mute rather than deaf, which reads as a dead board.
## Session ## Session
After the knock the loader stays in its command loop until a jump takes it away After the knock the loader stays in its command loop until a jump takes it away
@@ -437,7 +495,7 @@ ATmega328P profiles (addresses for its 32 KiB):
| BOOTSZ | BOOTRST | Behavior | | BOOTSZ | BOOTRST | Behavior |
|---|---|---| |---|---|---|
| 256 words (512 B) | programmed | *Standalone*: reset always enters the loader; **self-update impossible** (the staging slot lies outside the boot section, where SPM is disabled). | | 256 words (512 B) | programmed | *Standalone*: reset always enters the loader; **self-update impossible** (the staging slot lies outside the boot section, where SPM is disabled). |
| 512 words (1 KB) | unprogrammed | *Self-update, app-first*: reset always boots the application, which owns all 31.5 KB and must offer its own jump to 0x7e00 to reach the loader (a virgin chip reaches it by reset across erased flash). Updates are power-fail-safe except mid-rewrite of the resident slot itself (no reset path leads to the staging copy then). | | 512 words (1 KB) | unprogrammed | *Self-update, app-first*: reset always boots the application, which owns all 31.5 KB and must offer its own jump to 0x7e00 to reach the loader (a virgin chip reaches it by reset across erased flash; *Entering from a running application* under Activation is how that jump is written). Updates are power-fail-safe except mid-rewrite of the resident slot itself (no reset path leads to the staging copy then). |
| 512 words (1 KB) | programmed | *Self-update, loader-first*: reset lands at 0x7c00 — the staging slot, normally erased, so execution walks up into the loader; during an update it is the staging copy itself, so a mid-rewrite power loss recovers by reset. The loss windows move to the staging install/retire page writes instead (page-write scale). The host keeps `[0x7c00, 0x7e00)` clear of application data (`--force` overrides). | | 512 words (1 KB) | programmed | *Self-update, loader-first*: reset lands at 0x7c00 — the staging slot, normally erased, so execution walks up into the loader; during an update it is the staging copy itself, so a mid-rewrite power loss recovers by reset. The loss windows move to the staging install/retire page writes instead (page-write scale). The host keeps `[0x7c00, 0x7e00)` clear of application data (`--force` overrides). |
Applications are flashed unmodified here — word 0 stays the application's own Applications are flashed unmodified here — word 0 stays the application's own