The window's per-poll cycle counts were hand-counted for a uint32_t countdown, but every default window fits uint24_t, whose decrement chain is one sbci shorter — so deployed loaders ran 9/10ths of their stated seconds (a 328P's 8 s was 7.2 s on the wire). No golden-asm pin can hold this: the loops compile in consumer context. pbwindow.py measures the behavior instead: it installs a real application beside the loader through the host tool's own plan_flash (surgery included), starts the simulator with the line idle, and reads the cycle of the first transmit — the application's banner, so that cycle is the window. Held at plus or minus 2 percent per chip (pureboot.window), red at -10.0 percent against the old constants, green with poll_cycles now counted for the narrow countdown (hardware 9, software 7; window_polls() solves narrow-first and adds the wide loop's cycle where the count forces uint32_t — a count narrow only at the wide cost stays wide, so the choice cannot oscillate). The autobaud window is its poll budget at the measured ten cycles a poll, gated the same way (pureboot.window.autobaud), and the README carries that arithmetic now. No version bump: timing-window precision is not meaningful behavior, v7 stays. The gate flushed out two runner gaps. The software bridge accepted any falling edge as a start bit, so the device's own TX-init glitch decoded as a stray byte; it re-samples mid-bit now and abandons a false start, as silicon does. And after avr_reset, the idle-line re-raise was silently dropped: ioport pin irqs are IRQ_FLAG_FILTERED and the irq's cached value survives the reset the port latch does not, so the device read the line stuck low, calibrate() measured reset-to-first-edge as one wrapping pulse, and the first knock after a reset could boot the application instead of locking — the intermittent autobaud failure. bridge_reset forces a real transition (0 then 1, no cycles between). The README's Autobaud column now carries each chip's worst configuration — autobaud with OSCCAL baked, on a USART's own pins where the chip has one (tinies: autobaud + OSCCAL) — the numbers the existing pureboot_autobaud_osccal[_on_usart0] matrix points already gate; sizes.py checks the column against exactly those targets. Tool sizes and window prose updated with it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
577 lines
31 KiB
CMake
577 lines
31 KiB
CMake
cmake_minimum_required(VERSION 3.28)
|
|
|
|
project(tsb_libavr LANGUAGES CXX)
|
|
|
|
# libavr rides as the pinned submodule; LIBAVR_ROOT (cache or environment)
|
|
# overrides it for tandem development against a working tree. The toolchain
|
|
# file comes from the submodule via CMakePresets.json either way.
|
|
if(NOT LIBAVR_ROOT AND DEFINED ENV{LIBAVR_ROOT})
|
|
set(LIBAVR_ROOT $ENV{LIBAVR_ROOT})
|
|
endif()
|
|
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")
|
|
endif()
|
|
add_subdirectory(${LIBAVR_ROOT} libavr-build)
|
|
|
|
if(PROJECT_IS_TOP_LEVEL)
|
|
add_compile_options(-Werror) # warnings are errors for the port's own code
|
|
enable_testing()
|
|
|
|
# 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
|
|
# 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++)
|
|
find_package(Python3 COMPONENTS Interpreter)
|
|
if(_host_cxx AND Python3_FOUND)
|
|
set(PB_DEVICE ${CMAKE_BINARY_DIR}/pureboot_device)
|
|
execute_process(
|
|
COMMAND ${_host_cxx} -std=c++23 -Wall -Wextra -O2
|
|
-I/usr/include/simavr -I/usr/include/simavr/parts
|
|
-o ${PB_DEVICE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pureboot_device.cpp
|
|
-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")
|
|
unset(PB_DEVICE)
|
|
endif()
|
|
if(LIBAVR_MCU STREQUAL "atmega328p")
|
|
set(TSB_DEVICE ${CMAKE_BINARY_DIR}/tsb_device)
|
|
execute_process(
|
|
COMMAND ${_host_cxx} -std=c++23 -Wall -Wextra -O2
|
|
-I/usr/include/simavr -I/usr/include/simavr/parts
|
|
-o ${TSB_DEVICE} ${CMAKE_CURRENT_SOURCE_DIR}/test/device.cpp
|
|
-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")
|
|
unset(TSB_DEVICE)
|
|
endif()
|
|
endif()
|
|
endif()
|
|
endif()
|
|
|
|
# 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.
|
|
function(add_image_outputs name)
|
|
add_custom_command(TARGET ${name} POST_BUILD
|
|
COMMAND ${CMAKE_OBJCOPY} -O ihex -R .eeprom
|
|
$<TARGET_FILE:${name}> $<TARGET_FILE:${name}>.hex
|
|
COMMAND ${CMAKE_OBJCOPY} -O binary -R .eeprom
|
|
$<TARGET_FILE:${name}> $<TARGET_FILE:${name}>.bin)
|
|
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
|
|
# 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,
|
|
# 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
|
|
# the application's reset vector, pinned to 0 here so the loaders jump to a
|
|
# named function; --pmem-wrap-around lets relaxation turn that absolute jump
|
|
# into the wrapped rjmp AVR's modulo-flash PC actually executes.
|
|
# 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
|
|
# 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
|
|
# 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
|
|
# 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
|
|
# (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
|
|
# variables) with every pureboot lesson applied. 638 B in the
|
|
# 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>)
|
|
function(add_tsb_variant name bytes)
|
|
math(EXPR base_dec "32768 - ${bytes}")
|
|
math(EXPR base_hex "${base_dec}" OUTPUT_FORMAT HEXADECIMAL)
|
|
add_executable(${name} tsb/${name}.cpp)
|
|
target_link_libraries(${name} PRIVATE libavr)
|
|
target_link_options(${name} PRIVATE -nostartfiles -Wl,--section-start=.text=${base_hex}
|
|
-Wl,--defsym=tsb_app=0 -Wl,--pmem-wrap-around=32k)
|
|
add_custom_command(TARGET ${name} POST_BUILD COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${name}>)
|
|
add_image_outputs(${name})
|
|
if(PROJECT_IS_TOP_LEVEL)
|
|
add_test(NAME ${name}.size
|
|
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:${name}>
|
|
-DLIMIT=${bytes} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
|
|
if(DEFINED TSB_DEVICE)
|
|
add_test(NAME ${name}.protocol
|
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/tsbtest.py
|
|
${TSB_DEVICE} $<TARGET_FILE:${name}> ${base_hex})
|
|
endif()
|
|
endif()
|
|
endfunction()
|
|
|
|
# The tsb tiers reimplement the ATmega328P-only reference protocol; the other
|
|
# chips build pureboot alone.
|
|
if(LIBAVR_MCU STREQUAL "atmega328p")
|
|
add_tsb_variant(tsb_asm 512)
|
|
add_tsb_variant(tsb_policy 1024)
|
|
add_tsb_variant(tsb_pure 1024)
|
|
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.
|
|
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,
|
|
# 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 —
|
|
# 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.
|
|
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
|
|
# 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})
|
|
if(PROJECT_IS_TOP_LEVEL)
|
|
get_target_property(_pb_stock_hz pureboot PUREBOOT_HZ)
|
|
get_target_property(_pb_stock_baud pureboot PUREBOOT_BAUD)
|
|
|
|
add_test(NAME pureboot.size
|
|
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:pureboot>
|
|
-DLIMIT=${PUREBOOT_LIMIT} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
|
|
if(Python3_FOUND)
|
|
add_test(NAME pureboot.pi
|
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/check_pi.py
|
|
${CMAKE_OBJDUMP} ${CMAKE_OBJCOPY} ${CMAKE_CXX_COMPILER} ${LIBAVR_MCU}
|
|
$<TARGET_FILE:pureboot>
|
|
${CMAKE_BINARY_DIR}/CMakeFiles/pureboot.dir/pureboot/pureboot.cpp.obj
|
|
${PUREBOOT_BASE_HEX})
|
|
add_test(NAME pureboot.planner
|
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/test_planner.py
|
|
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py)
|
|
add_test(NAME pureboot.scan
|
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/test_scan.py
|
|
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py)
|
|
# CMakePresets.json is generated; hand edits drift the moment the
|
|
# generator reruns, so the gate holds the pair together.
|
|
add_test(NAME presets.generated
|
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/make_presets.py
|
|
--check)
|
|
add_test(NAME pureboot.handshake
|
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/test_handshake.py)
|
|
add_test(NAME pureboot.updatelink
|
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/test_update_link.py)
|
|
endif()
|
|
|
|
# The protocol test flashes this fixture through the loader with the real
|
|
# host tool and expects its banner after the hand-over; a normally linked
|
|
# application whose reset vector is what the tinies' surgery re-homes.
|
|
if(DEFINED PB_DEVICE)
|
|
add_executable(pbapp test/pbapp.cpp)
|
|
target_link_libraries(pbapp PRIVATE libavr)
|
|
add_custom_command(TARGET pbapp POST_BUILD
|
|
COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:pbapp> $<TARGET_FILE:pbapp>.bin)
|
|
add_test(NAME pureboot.protocol
|
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbtest.py
|
|
${PB_DEVICE} $<TARGET_FILE:pureboot> ${PUREBOOT_SIM_MCU} ${_pb_stock_hz}
|
|
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_pb_stock_baud} ${PUREBOOT_EEPROM}
|
|
$<TARGET_FILE:pbapp>.bin ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
|
${CMAKE_BINARY_DIR}/pbtest-work)
|
|
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
|
|
# mis-counted cycle per poll is a 10 % shift).
|
|
add_test(NAME pureboot.window
|
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbwindow.py
|
|
--device ${PB_DEVICE} --loader $<TARGET_FILE:pureboot>
|
|
--mcu ${PUREBOOT_SIM_MCU} --hz ${_pb_stock_hz}
|
|
--base ${PUREBOOT_BASE_HEX} --page ${PUREBOOT_PAGE}
|
|
--baud ${_pb_stock_baud} --app $<TARGET_FILE:pbapp>.bin
|
|
--seconds ${PUREBOOT_TIMEOUT}
|
|
--tool ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
|
--workdir ${CMAKE_BINARY_DIR}/pbwindow-work)
|
|
set_tests_properties(pureboot.window PROPERTIES TIMEOUT 300)
|
|
|
|
# The position-independence acceptance test: the identical image,
|
|
# installed one slot lower, must serve the full command set.
|
|
add_test(NAME pureboot.reloc
|
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbreloc.py
|
|
${PB_DEVICE} $<TARGET_FILE:pureboot> ${PUREBOOT_SIM_MCU} ${_pb_stock_hz}
|
|
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_pb_stock_baud}
|
|
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
|
${CMAKE_BINARY_DIR}/pbreloc-work)
|
|
set_tests_properties(pureboot.reloc PROPERTIES TIMEOUT 180
|
|
ENVIRONMENT "PB_OBJCOPY=${CMAKE_OBJCOPY}")
|
|
|
|
# Entering the loader from a running application with no reset
|
|
# 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.
|
|
if(LIBAVR_MCU STREQUAL "atmega328p")
|
|
add_test(NAME pureboot.dirty
|
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbdirty.py
|
|
${PB_DEVICE} $<TARGET_FILE:pureboot> ${PUREBOOT_SIM_MCU} ${_pb_stock_hz}
|
|
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_pb_stock_baud}
|
|
$<TARGET_FILE:pbapp>.bin
|
|
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
|
${CMAKE_BINARY_DIR}/pbdirty-work)
|
|
set_tests_properties(pureboot.dirty PROPERTIES TIMEOUT 180)
|
|
endif()
|
|
|
|
# Re-homing: a loader mistakenly programmed at address 0 (a raw .bin
|
|
# handed to a programmer) or sitting in the staging slot must heal
|
|
# into the canonical slot through the ordinary --update-loader flow.
|
|
# Patched-vector behavior, so one representative chip carries it.
|
|
if(LIBAVR_MCU STREQUAL "attiny85")
|
|
add_test(NAME pureboot.rehome
|
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbrehome.py
|
|
${PB_DEVICE} $<TARGET_FILE:pureboot> $<TARGET_FILE:pureboot9>.bin
|
|
${PUREBOOT_SIM_MCU} ${_pb_stock_hz} ${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE}
|
|
${_pb_stock_baud} $<TARGET_FILE:pbapp>.bin
|
|
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
|
${CMAKE_BINARY_DIR}/pbrehome-work)
|
|
set_tests_properties(pureboot.rehome PROPERTIES TIMEOUT 180)
|
|
endif()
|
|
|
|
# The self-update end-to-end: the re-timed build (same source, only
|
|
# 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)
|
|
add_test(NAME pureboot.update
|
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbupdate.py
|
|
${PB_DEVICE} $<TARGET_FILE:pureboot> $<TARGET_FILE:pureboot9>
|
|
${PUREBOOT_SIM_MCU} ${_pb_stock_hz} ${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE}
|
|
${_pb_stock_baud} $<TARGET_FILE:pbapp>.bin
|
|
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
|
${CMAKE_BINARY_DIR}/pbupdate-work)
|
|
set_tests_properties(pureboot.update PROPERTIES TIMEOUT 600
|
|
ENVIRONMENT "PB_OBJCOPY=${CMAKE_OBJCOPY}")
|
|
endif()
|
|
|
|
# The size matrix: every configuration axis that could move the image
|
|
# 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
|
|
# 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)
|
|
pureboot_add_loader(${name} ${ARGN})
|
|
add_test(NAME ${name}.size
|
|
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:${name}>
|
|
-DLIMIT=${PUREBOOT_LIMIT} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
|
|
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
|
|
# 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)
|
|
add_test(NAME pureboot_autobaud.size
|
|
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:pureboot_autobaud>
|
|
-DLIMIT=${PUREBOOT_LIMIT} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
|
|
# The measured unit's home is wire contract, not layout accident: the
|
|
# host reads the bit period from it (--info's measured clock). In the
|
|
# GPIOR home the image must carry no RAM copy at all; in the RAM home it
|
|
# is the loader's only RAM object, at the very start of SRAM.
|
|
add_test(NAME pureboot_autobaud.unit
|
|
COMMAND ${CMAKE_COMMAND} -DOBJDUMP=${CMAKE_OBJDUMP} -DELF=$<TARGET_FILE:pureboot_autobaud>
|
|
-DRAM_START=${PUREBOOT_RAM_START} -DGPIOR=${PUREBOOT_UNIT_GPIOR}
|
|
-P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_unit.cmake)
|
|
|
|
# One point of the exhaustive matrix, named from its resolved parameters
|
|
# so the enumeration cannot collide with itself. `pins` is empty for the
|
|
# default pair, or the index of the USART whose own pins a bit-banged
|
|
# link sits on. Unreachable rates drop out here rather than aborting the
|
|
# configure.
|
|
function(pureboot_matrix_point hz baud link pins)
|
|
set(_name pbm_${hz}_${baud}_${link})
|
|
if(link STREQUAL "software")
|
|
pureboot_baud_feasible(${hz} ${baud} 1 _ok)
|
|
set(_args SERIAL software)
|
|
if(NOT pins STREQUAL "")
|
|
list(APPEND _args RX ${PUREBOOT_USART${pins}_RX} TX ${PUREBOOT_USART${pins}_TX})
|
|
set(_name ${_name}_on${pins})
|
|
endif()
|
|
else()
|
|
pureboot_baud_feasible(${hz} ${baud} 0 _ok)
|
|
set(_args USART ${link})
|
|
endif()
|
|
if(_ok)
|
|
pureboot_size_variant(${_name} CLOCK ${hz} BAUD ${baud} ${_args})
|
|
endif()
|
|
endfunction()
|
|
|
|
# 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).
|
|
if(LIBAVR_MCU MATCHES "^attiny13")
|
|
set(_matrix_clocks 1200000 4800000 9600000)
|
|
set(_full_clocks 128000 600000 1200000 4800000 9600000)
|
|
else()
|
|
set(_matrix_clocks 1000000 8000000 16000000)
|
|
set(_full_clocks 128000 1000000 1843200 2000000 3686400 4000000 7372800 8000000
|
|
11059200 12000000 14745600 16000000 18432000 20000000)
|
|
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
|
|
# 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.
|
|
#
|
|
# 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
|
|
# 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)
|
|
list(APPEND _full_bauds 16000 4800 2400 1200)
|
|
if(DEFINED ENV{PUREBOOT_FULL_MATRIX})
|
|
foreach(_matrix_hz IN LISTS _full_clocks)
|
|
foreach(_matrix_baud IN LISTS _full_bauds)
|
|
pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} software "")
|
|
if(PUREBOOT_HAS_USART)
|
|
pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} software 0)
|
|
pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} 0 "")
|
|
endif()
|
|
if(PUREBOOT_HAS_USART1)
|
|
pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} software 1)
|
|
pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} 1 "")
|
|
endif()
|
|
endforeach()
|
|
endforeach()
|
|
else()
|
|
foreach(_matrix_hz IN LISTS _matrix_clocks)
|
|
math(EXPR _matrix_khz "${_matrix_hz} / 1000")
|
|
if(PUREBOOT_HAS_USART OR NOT _matrix_hz EQUAL _pb_stock_hz)
|
|
pureboot_size_variant(pureboot_sw_${_matrix_khz}k CLOCK ${_matrix_hz} SERIAL software)
|
|
endif()
|
|
if(PUREBOOT_HAS_USART AND NOT _matrix_hz EQUAL _pb_stock_hz)
|
|
pureboot_size_variant(pureboot_hw_${_matrix_khz}k CLOCK ${_matrix_hz} SERIAL hardware)
|
|
endif()
|
|
if(PUREBOOT_HAS_USART1 AND NOT _matrix_hz EQUAL _pb_stock_hz)
|
|
pureboot_size_variant(pureboot_usart1_${_matrix_khz}k CLOCK ${_matrix_hz} USART 1)
|
|
endif()
|
|
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
|
|
# against the fastest clock, whose bit spin needs the 16-bit delay
|
|
# 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
|
|
SERIAL software RX ${PUREBOOT_USART0_RX} TX ${PUREBOOT_USART0_TX})
|
|
endif()
|
|
if(PUREBOOT_HAS_USART1)
|
|
pureboot_size_variant(pureboot_sw_wide_on_usart1 CLOCK ${_matrix_top_hz} BAUD 9600
|
|
SERIAL software RX ${PUREBOOT_USART1_RX} TX ${PUREBOOT_USART1_TX})
|
|
endif()
|
|
endif()
|
|
if(PUREBOOT_HAS_USART1)
|
|
pureboot_size_variant(pureboot_usart1 USART 1)
|
|
endif()
|
|
|
|
# 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
|
|
# 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.
|
|
if(PUREBOOT_HAS_USART)
|
|
pureboot_size_variant(pureboot_sw_on_usart0 SERIAL software
|
|
RX ${PUREBOOT_USART0_RX} TX ${PUREBOOT_USART0_TX})
|
|
pureboot_size_variant(pureboot_autobaud_on_usart0 SERIAL autobaud
|
|
RX ${PUREBOOT_USART0_RX} TX ${PUREBOOT_USART0_TX})
|
|
endif()
|
|
if(PUREBOOT_HAS_USART1)
|
|
pureboot_size_variant(pureboot_sw_on_usart1 SERIAL software
|
|
RX ${PUREBOOT_USART1_RX} TX ${PUREBOOT_USART1_TX})
|
|
pureboot_size_variant(pureboot_autobaud_on_usart1 SERIAL autobaud
|
|
RX ${PUREBOOT_USART1_RX} TX ${PUREBOOT_USART1_TX})
|
|
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
|
|
# write, and these points hold both of its addressing encodings to every
|
|
# chip's budget.
|
|
pureboot_size_variant(pureboot_osccal OSCCAL 0x9c)
|
|
pureboot_size_variant(pureboot_autobaud_osccal SERIAL autobaud OSCCAL 0x9c)
|
|
if(PUREBOOT_HAS_USART)
|
|
pureboot_size_variant(pureboot_autobaud_osccal_on_usart0 SERIAL autobaud OSCCAL 0x9c
|
|
RX ${PUREBOOT_USART0_RX} TX ${PUREBOOT_USART0_TX})
|
|
endif()
|
|
|
|
# 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).
|
|
if(LIBAVR_MCU MATCHES "^(atmega328p|attiny85)$" AND DEFINED PB_DEVICE)
|
|
if(LIBAVR_MCU STREQUAL "atmega328p")
|
|
set(_osccal_addr 0x66)
|
|
else()
|
|
set(_osccal_addr 0x51)
|
|
endif()
|
|
get_target_property(_osccal_hz pureboot_osccal PUREBOOT_HZ)
|
|
get_target_property(_osccal_baud pureboot_osccal PUREBOOT_BAUD)
|
|
add_test(NAME pureboot.osccal
|
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbosccal.py
|
|
${PB_DEVICE} $<TARGET_FILE:pureboot_osccal> ${PUREBOOT_SIM_MCU}
|
|
${_osccal_hz} ${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_osccal_baud}
|
|
${_osccal_addr} 0x9c ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
|
${CMAKE_BINARY_DIR}/pbosccal-work)
|
|
set_tests_properties(pureboot.osccal PROPERTIES TIMEOUT 120)
|
|
endif()
|
|
|
|
# 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
|
|
# configuration plumbing produces a working loader, not just one that
|
|
# fits.
|
|
if(LIBAVR_MCU STREQUAL "atmega328p" AND DEFINED PB_DEVICE)
|
|
pureboot_size_variant(pureboot_custom CLOCK 1000000 SERIAL software RX pb5 TX pb1)
|
|
get_target_property(_custom_hz pureboot_custom PUREBOOT_HZ)
|
|
get_target_property(_custom_baud pureboot_custom PUREBOOT_BAUD)
|
|
get_target_property(_custom_link pureboot_custom PUREBOOT_LINK)
|
|
add_executable(pbapp_custom test/pbapp.cpp)
|
|
target_link_libraries(pbapp_custom PRIVATE libavr)
|
|
target_compile_definitions(pbapp_custom PRIVATE PUREBOOT_CLOCK_HZ=${_custom_hz}
|
|
PUREBOOT_BAUD=${_custom_baud} PUREBOOT_SOFT_SERIAL PUREBOOT_TX=pb1)
|
|
add_custom_command(TARGET pbapp_custom POST_BUILD
|
|
COMMAND ${CMAKE_OBJCOPY} -O binary
|
|
$<TARGET_FILE:pbapp_custom> $<TARGET_FILE:pbapp_custom>.bin)
|
|
add_test(NAME pureboot.custom
|
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbtest.py
|
|
${PB_DEVICE} $<TARGET_FILE:pureboot_custom> ${PUREBOOT_SIM_MCU} ${_custom_hz}
|
|
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_custom_baud} ${PUREBOOT_EEPROM}
|
|
$<TARGET_FILE:pbapp_custom>.bin ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
|
${CMAKE_BINARY_DIR}/pbcustom-work ${_custom_link})
|
|
set_tests_properties(pureboot.custom PROPERTIES TIMEOUT 180)
|
|
endif()
|
|
|
|
# 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
|
|
# on silicon; the runner supplies the pin ownership simavr has no model
|
|
# for, which is what lets this fail when the release is gone.
|
|
if(LIBAVR_MCU STREQUAL "atmega328p" AND DEFINED PB_DEVICE)
|
|
get_target_property(_mute_hz pureboot_sw_on_usart0 PUREBOOT_HZ)
|
|
get_target_property(_mute_baud pureboot_sw_on_usart0 PUREBOOT_BAUD)
|
|
get_target_property(_mute_link pureboot_sw_on_usart0 PUREBOOT_LINK)
|
|
add_executable(pbapp_handover test/pbapp.cpp)
|
|
target_link_libraries(pbapp_handover PRIVATE libavr)
|
|
target_compile_definitions(pbapp_handover PRIVATE PUREBOOT_CLOCK_HZ=${_mute_hz}
|
|
PUREBOOT_BAUD=${_mute_baud} PUREBOOT_HANDOVER)
|
|
add_custom_command(TARGET pbapp_handover POST_BUILD
|
|
COMMAND ${CMAKE_OBJCOPY} -O binary
|
|
$<TARGET_FILE:pbapp_handover> $<TARGET_FILE:pbapp_handover>.bin)
|
|
add_test(NAME pureboot.mute
|
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbmute.py
|
|
${PB_DEVICE} $<TARGET_FILE:pureboot_sw_on_usart0> ${PUREBOOT_SIM_MCU} ${_mute_hz}
|
|
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_mute_baud}
|
|
$<TARGET_FILE:pbapp_handover>.bin
|
|
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
|
${CMAKE_BINARY_DIR}/pbmute-work ${_mute_link})
|
|
set_tests_properties(pureboot.mute PROPERTIES TIMEOUT 180)
|
|
endif()
|
|
|
|
# The second USART, driven for real on one chip: instance selection is
|
|
# compile-checked everywhere, but only a live session proves the loader
|
|
# initialized and polls the USART it claims to. The fixture application
|
|
# banners on the same instance.
|
|
if(LIBAVR_MCU STREQUAL "atmega644a" AND DEFINED PB_DEVICE)
|
|
get_target_property(_usart1_hz pureboot_usart1 PUREBOOT_HZ)
|
|
get_target_property(_usart1_baud pureboot_usart1 PUREBOOT_BAUD)
|
|
add_executable(pbapp_usart1 test/pbapp.cpp)
|
|
target_link_libraries(pbapp_usart1 PRIVATE libavr)
|
|
target_compile_definitions(pbapp_usart1 PRIVATE PUREBOOT_CLOCK_HZ=${_usart1_hz}
|
|
PUREBOOT_BAUD=${_usart1_baud} PUREBOOT_USART=1)
|
|
add_custom_command(TARGET pbapp_usart1 POST_BUILD
|
|
COMMAND ${CMAKE_OBJCOPY} -O binary
|
|
$<TARGET_FILE:pbapp_usart1> $<TARGET_FILE:pbapp_usart1>.bin)
|
|
add_test(NAME pureboot.usart1
|
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbtest.py
|
|
${PB_DEVICE} $<TARGET_FILE:pureboot_usart1> ${PUREBOOT_SIM_MCU} ${_usart1_hz}
|
|
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_usart1_baud} ${PUREBOOT_EEPROM}
|
|
$<TARGET_FILE:pbapp_usart1>.bin ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
|
${CMAKE_BINARY_DIR}/pbusart1-work usart1)
|
|
set_tests_properties(pureboot.usart1 PROPERTIES TIMEOUT 180)
|
|
endif()
|
|
|
|
# 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
|
|
# 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.
|
|
if(LIBAVR_MCU MATCHES "^atmega(328p|1284p)$" AND DEFINED PB_DEVICE)
|
|
add_executable(pbapp_autobaud test/pbapp.cpp)
|
|
target_link_libraries(pbapp_autobaud PRIVATE libavr)
|
|
target_compile_definitions(pbapp_autobaud PRIVATE PUREBOOT_CLOCK_HZ=1000000
|
|
PUREBOOT_BAUD=9600 PUREBOOT_SOFT_SERIAL PUREBOOT_TX=pb1)
|
|
add_custom_command(TARGET pbapp_autobaud POST_BUILD
|
|
COMMAND ${CMAKE_OBJCOPY} -O binary
|
|
$<TARGET_FILE:pbapp_autobaud> $<TARGET_FILE:pbapp_autobaud>.bin)
|
|
add_test(NAME pureboot.autobaud
|
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbautobaud.py
|
|
${PB_DEVICE} $<TARGET_FILE:pureboot_autobaud> ${PUREBOOT_SIM_MCU}
|
|
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} $<TARGET_FILE:pbapp_autobaud>.bin
|
|
1000000 9600 ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
|
${CMAKE_BINARY_DIR}/pbautobaud-work)
|
|
set_tests_properties(pureboot.autobaud PROPERTIES TIMEOUT 240)
|
|
|
|
# The autobaud window: the calibration poll budget, at the measured
|
|
# 10 cycles a poll (pbwindow.py pins the constant the README's
|
|
# seconds arithmetic uses; the budget itself is the clock-free knob).
|
|
add_test(NAME pureboot.window.autobaud
|
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbwindow.py
|
|
--device ${PB_DEVICE} --loader $<TARGET_FILE:pureboot_autobaud>
|
|
--mcu ${PUREBOOT_SIM_MCU} --hz 1000000
|
|
--base ${PUREBOOT_BASE_HEX} --page ${PUREBOOT_PAGE}
|
|
--baud 9600 --app $<TARGET_FILE:pbapp_autobaud>.bin
|
|
--autobaud-polls 4000000 --link sw
|
|
--tool ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
|
--workdir ${CMAKE_BINARY_DIR}/pbwindow-autobaud-work)
|
|
set_tests_properties(pureboot.window.autobaud PROPERTIES TIMEOUT 300)
|
|
endif()
|
|
endif()
|