pureboot: the 1284P rides a 1 KiB slot — its own boot-sector minimum

The far machinery (ELPM reads, RAMPZ page commands, wire-word math)
costs ~46 B over the m328P's 504, and the tsb-calibrated C++-to-asm
gap says no implementation of this feature set reaches 512 on this
chip — a boundary its hardware does not have anyway: the 1284P's
smallest boot sector is 1 KiB. The slot therefore becomes
per-geometry (512 B, or 1 KiB past 64 KiB), which the host derives
from the word-addressing flag; slot arithmetic unifies (the index is
the wire high byte with its low bit dropped in either unit), the
update preflight demands a two-slot boot section in the chip's own
terms, and pbapp's hand-back jumps to the real slot base. libavr's
far primitives split their RAMPZ/Z asm operands (a page never
crosses 64 KiB, so callers keep a byte and a 16-bit cursor — the
32-bit address folds away; flash_load_far's byte form becomes the
out-RAMPZ+elpm pair avr-libc's pgm_read_byte_far rebuilds per call),
and the host splits reads at 64 KiB boundaries. All ten chips pass
the full suite — the 1284P at 558 B including protocol, relocation,
and the power-fail self-update — with pureboot byte-identical across
generated and reflect modes everywhere, and the original three
chips' images unchanged to the byte (488/502/504).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 13:54:40 +02:00
parent 0fa53e1cad
commit 65f8fdd537
8 changed files with 122 additions and 72 deletions

View File

@@ -36,7 +36,7 @@ else:
PROMPT = b"+"
PROTOCOL_VERSION = 1
SLOT = 512 # the loader slot size; also the self-update staging distance
SLOT = 512 # the loader slot on byte-addressed chips; word-addressed ones (>64 KiB) use 1 KiB — their own smallest boot sector
class Error(Exception):
@@ -278,8 +278,9 @@ class Info:
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
self.slot = 1024 if self.word_flash else SLOT
self.flash_size = self.base + self.slot
self.stage = self.base - self.slot # where a staging copy of the loader goes
# The hand-over target, as the word address 'J' takes: the trampoline
# below the loader (tinies), or word 0 (mega — the application's own
# reset vector; BOOTRST re-vectors a reset into the loader instead).
@@ -349,10 +350,18 @@ class Loader:
def read_flash(self, address, count):
if not self.info.word_flash:
return self._stream_read("R", address, count)
# Word-addressed wire: widen to even bounds, read, trim.
# Word-addressed wire: widen to even bounds and never let one read
# cross a 64 KiB boundary (the device holds RAMPZ for a whole run).
start = address & ~1
span = (address + count + 1 & ~1) - start
data = self._stream_read("R", start, span, address_scale=2)
data = b""
at = start
remaining = span
while remaining:
chunk = min(remaining, 0x10000 - (at & 0xFFFF))
data += self._stream_read("R", at, chunk, address_scale=2)
at += chunk
remaining -= chunk
return data[address - start : address - start + count]
def read_eeprom(self, address, count):
@@ -564,12 +573,13 @@ def staging_content(image, info):
that word, which for a staging copy is the slot's own last word: an rjmp
to the resident base. The staging copy's fall-through and 'J'-free exit
both land in a loader instead of garbage."""
if len(image) > (SLOT - 2 if info.patch_vector else SLOT):
raise Error(f"loader image is {len(image)} B, the slot holds {SLOT - 2 if info.patch_vector else SLOT}")
content = bytearray(image) + bytearray([0xFF] * (SLOT - len(image)))
slot = info.slot
if len(image) > (slot - 2 if info.patch_vector else slot):
raise Error(f"loader image is {len(image)} B, the slot holds {slot - 2 if info.patch_vector else slot}")
content = bytearray(image) + bytearray([0xFF] * (slot - len(image)))
if info.patch_vector:
through = rjmp_to((info.base - 2) // 2, info.base // 2, info.flash_size // 2)
content[SLOT - 2], content[SLOT - 1] = through & 0xFF, through >> 8
content[slot - 2], content[slot - 1] = through & 0xFF, through >> 8
return bytes(content)
@@ -592,8 +602,8 @@ def update_preflight(image, info, fuse_bytes):
raise Error(
f"cannot self-update: the staging slot {info.stage:#06x} lies below the "
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"
f"— a boot section of at least two slots ({2 * info.slot} B, BOOTSZ) is "
f"required, and only an external programmer can change fuses"
)
if not bootrst:
warnings.append(
@@ -634,7 +644,7 @@ class UpdateState:
self.data = {
"signature": info.signature.hex(),
"base": info.base,
"staging": loader.read_flash(info.stage, SLOT).hex(),
"staging": loader.read_flash(info.stage, info.slot).hex(),
"page0": loader.read_flash(0, info.page).hex() if info.patch_vector else "",
}
with open(self.path, "w") as f:
@@ -690,7 +700,7 @@ def op_update_loader(loader, wait, path, state_path, fuse_bytes):
for warning in update_preflight(image, info, fuse_bytes):
print(f"note: {warning}")
staged = staging_content(image, info)
resident = bytes(image) + bytes([0xFF] * (SLOT - len(image)))
resident = bytes(image) + bytes([0xFF] * (info.slot - len(image)))
page = info.page
state = UpdateState(state_path)
@@ -700,7 +710,7 @@ def op_update_loader(loader, wait, path, state_path, fuse_bytes):
# address 0 (the 1 KB tiny13A), its first page carries the reset vector:
# written last, so any earlier interruption still resets into the old
# resident, and from then on resets enter the staging copy.
order = list(range(0, SLOT, page))
order = list(range(0, info.slot, page))
if info.stage == 0:
order = order[1:] + [0]
if write_differing(loader, info.stage, staged, order):
@@ -723,7 +733,7 @@ def op_update_loader(loader, wait, path, state_path, fuse_bytes):
loader.enter_copy(info.base, wait)
if redirect:
write_differing(loader, 0, state.page0)
order = list(range(0, SLOT, page))
order = list(range(0, info.slot, page))
if info.stage == 0:
order = [0] + order[1:]
write_differing(loader, info.stage, state.staging, order)