From 9f926a176c182ca17a16b1d68db53929df5e4b7d Mon Sep 17 00:00:00 2001 From: michael Date: Sun, 5 Jul 2026 21:59:31 -0700 Subject: [PATCH] fix(flash): merge segments sharing a flash page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found on the first real-hardware flash (PM3 Easy). The fullimage ELF has two adjacent PT_LOAD segments (seg1 ends at 0x15E5AC, seg2 starts there) that share the 0x15E400 flash page. build_blocks aligned each segment independently, so the shared page was written twice — seg2's 0xFF-padded head, written last, erase-programmed the page and wiped seg1's real bytes there (the SAM7 programs a whole page atomically). That region holds version_information, so the flashed OS booted but reported 'Missing/Invalid version information'. Fix: merge segments whose block-aligned spans touch into one buffer, so every shared page is written once with both segments' real data; 0xFF only fills gaps and aligned outer edges. Re-flash verified on hardware: 3df2ed2e8 -> 303bd5873, valid version, matches_pin true. Bootrom untouched throughout (fullimage-only). Co-Authored-By: Claude Opus 4.8 --- pm3py/core/flash.py | 41 +++++++++++++++++++++++++++++++---------- tests/test_flash.py | 21 +++++++++++++++++++++ 2 files changed, 52 insertions(+), 10 deletions(-) diff --git a/pm3py/core/flash.py b/pm3py/core/flash.py index 3f284a6..5f8c384 100644 --- a/pm3py/core/flash.py +++ b/pm3py/core/flash.py @@ -98,19 +98,40 @@ def build_blocks(segments: list[tuple[int, bytes]], block_size: int = FLASH_BLOCK_SIZE) -> list[tuple[int, bytes]]: """Turn segments into a list of (address, block) writes. - Each segment is padded out to ``block_size`` boundaries with 0xFF (erased - flash), then split into block-sized chunks. Addresses are block-aligned so - each chunk maps onto whole flash pages. + Segments that fall in the same or overlapping flash blocks are merged into a + single buffer first, so a page shared by two adjacent segments is written + once with both segments' real data. Writing such a page twice would be a bug: + the SAM7 erase-programs each page atomically, so a second write of the shared + page (with 0xFF padding where the other segment's data lives) would wipe the + first segment's bytes. Gaps within a merged run and the aligned outer edges + are filled with 0xFF (erased flash). """ + if not segments: + return [] + segs = sorted(segments) + + # Group consecutive segments whose block-aligned spans touch or overlap. + groups: list[list[tuple[int, bytes]]] = [[segs[0]]] + for paddr, data in segs[1:]: + g = groups[-1] + g_end = max(a + len(d) for a, d in g) + if paddr < _align_up(g_end, block_size): + g.append((paddr, data)) + else: + groups.append([(paddr, data)]) + blocks: list[tuple[int, bytes]] = [] - for paddr, data in segments: - start = _align_down(paddr, block_size) - pre = paddr - start - end = _align_up(paddr + len(data), block_size) - post = end - (paddr + len(data)) - blob = b"\xFF" * pre + data + b"\xFF" * post + for g in groups: + g_start = g[0][0] + g_end = max(a + len(d) for a, d in g) + astart = _align_down(g_start, block_size) + aend = _align_up(g_end, block_size) + blob = bytearray(b"\xFF" * (aend - astart)) + for paddr, data in g: + off = paddr - astart + blob[off:off + len(data)] = data for i in range(0, len(blob), block_size): - blocks.append((start + i, blob[i:i + block_size])) + blocks.append((astart + i, bytes(blob[i:i + block_size]))) return blocks diff --git a/tests/test_flash.py b/tests/test_flash.py index cf1b9e5..57a4605 100644 --- a/tests/test_flash.py +++ b/tests/test_flash.py @@ -105,6 +105,27 @@ def test_build_blocks_aligns_and_pads(): assert blocks[1][1][-100:] == b"\xFF" * 100 +def test_build_blocks_merges_shared_page(): + # Two adjacent segments sharing the flash page at 0x102200: seg1 ends at + # 0x1022AC, seg2 starts there. The shared page must keep seg1's real tail — + # writing it twice (seg2's 0xFF head last) would wipe it (the on-hardware bug). + seg1 = (0x102000, b"\x11" * 0x2AC) # ends 0x1022AC (unaligned) + seg2 = (0x1022AC, b"\x22" * 0x100) # ends 0x1023AC + blocks = dict(build_blocks([seg1, seg2])) + shared = blocks[0x102200] # 0x102200..0x102400 + assert shared[0x00:0xAC] == b"\x11" * 0xAC # seg1 tail preserved, NOT 0xFF + assert shared[0xAC:0x100] == b"\x22" * 0x54 # seg2 head + # no duplicate block addresses (would mean a double-write of a page) + addrs = [a for a, _ in build_blocks([seg1, seg2])] + assert len(addrs) == len(set(addrs)) + + +def test_build_blocks_separate_when_far_apart(): + # A real gap between segments -> separate groups, no giant fill between them. + blocks = build_blocks([(0x102000, b"\x11" * 16), (0x120000, b"\x22" * 16)]) + assert {a for a, _ in blocks} == {0x102000, 0x120000} + + def test_build_blocks_pads_low_side(): # unaligned start -> leading 0xFF padding down to the block boundary blocks = build_blocks([(0x102010, b"\x22" * 16)])