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:
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user