#!/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. Usage: pbtest.py [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. Exits 0 if every scenario passes. """ 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 + 512 > 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 + 512) // 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()