pureboot: the classic megas and the word-addressed 1284P groundwork

Device: boot-section detection probes SPMCR beside SPMCSR, the link
picks any hardware USART through the instance-aware lookups (URSEL
chips included), WDRF reads MCUSR-or-MCUCSR, and the >64 KiB shape
lands — word-addressed wire flash (info flag bit 1, page byte 0 means
256, base as a word address), far reads through flash_load_far, a
single 32-bit byte-cursor page walk (the 256-byte page wraps its low
byte exactly), and slot arithmetic in words (the return address
already is one). Host: addresses stay bytes internally and scale at
the wire, the boot-fuse decode becomes a per-signature table (byte
index + BOOTSZ ladder — the m168A's lives in EXTENDED), and the
planner tests pin every chip's ladder plus the word-addressed info
decode. Tests: the device runner serves every mega over the USART pty,
pbapp banners over the right link, the update rehearsal synthesizes
its assumed fuses from the tool's own table, and the PI lint tracks
the renamed info symbol. All six classic-mega/168A targets pass the
full suite (size, PI, planner, protocol, reloc, self-update) at
466–504 B; the 1284P builds await a libavr far-path slimming to make
its 512.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 12:00:19 +02:00
parent a1ff032c87
commit 0fa53e1cad
12 changed files with 755 additions and 126 deletions

View File

@@ -270,10 +270,14 @@ class Info:
raise Error(f"protocol version {raw[2]}, tool speaks {PROTOCOL_VERSION}")
self.raw = bytes(raw)
self.signature = raw[3:6]
self.page = raw[6]
self.base = raw[7] | (raw[8] << 8)
self.eeprom_size = raw[9] | (raw[10] << 8)
self.page = raw[6] or 256 # the wire count convention: 0 means 256
self.patch_vector = bool(raw[11] & 1)
# Large chips speak word addresses for flash (bit 1); the host keeps
# every address in bytes and converts at the wire.
self.word_flash = bool(raw[11] & 2)
scale = 2 if self.word_flash else 1
self.base = (raw[7] | (raw[8] << 8)) * scale
self.eeprom_size = raw[9] | (raw[10] << 8)
self.flash_size = self.base + SLOT
self.stage = self.base - SLOT # where a staging copy of the loader goes
# The hand-over target, as the word address 'J' takes: the trampoline
@@ -331,25 +335,33 @@ class Loader:
self._expect_prompt(timeout)
return reply
def _stream_read(self, command, address, count):
def _stream_read(self, command, address, count, address_scale=1):
data = b""
while count:
chunk = min(count, 256)
head = bytes((ord(command), address & 0xFF, address >> 8, chunk & 0xFF))
wire = address // address_scale
head = bytes((ord(command), wire & 0xFF, wire >> 8, chunk & 0xFF))
data += self._command(head, chunk, 5.0)
address += chunk
count -= chunk
return data
def read_flash(self, address, count):
return self._stream_read("R", address, count)
if not self.info.word_flash:
return self._stream_read("R", address, count)
# Word-addressed wire: widen to even bounds, read, trim.
start = address & ~1
span = (address + count + 1 & ~1) - start
data = self._stream_read("R", start, span, address_scale=2)
return data[address - start : address - start + count]
def read_eeprom(self, address, count):
return self._stream_read("r", address, count)
def write_page(self, address, data):
assert len(data) == self.info.page and address % self.info.page == 0
head = bytes((ord("W"), address & 0xFF, address >> 8))
wire = address // (2 if self.info.word_flash else 1)
head = bytes((ord("W"), wire & 0xFF, wire >> 8))
self._command(head + data, 0, 2.0)
def write_eeprom(self, address, data):
@@ -495,14 +507,32 @@ def covered(pages, info, skip_blank):
# ----------------------------------------------------------------- fuses ---
def mega_boot(high_fuse):
"""Decode the ATmega328P high fuse's boot configuration (DS40002061B
§27.3, Table 27-13/27-16): BOOTSZ1:0 in bits 2:1 select the boot-section
words, BOOTRST in bit 0 (programmed = 0) re-vectors reset to its start.
Returns (bootrst_programmed, boot_section_start_byte)."""
bootsz = (high_fuse >> 1) & 0x03
words = {0b11: 256, 0b10: 512, 0b01: 1024, 0b00: 2048}[bootsz]
return (high_fuse & 1) == 0, 0x8000 - words * 2
# Per-chip boot fuse geometry, keyed by the signature's family/part bytes:
# which byte of the 'F' reply (low, lock, extended, high) carries BOOTSZ/
# BOOTRST, and the BOOTSZ->words ladder. Sources: Atmel-2486/2466/2503
# (HIGH fuse), Atmel-8271 (m168A: EXTENDED; m328P: HIGH), Atmel-42719.
BOOT_FUSE = {
bytes((0x93, 0x07)): (3, {0b11: 128, 0b10: 256, 0b01: 512, 0b00: 1024}), # m8/8A
bytes((0x94, 0x03)): (3, {0b11: 128, 0b10: 256, 0b01: 512, 0b00: 1024}), # m16
bytes((0x95, 0x02)): (3, {0b11: 256, 0b10: 512, 0b01: 1024, 0b00: 2048}), # m32/32A
bytes((0x94, 0x06)): (2, {0b11: 128, 0b10: 256, 0b01: 512, 0b00: 1024}), # m168A
bytes((0x95, 0x0F)): (3, {0b11: 256, 0b10: 512, 0b01: 1024, 0b00: 2048}), # m328P
bytes((0x97, 0x05)): (3, {0b11: 512, 0b10: 1024, 0b01: 2048, 0b00: 4096}), # 1284P
}
def mega_boot(info, fuse_bytes):
"""Decode a mega's boot configuration from its fuses (the byte and the
BOOTSZ ladder are per chip): BOOTSZ1:0 in bits 2:1 select the
boot-section words, BOOTRST in bit 0 (programmed = 0) re-vectors reset
to its start. Returns (bootrst_programmed, boot_section_start_byte)."""
entry = BOOT_FUSE.get(bytes(info.signature[1:3]))
if entry is None:
raise Error(f"unknown mega signature {info.signature.hex()} — no boot fuse map")
which, ladder = entry
fuse = fuse_bytes[which]
words = ladder[(fuse >> 1) & 0x03]
return (fuse & 1) == 0, info.flash_size - words * 2
# ---------------------------------------------------------- loader update ---
@@ -557,12 +587,11 @@ def update_preflight(image, info, fuse_bytes):
if not info.patch_vector:
if fuse_bytes is None:
raise Error("a loader update on this chip needs its fuses — unreadable? pass --assume-fuses")
high = fuse_bytes[3]
bootrst, bls_start = mega_boot(high)
bootrst, bls_start = mega_boot(info, fuse_bytes)
if info.stage < bls_start:
raise Error(
f"cannot self-update: the staging slot {info.stage:#06x} lies below the "
f"boot section ({bls_start:#06x}, high fuse {high:#04x}) where SPM is disabled "
f"boot section ({bls_start:#06x}) where SPM is disabled "
f"— a boot section of at least 1 KB (BOOTSZ) is required, and only an "
f"external programmer can change fuses"
)
@@ -710,7 +739,7 @@ def check_walk_region(pages, info, fuse_bytes, force):
Only checkable when the fuses are known (--fuses or --assume-fuses)."""
if info.patch_vector or fuse_bytes is None:
return
bootrst, bls_start = mega_boot(fuse_bytes[3])
bootrst, bls_start = mega_boot(info, fuse_bytes)
if not bootrst or bls_start >= info.base:
return
overlap = [a for a in sorted(pages) if a >= bls_start and pages[a].count(0xFF) != len(pages[a])]