The word-addressed 1284s were the one family deploying in a 1 KiB slot, because the far-flash machinery (ELPM reads, RAMPZ page commands, a word-addressed wire) did not fit 512 B. It does now: 478 B stock, 494 B in the heaviest configuration the build can produce. They take the 644s' geometry, where the smallest boot section holds the resident slot and its staging slot together. The loader's version goes to 3; the host tool did not change, so its own version stays 2 and only the window it speaks widens. Most of the saving is one restructure. The info block and a flash read are the same act, so giving all four streamed commands one address-and-count path leaves exactly one call site for the flash streamer: it inlines into the never-returning command loop and its 24-bit cursor stops being saved and restored around every transmit. Around it, the ack byte moved out of line, the wire's byte pair is bit_cast into the word it already is, the fuse loop ends on its count, the info block's in-slot offset is taken as the one-byte relocation it is, and -fno-expensive-optimizations gives way to -fno-move-loop-invariants -fno-tree-ter. Every chip shrank 14-18 B. The size matrix grew the axes it was missing: the USART1 instance across the whole clock ladder, and the shape a slow baud gives a software UART — past 255 delay iterations libavr takes the 16-bit delay loop, which the ladder default never selects and which was 4 B over the 1284's slot the first time it was built. The protocol fixture stopped deriving the loader entry from the flash size; on the 1284s it had been jumping a slot low and reaching the loader only because erased flash walked it up. Docs and comments were consolidated across the port in the same pass: the README carries a per-chip size table instead of prose, and prose that restated the code is gone — 190 lines, no behaviour with it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
117 lines
3.3 KiB
C++
117 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 in the top slot — 512 bytes on every
|
|
// chip. The jump takes a word address, which is what makes the
|
|
// >64 KiB chips' entry reachable through a 16-bit pointer at all.
|
|
constexpr std::uint32_t slot = 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();
|
|
}
|