check.sh proves the protocol under simavr on every chip; it cannot prove a board. Two things live only on silicon — an RC oscillator that is not on its nominal, and a reset edge that has to come from somewhere — and until now the scripts that reached them were per-session scratch on the machine holding the programmer, which is where the ATtiny13A run's findings nearly stayed. pbrig.py is the primitives, knowing nothing per-board: every deployment fact is a flag or a PUREBOOT_* variable. Two rig facts are encoded in it because neither is guessable and each cost a session to learn: an ISP access *is* the reset edge where the adapter's DTR is unwired, so a session begins with an ISP touch and knocks immediately after; and avrdude splits -U on colons, so a Windows drive letter breaks the spec and every file goes as a bare name with avrdude run in its own directory. Its `rate` subcommand is the one that turns "the loader is silent, so the wiring must be wrong" into a number, by sweeping the host rate against a fixed cycles-per-bit transmitter — PUREBOOT_HEARTBEAT makes the existing fixture into one, software link only, since the hardware-link idle owes the self-update tests its command loop. pbhw.py takes every bound from the info block the loader reports, so one run covers a 1 KiB tiny and a 128 KiB mega alike. Both are exercised on an ATtiny13A: backup verified against a known-good capture, the clock measured at 9.048 MHz against a 9.6 MHz nominal, and the suite 11/11. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
159 lines
4.9 KiB
C++
159 lines
4.9 KiB
C++
// 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 <libavr/libavr.hpp>
|
|
|
|
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_USART)
|
|
#define PUREBOOT_USART 0
|
|
#endif
|
|
|
|
consteval bool use_hardware()
|
|
{
|
|
#if defined(PUREBOOT_SOFT_SERIAL)
|
|
return false;
|
|
#else
|
|
return avr::hw::db.has_instance("USART0") || avr::hw::db.has_instance("USART");
|
|
#endif
|
|
}
|
|
|
|
using dev = avr::device<{.clock = clock()}>;
|
|
|
|
template <avr::hertz_t C, bool Hardware = use_hardware()>
|
|
struct link {
|
|
#if defined(PUREBOOT_BAUD)
|
|
static constexpr avr::baud_t baud{PUREBOOT_BAUD};
|
|
#else
|
|
static constexpr avr::baud_t baud{115200};
|
|
#endif
|
|
using tx_t = avr::uart::usart<'0' + PUREBOOT_USART, C, {.baud = baud, .max_baud_error = 2.5_pct}>;
|
|
static void tx(char c)
|
|
{
|
|
tx_t::write(static_cast<std::uint8_t>(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<void (*)()>(static_cast<std::uint16_t>((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.
|
|
if (command == 'D') {
|
|
for (std::uint16_t at = 0; at < avr::spm::page_bytes; at += 2)
|
|
avr::spm::fill(at, 0xdead);
|
|
tx('D');
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
};
|
|
|
|
template <avr::hertz_t C>
|
|
struct link<C, false> {
|
|
#if defined(PUREBOOT_BAUD)
|
|
static constexpr avr::baud_t baud{PUREBOOT_BAUD};
|
|
#else
|
|
static constexpr avr::baud_t baud{57600};
|
|
#endif
|
|
using tx_t = avr::uart::software_tx<C, avr::PUREBOOT_TX, baud>;
|
|
static void tx(char c)
|
|
{
|
|
tx_t::write(static_cast<std::uint8_t>(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()
|
|
{
|
|
avr::init<typename link<dev::clock>::tx_t>();
|
|
#if !defined(PUREBOOT_HANDOVER)
|
|
link<dev::clock>::tx('A');
|
|
link<dev::clock>::tx('P');
|
|
link<dev::clock>::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<dev::clock>::idle();
|
|
}
|