pureboot 3: a 512-byte slot on every chip, the 1284s included
The word-addressed 1284s were the one family deploying in a 1 KiB slot, because the far-flash machinery (ELPM reads, RAMPZ page commands, a word-addressed wire) did not fit 512 B. It does now: 478 B stock, 494 B in the heaviest configuration the build can produce. They take the 644s' geometry, where the smallest boot section holds the resident slot and its staging slot together. The loader's version goes to 3; the host tool did not change, so its own version stays 2 and only the window it speaks widens. Most of the saving is one restructure. The info block and a flash read are the same act, so giving all four streamed commands one address-and-count path leaves exactly one call site for the flash streamer: it inlines into the never-returning command loop and its 24-bit cursor stops being saved and restored around every transmit. Around it, the ack byte moved out of line, the wire's byte pair is bit_cast into the word it already is, the fuse loop ends on its count, the info block's in-slot offset is taken as the one-byte relocation it is, and -fno-expensive-optimizations gives way to -fno-move-loop-invariants -fno-tree-ter. Every chip shrank 14-18 B. The size matrix grew the axes it was missing: the USART1 instance across the whole clock ladder, and the shape a slow baud gives a software UART — past 255 delay iterations libavr takes the 16-bit delay loop, which the ladder default never selects and which was 4 B over the 1284's slot the first time it was built. The protocol fixture stopped deriving the loader entry from the flash size; on the 1284s it had been jumping a slot low and reaching the loader only because erased flash walked it up. Docs and comments were consolidated across the port in the same pass: the README carries a per-chip size table instead of prose, and prose that restated the code is gone — 190 lines, no behaviour with it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,46 +2,61 @@
|
||||
|
||||
A serial bootloader on [libavr](https://git.blackmark.me/avr/libavr), pure by
|
||||
constraint: one C++ source, no inline assembly, no global register variables
|
||||
(attributes and compiler flags allowed), built for **every chip libavr
|
||||
targets — all 37 — in 512 bytes each**: 434 B on the tiny13s, 438–442 B on
|
||||
the tiny25/45/85, 412–452 B across the megas, and 506 B on the
|
||||
ATmega1284/1284P, whose far-flash machinery (ELPM reads, RAMPZ page commands,
|
||||
word-addressed wire) is the heaviest. Those are the stock deployments;
|
||||
choosing the software UART where the chip has a USART costs 8–46 B more (a
|
||||
bit-bang against a peripheral), which every chip still absorbs inside its
|
||||
slot — on the 1284s that means their 1 KiB boot sector, where the
|
||||
software-serial image lands at 546 B. Bringing the 1284's default build
|
||||
under 512 at all is what the loop-placement attributes on the byte streamers
|
||||
(`pureboot.cpp`) and the codegen flags on the loader TU (`CMakeLists.txt`)
|
||||
are for; measured against each chip's own budget the tightest is the
|
||||
ATmega328P, 50 B spare. Clock, baud, serial backend and
|
||||
pins are per-build configuration (below); the size matrix in the test suite
|
||||
holds every combination inside its slot. The device speaks primitives; every
|
||||
composite — verify, erase, reset-vector surgery, updating the loader itself —
|
||||
lives in the host tool (`pureboot.py`).
|
||||
|
||||
The 1284s still *deploy* in a 1 KiB slot, their smallest boot sector being
|
||||
512 words; at 506 B the image would also fit the 644's
|
||||
two-512-byte-slots-per-boot-sector geometry.
|
||||
(attributes and compiler flags allowed), **512 bytes on every chip libavr
|
||||
targets — all 37**. The device speaks primitives; every composite — verify,
|
||||
erase, reset-vector surgery, updating the loader itself — lives in the host
|
||||
tool (`pureboot.py`).
|
||||
|
||||
The image is **position-independent**: control flow is PC-relative, the
|
||||
read/write paths take wire addresses, the write guard protects the slot the
|
||||
code is *running* in (from the runtime return address), the info block is
|
||||
addressed from that same anchor, and the application jump is an indirect
|
||||
call to an absolute entry. The identical binary therefore runs from any
|
||||
slot with every command intact — which makes pureboot **its own staging
|
||||
loader**: the host installs the same binary one slot below the resident,
|
||||
jumps into it, and lets it rewrite the resident. The slot is 512 bytes
|
||||
(1 KiB on the word-addressed large chips, matching their boot-sector
|
||||
minimum); on the tinies the budget is 510, not 512: a slot's last word
|
||||
belongs to the host-managed trampoline (below).
|
||||
addressed from that same anchor, and the application jump is an indirect call
|
||||
to an absolute entry. The identical binary therefore runs from any slot with
|
||||
every command intact, which makes pureboot **its own staging loader**: the
|
||||
host installs the same binary one slot below the resident, jumps into it, and
|
||||
lets it rewrite the resident.
|
||||
|
||||
## Chips
|
||||
|
||||
Sizes are the default configuration: the hardware USART0 at 115200 8N1 on a
|
||||
16 MHz crystal, or the software UART on RX = PB0 / TX = PB1 at 57600 8N1 on
|
||||
the tinies' RC oscillator (9.6 MHz on the t13s, 8 MHz above). Every axis moves
|
||||
per build — see *Configuration*; the largest image any of them produces is a
|
||||
software UART at a slow baud, which on the 1284s is 494 B, the tightest fit in
|
||||
the whole matrix at 18 B spare.
|
||||
|
||||
| Chip | Flash | Loader at | Link | Size |
|
||||
|---|---|---|---|---|
|
||||
| ATtiny13, ATtiny13A † | 1 KiB | 0x0200 | software | 416 B |
|
||||
| ATtiny25 † | 2 KiB | 0x0600 | software | 420 B |
|
||||
| ATtiny45 † | 4 KiB | 0x0e00 | software | 424 B |
|
||||
| ATtiny85 † | 8 KiB | 0x1e00 | software | 424 B |
|
||||
| ATmega8, 8A | 8 KiB | 0x1e00 | USART0 | 396 B |
|
||||
| ATmega16, 16A | 16 KiB | 0x3e00 | USART0 | 400 B |
|
||||
| ATmega32, 32A | 32 KiB | 0x7e00 | USART0 | 400 B |
|
||||
| ATmega48, 48A, 48P, 48PA † | 4 KiB | 0x0e00 | USART0 | 414 B |
|
||||
| ATmega88, 88A, 88P, 88PA | 8 KiB | 0x1e00 | USART0 | 434 B |
|
||||
| ATmega168, 168A, 168P, 168PA | 16 KiB | 0x3e00 | USART0 | 438 B |
|
||||
| ATmega328, 328P | 32 KiB | 0x7e00 | USART0 | 438 B |
|
||||
| ATmega164A, 164P, 164PA | 16 KiB | 0x3e00 | USART0 | 438 B |
|
||||
| ATmega324A, 324P, 324PA | 32 KiB | 0x7e00 | USART0 | 438 B |
|
||||
| ATmega644, 644A, 644P, 644PA | 64 KiB | 0xfe00 | USART0 | 432 B |
|
||||
| ATmega1284, 1284P | 128 KiB | 0x1fe00 | USART0 | 478 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.
|
||||
|
||||
The 1284s are the heaviest because they alone carry the far-flash machinery —
|
||||
ELPM reads, RAMPZ page commands, a word-addressed wire.
|
||||
|
||||
The software UART enables the RX pull-up; TX idles high. All multi-byte wire
|
||||
quantities are little-endian.
|
||||
|
||||
## Configuration
|
||||
|
||||
Every deployment axis is a build parameter, resolved by the CMake function
|
||||
`pureboot_add_loader()` (in `pureboot/CMakeLists.txt`) — the one way a
|
||||
loader target is created, by this repo's own build and by a downstream
|
||||
project alike:
|
||||
Every deployment axis is a build parameter of `pureboot_add_loader()` (in
|
||||
`pureboot/CMakeLists.txt`) — the one way a loader target is created, by this
|
||||
repo's build and by a downstream project alike:
|
||||
|
||||
| Argument | Meaning | Default |
|
||||
|---|---|---|
|
||||
@@ -55,15 +70,14 @@ project alike:
|
||||
The default baud is the fastest of 115200/57600/38400/19200/9600 the clock
|
||||
reaches within 2.5 % — the same U2X-included divisor search libavr's baud
|
||||
solver runs — and on a software build additionally within the polled
|
||||
receiver's 100-cycles-a-bit floor. 16 MHz lands 115200, 8 MHz 57600,
|
||||
1 MHz 9600. Whatever is picked or overridden is re-checked in the compile:
|
||||
an infeasible clock/baud/backend combination, or a USART the chip does not
|
||||
have, fails with a named static assert.
|
||||
receiver's 100-cycles-a-bit floor. Whatever is picked or overridden is
|
||||
re-checked in the compile: an infeasible combination, or a USART the chip does
|
||||
not have, fails with a named static assert.
|
||||
|
||||
A downstream project brings its usual libavr setup (the `libavr` target,
|
||||
the chip via the `LIBAVR_MCU` toolchain preset), consumes this directory,
|
||||
and states its deployment — for example an ATmega328P on its shipped
|
||||
1 MHz fuses with the software UART on hand-picked pins:
|
||||
A downstream project brings its usual libavr setup (the `libavr` target, the
|
||||
chip via the `LIBAVR_MCU` toolchain preset), consumes this directory, and
|
||||
states its deployment — an ATmega328P on its shipped 1 MHz fuses with the
|
||||
software UART on hand-picked pins, say:
|
||||
|
||||
```cmake
|
||||
FetchContent_Declare(bootloader GIT_REPOSITORY git@git.blackmark.me:avr/bootloader.git GIT_TAG main)
|
||||
@@ -74,57 +88,40 @@ pureboot_add_loader(myboot CLOCK 1000000 SERIAL software TX pb1 RX pb5)
|
||||
```
|
||||
|
||||
The function emits the ELF plus `myboot.hex` (the programmer artifact) and
|
||||
`myboot.bin` (the self-update image), prints the size, and stamps the
|
||||
resolved deployment on the target as the `PUREBOOT_HZ`, `PUREBOOT_BAUD`
|
||||
and `PUREBOOT_LINK` properties — what a flashing script or test harness
|
||||
needs to speak to the build. This exact example deployment runs the full
|
||||
protocol suite in CI (`pureboot.custom`).
|
||||
|
||||
## Link
|
||||
|
||||
The stock builds assume the family's natural deployment; any axis moves
|
||||
per build (above).
|
||||
|
||||
| Chip | Serial | Baud | Clock assumed |
|
||||
|---|---|---|---|
|
||||
| every ATmega | the hardware USART (USART0), RXD/TXD per pinout | 115200 8N1 | 16 MHz crystal |
|
||||
| ATtiny25/45/85 | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 8 MHz internal RC |
|
||||
| ATtiny13/13A | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 9.6 MHz internal RC |
|
||||
|
||||
The software-UART RX pin has its pull-up enabled; TX idles high. All
|
||||
multi-byte quantities on the wire are little-endian.
|
||||
`myboot.bin` (the self-update image), prints the size, and stamps the resolved
|
||||
deployment on the target as the `PUREBOOT_HZ`, `PUREBOOT_BAUD` and
|
||||
`PUREBOOT_LINK` properties — what a flashing script or test harness needs to
|
||||
speak to the build. This exact deployment runs the full protocol suite in CI
|
||||
(`pureboot.custom`).
|
||||
|
||||
## Activation
|
||||
|
||||
Reset enters the loader (BOOTRST on the boot-sectioned megas; the patched
|
||||
reset vector on the tinies and the boot-section-less m48s) — except a
|
||||
watchdog reset, which hands straight to the application (the application
|
||||
owns its watchdog; it must clear WDRF itself, which also releases the
|
||||
WDRF-forced WDE).
|
||||
Reset enters the loader (BOOTRST on the boot-sectioned megas, the patched
|
||||
reset vector elsewhere) — except a watchdog reset, which hands straight to the
|
||||
application, since the application owns its watchdog and must clear WDRF
|
||||
itself.
|
||||
|
||||
The host then has one activation window per awaited byte to knock: `p` then
|
||||
`b`. Each awaited byte gets a fresh window; any other byte is discarded and
|
||||
awaited again (line noise cannot lock the loader, only delay it). A window
|
||||
expiring with an idle line boots the application.
|
||||
The host then knocks `p` then `b`, each awaited byte under a fresh activation
|
||||
window; any other byte is discarded and awaited again, so line noise can delay
|
||||
the loader but never lock it. A window expiring on an idle line boots the
|
||||
application.
|
||||
|
||||
The window length is a compile-time constant — 8 s by default, another
|
||||
value via `pureboot_add_loader(... TIMEOUT <s>)` (the stock target keeps
|
||||
the `PUREBOOT_TIMEOUT` cache variable) — so the whole EEPROM belongs to
|
||||
the application; pureboot never uses it for its own state. Re-timing a
|
||||
deployed loader is a self-update with a re-timed build (below).
|
||||
The window is a compile-time constant (`TIMEOUT`, 8 s by default), so the whole
|
||||
EEPROM belongs to the application — pureboot keeps no state of its own.
|
||||
Re-timing a deployed loader is a self-update with a re-timed build.
|
||||
|
||||
## Session
|
||||
|
||||
After the knock the loader stays in its command loop until `J` jumps away or
|
||||
the chip resets. Before reading each command it waits for any pending EEPROM
|
||||
write to finish and sends the prompt `+` (0x2b) — the prompt is therefore
|
||||
also the completion ack of the previous command. A session is: await `+`,
|
||||
send a command, read its reply, repeat.
|
||||
write and sends the prompt `+` (0x2b), which is therefore also the previous
|
||||
command's completion ack. A session is: await `+`, send a command, read its
|
||||
reply, repeat.
|
||||
|
||||
On chips whose flash exceeds 64 KiB (the 1284s — info-block flag bit 1) the
|
||||
`R`/`W` flash addresses are **word** addresses; everywhere else they are byte
|
||||
addresses (the 644s' 64 KiB is exactly the 16-bit byte space and stays
|
||||
byte-addressed). EEPROM addresses are always bytes, counts always bytes.
|
||||
addresses (the 644s' 64 KiB is exactly the 16-bit byte space). EEPROM
|
||||
addresses and all counts are bytes.
|
||||
|
||||
| Cmd | Arguments | Reply |
|
||||
|---|---|---|
|
||||
@@ -137,101 +134,75 @@ byte-addressed). EEPROM addresses are always bytes, counts always bytes.
|
||||
| `J` | word address (16-bit) | `+`, then execution continues there |
|
||||
| other | — | ignored; the loop re-prompts (send a junk byte, await `+`, to resync) |
|
||||
|
||||
`W` streams exactly one SPM page (size from the info block) into the buffer,
|
||||
then erases and programs; the address must be page-aligned. Pages inside the
|
||||
512-byte slot the loader is *running* in are drained but never programmed — a
|
||||
broken host cannot brick the running copy, and a staged copy may rewrite the
|
||||
resident slot.
|
||||
`W` streams exactly one page-aligned SPM page (size from the info block) into
|
||||
the buffer, then erases and programs — except pages inside the 512-byte slot
|
||||
the loader is *running* in, which are drained and left alone, so a broken host
|
||||
cannot brick the running copy and a staged copy may rewrite the resident.
|
||||
|
||||
The loader never clears the SPM buffer before a fill, so **one `W` may
|
||||
program the wrong bytes, and the host is what fixes it**. The buffer is
|
||||
write-once per word until cleared, and two things leave words in it: a
|
||||
refused page (drained, never programmed) and — where SPM runs from anywhere,
|
||||
the tinies and the m48s — an application that self-programmed before
|
||||
entering. The next `W` takes those stale words, and clears them: a page write
|
||||
auto-erases the buffer (§26.2.1; §19.2 on the tinies), so repeating it
|
||||
programs correctly. The host therefore verifies every page it writes and
|
||||
rewrites what comes back wrong (three retries, then it stops); a host that
|
||||
programs without reading back cannot trust the first `W` after either event.
|
||||
The loader never clears the SPM buffer before a fill, so **one `W` may program
|
||||
the wrong bytes, and the host is what fixes it**. The buffer is write-once per
|
||||
word until cleared, and two things leave words in it: a refused page, and —
|
||||
where SPM runs from anywhere, the tinies and the m48s — an application that
|
||||
self-programmed before entering. The next `W` takes those stale words and
|
||||
clears them, since a page write auto-erases the buffer (§26.2.1; §19.2 on the
|
||||
tinies), so repeating it programs correctly. The host therefore verifies every
|
||||
page it writes and rewrites what comes back wrong (three retries, then it
|
||||
stops).
|
||||
|
||||
`w` is host-paced: send the next byte only after the previous
|
||||
byte's `+`. `F` returns the bytes in the hardware's Z order; on a chip
|
||||
without an extended fuse byte (the ATtiny13A) that slot carries no meaning.
|
||||
Fuse *writing* does not exist: SPM reaches flash (and, on the mega, lock
|
||||
bits) only — fuse bytes are external-programming territory by hardware.
|
||||
`w` is host-paced: send the next byte only after the previous byte's `+`. `F`
|
||||
returns the bytes in the hardware's Z order; on a chip without an extended
|
||||
fuse byte that slot carries no meaning. Fuse *writing* does not exist — SPM
|
||||
reaches flash and boot lock bits only.
|
||||
|
||||
`J` is the one control-transfer primitive: the host uses it to run the
|
||||
application (word 0 on the mega, the trampoline word on the tinies — both
|
||||
known from the info block) and to move between loader copies during a
|
||||
self-update. A jump to a loader slot's base re-enters that copy's own
|
||||
startup; it must then be knocked afresh.
|
||||
`J` is the one control-transfer primitive: it runs the application (word 0 or
|
||||
the trampoline word, both known from the info block) and moves between loader
|
||||
copies during a self-update. A jump to a slot's base re-enters that copy's own
|
||||
startup, which must then be knocked afresh.
|
||||
|
||||
The info block (`b`):
|
||||
|
||||
| Offset | Content |
|
||||
|---|---|
|
||||
| 0–2 | `'P'`, `'B'`, pureboot version (2) |
|
||||
| 0–2 | `'P'`, `'B'`, pureboot version (3) |
|
||||
| 3–5 | device signature |
|
||||
| 6 | SPM page size in bytes (0 means 256) |
|
||||
| 7–8 | loader base — application flash ends here (a word address when bit 1 is set) |
|
||||
| 9–10 | EEPROM size |
|
||||
| 11 | bit 0: host must patch the reset vector (no hardware boot section); bit 1: flash wire addresses are word addresses |
|
||||
|
||||
Composites are the host's job: verify = read back and compare, erase =
|
||||
write `0xff` (per page for flash, per byte for EEPROM).
|
||||
|
||||
## Version
|
||||
|
||||
The third byte of the info block is the **pureboot version** — the loader's
|
||||
one identity number, and the only way to tell what a deployed loader is.
|
||||
Nothing else is numbered: the wire protocol has no version of its own, a
|
||||
pureboot version implies its protocol, and the host tool is what holds that
|
||||
map. It states the window of loader versions it speaks
|
||||
(`OLDEST_LOADER`/`NEWEST_LOADER` in `pureboot.py`); a version that changes
|
||||
the protocol becomes the new floor there. So far none has: pureboot 1 and 2
|
||||
speak the identical session, and a loader newer than the tool is refused by
|
||||
name rather than decoded on the assumption that nothing moved.
|
||||
The info block's third byte is the **pureboot version** — the loader's one
|
||||
identity number, and the only way to tell what a deployed loader is. Nothing
|
||||
else is numbered: the wire protocol has no version, a pureboot version implies
|
||||
it, and the host tool holds that map. The tool states the window of loader
|
||||
versions it speaks (`OLDEST_LOADER`/`NEWEST_LOADER` in `pureboot.py`), and a
|
||||
version that changes the protocol becomes the new floor there. None has so
|
||||
far: 1 through 3 speak the identical session. A loader newer than the tool is
|
||||
refused by name rather than decoded on the assumption that nothing moved.
|
||||
|
||||
The tool carries its own version, free to drift from the loader's:
|
||||
`--version` prints both it and the window.
|
||||
The tool carries its own version, free to drift; `--version` prints it and the
|
||||
window.
|
||||
|
||||
## Deployment
|
||||
|
||||
The build leaves three artifacts per chip. The ELF is a container for the
|
||||
tests and objcopy — never flashed. The **.hex is the programmer artifact**:
|
||||
it carries its own addresses and lands the loader in its top slot,
|
||||
touching nothing else. The **.bin is the self-update image** — the slot's
|
||||
bare bytes with no addressing, which a programmer would put at address 0.
|
||||
On a boot-sectioned mega a copy at 0 is dead weight (SPM only executes
|
||||
from the boot section, so it cannot even heal itself — reflash the .hex);
|
||||
on the patched-vector chips it *runs* (the image is position-independent
|
||||
and reset enters word 0), reports its canonical geometry, and the ordinary
|
||||
`--update-loader` flow re-homes a build into the top slot from any
|
||||
position — the staging install and the word-0 redirect execute from
|
||||
copies outside page 0's slot, and a copy sitting in the staging slot
|
||||
itself is recognized as the installed staging copy and left in place (it
|
||||
streams the new resident like any staged copy, so an older build installs
|
||||
a newer one). `pureboot.rehome` is the acceptance test for both
|
||||
positions. Flashing the application afterwards overwrites the stale copy,
|
||||
vector surgery included.
|
||||
tests and objcopy, never flashed. The **.hex is the programmer artifact**: it
|
||||
carries its own addresses and lands the loader in its top slot, touching
|
||||
nothing else. The **.bin is the self-update image** — the slot's bare bytes.
|
||||
|
||||
**Boot-sectioned megas**: program the loader at `flash − slot` with an
|
||||
external programmer. Every such mega has a BOOTSZ step whose boot section
|
||||
is exactly the loader slot — 512 B, the second-smallest step on the 8 KiB
|
||||
and 16 KiB chips (m8, m88, m16, m168, m164), the smallest on the 32 KiB
|
||||
ones (m32, m328, m324); on the 1284s that step is the smallest, 512 words,
|
||||
which is why their slot is 1 KiB — so the ATmega328P profiles below apply
|
||||
to every one of them with its own addresses and slot size; the per-chip
|
||||
BOOTSZ ladders live in the host tool (`BOOT_FUSE`). The 1284s' numbers:
|
||||
standalone = BOOTSZ 512 words (reset at the loader base 0x1fc00);
|
||||
self-update = 1024 words, covering both 1 KiB slots, the loader-first
|
||||
reset landing at 0x1f800 — the staging slot, walked across when erased.
|
||||
**Boot-sectioned megas**: program the loader at `flash − 512` with an external
|
||||
programmer. Every such mega has a BOOTSZ step whose boot section is exactly
|
||||
the 512-byte slot — the second-smallest step on the 8 KiB and 16 KiB chips,
|
||||
the smallest on the 32 KiB ones — so the ATmega328P profiles below apply to
|
||||
every one of them with its own addresses; the per-chip BOOTSZ ladders live in
|
||||
the host tool (`BOOT_FUSE`).
|
||||
|
||||
The **644s** are the geometry's sweet spot: their smallest boot section
|
||||
(512 words = 1 KiB) is exactly *two* 512-byte slots, so the resident and
|
||||
its staging slot both live inside the minimum section — self-update needs
|
||||
no fuse step up, and the standalone profile does not exist (reset lands at
|
||||
0xfc00, one erased slot below the loader: the loader-first walk built in).
|
||||
The **644s and 1284s** are the geometry's sweet spot: their smallest boot
|
||||
section (512 words = 1 KiB) is exactly *two* slots, so the resident and its
|
||||
staging slot both live inside the minimum section. Self-update needs no fuse
|
||||
step up, and the standalone profile does not exist — reset lands one erased
|
||||
slot below the loader (0xfc00 / 0x1fc00) and walks up into it.
|
||||
|
||||
ATmega328P profiles (addresses for its 32 KiB):
|
||||
|
||||
@@ -241,155 +212,135 @@ ATmega328P profiles (addresses for its 32 KiB):
|
||||
| 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) | 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 — word 0 stays the application's own
|
||||
Applications are flashed unmodified here — word 0 stays the application's own
|
||||
reset vector, and the hand-over jumps to 0.
|
||||
|
||||
**Patched-vector chips — the tinies and the m48s** (no boot section; the
|
||||
m48s' SPM runs from the entire flash, Atmel-8271 §26): program the loader
|
||||
at `flash − 512`; erased flash below it walks up into the loader, so a
|
||||
virgin chip activates. When flashing an application the host performs
|
||||
reset-vector surgery: word 0 is rewritten to `rjmp` to the loader base, and
|
||||
the application's own entry is re-encoded as a trampoline `rjmp` in the
|
||||
word just below the loader (`base − 2`, where the hand-over jumps). Every
|
||||
other vector stays the application's. The patched page 0 and the trampoline
|
||||
page are written *first*, so from the first write on an interrupted flash
|
||||
still resets into the loader; an erase runs top-down for the same reason.
|
||||
The m48s speak this profile over their hardware USART — no fuse preflight,
|
||||
BOOTRST does not exist there.
|
||||
**Patched-vector chips — the tinies and the m48s** (no boot section; the m48s'
|
||||
SPM runs from the entire flash, Atmel-8271 §26): program the loader at
|
||||
`flash − 512`; erased flash below it walks up into the loader, so a virgin
|
||||
chip activates. Flashing an application then takes reset-vector surgery: word
|
||||
0 becomes an `rjmp` to the loader base, and the application's own entry is
|
||||
re-encoded as a trampoline `rjmp` in the word just below the loader
|
||||
(`base − 2`, where the hand-over jumps). Every other vector stays the
|
||||
application's. The patched page 0 and the trampoline page are written *first*
|
||||
and an erase runs top-down, so from the first write on an interruption still
|
||||
resets into the loader.
|
||||
|
||||
A .bin programmed at address 0 by mistake is dead weight on a boot-sectioned
|
||||
mega (SPM only executes from the boot section — reflash the .hex), but *runs*
|
||||
on a patched-vector chip, and the ordinary `--update-loader` flow re-homes it
|
||||
into the top slot from there (`pureboot.rehome`).
|
||||
|
||||
## Updating the loader
|
||||
|
||||
`pureboot.py --update-loader new_pureboot.bin` replaces the resident loader
|
||||
with any pureboot build — a re-timed window, a newer version — using the
|
||||
loader itself as its own staging loader. The image is the loader's own 512
|
||||
bytes as a raw binary, or the Intel HEX the build emits beside it, which
|
||||
links the loader at its base inside an otherwise blank flash image:
|
||||
bytes as a raw binary, or the Intel HEX the build emits beside it.
|
||||
|
||||
The preflight refuses an image built for another chip: the info block
|
||||
embedded in every pureboot binary (signature, page size, loader base,
|
||||
EEPROM size, flags) must match the device's own, and the error names both.
|
||||
Die revisions share their base signature and geometry, so their images are
|
||||
interchangeable — as the silicon is. `loader_image()` also accepts a
|
||||
padded image (a raw .bin padded from 0, or a whole-flash read-back with
|
||||
the loader resident) and peels it to the slot content by the embedded base.
|
||||
The preflight refuses an image built for another chip: the info block embedded
|
||||
in every pureboot binary (signature, page size, loader base, EEPROM size,
|
||||
flags) must match the device's own, and the error names both. Die revisions
|
||||
share their base signature and geometry, so their images are interchangeable —
|
||||
as the silicon is.
|
||||
|
||||
1. The staging slot `[base−slot, base)` is saved to a host-side state file
|
||||
(on the 1 KB tiny13s that is the whole application, vectors included).
|
||||
2. The resident installs the identical update image there. On the
|
||||
patched-vector chips the host composes the slot's last word — the same
|
||||
address as the resident's trampoline — as a jump to the resident base,
|
||||
so even an abandoned staging copy times out into a loader, never into
|
||||
garbage. A loader already sitting whole in the staging slot (its info
|
||||
block in place, the slot unchanged since the update began) is left as
|
||||
the staging copy instead — rewriting it would only meet its own
|
||||
running-slot guard.
|
||||
3. `J` enters the staging copy, which rewrites the resident slot. On the
|
||||
patched-vector chips whose staging slot sits away from page 0 the host
|
||||
first re-aims word 0 at the staging copy, so a power loss mid-rewrite
|
||||
still resets into a loader; on the tiny13s the staging slot carries the
|
||||
reset vector itself.
|
||||
1. The staging slot `[base−512, base)` is saved to a host-side state file (on
|
||||
the 1 KB tiny13s that is the whole application, vectors included).
|
||||
2. The resident installs the update image there. On the patched-vector chips
|
||||
the host composes the slot's last word as a jump to the resident base, so
|
||||
even an abandoned staging copy times out into a loader. A loader already
|
||||
sitting whole in the staging slot is left as the staging copy instead —
|
||||
rewriting it would only meet its own running-slot guard.
|
||||
3. `J` enters the staging copy, which rewrites the resident slot. Where a
|
||||
patched reset vector routes through the resident, the host first re-aims
|
||||
word 0 at the staging copy, so a power loss mid-rewrite still resets into a
|
||||
loader; on the tiny13s the staging slot carries the reset vector itself.
|
||||
4. `J` enters the new resident, which restores the staging slot's saved
|
||||
content (word 0 and the trampoline with it) and the state file is
|
||||
discarded.
|
||||
content, and the state file is discarded.
|
||||
|
||||
Every phase is idempotent and keyed off the actual flash state: re-running
|
||||
the same command after any interruption resumes and completes. The state
|
||||
file carries the only bytes not recoverable from the device; if it is lost
|
||||
mid-update the update still completes, and the staging region is restored by
|
||||
reflashing the application. A boot-sectioned mega needs its fuses for the
|
||||
preflight (BOOTSZ gate, profile notes) — read from the device, or supplied
|
||||
with `--assume-fuses` where reading is impossible (simulators); the
|
||||
patched-vector chips need none.
|
||||
Every phase is idempotent and keyed off the actual flash state, so re-running
|
||||
the same command after any interruption resumes and completes. The state file
|
||||
carries the only bytes not recoverable from the device; losing it mid-update
|
||||
still completes the update, and the staging region comes back by reflashing
|
||||
the application. A boot-sectioned mega needs its fuses for the preflight — read
|
||||
from the device, or supplied with `--assume-fuses` where reading is impossible
|
||||
(simulators).
|
||||
|
||||
## Host tool
|
||||
|
||||
`pureboot.py` — Python 3, standard library only. The port layer is the one
|
||||
platform-specific part: termios drives any tty on POSIX (a USB adapter as
|
||||
well as a simavr pty), the Win32 serial API through `ctypes` drives a COM
|
||||
port on Windows (`--port COM6`; the `\\.\` form for two-digit ports is
|
||||
supplied by the tool). Opening the port asserts DTR and RTS on both, so a
|
||||
board that wires DTR to reset gets its reset pulse and opens the activation
|
||||
window by itself.
|
||||
platform-specific part: termios drives any tty on POSIX (a USB adapter as well
|
||||
as a simavr pty), the Win32 serial API through `ctypes` drives a COM port on
|
||||
Windows (`--port COM6`; the `\\.\` form for two-digit ports is supplied by the
|
||||
tool). Opening the port asserts DTR and RTS on both, so a board that wires DTR
|
||||
to reset gets its reset pulse and opens the activation window by itself.
|
||||
|
||||
pureboot.py --port /dev/ttyUSB0 --baud 57600 \
|
||||
--info --fuses --flash app.hex
|
||||
|
||||
Operations run in a fixed order within one session: info, fuses, loader
|
||||
update, flash (erase / program / read / verify), EEPROM (erase / program /
|
||||
read / verify) — then the loader hands over to the application; `--stay`
|
||||
keeps the session alive instead, and a later invocation reconnects into it
|
||||
(the knock converges there too). `--flash` and `--eeprom` verify by
|
||||
read-back unless `--no-verify`, and a flash page that reads back wrong is
|
||||
rewritten up to three times before the run stops — the loader leaves one
|
||||
recoverable way for a page to land wrong (see `W` above), and rewriting is
|
||||
what clears it. `--verify-flash` only reports. Images are raw binary, or
|
||||
Intel HEX by extension. `--force` overrides the refusable safety checks (today: flashing
|
||||
application data into a mega's reset walk region).
|
||||
update, flash (erase / program / read / verify), EEPROM (the same) — then the
|
||||
loader hands over to the application. `--stay` keeps the session alive
|
||||
instead, and a later invocation reconnects into it. `--flash` and `--eeprom`
|
||||
verify by read-back unless `--no-verify`, and a flash page that reads back
|
||||
wrong is rewritten up to three times before the run stops (see `W` above).
|
||||
`--verify-flash` only reports. Images are raw binary, or Intel HEX by
|
||||
extension. `--force` overrides the refusable safety checks — today, flashing
|
||||
application data into a mega's reset walk region.
|
||||
|
||||
Readouts come one fact per line: `--info` prints the decoded info block
|
||||
field by field, the loader's version first; `--fuses` each fuse byte on its
|
||||
own line — plus, on a boot-sectioned mega, the decoded meaning (where the
|
||||
BOOTSZ section starts, what BOOTRST does to reset). Transfers that take
|
||||
wire time — programming, reading, erasing, verifying, the update phases —
|
||||
draw a transient progress bar on stderr when it is a tty; logs and pipes
|
||||
see only the summary lines.
|
||||
`-v`/`--verbose` adds the decisions as they happen: knock counts, the
|
||||
programming plan (vector-surgery targets, skipped blank pages), update
|
||||
state handling and per-phase page counts.
|
||||
Readouts come one fact per line: `--info` decodes the info block field by
|
||||
field, `--fuses` each fuse byte plus, on a boot-sectioned mega, its decoded
|
||||
meaning. Transfers that take wire time draw a transient progress bar on stderr
|
||||
when it is a tty. `-v`/`--verbose` adds the decisions as they happen: knock
|
||||
counts, the programming plan, update state handling and per-phase page counts.
|
||||
|
||||
## Tests
|
||||
|
||||
`tools/check.sh` runs every chip's workflow (`tools/check.sh --full` adds
|
||||
the reflect-mode builds of libavr's spot set; `tools/make_presets.py`
|
||||
regenerates the presets). Per chip preset, `ctest` runs:
|
||||
`tools/check.sh` runs every chip's workflow (`--full` adds the reflect-mode
|
||||
builds of libavr's spot set; `tools/make_presets.py` regenerates the presets).
|
||||
Per chip preset, `ctest` runs:
|
||||
|
||||
- `pureboot.size` — the 510-byte (tinies) / 512-byte (mega) budget;
|
||||
- `pureboot_*.size` — the size matrix: the serial backends × the clock
|
||||
ladder (1/8/16 MHz; the t13s' own RC menu), plus the USART1 build on the
|
||||
x4 chips — every configuration axis that could move the image, each
|
||||
variant against the same slot budget (pins are immediate operands and the
|
||||
timeout is a constant: size-neutral);
|
||||
- `pureboot.custom` (328P) — the configured-deployment acceptance test: the
|
||||
1 MHz software-serial TX=PB1/RX=PB5 build from the configuration example
|
||||
drives the full protocol suite through the runner's GPIO bridge, fixture
|
||||
application included;
|
||||
- `pureboot.usart1` (644A) — the same protocol suite over the second
|
||||
hardware USART: instance selection is compile-checked everywhere, but
|
||||
only a live session proves the loader polls the USART it claims;
|
||||
- `pureboot.pi` — the position-independence lint: no absolute `jmp`/`call`
|
||||
in the image, the info block within its first 256 bytes;
|
||||
- `pureboot.planner` — the host tool's pure logic: programming orders and
|
||||
their recovery properties, the surgery, the staging composition, the
|
||||
boot-fuse decode, the update preflight's error/warning matrix over
|
||||
synthetic fuse bytes, and the repairing verify against a fake device — one
|
||||
bad write repaired in a single rewrite, a page that never comes good
|
||||
stopping after exactly three;
|
||||
- `pureboot.size` — the 510-byte (patched-vector) / 512-byte budget;
|
||||
- `pureboot_*.size` — the size matrix: the serial backends × the clock ladder
|
||||
(1/8/16 MHz; the t13s' own RC menu), the USART1 instance across that same
|
||||
ladder on the x4 chips, and `pureboot_sw_wide`, the slowest ladder rate at
|
||||
the fastest clock — where a software UART's per-bit spin outgrows its
|
||||
one-register delay loop and takes the 16-bit one. That is the largest image
|
||||
the configuration space produces, and a shape the ladder default (always the
|
||||
*fastest* rate a clock reaches) never picks. Pins are immediate operands and
|
||||
the timeout is a constant: neither is an axis;
|
||||
- `pureboot.pi` — the position-independence lint: no absolute `jmp`/`call`, the
|
||||
info block within the image's first 256 bytes;
|
||||
- `pureboot.planner` — the host tool's pure logic: programming orders and their
|
||||
recovery properties, the surgery, the staging composition, the boot-fuse
|
||||
decode, the update preflight over synthetic fuse bytes, and the repairing
|
||||
verify against a fake device;
|
||||
- `pureboot.protocol` — end to end against a simavr device
|
||||
(`test/pureboot_device.c` — a hardware USART as a pty, or a cycle-timed
|
||||
GPIO⇄pty bridge for a software-UART build, selected with `-l` to match
|
||||
the loader's link; plus the SPM/NVM module simavr's tiny cores lack)
|
||||
driven by the real host tool through
|
||||
knock-from-reset, program + verify of both memories, session reconnect, an
|
||||
external reset through the patched vector, and the hand-over to a fixture
|
||||
application whose banner proves the launch — cross-checked against the
|
||||
simulator's ground-truth memory dumps and an independent decode of the
|
||||
surgery's rjmp words;
|
||||
- `pureboot.reloc` — the identical image installed one slot below the
|
||||
resident serves the complete command set from there (the
|
||||
position-independence acceptance test);
|
||||
- `pureboot.dirty` (328P) — entering the loader from a running application
|
||||
with no reset between, over an SPM page buffer the fixture deliberately
|
||||
dirtied: the case the loader declines to guard against. A bare verify must
|
||||
see the corruption, the repairing verify must fix it in one rewrite, and a
|
||||
plain verify afterwards must pass. On the boot-sectioned megas hardware
|
||||
forbids the state outright (SPM runs only from the boot section, and reset
|
||||
erases the buffer), but simavr dispatches SPM from anywhere — which is what
|
||||
makes the path constructible at all;
|
||||
- `pureboot.update` — the full `--update-loader` flow to a re-timed build,
|
||||
then every power-fail phase: the device is killed mid-write, restarted
|
||||
from its flash dump, and a re-run must complete the update with the
|
||||
application intact throughout.
|
||||
(`test/pureboot_device.c`: a hardware USART as a pty, or a cycle-timed
|
||||
GPIO⇄pty bridge for a software-UART build, plus the SPM/NVM module simavr's
|
||||
tiny cores lack) driven by the real host tool through knock-from-reset,
|
||||
program + verify of both memories, session reconnect, an external reset
|
||||
through the patched vector, and the hand-over to a fixture application whose
|
||||
banner proves the launch — cross-checked against the simulator's
|
||||
ground-truth memory dumps and an independent decode of the surgery;
|
||||
- `pureboot.reloc` — the identical image one slot below the resident serves the
|
||||
complete command set from there;
|
||||
- `pureboot.rehome` (t85) — a loader programmed at address 0 or in the staging
|
||||
slot re-homes into the top slot through the ordinary update flow;
|
||||
- `pureboot.custom` (328P) — the configuration example's 1 MHz software-serial
|
||||
build driving the full protocol suite, proving the plumbing produces a
|
||||
working loader and not just one that fits;
|
||||
- `pureboot.usart1` (644A) — the same suite over the second hardware USART:
|
||||
instance selection is compile-checked everywhere, but only a live session
|
||||
proves the loader polls the USART it claims;
|
||||
- `pureboot.dirty` (328P) — entering the loader from a running application over
|
||||
an SPM buffer it deliberately dirtied, the case the loader declines to guard:
|
||||
a bare verify must see the corruption and the repairing verify must fix it in
|
||||
one rewrite. Hardware forbids the state here, but simavr dispatches SPM from
|
||||
anywhere, which is what makes the path constructible;
|
||||
- `pureboot.update` — the full `--update-loader` flow, then every power-fail
|
||||
phase: the device is killed mid-write, restarted from its flash dump, and a
|
||||
re-run must complete the update with the application intact.
|
||||
|
||||
`size`, `pi`, and `planner` are host logic and run anywhere; the
|
||||
simulator-driven targets need simavr and a pty, so they are POSIX-only —
|
||||
on Windows the tool is exercised against real hardware.
|
||||
`size`, `pi` and `planner` are host logic and run anywhere; the
|
||||
simulator-driven targets need simavr and a pty, so they are POSIX-only.
|
||||
|
||||
Reference in New Issue
Block a user