build: the libavr pin advances past phase 6, at byte parity everywhere
The pin crosses libavr's phase 6 - the renamed system surface, the named serial configs, the receiver-tolerance table, the paged SPM receipts - and every loader image comes out size-identical: the full matrix on six representative chips (the exhaustive cross product on three of them), the stock and autobaud columns untouched, the four tsb tiers back on their recorded floors at 510/526/638/836. Byte parity was not free, and the two libavr defects it surfaced were fixed there rather than absorbed here. The EEPROM write procedure's step 2 - the SPMEN spin - had landed unconditionally and cost every build six bytes for a wait a polled loader can never take; it is scoped now, and the loaders state the datasheet's own omission clause (spm_interlock::omitted, DS40002061B 8.6.3). The blocking page erase/write grew an internal wait the tiers' settle() already provides, so the tiers issue the command form and pureboot keeps its host-driven sp_spm path. What the port states rather than inherits: the stock 115200 at 16 MHz sits +2.1 % past the receiver-tolerance table libavr now holds rates to, so the hardware links say .allow_baud_error = true - the same 2.5 % envelope pureboot_baud_feasible() has always enforced, proven on silicon across the fleet. rx_ready() reads readable() now. Alongside the pin: rule 33's ASCII sweep over every source (docs keep their typography), rule 34's InsertBraces in .clang-format with the tree reformatted, std::array over the simavr runners' raw buffers, and the stale Studio size in ide/README.md replaced by the claim its check-flags gate actually holds. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
// TinySafeBoot on libavr — tier 3: full feature parity in the 512-byte boot
|
||||
// TinySafeBoot on libavr - tier 3: full feature parity in the 512-byte boot
|
||||
// section, in C++ except where the C ABI itself is the cost.
|
||||
//
|
||||
// The complete TinySafeBoot feature set — watchdog-reset bail, one-wire
|
||||
// The complete TinySafeBoot feature set - watchdog-reset bail, one-wire
|
||||
// half-duplex UART, a config-page activation timeout, the password gate,
|
||||
// emergency erase, and config/flash/EEPROM read-write — at 510 bytes in the
|
||||
// emergency erase, and config/flash/EEPROM read-write - at 510 bytes in the
|
||||
// 512-byte BOOTSZ=11 section the hand-written oracle occupies (500 B). This tier used to be one
|
||||
// monolithic inline-asm routine; it is now the tricks tier's C++ (same
|
||||
// register protocol, same structure — see tsb_tricks.cpp, including the
|
||||
// register protocol, same structure - see tsb_tricks.cpp, including the
|
||||
// global-register miscompile rules) with exactly two routines kept in
|
||||
// assembly, the two whose remaining cost *is* the calling convention:
|
||||
//
|
||||
@@ -16,12 +16,12 @@
|
||||
// countdown.
|
||||
// store the page-store loop: C++ cannot hold the receive byte pair and the
|
||||
// walked Z pointer across the rx calls without call-saved staging
|
||||
// (push/pop + a Y→Z copy per word); the asm calls rx knowing exactly
|
||||
// (push/pop + a Y->Z copy per word); the asm calls rx knowing exactly
|
||||
// which registers it touches and walks Z live across the whole page.
|
||||
//
|
||||
// Everything else — bring-up, activation, password gate, emergency erase,
|
||||
// Everything else - bring-up, activation, password gate, emergency erase,
|
||||
// dispatch, every SPM/EEPROM/flash primitive, every geometry/baud/info
|
||||
// constant — is C++ on libavr, and the two asm routines splice into the same
|
||||
// constant - is C++ on libavr, and the two asm routines splice into the same
|
||||
// global-register protocol the C++ uses (g_addr in Y, g_cnt in r16, g_window
|
||||
// in r7, g_receiving in r6), so calls cross the boundary with no marshalling.
|
||||
//
|
||||
@@ -41,10 +41,16 @@ namespace hw = avr::hw;
|
||||
namespace tsb {
|
||||
namespace {
|
||||
|
||||
// The loader is purely polled — it never enables interrupts — so every SPM and
|
||||
// The loader is purely polled - it never enables interrupts - so every SPM and
|
||||
// EEPROM lock folds to nothing under this posture.
|
||||
constexpr auto off = avr::irq::guard_policy::unused;
|
||||
|
||||
// Strict request/response: every SPM operation is waited out before the next
|
||||
// byte moves, so no flash operation is ever in flight at an EEPROM access -
|
||||
// the write procedure's step 2 has nothing to guard, the omission the
|
||||
// datasheet grants (DS40002061B section 8.6.3).
|
||||
constexpr auto no_spm = ee::spm_interlock::omitted;
|
||||
|
||||
constexpr std::uint8_t confirm = '!';
|
||||
constexpr std::uint8_t request = '?';
|
||||
constexpr std::uint8_t knock = '@';
|
||||
@@ -65,7 +71,7 @@ constexpr std::uint8_t comm_window = 200;
|
||||
constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 20;
|
||||
|
||||
// Fixed 115200 8N1; the library solves UBRR + U2X from clock and baud.
|
||||
constexpr auto baud = avr::uart::solve_baud(16_MHz, 115200_Bd);
|
||||
constexpr auto baud = avr::uart::solve_baud(16_MHz, 115200_Bd, 8, avr::uart::parity::none);
|
||||
|
||||
// The 16-byte device-info block, streamed out on activation.
|
||||
// clang-format off
|
||||
@@ -94,7 +100,7 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||
// Bounded byte receive (asm 1 of 2): release the one-wire line on a direction
|
||||
// change, poll RXC0 under the oracle's nested X-register countdown seeded from
|
||||
// g_window (floored against lockout), byte or 0-on-silence in r24. Z survives
|
||||
// — the property the store's word loop rides on.
|
||||
// - the property the store's word loop rides on.
|
||||
[[gnu::noinline, gnu::noclone]] std::uint8_t rx()
|
||||
{
|
||||
std::uint8_t byte;
|
||||
@@ -115,7 +121,7 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||
" brne 3b \n\t"
|
||||
" sbiw r26, 1 \n\t"
|
||||
" brcc 2b \n\t"
|
||||
" clr %[b] \n\t" // silence → 0, which no compare accepts
|
||||
" clr %[b] \n\t" // silence -> 0, which no compare accepts
|
||||
" rjmp 5f \n\t"
|
||||
"4: lds %[b], %[udr0] \n\t"
|
||||
"5: \n\t"
|
||||
@@ -129,7 +135,7 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||
// One-wire transmit: take the line (TXEN0 alone) on a direction change with a
|
||||
// turn-around guard, put the byte out, hold the line until the whole frame is
|
||||
// out (TXC0, not UDRE0), W1C TXC0 by storing the sampled status back (keeps
|
||||
// U2X0). Plain C++ — it compiles *smaller* than the oracle's routine.
|
||||
// U2X0). Plain C++ - it compiles *smaller* than the oracle's routine.
|
||||
[[gnu::noinline, gnu::noclone]] void tx(std::uint8_t byte)
|
||||
{
|
||||
if (g_receiving) {
|
||||
@@ -153,7 +159,7 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||
return rx();
|
||||
}
|
||||
|
||||
// One flash byte ← [g_addr++] (the advance right before ret — the
|
||||
// One flash byte <- [g_addr++] (the advance right before ret - the
|
||||
// global-register rule, see tsb_tricks.cpp).
|
||||
[[gnu::noinline, gnu::noclone]] std::uint8_t sflash()
|
||||
{
|
||||
@@ -162,18 +168,18 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||
return byte;
|
||||
}
|
||||
|
||||
// One EEPROM byte ← [g_addr++].
|
||||
// One EEPROM byte <- [g_addr++].
|
||||
[[gnu::noinline, gnu::noclone]] std::uint8_t eerd()
|
||||
{
|
||||
std::uint8_t byte = ee::read(g_addr);
|
||||
std::uint8_t byte = ee::read<no_spm>(g_addr);
|
||||
++g_addr;
|
||||
return byte;
|
||||
}
|
||||
|
||||
// One EEPROM byte → [g_addr++].
|
||||
// One EEPROM byte -> [g_addr++].
|
||||
[[gnu::noinline, gnu::noclone]] void eewr(std::uint8_t byte)
|
||||
{
|
||||
ee::write<off>(g_addr, byte);
|
||||
ee::write<off, no_spm>(g_addr, byte);
|
||||
++g_addr;
|
||||
}
|
||||
|
||||
@@ -185,7 +191,7 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||
} while (--g_cnt);
|
||||
}
|
||||
|
||||
// Wait out a running SPM op, then re-open the RWW section — after every page
|
||||
// Wait out a running SPM op, then re-open the RWW section - after every page
|
||||
// op and before handing over, as the oracle does.
|
||||
[[gnu::noinline, gnu::noclone]] void settle()
|
||||
{
|
||||
@@ -201,12 +207,12 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
tsb_app();
|
||||
}
|
||||
|
||||
// Step g_addr one page down and erase that page (the decrement lives here —
|
||||
// Step g_addr one page down and erase that page (the decrement lives here -
|
||||
// the global-register rule).
|
||||
[[gnu::noinline, gnu::noclone]] void erase_below()
|
||||
{
|
||||
g_addr -= page;
|
||||
spm::erase_page<off>(g_addr);
|
||||
spm::command<off>(spm::op::erase, g_addr);
|
||||
settle();
|
||||
}
|
||||
|
||||
@@ -221,7 +227,7 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
}
|
||||
|
||||
// Stream one host page into the erased flash page at g_addr (asm 2 of 2): the
|
||||
// word pair stages in r0:r1 straight from rx (whose register set is known —
|
||||
// word pair stages in r0:r1 straight from rx (whose register set is known -
|
||||
// the cross-call liveness C++ cannot express), Z walks the page and PGWRT
|
||||
// programs it. g_addr is left at the next page base.
|
||||
[[gnu::noinline, gnu::noclone]] void store_flash()
|
||||
@@ -256,11 +262,12 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
{
|
||||
// A watchdog reset hands straight back to the application, as the
|
||||
// reference loader does, rather than re-entering the bootloader.
|
||||
if (hw::mcusr::wdrf.test())
|
||||
if (hw::mcusr::wdrf.test()) {
|
||||
appjump();
|
||||
}
|
||||
|
||||
// Lean bring-up from reset state: UCSR0C already reads 8N1, UBRR0H reads
|
||||
// 0, and rx()/tx() raise RXEN0/TXEN0 on first use — only the divisor low
|
||||
// 0, and rx()/tx() raise RXEN0/TXEN0 on first use - only the divisor low
|
||||
// byte and U2X0 need a store. The library still does the datasheet work.
|
||||
static_assert(baud.u2x && baud.ubrr < 256, "lean bring-up writes UBRR0L only, with U2X0");
|
||||
hw::ubrr0::write(static_cast<std::uint8_t>(baud.ubrr));
|
||||
@@ -271,13 +278,15 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
// same reason.
|
||||
g_receiving = 0;
|
||||
|
||||
// Activation: 3×'@', each inside the config page's timeout window (rx
|
||||
// floors it so a corrupt page cannot lock the loader out); anything else —
|
||||
// including silence — hands over.
|
||||
// Activation: 3x'@', each inside the config page's timeout window (rx
|
||||
// floors it so a corrupt page cannot lock the loader out); anything else -
|
||||
// including silence - hands over.
|
||||
g_window = avr::flash_load(flash_ptr(app_end + 2));
|
||||
for (std::uint8_t k = 3; k; --k)
|
||||
if (rx() != knock)
|
||||
for (std::uint8_t k = 3; k; --k) {
|
||||
if (rx() != knock) {
|
||||
appjump();
|
||||
}
|
||||
}
|
||||
g_window = comm_window;
|
||||
|
||||
// Password gate (config page from app_end+3, 0xff-terminated; a blank
|
||||
@@ -298,10 +307,12 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
}
|
||||
std::uint8_t got = rx();
|
||||
if (got == 0) {
|
||||
if (mask == 0)
|
||||
if (mask == 0) {
|
||||
continue;
|
||||
if (rcnf() != confirm || rcnf() != confirm)
|
||||
}
|
||||
if (rcnf() != confirm || rcnf() != confirm) {
|
||||
appjump();
|
||||
}
|
||||
erase_application(); // leaves g_addr = 0 for the EEPROM walk
|
||||
do {
|
||||
eewr(0xff);
|
||||
@@ -310,8 +321,9 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
erase_below();
|
||||
break;
|
||||
}
|
||||
if (got != expected)
|
||||
if (got != expected) {
|
||||
mask = 0;
|
||||
}
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
@@ -320,23 +332,27 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
switch (rx()) {
|
||||
case 'f': // read application flash, one page per host '!'
|
||||
for (;;) {
|
||||
if (rx() != confirm)
|
||||
if (rx() != confirm) {
|
||||
break;
|
||||
}
|
||||
g_cnt = page;
|
||||
sendf();
|
||||
if (g_addr >= app_end)
|
||||
if (g_addr >= app_end) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'F': // erase the application, then take pages behind '?'
|
||||
erase_application(); // leaves g_addr = 0, the write start
|
||||
while (rcnf() == confirm)
|
||||
while (rcnf() == confirm) {
|
||||
store_flash();
|
||||
}
|
||||
break;
|
||||
case 'e': // read EEPROM, one page per host '!', until the host stops
|
||||
for (;;) {
|
||||
if (rx() != confirm)
|
||||
if (rx() != confirm) {
|
||||
break;
|
||||
}
|
||||
g_cnt = page;
|
||||
do {
|
||||
tx(eerd());
|
||||
@@ -358,8 +374,9 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
sendf();
|
||||
break;
|
||||
case 'C': // replace the config page, then echo it back to verify
|
||||
if (rcnf() != confirm)
|
||||
if (rcnf() != confirm) {
|
||||
break;
|
||||
}
|
||||
g_addr = app_end + page;
|
||||
erase_below(); // leaves g_addr = app_end, the store target
|
||||
store_flash();
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
// TinySafeBoot on libavr — the policy floor: pureboot's rules, measured.
|
||||
// TinySafeBoot on libavr - the policy floor: pureboot's rules, measured.
|
||||
//
|
||||
// The full TinySafeBoot feature set — watchdog bail, one-wire half-duplex,
|
||||
// The full TinySafeBoot feature set - watchdog bail, one-wire half-duplex,
|
||||
// config-page activation timeout, password gate, emergency erase, and
|
||||
// config/flash/EEPROM read-write — under philosophy #5 exactly as pureboot
|
||||
// config/flash/EEPROM read-write - under philosophy #5 exactly as pureboot
|
||||
// obeys it: no assembly, no register variables; code, attributes, and flags
|
||||
// only. Every lesson pureboot's development produced is applied — the
|
||||
// only. Every lesson pureboot's development produced is applied - the
|
||||
// library's half-duplex serial and startup entry, lean bring-up from reset
|
||||
// state, one merged send loop over both memories, oracle-shaped loop bounds,
|
||||
// locals threaded through noinline primitives, pureboot's codegen flags —
|
||||
// locals threaded through noinline primitives, pureboot's codegen flags -
|
||||
// and the result is 638 bytes: 198 below the idiomatic tier, and 126 above
|
||||
// the 512 B boot section the tricks/asm tiers reach with the banned
|
||||
// mechanisms (526/510). This tier exists to keep that number an artifact
|
||||
// rather than a claim: the gap to 512 is the rent of policy-clean C++ —
|
||||
// rather than a claim: the gap to 512 is the rent of policy-clean C++ -
|
||||
// helpers that hold a cursor across rx()/tx() pay push/pop and argument
|
||||
// threading where a global-register protocol pays nothing, and both
|
||||
// control-flow merges tried (a parametrized paged session, a merged store
|
||||
@@ -32,16 +32,25 @@ namespace ee = avr::eeprom;
|
||||
|
||||
using dev = avr::device<{.clock = 16_MHz}>;
|
||||
// One-wire: RX and TX share the line, exactly as the native-UART TSB expects.
|
||||
using serial_t = dev::uart0<{.baud = 115200_Bd, .max_baud_error = 3_pct, .half_duplex = true}>;
|
||||
// 115200 at 16 MHz lands +2.1 % off, past the receiver-tolerance table the
|
||||
// solver holds rates to - the oracle's own deployment has run there for a
|
||||
// decade, so the override states that it is meant.
|
||||
using serial_t = dev::uart0<{.baud = 115200_Bd, .allow_baud_error = true, .half_duplex = true}>;
|
||||
inline constexpr serial_t serial{};
|
||||
|
||||
namespace tsb {
|
||||
namespace {
|
||||
|
||||
// The loader is purely polled — it never enables interrupts — so every SPM and
|
||||
// The loader is purely polled - it never enables interrupts - so every SPM and
|
||||
// EEPROM lock folds to nothing under this posture.
|
||||
constexpr auto off = avr::irq::guard_policy::unused;
|
||||
|
||||
// Strict request/response: every SPM operation is waited out before the next
|
||||
// byte moves, so no flash operation is ever in flight at an EEPROM access -
|
||||
// the write procedure's step 2 has nothing to guard, the omission the
|
||||
// datasheet grants (DS40002061B section 8.6.3).
|
||||
constexpr auto no_spm = ee::spm_interlock::omitted;
|
||||
|
||||
// The handshake bytes, identical across every TSB host.
|
||||
constexpr std::uint8_t confirm = '!';
|
||||
constexpr std::uint8_t request = '?';
|
||||
@@ -87,8 +96,8 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||
}
|
||||
|
||||
// Bounded byte receive: poll under nested countdowns, 0 on silence. The 0
|
||||
// then falls through every compare — not a knock, not a confirm, not a
|
||||
// command — so a silent host unwinds the loader to the application from
|
||||
// then falls through every compare - not a knock, not a confirm, not a
|
||||
// command - so a silent host unwinds the loader to the application from
|
||||
// anywhere, and a mid-session cable pull cannot wedge it. The line release on
|
||||
// a direction change is the serial backend's.
|
||||
[[gnu::noinline]] std::uint8_t rx()
|
||||
@@ -97,8 +106,9 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||
do {
|
||||
std::uint8_t fine = 0;
|
||||
do {
|
||||
if (auto byte = serial.read())
|
||||
if (auto byte = serial.read()) {
|
||||
return *byte;
|
||||
}
|
||||
} while (--fine);
|
||||
} while (--outer);
|
||||
return 0;
|
||||
@@ -123,18 +133,18 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||
[[gnu::noinline]] void send_block(bool eep, std::uint16_t at, std::uint8_t count)
|
||||
{
|
||||
do {
|
||||
tx(eep ? ee::read(at) : avr::flash_load(flash_ptr(at)));
|
||||
tx(eep ? ee::read<no_spm>(at) : avr::flash_load(flash_ptr(at)));
|
||||
++at;
|
||||
} while (--count);
|
||||
}
|
||||
|
||||
// One EEPROM byte in — shared by the emergency wipe and the 'E' stream.
|
||||
// One EEPROM byte in - shared by the emergency wipe and the 'E' stream.
|
||||
[[gnu::noinline]] void eeput(std::uint16_t at, std::uint8_t value)
|
||||
{
|
||||
ee::write<off>(at, value);
|
||||
ee::write<off, no_spm>(at, value);
|
||||
}
|
||||
|
||||
// Wait out a running SPM op, then re-open the RWW section — after every page
|
||||
// Wait out a running SPM op, then re-open the RWW section - after every page
|
||||
// op and before handing over, as the oracle does.
|
||||
[[gnu::noinline]] void settle()
|
||||
{
|
||||
@@ -142,19 +152,20 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||
spm::rww_enable<off>();
|
||||
}
|
||||
|
||||
// One host page straight into the erased flash page at `at` — through the SPM
|
||||
// word buffer (low byte then high), no SRAM staging — then committed. `at`
|
||||
// One host page straight into the erased flash page at `at` - through the SPM
|
||||
// word buffer (low byte then high), no SRAM staging - then committed. `at`
|
||||
// names a page base, so the cursor's low byte reaching the boundary ends the
|
||||
// walk.
|
||||
[[gnu::noinline]] void store_flash_page(std::uint16_t at)
|
||||
{
|
||||
const auto open = spm::page::begin<spm::from::boot_section, off>(at);
|
||||
do {
|
||||
std::uint8_t low = rx();
|
||||
std::uint8_t high = rx();
|
||||
spm::fill<off>(at, std::bit_cast<std::uint16_t>(std::array{low, high}));
|
||||
spm::fill<off>(open, at, std::bit_cast<std::uint16_t>(std::array{low, high}));
|
||||
at += 2;
|
||||
} while (static_cast<std::uint8_t>(at) & (page - 1));
|
||||
spm::write_page<off>(at - page);
|
||||
spm::command<off>(spm::op::write, at - page);
|
||||
settle();
|
||||
}
|
||||
|
||||
@@ -166,12 +177,12 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
tsb_app();
|
||||
}
|
||||
|
||||
// Step one page down and erase it — the erase shared by the whole-app walk,
|
||||
// Step one page down and erase it - the erase shared by the whole-app walk,
|
||||
// the config rewrite and the emergency wipe; hands the stepped address back.
|
||||
[[gnu::noinline]] std::uint16_t erase_below(std::uint16_t at)
|
||||
{
|
||||
at -= page;
|
||||
spm::erase_page<off>(at);
|
||||
spm::command<off>(spm::op::erase, at);
|
||||
settle();
|
||||
return at;
|
||||
}
|
||||
@@ -192,27 +203,30 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
{
|
||||
// A watchdog reset hands straight back to the application, as the
|
||||
// reference loader does, rather than re-entering the bootloader.
|
||||
if (avr::hw::mcusr::wdrf.test())
|
||||
if (avr::hw::mcusr::wdrf.test()) {
|
||||
appjump();
|
||||
}
|
||||
|
||||
// Lean bring-up from reset state: UCSR0C already reads 8N1, UBRR0H reads
|
||||
// 0, and the half-duplex write()/read() raise TXEN0/RXEN0 on first use —
|
||||
// 0, and the half-duplex write()/read() raise TXEN0/RXEN0 on first use -
|
||||
// only the divisor low byte and U2X0 need a store. The solver still does
|
||||
// the datasheet work; the asserts pin the reset-state assumptions.
|
||||
{
|
||||
constexpr auto sol = avr::uart::solve_baud(dev::clock, 115200_Bd);
|
||||
constexpr auto sol = avr::uart::solve_baud(dev::clock, 115200_Bd, 8, avr::uart::parity::none);
|
||||
static_assert(sol.u2x && sol.ubrr < 256, "lean bring-up writes UBRR0L only, with U2X0");
|
||||
avr::hw::ubrr0::write(static_cast<std::uint8_t>(sol.ubrr));
|
||||
avr::hw::ucsr0a::write(avr::hw::ucsr0a::u2x0(1));
|
||||
}
|
||||
|
||||
// Activation: 3×'@', each inside the config page's timeout window
|
||||
// (floored so a corrupt page cannot lock the loader out); anything else —
|
||||
// including silence — hands over.
|
||||
// Activation: 3x'@', each inside the config page's timeout window
|
||||
// (floored so a corrupt page cannot lock the loader out); anything else -
|
||||
// including silence - hands over.
|
||||
window = avr::flash_load(flash_ptr(app_end + 2)) | act_min;
|
||||
for (std::uint8_t k = 3; k; --k)
|
||||
if (rx() != knock)
|
||||
for (std::uint8_t k = 3; k; --k) {
|
||||
if (rx() != knock) {
|
||||
appjump();
|
||||
}
|
||||
}
|
||||
window = comm_window;
|
||||
|
||||
// Password gate (config page from app_end+3, 0xff-terminated; a blank
|
||||
@@ -231,10 +245,12 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
}
|
||||
std::uint8_t got = rx();
|
||||
if (got == 0) {
|
||||
if (mask == 0)
|
||||
if (mask == 0) {
|
||||
continue;
|
||||
if (rcnf() != confirm || rcnf() != confirm)
|
||||
}
|
||||
if (rcnf() != confirm || rcnf() != confirm) {
|
||||
appjump();
|
||||
}
|
||||
std::uint16_t a = erase_application();
|
||||
do {
|
||||
eeput(a, 0xff);
|
||||
@@ -242,8 +258,9 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
erase_below(app_end + page);
|
||||
break;
|
||||
}
|
||||
if (got != expected)
|
||||
if (got != expected) {
|
||||
mask = 0;
|
||||
}
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
@@ -252,22 +269,25 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
switch (command) {
|
||||
case 'f': // read application flash, one page per host '!'
|
||||
for (std::uint16_t a = 0; a < app_end; a += page) {
|
||||
if (rx() != confirm)
|
||||
if (rx() != confirm) {
|
||||
break;
|
||||
}
|
||||
send_block(false, a, page);
|
||||
}
|
||||
break;
|
||||
case 'e': // read EEPROM, one page per host '!', until the host stops
|
||||
for (std::uint16_t a = 0;; a += page) {
|
||||
if (rx() != confirm)
|
||||
if (rx() != confirm) {
|
||||
break;
|
||||
}
|
||||
send_block(true, a, page);
|
||||
}
|
||||
break;
|
||||
case 'F': { // erase the application, then take pages behind '?'
|
||||
std::uint16_t a = erase_application();
|
||||
for (; rcnf() == confirm; a += page)
|
||||
for (; rcnf() == confirm; a += page) {
|
||||
store_flash_page(a);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'E': // take EEPROM pages behind '?', each write host-paced
|
||||
@@ -284,8 +304,9 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
send_block(false, app_end, page);
|
||||
break;
|
||||
case 'C': // replace the config page, then echo it back to verify
|
||||
if (rcnf() != confirm)
|
||||
if (rcnf() != confirm) {
|
||||
break;
|
||||
}
|
||||
store_flash_page(erase_below(app_end + page));
|
||||
goto read_config;
|
||||
default: // 'q' or any other byte runs the application
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// TinySafeBoot on libavr — tier 1: pure, idiomatic C++.
|
||||
// TinySafeBoot on libavr - tier 1: pure, idiomatic C++.
|
||||
//
|
||||
// A serial flash bootloader for the ATmega328P boot section, reimplementing the
|
||||
// TinySafeBoot native-UART fixed-baud protocol on libavr with the full feature
|
||||
// set of the hand-written oracle: a watchdog-reset bail, one-wire half-duplex,
|
||||
// a config-page activation timeout, the password gate, emergency erase, and
|
||||
// config/flash/EEPROM read-write. This variant is written for clarity —
|
||||
// config/flash/EEPROM read-write. This variant is written for clarity -
|
||||
// well-factored functions, no compiler-specific size hacks, no inline assembly.
|
||||
// The one-wire wiring, the flash-resident info block and every SPM/EEPROM lock
|
||||
// are libavr's to handle; the only attribute is the naked reset entry that
|
||||
@@ -20,16 +20,25 @@ namespace ee = avr::eeprom;
|
||||
|
||||
using dev = avr::device<{.clock = 16_MHz}>;
|
||||
// One-wire: RX and TX share the line, exactly as the native-UART TSB expects.
|
||||
using serial_t = dev::uart0<{.baud = 115200_Bd, .max_baud_error = 3_pct, .half_duplex = true}>;
|
||||
// 115200 at 16 MHz lands +2.1 % off, past the receiver-tolerance table the
|
||||
// solver holds rates to - the oracle's own deployment has run there for a
|
||||
// decade, so the override states that it is meant.
|
||||
using serial_t = dev::uart0<{.baud = 115200_Bd, .allow_baud_error = true, .half_duplex = true}>;
|
||||
inline constexpr serial_t serial{};
|
||||
|
||||
namespace tsb {
|
||||
namespace {
|
||||
|
||||
// The loader is purely polled — it never enables interrupts — so every SPM and
|
||||
// The loader is purely polled - it never enables interrupts - so every SPM and
|
||||
// EEPROM lock folds to nothing under this posture.
|
||||
constexpr auto off = avr::irq::guard_policy::unused;
|
||||
|
||||
// Strict request/response: every SPM operation is waited out before the next
|
||||
// byte moves, so no flash operation is ever in flight at an EEPROM access -
|
||||
// the write procedure's step 2 has nothing to guard, the omission the
|
||||
// datasheet grants (DS40002061B section 8.6.3).
|
||||
constexpr auto no_spm = ee::spm_interlock::omitted;
|
||||
|
||||
// The handshake bytes, identical across every TSB host.
|
||||
constexpr std::uint8_t confirm = '!';
|
||||
constexpr std::uint8_t request = '?';
|
||||
@@ -83,14 +92,16 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||
// Stream `count` bytes to the host, from flash (LPM) or from EEPROM.
|
||||
void send_flash(std::uint16_t addr, std::uint8_t count)
|
||||
{
|
||||
while (count--)
|
||||
while (count--) {
|
||||
tx(avr::flash_load(flash_ptr(addr++)));
|
||||
}
|
||||
}
|
||||
|
||||
void send_eeprom(std::uint16_t addr, std::uint8_t count)
|
||||
{
|
||||
while (count--)
|
||||
tx(ee::read(addr++));
|
||||
while (count--) {
|
||||
tx(ee::read<no_spm>(addr++));
|
||||
}
|
||||
}
|
||||
|
||||
// Prompt the host with '?' and report whether it answered '!'.
|
||||
@@ -101,32 +112,33 @@ bool request_confirm()
|
||||
}
|
||||
|
||||
// Stream one page from the host straight into the already-erased flash page at
|
||||
// `addr`, filling the SPM word buffer low byte then high — no SRAM staging, so
|
||||
// `addr`, filling the SPM word buffer low byte then high - no SRAM staging, so
|
||||
// receiving and programming are the same loop.
|
||||
void store_flash_page(std::uint16_t addr)
|
||||
{
|
||||
const auto open = spm::page::begin<spm::from::boot_section, off>(addr);
|
||||
for (std::uint16_t i = 0; i < page; i += 2) {
|
||||
std::uint8_t lo = rx();
|
||||
std::uint8_t hi = rx();
|
||||
spm::fill<off>(addr + i, static_cast<std::uint16_t>(lo | (hi << 8)));
|
||||
spm::fill<off>(open, addr + i, static_cast<std::uint16_t>(lo | (hi << 8)));
|
||||
}
|
||||
spm::write_page<off>(addr);
|
||||
spm::wait();
|
||||
spm::write_page<spm::from::boot_section, off>(addr); // blocking: waits the write out
|
||||
}
|
||||
|
||||
// Stream one page from the host straight into EEPROM, byte by byte.
|
||||
void store_eeprom_page(std::uint16_t addr)
|
||||
{
|
||||
for (std::uint16_t i = 0; i < page; ++i)
|
||||
ee::write<off>(addr + i, rx());
|
||||
for (std::uint16_t i = 0; i < page; ++i) {
|
||||
ee::write<off, no_spm>(addr + i, rx());
|
||||
}
|
||||
}
|
||||
|
||||
// Erase one flash page and wait it out — the erase step shared by the whole-app
|
||||
// erase, the config-page rewrite and the emergency wipe.
|
||||
// Erase one flash page, waited out by the blocking spelling - the erase step
|
||||
// shared by the whole-app erase, the config-page rewrite and the emergency
|
||||
// wipe.
|
||||
void erase_page(std::uint16_t addr)
|
||||
{
|
||||
spm::erase_page<off>(addr);
|
||||
spm::wait();
|
||||
spm::erase_page<spm::from::boot_section, off>(addr);
|
||||
}
|
||||
|
||||
// Erase the whole application, one page at a time, top-down as the reference
|
||||
@@ -157,8 +169,9 @@ extern "C" [[noreturn]] void tsb_app();
|
||||
void read_flash()
|
||||
{
|
||||
for (std::uint16_t a = 0; a < app_end; a += page) {
|
||||
if (rx() != confirm)
|
||||
if (rx() != confirm) {
|
||||
return;
|
||||
}
|
||||
send_flash(a, page);
|
||||
}
|
||||
}
|
||||
@@ -167,8 +180,9 @@ void read_flash()
|
||||
void read_eeprom()
|
||||
{
|
||||
for (std::uint16_t a = 0;; a += page) {
|
||||
if (rx() != confirm)
|
||||
if (rx() != confirm) {
|
||||
return;
|
||||
}
|
||||
send_eeprom(a, page);
|
||||
}
|
||||
}
|
||||
@@ -178,22 +192,25 @@ void read_eeprom()
|
||||
void write_flash()
|
||||
{
|
||||
erase_application();
|
||||
for (std::uint16_t a = 0; request_confirm(); a += page)
|
||||
for (std::uint16_t a = 0; request_confirm(); a += page) {
|
||||
store_flash_page(a);
|
||||
}
|
||||
}
|
||||
|
||||
// 'E': take pages the host offers behind '?' into EEPROM.
|
||||
void write_eeprom()
|
||||
{
|
||||
for (std::uint16_t a = 0; request_confirm(); a += page)
|
||||
for (std::uint16_t a = 0; request_confirm(); a += page) {
|
||||
store_eeprom_page(a);
|
||||
}
|
||||
}
|
||||
|
||||
// 'C': replace the config page, then echo it back for the host to verify.
|
||||
void write_config()
|
||||
{
|
||||
if (!request_confirm())
|
||||
if (!request_confirm()) {
|
||||
return;
|
||||
}
|
||||
erase_page(app_end);
|
||||
store_flash_page(app_end);
|
||||
spm::rww_enable<off>();
|
||||
@@ -206,8 +223,9 @@ void write_config()
|
||||
void emergency_erase()
|
||||
{
|
||||
erase_application();
|
||||
for (std::uint16_t a = 0; a <= eeprom_end; ++a)
|
||||
ee::write<off>(a, 0xff);
|
||||
for (std::uint16_t a = 0; a <= eeprom_end; ++a) {
|
||||
ee::write<off, no_spm>(a, 0xff);
|
||||
}
|
||||
erase_page(app_end);
|
||||
spm::rww_enable<off>();
|
||||
}
|
||||
@@ -222,14 +240,18 @@ gate password_gate()
|
||||
{
|
||||
for (const std::uint8_t *pw = flash_ptr(app_end + 3);; ++pw) {
|
||||
std::uint8_t expected = avr::flash_load(pw);
|
||||
if (expected == 0xff)
|
||||
if (expected == 0xff) {
|
||||
return gate::pass;
|
||||
}
|
||||
std::uint8_t got = rx();
|
||||
if (got == 0)
|
||||
if (got == 0) {
|
||||
return gate::emergency;
|
||||
if (got != expected)
|
||||
for (;;)
|
||||
}
|
||||
if (got != expected) {
|
||||
for (;;) {
|
||||
rx();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,8 +259,9 @@ gate password_gate()
|
||||
{
|
||||
// A watchdog reset hands straight back to the application, as the reference
|
||||
// loader does, rather than re-entering the bootloader.
|
||||
if (avr::hw::mcusr::wdrf.test())
|
||||
if (avr::hw::mcusr::wdrf.test()) {
|
||||
appjump();
|
||||
}
|
||||
|
||||
avr::init<serial_t>();
|
||||
|
||||
@@ -248,10 +271,11 @@ gate password_gate()
|
||||
__uint24 idle = static_cast<__uint24>(avr::flash_load(flash_ptr(app_end + 2)) | 16) << 16;
|
||||
std::uint8_t knocks = 0;
|
||||
while (knocks < 3) {
|
||||
if (auto byte = serial.read())
|
||||
if (auto byte = serial.read()) {
|
||||
knocks = *byte == knock ? knocks + 1 : 0;
|
||||
else if (--idle == 0)
|
||||
} else if (--idle == 0) {
|
||||
appjump();
|
||||
}
|
||||
}
|
||||
|
||||
switch (password_gate()) {
|
||||
@@ -259,8 +283,9 @@ gate password_gate()
|
||||
send_flash(reinterpret_cast<std::uint16_t>(info::storage.data()), info::size());
|
||||
break;
|
||||
case gate::emergency:
|
||||
if (!request_confirm() || !request_confirm())
|
||||
if (!request_confirm() || !request_confirm()) {
|
||||
appjump();
|
||||
}
|
||||
emergency_erase();
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
// TinySafeBoot on libavr — tier 2: C++ with compiler trickery, no assembly.
|
||||
// TinySafeBoot on libavr - tier 2: C++ with compiler trickery, no assembly.
|
||||
//
|
||||
// The full TinySafeBoot feature set — watchdog bail, one-wire half-duplex,
|
||||
// The full TinySafeBoot feature set - watchdog bail, one-wire half-duplex,
|
||||
// config-page activation timeout, password gate, emergency erase, and
|
||||
// config/flash/EEPROM read-write — in pure C++, 526 bytes: 14 over the 512-byte
|
||||
// config/flash/EEPROM read-write - in pure C++, 526 bytes: 14 over the 512-byte
|
||||
// boot section the hand-written oracle fits, from 168 over at this tier's first
|
||||
// floor. The structure mirrors the oracle's: a handful of tiny noinline
|
||||
// primitives sharing one whole-loader register allocation, expressed as global
|
||||
// register variables so no helper ever saves, spills, or reloads any of it.
|
||||
//
|
||||
// The register protocol (all call-saved, so calls preserve them by ABI):
|
||||
// Y (r28:r29) g_addr the walked flash/EEPROM address — adiw-able
|
||||
// r16 g_cnt byte countdown of the running block — ldi-able
|
||||
// Y (r28:r29) g_addr the walked flash/EEPROM address - adiw-able
|
||||
// r16 g_cnt byte countdown of the running block - ldi-able
|
||||
// r7 g_window rx timeout, roughly 30 ms units at 16 MHz
|
||||
// r6 g_receiving one-wire direction latch, cleared at bring-up
|
||||
// (power-on registers are undefined)
|
||||
@@ -18,10 +18,10 @@
|
||||
// GCC 16.1 miscompiles stores into global register variables: an update whose
|
||||
// remaining uses all hide inside callees is deleted whenever a CALL follows it
|
||||
// before any jump/ret (the backend's liveness walk lumps fixed registers with
|
||||
// call-clobbered ones — minimal repro in libavr's
|
||||
// call-clobbered ones - minimal repro in libavr's
|
||||
// local/scratch/probes/gcc-avr-globalreg-repro.cpp, lessons.md entry). Every
|
||||
// g_* update below therefore sits where a *local* read or a jump/ret follows
|
||||
// it — the helpers advance g_addr immediately before returning, and rx()
|
||||
// it - the helpers advance g_addr immediately before returning, and rx()
|
||||
// re-floors the window on every call instead of storing the floored value
|
||||
// once. The layout is load-bearing; do not "simplify" it.
|
||||
//
|
||||
@@ -41,10 +41,16 @@ namespace hw = avr::hw;
|
||||
namespace tsb {
|
||||
namespace {
|
||||
|
||||
// The loader is purely polled — it never enables interrupts — so every SPM and
|
||||
// The loader is purely polled - it never enables interrupts - so every SPM and
|
||||
// EEPROM lock folds to nothing under this posture.
|
||||
constexpr auto off = avr::irq::guard_policy::unused;
|
||||
|
||||
// Strict request/response: every SPM operation is waited out before the next
|
||||
// byte moves, so no flash operation is ever in flight at an EEPROM access -
|
||||
// the write procedure's step 2 has nothing to guard, the omission the
|
||||
// datasheet grants (DS40002061B section 8.6.3).
|
||||
constexpr auto no_spm = ee::spm_interlock::omitted;
|
||||
|
||||
constexpr std::uint8_t confirm = '!';
|
||||
constexpr std::uint8_t request = '?';
|
||||
constexpr std::uint8_t knock = '@';
|
||||
@@ -65,7 +71,7 @@ constexpr std::uint8_t comm_window = 200;
|
||||
constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 20;
|
||||
|
||||
// Fixed 115200 8N1; the library solves UBRR + U2X from clock and baud.
|
||||
constexpr auto baud = avr::uart::solve_baud(16_MHz, 115200_Bd);
|
||||
constexpr auto baud = avr::uart::solve_baud(16_MHz, 115200_Bd, 8, avr::uart::parity::none);
|
||||
|
||||
// The 16-byte device-info block, streamed out on activation.
|
||||
// clang-format off
|
||||
@@ -93,8 +99,8 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||
|
||||
// Bounded byte receive, the oracle's shape: release the one-wire line on a
|
||||
// direction change, poll RXC0 under nested countdowns, 0 on silence. The 0
|
||||
// then falls through every compare — not a knock, not a confirm, not a
|
||||
// command — so a silent host unwinds the loader to the application from
|
||||
// then falls through every compare - not a knock, not a confirm, not a
|
||||
// command - so a silent host unwinds the loader to the application from
|
||||
// anywhere, and a mid-session cable pull cannot wedge it.
|
||||
[[gnu::noinline, gnu::noclone]] std::uint8_t rx()
|
||||
{
|
||||
@@ -102,24 +108,25 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||
g_receiving = 1;
|
||||
hw::ucsr0b::write(hw::ucsr0b::rxen0(1)); // RXEN0 alone: release and listen
|
||||
}
|
||||
// act_min ORs in here, per call, not once into g_window at setup — the
|
||||
// act_min ORs in here, per call, not once into g_window at setup - the
|
||||
// one placement the global-register-store miscompile cannot delete.
|
||||
std::uint16_t outer = static_cast<std::uint16_t>(g_window | act_min) << 8;
|
||||
do {
|
||||
std::uint8_t fine = 0;
|
||||
do {
|
||||
auto status = hw::ucsr0a::read();
|
||||
if (status & hw::ucsr0a::rxc0(1).value)
|
||||
if (status & hw::ucsr0a::rxc0(1).value) {
|
||||
return hw::udr0::read();
|
||||
}
|
||||
} while (--fine);
|
||||
} while (--outer);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// One-wire transmit: take the line (TXEN0 alone — the receiver must be off
|
||||
// One-wire transmit: take the line (TXEN0 alone - the receiver must be off
|
||||
// while driving) on a direction change, with a turn-around guard so a shorted
|
||||
// peer can switch first; then hold the line until the whole frame is out
|
||||
// (TXC0, not UDRE0 — the stop bit must be on the wire before a caller may
|
||||
// (TXC0, not UDRE0 - the stop bit must be on the wire before a caller may
|
||||
// release the line), and W1C TXC0 by storing the sampled status back, which
|
||||
// keeps U2X0.
|
||||
[[gnu::noinline, gnu::noclone]] void tx(std::uint8_t byte)
|
||||
@@ -145,7 +152,7 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||
return rx();
|
||||
}
|
||||
|
||||
// One flash byte ← [g_addr++] (the advance right before ret — see header).
|
||||
// One flash byte <- [g_addr++] (the advance right before ret - see header).
|
||||
[[gnu::noinline, gnu::noclone]] std::uint8_t sflash()
|
||||
{
|
||||
std::uint8_t byte = avr::flash_load(flash_ptr(g_addr));
|
||||
@@ -153,18 +160,18 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||
return byte;
|
||||
}
|
||||
|
||||
// One EEPROM byte ← [g_addr++].
|
||||
// One EEPROM byte <- [g_addr++].
|
||||
[[gnu::noinline, gnu::noclone]] std::uint8_t eerd()
|
||||
{
|
||||
std::uint8_t byte = ee::read(g_addr);
|
||||
std::uint8_t byte = ee::read<no_spm>(g_addr);
|
||||
++g_addr;
|
||||
return byte;
|
||||
}
|
||||
|
||||
// One EEPROM byte → [g_addr++].
|
||||
// One EEPROM byte -> [g_addr++].
|
||||
[[gnu::noinline, gnu::noclone]] void eewr(std::uint8_t byte)
|
||||
{
|
||||
ee::write<off>(g_addr, byte);
|
||||
ee::write<off, no_spm>(g_addr, byte);
|
||||
++g_addr;
|
||||
}
|
||||
|
||||
@@ -176,7 +183,7 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||
} while (--g_cnt);
|
||||
}
|
||||
|
||||
// Wait out a running SPM op, then re-open the RWW section — after every page
|
||||
// Wait out a running SPM op, then re-open the RWW section - after every page
|
||||
// op and before handing over, as the oracle does.
|
||||
[[gnu::noinline, gnu::noclone]] void settle()
|
||||
{
|
||||
@@ -198,12 +205,12 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
[[gnu::noinline, gnu::noclone]] void erase_below()
|
||||
{
|
||||
g_addr -= page;
|
||||
spm::erase_page<off>(g_addr);
|
||||
spm::command<off>(spm::op::erase, g_addr);
|
||||
settle();
|
||||
}
|
||||
|
||||
// Erase the whole application, top-down like the oracle: the loop bound is a
|
||||
// compare with zero, and g_addr = 0 — the value every caller wants next — is
|
||||
// compare with zero, and g_addr = 0 - the value every caller wants next - is
|
||||
// handed back for free.
|
||||
[[gnu::noinline, gnu::noclone]] void erase_application()
|
||||
{
|
||||
@@ -214,18 +221,19 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
}
|
||||
|
||||
// Stream one host page into the erased flash page at g_addr (SPM word buffer,
|
||||
// low byte then high) — no SRAM staging, receive and program are one loop.
|
||||
// low byte then high) - no SRAM staging, receive and program are one loop.
|
||||
// g_addr is left at the next page base.
|
||||
[[gnu::noinline, gnu::noclone]] void store_flash()
|
||||
{
|
||||
const auto open = spm::page::begin<spm::from::boot_section, off>(g_addr);
|
||||
g_cnt = page / 2;
|
||||
do {
|
||||
std::uint16_t word = rx();
|
||||
word |= static_cast<std::uint16_t>(rx()) << 8;
|
||||
spm::fill<off>(g_addr, word);
|
||||
spm::fill<off>(open, g_addr, word);
|
||||
g_addr += 2;
|
||||
} while (--g_cnt);
|
||||
spm::write_page<off>(g_addr - page);
|
||||
spm::command<off>(spm::op::write, g_addr - page);
|
||||
settle();
|
||||
}
|
||||
|
||||
@@ -233,11 +241,12 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
{
|
||||
// A watchdog reset hands straight back to the application, as the
|
||||
// reference loader does, rather than re-entering the bootloader.
|
||||
if (hw::mcusr::wdrf.test())
|
||||
if (hw::mcusr::wdrf.test()) {
|
||||
appjump();
|
||||
}
|
||||
|
||||
// Lean bring-up from reset state: UCSR0C already reads 8N1, UBRR0H reads
|
||||
// 0, and rx()/tx() raise RXEN0/TXEN0 on first use — only the divisor low
|
||||
// 0, and rx()/tx() raise RXEN0/TXEN0 on first use - only the divisor low
|
||||
// byte and U2X0 need a store. The library still does the datasheet work.
|
||||
static_assert(baud.u2x && baud.ubrr < 256, "lean bring-up writes UBRR0L only, with U2X0");
|
||||
hw::ubrr0::write(static_cast<std::uint8_t>(baud.ubrr));
|
||||
@@ -248,13 +257,15 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
// same reason.
|
||||
g_receiving = 0;
|
||||
|
||||
// Activation: 3×'@', each inside the config page's timeout window (rx
|
||||
// floors it so a corrupt page cannot lock the loader out); anything else —
|
||||
// including silence — hands over.
|
||||
// Activation: 3x'@', each inside the config page's timeout window (rx
|
||||
// floors it so a corrupt page cannot lock the loader out); anything else -
|
||||
// including silence - hands over.
|
||||
g_window = avr::flash_load(flash_ptr(app_end + 2));
|
||||
for (std::uint8_t k = 3; k; --k)
|
||||
if (rx() != knock)
|
||||
for (std::uint8_t k = 3; k; --k) {
|
||||
if (rx() != knock) {
|
||||
appjump();
|
||||
}
|
||||
}
|
||||
g_window = comm_window;
|
||||
|
||||
// Password gate (config page from app_end+3, 0xff-terminated; a blank
|
||||
@@ -275,10 +286,12 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
}
|
||||
std::uint8_t got = rx();
|
||||
if (got == 0) {
|
||||
if (mask == 0)
|
||||
if (mask == 0) {
|
||||
continue;
|
||||
if (rcnf() != confirm || rcnf() != confirm)
|
||||
}
|
||||
if (rcnf() != confirm || rcnf() != confirm) {
|
||||
appjump();
|
||||
}
|
||||
erase_application(); // leaves g_addr = 0 for the EEPROM walk
|
||||
do {
|
||||
eewr(0xff);
|
||||
@@ -287,8 +300,9 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
erase_below();
|
||||
break;
|
||||
}
|
||||
if (got != expected)
|
||||
if (got != expected) {
|
||||
mask = 0;
|
||||
}
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
@@ -297,23 +311,27 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
switch (rx()) {
|
||||
case 'f': // read application flash, one page per host '!'
|
||||
for (;;) {
|
||||
if (rx() != confirm)
|
||||
if (rx() != confirm) {
|
||||
break;
|
||||
}
|
||||
g_cnt = page;
|
||||
sendf();
|
||||
if (g_addr >= app_end)
|
||||
if (g_addr >= app_end) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'F': // erase the application, then take pages behind '?'
|
||||
erase_application(); // leaves g_addr = 0, the write start
|
||||
while (rcnf() == confirm)
|
||||
while (rcnf() == confirm) {
|
||||
store_flash();
|
||||
}
|
||||
break;
|
||||
case 'e': // read EEPROM, one page per host '!', until the host stops
|
||||
for (;;) {
|
||||
if (rx() != confirm)
|
||||
if (rx() != confirm) {
|
||||
break;
|
||||
}
|
||||
g_cnt = page;
|
||||
do {
|
||||
tx(eerd());
|
||||
@@ -335,8 +353,9 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
sendf();
|
||||
break;
|
||||
case 'C': // replace the config page, then echo it back to verify
|
||||
if (rcnf() != confirm)
|
||||
if (rcnf() != confirm) {
|
||||
break;
|
||||
}
|
||||
g_addr = app_end + page;
|
||||
erase_below(); // leaves g_addr = app_end, the store target
|
||||
store_flash();
|
||||
|
||||
Reference in New Issue
Block a user