fix(flash): merge segments sharing a flash page

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 <noreply@anthropic.com>
This commit is contained in:
michael
2026-07-05 21:59:31 -07:00
parent b0b46b3919
commit 9f926a176c
2 changed files with 52 additions and 10 deletions

View File

@@ -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

View File

@@ -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)])