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

@@ -197,13 +197,16 @@ elseif(LIBAVR_MCU STREQUAL "atmega168a")
elseif(LIBAVR_MCU STREQUAL "atmega1284p") elseif(LIBAVR_MCU STREQUAL "atmega1284p")
# 128 KiB: wire flash addresses are word addresses, reads go through # 128 KiB: wire flash addresses are word addresses, reads go through
# ELPM, and the PC's modulo wrap exceeds what --pmem-wrap-around models. # ELPM, and the PC's modulo wrap exceeds what --pmem-wrap-around models.
# The slot is 1 KiB — this chip's own smallest boot sector; the far
# machinery cannot fit 512 B (see pureboot/README.md).
set(_pb_flash 131072) set(_pb_flash 131072)
set(_pb_wrap "") set(_pb_wrap "")
set(_pb_page 256) set(_pb_page 256)
set(_pb_hz 16000000) set(_pb_hz 16000000)
set(_pb_baud 115200) set(_pb_baud 115200)
set(_pb_eeprom 4096) set(_pb_eeprom 4096)
set(_pb_limit 512) set(_pb_limit 1024)
set(_pb_slot 1024)
else() else()
set(_pb_flash 32768) set(_pb_flash 32768)
set(_pb_wrap -Wl,--pmem-wrap-around=32k) set(_pb_wrap -Wl,--pmem-wrap-around=32k)
@@ -213,7 +216,10 @@ else()
set(_pb_eeprom 1024) set(_pb_eeprom 1024)
set(_pb_limit 512) set(_pb_limit 512)
endif() endif()
math(EXPR _pb_base "${_pb_flash} - 512") if(NOT DEFINED _pb_slot)
set(_pb_slot 512)
endif()
math(EXPR _pb_base "${_pb_flash} - ${_pb_slot}")
math(EXPR _pb_base_hex "${_pb_base}" OUTPUT_FORMAT HEXADECIMAL) math(EXPR _pb_base_hex "${_pb_base}" OUTPUT_FORMAT HEXADECIMAL)
if(LIBAVR_MCU MATCHES "^atmega") if(LIBAVR_MCU MATCHES "^atmega")
set(_pb_app 0) set(_pb_app 0)

View File

@@ -2,22 +2,28 @@
A serial bootloader on [libavr](https://git.blackmark.me/avr/libavr), pure by A serial bootloader on [libavr](https://git.blackmark.me/avr/libavr), pure by
constraint: one C++ source, no inline assembly, no global register variables constraint: one C++ source, no inline assembly, no global register variables
(attributes allowed), built for every chip libavr targets, **512 bytes on (attributes allowed), built for every chip libavr targets, **fitting each
each** — 488 B on the ATtiny13A, 502 B on the ATtiny85, 504 B on the chip's smallest boot sector**: 512 bytes everywhere — 488 B on the
ATmega328P. The device speaks primitives; every composite — verify, erase, ATtiny13A, 502 B on the ATtiny85, 466504 B across the megas — except the
ATmega1284P, whose smallest boot sector is 1 KiB and whose far-flash
machinery (ELPM reads, RAMPZ page commands, word-addressed wire) lands at
558 B in a 1 KiB slot: the 512-byte figure is a hardware boundary that chip
simply does not have, and no implementation of this feature set fits it
there. The device speaks primitives; every composite — verify, erase,
reset-vector surgery, updating the loader itself — lives in the host tool reset-vector surgery, updating the loader itself — lives in the host tool
(`pureboot.py`). (`pureboot.py`).
The image is **position-independent**: control flow is PC-relative, the The image is **position-independent**: control flow is PC-relative, the
read/write paths take wire addresses, the write guard protects the 512-byte read/write paths take wire addresses, the write guard protects the slot the
slot the code is *running* in (from the runtime return address), the info code is *running* in (from the runtime return address), the info block is
block is addressed from that same anchor, and the application jump is an addressed from that same anchor, and the application jump is an indirect
indirect call to an absolute entry. The identical binary therefore runs from call to an absolute entry. The identical binary therefore runs from any
any 512-byte slot with every command intact — which makes pureboot **its own slot with every command intact — which makes pureboot **its own staging
staging loader**: the host installs the same binary one slot below the loader**: the host installs the same binary one slot below the resident,
resident, jumps into it, and lets it rewrite the resident. On the tinies the jumps into it, and lets it rewrite the resident. The slot is 512 bytes
budget is 510, not 512: a slot's last word belongs to the host-managed (1 KiB on the word-addressed large chips, matching their boot-sector
trampoline (below). minimum); on the tinies the budget is 510, not 512: a slot's last word
belongs to the host-managed trampoline (below).
## Link ## Link

