diff --git a/CMakeLists.txt b/CMakeLists.txt index 57e4948..e286e04 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -231,12 +231,13 @@ if(PROJECT_IS_TOP_LEVEL) endif() # The size matrix: every configuration axis that could move the image - # size — the serial backend (different code), the clock and its ladder - # baud (different constants and divisor shapes), the USART instance - # (different register class) — each combination must still fit the - # chip's slot budget. Pins are size-neutral (port and bit are immediate - # operands) and the timeout is a constant, so neither adds an axis. The - # stock build is one point of this matrix and already has its test. + # size — the serial backend (different code), the USART instance + # (different registers), the clock (different constants), and the baud + # through the two shapes its bit timing takes — each combination must + # still fit the chip's slot budget. Pins are size-neutral (port and bit + # are immediate operands) and the timeout is a constant, so neither adds + # an axis. The stock build is one point of this matrix and already has + # its test. function(pureboot_size_variant name) pureboot_add_loader(${name} ${ARGN}) 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) pureboot_size_variant(pureboot_hw_${_matrix_khz}k CLOCK ${_matrix_hz} SERIAL hardware) 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() if(PUREBOOT_HAS_USART1) pureboot_size_variant(pureboot_usart1 USART 1) 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 # 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 diff --git a/pureboot/CMakeLists.txt b/pureboot/CMakeLists.txt index 104d8d7..6216c7c 100644 --- a/pureboot/CMakeLists.txt +++ b/pureboot/CMakeLists.txt @@ -17,11 +17,11 @@ # software-UART cycle-floor static asserts re-check whatever is passed. # 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 -# the word-addressed 1284s), and the deployment defaults (crystal assumption -# on the megas, calibrated RC on the tinies). The USART flags mirror the -# hardware inventory the loader's own static asserts check (the plain 644 is -# the x4 family's one single-USART die, Atmel-2593). +# modulo needs, plus the deployment defaults (crystal assumption on the +# megas, calibrated RC on the tinies). The loader slot is 512 bytes on every +# chip. The USART flags mirror the hardware inventory the loader's own static +# asserts check (the plain 644 is the x4 family's one single-USART die, +# Atmel-2593). set(_pb_has_usart 1) set(_pb_has_usart1 0) if(LIBAVR_MCU MATCHES "^attiny13a?$") @@ -106,31 +106,25 @@ elseif(LIBAVR_MCU MATCHES "^atmega644(a|p|pa)?$") elseif(LIBAVR_MCU MATCHES "^atmega1284p?$") # 128 KiB: wire flash addresses are word addresses, reads go through # 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 README.md). + # Its smallest boot section (512 words = 1 KiB) holds the loader and its + # staging slot together, the 644's geometry (see README.md). set(_pb_flash 131072) set(_pb_wrap "") set(_pb_page 256) set(_pb_hz 16000000) set(_pb_eeprom 4096) - set(_pb_slot 1024) - set(_pb_limit 1024) set(_pb_has_usart1 1) else() message(FATAL_ERROR "pureboot: no geometry for ${LIBAVR_MCU}") endif() -if(NOT DEFINED _pb_slot) - set(_pb_slot 512) -endif() +set(_pb_slot 512) math(EXPR _pb_base "${_pb_flash} - ${_pb_slot}") math(EXPR _pb_base_hex "${_pb_base}" OUTPUT_FORMAT HEXADECIMAL) # 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. if(LIBAVR_MCU MATCHES "^atmega" AND NOT LIBAVR_MCU MATCHES "^atmega48") set(_pb_app 0) - if(NOT DEFINED _pb_limit) - set(_pb_limit ${_pb_slot}) - endif() + set(_pb_limit ${_pb_slot}) else() math(EXPR _pb_app "${_pb_base} - 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) target_link_libraries(${name} PRIVATE libavr) target_compile_definitions(${name} PRIVATE ${_defines}) - # Codegen shaping for the loader TU only, worth ~40 B on every chip and - # what carries the far-flash 1284 build under 512. At -Os GCC otherwise - # rewrites the byte-stream loops' counters into end-pointer forms that - # cost registers (-fno-ivopts, -fno-split-wide-types), leaves register - # pressure on the table with the default allocator - # (-fira-algorithm=priority), and spends bytes on rewrites a - # straight-line loader gains nothing from. + # Codegen shaping for the loader TU only, worth 14–36 B depending on the + # chip. At -Os GCC otherwise rewrites the byte-stream loops' counters into + # end-pointer forms that cost registers (-fno-ivopts, + # -fno-split-wide-types), leaves register pressure on the table with the + # default allocator (-fira-algorithm=priority), and keeps loop-invariant + # immediates and expression temporaries in registers + # (-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 - -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} -Wl,--defsym=pureboot_app=${_app} ${_wrap}) add_custom_command(TARGET ${name} POST_BUILD COMMAND ${CMAKE_SIZE} $) diff --git a/pureboot/README.md b/pureboot/README.md index bb5f4e9..3c0ee9e 100644 --- a/pureboot/README.md +++ b/pureboot/README.md @@ -3,26 +3,19 @@ A serial bootloader on [libavr](https://git.blackmark.me/avr/libavr), pure by constraint: one C++ source, no inline assembly, no global register variables (attributes and compiler flags allowed), built for **every chip libavr -targets — all 37 — in 512 bytes each**: 434 B on the tiny13s, 438–442 B on -the tiny25/45/85, 412–452 B across the megas, and 506 B on the +targets — all 37 — in 512 bytes each**: 416 B on the tiny13s, 420–424 B on +the tiny25/45/85, 396–438 B across the megas, and 478 B on the ATmega1284/1284P, whose far-flash machinery (ELPM reads, RAMPZ page commands, -word-addressed wire) is the heaviest. Those are the stock deployments; -choosing the software UART where the chip has a USART costs 8–46 B more (a -bit-bang against a peripheral), which every chip still absorbs inside its -slot — on the 1284s that means their 1 KiB boot sector, where the -software-serial image lands at 546 B. Bringing the 1284's default build -under 512 at all is what the loop-placement attributes on the byte streamers -(`pureboot.cpp`) and the codegen flags on the loader TU (`CMakeLists.txt`) -are for; measured against each chip's own budget the tightest is the -ATmega328P, 50 B spare. Clock, baud, serial backend and -pins are per-build configuration (below); the size matrix in the test suite -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. +word-addressed wire) is the heaviest. Those are the stock deployments; the +rest of the configuration space costs a little more, and the dearest point +is a software UART at a slow baud — 494 B on the 1284s, the tightest image +in the matrix at 18 B spare. Keeping the far-flash build inside 512 is what +the single call site the streamed commands share (`pureboot.cpp`) and the +codegen flags on the loader TU (`CMakeLists.txt`) are for. Clock, baud, +serial backend and pins are per-build configuration (below); the size matrix +in the test suite 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 image is **position-independent**: control flow is PC-relative, 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 slot with every command intact — which makes pureboot **its own staging 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 -(1 KiB on the word-addressed large chips, matching their boot-sector -minimum); on the tinies the budget is 510, not 512: a slot's last word +jumps into it, and lets it rewrite the resident. The slot is 512 bytes on +every chip; on the tinies the budget is 510, not 512: a slot's last word belongs to the host-managed trampoline (below). ## 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, 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 -is exactly the loader slot — 512 B, the second-smallest step on the 8 KiB -and 16 KiB chips (m8, m88, m16, m168, m164), the smallest on the 32 KiB -ones (m32, m328, m324); on the 1284s that step is the smallest, 512 words, -which is why their slot is 1 KiB — so the ATmega328P profiles below apply -to every one of them with its own addresses and slot size; the per-chip -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. +is exactly the 512-byte slot — the second-smallest step on the 8 KiB and +16 KiB chips (m8, m88, m16, m168, m164), the smallest on the 32 KiB ones +(m32, m328, m324) — so the ATmega328P profiles below apply to every one of +them with its own addresses; the per-chip BOOTSZ ladders live in the host +tool (`BOOT_FUSE`). -The **644s** are the geometry's sweet spot: their smallest boot section -(512 words = 1 KiB) is exactly *two* 512-byte slots, so the resident and -its staging slot both live inside the minimum section — self-update needs -no fuse step up, and the standalone profile does not exist (reset lands at -0xfc00, one erased slot below the loader: the loader-first walk built in). +The **644s and 1284s** are the geometry's sweet spot: their smallest boot +section (512 words = 1 KiB) is exactly *two* 512-byte slots, so the +resident and its staging slot both live inside the minimum section — +self-update needs no fuse step up, and the standalone profile does not +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): @@ -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: - `pureboot.size` — the 510-byte (tinies) / 512-byte (mega) budget; -- `pureboot_*.size` — the size matrix: the serial backends × the clock - ladder (1/8/16 MHz; the t13s' own RC menu), plus the USART1 build on the - x4 chips — every configuration axis that could move the image, each - variant against the same slot budget (pins are immediate operands and the - timeout is a constant: size-neutral); +- `pureboot_*.size` — the size matrix: every configuration axis that could + move the image, each variant against the same slot budget. The serial + backends × the clock ladder (1/8/16 MHz; the t13s' own RC menu), the + USART1 instance across that same ladder on the x4 chips, and + `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 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 diff --git a/pureboot/pureboot.cpp b/pureboot/pureboot.cpp index f573b51..faaac00 100644 --- a/pureboot/pureboot.cpp +++ b/pureboot/pureboot.cpp @@ -59,30 +59,26 @@ consteval std::int16_t wdrf_field() return avr::hw::db.field_index(reg, "WDRF"); } -// Geometry: the resident loader owns the top slot of flash — 512 bytes, -// except on the >64 KiB chips whose own smallest boot sector is 1 KiB (the -// 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 +// Geometry: the resident loader owns the top 512 bytes of flash, and the +// slot below it is where a staging copy goes. The word below the slot is // the trampoline (the application's relocated reset vector) on chips // 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 on while the RWW section programs; everywhere else it halts through // 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::uint16_t page = spm::page_bytes; 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 // 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 -// 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. +// is a word address instead ('J' always was one). In those units the slot +// is 256 words: one value of a wire address's high byte, where a +// byte-addressed chip's 512 bytes span two. constexpr bool word_flash = spm::flash_bytes > 65536; constexpr std::uint16_t wire_base = word_flash ? static_cast(base / 2) : static_cast(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 // may override it): the whole EEPROM belongs to the application, and @@ -279,13 +275,20 @@ std::uint8_t rx_deadline() return static_cast(low | (link::rx() << 8)); } -// The streamers take the count in the wire's 8-bit form: 0 means 256. -// -// Two functions, because they want opposite placement and placement is an -// attribute: the byte-addressed loop is small enough to inline into both -// callers, the word-addressed one stays out of line but flattened — a call to -// the transmit inside it would strand the 24-bit cursor in callee-saved -// registers. `word_flash` picks at the call site. +// 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 +// identity that a shift-and-or spelling makes the compiler rediscover. Callers +// read the bytes into named variables first — the wire order is a sequence of +// reads, never an argument order. +[[gnu::always_inline]] inline std::uint16_t word_of(std::array pair) +{ + return std::bit_cast(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) { 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, // carried explicitly (the reassembled 32-bit address folds away inside the // 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(address >> 15); std::uint16_t z = static_cast(address << 1); @@ -317,6 +320,13 @@ std::uint8_t rx_deadline() 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) { do @@ -332,7 +342,7 @@ void store_eeprom(std::uint16_t address, std::uint8_t count) { do { ee::write(address++, link::rx()); - link::tx(ack); + tx_ack(); } 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 // 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 - // in-page offset. The slot index is one high byte of the wire address — - // two values on byte-addressed chips (the & ~1), bits 16:9 re-packed on - // the large ones. + // in-page offset. The slot index is the wire address's high byte — which + // on byte-addressed chips means the byte address's, with its low bit + // dropped (a slot is two of those). spm::flash_address_t address; std::uint8_t page_high; if constexpr (word_flash) { // 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 - // whose low byte is the whole in-page offset (256-byte pages). The - // slot index is simply the wire word address's high byte. + // whose low byte is the whole in-page offset (256-byte pages). const std::uint8_t rampz = static_cast(wire_address >> 15); const std::uint16_t z0 = static_cast(wire_address << 1); std::uint16_t z = z0; do { std::uint8_t low = link::rx(); std::uint8_t high = link::rx(); - spm::fill((static_cast(rampz) << 16) | z, - static_cast(low | (high << 8))); + spm::fill((static_cast(rampz) << 16) | z, word_of({low, high})); z += 2; } while (static_cast(z)); address = (static_cast(rampz) << 16) | z0; - page_high = static_cast(wire_address >> 8) & 0xfe; + page_high = static_cast(wire_address >> 8); } else { address = static_cast(wire_address); do { std::uint8_t low = link::rx(); std::uint8_t high = link::rx(); - spm::fill(address, static_cast(low | (high << 8))); + spm::fill(address, word_of({low, high})); address += 2; } while (static_cast(address) & (page - 1)); 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; do link::tx(spm::read_fuse(static_cast(which))); - while (++which & 3); + while (++which != 4); } [[noreturn]] void run() @@ -437,7 +445,7 @@ void send_fuses() // byte pick a hand assembler writes — `>> 8` leaves the swap materialized. const std::uint16_t ra_words = reinterpret_cast(__builtin_return_address(0)); const std::uint8_t ra_high = static_cast(std::byteswap(ra_words)); - const std::uint8_t slot_high = word_flash ? ra_high & 0xfe : static_cast(ra_high << 1); + const std::uint8_t slot_high = word_flash ? ra_high : static_cast(ra_high << 1); // 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. @@ -448,40 +456,51 @@ void send_fuses() // 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. ee::wait(); - link::tx(ack); + tx_ack(); const std::uint8_t command = link::rx(); 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(info_data.storage.data()); - const std::uint8_t low = - word_flash ? static_cast(link_low >> 1) : static_cast(link_low); - send_flash(static_cast(low | (slot_high << 8)), static_cast(info_data.size())); - break; - } case 'J': { // jump to a wire word address: hand-over and staging transfer auto target = reinterpret_cast(rx16()); - link::tx(ack); + tx_ack(); link::drain(); jump(target); } + case 'b': // info block, read relative to the running slot case 'R': // read flash: addr16, n8 (0 = 256) case 'r': // read EEPROM: addr16, n8 case 'w': { // write EEPROM: addr16, n8, then n bytes each acked - std::uint16_t address = rx16(); - std::uint8_t count = link::rx(); - if (command == 'R') - send_flash(address, count); - else if (command == 'r') + // Every streamed command through one address-and-count path: 'b' + // is a flash read whose arguments the loader already knows, so it + // joins the wire-argument three here rather than streaming from a + // call site of its own. That leaves the image with exactly one + // 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(reinterpret_cast(info_data.storage.data())); + const std::uint8_t low = word_flash ? static_cast(link_byte >> 1) : link_byte; + address = static_cast(low | (slot_high << 8)); + count = static_cast(info_data.size()); + } else { + address = rx16(); + count = link::rx(); + } + if (command == 'r') send_eeprom(address, count); - else + else if (command == 'w') store_eeprom(address, count); + else + send_flash(address, count); break; } case 'W': // program one flash page: addr16, page bytes diff --git a/pureboot/pureboot.py b/pureboot/pureboot.py index 0a35a46..cf96d93 100644 --- a/pureboot/pureboot.py +++ b/pureboot/pureboot.py @@ -43,7 +43,7 @@ VERSION = 2 # this tool's own version — free to drift from a loader's # becomes the new floor here. OLDEST_LOADER = 1 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 VERBOSE = False @@ -336,9 +336,8 @@ class Info: scale = 2 if self.word_flash else 1 self.base = (raw[7] | (raw[8] << 8)) * scale self.eeprom_size = raw[9] | (raw[10] << 8) - self.slot = 1024 if self.word_flash else SLOT - self.flash_size = self.base + self.slot - self.stage = self.base - self.slot # where a staging copy of the loader goes + self.flash_size = self.base + SLOT + self.stage = self.base - SLOT # where a staging copy of the loader goes # The hand-over target, as the word address 'J' takes: the trampoline # below the loader (tinies), or word 0 (mega — the application's own # 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" + (", word-addressed wire" if self.word_flash else ""), 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"EEPROM {self.eeprom_size} B", 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 to the resident base. The staging copy's fall-through and 'J'-free exit both land in a loader instead of garbage.""" - slot = info.slot - if len(image) > (slot - 2 if info.patch_vector else slot): - 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))) + budget = SLOT - 2 if info.patch_vector else SLOT + if len(image) > budget: + raise Error(f"loader image is {len(image)} B, the slot holds {budget}") + content = bytearray(image) + bytearray([0xFF] * (SLOT - len(image))) if info.patch_vector: 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) @@ -713,7 +712,7 @@ def update_preflight(image, info, fuse_bytes): raise Error( f"cannot self-update: the staging slot {info.stage:#06x} lies below the " 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" ) if not bootrst: @@ -755,7 +754,7 @@ class UpdateState: self.data = { "signature": info.signature.hex(), "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 "", } 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 verbose(f"installing pureboot {update.version} over pureboot {info.version}") 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 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 # from its snapshot and takes the install path below, which completes # 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]) 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") @@ -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, # so any earlier interruption still resets into the old resident, # 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: order = order[1:] + [0] 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: verbose("word 0 restored") write_differing(loader, 0, state.page0) - order = list(range(0, info.slot, page)) + order = list(range(0, SLOT, page)) if info.stage == 0: order = [0] + order[1:] write_differing(loader, info.stage, state.staging, order, label="staging restore") diff --git a/test/pbapp.cpp b/test/pbapp.cpp index 02b4ddf..a501faa 100644 --- a/test/pbapp.cpp +++ b/test/pbapp.cpp @@ -66,9 +66,10 @@ struct link { } [[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; + // 'L' hands back to the loader in the top slot — 512 bytes on every + // chip. The jump takes a word address, which is what makes the + // >64 KiB chips' entry reachable through a 16-bit pointer at all. + constexpr std::uint32_t slot = 512; for (;;) { auto command = tx_t::read_blocking(); if (command == 'L') diff --git a/test/pbrehome.py b/test/pbrehome.py index 56f1a32..ee0f7d7 100644 --- a/test/pbrehome.py +++ b/test/pbrehome.py @@ -90,7 +90,7 @@ def main(): # 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 # 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, mcu, hz) print("re-home from the staging slot: converged") diff --git a/test/pbreloc.py b/test/pbreloc.py index 89e85b8..3357526 100644 --- a/test/pbreloc.py +++ b/test/pbreloc.py @@ -83,7 +83,7 @@ def main(): # Restore the resident image through the staged copy, then 'J' back # 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) back_info = loader.enter_copy(base, 25) if back_info.raw != resident_info: diff --git a/test/pbtest.py b/test/pbtest.py index 9a77888..5768ea1 100644 --- a/test/pbtest.py +++ b/test/pbtest.py @@ -57,7 +57,7 @@ def main(): # the page byte is the wire's 0-means-256. mega = mcu.startswith("atmega") 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 flags = (1 if patch else 0) | (2 if word_flash else 0) info = pb.Info( @@ -127,7 +127,7 @@ def main(): # loader, the trampoline on the application's own entry (patched-vector # chips only — a boot-sectioned mega's word 0 stays the application's). if patch: - flash_words = (base + 512) // 2 + flash_words = (base + pb.SLOT) // 2 app = open(app_bin, "rb").read() word0 = flash_true[0] | (flash_true[1] << 8) if rjmp_decode(word0, 0, flash_words) != base // 2: diff --git a/test/pbupdate.py b/test/pbupdate.py index a0cd7c4..1753ea5 100644 --- a/test/pbupdate.py +++ b/test/pbupdate.py @@ -50,7 +50,7 @@ def assumed_fuses(pb, image): image's embedded signature.""" info = pb.image_info(image) which, ladder = pb.BOOT_FUSE[bytes(info.signature[1:3])] - bits = min((b for b in ladder if ladder[b] * 2 >= 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[which] = 0xF8 | (bits << 1) | 1 return bytes(fuses) @@ -93,16 +93,13 @@ def main(): # The m48s are megas without a boot section: patched vector, no fuse # preflight, and the same reset-to-0 the tinies get. 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 sys.path.insert(0, os.path.dirname(os.path.abspath(tool))) sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import pbsim import pureboot as pb + slot = pb.SLOT os.makedirs(workdir, exist_ok=True) objcopy = os.environ.get("PB_OBJCOPY", "avr-objcopy") images = {} diff --git a/test/test_planner.py b/test/test_planner.py index 1087f5a..2bd0a6f 100644 --- a/test/test_planner.py +++ b/test/test_planner.py @@ -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, *signature, page & 0xFF, wire_base & 0xFF, wire_base >> 8, 0, 2, flags)) 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 @@ -94,9 +95,7 @@ def main(): ((0x1E, 0x97, 0x05), 0x20000, 3, {0b11: 0x1FC00, 0b10: 0x1F800, 0b01: 0x1F000, 0b00: 0x1E000}), # 1284P ) for signature, flash, which, ladder in cases: - # 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, + chip = info_of(pb, flash - pb.SLOT, 128 if flash < 0x20000 else 0, False, flash, signature=signature, word_flash=flash > 0x10000) for bits, start in ladder.items(): fuses = bytearray((0xFF, 0xFF, 0xFF, 0xFF)) @@ -109,10 +108,12 @@ def main(): if prog or at != start: fail(f"mega_boot {signature[1]:02x}{signature[2]:02b} unprogrammed: {prog} {at:#07x}") - # Word-addressed info decode: the 1284P's base/page ride the wire scaled, - # and its slot is 1 KiB. - 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: + # Word-addressed info decode: the 1284P's base and page ride the wire + # scaled — a 17-bit base halved into the block's two bytes, a 256-byte page + # spelled 0 — and its slot is the same 512 bytes as everywhere else, so its + # 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}") # 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) != []: 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 # in the walk span errors without --force; erased spans and unprogrammed # BOOTRST pass.