tsb: third size pass — restructure to the oracle's shape

The second pass concluded the 168 B tricks->asm gap was per-call ABI
cost. Most of it was structure. Rebuilt around the oracle's own shape —
argless noinline primitives over a whole-loader call-saved register
protocol (g_addr in Y, count r16, window r7, direction latch r6), a
top-down erase_below whose loop tests against zero and hands callers
g_addr = 0 for free, bounded rx everywhere (a silent host unwinds to
the app from any state, as the oracle does), and a named tsb_app entry
that --pmem-wrap-around=32k relaxes to the wrapped rjmp:

  tsb_asm    510 B in the 512 B section (oracle: 500), C++ except rx
             and the page-store loop — the two routines whose remaining
             cost is the calling convention itself (~30 asm lines, was
             ~280)
  tsb_tricks 526 B, no assembly at all (was 666)
  tsb_pure   836 B, still one readable function per command (was 842)

Every g_* update placement works around a GCC 16.1 wrong-code bug
(stores into global register variables deleted when only callees read
them — repro and rules in libavr dev/lessons.md). Also fixes two
latent hardware bugs all earlier tiers carried, masked by simavr's
zeroed register file: the crt-less entries never established
__zero_reg__ = 0, and the direction latch was read before written —
power-on registers are undefined.

All tiers full oracle feature parity, protocol tests green in both
libavr modes, .text byte-identical across modes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 01:00:27 +02:00
parent 2b705ad130
commit 74d8b92885
5 changed files with 659 additions and 552 deletions

View File

@@ -45,7 +45,7 @@ constexpr std::uint16_t app_end = spm::flash_bytes - boot_bytes - page;
constexpr std::uint16_t eeprom_end = avr::hw::db.mem.eeprom_size - 1;
// Firmware version stamp: YY*512 + MM*32 + DD, the encoding the host decodes.
constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 19;
constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 20;
// The 16-byte device-info block the host reads on activation. A flash_table
// keeps it in progmem with no .data image (there is no crt to copy one).
@@ -129,21 +129,27 @@ void erase_page(std::uint16_t addr)
spm::wait();
}
// Erase the whole application, one page at a time (unwritten pages stay erased).
// Erase the whole application, one page at a time, top-down as the reference
// loader does (unwritten pages stay erased and the host cannot observe the
// order; the loop bound becomes a compare with zero).
void erase_application()
{
for (std::uint16_t a = 0; a < app_end; a += page)
for (std::uint16_t a = app_end; a != 0;) {
a -= page;
erase_page(a);
}
spm::rww_enable<off>();
}
// Run the application: reset vector at 0x0000. Any non-command byte, a wrong
// password, or an idle programmer port lands here.
// The application's reset vector; the linker pins it to 0x0000 (--defsym).
extern "C" [[noreturn]] void tsb_app();
// Run the application. Any non-command byte, a wrong password, or an idle
// programmer port lands here.
[[noreturn]] void appjump()
{
spm::wait(); // make sure any pending SPM finished before handing over
reinterpret_cast<void (*)()>(0)();
__builtin_unreachable();
tsb_app();
}
// 'f': stream the application flash back, one page per host '!'. Self-terminates
@@ -295,5 +301,8 @@ gate password_gate()
extern "C" [[gnu::naked, gnu::used, gnu::section(".vectors")]] void __boot_entry()
{
SP = RAMEND;
// The one line of crt this loader needs: compiled code assumes
// __zero_reg__ (r1) is 0, and power-on registers are undefined.
asm volatile("clr __zero_reg__");
tsb::run();
}