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:
@@ -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)
|
||||||
|
|||||||
@@ -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, 466–504 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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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))();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user