Files
bootloader/test/pbreloc.py
BlackMark 65f8fdd537 pureboot: the 1284P rides a 1 KiB slot — its own boot-sector minimum
The far machinery (ELPM reads, RAMPZ page commands, wire-word math)
costs ~46 B over the m328P's 504, and the tsb-calibrated C++-to-asm
gap says no implementation of this feature set reaches 512 on this
chip — a boundary its hardware does not have anyway: the 1284P's
smallest boot sector is 1 KiB. The slot therefore becomes
per-geometry (512 B, or 1 KiB past 64 KiB), which the host derives
from the word-addressing flag; slot arithmetic unifies (the index is
the wire high byte with its low bit dropped in either unit), the
update preflight demands a two-slot boot section in the chip's own
terms, and pbapp's hand-back jumps to the real slot base. libavr's
far primitives split their RAMPZ/Z asm operands (a page never
crosses 64 KiB, so callers keep a byte and a 16-bit cursor — the
32-bit address folds away; flash_load_far's byte form becomes the
out-RAMPZ+elpm pair avr-libc's pgm_read_byte_far rebuilds per call),
and the host splits reads at 64 KiB boundaries. All ten chips pass
the full suite — the 1284P at 558 B including protocol, relocation,
and the power-fail self-update — with pureboot byte-identical across
generated and reflect modes everywhere, and the original three
chips' images unchanged to the byte (488/502/504).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 13:54:40 +02:00

95 lines
3.8 KiB
Python

#!/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.
Usage: pbreloc.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
<baud> <tool_py> <workdir>
"""
import os
import subprocess
import sys
def fail(message):
print(f"FAIL: {message}")
sys.exit(1)
def main():
device_bin, elf, mcu, hz, base_hex, page, baud, tool, workdir = sys.argv[1:]
base, page, baud = int(base_hex, 0), int(page), int(baud)
stage = None # derived from the device's own info (slot-sized) below
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)
objcopy = os.environ.get("PB_OBJCOPY", "avr-objcopy")
image_path = os.path.join(workdir, "pureboot.bin")
subprocess.run([objcopy, "-O", "binary", elf, image_path], check=True)
image = open(image_path, "rb").read()
device = pbsim.Device(device_bin, elf, mcu, hz, base_hex, page, baud, os.path.join(workdir, "dump.bin"))
try:
port = pb.Port(device.pty, baud)
loader = pb.Loader(port)
info = loader.connect(25)
if info.base != base:
fail(f"info reports base {info.base:#06x}")
resident_info = info.raw
# Install the staging copy exactly as the update flow would.
stage = info.stage
staged = pb.staging_content(image, info)
pb.write_differing(loader, stage, staged)
# Enter it; from here on, every command runs in the relocated copy.
staged_info = loader.enter_copy(stage, 25)
if staged_info.raw != resident_info:
fail(f"staged info {staged_info.raw.hex()} != resident info {resident_info.hex()}")
# 'R' from the staged copy already proved itself in the install
# verify; 'F' must answer 4 bytes (values are unmodeled in simavr).
if len(loader.read_fuses()) != 4:
fail("fuse read from the staged copy")
# EEPROM round-trip through the staged copy.
pattern = bytes(range(0x50, 0x60))
loader.write_eeprom(0, pattern)
if loader.read_eeprom(0, len(pattern)) != pattern:
fail("EEPROM round-trip through the staged copy")
# The guard, both ways: its own slot refused (drained, unchanged),
# the resident slot writable.
before = loader.read_flash(stage, page)
loader.write_page(stage, bytes(page))
if loader.read_flash(stage, page) != before:
fail("the staged copy's guard let its own slot change")
marker = bytes((i * 3) & 0xFF for i in range(page))
loader.write_page(base, marker)
if loader.read_flash(base, page) != marker:
fail("the staged copy could not write the resident slot")
# 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))
pb.write_differing(loader, base, resident)
back_info = loader.enter_copy(base, 25)
if back_info.raw != resident_info:
fail("the restored resident does not serve its info block")
port.close()
finally:
device.stop()
print("pbreloc: the relocated copy serves the full command set")
if __name__ == "__main__":
main()