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:
@@ -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])]
|
||||
|
||||
Reference in New Issue
Block a user