Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ab842c8d99 |
@@ -170,7 +170,7 @@ The info block (`b`):
|
|||||||
|
|
||||||
| Offset | Content |
|
| Offset | Content |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 0–2 | `'P'`, `'B'`, protocol version (1) |
|
| 0–2 | `'P'`, `'B'`, pureboot version (2) |
|
||||||
| 3–5 | device signature |
|
| 3–5 | device signature |
|
||||||
| 6 | SPM page size in bytes (0 means 256) |
|
| 6 | SPM page size in bytes (0 means 256) |
|
||||||
| 7–8 | loader base — application flash ends here (a word address when bit 1 is set) |
|
| 7–8 | loader base — application flash ends here (a word address when bit 1 is set) |
|
||||||
@@ -180,6 +180,21 @@ The info block (`b`):
|
|||||||
Composites are the host's job: verify = read back and compare, erase =
|
Composites are the host's job: verify = read back and compare, erase =
|
||||||
write `0xff` (per page for flash, per byte for EEPROM).
|
write `0xff` (per page for flash, per byte for EEPROM).
|
||||||
|
|
||||||
|
## Version
|
||||||
|
|
||||||
|
The third byte of the info block is the **pureboot version** — the loader's
|
||||||
|
one identity number, and the only way to tell what a deployed loader is.
|
||||||
|
Nothing else is numbered: the wire protocol has no version of its own, a
|
||||||
|
pureboot version implies its protocol, and the host tool is what holds that
|
||||||
|
map. It states the window of loader versions it speaks
|
||||||
|
(`OLDEST_LOADER`/`NEWEST_LOADER` in `pureboot.py`); a version that changes
|
||||||
|
the protocol becomes the new floor there. So far none has: pureboot 1 and 2
|
||||||
|
speak the identical session, and a loader newer than the tool is refused by
|
||||||
|
name rather than decoded on the assumption that nothing moved.
|
||||||
|
|
||||||
|
The tool carries its own version, free to drift from the loader's:
|
||||||
|
`--version` prints both it and the window.
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
The build leaves three artifacts per chip. The ELF is a container for the
|
The build leaves three artifacts per chip. The ELF is a container for the
|
||||||
@@ -245,7 +260,7 @@ BOOTRST does not exist there.
|
|||||||
## Updating the loader
|
## Updating the loader
|
||||||
|
|
||||||
`pureboot.py --update-loader new_pureboot.bin` replaces the resident loader
|
`pureboot.py --update-loader new_pureboot.bin` replaces the resident loader
|
||||||
with any pureboot build — a re-timed window, a newer protocol — using the
|
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
|
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
|
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:
|
links the loader at its base inside an otherwise blank flash image:
|
||||||
@@ -312,11 +327,12 @@ Intel HEX by extension. `--force` overrides the refusable safety checks (today:
|
|||||||
application data into a mega's reset walk region).
|
application data into a mega's reset walk region).
|
||||||
|
|
||||||
Readouts come one fact per line: `--info` prints the decoded info block
|
Readouts come one fact per line: `--info` prints the decoded info block
|
||||||
field by field, `--fuses` each fuse byte on its own line — plus, on a
|
field by field, the loader's version first; `--fuses` each fuse byte on its
|
||||||
boot-sectioned mega, the decoded meaning (where the BOOTSZ section starts,
|
own line — plus, on a boot-sectioned mega, the decoded meaning (where the
|
||||||
what BOOTRST does to reset). Transfers that take wire time — programming,
|
BOOTSZ section starts, what BOOTRST does to reset). Transfers that take
|
||||||
reading, erasing, verifying, the update phases — draw a transient progress
|
wire time — programming, reading, erasing, verifying, the update phases —
|
||||||
bar on stderr when it is a tty; logs and pipes see only the summary lines.
|
draw a transient progress bar on stderr when it is a tty; logs and pipes
|
||||||
|
see only the summary lines.
|
||||||
`-v`/`--verbose` adds the decisions as they happen: knock counts, the
|
`-v`/`--verbose` adds the decisions as they happen: knock counts, the
|
||||||
programming plan (vector-surgery targets, skipped blank pages), update
|
programming plan (vector-surgery targets, skipped blank pages), update
|
||||||
state handling and per-phase page counts.
|
state handling and per-phase page counts.
|
||||||
|
|||||||
@@ -92,6 +92,12 @@ constexpr std::uint16_t wire_page_mask = word_flash ? (page / 2 - 1) : (page - 1
|
|||||||
#endif
|
#endif
|
||||||
constexpr std::uint8_t timeout_seconds = PUREBOOT_TIMEOUT;
|
constexpr std::uint8_t timeout_seconds = PUREBOOT_TIMEOUT;
|
||||||
|
|
||||||
|
// The pureboot version: the loader's one identity number, carried in the info
|
||||||
|
// block so a host can tell a deployed loader apart from another. The wire
|
||||||
|
// protocol has no number of its own — a version implies its protocol, and the
|
||||||
|
// host tool is what holds that map (README.md).
|
||||||
|
constexpr std::uint8_t version = 2;
|
||||||
|
|
||||||
// The 12-byte info block the host reads with the 'b' command, flash-resident
|
// The 12-byte info block the host reads with the 'b' command, flash-resident
|
||||||
// through flash_table (there is no crt to copy a .data image, and its storage
|
// through flash_table (there is no crt to copy a .data image, and its storage
|
||||||
// carries the word alignment 'b' needs to halve the address on the large
|
// carries the word alignment 'b' needs to halve the address on the large
|
||||||
@@ -99,7 +105,7 @@ constexpr std::uint8_t timeout_seconds = PUREBOOT_TIMEOUT;
|
|||||||
inline constexpr avr::flash_table<std::array<std::uint8_t, 12>{
|
inline constexpr avr::flash_table<std::array<std::uint8_t, 12>{
|
||||||
'P',
|
'P',
|
||||||
'B',
|
'B',
|
||||||
1, // magic, protocol version
|
version, // magic, then the loader's version
|
||||||
avr::hw::db.signature[0],
|
avr::hw::db.signature[0],
|
||||||
avr::hw::db.signature[1],
|
avr::hw::db.signature[1],
|
||||||
avr::hw::db.signature[2],
|
avr::hw::db.signature[2],
|
||||||
|
|||||||
@@ -35,7 +35,14 @@ else:
|
|||||||
import termios
|
import termios
|
||||||
|
|
||||||
PROMPT = b"+"
|
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
|
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
|
RETRIES = 3 # rewrites of a page that reads back wrong, before the run stops
|
||||||
|
|
||||||
@@ -313,8 +320,12 @@ class Info:
|
|||||||
def __init__(self, raw):
|
def __init__(self, raw):
|
||||||
if len(raw) != 12 or raw[0:2] != b"PB":
|
if len(raw) != 12 or raw[0:2] != b"PB":
|
||||||
raise Error(f"bad info block: {raw.hex()}")
|
raise Error(f"bad info block: {raw.hex()}")
|
||||||
if raw[2] != PROTOCOL_VERSION:
|
self.version = raw[2]
|
||||||
raise Error(f"protocol version {raw[2]}, tool speaks {PROTOCOL_VERSION}")
|
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.raw = bytes(raw)
|
||||||
self.signature = raw[3:6]
|
self.signature = raw[3:6]
|
||||||
self.page = raw[6] or 256 # the wire count convention: 0 means 256
|
self.page = raw[6] or 256 # the wire count convention: 0 means 256
|
||||||
@@ -349,6 +360,7 @@ class Info:
|
|||||||
else:
|
else:
|
||||||
hand_over = "hardware boot section, jump to word 0"
|
hand_over = "hardware boot section, jump to word 0"
|
||||||
return (
|
return (
|
||||||
|
f"version pureboot {self.version}",
|
||||||
f"signature {' '.join(f'{b:02x}' for b in self.signature)}",
|
f"signature {' '.join(f'{b:02x}' for b in self.signature)}",
|
||||||
f"flash {self.flash_size} B, {self.page} B pages"
|
f"flash {self.flash_size} B, {self.page} B pages"
|
||||||
+ (", word-addressed wire" if self.word_flash else ""),
|
+ (", word-addressed wire" if self.word_flash else ""),
|
||||||
@@ -638,9 +650,15 @@ def mega_boot(info, fuse_bytes):
|
|||||||
|
|
||||||
|
|
||||||
def image_info(image):
|
def image_info(image):
|
||||||
"""The info block embedded in a pureboot binary, or None."""
|
"""The info block embedded in a pureboot binary, or None. Searched per
|
||||||
at = image.find(b"PB" + bytes((PROTOCOL_VERSION,)))
|
known loader version, so the magic stays three selective bytes rather than
|
||||||
return Info(image[at : at + 12]) if 0 <= at <= len(image) - 12 else None
|
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):
|
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."""
|
"""Errors and warnings before any flash is touched. Returns warnings."""
|
||||||
embedded = image_info(image)
|
embedded = image_info(image)
|
||||||
if embedded is None:
|
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:]:
|
if embedded.raw[3:] != info.raw[3:]:
|
||||||
raise Error(
|
raise Error(
|
||||||
f"update image is for another target: it declares "
|
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)
|
image = loader_image(path)
|
||||||
for warning in update_preflight(image, info, fuse_bytes):
|
for warning in update_preflight(image, info, fuse_bytes):
|
||||||
print(f"note: {warning}")
|
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)
|
staged = staging_content(image, info)
|
||||||
resident = bytes(image) + bytes([0xFF] * (info.slot - len(image)))
|
resident = bytes(image) + bytes([0xFF] * (info.slot - len(image)))
|
||||||
page = info.page
|
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")
|
write_differing(loader, info.stage, state.staging, order, label="staging restore")
|
||||||
|
|
||||||
state.discard()
|
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):
|
def check_walk_region(pages, info, fuse_bytes, force):
|
||||||
@@ -1052,6 +1075,8 @@ def main():
|
|||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="pureboot host tool", epilog="operations run in the order listed above"
|
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("--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("--baud", type=int, default=115200, help="115200 mega, 57600 tinies")
|
||||||
parser.add_argument("--wait", type=float, default=30.0, help="seconds to keep knocking")
|
parser.add_argument("--wait", type=float, default=30.0, help="seconds to keep knocking")
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ def main():
|
|||||||
wire_base = base // 2 if word_flash else base
|
wire_base = base // 2 if word_flash else base
|
||||||
flags = (1 if patch else 0) | (2 if word_flash else 0)
|
flags = (1 if patch else 0) | (2 if word_flash else 0)
|
||||||
info = pb.Info(
|
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([wire_base & 0xFF, wire_base >> 8, eeprom_size & 0xFF, eeprom_size >> 8])
|
||||||
+ bytes([flags])
|
+ bytes([flags])
|
||||||
)
|
)
|
||||||
@@ -71,7 +71,7 @@ def main():
|
|||||||
# Session 1: knock from reset, identify, program everything, stay.
|
# Session 1: knock from reset, identify, program everything, stay.
|
||||||
out = pbsim.run_tool(tool, device.pty, baud, "--info", "--fuses", "--flash", app_bin,
|
out = pbsim.run_tool(tool, device.pty, baud, "--info", "--fuses", "--flash", app_bin,
|
||||||
"--eeprom", ee_path, "--stay")
|
"--eeprom", ee_path, "--stay")
|
||||||
for needed in ("signature", "fuses", "verify:", "stays"):
|
for needed in ("version", "signature", "fuses", "verify:", "stays"):
|
||||||
if needed not in out:
|
if needed not in out:
|
||||||
fail(f"session 1 output lacks {needed!r}")
|
fail(f"session 1 output lacks {needed!r}")
|
||||||
|
|
||||||
@@ -101,7 +101,12 @@ def main():
|
|||||||
port = pb.Port(device.pty, baud)
|
port = pb.Port(device.pty, baud)
|
||||||
try:
|
try:
|
||||||
loader = pb.Loader(port)
|
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()
|
loader.run_application()
|
||||||
banner = port.read_exact(3, 5.0)
|
banner = port.read_exact(3, 5.0)
|
||||||
if banner != b"APP":
|
if banner != b"APP":
|
||||||
|
|||||||
@@ -27,12 +27,12 @@ def expect_error(what, fn, *needles):
|
|||||||
fail(f"{what}: no error raised")
|
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
|
scale = 2 if word_flash else 1
|
||||||
wire_base = base // scale
|
wire_base = base // scale
|
||||||
flags = (1 if patch else 0) | (2 if word_flash else 0)
|
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,
|
raw = bytes((0x50, 0x42, pb.NEWEST_LOADER if version is None else version,
|
||||||
0, 2, flags))
|
*signature, page & 0xFF, wire_base & 0xFF, wire_base >> 8, 0, 2, flags))
|
||||||
info = pb.Info(raw)
|
info = pb.Info(raw)
|
||||||
assert info.flash_size == flash
|
assert info.flash_size == flash
|
||||||
return info
|
return info
|
||||||
@@ -55,6 +55,21 @@ def main():
|
|||||||
tiny = info_of(pb, 0x1E00, 64, True, 0x2000)
|
tiny = info_of(pb, 0x1E00, 64, True, 0x2000)
|
||||||
mega = info_of(pb, 0x7E00, 128, False, 0x8000, signature=(0x1E, 0x95, 0x0F))
|
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
|
# 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)
|
# index (EXTENDED on the x8 line except the m328s' HIGH, HIGH elsewhere)
|
||||||
# and the per-family ladders (Atmel-2486/2466/2503/2545/8271/DS40002065/
|
# 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")
|
fail("image_info misses the embedded block")
|
||||||
if pb.image_info(bytes((0xAA,)) * 40) is not None:
|
if pb.image_info(bytes((0xAA,)) * 40) is not None:
|
||||||
fail("image_info invents a block")
|
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
|
# 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
|
# .bin padded from address 0 (or a whole-flash read-back with the loader
|
||||||
|
|||||||
Reference in New Issue
Block a user