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>
116 lines
3.3 KiB
C++
116 lines
3.3 KiB
C++
// Test-fixture application for the pureboot protocol tests: prints "APP" on
|
|
// the chip's serial link (the same link the loader uses) — the proof that
|
|
// the loader's hand-over, and on the tinies the host's reset-vector
|
|
// surgery, actually launched it. Linked normally (crt, vectors at 0); on
|
|
// the tinies its reset vector is the rjmp the host re-homes.
|
|
//
|
|
// On the hardware-USART link it then listens, and an 'L' makes it jump into
|
|
// the resident loader — the application-owned loader entry a
|
|
// BOOTRST-unprogrammed mega relies on (reset always boots the application
|
|
// there), exercised by the self-update tests. The software link idles:
|
|
// reset reaches those loaders through the patched vector (or the runner
|
|
// models BOOTRST), so the application owes them nothing.
|
|
//
|
|
// The fixture speaks the deployment its loader was built for: the same
|
|
// PUREBOOT_* defines configure it, and without them it assumes the stock
|
|
// deployment (the crystal/RC clock table below, the chip's natural link).
|
|
#include <libavr/libavr.hpp>
|
|
|
|
using namespace avr::literals;
|
|
|
|
namespace {
|
|
|
|
consteval avr::hertz_t clock()
|
|
{
|
|
#if defined(PUREBOOT_CLOCK_HZ)
|
|
return avr::hertz_t{PUREBOOT_CLOCK_HZ};
|
|
#else
|
|
auto name = std::string_view{avr::hw::db.name};
|
|
if (name.starts_with("ATtiny13"))
|
|
return 9.6_MHz;
|
|
if (name.starts_with("ATtiny"))
|
|
return 8_MHz;
|
|
return 16_MHz;
|
|
#endif
|
|
}
|
|
|
|
#if !defined(PUREBOOT_TX)
|
|
#define PUREBOOT_TX pb1
|
|
#endif
|
|
#if !defined(PUREBOOT_USART)
|
|
#define PUREBOOT_USART 0
|
|
#endif
|
|
|
|
consteval bool use_hardware()
|
|
{
|
|
#if defined(PUREBOOT_SOFT_SERIAL)
|
|
return false;
|
|
#else
|
|
return avr::hw::db.has_instance("USART0") || avr::hw::db.has_instance("USART");
|
|
#endif
|
|
}
|
|
|
|
using dev = avr::device<{.clock = clock()}>;
|
|
|
|
template <avr::hertz_t C, bool Hardware = use_hardware()>
|
|
struct link {
|
|
#if defined(PUREBOOT_BAUD)
|
|
static constexpr avr::baud_t baud{PUREBOOT_BAUD};
|
|
#else
|
|
static constexpr avr::baud_t baud{115200};
|
|
#endif
|
|
using tx_t = avr::uart::usart<'0' + PUREBOOT_USART, C, {.baud = baud, .max_baud_error = 2.5_pct}>;
|
|
static void tx(char c)
|
|
{
|
|
tx_t::write(static_cast<std::uint8_t>(c));
|
|
}
|
|
[[noreturn]] static void idle()
|
|
{
|
|
// 'L' hands back to the loader at the top slot — 512 bytes, or the
|
|
// 1 KiB the >64 KiB chips use.
|
|
constexpr std::uint32_t slot = avr::hw::db.mem.flash_size > 65536 ? 1024 : 512;
|
|
for (;;) {
|
|
auto command = tx_t::read_blocking();
|
|
if (command == 'L')
|
|
reinterpret_cast<void (*)()>(static_cast<std::uint16_t>((avr::hw::db.mem.flash_size - slot) / 2))();
|
|
// 'D' leaves every word of the SPM page buffer dirty, so that a
|
|
// following 'L' enters the loader with the buffer it never clears.
|
|
if (command == 'D') {
|
|
for (std::uint16_t at = 0; at < avr::spm::page_bytes; at += 2)
|
|
avr::spm::fill(at, 0xdead);
|
|
tx('D');
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
template <avr::hertz_t C>
|
|
struct link<C, false> {
|
|
#if defined(PUREBOOT_BAUD)
|
|
static constexpr avr::baud_t baud{PUREBOOT_BAUD};
|
|
#else
|
|
static constexpr avr::baud_t baud{57600};
|
|
#endif
|
|
using tx_t = avr::uart::software_tx<C, avr::PUREBOOT_TX, baud>;
|
|
static void tx(char c)
|
|
{
|
|
tx_t::write(static_cast<std::uint8_t>(c));
|
|
}
|
|
[[noreturn]] static void idle()
|
|
{
|
|
while (true) {
|
|
}
|
|
}
|
|
};
|
|
|
|
} // namespace
|
|
|
|
int main()
|
|
{
|
|
avr::init<typename link<dev::clock>::tx_t>();
|
|
link<dev::clock>::tx('A');
|
|
link<dev::clock>::tx('P');
|
|
link<dev::clock>::tx('P');
|
|
link<dev::clock>::idle();
|
|
}
|