pureboot: no SPM buffer discard, the host repairs instead
The temporary page buffer is write-once per word, so a page filled over one an earlier writer left dirty programs the stale words. The same datasheet clause carries the cure: the buffer auto-erases after a page write (§26.2.1; §19.2 on the tinies), so the corruption clears itself by happening, and rewriting the page programs correctly. The loader therefore clears the buffer nowhere. The tinies' CTPB and the m48s' RWWSRE discard are gone; the boot-sectioned megas keep only the trailing RWWSRE they need anyway to re-enable the RWW section for read-back, which discards the buffer as a side effect and keeps them off the path entirely. 434 B on the tiny13s, 438-442 on the tiny25/45/85, 430 on the m48s; the megas are unchanged, the 1284s still 506. The host takes over the guarantee: a flash page that reads back wrong is rewritten up to RETRIES times before the run stops. Both read-back paths repair — verify_pages for programming, and write_differing, which is the loader-update path where a page left wrong is a half-written loader slot. That one is not hypothetical: deleting the discard made attiny85 pureboot.rehome fail deterministically there, the only flow still assuming the old contract. Protocol-visible, so README's W command says it: one W may program the wrong bytes after a refused page, or after an application that self-programmed entered without a reset, and a host that programs without reading back cannot trust it. Tests: pureboot.dirty drives the case the loader declines to guard — the fixture application dirties every buffer word and jumps in with no reset (hardware forbids that on a boot-sectioned mega, but simavr dispatches SPM from anywhere, which is what makes it constructible) — and asserts a bare verify sees the corruption, the repairing verify fixes it in one rewrite, and it stays fixed. pbreloc asserts the same shape after a refusal. test_planner covers the bound against a fake device: one bad write repaired in a single rewrite, a page that never comes good stopping after exactly three. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -67,13 +67,11 @@ consteval std::int16_t wdrf_field()
|
||||
// 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.
|
||||
// the operation.
|
||||
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 = 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 —
|
||||
@@ -94,11 +92,11 @@ constexpr std::uint16_t wire_page_mask = word_flash ? (page / 2 - 1) : (page - 1
|
||||
#endif
|
||||
constexpr std::uint8_t timeout_seconds = PUREBOOT_TIMEOUT;
|
||||
|
||||
// The 12-byte info block the host reads with the 'b' command; flash-resident
|
||||
// (there is no crt to copy a .data image), word-aligned so its wire (word)
|
||||
// address is exact on the large chips. The page byte is the wire count
|
||||
// convention: 0 means 256.
|
||||
[[gnu::progmem]] alignas(2) inline constexpr std::array<std::uint8_t, 12> info_data = {
|
||||
// The 12-byte info block the host reads with the 'b' command, flash-resident
|
||||
// through flash_table (there is no crt to copy a .data image, and its storage
|
||||
// carries the word alignment 'b' needs to halve the address on the large
|
||||
// chips). The page byte is the wire count convention: 0 means 256.
|
||||
inline constexpr avr::flash_table<std::array<std::uint8_t, 12>{
|
||||
'P',
|
||||
'B',
|
||||
1, // magic, protocol version
|
||||
@@ -113,7 +111,8 @@ constexpr std::uint8_t timeout_seconds = PUREBOOT_TIMEOUT;
|
||||
// bit 0: host must patch the reset vector (no hardware boot section);
|
||||
// bit 1: flash wire addresses are word addresses
|
||||
static_cast<std::uint8_t>((boot_section ? 0 : 1) | (word_flash ? 2 : 0)),
|
||||
};
|
||||
}>
|
||||
info_data;
|
||||
|
||||
// The serial link. PUREBOOT_USART forces a hardware USART instance,
|
||||
// PUREBOOT_SOFT_SERIAL the polled software UART (no vector — the table
|
||||
@@ -139,16 +138,6 @@ constexpr char usart_digit = '0' + PUREBOOT_USART;
|
||||
constexpr char usart_digit = '0';
|
||||
#endif
|
||||
|
||||
// Whether the chip carries the selected USART: the suffixed instance name,
|
||||
// or — for instance 0 — the classic megas' un-numbered block.
|
||||
consteval bool usart_exists()
|
||||
{
|
||||
const char name[] = {'U', 'S', 'A', 'R', 'T', usart_digit};
|
||||
if (avr::hw::db.has_instance(std::string_view{name, sizeof(name)}))
|
||||
return true;
|
||||
return usart_digit == '0' && avr::hw::db.has_instance("USART");
|
||||
}
|
||||
|
||||
template <avr::hertz_t C>
|
||||
struct hardware_link {
|
||||
using uart = avr::uart::usart<usart_digit, C, {.baud = wire_baud, .max_baud_error = 2.5_pct}>;
|
||||
@@ -219,12 +208,13 @@ struct software_link {
|
||||
};
|
||||
|
||||
#if defined(PUREBOOT_USART)
|
||||
static_assert(usart_exists(), "PUREBOOT_USART selects a hardware USART this chip does not have");
|
||||
static_assert(avr::uart::has_usart<usart_digit>(), "PUREBOOT_USART selects a hardware USART this chip does not have");
|
||||
using link = hardware_link<dev::clock>;
|
||||
#elif defined(PUREBOOT_SOFT_SERIAL)
|
||||
using link = software_link<dev::clock>;
|
||||
#else
|
||||
using link = std::conditional_t<usart_exists(), hardware_link<dev::clock>, software_link<dev::clock>>;
|
||||
using link =
|
||||
std::conditional_t<avr::uart::has_usart<usart_digit>(), hardware_link<dev::clock>, software_link<dev::clock>>;
|
||||
#endif
|
||||
|
||||
// The application's entry, an absolute address the linker pins (--defsym in
|
||||
@@ -274,36 +264,51 @@ std::uint8_t rx_deadline()
|
||||
return link::rx();
|
||||
}
|
||||
|
||||
std::uint16_t rx16()
|
||||
// Inlined into its call sites: reading two bytes across a call otherwise
|
||||
// strands the first in a call-saved register the caller must push/pop; folded
|
||||
// into the (noreturn) command loop that cost disappears.
|
||||
[[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 streamers take the count in the wire's 8-bit form: 0 means 256.
|
||||
// send_flash stays out of line: its two callers ('b' and 'R') otherwise each
|
||||
// inline a private copy of the loop. On the large chips the address is a
|
||||
// word address and the read goes through ELPM (flash_load_far).
|
||||
[[gnu::noinline]] void send_flash(std::uint16_t address, std::uint8_t count)
|
||||
//
|
||||
// Two functions, because they want opposite placement and placement is an
|
||||
// attribute: the byte-addressed loop is small enough to inline into both
|
||||
// callers, the word-addressed one stays out of line but flattened — a call to
|
||||
// the transmit inside it would strand the 24-bit cursor in callee-saved
|
||||
// registers. `word_flash` picks at the call site.
|
||||
[[maybe_unused, gnu::always_inline]] inline void send_flash_near(std::uint16_t address, std::uint8_t count)
|
||||
{
|
||||
if constexpr (word_flash) {
|
||||
// The 24-bit cursor as the machine holds it: the RAMPZ byte and a
|
||||
// 16-bit Z, carried explicitly (the reassembled 32-bit address
|
||||
// folds away inside the inlined far load). A single read never
|
||||
// crosses a 64 KiB boundary — the protocol forbids it and the host
|
||||
// splits its chunks there — so RAMPZ holds for the whole run.
|
||||
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; // robustness for a host that reads across 64 KiB
|
||||
} while (--count);
|
||||
} else {
|
||||
do
|
||||
link::tx(avr::flash_load(reinterpret_cast<const std::uint8_t *>(address++)));
|
||||
while (--count);
|
||||
}
|
||||
do
|
||||
link::tx(avr::flash_load(reinterpret_cast<const std::uint8_t *>(address++)));
|
||||
while (--count);
|
||||
}
|
||||
|
||||
// The 24-bit cursor as the machine holds it: the RAMPZ byte and a 16-bit Z,
|
||||
// carried explicitly (the reassembled 32-bit address folds away inside the
|
||||
// inlined far load).
|
||||
[[maybe_unused, gnu::flatten, gnu::noinline]] 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));
|
||||
// The protocol never reads across 64 KiB, but carrying the wrap is
|
||||
// smaller than the flat 32-bit cursor GCC builds without it.
|
||||
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);
|
||||
}
|
||||
|
||||
void send_eeprom(std::uint16_t address, std::uint8_t count)
|
||||
@@ -331,19 +336,15 @@ void store_eeprom(std::uint16_t address, std::uint8_t count)
|
||||
// itself. `slot_high` is the high byte of that running slot's base (run()
|
||||
// derives it); a broken host thus cannot brick the running loader, and a
|
||||
// copy flashed one slot lower may rewrite the slot above it — how pureboot
|
||||
// updates itself. On the mega the RWW section is re-enabled so reads work
|
||||
// immediately.
|
||||
// updates itself.
|
||||
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 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>();
|
||||
// No discard before the fill: the buffer is write-once per word
|
||||
// (§26.2.1), so filling over one a refused page or an application left
|
||||
// dirty programs stale words — but a page write auto-erases the buffer
|
||||
// (§26.2.1; §19.2 on the tinies), so that write clears the condition and
|
||||
// the host's read-back rewrites the page.
|
||||
|
||||
// One induction either way. On the byte-addressed chips the wire address
|
||||
// itself walks the page (aligned, so the offset bits wrap to zero); on
|
||||
// the word-addressed large chips the wire word address becomes a 32-bit
|
||||
@@ -389,11 +390,14 @@ void program_flash(std::uint16_t wire_address, std::uint8_t slot_high)
|
||||
if constexpr (boot_section)
|
||||
spm::wait();
|
||||
spm::write_page<off>(address);
|
||||
if constexpr (boot_section) {
|
||||
if constexpr (boot_section)
|
||||
spm::wait();
|
||||
spm::rww_enable<off>();
|
||||
}
|
||||
}
|
||||
// The megas program with their RWW section disabled; reads need it back
|
||||
// on. The same store discards the buffer (§26.2.2), so they never meet
|
||||
// the stale-word case above. boot_section implies an RWW section.
|
||||
if constexpr (boot_section)
|
||||
spm::rww_enable<off>();
|
||||
}
|
||||
|
||||
// The four fuse/lock bytes in the hardware's own Z order: low, lock,
|
||||
@@ -449,7 +453,7 @@ void send_fuses()
|
||||
// runtime address is the running slot's. Composed from the two
|
||||
// bytes — the high half is runtime data, so no absolute address
|
||||
// is ever materialized.
|
||||
const auto link_low = reinterpret_cast<std::uint16_t>(info_data.data());
|
||||
const auto link_low = reinterpret_cast<std::uint16_t>(info_data.storage.data());
|
||||
const std::uint8_t low =
|
||||
word_flash ? static_cast<std::uint8_t>(link_low >> 1) : static_cast<std::uint8_t>(link_low);
|
||||
send_flash(static_cast<std::uint16_t>(low | (slot_high << 8)), static_cast<std::uint8_t>(info_data.size()));
|
||||
|
||||
Reference in New Issue
Block a user