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>
148 lines
6.5 KiB
Python
148 lines
6.5 KiB
Python
#!/usr/bin/env python3
|
|
"""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
|
|
loader built off the chip's natural serial default.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
|
|
|
|
def fail(message):
|
|
print(f"FAIL: {message}")
|
|
sys.exit(1)
|
|
|
|
|
|
def rjmp_decode(word, at, flash_words):
|
|
"""Where an rjmp word at word-address `at` lands — deliberately written
|
|
against the instruction-set definition (12-bit signed offset), not with
|
|
the host tool's encoder, so an encoding bug cannot verify itself."""
|
|
if word & 0xF000 != 0xC000:
|
|
fail(f"word at {at * 2:#06x} is {word:#06x}, not an rjmp")
|
|
offset = word & 0x0FFF
|
|
if offset >= 0x800:
|
|
offset -= 0x1000
|
|
return (at + 1 + offset) % flash_words
|
|
|
|
|
|
def main():
|
|
args = sys.argv[1:]
|
|
link = args.pop() if len(args) == 12 else None
|
|
(device_bin, elf, mcu, hz, base_hex, page, baud, eeprom_size, app_bin, tool, workdir) = args
|
|
base, page, baud, eeprom_size = int(base_hex, 0), int(page), int(baud), int(eeprom_size)
|
|
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
|
|
|
|
os.makedirs(workdir, exist_ok=True)
|
|
ee_image = bytes(range(0xA0, 0xB0))
|
|
ee_path = os.path.join(workdir, "ee.bin")
|
|
open(ee_path, "wb").write(ee_image)
|
|
dump = os.path.join(workdir, "flash_dump.bin")
|
|
read_flash = os.path.join(workdir, "readback_flash.bin")
|
|
read_eeprom = os.path.join(workdir, "readback_eeprom.bin")
|
|
|
|
# The geometry the host will discover, for computing the expected image:
|
|
# the boot-sectioned megas need no vector surgery (the tinies and the
|
|
# boot-section-less m48s do), the large chips speak word addresses, and
|
|
# the page byte is the wire's 0-means-256.
|
|
mega = mcu.startswith("atmega")
|
|
patch = not mega or mcu.startswith("atmega48")
|
|
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(
|
|
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])
|
|
)
|
|
|
|
device = pbsim.Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump, link=link)
|
|
try:
|
|
# 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 ("version", "signature", "fuses", "verify:", "stays"):
|
|
if needed not in out:
|
|
fail(f"session 1 output lacks {needed!r}")
|
|
|
|
# Session 2: reconnect into the live session, verify, dump, hand over
|
|
# is deferred — the pty must be reopened for the APP banner first.
|
|
out = pbsim.run_tool(tool, device.pty, baud, "--verify-flash", app_bin, "--verify-eeprom", ee_path,
|
|
"--read-flash", read_flash, "--read-eeprom", read_eeprom, "--stay")
|
|
if out.count("verify:") != 2:
|
|
fail("session 2 did not verify both memories")
|
|
|
|
eeprom_back = open(read_eeprom, "rb").read()
|
|
if eeprom_back[: len(ee_image)] != ee_image:
|
|
fail("EEPROM read-back mismatch")
|
|
|
|
# The expected post-surgery flash, straight from the tool's planner.
|
|
pages = pb.plan_flash(open(app_bin, "rb").read(), info)
|
|
flash_back = open(read_flash, "rb").read()
|
|
for address, data in pages.items():
|
|
if flash_back[address : address + page] != data:
|
|
fail(f"flash read-back mismatch in page {address:#06x}")
|
|
|
|
# An external reset re-enters through the patched word 0 (tinies; the
|
|
# runner resets them to address 0 like silicon) or BOOTRST (mega).
|
|
# The loader must answer a fresh knock, and the 'J' hand-over must
|
|
# land in the application, which banners on the same link.
|
|
device.reset()
|
|
port = pb.Port(device.pty, baud)
|
|
try:
|
|
loader = pb.Loader(port)
|
|
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":
|
|
fail(f"application banner was {banner!r}")
|
|
finally:
|
|
port.close()
|
|
finally:
|
|
device.stop()
|
|
|
|
# Ground truth: the simulator's own memories, against the host's view.
|
|
flash_true = open(dump, "rb").read()
|
|
if flash_true[:base] != flash_back:
|
|
fail("host flash read-back differs from the simulator's flash")
|
|
if flash_true[base] == 0xFF and flash_true[base + 1] == 0xFF:
|
|
fail("loader region looks erased in the ground-truth dump")
|
|
|
|
# The surgery, decoded independently: the patched vector must land on the
|
|
# 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 + 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:
|
|
fail("patched reset vector does not land on the loader base")
|
|
trampoline = flash_true[base - 2] | (flash_true[base - 1] << 8)
|
|
original = app[0] | (app[1] << 8)
|
|
if rjmp_decode(trampoline, (base - 2) // 2, flash_words) != rjmp_decode(original, 0, flash_words):
|
|
fail("trampoline does not land on the application's own entry")
|
|
ee_true_path = dump + ".eeprom"
|
|
if os.path.exists(ee_true_path):
|
|
ee_true = open(ee_true_path, "rb").read()
|
|
if ee_true[: len(ee_image)] != ee_image:
|
|
fail("ground-truth EEPROM does not match what was programmed")
|
|
|
|
print("pbtest: all scenarios pass")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|