pureboot: the unified autobaud loader, and the hang that settled the decision
Hardware testing found that a lone calibration pulse wedged the autobaud loader: run() budgeted only the start-edge wait in measure(), and the rx() that read the knock behind it was unbudgeted, so one stray low pulse held an unattended device in the loader and the application never ran. Bound the whole activation — an expired knock budget returns a byte that cannot be the knock, so control falls back into the budgeted measure() and an idle line boots the app there. That fix costs ~22 B, which neither version under review could absorb: the pure one goes 508 -> 530 on the 1284P and the register one 512 -> 534, both over a 512 B slot. Their margin was never spare capacity, it was the space the missing fix should have occupied. So the choice between them is moot; both are kept for the record and no longer built. pureboot_autobaud_uni.cpp replaces them at 464 B. It is pureboot 5: one read command and one write command over named spaces (G/g, sel8, addr16, n8) instead of four per-memory bodies, which collapses four transfer loops into one. The selector's high nibble carries flash's bank, so the shared cursor stays 16 bits and no command speaks word addresses. Three things fall out of the freed space: RAM read/write — the missing feature, and with it arbitrary I/O access, since AVR maps peripherals into the data space; host-issued SPM, so W's hardcoded erase/write/RWW tail becomes three writes to a space and any SPM operation is reachable; and W on the same selector-and-address decode as everything else. Strictly pure throughout: no inline asm, no global register variable, and no GPIOR either — the unit lives in a .noinit static, so the loader claims no chip resource and the chips without GPIOR stop being a special case. pureboot.py speaks both generations, keyed on the version, so the fixed-baud path is untouched; --peek/--poke reach the new data space. pbautobaud.py adds a RAM round-trip and a regression for the hang: a lone pulse must still let the app boot. All 37 chips plus the 12-preset reflect spot set build and size-test green, 444-466 B, worst case 46 B under budget. Sim suites 100%: 1284P 17/17, 328P 23/23. Only real-hardware acceptance remains (pureboot/autobaud.md). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -30,10 +30,29 @@ VERSION = 3 # this tool's own version — free to drift from a loader's
|
||||
# map lives: every version so far speaks the same protocol, and one that
|
||||
# changes it becomes the new floor here.
|
||||
OLDEST_LOADER = 1
|
||||
NEWEST_LOADER = 4
|
||||
NEWEST_LOADER = 5
|
||||
SLOT = 512 # the loader slot, on every chip
|
||||
RETRIES = 3 # rewrites of a page that reads back wrong, before the run stops
|
||||
|
||||
# pureboot 5 replaced the four per-memory commands with one unified pair: 'G'
|
||||
# reads and 'g' writes, both taking a selector byte, a 16-bit address and a
|
||||
# count, over the spaces below. The loader carries one transfer loop instead of
|
||||
# four bodies, which is what paid for RAM access and for the calibration fix.
|
||||
UNIFIED_LOADER = 5
|
||||
SP_FLASH, SP_EEPROM, SP_RAM, SP_FUSE, SP_SPM = 0, 1, 2, 3, 4
|
||||
|
||||
# A selector's high nibble is flash's third address byte, so a transfer names a
|
||||
# byte address within a 64 KiB bank and never has to speak word addresses. No
|
||||
# single transfer may cross a bank boundary — the host chunks to keep that true.
|
||||
def selector(space, address):
|
||||
return space | ((address >> 16) << 4)
|
||||
|
||||
|
||||
# The SPM operations pureboot 5 leaves to the host: a write to SP_SPM hands its
|
||||
# data byte to SPMCSR and fires the instruction at the selected flash address.
|
||||
# Every part pureboot targets agrees on these encodings.
|
||||
SPM_ERASE, SPM_WRITE, SPM_RWWSRE = 0x03, 0x05, 0x11
|
||||
|
||||
# Calibration byte for the autobaud loader: 0xC0 is a start bit plus six zero
|
||||
# data bits — one low pulse of seven bit-times, which the loader times into its
|
||||
# per-bit unit. Sent at whatever baud the host chose; the loader locks to it.
|
||||
@@ -509,7 +528,58 @@ class Loader:
|
||||
count -= chunk
|
||||
return data
|
||||
|
||||
@property
|
||||
def unified(self):
|
||||
"""pureboot 5 and later: one 'G'/'g' pair over selector-named spaces."""
|
||||
return self.info is not None and self.info.version >= UNIFIED_LOADER
|
||||
|
||||
def _read_space(self, space, address, count):
|
||||
"""A run out of any space, chunked to 256 bytes and to bank bounds."""
|
||||
data = b""
|
||||
while count:
|
||||
chunk = min(count, 256, 0x10000 - (address & 0xFFFF))
|
||||
head = bytes((ord("G"), selector(space, address), address & 0xFF,
|
||||
(address >> 8) & 0xFF, chunk & 0xFF))
|
||||
data += self._command(head, chunk, 5.0)
|
||||
address += chunk
|
||||
count -= chunk
|
||||
return data
|
||||
|
||||
def _write_space(self, space, address, data, progress=None):
|
||||
"""A run into any space. Each byte is acked as its write begins — an
|
||||
EEPROM cell and an SPM operation both need that pacing, and the ack is
|
||||
what the loader sends in place of a completion status."""
|
||||
offset = 0
|
||||
while offset < len(data):
|
||||
chunk = data[offset : offset + min(256, 0x10000 - (address & 0xFFFF))]
|
||||
head = bytes((ord("g"), selector(space, address), address & 0xFF,
|
||||
(address >> 8) & 0xFF, len(chunk) & 0xFF))
|
||||
self.port.write(head)
|
||||
for byte in chunk:
|
||||
self.port.write(bytes((byte,)))
|
||||
self._expect_prompt()
|
||||
if progress:
|
||||
progress.step()
|
||||
self._expect_prompt() # the next command prompt
|
||||
address += len(chunk)
|
||||
offset += len(chunk)
|
||||
|
||||
def spm(self, operation, address):
|
||||
"""One SPM operation at a flash address — the erase, write and RWW
|
||||
re-enable that pureboot 4 ran inside 'W' and pureboot 5 leaves here."""
|
||||
self._write_space(SP_SPM, address, bytes((operation,)))
|
||||
|
||||
def read_ram(self, address, count):
|
||||
"""Data space: SRAM, and with it the register file and every I/O
|
||||
register, which share the address space on AVR. New in pureboot 5."""
|
||||
return self._read_space(SP_RAM, address, count)
|
||||
|
||||
def write_ram(self, address, data):
|
||||
self._write_space(SP_RAM, address, data)
|
||||
|
||||
def read_flash(self, address, count):
|
||||
if self.unified:
|
||||
return self._read_space(SP_FLASH, address, count)
|
||||
if not self.info.word_flash:
|
||||
return self._stream_read("R", address, count)
|
||||
# Word-addressed wire: widen to even bounds and never let one read
|
||||
@@ -527,15 +597,32 @@ class Loader:
|
||||
return data[address - start : address - start + count]
|
||||
|
||||
def read_eeprom(self, address, count):
|
||||
if self.unified:
|
||||
return self._read_space(SP_EEPROM, 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
|
||||
if self.unified:
|
||||
# 'W' fills the page buffer and stops there; the erase and the write
|
||||
# are host-issued SPM operations. Only a chip with a boot section
|
||||
# has RWW to re-enable — on the others bit 4 of SPMCSR means
|
||||
# something else entirely, so it must not be sent.
|
||||
head = bytes((ord("W"), selector(SP_FLASH, address), address & 0xFF, (address >> 8) & 0xFF))
|
||||
self._command(head + data, 0, 2.0)
|
||||
self.spm(SPM_ERASE, address)
|
||||
self.spm(SPM_WRITE, address)
|
||||
if not self.info.patch_vector:
|
||||
self.spm(SPM_RWWSRE, address)
|
||||
return
|
||||
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, progress=None):
|
||||
if self.unified:
|
||||
self._write_space(SP_EEPROM, address, data, progress)
|
||||
return
|
||||
offset = 0
|
||||
while offset < len(data):
|
||||
chunk = data[offset : offset + 256]
|
||||
@@ -551,6 +638,8 @@ class Loader:
|
||||
offset += len(chunk)
|
||||
|
||||
def read_fuses(self):
|
||||
if self.unified:
|
||||
return self._read_space(SP_FUSE, 0, 4)
|
||||
return self._command(b"F", 4, 2.0)
|
||||
|
||||
def jump(self, word_address):
|
||||
@@ -1107,6 +1196,37 @@ def op_read_eeprom(loader, path):
|
||||
print(f"read EEPROM: {len(data)} B -> {path}")
|
||||
|
||||
|
||||
def _require_unified(loader, what):
|
||||
if not loader.unified:
|
||||
raise Error(f"{what} needs pureboot {UNIFIED_LOADER} or later; this loader is {loader.info.version}")
|
||||
|
||||
|
||||
def _peek_spec(spec):
|
||||
"""ADDR[:N] — addresses and counts in any Python integer base."""
|
||||
address, _, count = spec.partition(":")
|
||||
return int(address, 0), int(count, 0) if count else 1
|
||||
|
||||
|
||||
def op_peek(loader, spec):
|
||||
_require_unified(loader, "--peek")
|
||||
address, count = _peek_spec(spec)
|
||||
data = loader.read_ram(address, count)
|
||||
for offset in range(0, len(data), 16):
|
||||
row = data[offset : offset + 16]
|
||||
text = "".join(chr(b) if 0x20 <= b < 0x7F else "." for b in row)
|
||||
print(f"{address + offset:#06x} {row.hex(' '):<47} {text}")
|
||||
|
||||
|
||||
def op_poke(loader, spec):
|
||||
_require_unified(loader, "--poke")
|
||||
address, _, payload = spec.partition(":")
|
||||
if not payload:
|
||||
raise Error("--poke needs ADDR:HEX, for example 0x200:deadbeef")
|
||||
data = bytes.fromhex(payload.replace(" ", ""))
|
||||
loader.write_ram(int(address, 0), data)
|
||||
print(f"poke: {len(data)} B at {int(address, 0):#06x}")
|
||||
|
||||
|
||||
def op_fuses(loader):
|
||||
low, lock, extended, high = loader.read_fuses()
|
||||
print("fuses:")
|
||||
@@ -1158,6 +1278,10 @@ def main():
|
||||
parser.add_argument("--eeprom", metavar="FILE", help="program the EEPROM (bin or ihex)")
|
||||
parser.add_argument("--read-eeprom", metavar="FILE", help="dump the EEPROM")
|
||||
parser.add_argument("--verify-eeprom", metavar="FILE", help="compare EEPROM against an image")
|
||||
parser.add_argument("--peek", metavar="ADDR[:N]", help="read N bytes of data space (SRAM, registers, "
|
||||
"I/O) — pureboot 5 and later")
|
||||
parser.add_argument("--poke", metavar="ADDR:HEX", help="write hex bytes into data space — "
|
||||
"pureboot 5 and later")
|
||||
parser.add_argument("--force", action="store_true", help="override refusable safety checks")
|
||||
parser.add_argument("--stay", action="store_true", help="leave the loader in its session")
|
||||
parser.add_argument("-v", "--verbose", action="store_true",
|
||||
@@ -1209,6 +1333,10 @@ def main():
|
||||
op_read_eeprom(loader, args.read_eeprom)
|
||||
if args.verify_eeprom:
|
||||
op_verify_eeprom(loader, args.verify_eeprom)
|
||||
if args.poke:
|
||||
op_poke(loader, args.poke)
|
||||
if args.peek:
|
||||
op_peek(loader, args.peek)
|
||||
if args.stay:
|
||||
print("loader stays in its session (reset to leave)")
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user