// Test-fixture application for the pureboot protocol tests: prints "APP" on // the chip's serial link (the same link the loader uses) - the proof that // the loader's hand-over, and on the tinies the host's reset-vector // surgery, actually launched it. Linked normally (crt, vectors at 0); on // the tinies its reset vector is the rjmp the host re-homes. // // On the hardware-USART link it then listens, and an 'L' makes it jump into // the resident loader - the application-owned loader entry a // BOOTRST-unprogrammed mega relies on (reset always boots the application // there), exercised by the self-update tests. The software link idles: // reset reaches those loaders through the patched vector (or the runner // models BOOTRST), so the application owes them nothing. // // PUREBOOT_HANDOVER drops the listening and jumps straight in, leaving the // USART enabled behind it - the hand-over state a loader bit-banging on that // USART's own pins has to survive. // // The fixture speaks the deployment its loader was built for: the same // PUREBOOT_* defines configure it, and without them it assumes the stock // deployment (the crystal/RC clock table below, the chip's natural link). #include using namespace avr::literals; namespace { consteval avr::hertz_t clock() { #if defined(PUREBOOT_CLOCK_HZ) return avr::hertz_t{PUREBOOT_CLOCK_HZ}; #else auto name = std::string_view{avr::hw::db.name}; if (name.starts_with("ATtiny13")) { return 9.6_MHz; } if (name.starts_with("ATtiny")) { return 8_MHz; } return 16_MHz; #endif } #if !defined(PUREBOOT_TX) #define PUREBOOT_TX pb1 #endif #if !defined(PUREBOOT_RX) #define PUREBOOT_RX pb0 #endif #if !defined(PUREBOOT_USART) #define PUREBOOT_USART 0 #endif consteval bool use_hardware() { #if defined(PUREBOOT_SOFT_SERIAL) return false; #else return avr::uart::has_usart<0>(); #endif } using dev = avr::device<{.clock = clock()}>; template struct link { #if defined(PUREBOOT_BAUD) static constexpr avr::baud_t baud{PUREBOOT_BAUD}; #else static constexpr avr::baud_t baud{115200}; #endif // The fixture speaks whatever rate the loader was built for, stock // 115200 at 16 MHz included, which sits past the receiver-tolerance // table's bound - the same deployment envelope the loader itself states. using tx_t = avr::uart::usart; static void init() { avr::init(); } static void tx(char c) { tx_t::write(static_cast(c)); } // The loader sits in the top slot - 512 bytes on every chip. The jump // takes a word address, which is what makes the >64 KiB chips' entry // reachable through a 16-bit pointer at all. static void enter_loader() { constexpr std::uint32_t slot = 512; reinterpret_cast(static_cast((avr::hw::db.mem.flash_size - slot) / 2))(); } [[noreturn]] static void idle() { #if defined(PUREBOOT_HANDOVER) // Hand back at once, with this USART still enabled - the state that // leaves a bit-banged loader on its pins mute unless the loader // releases it. Unconditional because there is no command wire to // wait on: that loader's link is the pins, not this peripheral. enter_loader(); __builtin_unreachable(); #else for (;;) { auto command = tx_t::read_blocking(); if (command == 'L') { enter_loader(); } // 'D' leaves every word of the SPM page buffer dirty, so that a // following 'L' enters the loader with the buffer it never clears. // Hardware refuses application-section SPM on a boot-sectioned // part; simavr dispatches it anyway, which is the whole reason the // state is constructible - the stated section is the compilable // fiction that matches what the simulator runs. if (command == 'D') { const auto open = avr::spm::page::begin(0); for (std::uint16_t at = 0; at < avr::spm::page_bytes; at += 2) { avr::spm::fill(open, at, 0xdead); } tx('D'); } } #endif } }; template struct link { #if defined(PUREBOOT_BAUD) static constexpr avr::baud_t baud{PUREBOOT_BAUD}; #else static constexpr avr::baud_t baud{57600}; #endif // A shared-pin deployment (RX == TX) banners as a guest on its own line: // the pull-up input is the released line, the transmitter takes the pin // for exactly one frame per byte - the shape a real one-wire application // beside this loader uses. static constexpr bool one_wire = avr::PUREBOOT_RX == avr::PUREBOOT_TX; using tx_t = avr::uart::software_tx; static void init() { // The guest transmitter configures no pin; the released line - the // pull-up input a receiver would own - is established here. if constexpr (one_wire) { avr::init, tx_t>(); } else { avr::init(); } } static void tx(char c) { tx_t::write(static_cast(c)); } [[noreturn]] static void idle() { #if defined(PUREBOOT_HEARTBEAT) // Repeat the banner forever, which turns the fixture into a fixed // cycles-per-bit transmitter: `tools/pbrig.py rate` sweeps the host rate // against it to find the part's true bit rate, and from that the clock // its RC oscillator is really running at. Only the *bit* timing carries // the measurement - the delay merely spaces the lines out, so its own // error does not matter. Software link only: the hardware-link idle owes // the self-update tests a command loop, and a crystal deployment has // nothing to measure. while (true) { tx('A'); tx('P'); tx('P'); tx('\r'); tx('\n'); dev::delay<50_ms>(); } #else while (true) { } #endif } }; } // namespace int main() { link::init(); #if !defined(PUREBOOT_HANDOVER) link::tx('A'); link::tx('P'); link::tx('P'); #endif // The hand-over fixture stays silent: nothing is listening on the USART it // brings up - the loader it hands to speaks those pins directly - so its // banner would be a write into a peer that does not exist. link::idle(); }