build: the libavr pin advances past phase 6, at byte parity everywhere

The pin crosses libavr's phase 6 - the renamed system surface, the named
serial configs, the receiver-tolerance table, the paged SPM receipts -
and every loader image comes out size-identical: the full matrix on six
representative chips (the exhaustive cross product on three of them),
the stock and autobaud columns untouched, the four tsb tiers back on
their recorded floors at 510/526/638/836.

Byte parity was not free, and the two libavr defects it surfaced were
fixed there rather than absorbed here. The EEPROM write procedure's
step 2 - the SPMEN spin - had landed unconditionally and cost every
build six bytes for a wait a polled loader can never take; it is scoped
now, and the loaders state the datasheet's own omission clause
(spm_interlock::omitted, DS40002061B 8.6.3). The blocking page
erase/write grew an internal wait the tiers' settle() already provides,
so the tiers issue the command form and pureboot keeps its host-driven
sp_spm path.

What the port states rather than inherits: the stock 115200 at 16 MHz
sits +2.1 % past the receiver-tolerance table libavr now holds rates
to, so the hardware links say .allow_baud_error = true - the same
2.5 % envelope pureboot_baud_feasible() has always enforced, proven on
silicon across the fleet. rx_ready() reads readable() now.

Alongside the pin: rule 33's ASCII sweep over every source (docs keep
their typography), rule 34's InsertBraces in .clang-format with the
tree reformatted, std::array over the simavr runners' raw buffers, and
the stale Studio size in ide/README.md replaced by the claim its
check-flags gate actually holds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-09 11:43:44 +02:00
parent 0cb83ff36f
commit e4390d2ba8
38 changed files with 791 additions and 625 deletions

View File

