6 Commits
Author SHA1 Message Date
BlackMarkandClaude Opus 5.5 68f0089e8c deps: the pin advances to the reviewed libavr
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 19:38:59 +02:00
BlackMarkandClaude Opus 5.5 e395c0344f tsb: the protocol the tiers share is written once, and its bytes by name
tsb/tsb.hpp holds the link, the handshake and command bytes, the guard
policy and SPM interlock, the boot geometry, act_min/comm_window and a
consteval builder for the 16-byte info block, whose BUILDSTATE and ATmega
marker bytes are now named rather than commented. Every tier's image is
byte-identical to before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 12:24:12 +02:00
BlackMarkandClaude Opus 5.5 44db419c4b chore: the editor config describes this repository, in ASCII
.clangd said the repository rides as a submodule in its consumers, which none
does, and removed three codegen flags only pureboot's loaders carry; its em
dashes are ASCII now. The settings.json comment built on the same premise goes,
and the oracle README states the variant rather than who chose it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 21:41:55 +02:00
BlackMarkandClaude Opus 5.5 f50032ee46 build: presets.generated runs, and the tier list lives in the sources
make_presets.py documented a presets.generated test that was never registered,
so a hand edit to CMakePresets.json could drift unseen; it is registered now,
and the generator names its encoding and line ending so a regeneration on
Windows writes the same file. The CMake comment's per-tier essay repeated each
source's own header - and claimed naked stubs where all four use
startup::entry - so it points there instead, and the flash size the boot base
and the wrap-around both follow from is named once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 21:41:55 +02:00
BlackMarkandClaude Opus 5.5 bc671618a7 test: the simavr device stops dumping a flash image nobody reads
Every protocol run wrote the simulated flash to /tmp/tsb_dump.bin on exit -
outside the repository, from four tests that run in parallel into one file - for
a cross-check no test made. The dump goes, and the device's clock and page size
are named.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 21:41:55 +02:00
BlackMarkandClaude Opus 5.5 aafaf25a47 tsb: one spelling of the clock and the baud, and the asm's bits by name
The tricks and asm tiers spelled 16_MHz three times each and now name it once;
the policy tier's lean bring-up solves the baud from the link config the
serial unit is declared with instead of restating 115200 8N1. The asm tier's
immediates - RXEN0, RXC0's bit, SPMEN, PGWRT|SPMEN and the three UART
addresses - are operands read from the chip database rather than numbers with
comments beside them, so avr/io.h is left for SPMCSR's I/O address alone; pure
and tricks drop an include nothing used. Headers that described the code as it
was are corrected (pure's "only attribute", the policy tier's history), the
rule references leave the comments, and locals are const auto.

All four images are byte-identical to before, in both modes for tsb_asm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 21:41:55 +02:00
13 changed files with 290 additions and 432 deletions

No files matched your search

