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:
@@ -98,19 +98,40 @@ def build_blocks(segments: list[tuple[int, bytes]],
|
|||||||
block_size: int = FLASH_BLOCK_SIZE) -> list[tuple[int, bytes]]:
|
block_size: int = FLASH_BLOCK_SIZE) -> list[tuple[int, bytes]]:
|
||||||
"""Turn segments into a list of (address, block) writes.
|
"""Turn segments into a list of (address, block) writes.
|
||||||
|
|
||||||
Each segment is padded out to ``block_size`` boundaries with 0xFF (erased
|
Segments that fall in the same or overlapping flash blocks are merged into a
|
||||||
flash), then split into block-sized chunks. Addresses are block-aligned so
|
single buffer first, so a page shared by two adjacent segments is written
|
||||||
each chunk maps onto whole flash pages.
|
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]] = []
|
blocks: list[tuple[int, bytes]] = []
|
||||||
for paddr, data in segments:
|
for g in groups:
|
||||||
start = _align_down(paddr, block_size)
|
g_start = g[0][0]
|
||||||
pre = paddr - start
|
g_end = max(a + len(d) for a, d in g)
|
||||||
end = _align_up(paddr + len(data), block_size)
|
astart = _align_down(g_start, block_size)
|
||||||
post = end - (paddr + len(data))
|
aend = _align_up(g_end, block_size)
|
||||||
blob = b"\xFF" * pre + data + b"\xFF" * post
|
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):
|
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
|
return blocks
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -105,6 +105,27 @@ def test_build_blocks_aligns_and_pads():
|
|||||||
assert blocks[1][1][-100:] == b"\xFF" * 100
|
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():
|
def test_build_blocks_pads_low_side():
|
||||||
# unaligned start -> leading 0xFF padding down to the block boundary
|
# unaligned start -> leading 0xFF padding down to the block boundary
|
||||||
blocks = build_blocks([(0x102010, b"\x22" * 16)])
|
blocks = build_blocks([(0x102010, b"\x22" * 16)])
|
||||||
|
|||||||
Reference in New Issue
Block a user