diff --git a/tsb/tsb_asm.cpp b/tsb/tsb_asm.cpp index eef2dda..8cba7f1 100644 --- a/tsb/tsb_asm.cpp +++ b/tsb/tsb_asm.cpp @@ -32,7 +32,7 @@ #include -#include // SP / RAMEND for the crt-free boot entry, SFR addresses for the asm routines +#include // SPMCSR's I/O address for the page-store routine using namespace avr::literals; namespace spm = avr::spm; @@ -56,6 +56,8 @@ constexpr std::uint8_t confirm = '!'; constexpr std::uint8_t request = '?'; constexpr std::uint8_t knock = '@'; +constexpr auto clock = 16_MHz; + // Boot geometry for the 512 B boot section (BOOTSZ=11); the page size and the // flash/EEPROM extents are the chip database's to know. app_end is the config // page (TSB's LASTPAGE), one page below the boot section. @@ -65,20 +67,20 @@ constexpr std::uint16_t app_end = spm::flash_bytes - boot_bytes - page; constexpr std::uint16_t eeprom_end = avr::hw::db.mem.eeprom_size - 1; // Lockout-proof floor for the activation window: the oracle's F_CPU/1MHz, so -// it follows the clock rather than restating it (rule 41). -constexpr auto act_min = static_cast((16_MHz).hz / 1'000'000); +// it follows the clock rather than restating it. +constexpr auto act_min = static_cast(clock.hz / 1'000'000); // Post-activation window: the host gets seconds, not milliseconds, mid-session. 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, 8, avr::uart::parity::none); +constexpr auto baud = avr::uart::solve_baud(clock, 115200_Bd, 8, avr::uart::parity::none); // One bit time on the wire: the turn-around a shared-line peer needs to stop // driving before this one starts. Derived from the solved rate, so it follows // the link rather than a count measured against one. -constexpr auto guard_cycles = static_cast((16_MHz).hz / baud.actual); +constexpr auto guard_cycles = static_cast(clock.hz / baud.actual); // The 16-byte device-info block, streamed out on activation. // clang-format off @@ -113,7 +115,7 @@ const std::uint8_t *flash_ptr(std::uint16_t addr) std::uint8_t byte; asm volatile(" tst %[dir] \n\t" // already receiving? keep the line released " brne 1f \n\t" - " ldi %[b], 0x10 \n\t" // RXEN0 alone: release the line and listen + " ldi %[b], %[rxen] \n\t" // the receiver alone: release the line and listen " sts %[ucsr0b], %[b] \n\t" " ser %[b] \n\t" " mov %[dir], %[b] \n\t" @@ -122,7 +124,7 @@ const std::uint8_t *flash_ptr(std::uint16_t addr) " clr r26 \n\t" "2: ser %[b] \n\t" "3: lds %[b], %[ucsr0a] \n\t" - " sbrc %[b], 7 \n\t" // RXC0 + " sbrc %[b], %[rxc] \n\t" " rjmp 4f \n\t" " dec %[b] \n\t" " brne 3b \n\t" @@ -133,8 +135,9 @@ const std::uint8_t *flash_ptr(std::uint16_t addr) "4: lds %[b], %[udr0] \n\t" "5: \n\t" : [b] "=&d"(byte), [dir] "+r"(g_receiving) - : [to] "r"(g_window), [actmin] "M"(act_min), [ucsr0a] "n"(_SFR_MEM_ADDR(UCSR0A)), - [ucsr0b] "n"(_SFR_MEM_ADDR(UCSR0B)), [udr0] "n"(_SFR_MEM_ADDR(UDR0)) + : [to] "r"(g_window), [actmin] "M"(act_min), [rxen] "M"(hw::ucsr0b::rxen0(1).value), + [rxc] "I"(std::countr_zero(hw::ucsr0a::rxc0.mask)), [ucsr0a] "n"(hw::ucsr0a::addr), + [ucsr0b] "n"(hw::ucsr0b::addr), [udr0] "n"(hw::udr0::addr) : "r26", "r27", "cc"); return byte; } @@ -169,7 +172,7 @@ const std::uint8_t *flash_ptr(std::uint16_t addr) // global-register rule, see tsb_tricks.cpp). [[gnu::noinline, gnu::noclone]] std::uint8_t sflash() { - std::uint8_t byte = avr::flash_load(flash_ptr(g_addr)); + const auto byte = avr::flash_load(flash_ptr(g_addr)); ++g_addr; return byte; } @@ -177,7 +180,7 @@ const std::uint8_t *flash_ptr(std::uint16_t addr) // One EEPROM byte <- [g_addr++]. [[gnu::noinline, gnu::noclone]] std::uint8_t eerd() { - std::uint8_t byte = ee::read(g_addr); + const auto byte = ee::read(g_addr); ++g_addr; return byte; } @@ -213,7 +216,7 @@ 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() { @@ -233,7 +236,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() @@ -243,8 +246,8 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def "1: rcall %x[rx] \n\t" " mov r0, r24 \n\t" // word low byte " rcall %x[rx] \n\t" - " mov r1, r24 \n\t" // word high byte - " ldi r24, 0x01 \n\t" // SPMEN: buffer the word at Z + " mov r1, r24 \n\t" // word high byte + " ldi r24, %[fill] \n\t" // buffer the word at Z " out %[spmcsr], r24 \n\t" " spm \n\t" " clr r1 \n\t" @@ -254,12 +257,13 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def " movw %[base], r30 \n\t" // g_addr = the next page base " subi r30, %[pagelo] \n\t" // Z back to this page's base " sbci r31, %[pagehi] \n\t" - " ldi r24, 0x05 \n\t" // PGWRT | SPMEN: program the page + " ldi r24, %[write] \n\t" // program the page " out %[spmcsr], r24 \n\t" " spm \n\t" : [base] "+r"(g_addr) - : [rx] "i"(&rx), [spmcsr] "I"(_SFR_IO_ADDR(SPMCSR)), [words] "M"(page / 2), [pagelo] "M"(page & 0xff), - [pagehi] "M"(page >> 8) + : [rx] "i"(&rx), [spmcsr] "I"(_SFR_IO_ADDR(SPMCSR)), [fill] "M"(hw::spmcsr::spmen(1).value), + [write] "M"(hw::spmcsr::pgwrt(1).value | hw::spmcsr::spmen(1).value), [words] "M"(page / 2), + [pagelo] "M"(page & 0xff), [pagehi] "M"(page >> 8) : "r0", "r1", "r20", "r24", "r26", "r27", "r30", "r31", "cc", "memory"); settle(); } @@ -285,7 +289,7 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def g_receiving = 0; // Activation: 3x'@', each inside the config page's timeout window (rx - // floors it so a corrupt page cannot lock the loader out); anything else - + // 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) { @@ -311,7 +315,7 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def sendf(); break; } - std::uint8_t got = rx(); + const auto got = rx(); if (got == 0) { if (mask == 0) { continue; diff --git a/tsb/tsb_policy.cpp b/tsb/tsb_policy.cpp index 81030ff..25913bb 100644 --- a/tsb/tsb_policy.cpp +++ b/tsb/tsb_policy.cpp @@ -2,23 +2,17 @@ // // 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 -// obeys it: no assembly, no register variables; code, attributes, and flags -// only. Every lesson pureboot's development produced is applied - the +// config/flash/EEPROM read-write - with no assembly and no register variables: +// code, attributes and flags only. It applies pureboot's size techniques - 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 - -// and the result sits below the idiomatic tier and above the 512 B boot -// section the tricks/asm tiers reach with the banned mechanisms -// (oracle/README.md holds all four). This tier exists to keep that gap an -// artifact -// 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 -// loop) measured larger than the split cases they replaced. TSB's wire fixes -// the per-command loop shapes on the device, so pureboot 5's one-transfer- -// loop collapse has no purchase here. +// locals threaded through noinline primitives, pureboot's codegen flags - and +// lands between the idiomatic tier and the 512 B section the tricks/asm tiers +// reach with the banned mechanisms (oracle/README.md holds all four). 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 loop) measured larger than the split cases. // // The wire protocol is strict request/response, which is what makes the // shared line safe: the device drives it only between a received command and @@ -36,11 +30,12 @@ using dev = avr::device<{.clock = 16_MHz}>; // 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<{ +constexpr avr::uart::config link{ .baud = 115200_Bd, .allow_baud_error = true, .half_duplex = true, -}>; +}; +using serial_t = dev::uart0; inline constexpr serial_t serial{}; namespace tsb { @@ -70,7 +65,7 @@ constexpr std::uint16_t app_end = spm::flash_bytes - boot_bytes - page; constexpr std::uint16_t eeprom_end = avr::hw::db.mem.eeprom_size - 1; // Lockout-proof floor for the activation window: the oracle's F_CPU/1MHz, so -// it follows the clock rather than restating it (rule 41). +// it follows the clock rather than restating it. constexpr auto act_min = static_cast(dev::clock.hz / 1'000'000); // Post-activation window: the host gets seconds, not milliseconds, mid-session. constexpr std::uint8_t comm_window = 200; @@ -166,8 +161,8 @@ const std::uint8_t *flash_ptr(std::uint16_t addr) { const auto open = spm::page::begin(at); do { - std::uint8_t low = rx(); - std::uint8_t high = rx(); + const auto low = rx(); + const auto high = rx(); spm::fill(open, at, std::bit_cast(std::array{low, high})); at += 2; } while (static_cast(at) & (page - 1)); @@ -214,18 +209,18 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def } // 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, 8, avr::uart::parity::none); + constexpr auto sol = avr::uart::solve_baud(dev::clock, link.baud, link.data_bits, link.par); static_assert(sol.u2x && sol.ubrr < 256, "lean bring-up writes UBRR0L only, with U2X0"); avr::hw::ubrr0::write(static_cast(sol.ubrr)); avr::hw::ucsr0a::write(avr::hw::ucsr0a::u2x0(1)); } // Activation: 3x'@', each inside the config page's timeout window - // (floored so a corrupt page cannot lock the loader out); anything else - + // (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) { @@ -243,13 +238,13 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def std::uint16_t at = app_end + 3; std::uint8_t mask = 0xff; for (;;) { - std::uint8_t expected = avr::flash_load(flash_ptr(at)) & mask; + const std::uint8_t expected = avr::flash_load(flash_ptr(at)) & mask; ++at; if (expected == 0xff) { send_block(false, reinterpret_cast(info.data()), info.size()); break; } - std::uint8_t got = rx(); + const auto got = rx(); if (got == 0) { if (mask == 0) { continue; @@ -271,7 +266,7 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def for (;;) { tx(confirm); // Mainloop ready - const std::uint8_t command = rx(); + const auto command = rx(); switch (command) { case 'f': // read application flash, one page per host '!' for (std::uint16_t a = 0; a < app_end; a += page) { diff --git a/tsb/tsb_pure.cpp b/tsb/tsb_pure.cpp index bbfab3e..0ba66a3 100644 --- a/tsb/tsb_pure.cpp +++ b/tsb/tsb_pure.cpp @@ -4,16 +4,13 @@ // 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 -// stands in for the absent C runtime. +// The one-wire wiring, the flash-resident info block, every SPM/EEPROM lock and +// the crt-free reset entry are libavr's to handle. #include -#include // SP / RAMEND for the crt-free boot entry - using namespace avr::literals; namespace spm = avr::spm; namespace ee = avr::eeprom; @@ -144,8 +141,8 @@ void store_flash_page(std::uint16_t addr) { const auto open = spm::page::begin(addr); for (std::uint16_t i = 0; i < page; i += 2) { - std::uint8_t lo = rx(); - std::uint8_t hi = rx(); + const auto lo = rx(); + const auto hi = rx(); spm::fill(open, addr + i, static_cast(lo | (hi << 8))); } spm::write_page(addr); // blocking: waits the write out @@ -265,11 +262,11 @@ enum class gate : std::uint8_t { pass, emergency }; gate password_gate() { for (const std::uint8_t *pw = flash_ptr(app_end + 3);; ++pw) { - std::uint8_t expected = avr::flash_load(pw); + const auto expected = avr::flash_load(pw); if (expected == 0xff) { return gate::pass; } - std::uint8_t got = rx(); + const auto got = rx(); if (got == 0) { return gate::emergency; } diff --git a/tsb/tsb_tricks.cpp b/tsb/tsb_tricks.cpp index 09ed7c2..6d99815 100644 --- a/tsb/tsb_tricks.cpp +++ b/tsb/tsb_tricks.cpp @@ -31,8 +31,6 @@ #include -#include // SP / RAMEND for the crt-free boot entry - using namespace avr::literals; namespace spm = avr::spm; namespace ee = avr::eeprom; @@ -55,6 +53,8 @@ constexpr std::uint8_t confirm = '!'; constexpr std::uint8_t request = '?'; constexpr std::uint8_t knock = '@'; +constexpr auto clock = 16_MHz; + // Boot geometry for the 1 KB boot section (BOOTSZ=10); the page size and the // flash/EEPROM extents are the chip database's to know. app_end is the config // page (TSB's LASTPAGE), one page below the boot section. @@ -64,20 +64,20 @@ constexpr std::uint16_t app_end = spm::flash_bytes - boot_bytes - page; constexpr std::uint16_t eeprom_end = avr::hw::db.mem.eeprom_size - 1; // Lockout-proof floor for the activation window: the oracle's F_CPU/1MHz, so -// it follows the clock rather than restating it (rule 41). -constexpr auto act_min = static_cast((16_MHz).hz / 1'000'000); +// it follows the clock rather than restating it. +constexpr auto act_min = static_cast(clock.hz / 1'000'000); // Post-activation window: the host gets seconds, not milliseconds, mid-session. 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, 8, avr::uart::parity::none); +constexpr auto baud = avr::uart::solve_baud(clock, 115200_Bd, 8, avr::uart::parity::none); // One bit time on the wire: the turn-around a shared-line peer needs to stop // driving before this one starts. Derived from the solved rate, so it follows // the link rather than a count measured against one. -constexpr auto guard_cycles = static_cast((16_MHz).hz / baud.actual); +constexpr auto guard_cycles = static_cast(clock.hz / baud.actual); // The 16-byte device-info block, streamed out on activation. // clang-format off @@ -160,7 +160,7 @@ const std::uint8_t *flash_ptr(std::uint16_t addr) // 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)); + const auto byte = avr::flash_load(flash_ptr(g_addr)); ++g_addr; return byte; } @@ -168,7 +168,7 @@ const std::uint8_t *flash_ptr(std::uint16_t addr) // One EEPROM byte <- [g_addr++]. [[gnu::noinline, gnu::noclone]] std::uint8_t eerd() { - std::uint8_t byte = ee::read(g_addr); + const auto byte = ee::read(g_addr); ++g_addr; return byte; } @@ -263,7 +263,7 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def g_receiving = 0; // Activation: 3x'@', each inside the config page's timeout window (rx - // floors it so a corrupt page cannot lock the loader out); anything else - + // 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) { @@ -289,7 +289,7 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def sendf(); break; } - std::uint8_t got = rx(); + const auto got = rx(); if (got == 0) { if (mask == 0) { continue;