+5 -11
View File
@@ -1,13 +1,10 @@
# Editor accommodations for the second frontend. No compilation database is # Editor accommodations for the second frontend: only what is true of the
# named here: this repo rides as a submodule in its consumers, and this file # code wherever it is built. The build tree is named in .vscode/settings.json.
# travels with it — a consumer's own database then covers these sources, with
# that project's loader flags. The checkout that is opened as a folder names
# its build tree in .vscode/settings.json instead.
CompileFlags: CompileFlags:
Add: Add:
# clang has no 24-bit integer and GCC's are keywords, not macros, so the # clang has no 24-bit integer and GCC's are keywords, not macros, so the
# editor needs a stand-in for avr::uint24_t. The next width up is the only # editor needs a stand-in for avr::uint24_t. The next width up is the only
# one available — clang rejects _BitInt(24) on this target. # one available - clang rejects _BitInt(24) on this target.
- -D__uint24=unsigned long - -D__uint24=unsigned long
- -D__int24=long - -D__int24=long
# clangd forwards the driver's system includes but not its own header # clangd forwards the driver's system includes but not its own header
@@ -16,15 +13,12 @@ CompileFlags:
- -D__STDC_LIMIT_MACROS - -D__STDC_LIMIT_MACROS
- -D__STDC_CONSTANT_MACROS - -D__STDC_CONSTANT_MACROS
# isr::emit spells a vector number into [[gnu::signal(N)]], which clang # isr::emit spells a vector number into [[gnu::signal(N)]], which clang
# rejects rather than ignores — enough of them in one TU to reach the # rejects rather than ignores - enough of them in one TU to reach the
# default limit of 19 inside the headers and truncate the parse. # default limit of 19 inside the headers and truncate the parse.
- -ferror-limit=0 - -ferror-limit=0
Remove: Remove:
# Codegen shaping the loader TUs carry and clang has no spelling for. # Codegen shaping the policy tier carries and clang has no spelling for.
- -fira-algorithm=*
- -fno-split-wide-types
- -fno-tree-ter - -fno-tree-ter
- -fno-ivopts
- -fno-move-loop-invariants - -fno-move-loop-invariants
# The build promotes warnings for the compiler that has to be right about # The build promotes warnings for the compiler that has to be right about
# them; in the editor the flag paints a second frontend's opinions in the # them; in the editor the flag paints a second frontend's opinions in the
+1 -3
View File
@@ -5,9 +5,7 @@
"C_Cpp.intelliSenseEngine": "disabled", "C_Cpp.intelliSenseEngine": "disabled",
// --query-driver lets clangd ask the cross compiler for its own system // --query-driver lets clangd ask the cross compiler for its own system
// includes and target. The database is named here rather than in .clangd // includes and target.
// because that file travels with the driver into a consumer's submodule,
// where a build tree of this repo's own need not exist.
"clangd.arguments": [ "clangd.arguments": [
"--compile-commands-dir=${workspaceFolder}/build/atmega328p-generated", "--compile-commands-dir=${workspaceFolder}/build/atmega328p-generated",
"--query-driver=**avr-g++*", "--query-driver=**avr-g++*",
+22 -39
View File
@@ -34,9 +34,8 @@ if(PROJECT_IS_TOP_LEVEL)
# distribution's compiler speaks in full). # distribution's compiler speaks in full).
# #
# **A host that cannot build it registers those tests anyway and skips # **A host that cannot build it registers those tests anyway and skips
# them.** They used to be left out, which makes the suite a different size # them**: leaving them out would make the suite's size a property of the
# on a different machine - and a suite whose size is a property of the # machine, and such a suite is one nothing can be compared against.
# machine is one nothing can be compared against.
set(TSB_DEVICE ${CMAKE_BINARY_DIR}/tsb_device) set(TSB_DEVICE ${CMAKE_BINARY_DIR}/tsb_device)
find_program(_host_cxx NAMES c++ g++) find_program(_host_cxx NAMES c++ g++)
set(_tsb_absent "${LIBAVR_NO_PYTHON}") set(_tsb_absent "${LIBAVR_NO_PYTHON}")
@@ -66,6 +65,11 @@ if(PROJECT_IS_TOP_LEVEL)
if(_tsb_absent) if(_tsb_absent)
message(STATUS "the protocol tests skip here - ${_tsb_absent}") message(STATUS "the protocol tests skip here - ${_tsb_absent}")
endif() endif()
# CMakePresets.json is generated; the gate holds it to its generator.
libavr_launcher(_presets_python "${LIBAVR_NO_PYTHON}" ${Python3_EXECUTABLE})
add_test(NAME presets.generated
COMMAND ${_presets_python} ${CMAKE_CURRENT_SOURCE_DIR}/tools/make_presets.py --check)
endif() endif()
# The ELF is only a container (symbols, section headers) and is never flashed - # The ELF is only a container (symbols, section headers) and is never flashed -
@@ -82,48 +86,27 @@ function(add_image_outputs name)
$<TARGET_FILE:${name}> $<TARGET_FILE:${name}>.bin) $<TARGET_FILE:${name}> $<TARGET_FILE:${name}>.bin)
endfunction() endfunction()
# The TinySafeBoot protocol reimplemented on libavr in variants that trade # The TinySafeBoot protocol reimplemented on libavr in four tiers that trade
# clarity for size. Each links into the ATmega328P boot section (BOOTSZ selects # clarity for size, each with the full oracle feature set; each source's header
# its size; BOOTRST vectors a reset to its base) with -nostartfiles - a polled # says what its tier is allowed, and oracle/README.md compares what they
# loader has no use for the crt or the vector table. The entry sits in # measure. Each links into the ATmega328P boot section (BOOTSZ selects its
# .vectors, laid first, and runs - avr::startup::entry on the policy tier, # size; BOOTRST vectors a reset to its base) with -nostartfiles - a polled
# the experiment tiers' own naked stubs elsewhere, each documented in its # loader has no use for the crt or the vector table - and its entry sits in
# source. The boot base is FLASHEND+1 minus the section size; the linker # .vectors, laid first. The boot base is the flash size minus the section size;
# section-start and the source's boot_bytes agree. tsb_app is # the linker section-start and the source's boot_bytes agree. tsb_app is the
# the application's reset vector, pinned to 0 here so the loaders jump to a # application's reset vector, pinned to 0 here so the loaders jump to a named
# named function; --pmem-wrap-around lets relaxation turn that absolute jump # function; --pmem-wrap-around lets relaxation turn that absolute jump into the
# into the wrapped rjmp AVR's modulo-flash PC actually executes. # wrapped rjmp AVR's modulo-flash PC actually executes.
# All four implement the full oracle feature set (see oracle/README.md): set(TSB_FLASH_KIB 32)
# 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".
# 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. Everything else, bring-up to
# dispatch, is C++ on libavr.
# tsb_tricks - no asm at all: the whole-loader register allocation lives in
# global register variables (Y walks the page pointer), every
# helper is a tiny noinline primitive placed by the
# global-register store rules, pages stream straight to
# SPM/EEPROM.
# tsb_pure - pure idiomatic libavr, one function per command, TU-local
# (internal linkage), streaming (no SRAM page buffer).
# tsb_policy - the policy floor: no inline assembly and no global register
# variables, which is philosophy #5's own bound, and the
# measured evidence that the 512 B fit is a property of the
# mechanisms it bans.
#
# What each measures is oracle/README.md's table, which is the one place the
# four numbers and the hand-written loader's own are compared.
#
# add_tsb_variant(<name> <boot-section-bytes>) # add_tsb_variant(<name> <boot-section-bytes>)
function(add_tsb_variant name bytes) function(add_tsb_variant name bytes)
math(EXPR base_dec "32768 - ${bytes}") math(EXPR base_dec "${TSB_FLASH_KIB} * 1024 - ${bytes}")
math(EXPR base_hex "${base_dec}" OUTPUT_FORMAT HEXADECIMAL) math(EXPR base_hex "${base_dec}" OUTPUT_FORMAT HEXADECIMAL)
add_executable(${name} tsb/${name}.cpp) add_executable(${name} tsb/${name}.cpp)
target_link_libraries(${name} PRIVATE libavr) target_link_libraries(${name} PRIVATE libavr)
target_link_options(${name} PRIVATE -nostartfiles -Wl,--section-start=.text=${base_hex} target_link_options(${name} PRIVATE -nostartfiles -Wl,--section-start=.text=${base_hex}
-Wl,--defsym=tsb_app=0 -Wl,--pmem-wrap-around=32k) -Wl,--defsym=tsb_app=0 -Wl,--pmem-wrap-around=${TSB_FLASH_KIB}k)
add_custom_command(TARGET ${name} POST_BUILD COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${name}>) add_custom_command(TARGET ${name} POST_BUILD COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${name}>)
add_image_outputs(${name}) add_image_outputs(${name})
if(PROJECT_IS_TOP_LEVEL) if(PROJECT_IS_TOP_LEVEL)
+1 -1
Submodule libavr updated: 93d8b0e491...73fde9c243.
+3 -3
View File
@@ -13,9 +13,9 @@ in AVR assembly. It is the size-and-feature bar for the port's `tsb_asm` tier.
## Why this variant ## Why this variant
The user chose the fixed-baud, hardware-UART variant deliberately: it is the one The fixed-baud, hardware-UART variant is the one whose feature set the port must
whose feature set the port must match. It fits the **complete** TSB feature set match. It fits the **complete** TSB feature set into the 512-byte ATmega boot
into the 512-byte ATmega boot section: section:
| Feature | Oracle routine | | Feature | Oracle routine |
|---|---| |---|---|
+17 -26
View File
@@ -1,12 +1,8 @@
// simavr "device" for the TSB bootloader: load the boot-linked ELF into the // simavr "device" for the TSB bootloader: load the boot-linked ELF into the
// ATmega328P boot section, enter it (BOOTRST is not modelled, so we set PC to // ATmega328P boot section, enter it (BOOTRST is not modelled, so PC is set to
// the boot base, exactly as simavr's own board_simduino does), and expose // the boot base, exactly as simavr's own board_simduino does), and expose
// UART0 as a pty. A host client (Python pyserial, or the real tsbloader) then // UART0 as a pty. A host client (Python pyserial, or the real tsbloader) then
// speaks the TSB protocol over that pty and actually flashes the device. // speaks the TSB protocol over that pty and actually flashes the device.
//
// SPM genuinely writes avr->flash on the mega cores, so on exit (or SIGTERM)
// we dump the flash image to a file for a ground-truth cross-check against
// what the client read back through the bootloader.
#include <array> #include <array>
#include <csignal> #include <csignal>
#include <cstdint> #include <cstdint>
@@ -28,19 +24,16 @@ extern "C" {
namespace { namespace {
// What the loaders are built for: tsb/*.cpp's device clock and the chip's SPM
// page, which is also the config page sitting directly below the boot section.
constexpr std::uint32_t clock_hz = 16'000'000;
constexpr std::uint32_t page_bytes = 128;
avr_t *avr; avr_t *avr;
uart_pty_t uart_pty; uart_pty_t uart_pty;
const char *dump_path;
[[noreturn]] void finish(int) [[noreturn]] void finish(int)
{ {
if (dump_path) {
std::FILE *f = std::fopen(dump_path, "wb");
if (f) {
std::fwrite(avr->flash, 1, avr->flashend + 1, f);
std::fclose(f);
}
}
uart_pty_stop(&uart_pty); uart_pty_stop(&uart_pty);
_exit(0); _exit(0);
} }
@@ -49,12 +42,11 @@ const char *dump_path;
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
if (argc < 3) { if (argc != 3) {
std::println(stderr, "usage: {} <tsb.elf> <boot_base_hex> [flash_dump.bin]", argv[0]); std::println(stderr, "usage: {} <tsb.elf> <boot_base_hex>", argv[0]);
return 2; return 2;
} }
auto boot_base = static_cast<std::uint32_t>(std::strtoul(argv[2], nullptr, 0)); const auto boot_base = static_cast<std::uint32_t>(std::strtoul(argv[2], nullptr, 0));
dump_path = argc >= 4 ? argv[3] : nullptr;
avr = avr_make_mcu_by_name("atmega328p"); avr = avr_make_mcu_by_name("atmega328p");
if (!avr) { if (!avr) {
@@ -62,14 +54,14 @@ int main(int argc, char *argv[])
return 1; return 1;
} }
avr_init(avr); avr_init(avr);
avr->frequency = 16000000; avr->frequency = clock_hz;
// Real flash powers up erased (0xff); the app region must look erased // Real flash powers up erased (0xff); the app region must look erased
// before the bootloader programs it. // before the bootloader programs it.
std::memset(avr->flash, 0xff, avr->flashend + 1); std::memset(avr->flash, 0xff, avr->flashend + 1);
// simavr's ELF loader flattens the flash base to 0 (it expects an app at // simavr's ELF loader flattens the flash base to 0 (it expects an app at
// 0x0), but it hands back the boot code in fw.flash; place it at the boot // 0x0), but it hands back the boot code in fw.flash, so it is placed at the
// section base ourselves and enter there (BOOTRST is not modelled). // boot section base by hand.
elf_firmware_t fw{}; elf_firmware_t fw{};
if (elf_read_firmware(argv[1], &fw) != 0) { if (elf_read_firmware(argv[1], &fw) != 0) {
std::println(stderr, "device: cannot read {}", argv[1]); std::println(stderr, "device: cannot read {}", argv[1]);
@@ -88,12 +80,11 @@ int main(int argc, char *argv[])
avr->pc = boot_base; avr->pc = boot_base;
avr->codeend = avr->flashend; avr->codeend = avr->flashend;
// Optional: seed the config page (one page below the boot section) with a // Optional: seed the config page with a hex byte string, so the password
// hex byte string, so the password gate and emergency erase can be tested. // gate and emergency erase can be tested.
// Layout: [appjump lo][appjump hi][timeout][password...][0xff]. // Layout: [appjump lo][appjump hi][timeout][password...][0xff].
const char *cfg = std::getenv("TSB_CONFIG"); if (const char *cfg = std::getenv("TSB_CONFIG")) {
if (cfg) { const auto app_end = boot_base - page_bytes;
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) { 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::uint8_t>(std::strtoul(pair.data(), nullptr, 16)); avr->flash[app_end + i / 2] = static_cast<std::uint8_t>(std::strtoul(pair.data(), nullptr, 16));
@@ -119,7 +110,7 @@ int main(int argc, char *argv[])
std::signal(SIGINT, finish); std::signal(SIGINT, finish);
for (;;) { for (;;) {
int state = avr_run(avr); const auto state = avr_run(avr);
if (state == cpu_Done || state == cpu_Crashed) { if (state == cpu_Done || state == cpu_Crashed) {
break; break;
} }
+3 -5
View File
@@ -23,14 +23,13 @@ class Device:
page (via the device's TSB_CONFIG hook) so the password gate and emergency page (via the device's TSB_CONFIG hook) so the password gate and emergency
erase are exercisable.""" erase are exercisable."""
def __init__(self, binary, elf, boot_base, dump="/tmp/tsb_dump.bin", config=None): def __init__(self, binary, elf, boot_base, config=None):
env = dict(os.environ) env = dict(os.environ)
if config is not None: if config is not None:
env["TSB_CONFIG"] = config env["TSB_CONFIG"] = config
self.proc = subprocess.Popen( self.proc = subprocess.Popen(
[binary, elf, boot_base, dump], [binary, elf, boot_base],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=env) stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=env)
self.dump = dump
self.pty = None self.pty = None
deadline = time.time() + 5 deadline = time.time() + 5
while time.time() < deadline: while time.time() < deadline:
@@ -166,7 +165,6 @@ class Host:
self.s.write(bytes([CONFIRM])) self.s.write(bytes([CONFIRM]))
self._expect(CONFIRM, "emergency mainloop ready") self._expect(CONFIRM, "emergency mainloop ready")
# A wrong password byte hangs the loader, still draining the line. Two # A wrong password byte hangs the loader, still draining the line. Two
# things must not happen: it must not activate, and it must not fall # things must not happen: it must not activate, and it must not fall
# through to the emergency erase - a byte the gate has already refused # through to the emergency erase - a byte the gate has already refused
@@ -236,7 +234,7 @@ def scenario_emergency(host):
def scenario_wrong_password(host): def scenario_wrong_password(host):
"""A wrong password byte neither activates the loader nor opens the """A wrong password byte neither activates the loader nor opens the
emergency erase behind it - the oracle carries a dedicated fix for the emergency erase behind it - the oracle carries a dedicated fix for the
second, and nothing here exercised either half.""" second."""
check(host.refuse_password(PW_BYTES[0] ^ 1) == b"", "a wrong password byte draws no reply") check(host.refuse_password(PW_BYTES[0] ^ 1) == b"", "a wrong password byte draws no reply")
check(host.say(0x00) == b"", "a 0 byte after it does not request the erase") check(host.say(0x00) == b"", "a 0 byte after it does not request the erase")
check(host.say(CONFIRM) == b"", "and neither does a confirm") check(host.say(CONFIRM) == b"", "and neither does a confirm")
+2 -2
View File
@@ -75,12 +75,12 @@ def main():
rendered = json.dumps(presets, indent=1) + "\n" rendered = json.dumps(presets, indent=1) + "\n"
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "CMakePresets.json") path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "CMakePresets.json")
if "--check" in sys.argv[1:]: if "--check" in sys.argv[1:]:
current = open(path).read() if os.path.exists(path) else "" current = open(path, encoding="utf-8").read() if os.path.exists(path) else ""
if current != rendered: if current != rendered:
print("CMakePresets.json does not match its generator - run tools/make_presets.py") print("CMakePresets.json does not match its generator - run tools/make_presets.py")
return 1 return 1
return 0 return 0
with open(path, "w") as f: with open(path, "w", encoding="utf-8", newline="\n") as f:
f.write(rendered) f.write(rendered)
print(f"{len(CHIPS)} chips, {len(REFLECT_SPOT)} reflect: {os.path.normpath(path)}") print(f"{len(CHIPS)} chips, {len(REFLECT_SPOT)} reflect: {os.path.normpath(path)}")
return 0 return 0
+106
View File
@@ -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 <libavr/libavr.hpp>
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<std::uint32_t>(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<std::uint8_t>(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<std::uint8_t>(word); };
const auto high = [](std::uint16_t word) { return static_cast<std::uint8_t>(word >> 8); };
const std::uint16_t app_end_words = app_end / 2;
return std::to_array<std::uint8_t>({
'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<const std::uint8_t *>(addr);
}
} // namespace tsb
+35 -87
View File
@@ -30,80 +30,26 @@
// line safe: the device drives it only between a received command and its // line safe: the device drives it only between a received command and its
// reply, and releases it (RXEN0 only) whenever it waits. // reply, and releases it (RXEN0 only) whenever it waits.
#include <libavr/libavr.hpp> #include "tsb.hpp"
#include <avr/io.h> // SP / RAMEND for the crt-free boot entry, SFR addresses for the asm routines #include <avr/io.h> // 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 hw = avr::hw;
namespace tsb { namespace tsb {
namespace { namespace {
// The loader is purely polled - it never enables interrupts - so every SPM and // The boot section: 512 B, BOOTSZ=11.
// 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 = '@';
// 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;
constexpr std::uint16_t boot_bytes = 512; constexpr std::uint16_t boot_bytes = 512;
constexpr std::uint16_t app_end = spm::flash_bytes - boot_bytes - page; constexpr std::uint16_t app_end = config_page(boot_bytes);
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 [[gnu::progmem]] constexpr auto info = info_block(date_stamp(26, 7, 20), app_end);
// it follows the clock rather than restating it (rule 41).
constexpr auto act_min = static_cast<std::uint8_t>((16_MHz).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);
// 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<std::uint32_t>((16_MHz).hz / baud.actual);
// The 16-byte device-info block, streamed out on activation.
// clang-format off
[[gnu::progmem]] constexpr auto info = std::to_array<std::uint8_t>({
'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
register std::uint16_t g_addr asm("r28"); register std::uint16_t g_addr asm("r28");
register std::uint8_t g_cnt asm("r16"); register std::uint8_t g_cnt asm("r16");
register std::uint8_t g_window asm("r7"); register std::uint8_t g_window asm("r7");
register std::uint8_t g_receiving asm("r6"); register std::uint8_t g_receiving asm("r6");
const std::uint8_t *flash_ptr(std::uint16_t addr)
{
return reinterpret_cast<const std::uint8_t *>(addr);
}
// Bounded byte receive (asm 1 of 2): release the one-wire line on a direction // 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 // 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 // g_window (floored against lockout), byte or 0-on-silence in r24. Z survives
@@ -113,7 +59,7 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
std::uint8_t byte; std::uint8_t byte;
asm volatile(" tst %[dir] \n\t" // already receiving? keep the line released asm volatile(" tst %[dir] \n\t" // already receiving? keep the line released
" brne 1f \n\t" " 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" " sts %[ucsr0b], %[b] \n\t"
" ser %[b] \n\t" " ser %[b] \n\t"
" mov %[dir], %[b] \n\t" " mov %[dir], %[b] \n\t"
@@ -122,7 +68,7 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
" clr r26 \n\t" " clr r26 \n\t"
"2: ser %[b] \n\t" "2: ser %[b] \n\t"
"3: lds %[b], %[ucsr0a] \n\t" "3: lds %[b], %[ucsr0a] \n\t"
" sbrc %[b], 7 \n\t" // RXC0 " sbrc %[b], %[rxc] \n\t"
" rjmp 4f \n\t" " rjmp 4f \n\t"
" dec %[b] \n\t" " dec %[b] \n\t"
" brne 3b \n\t" " brne 3b \n\t"
@@ -133,8 +79,9 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
"4: lds %[b], %[udr0] \n\t" "4: lds %[b], %[udr0] \n\t"
"5: \n\t" "5: \n\t"
: [b] "=&d"(byte), [dir] "+r"(g_receiving) : [b] "=&d"(byte), [dir] "+r"(g_receiving)
: [to] "r"(g_window), [actmin] "M"(act_min), [ucsr0a] "n"(_SFR_MEM_ADDR(UCSR0A)), : [to] "r"(g_window), [actmin] "M"(act_min), [rxen] "M"(hw::ucsr0b::rxen0(1).value),
[ucsr0b] "n"(_SFR_MEM_ADDR(UCSR0B)), [udr0] "n"(_SFR_MEM_ADDR(UDR0)) [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"); : "r26", "r27", "cc");
return byte; return byte;
} }
@@ -169,7 +116,7 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
// global-register rule, see tsb_tricks.cpp). // global-register rule, see tsb_tricks.cpp).
[[gnu::noinline, gnu::noclone]] std::uint8_t sflash() [[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; ++g_addr;
return byte; return byte;
} }
@@ -177,7 +124,7 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
// One EEPROM byte <- [g_addr++]. // One EEPROM byte <- [g_addr++].
[[gnu::noinline, gnu::noclone]] std::uint8_t eerd() [[gnu::noinline, gnu::noclone]] std::uint8_t eerd()
{ {
std::uint8_t byte = ee::read<no_spm>(g_addr); const auto byte = ee::read<no_spm>(g_addr);
++g_addr; ++g_addr;
return byte; return byte;
} }
@@ -213,7 +160,7 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
tsb_app(); 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). // the global-register rule).
[[gnu::noinline, gnu::noclone]] void erase_below() [[gnu::noinline, gnu::noclone]] void erase_below()
{ {
@@ -233,7 +180,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 // 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 // the cross-call liveness C++ cannot express), Z walks the page and PGWRT
// programs it. g_addr is left at the next page base. // programs it. g_addr is left at the next page base.
[[gnu::noinline, gnu::noclone]] void store_flash() [[gnu::noinline, gnu::noclone]] void store_flash()
@@ -243,8 +190,8 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
"1: rcall %x[rx] \n\t" "1: rcall %x[rx] \n\t"
" mov r0, r24 \n\t" // word low byte " mov r0, r24 \n\t" // word low byte
" rcall %x[rx] \n\t" " rcall %x[rx] \n\t"
" mov r1, r24 \n\t" // word high byte " mov r1, r24 \n\t" // word high byte
" ldi r24, 0x01 \n\t" // SPMEN: buffer the word at Z " ldi r24, %[fill] \n\t" // buffer the word at Z
" out %[spmcsr], r24 \n\t" " out %[spmcsr], r24 \n\t"
" spm \n\t" " spm \n\t"
" clr r1 \n\t" " clr r1 \n\t"
@@ -254,12 +201,13 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
" movw %[base], r30 \n\t" // g_addr = the next page base " movw %[base], r30 \n\t" // g_addr = the next page base
" subi r30, %[pagelo] \n\t" // Z back to this page's base " subi r30, %[pagelo] \n\t" // Z back to this page's base
" sbci r31, %[pagehi] \n\t" " 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" " out %[spmcsr], r24 \n\t"
" spm \n\t" " spm \n\t"
: [base] "+r"(g_addr) : [base] "+r"(g_addr)
: [rx] "i"(&rx), [spmcsr] "I"(_SFR_IO_ADDR(SPMCSR)), [words] "M"(page / 2), [pagelo] "M"(page & 0xff), : [rx] "i"(&rx), [spmcsr] "I"(_SFR_IO_ADDR(SPMCSR)), [fill] "M"(hw::spmcsr::spmen(1).value),
[pagehi] "M"(page >> 8) [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"); : "r0", "r1", "r20", "r24", "r26", "r27", "r30", "r31", "cc", "memory");
settle(); settle();
} }
@@ -284,23 +232,23 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
// same reason. // same reason.
g_receiving = 0; g_receiving = 0;
// Activation: 3x'@', each inside the config page's timeout window (rx // Activation: activation_knocks '@', each inside the config page's timeout
// floors it so a corrupt page cannot lock the loader out); anything else - // window (rx floors it so a corrupt page cannot lock the loader out);
// including silence - hands over. // anything else - including silence - hands over.
g_window = avr::flash_load(flash_ptr(app_end + 2)); g_window = avr::flash_load(flash_ptr(app_end + timeout_at));
for (std::uint8_t k = 3; k; --k) { for (std::uint8_t k = activation_knocks; k; --k) {
if (rx() != knock) { if (rx() != knock) {
appjump(); appjump();
} }
} }
g_window = comm_window; 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 // 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 // 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; // emergency erase behind two confirms. On pass the info block goes out;
// the emergency path skips it and drops into the command loop. // 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; std::uint8_t mask = 0xff;
for (;;) { for (;;) {
std::uint8_t expected = avr::flash_load(flash_ptr(g_addr)) & mask; std::uint8_t expected = avr::flash_load(flash_ptr(g_addr)) & mask;
@@ -311,7 +259,7 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
sendf(); sendf();
break; break;
} }
std::uint8_t got = rx(); const auto got = rx();
if (got == 0) { if (got == 0) {
if (mask == 0) { if (mask == 0) {
continue; continue;
@@ -336,7 +284,7 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
tx(confirm); // Mainloop ready tx(confirm); // Mainloop ready
g_addr = 0; g_addr = 0;
switch (rx()) { switch (rx()) {
case 'f': // read application flash, one page per host '!' case command::read_flash: // one page per host '!'
for (;;) { for (;;) {
if (rx() != confirm) { if (rx() != confirm) {
break; break;
@@ -348,13 +296,13 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
} }
} }
break; break;
case 'F': // erase the application, then take pages behind '?' case command::write_flash: // erase the application, then take pages behind '?'
erase_application(); // leaves g_addr = 0, the write start erase_application(); // leaves g_addr = 0, the write start
while (rcnf() == confirm) { while (rcnf() == confirm) {
store_flash(); store_flash();
} }
break; 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 (;;) { for (;;) {
if (rx() != confirm) { if (rx() != confirm) {
break; break;
@@ -365,7 +313,7 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
} while (--g_cnt); } while (--g_cnt);
} }
break; break;
case 'E': // take EEPROM pages behind '?' case command::write_eeprom: // pages behind '?'
while (rcnf() == confirm) { while (rcnf() == confirm) {
g_cnt = page; g_cnt = page;
do { do {
@@ -373,13 +321,13 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
} while (--g_cnt); } while (--g_cnt);
} }
break; break;
case 'c': // read the config page case command::read_config:
read_config: read_config:
g_addr = app_end; g_addr = app_end;
g_cnt = page; g_cnt = page;
sendf(); sendf();
break; 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) { if (rcnf() != confirm) {
break; break;
} }
+38 -93
View File
@@ -2,105 +2,51 @@
// //
// 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-page activation timeout, password gate, emergency erase, and
// config/flash/EEPROM read-write - under philosophy #5 exactly as pureboot // config/flash/EEPROM read-write - with no assembly and no register variables:
// obeys it: no assembly, no register variables; code, attributes, and flags // code, attributes and flags only. It applies pureboot's size techniques - the
// only. Every lesson pureboot's development produced is applied - the
// library's half-duplex serial and startup entry, lean bring-up from reset // 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, // 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
// and the result sits below the idiomatic tier and above the 512 B boot // lands between the idiomatic tier and the 512 B section the tricks/asm tiers
// section the tricks/asm tiers reach with the banned mechanisms // reach with the banned mechanisms (oracle/README.md holds all four). The gap
// (oracle/README.md holds all four). This tier exists to keep that gap an // to 512 is the rent of policy-clean C++: helpers that hold a cursor across
// artifact // rx()/tx() pay push/pop and argument threading where a global-register
// rather than a claim: the gap to 512 is the rent of policy-clean C++ - // protocol pays nothing, and both control-flow merges tried (a parametrized
// helpers that hold a cursor across rx()/tx() pay push/pop and argument // paged session, a merged store loop) measured larger than the split cases.
// 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.
// //
// The wire protocol is strict request/response, which is what makes the // 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 // shared line safe: the device drives it only between a received command and
// its reply, and releases it (the library's half-duplex choreography) // its reply, and releases it (the library's half-duplex choreography)
// whenever it waits. // whenever it waits.
#include <libavr/libavr.hpp> #include "tsb.hpp"
using namespace avr::literals; using dev = avr::device<{.clock = tsb::clock}>;
namespace spm = avr::spm;
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. // 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 // 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 // solver holds rates to - the oracle's own deployment has run there for a
// decade, so the override states that it is meant. // decade, so the override states that it is meant.
using serial_t = dev::uart0<{ constexpr avr::uart::config link{
.baud = 115200_Bd, .baud = tsb::baud_rate,
.allow_baud_error = true, .allow_baud_error = true,
.half_duplex = true, .half_duplex = true,
}>; };
using serial_t = dev::uart0<link>;
inline constexpr serial_t serial{}; inline constexpr serial_t serial{};
namespace tsb { namespace tsb {
namespace { namespace {
// The loader is purely polled - it never enables interrupts - so every SPM and // The boot section: 1 KB, BOOTSZ=10.
// 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;
constexpr std::uint16_t boot_bytes = 1024; constexpr std::uint16_t boot_bytes = 1024;
constexpr std::uint16_t app_end = spm::flash_bytes - boot_bytes - page; constexpr std::uint16_t app_end = config_page(boot_bytes);
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 [[gnu::progmem]] constexpr auto info = info_block(date_stamp(26, 7, 27), app_end);
// it follows the clock rather than restating it (rule 41).
constexpr auto act_min = static_cast<std::uint8_t>(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<std::uint8_t>({
'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
// The receive window, pre-floored where it is set. In .noinit: there is no // 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. // crt to clear a .bss image, and run() stores it before the first receive.
[[gnu::section(".noinit")]] std::uint8_t window; [[gnu::section(".noinit")]] std::uint8_t window;
const std::uint8_t *flash_ptr(std::uint16_t addr)
{
return reinterpret_cast<const std::uint8_t *>(addr);
}
// Bounded byte receive: poll under nested countdowns, 0 on silence. The 0 // 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 // 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 // command - so a silent host unwinds the loader to the application from
@@ -166,8 +112,8 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
{ {
const auto open = spm::page::begin<spm::from::boot_section, off>(at); const auto open = spm::page::begin<spm::from::boot_section, off>(at);
do { do {
std::uint8_t low = rx(); const auto low = rx();
std::uint8_t high = rx(); const auto high = rx();
spm::fill<off>(open, 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; at += 2;
} while (static_cast<std::uint8_t>(at) & (page - 1)); } while (static_cast<std::uint8_t>(at) & (page - 1));
@@ -214,42 +160,42 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
} }
// Lean bring-up from reset state: UCSR0C already reads 8N1, UBRR0H reads // 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 // only the divisor low byte and U2X0 need a store. The solver still does
// the datasheet work; the asserts pin the reset-state assumptions. // 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"); 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::ubrr0::write(static_cast<std::uint8_t>(sol.ubrr));
avr::hw::ucsr0a::write(avr::hw::ucsr0a::u2x0(1)); avr::hw::ucsr0a::write(avr::hw::ucsr0a::u2x0(1));
} }
// Activation: 3x'@', each inside the config page's timeout window // Activation: activation_knocks '@', each inside the config page's timeout
// (floored so a corrupt page cannot lock the loader out); anything else - // window (floored so a corrupt page cannot lock the loader out); anything
// including silence - hands over. // else - including silence - hands over.
window = avr::flash_load(flash_ptr(app_end + 2)) | act_min; window = avr::flash_load(flash_ptr(app_end + timeout_at)) | act_min;
for (std::uint8_t k = 3; k; --k) { for (std::uint8_t k = activation_knocks; k; --k) {
if (rx() != knock) { if (rx() != knock) {
appjump(); appjump();
} }
} }
window = comm_window; 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 // 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 // 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; // emergency erase behind two confirms. On pass the info block goes out;
// the emergency path skips it and drops into the command loop. // 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; std::uint8_t mask = 0xff;
for (;;) { 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; ++at;
if (expected == 0xff) { if (expected == 0xff) {
send_block(false, reinterpret_cast<std::uint16_t>(info.data()), info.size()); send_block(false, reinterpret_cast<std::uint16_t>(info.data()), info.size());
break; break;
} }
std::uint8_t got = rx(); const auto got = rx();
if (got == 0) { if (got == 0) {
if (mask == 0) { if (mask == 0) {
continue; continue;
@@ -271,9 +217,8 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
for (;;) { for (;;) {
tx(confirm); // Mainloop ready tx(confirm); // Mainloop ready
const std::uint8_t command = rx(); switch (rx()) {
switch (command) { case command::read_flash: // one page per host '!'
case 'f': // read application flash, one page per host '!'
for (std::uint16_t a = 0; a < app_end; a += page) { for (std::uint16_t a = 0; a < app_end; a += page) {
if (rx() != confirm) { if (rx() != confirm) {
break; break;
@@ -281,7 +226,7 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
send_block(false, a, page); send_block(false, a, page);
} }
break; 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) { for (std::uint16_t a = 0;; a += page) {
if (rx() != confirm) { if (rx() != confirm) {
break; break;
@@ -289,14 +234,14 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
send_block(true, a, page); send_block(true, a, page);
} }
break; 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(); std::uint16_t a = erase_application();
for (; rcnf() == confirm; a += page) { for (; rcnf() == confirm; a += page) {
store_flash_page(a); store_flash_page(a);
} }
break; 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;) { for (std::uint16_t a = 0; rcnf() == confirm;) {
std::uint8_t count = page; std::uint8_t count = page;
do { do {
@@ -305,11 +250,11 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
} while (--count); } while (--count);
} }
break; break;
case 'c': // read the config page case command::read_config:
read_config: read_config:
send_block(false, app_end, page); send_block(false, app_end, page);
break; 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) { if (rcnf() != confirm) {
break; break;
} }
+33 -83
View File
@@ -4,27 +4,20 @@
// TinySafeBoot native-UART fixed-baud protocol on libavr with the full feature // 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, // 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 // 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. // 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 // The one-wire wiring, the flash-resident info block, every SPM/EEPROM lock and
// are libavr's to handle; the only attribute is the naked reset entry that // the crt-free reset entry are libavr's to handle.
// stands in for the absent C runtime.
#include <libavr/libavr.hpp> #include "tsb.hpp"
#include <avr/io.h> // SP / RAMEND for the crt-free boot entry using dev = avr::device<{.clock = tsb::clock}>;
using namespace avr::literals;
namespace spm = avr::spm;
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. // 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 // 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 // solver holds rates to - the oracle's own deployment has run there for a
// decade, so the override states that it is meant. // decade, so the override states that it is meant.
using serial_t = dev::uart0<{ using serial_t = dev::uart0<{
.baud = 115200_Bd, .baud = tsb::baud_rate,
.allow_baud_error = true, .allow_baud_error = true,
.half_duplex = true, .half_duplex = true,
}>; }>;
@@ -33,53 +26,15 @@ inline constexpr serial_t serial{};
namespace tsb { namespace tsb {
namespace { namespace {
// The loader is purely polled - it never enables interrupts - so every SPM and // The boot section: 1 KB, BOOTSZ=10.
// 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;
constexpr std::uint16_t boot_bytes = 1024; constexpr std::uint16_t boot_bytes = 1024;
constexpr std::uint16_t app_end = spm::flash_bytes - boot_bytes - page; constexpr std::uint16_t app_end = config_page(boot_bytes);
constexpr std::uint16_t eeprom_end = avr::hw::db.mem.eeprom_size - 1;
// Firmware version stamp: YY*512 + MM*32 + DD, the encoding the host decodes. // A flash_table keeps the info block in progmem with no .data image (there is
constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 20; // no crt to copy one).
inline constexpr auto info_data = info_block(date_stamp(26, 7, 20), app_end);
// 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<std::uint8_t>({
'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
using info = avr::flash_table<info_data>; using info = avr::flash_table<info_data>;
// 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<std::uint8_t>(dev::clock.hz / 1'000'000);
// The receive window, pre-floored where it is set. In .noinit: there is no crt // 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. // to clear a .bss image, and run() stores it before the first receive.
[[gnu::section(".noinit")]] std::uint8_t window; [[gnu::section(".noinit")]] std::uint8_t window;
@@ -110,11 +65,6 @@ void tx(std::uint8_t byte)
serial.write(byte); serial.write(byte);
} }
const std::uint8_t *flash_ptr(std::uint16_t addr)
{
return reinterpret_cast<const std::uint8_t *>(addr);
}
// Stream `count` bytes to the host, from flash (LPM) or from EEPROM. // Stream `count` bytes to the host, from flash (LPM) or from EEPROM.
void send_flash(std::uint16_t addr, std::uint8_t count) void send_flash(std::uint16_t addr, std::uint8_t count)
{ {
@@ -144,8 +94,8 @@ void store_flash_page(std::uint16_t addr)
{ {
const auto open = spm::page::begin<spm::from::boot_section, off>(addr); const auto open = spm::page::begin<spm::from::boot_section, off>(addr);
for (std::uint16_t i = 0; i < page; i += 2) { for (std::uint16_t i = 0; i < page; i += 2) {
std::uint8_t lo = rx(); const auto lo = rx();
std::uint8_t hi = rx(); const auto hi = rx();
spm::fill<off>(open, 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<spm::from::boot_section, off>(addr); // blocking: waits the write out spm::write_page<spm::from::boot_section, off>(addr); // blocking: waits the write out
@@ -190,8 +140,8 @@ extern "C" [[noreturn]] void tsb_app();
tsb_app(); tsb_app();
} }
// 'f': stream the application flash back, one page per host '!'. Self-terminates // Stream the application flash back, one page per host '!'. Self-terminates at
// at the application boundary; the host normally stops earlier with a non-'!'. // the application boundary; the host normally stops earlier with a non-'!'.
void read_flash() void read_flash()
{ {
for (std::uint16_t a = 0; a < app_end; a += page) { for (std::uint16_t a = 0; a < app_end; a += page) {
@@ -202,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() void read_eeprom()
{ {
for (std::uint16_t a = 0;; a += page) { for (std::uint16_t a = 0;; a += page) {
@@ -213,8 +163,8 @@ void read_eeprom()
} }
} }
// 'F': erase the whole application first, then take pages the host offers // Erase the whole application first, then take pages the host offers behind
// behind '?'. // '?'.
void write_flash() void write_flash()
{ {
erase_application(); erase_application();
@@ -223,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() void write_eeprom()
{ {
for (std::uint16_t a = 0; request_confirm(); a += page) { for (std::uint16_t a = 0; request_confirm(); a += page) {
@@ -231,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() void write_config()
{ {
if (!request_confirm()) { if (!request_confirm()) {
@@ -256,7 +206,7 @@ void emergency_erase()
spm::rww_enable<off>(); spm::rww_enable<off>();
} }
// 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 // 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 // emergency erase; a wrong byte hangs the loader, still draining the line, so a
// wrong password can never fall through to the erase. // wrong password can never fall through to the erase.
@@ -264,12 +214,12 @@ enum class gate : std::uint8_t { pass, emergency };
gate password_gate() 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) {
std::uint8_t expected = avr::flash_load(pw); const auto expected = avr::flash_load(pw);
if (expected == 0xff) { if (expected == 0xff) {
return gate::pass; return gate::pass;
} }
std::uint8_t got = rx(); const auto got = rx();
if (got == 0) { if (got == 0) {
return gate::emergency; return gate::emergency;
} }
@@ -291,14 +241,14 @@ gate password_gate()
avr::init<serial_t>(); avr::init<serial_t>();
// 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 // 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 // loader out. An idle port times out and boots the application; the same
// window then bounds every receive of the session. // 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; __uint24 idle = static_cast<__uint24>(window) << 16;
std::uint8_t knocks = 0; std::uint8_t knocks = 0;
while (knocks < 3) { while (knocks < activation_knocks) {
if (auto byte = serial.read()) { if (auto byte = serial.read()) {
knocks = *byte == knock ? knocks + 1 : 0; knocks = *byte == knock ? knocks + 1 : 0;
} else if (--idle == 0) { } else if (--idle == 0) {
@@ -321,22 +271,22 @@ gate password_gate()
for (;;) { for (;;) {
tx(confirm); // Mainloop ready tx(confirm); // Mainloop ready
switch (rx()) { switch (rx()) {
case 'f': case command::read_flash:
read_flash(); read_flash();
break; break;
case 'F': case command::write_flash:
write_flash(); write_flash();
break; break;
case 'e': case command::read_eeprom:
read_eeprom(); read_eeprom();
break; break;
case 'E': case command::write_eeprom:
write_eeprom(); write_eeprom();
break; break;
case 'c': case command::read_config:
send_flash(app_end, page); send_flash(app_end, page);
break; break;
case 'C': case command::write_config:
write_config(); write_config();
break; break;
default: default:
+24 -79
View File
@@ -29,80 +29,24 @@
// line safe: the device drives it only between a received command and its // line safe: the device drives it only between a received command and its
// reply, and releases it (RXEN0 only) whenever it waits. // reply, and releases it (RXEN0 only) whenever it waits.
#include <libavr/libavr.hpp> #include "tsb.hpp"
#include <avr/io.h> // SP / RAMEND for the crt-free boot entry
using namespace avr::literals;
namespace spm = avr::spm;
namespace ee = avr::eeprom;
namespace hw = avr::hw; namespace hw = avr::hw;
namespace tsb { namespace tsb {
namespace { namespace {
// The loader is purely polled - it never enables interrupts - so every SPM and // The boot section: 1 KB, BOOTSZ=10.
// 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 = '@';
// 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;
constexpr std::uint16_t boot_bytes = 1024; constexpr std::uint16_t boot_bytes = 1024;
constexpr std::uint16_t app_end = spm::flash_bytes - boot_bytes - page; constexpr std::uint16_t app_end = config_page(boot_bytes);
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 [[gnu::progmem]] constexpr auto info = info_block(date_stamp(26, 7, 20), app_end);
// it follows the clock rather than restating it (rule 41).
constexpr auto act_min = static_cast<std::uint8_t>((16_MHz).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);
// 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<std::uint32_t>((16_MHz).hz / baud.actual);
// The 16-byte device-info block, streamed out on activation.
// clang-format off
[[gnu::progmem]] constexpr auto info = std::to_array<std::uint8_t>({
'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
register std::uint16_t g_addr asm("r28"); register std::uint16_t g_addr asm("r28");
register std::uint8_t g_cnt asm("r16"); register std::uint8_t g_cnt asm("r16");
register std::uint8_t g_window asm("r7"); register std::uint8_t g_window asm("r7");
register std::uint8_t g_receiving asm("r6"); register std::uint8_t g_receiving asm("r6");
const std::uint8_t *flash_ptr(std::uint16_t addr)
{
return reinterpret_cast<const std::uint8_t *>(addr);
}
// Bounded byte receive, the oracle's shape: release the one-wire line on a // 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 // 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 // then falls through every compare - not a knock, not a confirm, not a
@@ -157,10 +101,11 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
return rx(); 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() [[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; ++g_addr;
return byte; return byte;
} }
@@ -168,7 +113,7 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
// One EEPROM byte <- [g_addr++]. // One EEPROM byte <- [g_addr++].
[[gnu::noinline, gnu::noclone]] std::uint8_t eerd() [[gnu::noinline, gnu::noclone]] std::uint8_t eerd()
{ {
std::uint8_t byte = ee::read<no_spm>(g_addr); const auto byte = ee::read<no_spm>(g_addr);
++g_addr; ++g_addr;
return byte; return byte;
} }
@@ -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, // 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 // 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() [[gnu::noinline, gnu::noclone]] void erase_below()
{ {
g_addr -= page; g_addr -= page;
@@ -262,23 +207,23 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
// same reason. // same reason.
g_receiving = 0; g_receiving = 0;
// Activation: 3x'@', each inside the config page's timeout window (rx // Activation: activation_knocks '@', each inside the config page's timeout
// floors it so a corrupt page cannot lock the loader out); anything else - // window (rx floors it so a corrupt page cannot lock the loader out);
// including silence - hands over. // anything else - including silence - hands over.
g_window = avr::flash_load(flash_ptr(app_end + 2)); g_window = avr::flash_load(flash_ptr(app_end + timeout_at));
for (std::uint8_t k = 3; k; --k) { for (std::uint8_t k = activation_knocks; k; --k) {
if (rx() != knock) { if (rx() != knock) {
appjump(); appjump();
} }
} }
g_window = comm_window; 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 // 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 // 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; // emergency erase behind two confirms. On pass the info block goes out;
// the emergency path skips it and drops into the command loop. // 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; std::uint8_t mask = 0xff;
for (;;) { for (;;) {
std::uint8_t expected = avr::flash_load(flash_ptr(g_addr)) & mask; std::uint8_t expected = avr::flash_load(flash_ptr(g_addr)) & mask;
@@ -289,7 +234,7 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
sendf(); sendf();
break; break;
} }
std::uint8_t got = rx(); const auto got = rx();
if (got == 0) { if (got == 0) {
if (mask == 0) { if (mask == 0) {
continue; continue;
@@ -314,7 +259,7 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
tx(confirm); // Mainloop ready tx(confirm); // Mainloop ready
g_addr = 0; g_addr = 0;
switch (rx()) { switch (rx()) {
case 'f': // read application flash, one page per host '!' case command::read_flash: // one page per host '!'
for (;;) { for (;;) {
if (rx() != confirm) { if (rx() != confirm) {
break; break;
@@ -326,13 +271,13 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
} }
} }
break; break;
case 'F': // erase the application, then take pages behind '?' case command::write_flash: // erase the application, then take pages behind '?'
erase_application(); // leaves g_addr = 0, the write start erase_application(); // leaves g_addr = 0, the write start
while (rcnf() == confirm) { while (rcnf() == confirm) {
store_flash(); store_flash();
} }
break; 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 (;;) { for (;;) {
if (rx() != confirm) { if (rx() != confirm) {
break; break;
@@ -343,7 +288,7 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
} while (--g_cnt); } while (--g_cnt);
} }
break; break;
case 'E': // take EEPROM pages behind '?' case command::write_eeprom: // pages behind '?'
while (rcnf() == confirm) { while (rcnf() == confirm) {
g_cnt = page; g_cnt = page;
do { do {
@@ -351,13 +296,13 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
} while (--g_cnt); } while (--g_cnt);
} }
break; break;
case 'c': // read the config page case command::read_config:
read_config: read_config:
g_addr = app_end; g_addr = app_end;
g_cnt = page; g_cnt = page;
sendf(); sendf();
break; 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) { if (rcnf() != confirm) {
break; break;
} }