pureboot: no SPM buffer discard, the host repairs instead
The temporary page buffer is write-once per word, so a page filled over one an earlier writer left dirty programs the stale words. The same datasheet clause carries the cure: the buffer auto-erases after a page write (§26.2.1; §19.2 on the tinies), so the corruption clears itself by happening, and rewriting the page programs correctly. The loader therefore clears the buffer nowhere. The tinies' CTPB and the m48s' RWWSRE discard are gone; the boot-sectioned megas keep only the trailing RWWSRE they need anyway to re-enable the RWW section for read-back, which discards the buffer as a side effect and keeps them off the path entirely. 434 B on the tiny13s, 438-442 on the tiny25/45/85, 430 on the m48s; the megas are unchanged, the 1284s still 506. The host takes over the guarantee: a flash page that reads back wrong is rewritten up to RETRIES times before the run stops. Both read-back paths repair — verify_pages for programming, and write_differing, which is the loader-update path where a page left wrong is a half-written loader slot. That one is not hypothetical: deleting the discard made attiny85 pureboot.rehome fail deterministically there, the only flow still assuming the old contract. Protocol-visible, so README's W command says it: one W may program the wrong bytes after a refused page, or after an application that self-programmed entered without a reset, and a host that programs without reading back cannot trust it. Tests: pureboot.dirty drives the case the loader declines to guard — the fixture application dirties every buffer word and jumps in with no reset (hardware forbids that on a boot-sectioned mega, but simavr dispatches SPM from anywhere, which is what makes it constructible) — and asserts a bare verify sees the corruption, the repairing verify fixes it in one rewrite, and it stays fixed. pbreloc asserts the same shape after a refusal. test_planner covers the bound against a fake device: one bad write repaired in a single rewrite, a page that never comes good stopping after exactly three. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,19 +2,22 @@
|
||||
|
||||
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 allowed), built for **every chip libavr targets — all 37 —
|
||||
fitting each chip's smallest boot sector**: 512 bytes everywhere — 470 B on
|
||||
the tiny13s, ~484 B on the tiny25/45/85, 458–506 B across the megas and
|
||||
every configured variant of them (the m8's software-serial build is the
|
||||
fattest) — except the ATmega1284/1284P, whose smallest boot sector is 1 KiB
|
||||
and whose far-flash machinery (ELPM reads, RAMPZ page commands,
|
||||
word-addressed wire) lands at 556–562 B in a 1 KiB slot: the 512-byte
|
||||
figure is a hardware boundary those chips simply do not have, and no
|
||||
implementation of this feature set fits it there. 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`).
|
||||
(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 every configured variant of
|
||||
them, and 506 B on the ATmega1284/1284P, whose far-flash machinery (ELPM
|
||||
reads, RAMPZ page commands, word-addressed wire) is the heaviest. The 1284s
|
||||
are the tight case: the loop-placement attributes on the byte streamers
|
||||
(`pureboot.cpp`) and the codegen flags on the loader TU (`CMakeLists.txt`)
|
||||
are what carry that build under the line. 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.
|
||||
|
||||
The image is **position-independent**: control flow is PC-relative, the
|
||||
read/write paths take wire addresses, the write guard protects the slot the
|
||||
@@ -133,7 +136,20 @@ byte-addressed). EEPROM addresses are always bytes, counts always bytes.
|
||||
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` is host-paced: send the next byte only after the previous
|
||||
resident slot.
|
||||
|
||||
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.
|
||||
|
||||
`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
|
||||
@@ -283,8 +299,11 @@ 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`; images are raw binary, or Intel HEX by
|
||||
extension. `--force` overrides the refusable safety checks (today: flashing
|
||||
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).
|
||||
|
||||
Readouts come one fact per line: `--info` prints the decoded info block
|
||||
@@ -320,8 +339,10 @@ regenerates the presets). Per chip preset, `ctest` runs:
|
||||
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, and the update preflight's error/warning matrix over
|
||||
synthetic fuse bytes;
|
||||
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.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
|
||||
@@ -335,6 +356,14 @@ regenerates the presets). Per chip preset, `ctest` runs:
|
||||
- `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
|
||||
|
||||
Reference in New Issue
Block a user