pureboot: a 512-byte slot on every chip, the 1284s included

The word-addressed 1284s were the one family deploying in a 1 KiB slot,
because the far-flash machinery (ELPM reads, RAMPZ page commands, a
word-addressed wire) did not fit 512 B. It does now: 478 B stock, 494 B in
the heaviest configuration the build can produce. They take the 644s'
geometry, where the smallest boot section holds the resident slot and its
staging slot together.

Most of the saving is one restructure. The info block and a flash read are
the same act, so giving all four streamed commands one address-and-count
path leaves exactly one call site for the flash streamer: it inlines into
the never-returning command loop and its 24-bit cursor stops being saved
and restored around every transmit. Around it, the ack byte moved out of
line, the wire's byte pair is bit_cast into the word it already is, the
fuse loop ends on its count, the info block's in-slot offset is taken as
the one-byte relocation it is, and -fno-expensive-optimizations gives way
to -fno-move-loop-invariants -fno-tree-ter. Every chip shrank 14-18 B.

The size matrix grew the axes it was missing: the USART1 instance across
the whole clock ladder, and the shape a slow baud gives a software UART —
past 255 delay iterations libavr takes the 16-bit delay loop, which the
ladder default never selects and which was 4 B over the 1284's slot the
first time it was built.

The protocol fixture stopped deriving the loader entry from the flash
size; on the 1284s it had been jumping a slot low and reaching the loader
only because erased flash walked it up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 19:02:59 +02:00
parent 12ab035fd3
commit 9196ba5711
11 changed files with 191 additions and 159 deletions

View File

