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:
@@ -61,7 +61,7 @@ def main():
|
||||
wire_base = base // 2 if word_flash else base
|
||||
flags = (1 if patch else 0) | (2 if word_flash else 0)
|
||||
info = pb.Info(
|
||||
bytes([ord("P"), ord("B"), 1, 0, 0, 0, page & 0xFF])
|
||||
bytes([ord("P"), ord("B"), pb.NEWEST_LOADER, 0, 0, 0, page & 0xFF])
|
||||
+ bytes([wire_base & 0xFF, wire_base >> 8, eeprom_size & 0xFF, eeprom_size >> 8])
|
||||
+ bytes([flags])
|
||||
)
|
||||
@@ -71,7 +71,7 @@ def main():
|
||||
# Session 1: knock from reset, identify, program everything, stay.
|
||||
out = pbsim.run_tool(tool, device.pty, baud, "--info", "--fuses", "--flash", app_bin,
|
||||
"--eeprom", ee_path, "--stay")
|
||||
for needed in ("signature", "fuses", "verify:", "stays"):
|
||||
for needed in ("version", "signature", "fuses", "verify:", "stays"):
|
||||
if needed not in out:
|
||||
fail(f"session 1 output lacks {needed!r}")
|
||||
|
||||
@@ -101,7 +101,12 @@ def main():
|
||||
port = pb.Port(device.pty, baud)
|
||||
try:
|
||||
loader = pb.Loader(port)
|
||||
loader.connect(15)
|
||||
live = loader.connect(15)
|
||||
# The loader built from this tree and the tool beside it must
|
||||
# agree on where the version numbering stands: a bump the tool
|
||||
# was never told about is a loader it would refuse to speak to.
|
||||
if live.version != pb.NEWEST_LOADER:
|
||||
fail(f"loader reports pureboot {live.version}, the tool's newest is {pb.NEWEST_LOADER}")
|
||||
loader.run_application()
|
||||
banner = port.read_exact(3, 5.0)
|
||||
if banner != b"APP":
|
||||
|
||||
@@ -27,12 +27,12 @@ def expect_error(what, fn, *needles):
|
||||
fail(f"{what}: no error raised")
|
||||
|
||||
|
||||
def info_of(pb, base, page, patch, flash, signature=(0x1E, 0x93, 0x0B), word_flash=False):
|
||||
def info_of(pb, base, page, patch, flash, signature=(0x1E, 0x93, 0x0B), word_flash=False, version=None):
|
||||
scale = 2 if word_flash else 1
|
||||
wire_base = base // scale
|
||||
flags = (1 if patch else 0) | (2 if word_flash else 0)
|
||||
raw = bytes((0x50, 0x42, 1, *signature, page & 0xFF, wire_base & 0xFF, wire_base >> 8,
|
||||
0, 2, flags))
|
||||
raw = bytes((0x50, 0x42, pb.NEWEST_LOADER if version is None else version,
|
||||
*signature, page & 0xFF, wire_base & 0xFF, wire_base >> 8, 0, 2, flags))
|
||||
info = pb.Info(raw)
|
||||
assert info.flash_size == flash
|
||||
return info
|
||||
@@ -55,6 +55,21 @@ def main():
|
||||
tiny = info_of(pb, 0x1E00, 64, True, 0x2000)
|
||||
mega = info_of(pb, 0x7E00, 128, False, 0x8000, signature=(0x1E, 0x95, 0x0F))
|
||||
|
||||
# Versioning: the block's third byte is the loader's version, and the tool
|
||||
# speaks a window of them. Every version in the window decodes, so an older
|
||||
# deployed loader stays usable; one above the window is refused by name,
|
||||
# since which version changed the protocol is knowledge only the tool
|
||||
# holds, and it holds none about a version it has never heard of.
|
||||
for version in range(pb.OLDEST_LOADER, pb.NEWEST_LOADER + 1):
|
||||
if info_of(pb, 0x1E00, 64, True, 0x2000, version=version).version != version:
|
||||
fail(f"pureboot {version} does not decode")
|
||||
expect_error(
|
||||
"unknown loader version",
|
||||
lambda: info_of(pb, 0x1E00, 64, True, 0x2000, version=pb.NEWEST_LOADER + 1),
|
||||
f"pureboot {pb.NEWEST_LOADER + 1}",
|
||||
"newer tool",
|
||||
)
|
||||
|
||||
# mega_boot: BOOTSZ words and the BOOTRST sense per chip — the fuse byte
|
||||
# index (EXTENDED on the x8 line except the m328s' HIGH, HIGH elsewhere)
|
||||
# and the per-family ladders (Atmel-2486/2466/2503/2545/8271/DS40002065/
|
||||
@@ -154,6 +169,12 @@ def main():
|
||||
fail("image_info misses the embedded block")
|
||||
if pb.image_info(bytes((0xAA,)) * 40) is not None:
|
||||
fail("image_info invents a block")
|
||||
# An older loader's image stays readable, so a deployed build can be
|
||||
# identified and installed like any other.
|
||||
old = info_of(pb, 0x1E00, 64, True, 0x2000, version=pb.OLDEST_LOADER)
|
||||
found_old = pb.image_info(bytes((0xAA,)) * 10 + old.raw)
|
||||
if found_old is None or found_old.version != pb.OLDEST_LOADER:
|
||||
fail("image_info misses an older loader's block")
|
||||
|
||||
# loader_image must peel a padded image down to the slot content: a raw
|
||||
# .bin padded from address 0 (or a whole-flash read-back with the loader
|
||||
|
||||
Reference in New Issue
Block a user