pureboot 3: a 512-byte slot on every chip, the 1284s included
The word-addressed 1284s were the one family deploying in a 1 KiB slot, because the far-flash machinery (ELPM reads, RAMPZ page commands, a word-addressed wire) did not fit 512 B. It does now: 478 B stock, 494 B in the heaviest configuration the build can produce. They take the 644s' geometry, where the smallest boot section holds the resident slot and its staging slot together. The loader's version goes to 3; the host tool did not change, so its own version stays 2 and only the window it speaks widens. Most of the saving is one restructure. The info block and a flash read are the same act, so giving all four streamed commands one address-and-count path leaves exactly one call site for the flash streamer: it inlines into the never-returning command loop and its 24-bit cursor stops being saved and restored around every transmit. Around it, the ack byte moved out of line, the wire's byte pair is bit_cast into the word it already is, the fuse loop ends on its count, the info block's in-slot offset is taken as the one-byte relocation it is, and -fno-expensive-optimizations gives way to -fno-move-loop-invariants -fno-tree-ter. Every chip shrank 14-18 B. The size matrix grew the axes it was missing: the USART1 instance across the whole clock ladder, and the shape a slow baud gives a software UART — past 255 delay iterations libavr takes the 16-bit delay loop, which the ladder default never selects and which was 4 B over the 1284's slot the first time it was built. The protocol fixture stopped deriving the loader entry from the flash size; on the 1284s it had been jumping a slot low and reaching the loader only because erased flash walked it up. Docs and comments were consolidated across the port in the same pass: the README carries a per-chip size table instead of prose, and prose that restated the code is gone — 190 lines, no behaviour with it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,17 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Position-independence lint for the pureboot image.
|
||||
"""Position-independence lint: the two link-time facts that let the identical
|
||||
image run from any slot, asserted from the built ELF.
|
||||
|
||||
The self-staging design lets the identical binary run from any 512-byte
|
||||
slot, which holds only if nothing in the image addresses itself absolutely.
|
||||
Two link-time facts guarantee it, both asserted here from the built ELF:
|
||||
|
||||
1. No absolute jmp/call opcodes — all control flow is PC-relative
|
||||
(rjmp/rcall/ijmp/icall). -mrelax normally guarantees this; a code
|
||||
change that grows a branch out of relaxation range would break it
|
||||
silently.
|
||||
2. The info block sits within the image's first 256 bytes: the 'b'
|
||||
command rebuilds its address as (running slot high byte : low byte of
|
||||
the link address), which needs the offset to fit that low byte.
|
||||
1. No absolute jmp/call — -mrelax normally guarantees it, but a branch that
|
||||
grows out of relaxation range would break it silently.
|
||||
2. The info block within the image's first 256 bytes: 'b' rebuilds its
|
||||
address as (running slot high byte : link address low byte).
|
||||
|
||||
Usage: check_pi.py <objdump> <nm> <elf> <text_start_hex>
|
||||
"""
|
||||
|
||||
@@ -66,9 +66,10 @@ struct link {
|
||||
}
|
||||
[[noreturn]] static void idle()
|
||||
{
|
||||
// 'L' hands back to the loader at the top slot — 512 bytes, or the
|
||||
// 1 KiB the >64 KiB chips use.
|
||||
constexpr std::uint32_t slot = avr::hw::db.mem.flash_size > 65536 ? 1024 : 512;
|
||||
// 'L' hands back to the loader in the top slot — 512 bytes on every
|
||||
// chip. The jump takes a word address, which is what makes the
|
||||
// >64 KiB chips' entry reachable through a 16-bit pointer at all.
|
||||
constexpr std::uint32_t slot = 512;
|
||||
for (;;) {
|
||||
auto command = tx_t::read_blocking();
|
||||
if (command == 'L')
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Dirty-page-buffer acceptance test: the loader carries no buffer discard,
|
||||
so a page filled over words an earlier writer left behind programs those
|
||||
instead. This asserts the whole contract — the corruption is real and a bare
|
||||
verify sees it, the repairing verify fixes it in one rewrite (the write that
|
||||
took the stale words auto-erased the buffer), and it stays fixed.
|
||||
"""Dirty-page-buffer acceptance test: with no discard in the loader, a page
|
||||
filled over words an earlier writer left takes those instead. The whole
|
||||
contract is asserted — a bare verify sees the corruption, the repairing
|
||||
verify fixes it in one rewrite, and it stays fixed.
|
||||
|
||||
The state is reached the way the loader cannot prevent: an application
|
||||
dirties the buffer and jumps in with no reset between. Real boot-sectioned
|
||||
megas forbid that outright — SPM executes only from the boot section
|
||||
(Atmel-8271 §26.2) — but simavr dispatches SPM from anywhere, which is what
|
||||
makes the path constructible at all.
|
||||
The state is reached the one way the loader cannot prevent: an application
|
||||
dirties the buffer and jumps in with no reset between. Boot-sectioned megas
|
||||
forbid that outright (SPM runs only from the boot section, Atmel-8271 §26.2),
|
||||
but simavr dispatches SPM from anywhere, which is what makes it constructible.
|
||||
|
||||
Usage: pbdirty.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
|
||||
<baud> <app_bin> <tool_py> <workdir>
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Re-homing acceptance test: a pureboot image programmed somewhere other
|
||||
than its canonical top slot must still be a working loader —
|
||||
position-independent, guarding its accidental slot — and the ordinary
|
||||
"""Re-homing acceptance test: an image programmed somewhere other than its
|
||||
canonical slot must still be a working loader, and the ordinary
|
||||
--update-loader flow must put a build into the top slot from there.
|
||||
|
||||
Two positions are exercised. Address 0 (a raw .bin handed to a programmer,
|
||||
which defaults to offset 0): the staging install and the word-0 redirect
|
||||
both run from copies whose slots are not page 0's, so the running-slot
|
||||
guard never blocks the flow. The staging slot itself: a loader already
|
||||
sitting there IS the installed staging copy — the tool recognizes it by
|
||||
its embedded info block and leaves it in place instead of tripping the
|
||||
copy's own guard on the composed through-word — and that (older) copy
|
||||
streams the new resident like any staged copy. In both cases flashing an
|
||||
application through the healed resident overwrites the stale copy, vector
|
||||
surgery included, and the banner proves the launch.
|
||||
Two positions. Address 0, a raw .bin handed to a programmer: the staging
|
||||
install and the word-0 redirect run from copies outside page 0's slot, so the
|
||||
running-slot guard never blocks them. And the staging slot itself, where a
|
||||
loader already sitting there IS the staging copy — recognized by its embedded
|
||||
block and left in place, then streaming the new resident like any staged copy.
|
||||
|
||||
Usage: pbrehome.py <device_bin> <pureboot_elf> <update_bin> <mcu> <hz>
|
||||
<base_hex> <page> <baud> <app_bin> <tool_py> <workdir>
|
||||
@@ -90,7 +84,7 @@ def main():
|
||||
# The staging slot: erased flash with the loader sitting exactly where
|
||||
# a staging copy would — the tool must leave it in place and let it
|
||||
# stream the (different) update build into the resident slot.
|
||||
stage = base - 512
|
||||
stage = base - pb.SLOT
|
||||
rehome_from(pbsim, pb, device_bin, elf, hex(stage), hex(stage), update_bin, base, page, baud, app_bin, workdir,
|
||||
mcu, hz)
|
||||
print("re-home from the staging slot: converged")
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Position-independence acceptance test: the identical pureboot binary,
|
||||
flashed one slot below the resident loader, must serve the complete command
|
||||
set from there. The resident installs it (through-word composed by the host
|
||||
layer), 'J' transfers control, and every command is exercised against the
|
||||
staged copy — the info block must come back byte-identical, the write guard
|
||||
must protect the staged copy's own slot and permit the resident's, and the
|
||||
staged copy must be able to rewrite the resident slot verbatim.
|
||||
"""Position-independence acceptance test: the identical binary, flashed one
|
||||
slot below the resident, must serve the complete command set from there. The
|
||||
info block must come back byte-identical, the write guard must refuse the
|
||||
staged copy's own slot and permit the resident's, and the staged copy must be
|
||||
able to rewrite the resident verbatim.
|
||||
|
||||
Usage: pbreloc.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
|
||||
<baud> <tool_py> <workdir>
|
||||
@@ -83,7 +81,7 @@ def main():
|
||||
|
||||
# Restore the resident image through the staged copy, then 'J' back
|
||||
# into it and prove it lives.
|
||||
resident = image + b"\xff" * (info.slot - len(image))
|
||||
resident = image + b"\xff" * (pb.SLOT - len(image))
|
||||
pb.write_differing(loader, base, resident)
|
||||
back_info = loader.enter_copy(base, 25)
|
||||
if back_info.raw != resident_info:
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
"""End-to-end pureboot protocol test: spawn the simavr device, then drive it
|
||||
with the real host tool (pureboot.py, as a subprocess over the device's pty)
|
||||
through flash + EEPROM + fuse + hand-over scenarios, and cross-check
|
||||
the tool's view against the simulator's ground-truth memory dumps.
|
||||
"""End-to-end protocol test: drive the simavr device with the real host tool
|
||||
over its pty through flash, EEPROM, fuse and hand-over scenarios, and
|
||||
cross-check the tool's view against the simulator's ground-truth dumps.
|
||||
|
||||
Usage: pbtest.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
|
||||
<baud> <eeprom_size> <app_bin> <tool_py> <workdir> [link]
|
||||
|
||||
The optional link is the runner's -l spec (usart1, sw:B5,B1, ...) for a
|
||||
The optional link is the runner's -l spec (usart1, sw:B5,B1, ...), for a
|
||||
loader built off the chip's natural serial default.
|
||||
Exits 0 if every scenario passes.
|
||||
"""
|
||||
|
||||
import os
|
||||
@@ -57,7 +55,7 @@ def main():
|
||||
# the page byte is the wire's 0-means-256.
|
||||
mega = mcu.startswith("atmega")
|
||||
patch = not mega or mcu.startswith("atmega48")
|
||||
word_flash = base + 512 > 0x10000
|
||||
word_flash = base + pb.SLOT > 0x10000
|
||||
wire_base = base // 2 if word_flash else base
|
||||
flags = (1 if patch else 0) | (2 if word_flash else 0)
|
||||
info = pb.Info(
|
||||
@@ -127,7 +125,7 @@ def main():
|
||||
# loader, the trampoline on the application's own entry (patched-vector
|
||||
# chips only — a boot-sectioned mega's word 0 stays the application's).
|
||||
if patch:
|
||||
flash_words = (base + 512) // 2
|
||||
flash_words = (base + pb.SLOT) // 2
|
||||
app = open(app_bin, "rb").read()
|
||||
word0 = flash_true[0] | (flash_true[1] << 8)
|
||||
if rjmp_decode(word0, 0, flash_words) != base // 2:
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Self-update end-to-end: an application is flashed, then the loader
|
||||
replaces itself with a re-timed build through the host tool's
|
||||
--update-loader — and the power-fail phases of that update are rehearsed by
|
||||
killing the simulated device mid-write, restarting it from its flash dump,
|
||||
and letting a re-run complete the update.
|
||||
"""Self-update end-to-end: an application is flashed, the loader replaces
|
||||
itself with a re-timed build, and every power-fail phase is rehearsed by
|
||||
killing the device mid-write, restarting it from its flash dump, and letting
|
||||
a re-run complete the update.
|
||||
|
||||
The boot-sectioned megas run the BOOTRST-unprogrammed profile (reset boots
|
||||
the application; the fixture application's 'L' jump is the application-owned
|
||||
loader entry), with --assume-fuses standing in for the fuse read simavr
|
||||
cannot model. The patched-vector chips — the tinies and the m48s — reset
|
||||
into a loader at every phase by construction: the t13a because its staging
|
||||
slot carries the reset vector itself, the others through the word-0 redirect
|
||||
the tool plants around the resident rewrite.
|
||||
The boot-sectioned megas run the BOOTRST-unprogrammed profile — reset boots
|
||||
the application, whose 'L' is the application-owned loader entry — with
|
||||
--assume-fuses standing in for the fuse read simavr cannot model.
|
||||
|
||||
Usage: pbupdate.py <device_bin> <pureboot_elf> <update_elf> <mcu> <hz>
|
||||
<base_hex> <page> <baud> <app_bin> <tool_py> <workdir>
|
||||
@@ -50,7 +45,7 @@ def assumed_fuses(pb, image):
|
||||
image's embedded signature."""
|
||||
info = pb.image_info(image)
|
||||
which, ladder = pb.BOOT_FUSE[bytes(info.signature[1:3])]
|
||||
bits = min((b for b in ladder if ladder[b] * 2 >= 2 * info.slot), key=lambda b: ladder[b])
|
||||
bits = min((b for b in ladder if ladder[b] * 2 >= 2 * pb.SLOT), key=lambda b: ladder[b])
|
||||
fuses = bytearray((0xFF, 0xFF, 0xFF, 0xFF))
|
||||
fuses[which] = 0xF8 | (bits << 1) | 1
|
||||
return bytes(fuses)
|
||||
@@ -93,16 +88,13 @@ def main():
|
||||
# The m48s are megas without a boot section: patched vector, no fuse
|
||||
# preflight, and the same reset-to-0 the tinies get.
|
||||
patch = not mega or mcu.startswith("atmega48")
|
||||
# Word-addressed (>64 KiB) chips use the 1 KiB slot; their loader base
|
||||
# itself sits beyond the 16-bit byte space — the 644's base + slot only
|
||||
# touches the 64 KiB boundary and stays byte-addressed.
|
||||
slot = 1024 if base >= 0x10000 and mega else 512
|
||||
reset_hex = "0" if mega else None # the boot-sectioned mega runs BOOTRST-unprogrammed here
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import pbsim
|
||||
import pureboot as pb
|
||||
|
||||
slot = pb.SLOT
|
||||
os.makedirs(workdir, exist_ok=True)
|
||||
objcopy = os.environ.get("PB_OBJCOPY", "avr-objcopy")
|
||||
images = {}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Host-tool unit tests — the pure planning and policy logic, no simulator:
|
||||
the flash-programming orders and their recovery properties, the reset-vector
|
||||
surgery, the staging-slot composition, the mega boot-fuse decode, and the
|
||||
update preflight's error/warning matrix (fuse combinations simavr cannot
|
||||
model reach it here as synthetic bytes).
|
||||
"""Host-tool unit tests — the planning and policy logic, no simulator:
|
||||
programming orders and their recovery properties, the reset-vector surgery,
|
||||
the staging composition, the boot-fuse decode, and the update preflight over
|
||||
fuse combinations simavr cannot model.
|
||||
|
||||
Usage: test_planner.py <tool_py>
|
||||
"""
|
||||
@@ -34,7 +33,8 @@ def info_of(pb, base, page, patch, flash, signature=(0x1E, 0x93, 0x0B), word_fla
|
||||
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
|
||||
if info.flash_size != flash:
|
||||
fail(f"info_of({base:#x}) decodes to {info.flash_size:#x} of flash, not {flash:#x}")
|
||||
return info
|
||||
|
||||
|
||||
@@ -94,9 +94,7 @@ def main():
|
||||
((0x1E, 0x97, 0x05), 0x20000, 3, {0b11: 0x1FC00, 0b10: 0x1F800, 0b01: 0x1F000, 0b00: 0x1E000}), # 1284P
|
||||
)
|
||||
for signature, flash, which, ladder in cases:
|
||||
# Word-addressed chips carry the 1 KiB slot (their smallest boot sector).
|
||||
slot = 1024 if flash > 0x10000 else 512
|
||||
chip = info_of(pb, flash - slot, 128 if flash < 0x20000 else 0, False, flash,
|
||||
chip = info_of(pb, flash - pb.SLOT, 128 if flash < 0x20000 else 0, False, flash,
|
||||
signature=signature, word_flash=flash > 0x10000)
|
||||
for bits, start in ladder.items():
|
||||
fuses = bytearray((0xFF, 0xFF, 0xFF, 0xFF))
|
||||
@@ -109,10 +107,12 @@ def main():
|
||||
if prog or at != start:
|
||||
fail(f"mega_boot {signature[1]:02x}{signature[2]:02b} unprogrammed: {prog} {at:#07x}")
|
||||
|
||||
# Word-addressed info decode: the 1284P's base/page ride the wire scaled,
|
||||
# and its slot is 1 KiB.
|
||||
big = info_of(pb, 0x1FC00, 0, False, 0x20000, signature=(0x1E, 0x97, 0x05), word_flash=True)
|
||||
if big.page != 256 or big.base != 0x1FC00 or big.stage != 0x1F800 or big.slot != 1024:
|
||||
# Word-addressed info decode: the 1284P's base and page ride the wire
|
||||
# scaled — a 17-bit base halved into the block's two bytes, a 256-byte page
|
||||
# spelled 0 — and its slot is the same 512 bytes as everywhere else, so its
|
||||
# staging slot lands inside the 1 KiB minimum boot section.
|
||||
big = info_of(pb, 0x1FE00, 0, False, 0x20000, signature=(0x1E, 0x97, 0x05), word_flash=True)
|
||||
if big.page != 256 or big.base != 0x1FE00 or big.stage != 0x1FC00:
|
||||
fail(f"word-addressed info decode: page {big.page}, base {big.base:#x}, stage {big.stage:#x}")
|
||||
|
||||
# Surgery: word 0 lands on the loader, the trampoline on the original
|
||||
@@ -215,6 +215,15 @@ def main():
|
||||
if pb.update_preflight(bytes((0xAA,)) * 8 + tiny.raw, tiny, None) != []:
|
||||
fail("tiny preflight should pass without fuses")
|
||||
|
||||
# The 1284s' smallest boot section (512 words) is exactly the resident
|
||||
# slot plus its staging slot, so self-update is possible at the minimum
|
||||
# BOOTSZ — no fuse step up, the 644's geometry. That holds only while a
|
||||
# slot is 512 B: at 1 KiB the staging slot would fall outside the section
|
||||
# and the preflight would refuse.
|
||||
notes = pb.update_preflight(bytes((0xAA,)) * 8 + big.raw, big, fuses(0xFE))
|
||||
if not any("staging slot" in n for n in notes):
|
||||
fail(f"1284 minimum-BOOTSZ notes: {notes}")
|
||||
|
||||
# The walk-region refusal: BOOTRST aimed below the loader plus app data
|
||||
# in the walk span errors without --force; erased spans and unprogrammed
|
||||
# BOOTRST pass.
|
||||
|
||||
Reference in New Issue
Block a user