pureboot: take the emitted loader HEX for --update-loader

The build emits an Intel HEX beside every loader image, but --update-loader
could not consume one: load_image() anchors every image at address zero, and
a loader HEX links at its base, so it decoded to a 32760-byte blob carrying
504 bytes of loader at the end. staging_content() then refused it as "loader
image is 32760 B, the slot holds 512" - an error naming neither the cause nor
the raw .bin the tool wanted instead.

Drop the blank below the base in the update path. The base comes from the
image's own info block rather than the device's, so an image built for
another target survives the slice intact and the preflight still reports it
as another target rather than failing to find an info block at all.

Verified on an ATmega328P: the full self-update flow driven straight from
pureboot_timeout-5s.hex, resident slot byte-for-byte against the image
afterwards, application preserved; both refusal paths unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-20 22:46:50 +02:00
parent 82a31d4f16
commit e57b79c94b
2 changed files with 18 additions and 2 deletions

View File

@@ -124,7 +124,9 @@ the loader; an erase runs top-down for the same reason.
`pureboot.py --update-loader new_pureboot.bin` replaces the resident loader
with any pureboot build — a re-timed window, a newer protocol — using the
loader itself as its own staging loader:
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:
1. The staging slot `[base512, base)` is saved to a host-side state file
(on the 1 KB tiny13A that is the whole application, vectors included).

View File

@@ -514,6 +514,20 @@ def image_info(image):
return Info(image[at : at + 12]) if 0 <= at <= len(image) - 12 else None
def loader_image(path):
"""A loader update image, as the slot's own content. A raw binary is that
already; an Intel HEX links the loader at its base inside an otherwise
blank flash image, and load_image() anchors every image at zero, so the
blank below the base is dropped here. The base comes from the image's own
info block rather than the device's, so an image built for somewhere else
survives intact and the preflight can say so."""
image = load_image(path)
embedded = image_info(image)
if embedded and len(image) > embedded.base:
image = image[embedded.base :]
return image
def staging_content(image, info):
"""The 512-byte staging-slot content: the image, padding, and — on
chips whose hand-over jumps through the word below the resident loader —
@@ -643,7 +657,7 @@ def op_update_loader(loader, wait, path, state_path, fuse_bytes):
actual flash state, so a re-run after any interruption resumes; the
state file carries the bytes the staging slot held."""
info = loader.info
image = load_image(path)
image = loader_image(path)
for warning in update_preflight(image, info, fuse_bytes):
print(f"note: {warning}")
staged = staging_content(image, info)