diff --git a/CMakeLists.txt b/CMakeLists.txt index d12e6dc..dfb7382 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,11 +54,15 @@ endif() # section, beating the hand-written oracle (500 B) at its own # feature set. The core is asm because C++ cannot reach it under # 512 (the tricks tier is the proof). -# tsb_tricks — compiler trickery, no asm (global-register page walk, forced -# flash/EEPROM path sharing, streaming stores, arithmetic command -# decode): 778 B in the 1 KB section (BOOTSZ=10). -# tsb_pure — pure idiomatic libavr, one function per command, streaming (no -# SRAM page buffer): 896 B in the 1 KB section. +# tsb_tricks — compiler trickery, no asm: the hot page pointer is walked in Y +# (adiw), flash/EEPROM paths share one runtime-flag body, every +# single-call handler is always_inline'd into the no-prologue +# reset entry, pages stream straight to SPM/EEPROM, and the +# bring-up is the two reset-non-default registers only. 666 B in +# the 1 KB section (BOOTSZ=10). +# tsb_pure — pure idiomatic libavr, one function per command, TU-local +# (internal linkage), streaming (no SRAM page buffer): 842 B in +# the 1 KB section. # # add_tsb_variant( ) function(add_tsb_variant name bytes) diff --git a/tsb/tsb_pure.cpp b/tsb/tsb_pure.cpp index 5afd4b8..88d5d35 100644 --- a/tsb/tsb_pure.cpp +++ b/tsb/tsb_pure.cpp @@ -24,6 +24,7 @@ using serial_t = dev::uart0<{.baud = 115200_Bd, .max_baud_error = 3_pct, .half_d inline constexpr serial_t serial{}; namespace tsb { +namespace { // The loader is purely polled — it never enables interrupts — so every SPM and // EEPROM lock folds to nothing under this posture. @@ -80,13 +81,13 @@ const std::uint8_t *flash_ptr(std::uint16_t addr) } // Stream `count` bytes to the host, from flash (LPM) or from EEPROM. -void send_flash(std::uint16_t addr, std::uint16_t count) +void send_flash(std::uint16_t addr, std::uint8_t count) { while (count--) tx(avr::flash_load(flash_ptr(addr++))); } -void send_eeprom(std::uint16_t addr, std::uint16_t count) +void send_eeprom(std::uint16_t addr, std::uint8_t count) { while (count--) tx(ee::read(addr++)); @@ -238,7 +239,7 @@ gate password_gate() // Activation: the host knocks three '@' inside a window whose length is the // config page's timeout byte (floored so a corrupt page can never lock the // loader out). An idle port times out and boots the application. - std::uint32_t idle = static_cast(avr::flash_load(flash_ptr(app_end + 2)) | 16) << 16; + __uint24 idle = static_cast<__uint24>(avr::flash_load(flash_ptr(app_end + 2)) | 16) << 16; std::uint8_t knocks = 0; while (knocks < 3) { if (auto byte = serial.read()) @@ -285,6 +286,7 @@ gate password_gate() } } +} // namespace } // namespace tsb // Reset lands here: BOOTRST vectors to the boot section base and .vectors is diff --git a/tsb/tsb_tricks.cpp b/tsb/tsb_tricks.cpp index f90692b..21ede3c 100644 --- a/tsb/tsb_tricks.cpp +++ b/tsb/tsb_tricks.cpp @@ -6,10 +6,14 @@ // one-handler-per-command shape is traded for size. Flash and EEPROM share a // single code path selected by a *runtime* flag decoded from the command byte, // so the compiler cannot constant-propagate it into two clones; attributes -// (noinline/noclone) pin that sharing down; the hot page address and byte -// counter live in call-saved global registers to erase the prologue push/pop -// that C++ function decomposition otherwise pays; and pages stream straight to -// SPM/EEPROM with no SRAM staging. No inline assembly. +// (noinline/noclone) pin that genuinely-shared sharing down, while every handler +// that is called only once is `always_inline`d into run() — which, being +// [[noreturn]] and entered from the naked reset vector, pays no prologue/epilogue, +// so their push/pop of call-saved registers vanishes. The hot page address lives +// in the Y register (call-saved and adiw-able) so it is walked, not respilled; +// pages stream straight to SPM/EEPROM with no SRAM staging; and the bring-up +// writes only the two registers that are not already at their reset value. No +// inline assembly. #include @@ -38,6 +42,9 @@ constexpr std::uint16_t eeprom_end = avr::hw::db.mem.eeprom_size - 1; constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 19; +// Fixed 115200 8N1; the library solves UBRR + U2X from clock and baud. +constexpr auto baud = avr::uart::detail::solve_baud(16_MHz, 115200_Bd); + // clang-format off [[gnu::progmem]] constexpr std::uint8_t info[16] = { 'T', 'S', 'B', @@ -54,8 +61,11 @@ constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 19; // The hot page walk lives in call-saved global registers, TSB-style: g_addr is // the running flash/EEPROM byte address, g_cnt the byte countdown. Being global // they are never spilled around the rx/tx/spm calls the way a local would be — -// r4-r7 are call-saved, so the library's UART and SPM helpers preserve them. -register std::uint16_t g_addr asm("r4"); +// the library's UART and SPM helpers preserve the call-saved registers. g_addr +// takes Y (r28:r29): call-saved like any low pair, but also adiw-able and +// directly immediate-arithmetic-able, so advancing it is one instruction where a +// low pair would need a copy into a high register first. +register std::uint16_t g_addr asm("r28"); register std::uint8_t g_cnt asm("r6"); std::uint8_t rx() @@ -92,22 +102,26 @@ const std::uint8_t *flash_ptr(std::uint16_t addr) // Stream one page straight from the host into the already-erased flash page at // g_addr (SPM word buffer, low byte then high) or into EEPROM — no SRAM staging, // so receive and store are one loop. The memory is a run-time flag. +// g_addr walks the page (adiw on Y) and is left at the next page base, so the +// caller advances nothing. write_page needs the base, recovered as g_addr-page. [[gnu::noinline, gnu::noclone]] void store_page(bool flash) { - g_cnt = 0; if (flash) { + g_cnt = page / 2; do { std::uint8_t lo = rx(); std::uint8_t hi = rx(); - spm::fill(g_addr + g_cnt, static_cast(lo | (hi << 8))); - g_cnt += 2; - } while (g_cnt != page); - spm::write_page(g_addr); + spm::fill(g_addr, static_cast(lo | (hi << 8))); + g_addr += 2; + } while (--g_cnt); + spm::write_page(g_addr - page); spm::wait(); } else { + g_cnt = page; do { - ee::write(g_addr + g_cnt, rx()); - } while (++g_cnt != page); + ee::write(g_addr, rx()); + ++g_addr; + } while (--g_cnt); } } @@ -147,7 +161,7 @@ const std::uint8_t *flash_ptr(std::uint16_t addr) // 'f'/'e': stream memory back one page per host '!'. send advances g_addr, so // flash self-terminates at the application boundary; EEPROM runs until the host // stops. -[[gnu::noinline]] void read_mem(bool flash) +[[gnu::always_inline]] inline void read_mem(bool flash) { g_addr = 0; for (;;) { @@ -162,19 +176,17 @@ const std::uint8_t *flash_ptr(std::uint16_t addr) // 'F'/'E': flash erases the whole application first, then both take the pages // the host offers behind '?'. -[[gnu::noinline]] void write_mem(bool flash) +[[gnu::always_inline]] inline void write_mem(bool flash) { if (flash) erase_application(); g_addr = 0; - while (request_confirm()) { + while (request_confirm()) store_page(flash); - g_addr += page; - } } // 'C': replace the config page, then echo it back for the host to verify. -void write_config() +[[gnu::always_inline]] inline void write_config() { if (!request_confirm()) return; @@ -188,7 +200,7 @@ void write_config() } // Emergency erase: wipe the application flash, the EEPROM and the config page. -[[gnu::noinline]] void emergency_erase() +[[gnu::always_inline]] inline void emergency_erase() { erase_application(); g_addr = 0; @@ -203,7 +215,7 @@ void write_config() // the loader (still draining the line), so it can never fall through to erase. enum class gate : std::uint8_t { pass, emergency }; -[[gnu::noinline]] gate password_gate() +[[gnu::always_inline]] inline gate password_gate() { for (const std::uint8_t *pw = flash_ptr(app_end + 3);; ++pw) { std::uint8_t expected = avr::flash_load(pw); @@ -223,9 +235,15 @@ enum class gate : std::uint8_t { pass, emergency }; if (avr::hw::mcusr::wdrf.test()) appjump(); - avr::init(); + // Lean bring-up: at reset UCSR0C already reads 8N1 and UBRR0H reads 0, and + // read()/write() enable RXEN0/TXEN0 on first use, so only the baud divisor low + // byte (it fits 8 bits — see the static_assert) and U2X0 must be written. The + // library surface still does the datasheet work. + static_assert(baud.u2x && baud.ubrr < 256, "lean bring-up writes UBRR0L only, with U2X0"); + avr::hw::reg<"UBRR0">::write(static_cast(baud.ubrr)); + avr::hw::ucsr0a::write(avr::hw::ucsr0a::u2x0(1)); - std::uint32_t idle = static_cast(avr::flash_load(flash_ptr(app_end + 2)) | 16) << 16; + __uint24 idle = static_cast<__uint24>(avr::flash_load(flash_ptr(app_end + 2)) | 16) << 16; std::uint8_t knocks = 0; while (knocks < 3) { if (auto byte = serial.read())