@@ -231,12 +231,13 @@ if(PROJECT_IS_TOP_LEVEL)
endif() endif()
# The size matrix: every configuration axis that could move the image # The size matrix: every configuration axis that could move the image
# size — the serial backend (different code), the clock and its ladder # size — the serial backend (different code), the USART instance
# baud (different constants and divisor shapes), the USART instance # (different registers), the clock (different constants), and the baud
# (different register class) — each combination must still fit the # through the two shapes its bit timing takes — each combination must
# chip's slot budget. Pins are size-neutral (port and bit are immediate # still fit the chip's slot budget. Pins are size-neutral (port and bit
# operands) and the timeout is a constant, so neither adds an axis. The # are immediate operands) and the timeout is a constant, so neither adds
# stock build is one point of this matrix and already has its test. # an axis. The stock build is one point of this matrix and already has
# its test.
function(pureboot_size_variant name) function(pureboot_size_variant name)
pureboot_add_loader(${name} ${ARGN}) pureboot_add_loader(${name} ${ARGN})
add_test(NAME ${name}.size add_test(NAME ${name}.size
@@ -260,11 +261,26 @@ if(PROJECT_IS_TOP_LEVEL)
if(PUREBOOT_HAS_USART AND NOT _matrix_hz EQUAL _pb_stock_hz) if(PUREBOOT_HAS_USART AND NOT _matrix_hz EQUAL _pb_stock_hz)
pureboot_size_variant(pureboot_hw_${_matrix_khz}k CLOCK ${_matrix_hz} SERIAL hardware) pureboot_size_variant(pureboot_hw_${_matrix_khz}k CLOCK ${_matrix_hz} SERIAL hardware)
endif() endif()
if(PUREBOOT_HAS_USART1 AND NOT _matrix_hz EQUAL _pb_stock_hz)
pureboot_size_variant(pureboot_usart1_${_matrix_khz}k CLOCK ${_matrix_hz} USART 1)
endif()
endforeach() endforeach()
if(PUREBOOT_HAS_USART1) if(PUREBOOT_HAS_USART1)
pureboot_size_variant(pureboot_usart1 USART 1) pureboot_size_variant(pureboot_usart1 USART 1)
endif() endif()
# The baud axis, whose one size-bearing shape the ladder never picks: a
# software UART spins out each bit with _delay_loop_1 while the count
# fits a byte and with the 16-bit _delay_loop_2 beyond it, two words more
# setup at every one of its five sites — the largest image the
# configuration space produces. The ladder default takes the *fastest*
# rate a clock reaches, which always lands in the byte, so the wide form
# needs the slowest ladder rate against the fastest clock to appear. The
# hardware USART has no such shape: its baud is a divisor constant, and
# the ladder's U2X solutions are already its larger form.
list(GET _matrix_clocks -1 _matrix_top_hz)
pureboot_size_variant(pureboot_sw_wide CLOCK ${_matrix_top_hz} BAUD 9600 SERIAL software)
# One configured deployment end to end — a real board's shape rather # One configured deployment end to end — a real board's shape rather
# than the stock assumption: the ATmega328P on its shipped 1 MHz fuses, # than the stock assumption: the ATmega328P on its shipped 1 MHz fuses,
# the software UART on hand-picked pins (TX = PB1, RX = PB5), the ladder # the software UART on hand-picked pins (TX = PB1, RX = PB5), the ladder

View File

@@ -17,11 +17,11 @@
# software-UART cycle-floor static asserts re-check whatever is passed. # software-UART cycle-floor static asserts re-check whatever is passed.
# Per-family geometry: flash/page/EEPROM sizes and the linker wrap the PC # Per-family geometry: flash/page/EEPROM sizes and the linker wrap the PC
# modulo needs, the loader slot (each chip's smallest boot sector — 1 KiB on # modulo needs, plus the deployment defaults (crystal assumption on the
# the word-addressed 1284s), and the deployment defaults (crystal assumption # megas, calibrated RC on the tinies). The loader slot is 512 bytes on every
# on the megas, calibrated RC on the tinies). The USART flags mirror the # chip. The USART flags mirror the hardware inventory the loader's own static
# hardware inventory the loader's own static asserts check (the plain 644 is # asserts check (the plain 644 is the x4 family's one single-USART die,
# the x4 family's one single-USART die, Atmel-2593). # Atmel-2593).
set(_pb_has_usart 1) set(_pb_has_usart 1)
set(_pb_has_usart1 0) set(_pb_has_usart1 0)
if(LIBAVR_MCU MATCHES "^attiny13a?$") if(LIBAVR_MCU MATCHES "^attiny13a?$")
@@ -106,31 +106,25 @@ elseif(LIBAVR_MCU MATCHES "^atmega644(a|p|pa)?$")
elseif(LIBAVR_MCU MATCHES "^atmega1284p?$") elseif(LIBAVR_MCU MATCHES "^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 # Its smallest boot section (512 words = 1 KiB) holds the loader and its
# machinery cannot fit 512 B (see README.md). # staging slot together, the 644's geometry (see 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_eeprom 4096) set(_pb_eeprom 4096)
set(_pb_slot 1024)
set(_pb_limit 1024)
set(_pb_has_usart1 1) set(_pb_has_usart1 1)
else() else()
message(FATAL_ERROR "pureboot: no geometry for ${LIBAVR_MCU}") message(FATAL_ERROR "pureboot: no geometry for ${LIBAVR_MCU}")
endif() endif()
if(NOT DEFINED _pb_slot) set(_pb_slot 512)
set(_pb_slot 512)
endif()
math(EXPR _pb_base "${_pb_flash} - ${_pb_slot}") 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)
# Patched-vector chips hand over through the trampoline word below the slot, # Patched-vector chips hand over through the trampoline word below the slot,
# which is also the slot's own last word — their budget is slot 2. # which is also the slot's own last word — their budget is slot 2.
if(LIBAVR_MCU MATCHES "^atmega" AND NOT LIBAVR_MCU MATCHES "^atmega48") if(LIBAVR_MCU MATCHES "^atmega" AND NOT LIBAVR_MCU MATCHES "^atmega48")
set(_pb_app 0) set(_pb_app 0)
if(NOT DEFINED _pb_limit)
set(_pb_limit ${_pb_slot}) set(_pb_limit ${_pb_slot})
endif()
else() else()
math(EXPR _pb_app "${_pb_base} - 2") math(EXPR _pb_app "${_pb_base} - 2")
math(EXPR _pb_limit "${_pb_slot} - 2") math(EXPR _pb_limit "${_pb_slot} - 2")
@@ -294,15 +288,17 @@ function(pureboot_add_loader name)
add_executable(${name} ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/pureboot.cpp) add_executable(${name} ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/pureboot.cpp)
target_link_libraries(${name} PRIVATE libavr) target_link_libraries(${name} PRIVATE libavr)
target_compile_definitions(${name} PRIVATE ${_defines}) target_compile_definitions(${name} PRIVATE ${_defines})
# Codegen shaping for the loader TU only, worth ~40 B on every chip and # Codegen shaping for the loader TU only, worth 1436 B depending on the
# what carries the far-flash 1284 build under 512. At -Os GCC otherwise # chip. At -Os GCC otherwise rewrites the byte-stream loops' counters into
# rewrites the byte-stream loops' counters into end-pointer forms that # end-pointer forms that cost registers (-fno-ivopts,
# cost registers (-fno-ivopts, -fno-split-wide-types), leaves register # -fno-split-wide-types), leaves register pressure on the table with the
# pressure on the table with the default allocator # default allocator (-fira-algorithm=priority), and keeps loop-invariant
# (-fira-algorithm=priority), and spends bytes on rewrites a # immediates and expression temporaries in registers
# straight-line loader gains nothing from. # (-fno-move-loop-invariants, -fno-tree-ter) — but every loop body here
# contains a call, so a register held across it costs more than the
# load-immediate it saves.
target_compile_options(${name} PRIVATE target_compile_options(${name} PRIVATE
-fno-ivopts -fira-algorithm=priority -fno-expensive-optimizations -fno-split-wide-types) -fno-ivopts -fira-algorithm=priority -fno-move-loop-invariants -fno-tree-ter -fno-split-wide-types)
target_link_options(${name} PRIVATE -nostartfiles -Wl,--section-start=.text=${_base_hex} target_link_options(${name} PRIVATE -nostartfiles -Wl,--section-start=.text=${_base_hex}
-Wl,--defsym=pureboot_app=${_app} ${_wrap}) -Wl,--defsym=pureboot_app=${_app} ${_wrap})
add_custom_command(TARGET ${name} POST_BUILD COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${name}>) add_custom_command(TARGET ${name} POST_BUILD COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${name}>)

View File

@@ -3,26 +3,19 @@
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 and compiler flags allowed), built for **every chip libavr (attributes and compiler flags allowed), built for **every chip libavr
targets — all 37 — in 512 bytes each**: 434 B on the tiny13s, 438442 B on targets — all 37 — in 512 bytes each**: 416 B on the tiny13s, 420424 B on
the tiny25/45/85, 412452 B across the megas, and 506 B on the the tiny25/45/85, 396438 B across the megas, and 478 B on the
ATmega1284/1284P, whose far-flash machinery (ELPM reads, RAMPZ page commands, ATmega1284/1284P, whose far-flash machinery (ELPM reads, RAMPZ page commands,
word-addressed wire) is the heaviest. Those are the stock deployments; word-addressed wire) is the heaviest. Those are the stock deployments; the
choosing the software UART where the chip has a USART costs 846 B more (a rest of the configuration space costs a little more, and the dearest point
bit-bang against a peripheral), which every chip still absorbs inside its is a software UART at a slow baud — 494 B on the 1284s, the tightest image
slot — on the 1284s that means their 1 KiB boot sector, where the in the matrix at 18 B spare. Keeping the far-flash build inside 512 is what
software-serial image lands at 546 B. Bringing the 1284's default build the single call site the streamed commands share (`pureboot.cpp`) and the
under 512 at all is what the loop-placement attributes on the byte streamers codegen flags on the loader TU (`CMakeLists.txt`) are for. Clock, baud,
(`pureboot.cpp`) and the codegen flags on the loader TU (`CMakeLists.txt`) serial backend and pins are per-build configuration (below); the size matrix
are for; measured against each chip's own budget the tightest is the in the test suite holds every combination inside its slot. The device speaks
ATmega328P, 50 B spare. Clock, baud, serial backend and primitives; every composite — verify, erase, reset-vector surgery, updating
pins are per-build configuration (below); the size matrix in the test suite the loader itself — lives in the host tool (`pureboot.py`).
holds every combination inside its slot. The device speaks primitives; every
composite — verify, erase, reset-vector surgery, updating the loader itself —
lives in the host tool (`pureboot.py`).
The 1284s still *deploy* in a 1 KiB slot, their smallest boot sector being
512 words; at 506 B the image would also fit the 644's
two-512-byte-slots-per-boot-sector geometry.
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 slot the read/write paths take wire addresses, the write guard protects the slot the
@@ -31,9 +24,8 @@ addressed from that same anchor, and the application jump is an indirect
call to an absolute entry. The identical binary therefore runs from any call to an absolute entry. The identical binary therefore runs from any
slot with every command intact — which makes pureboot **its own staging slot with every command intact — which makes pureboot **its own staging
loader**: the host installs the same binary one slot below the resident, loader**: the host installs the same binary one slot below the resident,
jumps into it, and lets it rewrite the resident. The slot is 512 bytes jumps into it, and lets it rewrite the resident. The slot is 512 bytes on
(1 KiB on the word-addressed large chips, matching their boot-sector every chip; on the tinies the budget is 510, not 512: a slot's last word
minimum); on the tinies the budget is 510, not 512: a slot's last word
belongs to the host-managed trampoline (below). belongs to the host-managed trampoline (below).
## Configuration ## Configuration
@@ -215,23 +207,20 @@ a newer one). `pureboot.rehome` is the acceptance test for both
positions. Flashing the application afterwards overwrites the stale copy, positions. Flashing the application afterwards overwrites the stale copy,
vector surgery included. vector surgery included.
**Boot-sectioned megas**: program the loader at `flash slot` with an **Boot-sectioned megas**: program the loader at `flash 512` with an
external programmer. Every such mega has a BOOTSZ step whose boot section external programmer. Every such mega has a BOOTSZ step whose boot section
is exactly the loader slot — 512 B, the second-smallest step on the 8 KiB is exactly the 512-byte slot — the second-smallest step on the 8 KiB and
and 16 KiB chips (m8, m88, m16, m168, m164), the smallest on the 32 KiB 16 KiB chips (m8, m88, m16, m168, m164), the smallest on the 32 KiB ones
ones (m32, m328, m324); on the 1284s that step is the smallest, 512 words, (m32, m328, m324) — so the ATmega328P profiles below apply to every one of
which is why their slot is 1 KiB — so the ATmega328P profiles below apply them with its own addresses; the per-chip BOOTSZ ladders live in the host
to every one of them with its own addresses and slot size; the per-chip tool (`BOOT_FUSE`).
BOOTSZ ladders live in the host tool (`BOOT_FUSE`). The 1284s' numbers:
standalone = BOOTSZ 512 words (reset at the loader base 0x1fc00);
self-update = 1024 words, covering both 1 KiB slots, the loader-first
reset landing at 0x1f800 — the staging slot, walked across when erased.
The **644s** are the geometry's sweet spot: their smallest boot section The **644s and 1284s** are the geometry's sweet spot: their smallest boot
(512 words = 1 KiB) is exactly *two* 512-byte slots, so the resident and section (512 words = 1 KiB) is exactly *two* 512-byte slots, so the
its staging slot both live inside the minimum section — self-update needs resident and its staging slot both live inside the minimum section —
no fuse step up, and the standalone profile does not exist (reset lands at self-update needs no fuse step up, and the standalone profile does not
0xfc00, one erased slot below the loader: the loader-first walk built in). exist (reset lands one erased slot below the loader — 0xfc00 on the 644s,
0x1fc00 on the 1284s: the loader-first walk built in).
ATmega328P profiles (addresses for its 32 KiB): ATmega328P profiles (addresses for its 32 KiB):
@@ -344,11 +333,16 @@ the reflect-mode builds of libavr's spot set; `tools/make_presets.py`
regenerates the presets). Per chip preset, `ctest` runs: regenerates the presets). Per chip preset, `ctest` runs:
- `pureboot.size` — the 510-byte (tinies) / 512-byte (mega) budget; - `pureboot.size` — the 510-byte (tinies) / 512-byte (mega) budget;
- `pureboot_*.size` — the size matrix: the serial backends × the clock - `pureboot_*.size` — the size matrix: every configuration axis that could
ladder (1/8/16 MHz; the t13s' own RC menu), plus the USART1 build on the move the image, each variant against the same slot budget. The serial
x4 chips — every configuration axis that could move the image, each backends × the clock ladder (1/8/16 MHz; the t13s' own RC menu), the
variant against the same slot budget (pins are immediate operands and the USART1 instance across that same ladder on the x4 chips, and
timeout is a constant: size-neutral); `pureboot_sw_wide` — the slowest ladder rate at the fastest clock, where
a software UART's per-bit spin outgrows its one-register delay loop and
takes the 16-bit one, the largest image the configuration space produces
and a shape the ladder default (always the *fastest* rate a clock
reaches) never picks. Pins are immediate operands and the timeout is a
constant: neither is an axis;
- `pureboot.custom` (328P) — the configured-deployment acceptance test: the - `pureboot.custom` (328P) — the configured-deployment acceptance test: the
1 MHz software-serial TX=PB1/RX=PB5 build from the configuration example 1 MHz software-serial TX=PB1/RX=PB5 build from the configuration example
drives the full protocol suite through the runner's GPIO bridge, fixture drives the full protocol suite through the runner's GPIO bridge, fixture

View File

@@ -59,30 +59,26 @@ 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 slot of flash — 512 bytes, // Geometry: the resident loader owns the top 512 bytes of flash, and the
// except on the >64 KiB chips whose own smallest boot sector is 1 KiB (the // slot below it is where a staging copy goes. The word below the slot is
// 1284s): there the slot is 1 KiB, matching the hardware boundary the
// 512-byte figure comes from everywhere else. The word below the slot is
// the trampoline (the application's relocated reset vector) on chips // the trampoline (the application's relocated reset vector) on chips
// without a hardware boot section — the tinies and the m48s, whose SPM // without a hardware boot section — the tinies and the m48s, whose SPM
// runs from anywhere (Atmel-8271 §26). A boot section also means the CPU // runs from anywhere (Atmel-8271 §26). A boot section also means the CPU
// runs on while the RWW section programs; everywhere else it halts through // runs on while the RWW section programs; everywhere else it halts through
// the operation. // the operation.
constexpr std::uint16_t slot_bytes = spm::flash_bytes > 65536 ? 1024 : 512; constexpr std::uint16_t slot_bytes = 512;
constexpr std::uint32_t base = spm::flash_bytes - slot_bytes; 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 = avr::hw::curated::has_boot_section(); constexpr bool boot_section = avr::hw::curated::has_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). A slot spans the same // is a word address instead ('J' always was one). In those units the slot
// wire-high-byte pair in either unit (512 B = 2 x 256 bytes, 1 KiB = // is 256 words: one value of a wire address's high byte, where a
// 2 x 256 words), so the slot index is the high byte with its low bit // byte-addressed chip's 512 bytes span two.
// dropped everywhere.
constexpr bool word_flash = spm::flash_bytes > 65536; constexpr bool word_flash = spm::flash_bytes > 65536;
constexpr std::uint16_t wire_base = constexpr std::uint16_t wire_base =
word_flash ? static_cast<std::uint16_t>(base / 2) : static_cast<std::uint16_t>(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);
// The activation window, in seconds, is a compile-time constant (the build // The activation window, in seconds, is a compile-time constant (the build
// may override it): the whole EEPROM belongs to the application, and // may override it): the whole EEPROM belongs to the application, and
@@ -279,13 +275,20 @@ std::uint8_t rx_deadline()
return static_cast<std::uint16_t>(low | (link::rx() << 8)); return static_cast<std::uint16_t>(low | (link::rx() << 8));
} }
// The streamers take the count in the wire's 8-bit form: 0 means 256. // The wire's little-endian byte pair as the word it is: AVR is little-endian
// // too, so the pair already *is* the value's storage and the cast is the
// Two functions, because they want opposite placement and placement is an // identity that a shift-and-or spelling makes the compiler rediscover. Callers
// attribute: the byte-addressed loop is small enough to inline into both // read the bytes into named variables first — the wire order is a sequence of
// callers, the word-addressed one stays out of line but flattened — a call to // reads, never an argument order.
// the transmit inside it would strand the 24-bit cursor in callee-saved [[gnu::always_inline]] inline std::uint16_t word_of(std::array<std::uint8_t, 2> pair)
// registers. `word_flash` picks at the call site. {
return std::bit_cast<std::uint16_t>(pair);
}
// The streamers take the count in the wire's 8-bit form: 0 means 256. Two
// functions, one per addressing mode, both folded into the single command
// that streams flash — where the far one's 24-bit cursor is free to sit in
// the command loop's own call-saved registers.
[[maybe_unused, gnu::always_inline]] inline void send_flash_near(std::uint16_t address, std::uint8_t count) [[maybe_unused, gnu::always_inline]] inline void send_flash_near(std::uint16_t address, std::uint8_t count)
{ {
do do
@@ -296,7 +299,7 @@ std::uint8_t rx_deadline()
// The 24-bit cursor as the machine holds it: the RAMPZ byte and a 16-bit Z, // The 24-bit cursor as the machine holds it: the RAMPZ byte and a 16-bit Z,
// carried explicitly (the reassembled 32-bit address folds away inside the // carried explicitly (the reassembled 32-bit address folds away inside the
// inlined far load). // inlined far load).
[[maybe_unused, gnu::flatten, gnu::noinline]] void send_flash_far(std::uint16_t address, std::uint8_t count) [[maybe_unused, gnu::always_inline]] inline void send_flash_far(std::uint16_t address, std::uint8_t count)
{ {
std::uint8_t rampz = static_cast<std::uint8_t>(address >> 15); std::uint8_t rampz = static_cast<std::uint8_t>(address >> 15);
std::uint16_t z = static_cast<std::uint16_t>(address << 1); std::uint16_t z = static_cast<std::uint16_t>(address << 1);
@@ -317,6 +320,13 @@ std::uint8_t rx_deadline()
send_flash_near(address, count); send_flash_near(address, count);
} }
// The prompt, which is also every ack: out of line because three sites send
// it, and a call is shorter than each carrying its own load-immediate.
[[gnu::noinline]] void tx_ack()
{
link::tx(ack);
}
void send_eeprom(std::uint16_t address, std::uint8_t count) void send_eeprom(std::uint16_t address, std::uint8_t count)
{ {
do do
@@ -332,7 +342,7 @@ void store_eeprom(std::uint16_t address, std::uint8_t count)
{ {
do { do {
ee::write<off>(address++, link::rx()); ee::write<off>(address++, link::rx());
link::tx(ack); tx_ack();
} while (--count); } while (--count);
} }
@@ -355,34 +365,32 @@ void program_flash(std::uint16_t wire_address, std::uint8_t slot_high)
// itself walks the page (aligned, so the offset bits wrap to zero); on // itself walks the page (aligned, so the offset bits wrap to zero); on
// the word-addressed large chips the wire word address becomes a 32-bit // the word-addressed large chips the wire word address becomes a 32-bit
// byte cursor once, and their 256-byte page makes its low byte the whole // byte cursor once, and their 256-byte page makes its low byte the whole
// in-page offset. The slot index is one high byte of the wire address — // in-page offset. The slot index is the wire address's high byte — which
// two values on byte-addressed chips (the & ~1), bits 16:9 re-packed on // on byte-addressed chips means the byte address's, with its low bit
// the large ones. // dropped (a slot is two of those).
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) {
// Pages are aligned, so one page never crosses a 64 KiB boundary: // Pages are aligned, so one page never crosses a 64 KiB boundary:
// RAMPZ is a per-page constant and the fill cursor is a 16-bit Z // 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 // whose low byte is the whole in-page offset (256-byte pages).
// 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::uint8_t rampz = static_cast<std::uint8_t>(wire_address >> 15);
const std::uint16_t z0 = static_cast<std::uint16_t>(wire_address << 1); const std::uint16_t z0 = static_cast<std::uint16_t>(wire_address << 1);
std::uint16_t z = z0; 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>((static_cast<spm::flash_address_t>(rampz) << 16) | z, spm::fill<off>((static_cast<spm::flash_address_t>(rampz) << 16) | z, word_of({low, high}));
static_cast<std::uint16_t>(low | (high << 8)));
z += 2; z += 2;
} while (static_cast<std::uint8_t>(z)); } while (static_cast<std::uint8_t>(z));
address = (static_cast<spm::flash_address_t>(rampz) << 16) | z0; address = (static_cast<spm::flash_address_t>(rampz) << 16) | z0;
page_high = static_cast<std::uint8_t>(wire_address >> 8) & 0xfe; page_high = static_cast<std::uint8_t>(wire_address >> 8);
} else { } else {
address = static_cast<spm::flash_address_t>(wire_address); address = static_cast<spm::flash_address_t>(wire_address);
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>(address, word_of({low, high}));
address += 2; address += 2;
} while (static_cast<std::uint8_t>(address) & (page - 1)); } while (static_cast<std::uint8_t>(address) & (page - 1));
address -= 2; // back inside the page — erase and write ignore the word bits address -= 2; // back inside the page — erase and write ignore the word bits
@@ -414,7 +422,7 @@ void send_fuses()
std::uint8_t which = 0; std::uint8_t which = 0;
do do
link::tx(spm::read_fuse<off>(static_cast<spm::fuse>(which))); link::tx(spm::read_fuse<off>(static_cast<spm::fuse>(which)));
while (++which & 3); while (++which != 4);
} }
[[noreturn]] void run() [[noreturn]] void run()
@@ -437,7 +445,7 @@ void send_fuses()
// byte pick a hand assembler writes — `>> 8` leaves the swap materialized. // byte pick a hand assembler writes — `>> 8` leaves the swap materialized.
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 ra_high = static_cast<std::uint8_t>(std::byteswap(ra_words)); const std::uint8_t ra_high = static_cast<std::uint8_t>(std::byteswap(ra_words));
const std::uint8_t slot_high = word_flash ? ra_high & 0xfe : static_cast<std::uint8_t>(ra_high << 1); const std::uint8_t slot_high = word_flash ? ra_high : static_cast<std::uint8_t>(ra_high << 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.
@@ -448,40 +456,51 @@ void send_fuses()
// No prompt while an EEPROM write runs: a pending write blocks SPM // No prompt while an EEPROM write runs: a pending write blocks SPM
// and fuse reads (§26.2.1), and the ack tells the host all is done. // and fuse reads (§26.2.1), and the ack tells the host all is done.
ee::wait(); ee::wait();
link::tx(ack); tx_ack();
const std::uint8_t command = link::rx(); const std::uint8_t command = link::rx();
switch (command) { switch (command) {
case 'b': { // info block, read relative to the running slot
// The block sits in the image's first 256 bytes (the build lint
// asserts it), and slots are 512-aligned — so the low byte of its
// link address (in wire units: bytes, or words on the large
// chips) is its offset in any slot, and the high byte of its
// runtime address is the running slot's. Composed from the two
// bytes — the high half is runtime data, so no absolute address
// is ever materialized.
const auto link_low = reinterpret_cast<std::uint16_t>(info_data.storage.data());
const std::uint8_t low =
word_flash ? static_cast<std::uint8_t>(link_low >> 1) : static_cast<std::uint8_t>(link_low);
send_flash(static_cast<std::uint16_t>(low | (slot_high << 8)), static_cast<std::uint8_t>(info_data.size()));
break;
}
case 'J': { // jump to a wire word address: hand-over and staging transfer case 'J': { // jump to a wire word address: hand-over and staging transfer
auto target = reinterpret_cast<void (*)()>(rx16()); auto target = reinterpret_cast<void (*)()>(rx16());
link::tx(ack); tx_ack();
link::drain(); link::drain();
jump(target); jump(target);
} }
case 'b': // info block, read relative to the running slot
case 'R': // read flash: addr16, n8 (0 = 256) case 'R': // read flash: addr16, n8 (0 = 256)
case 'r': // read EEPROM: addr16, n8 case 'r': // read EEPROM: addr16, n8
case 'w': { // write EEPROM: addr16, n8, then n bytes each acked case 'w': { // write EEPROM: addr16, n8, then n bytes each acked
std::uint16_t address = rx16(); // Every streamed command through one address-and-count path: 'b'
std::uint8_t count = link::rx(); // is a flash read whose arguments the loader already knows, so it
if (command == 'R') // joins the wire-argument three here rather than streaming from a
send_flash(address, count); // call site of its own. That leaves the image with exactly one
else if (command == 'r') // flash streamer, and on the word-addressed chips it is what lets
// the streamer's 24-bit cursor live in this never-returning loop's
// own call-saved registers instead of being saved and restored
// around a call.
std::uint16_t address;
std::uint8_t count;
if (command == 'b') {
// The block sits in the image's first 256 bytes (the build
// lint asserts it), and slots are 512-aligned — so the low
// byte of its link address is its offset in any slot, halved
// where wire units are words. The high byte is the running
// slot's, runtime data, so no absolute address is ever
// materialized.
const auto link_byte =
static_cast<std::uint8_t>(reinterpret_cast<std::uint16_t>(info_data.storage.data()));
const std::uint8_t low = word_flash ? static_cast<std::uint8_t>(link_byte >> 1) : link_byte;
address = static_cast<std::uint16_t>(low | (slot_high << 8));
count = static_cast<std::uint8_t>(info_data.size());
} else {
address = rx16();
count = link::rx();
}
if (command == 'r')
send_eeprom(address, count); send_eeprom(address, count);
else else if (command == 'w')
store_eeprom(address, count); store_eeprom(address, count);
else
send_flash(address, count);
break; break;
} }
case 'W': // program one flash page: addr16, page bytes case 'W': // program one flash page: addr16, page bytes

View File

@@ -43,7 +43,7 @@ VERSION = 2 # this tool's own version — free to drift from a loader's
# becomes the new floor here. # becomes the new floor here.
OLDEST_LOADER = 1 OLDEST_LOADER = 1
NEWEST_LOADER = 2 NEWEST_LOADER = 2
SLOT = 512 # the loader slot on byte-addressed chips; word-addressed ones (>64 KiB) use 1 KiB — their own smallest boot sector SLOT = 512 # the loader slot, on every chip
RETRIES = 3 # rewrites of a page that reads back wrong, before the run stops RETRIES = 3 # rewrites of a page that reads back wrong, before the run stops
VERBOSE = False VERBOSE = False
@@ -336,9 +336,8 @@ 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.slot = 1024 if self.word_flash else SLOT self.flash_size = self.base + SLOT
self.flash_size = self.base + self.slot self.stage = self.base - SLOT # where a staging copy of the loader goes
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).
@@ -365,7 +364,7 @@ class Info:
f"flash {self.flash_size} B, {self.page} B pages" f"flash {self.flash_size} B, {self.page} B pages"
+ (", word-addressed wire" if self.word_flash else ""), + (", word-addressed wire" if self.word_flash else ""),
f"application 0x0000..{self.base - 1:#06x} ({self.base} B)", f"application 0x0000..{self.base - 1:#06x} ({self.base} B)",
f"loader {self.base:#06x} ({self.slot} B slot)", f"loader {self.base:#06x} ({SLOT} B slot)",
f"staging {self.stage:#06x}", f"staging {self.stage:#06x}",
f"EEPROM {self.eeprom_size} B", f"EEPROM {self.eeprom_size} B",
f"hand-over {hand_over}", f"hand-over {hand_over}",
@@ -681,13 +680,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."""
slot = info.slot budget = SLOT - 2 if info.patch_vector else SLOT
if len(image) > (slot - 2 if info.patch_vector else slot): if len(image) > budget:
raise Error(f"loader image is {len(image)} B, the slot holds {slot - 2 if info.patch_vector else slot}") raise Error(f"loader image is {len(image)} B, the slot holds {budget}")
content = bytearray(image) + bytearray([0xFF] * (slot - len(image))) 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)
@@ -713,7 +712,7 @@ 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 two slots ({2 * info.slot} B, BOOTSZ) is " f"— a boot section of at least two slots ({2 * SLOT} B, BOOTSZ) is "
f"required, and only an external programmer can change fuses" f"required, and only an external programmer can change fuses"
) )
if not bootrst: if not bootrst:
@@ -755,7 +754,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, info.slot).hex(), "staging": loader.read_flash(info.stage, 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:
@@ -835,7 +834,7 @@ def op_update_loader(loader, wait, path, state_path, fuse_bytes):
update = image_info(image) # the preflight proved it is there update = image_info(image) # the preflight proved it is there
verbose(f"installing pureboot {update.version} over pureboot {info.version}") verbose(f"installing pureboot {update.version} over pureboot {info.version}")
staged = staging_content(image, info) staged = staging_content(image, info)
resident = bytes(image) + bytes([0xFF] * (info.slot - len(image))) resident = bytes(image) + bytes([0xFF] * (SLOT - len(image)))
page = info.page page = info.page
state = UpdateState(state_path) state = UpdateState(state_path)
@@ -858,7 +857,7 @@ def op_update_loader(loader, wait, path, state_path, fuse_bytes):
# state file's snapshot) — a resumed, half-written install differs # state file's snapshot) — a resumed, half-written install differs
# from its snapshot and takes the install path below, which completes # from its snapshot and takes the install path below, which completes
# it page by page. # it page by page.
current = loader.read_flash(info.stage, info.slot) current = loader.read_flash(info.stage, SLOT)
staged_loader = image_info(current[:268]) staged_loader = image_info(current[:268])
if staged_loader is not None and staged_loader.raw == info.raw and current == state.staging: if staged_loader is not None and staged_loader.raw == info.raw and current == state.staging:
print("staging slot already holds a loader — left in place") print("staging slot already holds a loader — left in place")
@@ -867,7 +866,7 @@ def op_update_loader(loader, wait, path, state_path, fuse_bytes):
# tiny13s), its first page carries the reset vector: written last, # tiny13s), its first page carries the reset vector: written last,
# so any earlier interruption still resets into the old resident, # so any earlier interruption still resets into the old resident,
# and from then on resets enter the staging copy. # and from then on resets enter the staging copy.
order = list(range(0, info.slot, page)) order = list(range(0, 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, label="staging copy"): if write_differing(loader, info.stage, staged, order, label="staging copy"):
@@ -894,7 +893,7 @@ def op_update_loader(loader, wait, path, state_path, fuse_bytes):
if redirect: if redirect:
verbose("word 0 restored") verbose("word 0 restored")
write_differing(loader, 0, state.page0) write_differing(loader, 0, state.page0)
order = list(range(0, info.slot, page)) order = list(range(0, 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, label="staging restore") write_differing(loader, info.stage, state.staging, order, label="staging restore")

View File

@@ -66,9 +66,10 @@ struct link {
} }
[[noreturn]] static void idle() [[noreturn]] static void idle()
{ {
// 'L' hands back to the loader at the top slot — 512 bytes, or the // 'L' hands back to the loader in the top slot — 512 bytes on every
// 1 KiB the >64 KiB chips use. // chip. The jump takes a word address, which is what makes the
constexpr std::uint32_t slot = avr::hw::db.mem.flash_size > 65536 ? 1024 : 512; // >64 KiB chips' entry reachable through a 16-bit pointer at all.
constexpr std::uint32_t slot = 512;
for (;;) { for (;;) {
auto command = tx_t::read_blocking(); auto command = tx_t::read_blocking();
if (command == 'L') if (command == 'L')

View File

@@ -90,7 +90,7 @@ def main():
# The staging slot: erased flash with the loader sitting exactly where # The staging slot: erased flash with the loader sitting exactly where
# a staging copy would — the tool must leave it in place and let it # a staging copy would — the tool must leave it in place and let it
# stream the (different) update build into the resident slot. # stream the (different) update build into the resident slot.
stage = base - 512 stage = base - pb.SLOT
rehome_from(pbsim, pb, device_bin, elf, hex(stage), hex(stage), update_bin, base, page, baud, app_bin, workdir, rehome_from(pbsim, pb, device_bin, elf, hex(stage), hex(stage), update_bin, base, page, baud, app_bin, workdir,
mcu, hz) mcu, hz)
print("re-home from the staging slot: converged") print("re-home from the staging slot: converged")

View File

@@ -83,7 +83,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" * (info.slot - len(image)) resident = image + b"\xff" * (pb.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

@@ -57,7 +57,7 @@ def main():
# the page byte is the wire's 0-means-256. # the page byte is the wire's 0-means-256.
mega = mcu.startswith("atmega") mega = mcu.startswith("atmega")
patch = not mega or mcu.startswith("atmega48") patch = not mega or mcu.startswith("atmega48")
word_flash = base + 512 > 0x10000 word_flash = base + pb.SLOT > 0x10000
wire_base = base // 2 if word_flash else base wire_base = base // 2 if word_flash else base
flags = (1 if patch else 0) | (2 if word_flash else 0) flags = (1 if patch else 0) | (2 if word_flash else 0)
info = pb.Info( info = pb.Info(
@@ -127,7 +127,7 @@ def main():
# loader, the trampoline on the application's own entry (patched-vector # loader, the trampoline on the application's own entry (patched-vector
# chips only — a boot-sectioned mega's word 0 stays the application's). # chips only — a boot-sectioned mega's word 0 stays the application's).
if patch: if patch:
flash_words = (base + 512) // 2 flash_words = (base + pb.SLOT) // 2
app = open(app_bin, "rb").read() app = open(app_bin, "rb").read()
word0 = flash_true[0] | (flash_true[1] << 8) word0 = flash_true[0] | (flash_true[1] << 8)
if rjmp_decode(word0, 0, flash_words) != base // 2: if rjmp_decode(word0, 0, flash_words) != base // 2:

View File

@@ -50,7 +50,7 @@ def assumed_fuses(pb, image):
image's embedded signature.""" 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 >= 2 * info.slot), key=lambda b: ladder[b]) bits = min((b for b in ladder if ladder[b] * 2 >= 2 * pb.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)
@@ -93,16 +93,13 @@ def main():
# The m48s are megas without a boot section: patched vector, no fuse # The m48s are megas without a boot section: patched vector, no fuse
# preflight, and the same reset-to-0 the tinies get. # preflight, and the same reset-to-0 the tinies get.
patch = not mega or mcu.startswith("atmega48") patch = not mega or mcu.startswith("atmega48")
# Word-addressed (>64 KiB) chips use the 1 KiB slot; their loader base
# itself sits beyond the 16-bit byte space — the 644's base + slot only
# touches the 64 KiB boundary and stays byte-addressed.
slot = 1024 if base >= 0x10000 and mega else 512
reset_hex = "0" if mega else None # the boot-sectioned mega runs BOOTRST-unprogrammed here reset_hex = "0" if mega else None # the boot-sectioned 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__)))
import pbsim import pbsim
import pureboot as pb import pureboot as pb
slot = pb.SLOT
os.makedirs(workdir, exist_ok=True) os.makedirs(workdir, exist_ok=True)
objcopy = os.environ.get("PB_OBJCOPY", "avr-objcopy") objcopy = os.environ.get("PB_OBJCOPY", "avr-objcopy")
images = {} images = {}

View File

@@ -34,7 +34,8 @@ def info_of(pb, base, page, patch, flash, signature=(0x1E, 0x93, 0x0B), word_fla
raw = bytes((0x50, 0x42, pb.NEWEST_LOADER if version is None else version, raw = bytes((0x50, 0x42, pb.NEWEST_LOADER if version is None else version,
*signature, page & 0xFF, wire_base & 0xFF, wire_base >> 8, 0, 2, flags)) *signature, page & 0xFF, wire_base & 0xFF, wire_base >> 8, 0, 2, flags))
info = pb.Info(raw) info = pb.Info(raw)
assert info.flash_size == flash if info.flash_size != flash:
fail(f"info_of({base:#x}) decodes to {info.flash_size:#x} of flash, not {flash:#x}")
return info return info
@@ -94,9 +95,7 @@ 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:
# Word-addressed chips carry the 1 KiB slot (their smallest boot sector). chip = info_of(pb, flash - pb.SLOT, 128 if flash < 0x20000 else 0, False, flash,
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))
@@ -109,10 +108,12 @@ 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 and page ride the wire
# and its slot is 1 KiB. # scaled — a 17-bit base halved into the block's two bytes, a 256-byte page
big = info_of(pb, 0x1FC00, 0, False, 0x20000, signature=(0x1E, 0x97, 0x05), word_flash=True) # spelled 0 — and its slot is the same 512 bytes as everywhere else, so its
if big.page != 256 or big.base != 0x1FC00 or big.stage != 0x1F800 or big.slot != 1024: # staging slot lands inside the 1 KiB minimum boot section.
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}") 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
@@ -215,6 +216,15 @@ def main():
if pb.update_preflight(bytes((0xAA,)) * 8 + tiny.raw, tiny, None) != []: if pb.update_preflight(bytes((0xAA,)) * 8 + tiny.raw, tiny, None) != []:
fail("tiny preflight should pass without fuses") fail("tiny preflight should pass without fuses")
# The 1284s' smallest boot section (512 words) is exactly the resident
# slot plus its staging slot, so self-update is possible at the minimum
# BOOTSZ — no fuse step up, the 644's geometry. That holds only while a
# slot is 512 B: at 1 KiB the staging slot would fall outside the section
# and the preflight would refuse.
notes = pb.update_preflight(bytes((0xAA,)) * 8 + big.raw, big, fuses(0xFE))
if not any("staging slot" in n for n in notes):
fail(f"1284 minimum-BOOTSZ notes: {notes}")
# The walk-region refusal: BOOTRST aimed below the loader plus app data # The walk-region refusal: BOOTRST aimed below the loader plus app data
# in the walk span errors without --force; erased spans and unprogrammed # in the walk span errors without --force; erased spans and unprogrammed
# BOOTRST pass. # BOOTRST pass.