pbhw: an absent probe is not a destroyed loader
The slot-survives-erase check reads back over ISP by design — an independent reader is the only witness worth having about a loader that has just been asked to erase around itself. With the one probe on another board it printed "ISP read failed" as a red, which is the same word a destroyed loader would get, and it is permanently red on the two deployments with no ISP header at all. It names its witness now and falls back to the link when there is no programmer, saying that the loader is then reporting on its own slot — weaker for exactly the reason it is worth having, since a destroyed loader could not answer at all. An absent instrument is a fact about the bench and a wrong byte is a verdict on the subject; a check that prints them identically stops being read. Both paths exercised on hardware: ISP on the Uno, the link on the ATtiny13A. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -161,30 +161,63 @@ class Suite:
|
|||||||
self.check("application flash reads back", rc == 0 and len(got) == info.base,
|
self.check("application flash reads back", rc == 0 and len(got) == info.base,
|
||||||
f"{len(got)} B of {info.base}")
|
f"{len(got)} B of {info.base}")
|
||||||
|
|
||||||
|
def _witness(self, info, slot_length: int):
|
||||||
|
"""Read back the erased region and the loader slot: (erased, slot, how).
|
||||||
|
|
||||||
|
Prefers ISP, because an independent reader is the only one that can
|
||||||
|
testify about a loader just asked to erase around itself. Where no
|
||||||
|
programmer is attached the link answers instead — which is weaker for
|
||||||
|
exactly the reason it is worth having, a destroyed loader being unable
|
||||||
|
to report anything at all. The two are never printed under one word:
|
||||||
|
an absent probe is a fact about the bench, a wrong byte is a verdict on
|
||||||
|
the loader, and a check that conflates them stops being read.
|
||||||
|
"""
|
||||||
|
limit = info.base - 2 if info.patch_vector else info.base
|
||||||
|
whole = self.work / "whole.bin"
|
||||||
|
if self.rig.read_memory("flash", whole, "r"):
|
||||||
|
image = whole.read_bytes()
|
||||||
|
image += b"\xff" * (info.flash_size - len(image))
|
||||||
|
return image[0:limit], image[info.base:info.base + slot_length], "ISP"
|
||||||
|
|
||||||
|
module = pbrig.load_pureboot(self.rig.d.pureboot)
|
||||||
|
port = self.rig.open_port()
|
||||||
|
try:
|
||||||
|
loader = module.Loader(port)
|
||||||
|
if self.rig.d.autobaud:
|
||||||
|
loader.connect_autobaud(self.rig.d.wait)
|
||||||
|
else:
|
||||||
|
loader.connect(self.rig.d.wait)
|
||||||
|
return (loader.read_flash(0, limit),
|
||||||
|
loader.read_flash(info.base, slot_length),
|
||||||
|
"the link, no probe attached — the loader's own account")
|
||||||
|
except Exception as error: # noqa: BLE001 — a dead link is a result
|
||||||
|
print(f" skip slot checks: no programmer, and the link did not "
|
||||||
|
f"answer either ({str(error)[:60]})")
|
||||||
|
return None, None, ""
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
port.close()
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
pass
|
||||||
|
|
||||||
def erase_and_slot(self, info, loader_image: pathlib.Path | None) -> None:
|
def erase_and_slot(self, info, loader_image: pathlib.Path | None) -> None:
|
||||||
rc, out = self.rig.pureboot("--erase-flash")
|
rc, out = self.rig.pureboot("--erase-flash")
|
||||||
self.check("application region erases", rc == 0, self._brief(out))
|
self.check("application region erases", rc == 0, self._brief(out))
|
||||||
|
|
||||||
# The slot must be untouched by an application erase, which only an
|
want = loader_image.read_bytes() if loader_image and loader_image.exists() else b""
|
||||||
# independent read can show — so this one goes over ISP, not the link.
|
erased, slot, how = self._witness(info, len(want))
|
||||||
whole = self.work / "whole.bin"
|
if erased is None:
|
||||||
if not self.rig.read_memory("flash", whole, "r"):
|
|
||||||
self.check("loader slot survives the erase", False, "ISP read failed")
|
|
||||||
return
|
return
|
||||||
image = whole.read_bytes()
|
|
||||||
image += b"\xff" * (info.flash_size - len(image))
|
|
||||||
|
|
||||||
# Erased application flash, up to the trampoline word the host composes
|
# Erased application flash, up to the trampoline word the host composes
|
||||||
# on a patched-vector part.
|
# on a patched-vector part.
|
||||||
limit = info.base - 2 if info.patch_vector else info.base
|
limit = info.base - 2 if info.patch_vector else info.base
|
||||||
self.check("erased application region is 0xff",
|
self.check("erased application region is 0xff",
|
||||||
set(image[0:limit]) <= {0xFF}, f"0x0000..{limit:#06x}")
|
set(erased) <= {0xFF}, f"0x0000..{limit:#06x} via {how}")
|
||||||
|
|
||||||
if loader_image and loader_image.exists():
|
if want:
|
||||||
want = loader_image.read_bytes()
|
self.check("loader slot survives the erase", slot == want,
|
||||||
got = image[info.base:info.base + len(want)]
|
f"{len(want)} B at {info.base:#06x} via {how}")
|
||||||
self.check("loader slot survives the erase", got == want,
|
|
||||||
f"{len(want)} B at {info.base:#06x}")
|
|
||||||
else:
|
else:
|
||||||
print(" skip loader slot comparison (pass --loader <image.bin>)")
|
print(" skip loader slot comparison (pass --loader <image.bin>)")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user