The temporary page buffer is write-once per word, so a page filled over one an earlier writer left dirty programs the stale words. The same datasheet clause carries the cure: the buffer auto-erases after a page write (§26.2.1; §19.2 on the tinies), so the corruption clears itself by happening, and rewriting the page programs correctly. The loader therefore clears the buffer nowhere. The tinies' CTPB and the m48s' RWWSRE discard are gone; the boot-sectioned megas keep only the trailing RWWSRE they need anyway to re-enable the RWW section for read-back, which discards the buffer as a side effect and keeps them off the path entirely. 434 B on the tiny13s, 438-442 on the tiny25/45/85, 430 on the m48s; the megas are unchanged, the 1284s still 506. The host takes over the guarantee: a flash page that reads back wrong is rewritten up to RETRIES times before the run stops. Both read-back paths repair — verify_pages for programming, and write_differing, which is the loader-update path where a page left wrong is a half-written loader slot. That one is not hypothetical: deleting the discard made attiny85 pureboot.rehome fail deterministically there, the only flow still assuming the old contract. Protocol-visible, so README's W command says it: one W may program the wrong bytes after a refused page, or after an application that self-programmed entered without a reset, and a host that programs without reading back cannot trust it. Tests: pureboot.dirty drives the case the loader declines to guard — the fixture application dirties every buffer word and jumps in with no reset (hardware forbids that on a boot-sectioned mega, but simavr dispatches SPM from anywhere, which is what makes it constructible) — and asserts a bare verify sees the corruption, the repairing verify fixes it in one rewrite, and it stays fixed. pbreloc asserts the same shape after a refusal. test_planner covers the bound against a fake device: one bad write repaired in a single rewrite, a page that never comes good stopping after exactly three. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
91 lines
3.4 KiB
Python
91 lines
3.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Dirty-page-buffer acceptance test: the loader carries no buffer discard,
|
|
so a page filled over words an earlier writer left behind programs those
|
|
instead. This asserts the whole contract — the corruption is real and a bare
|
|
verify sees it, the repairing verify fixes it in one rewrite (the write that
|
|
took the stale words auto-erased the buffer), and it stays fixed.
|
|
|
|
The state is reached the way the loader cannot prevent: an application
|
|
dirties the buffer and jumps in with no reset between. Real boot-sectioned
|
|
megas forbid that outright — SPM executes only from the boot section
|
|
(Atmel-8271 §26.2) — but simavr dispatches SPM from anywhere, which is what
|
|
makes the path constructible at all.
|
|
|
|
Usage: pbdirty.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
|
|
<baud> <app_bin> <tool_py> <workdir>
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
|
|
|
|
def fail(message):
|
|
print(f"FAIL: {message}")
|
|
sys.exit(1)
|
|
|
|
|
|
def main():
|
|
device_bin, elf, mcu, hz, base_hex, page, baud, app_bin, tool, workdir = sys.argv[1:]
|
|
page, baud = int(page), int(baud)
|
|
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)
|
|
dump = os.path.join(workdir, "dump.bin")
|
|
|
|
# Reset boots the application on a BOOTRST-unprogrammed mega; its 'L' is
|
|
# the loader entry this test needs, reached without a reset.
|
|
device = pbsim.Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump, reset_hex="0")
|
|
try:
|
|
port = pb.Port(device.pty, baud)
|
|
loader = pb.Loader(port)
|
|
loader.connect(25)
|
|
|
|
# Install the application and hand over to it.
|
|
pb.op_flash(loader, app_bin, erase=False, verify=True)
|
|
loader.run_application()
|
|
if port.read_exact(3, 5.0) != b"APP":
|
|
fail("the application did not start")
|
|
|
|
port.write(b"D")
|
|
if port.read_exact(1, 5.0) != b"D":
|
|
fail("the application did not acknowledge dirtying the page buffer")
|
|
port.write(b"L")
|
|
loader = pb.Loader(port)
|
|
loader.connect(25)
|
|
|
|
# Program by hand, so the corruption is observable before anything
|
|
# repairs it.
|
|
pages = pb.plan_flash(open(app_bin, "rb").read(), loader.info)
|
|
for address in sorted(pages):
|
|
loader.write_page(address, pages[address])
|
|
try:
|
|
pb.verify_pages(loader, pages)
|
|
except pb.Error as error:
|
|
if "verify failed" not in str(error):
|
|
fail(f"the read-back failed, but not at verify: {error}")
|
|
else:
|
|
# Either the fixture no longer dirties the buffer, or the loader
|
|
# clears it again — in which case this test's premise is gone.
|
|
fail("programming over a dirty page buffer came back clean")
|
|
|
|
# What the programming path uses: one rewrite settles it, and it stays
|
|
# settled.
|
|
pb.verify_pages(loader, pages, repair=True)
|
|
pb.verify_pages(loader, pages)
|
|
|
|
# Ground truth beyond the loader's own read-back.
|
|
loader.run_application()
|
|
if port.read_exact(3, 5.0) != b"APP":
|
|
fail("the application did not start after the recovered write")
|
|
port.close()
|
|
finally:
|
|
device.stop()
|
|
print("pbdirty: a dirty page buffer is caught by verify and cleared by the retry")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|