// pureboot — a serial bootloader on libavr, pure by constraint: one C++ // source with no inline assembly and no global register variables, built for // every chip libavr targets, 512 bytes on each. The device speaks primitives // — read/program flash, read/write EEPROM, fuse bytes, an info block, run — // and everything composite (verify, erase, reset-vector surgery on the // tinies, timeout configuration) lives in the host tool. Protocol reference: // README.md next to this file. // // Entry: reset lands in avr::startup::entry below (BOOTRST on the mega; the // patched reset vector — or erased flash walking up into the loader — on the // tinies). A watchdog reset hands straight to the application. Otherwise the // host has one activation window — EEPROM's last cell, in seconds — to knock // ("pb"); an idle line boots the application. A session then stays in the // command loop until 'G' hands over or the chip resets. #include using namespace avr::literals; namespace spm = avr::spm; namespace ee = avr::eeprom; namespace pureboot { namespace { // Purely polled — interrupts stay off, every guard folds to nothing. constexpr auto off = avr::irq::guard_policy::unused; constexpr std::uint8_t ack = '+'; // Per-chip personality, from the chip database: the clocks the dogfood // boards run (16 MHz crystal on the mega, calibrated RC on the tinies) and // the device signature (compile-time data — the tiny13A cannot even read its // signature row from code). consteval avr::hertz_t clock() { if (avr::hw::db.name == "ATtiny13A") return 9.6_MHz; if (avr::hw::db.name == "ATtiny85") return 8_MHz; return 16_MHz; } consteval std::array signature() { if (avr::hw::db.name == "ATtiny13A") return {0x1e, 0x90, 0x07}; if (avr::hw::db.name == "ATtiny85") return {0x1e, 0x93, 0x0b}; return {0x1e, 0x95, 0x0f}; } using dev = avr::device<{.clock = clock()}>; // Geometry: the loader owns the top 512 bytes of flash; the byte below it is // the trampoline word (the application's relocated reset vector) on chips // without a hardware boot section. The RWWSRE bit marks a separate boot // section — on classic AVR the two capabilities coincide. constexpr std::uint16_t boot_bytes = 512; constexpr std::uint16_t base = static_cast(spm::flash_bytes - boot_bytes); constexpr std::uint16_t page = spm::page_bytes; constexpr bool boot_section = avr::hw::db.field_index("SPMCSR", "RWWSRE") >= 0; // The activation timeout lives in EEPROM's last cell, in seconds; the host // rewrites it with the ordinary EEPROM-write command. An unprogrammed cell — // 0x00 or the erased 0xff — means the 4 s default: a stray value can never // floor the window to nothing and lock the loader out, and erasing the whole // EEPROM resets the timeout instead of maxing it to 255 s. constexpr std::uint16_t timeout_cell = avr::hw::db.mem.eeprom_size - 1; constexpr std::uint8_t default_seconds = 4; // The 12-byte info block the host reads with the 'b' command; flash-resident // (there is no crt to copy a .data image). inline constexpr std::array info_data = { 'P', 'B', 1, // magic, protocol version signature()[0], signature()[1], signature()[2], static_cast(page), base & 0xff, base >> 8, // app flash ends here; loader base avr::hw::db.mem.eeprom_size & 0xff, avr::hw::db.mem.eeprom_size >> 8, boot_section ? 0 : 1, // bit 0: host must patch the reset vector (no hardware boot section) }; using info = avr::flash_table; // The serial link: the hardware USART where the chip has one, the polled // software UART (no vector — the table belongs to the application) on PB0/PB1 // elsewhere. Both are class templates on the clock so only the selected // backend is ever instantiated. pending() is the cheap line test the // activation window polls; rx() then picks the byte up. template consteval std::int16_t rxc_field() { return avr::hw::db.field_index("UCSR0A", "RXC0"); } template struct hardware_link { using uart = avr::uart::usart0; static void init() { avr::init(); } static bool pending() { return avr::hw::field_impl()>::test(); } static std::uint8_t rx() { return uart::read_blocking(); } static void tx(std::uint8_t byte) { uart::write(byte); } }; template struct software_link { using rx_t = avr::uart::software_rx_polled; using tx_t = avr::uart::software_tx; static void init() { avr::init(); } static bool pending() { return !avr::io::input::read(); // a start bit has begun } static std::uint8_t rx() { return rx_t::template read_blocking(); } static void tx(std::uint8_t byte) { tx_t::template write(byte); } }; using link = std::conditional_t, software_link>; // The application's entry: the linker pins pureboot_app to 0x0000 on the // mega (reset re-vectors here through BOOTRST, so address 0 stays the // application's own vector) and to the trampoline word at base - 2 on the // tinies (--defsym in CMakeLists.txt). extern "C" [[noreturn]] void pureboot_app(); [[noreturn]] void run_app() { pureboot_app(); } // One activation tick is 65536 pending() polls — a pin (or flag) test plus a // 16-bit countdown, about 8 cycles. Whole-second precision is all the // timeout cell promises; the seconds count stays a loop bound (a runtime // multiply would drag libgcc's __mulhi3 into the MUL-less tinies). consteval std::uint16_t ticks_per_second() { return static_cast(dev::clock.hz / (65536ull * 8u)); } static_assert(ticks_per_second() >= 1); bool pending_before(std::uint8_t seconds) { do { std::uint16_t ticks = ticks_per_second(); do { std::uint16_t spins = 0; // wraps first, so 65536 polls per tick do { if (link::pending()) return true; } while (--spins); } while (--ticks); } while (--seconds); return false; } // A knock byte under the activation deadline: an idle line means no host is // there, and the application runs. std::uint8_t rx_deadline(std::uint8_t seconds) { if (!pending_before(seconds)) run_app(); return link::rx(); } std::uint16_t rx16() { std::uint8_t low = link::rx(); return static_cast(low | (link::rx() << 8)); } const std::uint8_t *flash_ptr(std::uint16_t address) { return reinterpret_cast(address); } // The streamers take the count in the wire's 8-bit form: 0 means 256. void send_flash(std::uint16_t address, std::uint8_t count) { do link::tx(avr::flash_load(flash_ptr(address++))); while (--count); } void send_eeprom(std::uint16_t address, std::uint8_t count) { do link::tx(ee::read(address++)); while (--count); } // EEPROM write, host-paced: each ack goes out once the byte's write has // begun, so the next byte arrives while it completes and the following // write's own ready-wait sees an idle line. Nothing is ever missed, on // either serial backend, without a buffer. void store_eeprom(std::uint16_t address, std::uint8_t count) { do { ee::write(address++, link::rx()); link::tx(ack); } while (--count); } // One flash page: stream the bytes into the SPM buffer as little-endian // words, then erase and program. Addresses in the loader's own 512 bytes // are drained but never programmed — a broken host cannot brick the chip. // On the mega the RWW section is re-enabled so reads work immediately. void program_flash(std::uint16_t address) { for (std::uint16_t i = 0; i < page; i += 2) { std::uint8_t low = link::rx(); std::uint8_t high = link::rx(); spm::fill(address + i, static_cast(low | (high << 8))); } if (address < base) { spm::erase_page(address); spm::wait(); spm::write_page(address); spm::wait(); if constexpr (boot_section) spm::rww_enable(); } } // The four fuse/lock bytes in the hardware's own Z order: low, lock, // extended, high. Writing fuses is not a thing self-programming can do on // AVR — SPM reaches flash (and boot lock bits) only. void send_fuses() { for (std::uint8_t which = 0; which < 4; ++which) link::tx(spm::read_fuse(static_cast(which))); } [[noreturn]] void run() { // A watchdog reset belongs to the application (whose watchdog stays // forced on until it clears WDRF) — no activation window in its way. if (avr::hw::mcusr::wdrf.test()) run_app(); link::init(); std::uint8_t seconds = ee::read(timeout_cell); if (seconds == 0 || seconds == 0xff) seconds = default_seconds; // The knock: 'p' then 'b', each under a fresh window; any other byte is // line noise and waits again. Falling out of a window runs the app. while (rx_deadline(seconds) != 'p' || rx_deadline(seconds) != 'b') { } for (;;) { // No prompt while an EEPROM write runs: a pending write blocks SPM // and fuse reads (§26.2.1), and the ack tells the host all is done. ee::wait(); link::tx(ack); switch (link::rx()) { case 'b': // info block send_flash(reinterpret_cast(info::storage.data()), info::size()); break; case 'R': { // read flash: addr16, n8 (0 = 256) std::uint16_t address = rx16(); send_flash(address, link::rx()); break; } case 'W': // program one flash page: addr16, page bytes program_flash(rx16()); break; case 'r': { // read EEPROM: addr16, n8 std::uint16_t address = rx16(); send_eeprom(address, link::rx()); break; } case 'w': { // write EEPROM: addr16, n8, then n bytes each acked std::uint16_t address = rx16(); store_eeprom(address, link::rx()); break; } case 'F': // fuse and lock bytes send_fuses(); break; case 'G': // hand over to the application link::tx(ack); run_app(); default: // unknown bytes are ignored; the loop re-acks break; } } } } // namespace } // namespace pureboot template struct avr::startup::entry;