pureboot.py: --scan walks a silent loader's rate; --info decodes the clock

The RC-oscillator answer's host half. --scan probes ±10 % around the built
rate in 2 % steps, nearest first, one activation window (one reset) per
probe: a fixed-baud loader whose oscillator drifted answers at the ratio,
and the report gives the session workaround (--baud), the offset, the
OSCCAL direction at ~1 %/step, and the autobaud way out. The walk and the
advice are logic-tested (test_scan.py, red-proven on the trim direction) —
a pty carries bytes at any rate, so the wire cannot arbitrate them.

On an autobaud session --info now reads the measured bit period from
ram_start — the geometry table gains that column — and undoes the unit's
encoding ((cycles − 8) / 4, floored: libavr's spin granule and per-bit
overhead), so the printed clock is the true one within a granule; --clock
turns it into a stated drift. The autobaud end-to-end asserts the figure
inside exactly that envelope at both clock points.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 01:01:35 +02:00
parent 69f089e53a
commit 52c4cdab32
4 changed files with 188 additions and 28 deletions

View File

@@ -16,6 +16,7 @@ baked in.
"""
import os
import re
import sys
import time
@@ -58,11 +59,23 @@ def main():
try:
# The host tool, in autobaud mode, sends the 0xC0 calibration pulse
# and a single knock at `baud`; the loader locks to it.
out = pbsim.run_tool(tool, device.pty, baud, "--autobaud", "--info", "--fuses",
"--flash", app_bin, "--eeprom", ee_path, "--stay")
out = pbsim.run_tool(tool, device.pty, baud, "--autobaud", "--info", "--clock", str(hz),
"--fuses", "--flash", app_bin, "--eeprom", ee_path, "--stay")
for needed in ("version", "signature", "fuses", "verify:", "stays"):
if needed not in out:
fail(f"{label}: session output lacks {needed!r}\n{out}")
# The measured clock, decoded from the unit at ram_start. The
# runner's clock is exact, so the figure must land inside the
# encoding's own envelope: the loader floors the bit period to
# 4-cycle spin granules after an 8-cycle discount, and the edge
# poll can shave a few cycles more — one granule of slack below
# the true clock, none above (in cycles per bit, times the rate).
measured = re.search(r"measured\s+(\d+) Hz", out)
if not measured:
fail(f"{label}: --info lacks the measured clock\n{out}")
measured = int(measured.group(1))
if not hz - 19 * baud <= measured <= hz + 4 * baud:
fail(f"{label}: measured clock {measured} Hz is {measured - hz:+d} off the true {hz}")
# Read both memories back over the locked link and check them.
read_flash = os.path.join(workdir, f"rf_{label}.bin")
read_eeprom = os.path.join(workdir, f"re_{label}.bin")

53
test/test_scan.py Normal file
View File

@@ -0,0 +1,53 @@
#!/usr/bin/env python3
"""--scan's walk and report logic, no simulator: the probe order, the rate
arithmetic, and the advice's direction. The rate physics itself is not
sim-testable — a pty carries bytes at any termios rate — so what the wire
would arbitrate is pinned here as logic instead.
Usage: test_scan.py <tool_py>
"""
import os
import sys
def fail(message):
print(f"FAIL: {message}")
sys.exit(1)
def main():
sys.path.insert(0, os.path.dirname(os.path.abspath(sys.argv[1])))
import pureboot as pb
walk = pb.scan_ratios()
if walk != [0, -2, 2, -4, 4, -6, 6, -8, 8, -10, 10]:
fail(f"probe walk is not built-rate-first, nearest-out: {walk}")
if pb.scan_rate(9600, 4) != 9984 or pb.scan_rate(9600, -4) != 9216:
fail("probe rate arithmetic")
if pb.scan_rate(115200, 0) != 115200:
fail("the built rate must probe unchanged")
# A loader answering fast means a fast oscillator: the trim goes down.
report = "\n".join(pb.scan_report(9600, 4, 6))
for needle in ("9984", "+4 %", "--baud 9984", "4 steps lower", "pureboot 6"):
if needle not in report:
fail(f"+4 % report lacks {needle!r}:\n{report}")
report = "\n".join(pb.scan_report(9600, -6, 6))
if "6 steps higher" not in report:
fail(f"-6 % report advises the wrong direction:\n{report}")
report = "\n".join(pb.scan_report(9600, 0, 6))
if "none" not in report or "steps" in report:
fail(f"an on-rate answer must advise no trim:\n{report}")
report = "\n".join(pb.scan_report(9600, 4, 6, clock=9600000))
if "9984000" not in report:
fail(f"the absolute clock must scale with the found ratio:\n{report}")
print("OK")
if __name__ == "__main__":
main()