@@ -12,7 +12,7 @@ if(NOT LIBAVR_ROOT)
set(LIBAVR_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/libavr)
endif()
if(NOT EXISTS ${LIBAVR_ROOT}/CMakeLists.txt)
message(FATAL_ERROR "libavr not found at ${LIBAVR_ROOT} run: git submodule update --init libavr")
message(FATAL_ERROR "libavr not found at ${LIBAVR_ROOT} - run: git submodule update --init libavr")
endif()
add_subdirectory(${LIBAVR_ROOT} libavr-build)
@@ -22,7 +22,7 @@ if(PROJECT_IS_TOP_LEVEL)
# The behavioral tests drive the real wire protocols over a simavr pty
# (as the host tools do) and actually flash the device. The runners are
# host programs built at configure time against libsimavr (C++23 what
# host programs built at configure time against libsimavr (C++23 - what
# the distribution's compiler speaks in full); if they or Python are
# missing, only the size tests run.
find_program(_host_cxx NAMES c++ g++)
@@ -36,7 +36,7 @@ if(PROJECT_IS_TOP_LEVEL)
-lsimavr -lsimavrparts -lelf -lutil
RESULT_VARIABLE _pbdev_res ERROR_VARIABLE _pbdev_err)
if(NOT _pbdev_res EQUAL 0)
message(STATUS "pureboot_device not built (${_pbdev_err}) protocol tests skipped")
message(STATUS "pureboot_device not built (${_pbdev_err}) - protocol tests skipped")
unset(PB_DEVICE)
endif()
if(LIBAVR_MCU STREQUAL "atmega328p")
@@ -48,19 +48,19 @@ if(PROJECT_IS_TOP_LEVEL)
-lsimavr -lsimavrparts -lelf
RESULT_VARIABLE _dev_res ERROR_VARIABLE _dev_err)
if(NOT _dev_res EQUAL 0)
message(STATUS "tsb_device not built (${_dev_err}) protocol tests skipped")
message(STATUS "tsb_device not built (${_dev_err}) - protocol tests skipped")
unset(TSB_DEVICE)
endif()
endif()
endif()
endif()
# The ELF is only a container (symbols, section headers) and is never flashed
# The ELF is only a container (symbols, section headers) and is never flashed -
# and the host tool's load_image() dispatches on extension, so handing it one
# would silently program the header bytes. Every loader image therefore gets
# both flashable forms beside it at link time: .hex for avrdude, and .bin for
# the host tool's raw path (which is what the reloc and update tests convert to
# on the fly). .eeprom is dropped EEPROM content is its own update.
# on the fly). .eeprom is dropped - EEPROM content is its own update.
function(add_image_outputs name)
add_custom_command(TARGET ${name} POST_BUILD
COMMAND ${CMAKE_OBJCOPY} -O ihex -R .eeprom
@@ -71,9 +71,9 @@ endfunction()
# The TinySafeBoot protocol reimplemented on libavr in variants that trade
# clarity for size. Each links into the ATmega328P boot section (BOOTSZ selects
# its size; BOOTRST vectors a reset to its base) with -nostartfiles a polled
# its size; BOOTRST vectors a reset to its base) with -nostartfiles - a polled
# loader has no use for the crt or the vector table. The entry sits in
# .vectors, laid first, and runs avr::startup::entry on the policy tier,
# .vectors, laid first, and runs - avr::startup::entry on the policy tier,
# the experiment tiers' own naked stubs elsewhere, each documented in its
# source. The boot base is FLASHEND+1 minus the section size; the linker
# section-start and the source's boot_bytes agree. tsb_app is
@@ -83,26 +83,26 @@ endfunction()
# All four implement the full oracle feature set (see oracle/README.md):
# watchdog bail, one-wire half-duplex, config-page activation timeout, password
# gate, emergency erase, config/flash/EEPROM read-write. They differ only in how,
# and the size gradient is the cost of that "how" see dev/lessons.md.
# tsb_asm the tricks tier's C++ with exactly two routines in asm (the
# bounded rx and the page-store loop the two whose remaining
# and the size gradient is the cost of that "how" - see dev/lessons.md.
# tsb_asm - the tricks tier's C++ with exactly two routines in asm (the
# bounded rx and the page-store loop - the two whose remaining
# cost is the C ABI itself): 510 B in the 512 B section the
# hand-written 500 B oracle occupies. Everything else, from
# bring-up to dispatch, is C++ on libavr.
# tsb_tricks no asm at all: the whole-loader register allocation lives in
# tsb_tricks - no asm at all: the whole-loader register allocation lives in
# global register variables (Y walks the page pointer), every
# helper is a tiny noinline primitive placed by the
# global-register store rules, pages stream straight to
# SPM/EEPROM, and the bring-up is the two reset-non-default
# registers only. 526 B in the 1 KB section (BOOTSZ=10) 14
# registers only. 526 B in the 1 KB section (BOOTSZ=10) - 14
# over the oracle's section, from 168 over at this tier's first
# floor.
# tsb_pure pure idiomatic libavr, one function per command, TU-local
# tsb_pure - pure idiomatic libavr, one function per command, TU-local
# (internal linkage), streaming (no SRAM page buffer): 836 B in
# the 1 KB section.
# tsb_policy the policy floor: pureboot's rules (no asm, no register
# tsb_policy - the policy floor: pureboot's rules (no asm, no register
# variables) with every pureboot lesson applied. 638 B in the
# 1 KB section the measured evidence that the 512 B fit is a
# 1 KB section - the measured evidence that the 512 B fit is a
# property of the mechanisms philosophy #5 bans.
#
# add_tsb_variant(<name> <boot-section-bytes>)
@@ -136,14 +136,14 @@ if(LIBAVR_MCU STREQUAL "atmega328p")
add_tsb_variant(tsb_tricks 1024)
# The policy tier's floor is measured with the loop flags pureboot's size
# work found (a loader's loop bodies all contain calls); the other tiers
# keep the flag set their recorded floors were measured with none.
# 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()
# pureboot the pure-constraint port (see pureboot/README.md): one source,
# pureboot - the pure-constraint port (see pureboot/README.md): one source,
# no inline assembly, no global register variables, every libavr chip,
# fitting each chip's smallest boot sector. The geometry and the
# pureboot_add_loader() deployment function live in pureboot/CMakeLists.txt
# pureboot_add_loader() deployment function live in pureboot/CMakeLists.txt -
# the unit a downstream project consumes; everything below is this port's
# own build: the stock loaders, their tests, and the size matrix. The
# distinct binary dir keeps the `pureboot` target's output name free.
@@ -151,7 +151,7 @@ add_subdirectory(pureboot pureboot-cmake)
# The stock loader: the family-default deployment (crystal/RC clock, the
# chip's natural link, default pins). The activation window stays a cache
# variable re-timing a deployed loader is a self-update with a re-timed
# variable - re-timing a deployed loader is a self-update with a re-timed
# build. pureboot9 is that re-timed build, and what the update test installs.
set(PUREBOOT_TIMEOUT 8 CACHE STRING "pureboot activation window, seconds")
pureboot_add_loader(pureboot TIMEOUT ${PUREBOOT_TIMEOUT})
@@ -203,8 +203,8 @@ if(PROJECT_IS_TOP_LEVEL)
set_tests_properties(pureboot.protocol PROPERTIES TIMEOUT 180)
# The activation window as a measured duration: application installed,
# line idle, the first transmit is the application's banner its
# cycle is the window the source declares, held to ±2 % (one
# line idle, the first transmit is the application's banner - its
# cycle is the window the source declares, held to +/-2 % (one
# mis-counted cycle per poll is a 10 % shift).
add_test(NAME pureboot.window
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbwindow.py
@@ -218,8 +218,8 @@ if(PROJECT_IS_TOP_LEVEL)
set_tests_properties(pureboot.window PROPERTIES TIMEOUT 300)
# The half-duplex loader's window, same gate: its poll runs through
# rx_ready()'s release-line test, whose outlined call re-shapes the
# whole loop a per-class cycle count (poll_cost() in pureboot.cpp)
# readable()'s release-line test, whose outlined call re-shapes the
# whole loop - a per-class cycle count (poll_cost() in pureboot.cpp)
# that only the built image can prove, chip by chip.
if(PUREBOOT_HAS_USART)
add_test(NAME pureboot.window.halfduplex
@@ -268,7 +268,7 @@ if(PROJECT_IS_TOP_LEVEL)
set_tests_properties(pureboot.glitch PROPERTIES TIMEOUT 180)
# Entering the loader from a running application with no reset
# between, over a page buffer the application dirtied the case the
# between, over a page buffer the application dirtied - the case the
# loader declines to guard and the host repairs. Hardware forbids the
# state here (SPM runs only from the boot section); simavr does not,
# which is what makes it constructible.
@@ -299,7 +299,7 @@ if(PROJECT_IS_TOP_LEVEL)
endif()
# The self-update end-to-end: the re-timed build (same source, only
# the timeout differs a byte-different image) replaces the resident
# the timeout differs - a byte-different image) replaces the resident
# through --update-loader, with every power-fail phase rehearsed from
# the runner's flash dumps.
pureboot_add_loader(pureboot9 TIMEOUT 9)
@@ -315,11 +315,11 @@ if(PROJECT_IS_TOP_LEVEL)
endif()
# The size matrix: every configuration axis that could move the image
# size the serial backend (different code), the USART instance
# size - the serial backend (different code), the USART instance
# (different registers), the clock (different constants), the baud
# through the shapes its bit timing takes, and the pins through the one
# thing they decide (whether a bit-banged link has to release the USART
# that owns them) each combination must still fit the chip's slot
# that owns them) - each combination must still fit the chip's slot
# budget. The timeout is a constant and adds no axis. The stock build is
# one point of this matrix and already has its test.
function(pureboot_size_variant name)
@@ -330,7 +330,7 @@ if(PROJECT_IS_TOP_LEVEL)
endfunction()
# The autobaud loader: one clock-agnostic image per chip, so it has no
# clock x baud axis of its own the matrix below sweeps those for the
# clock x baud axis of its own - the matrix below sweeps those for the
# fixed-baud builds, and this one binary has to serve all of them at run
# time. Size-tested against the same per-chip budget as every other variant.
pureboot_add_loader(pureboot_autobaud SERIAL autobaud)
@@ -386,7 +386,7 @@ if(PROJECT_IS_TOP_LEVEL)
# Clock points: the shipped-fuse floor (CKDIV8), the calibrated RC, and
# the crystal the stock build assumes (the tiny13's ladder is its own RC
# menu it has no crystal option).
# menu - it has no crystal option).
if(LIBAVR_MCU MATCHES "^attiny13")
set(_matrix_clocks 1200000 4800000 9600000)
set(_full_clocks 128000 600000 1200000 4800000 9600000)
@@ -397,20 +397,20 @@ if(PROJECT_IS_TOP_LEVEL)
endif()
# The exhaustive cross product: every clock a deployment plausibly runs
# the internal oscillators, the shipped CKDIV8 floor, the plain
# crystals and the UART crystals against every rate, against every
# - the internal oscillators, the shipped CKDIV8 floor, the plain
# crystals and the UART crystals - against every rate, against every
# backend. Beyond the ladder the list carries the slow rates a
# sub-megahertz oscillator is left with, which no ladder rate reaches
# (16000 Bd is the only rate the 128 kHz oscillator holds exactly); at
# the fast clocks those same rates also select the software UART's
# 16-bit _delay_loop_2 bit spin (two words more setup at each of its five
# sites), the largest image the space produces and a shape the ladder
# default always the *fastest* rate a clock reaches never picks.
# default - always the *fastest* rate a clock reaches - never picks.
#
# Every chip runs the full cross product: the size-bearing classes (flash
# addressing, hand-over shape, page size, USART inventory) are what make
# the image differ, and a chip outside them is expected to match its class
# but "expected" is what a matrix is for, and the whole sweep is cheap
# - but "expected" is what a matrix is for, and the whole sweep is cheap
# enough to run rather than reason about. PUREBOOT_FULL_MATRIX is what
# selects it; the compact matrix below is the per-commit default.
get_property(_full_bauds GLOBAL PROPERTY PUREBOOT_BAUD_LADDER)
@@ -449,9 +449,9 @@ if(PROJECT_IS_TOP_LEVEL)
endforeach()
list(GET _matrix_clocks -1 _matrix_top_hz)
pureboot_size_variant(pureboot_sw_wide CLOCK ${_matrix_top_hz} BAUD 9600 SERIAL software)
# The pin axis at the widest software image the slowest ladder rate
# The pin axis at the widest software image - the slowest ladder rate
# against the fastest clock, whose bit spin needs the 16-bit delay
# loop with the USART release on top of it. The exhaustive sweep
# loop - with the USART release on top of it. The exhaustive sweep
# above carries the same axis across its whole cross product.
if(PUREBOOT_HAS_USART)
pureboot_size_variant(pureboot_sw_wide_on_usart0 CLOCK ${_matrix_top_hz} BAUD 9600
@@ -468,7 +468,7 @@ if(PROJECT_IS_TOP_LEVEL)
# The pin axis at its fixed points, in both matrix modes. The autobaud
# loader carries no clock and no baud, so the sweep has nothing to vary
# for it yet it is the tightest image in the space, and on a USART's
# for it - yet it is the tightest image in the space, and on a USART's
# own pins it pays the release too: that combination is the one that
# overflowed the 1284's slot. The software build on those pins is the
# same deployment the mute test drives.
@@ -486,7 +486,7 @@ if(PROJECT_IS_TOP_LEVEL)
endif()
# The OSCCAL axis at its fixed points: the stock shape, and the tightest
# image in the space with the trim on top the axis adds one register
# image in the space with the trim on top - the axis adds one register
# write, and these points hold both of its addressing encodings to every
# chip's budget.
pureboot_size_variant(pureboot_osccal OSCCAL 0x9c)
@@ -499,9 +499,9 @@ if(PROJECT_IS_TOP_LEVEL)
# The one-wire axis at its fixed points, in both matrix modes (the
# exhaustive sweep carries the same shapes across its cross product):
# the software link folded onto one pin, the tightest autobaud image
# likewise on the default pin and on the USART's own RXD, whose
# likewise - on the default pin and on the USART's own RXD, whose
# release the now-driven shared pin needs where a receive-only link
# would not and the hardware USART's half-duplex turn-around, stock
# would not - and the hardware USART's half-duplex turn-around, stock
# and at the widest fixed-baud shape.
# The two spellings deliberately split across the two points: HALF_DUPLEX
# folds TX onto RX, RX == TX states the same thing directly.
@@ -520,10 +520,10 @@ if(PROJECT_IS_TOP_LEVEL)
pureboot_size_variant(pureboot_usart1_hd USART 1 HALF_DUPLEX)
endif()
# The trim byte, observed through the wire from the first prompt one
# The trim byte, observed through the wire from the first prompt - one
# chip per OSCCAL addressing class: extended I/O on the 328P (data 0x66,
# an sts DS40002061B §36), plain I/O on the 85 (data 0x51, an out
# Atmel-2586 §21).
# an sts - DS40002061B section 36), plain I/O on the 85 (data 0x51, an out -
# Atmel-2586 section 21).
if(LIBAVR_MCU MATCHES "^(atmega328p|attiny85)$" AND DEFINED PB_DEVICE)
if(LIBAVR_MCU STREQUAL "atmega328p")
set(_osccal_addr 0x66)
@@ -541,11 +541,11 @@ if(PROJECT_IS_TOP_LEVEL)
set_tests_properties(pureboot.osccal PROPERTIES TIMEOUT 120)
endif()
# One configured deployment end to end a real board's shape rather
# One configured deployment end to end - a real board's shape rather
# than the stock assumption: the ATmega328P on its shipped 1 MHz fuses,
# the software UART on hand-picked pins (TX = PB1, RX = PB5), the ladder
# baud (9600). The full protocol suite runs against it, fixture
# application included, over the runner's GPIO bridge proving the
# application included, over the runner's GPIO bridge - proving the
# configuration plumbing produces a working loader, not just one that
# fits.
if(LIBAVR_MCU STREQUAL "atmega328p" AND DEFINED PB_DEVICE)
@@ -569,7 +569,7 @@ if(PROJECT_IS_TOP_LEVEL)
set_tests_properties(pureboot.custom PROPERTIES TIMEOUT 180)
endif()
# Hand-over with the USART that owns the loader's pins left enabled the
# Hand-over with the USART that owns the loader's pins left enabled - the
# state an application reaches by jumping in without a reset, and the one
# that made a bit-banged loader on PD0/PD1 (where the Uno's USB bridge
# lands) receive and obey while answering nothing. Run where it was found
@@ -634,7 +634,7 @@ if(PROJECT_IS_TOP_LEVEL)
set_tests_properties(pureboot.onewire PROPERTIES TIMEOUT 180)
# The hardware USART's half-duplex turn-around, end to end: every
# reply byte runs drive-line, TXC-hold, release against simavr's
# reply byte runs drive-line, TXC-hold, release - against simavr's
# RXEN-gated receiver, which drops input to a disabled receiver the
# way silicon does. The pty is a two-wire transport, so the host
# needs no echo discard here; the off-chip tie itself is the
@@ -674,7 +674,7 @@ if(PROJECT_IS_TOP_LEVEL)
# The autobaud loader driven end to end over the software-UART bridge:
# the host sends the 0xC0 calibration pulse, the loader times it, locks,
# and programs. Run on the near-flash 328P
# and the word-addressed 1284P the two flash-addressing classes and each
# and the word-addressed 1284P - the two flash-addressing classes - and each
# at two clocks with the one binary, which is the clock-agnostic property
# autobaud exists for (test/pbautobaud.py). The fixture application banners
# over the same software link at the first clock's rate.
@@ -696,7 +696,7 @@ if(PROJECT_IS_TOP_LEVEL)
# The tightest deployment in the space, end to end: the autobaud
# loader folded onto the USART's own RXD with the OSCCAL trim baked
# one-wire calibration, the receive-side release, and the host's
# - one-wire calibration, the receive-side release, and the host's
# echo discard, over the same two-clock sweep. One chip carries it;
# the shape is chip-independent.
if(LIBAVR_MCU STREQUAL "atmega328p")