From c763ca856aa8e6860b53e8c9c8ed48f8ea5ce899 Mon Sep 17 00:00:00 2001 From: BlackMark Date: Wed, 22 Jul 2026 23:43:41 +0200 Subject: [PATCH] pureboot: W addresses a page, not a word in it The in-page bits of a W address are dropped so the fill always walks from the page base; the wire contract is one page of data for any address inside it, on both the byte- and the word-addressed path. +2 B. Co-Authored-By: Claude Opus 4.8 (1M context) --- pureboot/README.md | 6 +++--- pureboot/pureboot.cpp | 16 +++++++++------- test/pbtest.py | 11 +++++++++++ 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/pureboot/README.md b/pureboot/README.md index 5924c34..a7a7c9a 100644 --- a/pureboot/README.md +++ b/pureboot/README.md @@ -127,15 +127,15 @@ addresses and all counts are bytes. |---|---|---| | `b` | — | the 12-byte info block | | `R` | addr16, n8 | n flash bytes (n = 0 means 256) | -| `W` | addr16, then one page of data | — (completion = next prompt) | +| `W` | addr16 (any address in the page), then one page of data | — (completion = next prompt) | | `r` | addr16, n8 | n EEPROM bytes (n = 0 means 256) | | `w` | addr16, n8, then n data bytes | `+` per byte, sent once its write has begun | | `F` | — | 4 bytes: low fuse, lock, extended fuse, high fuse | | `J` | word address (16-bit) | `+`, then execution continues there | | other | — | ignored; the loop re-prompts (send a junk byte, await `+`, to resync) | -`W` streams exactly one page-aligned SPM page (size from the info block) into -the buffer, then erases and programs — except pages inside the 512-byte slot +`W` streams exactly one SPM page (size from the info block) into the buffer, +then erases and programs — except pages inside the 512-byte slot the loader is *running* in, which are drained and left alone, so a broken host cannot brick the running copy and a staged copy may rewrite the resident. diff --git a/pureboot/pureboot.cpp b/pureboot/pureboot.cpp index be441bc..f365139 100644 --- a/pureboot/pureboot.cpp +++ b/pureboot/pureboot.cpp @@ -309,18 +309,20 @@ void store_eeprom(std::uint16_t address, std::uint8_t count) // that write clears the condition and the host's read-back rewrites the page. void program_flash(std::uint16_t wire_address, std::uint8_t slot_high) { - // One induction either way: a byte-addressed wire address walks the page - // itself (aligned, so the offset bits wrap to zero), while a word one - // becomes a byte cursor once. The slot index is the wire address's high - // byte — on byte-addressed chips the byte address's, with the low bit - // dropped, since a slot is two of those. + // The address names a page, so its in-page bits are dropped and the walk + // starts at the page base — one induction either way: a byte-addressed + // wire address walks the page itself (the offset bits wrap back to zero), + // while a word one becomes a byte cursor once. The slot index is the wire + // address's high byte — on byte-addressed chips the byte address's, with + // the low bit dropped, since a slot is two of those. spm::flash_address_t address; std::uint8_t page_high; if constexpr (word_flash) { // A page is aligned, so it never crosses 64 KiB: RAMPZ is a per-page // constant and the 16-bit Z's low byte is the whole in-page offset. const std::uint8_t rampz = static_cast(wire_address >> 15); - const std::uint16_t z0 = static_cast(wire_address << 1); + const std::uint16_t z0 = + static_cast(wire_address << 1) & ~static_cast(page - 1); std::uint16_t z = z0; do { std::uint8_t low = link::rx(); @@ -331,7 +333,7 @@ void program_flash(std::uint16_t wire_address, std::uint8_t slot_high) address = (static_cast(rampz) << 16) | z0; page_high = static_cast(wire_address >> 8); } else { - address = static_cast(wire_address); + address = static_cast(wire_address & ~static_cast(page - 1)); do { std::uint8_t low = link::rx(); std::uint8_t high = link::rx(); diff --git a/test/pbtest.py b/test/pbtest.py index 7a4aad5..69d1c6f 100644 --- a/test/pbtest.py +++ b/test/pbtest.py @@ -105,6 +105,17 @@ def main(): # 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}") + + # A W addressed inside a page rather than at its base must still + # consume exactly one page and prompt. The loader's own slot is + # the target — it is drained and never programmed — and the + # payload is erased-state bytes, so the probe can disturb neither + # the image nor the page buffer it leaves behind. + wire = wire_base + 1 + port.write(bytes((ord("W"), wire & 0xFF, wire >> 8)) + b"\xff" * page) + if port.read_exact(1, 5.0) != pb.PROMPT: + fail("unaligned W did not return to the prompt") + loader.run_application() banner = port.read_exact(3, 5.0) if banner != b"APP":