diff --git a/CMakeLists.txt b/CMakeLists.txt index e93db8d..0835b9e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -83,7 +83,7 @@ endfunction() # All four implement the full oracle feature set (see oracle/README.md): # watchdog bail, one-wire half-duplex, config-page activation timeout, password # gate, emergency erase, config/flash/EEPROM read-write. They differ only in how, -# and the size gradient is the cost of that "how" - see dev/lessons.md. +# and the size gradient is the cost of that "how". # tsb_asm - the tricks tier's C++ with exactly two routines in asm (the # bounded rx and the page-store loop - the two whose remaining # cost is the C ABI itself): 510 B in the 512 B section the diff --git a/libavr b/libavr index 07a0c40..4c7d4d6 160000 --- a/libavr +++ b/libavr @@ -1 +1 @@ -Subproject commit 07a0c4023564eb4bd72e33ff84399105390716e9 +Subproject commit 4c7d4d6ff3af35dc1593a7311a91a1410578e978 diff --git a/pureboot/pureboot.cpp b/pureboot/pureboot.cpp index 8b09092..d7f27dd 100644 --- a/pureboot/pureboot.cpp +++ b/pureboot/pureboot.cpp @@ -127,8 +127,13 @@ constexpr std::uint8_t version = 9; // // Never read from flash by the loader - 'b' answers out of this array, but at // constant indices, so those fold to immediates and no runtime address of it -// is ever formed. `used` keeps the compiler from dropping the copy the host -// needs and `retain` keeps --gc-sections from collecting it. +// is ever formed. That folding is a correctness property, not a size one: the +// bytes live in program memory and a formed address would be dereferenced as +// *data* space, which is why this is the raw array rule 36 otherwise bans - a +// `std::array` here stops the read loop unrolling and emits exactly that +// `ld` (measured: +8 B and a wrong answer on the wire). `used` keeps the +// compiler from dropping the copy the host needs and `retain` keeps +// --gc-sections from collecting it. // clang-format off [[gnu::used, gnu::retain, gnu::section(".text.stamp")]] inline constexpr std::uint8_t identity_stamp[]{ @@ -220,7 +225,12 @@ struct hardware_link { // the fleet, and the datasheet's stricter per-frame tolerance table would // refuse the stock 115200 at 16 MHz (+2.1 %) that every deployed board // runs. .allow_baud_error states that this is meant. - using uart = avr::uart::usart; + using uart = avr::uart::usart; // The compiled idle poll around the window's narrow (uint24_t) countdown: // the RXC test, then sbiw + sbci + brne (5). The test's cost follows the diff --git a/test/check_unit.cmake b/test/check_unit.cmake index f903794..25e01ab 100644 --- a/test/check_unit.cmake +++ b/test/check_unit.cmake @@ -11,26 +11,26 @@ if(NOT _res EQUAL 0) message(FATAL_ERROR "${OBJDUMP} -t ${ELF} failed") endif() -# The symbol line: "00800100 l O .noinit 00000002 unit_E". -string(REGEX MATCH "\n0*([0-9a-f]+)[^\n]+[ \t][^ \t\n]*unit_E\n" _line "${_syms}") +# The symbol line: "00800100 l O .noinit 00000002 6m_unitE". +string(REGEX MATCH "\n0*([0-9a-f]+)[^\n]+[ \t][^ \t\n]*6m_unitE\n" _line "${_syms}") if(GPIOR) if(_line) - message(FATAL_ERROR "unit_ RAM symbol present although the unit's home is GPIOR ${GPIOR} - " + message(FATAL_ERROR "m_unit RAM symbol present although the unit's home is GPIOR ${GPIOR} - " "the host peeks the pair, and a RAM copy would be dead weight") endif() - message(STATUS "no unit_ RAM object - the unit lives in the GPIOR pair at ${GPIOR}") + message(STATUS "no m_unit RAM object - the unit lives in the GPIOR pair at ${GPIOR}") return() endif() if(NOT _line) - message(FATAL_ERROR "no unit_ symbol in ${ELF} - is this the autobaud loader?") + message(FATAL_ERROR "no m_unit symbol in ${ELF} - is this the autobaud loader?") endif() # AVR data-space symbols carry the 0x800000 VMA offset. math(EXPR _want "0x800000 + ${RAM_START}" OUTPUT_FORMAT HEXADECIMAL) math(EXPR _have "0x${CMAKE_MATCH_1}" OUTPUT_FORMAT HEXADECIMAL) if(NOT _have STREQUAL _want) - message(FATAL_ERROR "unit_ sits at ${_have}, ram_start is ${_want} - the host peeks ram_start") + message(FATAL_ERROR "m_unit sits at ${_have}, ram_start is ${_want} - the host peeks ram_start") endif() -message(STATUS "unit_ at ${_have} == ram_start") +message(STATUS "m_unit at ${_have} == ram_start") diff --git a/test/device.cpp b/test/device.cpp index 990dd6b..91568fd 100644 --- a/test/device.cpp +++ b/test/device.cpp @@ -95,7 +95,7 @@ int main(int argc, char *argv[]) if (cfg) { std::uint32_t app_end = boot_base - 128; // config page sits directly below the boot code for (int i = 0; cfg[i] && cfg[i + 1]; i += 2) { - const std::array pair = {cfg[i], cfg[i + 1], 0}; + const std::array pair{cfg[i], cfg[i + 1], '\0'}; avr->flash[app_end + i / 2] = static_cast(std::strtoul(pair.data(), nullptr, 16)); } } diff --git a/test/pbautobaud.py b/test/pbautobaud.py index 882035d..413101e 100644 --- a/test/pbautobaud.py +++ b/test/pbautobaud.py @@ -94,12 +94,11 @@ def main(): fail(f"{label}: EEPROM read-back mismatch") if hand_over: - # Regression: a calibration pulse with no knock behind it must - # not wedge the loader. The knock's edge wait used to be - # unbudgeted, so one stray low pulse - EMI, or a host that opens - # the port and never knocks - held the loader forever and the - # application never ran. The whole activation is bounded now, so - # the window closes and the app boots; the banner is the proof. + # A calibration pulse with no knock behind it must not wedge + # the loader: the whole activation is bounded, including the + # knock's edge wait, so one stray low pulse - EMI, or a host + # that opens the port and never knocks - closes the window and + # boots the application. The banner is the proof. # (The pause lets the loader reach its measurement loop, so the # pulse is genuinely seen and the test cannot pass vacuously.) device.reset() diff --git a/test/pbreloc.py b/test/pbreloc.py index ab14909..e406154 100644 --- a/test/pbreloc.py +++ b/test/pbreloc.py @@ -64,11 +64,9 @@ def main(): fail("EEPROM round-trip through the staged copy") # The resident slot, written from the copy standing beside it - the - # whole point of relocating. pureboot 9 dropped the running-slot guard - # that used to sit behind this, so the probe that used to accompany it - # (aim a write at the copy's *own* slot and watch it be refused) is - # gone with it: there is nothing to refuse now, and a copy that erases - # the page it is executing from does not come back to report it. + # whole point of relocating. There is no running-slot guard to probe + # against: nothing here refuses an address, and a copy that erases the + # page it is executing from does not come back to report it. # pbselfwrite.py gates that direction on a device it is allowed to # destroy. marker = bytes((i * 3) & 0xFF for i in range(page)) diff --git a/tools/check.sh b/tools/check.sh index a0370b1..3ea86aa 100755 --- a/tools/check.sh +++ b/tools/check.sh @@ -24,18 +24,32 @@ CHIPS=(attiny13 attiny13a attiny25 attiny45 attiny85 REFLECT_SPOT=(attiny13a attiny85 atmega8 atmega16a atmega32a atmega48pa atmega88 atmega168pa atmega328p atmega164a atmega644p atmega1284) +# Every preset runs even after one goes red, and the gate fails at the end +# naming all of them: stopping at the first failure turns a red - a stale size +# canary above all - into an alibi for every chip behind it, and a loader can +# ship on a chip this gate has not compiled since. +red=() +run_preset() { + echo "==== $1 ====" + cmake --workflow --preset "$1" "${@:2}" || red+=("$1") +} + for chip in "${CHIPS[@]}"; do - echo "==== $chip ====" - cmake --workflow --preset "$chip-generated" "$@" + run_preset "$chip-generated" "$@" done if ((full)); then for chip in "${REFLECT_SPOT[@]}"; do - echo "==== $chip reflect ====" - cmake --workflow --preset "$chip-reflect" "$@" + run_preset "$chip-reflect" "$@" done fi +if ((${#red[@]})); then + printf '==== red presets ====\n' >&2 + printf ' %s\n' "${red[@]}" >&2 + exit 1 +fi + # Every tree is freshly built now - the one moment the README's size table # can be held to what the images measure (a per-preset ctest sees only its # own chip; the table needs all of them, and ungated it drifts: a diff --git a/tsb/tsb_asm.cpp b/tsb/tsb_asm.cpp index 8435e37..05ea81a 100644 --- a/tsb/tsb_asm.cpp +++ b/tsb/tsb_asm.cpp @@ -4,11 +4,11 @@ // 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 -// 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 -// global-register miscompile rules) with exactly two routines kept in -// assembly, the two whose remaining cost *is* the calling convention: +// 512-byte BOOTSZ=11 section the hand-written oracle occupies (500 B). The +// body is the tricks tier's C++ (same 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: // // rx the bounded receive: C++ must re-floor the timeout window on every // call (the global-register-store miscompile) and split it across @@ -75,7 +75,7 @@ constexpr auto baud = avr::uart::solve_baud(16_MHz, 115200_Bd, 8, avr::uart::par // The 16-byte device-info block, streamed out on activation. // clang-format off -[[gnu::progmem]] constexpr std::uint8_t info[16] = { +[[gnu::progmem]] constexpr auto info = std::to_array({ 'T', 'S', 'B', build_date & 0xFF, build_date >> 8, 0xF3, // status: native-UART fixed-baud lineage @@ -84,7 +84,7 @@ constexpr auto baud = avr::uart::solve_baud(16_MHz, 115200_Bd, 8, avr::uart::par (app_end / 2) & 0xFF, (app_end / 2) >> 8, eeprom_end & 0xFF, eeprom_end >> 8, 0xAA, 0xAA, -}; +}); // clang-format on register std::uint16_t g_addr asm("r28"); @@ -141,8 +141,8 @@ const std::uint8_t *flash_ptr(std::uint16_t addr) if (g_receiving) { g_receiving = 0; hw::ucsr0b::write(hw::ucsr0b::txen0(1)); - for (std::uint8_t guard = 46; guard; --guard) - ; + for (std::uint8_t guard = 46; guard; --guard) { + } } hw::udr0::write(byte); std::uint8_t status; @@ -300,7 +300,7 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def std::uint8_t expected = avr::flash_load(flash_ptr(g_addr)) & mask; ++g_addr; if (expected == 0xff) { - g_addr = reinterpret_cast(&info[0]); + g_addr = reinterpret_cast(info.data()); g_cnt = sizeof(info); sendf(); break; diff --git a/tsb/tsb_policy.cpp b/tsb/tsb_policy.cpp index 12a1a20..64d8d67 100644 --- a/tsb/tsb_policy.cpp +++ b/tsb/tsb_policy.cpp @@ -35,7 +35,11 @@ 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<{.baud = 115200_Bd, .allow_baud_error = true, .half_duplex = true}>; +using serial_t = dev::uart0<{ + .baud = 115200_Bd, + .allow_baud_error = true, + .half_duplex = true, +}>; inline constexpr serial_t serial{}; namespace tsb { @@ -74,7 +78,7 @@ 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 std::uint8_t info[16] = { +[[gnu::progmem]] constexpr auto info = std::to_array({ 'T', 'S', 'B', build_date & 0xFF, build_date >> 8, 0xF3, // status: native-UART fixed-baud lineage @@ -83,7 +87,7 @@ constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 27; (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 // The receive window, pre-floored where it is set. In .noinit: there is no @@ -240,7 +244,7 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def std::uint8_t expected = avr::flash_load(flash_ptr(at)) & mask; ++at; if (expected == 0xff) { - send_block(false, reinterpret_cast(&info[0]), sizeof info); + send_block(false, reinterpret_cast(info.data()), info.size()); break; } std::uint8_t got = rx(); diff --git a/tsb/tsb_pure.cpp b/tsb/tsb_pure.cpp index ea88252..6245488 100644 --- a/tsb/tsb_pure.cpp +++ b/tsb/tsb_pure.cpp @@ -23,7 +23,11 @@ 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<{.baud = 115200_Bd, .allow_baud_error = true, .half_duplex = true}>; +using serial_t = dev::uart0<{ + .baud = 115200_Bd, + .allow_baud_error = true, + .half_duplex = true, +}>; inline constexpr serial_t serial{}; namespace tsb { diff --git a/tsb/tsb_tricks.cpp b/tsb/tsb_tricks.cpp index c1144ba..bd90d7c 100644 --- a/tsb/tsb_tricks.cpp +++ b/tsb/tsb_tricks.cpp @@ -19,7 +19,7 @@ // 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 -// local/scratch/probes/gcc-avr-globalreg-repro.cpp, lessons.md entry). Every +// test/upstream/gcc-avr-globalreg-repro.cpp). 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() // re-floors the window on every call instead of storing the floored value @@ -75,7 +75,7 @@ constexpr auto baud = avr::uart::solve_baud(16_MHz, 115200_Bd, 8, avr::uart::par // The 16-byte device-info block, streamed out on activation. // clang-format off -[[gnu::progmem]] constexpr std::uint8_t info[16] = { +[[gnu::progmem]] constexpr auto info = std::to_array({ 'T', 'S', 'B', build_date & 0xFF, build_date >> 8, 0xF3, // status: native-UART fixed-baud lineage @@ -84,7 +84,7 @@ constexpr auto baud = avr::uart::solve_baud(16_MHz, 115200_Bd, 8, avr::uart::par (app_end / 2) & 0xFF, (app_end / 2) >> 8, eeprom_end & 0xFF, eeprom_end >> 8, 0xAA, 0xAA, -}; +}); // clang-format on register std::uint16_t g_addr asm("r28"); @@ -134,8 +134,8 @@ const std::uint8_t *flash_ptr(std::uint16_t addr) if (g_receiving) { g_receiving = 0; hw::ucsr0b::write(hw::ucsr0b::txen0(1)); - for (std::uint8_t guard = 46; guard; --guard) - ; + for (std::uint8_t guard = 46; guard; --guard) { + } } hw::udr0::write(byte); std::uint8_t status; @@ -279,7 +279,7 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def std::uint8_t expected = avr::flash_load(flash_ptr(g_addr)) & mask; ++g_addr; if (expected == 0xff) { - g_addr = reinterpret_cast(&info[0]); + g_addr = reinterpret_cast(info.data()); g_cnt = sizeof(info); sendf(); break;