check_walk_region() refuses an image writing into the span reset crosses to reach the loader, and a plain --flash never ran it: the fuses were read for --fuses and for --update-loader, so the tool read them to protect the loader and never to protect the reset path. It cost an ssd1306 board - an application grown through 0x7c00 on a 328P with hfuse 0xdc, reset landing mid-function, an ICE the only way back. The check now fetches its own input, so the operation that asks for no fuses cannot skip it and neither can a direct API caller: pbdirty and pbmute call op_flash() as a library and are guarded without a line changing in them. Fuses that cannot be read are a refusal naming --assume-fuses and --force, because unknown is not empty. The rig had to stop lying first. The fuse read is an LPM diverted by BLBSET, which simavr executes straight out of flash with no hook, so a fuse read answered flash bytes and --fuses had been printing them on every chip. The runner models the diversion at the SPMCSR write, -f states the profile - and stores the register itself, since a registered handler replaces simavr's store and would otherwise swallow every SPM command on the cores where nothing else watches it. pureboot.walk reproduces the brick: the unfixed tool writes 249 pages through 0x7c00 in silence, the fixed one refuses. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
76 lines
2.8 KiB
Python
76 lines
2.8 KiB
Python
"""Shared simavr harness for the pureboot tests: spawn the device runner,
|
|
hand out its pty, restart it from a flash dump (the power-fail path), and
|
|
keep its chatter out of undrained pipes."""
|
|
|
|
import os
|
|
import signal
|
|
import subprocess
|
|
|
|
|
|
class Device:
|
|
def __init__(self, binary, elf, mcu, hz, base_hex, page, baud, dump, reset_hex=None, resume=None, link=None,
|
|
window=False, fuses=None):
|
|
cmd = [binary]
|
|
if link:
|
|
cmd += ["-l", link]
|
|
if window:
|
|
cmd.append("-w") # report the first-transmit cycle, free-run idle
|
|
if fuses:
|
|
# What a fuse read answers, low,lock,extended,high. Unprogrammed
|
|
# otherwise, which is the profile every other test here runs.
|
|
cmd += ["-f", fuses]
|
|
cmd += [elf, mcu, hz, base_hex, str(page), str(baud), dump]
|
|
if reset_hex is not None or resume is not None:
|
|
# Chips without a hardware boot section - the tinies and the
|
|
# m48s - reset to address 0 like silicon; the boot-sectioned
|
|
# megas re-vector to the loader base (BOOTRST).
|
|
patch = not mcu.startswith("atmega") or mcu.startswith("atmega48")
|
|
cmd.append(reset_hex if reset_hex is not None else ("0" if patch else base_hex))
|
|
if resume is not None:
|
|
cmd.append(resume)
|
|
self.log = open(dump + ".log", "a")
|
|
self.proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=self.log, text=True)
|
|
self.dump = dump
|
|
self.pty = None
|
|
for _ in range(50):
|
|
line = self.proc.stdout.readline()
|
|
if not line:
|
|
break
|
|
if line.startswith("PB_PTY"):
|
|
self.pty = line.split()[1]
|
|
break
|
|
if not self.pty:
|
|
self.stop()
|
|
raise RuntimeError("device did not report a pty")
|
|
|
|
def reset(self):
|
|
"""The external reset line: SIGUSR1 re-enters at the reset vector."""
|
|
self.proc.send_signal(signal.SIGUSR1)
|
|
|
|
def power_fail(self):
|
|
"""SIGTERM: the runner dumps its flash and exits - the image a
|
|
restart resumes from."""
|
|
self.stop()
|
|
return self.dump
|
|
|
|
def stop(self):
|
|
self.proc.terminate()
|
|
try:
|
|
self.proc.wait(timeout=5)
|
|
except subprocess.TimeoutExpired:
|
|
self.proc.kill()
|
|
self.log.close()
|
|
|
|
|
|
def run_tool(tool, pty, baud, *args, timeout=180):
|
|
result = subprocess.run(
|
|
[os.environ.get("PYTHON", "python3"), tool, "--port", pty, "--baud", str(baud), "--wait", "25", *args],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=timeout,
|
|
)
|
|
print(result.stdout, end="")
|
|
if result.returncode != 0:
|
|
raise RuntimeError(f"tool exited {result.returncode}: {result.stderr.strip()}")
|
|
return result.stdout
|