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) include(${LIBAVR_ROOT}/cmake/checks.cmake) if(PROJECT_IS_TOP_LEVEL) add_compile_options(-Werror) # warnings are errors for the port's own code enable_testing() # Rules 11 and 33 over this repo's own sources. The oracle's assembly needs # no exclusion: it is neither formatted nor ASCII-checked, being in neither # glob, which is the right answer for a vendored reference whose text is # the artifact. libavr_format_test() # The behavioral tests drive the real wire protocols over a simavr pty # (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.** They used to be left out, which makes the suite a different size # on a different machine - and a suite whose size is a property of the # machine is one nothing can be compared against. 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( 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) # One bounded line of it: this becomes a single argument on a # command line, and the reading has to say what stopped the build # 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() libavr_launcher(_tsb_python "${_tsb_absent}" ${Python3_EXECUTABLE}) if(_tsb_absent) message(STATUS "the protocol tests skip here - ${_tsb_absent}") 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: no inline assembly and no global register # variables, which is philosophy #5's own bound, and the # measured evidence that the 512 B fit is a property of the # mechanisms it bans. # # What each measures is oracle/README.md's table, which is the one place the # four numbers and the hand-written loader's own are compared. # # add_tsb_variant( ) 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) add_test(NAME ${name}.protocol COMMAND ${_tsb_python} ${CMAKE_CURRENT_SOURCE_DIR}/test/tsbtest.py ${TSB_DEVICE} $ ${base_hex}) endif() 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_policy 1024) add_tsb_variant(tsb_pure 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()