'W' handed the loader a whole page with no ack inside it and sp_spm handed any wire byte to SPMCSR, so a dropped byte re-aligned the stream and page data arrived where commands belong. That is how a page-address byte became BLBSET|SELFPRGEN on the tempmon board and programmed its lock bits. The first answer was to refuse that one command. It was the wrong shape twice over: it forbade a lock-bit write the owner may want, and it left every other command decided by bytes nobody checked. v9 checks them instead. One header for every command — opcode, selector, address, count, seal — folded and compared before the command is decoded, and *answered* before any payload moves: '+' accepts, 0xd4 (the ack inverted) refuses and nothing happened. An ack cannot do this job; it reports a command that has already run. It is smaller than v8 everywhere: 1284P 506→480, m8 498→480, 328P 484→468, t13A 474→460. The seal costs 14 bytes; bit opcodes in place of the letters pay for it twice over, since a letter costs a compare and a branch where a bit costs a skip. Both guards go — the lock-bit refusal because the seal covers it, the running-slot write guard because what it defended against was a wire fault naming an address and a wire fault can no longer name one. That one is a real trade: a host bug aimed at the running slot now lands. It buys a resident copy that can write its own slot, which is the only self-update route on a chip whose boot section *is* the slot. Two things the tests caught, both introduced here. Removing the invalid-opcode arm made every byte a command, so the knock stopped being harmless against a loader already in session and ate the five bytes behind it — identify moves to bit 5, which both 'p' and 'b' carry, so the knock is inert again and version discovery still works before the version is known. And the SPM value rides the count field because a data byte would arrive after the seal was checked. pbselfwrite and pbglitch are the new gates, both red-green: the same erase of the running page refused unsealed and performed sealed, and every header byte damaged after sealing refused where the identical damage before sealing is obeyed. Both judge by the simulator's flash, not the loader's opinion of it. pbreloc and pbrehome lose their write-guard probes, which is what those two gates replace. Defeating the seal in the loader turns seven tests red. 37 of 37 chips green with the exhaustive size matrix; README protocol section and every size row rewritten. pbhw gains an adversarial --seal-rounds sweep for the bench. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
683 lines
28 KiB
C++
683 lines
28 KiB
C++
// pureboot — a serial bootloader on libavr: one C++ source, no inline
|
||
// assembly, no global register variables, a 512-byte slot on every chip
|
||
// libavr targets. The device speaks primitives; every composite (verify, erase,
|
||
// reset-vector surgery, self-update) lives in the host tool. Protocol,
|
||
// deployment and configuration: README.md next to this file.
|
||
//
|
||
// The image is position-independent — PC-relative control flow and wire
|
||
// addresses in, no absolute address formed anywhere — so the identical binary
|
||
// runs from any slot. That is what makes a copy one slot below able to rewrite
|
||
// the resident one, and every change here has to keep it (test/check_pi.py).
|
||
// It does not need to know *which* slot it is in: nothing here refuses an
|
||
// address, so there is no running-slot comparison to anchor.
|
||
|
||
#include <chrono>
|
||
|
||
#include <libavr/libavr.hpp>
|
||
|
||
using namespace avr::literals;
|
||
namespace spm = avr::spm;
|
||
namespace ee = avr::eeprom;
|
||
|
||
namespace pureboot {
|
||
namespace {
|
||
|
||
// Purely polled: every interrupt guard folds to nothing.
|
||
constexpr auto off = avr::irq::guard_policy::unused;
|
||
|
||
constexpr std::uint8_t ack = '+';
|
||
|
||
// The refusal, which is the ack inverted: on a link whose whole problem is
|
||
// flipped bits, the byte saying "nothing happened" should be as far as a byte
|
||
// can be from the one saying "it did", and the complement is all eight bits.
|
||
// It is also the only spelling that needs no justifying — every other value
|
||
// would be a choice.
|
||
constexpr std::uint8_t nak = static_cast<std::uint8_t>(~ack);
|
||
|
||
// What a command's fields must fold to. Any non-zero constant does: zero is
|
||
// what a run of one repeated byte folds to, and a repeated byte is the shape of
|
||
// both a line stuck at a level and a page of erased flash arriving where a
|
||
// header belongs.
|
||
constexpr std::uint8_t seal = 0x5a;
|
||
|
||
// The opcode, as bits rather than letters. Each is a one-instruction skip,
|
||
// where a set of arbitrary values costs a compare and a branch apiece — and
|
||
// with the seal deciding what is a command at all, there is nothing left for a
|
||
// readable spelling to buy. A transfer is the absence of the other three, and
|
||
// its direction is the low bit.
|
||
//
|
||
// Identify is bit 5 for one reason: 'p' and 'b' both carry it, and those are
|
||
// the knock. Two things follow that no other assignment gives. A host cannot
|
||
// know which generation it is talking to until something has answered, so the
|
||
// command reporting the version has to mean the same thing before the version
|
||
// is known — 'b' still asks it. And a knock aimed at a loader that is
|
||
// *already* in session has to stay harmless: with no opcode reserved as
|
||
// invalid, every byte now starts a command, so a knock that meant nothing to
|
||
// earlier generations would otherwise consume the five header bytes behind it
|
||
// and put the stream out of step. Answering both knock bytes with the identity
|
||
// keeps the reconnect exactly as cheap as it was.
|
||
enum : std::uint8_t { op_write = 1, op_fill = 4, op_jump = 8, op_identify = 0x20 };
|
||
|
||
// Deployment parameters come from the build (pureboot_add_loader()). The
|
||
// signature is not one of them: the chip database is the only universal
|
||
// source — a tiny13A cannot read its own signature row from code. An autobaud
|
||
// build carries no clock and no baud at all; it measures both.
|
||
#if !defined(PUREBOOT_AUTOBAUD) && (!defined(PUREBOOT_CLOCK_HZ) || !defined(PUREBOOT_BAUD))
|
||
#error \
|
||
"PUREBOOT_CLOCK_HZ and PUREBOOT_BAUD select this build's clock and baud — create loader targets with pureboot_add_loader(), or PUREBOOT_AUTOBAUD for a clock-free one (README.md)"
|
||
#endif
|
||
|
||
#if !defined(PUREBOOT_AUTOBAUD)
|
||
using dev = avr::device<{.clock = avr::hertz_t{PUREBOOT_CLOCK_HZ}}>;
|
||
constexpr avr::baud_t wire_baud{PUREBOOT_BAUD};
|
||
#endif
|
||
|
||
// The loader owns the top 512 bytes; a staging copy goes in the slot below.
|
||
// Chips without a hardware boot section — the tinies and the m48s, whose SPM
|
||
// runs from anywhere (Atmel-8271 §26) — keep the application's relocated
|
||
// reset vector in the word under the slot. The size itself is the linker's and
|
||
// the host's business: nothing in here needs to know where the slot ends.
|
||
constexpr std::uint16_t page = spm::page_bytes;
|
||
constexpr bool boot_section = avr::hw::curated::has_boot_section();
|
||
|
||
// Past 64 KiB one bank of flash does not cover the chip, so a transfer's
|
||
// selector byte carries the bank and the wire address stays a byte address
|
||
// within it. 'J' is the exception: it is a word address everywhere, because
|
||
// that is what the hardware's own jump takes.
|
||
constexpr bool banked_flash = spm::flash_bytes > 65536;
|
||
|
||
// A compile-time window, so the whole EEPROM belongs to the application;
|
||
// re-timing a deployed loader is a self-update with a re-timed build. An
|
||
// autobaud build has no clock to convert seconds against and counts polls.
|
||
#if !defined(PUREBOOT_TIMEOUT)
|
||
#define PUREBOOT_TIMEOUT 8
|
||
#endif
|
||
constexpr std::uint8_t timeout_seconds = PUREBOOT_TIMEOUT;
|
||
|
||
#if !defined(PUREBOOT_AUTOBAUD_POLLS)
|
||
#define PUREBOOT_AUTOBAUD_POLLS 4000000
|
||
#endif
|
||
constexpr avr::uint24_t autobaud_budget = PUREBOOT_AUTOBAUD_POLLS;
|
||
|
||
// A build may bake a measured oscillator trim (README.md: the RC-oscillator
|
||
// deployment answer); the byte is applied at the top of run(). Orthogonal to
|
||
// the serial backend — an autobaud build may carry it for the application's
|
||
// benefit alone.
|
||
#if defined(PUREBOOT_OSCCAL)
|
||
static_assert(PUREBOOT_OSCCAL >= 0 && PUREBOOT_OSCCAL <= 0xff, "PUREBOOT_OSCCAL is one OSCCAL byte");
|
||
#endif
|
||
|
||
// The loader's one identity number. The protocol carries none of its own —
|
||
// a version implies it, and the host tool holds that map (README.md).
|
||
constexpr std::uint8_t version = 9;
|
||
|
||
// The image's identity stamp, for the host tool rather than for the wire: an
|
||
// update image is a bare 512-byte slot, and without this nothing in it says
|
||
// which chip it was built for. The tool refuses to install an image whose
|
||
// stamp does not match the device — flashing a foreign loader bricks the
|
||
// target, and the loader itself cannot check what has already replaced it.
|
||
//
|
||
// Never read from flash by the loader — 'b' answers out of this array, but at
|
||
// constant indices, so those fold to immediates and no runtime address of it
|
||
// is ever formed. `used` keeps the compiler from dropping the copy the host
|
||
// needs and `retain` keeps --gc-sections from collecting it.
|
||
// clang-format off
|
||
[[gnu::used, gnu::retain, gnu::section(".text.stamp")]]
|
||
inline constexpr std::uint8_t identity_stamp[]{
|
||
'P', 'B', // the magic the host scans an image for
|
||
version, // and from here on, exactly what 'b' answers
|
||
avr::hw::db.signature[0],
|
||
avr::hw::db.signature[1],
|
||
avr::hw::db.signature[2],
|
||
};
|
||
// clang-format on
|
||
// Where the identity proper starts: past the magic the host scans for.
|
||
constexpr std::uint8_t stamp_identity = 2;
|
||
|
||
// The address spaces a transfer can name, in a selector byte's low nibble.
|
||
// Flash is 0 so it is the cheapest to select.
|
||
//
|
||
// spm_ops is the one that is not memory: naming it hands the count field to
|
||
// SPMCSR and fires the instruction at the transfer's address, which is how
|
||
// page erase, page write and RWW re-enable reach the wire without the loader
|
||
// carrying a command for each. The hardware's four-cycle store-to-SPM window
|
||
// is why this is one fused primitive and not a poke of SPMCSR — no host can
|
||
// hit that window across a serial link.
|
||
//
|
||
// It is the one space with no direction: the opcode's write bit is not
|
||
// consulted, because a sealed command naming this space says what it means and
|
||
// there is nothing for the other direction to denote. Testing the bit anyway
|
||
// would cost six bytes to catch a host contradicting itself, which is the same
|
||
// trade the running-slot guard lost.
|
||
enum : std::uint8_t { sp_flash = 0, sp_eeprom = 1, sp_data = 2, sp_fuse = 3, sp_spm = 4 };
|
||
|
||
// A selector's high nibble is the flash bank — the address bits above the
|
||
// 16-bit wire address, RAMPZ on the chips that have one. Keeping it here
|
||
// rather than widening the wire address is what lets one 16-bit cursor serve
|
||
// every space: a 24-bit cursor would pay its extra byte on EEPROM and data
|
||
// reads that can never need it.
|
||
[[gnu::always_inline]] inline std::uint8_t space_of(std::uint8_t selector)
|
||
{
|
||
return selector & 0x0f;
|
||
}
|
||
|
||
[[gnu::always_inline]] inline std::uint8_t bank_of(std::uint8_t selector)
|
||
{
|
||
return static_cast<std::uint8_t>(selector >> 4);
|
||
}
|
||
|
||
// The serial link, per the build's PUREBOOT_USART / PUREBOOT_SOFT_SERIAL /
|
||
// PUREBOOT_AUTOBAUD, defaulting to the chip's USART0 where it has one. The
|
||
// software receiver is the polled one: the vector table belongs to the
|
||
// application. Templates on the clock, so only the selected backend
|
||
// instantiates. pending() is the cheap line test the activation window polls;
|
||
// drain() holds until the last frame is off the wire, so a hand-over cannot
|
||
// let the target's re-init clip the ack.
|
||
#if defined(PUREBOOT_SOFT_SERIAL) && defined(PUREBOOT_USART)
|
||
#error "PUREBOOT_SOFT_SERIAL and PUREBOOT_USART select opposing serial backends"
|
||
#endif
|
||
#if defined(PUREBOOT_AUTOBAUD) && defined(PUREBOOT_USART)
|
||
#error "PUREBOOT_AUTOBAUD measures a software link; it cannot drive a hardware USART"
|
||
#endif
|
||
#if defined(PUREBOOT_HALF_DUPLEX) && (defined(PUREBOOT_SOFT_SERIAL) || defined(PUREBOOT_AUTOBAUD))
|
||
#error "PUREBOOT_HALF_DUPLEX is the hardware USART's one-wire mode; a software link goes one-wire by RX == TX"
|
||
#endif
|
||
#if !defined(PUREBOOT_RX)
|
||
#define PUREBOOT_RX pb0
|
||
#endif
|
||
#if !defined(PUREBOOT_TX)
|
||
#define PUREBOOT_TX pb1
|
||
#endif
|
||
#if defined(PUREBOOT_USART)
|
||
constexpr int usart_unit = PUREBOOT_USART;
|
||
#else
|
||
constexpr int usart_unit = 0;
|
||
#endif
|
||
|
||
// One-wire on the hardware USART (PUREBOOT_HALF_DUPLEX): RXD and TXD tied
|
||
// together off-chip, exactly one direction enabled at a time — the library's
|
||
// .half_duplex turn-around. The activation window is unchanged; only its
|
||
// poll grows the release-line test rx_ready() carries in this mode.
|
||
constexpr bool hw_half_duplex =
|
||
#if defined(PUREBOOT_HALF_DUPLEX)
|
||
true;
|
||
#else
|
||
false;
|
||
#endif
|
||
|
||
template <avr::hertz_t C, avr::baud_t B>
|
||
struct hardware_link {
|
||
using uart = avr::uart::usart<usart_unit, C, {.baud = B, .max_baud_error = 2.5_pct, .half_duplex = hw_half_duplex}>;
|
||
|
||
// The compiled idle poll around the window's narrow (uint24_t) countdown:
|
||
// the RXC test, then sbiw + sbci + brne (5). The test's cost follows the
|
||
// status register's home — a 2-cycle bit-skip where UCSRnA sits in
|
||
// bit-addressable I/O (the classic megas), lds + skip (4) in extended
|
||
// I/O. Half-duplex polls through rx_ready()'s release-line test, which
|
||
// -Os outlines: the rcall (3), the UCSR#B read and not-taken skip with
|
||
// the jump over the write (I/O 3, extended 5), the ret (4) — and the
|
||
// call in the loop body pushes the countdown into call-saved registers,
|
||
// where the uint24_t step is ldi+sub+sbc+sbc (4) instead of sbiw+sbci
|
||
// (3). Measured off the built loops: 18 a poll in bit-addressable I/O,
|
||
// 22 in extended. A uint32_t countdown pays one more sbci —
|
||
// window_polls() adds it where the count forces the wide type. Held per
|
||
// chip by the pureboot.window gates. The lookup rides the baud parameter
|
||
// so it stays dependent: the trait is an incomplete type on the
|
||
// USART-less chips, which parse this template without ever instantiating
|
||
// it.
|
||
template <avr::baud_t Baud, typename U = avr::hw::usart_of<usart_unit>>
|
||
static consteval std::uint8_t poll_cost()
|
||
{
|
||
if (hw_half_duplex)
|
||
return U::ucsra::addr < 0x40 ? 18 : 22;
|
||
return U::ucsra::addr < 0x40 ? 7 : 9;
|
||
}
|
||
static constexpr std::uint8_t poll_cycles = poll_cost<B>();
|
||
|
||
static void init()
|
||
{
|
||
avr::init<uart>();
|
||
}
|
||
|
||
static bool pending()
|
||
{
|
||
return uart::rx_ready();
|
||
}
|
||
|
||
static std::uint8_t rx()
|
||
{
|
||
return uart::read_blocking();
|
||
}
|
||
|
||
static void tx(std::uint8_t byte)
|
||
{
|
||
uart::write(byte);
|
||
}
|
||
|
||
static void drain()
|
||
{
|
||
// A drain here always follows this link's own write — the frame is
|
||
// in flight by construction, so the completion the wait needs is
|
||
// guaranteed and the bounded default's countdown would be dead bytes.
|
||
uart::drain_unbounded();
|
||
}
|
||
};
|
||
|
||
template <avr::hertz_t C, avr::baud_t B>
|
||
struct software_link {
|
||
// RX == TX is the one-wire deployment: the transmitter becomes a guest
|
||
// on the receiver's pull-up line, taking the pin's direction for exactly
|
||
// one frame per byte.
|
||
using rx_t = avr::uart::software_rx_polled<C, avr::PUREBOOT_RX, B>;
|
||
using tx_t = avr::uart::software_tx<C, avr::PUREBOOT_TX, B, avr::PUREBOOT_RX == avr::PUREBOOT_TX>;
|
||
|
||
// The compiled idle poll around the window's narrow (uint24_t) countdown:
|
||
// sbis skipping the exit (2), sbiw + sbci + brne (5). A uint32_t
|
||
// countdown pays one more sbci — window_polls() adds it where the count
|
||
// forces the wide type. Held by the pureboot.window gate.
|
||
static constexpr std::uint8_t poll_cycles = 7;
|
||
|
||
static void init()
|
||
{
|
||
avr::init<rx_t, tx_t>();
|
||
}
|
||
|
||
static bool pending()
|
||
{
|
||
return rx_t::start_pending();
|
||
}
|
||
|
||
static std::uint8_t rx()
|
||
{
|
||
return rx_t::template read_blocking<off>();
|
||
}
|
||
|
||
static void tx(std::uint8_t byte)
|
||
{
|
||
tx_t::template write<off>(byte);
|
||
}
|
||
|
||
static void drain()
|
||
{
|
||
// The software transmitter returns only after the stop bit.
|
||
}
|
||
};
|
||
|
||
// The clock-free link: the bit period is measured from the host's calibration
|
||
// pulse instead of derived from a clock, so one image serves every F_CPU and
|
||
// every rate. Activation differs in kind from the other two — there is no
|
||
// clock to time a window against — so this backend brings its own, below.
|
||
struct autobaud_link {
|
||
// The unit in GPIOR2:GPIOR1 where the chip has them: the loader owns the
|
||
// whole chip while it runs, and the pair costs one word per access where
|
||
// the RAM word costs two — six words across the image.
|
||
using uart = avr::uart::software_autobaud<avr::PUREBOOT_RX, avr::PUREBOOT_TX, avr::uart::unit_home::gpior>;
|
||
|
||
static void init()
|
||
{
|
||
avr::init<uart>();
|
||
}
|
||
|
||
static std::uint8_t rx()
|
||
{
|
||
return uart::template read<off>();
|
||
}
|
||
|
||
static void tx(std::uint8_t byte)
|
||
{
|
||
uart::template write<off>(byte);
|
||
}
|
||
|
||
static void drain()
|
||
{
|
||
// A drain here always follows this link's own write — the frame is
|
||
// in flight by construction, so the completion the wait needs is
|
||
// guaranteed and the bounded default's countdown would be dead bytes.
|
||
uart::drain_unbounded();
|
||
}
|
||
};
|
||
|
||
#if defined(PUREBOOT_AUTOBAUD)
|
||
using link = autobaud_link;
|
||
#elif defined(PUREBOOT_USART)
|
||
static_assert(avr::uart::has_usart<usart_unit>(), "PUREBOOT_USART selects a hardware USART this chip does not have");
|
||
using link = hardware_link<dev::clock, wire_baud>;
|
||
#elif defined(PUREBOOT_SOFT_SERIAL)
|
||
using link = software_link<dev::clock, wire_baud>;
|
||
#else
|
||
using link = std::conditional_t<avr::uart::has_usart<usart_unit>(), hardware_link<dev::clock, wire_baud>,
|
||
software_link<dev::clock, wire_baud>>;
|
||
#endif
|
||
|
||
// The application's entry, pinned by the linker (--defsym): word 0 on a
|
||
// boot-sectioned mega, the trampoline at base − 2 elsewhere. Reaching it must
|
||
// not depend on where this copy runs, so the jump goes through a pointer, and
|
||
// [[gnu::noipa]] keeps the constant from folding back into a relative call.
|
||
extern "C" [[noreturn]] void pureboot_app();
|
||
|
||
[[gnu::noipa, noreturn]] void jump(void (*target)())
|
||
{
|
||
target();
|
||
__builtin_unreachable();
|
||
}
|
||
|
||
[[noreturn]] void run_app()
|
||
{
|
||
jump(pureboot_app);
|
||
}
|
||
|
||
// Activation: a bounded wait for the host, then the knock. Both forms boot the
|
||
// application when the window closes on an idle line, and both bound *every*
|
||
// wait — a knock awaited without a deadline would let one stray edge hold an
|
||
// unattended device in the loader forever.
|
||
#if defined(PUREBOOT_AUTOBAUD)
|
||
// The window is a fixed poll budget: with no clock, whole seconds cannot be
|
||
// timed. A uint24_t holds it — a fourth byte would cost two words at every
|
||
// countdown step for range never used.
|
||
void await_host()
|
||
{
|
||
for (;;) {
|
||
if (!link::uart::calibrate(autobaud_budget))
|
||
run_app();
|
||
// The calibration pulse has already proven a host is there, so one
|
||
// byte activates. A knock that never arrives falls back to calibrate(),
|
||
// whose own budget then boots the application.
|
||
if (link::uart::template read<off>(autobaud_budget) == 'p')
|
||
return;
|
||
}
|
||
}
|
||
#else
|
||
// The window as one countdown, divided by the backend's counted poll-loop
|
||
// cycles. Whole seconds is all it promises. The per-poll cost depends on the
|
||
// countdown's own width (a uint32_t decrement chain is one sbci longer), and
|
||
// the width depends on the poll count — solved narrow-first: a count that
|
||
// fits 24 bits at the narrow cost keeps the narrow loop, anything else takes
|
||
// the wide loop at its own cost. A count fitting 24 bits only at the wide
|
||
// cost stays wide, so the choice cannot oscillate on the boundary.
|
||
consteval std::uint32_t polls_at(std::uint32_t per_poll)
|
||
{
|
||
// Whole-window cycles first, then the per-poll division: one truncation
|
||
// instead of one per second. Same instructions either way — only the
|
||
// countdown's immediate moves.
|
||
return static_cast<std::uint32_t>(dev::cycles_for<std::chrono::seconds{timeout_seconds}>() / per_poll);
|
||
}
|
||
|
||
consteval bool narrow_window()
|
||
{
|
||
return polls_at(link::poll_cycles) <= 0xffffff;
|
||
}
|
||
|
||
consteval std::uint32_t window_polls()
|
||
{
|
||
return polls_at(narrow_window() ? link::poll_cycles : link::poll_cycles + 1u);
|
||
}
|
||
|
||
// The countdown in the narrowest type that holds it: a fourth byte would
|
||
// cost a wider decrement chain at every poll for range most windows never
|
||
// use (the autobaud budget makes the same choice).
|
||
using window_t = std::conditional_t<narrow_window(), avr::uint24_t, std::uint32_t>;
|
||
|
||
bool pending_before_deadline()
|
||
{
|
||
window_t polls = window_polls();
|
||
do {
|
||
if (link::pending())
|
||
return true;
|
||
} while (--polls);
|
||
return false;
|
||
}
|
||
|
||
// A knock byte under the deadline: an idle window means no host, so the
|
||
// application runs.
|
||
std::uint8_t rx_deadline()
|
||
{
|
||
if (!pending_before_deadline())
|
||
run_app();
|
||
return link::rx();
|
||
}
|
||
|
||
void await_host()
|
||
{
|
||
// 'p' then 'b', each under a fresh window; anything else is line noise.
|
||
while (rx_deadline() != 'p' || rx_deadline() != 'b') {
|
||
}
|
||
}
|
||
#endif
|
||
|
||
// 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<std::uint16_t>(low | (link::rx() << 8));
|
||
}
|
||
|
||
// The wire's byte pair as the word it is — AVR is little-endian too, so the
|
||
// cast is the identity a shift-and-or spelling makes the compiler rediscover.
|
||
// Callers read into named variables first: the wire order is a sequence of
|
||
// reads, not an argument order.
|
||
[[gnu::always_inline]] inline std::uint16_t word_of(std::array<std::uint8_t, 2> pair)
|
||
{
|
||
return std::bit_cast<std::uint16_t>(pair);
|
||
}
|
||
|
||
// Out of line: several sites send it, and a call is shorter than a
|
||
// load-immediate at each.
|
||
[[gnu::noinline]] void tx_ack()
|
||
{
|
||
link::tx(ack);
|
||
}
|
||
|
||
// A wire address and its selector's bank as the flash address they name.
|
||
[[gnu::always_inline]] inline spm::flash_address_t flash_address([[maybe_unused]] std::uint8_t bank, std::uint16_t at)
|
||
{
|
||
if constexpr (banked_flash)
|
||
return (static_cast<spm::flash_address_t>(bank) << 16) | at;
|
||
else
|
||
return at;
|
||
}
|
||
|
||
// One byte out of any space. Every accessor shares the transfer's cursor, its
|
||
// loop and its call site, so a space costs only its own instruction rather
|
||
// than a body, a loop and a dispatch arm of its own.
|
||
[[gnu::always_inline]] inline std::uint8_t load(std::uint8_t space, [[maybe_unused]] std::uint8_t bank,
|
||
std::uint16_t at)
|
||
{
|
||
if (space == sp_eeprom)
|
||
return ee::read(at);
|
||
if (space == sp_data)
|
||
return *reinterpret_cast<volatile std::uint8_t *>(at);
|
||
if (space == sp_fuse)
|
||
return spm::read_fuse<off>(static_cast<spm::fuse>(at));
|
||
if constexpr (banked_flash)
|
||
return avr::flash_load_far<std::uint8_t>(flash_address(bank, at));
|
||
else
|
||
return avr::flash_load(reinterpret_cast<const std::uint8_t *>(at));
|
||
}
|
||
|
||
// One byte into a writable space. Flash is not one of them — it arrives a
|
||
// page at a time through 'W' and is committed by the sealed SPM command — and
|
||
// the fuses are not writable at all: SPM reaches flash and boot lock bits only.
|
||
[[gnu::always_inline]] inline void store(std::uint8_t space, std::uint16_t at, std::uint8_t value)
|
||
{
|
||
if (space == sp_data) {
|
||
*reinterpret_cast<volatile std::uint8_t *>(at) = value;
|
||
return;
|
||
}
|
||
// Host-paced: the ack goes out once the write has begun, so the next byte
|
||
// arrives while it completes and nothing is missed without a buffer.
|
||
ee::write<off>(at, value);
|
||
}
|
||
|
||
// The irreversible half of the protocol, and the whole of it: page erase, page
|
||
// write and the lock bits are one SPM command each, and nothing else the loader
|
||
// does outlasts being done again. Reached only from a sealed command (run()),
|
||
// so both the byte handed to SPMCSR and the address it fires at are the ones
|
||
// the host computed its seal over.
|
||
//
|
||
// Nothing here refuses an address. A loader that will not write its own slot
|
||
// cannot plant anything in it either, and a resident copy able to rewrite its
|
||
// own trailing page is what lets a 512-byte boot section — where no staging
|
||
// copy can run SPM at all — carry an SPM primitive for an application-side
|
||
// installer to drive. The protection that made the guard look necessary is the
|
||
// seal: a wire fault can no longer name an address, only a host can, and a host
|
||
// that names this one means it.
|
||
[[gnu::always_inline]] inline void commit(std::uint8_t bank, std::uint16_t at, std::uint8_t value)
|
||
{
|
||
spm::command<off>(value, flash_address(bank, at));
|
||
// Only a boot-sectioned mega runs on while its RWW section programs;
|
||
// everywhere else the CPU halts through erase and write, so the wait
|
||
// is already over by the time it returns.
|
||
if constexpr (boot_section)
|
||
spm::wait();
|
||
}
|
||
|
||
// One page into the SPM buffer, and only that: the erase and the write that
|
||
// commit it are host-issued sp_spm stores, which reach the same fused
|
||
// store-and-SPM pair through the transfer path's own address and data.
|
||
//
|
||
// Nothing discards the buffer first: it is write-once per word (§26.2.1), so
|
||
// filling over a refused page or an application's leavings programs stale
|
||
// words — but a page write auto-erases it (§26.2.1; §19.2 on the tinies), so
|
||
// that write clears the condition and the host's read-back rewrites the page.
|
||
void fill_page(std::uint8_t bank, std::uint16_t at)
|
||
{
|
||
// The address names a page, so its in-page bits are dropped and the walk
|
||
// starts at the page base; the low byte of the cursor is the whole in-page
|
||
// offset, since a page is aligned and never crosses a bank.
|
||
std::uint16_t z = at & ~static_cast<std::uint16_t>(page - 1);
|
||
do {
|
||
std::uint8_t low = link::rx();
|
||
std::uint8_t high = link::rx();
|
||
spm::fill<off>(flash_address(bank, z), word_of({low, high}));
|
||
z += 2;
|
||
} while (static_cast<std::uint8_t>(z) & (page - 1));
|
||
}
|
||
|
||
[[noreturn]] void run()
|
||
{
|
||
#if defined(PUREBOOT_OSCCAL)
|
||
// The build's oscillator trim, ahead of everything — the WDRF bail
|
||
// included — so every path out of reset, the watchdog hand-over to the
|
||
// application first among them, runs on the corrected clock.
|
||
avr::clock::calibrate(PUREBOOT_OSCCAL);
|
||
#endif
|
||
// A watchdog reset belongs to the application, whose watchdog stays forced
|
||
// on until it clears WDRF — no activation window in its way.
|
||
if (avr::power::peek_reset_cause().watchdog)
|
||
run_app();
|
||
|
||
link::init();
|
||
await_host();
|
||
|
||
for (;;) {
|
||
// No prompt while an EEPROM write runs: it blocks SPM and fuse reads
|
||
// (§26.2.1), and the prompt is the previous command's completion ack.
|
||
ee::wait();
|
||
tx_ack();
|
||
const std::uint8_t command = link::rx();
|
||
if (command & op_identify) {
|
||
// Identity: the version, then the three signature bytes. Straight
|
||
// out of the stamp, so the wire and the image can never disagree
|
||
// about what this loader is. The indices are constant and the array
|
||
// is constexpr, so these are immediates, not flash reads: nothing
|
||
// here needs the stamp's runtime address. Unsealed, because it
|
||
// takes no argument and changes nothing — and because a command
|
||
// that cannot be got wrong is what a lost host resynchronises on.
|
||
for (std::uint8_t at = stamp_identity; at != sizeof identity_stamp; ++at)
|
||
link::tx(identity_stamp[at]);
|
||
} else {
|
||
// One decode, one cursor and one loop for every space, both
|
||
// directions and the jump: a command per memory would carry a copy
|
||
// of all three each. The jump — the hand-over and staging transfer
|
||
// — carries a selector it ignores so its address rides the same two
|
||
// reads as everything else; the page fill joins the same decode
|
||
// rather than keeping an address form of its own, so flash
|
||
// addressing is uniform across every command that names it. Both
|
||
// carry the count they do not use for the same reason: one header
|
||
// shape is one decode, and one seal covers a fixed set of bytes.
|
||
const std::uint8_t selector = link::rx();
|
||
const std::uint8_t space = space_of(selector);
|
||
const std::uint8_t bank = bank_of(selector);
|
||
std::uint16_t at = rx16();
|
||
std::uint8_t count = link::rx();
|
||
const std::uint8_t sealed = link::rx();
|
||
|
||
// The seal: every field that decides what this command does folded
|
||
// into one byte the host chose, tested before any of it happens.
|
||
//
|
||
// Checked here rather than acknowledged afterwards, which is the
|
||
// whole point. An ack reports a command that has already run, and
|
||
// for the one command that cannot be taken back a report is not a
|
||
// defence. Once the running-slot guard is gone the address is as
|
||
// fatal as the command byte — a wrong one reaches the loader's own
|
||
// page — so the seal covers the act and the place together, and a
|
||
// stream that lost or mangled either cannot produce it.
|
||
//
|
||
// Folded here, after the last read, and never accumulated across
|
||
// the reads: every field is still live at this point because the
|
||
// command needs it anyway, so the fold costs one xor each and no
|
||
// register. An accumulator would have to survive four calls, and
|
||
// paying for that in call-saved registers costs more than the whole
|
||
// check costs in arithmetic — measured at fourteen bytes, on a
|
||
// budget of ten.
|
||
std::uint8_t fold = command;
|
||
fold ^= selector;
|
||
fold ^= static_cast<std::uint8_t>(at);
|
||
fold ^= static_cast<std::uint8_t>(at >> 8);
|
||
fold ^= count;
|
||
fold ^= sealed;
|
||
// The verdict, and it is not a courtesy. Every command whose
|
||
// payload the host sends without waiting — a page fill, a write
|
||
// burst — would otherwise be handed to a loader that has already
|
||
// gone back to reading commands, so a *detected* error would
|
||
// become the desync the seal exists to prevent: a 128-byte page
|
||
// read as command headers is twenty-one more chances at the one in
|
||
// two hundred and fifty-six. Answering the seal before the payload
|
||
// is what keeps a refusal local to the command that earned it.
|
||
//
|
||
// An unknown opcode lands here too — every bit pattern is now some
|
||
// command, so it is the seal, not a table of valid letters, that
|
||
// rejects noise, and the host hears about it either way.
|
||
if (fold != seal) {
|
||
link::tx(nak);
|
||
} else {
|
||
tx_ack();
|
||
if (command & op_jump) {
|
||
link::drain();
|
||
jump(reinterpret_cast<void (*)()>(at));
|
||
} else if (command & op_fill) {
|
||
fill_page(bank, at);
|
||
} else if (space == sp_spm) {
|
||
// An SPM command is the whole of what this loader can do
|
||
// that doing again will not undo, and it is one byte — so
|
||
// it rides the count field, inside the seal, rather than
|
||
// arriving as data after the seal has been checked. Which
|
||
// is also what makes a deliberate lock-bit write
|
||
// expressible, where refusing it outright did not.
|
||
commit(bank, at, count);
|
||
} else {
|
||
do {
|
||
// Direction is one bit of the opcode, so the loop
|
||
// picks it with a one-word skip.
|
||
if (command & op_write) {
|
||
store(space, at, link::rx());
|
||
tx_ack();
|
||
} else
|
||
link::tx(load(space, bank, at));
|
||
++at;
|
||
} while (--count);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
} // namespace
|
||
} // namespace pureboot
|
||
|
||
// stack::hardware: activation is reset-only, so the reset logic's own
|
||
// SP = RAMEND stands wherever the datasheet guarantees it (the classic
|
||
// megas still get the write); a 'J' entry runs on the caller's live stack.
|
||
template struct avr::startup::entry<pureboot::run, avr::startup::stack::hardware>;
|