// TinySafeBoot on libavr — tier 3: full feature parity in ≤512 B. // // The complete TinySafeBoot feature set — watchdog-reset bail, one-wire // half-duplex UART, a config-page activation timeout, the password gate, // emergency erase, and config/flash/EEPROM read-write — reimplemented for the // 512-byte ATmega328P boot section. Matching the hand-written assembly oracle's // size and features at once is only reachable at assembly density, so the loader // body is one cohesive inline-asm routine. libavr still does the datasheet work: // every geometry, baud and info-block constant below is computed by the library, // never hand-entered, and the loader references them as assembler immediates. // // The wire protocol is strict request/response, which makes the one-wire // turn-around safe: the device owns the line whenever it drives a byte and // releases it (RX-only) whenever it waits for one. #include #include // __SPM_ENABLE and the SPM page-op bit names #include // SFR addresses / bit numbers for the boot entry using namespace avr::literals; namespace spm = avr::spm; namespace tsb { // Boot geometry — the chip database's to know, not ours. constexpr std::uint16_t page = spm::page_bytes; // 128 constexpr std::uint16_t boot_bytes = 512; // BOOTSZ=11 constexpr std::uint16_t app_end = spm::flash_bytes - boot_bytes - page; // config page base constexpr std::uint16_t eeprom_end = avr::hw::db.mem.eeprom_size - 1; // Fixed 115200 8N1; the library solves UBRR + U2X from clock and baud. constexpr auto baud = avr::uart::detail::solve_baud(16_MHz, 115200_Bd); static_assert(baud.u2x && baud.ubrr < 256, "asm bring-up writes UBRR0L only, with U2X0"); // Activation window: the config page's timeout byte, floored so a corrupt page // can never lock the loader out (at least the clock rate in MHz → ~0.5 s here). constexpr std::uint8_t act_min = 16; // Post-activation communication timeout (~several seconds); the loader bails to // the application if the host falls silent mid-session. constexpr std::uint8_t comm_timeout = 200; constexpr std::uint8_t confirm = '!'; constexpr std::uint8_t request = '?'; constexpr std::uint8_t knock = '@'; constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 19; // The 16-byte device-info block, LPM-read on activation. A plain progmem array: // the loader streams it straight out with LPM, so a flash_table wrapper would // add nothing here. // clang-format off [[gnu::progmem]] constexpr std::uint8_t info[16] = { 'T', 'S', 'B', build_date & 0xFF, build_date >> 8, 0xF3, // status: native-UART fixed-baud lineage 0x1E, 0x95, 0x0F, // ATmega328P signature page / 2, // page size in words (app_end / 2) & 0xFF, (app_end / 2) >> 8, eeprom_end & 0xFF, eeprom_end >> 8, 0xAA, 0xAA, }; // clang-format on } // namespace tsb // Reset lands here: BOOTRST vectors to the boot base, .vectors is laid first, and // 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 ------------------------------------------------------ " 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 (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<