13 Commits
Author SHA1 Message Date
BlackMarkandClaude Fable 5 da730b7bb5 tsb: third size pass — restructure to the oracle's shape
The second pass concluded the 168 B tricks->asm gap was per-call ABI
cost. Most of it was structure. Rebuilt around the oracle's own shape —
argless noinline primitives over a whole-loader call-saved register
protocol (g_addr in Y, count r16, window r7, direction latch r6), a
top-down erase_below whose loop tests against zero and hands callers
g_addr = 0 for free, bounded rx everywhere (a silent host unwinds to
the app from any state, as the oracle does), and a named tsb_app entry
that --pmem-wrap-around=32k relaxes to the wrapped rjmp:

  tsb_asm    510 B in the 512 B section (oracle: 500), C++ except rx
             and the page-store loop — the two routines whose remaining
             cost is the calling convention itself (~30 asm lines, was
             ~280)
  tsb_tricks 526 B, no assembly at all (was 666)
  tsb_pure   836 B, still one readable function per command (was 842)

Every g_* update placement works around a GCC 16.1 wrong-code bug
(stores into global register variables deleted when only callees read
them — repro and rules in libavr dev/lessons.md). Also fixes two
latent hardware bugs all earlier tiers carried, masked by simavr's
zeroed register file: the crt-less entries never established
__zero_reg__ = 0, and the direction latch was read before written —
power-on registers are undefined.

All tiers full oracle feature parity, protocol tests green in both
libavr modes, .text byte-identical across modes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYufebsiWvGkAJ2fLAB1gT
2026-07-20 01:00:27 +02:00
BlackMarkandClaude Opus 4.8 c351bee257 tsb: beat the first-pass size floors (tricks 666, pure 842)
tricks 778->666: always_inline every single-call handler into the
[[noreturn]] reset entry (which pays no prologue, so their push/pop of
call-saved registers vanishes), walk the page pointer in Y (adiw, base
recovered as g_addr-page) instead of recomputing Z=base+offset, bring
the UART up in the two registers that are not already at their reset
value, and seed the activation counter as __uint24.

pure 896->842: TU-local internal linkage (proper hygiene, and it lets
the compiler inline the one-call handlers), a byte-wide activation
count, __uint24 timeout. Still one readable function per command.

