The far machinery (ELPM reads, RAMPZ page commands, wire-word math) costs ~46 B over the m328P's 504, and the tsb-calibrated C++-to-asm gap says no implementation of this feature set reaches 512 on this chip — a boundary its hardware does not have anyway: the 1284P's smallest boot sector is 1 KiB. The slot therefore becomes per-geometry (512 B, or 1 KiB past 64 KiB), which the host derives from the word-addressing flag; slot arithmetic unifies (the index is the wire high byte with its low bit dropped in either unit), the update preflight demands a two-slot boot section in the chip's own terms, and pbapp's hand-back jumps to the real slot base. libavr's far primitives split their RAMPZ/Z asm operands (a page never crosses 64 KiB, so callers keep a byte and a 16-bit cursor — the 32-bit address folds away; flash_load_far's byte form becomes the out-RAMPZ+elpm pair avr-libc's pgm_read_byte_far rebuilds per call), and the host splits reads at 64 KiB boundaries. All ten chips pass the full suite — the 1284P at 558 B including protocol, relocation, and the power-fail self-update — with pureboot byte-identical across generated and reflect modes everywhere, and the original three chips' images unchanged to the byte (488/502/504). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
306 lines
14 KiB
CMake
306 lines
14 KiB
CMake
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
|
|
$<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 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(<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_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} $<TARGET_FILE:pureboot>)
|
|
add_image_outputs(pureboot)
|
|
if(PROJECT_IS_TOP_LEVEL)
|
|
add_test(NAME pureboot.size
|
|
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:pureboot>
|
|
-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} $<TARGET_FILE:pureboot> ${_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 $<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> ${_pb_sim_mcu} ${_pb_hz} ${_pb_base_hex}
|
|
${_pb_page} ${_pb_baud} ${_pb_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 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> ${_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} $<TARGET_FILE:pureboot> $<TARGET_FILE:pureboot9> ${_pb_sim_mcu}
|
|
${_pb_hz} ${_pb_base_hex} ${_pb_page} ${_pb_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()
|
|
endif()
|