Measure the host's bit timing at runtime from a 0xC0 calibration pulse, so one clock-agnostic image per chip runs at any F_CPU — the RC-oscillator deployments no longer need a per-clock build. Two source files, differing only in the write-guard/purity tradeoff: pureboot_autobaud_pure.cpp (the measured unit in the GPIOR I/O scratch registers, running-slot write guard dropped, 508 B on the 1284) stays strictly pure; pureboot_autobaud_reg.cpp (unit in one global register variable, guard kept, 512 B) keeps every feature at the cost of that single GRV. Both fit 512/510 on all 37 chips and share two licensed simplifications: a slimmed info block (version + signature; the host derives geometry from the chip database) and a single-byte activation knock. pureboot/autobaud.md records the decision, the hand-assembly floor (506 B) that set the target, and the compiler-knob path to it. Size-tested on every chip via pureboot_add_autobaud(); the fixed-baud loader is untouched. Sim validation, the host calibration handshake, and real-hardware acceptance remain. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
369 lines
12 KiB
C++
369 lines
12 KiB
C++
// 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 <libavr/libavr.hpp>
|
||
|
||
#include <util/delay_basic.h>
|
||
|
||
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<std::size_t>(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 <bool Gpior = have_gpior>
|
||
[[gnu::always_inline]] inline std::uint16_t get_unit()
|
||
{
|
||
if constexpr (Gpior)
|
||
return static_cast<std::uint16_t>(hw::reg_impl<hw::db.reg_index("GPIOR1")>::read() |
|
||
(hw::reg_impl<hw::db.reg_index("GPIOR2")>::read() << 8));
|
||
else
|
||
return unit_backing;
|
||
}
|
||
|
||
template <bool Gpior = have_gpior>
|
||
[[gnu::always_inline]] inline void put_unit(std::uint16_t u)
|
||
{
|
||
if constexpr (Gpior) {
|
||
hw::reg_impl<hw::db.reg_index("GPIOR1")>::write(static_cast<std::uint8_t>(u));
|
||
hw::reg_impl<hw::db.reg_index("GPIOR2")>::write(static_cast<std::uint8_t>(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<avr::PUREBOOT_RX, avr::io::pull::up>;
|
||
using out_t = avr::io::output<avr::PUREBOOT_TX>;
|
||
|
||
static void init()
|
||
{
|
||
avr::init<in_t, out_t>();
|
||
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<std::uint16_t>(count >> 2);
|
||
put_unit(u);
|
||
return u;
|
||
}
|
||
|
||
static std::uint8_t rx()
|
||
{
|
||
const std::uint16_t unit = get_unit();
|
||
while (in_t::read()) // await the start edge
|
||
;
|
||
_delay_loop_2(static_cast<std::uint16_t>(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 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<std::uint16_t>(low | (link::rx() << 8));
|
||
}
|
||
|
||
[[gnu::always_inline]] inline std::uint16_t word_of(std::array<std::uint8_t, 2> pair)
|
||
{
|
||
return std::bit_cast<std::uint16_t>(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<const std::uint8_t *>(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<std::uint8_t>(address >> 15);
|
||
std::uint16_t z = static_cast<std::uint16_t>(address << 1);
|
||
do {
|
||
link::tx(avr::flash_load_far<std::uint8_t>((static_cast<std::uint32_t>(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<off>(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<std::uint8_t>(wire_address >> 15);
|
||
const std::uint16_t z0 = static_cast<std::uint16_t>(wire_address << 1) & ~static_cast<std::uint16_t>(page - 1);
|
||
std::uint16_t z = z0;
|
||
do {
|
||
std::uint8_t low = link::rx();
|
||
std::uint8_t high = link::rx();
|
||
spm::fill<off>((static_cast<spm::flash_address_t>(rampz) << 16) | z, word_of({low, high}));
|
||
z += 2;
|
||
} while (static_cast<std::uint8_t>(z));
|
||
address = (static_cast<spm::flash_address_t>(rampz) << 16) | z0;
|
||
} else {
|
||
address = static_cast<spm::flash_address_t>(wire_address & ~static_cast<std::uint16_t>(page - 1));
|
||
do {
|
||
std::uint8_t low = link::rx();
|
||
std::uint8_t high = link::rx();
|
||
spm::fill<off>(address, word_of({low, high}));
|
||
address += 2;
|
||
} while (static_cast<std::uint8_t>(address) & (page - 1));
|
||
address -= 2; // back inside the page — erase and write ignore the word bits
|
||
}
|
||
spm::erase_page<off>(address);
|
||
if constexpr (boot_section)
|
||
spm::wait();
|
||
spm::write_page<off>(address);
|
||
if constexpr (boot_section)
|
||
spm::wait();
|
||
if constexpr (boot_section)
|
||
spm::rww_enable<off>();
|
||
}
|
||
|
||
void send_fuses()
|
||
{
|
||
std::uint8_t which = 0;
|
||
do
|
||
link::tx(spm::read_fuse<off>(static_cast<spm::fuse>(which)));
|
||
while (++which != 4);
|
||
}
|
||
|
||
[[noreturn]] void run()
|
||
{
|
||
if (hw::field_impl<wdrf_field()>::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() == '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<void (*)()>(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<pureboot::run>;
|