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 $ $.hex COMMAND ${CMAKE_OBJCOPY} -O binary -R .eeprom $ $.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". # tsb_asm - the tricks tier's C++ with exactly two routines in asm: the # bounded rx and the page-store loop, the two whose remaining # cost is the C ABI itself. Everything else, bring-up to # dispatch, is C++ on libavr. # tsb_tricks - no asm at all: the whole-loader register allocation lives in # global register variables (Y walks the page pointer), every # helper is a tiny noinline primitive placed by the # global-register store rules, pages stream straight to # SPM/EEPROM. # tsb_pure - pure idiomatic libavr, one function per command, TU-local # (internal linkage), streaming (no SRAM page buffer). # tsb_policy - the policy floor: pureboot's rules (no asm, no register # variables) with every pureboot lesson applied, and the # measured evidence that the 512 B fit is a property of the # mechanisms philosophy #5 bans. # # What each measures is oracle/README.md's table, which is the one place the # four numbers and the hand-written loader's own are compared. # # add_tsb_variant( ) 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} $) add_image_outputs(${name}) if(PROJECT_IS_TOP_LEVEL) add_test(NAME ${name}.size COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$ -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} $ ${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=$ -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} $ ${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 $ $.bin) add_test(NAME pureboot.protocol COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbtest.py ${PB_DEVICE} $ ${PUREBOOT_SIM_MCU} ${_pb_stock_hz} ${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_pb_stock_baud} ${PUREBOOT_EEPROM} $.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 $ --mcu ${PUREBOOT_SIM_MCU} --hz ${_pb_stock_hz} --base ${PUREBOOT_BASE_HEX} --page ${PUREBOOT_PAGE} --baud ${_pb_stock_baud} --app $.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 half-duplex loader's window, same gate: its poll runs through # 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 COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbwindow.py --device ${PB_DEVICE} --loader $ --mcu ${PUREBOOT_SIM_MCU} --hz ${_pb_stock_hz} --base ${PUREBOOT_BASE_HEX} --page ${PUREBOOT_PAGE} --baud ${_pb_stock_baud} --app $.bin --seconds ${PUREBOOT_TIMEOUT} --tool ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py --workdir ${CMAKE_BINARY_DIR}/pbwindow-hd-work) set_tests_properties(pureboot.window.halfduplex PROPERTIES TIMEOUT 300) endif() # 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} $ ${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}") # The seal against the one command that proves it: erasing the page the # loader runs from, refused unsealed and honoured sealed. Destroys the # loader by design, so it gets a device of its own. add_test(NAME pureboot.selfwrite COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbselfwrite.py ${PB_DEVICE} $ ${PUREBOOT_SIM_MCU} ${_pb_stock_hz} ${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_pb_stock_baud} ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py ${CMAKE_BINARY_DIR}/pbselfwrite-work) set_tests_properties(pureboot.selfwrite PROPERTIES TIMEOUT 180) # The seal against a link that damages bytes on purpose: every header # field flipped after sealing must be refused, and the identical flip # applied before sealing must be obeyed. add_test(NAME pureboot.glitch COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbglitch.py ${PB_DEVICE} $ ${PUREBOOT_SIM_MCU} ${_pb_stock_hz} ${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_pb_stock_baud} ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py ${CMAKE_BINARY_DIR}/pbglitch-work) 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 # 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} $ ${PUREBOOT_SIM_MCU} ${_pb_stock_hz} ${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_pb_stock_baud} $.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} $ $.bin ${PUREBOOT_SIM_MCU} ${_pb_stock_hz} ${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_pb_stock_baud} $.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} $ $ ${PUREBOOT_SIM_MCU} ${_pb_stock_hz} ${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_pb_stock_baud} $.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=$ -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=$ -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=$ -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. # The optional trailing argument is the one-wire shape of the same link: # ONE_WIRE folds a software point onto its RX pin (the default, or the # named USART's RXD), HALF_DUPLEX is the hardware USART's turn-around. 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() if(ARGC GREATER 4 AND ARGV4 STREQUAL "ONE_WIRE") if(NOT pins STREQUAL "") set(_args SERIAL software RX ${PUREBOOT_USART${pins}_RX} TX ${PUREBOOT_USART${pins}_RX}) else() list(APPEND _args RX pb0 TX pb0) endif() set(_name ${_name}_1w) endif() else() pureboot_baud_feasible(${hz} ${baud} 0 _ok) set(_args USART ${link}) if(ARGC GREATER 4 AND ARGV4 STREQUAL "HALF_DUPLEX") list(APPEND _args HALF_DUPLEX) set(_name ${_name}_hd) endif() 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 "") pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} software "" ONE_WIRE) if(PUREBOOT_HAS_USART) pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} software 0) pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} software 0 ONE_WIRE) pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} 0 "") pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} 0 "" HALF_DUPLEX) endif() if(PUREBOOT_HAS_USART1) pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} software 1) pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} software 1 ONE_WIRE) pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} 1 "") pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} 1 "" HALF_DUPLEX) 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 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 # release the now-driven shared pin needs where a receive-only link # 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. pureboot_size_variant(pureboot_1w SERIAL software RX pb0 HALF_DUPLEX) pureboot_size_variant(pureboot_1w_autobaud_osccal SERIAL autobaud OSCCAL 0x9c RX pb0 TX pb0) if(PUREBOOT_HAS_USART) pureboot_size_variant(pureboot_1w_on_usart0 SERIAL software RX ${PUREBOOT_USART0_RX} TX ${PUREBOOT_USART0_RX}) pureboot_size_variant(pureboot_1w_autobaud_osccal_on_usart0 SERIAL autobaud OSCCAL 0x9c RX ${PUREBOOT_USART0_RX} TX ${PUREBOOT_USART0_RX}) pureboot_size_variant(pureboot_hd HALF_DUPLEX) list(GET _matrix_clocks -1 _hd_top_hz) pureboot_size_variant(pureboot_hd_wide CLOCK ${_hd_top_hz} BAUD 9600 HALF_DUPLEX) endif() if(PUREBOOT_HAS_USART1) pureboot_size_variant(pureboot_usart1_hd USART 1 HALF_DUPLEX) 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 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) 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} $ ${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 $ $.bin) add_test(NAME pureboot.custom COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbtest.py ${PB_DEVICE} $ ${PUREBOOT_SIM_MCU} ${_custom_hz} ${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_custom_baud} ${PUREBOOT_EEPROM} $.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 $ $.bin) add_test(NAME pureboot.mute COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbmute.py ${PB_DEVICE} $ ${PUREBOOT_SIM_MCU} ${_mute_hz} ${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_mute_baud} $.bin ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py ${CMAKE_BINARY_DIR}/pbmute-work ${_mute_link}) set_tests_properties(pureboot.mute PROPERTIES TIMEOUT 180) # The same hand-over against the one-wire deployment on that USART's # RXD: RXEN forces the shared pin's direction, so a loader that only # released the transmit-side hold would read the wire and answer into # a pin it cannot drive. The host runs with the --one-wire echo # discard, which the bridge's shared-line model feeds for real. get_target_property(_mute1w_link pureboot_1w_on_usart0 PUREBOOT_LINK) add_test(NAME pureboot.mute.onewire COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbmute.py ${PB_DEVICE} $ ${PUREBOOT_SIM_MCU} ${_mute_hz} ${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_mute_baud} $.bin ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py ${CMAKE_BINARY_DIR}/pbmute-1w-work ${_mute1w_link}) set_tests_properties(pureboot.mute.onewire PROPERTIES TIMEOUT 180) # The full protocol suite over one shared pin: the loader folded onto # PB0, the bridge following the pin's direction, the fixture # bannering as a guest on the same line, and the host discarding its # own echo throughout. get_target_property(_1w_hz pureboot_1w PUREBOOT_HZ) get_target_property(_1w_baud pureboot_1w PUREBOOT_BAUD) get_target_property(_1w_link pureboot_1w PUREBOOT_LINK) add_executable(pbapp_1w test/pbapp.cpp) target_link_libraries(pbapp_1w PRIVATE libavr) target_compile_definitions(pbapp_1w PRIVATE PUREBOOT_CLOCK_HZ=${_1w_hz} PUREBOOT_BAUD=${_1w_baud} PUREBOOT_SOFT_SERIAL PUREBOOT_RX=pb0 PUREBOOT_TX=pb0) add_custom_command(TARGET pbapp_1w POST_BUILD COMMAND ${CMAKE_OBJCOPY} -O binary $ $.bin) add_test(NAME pureboot.onewire COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbtest.py ${PB_DEVICE} $ ${PUREBOOT_SIM_MCU} ${_1w_hz} ${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_1w_baud} ${PUREBOOT_EEPROM} $.bin ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py ${CMAKE_BINARY_DIR}/pb1w-work ${_1w_link}) 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 # 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 # hardware bench's item. add_test(NAME pureboot.halfduplex COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbtest.py ${PB_DEVICE} $ ${PUREBOOT_SIM_MCU} ${_pb_stock_hz} ${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_pb_stock_baud} ${PUREBOOT_EEPROM} $.bin ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py ${CMAKE_BINARY_DIR}/pbhd-work) set_tests_properties(pureboot.halfduplex 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 $ $.bin) add_test(NAME pureboot.usart1 COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbtest.py ${PB_DEVICE} $ ${PUREBOOT_SIM_MCU} ${_usart1_hz} ${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_usart1_baud} ${PUREBOOT_EEPROM} $.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 $ $.bin) add_test(NAME pureboot.autobaud COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbautobaud.py ${PB_DEVICE} $ ${PUREBOOT_SIM_MCU} ${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} $.bin 1000000 9600 ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py ${CMAKE_BINARY_DIR}/pbautobaud-work) set_tests_properties(pureboot.autobaud PROPERTIES TIMEOUT 240) # 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 # echo discard, over the same two-clock sweep. One chip carries it; # the shape is chip-independent. if(LIBAVR_MCU STREQUAL "atmega328p") get_target_property(_ab1w_link pureboot_1w_autobaud_osccal_on_usart0 PUREBOOT_LINK) add_executable(pbapp_autobaud_1w test/pbapp.cpp) target_link_libraries(pbapp_autobaud_1w PRIVATE libavr) target_compile_definitions(pbapp_autobaud_1w PRIVATE PUREBOOT_CLOCK_HZ=1000000 PUREBOOT_BAUD=9600 PUREBOOT_SOFT_SERIAL PUREBOOT_RX=pd0 PUREBOOT_TX=pd0) add_custom_command(TARGET pbapp_autobaud_1w POST_BUILD COMMAND ${CMAKE_OBJCOPY} -O binary $ $.bin) add_test(NAME pureboot.autobaud.onewire COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbautobaud.py ${PB_DEVICE} $ ${PUREBOOT_SIM_MCU} ${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} $.bin 1000000 9600 ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py ${CMAKE_BINARY_DIR}/pbautobaud-1w-work ${_ab1w_link}) set_tests_properties(pureboot.autobaud.onewire PROPERTIES TIMEOUT 240) endif() # 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 $ --mcu ${PUREBOOT_SIM_MCU} --hz 1000000 --base ${PUREBOOT_BASE_HEX} --page ${PUREBOOT_PAGE} --baud 9600 --app $.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()