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:
@@ -49,8 +49,13 @@ loaders run 4–10 B smaller.
|
||||
| ATmega644, 644A, 644P, 644PA | 64 KiB | 0xfe00 | USART0 | 372 B | 462 B |
|
||||
| ATmega1284, 1284P | 128 KiB | 0x1fe00 | USART0 | 390 B | 480 B |
|
||||
|
||||
† No hardware boot section: the host patches the reset vector, and the budget
|
||||
is 510 bytes, since the slot's last word is the trampoline.
|
||||
† No hardware boot section: the host patches the reset vector, and an image's
|
||||
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
|
||||
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
|
||||
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
|
||||
|
||||
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 |
|
||||
|---|---|---|
|
||||
| 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). |
|
||||
|
||||
Applications are flashed unmodified here — word 0 stays the application's own
|
||||
|
||||
Reference in New Issue
Block a user