asm unchanged at 498: its C++-expressible parts are already C++; the
core stays asm (the 666 B all-tricks tier is 168 B over — per-call ABI
tax, not a feature). All three cross-mode byte-identical, protocol green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHeP42XU3wf6RfhyuxBTE5
2026-07-19 20:15:49 +02:00
BlackMarkandClaude Opus 4.8 34e7f1be34 tsb: drive each tier to its size floor
asm 502->498 B (below the oracle's 500): the stack bring-up moves to plain C++,
and a register is reserved for the config-page high byte instead of reloading it
at each app-flash-boundary compare. tricks 808->778 B: shared erase/rww helpers
plus the libavr half-duplex W1C fix. pure 950->896 B and no SRAM: streams
rx->SPM/EEPROM instead of staging a 128 B page buffer. All three keep full oracle
feature parity and stay byte-identical across modes; protocol tests (round-trip +
password + emergency erase) green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHeP42XU3wf6RfhyuxBTE5
2026-07-19 18:47:38 +02:00
BlackMarkandClaude Opus 4.8 445e187722 tsb: document the three tiers at full parity in the build file
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHeP42XU3wf6RfhyuxBTE5
2026-07-19 16:50:18 +02:00
BlackMarkandClaude Opus 4.8 250aba5cfb tsb: protocol test covers the password gate and emergency erase
Each scenario group now runs on its own freshly-reset device: the round-trip
on a blank config page, plus a password-config device that must be sent the
password after the knock to activate, and an emergency-erase device where a
0-byte + two confirms wipes flash, EEPROM and the config page (verified by
reading all three back as 0xff). All three tiers pass every group.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHeP42XU3wf6RfhyuxBTE5
2026-07-19 16:49:20 +02:00
BlackMarkandClaude Opus 4.8 5c900720e3 tsb: pure and tricks tiers reach full oracle feature parity
Both tiers gain the features the asm tier already carries — one-wire
half-duplex (via libavr's new .half_duplex), the config-page activation
timeout, and emergency erase (password \0 + double-confirm wipes flash,
EEPROM and the config page) — on top of the watchdog bail, password gate and
config/flash/EEPROM read-write they already had. pure stays idiomatic
(flash_table info block, one function per command) at 950 B; tricks keeps its
compiler trickery (call-saved global-register page walk, unified runtime-flag
paths pinned noinline/noclone, streaming stores, arithmetic command decode)
at 808 B. Both byte-identical across generated and reflect modes; the size
gradient across the three tiers is now 502 / 808 / 950 B.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHeP42XU3wf6RfhyuxBTE5
2026-07-19 16:47:21 +02:00
BlackMarkandClaude Opus 4.8 7d6ef959b2 tsb: asm tier reaches full oracle feature parity at 502 B
Rewrite the inline-asm tier so it matches the hand-written fixed-baud oracle's
feature set inside the 512 B boot section: watchdog-reset bail, one-wire
half-duplex (RXEN/TXEN toggled per direction, TX turnaround guard),
config-page activation timeout, the password gate (wrong byte hangs draining
the UART), emergency erase (password \0 + double-confirm wipes flash, EEPROM
and the config page), and config/flash/EEPROM read-write. Every geometry,
baud and info-block constant comes from libavr consteval; only the dense
control flow is hand-written. 502 B, byte-identical across generated and
reflect modes.

Test harness: seed the config page from TSB_CONFIG so the password and
emergency-erase paths are exercisable, and clear simavr's AVR_UART_FLAG_POLL_
SLEEP — a host-CPU-saving usleep(1)-per-idle-poll hack that models no hardware
and paces a one-wire loader (which releases TX between bytes) in real time,
distorting protocol timing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHeP42XU3wf6RfhyuxBTE5
2026-07-19 16:23:16 +02:00
BlackMarkandClaude Opus 4.8 11ffbce2e2 tsb: vendor the fixed-baud assembly oracle as the size/feature bar
The Seed Robotics native-UART fixed-baud TinySafeBoot (GPLv3), reference
only — not built. Assembles to 500 B with the full feature set, proving
≤512 B and full feature parity are simultaneously reachable. Also drops the
stale empty stk500v2/ leftover.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHeP42XU3wf6RfhyuxBTE5
2026-07-19 15:29:58 +02:00
BlackMarkandClaude Opus 4.8 f32a27ff15 tsb: use the named register surface
Direct register access now reads through the named surface
(hw::mcusr::wdrf.test(), hw::ucsr0b::write(...)) instead of the string form,
matching how libavr itself is written. Zero-overhead: pure 740 B, tricks 658 B,
asm 508 B unchanged, all byte-identical across modes, protocol green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 14:55:01 +02:00
BlackMarkandClaude Opus 4.8 57d94cf631 tsb: refactor the pure tier onto libavr sugar
The showcase tier now leans on the helpers it fed back instead of reaching under
them: the info block is an avr::flash_table (no raw [[gnu::progmem]]), a page is
filled with spm::fill(addr, span) (no hand-packed lo|hi<<8 loop), and the
WDT-reset bail reads field<"MCUSR","WDRF">::test() (no read() & {}(1).value).

Zero-overhead throughout: .text stays 740 B, byte-identical across generated and
reflect modes, protocol test green. The info block streams through the existing
address-based send_flash rather than a range-for over the flash_table — the
range-for is a distinct loop that cannot share the loader's one flash streamer,
so it would add 14 B for no functional gain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 13:33:53 +02:00
BlackMarkandClaude Opus 4.8 8203a24f33 tsb: slim the port branch to the libavr reimplementation
main carried the whole pre-libavr tree beside the port: the other-bootloader
directories (blink, stk500v2), the Atmel Studio solution/project, and — dead in
the tsb dir itself — four submodule links to the superseded io/flash/uart/type
libraries the libavr sources never include. None are build inputs; CMake drives
the three variants through FetchContent. master keeps the full legacy tree
untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 13:12:56 +02:00
BlackMarkandClaude Opus 4.8 2906da3272 tsb: drop the local -O3 strip, now handled by the libavr toolchain
The -O3 leak is fixed upstream (cmake/release-os.cmake via CMAKE_PROJECT_INCLUDE),
so the port no longer needs its own string(REPLACE); a Release build is -Os
through the toolchain file. Verified: all three variants build at their sizes
(508/658/740) and pass the size + protocol ctest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 10:52:13 +02:00
BlackMarkandClaude Opus 4.8 64c1e484b5 tsb: reimplement TinySafeBoot on libavr in three size tiers
The native-UART fixed-baud TinySafeBoot protocol, ported onto libavr as a
crt-free boot-section loader, in three variants that trade clarity for size:

  tsb_pure   740 B  idiomatic C++: SRAM page buffer, separate flash/EEPROM
                    leaves, shared framing; the polled `unused` guard posture.
  tsb_tricks 658 B  unified runtime-flag paths (noinline/noclone), call-saved
                    global-register page walk — attributes only, no asm.
  tsb_asm    508 B  streaming store + hand-rolled UART/SPM/EEPROM/erase loops;
                    fits the 512 B boot section (BOOTSZ=11). Trims the optional
                    password gate and WDT-reset bail — unreachable in C++ with
                    both (hand-asm is ~15 % denser). Tiers 1-2 keep them and
                    live in the 1 KB section they fit.

All three are .text byte-identical across libavr's generated and reflect modes.
The CMake build strips the leaked -O3 (a Release build is silently -O3, not the
-Os this loader is measured against) and gates each variant's size against its
section. A simavr harness (test/device.c + test/tsbtest.py) drives the real wire
protocol over a pty and flashes the device; the size and protocol tests run in
ctest. Verified byte-for-byte against the reference tsbloader_adv (C#/mono):
activate, read info, flash write + verify.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 05:00:51 +02:00
25 changed files with 550 additions and 1417 deletions

No files matched your search

+1 -2
View File
@@ -7,9 +7,8 @@ TabWidth: 4
UseTab: ForIndentation UseTab: ForIndentation
AlignEscapedNewlines: DontAlign AlignEscapedNewlines: DontAlign
AllowShortFunctionsOnASingleLine: Empty AllowShortFunctionsOnASingleLine: Empty
BreakTemplateDeclarations: Yes AlwaysBreakTemplateDeclarations: true
BreakBeforeBraces: Custom BreakBeforeBraces: Custom
BraceWrapping: BraceWrapping:
AfterFunction: true AfterFunction: true
InsertBraces: true
... ...
-32
View File
@@ -1,32 +0,0 @@
# Editor accommodations for the second frontend: only what is true of the
# code wherever it is built. The build tree is named in .vscode/settings.json.
CompileFlags:
Add:
# 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
# one available - clang rejects _BitInt(24) on this target.
- -D__uint24=unsigned long
- -D__int24=long
# clangd forwards the driver's system includes but not its own header
# directory, so <stdint.h> resolves to avr-libc's, which still gates the
# limit and constant macros on the C++98 opt-in.
- -D__STDC_LIMIT_MACROS
- -D__STDC_CONSTANT_MACROS
# 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
# default limit of 19 inside the headers and truncate the parse.
- -ferror-limit=0
Remove:
# Codegen shaping the policy tier carries and clang has no spelling for.
- -fno-tree-ter
- -fno-move-loop-invariants
# 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
# colour reserved for things that do not compile.
- -Werror
Diagnostics:
Suppress:
# clang's AVR `signal` attribute takes no arguments and it knows none of
# progmem, naked or OS_main. A misspelling is what the build is for.
- attribute_wrong_number_arguments
- unknown-attributes
+5 -8
View File
@@ -1,11 +1,8 @@
# Line endings are the repository's, not the editing machine's: this checkout *.h eol=lf
# is reached from two hosts, and a file rewritten by a Windows tool comes back *.hpp eol=lf
# with every line changed unless something says otherwise. Naming the source *.c eol=lf
# extensions left Markdown, Python, shell and CMake to whatever the writing *.cpp eol=lf
# tool defaulted to, which is CRLF on one of the two. .git* eol=lf
* text=auto eol=lf
# Atmel Studio writes these and expects them back.
*.vcxproj* eol=crlf *.vcxproj* eol=crlf
*.cppproj eol=crlf *.cppproj eol=crlf
*.sln eol=crlf *.sln eol=crlf
-5
View File
@@ -12,10 +12,5 @@ Debug
# CMake / clangd # CMake / clangd
/build/ /build/
/local/
compile_commands.json compile_commands.json
.cache/ .cache/
# Python
__pycache__/
*.pyc
-3
View File
@@ -1,3 +0,0 @@
[submodule "libavr"]
path = libavr
url = ../libavr.git
-6
View File
@@ -1,6 +0,0 @@
{
"recommendations": [
"llvm-vs-code-extensions.vscode-clangd",
"ms-vscode.cmake-tools"
]
}
-34
View File
@@ -1,34 +0,0 @@
{
// clangd is the language server; the cpptools engine would parse every file
// a second time and disagree, since nothing tells it about a cross
// compiler.
"C_Cpp.intelliSenseEngine": "disabled",
// --query-driver lets clangd ask the cross compiler for its own system
// includes and target.
"clangd.arguments": [
"--compile-commands-dir=${workspaceFolder}/build/atmega328p-generated",
"--query-driver=**avr-g++*",
"--header-insertion=never"
],
// The presets are the build interface, and the toolchain file inside the
// libavr submodule is the one place the compiler is chosen. **No prefix is
// named here**: a committed file may not name a path that is true of one
// machine (libavr guidance rule 50), so the gitignored local/machine.cmake
// at this repository's root is where a checkout says where its toolchain
// is - one file, and it answers for both hosts.
"cmake.useCMakePresets": "always",
"cmake.configureOnOpen": true,
"cmake.options.statusBarVisibility": "compact",
"files.watcherExclude": {
"**/build/**": true,
"**/libavr/**": true
},
"files.associations": {
".clangd": "yaml",
".clang-format": "yaml"
}
}
+54 -99
View File
@@ -2,139 +2,94 @@ cmake_minimum_required(VERSION 3.28)
project(tsb_libavr LANGUAGES CXX) project(tsb_libavr LANGUAGES CXX)
# libavr rides as the pinned submodule; LIBAVR_ROOT (cache or environment) # libavr from a local checkout (LIBAVR_ROOT) or the forge; the toolchain file
# overrides it for tandem development against a working tree. The toolchain # comes from the same checkout via CMakePresets.json.
# file comes from the submodule via CMakePresets.json either way. include(FetchContent)
if(NOT LIBAVR_ROOT AND DEFINED ENV{LIBAVR_ROOT}) if(NOT LIBAVR_ROOT AND DEFINED ENV{LIBAVR_ROOT})
set(LIBAVR_ROOT $ENV{LIBAVR_ROOT}) set(LIBAVR_ROOT $ENV{LIBAVR_ROOT})
endif() endif()
if(NOT LIBAVR_ROOT) if(LIBAVR_ROOT)
set(LIBAVR_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/libavr) FetchContent_Declare(libavr SOURCE_DIR ${LIBAVR_ROOT})
else()
FetchContent_Declare(libavr GIT_REPOSITORY git@git.blackmark.me:avr/libavr.git GIT_TAG main)
endif() endif()
if(NOT EXISTS ${LIBAVR_ROOT}/CMakeLists.txt) FetchContent_MakeAvailable(libavr)
message(FATAL_ERROR "libavr not found at ${LIBAVR_ROOT} - run: git submodule update --init libavr")
endif()
add_subdirectory(${LIBAVR_ROOT} libavr-build)
include(${LIBAVR_ROOT}/cmake/checks.cmake)
if(PROJECT_IS_TOP_LEVEL) if(PROJECT_IS_TOP_LEVEL)
add_compile_options(-Werror) # warnings are errors for the port's own code add_compile_options(-Werror) # warnings are errors for the port's own code
enable_testing() enable_testing()
# Rules 11 and 33 over this repo's own sources. The oracle's assembly needs # The behavioral test drives the real TinySafeBoot wire protocol over a
# no exclusion: it is neither formatted nor ASCII-checked, being in neither # simavr pty (as the host tools do) and actually flashes the device. The
# glob, which is the right answer for a vendored reference whose text is # runner is a host program built at configure time against libsimavr; if it
# the artifact. # or Python is missing, only the size tests run.
libavr_format_test() find_program(_host_cc NAMES cc gcc)
find_package(Python3 COMPONENTS Interpreter)
# The behavioral tests drive the real wire protocols over a simavr pty if(_host_cc AND Python3_FOUND)
# (as the host tools do) and actually flash the device. The runner is a
# host program built at configure time against libsimavr (C++23 - what the
# distribution's compiler speaks in full).
#
# **A host that cannot build it registers those tests anyway and skips
# them**: leaving them out would make the suite's size a property of the
# machine, and such a suite 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++)
set(_tsb_absent "${LIBAVR_NO_PYTHON}")
if(NOT _host_cxx)
set(_tsb_absent "no host C++ compiler on PATH, and the simavr device is a host program")
elseif(NOT _tsb_absent)
execute_process( execute_process(
COMMAND ${_host_cxx} -std=c++23 -Wall -Wextra -O2 COMMAND ${_host_cc} -O2 -I/usr/include/simavr -I/usr/include/simavr/parts
-I/usr/include/simavr -I/usr/include/simavr/parts -o ${TSB_DEVICE} ${CMAKE_CURRENT_SOURCE_DIR}/test/device.c
-o ${TSB_DEVICE} ${CMAKE_CURRENT_SOURCE_DIR}/test/device.cpp
-lsimavr -lsimavrparts -lelf -lsimavr -lsimavrparts -lelf
RESULT_VARIABLE _dev_res ERROR_VARIABLE _dev_err) RESULT_VARIABLE _dev_res ERROR_VARIABLE _dev_err)
if(NOT _dev_res EQUAL 0) if(NOT _dev_res EQUAL 0)
# One bounded line of it: this becomes a single argument on a message(STATUS "tsb_device not built (${_dev_err}) — protocol tests skipped")
# command line, and the reading has to say what stopped the build unset(TSB_DEVICE)
# rather than that something did.
string(REGEX REPLACE "[\r\n\t]+" " " _dev_err "${_dev_err}")
string(REPLACE ";" "," _dev_err "${_dev_err}")
string(LENGTH "${_dev_err}" _dev_len)
if(_dev_len GREATER 240)
string(SUBSTRING "${_dev_err}" 0 240 _dev_err)
endif()
set(_tsb_absent "test/device.cpp does not build here: ${_dev_err}")
endif() endif()
endif() endif()
libavr_launcher(_tsb_python "${_tsb_absent}" ${Python3_EXECUTABLE})
if(_tsb_absent)
message(STATUS "the protocol tests skip here - ${_tsb_absent}")
endif() endif()
# CMakePresets.json is generated; the gate holds it to its generator. # The TinySafeBoot protocol reimplemented on libavr in three variants that trade
libavr_launcher(_presets_python "${LIBAVR_NO_PYTHON}" ${Python3_EXECUTABLE}) # clarity for size. Each links into the ATmega328P boot section (BOOTSZ selects
add_test(NAME presets.generated # its size; BOOTRST vectors a reset to its base) with -nostartfiles — a polled
COMMAND ${_presets_python} ${CMAKE_CURRENT_SOURCE_DIR}/tools/make_presets.py --check) # loader has no use for the crt or the vector table. The naked entry sits in
endif() # .vectors, laid first, and runs. The boot base is FLASHEND+1 minus the section
# size; the linker section-start and the source's boot_bytes agree. tsb_app is
# The ELF is only a container (symbols, section headers) and is never flashed - # the application's reset vector, pinned to 0 here so the loaders jump to a
# and the host tool's load_image() dispatches on extension, so handing it one # named function; --pmem-wrap-around lets relaxation turn that absolute jump
# would silently program the header bytes. Every loader image therefore gets # into the wrapped rjmp AVR's modulo-flash PC actually executes.
# both flashable forms beside it at link time: .hex for avrdude, and .bin for # All three implement the full oracle feature set (see oracle/README.md):
# the host tool's raw path (which is what the reloc and update tests convert to # watchdog bail, one-wire half-duplex, config-page activation timeout, password
# on the fly). .eeprom is dropped - EEPROM content is its own update. # gate, emergency erase, config/flash/EEPROM read-write. They differ only in how,
function(add_image_outputs name) # and the size gradient is the cost of that "how" — see dev/lessons.md.
add_custom_command(TARGET ${name} POST_BUILD # tsb_asm — the tricks tier's C++ with exactly two routines in asm (the
COMMAND ${CMAKE_OBJCOPY} -O ihex -R .eeprom # bounded rx and the page-store loop — the two whose remaining
$<TARGET_FILE:${name}> $<TARGET_FILE:${name}>.hex # cost is the C ABI itself): 510 B in the 512 B section the
COMMAND ${CMAKE_OBJCOPY} -O binary -R .eeprom # hand-written 500 B oracle occupies. Everything else, from
$<TARGET_FILE:${name}> $<TARGET_FILE:${name}>.bin) # bring-up to dispatch, is C++ on libavr.
endfunction() # tsb_tricks — no asm at all: the whole-loader register allocation lives in
# global register variables (Y walks the page pointer), every
# The TinySafeBoot protocol reimplemented on libavr in four tiers that trade # helper is a tiny noinline primitive placed by the
# clarity for size, each with the full oracle feature set; each source's header # global-register store rules, pages stream straight to
# says what its tier is allowed, and oracle/README.md compares what they # SPM/EEPROM, and the bring-up is the two reset-non-default
# measure. Each links into the ATmega328P boot section (BOOTSZ selects its # registers only. 526 B in the 1 KB section (BOOTSZ=10) — 14
# size; BOOTRST vectors a reset to its base) with -nostartfiles - a polled # over the oracle's section, from 168 over at this tier's first
# loader has no use for the crt or the vector table - and its entry sits in # floor.
# .vectors, laid first. The boot base is the flash size minus the section size; # tsb_pure — pure idiomatic libavr, one function per command, TU-local
# the linker section-start and the source's boot_bytes agree. tsb_app is the # (internal linkage), streaming (no SRAM page buffer): 836 B in
# application's reset vector, pinned to 0 here so the loaders jump to a named # the 1 KB section.
# function; --pmem-wrap-around lets relaxation turn that absolute jump into the #
# wrapped rjmp AVR's modulo-flash PC actually executes.
set(TSB_FLASH_KIB 32)
# 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 "${TSB_FLASH_KIB} * 1024 - ${bytes}") math(EXPR base_dec "32768 - ${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=${TSB_FLASH_KIB}k) -Wl,--defsym=tsb_app=0 -Wl,--pmem-wrap-around=32k)
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})
if(PROJECT_IS_TOP_LEVEL) if(PROJECT_IS_TOP_LEVEL)
add_test(NAME ${name}.size add_test(NAME ${name}.size
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:${name}> COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:${name}>
-DLIMIT=${bytes} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake) -DLIMIT=${bytes} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
if(DEFINED TSB_DEVICE)
add_test(NAME ${name}.protocol add_test(NAME ${name}.protocol
COMMAND ${_tsb_python} ${CMAKE_CURRENT_SOURCE_DIR}/test/tsbtest.py COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/tsbtest.py
${TSB_DEVICE} $<TARGET_FILE:${name}> ${base_hex}) ${TSB_DEVICE} $<TARGET_FILE:${name}> ${base_hex})
endif() endif()
endif()
endfunction() endfunction()
# The tiers reimplement the ATmega328P-only reference protocol, so the guard is
# the whole of what this repo builds.
if(LIBAVR_MCU STREQUAL "atmega328p")
add_tsb_variant(tsb_asm 512) add_tsb_variant(tsb_asm 512)
add_tsb_variant(tsb_policy 1024)
add_tsb_variant(tsb_pure 1024) add_tsb_variant(tsb_pure 1024)
add_tsb_variant(tsb_tricks 1024) add_tsb_variant(tsb_tricks 1024)
# The policy tier's floor needs these two: a loader's loop bodies all
# contain calls, which is what makes hoisting an invariant out of one cost
# more than it saves. The other tiers keep the flag set their recorded
# floors were measured with - none.
target_compile_options(tsb_policy PRIVATE -fno-move-loop-invariants -fno-tree-ter)
endif()
# Every test registered above carries the marker a stubbed launcher prints, so
# a check this host cannot run reads as Skipped rather than Failed.
if(PROJECT_IS_TOP_LEVEL)
libavr_skip_unverified()
endif()
+11 -51
View File
@@ -6,7 +6,7 @@
"hidden": true, "hidden": true,
"generator": "Ninja", "generator": "Ninja",
"binaryDir": "${sourceDir}/build/${presetName}", "binaryDir": "${sourceDir}/build/${presetName}",
"toolchainFile": "${sourceDir}/libavr/cmake/avr-toolchain.cmake", "toolchainFile": "$env{LIBAVR_ROOT}/cmake/avr-toolchain.cmake",
"cacheVariables": { "cacheVariables": {
"CMAKE_BUILD_TYPE": "Release", "CMAKE_BUILD_TYPE": "Release",
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON", "CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
@@ -16,69 +16,29 @@
{ {
"name": "atmega328p-generated", "name": "atmega328p-generated",
"inherits": "base", "inherits": "base",
"cacheVariables": { "cacheVariables": { "LIBAVR_MCU": "atmega328p", "LIBAVR_REFLECT": "OFF" }
"LIBAVR_MCU": "atmega328p",
"LIBAVR_REFLECT": "OFF"
}
}, },
{ {
"name": "atmega328p-reflect", "name": "atmega328p-reflect",
"inherits": "base", "inherits": "base",
"cacheVariables": { "cacheVariables": { "LIBAVR_MCU": "atmega328p", "LIBAVR_REFLECT": "ON" }
"LIBAVR_MCU": "atmega328p",
"LIBAVR_REFLECT": "ON"
}
} }
], ],
"buildPresets": [ "buildPresets": [
{ { "name": "atmega328p-generated", "configurePreset": "atmega328p-generated" },
"name": "atmega328p-generated", { "name": "atmega328p-reflect", "configurePreset": "atmega328p-reflect" }
"configurePreset": "atmega328p-generated"
},
{
"name": "atmega328p-reflect",
"configurePreset": "atmega328p-reflect"
}
],
"testPresets": [
{
"name": "atmega328p-generated",
"configurePreset": "atmega328p-generated",
"output": {
"outputOnFailure": true
}
}
], ],
"workflowPresets": [ "workflowPresets": [
{ {
"name": "atmega328p-generated", "name": "atmega328p-generated",
"steps": [ "steps": [
{ { "type": "configure", "name": "atmega328p-generated" },
"type": "configure", { "type": "build", "name": "atmega328p-generated" },
"name": "atmega328p-generated" { "type": "test", "name": "atmega328p-generated" }
},
{
"type": "build",
"name": "atmega328p-generated"
},
{
"type": "test",
"name": "atmega328p-generated"
}
]
},
{
"name": "atmega328p-reflect",
"steps": [
{
"type": "configure",
"name": "atmega328p-reflect"
},
{
"type": "build",
"name": "atmega328p-reflect"
}
] ]
} }
],
"testPresets": [
{ "name": "atmega328p-generated", "configurePreset": "atmega328p-generated", "output": { "outputOnFailure": true } }
] ]
} }
-67
View File
@@ -1,67 +0,0 @@
# Atmel Studio
`master` carries `bootloader.atsln`, so this branch does too: `ide/bootloader.atsln`
builds the loader from the same source Ninja does, to a **byte-identical
`.text`** — the `tsb_asm` tier in its 512-byte section (`check-flags.py` below
is what holds the flag sets equal, so the size is Ninja's own). CMake remains
the build system; the solution is here so the port opens in Studio as its
predecessor did.
## One project, of four tiers
A `.cppproj` is one binary at one set of flags. `tsb_asm` is the tier that
occupies the same 512-byte section `master`'s `tsb` project targeted, which is
the one worth opening in Studio.
The other three tiers (`tsb_pure`, `tsb_tricks`, `tsb_policy`) are not here.
They differ from `tsb_asm` in their source file, their section size, and — for
`tsb_policy` — two loop flags; nothing about that is a Studio concern, and what
they exist to demonstrate is a size gradient only the CMake size tests measure.
Adding one is a copy of `tsb_asm/tsb_asm.cppproj` in its own directory, with its
name, its GUID, its source path and its `--section-start` changed (`0x7c00` for
the 1 KiB tiers), plus four lines in the solution.
`avrdevice` is a project property, so each project gets its own directory:
Studio builds into `<project dir>/<Configuration>` whatever `OutputDirectory`
says, and two projects sharing a directory would share one object file.
## Debug keeps `-Os`
Both configurations compile at `-Os`; Debug adds only `-gdwarf-4`. The `.text`
is therefore identical in both, which is the point — a loader's section is a
**correctness** bound and not a budget. A debug configuration that silently
overruns the section is worse than none, and DWARF costs no flash, so the
optimisation level stays where correctness needs it.
## What Studio needs from the machine
libavr from the **submodule**, found at
`$(MSBuildProjectDirectory)\..\..\libavr\include` — correct by construction, and
anchored to the project because a plain relative path resolves against the
generated makefile's directory (the configuration's output directory), not the
project's. There is no `LIBAVR_ROOT` escape hatch: a variable exported in a
shell is invisible to Studio launched from the Start menu, and the failure reads
as a missing `libavr/libavr.hpp` — which is what the submodule answers.
A GCC 16.1 toolchain registered as flavour `avr-g++-16.1.0`, nothing older
reaching `-std=c++26`.
## Generating and gating
One generated file is required before the project will load at all, and one
command checks the flags have not drifted (both from libavr's
`tools/atmelstudio/`):
```sh
python libavr/tools/atmelstudio/componentinfo.py \
ide/tsb_asm/tsb_asm.componentinfo.xml --device ATmega328P
python libavr/tools/atmelstudio/check-flags.py \
--solution ide/bootloader.atsln --project tsb_asm --target tsb_asm \
--compile-commands build/atmega328p-generated/compile_commands.json \
--log build/as-tsb_asm.log
```
Release is what the gate compares — the presets define no debug build, and
Debug differs from Release only in `-gdwarf-4`.
Legacy (the yazoalfa-era submodules) stays on `master`.
-22
View File
@@ -1,22 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Atmel Studio Solution File, Format Version 11.00
VisualStudioVersion = 14.0.23107.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{E66E83B9-2572-4076-B26E-6BE79FF3018A}") = "tsb_asm", "tsb_asm\tsb_asm.cppproj", "{6618D3BE-7EB3-49A2-9113-F128E396FF06}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|AVR = Debug|AVR
Release|AVR = Release|AVR
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{6618D3BE-7EB3-49A2-9113-F128E396FF06}.Debug|AVR.ActiveCfg = Debug|AVR
{6618D3BE-7EB3-49A2-9113-F128E396FF06}.Debug|AVR.Build.0 = Debug|AVR
{6618D3BE-7EB3-49A2-9113-F128E396FF06}.Release|AVR.ActiveCfg = Release|AVR
{6618D3BE-7EB3-49A2-9113-F128E396FF06}.Release|AVR.Build.0 = Release|AVR
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
-112
View File
@@ -1,112 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="14.0">
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
<ProjectVersion>7.0</ProjectVersion>
<ToolchainName>com.Atmel.AVRGCC8.CPP</ToolchainName>
<ProjectGuid>6618d3be-7eb3-49a2-9113-f128e396ff06</ProjectGuid>
<avrdevice>ATmega328P</avrdevice>
<avrdeviceseries>none</avrdeviceseries>
<OutputType>Executable</OutputType>
<Language>CPP</Language>
<OutputFileName>$(MSBuildProjectName)</OutputFileName>
<OutputFileExtension>.elf</OutputFileExtension>
<OutputDirectory>$(MSBuildProjectDirectory)\$(Configuration)</OutputDirectory>
<AssemblyName>tsb_asm</AssemblyName>
<Name>tsb_asm</Name>
<RootNamespace>tsb_asm</RootNamespace>
<ToolchainFlavour>avr-g++-16.1.0</ToolchainFlavour>
<KeepTimersRunning>true</KeepTimersRunning>
<OverrideVtor>false</OverrideVtor>
<CacheFlash>true</CacheFlash>
<ProgFlashFromRam>true</ProgFlashFromRam>
<RamSnippetAddress>0x20000000</RamSnippetAddress>
<UncachedRange />
<preserveEEPROM>true</preserveEEPROM>
<OverrideVtorValue>exception_table</OverrideVtorValue>
<BootSegment>2</BootSegment>
<ResetRule>0</ResetRule>
<eraseonlaunchrule>0</eraseonlaunchrule>
<EraseKey />
<AsfFrameworkConfig>
<framework-data xmlns="">
<options />
<configurations />
<files />
<documentation help="" />
<offline-documentation help="" />
<dependencies>
<content-extension eid="atmel.asf" uuidref="Atmel.ASF" version="3.52.0" />
</dependencies>
</framework-data>
</AsfFrameworkConfig>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<ToolchainSettings>
<AvrGccCpp>
<avrgcc.common.Device>-mmcu=atmega328p</avrgcc.common.Device>
<avrgcc.common.outputfiles.hex>True</avrgcc.common.outputfiles.hex>
<avrgcc.common.outputfiles.lss>True</avrgcc.common.outputfiles.lss>
<avrgcc.common.outputfiles.eep>True</avrgcc.common.outputfiles.eep>
<avrgcc.common.outputfiles.srec>True</avrgcc.common.outputfiles.srec>
<avrgcc.common.outputfiles.usersignatures>False</avrgcc.common.outputfiles.usersignatures>
<avrgcccpp.compiler.symbols.DefSymbols>
<ListValues>
<Value>NDEBUG</Value>
</ListValues>
</avrgcccpp.compiler.symbols.DefSymbols>
<avrgcccpp.compiler.directories.IncludePaths>
<ListValues>
<Value>$(MSBuildProjectDirectory)\..\..\libavr\include</Value>
</ListValues>
</avrgcccpp.compiler.directories.IncludePaths>
<avrgcccpp.compiler.optimization.level>Optimize for size (-Os)</avrgcccpp.compiler.optimization.level>
<avrgcccpp.compiler.optimization.PrepareFunctionsForGarbageCollection>True</avrgcccpp.compiler.optimization.PrepareFunctionsForGarbageCollection>
<avrgcccpp.compiler.optimization.PrepareDataForGarbageCollection>True</avrgcccpp.compiler.optimization.PrepareDataForGarbageCollection>
<avrgcccpp.compiler.warnings.AllWarnings>True</avrgcccpp.compiler.warnings.AllWarnings>
<avrgcccpp.compiler.miscellaneous.OtherFlags>-std=c++26 -Wextra -Werror -mrelax -fno-exceptions -fno-rtti -fno-threadsafe-statics</avrgcccpp.compiler.miscellaneous.OtherFlags>
<avrgcccpp.linker.optimization.GarbageCollectUnusedSections>True</avrgcccpp.linker.optimization.GarbageCollectUnusedSections>
<avrgcccpp.linker.miscellaneous.LinkerFlags>-mrelax -nostartfiles -Wl,--section-start=.text=0x7e00 -Wl,--defsym=tsb_app=0 -Wl,--pmem-wrap-around=32k</avrgcccpp.linker.miscellaneous.LinkerFlags>
</AvrGccCpp>
</ToolchainSettings>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<ToolchainSettings>
<AvrGccCpp>
<avrgcc.common.Device>-mmcu=atmega328p</avrgcc.common.Device>
<avrgcc.common.outputfiles.hex>True</avrgcc.common.outputfiles.hex>
<avrgcc.common.outputfiles.lss>True</avrgcc.common.outputfiles.lss>
<avrgcc.common.outputfiles.eep>True</avrgcc.common.outputfiles.eep>
<avrgcc.common.outputfiles.srec>True</avrgcc.common.outputfiles.srec>
<avrgcc.common.outputfiles.usersignatures>False</avrgcc.common.outputfiles.usersignatures>
<avrgcccpp.compiler.symbols.DefSymbols>
<ListValues>
<Value>DEBUG</Value>
</ListValues>
</avrgcccpp.compiler.symbols.DefSymbols>
<avrgcccpp.compiler.directories.IncludePaths>
<ListValues>
<Value>$(MSBuildProjectDirectory)\..\..\libavr\include</Value>
</ListValues>
</avrgcccpp.compiler.directories.IncludePaths>
<avrgcccpp.compiler.optimization.level>Optimize for size (-Os)</avrgcccpp.compiler.optimization.level>
<avrgcccpp.compiler.optimization.PrepareFunctionsForGarbageCollection>True</avrgcccpp.compiler.optimization.PrepareFunctionsForGarbageCollection>
<avrgcccpp.compiler.optimization.PrepareDataForGarbageCollection>True</avrgcccpp.compiler.optimization.PrepareDataForGarbageCollection>
<avrgcccpp.compiler.warnings.AllWarnings>True</avrgcccpp.compiler.warnings.AllWarnings>
<avrgcccpp.compiler.miscellaneous.OtherFlags>-std=c++26 -Wextra -Werror -mrelax -fno-exceptions -fno-rtti -fno-threadsafe-statics -gdwarf-4</avrgcccpp.compiler.miscellaneous.OtherFlags>
<avrgcccpp.linker.optimization.GarbageCollectUnusedSections>True</avrgcccpp.linker.optimization.GarbageCollectUnusedSections>
<avrgcccpp.linker.miscellaneous.LinkerFlags>-mrelax -nostartfiles -Wl,--section-start=.text=0x7e00 -Wl,--defsym=tsb_app=0 -Wl,--pmem-wrap-around=32k</avrgcccpp.linker.miscellaneous.LinkerFlags>
</AvrGccCpp>
</ToolchainSettings>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\..\tsb\tsb_asm.cpp">
<SubType>compile</SubType>
<Link>tsb\tsb_asm.cpp</Link>
</Compile>
</ItemGroup>
<ItemGroup>
<Folder Include="tsb" />
</ItemGroup>
<Import Project="$(AVRSTUDIO_EXE_PATH)\Vs\Compiler.targets" />
</Project>
Submodule libavr deleted from 73fde9c243.
+8 -20
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 fixed-baud, hardware-UART variant is the one whose feature set the port must The user chose the fixed-baud, hardware-UART variant deliberately: it is the one
match. It fits the **complete** TSB feature set into the 512-byte ATmega boot whose feature set the port must match. It fits the **complete** TSB feature set
section: into the 512-byte ATmega boot section:
| Feature | Oracle routine | | Feature | Oracle routine |
|---|---| |---|---|
@@ -38,23 +38,11 @@ avra -I /usr/share/avra tsb-fixedbaud.asm # after uncommenting .include "m328P
``` ```
**500 bytes with every feature** — the proof that ≤512 B and full feature parity **500 bytes with every feature** — the proof that ≤512 B and full feature parity
are simultaneously reachable. The port's four tiers reach it from the other are simultaneously reachable. The port's `tsb_asm` tier meets the same bar at
side, and the gradient between them is the cost of the mechanisms each is 510 B in the same 512 B section, written in C++ on libavr except the two
allowed: routines whose remaining cost is the calling convention itself (the bounded rx
and the page-store loop); `tsb_tricks` needs no assembly at all at 526 B, and
| tier | bytes | section | what it is allowed | `tsb_pure` stays fully idiomatic at 836 B, both in the 1 KB section.
|---|---|---|---|
| oracle | 500 | 512 B | hand-written assembly, the reference |
| `tsb_asm` | 512 | 512 B | C++ on libavr, two routines in asm |
| `tsb_tricks` | 528 | 1 KB | no asm; global register variables |
| `tsb_policy` | 630 | 1 KB | pureboot's rules: no asm, no register variables |
| `tsb_pure` | 776 | 1 KB | idiomatic libavr throughout |
The two routines `tsb_asm` keeps are the ones whose remaining cost is the
calling convention itself: the bounded rx and the page-store loop. It fills
its section exactly, with the same one-bit-time turn-around guard the oracle
spends six bytes on - every tier implements the whole feature set, which is
what makes the column a gradient rather than four different loaders.
The oracle targets 20 MHz / 33333 baud; the port targets 16 MHz / 115200 baud The oracle targets 20 MHz / 33333 baud; the port targets 16 MHz / 115200 baud
(what the simavr protocol test drives). Baud and geometry differ, code size and (what the simavr protocol test drives). Baud and geometry differ, code size and
-3
View File
@@ -4,9 +4,6 @@ if(NOT _res EQUAL 0)
endif() endif()
# avr-size line 2 is "<text> <data> <bss> <dec> <hex> <file>". # avr-size line 2 is "<text> <data> <bss> <dec> <hex> <file>".
string(REGEX MATCH "\n[ \t]*([0-9]+)" _m "${_out}") string(REGEX MATCH "\n[ \t]*([0-9]+)" _m "${_out}")
if(NOT _m)
message(FATAL_ERROR "could not read a .text size out of ${SIZE_TOOL}'s output for ${ELF}:\n${_out}")
endif()
set(_text ${CMAKE_MATCH_1}) set(_text ${CMAKE_MATCH_1})
if(_text GREATER LIMIT) if(_text GREATER LIMIT)
message(FATAL_ERROR ".text is ${_text} bytes, over the ${LIMIT}-byte boot section") message(FATAL_ERROR ".text is ${_text} bytes, over the ${LIMIT}-byte boot section")
+109
View File
@@ -0,0 +1,109 @@
// 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
// 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
// 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 <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "avr_uart.h"
#include "sim_avr.h"
#include "sim_elf.h"
#include "uart_pty.h"
static avr_t *avr;
static uart_pty_t uart_pty;
static const char *dump_path;
static void finish(int sig)
{
(void)sig;
if (dump_path) {
FILE *f = fopen(dump_path, "wb");
if (f) {
fwrite(avr->flash, 1, avr->flashend + 1, f);
fclose(f);
}
}
uart_pty_stop(&uart_pty);
_exit(0);
}
int main(int argc, char *argv[])
{
if (argc < 3) {
fprintf(stderr, "usage: %s <tsb.elf> <boot_base_hex> [flash_dump.bin]\n", argv[0]);
return 2;
}
uint32_t boot_base = (uint32_t)strtoul(argv[2], NULL, 0);
dump_path = argc >= 4 ? argv[3] : NULL;
avr = avr_make_mcu_by_name("atmega328p");
if (!avr) {
fprintf(stderr, "device: no ATmega328P core\n");
return 1;
}
avr_init(avr);
avr->frequency = 16000000;
// Real flash powers up erased (0xff); the app region must look erased
// before the bootloader programs it.
memset(avr->flash, 0xff, avr->flashend + 1);
// 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
// section base ourselves and enter there (BOOTRST is not modelled).
elf_firmware_t fw = {0};
if (elf_read_firmware(argv[1], &fw) != 0) {
fprintf(stderr, "device: cannot read %s\n", argv[1]);
return 1;
}
memcpy(avr->flash + boot_base, fw.flash, fw.flashsize);
avr->pc = boot_base;
avr->codeend = avr->flashend;
// Optional: seed the config page (one page below the boot section) with a
// hex byte string, so the password gate and emergency erase can be tested.
// Layout: [appjump lo][appjump hi][timeout][password...][0xff].
const char *cfg = getenv("TSB_CONFIG");
if (cfg) {
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) {
char b[3] = {cfg[i], cfg[i + 1], 0};
avr->flash[app_end + i / 2] = (uint8_t)strtoul(b, NULL, 16);
}
}
// POLL_SLEEP makes simavr usleep(1) on every status-register read while the
// UART is idle — a host-CPU-saving hack that models no hardware and paces a
// tight-polling loader (one that releases TX between bytes, as one-wire does)
// in real time, distorting protocol timing. Clear it so the loader runs at
// true cycle speed.
uint32_t uflags = 0;
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS('0'), &uflags);
uflags &= ~AVR_UART_FLAG_POLL_SLEEP;
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS('0'), &uflags);
uart_pty_init(avr, &uart_pty);
uart_pty_connect(&uart_pty, '0');
printf("TSB_PTY %s\n", uart_pty.pty.slavename);
fflush(stdout);
signal(SIGTERM, finish);
signal(SIGINT, finish);
for (;;) {
int state = avr_run(avr);
if (state == cpu_Done || state == cpu_Crashed)
break;
}
finish(0);
return 0;
}
-119
View File
@@ -1,119 +0,0 @@
// simavr "device" for the TSB bootloader: load the boot-linked ELF into the
// 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
// 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.
#include <array>
#include <csignal>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <print>
#include <unistd.h>
// The parts headers (uart_pty.h) carry no C++ linkage guards of their own,
// unlike simavr's core headers - the block covers both harmlessly.
extern "C" {
#include "avr_uart.h"
#include "sim_avr.h"
#include "sim_elf.h"
#include "uart_pty.h"
}
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;
uart_pty_t uart_pty;
[[noreturn]] void finish(int)
{
uart_pty_stop(&uart_pty);
_exit(0);
}
} // namespace
int main(int argc, char *argv[])
{
if (argc != 3) {
std::println(stderr, "usage: {} <tsb.elf> <boot_base_hex>", argv[0]);
return 2;
}
const auto boot_base = static_cast<std::uint32_t>(std::strtoul(argv[2], nullptr, 0));
avr = avr_make_mcu_by_name("atmega328p");
if (!avr) {
std::println(stderr, "device: no ATmega328P core");
return 1;
}
avr_init(avr);
avr->frequency = clock_hz;
// Real flash powers up erased (0xff); the app region must look erased
// before the bootloader programs it.
std::memset(avr->flash, 0xff, avr->flashend + 1);
// 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, so it is placed at the
// boot section base by hand.
elf_firmware_t fw{};
if (elf_read_firmware(argv[1], &fw) != 0) {
std::println(stderr, "device: cannot read {}", argv[1]);
return 1;
}
// An image that runs past flash end cannot execute on hardware, and a
// naive copy of it would smash the heap beyond avr->flash - after which
// the simulation misbehaves in ways that point everywhere but here.
// Refuse it loudly instead.
if (boot_base + fw.flashsize > avr->flashend + 1) {
std::println(stderr, "device: {} B at {:#x} runs past flash end {:#x} - image does not fit its slot",
fw.flashsize, boot_base, avr->flashend);
return 1;
}
std::memcpy(avr->flash + boot_base, fw.flash, fw.flashsize);
avr->pc = boot_base;
avr->codeend = avr->flashend;
// Optional: seed the config page with a hex byte string, so the password
// gate and emergency erase can be tested.
// Layout: [appjump lo][appjump hi][timeout][password...][0xff].
if (const char *cfg = std::getenv("TSB_CONFIG")) {
const auto app_end = boot_base - page_bytes;
for (int i = 0; cfg[i] && cfg[i + 1]; i += 2) {
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));
}
}
// POLL_SLEEP makes simavr usleep(1) on every status-register read while the
// UART is idle - a host-CPU-saving hack that models no hardware and paces a
// tight-polling loader (one that releases TX between bytes, as one-wire does)
// in real time, distorting protocol timing. Clear it so the loader runs at
// true cycle speed.
std::uint32_t uflags = 0;
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS('0'), &uflags);
uflags &= ~AVR_UART_FLAG_POLL_SLEEP;
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS('0'), &uflags);
uart_pty_init(avr, &uart_pty);
uart_pty_connect(&uart_pty, '0');
std::println("TSB_PTY {}", uart_pty.pty.slavename);
std::fflush(stdout);
std::signal(SIGTERM, finish);
std::signal(SIGINT, finish);
for (;;) {
const auto state = avr_run(avr);
if (state == cpu_Done || state == cpu_Crashed) {
break;
}
}
finish(0);
}
+5 -27
View File
@@ -23,13 +23,14 @@ 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, config=None): def __init__(self, binary, elf, boot_base, dump="/tmp/tsb_dump.bin", 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], [binary, elf, boot_base, dump],
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:
@@ -143,7 +144,7 @@ class Host:
self._expect(CONFIRM, "C end") self._expect(CONFIRM, "C end")
return echo return echo
# Activation when the config page carries a password: 3x'@' then the # Activation when the config page carries a password: 3×'@' then the
# password bytes, then the info block + mainloop '!'. # password bytes, then the info block + mainloop '!'.
def activate_password(self, password): def activate_password(self, password):
self.s.reset_input_buffer() self.s.reset_input_buffer()
@@ -165,19 +166,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
# 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
# reaching the erase would let a guess wipe the part.
def refuse_password(self, byte):
self.s.reset_input_buffer()
self.s.write(bytes([KNOCK, KNOCK, KNOCK, byte]))
return self.s.read(1)
def say(self, byte):
self.s.write(bytes([byte]))
return self.s.read(1)
def check(cond, msg): def check(cond, msg):
if not cond: if not cond:
@@ -193,7 +181,7 @@ PW_BYTES = bytes([0x50, 0x57])
def scenario_roundtrip(host): def scenario_roundtrip(host):
"""Activation + info block + flash/EEPROM/config read-write round-trips, on """Activation + info block + flash/EEPROM/config read-write round-trips, on
a device with a blank (erased) config page - the usual no-password case.""" a device with a blank (erased) config page — the usual no-password case."""
info = host.activate() info = host.activate()
check(info[0:3] == b"TSB", f"magic 'TSB' (got {info[0:3]!r})") check(info[0:3] == b"TSB", f"magic 'TSB' (got {info[0:3]!r})")
check(info[6:9] == bytes([0x1E, 0x95, 0x0F]), f"signature 1E 95 0F (got {info[6:9].hex()})") check(info[6:9] == bytes([0x1E, 0x95, 0x0F]), f"signature 1E 95 0F (got {info[6:9].hex()})")
@@ -231,15 +219,6 @@ def scenario_emergency(host):
check(host.read_eeprom(1) == b"\xff" * PAGE, "EEPROM wiped") check(host.read_eeprom(1) == b"\xff" * PAGE, "EEPROM wiped")
def scenario_wrong_password(host):
"""A wrong password byte neither activates the loader nor opens the
emergency erase behind it - the oracle carries a dedicated fix for the
second."""
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(CONFIRM) == b"", "and neither does a confirm")
def main(): def main():
binary, elf, boot_base = sys.argv[1], sys.argv[2], sys.argv[3] binary, elf, boot_base = sys.argv[1], sys.argv[2], sys.argv[3]
failures = [] failures = []
@@ -250,7 +229,6 @@ def main():
("round-trip", None, scenario_roundtrip), ("round-trip", None, scenario_roundtrip),
("password activation", PW_CONFIG, scenario_password), ("password activation", PW_CONFIG, scenario_password),
("emergency erase", PW_CONFIG, scenario_emergency), ("emergency erase", PW_CONFIG, scenario_emergency),
("wrong password", PW_CONFIG, scenario_wrong_password),
] ]
for name, config, fn in groups: for name, config, fn in groups:
print(f"--- {name} ---") print(f"--- {name} ---")
-60
View File
@@ -1,60 +0,0 @@
#!/bin/bash
# The port's gate: the generated workflow - build, size tests, and the
# simulator-driven protocol suite. --full adds the reflect build, which
# compiles the same TUs through libavr's other producer. libavr resolves from
# the `libavr/` submodule; LIBAVR_ROOT overrides it for a working tree.
set -e
cd "$(dirname "$0")/.."
full=0
[[ "$1" == "--full" ]] && { full=1; shift; }
# The chip lists come from the presets rather than being spelled a second time
# here: a chip added to make_presets.py and missed in a copy of its list would
# be a gate that silently never builds it, which is the one failure mode a gate
# cannot report. tools/make_presets.py is the single source, CMakePresets.json
# is its output, and this reads that.
readarray -t WORKFLOWS < <(python3 -c '
import json, sys
presets = json.load(open("CMakePresets.json"))["workflowPresets"]
print("\n".join(p["name"] for p in presets))')
if ((${#WORKFLOWS[@]} == 0)); then
echo "no workflow presets in CMakePresets.json - run tools/make_presets.py" >&2
exit 1
fi
CHIPS=()
REFLECT_SPOT=()
for workflow in "${WORKFLOWS[@]}"; do
case $workflow in
*-generated) CHIPS+=("${workflow%-generated}") ;;
*-reflect) REFLECT_SPOT+=("${workflow%-reflect}") ;;
esac
done
# 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
run_preset "$chip-generated" "$@"
done
if ((full)); then
for chip in "${REFLECT_SPOT[@]}"; do
run_preset "$chip-reflect" "$@"
done
fi
if ((${#red[@]})); then
printf '==== red presets ====\n' >&2
printf ' %s\n' "${red[@]}" >&2
exit 1
fi
echo "check: every chip green"
-90
View File
@@ -1,90 +0,0 @@
#!/usr/bin/env python3
"""Regenerate CMakePresets.json - one uniform pipeline per chip.
The tiers reimplement the ATmega328P-only reference protocol, so that is the
whole chip list. It stays a generated file rather than a hand-written one
because the shape - configure, build, test, workflow, and a reflect pair
without tests - is the shape a second chip would need too.
Run from the repo root: tools/make_presets.py - or with --check, which
verifies the committed file matches this generator and edits nothing (the
ctest entry `presets.generated` runs that, so drift reds the gate).
"""
import json
import os
import sys
CHIPS = [
"atmega328p",
]
# The reflect pair: the same chip, built in libavr's other mode.
REFLECT_SPOT = [
"atmega328p",
]
def main():
configure = [{
"name": "base",
"hidden": True,
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/${presetName}",
"toolchainFile": "${sourceDir}/libavr/cmake/avr-toolchain.cmake",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
"CMAKE_COLOR_DIAGNOSTICS": "ON",
},
}]
build, test, workflows = [], [], []
def add(chip, mode):
name = f"{chip}-{mode}"
configure.append({
"name": name,
"inherits": "base",
"cacheVariables": {
"LIBAVR_MCU": chip,
"LIBAVR_REFLECT": "ON" if mode == "reflect" else "OFF",
},
})
build.append({"name": name, "configurePreset": name})
steps = [{"type": "configure", "name": name}, {"type": "build", "name": name}]
if mode == "generated":
test.append({"name": name, "configurePreset": name, "output": {"outputOnFailure": True}})
steps.append({"type": "test", "name": name})
workflows.append({"name": name, "steps": steps})
for chip in CHIPS:
add(chip, "generated")
for chip in REFLECT_SPOT:
add(chip, "reflect")
# CMake rejects unknown fields in the presets root, $comment included, so
# the file cannot carry a generated-file marker; the --check ctest is the
# whole of rule 10's guard here.
presets = {
"version": 8,
"configurePresets": configure,
"buildPresets": build,
"testPresets": test,
"workflowPresets": workflows,
}
rendered = json.dumps(presets, indent=1) + "\n"
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "CMakePresets.json")
if "--check" in sys.argv[1:]:
current = open(path, encoding="utf-8").read() if os.path.exists(path) else ""
if current != rendered:
print("CMakePresets.json does not match its generator - run tools/make_presets.py")
return 1
return 0
with open(path, "w", encoding="utf-8", newline="\n") as f:
f.write(rendered)
print(f"{len(CHIPS)} chips, {len(REFLECT_SPOT)} reflect: {os.path.normpath(path)}")
return 0
if __name__ == "__main__":
sys.exit(main())
-106
View File
@@ -1,106 +0,0 @@
// 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
+120 -83
View File
@@ -1,15 +1,14 @@
// TinySafeBoot on libavr - tier 3: full feature parity in the 512-byte boot // TinySafeBoot on libavr — tier 3: full feature parity in the 512-byte boot
// section, in C++ except where the C ABI itself is the cost. // section, in C++ except where the C ABI itself is the cost.
// //
// The complete TinySafeBoot feature set - watchdog-reset bail, one-wire // The complete TinySafeBoot feature set — watchdog-reset bail, one-wire
// half-duplex UART, a config-page activation timeout, the password gate, // half-duplex UART, a config-page activation timeout, the password gate,
// emergency erase, and config/flash/EEPROM read-write - inside the 512-byte // emergency erase, and config/flash/EEPROM read-write — at 510 bytes in the
// BOOTSZ=11 section the hand-written oracle occupies (oracle/README.md holds // 512-byte BOOTSZ=11 section the hand-written oracle occupies (500 B). This tier used to be one
// what each tier measures, in one table rather than four). The // monolithic inline-asm routine; it is now the tricks tier's C++ (same
// body is the tricks tier's C++ (same register protocol, same structure - see // register protocol, same structure — see tsb_tricks.cpp, including the
// tsb_tricks.cpp, including the global-register miscompile rules) with exactly // global-register miscompile rules) with exactly two routines kept in
// two routines kept in assembly, the two whose remaining cost *is* the calling // assembly, the two whose remaining cost *is* the calling convention:
// convention:
// //
// rx the bounded receive: C++ must re-floor the timeout window on every // rx the bounded receive: C++ must re-floor the timeout window on every
// call (the global-register-store miscompile) and split it across // call (the global-register-store miscompile) and split it across
@@ -17,12 +16,12 @@
// countdown. // countdown.
// store the page-store loop: C++ cannot hold the receive byte pair and the // store the page-store loop: C++ cannot hold the receive byte pair and the
// walked Z pointer across the rx calls without call-saved staging // walked Z pointer across the rx calls without call-saved staging
// (push/pop + a Y->Z copy per word); the asm calls rx knowing exactly // (push/pop + a Y→Z copy per word); the asm calls rx knowing exactly
// which registers it touches and walks Z live across the whole page. // which registers it touches and walks Z live across the whole page.
// //
// Everything else - bring-up, activation, password gate, emergency erase, // Everything else — bring-up, activation, password gate, emergency erase,
// dispatch, every SPM/EEPROM/flash primitive, every geometry/baud/info // dispatch, every SPM/EEPROM/flash primitive, every geometry/baud/info
// constant - is C++ on libavr, and the two asm routines splice into the same // constant — is C++ on libavr, and the two asm routines splice into the same
// global-register protocol the C++ uses (g_addr in Y, g_cnt in r16, g_window // global-register protocol the C++ uses (g_addr in Y, g_cnt in r16, g_window
// in r7, g_receiving in r6), so calls cross the boundary with no marshalling. // in r7, g_receiving in r6), so calls cross the boundary with no marshalling.
// //
@@ -30,36 +29,78 @@
// 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 "tsb.hpp" #include <libavr/libavr.hpp>
#include <avr/io.h> // SPMCSR's I/O address for the page-store routine #include <avr/io.h> // SP / RAMEND for the crt-free boot entry, SFR addresses for the asm routines
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 boot section: 512 B, BOOTSZ=11. // The loader is purely polled — it never enables interrupts — so every SPM and
constexpr std::uint16_t boot_bytes = 512; // EEPROM lock folds to nothing under this posture.
constexpr std::uint16_t app_end = config_page(boot_bytes); constexpr auto off = avr::irq::guard_policy::unused;
[[gnu::progmem]] constexpr auto info = info_block(date_stamp(26, 7, 20), app_end); 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 app_end = spm::flash_bytes - boot_bytes - page;
constexpr std::uint16_t eeprom_end = avr::hw::db.mem.eeprom_size - 1;
// Lockout-proof floor for the activation window (the oracle's F_CPU/1MHz).
constexpr std::uint8_t act_min = 16;
// 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::detail::solve_baud(16_MHz, 115200_Bd);
// The 16-byte device-info block, streamed out on activation.
// clang-format off
[[gnu::progmem]] constexpr std::uint8_t info[16] = {
'T', 'S', 'B',
build_date & 0xFF, build_date >> 8,
0xF3, // status: native-UART fixed-baud lineage
0x1E, 0x95, 0x0F, // ATmega328P signature
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
// - the property the store's word loop rides on. // — the property the store's word loop rides on.
[[gnu::noinline, gnu::noclone]] std::uint8_t rx() [[gnu::noinline, gnu::noclone]] std::uint8_t rx()
{ {
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], %[rxen] \n\t" // the receiver alone: release the line and listen " ldi %[b], 0x10 \n\t" // RXEN0 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"
@@ -68,20 +109,19 @@ register std::uint8_t g_receiving asm("r6");
" 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], %[rxc] \n\t" " sbrc %[b], 7 \n\t" // RXC0
" rjmp 4f \n\t" " rjmp 4f \n\t"
" dec %[b] \n\t" " dec %[b] \n\t"
" brne 3b \n\t" " brne 3b \n\t"
" sbiw r26, 1 \n\t" " sbiw r26, 1 \n\t"
" brcc 2b \n\t" " brcc 2b \n\t"
" clr %[b] \n\t" // silence -> 0, which no compare accepts " clr %[b] \n\t" // silence → 0, which no compare accepts
" rjmp 5f \n\t" " rjmp 5f \n\t"
"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), [rxen] "M"(hw::ucsr0b::rxen0(1).value), : [to] "r"(g_window), [actmin] "M"(act_min), [ucsr0a] "n"(_SFR_MEM_ADDR(UCSR0A)),
[rxc] "I"(std::countr_zero(hw::ucsr0a::rxc0.mask)), [ucsr0a] "n"(hw::ucsr0a::addr), [ucsr0b] "n"(_SFR_MEM_ADDR(UCSR0B)), [udr0] "n"(_SFR_MEM_ADDR(UDR0))
[ucsr0b] "n"(hw::ucsr0b::addr), [udr0] "n"(hw::udr0::addr)
: "r26", "r27", "cc"); : "r26", "r27", "cc");
return byte; return byte;
} }
@@ -89,13 +129,14 @@ register std::uint8_t g_receiving asm("r6");
// One-wire transmit: take the line (TXEN0 alone) on a direction change with a // One-wire transmit: take the line (TXEN0 alone) on a direction change with a
// turn-around guard, put the byte out, hold the line until the whole frame is // turn-around guard, put the byte out, hold the line until the whole frame is
// out (TXC0, not UDRE0), W1C TXC0 by storing the sampled status back (keeps // out (TXC0, not UDRE0), W1C TXC0 by storing the sampled status back (keeps
// U2X0). Plain C++ - it compiles *smaller* than the oracle's routine. // U2X0). Plain C++ — it compiles *smaller* than the oracle's routine.
[[gnu::noinline, gnu::noclone]] void tx(std::uint8_t byte) [[gnu::noinline, gnu::noclone]] void tx(std::uint8_t byte)
{ {
if (g_receiving) { if (g_receiving) {
g_receiving = 0; g_receiving = 0;
hw::ucsr0b::write(hw::ucsr0b::txen0(1)); hw::ucsr0b::write(hw::ucsr0b::txen0(1));
avr::delay::cycles<guard_cycles>(); for (std::uint8_t guard = 46; guard; --guard)
;
} }
hw::udr0::write(byte); hw::udr0::write(byte);
std::uint8_t status; std::uint8_t status;
@@ -112,27 +153,27 @@ register std::uint8_t g_receiving asm("r6");
return rx(); return rx();
} }
// One flash byte <- [g_addr++] (the advance right before ret - the // One flash byte ← [g_addr++] (the advance right before ret — the
// global-register rule, see tsb_tricks.cpp). // global-register rule, see tsb_tricks.cpp).
[[gnu::noinline, gnu::noclone]] std::uint8_t sflash() [[gnu::noinline, gnu::noclone]] std::uint8_t sflash()
{ {
const auto byte = avr::flash_load(flash_ptr(g_addr)); std::uint8_t byte = avr::flash_load(flash_ptr(g_addr));
++g_addr; ++g_addr;
return byte; return byte;
} }
// 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()
{ {
const auto byte = ee::read<no_spm>(g_addr); std::uint8_t byte = ee::read(g_addr);
++g_addr; ++g_addr;
return byte; return byte;
} }
// One EEPROM byte -> [g_addr++]. // One EEPROM byte → [g_addr++].
[[gnu::noinline, gnu::noclone]] void eewr(std::uint8_t byte) [[gnu::noinline, gnu::noclone]] void eewr(std::uint8_t byte)
{ {
ee::write<off, no_spm>(g_addr, byte); ee::write<off>(g_addr, byte);
++g_addr; ++g_addr;
} }
@@ -144,7 +185,7 @@ register std::uint8_t g_receiving asm("r6");
} while (--g_cnt); } while (--g_cnt);
} }
// Wait out a running SPM op, then re-open the RWW section - after every page // Wait out a running SPM op, then re-open the RWW section — after every page
// op and before handing over, as the oracle does. // op and before handing over, as the oracle does.
[[gnu::noinline, gnu::noclone]] void settle() [[gnu::noinline, gnu::noclone]] void settle()
{ {
@@ -160,12 +201,12 @@ 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()
{ {
g_addr -= page; g_addr -= page;
spm::command<off>(spm::op::erase, g_addr); spm::erase_page<off>(g_addr);
settle(); settle();
} }
@@ -180,7 +221,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()
@@ -191,7 +232,7 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
" 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, %[fill] \n\t" // buffer the word at Z " ldi r24, 0x01 \n\t" // SPMEN: 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"
@@ -201,13 +242,12 @@ 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, %[write] \n\t" // program the page " ldi r24, 0x05 \n\t" // PGWRT | SPMEN: 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)), [fill] "M"(hw::spmcsr::spmen(1).value), : [rx] "i"(&rx), [spmcsr] "I"(_SFR_IO_ADDR(SPMCSR)), [words] "M"(page / 2), [pagelo] "M"(page & 0xff),
[write] "M"(hw::spmcsr::pgwrt(1).value | hw::spmcsr::spmen(1).value), [words] "M"(page / 2), [pagehi] "M"(page >> 8)
[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();
} }
@@ -216,15 +256,14 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
{ {
// A watchdog reset hands straight back to the application, as the // A watchdog reset hands straight back to the application, as the
// reference loader does, rather than re-entering the bootloader. // reference loader does, rather than re-entering the bootloader.
if (hw::mcusr::wdrf.test()) { if (hw::mcusr::wdrf.test())
appjump(); appjump();
}
// 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 rx()/tx() raise RXEN0/TXEN0 on first use - only the divisor low // 0, and rx()/tx() raise RXEN0/TXEN0 on first use — only the divisor low
// byte and U2X0 need a store. The library still does the datasheet work. // byte and U2X0 need a store. The library still does the datasheet work.
static_assert(baud.u2x && baud.ubrr < 256, "lean bring-up writes UBRR0L only, with U2X0"); static_assert(baud.u2x && baud.ubrr < 256, "lean bring-up writes UBRR0L only, with U2X0");
hw::ubrr0::write(static_cast<std::uint8_t>(baud.ubrr)); hw::reg<"UBRR0">::write(static_cast<std::uint8_t>(baud.ubrr));
hw::ucsr0a::write(hw::ucsr0a::u2x0(1)); hw::ucsr0a::write(hw::ucsr0a::u2x0(1));
// General-purpose registers are undefined at power-on (no crt zeroes them); // General-purpose registers are undefined at power-on (no crt zeroes them);
// the direction latch must start "not receiving" so the first rx() enables // the direction latch must start "not receiving" so the first rx() enables
@@ -232,41 +271,37 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
// same reason. // same reason.
g_receiving = 0; g_receiving = 0;
// Activation: activation_knocks '@', each inside the config page's timeout // Activation: 3×'@', each inside the config page's timeout window (rx
// window (rx floors it so a corrupt page cannot lock the loader out); // floors it so a corrupt page cannot lock the loader out); anything else —
// anything else - including silence - hands over. // including silence — hands over.
g_window = avr::flash_load(flash_ptr(app_end + timeout_at)); g_window = avr::flash_load(flash_ptr(app_end + 2));
for (std::uint8_t k = activation_knocks; k; --k) { for (std::uint8_t k = 3; k; --k)
if (rx() != knock) { if (rx() != knock)
appjump(); appjump();
}
}
g_window = comm_window; g_window = comm_window;
// Password gate (config page from password_at, 0xff-terminated; a blank // Password gate (config page from app_end+3, 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 + password_at; g_addr = app_end + 3;
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;
++g_addr; ++g_addr;
if (expected == 0xff) { if (expected == 0xff) {
g_addr = reinterpret_cast<std::uint16_t>(info.data()); g_addr = reinterpret_cast<std::uint16_t>(&info[0]);
g_cnt = sizeof(info); g_cnt = sizeof(info);
sendf(); sendf();
break; break;
} }
const auto got = rx(); std::uint8_t got = rx();
if (got == 0) { if (got == 0) {
if (mask == 0) { if (mask == 0)
continue; continue;
} if (rcnf() != confirm || rcnf() != confirm)
if (rcnf() != confirm || rcnf() != confirm) {
appjump(); appjump();
}
erase_application(); // leaves g_addr = 0 for the EEPROM walk erase_application(); // leaves g_addr = 0 for the EEPROM walk
do { do {
eewr(0xff); eewr(0xff);
@@ -275,45 +310,40 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
erase_below(); erase_below();
break; break;
} }
if (got != expected) { if (got != expected)
mask = 0; mask = 0;
} }
}
for (;;) { for (;;) {
tx(confirm); // Mainloop ready tx(confirm); // Mainloop ready
g_addr = 0; g_addr = 0;
switch (rx()) { switch (rx()) {
case command::read_flash: // one page per host '!' case 'f': // read application flash, one page per host '!'
for (;;) { for (;;) {
if (rx() != confirm) { if (rx() != confirm)
break; break;
}
g_cnt = page; g_cnt = page;
sendf(); sendf();
if (g_addr >= app_end) { if (g_addr >= app_end)
break; break;
} }
}
break; break;
case command::write_flash: // erase the application, then take pages behind '?' case 'F': // 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 command::read_eeprom: // one page per host '!', until the host stops case 'e': // read EEPROM, one page per host '!', until the host stops
for (;;) { for (;;) {
if (rx() != confirm) { if (rx() != confirm)
break; break;
}
g_cnt = page; g_cnt = page;
do { do {
tx(eerd()); tx(eerd());
} while (--g_cnt); } while (--g_cnt);
} }
break; break;
case command::write_eeprom: // pages behind '?' case 'E': // take EEPROM pages behind '?'
while (rcnf() == confirm) { while (rcnf() == confirm) {
g_cnt = page; g_cnt = page;
do { do {
@@ -321,16 +351,15 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
} while (--g_cnt); } while (--g_cnt);
} }
break; break;
case command::read_config: case 'c': // read the config page
read_config: read_config:
g_addr = app_end; g_addr = app_end;
g_cnt = page; g_cnt = page;
sendf(); sendf();
break; break;
case command::write_config: // then echo it back to verify case 'C': // replace the config page, then echo it back to verify
if (rcnf() != confirm) { if (rcnf() != confirm)
break; break;
}
g_addr = app_end + page; g_addr = app_end + page;
erase_below(); // leaves g_addr = app_end, the store target erase_below(); // leaves g_addr = app_end, the store target
store_flash(); store_flash();
@@ -344,6 +373,14 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
} // namespace } // namespace
} // namespace tsb } // namespace tsb
// Reset lands at the boot section base (BOOTRST): the entry stub in .vectors // Reset lands here: BOOTRST vectors to the boot section base and .vectors is
// is laid first and does the one line of crt a crt-less image needs. // laid first, so this is the first instruction executed. No crt ran, so set
template struct avr::startup::entry<tsb::run, avr::startup::stack::hardware>; // the stack pointer before anything is called.
extern "C" [[gnu::naked, gnu::used, gnu::section(".vectors")]] void __boot_entry()
{
SP = RAMEND;
// The one line of crt this loader needs: compiled code assumes
// __zero_reg__ (r1) is 0, and power-on registers are undefined.
asm volatile("clr __zero_reg__");
tsb::run();
}
-274
View File
@@ -1,274 +0,0 @@
// TinySafeBoot on libavr - the policy floor: pureboot's rules, measured.
//
// The full TinySafeBoot feature set - watchdog bail, one-wire half-duplex,
// config-page activation timeout, password gate, emergency erase, and
// config/flash/EEPROM read-write - with no assembly and no register variables:
// code, attributes and flags only. It applies pureboot's size techniques - the
// library's half-duplex serial and startup entry, lean bring-up from reset
// state, one merged send loop over both memories, oracle-shaped loop bounds,
// locals threaded through noinline primitives, pureboot's codegen flags - and
// lands between the idiomatic tier and the 512 B section the tricks/asm tiers
// reach with the banned mechanisms (oracle/README.md holds all four). The gap
// to 512 is the rent of policy-clean C++: helpers that hold a cursor across
// rx()/tx() pay push/pop and argument threading where a global-register
// protocol pays nothing, and both control-flow merges tried (a parametrized
// paged session, a merged store loop) measured larger than the split cases.
//
// The wire protocol is strict request/response, which is what makes the
// shared line safe: the device drives it only between a received command and
// its reply, and releases it (the library's half-duplex choreography)
// whenever it waits.
#include "tsb.hpp"
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 = tsb::baud_rate,
.allow_baud_error = true,
.half_duplex = true,
};
using serial_t = dev::uart0<link>;
inline constexpr serial_t serial{};
namespace tsb {
namespace {
// The boot section: 1 KB, BOOTSZ=10.
constexpr std::uint16_t boot_bytes = 1024;
constexpr std::uint16_t app_end = config_page(boot_bytes);
[[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;
// 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
// anywhere, and a mid-session cable pull cannot wedge it. The line release on
// a direction change is the serial backend's.
[[gnu::noinline]] std::uint8_t rx()
{
std::uint16_t outer = static_cast<std::uint16_t>(window) << 8;
do {
std::uint8_t fine = 0;
do {
if (auto byte = serial.read()) {
return *byte;
}
} while (--fine);
} while (--outer);
return 0;
}
// One-wire transmit: the backend takes the line with a turn-around guard and
// holds it until the whole frame is out.
[[gnu::noinline]] void tx(std::uint8_t byte)
{
serial.write(byte);
}
// '?', then hand back the host's reply for the callers' one-byte compare.
[[gnu::noinline]] std::uint8_t rcnf()
{
tx(request);
return rx();
}
// The one send loop: the info block, the config page, application flash and
// EEPROM pages all stream through here.
[[gnu::noinline]] void send_block(bool eep, std::uint16_t at, std::uint8_t count)
{
do {
tx(eep ? ee::read<no_spm>(at) : avr::flash_load(flash_ptr(at)));
++at;
} while (--count);
}
// One EEPROM byte in - shared by the emergency wipe and the 'E' stream.
[[gnu::noinline]] void eeput(std::uint16_t at, std::uint8_t value)
{
ee::write<off, no_spm>(at, value);
}
// Wait out a running SPM op, then re-open the RWW section - after every page
// op and before handing over, as the oracle does.
[[gnu::noinline]] void settle()
{
spm::wait();
spm::rww_enable<off>();
}
// One host page straight into the erased flash page at `at` - through the SPM
// word buffer (low byte then high), no SRAM staging - then committed. `at`
// names a page base, so the cursor's low byte reaching the boundary ends the
// walk.
[[gnu::noinline]] void store_flash_page(std::uint16_t at)
{
const auto open = spm::page::begin<spm::from::boot_section, off>(at);
do {
const auto low = rx();
const auto high = rx();
spm::fill<off>(open, at, std::bit_cast<std::uint16_t>(std::array{low, high}));
at += 2;
} while (static_cast<std::uint8_t>(at) & (page - 1));
spm::command<off>(spm::op::write, at - page);
settle();
}
extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --defsym=tsb_app=0
[[noreturn]] void appjump()
{
settle();
tsb_app();
}
// Step one page down and erase it - the erase shared by the whole-app walk,
// the config rewrite and the emergency wipe; hands the stepped address back.
[[gnu::noinline]] std::uint16_t erase_below(std::uint16_t at)
{
at -= page;
spm::command<off>(spm::op::erase, at);
settle();
return at;
}
// Erase the whole application, top-down like the oracle: the loop bound is a
// compare with zero, and the returned 0 is the address every caller wants
// next.
[[gnu::noinline]] std::uint16_t erase_application()
{
std::uint16_t at = app_end;
do {
at = erase_below(at);
} while (at != 0);
return at;
}
[[noreturn]] void run()
{
// A watchdog reset hands straight back to the application, as the
// reference loader does, rather than re-entering the bootloader.
if (avr::hw::mcusr::wdrf.test()) {
appjump();
}
// Lean bring-up from reset state: UCSR0C already reads 8N1, UBRR0H reads
// 0, and the half-duplex write()/read() raise TXEN0/RXEN0 on first use -
// only the divisor low byte and U2X0 need a store. The solver still does
// the datasheet work; the asserts pin the reset-state assumptions.
{
constexpr auto sol = avr::uart::solve_baud(dev::clock, link.baud, link.data_bits, link.par);
static_assert(sol.u2x && sol.ubrr < 256, "lean bring-up writes UBRR0L only, with U2X0");
avr::hw::ubrr0::write(static_cast<std::uint8_t>(sol.ubrr));
avr::hw::ucsr0a::write(avr::hw::ucsr0a::u2x0(1));
}
// 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 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 + password_at;
std::uint8_t mask = 0xff;
for (;;) {
const std::uint8_t expected = avr::flash_load(flash_ptr(at)) & mask;
++at;
if (expected == 0xff) {
send_block(false, reinterpret_cast<std::uint16_t>(info.data()), info.size());
break;
}
const auto got = rx();
if (got == 0) {
if (mask == 0) {
continue;
}
if (rcnf() != confirm || rcnf() != confirm) {
appjump();
}
std::uint16_t a = erase_application();
do {
eeput(a, 0xff);
} while (++a <= eeprom_end);
erase_below(app_end + page);
break;
}
if (got != expected) {
mask = 0;
}
}
for (;;) {
tx(confirm); // Mainloop ready
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;
}
send_block(false, a, page);
}
break;
case command::read_eeprom: // one page per host '!', until the host stops
for (std::uint16_t a = 0;; a += page) {
if (rx() != confirm) {
break;
}
send_block(true, a, page);
}
break;
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 command::write_eeprom: // pages behind '?', each write host-paced
for (std::uint16_t a = 0; rcnf() == confirm;) {
std::uint8_t count = page;
do {
eeput(a, rx());
++a;
} while (--count);
}
break;
case command::read_config:
read_config:
send_block(false, app_end, page);
break;
case command::write_config: // then echo it back to verify
if (rcnf() != confirm) {
break;
}
store_flash_page(erase_below(app_end + page));
goto read_config;
default: // 'q' or any other byte runs the application
appjump();
}
}
}
} // namespace
} // namespace tsb
// Reset lands at the boot section base (BOOTRST): the entry stub in .vectors
// is laid first and does the one line of crt a crt-less image needs.
template struct avr::startup::entry<tsb::run, avr::startup::stack::hardware>;
+119 -114
View File
@@ -1,83 +1,96 @@
// TinySafeBoot on libavr - tier 1: pure, idiomatic C++. // TinySafeBoot on libavr — tier 1: pure, idiomatic C++.
// //
// A serial flash bootloader for the ATmega328P boot section, reimplementing the // A serial flash bootloader for the ATmega328P boot section, reimplementing the
// 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, every SPM/EEPROM lock and // The one-wire wiring, the flash-resident info block and every SPM/EEPROM lock
// the crt-free reset entry are libavr's to handle. // are libavr's to handle; the only attribute is the naked reset entry that
// stands in for the absent C runtime.
#include "tsb.hpp" #include <libavr/libavr.hpp>
using dev = avr::device<{.clock = tsb::clock}>; #include <avr/io.h> // SP / RAMEND for the crt-free boot entry
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 using serial_t = dev::uart0<{.baud = 115200_Bd, .max_baud_error = 3_pct, .half_duplex = true}>;
// 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 = tsb::baud_rate,
.allow_baud_error = true,
.half_duplex = true,
}>;
inline constexpr serial_t serial{}; inline constexpr serial_t serial{};
namespace tsb { namespace tsb {
namespace { namespace {
// The boot section: 1 KB, BOOTSZ=10. // The loader is purely polled — it never enables interrupts — so every SPM and
constexpr std::uint16_t boot_bytes = 1024; // EEPROM lock folds to nothing under this posture.
constexpr std::uint16_t app_end = config_page(boot_bytes); constexpr auto off = avr::irq::guard_policy::unused;
// A flash_table keeps the info block in progmem with no .data image (there is // The handshake bytes, identical across every TSB host.
// no crt to copy one). constexpr std::uint8_t confirm = '!';
inline constexpr auto info_data = info_block(date_stamp(26, 7, 20), app_end); 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 app_end = spm::flash_bytes - boot_bytes - page;
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.
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 std::array<std::uint8_t, 16> info_data = {
'T', 'S', 'B',
build_date & 0xFF, build_date >> 8,
0xF3, // status byte (native-UART fixed-baud lineage)
0x1E, 0x95, 0x0F, // ATmega328P signature
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 receive window, pre-floored where it is set. In .noinit: there is no crt // Blocking byte read/write over the one-wire line: read() releases the line to
// to clear a .bss image, and run() stores it before the first receive. // the receiver, write() takes it and holds it until the frame is out.
[[gnu::section(".noinit")]] std::uint8_t window;
// Bounded byte read over the one-wire line - read() releases the line to the
// receiver - answering 0 on silence. That 0 falls through every compare below:
// not a knock, not a confirm, not a command, so a silent host unwinds the
// loader to the application from anywhere and a mid-session cable pull cannot
// wedge it. The oracle lists that timeout among its own fixes, and a blocking
// read is how a tier loses it.
std::uint8_t rx() std::uint8_t rx()
{ {
std::uint16_t outer = static_cast<std::uint16_t>(window) << 8; return serial.read_blocking();
do {
std::uint8_t fine = 0;
do {
if (auto byte = serial.read()) {
return *byte;
}
} while (--fine);
} while (--outer);
return 0;
} }
// write() takes the line and holds it until the frame is out.
void tx(std::uint8_t byte) 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)
{ {
while (count--) { while (count--)
tx(avr::flash_load(flash_ptr(addr++))); tx(avr::flash_load(flash_ptr(addr++)));
} }
}
void send_eeprom(std::uint16_t addr, std::uint8_t count) void send_eeprom(std::uint16_t addr, std::uint8_t count)
{ {
while (count--) { while (count--)
tx(ee::read<no_spm>(addr++)); tx(ee::read(addr++));
}
} }
// Prompt the host with '?' and report whether it answered '!'. // Prompt the host with '?' and report whether it answered '!'.
@@ -88,33 +101,32 @@ bool request_confirm()
} }
// Stream one page from the host straight into the already-erased flash page at // Stream one page from the host straight into the already-erased flash page at
// `addr`, filling the SPM word buffer low byte then high - no SRAM staging, so // `addr`, filling the SPM word buffer low byte then high — no SRAM staging, so
// receiving and programming are the same loop. // receiving and programming are the same loop.
void store_flash_page(std::uint16_t addr) void store_flash_page(std::uint16_t addr)
{ {
const auto open = spm::page::begin<spm::from::boot_section, off>(addr);
for (std::uint16_t i = 0; i < page; i += 2) { for (std::uint16_t i = 0; i < page; i += 2) {
const auto lo = rx(); std::uint8_t lo = rx();
const auto hi = rx(); std::uint8_t hi = rx();
spm::fill<off>(open, addr + i, static_cast<std::uint16_t>(lo | (hi << 8))); spm::fill<off>(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<off>(addr);
spm::wait();
} }
// Stream one page from the host straight into EEPROM, byte by byte. // Stream one page from the host straight into EEPROM, byte by byte.
void store_eeprom_page(std::uint16_t addr) void store_eeprom_page(std::uint16_t addr)
{ {
for (std::uint16_t i = 0; i < page; ++i) { for (std::uint16_t i = 0; i < page; ++i)
ee::write<off, no_spm>(addr + i, rx()); ee::write<off>(addr + i, rx());
}
} }
// Erase one flash page, waited out by the blocking spelling - the erase step // Erase one flash page and wait it out — the erase step shared by the whole-app
// shared by the whole-app erase, the config-page rewrite and the emergency // erase, the config-page rewrite and the emergency wipe.
// wipe.
void erase_page(std::uint16_t addr) void erase_page(std::uint16_t addr)
{ {
spm::erase_page<spm::from::boot_section, off>(addr); spm::erase_page<off>(addr);
spm::wait();
} }
// Erase the whole application, one page at a time, top-down as the reference // Erase the whole application, one page at a time, top-down as the reference
@@ -140,53 +152,48 @@ extern "C" [[noreturn]] void tsb_app();
tsb_app(); tsb_app();
} }
// Stream the application flash back, one page per host '!'. Self-terminates at // 'f': stream the application flash back, one page per host '!'. Self-terminates
// the application boundary; the host normally stops earlier with a non-'!'. // at 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) {
if (rx() != confirm) { if (rx() != confirm)
return; return;
}
send_flash(a, page); send_flash(a, page);
} }
} }
// Stream EEPROM back, one page per host '!', until the host stops. // 'e': 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) {
if (rx() != confirm) { if (rx() != confirm)
return; return;
}
send_eeprom(a, page); send_eeprom(a, page);
} }
} }
// Erase the whole application first, then take pages the host offers behind // 'F': erase the whole application first, then take pages the host offers
// '?'. // behind '?'.
void write_flash() void write_flash()
{ {
erase_application(); erase_application();
for (std::uint16_t a = 0; request_confirm(); a += page) { for (std::uint16_t a = 0; request_confirm(); a += page)
store_flash_page(a); store_flash_page(a);
} }
}
// Take pages the host offers behind '?' into EEPROM. // 'E': 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)
store_eeprom_page(a); store_eeprom_page(a);
} }
}
// Replace the config page, then echo it back for the host to verify. // 'C': 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())
return; return;
}
erase_page(app_end); erase_page(app_end);
store_flash_page(app_end); store_flash_page(app_end);
spm::rww_enable<off>(); spm::rww_enable<off>();
@@ -199,14 +206,13 @@ void write_config()
void emergency_erase() void emergency_erase()
{ {
erase_application(); erase_application();
for (std::uint16_t a = 0; a <= eeprom_end; ++a) { for (std::uint16_t a = 0; a <= eeprom_end; ++a)
ee::write<off, no_spm>(a, 0xff); ee::write<off>(a, 0xff);
}
erase_page(app_end); erase_page(app_end);
spm::rww_enable<off>(); spm::rww_enable<off>();
} }
// The password gate. The config page holds the password at password_at, // The password gate. The config page holds the password at app_end+3,
// 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.
@@ -214,56 +220,47 @@ enum class gate : std::uint8_t { pass, emergency };
gate password_gate() gate password_gate()
{ {
for (const std::uint8_t *pw = flash_ptr(app_end + password_at);; ++pw) { for (const std::uint8_t *pw = flash_ptr(app_end + 3);; ++pw) {
const auto expected = avr::flash_load(pw); std::uint8_t 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;
} if (got != expected)
if (got != expected) { for (;;)
for (;;) {
rx(); rx();
} }
} }
}
}
[[noreturn]] void run() [[noreturn]] void run()
{ {
// A watchdog reset hands straight back to the application, as the reference // A watchdog reset hands straight back to the application, as the reference
// loader does, rather than re-entering the bootloader. // loader does, rather than re-entering the bootloader.
if (avr::hw::mcusr::wdrf.test()) { if (avr::hw::mcusr::wdrf.test())
appjump(); appjump();
}
avr::init<serial_t>(); avr::init<serial_t>();
// Activation: the host knocks activation_knocks times inside a window whose length is the // Activation: the host knocks three '@' 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.
// window then bounds every receive of the session. __uint24 idle = static_cast<__uint24>(avr::flash_load(flash_ptr(app_end + 2)) | 16) << 16;
window = avr::flash_load(flash_ptr(app_end + timeout_at)) | act_min;
__uint24 idle = static_cast<__uint24>(window) << 16;
std::uint8_t knocks = 0; std::uint8_t knocks = 0;
while (knocks < activation_knocks) { while (knocks < 3) {
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)
appjump(); appjump();
} }
}
switch (password_gate()) { switch (password_gate()) {
case gate::pass: case gate::pass:
send_flash(reinterpret_cast<std::uint16_t>(info::storage.data()), info::size()); send_flash(reinterpret_cast<std::uint16_t>(info::storage.data()), info::size());
break; break;
case gate::emergency: case gate::emergency:
if (!request_confirm() || !request_confirm()) { if (!request_confirm() || !request_confirm())
appjump(); appjump();
}
emergency_erase(); emergency_erase();
break; break;
} }
@@ -271,22 +268,22 @@ gate password_gate()
for (;;) { for (;;) {
tx(confirm); // Mainloop ready tx(confirm); // Mainloop ready
switch (rx()) { switch (rx()) {
case command::read_flash: case 'f':
read_flash(); read_flash();
break; break;
case command::write_flash: case 'F':
write_flash(); write_flash();
break; break;
case command::read_eeprom: case 'e':
read_eeprom(); read_eeprom();
break; break;
case command::write_eeprom: case 'E':
write_eeprom(); write_eeprom();
break; break;
case command::read_config: case 'c':
send_flash(app_end, page); send_flash(app_end, page);
break; break;
case command::write_config: case 'C':
write_config(); write_config();
break; break;
default: default:
@@ -298,6 +295,14 @@ gate password_gate()
} // namespace } // namespace
} // namespace tsb } // namespace tsb
// Reset lands at the boot section base (BOOTRST): the entry stub in .vectors // Reset lands here: BOOTRST vectors to the boot section base and .vectors is
// is laid first and does the one line of crt a crt-less image needs. // laid first, so this is the first instruction executed. No crt ran, so set the
template struct avr::startup::entry<tsb::run, avr::startup::stack::hardware>; // stack pointer before anything is called.
extern "C" [[gnu::naked, gnu::used, gnu::section(".vectors")]] void __boot_entry()
{
SP = RAMEND;
// The one line of crt this loader needs: compiled code assumes
// __zero_reg__ (r1) is 0, and power-on registers are undefined.
asm volatile("clr __zero_reg__");
tsb::run();
}
+118 -79
View File
@@ -1,16 +1,16 @@
// TinySafeBoot on libavr - tier 2: C++ with compiler trickery, no assembly. // TinySafeBoot on libavr — tier 2: C++ with compiler trickery, no assembly.
// //
// The full TinySafeBoot feature set - watchdog bail, one-wire half-duplex, // The full TinySafeBoot feature set — watchdog bail, one-wire half-duplex,
// config-page activation timeout, password gate, emergency erase, and // config-page activation timeout, password gate, emergency erase, and
// config/flash/EEPROM read-write - in pure C++, a little over the 512-byte boot // config/flash/EEPROM read-write — in pure C++, 526 bytes: 14 over the 512-byte
// section the hand-written oracle fits (oracle/README.md holds what each tier // boot section the hand-written oracle fits, from 168 over at this tier's first
// measures). The structure mirrors the oracle's: a handful of tiny noinline // floor. The structure mirrors the oracle's: a handful of tiny noinline
// primitives sharing one whole-loader register allocation, expressed as global // primitives sharing one whole-loader register allocation, expressed as global
// register variables so no helper ever saves, spills, or reloads any of it. // register variables so no helper ever saves, spills, or reloads any of it.
// //
// The register protocol (all call-saved, so calls preserve them by ABI): // The register protocol (all call-saved, so calls preserve them by ABI):
// Y (r28:r29) g_addr the walked flash/EEPROM address - adiw-able // Y (r28:r29) g_addr the walked flash/EEPROM address — adiw-able
// r16 g_cnt byte countdown of the running block - ldi-able // r16 g_cnt byte countdown of the running block — ldi-able
// r7 g_window rx timeout, roughly 30 ms units at 16 MHz // r7 g_window rx timeout, roughly 30 ms units at 16 MHz
// r6 g_receiving one-wire direction latch, cleared at bring-up // r6 g_receiving one-wire direction latch, cleared at bring-up
// (power-on registers are undefined) // (power-on registers are undefined)
@@ -18,10 +18,10 @@
// GCC 16.1 miscompiles stores into global register variables: an update whose // GCC 16.1 miscompiles stores into global register variables: an update whose
// remaining uses all hide inside callees is deleted whenever a CALL follows it // 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 // before any jump/ret (the backend's liveness walk lumps fixed registers with
// call-clobbered ones - minimal repro in libavr's // call-clobbered ones — minimal repro in libavr's
// test/upstream/gcc-avr-globalreg-repro.cpp). Every // local/scratch/probes/gcc-avr-globalreg-repro.cpp, lessons.md entry). Every
// g_* update below therefore sits where a *local* read or a jump/ret follows // g_* update below therefore sits where a *local* read or a jump/ret follows
// it - the helpers advance g_addr immediately before returning, and rx() // it — the helpers advance g_addr immediately before returning, and rx()
// re-floors the window on every call instead of storing the floored value // re-floors the window on every call instead of storing the floored value
// once. The layout is load-bearing; do not "simplify" it. // once. The layout is load-bearing; do not "simplify" it.
// //
@@ -29,28 +29,72 @@
// 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 "tsb.hpp" #include <libavr/libavr.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 boot section: 1 KB, BOOTSZ=10. // The loader is purely polled — it never enables interrupts — so every SPM and
constexpr std::uint16_t boot_bytes = 1024; // EEPROM lock folds to nothing under this posture.
constexpr std::uint16_t app_end = config_page(boot_bytes); constexpr auto off = avr::irq::guard_policy::unused;
[[gnu::progmem]] constexpr auto info = info_block(date_stamp(26, 7, 20), app_end); 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 app_end = spm::flash_bytes - boot_bytes - page;
constexpr std::uint16_t eeprom_end = avr::hw::db.mem.eeprom_size - 1;
// Lockout-proof floor for the activation window (the oracle's F_CPU/1MHz).
constexpr std::uint8_t act_min = 16;
// 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::detail::solve_baud(16_MHz, 115200_Bd);
// The 16-byte device-info block, streamed out on activation.
// clang-format off
[[gnu::progmem]] constexpr std::uint8_t info[16] = {
'T', 'S', 'B',
build_date & 0xFF, build_date >> 8,
0xF3, // status: native-UART fixed-baud lineage
0x1E, 0x95, 0x0F, // ATmega328P signature
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
// command - so a silent host unwinds the loader to the application from // command — so a silent host unwinds the loader to the application from
// anywhere, and a mid-session cable pull cannot wedge it. // anywhere, and a mid-session cable pull cannot wedge it.
[[gnu::noinline, gnu::noclone]] std::uint8_t rx() [[gnu::noinline, gnu::noclone]] std::uint8_t rx()
{ {
@@ -58,25 +102,24 @@ register std::uint8_t g_receiving asm("r6");
g_receiving = 1; g_receiving = 1;
hw::ucsr0b::write(hw::ucsr0b::rxen0(1)); // RXEN0 alone: release and listen hw::ucsr0b::write(hw::ucsr0b::rxen0(1)); // RXEN0 alone: release and listen
} }
// act_min ORs in here, per call, not once into g_window at setup - the // act_min ORs in here, per call, not once into g_window at setup — the
// one placement the global-register-store miscompile cannot delete. // one placement the global-register-store miscompile cannot delete.
std::uint16_t outer = static_cast<std::uint16_t>(g_window | act_min) << 8; std::uint16_t outer = static_cast<std::uint16_t>(g_window | act_min) << 8;
do { do {
std::uint8_t fine = 0; std::uint8_t fine = 0;
do { do {
auto status = hw::ucsr0a::read(); auto status = hw::ucsr0a::read();
if (status & hw::ucsr0a::rxc0(1).value) { if (status & hw::ucsr0a::rxc0(1).value)
return hw::udr0::read(); return hw::udr0::read();
}
} while (--fine); } while (--fine);
} while (--outer); } while (--outer);
return 0; return 0;
} }
// One-wire transmit: take the line (TXEN0 alone - the receiver must be off // One-wire transmit: take the line (TXEN0 alone — the receiver must be off
// while driving) on a direction change, with a turn-around guard so a shorted // while driving) on a direction change, with a turn-around guard so a shorted
// peer can switch first; then hold the line until the whole frame is out // peer can switch first; then hold the line until the whole frame is out
// (TXC0, not UDRE0 - the stop bit must be on the wire before a caller may // (TXC0, not UDRE0 — the stop bit must be on the wire before a caller may
// release the line), and W1C TXC0 by storing the sampled status back, which // release the line), and W1C TXC0 by storing the sampled status back, which
// keeps U2X0. // keeps U2X0.
[[gnu::noinline, gnu::noclone]] void tx(std::uint8_t byte) [[gnu::noinline, gnu::noclone]] void tx(std::uint8_t byte)
@@ -84,7 +127,8 @@ register std::uint8_t g_receiving asm("r6");
if (g_receiving) { if (g_receiving) {
g_receiving = 0; g_receiving = 0;
hw::ucsr0b::write(hw::ucsr0b::txen0(1)); hw::ucsr0b::write(hw::ucsr0b::txen0(1));
avr::delay::cycles<guard_cycles>(); for (std::uint8_t guard = 46; guard; --guard)
;
} }
hw::udr0::write(byte); hw::udr0::write(byte);
std::uint8_t status; std::uint8_t status;
@@ -101,27 +145,26 @@ register std::uint8_t g_receiving asm("r6");
return rx(); return rx();
} }
// One flash byte <- [g_addr++] (the advance right before ret - see the top of // One flash byte ← [g_addr++] (the advance right before ret — see header).
// this file).
[[gnu::noinline, gnu::noclone]] std::uint8_t sflash() [[gnu::noinline, gnu::noclone]] std::uint8_t sflash()
{ {
const auto byte = avr::flash_load(flash_ptr(g_addr)); std::uint8_t byte = avr::flash_load(flash_ptr(g_addr));
++g_addr; ++g_addr;
return byte; return byte;
} }
// 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()
{ {
const auto byte = ee::read<no_spm>(g_addr); std::uint8_t byte = ee::read(g_addr);
++g_addr; ++g_addr;
return byte; return byte;
} }
// One EEPROM byte -> [g_addr++]. // One EEPROM byte → [g_addr++].
[[gnu::noinline, gnu::noclone]] void eewr(std::uint8_t byte) [[gnu::noinline, gnu::noclone]] void eewr(std::uint8_t byte)
{ {
ee::write<off, no_spm>(g_addr, byte); ee::write<off>(g_addr, byte);
++g_addr; ++g_addr;
} }
@@ -133,7 +176,7 @@ register std::uint8_t g_receiving asm("r6");
} while (--g_cnt); } while (--g_cnt);
} }
// Wait out a running SPM op, then re-open the RWW section - after every page // Wait out a running SPM op, then re-open the RWW section — after every page
// op and before handing over, as the oracle does. // op and before handing over, as the oracle does.
[[gnu::noinline, gnu::noclone]] void settle() [[gnu::noinline, gnu::noclone]] void settle()
{ {
@@ -151,16 +194,16 @@ 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 the top of this file). // call would get it deleted (see header).
[[gnu::noinline, gnu::noclone]] void erase_below() [[gnu::noinline, gnu::noclone]] void erase_below()
{ {
g_addr -= page; g_addr -= page;
spm::command<off>(spm::op::erase, g_addr); spm::erase_page<off>(g_addr);
settle(); settle();
} }
// Erase the whole application, top-down like the oracle: the loop bound is a // Erase the whole application, top-down like the oracle: the loop bound is a
// compare with zero, and g_addr = 0 - the value every caller wants next - is // compare with zero, and g_addr = 0 — the value every caller wants next — is
// handed back for free. // handed back for free.
[[gnu::noinline, gnu::noclone]] void erase_application() [[gnu::noinline, gnu::noclone]] void erase_application()
{ {
@@ -171,19 +214,18 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
} }
// Stream one host page into the erased flash page at g_addr (SPM word buffer, // Stream one host page into the erased flash page at g_addr (SPM word buffer,
// low byte then high) - no SRAM staging, receive and program are one loop. // low byte then high) — no SRAM staging, receive and program are one loop.
// g_addr is left at the next page base. // g_addr is left at the next page base.
[[gnu::noinline, gnu::noclone]] void store_flash() [[gnu::noinline, gnu::noclone]] void store_flash()
{ {
const auto open = spm::page::begin<spm::from::boot_section, off>(g_addr);
g_cnt = page / 2; g_cnt = page / 2;
do { do {
std::uint16_t word = rx(); std::uint16_t word = rx();
word |= static_cast<std::uint16_t>(rx()) << 8; word |= static_cast<std::uint16_t>(rx()) << 8;
spm::fill<off>(open, g_addr, word); spm::fill<off>(g_addr, word);
g_addr += 2; g_addr += 2;
} while (--g_cnt); } while (--g_cnt);
spm::command<off>(spm::op::write, g_addr - page); spm::write_page<off>(g_addr - page);
settle(); settle();
} }
@@ -191,15 +233,14 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
{ {
// A watchdog reset hands straight back to the application, as the // A watchdog reset hands straight back to the application, as the
// reference loader does, rather than re-entering the bootloader. // reference loader does, rather than re-entering the bootloader.
if (hw::mcusr::wdrf.test()) { if (hw::mcusr::wdrf.test())
appjump(); appjump();
}
// 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 rx()/tx() raise RXEN0/TXEN0 on first use - only the divisor low // 0, and rx()/tx() raise RXEN0/TXEN0 on first use — only the divisor low
// byte and U2X0 need a store. The library still does the datasheet work. // byte and U2X0 need a store. The library still does the datasheet work.
static_assert(baud.u2x && baud.ubrr < 256, "lean bring-up writes UBRR0L only, with U2X0"); static_assert(baud.u2x && baud.ubrr < 256, "lean bring-up writes UBRR0L only, with U2X0");
hw::ubrr0::write(static_cast<std::uint8_t>(baud.ubrr)); hw::reg<"UBRR0">::write(static_cast<std::uint8_t>(baud.ubrr));
hw::ucsr0a::write(hw::ucsr0a::u2x0(1)); hw::ucsr0a::write(hw::ucsr0a::u2x0(1));
// General-purpose registers are undefined at power-on (no crt zeroes them); // General-purpose registers are undefined at power-on (no crt zeroes them);
// the direction latch must start "not receiving" so the first rx() enables // the direction latch must start "not receiving" so the first rx() enables
@@ -207,41 +248,37 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
// same reason. // same reason.
g_receiving = 0; g_receiving = 0;
// Activation: activation_knocks '@', each inside the config page's timeout // Activation: 3×'@', each inside the config page's timeout window (rx
// window (rx floors it so a corrupt page cannot lock the loader out); // floors it so a corrupt page cannot lock the loader out); anything else —
// anything else - including silence - hands over. // including silence — hands over.
g_window = avr::flash_load(flash_ptr(app_end + timeout_at)); g_window = avr::flash_load(flash_ptr(app_end + 2));
for (std::uint8_t k = activation_knocks; k; --k) { for (std::uint8_t k = 3; k; --k)
if (rx() != knock) { if (rx() != knock)
appjump(); appjump();
}
}
g_window = comm_window; g_window = comm_window;
// Password gate (config page from password_at, 0xff-terminated; a blank // Password gate (config page from app_end+3, 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 + password_at; g_addr = app_end + 3;
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;
++g_addr; ++g_addr;
if (expected == 0xff) { if (expected == 0xff) {
g_addr = reinterpret_cast<std::uint16_t>(info.data()); g_addr = reinterpret_cast<std::uint16_t>(&info[0]);
g_cnt = sizeof(info); g_cnt = sizeof(info);
sendf(); sendf();
break; break;
} }
const auto got = rx(); std::uint8_t got = rx();
if (got == 0) { if (got == 0) {
if (mask == 0) { if (mask == 0)
continue; continue;
} if (rcnf() != confirm || rcnf() != confirm)
if (rcnf() != confirm || rcnf() != confirm) {
appjump(); appjump();
}
erase_application(); // leaves g_addr = 0 for the EEPROM walk erase_application(); // leaves g_addr = 0 for the EEPROM walk
do { do {
eewr(0xff); eewr(0xff);
@@ -250,45 +287,40 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
erase_below(); erase_below();
break; break;
} }
if (got != expected) { if (got != expected)
mask = 0; mask = 0;
} }
}
for (;;) { for (;;) {
tx(confirm); // Mainloop ready tx(confirm); // Mainloop ready
g_addr = 0; g_addr = 0;
switch (rx()) { switch (rx()) {
case command::read_flash: // one page per host '!' case 'f': // read application flash, one page per host '!'
for (;;) { for (;;) {
if (rx() != confirm) { if (rx() != confirm)
break; break;
}
g_cnt = page; g_cnt = page;
sendf(); sendf();
if (g_addr >= app_end) { if (g_addr >= app_end)
break; break;
} }
}
break; break;
case command::write_flash: // erase the application, then take pages behind '?' case 'F': // 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 command::read_eeprom: // one page per host '!', until the host stops case 'e': // read EEPROM, one page per host '!', until the host stops
for (;;) { for (;;) {
if (rx() != confirm) { if (rx() != confirm)
break; break;
}
g_cnt = page; g_cnt = page;
do { do {
tx(eerd()); tx(eerd());
} while (--g_cnt); } while (--g_cnt);
} }
break; break;
case command::write_eeprom: // pages behind '?' case 'E': // take EEPROM pages behind '?'
while (rcnf() == confirm) { while (rcnf() == confirm) {
g_cnt = page; g_cnt = page;
do { do {
@@ -296,16 +328,15 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
} while (--g_cnt); } while (--g_cnt);
} }
break; break;
case command::read_config: case 'c': // read the config page
read_config: read_config:
g_addr = app_end; g_addr = app_end;
g_cnt = page; g_cnt = page;
sendf(); sendf();
break; break;
case command::write_config: // then echo it back to verify case 'C': // replace the config page, then echo it back to verify
if (rcnf() != confirm) { if (rcnf() != confirm)
break; break;
}
g_addr = app_end + page; g_addr = app_end + page;
erase_below(); // leaves g_addr = app_end, the store target erase_below(); // leaves g_addr = app_end, the store target
store_flash(); store_flash();
@@ -319,6 +350,14 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
} // namespace } // namespace
} // namespace tsb } // namespace tsb
// Reset lands at the boot section base (BOOTRST): the entry stub in .vectors // Reset lands here: BOOTRST vectors to the boot section base and .vectors is
// is laid first and does the one line of crt a crt-less image needs. // laid first, so this is the first instruction executed. No crt ran, so set
template struct avr::startup::entry<tsb::run, avr::startup::stack::hardware>; // the stack pointer before anything is called.
extern "C" [[gnu::naked, gnu::used, gnu::section(".vectors")]] void __boot_entry()
{
SP = RAMEND;
// The one line of crt this loader needs: compiled code assumes
// __zero_reg__ (r1) is 0, and power-on registers are undefined.
asm volatile("clr __zero_reg__");
tsb::run();
}