// TinySafeBoot on libavr — tier 3: full feature parity in the 512-byte boot // section, in C++ except where the C ABI itself is the cost. // // 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 — at 510 bytes in the // 512-byte BOOTSZ=11 section the hand-written oracle occupies (500 B). This tier used to be one // monolithic inline-asm routine; it is now the tricks tier's C++ (same // register protocol, same structure — see tsb_tricks.cpp, including the // global-register miscompile rules) with exactly two routines kept in // assembly, the two whose remaining cost *is* the calling convention: // // rx the bounded receive: C++ must re-floor the timeout window on every // call (the global-register-store miscompile) and split it across // call-saved registers; the asm keeps the oracle's X-register nested // countdown. // store the page-store loop: C++ cannot hold the receive byte pair and the // walked Z pointer across the rx calls without call-saved staging // (push/pop + a Y→Z copy per word); the asm calls rx knowing exactly // which registers it touches and walks Z live across the whole page. // // Everything else — bring-up, activation, password gate, emergency erase, // dispatch, every SPM/EEPROM/flash primitive, every geometry/baud/info // constant — is C++ on libavr, and the two asm routines splice into the same // global-register protocol the C++ uses (g_addr in Y, g_cnt in r16, g_window // in r7, g_receiving in r6), so calls cross the boundary with no marshalling. // // The wire protocol is strict request/response, which is what makes the shared // line safe: the device drives it only between a received command and its // reply, and releases it (RXEN0 only) whenever it waits. #include #include // SP / RAMEND for the crt-free boot entry, SFR addresses for the asm routines using namespace avr::literals; namespace spm = avr::spm; namespace ee = avr::eeprom; namespace hw = avr::hw; namespace tsb { namespace { // The loader is purely polled — it never enables interrupts — so every SPM and // EEPROM lock folds to nothing under this posture. constexpr auto off = avr::irq::guard_policy::unused; constexpr std::uint8_t confirm = '!'; constexpr std::uint8_t request = '?'; constexpr std::uint8_t knock = '@'; // Boot geometry for the 512 B boot section (BOOTSZ=11); the page size and the // flash/EEPROM extents are the chip database's to know. app_end is the config // page (TSB's LASTPAGE), one page below the boot section. constexpr std::uint16_t page = spm::page_bytes; constexpr std::uint16_t boot_bytes = 512; 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; // Lockout-proof floor for the activation window (the oracle's F_CPU/1MHz). constexpr std::uint8_t act_min = 16; // Post-activation window: the host gets seconds, not milliseconds, mid-session. constexpr std::uint8_t comm_window = 200; constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 20; // Fixed 115200 8N1; the library solves UBRR + U2X from clock and baud. constexpr auto baud = avr::uart::detail::solve_baud(16_MHz, 115200_Bd); // The 16-byte device-info block, streamed out on activation. // 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 register std::uint16_t g_addr asm("r28"); register std::uint8_t g_cnt asm("r16"); register std::uint8_t g_window asm("r7"); register std::uint8_t g_receiving asm("r6"); const std::uint8_t *flash_ptr(std::uint16_t addr) { return reinterpret_cast(addr); } // Bounded byte receive (asm 1 of 2): release the one-wire line on a direction // change, poll RXC0 under the oracle's nested X-register countdown seeded from // g_window (floored against lockout), byte or 0-on-silence in r24. Z survives // — the property the store's word loop rides on. [[gnu::noinline, gnu::noclone]] std::uint8_t rx() { std::uint8_t byte; asm volatile(" tst %[dir] \n\t" // already receiving? keep the line released " brne 1f \n\t" " ldi %[b], 0x10 \n\t" // RXEN0 alone: release the line and listen " sts %[ucsr0b], %[b] \n\t" " ser %[b] \n\t" " mov %[dir], %[b] \n\t" "1: mov r27, %[to] \n\t" // outer countdown high byte = window " ori r27, %[actmin] \n\t" // lockout-proof floor " clr r26 \n\t" "2: ser %[b] \n\t" "3: lds %[b], %[ucsr0a] \n\t" " sbrc %[b], 7 \n\t" // RXC0 " rjmp 4f \n\t" " dec %[b] \n\t" " brne 3b \n\t" " sbiw r26, 1 \n\t" " brcc 2b \n\t" " clr %[b] \n\t" // silence → 0, which no compare accepts " rjmp 5f \n\t" "4: lds %[b], %[udr0] \n\t" "5: \n\t" : [b] "=&d"(byte), [dir] "+r"(g_receiving) : [to] "r"(g_window), [actmin] "M"(act_min), [ucsr0a] "n"(_SFR_MEM_ADDR(UCSR0A)), [ucsr0b] "n"(_SFR_MEM_ADDR(UCSR0B)), [udr0] "n"(_SFR_MEM_ADDR(UDR0)) : "r26", "r27", "cc"); return byte; } // One-wire transmit: take the line (TXEN0 alone) on a direction change with a // turn-around guard, put the byte out, hold the line until the whole frame is // out (TXC0, not UDRE0), W1C TXC0 by storing the sampled status back (keeps // U2X0). Plain C++ — it compiles *smaller* than the oracle's routine. [[gnu::noinline, gnu::noclone]] void tx(std::uint8_t byte) { if (g_receiving) { g_receiving = 0; hw::ucsr0b::write(hw::ucsr0b::txen0(1)); for (std::uint8_t guard = 46; guard; --guard) ; } hw::udr0::write(byte); std::uint8_t status; do { status = hw::ucsr0a::read(); } while (!(status & hw::ucsr0a::txc0(1).value)); hw::ucsr0a::write(status); } // '?', then hand back the host's reply for the callers' one-byte compare. [[gnu::noinline, gnu::noclone]] std::uint8_t rcnf() { tx(request); return rx(); } // One flash byte ← [g_addr++] (the advance right before ret — the // global-register rule, see tsb_tricks.cpp). [[gnu::noinline, gnu::noclone]] std::uint8_t sflash() { std::uint8_t byte = avr::flash_load(flash_ptr(g_addr)); ++g_addr; return byte; } // One EEPROM byte ← [g_addr++]. [[gnu::noinline, gnu::noclone]] std::uint8_t eerd() { std::uint8_t byte = ee::read(g_addr); ++g_addr; return byte; } // One EEPROM byte → [g_addr++]. [[gnu::noinline, gnu::noclone]] void eewr(std::uint8_t byte) { ee::write(g_addr, byte); ++g_addr; } // Stream g_cnt flash bytes from g_addr to the host. [[gnu::noinline, gnu::noclone]] void sendf() { do { tx(sflash()); } while (--g_cnt); } // Wait out a running SPM op, then re-open the RWW section — after every page // op and before handing over, as the oracle does. [[gnu::noinline, gnu::noclone]] void settle() { spm::wait(); spm::rww_enable(); } extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --defsym=tsb_app=0 [[noreturn]] void appjump() { settle(); tsb_app(); } // Step g_addr one page down and erase that page (the decrement lives here — // the global-register rule). [[gnu::noinline, gnu::noclone]] void erase_below() { g_addr -= page; spm::erase_page(g_addr); settle(); } // Erase the whole application, top-down like the oracle: the loop bound is a // compare with zero, and g_addr = 0 is handed back for free. [[gnu::noinline, gnu::noclone]] void erase_application() { g_addr = app_end; do { erase_below(); } while (g_addr != 0); } // Stream one host page into the erased flash page at g_addr (asm 2 of 2): the // word pair stages in r0:r1 straight from rx (whose register set is known — // the cross-call liveness C++ cannot express), Z walks the page and PGWRT // programs it. g_addr is left at the next page base. [[gnu::noinline, gnu::noclone]] void store_flash() { asm volatile(" movw r30, r28 \n\t" // Z = page base; rx leaves Z live " ldi r20, %[words] \n\t" "1: rcall %x[rx] \n\t" " mov r0, r24 \n\t" // word low byte " rcall %x[rx] \n\t" " mov r1, r24 \n\t" // word high byte " ldi r24, 0x01 \n\t" // SPMEN: buffer the word at Z " out %[spmcsr], r24 \n\t" " spm \n\t" " clr r1 \n\t" " adiw r30, 2 \n\t" " dec r20 \n\t" " brne 1b \n\t" " movw %[base], r30 \n\t" // g_addr = the next page base " subi r30, %[pagelo] \n\t" // Z back to this page's base " sbci r31, %[pagehi] \n\t" " ldi r24, 0x05 \n\t" // PGWRT | SPMEN: program the page " out %[spmcsr], r24 \n\t" " spm \n\t" : [base] "+r"(g_addr) : [rx] "i"(&rx), [spmcsr] "I"(_SFR_IO_ADDR(SPMCSR)), [words] "M"(page / 2), [pagelo] "M"(page & 0xff), [pagehi] "M"(page >> 8) : "r0", "r1", "r20", "r24", "r26", "r27", "r30", "r31", "cc", "memory"); settle(); } [[noreturn, gnu::noinline]] void run() { // A watchdog reset hands straight back to the application, as the // reference loader does, rather than re-entering the bootloader. if (hw::mcusr::wdrf.test()) appjump(); // Lean bring-up from reset state: UCSR0C already reads 8N1, UBRR0H reads // 0, and rx()/tx() raise RXEN0/TXEN0 on first use — only the divisor low // byte and U2X0 need a store. The library still does the datasheet work. static_assert(baud.u2x && baud.ubrr < 256, "lean bring-up writes UBRR0L only, with U2X0"); hw::reg<"UBRR0">::write(static_cast(baud.ubrr)); hw::ucsr0a::write(hw::ucsr0a::u2x0(1)); // General-purpose registers are undefined at power-on (no crt zeroes them); // the direction latch must start "not receiving" so the first rx() enables // the receiver. The reference loader clears its shadow register for the // same reason. g_receiving = 0; // Activation: 3×'@', each inside the config page's timeout window (rx // floors it so a corrupt page cannot lock the loader out); anything else — // including silence — hands over. g_window = avr::flash_load(flash_ptr(app_end + 2)); for (std::uint8_t k = 3; k; --k) if (rx() != knock) appjump(); g_window = comm_window; // Password gate (config page from app_end+3, 0xff-terminated; a blank // page is no password). A wrong byte blanks the comparison and drains the // line forever, so a wrong password can never fall through; a 0 requests // emergency erase behind two confirms. On pass the info block goes out; // the emergency path skips it and drops into the command loop. g_addr = app_end + 3; std::uint8_t mask = 0xff; for (;;) { std::uint8_t expected = avr::flash_load(flash_ptr(g_addr)) & mask; ++g_addr; if (expected == 0xff) { g_addr = reinterpret_cast(&info[0]); g_cnt = sizeof(info); sendf(); break; } std::uint8_t got = rx(); if (got == 0) { if (mask == 0) continue; if (rcnf() != confirm || rcnf() != confirm) appjump(); erase_application(); // leaves g_addr = 0 for the EEPROM walk do { eewr(0xff); } while (g_addr <= eeprom_end); g_addr = app_end + page; erase_below(); break; } if (got != expected) mask = 0; } for (;;) { tx(confirm); // Mainloop ready g_addr = 0; switch (rx()) { case 'f': // read application flash, one page per host '!' for (;;) { if (rx() != confirm) break; g_cnt = page; sendf(); if (g_addr >= app_end) break; } break; case 'F': // erase the application, then take pages behind '?' erase_application(); // leaves g_addr = 0, the write start while (rcnf() == confirm) store_flash(); break; case 'e': // read EEPROM, one page per host '!', until the host stops for (;;) { if (rx() != confirm) break; g_cnt = page; do { tx(eerd()); } while (--g_cnt); } break; case 'E': // take EEPROM pages behind '?' while (rcnf() == confirm) { g_cnt = page; do { eewr(rx()); } while (--g_cnt); } break; case 'c': // read the config page read_config: g_addr = app_end; g_cnt = page; sendf(); break; case 'C': // replace the config page, then echo it back to verify if (rcnf() != confirm) break; g_addr = app_end + page; erase_below(); // leaves g_addr = app_end, the store target store_flash(); goto read_config; default: // 'q' or any other byte runs the application appjump(); } } } } // namespace } // namespace tsb // Reset lands here: BOOTRST vectors to the boot section base and .vectors is // laid first, so this is the first instruction executed. No crt ran, so set // the stack pointer before anything is called. 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(); }