pureboot: the device — one pure C++ source, 512 bytes, every chip
No inline assembly, no global register variables; libavr does the datasheet work. The device speaks primitives — flash read/page-program, EEPROM read/write, fuse read, info block, EEPROM-resident activation timeout, hand-over — and verify, erase, reset-vector surgery, and timeout configuration live in the host tool. 490 B on the ATtiny13A, 510 B on the ATtiny85, 484 B on the ATmega328P, each linked into the top 512 bytes of flash; per-chip size tests gate all three. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
326
pureboot/pureboot.cpp
Normal file
326
pureboot/pureboot.cpp
Normal file
@@ -0,0 +1,326 @@
|
||||
// pureboot — a serial bootloader on libavr, pure by constraint: one C++
|
||||
// source with no inline assembly and no global register variables, built for
|
||||
// every chip libavr targets, 512 bytes on each. The device speaks primitives
|
||||
// — read/program flash, read/write EEPROM, fuse bytes, an info block, run —
|
||||
// and everything composite (verify, erase, reset-vector surgery on the
|
||||
// tinies, timeout configuration) lives in the host tool. Protocol reference:
|
||||
// README.md next to this file.
|
||||
//
|
||||
// Entry: reset lands in avr::startup::entry below (BOOTRST on the mega; the
|
||||
// patched reset vector — or erased flash walking up into the loader — on the
|
||||
// tinies). A watchdog reset hands straight to the application. Otherwise the
|
||||
// host has one activation window — EEPROM's last cell, in seconds — to knock
|
||||
// ("pb"); an idle line boots the application. A session then stays in the
|
||||
// command loop until 'G' hands over or the chip resets.
|
||||
|
||||
#include <libavr/libavr.hpp>
|
||||
|
||||
using namespace avr::literals;
|
||||
namespace spm = avr::spm;
|
||||
namespace ee = avr::eeprom;
|
||||
|
||||
namespace pureboot {
|
||||
namespace {
|
||||
|
||||
// Purely polled — interrupts stay off, every guard folds to nothing.
|
||||
constexpr auto off = avr::irq::guard_policy::unused;
|
||||
|
||||
constexpr std::uint8_t ack = '+';
|
||||
|
||||
// Per-chip personality, from the chip database: the clocks the dogfood
|
||||
// boards run (16 MHz crystal on the mega, calibrated RC on the tinies) and
|
||||
// the device signature (compile-time data — the tiny13A cannot even read its
|
||||
// signature row from code).
|
||||
consteval avr::hertz_t clock()
|
||||
{
|
||||
if (avr::hw::db.name == "ATtiny13A")
|
||||
return 9.6_MHz;
|
||||
if (avr::hw::db.name == "ATtiny85")
|
||||
return 8_MHz;
|
||||
return 16_MHz;
|
||||
}
|
||||
|
||||
consteval std::array<std::uint8_t, 3> signature()
|
||||
{
|
||||
if (avr::hw::db.name == "ATtiny13A")
|
||||
return {0x1e, 0x90, 0x07};
|
||||
if (avr::hw::db.name == "ATtiny85")
|
||||
return {0x1e, 0x93, 0x0b};
|
||||
return {0x1e, 0x95, 0x0f};
|
||||
}
|
||||
|
||||
using dev = avr::device<{.clock = clock()}>;
|
||||
|
||||
// Geometry: the loader owns the top 512 bytes of flash; the byte below it is
|
||||
// the trampoline word (the application's relocated reset vector) on chips
|
||||
// without a hardware boot section. The RWWSRE bit marks a separate boot
|
||||
// section — on classic AVR the two capabilities coincide.
|
||||
constexpr std::uint16_t boot_bytes = 512;
|
||||
constexpr std::uint16_t base = static_cast<std::uint16_t>(spm::flash_bytes - boot_bytes);
|
||||
constexpr std::uint16_t page = spm::page_bytes;
|
||||
constexpr bool boot_section = avr::hw::db.field_index("SPMCSR", "RWWSRE") >= 0;
|
||||
|
||||
// The activation timeout lives in EEPROM's last cell, in seconds; the host
|
||||
// rewrites it with the ordinary EEPROM-write command. An unprogrammed cell —
|
||||
// 0x00 or the erased 0xff — means the 4 s default: a stray value can never
|
||||
// floor the window to nothing and lock the loader out, and erasing the whole
|
||||
// EEPROM resets the timeout instead of maxing it to 255 s.
|
||||
constexpr std::uint16_t timeout_cell = avr::hw::db.mem.eeprom_size - 1;
|
||||
constexpr std::uint8_t default_seconds = 4;
|
||||
|
||||
// The 12-byte info block the host reads with the 'b' command; flash-resident
|
||||
// (there is no crt to copy a .data image).
|
||||
inline constexpr std::array<std::uint8_t, 12> info_data = {
|
||||
'P',
|
||||
'B',
|
||||
1, // magic, protocol version
|
||||
signature()[0],
|
||||
signature()[1],
|
||||
signature()[2],
|
||||
static_cast<std::uint8_t>(page),
|
||||
base & 0xff,
|
||||
base >> 8, // app flash ends here; loader base
|
||||
avr::hw::db.mem.eeprom_size & 0xff,
|
||||
avr::hw::db.mem.eeprom_size >> 8,
|
||||
boot_section ? 0 : 1, // bit 0: host must patch the reset vector (no hardware boot section)
|
||||
};
|
||||
using info = avr::flash_table<info_data>;
|
||||
|
||||
// The serial link: the hardware USART where the chip has one, the polled
|
||||
// software UART (no vector — the table belongs to the application) on PB0/PB1
|
||||
// elsewhere. Both are class templates on the clock so only the selected
|
||||
// backend is ever instantiated. pending() is the cheap line test the
|
||||
// activation window polls; rx() then picks the byte up.
|
||||
template <avr::hertz_t C>
|
||||
consteval std::int16_t rxc_field()
|
||||
{
|
||||
return avr::hw::db.field_index("UCSR0A", "RXC0");
|
||||
}
|
||||
|
||||
template <avr::hertz_t C>
|
||||
struct hardware_link {
|
||||
using uart = avr::uart::usart0<C, {.baud = 115200_Bd, .max_baud_error = 2.5_pct}>;
|
||||
|
||||
static void init()
|
||||
{
|
||||
avr::init<uart>();
|
||||
}
|
||||
|
||||
static bool pending()
|
||||
{
|
||||
return avr::hw::field_impl<rxc_field<C>()>::test();
|
||||
}
|
||||
|
||||
static std::uint8_t rx()
|
||||
{
|
||||
return uart::read_blocking();
|
||||
}
|
||||
|
||||
static void tx(std::uint8_t byte)
|
||||
{
|
||||
uart::write(byte);
|
||||
}
|
||||
};
|
||||
|
||||
template <avr::hertz_t C>
|
||||
struct software_link {
|
||||
using rx_t = avr::uart::software_rx_polled<C, avr::pb0, 57600_Bd>;
|
||||
using tx_t = avr::uart::software_tx<C, avr::pb1, 57600_Bd>;
|
||||
|
||||
static void init()
|
||||
{
|
||||
avr::init<rx_t, tx_t>();
|
||||
}
|
||||
|
||||
static bool pending()
|
||||
{
|
||||
return !avr::io::input<avr::pb0>::read(); // a start bit has begun
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
using link = std::conditional_t<avr::hw::db.has_reg("UDR0"), hardware_link<dev::clock>, software_link<dev::clock>>;
|
||||
|
||||
// The application's entry: the linker pins pureboot_app to 0x0000 on the
|
||||
// mega (reset re-vectors here through BOOTRST, so address 0 stays the
|
||||
// application's own vector) and to the trampoline word at base - 2 on the
|
||||
// tinies (--defsym in CMakeLists.txt).
|
||||
extern "C" [[noreturn]] void pureboot_app();
|
||||
|
||||
[[noreturn]] void run_app()
|
||||
{
|
||||
pureboot_app();
|
||||
}
|
||||
|
||||
// One activation tick is 65536 pending() polls — a pin (or flag) test plus a
|
||||
// 16-bit countdown, about 8 cycles. Whole-second precision is all the
|
||||
// timeout cell promises; the seconds count stays a loop bound (a runtime
|
||||
// multiply would drag libgcc's __mulhi3 into the MUL-less tinies).
|
||||
consteval std::uint16_t ticks_per_second()
|
||||
{
|
||||
return static_cast<std::uint16_t>(dev::clock.hz / (65536ull * 8u));
|
||||
}
|
||||
static_assert(ticks_per_second() >= 1);
|
||||
|
||||
bool pending_before(std::uint8_t seconds)
|
||||
{
|
||||
do {
|
||||
std::uint16_t ticks = ticks_per_second();
|
||||
do {
|
||||
std::uint16_t spins = 0; // wraps first, so 65536 polls per tick
|
||||
do {
|
||||
if (link::pending())
|
||||
return true;
|
||||
} while (--spins);
|
||||
} while (--ticks);
|
||||
} while (--seconds);
|
||||
return false;
|
||||
}
|
||||
|
||||
// A knock byte under the activation deadline: an idle line means no host is
|
||||
// there, and the application runs.
|
||||
std::uint8_t rx_deadline(std::uint8_t seconds)
|
||||
{
|
||||
if (!pending_before(seconds))
|
||||
run_app();
|
||||
return link::rx();
|
||||
}
|
||||
|
||||
std::uint16_t rx16()
|
||||
{
|
||||
std::uint8_t low = link::rx();
|
||||
return static_cast<std::uint16_t>(low | (link::rx() << 8));
|
||||
}
|
||||
|
||||
const std::uint8_t *flash_ptr(std::uint16_t address)
|
||||
{
|
||||
return reinterpret_cast<const std::uint8_t *>(address);
|
||||
}
|
||||
|
||||
// The streamers take the count in the wire's 8-bit form: 0 means 256.
|
||||
void send_flash(std::uint16_t address, std::uint8_t count)
|
||||
{
|
||||
do
|
||||
link::tx(avr::flash_load(flash_ptr(address++)));
|
||||
while (--count);
|
||||
}
|
||||
|
||||
void send_eeprom(std::uint16_t address, std::uint8_t count)
|
||||
{
|
||||
do
|
||||
link::tx(ee::read(address++));
|
||||
while (--count);
|
||||
}
|
||||
|
||||
// EEPROM write, host-paced: each ack goes out once the byte's write has
|
||||
// begun, so the next byte arrives while it completes and the following
|
||||
// write's own ready-wait sees an idle line. Nothing is ever missed, on
|
||||
// either serial backend, without a buffer.
|
||||
void store_eeprom(std::uint16_t address, std::uint8_t count)
|
||||
{
|
||||
do {
|
||||
ee::write<off>(address++, link::rx());
|
||||
link::tx(ack);
|
||||
} while (--count);
|
||||
}
|
||||
|
||||
// One flash page: stream the bytes into the SPM buffer as little-endian
|
||||
// words, then erase and program. Addresses in the loader's own 512 bytes
|
||||
// are drained but never programmed — a broken host cannot brick the chip.
|
||||
// On the mega the RWW section is re-enabled so reads work immediately.
|
||||
void program_flash(std::uint16_t address)
|
||||
{
|
||||
for (std::uint16_t i = 0; i < page; i += 2) {
|
||||
std::uint8_t low = link::rx();
|
||||
std::uint8_t high = link::rx();
|
||||
spm::fill<off>(address + i, static_cast<std::uint16_t>(low | (high << 8)));
|
||||
}
|
||||
if (address < base) {
|
||||
spm::erase_page<off>(address);
|
||||
spm::wait();
|
||||
spm::write_page<off>(address);
|
||||
spm::wait();
|
||||
if constexpr (boot_section)
|
||||
spm::rww_enable<off>();
|
||||
}
|
||||
}
|
||||
|
||||
// The four fuse/lock bytes in the hardware's own Z order: low, lock,
|
||||
// extended, high. Writing fuses is not a thing self-programming can do on
|
||||
// AVR — SPM reaches flash (and boot lock bits) only.
|
||||
void send_fuses()
|
||||
{
|
||||
for (std::uint8_t which = 0; which < 4; ++which)
|
||||
link::tx(spm::read_fuse<off>(static_cast<spm::fuse>(which)));
|
||||
}
|
||||
|
||||
[[noreturn]] void run()
|
||||
{
|
||||
// A watchdog reset belongs to the application (whose watchdog stays
|
||||
// forced on until it clears WDRF) — no activation window in its way.
|
||||
if (avr::hw::mcusr::wdrf.test())
|
||||
run_app();
|
||||
|
||||
link::init();
|
||||
|
||||
std::uint8_t seconds = ee::read(timeout_cell);
|
||||
if (seconds == 0 || seconds == 0xff)
|
||||
seconds = default_seconds;
|
||||
|
||||
// The knock: 'p' then 'b', each under a fresh window; any other byte is
|
||||
// line noise and waits again. Falling out of a window runs the app.
|
||||
while (rx_deadline(seconds) != 'p' || rx_deadline(seconds) != 'b') {
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
// No prompt while an EEPROM write runs: a pending write blocks SPM
|
||||
// and fuse reads (§26.2.1), and the ack tells the host all is done.
|
||||
ee::wait();
|
||||
link::tx(ack);
|
||||
switch (link::rx()) {
|
||||
case 'b': // info block
|
||||
send_flash(reinterpret_cast<std::uint16_t>(info::storage.data()), info::size());
|
||||
break;
|
||||
case 'R': { // read flash: addr16, n8 (0 = 256)
|
||||
std::uint16_t address = rx16();
|
||||
send_flash(address, link::rx());
|
||||
break;
|
||||
}
|
||||
case 'W': // program one flash page: addr16, page bytes
|
||||
program_flash(rx16());
|
||||
break;
|
||||
case 'r': { // read EEPROM: addr16, n8
|
||||
std::uint16_t address = rx16();
|
||||
send_eeprom(address, link::rx());
|
||||
break;
|
||||
}
|
||||
case 'w': { // write EEPROM: addr16, n8, then n bytes each acked
|
||||
std::uint16_t address = rx16();
|
||||
store_eeprom(address, link::rx());
|
||||
break;
|
||||
}
|
||||
case 'F': // fuse and lock bytes
|
||||
send_fuses();
|
||||
break;
|
||||
case 'G': // hand over to the application
|
||||
link::tx(ack);
|
||||
run_app();
|
||||
default: // unknown bytes are ignored; the loop re-acks
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace pureboot
|
||||
|
||||
template struct avr::startup::entry<pureboot::run>;
|
||||
Reference in New Issue
Block a user