pureboot: a version number for the loader, not for the protocol
The info block's third byte was a protocol version that never moved in the loader's lifetime. It is the pureboot version now, and this change is version 2: the one number that says what a deployed loader is. Every loader already in the field answers 1. The protocol keeps no number of its own — a pureboot version implies it, and the host tool is what holds that map. pureboot.py states the loader-version window it speaks (OLDEST_LOADER/NEWEST_LOADER; a version that changes the protocol becomes the new floor there), so a loader newer than the tool is refused by name rather than decoded on the assumption nothing moved, while an older one is read, identified and installed like any other. The tool carries its own version, free to drift from the loader's: --version prints it and the window, --info leads with the device's, --update-loader names the version it installs. Tests: the planner unit pins the window — every version in it decodes, one above it is refused, an older loader's image is still found — and the live suite pins the built loader against the tool beside it, so a bump that reaches only one of them fails. The image is byte-identical to the previous build but for that byte. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -35,7 +35,14 @@ else:
|
||||
import termios
|
||||
|
||||
PROMPT = b"+"
|
||||
PROTOCOL_VERSION = 1
|
||||
VERSION = 2 # this tool's own version — free to drift from a loader's
|
||||
# The loader versions this tool speaks to. A pureboot version implies its wire
|
||||
# protocol — the protocol carries no number of its own — so knowing which
|
||||
# versions speak what is the tool's job, and this window is where it says so:
|
||||
# every pureboot so far speaks this protocol, and a version that changes it
|
||||
# becomes the new floor here.
|
||||
OLDEST_LOADER = 1
|
||||
NEWEST_LOADER = 2
|
||||
SLOT = 512 # the loader slot on byte-addressed chips; word-addressed ones (>64 KiB) use 1 KiB — their own smallest boot sector
|
||||
RETRIES = 3 # rewrites of a page that reads back wrong, before the run stops
|
||||
|
||||
@@ -313,8 +320,12 @@ class Info:
|
||||
def __init__(self, raw):
|
||||
if len(raw) != 12 or raw[0:2] != b"PB":
|
||||
raise Error(f"bad info block: {raw.hex()}")
|
||||
if raw[2] != PROTOCOL_VERSION:
|
||||
raise Error(f"protocol version {raw[2]}, tool speaks {PROTOCOL_VERSION}")
|
||||
self.version = raw[2]
|
||||
if not OLDEST_LOADER <= self.version <= NEWEST_LOADER:
|
||||
raise Error(
|
||||
f"pureboot {self.version}: this tool (version {VERSION}) speaks pureboot "
|
||||
f"{OLDEST_LOADER}..{NEWEST_LOADER} — a newer loader needs a newer tool"
|
||||
)
|
||||
self.raw = bytes(raw)
|
||||
self.signature = raw[3:6]
|
||||
self.page = raw[6] or 256 # the wire count convention: 0 means 256
|
||||
@@ -349,6 +360,7 @@ class Info:
|
||||
else:
|
||||
hand_over = "hardware boot section, jump to word 0"
|
||||
return (
|
||||
f"version pureboot {self.version}",
|
||||
f"signature {' '.join(f'{b:02x}' for b in self.signature)}",
|
||||
f"flash {self.flash_size} B, {self.page} B pages"
|
||||
+ (", word-addressed wire" if self.word_flash else ""),
|
||||
@@ -638,9 +650,15 @@ def mega_boot(info, fuse_bytes):
|
||||
|
||||
|
||||
def image_info(image):
|
||||
"""The info block embedded in a pureboot binary, or None."""
|
||||
at = image.find(b"PB" + bytes((PROTOCOL_VERSION,)))
|
||||
return Info(image[at : at + 12]) if 0 <= at <= len(image) - 12 else None
|
||||
"""The info block embedded in a pureboot binary, or None. Searched per
|
||||
known loader version, so the magic stays three selective bytes rather than
|
||||
two that code could carry by chance — and a binary this tool does not know
|
||||
the version of reads as no block at all, which is what it is to the tool."""
|
||||
for version in range(OLDEST_LOADER, NEWEST_LOADER + 1):
|
||||
at = image.find(b"PB" + bytes((version,)))
|
||||
if 0 <= at <= len(image) - 12:
|
||||
return Info(image[at : at + 12])
|
||||
return None
|
||||
|
||||
|
||||
def loader_image(path):
|
||||
@@ -677,7 +695,10 @@ def update_preflight(image, info, fuse_bytes):
|
||||
"""Errors and warnings before any flash is touched. Returns warnings."""
|
||||
embedded = image_info(image)
|
||||
if embedded is None:
|
||||
raise Error("no pureboot info block in the update image — not a pureboot binary?")
|
||||
raise Error(
|
||||
"no pureboot info block in the update image — not a pureboot binary, "
|
||||
f"or a version this tool ({VERSION}) does not know"
|
||||
)
|
||||
if embedded.raw[3:] != info.raw[3:]:
|
||||
raise Error(
|
||||
f"update image is for another target: it declares "
|
||||
@@ -811,6 +832,8 @@ def op_update_loader(loader, wait, path, state_path, fuse_bytes):
|
||||
image = loader_image(path)
|
||||
for warning in update_preflight(image, info, fuse_bytes):
|
||||
print(f"note: {warning}")
|
||||
update = image_info(image) # the preflight proved it is there
|
||||
verbose(f"installing pureboot {update.version} over pureboot {info.version}")
|
||||
staged = staging_content(image, info)
|
||||
resident = bytes(image) + bytes([0xFF] * (info.slot - len(image)))
|
||||
page = info.page
|
||||
@@ -877,7 +900,7 @@ def op_update_loader(loader, wait, path, state_path, fuse_bytes):
|
||||
write_differing(loader, info.stage, state.staging, order, label="staging restore")
|
||||
|
||||
state.discard()
|
||||
print(f"loader updated: {len(image)} B at {info.base:#06x}, staging region restored")
|
||||
print(f"loader updated: pureboot {update.version}, {len(image)} B at {info.base:#06x}, staging region restored")
|
||||
|
||||
|
||||
def check_walk_region(pages, info, fuse_bytes, force):
|
||||
@@ -1052,6 +1075,8 @@ def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="pureboot host tool", epilog="operations run in the order listed above"
|
||||
)
|
||||
parser.add_argument("--version", action="version", version=f"%(prog)s {VERSION} "
|
||||
f"(speaks pureboot {OLDEST_LOADER}..{NEWEST_LOADER})")
|
||||
parser.add_argument("--port", required=True, help="serial device: COM6, /dev/ttyUSB0, or a simavr pty")
|
||||
parser.add_argument("--baud", type=int, default=115200, help="115200 mega, 57600 tinies")
|
||||
parser.add_argument("--wait", type=float, default=30.0, help="seconds to keep knocking")
|
||||
|
||||
Reference in New Issue
Block a user