diff --git a/tsb/tsb.hpp b/tsb/tsb.hpp new file mode 100644 index 0000000..402e4f2 --- /dev/null +++ b/tsb/tsb.hpp @@ -0,0 +1,106 @@ +// The TinySafeBoot native-UART fixed-baud protocol, as every tier speaks it: +// the link, the handshake and command bytes, the boot geometry and the +// device-info block the host reads on activation. + +#pragma once + +#include + +namespace tsb { + +using namespace avr::literals; +namespace spm = avr::spm; +namespace ee = avr::eeprom; + +constexpr auto clock = 16_MHz; +constexpr auto baud_rate = 115200_Bd; + +// Fixed 8N1; the library solves UBRR + U2X from clock and baud. +constexpr auto baud = avr::uart::solve_baud(clock, baud_rate, 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. +constexpr auto guard_cycles = static_cast(clock.hz / baud.actual); + +// 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 = '@'; +constexpr std::uint8_t activation_knocks = 3; + +namespace command { +constexpr std::uint8_t read_flash = 'f'; +constexpr std::uint8_t write_flash = 'F'; +constexpr std::uint8_t read_eeprom = 'e'; +constexpr std::uint8_t write_eeprom = 'E'; +constexpr std::uint8_t read_config = 'c'; +constexpr std::uint8_t write_config = 'C'; +} // namespace command + +// The lockout-proof floor for the receive window: the oracle's F_CPU/1MHz. +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 page = spm::page_bytes; +constexpr std::uint16_t eeprom_end = avr::hw::db.mem.eeprom_size - 1; + +// The config page (TSB's LASTPAGE), one page below the boot section: the +// application's jump vector, then these. +consteval std::uint16_t config_page(std::uint16_t boot_bytes) +{ + return spm::flash_bytes - boot_bytes - page; +} +constexpr std::uint8_t timeout_at = 2; +constexpr std::uint8_t password_at = 3; + +// The firmware version stamp, in the encoding the host decodes. +consteval std::uint16_t date_stamp(std::uint16_t year, std::uint16_t month, std::uint16_t day) +{ + return year * 512 + month * 32 + day; +} + +// BUILDSTATE F3: the native-UART fixed-baud lineage. +constexpr std::uint8_t build_state = 0xF3; +constexpr std::uint8_t atmega_marker = 0xAA; + +consteval auto info_block(std::uint16_t build_date, std::uint16_t app_end) +{ + const auto low = [](std::uint16_t word) { return static_cast(word); }; + const auto high = [](std::uint16_t word) { return static_cast(word >> 8); }; + const std::uint16_t app_end_words = app_end / 2; + return std::to_array({ + 'T', + 'S', + 'B', + low(build_date), + high(build_date), + build_state, + avr::hw::db.signature[0], + avr::hw::db.signature[1], + avr::hw::db.signature[2], + page / 2, + low(app_end_words), + high(app_end_words), + low(eeprom_end), + high(eeprom_end), + atmega_marker, + atmega_marker, + }); +} + +inline const std::uint8_t *flash_ptr(std::uint16_t addr) +{ + return reinterpret_cast(addr); +} + +} // namespace tsb diff --git a/tsb/tsb_asm.cpp b/tsb/tsb_asm.cpp index 8cba7f1..90f264a 100644 --- a/tsb/tsb_asm.cpp +++ b/tsb/tsb_asm.cpp @@ -30,82 +30,26 @@ // line safe: the device drives it only between a received command and its // reply, and releases it (RXEN0 only) whenever it waits. -#include +#include "tsb.hpp" #include // SPMCSR's I/O address for the page-store routine -using namespace avr::literals; -namespace spm = avr::spm; -namespace ee = avr::eeprom; namespace hw = avr::hw; namespace tsb { namespace { -// 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 = '@'; - -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. -constexpr std::uint16_t page = spm::page_bytes; +// The boot section: 512 B, BOOTSZ=11. constexpr std::uint16_t boot_bytes = 512; -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; +constexpr std::uint16_t app_end = config_page(boot_bytes); -// Lockout-proof floor for the activation window: the oracle's F_CPU/1MHz, so -// 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(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(clock.hz / baud.actual); - -// The 16-byte device-info block, streamed out on activation. -// clang-format off -[[gnu::progmem]] constexpr auto info = std::to_array({ - 'T', 'S', 'B', - build_date & 0xFF, build_date >> 8, - 0xF3, // status: native-UART fixed-baud lineage - avr::hw::db.signature[0], avr::hw::db.signature[1], avr::hw::db.signature[2], - page / 2, // page size in words - (app_end / 2) & 0xFF, (app_end / 2) >> 8, - eeprom_end & 0xFF, eeprom_end >> 8, - 0xAA, 0xAA, -}); -// clang-format on +[[gnu::progmem]] constexpr auto info = info_block(date_stamp(26, 7, 20), app_end); register std::uint16_t g_addr asm("r28"); register std::uint8_t g_cnt asm("r16"); register std::uint8_t g_window asm("r7"); register std::uint8_t g_receiving asm("r6"); -const std::uint8_t *flash_ptr(std::uint16_t addr) -{ - return reinterpret_cast(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 @@ -288,23 +232,23 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def // same reason. 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 - - // including silence - hands over. - g_window = avr::flash_load(flash_ptr(app_end + 2)); - for (std::uint8_t k = 3; k; --k) { + // Activation: activation_knocks '@', 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 + timeout_at)); + for (std::uint8_t k = activation_knocks; k; --k) { if (rx() != knock) { appjump(); } } g_window = comm_window; - // Password gate (config page from app_end+3, 0xff-terminated; a blank + // Password gate (config page from password_at, 0xff-terminated; a blank // page is no password). A wrong byte blanks the comparison and drains the // line forever, so a wrong password can never fall through; a 0 requests // emergency erase behind two confirms. On pass the info block goes out; // the emergency path skips it and drops into the command loop. - g_addr = app_end + 3; + g_addr = app_end + password_at; std::uint8_t mask = 0xff; for (;;) { std::uint8_t expected = avr::flash_load(flash_ptr(g_addr)) & mask; @@ -340,7 +284,7 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def tx(confirm); // Mainloop ready g_addr = 0; switch (rx()) { - case 'f': // read application flash, one page per host '!' + case command::read_flash: // one page per host '!' for (;;) { if (rx() != confirm) { break; @@ -352,13 +296,13 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def } } break; - case 'F': // erase the application, then take pages behind '?' - erase_application(); // leaves g_addr = 0, the write start + case command::write_flash: // erase the application, then take pages behind '?' + erase_application(); // leaves g_addr = 0, the write start while (rcnf() == confirm) { store_flash(); } break; - case 'e': // read EEPROM, one page per host '!', until the host stops + case command::read_eeprom: // one page per host '!', until the host stops for (;;) { if (rx() != confirm) { break; @@ -369,7 +313,7 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def } while (--g_cnt); } break; - case 'E': // take EEPROM pages behind '?' + case command::write_eeprom: // pages behind '?' while (rcnf() == confirm) { g_cnt = page; do { @@ -377,13 +321,13 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def } while (--g_cnt); } break; - case 'c': // read the config page + case command::read_config: read_config: g_addr = app_end; g_cnt = page; sendf(); break; - case 'C': // replace the config page, then echo it back to verify + case command::write_config: // then echo it back to verify if (rcnf() != confirm) { break; } diff --git a/tsb/tsb_policy.cpp b/tsb/tsb_policy.cpp index 25913bb..0e531ca 100644 --- a/tsb/tsb_policy.cpp +++ b/tsb/tsb_policy.cpp @@ -19,19 +19,15 @@ // its reply, and releases it (the library's half-duplex choreography) // whenever it waits. -#include +#include "tsb.hpp" -using namespace avr::literals; -namespace spm = avr::spm; -namespace ee = avr::eeprom; - -using dev = avr::device<{.clock = 16_MHz}>; +using dev = avr::device<{.clock = tsb::clock}>; // One-wire: RX and TX share the line, exactly as the native-UART TSB expects. // 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. constexpr avr::uart::config link{ - .baud = 115200_Bd, + .baud = tsb::baud_rate, .allow_baud_error = true, .half_duplex = true, }; @@ -41,61 +37,16 @@ inline constexpr serial_t serial{}; namespace tsb { namespace { -// 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 = '?'; -constexpr std::uint8_t knock = '@'; - -// 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. -constexpr std::uint16_t page = spm::page_bytes; +// The boot section: 1 KB, BOOTSZ=10. constexpr std::uint16_t boot_bytes = 1024; -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; +constexpr std::uint16_t app_end = config_page(boot_bytes); -// Lockout-proof floor for the activation window: the oracle's F_CPU/1MHz, so -// 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; - -// Firmware version stamp: YY*512 + MM*32 + DD, the encoding the host decodes. -constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 27; - -// The 16-byte device-info block, streamed out on activation. -// clang-format off -[[gnu::progmem]] constexpr auto info = std::to_array({ - 'T', 'S', 'B', - build_date & 0xFF, build_date >> 8, - 0xF3, // status: native-UART fixed-baud lineage - avr::hw::db.signature[0], avr::hw::db.signature[1], avr::hw::db.signature[2], - page / 2, // page size in words - (app_end / 2) & 0xFF, (app_end / 2) >> 8, // app-flash boundary, words - eeprom_end & 0xFF, eeprom_end >> 8, - 0xAA, 0xAA, // ATmega processor-type marker (bytes 14 == 15) -}); -// clang-format on +[[gnu::progmem]] constexpr auto info = info_block(date_stamp(26, 7, 27), app_end); // The receive window, pre-floored where it is set. In .noinit: there is no // crt to clear a .bss image, and run() stores it before the first receive. [[gnu::section(".noinit")]] std::uint8_t window; -const std::uint8_t *flash_ptr(std::uint16_t addr) -{ - return reinterpret_cast(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 @@ -219,23 +170,23 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def 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 - - // including silence - hands over. - window = avr::flash_load(flash_ptr(app_end + 2)) | act_min; - for (std::uint8_t k = 3; k; --k) { + // Activation: activation_knocks '@', 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 + timeout_at)) | act_min; + for (std::uint8_t k = activation_knocks; k; --k) { if (rx() != knock) { appjump(); } } window = comm_window; - // Password gate (config page from app_end+3, 0xff-terminated; a blank + // Password gate (config page from password_at, 0xff-terminated; a blank // page is no password). A wrong byte blanks the comparison and drains the // line forever, so a wrong password can never fall through; a 0 requests // emergency erase behind two confirms. On pass the info block goes out; // the emergency path skips it and drops into the command loop. - std::uint16_t at = app_end + 3; + std::uint16_t at = app_end + password_at; std::uint8_t mask = 0xff; for (;;) { const std::uint8_t expected = avr::flash_load(flash_ptr(at)) & mask; @@ -266,9 +217,8 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def for (;;) { tx(confirm); // Mainloop ready - const auto command = rx(); - switch (command) { - case 'f': // read application flash, one page per host '!' + switch (rx()) { + case command::read_flash: // one page per host '!' for (std::uint16_t a = 0; a < app_end; a += page) { if (rx() != confirm) { break; @@ -276,7 +226,7 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def send_block(false, a, page); } break; - case 'e': // read EEPROM, one page per host '!', until the host stops + case command::read_eeprom: // one page per host '!', until the host stops for (std::uint16_t a = 0;; a += page) { if (rx() != confirm) { break; @@ -284,14 +234,14 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def send_block(true, a, page); } break; - case 'F': { // erase the application, then take pages behind '?' + case command::write_flash: { // erase the application, then take pages behind '?' std::uint16_t a = erase_application(); for (; rcnf() == confirm; a += page) { store_flash_page(a); } break; } - case 'E': // take EEPROM pages behind '?', each write host-paced + case command::write_eeprom: // pages behind '?', each write host-paced for (std::uint16_t a = 0; rcnf() == confirm;) { std::uint8_t count = page; do { @@ -300,11 +250,11 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def } while (--count); } break; - case 'c': // read the config page + case command::read_config: read_config: send_block(false, app_end, page); break; - case 'C': // replace the config page, then echo it back to verify + case command::write_config: // then echo it back to verify if (rcnf() != confirm) { break; } diff --git a/tsb/tsb_pure.cpp b/tsb/tsb_pure.cpp index 0ba66a3..26d60ca 100644 --- a/tsb/tsb_pure.cpp +++ b/tsb/tsb_pure.cpp @@ -9,19 +9,15 @@ // 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 "tsb.hpp" -using namespace avr::literals; -namespace spm = avr::spm; -namespace ee = avr::eeprom; - -using dev = avr::device<{.clock = 16_MHz}>; +using dev = avr::device<{.clock = tsb::clock}>; // One-wire: RX and TX share the line, exactly as the native-UART TSB expects. // 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, + .baud = tsb::baud_rate, .allow_baud_error = true, .half_duplex = true, }>; @@ -30,53 +26,15 @@ inline constexpr serial_t serial{}; namespace tsb { namespace { -// 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 = '?'; -constexpr std::uint8_t knock = '@'; - -// 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 (the LASTPAGE holding the app-jump vector, activation timeout and -// password), one page below the boot section. -constexpr std::uint16_t page = spm::page_bytes; +// The boot section: 1 KB, BOOTSZ=10. constexpr std::uint16_t boot_bytes = 1024; -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; +constexpr std::uint16_t app_end = config_page(boot_bytes); -// Firmware version stamp: YY*512 + MM*32 + DD, the encoding the host decodes. -constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 20; - -// The 16-byte device-info block the host reads on activation. A flash_table -// keeps it in progmem with no .data image (there is no crt to copy one). -// clang-format off -inline constexpr auto info_data = std::to_array({ - 'T', 'S', 'B', - build_date & 0xFF, build_date >> 8, - 0xF3, // status byte (native-UART fixed-baud lineage) - avr::hw::db.signature[0], avr::hw::db.signature[1], avr::hw::db.signature[2], - page / 2, // page size in words - (app_end / 2) & 0xFF, (app_end / 2) >> 8, // app-flash boundary, words - eeprom_end & 0xFF, eeprom_end >> 8, - 0xAA, 0xAA, // ATmega processor-type marker (bytes 14 == 15) -}); -// clang-format on +// A flash_table keeps the info block in progmem with no .data image (there is +// no crt to copy one). +inline constexpr auto info_data = info_block(date_stamp(26, 7, 20), app_end); using info = avr::flash_table; -// The lockout-proof floor for the receive window: the oracle's F_CPU/1MHz, so -// it follows the clock rather than restating it. -constexpr auto act_min = static_cast(dev::clock.hz / 1'000'000); - // The receive window, pre-floored where it is set. In .noinit: there is no crt // to clear a .bss image, and run() stores it before the first receive. [[gnu::section(".noinit")]] std::uint8_t window; @@ -107,11 +65,6 @@ void tx(std::uint8_t byte) serial.write(byte); } -const std::uint8_t *flash_ptr(std::uint16_t addr) -{ - return reinterpret_cast(addr); -} - // Stream `count` bytes to the host, from flash (LPM) or from EEPROM. void send_flash(std::uint16_t addr, std::uint8_t count) { @@ -187,8 +140,8 @@ extern "C" [[noreturn]] void tsb_app(); tsb_app(); } -// 'f': stream the application flash back, one page per host '!'. Self-terminates -// at the application boundary; the host normally stops earlier with a non-'!'. +// Stream the application flash back, one page per host '!'. Self-terminates at +// the application boundary; the host normally stops earlier with a non-'!'. void read_flash() { for (std::uint16_t a = 0; a < app_end; a += page) { @@ -199,7 +152,7 @@ void read_flash() } } -// 'e': stream EEPROM back, one page per host '!', until the host stops. +// Stream EEPROM back, one page per host '!', until the host stops. void read_eeprom() { for (std::uint16_t a = 0;; a += page) { @@ -210,8 +163,8 @@ void read_eeprom() } } -// 'F': erase the whole application first, then take pages the host offers -// behind '?'. +// Erase the whole application first, then take pages the host offers behind +// '?'. void write_flash() { erase_application(); @@ -220,7 +173,7 @@ void write_flash() } } -// 'E': take pages the host offers behind '?' into EEPROM. +// Take pages the host offers behind '?' into EEPROM. void write_eeprom() { for (std::uint16_t a = 0; request_confirm(); a += page) { @@ -228,7 +181,7 @@ void write_eeprom() } } -// 'C': replace the config page, then echo it back for the host to verify. +// Replace the config page, then echo it back for the host to verify. void write_config() { if (!request_confirm()) { @@ -253,7 +206,7 @@ void emergency_erase() spm::rww_enable(); } -// The password gate. The config page holds the password at app_end+3, +// The password gate. The config page holds the password at password_at, // terminated by 0xff (a blank page means no password). A byte of 0 requests // emergency erase; a wrong byte hangs the loader, still draining the line, so a // wrong password can never fall through to the erase. @@ -261,7 +214,7 @@ enum class gate : std::uint8_t { pass, emergency }; gate password_gate() { - for (const std::uint8_t *pw = flash_ptr(app_end + 3);; ++pw) { + for (const std::uint8_t *pw = flash_ptr(app_end + password_at);; ++pw) { const auto expected = avr::flash_load(pw); if (expected == 0xff) { return gate::pass; @@ -288,14 +241,14 @@ gate password_gate() avr::init(); - // Activation: the host knocks three '@' inside a window whose length is the + // Activation: the host knocks activation_knocks times inside a window whose length is the // config page's timeout byte, floored so a corrupt page can never lock the // loader out. An idle port times out and boots the application; the same // window then bounds every receive of the session. - window = avr::flash_load(flash_ptr(app_end + 2)) | act_min; + window = avr::flash_load(flash_ptr(app_end + timeout_at)) | act_min; __uint24 idle = static_cast<__uint24>(window) << 16; std::uint8_t knocks = 0; - while (knocks < 3) { + while (knocks < activation_knocks) { if (auto byte = serial.read()) { knocks = *byte == knock ? knocks + 1 : 0; } else if (--idle == 0) { @@ -318,22 +271,22 @@ gate password_gate() for (;;) { tx(confirm); // Mainloop ready switch (rx()) { - case 'f': + case command::read_flash: read_flash(); break; - case 'F': + case command::write_flash: write_flash(); break; - case 'e': + case command::read_eeprom: read_eeprom(); break; - case 'E': + case command::write_eeprom: write_eeprom(); break; - case 'c': + case command::read_config: send_flash(app_end, page); break; - case 'C': + case command::write_config: write_config(); break; default: diff --git a/tsb/tsb_tricks.cpp b/tsb/tsb_tricks.cpp index 6d99815..ecee03f 100644 --- a/tsb/tsb_tricks.cpp +++ b/tsb/tsb_tricks.cpp @@ -29,80 +29,24 @@ // line safe: the device drives it only between a received command and its // reply, and releases it (RXEN0 only) whenever it waits. -#include +#include "tsb.hpp" -using namespace avr::literals; -namespace spm = avr::spm; -namespace ee = avr::eeprom; namespace hw = avr::hw; namespace tsb { namespace { -// 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 = '@'; - -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. -constexpr std::uint16_t page = spm::page_bytes; +// The boot section: 1 KB, BOOTSZ=10. constexpr std::uint16_t boot_bytes = 1024; -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; +constexpr std::uint16_t app_end = config_page(boot_bytes); -// Lockout-proof floor for the activation window: the oracle's F_CPU/1MHz, so -// 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(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(clock.hz / baud.actual); - -// The 16-byte device-info block, streamed out on activation. -// clang-format off -[[gnu::progmem]] constexpr auto info = std::to_array({ - 'T', 'S', 'B', - build_date & 0xFF, build_date >> 8, - 0xF3, // status: native-UART fixed-baud lineage - avr::hw::db.signature[0], avr::hw::db.signature[1], avr::hw::db.signature[2], - page / 2, // page size in words - (app_end / 2) & 0xFF, (app_end / 2) >> 8, - eeprom_end & 0xFF, eeprom_end >> 8, - 0xAA, 0xAA, -}); -// clang-format on +[[gnu::progmem]] constexpr auto info = info_block(date_stamp(26, 7, 20), app_end); register std::uint16_t g_addr asm("r28"); register std::uint8_t g_cnt asm("r16"); register std::uint8_t g_window asm("r7"); register std::uint8_t g_receiving asm("r6"); -const std::uint8_t *flash_ptr(std::uint16_t addr) -{ - return reinterpret_cast(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 @@ -157,7 +101,8 @@ 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 the top of +// this file). [[gnu::noinline, gnu::noclone]] std::uint8_t sflash() { const auto byte = avr::flash_load(flash_ptr(g_addr)); @@ -206,7 +151,7 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def // Step g_addr one page down and erase that page. The decrement lives in here, // before the erase's own use of it, not in the caller's loop where a following -// call would get it deleted (see header). +// call would get it deleted (see the top of this file). [[gnu::noinline, gnu::noclone]] void erase_below() { g_addr -= page; @@ -262,23 +207,23 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def // same reason. 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 - - // including silence - hands over. - g_window = avr::flash_load(flash_ptr(app_end + 2)); - for (std::uint8_t k = 3; k; --k) { + // Activation: activation_knocks '@', 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 + timeout_at)); + for (std::uint8_t k = activation_knocks; k; --k) { if (rx() != knock) { appjump(); } } g_window = comm_window; - // Password gate (config page from app_end+3, 0xff-terminated; a blank + // Password gate (config page from password_at, 0xff-terminated; a blank // page is no password). A wrong byte blanks the comparison and drains the // line forever, so a wrong password can never fall through; a 0 requests // emergency erase behind two confirms. On pass the info block goes out; // the emergency path skips it and drops into the command loop. - g_addr = app_end + 3; + g_addr = app_end + password_at; std::uint8_t mask = 0xff; for (;;) { std::uint8_t expected = avr::flash_load(flash_ptr(g_addr)) & mask; @@ -314,7 +259,7 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def tx(confirm); // Mainloop ready g_addr = 0; switch (rx()) { - case 'f': // read application flash, one page per host '!' + case command::read_flash: // one page per host '!' for (;;) { if (rx() != confirm) { break; @@ -326,13 +271,13 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def } } break; - case 'F': // erase the application, then take pages behind '?' - erase_application(); // leaves g_addr = 0, the write start + case command::write_flash: // erase the application, then take pages behind '?' + erase_application(); // leaves g_addr = 0, the write start while (rcnf() == confirm) { store_flash(); } break; - case 'e': // read EEPROM, one page per host '!', until the host stops + case command::read_eeprom: // one page per host '!', until the host stops for (;;) { if (rx() != confirm) { break; @@ -343,7 +288,7 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def } while (--g_cnt); } break; - case 'E': // take EEPROM pages behind '?' + case command::write_eeprom: // pages behind '?' while (rcnf() == confirm) { g_cnt = page; do { @@ -351,13 +296,13 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def } while (--g_cnt); } break; - case 'c': // read the config page + case command::read_config: read_config: g_addr = app_end; g_cnt = page; sendf(); break; - case 'C': // replace the config page, then echo it back to verify + case command::write_config: // then echo it back to verify if (rcnf() != confirm) { break; }