tsb: beat the first-pass size floors (tricks 666, pure 842)

tricks 778->666: always_inline every single-call handler into the
[[noreturn]] reset entry (which pays no prologue, so their push/pop of
call-saved registers vanishes), walk the page pointer in Y (adiw, base
recovered as g_addr-page) instead of recomputing Z=base+offset, bring
the UART up in the two registers that are not already at their reset
value, and seed the activation counter as __uint24.

pure 896->842: TU-local internal linkage (proper hygiene, and it lets
the compiler inline the one-call handlers), a byte-wide activation
count, __uint24 timeout. Still one readable function per command.

asm unchanged at 498: its C++-expressible parts are already C++; the
core stays asm (the 666 B all-tricks tier is 168 B over — per-call ABI
tax, not a feature). All three cross-mode byte-identical, protocol green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-19 20:15:49 +02:00
parent fc5433fd7d
commit 2b705ad130
3 changed files with 55 additions and 31 deletions

View File

@@ -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<std::uint32_t>(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

View File

@@ -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 <libavr/libavr.hpp>
@@ -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<off>(g_addr + g_cnt, static_cast<std::uint16_t>(lo | (hi << 8)));
g_cnt += 2;
} while (g_cnt != page);
spm::write_page<off>(g_addr);
spm::fill<off>(g_addr, static_cast<std::uint16_t>(lo | (hi << 8)));
g_addr += 2;
} while (--g_cnt);
spm::write_page<off>(g_addr - page);
spm::wait();
} else {
g_cnt = page;
do {
ee::write<off>(g_addr + g_cnt, rx());
} while (++g_cnt != page);
ee::write<off>(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<serial_t>();
// 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<std::uint8_t>(baud.ubrr));
avr::hw::ucsr0a::write(avr::hw::ucsr0a::u2x0(1));
std::uint32_t idle = static_cast<std::uint32_t>(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())