pureboot 5: one command pair for every memory, and a clock-free backend
R/r/w/F collapse into G and g over a selector byte naming the space — flash,
EEPROM, data, fuse, SPM — with the flash bank in its high nibble. Four command
bodies, four transfer loops and four argument decodes become one of each, and
W joins the same decode instead of keeping an address form of its own. The
loader shrinks while gaining everything below: on the 1284P the stock build
goes 480 -> 432 B and the software one 496 -> 450.
What the freed space buys:
- Data space. On AVR one pointer spans SRAM, the register file and the whole
I/O space, so G over space 2 reads all three. pureboot keeps zero static
RAM and pushes no register, so at loader entry an application's SRAM is
still what the application left there — this is a post-mortem, not just a
poke hole. As its own command it needed a dispatch arm and a loop; as one
more space it is a single ld/st.
- Host-issued SPM. W fills the page buffer and stops; erase, write and RWW
re-enable are writes to space 4, which reach the same fused store-and-SPM
pair through the transfer's own address and data. Any SPM operation, lock
bits included, is now reachable and the loader carries no page-commit logic.
The four-cycle SPMCSR-to-SPM window is why that primitive stays fused: no
host can hit it across a serial link, and that — not the byte count — is
the floor on how low-level a bootloader's primitives can go.
- Byte addresses everywhere. The bank in the selector retires the
word-addressed wire the >64 KiB parts needed, so the 1284s stop being the
outlier.
SERIAL autobaud is a third backend on the same loader, over libavr's
software_autobaud: no clock, no baud, one binary per chip for every F_CPU and
every rate. Activation counts poll iterations rather than seconds and bounds
every wait, so a stray pulse cannot hold an unattended device.
b answers with the version and signature only; the host derives geometry from
the signature, which is what an autobaud build requires anyway. An update image
is a bare slot with no device to ask, so every image carries a six-byte stamp —
the same bytes b answers with, and the source of both — that the loader never
reads from flash and the host refuses to install a mismatch against. The
running-slot write guard moved onto the SPM commit, which covers erase and
write both where guarding W covered neither directly.
The position-independence lint now proves the property instead of a proxy for
it: the image must come out byte-identical linked at a different base.
-fno-move-loop-invariants left the tuned flag set — it was fitted to a command
loop carrying four transfer bodies and costs bytes now that it carries one.
Verified: the exhaustive matrix on all 37 chips (every clock x every baud x
every backend, non-standard rates included, plus the autobaud build) —
8174 size checks, no failures, tightest fit the 1284s' autobaud at 510 of 512.
Behavioral suites green on every chip class: t13a 10/10, t85 11/11, m8 13/13,
m16a 13/13, m48pa 13/13, 328P 23/23, 644A 17/17, 1284P 17/17. Data-space
round trip through --peek/--poke and the autobaud handshake are both red-green
proven.
The two prototype sources and their findings file go; the README carries the
protocol and dev/done.md in libavr carries the reasoning.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -26,14 +26,17 @@ constexpr std::uint8_t ack = '+';
|
||||
|
||||
// Deployment parameters come from the build (pureboot_add_loader()). The
|
||||
// signature is not one of them: the chip database is the only universal
|
||||
// source — a tiny13A cannot read its own signature row from code.
|
||||
#if !defined(PUREBOOT_CLOCK_HZ) || !defined(PUREBOOT_BAUD)
|
||||
// source — a tiny13A cannot read its own signature row from code. An autobaud
|
||||
// build carries no clock and no baud at all; it measures both.
|
||||
#if !defined(PUREBOOT_AUTOBAUD) && (!defined(PUREBOOT_CLOCK_HZ) || !defined(PUREBOOT_BAUD))
|
||||
#error \
|
||||
"PUREBOOT_CLOCK_HZ and PUREBOOT_BAUD select this build's clock and baud — create loader targets with pureboot_add_loader() (README.md)"
|
||||
"PUREBOOT_CLOCK_HZ and PUREBOOT_BAUD select this build's clock and baud — create loader targets with pureboot_add_loader(), or PUREBOOT_AUTOBAUD for a clock-free one (README.md)"
|
||||
#endif
|
||||
|
||||
#if !defined(PUREBOOT_AUTOBAUD)
|
||||
using dev = avr::device<{.clock = avr::hertz_t{PUREBOOT_CLOCK_HZ}}>;
|
||||
constexpr avr::baud_t wire_baud{PUREBOOT_BAUD};
|
||||
#endif
|
||||
|
||||
// The watchdog reset flag's home: MCUSR, or the classic megas' MCUCSR.
|
||||
consteval std::int16_t wdrf_field()
|
||||
@@ -47,60 +50,110 @@ consteval std::int16_t wdrf_field()
|
||||
// runs from anywhere (Atmel-8271 §26) — keep the application's relocated
|
||||
// reset vector in the word under the slot.
|
||||
constexpr std::uint16_t slot_bytes = 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();
|
||||
|
||||
// Past 64 KiB a byte address no longer fits the wire's 16 bits, so flash
|
||||
// addresses there are word addresses ('J' always was one). A slot is 256 of
|
||||
// those — one value of a wire address's high byte, where 512 bytes span two.
|
||||
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);
|
||||
// Past 64 KiB one bank of flash does not cover the chip, so a transfer's
|
||||
// selector byte carries the bank and the wire address stays a byte address
|
||||
// within it. 'J' is the exception: it is a word address everywhere, because
|
||||
// that is what the hardware's own jump takes.
|
||||
constexpr bool banked_flash = spm::flash_bytes > 65536;
|
||||
|
||||
// A compile-time window, so the whole EEPROM belongs to the application;
|
||||
// re-timing a deployed loader is a self-update with a re-timed build.
|
||||
// re-timing a deployed loader is a self-update with a re-timed build. An
|
||||
// autobaud build has no clock to convert seconds against and counts polls.
|
||||
#if !defined(PUREBOOT_TIMEOUT)
|
||||
#define PUREBOOT_TIMEOUT 8
|
||||
#endif
|
||||
constexpr std::uint8_t timeout_seconds = PUREBOOT_TIMEOUT;
|
||||
|
||||
#if !defined(PUREBOOT_AUTOBAUD_POLLS)
|
||||
#define PUREBOOT_AUTOBAUD_POLLS 4000000
|
||||
#endif
|
||||
constexpr avr::uint24_t autobaud_budget = PUREBOOT_AUTOBAUD_POLLS;
|
||||
|
||||
// The loader's one identity number. The protocol carries none of its own —
|
||||
// a version implies it, and the host tool holds that map (README.md).
|
||||
constexpr std::uint8_t version = 4;
|
||||
constexpr std::uint8_t version = 5;
|
||||
|
||||
// The 'b' reply, byte for byte (layout: README.md). Flash-resident because
|
||||
// no crt copies a .data image — and flash_table's storage carries the word
|
||||
// alignment 'b' needs to halve the address on the large chips.
|
||||
// One wire byte per line: this is the reply's layout, not a list.
|
||||
// The image's identity stamp, for the host tool rather than for the wire: an
|
||||
// update image is a bare 512-byte slot, and without this nothing in it says
|
||||
// which chip it was built for. The tool refuses to install an image whose
|
||||
// stamp does not match the device — flashing a foreign loader bricks the
|
||||
// target, and the loader itself cannot check what has already replaced it.
|
||||
//
|
||||
// Never read from flash by the loader — 'b' answers out of this array, but at
|
||||
// constant indices, so those fold to immediates and no runtime address of it
|
||||
// is ever formed. `used` keeps the compiler from dropping the copy the host
|
||||
// needs and `retain` keeps --gc-sections from collecting it.
|
||||
// clang-format off
|
||||
inline constexpr avr::flash_table<std::array<std::uint8_t, 12>{
|
||||
'P',
|
||||
'B',
|
||||
version,
|
||||
[[gnu::used, gnu::retain, gnu::section(".text.stamp")]]
|
||||
inline constexpr std::uint8_t identity_stamp[]{
|
||||
'P', 'B', // the magic the host scans an image for
|
||||
version, // and from here on, exactly what 'b' answers
|
||||
avr::hw::db.signature[0],
|
||||
avr::hw::db.signature[1],
|
||||
avr::hw::db.signature[2],
|
||||
static_cast<std::uint8_t>(page), // 0 means 256
|
||||
wire_base & 0xff,
|
||||
wire_base >> 8,
|
||||
avr::hw::db.mem.eeprom_size & 0xff,
|
||||
avr::hw::db.mem.eeprom_size >> 8,
|
||||
static_cast<std::uint8_t>((boot_section ? 0 : 1) | (word_flash ? 2 : 0)), // patch-vector, word-addressed
|
||||
}>
|
||||
info_data;
|
||||
};
|
||||
// clang-format on
|
||||
// Where the identity proper starts: past the magic the host scans for.
|
||||
constexpr std::uint8_t stamp_identity = 2;
|
||||
|
||||
// The serial link, per the build's PUREBOOT_USART / PUREBOOT_SOFT_SERIAL,
|
||||
// defaulting to the chip's USART0 where it has one. The software receiver is
|
||||
// the polled one: the vector table belongs to the application. Templates on
|
||||
// the clock, so only the selected backend instantiates. pending() is the
|
||||
// cheap line test the activation window polls; drain() holds until the last
|
||||
// frame is off the wire, so a hand-over cannot let the target's re-init clip
|
||||
// the ack.
|
||||
// The address spaces a transfer can name, in a selector byte's low nibble.
|
||||
// Flash is 0 so it is the cheapest to select.
|
||||
//
|
||||
// spm_ops is the one that is not memory: a write there hands its byte to
|
||||
// SPMCSR and fires the instruction at the transfer's address, which is how
|
||||
// page erase, page write and RWW re-enable reach the wire without the loader
|
||||
// carrying a command for each. The hardware's four-cycle store-to-SPM window
|
||||
// is why this is one fused primitive and not a poke of SPMCSR — no host can
|
||||
// hit that window across a serial link.
|
||||
enum : std::uint8_t { sp_flash = 0, sp_eeprom = 1, sp_data = 2, sp_fuse = 3, sp_spm = 4 };
|
||||
|
||||
// A selector's high nibble is the flash bank — the address bits above the
|
||||
// 16-bit wire address, RAMPZ on the chips that have one. Keeping it here
|
||||
// rather than widening the wire address is what lets one 16-bit cursor serve
|
||||
// every space: a 24-bit cursor would pay its extra byte on EEPROM and data
|
||||
// reads that can never need it.
|
||||
[[gnu::always_inline]] inline std::uint8_t space_of(std::uint8_t selector)
|
||||
{
|
||||
return selector & 0x0f;
|
||||
}
|
||||
|
||||
[[gnu::always_inline]] inline std::uint8_t bank_of(std::uint8_t selector)
|
||||
{
|
||||
return static_cast<std::uint8_t>(selector >> 4);
|
||||
}
|
||||
|
||||
// The slot a flash address falls in, as one byte. A slot is half as many words
|
||||
// as bytes, so the word address's high byte is exactly this index — which is
|
||||
// what lets the write guard compare a single byte, and what the running copy's
|
||||
// own return address yields for free.
|
||||
constexpr std::uint8_t slot_shift = std::countr_zero(slot_bytes);
|
||||
constexpr std::uint8_t bank_shift = 16 - slot_shift;
|
||||
|
||||
[[gnu::always_inline]] inline std::uint8_t slot_of([[maybe_unused]] std::uint8_t bank, std::uint16_t at)
|
||||
{
|
||||
const auto within = static_cast<std::uint8_t>(at >> slot_shift);
|
||||
if constexpr (banked_flash)
|
||||
return static_cast<std::uint8_t>((bank << bank_shift) | within);
|
||||
else
|
||||
return within;
|
||||
}
|
||||
|
||||
// The serial link, per the build's PUREBOOT_USART / PUREBOOT_SOFT_SERIAL /
|
||||
// PUREBOOT_AUTOBAUD, defaulting to the chip's USART0 where it has one. The
|
||||
// software receiver is the polled one: the vector table belongs to the
|
||||
// application. Templates on the clock, so only the selected backend
|
||||
// instantiates. pending() is the cheap line test the activation window polls;
|
||||
// drain() holds until the last frame is off the wire, so a hand-over cannot
|
||||
// let the target's re-init clip the ack.
|
||||
#if defined(PUREBOOT_SOFT_SERIAL) && defined(PUREBOOT_USART)
|
||||
#error "PUREBOOT_SOFT_SERIAL and PUREBOOT_USART select opposing serial backends"
|
||||
#endif
|
||||
#if defined(PUREBOOT_AUTOBAUD) && defined(PUREBOOT_USART)
|
||||
#error "PUREBOOT_AUTOBAUD measures a software link; it cannot drive a hardware USART"
|
||||
#endif
|
||||
#if !defined(PUREBOOT_RX)
|
||||
#define PUREBOOT_RX pb0
|
||||
#endif
|
||||
@@ -113,9 +166,9 @@ constexpr char usart_digit = '0' + PUREBOOT_USART;
|
||||
constexpr char usart_digit = '0';
|
||||
#endif
|
||||
|
||||
template <avr::hertz_t C>
|
||||
template <avr::hertz_t C, avr::baud_t B>
|
||||
struct hardware_link {
|
||||
using uart = avr::uart::usart<usart_digit, C, {.baud = wire_baud, .max_baud_error = 2.5_pct}>;
|
||||
using uart = avr::uart::usart<usart_digit, C, {.baud = B, .max_baud_error = 2.5_pct}>;
|
||||
|
||||
// The compiled idle poll: lds UCSR0A (2), sbrc skipping the exit (2),
|
||||
// sbiw + sbci + sbci + brne (6).
|
||||
@@ -147,10 +200,10 @@ struct hardware_link {
|
||||
}
|
||||
};
|
||||
|
||||
template <avr::hertz_t C>
|
||||
template <avr::hertz_t C, avr::baud_t B>
|
||||
struct software_link {
|
||||
using rx_t = avr::uart::software_rx_polled<C, avr::PUREBOOT_RX, wire_baud>;
|
||||
using tx_t = avr::uart::software_tx<C, avr::PUREBOOT_TX, wire_baud>;
|
||||
using rx_t = avr::uart::software_rx_polled<C, avr::PUREBOOT_RX, B>;
|
||||
using tx_t = avr::uart::software_tx<C, avr::PUREBOOT_TX, B>;
|
||||
|
||||
// The compiled idle poll: sbis skipping the exit (2), sbiw + sbci +
|
||||
// sbci + brne (6).
|
||||
@@ -182,14 +235,44 @@ struct software_link {
|
||||
}
|
||||
};
|
||||
|
||||
#if defined(PUREBOOT_USART)
|
||||
// The clock-free link: the bit period is measured from the host's calibration
|
||||
// pulse instead of derived from a clock, so one image serves every F_CPU and
|
||||
// every rate. Activation differs in kind from the other two — there is no
|
||||
// clock to time a window against — so this backend brings its own, below.
|
||||
struct autobaud_link {
|
||||
using uart = avr::uart::software_autobaud<avr::PUREBOOT_RX, avr::PUREBOOT_TX>;
|
||||
|
||||
static void init()
|
||||
{
|
||||
avr::init<uart>();
|
||||
}
|
||||
|
||||
static std::uint8_t rx()
|
||||
{
|
||||
return uart::template read<off>();
|
||||
}
|
||||
|
||||
static void tx(std::uint8_t byte)
|
||||
{
|
||||
uart::template write<off>(byte);
|
||||
}
|
||||
|
||||
static void drain()
|
||||
{
|
||||
uart::drain();
|
||||
}
|
||||
};
|
||||
|
||||
#if defined(PUREBOOT_AUTOBAUD)
|
||||
using link = autobaud_link;
|
||||
#elif defined(PUREBOOT_USART)
|
||||
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>;
|
||||
using link = hardware_link<dev::clock, wire_baud>;
|
||||
#elif defined(PUREBOOT_SOFT_SERIAL)
|
||||
using link = software_link<dev::clock>;
|
||||
using link = software_link<dev::clock, wire_baud>;
|
||||
#else
|
||||
using link =
|
||||
std::conditional_t<avr::uart::has_usart<usart_digit>(), hardware_link<dev::clock>, software_link<dev::clock>>;
|
||||
using link = std::conditional_t<avr::uart::has_usart<usart_digit>(), hardware_link<dev::clock, wire_baud>,
|
||||
software_link<dev::clock, wire_baud>>;
|
||||
#endif
|
||||
|
||||
// The application's entry, pinned by the linker (--defsym): word 0 on a
|
||||
@@ -209,6 +292,27 @@ extern "C" [[noreturn]] void pureboot_app();
|
||||
jump(pureboot_app);
|
||||
}
|
||||
|
||||
// Activation: a bounded wait for the host, then the knock. Both forms boot the
|
||||
// application when the window closes on an idle line, and both bound *every*
|
||||
// wait — a knock awaited without a deadline would let one stray edge hold an
|
||||
// unattended device in the loader forever.
|
||||
#if defined(PUREBOOT_AUTOBAUD)
|
||||
// The window is a fixed poll budget: with no clock, whole seconds cannot be
|
||||
// timed. A uint24_t holds it — a fourth byte would cost two words at every
|
||||
// countdown step for range never used.
|
||||
void await_host()
|
||||
{
|
||||
for (;;) {
|
||||
if (!link::uart::calibrate(autobaud_budget))
|
||||
run_app();
|
||||
// The calibration pulse has already proven a host is there, so one
|
||||
// byte activates. A knock that never arrives falls back to calibrate(),
|
||||
// whose own budget then boots the application.
|
||||
if (link::uart::template read<off>(autobaud_budget) == 'p')
|
||||
return;
|
||||
}
|
||||
}
|
||||
#else
|
||||
// The window as one 32-bit countdown, divided by the backend's counted
|
||||
// poll-loop cycles. Whole seconds is all it promises.
|
||||
consteval std::uint32_t window_polls()
|
||||
@@ -235,6 +339,14 @@ std::uint8_t rx_deadline()
|
||||
return link::rx();
|
||||
}
|
||||
|
||||
void await_host()
|
||||
{
|
||||
// 'p' then 'b', each under a fresh window; anything else is line noise.
|
||||
while (rx_deadline() != 'p' || rx_deadline() != 'b') {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Inlined: read across a call, the first byte strands in a call-saved
|
||||
// register the caller has to push and pop.
|
||||
[[gnu::always_inline]] inline std::uint16_t rx16()
|
||||
@@ -252,132 +364,92 @@ std::uint8_t rx_deadline()
|
||||
return std::bit_cast<std::uint16_t>(pair);
|
||||
}
|
||||
|
||||
// Counts arrive in the wire's 8-bit form: 0 means 256. Both streamers fold
|
||||
// into the one command that reads flash, which is what lets the far one's
|
||||
// 24-bit cursor sit in the command loop's own call-saved registers.
|
||||
[[maybe_unused, gnu::always_inline]] inline void send_flash_near(std::uint16_t address, std::uint8_t 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 apart; the reassembled address folds away inside the far load.
|
||||
[[maybe_unused, gnu::always_inline]] inline 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));
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Out of line: three sites send it, and a call is shorter than three
|
||||
// load-immediates.
|
||||
// Out of line: several sites send it, and a call is shorter than a
|
||||
// load-immediate at each.
|
||||
[[gnu::noinline]] void tx_ack()
|
||||
{
|
||||
link::tx(ack);
|
||||
}
|
||||
|
||||
void send_eeprom(std::uint16_t address, std::uint8_t count)
|
||||
// A wire address and its selector's bank as the flash address they name.
|
||||
[[gnu::always_inline]] inline spm::flash_address_t flash_address([[maybe_unused]] std::uint8_t bank, std::uint16_t at)
|
||||
{
|
||||
do
|
||||
link::tx(ee::read(address++));
|
||||
while (--count);
|
||||
if constexpr (banked_flash)
|
||||
return (static_cast<spm::flash_address_t>(bank) << 16) | at;
|
||||
else
|
||||
return at;
|
||||
}
|
||||
|
||||
// Host-paced: the ack goes out once the write has begun, so the next byte
|
||||
// arrives while it completes and nothing is missed without a buffer.
|
||||
void store_eeprom(std::uint16_t address, std::uint8_t count)
|
||||
// One byte out of any space. Every accessor shares the transfer's cursor, its
|
||||
// loop and its call site, so a space costs only its own instruction rather
|
||||
// than a body, a loop and a dispatch arm of its own.
|
||||
[[gnu::always_inline]] inline std::uint8_t load(std::uint8_t space, [[maybe_unused]] std::uint8_t bank,
|
||||
std::uint16_t at)
|
||||
{
|
||||
do {
|
||||
ee::write<off>(address++, link::rx());
|
||||
tx_ack();
|
||||
} while (--count);
|
||||
if (space == sp_eeprom)
|
||||
return ee::read(at);
|
||||
if (space == sp_data)
|
||||
return *reinterpret_cast<volatile std::uint8_t *>(at);
|
||||
if (space == sp_fuse)
|
||||
return spm::read_fuse<off>(static_cast<spm::fuse>(at));
|
||||
if constexpr (banked_flash)
|
||||
return avr::flash_load_far<std::uint8_t>(flash_address(bank, at));
|
||||
else
|
||||
return avr::flash_load(reinterpret_cast<const std::uint8_t *>(at));
|
||||
}
|
||||
|
||||
// One page into the SPM buffer, then erase and program — except the slot
|
||||
// this code is running in (`slot_high`, from run()), which is drained and
|
||||
// left alone. A broken host therefore cannot brick the running loader, and a
|
||||
// copy one slot lower may rewrite the resident one.
|
||||
// One byte into a writable space. Flash is not one of them — it arrives a
|
||||
// page at a time through 'W' and is committed through sp_spm — and the fuses
|
||||
// are not writable at all: SPM reaches flash and boot lock bits only.
|
||||
[[gnu::always_inline]] inline void store(std::uint8_t space, std::uint8_t bank, std::uint16_t at, std::uint8_t value,
|
||||
std::uint8_t slot_high)
|
||||
{
|
||||
if (space == sp_data) {
|
||||
*reinterpret_cast<volatile std::uint8_t *>(at) = value;
|
||||
return;
|
||||
}
|
||||
if (space == sp_spm) {
|
||||
// The running-slot write guard. An SPM command aimed at the slot this
|
||||
// code executes from is dropped, so a broken host cannot brick the
|
||||
// running loader — while a copy one slot lower may still rewrite the
|
||||
// resident one, which is what a self-update is. Guarding the commit
|
||||
// rather than the page fill covers erase and write both, and leaves a
|
||||
// refused page's words in the buffer: harmless, since the next page
|
||||
// write auto-erases it (§26.2.1).
|
||||
if (slot_of(bank, at) != slot_high)
|
||||
spm::command<off>(value, flash_address(bank, at));
|
||||
// Only a boot-sectioned mega runs on while its RWW section programs;
|
||||
// everywhere else the CPU halts through erase and write, so the wait
|
||||
// is already over by the time it returns.
|
||||
if constexpr (boot_section)
|
||||
spm::wait();
|
||||
return;
|
||||
}
|
||||
// Host-paced: the ack goes out once the write has begun, so the next byte
|
||||
// arrives while it completes and nothing is missed without a buffer.
|
||||
ee::write<off>(at, value);
|
||||
}
|
||||
|
||||
// One page into the SPM buffer, and only that: the erase and the write that
|
||||
// commit it are host-issued sp_spm stores, which reach the same fused
|
||||
// store-and-SPM pair through the transfer path's own address and data.
|
||||
//
|
||||
// Nothing discards the buffer first: it is write-once per word (§26.2.1), so
|
||||
// filling over a refused page or an application's leavings programs stale
|
||||
// words — but a page write auto-erases it (§26.2.1; §19.2 on the tinies), so
|
||||
// that write clears the condition and the host's read-back rewrites the page.
|
||||
void program_flash(std::uint16_t wire_address, std::uint8_t slot_high)
|
||||
void fill_page(std::uint8_t bank, std::uint16_t at)
|
||||
{
|
||||
// The address names a page, so its in-page bits are dropped and the walk
|
||||
// starts at the page base — one induction either way: a byte-addressed
|
||||
// wire address walks the page itself (the offset bits wrap back to zero),
|
||||
// while a word one becomes a byte cursor once. The slot index is the wire
|
||||
// address's high byte — on byte-addressed chips the byte address's, with
|
||||
// the low bit dropped, since a slot is two of those.
|
||||
spm::flash_address_t address;
|
||||
std::uint8_t page_high;
|
||||
if constexpr (word_flash) {
|
||||
// A page is aligned, so it never crosses 64 KiB: RAMPZ is a per-page
|
||||
// constant and the 16-bit Z's low byte is the whole in-page offset.
|
||||
const std::uint8_t rampz = static_cast<std::uint8_t>(wire_address >> 15);
|
||||
const std::uint16_t z0 = static_cast<std::uint16_t>(wire_address << 1) & ~static_cast<std::uint16_t>(page - 1);
|
||||
std::uint16_t z = z0;
|
||||
do {
|
||||
std::uint8_t low = link::rx();
|
||||
std::uint8_t high = link::rx();
|
||||
spm::fill<off>((static_cast<spm::flash_address_t>(rampz) << 16) | z, word_of({low, high}));
|
||||
z += 2;
|
||||
} while (static_cast<std::uint8_t>(z));
|
||||
address = (static_cast<spm::flash_address_t>(rampz) << 16) | z0;
|
||||
page_high = static_cast<std::uint8_t>(wire_address >> 8);
|
||||
} else {
|
||||
address = static_cast<spm::flash_address_t>(wire_address & ~static_cast<std::uint16_t>(page - 1));
|
||||
do {
|
||||
std::uint8_t low = link::rx();
|
||||
std::uint8_t high = link::rx();
|
||||
spm::fill<off>(address, word_of({low, high}));
|
||||
address += 2;
|
||||
} while (static_cast<std::uint8_t>(address) & (page - 1));
|
||||
address -= 2; // back inside the page — erase and write ignore the word bits
|
||||
page_high = static_cast<std::uint8_t>(address >> 8) & 0xfe;
|
||||
}
|
||||
if (page_high != slot_high) {
|
||||
// Only a boot-sectioned mega runs on while its RWW section programs;
|
||||
// everywhere else the CPU halts through erase and write.
|
||||
spm::erase_page<off>(address);
|
||||
if constexpr (boot_section)
|
||||
spm::wait();
|
||||
spm::write_page<off>(address);
|
||||
if constexpr (boot_section)
|
||||
spm::wait();
|
||||
}
|
||||
// Programming leaves the RWW section disabled; reads need it back on. The
|
||||
// same store discards the buffer (§26.2.2), so a boot-sectioned mega never
|
||||
// meets the stale-word case above.
|
||||
if constexpr (boot_section)
|
||||
spm::rww_enable<off>();
|
||||
}
|
||||
|
||||
// The four fuse and lock bytes in the hardware's own Z order: low, lock,
|
||||
// extended, high.
|
||||
void send_fuses()
|
||||
{
|
||||
std::uint8_t which = 0;
|
||||
do
|
||||
link::tx(spm::read_fuse<off>(static_cast<spm::fuse>(which)));
|
||||
while (++which != 4);
|
||||
// starts at the page base; the low byte of the cursor is the whole in-page
|
||||
// offset, since a page is aligned and never crosses a bank.
|
||||
std::uint16_t z = at & ~static_cast<std::uint16_t>(page - 1);
|
||||
do {
|
||||
std::uint8_t low = link::rx();
|
||||
std::uint8_t high = link::rx();
|
||||
spm::fill<off>(flash_address(bank, z), word_of({low, high}));
|
||||
z += 2;
|
||||
} while (static_cast<std::uint8_t>(z) & (page - 1));
|
||||
}
|
||||
|
||||
[[noreturn]] void run()
|
||||
@@ -389,19 +461,14 @@ void send_fuses()
|
||||
|
||||
link::init();
|
||||
|
||||
// The high byte of the slot this copy runs at, which the write guard and
|
||||
// the info block both follow: the return address is a word address, so its
|
||||
// high byte is the 256-word slot index, doubled back into byte terms where
|
||||
// the wire counts bytes. Taken as byteswap's low byte — the builtin already
|
||||
// swaps the two stacked bytes, and the double swap folds away, where `>> 8`
|
||||
// would leave the swap materialized.
|
||||
const std::uint16_t ra_words = reinterpret_cast<std::uint16_t>(__builtin_return_address(0));
|
||||
const std::uint8_t ra_high = static_cast<std::uint8_t>(std::byteswap(ra_words));
|
||||
const std::uint8_t slot_high = word_flash ? ra_high : static_cast<std::uint8_t>(ra_high << 1);
|
||||
// The slot this copy runs in, which the write guard follows: the return
|
||||
// address is a word address and a slot is half as many words as bytes, so
|
||||
// its high byte is the slot index outright. No absolute address is ever
|
||||
// formed, so the image stays position-independent.
|
||||
const auto return_words = reinterpret_cast<std::uint16_t>(__builtin_return_address(0));
|
||||
const auto slot_high = static_cast<std::uint8_t>(return_words >> 8);
|
||||
|
||||
// 'p' then 'b', each under a fresh window; anything else is line noise.
|
||||
while (rx_deadline() != 'p' || rx_deadline() != 'b') {
|
||||
}
|
||||
await_host();
|
||||
|
||||
for (;;) {
|
||||
// No prompt while an EEPROM write runs: it blocks SPM and fuse reads
|
||||
@@ -416,47 +483,43 @@ void send_fuses()
|
||||
link::drain();
|
||||
jump(target);
|
||||
}
|
||||
case 'b': // info block, read relative to the running slot
|
||||
case 'R': // read flash: addr16, n8 (0 = 256)
|
||||
case 'r': // read EEPROM: addr16, n8
|
||||
case 'w': { // write EEPROM: addr16, n8, then n bytes each acked
|
||||
// One address-and-count path for all four: 'b' is a flash read
|
||||
// whose arguments the loader already knows, so it joins the
|
||||
// wire-argument three rather than streaming from a call site of its
|
||||
// own. That leaves one flash streamer in the image, and lets its
|
||||
// cursor live in this never-returning loop's own call-saved
|
||||
// registers instead of being saved and restored around a call.
|
||||
std::uint16_t address;
|
||||
std::uint8_t count;
|
||||
if (command == 'b') {
|
||||
// The block sits in the image's first 256 bytes (check_pi.py
|
||||
// asserts it) and slots are 512-aligned, so the low byte of its
|
||||
// link address is its offset in any slot — halved where wire
|
||||
// units are words. The high byte is runtime data, so no
|
||||
// absolute address is ever materialized.
|
||||
const auto link_byte =
|
||||
static_cast<std::uint8_t>(reinterpret_cast<std::uint16_t>(info_data.storage.data()));
|
||||
const std::uint8_t low = word_flash ? static_cast<std::uint8_t>(link_byte >> 1) : link_byte;
|
||||
address = static_cast<std::uint16_t>(low | (slot_high << 8));
|
||||
count = static_cast<std::uint8_t>(info_data.size());
|
||||
} else {
|
||||
address = rx16();
|
||||
count = link::rx();
|
||||
case 'b': // identity: the version, then the three signature bytes
|
||||
// Straight out of the stamp, so the wire and the image can never
|
||||
// disagree about what this loader is. The indices are constant and
|
||||
// the array is constexpr, so these are immediates, not flash reads:
|
||||
// nothing here needs the stamp's runtime address.
|
||||
for (std::uint8_t at = stamp_identity; at != sizeof identity_stamp; ++at)
|
||||
link::tx(identity_stamp[at]);
|
||||
break;
|
||||
case 'W': // fill one flash page buffer: sel8, addr16, then page bytes
|
||||
case 'G': // read: sel8, addr16, n8 (0 = 256)
|
||||
case 'g': { // write: sel8, addr16, n8, then n bytes, each acked
|
||||
// One decode, one cursor and one loop for every space and both
|
||||
// directions: a command per memory would carry a copy of all three
|
||||
// each. 'W' joins the same decode rather than keeping an address
|
||||
// form of its own, so flash addressing is uniform across every
|
||||
// command that names it.
|
||||
const std::uint8_t selector = link::rx();
|
||||
const std::uint8_t space = space_of(selector);
|
||||
const std::uint8_t bank = bank_of(selector);
|
||||
std::uint16_t at = rx16();
|
||||
if (command == 'W') {
|
||||
fill_page(bank, at);
|
||||
break;
|
||||
}
|
||||
if (command == 'r')
|
||||
send_eeprom(address, count);
|
||||
else if (command == 'w')
|
||||
store_eeprom(address, count);
|
||||
else
|
||||
send_flash(address, count);
|
||||
std::uint8_t count = link::rx();
|
||||
do {
|
||||
// Read and write are one letter apart in case, so the direction
|
||||
// is a single bit and the loop picks it with a one-word skip.
|
||||
if (command & 0x20) {
|
||||
store(space, bank, at, link::rx(), slot_high);
|
||||
tx_ack();
|
||||
} else
|
||||
link::tx(load(space, bank, at));
|
||||
++at;
|
||||
} while (--count);
|
||||
break;
|
||||
}
|
||||
case 'W': // program one flash page: addr16, page bytes
|
||||
program_flash(rx16(), slot_high);
|
||||
break;
|
||||
case 'F': // fuse and lock bytes
|
||||
send_fuses();
|
||||
break;
|
||||
default: // unknown bytes are ignored; the loop re-acks
|
||||
break;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user