cmake_minimum_required(VERSION 3.28) project(tsb_libavr LANGUAGES CXX) # libavr from a local checkout (LIBAVR_ROOT) or the forge; the toolchain file # comes from the same checkout via CMakePresets.json. include(FetchContent) if(NOT LIBAVR_ROOT AND DEFINED ENV{LIBAVR_ROOT}) set(LIBAVR_ROOT $ENV{LIBAVR_ROOT}) endif() if(LIBAVR_ROOT) FetchContent_Declare(libavr SOURCE_DIR ${LIBAVR_ROOT}) else() FetchContent_Declare(libavr GIT_REPOSITORY git@git.blackmark.me:avr/libavr.git GIT_TAG main) endif() FetchContent_MakeAvailable(libavr) 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; if they or # Python are missing, only the size tests run. find_program(_host_cc NAMES cc gcc) find_package(Python3 COMPONENTS Interpreter) if(_host_cc AND Python3_FOUND) set(PB_DEVICE ${CMAKE_BINARY_DIR}/pureboot_device) execute_process( COMMAND ${_host_cc} -O2 -I/usr/include/simavr -I/usr/include/simavr/parts -o ${PB_DEVICE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pureboot_device.c -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_cc} -O2 -I/usr/include/simavr -I/usr/include/simavr/parts -o ${TSB_DEVICE} ${CMAKE_CURRENT_SOURCE_DIR}/test/device.c -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 three 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 naked entry sits in # .vectors, laid first, and runs. The boot base is FLASHEND+1 minus the section # size; the linker section-start and the source's boot_bytes agree. tsb_app is # the 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 three 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. # # 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_pure 1024) add_tsb_variant(tsb_tricks 1024) endif() # pureboot — the pure-constraint port (see pureboot/README.md): one source, # no inline assembly, no global register variables, every libavr chip, 512 # bytes each. The loader owns the top 512 bytes of flash on every chip; the # application entry symbol is address 0 on the mega (reset re-vectors to the # loader through BOOTRST, so word 0 stays the application's own vector) and # the trampoline word just below the loader on the tinies (host-side vector # surgery points it at the application). --pmem-wrap-around models AVR's # modulo-flash PC where the flash is big enough to need it. # # The image is position-independent (check_pi.py asserts the two link-time # facts that make it so), and on the tinies its budget is 510, not 512: the # slot's last word is the trampoline the host composes — the resident slot's # holds the application entry, and a staging copy's holds the jump through # which it reaches the loader it installed. The activation window is a # compile-time constant; a different PUREBOOT_TIMEOUT builds the re-timed # binary a self-update then installs. set(PUREBOOT_TIMEOUT 8 CACHE STRING "pureboot activation window, seconds") # Every mega runs the loader from its hardware boot section and boots the # application at word 0; the tinies get the trampoline surgery. All megas # assume a 16 MHz crystal at 115200 Bd; the tinies their internal RC at # 57600 Bd over the software UART. if(LIBAVR_MCU STREQUAL "attiny13a") set(_pb_flash 1024) set(_pb_wrap "") set(_pb_page 32) set(_pb_hz 9600000) set(_pb_baud 57600) set(_pb_eeprom 64) set(_pb_limit 510) elseif(LIBAVR_MCU STREQUAL "attiny85") set(_pb_flash 8192) set(_pb_wrap -Wl,--pmem-wrap-around=8k) set(_pb_page 64) set(_pb_hz 8000000) set(_pb_baud 57600) set(_pb_eeprom 512) set(_pb_limit 510) elseif(LIBAVR_MCU MATCHES "^atmega8a?$") set(_pb_flash 8192) set(_pb_wrap -Wl,--pmem-wrap-around=8k) set(_pb_page 64) set(_pb_hz 16000000) set(_pb_baud 115200) set(_pb_eeprom 512) set(_pb_limit 512) elseif(LIBAVR_MCU STREQUAL "atmega16") set(_pb_flash 16384) set(_pb_wrap -Wl,--pmem-wrap-around=16k) set(_pb_page 128) set(_pb_hz 16000000) set(_pb_baud 115200) set(_pb_eeprom 512) set(_pb_limit 512) elseif(LIBAVR_MCU MATCHES "^atmega32a?$") set(_pb_flash 32768) set(_pb_wrap -Wl,--pmem-wrap-around=32k) set(_pb_page 128) set(_pb_hz 16000000) set(_pb_baud 115200) set(_pb_eeprom 1024) set(_pb_limit 512) elseif(LIBAVR_MCU STREQUAL "atmega168a") set(_pb_flash 16384) set(_pb_wrap -Wl,--pmem-wrap-around=16k) set(_pb_page 128) set(_pb_hz 16000000) set(_pb_baud 115200) set(_pb_eeprom 512) set(_pb_limit 512) elseif(LIBAVR_MCU STREQUAL "atmega1284p") # 128 KiB: wire flash addresses are word addresses, reads go through # ELPM, and the PC's modulo wrap exceeds what --pmem-wrap-around models. # The slot is 1 KiB — this chip's own smallest boot sector; the far # machinery cannot fit 512 B (see pureboot/README.md). set(_pb_flash 131072) set(_pb_wrap "") set(_pb_page 256) set(_pb_hz 16000000) set(_pb_baud 115200) set(_pb_eeprom 4096) set(_pb_limit 1024) set(_pb_slot 1024) else() set(_pb_flash 32768) set(_pb_wrap -Wl,--pmem-wrap-around=32k) set(_pb_page 128) set(_pb_hz 16000000) set(_pb_baud 115200) set(_pb_eeprom 1024) set(_pb_limit 512) endif() if(NOT DEFINED _pb_slot) set(_pb_slot 512) endif() math(EXPR _pb_base "${_pb_flash} - ${_pb_slot}") math(EXPR _pb_base_hex "${_pb_base}" OUTPUT_FORMAT HEXADECIMAL) if(LIBAVR_MCU MATCHES "^atmega") set(_pb_app 0) else() math(EXPR _pb_app "${_pb_base} - 2") endif() # simavr names its cores after the base dies; the A revisions run on them. set(_pb_sim_mcu ${LIBAVR_MCU}) if(LIBAVR_MCU STREQUAL "atmega8a") set(_pb_sim_mcu atmega8) elseif(LIBAVR_MCU STREQUAL "atmega32a") set(_pb_sim_mcu atmega32) elseif(LIBAVR_MCU STREQUAL "atmega168a") set(_pb_sim_mcu atmega168) endif() add_executable(pureboot pureboot/pureboot.cpp) target_link_libraries(pureboot PRIVATE libavr) target_compile_definitions(pureboot PRIVATE PUREBOOT_TIMEOUT=${PUREBOOT_TIMEOUT}) target_link_options(pureboot PRIVATE -nostartfiles -Wl,--section-start=.text=${_pb_base_hex} -Wl,--defsym=pureboot_app=${_pb_app} ${_pb_wrap}) add_custom_command(TARGET pureboot POST_BUILD COMMAND ${CMAKE_SIZE} $) add_image_outputs(pureboot) if(PROJECT_IS_TOP_LEVEL) add_test(NAME pureboot.size COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$ -DLIMIT=${_pb_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_NM} $ ${_pb_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) 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} $ ${_pb_sim_mcu} ${_pb_hz} ${_pb_base_hex} ${_pb_page} ${_pb_baud} ${_pb_eeprom} $.bin ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py ${CMAKE_BINARY_DIR}/pbtest-work) set_tests_properties(pureboot.protocol PROPERTIES TIMEOUT 180) # 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} $ ${_pb_sim_mcu} ${_pb_hz} ${_pb_base_hex} ${_pb_page} ${_pb_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 self-update end-to-end: the re-timed build (same source, only # PUREBOOT_TIMEOUT differs — a byte-different image) replaces the # resident through --update-loader, with every power-fail phase # rehearsed from the runner's flash dumps. add_executable(pureboot9 pureboot/pureboot.cpp) target_link_libraries(pureboot9 PRIVATE libavr) target_compile_definitions(pureboot9 PRIVATE PUREBOOT_TIMEOUT=9) target_link_options(pureboot9 PRIVATE -nostartfiles -Wl,--section-start=.text=${_pb_base_hex} -Wl,--defsym=pureboot_app=${_pb_app} ${_pb_wrap}) add_image_outputs(pureboot9) add_test(NAME pureboot.update COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbupdate.py ${PB_DEVICE} $ $ ${_pb_sim_mcu} ${_pb_hz} ${_pb_base_hex} ${_pb_page} ${_pb_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() endif()