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:
@@ -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<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
|
||||
// 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));
|
||||
}
|
||||
|
||||
// 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<std::uint8_t, 2> pair)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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<std::uint8_t>(address >> 15);
|
||||
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);
|
||||
}
|
||||
|
||||
// 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<off>(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<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 {
|
||||
std::uint8_t low = link::rx();
|
||||
std::uint8_t high = link::rx();
|
||||
spm::fill<off>((static_cast<spm::flash_address_t>(rampz) << 16) | z,
|
||||
static_cast<std::uint16_t>(low | (high << 8)));
|
||||
spm::fill<off>((static_cast<spm::flash_address_t>(rampz) << 16) | z, word_of({low, high}));
|
||||
z += 2;
|
||||
} while (static_cast<std::uint8_t>(z));
|
||||
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 {
|
||||
address = static_cast<spm::flash_address_t>(wire_address);
|
||||
do {
|
||||
std::uint8_t low = 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;
|
||||
} while (static_cast<std::uint8_t>(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<off>(static_cast<spm::fuse>(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<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 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
|
||||
// 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<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
|
||||
auto target = reinterpret_cast<void (*)()>(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<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);
|
||||
else
|
||||
else if (command == 'w')
|
||||
store_eeprom(address, count);
|
||||
else
|
||||
send_flash(address, count);
|
||||
break;
|
||||
}
|
||||
case 'W': // program one flash page: addr16, page bytes
|
||||
|
||||
Reference in New Issue
Block a user