// SUPERSEDED — kept for the record, not built. With the activation hang fixed // (a lone calibration pulse used to wedge the loader, see autobaud.md) this // version is 530 B on the 1284P against a 512 B slot. Its 4 B of margin was // never spare capacity; it was the space the missing fix should have occupied. // pureboot_autobaud_uni.cpp replaces it at 464 B with more features. // // pureboot autobaud — the software-serial variant that measures the host's bit // timing at runtime, so one binary runs at any F_CPU: the image carries no // clock. THE PURE VERSION — no inline assembly, no global register variables, // exactly the constraints the fixed-baud loader keeps. // // Two source files exist for review (autobaud.md): // this one, pure, and pureboot_autobaud_reg.cpp, which keeps the running-slot // write guard at the cost of one global register variable. They differ only in // where the measured unit lives and whether the guard is present. // // What this version trades to fit 512 B in pure C++ (the 1284 at 508), each // licensed by "the host guarantees safety" (README.md) and the owner's approval // to simplify the info block: // - the measured per-bit unit lives in the two general-purpose I/O scratch // registers (GPIOR) where the chip has them, in a static otherwise — // reached through libavr's named register surface, so no asm and no global // register variable; the loader stays pure; // - the info block is slimmed to the version and the signature — the chip's // identity — from which the host derives page size, loader base, EEPROM // size and the addressing flags via its own chip database; // - no running-slot write guard: the host never programs the loader's own // slot, and a broken host bricking the target is the host's bug; // - a single-byte activation knock: the calibration pulse already proves a // host is present. // // Position independence is kept and is in fact total here: control flow is // PC-relative, the wire carries addresses, and with the slimmed info block and // no write guard nothing anchors on the runtime address at all. #include #include using namespace avr::literals; namespace spm = avr::spm; namespace ee = avr::eeprom; namespace hw = avr::hw; namespace pureboot { namespace { // Purely polled: every interrupt guard folds to nothing. constexpr auto off = avr::irq::guard_policy::unused; constexpr std::uint8_t ack = '+'; // Autobaud carries no clock, so PUREBOOT_CLOCK_HZ / PUREBOOT_BAUD are not // consulted; only the software-UART pins are a deployment parameter. #if !defined(PUREBOOT_RX) #define PUREBOOT_RX pb0 #endif #if !defined(PUREBOOT_TX) #define PUREBOOT_TX pb1 #endif // The watchdog reset flag's home: MCUSR, or the classic megas' MCUCSR. consteval std::int16_t wdrf_field() { auto reg = std::string_view{hw::db.regs[static_cast(avr::power::detail::reset_reg())].name}; return hw::db.field_index(reg, "WDRF"); } // The loader owns the top 512 bytes; a staging copy goes in the slot below. constexpr std::uint16_t slot_bytes = 512; constexpr std::uint32_t base = spm::flash_bytes - slot_bytes; constexpr std::uint16_t page = spm::page_bytes; constexpr bool boot_section = hw::curated::has_boot_section(); // Past 64 KiB a byte address no longer fits the wire's 16 bits, so flash // addresses there are word addresses ('J' always was one). constexpr bool word_flash = spm::flash_bytes > 65536; // The loader's one identity number (README.md); the slimmed info block carries // it and the signature, and the host maps a version to its protocol. constexpr std::uint8_t version = 4; // The activation window as a fixed poll budget: with no clock, whole seconds // cannot be timed. A __uint24 (AVR's three-byte type) holds it — a fourth byte // would cost two words at each countdown step for range never used. #if !defined(PUREBOOT_AUTOBAUD_POLLS) #define PUREBOOT_AUTOBAUD_POLLS 4000000 #endif constexpr __uint24 autobaud_budget = PUREBOOT_AUTOBAUD_POLLS; // The measured per-bit delay (in _delay_loop_2 four-cycle iterations). It lives // in the two adjacent general-purpose I/O scratch registers (GPIOR1:GPIOR2) // where the chip has them — in/out reach them in one word where a static's // lds/sts take two, and there is no .bss to clear — and in a plain static // otherwise (the t13, m8 and m16/32 have no GPIOR). Both are pure: the named // register surface, no inline asm, no global register variable. constexpr bool have_gpior = hw::db.reg_index("GPIOR1") >= 0 && hw::db.reg_index("GPIOR2") >= 0; std::uint16_t unit_backing; template [[gnu::always_inline]] inline std::uint16_t get_unit() { if constexpr (Gpior) return static_cast(hw::reg_impl::read() | (hw::reg_impl::read() << 8)); else return unit_backing; } template [[gnu::always_inline]] inline void put_unit(std::uint16_t u) { if constexpr (Gpior) { hw::reg_impl::write(static_cast(u)); hw::reg_impl::write(static_cast(u >> 8)); } else unit_backing = u; } // The autobaud software link: bit-banged with cycle-counted delays like the // fixed-baud software backend, but the per-bit delay is the measured unit, not // a consteval constant. rx/tx load the unit once into a local, so each bit // spins from a register with no per-bit reload. struct link { using in_t = avr::io::input; using out_t = avr::io::output; static void init() { avr::init(); out_t::set(); // idle high } // Time the calibration pulse into the unit. The host sends 0xC0 — a start // bit plus six zero data bits are one low pulse of seven bit-times — and the // counted poll loop is seven cycles an iteration, so the count is the pulse // length in cycles ÷ 7 × 7 = one bit period in cycles, and count >> 2 is that // period in _delay_loop_2's four-cycle iterations. Waits for the start edge // under the poll budget; 0 (returned, and stored) means the budget expired. static std::uint16_t measure(__uint24 budget) { while (in_t::read()) if (!--budget) return 0; std::uint16_t count = 0; while (!in_t::read()) ++count; const std::uint16_t u = static_cast(count >> 2); put_unit(u); return u; } // The eight data bits, entered just past the falling edge of the start bit. // Split out of rx() so the activation path can wait for that edge under a // budget while the command loop waits for it indefinitely. static std::uint8_t sample() { const std::uint16_t unit = get_unit(); _delay_loop_2(static_cast(unit + (unit >> 1))); // 1.5 bits to the LSB centre std::uint8_t value = 0; for (std::uint8_t bit = 0; bit < 8; ++bit) { value >>= 1; if (in_t::read()) value |= 0x80; _delay_loop_2(unit); } return value; } static std::uint8_t rx() { while (in_t::read()) // await the start edge ; return sample(); } // A byte under the poll budget, for the activation knock. An expired budget // returns 0, which is not the knock, so the caller falls back into the // budgeted measure() — and a line that stays idle boots the application // there. Without this the knock's edge wait was unbounded, so a single // spurious calibration pulse (EMI, or a host that opens the port and never // knocks) wedged the loader and the application never ran. static std::uint8_t rx_bounded(__uint24 budget) { while (in_t::read()) if (!--budget) return 0; return sample(); } static void tx(std::uint8_t value) { const std::uint16_t unit = get_unit(); out_t::clear(); // start bit _delay_loop_2(unit); for (std::uint8_t bit = 0; bit < 8; ++bit) { out_t::write(value & 1); value >>= 1; _delay_loop_2(unit); } out_t::set(); // stop bit _delay_loop_2(unit); } static void drain() { // The software transmitter returns only after the stop bit. } }; extern "C" [[noreturn]] void pureboot_app(); [[gnu::noipa, noreturn]] void jump(void (*target)()) { target(); __builtin_unreachable(); } [[gnu::noinline, noreturn]] void run_app() { jump(pureboot_app); } // Inlined: read across a call, the first byte strands in a call-saved register // the caller has to push and pop. [[gnu::always_inline]] inline std::uint16_t rx16() { std::uint16_t low = link::rx(); return static_cast(low | (link::rx() << 8)); } [[gnu::always_inline]] inline std::uint16_t word_of(std::array pair) { return std::bit_cast(pair); } [[maybe_unused, gnu::always_inline]] inline void send_flash_near(std::uint16_t address, std::uint8_t count) { do link::tx(avr::flash_load(reinterpret_cast(address++))); while (--count); } [[maybe_unused, gnu::always_inline]] inline void send_flash_far(std::uint16_t address, std::uint8_t count) { std::uint8_t rampz = static_cast(address >> 15); std::uint16_t z = static_cast(address << 1); do { link::tx(avr::flash_load_far((static_cast(rampz) << 16) | z)); if (++z == 0) ++rampz; } while (--count); } [[gnu::always_inline]] inline void send_flash(std::uint16_t address, std::uint8_t count) { if constexpr (word_flash) send_flash_far(address, count); else send_flash_near(address, count); } [[gnu::noinline]] void tx_ack() { link::tx(ack); } void send_eeprom(std::uint16_t address, std::uint8_t count) { do link::tx(ee::read(address++)); while (--count); } void store_eeprom(std::uint16_t address, std::uint8_t count) { do { ee::write(address++, link::rx()); tx_ack(); } while (--count); } // One page into the SPM buffer, then erase and program. No running-slot write // guard: the host guarantees it never targets the loader's own slot (the pure // version's one dropped safety net, licensed — README.md). void program_flash(std::uint16_t wire_address) { spm::flash_address_t address; if constexpr (word_flash) { const std::uint8_t rampz = static_cast(wire_address >> 15); const std::uint16_t z0 = static_cast(wire_address << 1) & ~static_cast(page - 1); std::uint16_t z = z0; do { std::uint8_t low = link::rx(); std::uint8_t high = link::rx(); spm::fill((static_cast(rampz) << 16) | z, word_of({low, high})); z += 2; } while (static_cast(z)); address = (static_cast(rampz) << 16) | z0; } else { address = static_cast(wire_address & ~static_cast(page - 1)); do { std::uint8_t low = link::rx(); std::uint8_t high = link::rx(); spm::fill(address, word_of({low, high})); address += 2; } while (static_cast(address) & (page - 1)); address -= 2; // back inside the page — erase and write ignore the word bits } spm::erase_page(address); if constexpr (boot_section) spm::wait(); spm::write_page(address); if constexpr (boot_section) spm::wait(); if constexpr (boot_section) spm::rww_enable(); } void send_fuses() { std::uint8_t which = 0; do link::tx(spm::read_fuse(static_cast(which))); while (++which != 4); } [[noreturn]] void run() { if (hw::field_impl::test()) run_app(); link::init(); // Measure the calibration pulse into the unit, then take one 'p' knock. An // expired budget (no host) boots the application; a pulse that decodes to // anything but 'p' re-measures. for (;;) { if (link::measure(autobaud_budget) == 0) run_app(); if (link::rx_bounded(autobaud_budget) == 'p') break; } for (;;) { ee::wait(); tx_ack(); const std::uint8_t command = link::rx(); switch (command) { case 'J': { // jump to a wire word address: hand-over and staging transfer auto target = reinterpret_cast(rx16()); tx_ack(); link::drain(); jump(target); } case 'b': // chip identity: version then the three signature bytes link::tx(version); link::tx(hw::db.signature[0]); link::tx(hw::db.signature[1]); link::tx(hw::db.signature[2]); break; case 'R': // read flash: addr16, n8 (0 = 256) case 'r': // read EEPROM: addr16, n8 case 'w': { // write EEPROM: addr16, n8, then n bytes each acked // One address-and-count path for the three, so the flash streamer // keeps a single call site and inlines into this never-returning // loop — its cursor then lives in the loop's own call-saved // registers instead of being saved and restored around a call // (the call-site-count lesson, autobaud.md). const std::uint16_t address = rx16(); const std::uint8_t count = link::rx(); if (command == 'r') send_eeprom(address, count); else if (command == 'w') store_eeprom(address, count); else send_flash(address, count); break; } case 'W': // program one flash page: addr16, page bytes program_flash(rx16()); break; case 'F': // fuse and lock bytes send_fuses(); break; default: // unknown bytes are ignored; the loop re-acks break; } } } } // namespace } // namespace pureboot template struct avr::startup::entry;