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:
@@ -37,7 +37,7 @@ def main():
|
||||
sys.exit(1)
|
||||
|
||||
symbols = subprocess.run([nm, "-C", elf], capture_output=True, text=True, check=True).stdout
|
||||
info = [line for line in symbols.splitlines() if "flash_table" in line and "::storage" in line]
|
||||
info = [line for line in symbols.splitlines() if "info_data" in line]
|
||||
if len(info) != 1:
|
||||
print(f"FAIL: expected one info-block storage symbol, found {len(info)}")
|
||||
sys.exit(1)
|
||||
|
||||
@@ -26,7 +26,8 @@ consteval avr::hertz_t clock()
|
||||
|
||||
using dev = avr::device<{.clock = clock()}>;
|
||||
|
||||
template <avr::hertz_t C, bool Hardware = avr::hw::db.has_reg("UDR0")>
|
||||
template <avr::hertz_t C,
|
||||
bool Hardware = avr::hw::db.has_instance("USART0") || avr::hw::db.has_instance("USART")>
|
||||
struct link {
|
||||
using tx_t = avr::uart::usart0<C, {.baud = 115200_Bd, .max_baud_error = 2.5_pct}>;
|
||||
static void tx(char c)
|
||||
|
||||
@@ -11,7 +11,7 @@ class Device:
|
||||
def __init__(self, binary, elf, mcu, hz, base_hex, page, baud, dump, reset_hex=None, resume=None):
|
||||
cmd = [binary, elf, mcu, hz, base_hex, str(page), str(baud), dump]
|
||||
if reset_hex is not None or resume is not None:
|
||||
cmd.append(reset_hex if reset_hex is not None else ("0" if mcu != "atmega328p" else base_hex))
|
||||
cmd.append(reset_hex if reset_hex is not None else ("0" if not mcu.startswith("atmega") else base_hex))
|
||||
if resume is not None:
|
||||
cmd.append(resume)
|
||||
self.log = open(dump + ".log", "a")
|
||||
|
||||
@@ -90,11 +90,17 @@ def main():
|
||||
read_flash = os.path.join(workdir, "readback_flash.bin")
|
||||
read_eeprom = os.path.join(workdir, "readback_eeprom.bin")
|
||||
|
||||
# The geometry the host will discover, for computing the expected image.
|
||||
# The geometry the host will discover, for computing the expected image:
|
||||
# megas carry a boot section (no vector surgery), the large ones speak
|
||||
# word addresses, and the page byte is the wire's 0-means-256.
|
||||
mega = mcu.startswith("atmega")
|
||||
word_flash = base + 512 > 0x10000
|
||||
wire_base = base // 2 if word_flash else base
|
||||
flags = (0 if mega else 1) | (2 if word_flash else 0)
|
||||
info = pb.Info(
|
||||
bytes([ord("P"), ord("B"), 1, 0, 0, 0, page])
|
||||
+ bytes([base & 0xFF, base >> 8, eeprom_size & 0xFF, eeprom_size >> 8])
|
||||
+ bytes([0 if mcu == "atmega328p" else 1])
|
||||
bytes([ord("P"), ord("B"), 1, 0, 0, 0, page & 0xFF])
|
||||
+ bytes([wire_base & 0xFF, wire_base >> 8, eeprom_size & 0xFF, eeprom_size >> 8])
|
||||
+ bytes([flags])
|
||||
)
|
||||
|
||||
device = Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump)
|
||||
@@ -150,8 +156,9 @@ def main():
|
||||
fail("loader region looks erased in the ground-truth dump")
|
||||
|
||||
# The surgery, decoded independently: the patched vector must land on the
|
||||
# loader, the trampoline on the application's own entry.
|
||||
if mcu != "atmega328p":
|
||||
# loader, the trampoline on the application's own entry (tinies only —
|
||||
# the megas' word 0 stays the application's).
|
||||
if not mega:
|
||||
flash_words = (base + 512) // 2
|
||||
app = open(app_bin, "rb").read()
|
||||
word0 = flash_true[0] | (flash_true[1] << 8)
|
||||
|
||||
@@ -41,7 +41,17 @@ class PowerFail(Exception):
|
||||
pass
|
||||
|
||||
|
||||
MEGA_FUSES = "ffffffdd" # high 0xdd: BOOTSZ = 1 KB, BOOTRST unprogrammed
|
||||
def assumed_fuses(pb, image):
|
||||
"""Synthetic 'F' bytes for --assume-fuses: the smallest boot section of
|
||||
at least 1 KB (what a self-update needs), BOOTRST unprogrammed — the
|
||||
per-chip BOOTSZ ladder and fuse byte come from the tool's own table,
|
||||
keyed by the update image's embedded signature."""
|
||||
info = pb.image_info(image)
|
||||
which, ladder = pb.BOOT_FUSE[bytes(info.signature[1:3])]
|
||||
bits = min((b for b in ladder if ladder[b] * 2 >= 1024), key=lambda b: ladder[b])
|
||||
fuses = bytearray((0xFF, 0xFF, 0xFF, 0xFF))
|
||||
fuses[which] = 0xF8 | (bits << 1) | 1
|
||||
return bytes(fuses)
|
||||
|
||||
|
||||
def make_fault_loader(pb, base, kill_region, kill_hits, device):
|
||||
@@ -77,7 +87,7 @@ def make_fault_loader(pb, base, kill_region, kill_hits, device):
|
||||
def main():
|
||||
(device_bin, elf, update_elf, mcu, hz, base_hex, page, baud, app_bin, tool, workdir) = sys.argv[1:]
|
||||
base, page, baud = int(base_hex, 0), int(page), int(baud)
|
||||
mega = mcu == "atmega328p"
|
||||
mega = mcu.startswith("atmega")
|
||||
reset_hex = "0" if mega else None # the mega runs BOOTRST-unprogrammed here
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
@@ -95,7 +105,7 @@ def main():
|
||||
fail("the update image is byte-identical to the resident build")
|
||||
dump = os.path.join(workdir, "dump.bin")
|
||||
state = os.path.join(workdir, "update.pbstate")
|
||||
fuses = bytes.fromhex(MEGA_FUSES) if mega else None
|
||||
fuses = assumed_fuses(pb, images["v0"]) if mega else None
|
||||
|
||||
def connect(device):
|
||||
port = pb.Port(device.pty, baud)
|
||||
@@ -136,7 +146,7 @@ def main():
|
||||
# A clean CLI update, resident -> v9.
|
||||
args = ["--update-loader", os.path.join(workdir, "v9.bin"), "--state", state, "--stay"]
|
||||
if mega:
|
||||
args += ["--assume-fuses", MEGA_FUSES]
|
||||
args += ["--assume-fuses", fuses.hex()]
|
||||
out = pbsim.run_tool(tool, device.pty, baud, *args)
|
||||
if "loader updated" not in out:
|
||||
fail("update did not report success")
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// the patched vector are not what is under test), and exposes the loader's
|
||||
// serial link as a pty for the real host tool:
|
||||
//
|
||||
// - ATmega328P: the hardware USART0 through simavr's uart_pty.
|
||||
// - Megas: the hardware USART through simavr's uart_pty.
|
||||
// - Tinies: an 8N1 bridge between a pty and the GPIO software UART
|
||||
// (drives PB0, the loader's RX; decodes PB1, its TX), timed against the
|
||||
// simulated cycle counter.
|
||||
@@ -279,7 +279,7 @@ int main(int argc, char *argv[])
|
||||
unsigned page = (unsigned)atoi(argv[5]);
|
||||
unsigned baud = (unsigned)atoi(argv[6]);
|
||||
dump_path = argv[7];
|
||||
use_uart_pty = strcmp(mcu_name, "atmega328p") == 0;
|
||||
use_uart_pty = strncmp(mcu_name, "atmega", 6) == 0; // every mega links over its hardware USART
|
||||
|
||||
avr = avr_make_mcu_by_name(mcu_name);
|
||||
if (!avr) {
|
||||
|
||||
@@ -27,9 +27,12 @@ def expect_error(what, fn, *needles):
|
||||
fail(f"{what}: no error raised")
|
||||
|
||||
|
||||
def info_of(pb, base, page, patch, flash):
|
||||
raw = bytes((0x50, 0x42, 1, 0x1E, 0x93, 0x0B, page, base & 0xFF, base >> 8,
|
||||
0, 2, 1 if patch else 0))
|
||||
def info_of(pb, base, page, patch, flash, signature=(0x1E, 0x93, 0x0B), word_flash=False):
|
||||
scale = 2 if word_flash else 1
|
||||
wire_base = base // scale
|
||||
flags = (1 if patch else 0) | (2 if word_flash else 0)
|
||||
raw = bytes((0x50, 0x42, 1, *signature, page & 0xFF, wire_base & 0xFF, wire_base >> 8,
|
||||
0, 2, flags))
|
||||
info = pb.Info(raw)
|
||||
assert info.flash_size == flash
|
||||
return info
|
||||
@@ -50,16 +53,38 @@ def main():
|
||||
import pureboot as pb
|
||||
|
||||
tiny = info_of(pb, 0x1E00, 64, True, 0x2000)
|
||||
mega = info_of(pb, 0x7E00, 128, False, 0x8000)
|
||||
mega = info_of(pb, 0x7E00, 128, False, 0x8000, signature=(0x1E, 0x95, 0x0F))
|
||||
|
||||
# mega_boot: BOOTSZ words and the BOOTRST sense, DS40002061B §27.
|
||||
for bits, start in ((0b11, 0x7E00), (0b10, 0x7C00), (0b01, 0x7800), (0b00, 0x7000)):
|
||||
prog, at = pb.mega_boot((0xF8 | (bits << 1)) & ~1)
|
||||
if not prog or at != start:
|
||||
fail(f"mega_boot BOOTSZ={bits:02b} programmed: {prog} {at:#06x}")
|
||||
prog, at = pb.mega_boot(0xF8 | (bits << 1) | 1)
|
||||
if prog or at != start:
|
||||
fail(f"mega_boot BOOTSZ={bits:02b} unprogrammed: {prog} {at:#06x}")
|
||||
# mega_boot: BOOTSZ words and the BOOTRST sense per chip — the fuse byte
|
||||
# index (HIGH everywhere but the m168A's EXTENDED) and the per-family
|
||||
# ladders (Atmel-2486/2466/2503/8271/42719). Synthetic 'F' replies: only
|
||||
# the boot byte carries meaning.
|
||||
cases = (
|
||||
((0x1E, 0x93, 0x07), 0x2000, 3, {0b11: 0x1F00, 0b10: 0x1E00, 0b01: 0x1C00, 0b00: 0x1800}), # m8
|
||||
((0x1E, 0x94, 0x03), 0x4000, 3, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m16
|
||||
((0x1E, 0x95, 0x02), 0x8000, 3, {0b11: 0x7E00, 0b10: 0x7C00, 0b01: 0x7800, 0b00: 0x7000}), # m32
|
||||
((0x1E, 0x94, 0x06), 0x4000, 2, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m168A
|
||||
((0x1E, 0x95, 0x0F), 0x8000, 3, {0b11: 0x7E00, 0b10: 0x7C00, 0b01: 0x7800, 0b00: 0x7000}), # m328P
|
||||
((0x1E, 0x97, 0x05), 0x20000, 3, {0b11: 0x1FC00, 0b10: 0x1F800, 0b01: 0x1F000, 0b00: 0x1E000}), # 1284P
|
||||
)
|
||||
for signature, flash, which, ladder in cases:
|
||||
chip = info_of(pb, flash - 512, 128 if flash < 0x20000 else 0, False, flash,
|
||||
signature=signature, word_flash=flash > 0x10000)
|
||||
for bits, start in ladder.items():
|
||||
fuses = bytearray((0xFF, 0xFF, 0xFF, 0xFF))
|
||||
fuses[which] = (0xF8 | (bits << 1)) & ~1
|
||||
prog, at = pb.mega_boot(chip, bytes(fuses))
|
||||
if not prog or at != start:
|
||||
fail(f"mega_boot {signature[1]:02x}{signature[2]:02x} BOOTSZ={bits:02b} programmed: {prog} {at:#07x}")
|
||||
fuses[which] |= 1
|
||||
prog, at = pb.mega_boot(chip, bytes(fuses))
|
||||
if prog or at != start:
|
||||
fail(f"mega_boot {signature[1]:02x}{signature[2]:02b} unprogrammed: {prog} {at:#07x}")
|
||||
|
||||
# Word-addressed info decode: the 1284P's base/page ride the wire scaled.
|
||||
big = info_of(pb, 0x1FE00, 0, False, 0x20000, signature=(0x1E, 0x97, 0x05), word_flash=True)
|
||||
if big.page != 256 or big.base != 0x1FE00 or big.stage != 0x1FC00:
|
||||
fail(f"word-addressed info decode: page {big.page}, base {big.base:#x}, stage {big.stage:#x}")
|
||||
|
||||
# Surgery: word 0 lands on the loader, the trampoline on the original
|
||||
# entry — checked with an independent decoder.
|
||||
|
||||
Reference in New Issue
Block a user