pureboot.py: an update follows the staging copy onto its own link

--update-loader works by entering copies of the *new* image and letting them
rewrite the resident. Those copies speak the link they were built for, but the
host went on knocking with the session's baud and backend — the resident's. Where
the image changed either, the staging copy was installed and then never answered:
resident untouched, and on a 1 KiB tiny the staging slot is the whole application
region, so the application was already gone.

The wire cannot be probed for it. 512 bytes of position-independent code carry no
header saying what rate they were built for, so the operator declares it:
--staged-baud and --staged-autobaud, applied from the jump into the staging copy
onward. Retuning goes through the open port — SetCommState or tcsetattr on the
live handle, never a reopen — because a DTR pulse would reset the copy being
talked to. Undeclared against a changed link it still cannot work, but the error
now names that as the cause instead of reporting the bare activation timeout that
sent the operator looking at wiring.

The README's idempotence claim needed the same qualification: from step 2 a
re-run must reach the new image, and after step 3 word 0 points at the staging
copy, so on a patched-vector part the resident's link reaches nothing at all.

Found on an ATtiny13A, where two controls differing only in the activation window
updated cleanly and so isolated the link as the variable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-27 16:31:27 +02:00
committed by BlackMark
parent 433bec3e58
commit 77dd45aeca
4 changed files with 297 additions and 17 deletions

View File

@@ -328,6 +328,23 @@ 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.
One thing the image cannot tell the host: **which link it speaks.** The update
works by entering copies of the *new* image (steps 3 and 4 below), so a build
made for another baud or another backend answers on that one and not on the
session's — and 512 bytes of position-independent code carry no header to read
it from. Where the new image's link differs, name it:
```sh
# a 57600 fixed-baud resident, replaced by an autobaud build
pureboot.py --port … --baud 57600 --update-loader ab.bin --staged-autobaud
# …or by a 38400 build of the same backend
pureboot.py --port … --baud 57600 --update-loader sw38400.bin --staged-baud 38400
```
The host retunes on the open port, so no DTR pulse resets the copy it is talking
to. Omit them against a changed link and the update stops after installing the
staging copy, saying so and naming this as the cause.
The preflight refuses an image built for another chip: the stamp every pureboot
binary carries must resolve to the device's own geometry, and the error names
both. Die revisions share their base signature and geometry, so their images
@@ -348,10 +365,15 @@ are interchangeable — as the silicon is.
content, and the state file is discarded.
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
the same command after any interruption resumes and completes — with one
qualification, which is the link again: from step 2 on, the copy the re-run has
to reach is the *new* image, so a resumed run needs the same `--staged-*` as the
first one. On a patched-vector part step 3 also re-aims word 0 at the staging
copy, so after that point a reset reaches the new image's link and **only** that
one; a re-run on the resident's link finds nothing at all. 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).
@@ -425,6 +447,11 @@ Per chip preset, `ctest` runs:
- `pureboot.handshake` — the host tool's activation must not hang on a target
that never falls quiet: the drain after a prompt is bounded by the handshake
deadline, and a well-behaved loader still connects;
- `pureboot.updatelink` — an update whose image changes the baud or the backend
must follow the staging copy onto *its* link, since that copy is the new image;
and where nothing was declared, the failure must name the link rather than
report a bare activation timeout, because by then the staging slot is written
and on a 1 KiB tiny that was the application;
- `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