// TinySafeBoot on libavr — tier 2: C++ with compiler trickery. // // Same protocol, libavr surface and full feature set as the pure variant // (tsb_pure.cpp) — watchdog bail, one-wire, config-page timeout, password gate, // emergency erase, config/flash/EEPROM read-write — but the readable // 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 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 #include // SP / RAMEND for the crt-free boot entry using namespace avr::literals; namespace spm = avr::spm; namespace ee = avr::eeprom; using dev = avr::device<{.clock = 16_MHz}>; using serial_t = dev::uart0<{.baud = 115200_Bd, .max_baud_error = 3_pct, .half_duplex = true}>; inline constexpr serial_t serial{}; namespace tsb { constexpr auto off = avr::irq::guard_policy::unused; constexpr std::uint8_t confirm = '!'; constexpr std::uint8_t request = '?'; constexpr std::uint8_t knock = '@'; constexpr std::uint16_t page = spm::page_bytes; constexpr std::uint16_t boot_bytes = 1024; 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; 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', build_date & 0xFF, build_date >> 8, 0xF3, 0x1E, 0x95, 0x0F, page / 2, (app_end / 2) & 0xFF, (app_end / 2) >> 8, eeprom_end & 0xFF, eeprom_end >> 8, 0xAA, 0xAA, }; // clang-format on // 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 — // 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() { return serial.read_blocking(); } void tx(std::uint8_t byte) { serial.write(byte); } const std::uint8_t *flash_ptr(std::uint16_t addr) { return reinterpret_cast(addr); } // Stream g_cnt bytes to the host from flash (LPM) or EEPROM, memory chosen at // run time so the optimiser cannot split the loop into two clones. [[gnu::noinline, gnu::noclone]] void send(bool flash) { do { tx(flash ? avr::flash_load(flash_ptr(g_addr)) : ee::read(g_addr)); ++g_addr; } while (--g_cnt); } [[gnu::noinline]] bool request_confirm() { tx(request); return rx() == confirm; } // 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) { if (flash) { g_cnt = page / 2; do { std::uint8_t lo = rx(); std::uint8_t hi = rx(); 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, rx()); ++g_addr; } while (--g_cnt); } } [[noreturn]] void appjump() { spm::wait(); reinterpret_cast(0)(); __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 { erase1(g_addr); g_addr += page; } while (g_addr < app_end); rww(); } // '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::always_inline]] inline void read_mem(bool flash) { g_addr = 0; for (;;) { if (rx() != confirm) return; g_cnt = page; send(flash); if (flash && g_addr >= app_end) return; } } // 'F'/'E': flash erases the whole application first, then both take the pages // the host offers behind '?'. [[gnu::always_inline]] inline void write_mem(bool flash) { if (flash) erase_application(); g_addr = 0; while (request_confirm()) store_page(flash); } // 'C': replace the config page, then echo it back for the host to verify. [[gnu::always_inline]] inline void write_config() { if (!request_confirm()) return; g_addr = app_end; erase1(g_addr); store_page(true); rww(); g_addr = app_end; g_cnt = page; send(true); } // Emergency erase: wipe the application flash, the EEPROM and the config page. [[gnu::always_inline]] inline void emergency_erase() { erase_application(); g_addr = 0; do { ee::write(g_addr, 0xff); } while (++g_addr <= eeprom_end); erase1(app_end); rww(); } // The password gate. A byte of 0 requests emergency erase; a wrong byte hangs // the loader (still draining the line), so it can never fall through to erase. enum class gate : std::uint8_t { pass, emergency }; [[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); if (expected == 0xff) return gate::pass; std::uint8_t got = rx(); if (got == 0) return gate::emergency; if (got != expected) for (;;) rx(); } } [[noreturn]] void run() { if (avr::hw::mcusr::wdrf.test()) appjump(); // 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)); __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()) knocks = *byte == knock ? knocks + 1 : 0; else if (--idle == 0) appjump(); } switch (password_gate()) { case gate::pass: g_addr = reinterpret_cast(&info[0]); g_cnt = sizeof(info); send(true); break; case gate::emergency: if (!request_confirm() || !request_confirm()) appjump(); emergency_erase(); break; } for (;;) { tx(confirm); // Mainloop ready // Decode the command arithmetically so flash/write stay run-time values: // bit 5 is the case bit (upper = write), the folded-lower letter picks the // memory. A single unified path serves f/F/e/E. std::uint8_t cmd = rx(); std::uint8_t lower = cmd | 0x20; bool write = (cmd & 0x20) == 0; if (lower == 'f' || lower == 'e') { bool flash = lower == 'f'; if (write) write_mem(flash); else read_mem(flash); } else if (lower == 'c') { if (write) { write_config(); } else { g_addr = app_end; g_cnt = page; send(true); } } else { appjump(); } } } } // namespace tsb extern "C" [[gnu::naked, gnu::used, gnu::section(".vectors")]] void __boot_entry() { SP = RAMEND; tsb::run(); }