pureboot: every libavr chip — 37 loaders, the m48 class, the 644 geometry

The chip table becomes family blocks covering all 37 targets. The m48s
are a new deployment class: no boot section, so the tiny profile spoken
over the hardware USART — host-patched reset vector, trampoline
hand-over, a 510-byte budget (474 B built), no fuse preflight — while
their RWWSRE store stays the buffer discard (Atmel-8271 §26.2); the
device keys the patch flag and the CPU-halt waits on the curated
boot-section capability and the discard on the RWWSRE bit itself. The
644s' 64 KiB is exactly the 16-bit byte space: plain LPM, byte wire
addresses, 498 B in a 512-byte slot — and their 1 KiB minimum boot
section holds the resident and staging slots together, so self-update
needs no fuse step (the update test's slot pick now keys word-flash on
base >= 64 KiB; base + slot merely touching the boundary stays
byte-addressed). The 1284 joins the 1284P's word-addressed 1 KiB slot at
558 B. BOOT_FUSE gains every boot-sectioned family's ladder and fuse
byte; the planner exercises them all. The sim scaffolding keys
patch-vector-ness instead of the atmega name prefix, the fixture app
picks its clock by family (the tiny25/45/13 builds surfaced the 16 MHz
fallthrough as garbled banners), and the runner's wrapped flash ioctl
performs the m48 discard simavr's no-RWW cores turn into a stray buffer
fill. Sizes across the fleet: 466-504 B megas, 474 B m48s, 498 B 644s,
488-502 B tinies, 558 B 1284s — every chip passing
size/pi/planner/protocol/reloc/update.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 18:53:04 +02:00
parent b7d082ff11
commit 9cbe2d682d
11 changed files with 2059 additions and 661 deletions

View File

@@ -16,9 +16,10 @@
// resident — how pureboot updates itself, host-driven, with no other
// firmware involved.
//
// 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
// Entry: reset lands in avr::startup::entry below (BOOTRST on the
// boot-sectioned megas; the patched reset vector — or erased flash walking
// up into the loader — on the tinies and the boot-section-less m48s). A
// watchdog reset hands straight to the application. Otherwise the
// host has one activation window per awaited knock byte ("pb"); an idle line
// boots the application. A session then stays in the command loop until 'J'
// jumps away or the chip resets.
@@ -44,9 +45,10 @@ constexpr std::uint8_t ack = '+';
// from code.
consteval avr::hertz_t clock()
{
if (avr::hw::db.name == "ATtiny13A")
auto name = std::string_view{avr::hw::db.name};
if (name.starts_with("ATtiny13"))
return 9.6_MHz;
if (avr::hw::db.name == "ATtiny85")
if (name.starts_with("ATtiny"))
return 8_MHz;
return 16_MHz;
}
@@ -62,21 +64,19 @@ consteval std::int16_t wdrf_field()
// Geometry: the resident loader owns the top slot of flash — 512 bytes,
// except on the >64 KiB chips whose own smallest boot sector is 1 KiB (the
// 1284P): there the slot is 1 KiB, matching the hardware boundary the
// 1284s): there the slot is 1 KiB, matching the hardware boundary the
// 512-byte figure comes from everywhere else. The word below the slot is
// the trampoline (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 (the m8/m32 packs
// spell its register SPMCR).
// without a hardware boot section — the tinies and the m48s, whose SPM
// runs from anywhere (Atmel-8271 §26). A boot section also means the CPU
// runs on while the RWW section programs; everywhere else it halts through
// the operation. The m48s still carry RWWSRE as their temporary-buffer
// discard (§26.2), so the discard picks by that bit, not by the section.
constexpr std::uint16_t slot_bytes = spm::flash_bytes > 65536 ? 1024 : 512;
constexpr std::uint32_t base = spm::flash_bytes - slot_bytes;
constexpr std::uint16_t page = spm::page_bytes;
constexpr bool boot_section = [] {
for (auto reg : {"SPMCSR", "SPMCR"})
if (avr::hw::db.field_index(reg, "RWWSRE") >= 0)
return true;
return false;
}();
constexpr bool boot_section = avr::hw::curated::has_boot_section();
constexpr bool rww_discard = spm::detail::has_rww();
// Past 64 KiB a byte address no longer fits the wire's 16 bits, so on the
// large chips every flash address on the wire — and all slot arithmetic —
@@ -85,7 +85,8 @@ constexpr bool boot_section = [] {
// 2 x 256 words), so the slot index is the high byte with its low bit
// dropped everywhere.
constexpr bool word_flash = spm::flash_bytes > 65536;
constexpr std::uint16_t wire_base = word_flash ? static_cast<std::uint16_t>(base / 2) : static_cast<std::uint16_t>(base);
constexpr std::uint16_t wire_base =
word_flash ? static_cast<std::uint16_t>(base / 2) : static_cast<std::uint16_t>(base);
constexpr std::uint16_t wire_page_mask = word_flash ? (page / 2 - 1) : (page - 1);
// The activation window, in seconds, is a compile-time constant (the build
@@ -332,9 +333,10 @@ void program_flash(std::uint16_t wire_address, std::uint8_t slot_high)
{
// A buffer word cannot be loaded twice without an erase (§26.2.1), so a
// refused page's drained data must not linger for the next write:
// discard the buffer up front — CTPB on the tinies; on the mega writing
// RWWSRE aborts a pending load (§26.2.2).
if constexpr (boot_section)
// discard the buffer up front — CTPB on the tinies; on the megas
// writing RWWSRE aborts a pending load (§26.2.2 — on the m48s that
// flush is the bit's whole documented job).
if constexpr (rww_discard)
spm::rww_enable<off>();
else
spm::clear_buffer<off>();
@@ -376,8 +378,9 @@ void program_flash(std::uint16_t wire_address, std::uint8_t slot_high)
page_high = static_cast<std::uint8_t>(address >> 8) & 0xfe;
}
if (page_high != slot_high) {
// The tinies halt the CPU through the erase and the write, so only
// the megas — running on while their RWW section programs — wait.
// The tinies and the m48s halt the CPU through the erase and the
// write, so only the boot-sectioned megas — running on while their
// RWW section programs — wait.
spm::erase_page<off>(address);
if constexpr (boot_section)
spm::wait();
@@ -416,8 +419,8 @@ void send_fuses()
// program_flash refuses this one slot and the info block is addressed
// from it, so both follow wherever the code was flashed.
const std::uint16_t ra_words = reinterpret_cast<std::uint16_t>(__builtin_return_address(0));
const std::uint8_t slot_high = word_flash ? static_cast<std::uint8_t>(ra_words >> 8) & 0xfe
: static_cast<std::uint8_t>((ra_words >> 8) << 1);
const std::uint8_t slot_high =
word_flash ? static_cast<std::uint8_t>(ra_words >> 8) & 0xfe : static_cast<std::uint8_t>((ra_words >> 8) << 1);
// 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.