From fc5433fd7de8f5ee60557df5f784826d32053db1 Mon Sep 17 00:00:00 2001 From: BlackMark Date: Sun, 19 Jul 2026 18:47:38 +0200 Subject: [PATCH] tsb: drive each tier to its size floor asm 502->498 B (below the oracle's 500): the stack bring-up moves to plain C++, and a register is reserved for the config-page high byte instead of reloading it at each app-flash-boundary compare. tricks 808->778 B: shared erase/rww helpers plus the libavr half-duplex W1C fix. pure 950->896 B and no SRAM: streams rx->SPM/EEPROM instead of staging a 128 B page buffer. All three keep full oracle feature parity and stay byte-identical across modes; protocol tests (round-trip + password + emergency erase) green. Co-Authored-By: Claude Opus 4.8 (1M context) --- CMakeLists.txt | 17 ++++++++---- tsb/tsb_asm.cpp | 27 +++++++++---------- tsb/tsb_pure.cpp | 65 +++++++++++++++++++++------------------------- tsb/tsb_tricks.cpp | 30 ++++++++++++++------- 4 files changed, 74 insertions(+), 65 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7f6713a..d12e6dc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -47,11 +47,18 @@ endif() # size; the linker section-start and the source's boot_bytes agree. # All three implement the full oracle feature set (see oracle/README.md): # watchdog bail, one-wire half-duplex, config-page activation timeout, password -# gate, emergency erase, config/flash/EEPROM read-write. They differ only in how. -# tsb_asm — minimal inline asm, the headline: 502 B in the 512 B section, -# matching the hand-written oracle's size and features. -# tsb_tricks — compiler trickery, no asm: 808 B in the 1 KB section (BOOTSZ=10). -# tsb_pure — pure idiomatic libavr: 950 B in the 1 KB section. +# gate, emergency erase, config/flash/EEPROM read-write. They differ only in how, +# and the size gradient is the cost of that "how" — see dev/lessons.md. +# tsb_asm — dense inline asm for the loader core, libavr for every geometry/ +# baud/info constant and the stack bring-up: 498 B in the 512 B +# 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. # # add_tsb_variant( ) function(add_tsb_variant name bytes) diff --git a/tsb/tsb_asm.cpp b/tsb/tsb_asm.cpp index cc340be..7136d13 100644 --- a/tsb/tsb_asm.cpp +++ b/tsb/tsb_asm.cpp @@ -65,18 +65,16 @@ constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 19; } // namespace tsb // Reset lands here: BOOTRST vectors to the boot base, .vectors is laid first, and -// no crt runs. The whole loader is this one naked routine. +// no crt runs. The stack pointer is the one piece of bring-up that reads as +// plainly in C++ as in assembler, so it is; the dense loader body follows. extern "C" [[gnu::naked, gnu::used, gnu::section(".vectors")]] void __boot_entry() { + SP = RAMEND; asm volatile( // --- bring-up ------------------------------------------------------ - " ldi r16, lo8(%[ramend]) \n\t" - " out %[spl], r16 \n\t" - " ldi r16, hi8(%[ramend]) \n\t" - " out %[sph], r16 \n\t" " in r16, %[mcusr] \n\t" // watchdog reset → hand straight back " sbrc r16, 3 \n\t" // MCUSR bit 3 = WDRF - " rjmp 9f \n\t" // 9: = appjump + " rjmp 9f \n\t" // 9: = appjump (drains SPM, jumps to 0) " ldi r16, %[ubrr] \n\t" // fixed baud, UBRR0L only " sts %[ubrr0l], r16 \n\t" " ldi r16, 0x02 \n\t" // 1< info_data = { // clang-format on using info = avr::flash_table; -// One page staged in SRAM. Scratch that is always filled before it is read, so -// it lives in .noinit — no startup clear (there is no crt) and no .text bytes. -[[gnu::section(".noinit")]] std::uint8_t buffer[page]; - // Blocking byte read/write over the one-wire line: read() releases the line to // the receiver, write() takes it and holds it until the frame is out. std::uint8_t rx() @@ -96,13 +92,6 @@ void send_eeprom(std::uint16_t addr, std::uint16_t count) tx(ee::read(addr++)); } -// Take one page from the host into the SRAM buffer. -void get_page() -{ - for (std::uint16_t i = 0; i < page; ++i) - buffer[i] = rx(); -} - // Prompt the host with '?' and report whether it answered '!'. bool request_confirm() { @@ -110,29 +99,40 @@ bool request_confirm() return rx() == confirm; } -// Program the SRAM buffer into one already-erased flash page (low byte then -// high, as the SPM word buffer wants). -void write_flash_page(std::uint16_t addr) +// Stream one page from the host straight into the already-erased flash page at +// `addr`, filling the SPM word buffer low byte then high — no SRAM staging, so +// receiving and programming are the same loop. +void store_flash_page(std::uint16_t addr) { - spm::fill(addr, std::span{buffer, page}); + for (std::uint16_t i = 0; i < page; i += 2) { + std::uint8_t lo = rx(); + std::uint8_t hi = rx(); + spm::fill(addr + i, static_cast(lo | (hi << 8))); + } spm::write_page(addr); spm::wait(); } -// Write the SRAM buffer into EEPROM byte by byte. -void write_eeprom_page(std::uint16_t addr) +// Stream one page from the host straight into EEPROM, byte by byte. +void store_eeprom_page(std::uint16_t addr) { for (std::uint16_t i = 0; i < page; ++i) - ee::write(addr + i, buffer[i]); + ee::write(addr + i, rx()); +} + +// Erase one flash page and wait it out — the erase step shared by the whole-app +// erase, the config-page rewrite and the emergency wipe. +void erase_page(std::uint16_t addr) +{ + spm::erase_page(addr); + spm::wait(); } // Erase the whole application, one page at a time (unwritten pages stay erased). void erase_application() { - for (std::uint16_t a = 0; a < app_end; a += page) { - spm::erase_page(a); - spm::wait(); - } + for (std::uint16_t a = 0; a < app_end; a += page) + erase_page(a); spm::rww_enable(); } @@ -171,19 +171,15 @@ void read_eeprom() void write_flash() { erase_application(); - for (std::uint16_t a = 0; request_confirm(); a += page) { - get_page(); - write_flash_page(a); - } + for (std::uint16_t a = 0; request_confirm(); a += page) + store_flash_page(a); } // 'E': take pages the host offers behind '?' into EEPROM. void write_eeprom() { - for (std::uint16_t a = 0; request_confirm(); a += page) { - get_page(); - write_eeprom_page(a); - } + for (std::uint16_t a = 0; request_confirm(); a += page) + store_eeprom_page(a); } // 'C': replace the config page, then echo it back for the host to verify. @@ -191,10 +187,8 @@ void write_config() { if (!request_confirm()) return; - get_page(); - spm::erase_page(app_end); - spm::wait(); - write_flash_page(app_end); + erase_page(app_end); + store_flash_page(app_end); spm::rww_enable(); send_flash(app_end, page); } @@ -207,8 +201,7 @@ void emergency_erase() erase_application(); for (std::uint16_t a = 0; a <= eeprom_end; ++a) ee::write(a, 0xff); - spm::erase_page(app_end); - spm::wait(); + erase_page(app_end); spm::rww_enable(); } diff --git a/tsb/tsb_tricks.cpp b/tsb/tsb_tricks.cpp index 69ad059..f90692b 100644 --- a/tsb/tsb_tricks.cpp +++ b/tsb/tsb_tricks.cpp @@ -118,16 +118,30 @@ const std::uint8_t *flash_ptr(std::uint16_t addr) __builtin_unreachable(); } +// One flash page erased and waited out. Shared, so the SPM erase command +// sequence is emitted once for the whole-app erase, the config page and the +// emergency wipe rather than three times. +[[gnu::noinline]] void erase1(std::uint16_t addr) +{ + spm::erase_page(addr); + spm::wait(); +} + +// Re-enable RWW flash reads after programming, shared for the same reason. +[[gnu::noinline]] void rww() +{ + spm::rww_enable(); +} + // Erase the whole application, one page at a time. [[gnu::noinline]] void erase_application() { g_addr = 0; do { - spm::erase_page(g_addr); - spm::wait(); + erase1(g_addr); g_addr += page; } while (g_addr < app_end); - spm::rww_enable(); + rww(); } // 'f'/'e': stream memory back one page per host '!'. send advances g_addr, so @@ -165,10 +179,9 @@ void write_config() if (!request_confirm()) return; g_addr = app_end; - spm::erase_page(g_addr); - spm::wait(); + erase1(g_addr); store_page(true); - spm::rww_enable(); + rww(); g_addr = app_end; g_cnt = page; send(true); @@ -182,9 +195,8 @@ void write_config() do { ee::write(g_addr, 0xff); } while (++g_addr <= eeprom_end); - spm::erase_page(app_end); - spm::wait(); - spm::rww_enable(); + erase1(app_end); + rww(); } // The password gate. A byte of 0 requests emergency erase; a wrong byte hangs