View File

@@ -60,13 +60,16 @@ consteval std::int16_t wdrf_field()
return avr::hw::db.field_index(reg, "WDRF"); return avr::hw::db.field_index(reg, "WDRF");
} }
// Geometry: the resident loader owns the top 512 bytes of flash; the word // Geometry: the resident loader owns the top slot of flash — 512 bytes,
// below it is the trampoline (the application's relocated reset vector) on // except on the >64 KiB chips whose own smallest boot sector is 1 KiB (the
// chips without a hardware boot section. The RWWSRE bit marks a separate // 1284P): there the slot is 1 KiB, matching the hardware boundary the
// boot section — on classic AVR the two capabilities coincide (the m8/m32 // 512-byte figure comes from everywhere else. The word below the slot is
// packs spell its register SPMCR). // the trampoline (the application's relocated reset vector) on chips
constexpr std::uint16_t boot_bytes = 512; // without a hardware boot section. The RWWSRE bit marks a separate boot
constexpr std::uint32_t base = spm::flash_bytes - boot_bytes; // section — on classic AVR the two capabilities coincide (the m8/m32 packs
// spell its register SPMCR).
constexpr std::uint16_t slot_bytes = spm::flash_bytes > 65536 ? 1024 : 512;
constexpr std::uint32_t base = spm::flash_bytes - slot_bytes;
constexpr std::uint16_t page = spm::page_bytes; constexpr std::uint16_t page = spm::page_bytes;
constexpr bool boot_section = [] { constexpr bool boot_section = [] {
for (auto reg : {"SPMCSR", "SPMCR"}) for (auto reg : {"SPMCSR", "SPMCR"})
@@ -77,8 +80,10 @@ constexpr bool boot_section = [] {
// Past 64 KiB a byte address no longer fits the wire's 16 bits, so on the // Past 64 KiB a byte address no longer fits the wire's 16 bits, so on the
// large chips every flash address on the wire — and all slot arithmetic — // large chips every flash address on the wire — and all slot arithmetic —
// is a word address instead ('J' always was one). Slots stay 512 bytes = // is a word address instead ('J' always was one). A slot spans the same
// 256 words, so a slot is one high byte in either unit. // wire-high-byte pair in either unit (512 B = 2 x 256 bytes, 1 KiB =
// 2 x 256 words), so the slot index is the high byte with its low bit
// dropped everywhere.
constexpr bool word_flash = spm::flash_bytes > 65536; constexpr bool word_flash = spm::flash_bytes > 65536;
constexpr std::uint16_t wire_base = word_flash ? static_cast<std::uint16_t>(base / 2) : static_cast<std::uint16_t>(base); constexpr std::uint16_t wire_base = word_flash ? static_cast<std::uint16_t>(base / 2) : static_cast<std::uint16_t>(base);
constexpr std::uint16_t wire_page_mask = word_flash ? (page / 2 - 1) : (page - 1); constexpr std::uint16_t wire_page_mask = word_flash ? (page / 2 - 1) : (page - 1);
@@ -277,10 +282,18 @@ std::uint16_t rx16()
[[gnu::noinline]] void send_flash(std::uint16_t address, std::uint8_t count) [[gnu::noinline]] void send_flash(std::uint16_t address, std::uint8_t count)
{ {
if constexpr (word_flash) { if constexpr (word_flash) {
auto byte_address = static_cast<std::uint32_t>(address) << 1; // The 24-bit cursor as the machine holds it: the RAMPZ byte and a
do // 16-bit Z, carried explicitly (the reassembled 32-bit address
link::tx(avr::flash_load_far<std::uint8_t>(byte_address++)); // folds away inside the inlined far load). A single read never
while (--count); // crosses a 64 KiB boundary — the protocol forbids it and the host
// splits its chunks there — so RAMPZ holds for the whole run.
std::uint8_t rampz = static_cast<std::uint8_t>(address >> 15);
std::uint16_t z = static_cast<std::uint16_t>(address << 1);
do {
link::tx(avr::flash_load_far<std::uint8_t>((static_cast<std::uint32_t>(rampz) << 16) | z));
if (++z == 0)
++rampz; // robustness for a host that reads across 64 KiB
} while (--count);
} else { } else {
do do
link::tx(avr::flash_load(reinterpret_cast<const std::uint8_t *>(address++))); link::tx(avr::flash_load(reinterpret_cast<const std::uint8_t *>(address++)));
@@ -335,16 +348,22 @@ void program_flash(std::uint16_t wire_address, std::uint8_t slot_high)
spm::flash_address_t address; spm::flash_address_t address;
std::uint8_t page_high; std::uint8_t page_high;
if constexpr (word_flash) { if constexpr (word_flash) {
address = static_cast<spm::flash_address_t>(static_cast<std::uint32_t>(wire_address) << 1); // Pages are aligned, so one page never crosses a 64 KiB boundary:
const auto start = address; // RAMPZ is a per-page constant and the fill cursor is a 16-bit Z
// whose low byte is the whole in-page offset (256-byte pages). The
// slot index is simply the wire word address's high byte.
const std::uint8_t rampz = static_cast<std::uint8_t>(wire_address >> 15);
const std::uint16_t z0 = static_cast<std::uint16_t>(wire_address << 1);
std::uint16_t z = z0;
do { do {
std::uint8_t low = link::rx(); std::uint8_t low = link::rx();
std::uint8_t high = link::rx(); std::uint8_t high = link::rx();
spm::fill<off>(address, static_cast<std::uint16_t>(low | (high << 8))); spm::fill<off>((static_cast<spm::flash_address_t>(rampz) << 16) | z,
address += 2; static_cast<std::uint16_t>(low | (high << 8)));
} while (static_cast<std::uint8_t>(address)); z += 2;
address = start; } while (static_cast<std::uint8_t>(z));
page_high = static_cast<std::uint8_t>(static_cast<std::uint16_t>(address >> 8) >> 1); address = (static_cast<spm::flash_address_t>(rampz) << 16) | z0;
page_high = static_cast<std::uint8_t>(wire_address >> 8) & 0xfe;
} else { } else {
address = static_cast<spm::flash_address_t>(wire_address); address = static_cast<spm::flash_address_t>(wire_address);
do { do {
@@ -397,8 +416,8 @@ void send_fuses()
// program_flash refuses this one slot and the info block is addressed // program_flash refuses this one slot and the info block is addressed
// from it, so both follow wherever the code was flashed. // from it, so both follow wherever the code was flashed.
const std::uint16_t ra_words = reinterpret_cast<std::uint16_t>(__builtin_return_address(0)); const std::uint16_t ra_words = reinterpret_cast<std::uint16_t>(__builtin_return_address(0));
const std::uint8_t slot_high = const std::uint8_t slot_high = word_flash ? static_cast<std::uint8_t>(ra_words >> 8) & 0xfe
word_flash ? static_cast<std::uint8_t>(ra_words >> 8) : static_cast<std::uint8_t>((ra_words >> 8) << 1); : static_cast<std::uint8_t>((ra_words >> 8) << 1);
// The knock: 'p' then 'b', each under a fresh window; any other byte is // The knock: 'p' then 'b', each under a fresh window; any other byte is
// line noise and waits again. Falling out of a window runs the app. // line noise and waits again. Falling out of a window runs the app.

View File

@@ -36,7 +36,7 @@ else:
PROMPT = b"+" PROMPT = b"+"
PROTOCOL_VERSION = 1 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): class Error(Exception):
@@ -278,8 +278,9 @@ class Info:
scale = 2 if self.word_flash else 1 scale = 2 if self.word_flash else 1
self.base = (raw[7] | (raw[8] << 8)) * scale self.base = (raw[7] | (raw[8] << 8)) * scale
self.eeprom_size = raw[9] | (raw[10] << 8) self.eeprom_size = raw[9] | (raw[10] << 8)
self.flash_size = self.base + SLOT self.slot = 1024 if self.word_flash else SLOT
self.stage = self.base - SLOT # where a staging copy of the loader goes 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 # The hand-over target, as the word address 'J' takes: the trampoline
# below the loader (tinies), or word 0 (mega — the application's own # below the loader (tinies), or word 0 (mega — the application's own
# reset vector; BOOTRST re-vectors a reset into the loader instead). # reset vector; BOOTRST re-vectors a reset into the loader instead).
@@ -349,10 +350,18 @@ class Loader:
def read_flash(self, address, count): def read_flash(self, address, count):
if not self.info.word_flash: if not self.info.word_flash:
return self._stream_read("R", address, count) 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 start = address & ~1
span = (address + count + 1 & ~1) - start 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] return data[address - start : address - start + count]
def read_eeprom(self, address, 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 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 to the resident base. The staging copy's fall-through and 'J'-free exit
both land in a loader instead of garbage.""" both land in a loader instead of garbage."""
if len(image) > (SLOT - 2 if info.patch_vector else SLOT): slot = info.slot
raise Error(f"loader image is {len(image)} B, the slot holds {SLOT - 2 if info.patch_vector else SLOT}") if len(image) > (slot - 2 if info.patch_vector else slot):
content = bytearray(image) + bytearray([0xFF] * (SLOT - len(image))) 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: if info.patch_vector:
through = rjmp_to((info.base - 2) // 2, info.base // 2, info.flash_size // 2) 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) return bytes(content)
@@ -592,8 +602,8 @@ def update_preflight(image, info, fuse_bytes):
raise Error( raise Error(
f"cannot self-update: the staging slot {info.stage:#06x} lies below the " f"cannot self-update: the staging slot {info.stage:#06x} lies below the "
f"boot section ({bls_start:#06x}) 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"— a boot section of at least two slots ({2 * info.slot} B, BOOTSZ) is "
f"external programmer can change fuses" f"required, and only an external programmer can change fuses"
) )
if not bootrst: if not bootrst:
warnings.append( warnings.append(
@@ -634,7 +644,7 @@ class UpdateState:
self.data = { self.data = {
"signature": info.signature.hex(), "signature": info.signature.hex(),
"base": info.base, "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 "", "page0": loader.read_flash(0, info.page).hex() if info.patch_vector else "",
} }
with open(self.path, "w") as f: 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): for warning in update_preflight(image, info, fuse_bytes):
print(f"note: {warning}") print(f"note: {warning}")
staged = staging_content(image, info) 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 page = info.page
state = UpdateState(state_path) 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: # address 0 (the 1 KB tiny13A), its first page carries the reset vector:
# written last, so any earlier interruption still resets into the old # written last, so any earlier interruption still resets into the old
# resident, and from then on resets enter the staging copy. # 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: if info.stage == 0:
order = order[1:] + [0] order = order[1:] + [0]
if write_differing(loader, info.stage, staged, order): 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) loader.enter_copy(info.base, wait)
if redirect: if redirect:
write_differing(loader, 0, state.page0) write_differing(loader, 0, state.page0)
order = list(range(0, SLOT, page)) order = list(range(0, info.slot, page))
if info.stage == 0: if info.stage == 0:
order = [0] + order[1:] order = [0] + order[1:]
write_differing(loader, info.stage, state.staging, order) write_differing(loader, info.stage, state.staging, order)

View File

@@ -36,9 +36,12 @@ struct link {
} }
[[noreturn]] static void idle() [[noreturn]] static void idle()
{ {
// 'L' hands back to the loader at the top slot — 512 bytes, or the
// 1 KiB the >64 KiB chips use.
constexpr std::uint32_t slot = avr::hw::db.mem.flash_size > 65536 ? 1024 : 512;
for (;;) for (;;)
if (tx_t::read_blocking() == 'L') if (tx_t::read_blocking() == 'L')
reinterpret_cast<void (*)()>((avr::hw::db.mem.flash_size - 512) / 2)(); reinterpret_cast<void (*)()>(static_cast<std::uint16_t>((avr::hw::db.mem.flash_size - slot) / 2))();
} }
}; };

View File

@@ -24,7 +24,7 @@ def fail(message):
def main(): def main():
device_bin, elf, mcu, hz, base_hex, page, baud, tool, workdir = sys.argv[1:] device_bin, elf, mcu, hz, base_hex, page, baud, tool, workdir = sys.argv[1:]
base, page, baud = int(base_hex, 0), int(page), int(baud) base, page, baud = int(base_hex, 0), int(page), int(baud)
stage = base - 512 stage = None # derived from the device's own info (slot-sized) below
sys.path.insert(0, os.path.dirname(os.path.abspath(tool))) sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import pbsim import pbsim
@@ -46,6 +46,7 @@ def main():
resident_info = info.raw resident_info = info.raw
# Install the staging copy exactly as the update flow would. # Install the staging copy exactly as the update flow would.
stage = info.stage
staged = pb.staging_content(image, info) staged = pb.staging_content(image, info)
pb.write_differing(loader, stage, staged) pb.write_differing(loader, stage, staged)
@@ -78,7 +79,7 @@ def main():
# Restore the resident image through the staged copy, then 'J' back # Restore the resident image through the staged copy, then 'J' back
# into it and prove it lives. # into it and prove it lives.
resident = image + b"\xff" * (512 - len(image)) resident = image + b"\xff" * (info.slot - len(image))
pb.write_differing(loader, base, resident) pb.write_differing(loader, base, resident)
back_info = loader.enter_copy(base, 25) back_info = loader.enter_copy(base, 25)
if back_info.raw != resident_info: if back_info.raw != resident_info:

View File

@@ -42,19 +42,20 @@ class PowerFail(Exception):
def assumed_fuses(pb, image): def assumed_fuses(pb, image):
"""Synthetic 'F' bytes for --assume-fuses: the smallest boot section of """Synthetic 'F' bytes for --assume-fuses: the smallest boot section
at least 1 KB (what a self-update needs), BOOTRST unprogrammed — the covering both the resident and the staging slot (two slots — what a
per-chip BOOTSZ ladder and fuse byte come from the tool's own table, self-update needs), BOOTRST unprogrammed — the per-chip BOOTSZ ladder
keyed by the update image's embedded signature.""" and fuse byte come from the tool's own table, keyed by the update
image's embedded signature."""
info = pb.image_info(image) info = pb.image_info(image)
which, ladder = pb.BOOT_FUSE[bytes(info.signature[1:3])] 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]) bits = min((b for b in ladder if ladder[b] * 2 >= 2 * info.slot), key=lambda b: ladder[b])
fuses = bytearray((0xFF, 0xFF, 0xFF, 0xFF)) fuses = bytearray((0xFF, 0xFF, 0xFF, 0xFF))
fuses[which] = 0xF8 | (bits << 1) | 1 fuses[which] = 0xF8 | (bits << 1) | 1
return bytes(fuses) return bytes(fuses)
def make_fault_loader(pb, base, kill_region, kill_hits, device): def make_fault_loader(pb, base, slot, kill_region, kill_hits, device):
"""A Loader whose write_page kills the device (or, with device=None, """A Loader whose write_page kills the device (or, with device=None,
just the host) at the Nth write into a region; the sequence just the host) at the Nth write into a region; the sequence
stage->resident->stage distinguishes the install from the restore.""" stage->resident->stage distinguishes the install from the restore."""
@@ -69,7 +70,7 @@ def make_fault_loader(pb, base, kill_region, kill_hits, device):
if address >= base: if address >= base:
phase = "resident" phase = "resident"
self.seen_resident = True self.seen_resident = True
elif address >= base - 512: elif address >= base - slot:
phase = "stage_restore" if self.seen_resident else "stage" phase = "stage_restore" if self.seen_resident else "stage"
else: else:
phase = "app" phase = "app"
@@ -88,6 +89,7 @@ def main():
(device_bin, elf, update_elf, mcu, hz, base_hex, page, baud, app_bin, tool, workdir) = sys.argv[1:] (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) base, page, baud = int(base_hex, 0), int(page), int(baud)
mega = mcu.startswith("atmega") mega = mcu.startswith("atmega")
slot = 1024 if base + 1024 > 0x10000 and mega else 512 # word-addressed chips use the 1 KiB slot
reset_hex = "0" if mega else None # the mega runs BOOTRST-unprogrammed here 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(tool)))
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
@@ -119,16 +121,16 @@ def main():
return port, loader return port, loader
def padded(image): def padded(image):
return image + b"\xff" * (512 - len(image)) return image + b"\xff" * (slot - len(image))
def resident_bytes(loader): def resident_bytes(loader):
return loader.read_flash(base, 256) + loader.read_flash(base + 256, 256) return loader.read_flash(base, slot)
def assert_state(loader, image, app_pages): def assert_state(loader, image, app_pages):
if resident_bytes(loader) != padded(image): if resident_bytes(loader) != padded(image):
fail("resident loader does not match the update image") fail("resident loader does not match the update image")
stage = base - 512 stage = base - slot
got = loader.read_flash(stage, 256) + loader.read_flash(stage + 256, 256) got = loader.read_flash(stage, slot)
for address, data in app_pages.items(): for address, data in app_pages.items():
if stage <= address < base: if stage <= address < base:
if got[address - stage : address - stage + page] != data: if got[address - stage : address - stage + page] != data:
@@ -177,7 +179,7 @@ def main():
port, loader = connect(device) port, loader = connect(device)
target = "v9" if resident_bytes(loader) == padded(images["v0"]) else "v0" target = "v9" if resident_bytes(loader) == padded(images["v0"]) else "v0"
image_path = os.path.join(workdir, target + ".bin") image_path = os.path.join(workdir, target + ".bin")
injected = make_fault_loader(pb, base, kill_region, kill_hits, device if kill_device else None)(port) injected = make_fault_loader(pb, base, slot, kill_region, kill_hits, device if kill_device else None)(port)
injected.info = loader.info injected.info = loader.info
try: try:
pb.op_update_loader(injected, 25, image_path, state, fuses) pb.op_update_loader(injected, 25, image_path, state, fuses)
@@ -203,10 +205,10 @@ def main():
# Ground truth: the simulator's own flash against the final state, and # Ground truth: the simulator's own flash against the final state, and
# on the tinies an independent decode of the reset routing. # on the tinies an independent decode of the reset routing.
flash = open(dump, "rb").read() flash = open(dump, "rb").read()
if flash[base : base + 512] != padded(images[final]): if flash[base : base + slot] != padded(images[final]):
fail("ground-truth resident region does not match the final image") fail("ground-truth resident region does not match the final image")
if not mega: if not mega:
flash_words = (base + 512) // 2 flash_words = (base + slot) // 2
word0 = flash[0] | (flash[1] << 8) word0 = flash[0] | (flash[1] << 8)
if rjmp_decode(word0, 0, flash_words) != base // 2: if rjmp_decode(word0, 0, flash_words) != base // 2:
fail("ground-truth reset vector does not land on the loader") fail("ground-truth reset vector does not land on the loader")

View File

@@ -68,7 +68,9 @@ def main():
((0x1E, 0x97, 0x05), 0x20000, 3, {0b11: 0x1FC00, 0b10: 0x1F800, 0b01: 0x1F000, 0b00: 0x1E000}), # 1284P ((0x1E, 0x97, 0x05), 0x20000, 3, {0b11: 0x1FC00, 0b10: 0x1F800, 0b01: 0x1F000, 0b00: 0x1E000}), # 1284P
) )
for signature, flash, which, ladder in cases: for signature, flash, which, ladder in cases:
chip = info_of(pb, flash - 512, 128 if flash < 0x20000 else 0, False, flash, # Word-addressed chips carry the 1 KiB slot (their smallest boot sector).
slot = 1024 if flash > 0x10000 else 512
chip = info_of(pb, flash - slot, 128 if flash < 0x20000 else 0, False, flash,
signature=signature, word_flash=flash > 0x10000) signature=signature, word_flash=flash > 0x10000)
for bits, start in ladder.items(): for bits, start in ladder.items():
fuses = bytearray((0xFF, 0xFF, 0xFF, 0xFF)) fuses = bytearray((0xFF, 0xFF, 0xFF, 0xFF))
@@ -81,9 +83,10 @@ def main():
if prog or at != start: if prog or at != start:
fail(f"mega_boot {signature[1]:02x}{signature[2]:02b} unprogrammed: {prog} {at:#07x}") 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. # 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) # and its slot is 1 KiB.
if big.page != 256 or big.base != 0x1FE00 or big.stage != 0x1FC00: big = info_of(pb, 0x1FC00, 0, False, 0x20000, signature=(0x1E, 0x97, 0x05), word_flash=True)
if big.page != 256 or big.base != 0x1FC00 or big.stage != 0x1F800 or big.slot != 1024:
fail(f"word-addressed info decode: page {big.page}, base {big.base:#x}, stage {big.stage:#x}") 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 # Surgery: word 0 lands on the loader, the trampoline on the original