diff --git a/CMakeLists.txt b/CMakeLists.txt index bec2e91..80f0bd3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -126,154 +126,31 @@ if(LIBAVR_MCU STREQUAL "atmega328p") 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. +# 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") -# Per-family geometry. The boot-sectioned megas run the loader from the -# hardware boot section and boot the application at word 0; the tinies and -# the boot-section-less m48s 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. --pmem-wrap-around models AVR's modulo-flash PC where -# the flash is big enough to need it (an rjmp reaches all of 4 KiB by -# itself). -if(LIBAVR_MCU MATCHES "^attiny13a?$") - set(_pb_flash 1024) - set(_pb_wrap "") - set(_pb_page 32) - set(_pb_hz 9600000) - set(_pb_baud 57600) - set(_pb_eeprom 64) -elseif(LIBAVR_MCU STREQUAL "attiny25") - set(_pb_flash 2048) - set(_pb_wrap "") - set(_pb_page 32) - set(_pb_hz 8000000) - set(_pb_baud 57600) - set(_pb_eeprom 128) -elseif(LIBAVR_MCU STREQUAL "attiny45") - set(_pb_flash 4096) - set(_pb_wrap "") - set(_pb_page 64) - set(_pb_hz 8000000) - set(_pb_baud 57600) - set(_pb_eeprom 256) -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) -elseif(LIBAVR_MCU MATCHES "^atmega48(a|p|pa)?$") - set(_pb_flash 4096) - set(_pb_wrap "") - set(_pb_page 64) - set(_pb_hz 16000000) - set(_pb_baud 115200) - set(_pb_eeprom 256) -elseif(LIBAVR_MCU MATCHES "^atmega8a?$" OR LIBAVR_MCU MATCHES "^atmega88(a|p|pa)?$") - 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) -elseif(LIBAVR_MCU MATCHES "^atmega16a?$" OR LIBAVR_MCU MATCHES "^atmega168(a|p|pa)?$" OR - LIBAVR_MCU MATCHES "^atmega164(a|p|pa)$") - 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) -elseif(LIBAVR_MCU MATCHES "^atmega32a?$" OR LIBAVR_MCU MATCHES "^atmega328p?$" OR - LIBAVR_MCU MATCHES "^atmega324(a|p|pa)$") - 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) -elseif(LIBAVR_MCU MATCHES "^atmega644(a|p|pa)?$") - # 64 KiB is exactly the 16-bit byte space: plain LPM reaches everything, - # and the smallest boot section (1 KiB) holds the loader and its staging - # slot together (see pureboot/README.md). - set(_pb_flash 65536) - set(_pb_wrap -Wl,--pmem-wrap-around=64k) - set(_pb_page 256) - set(_pb_hz 16000000) - set(_pb_baud 115200) - set(_pb_eeprom 2048) -elseif(LIBAVR_MCU MATCHES "^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_slot 1024) - set(_pb_limit 1024) -else() - message(FATAL_ERROR "pureboot: no geometry for ${LIBAVR_MCU}") -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) -# Patched-vector chips hand over through the trampoline word below the slot, -# which is also the slot's own last word — their budget is slot − 2. -if(LIBAVR_MCU MATCHES "^atmega" AND NOT LIBAVR_MCU MATCHES "^atmega48") - set(_pb_app 0) - if(NOT DEFINED _pb_limit) - set(_pb_limit ${_pb_slot}) - endif() -else() - math(EXPR _pb_app "${_pb_base} - 2") - math(EXPR _pb_limit "${_pb_slot} - 2") -endif() - -# simavr names its cores after the base dies; the A revisions run on them -# (the 644PA on the 644P core). -set(_pb_sim_mcu ${LIBAVR_MCU}) -if(LIBAVR_MCU MATCHES "^atmega(8|16|32|48|88|164|168|644)a$") - string(REGEX REPLACE "a$" "" _pb_sim_mcu ${LIBAVR_MCU}) -elseif(LIBAVR_MCU STREQUAL "atmega644pa") - set(_pb_sim_mcu atmega644p) -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) +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=${_pb_limit} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake) + -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_NM} $ ${_pb_base_hex}) + ${CMAKE_OBJDUMP} ${CMAKE_NM} $ ${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) @@ -289,9 +166,9 @@ if(PROJECT_IS_TOP_LEVEL) 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 + ${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) @@ -299,43 +176,127 @@ if(PROJECT_IS_TOP_LEVEL) # 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 + ${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}") # Re-homing: a loader mistakenly programmed at address 0 (a raw .bin - # handed to a programmer) must heal into the canonical slot through - # the ordinary --update-loader flow. Patched-vector behavior, so one - # representative chip carries it. + # 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 ${_pb_sim_mcu} - ${_pb_hz} ${_pb_base_hex} ${_pb_page} ${_pb_baud} $.bin + ${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 - # 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) + # 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} $ $ ${_pb_sim_mcu} - ${_pb_hz} ${_pb_base_hex} ${_pb_page} ${_pb_baud} $.bin + ${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 clock and its ladder + # baud (different constants and divisor shapes), the USART instance + # (different register class) — each combination must still fit the + # chip's slot budget. Pins are size-neutral (port and bit are immediate + # operands) and the timeout is a constant, so neither adds an 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() + + # 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) + else() + set(_matrix_clocks 1000000 8000000 16000000) + endif() + 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() + endforeach() + if(PUREBOOT_HAS_USART1) + pureboot_size_variant(pureboot_usart1 USART 1) + 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() + + # 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() endif() diff --git a/pureboot/CMakeLists.txt b/pureboot/CMakeLists.txt new file mode 100644 index 0000000..4231c4d --- /dev/null +++ b/pureboot/CMakeLists.txt @@ -0,0 +1,310 @@ +# pureboot as a consumable CMake unit: the per-chip geometry, the default +# baud ladder, and pureboot_add_loader() — the one way a loader target is +# created, both by this port's own build and by a downstream project. A +# downstream project brings its usual libavr setup (the `libavr` target and +# the LIBAVR_MCU toolchain preset), adds this directory, and states its +# deployment: +# +# add_subdirectory(bootloader/pureboot) +# pureboot_add_loader(myboot CLOCK 1000000 SERIAL software TX pb1 RX pb5) +# +# Every argument is optional — CLOCK defaults to the family assumption +# below, BAUD to the fastest standard rate the clock reaches within 2.5 % +# (the ladder), SERIAL to the chip's hardware USART where it has one +# (`hardware`/`software` force a backend, USART 1 picks the second +# instance), RX/TX to pb0/pb1 for the software UART, TIMEOUT to 8 s. +# Infeasible picks fail the build by name: libavr's baud-error and +# software-UART cycle-floor static asserts re-check whatever is passed. + +# Per-family geometry: flash/page/EEPROM sizes and the linker wrap the PC +# modulo needs, the loader slot (each chip's smallest boot sector — 1 KiB on +# the word-addressed 1284s), and the deployment defaults (crystal assumption +# on the megas, calibrated RC on the tinies). The USART flags mirror the +# hardware inventory the loader's own static asserts check (the plain 644 is +# the x4 family's one single-USART die, Atmel-2593). +set(_pb_has_usart 1) +set(_pb_has_usart1 0) +if(LIBAVR_MCU MATCHES "^attiny13a?$") + set(_pb_flash 1024) + set(_pb_wrap "") + set(_pb_page 32) + set(_pb_hz 9600000) + set(_pb_eeprom 64) + set(_pb_has_usart 0) +elseif(LIBAVR_MCU STREQUAL "attiny25") + set(_pb_flash 2048) + set(_pb_wrap "") + set(_pb_page 32) + set(_pb_hz 8000000) + set(_pb_eeprom 128) + set(_pb_has_usart 0) +elseif(LIBAVR_MCU STREQUAL "attiny45") + set(_pb_flash 4096) + set(_pb_wrap "") + set(_pb_page 64) + set(_pb_hz 8000000) + set(_pb_eeprom 256) + set(_pb_has_usart 0) +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_eeprom 512) + set(_pb_has_usart 0) +elseif(LIBAVR_MCU MATCHES "^atmega48(a|p|pa)?$") + set(_pb_flash 4096) + set(_pb_wrap "") + set(_pb_page 64) + set(_pb_hz 16000000) + set(_pb_eeprom 256) +elseif(LIBAVR_MCU MATCHES "^atmega8a?$" OR LIBAVR_MCU MATCHES "^atmega88(a|p|pa)?$") + set(_pb_flash 8192) + set(_pb_wrap -Wl,--pmem-wrap-around=8k) + set(_pb_page 64) + set(_pb_hz 16000000) + set(_pb_eeprom 512) +elseif(LIBAVR_MCU MATCHES "^atmega16a?$" OR LIBAVR_MCU MATCHES "^atmega168(a|p|pa)?$") + set(_pb_flash 16384) + set(_pb_wrap -Wl,--pmem-wrap-around=16k) + set(_pb_page 128) + set(_pb_hz 16000000) + set(_pb_eeprom 512) +elseif(LIBAVR_MCU MATCHES "^atmega164(a|p|pa)$") + set(_pb_flash 16384) + set(_pb_wrap -Wl,--pmem-wrap-around=16k) + set(_pb_page 128) + set(_pb_hz 16000000) + set(_pb_eeprom 512) + set(_pb_has_usart1 1) +elseif(LIBAVR_MCU MATCHES "^atmega32a?$" OR LIBAVR_MCU MATCHES "^atmega328p?$") + set(_pb_flash 32768) + set(_pb_wrap -Wl,--pmem-wrap-around=32k) + set(_pb_page 128) + set(_pb_hz 16000000) + set(_pb_eeprom 1024) +elseif(LIBAVR_MCU MATCHES "^atmega324(a|p|pa)$") + set(_pb_flash 32768) + set(_pb_wrap -Wl,--pmem-wrap-around=32k) + set(_pb_page 128) + set(_pb_hz 16000000) + set(_pb_eeprom 1024) + set(_pb_has_usart1 1) +elseif(LIBAVR_MCU MATCHES "^atmega644(a|p|pa)?$") + # 64 KiB is exactly the 16-bit byte space: plain LPM reaches everything, + # and the smallest boot section (1 KiB) holds the loader and its staging + # slot together (see README.md). The plain 644 is the family's one + # single-USART die. + set(_pb_flash 65536) + set(_pb_wrap -Wl,--pmem-wrap-around=64k) + set(_pb_page 256) + set(_pb_hz 16000000) + set(_pb_eeprom 2048) + if(NOT LIBAVR_MCU STREQUAL "atmega644") + set(_pb_has_usart1 1) + endif() +elseif(LIBAVR_MCU MATCHES "^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 README.md). + set(_pb_flash 131072) + set(_pb_wrap "") + set(_pb_page 256) + set(_pb_hz 16000000) + set(_pb_eeprom 4096) + set(_pb_slot 1024) + set(_pb_limit 1024) + set(_pb_has_usart1 1) +else() + message(FATAL_ERROR "pureboot: no geometry for ${LIBAVR_MCU}") +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) +# Patched-vector chips hand over through the trampoline word below the slot, +# which is also the slot's own last word — their budget is slot − 2. +if(LIBAVR_MCU MATCHES "^atmega" AND NOT LIBAVR_MCU MATCHES "^atmega48") + set(_pb_app 0) + if(NOT DEFINED _pb_limit) + set(_pb_limit ${_pb_slot}) + endif() +else() + math(EXPR _pb_app "${_pb_base} - 2") + math(EXPR _pb_limit "${_pb_slot} - 2") +endif() + +# simavr names its cores after the base dies; the A revisions run on them +# (the 644PA on the 644P core). +set(_pb_sim_mcu ${LIBAVR_MCU}) +if(LIBAVR_MCU MATCHES "^atmega(8|16|32|48|88|164|168|644)a$") + string(REGEX REPLACE "a$" "" _pb_sim_mcu ${LIBAVR_MCU}) +elseif(LIBAVR_MCU STREQUAL "atmega644pa") + set(_pb_sim_mcu atmega644p) +endif() + +# The function runs in its caller's scope, so everything it needs crosses +# scopes as global properties. +set_property(GLOBAL PROPERTY PUREBOOT_BASE_HEX ${_pb_base_hex}) +set_property(GLOBAL PROPERTY PUREBOOT_APP ${_pb_app}) +set_property(GLOBAL PROPERTY PUREBOOT_WRAP "${_pb_wrap}") +set_property(GLOBAL PROPERTY PUREBOOT_DEFAULT_HZ ${_pb_hz}) +set_property(GLOBAL PROPERTY PUREBOOT_HAS_USART ${_pb_has_usart}) +set_property(GLOBAL PROPERTY PUREBOOT_HAS_USART1 ${_pb_has_usart1}) + +# The port's own build (tests, the size matrix) reads the geometry from the +# parent scope; a downstream consumer gets the same variables for free. +set(PUREBOOT_BASE_HEX ${_pb_base_hex} PARENT_SCOPE) +set(PUREBOOT_PAGE ${_pb_page} PARENT_SCOPE) +set(PUREBOOT_SLOT ${_pb_slot} PARENT_SCOPE) +set(PUREBOOT_LIMIT ${_pb_limit} PARENT_SCOPE) +set(PUREBOOT_EEPROM ${_pb_eeprom} PARENT_SCOPE) +set(PUREBOOT_DEFAULT_HZ ${_pb_hz} PARENT_SCOPE) +set(PUREBOOT_HAS_USART ${_pb_has_usart} PARENT_SCOPE) +set(PUREBOOT_HAS_USART1 ${_pb_has_usart1} PARENT_SCOPE) +set(PUREBOOT_SIM_MCU ${_pb_sim_mcu} PARENT_SCOPE) + +# The fastest standard rate the clock reaches within 2.5 % — the same +# best-of-U2X-and-plain divisor search libavr's solve_baud runs, so a +# default never trips the compile-time error it is checked against. A +# software build additionally requires the polled receiver's 100-cycles-a-bit +# floor (its own static assert): at low clocks the U2X divisor still reaches +# rates the bit-banged sampler cannot, so the backend gates the ladder. +function(pureboot_default_baud clock software outvar) + foreach(baud 115200 57600 38400 19200 9600) + math(EXPR _cycles "${clock} / ${baud}") + if(software AND _cycles LESS 100) + continue() + endif() + foreach(divisor 8 16) + math(EXPR _step "${divisor} * ${baud}") + math(EXPR _n "(${clock} + ${_step} / 2) / ${_step}") + if(_n LESS 1 OR _n GREATER 4096) + continue() + endif() + math(EXPR _actual "${clock} / (${divisor} * ${_n})") + math(EXPR _delta "${_actual} - ${baud}") + if(_delta LESS 0) + math(EXPR _delta "-(${_delta})") + endif() + math(EXPR _error_bp "${_delta} * 10000 / ${baud}") + if(_error_bp LESS_EQUAL 250) + set(${outvar} ${baud} PARENT_SCOPE) + return() + endif() + endforeach() + endforeach() + message(FATAL_ERROR "pureboot: no standard baud rate fits a ${clock} Hz clock within 2.5 %") +endfunction() + +# pureboot_add_loader( [CLOCK ] [BAUD ] +# [SERIAL auto|hardware|software] [USART ] +# [RX ] [TX ] [TIMEOUT ]) +# +# Creates the loader target plus its flashable images (.hex for a +# programmer, .bin for --update-loader) and stamps the resolved +# deployment on the target: the PUREBOOT_HZ, PUREBOOT_BAUD and PUREBOOT_LINK +# properties (the link as usart0/usart1/sw:, — what a test harness +# needs to speak to the build). +function(pureboot_add_loader name) + cmake_parse_arguments(PB "" "CLOCK;BAUD;SERIAL;USART;RX;TX;TIMEOUT" "" ${ARGN}) + if(PB_UNPARSED_ARGUMENTS) + message(FATAL_ERROR "pureboot_add_loader(${name}): unknown arguments ${PB_UNPARSED_ARGUMENTS}") + endif() + get_property(_hz GLOBAL PROPERTY PUREBOOT_DEFAULT_HZ) + get_property(_base_hex GLOBAL PROPERTY PUREBOOT_BASE_HEX) + get_property(_app GLOBAL PROPERTY PUREBOOT_APP) + get_property(_wrap GLOBAL PROPERTY PUREBOOT_WRAP) + get_property(_usart GLOBAL PROPERTY PUREBOOT_HAS_USART) + get_property(_usart1 GLOBAL PROPERTY PUREBOOT_HAS_USART1) + + if(NOT PB_CLOCK) + set(PB_CLOCK ${_hz}) + endif() + if(NOT PB_TIMEOUT) + set(PB_TIMEOUT 8) + endif() + if(NOT PB_SERIAL) + set(PB_SERIAL auto) + endif() + if(DEFINED PB_USART AND PB_SERIAL STREQUAL "software") + message(FATAL_ERROR "pureboot_add_loader(${name}): USART ${PB_USART} contradicts SERIAL software") + endif() + if(DEFINED PB_USART) + set(PB_SERIAL hardware) + elseif(PB_SERIAL STREQUAL "hardware") + set(PB_USART 0) + endif() + + set(_serial_defines "") + if(PB_SERIAL STREQUAL "hardware") + if(PB_USART EQUAL 1 AND NOT _usart1) + message(FATAL_ERROR "pureboot_add_loader(${name}): ${LIBAVR_MCU} has no USART1") + elseif(NOT _usart) + message(FATAL_ERROR "pureboot_add_loader(${name}): ${LIBAVR_MCU} has no hardware USART") + endif() + set(_serial_defines PUREBOOT_USART=${PB_USART}) + set(_link usart${PB_USART}) + else() + if(PB_SERIAL STREQUAL "auto") + if(_usart AND (PB_RX OR PB_TX)) + message(WARNING "pureboot_add_loader(${name}): RX/TX apply to the software UART, " + "which auto does not pick on ${LIBAVR_MCU} — SERIAL software to force it") + endif() + if(_usart) + set(_link usart0) + else() + set(PB_SERIAL software) + endif() + endif() + if(PB_SERIAL STREQUAL "software") + if(NOT PB_RX) + set(PB_RX pb0) + endif() + if(NOT PB_TX) + set(PB_TX pb1) + endif() + foreach(_pin ${PB_RX} ${PB_TX}) + if(NOT _pin MATCHES "^p[a-h][0-7]$") + message(FATAL_ERROR "pureboot_add_loader(${name}): pin '${_pin}' is not of the form pb1") + endif() + endforeach() + set(_serial_defines PUREBOOT_SOFT_SERIAL PUREBOOT_RX=${PB_RX} PUREBOOT_TX=${PB_TX}) + # The link spec a test harness drives a GPIO bridge with: sw:, + # as the port letter and bit, the loader's own pin naming upcased. + string(SUBSTRING ${PB_RX} 1 2 _rx_pin) + string(SUBSTRING ${PB_TX} 1 2 _tx_pin) + string(TOUPPER "sw:${_rx_pin},${_tx_pin}" _link) + string(REPLACE "SW" "sw" _link ${_link}) + endif() + endif() + if(NOT PB_BAUD) + if(PB_SERIAL STREQUAL "software") + pureboot_default_baud(${PB_CLOCK} 1 PB_BAUD) + else() + pureboot_default_baud(${PB_CLOCK} 0 PB_BAUD) + endif() + endif() + + set(_defines PUREBOOT_CLOCK_HZ=${PB_CLOCK} PUREBOOT_BAUD=${PB_BAUD} PUREBOOT_TIMEOUT=${PB_TIMEOUT} + ${_serial_defines}) + + add_executable(${name} ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/pureboot.cpp) + target_link_libraries(${name} PRIVATE libavr) + target_compile_definitions(${name} PRIVATE ${_defines}) + target_link_options(${name} PRIVATE -nostartfiles -Wl,--section-start=.text=${_base_hex} + -Wl,--defsym=pureboot_app=${_app} ${_wrap}) + add_custom_command(TARGET ${name} POST_BUILD COMMAND ${CMAKE_SIZE} $) + # The ELF is a container (symbols, section headers), never flashed; the + # flashable forms sit beside it: .hex for a programmer, .bin (the slot's + # bare bytes) for the host tool's raw path and --update-loader. + add_custom_command(TARGET ${name} POST_BUILD + COMMAND ${CMAKE_OBJCOPY} -O ihex -R .eeprom + $ $.hex + COMMAND ${CMAKE_OBJCOPY} -O binary -R .eeprom + $ $.bin) + set_target_properties(${name} PROPERTIES PUREBOOT_HZ ${PB_CLOCK} PUREBOOT_BAUD ${PB_BAUD} + PUREBOOT_LINK ${_link}) +endfunction() diff --git a/pureboot/README.md b/pureboot/README.md index 0a485cd..d1d8b21 100644 --- a/pureboot/README.md +++ b/pureboot/README.md @@ -3,16 +3,18 @@ A serial bootloader on [libavr](https://git.blackmark.me/avr/libavr), pure by constraint: one C++ source, no inline assembly, no global register variables (attributes allowed), built for **every chip libavr targets — all 37 — -fitting each chip's smallest boot sector**: 512 bytes everywhere — 488 B on -the tiny13s, 498–502 B on the tiny25/45/85, 466–504 B across the megas -(474 B on the boot-section-less m48s, 498 B on the 644s) — except the -ATmega1284/1284P, whose smallest boot sector is 1 KiB and whose far-flash -machinery (ELPM reads, RAMPZ page commands, word-addressed wire) lands at -558 B in a 1 KiB slot: the 512-byte figure is a hardware boundary those -chips simply do not have, and no implementation of this feature set fits it -there. The device speaks primitives; every composite — verify, erase, -reset-vector surgery, updating the loader itself — lives in the host tool -(`pureboot.py`). +fitting each chip's smallest boot sector**: 512 bytes everywhere — 470 B on +the tiny13s, ~484 B on the tiny25/45/85, 458–506 B across the megas and +every configured variant of them (the m8's software-serial build is the +fattest) — except the ATmega1284/1284P, whose smallest boot sector is 1 KiB +and whose far-flash machinery (ELPM reads, RAMPZ page commands, +word-addressed wire) lands at 556–562 B in a 1 KiB slot: the 512-byte +figure is a hardware boundary those chips simply do not have, and no +implementation of this feature set fits it there. Clock, baud, serial +backend and pins are per-build configuration (below); the size matrix in +the test suite holds every combination inside its slot. The device speaks +primitives; every composite — verify, erase, reset-vector surgery, updating +the loader itself — lives in the host tool (`pureboot.py`). The image is **position-independent**: control flow is PC-relative, the read/write paths take wire addresses, the write guard protects the slot the @@ -26,16 +28,63 @@ jumps into it, and lets it rewrite the resident. The slot is 512 bytes minimum); on the tinies the budget is 510, not 512: a slot's last word belongs to the host-managed trampoline (below). +## Configuration + +Every deployment axis is a build parameter, resolved by the CMake function +`pureboot_add_loader()` (in `pureboot/CMakeLists.txt`) — the one way a +loader target is created, by this repo's own build and by a downstream +project alike: + +| Argument | Meaning | Default | +|---|---|---| +| `CLOCK ` | the clock the board runs | 16 MHz megas, 8 MHz t25/45/85, 9.6 MHz t13s | +| `BAUD ` | the wire rate | the ladder below | +| `SERIAL auto\|hardware\|software` | the link backend | `auto`: the hardware USART where the chip has one | +| `USART ` | the USART instance (x4 megas carry two) | 0 | +| `RX `, `TX ` | software-UART pins | `pb0`, `pb1` | +| `TIMEOUT ` | the activation window | 8 | + +The default baud is the fastest of 115200/57600/38400/19200/9600 the clock +reaches within 2.5 % — the same U2X-included divisor search libavr's baud +solver runs — and on a software build additionally within the polled +receiver's 100-cycles-a-bit floor. 16 MHz lands 115200, 8 MHz 57600, +1 MHz 9600. Whatever is picked or overridden is re-checked in the compile: +an infeasible clock/baud/backend combination, or a USART the chip does not +have, fails with a named static assert. + +A downstream project brings its usual libavr setup (the `libavr` target, +the chip via the `LIBAVR_MCU` toolchain preset), consumes this directory, +and states its deployment — for example an ATmega328P on its shipped +1 MHz fuses with the software UART on hand-picked pins: + +```cmake +FetchContent_Declare(bootloader GIT_REPOSITORY git@git.blackmark.me:avr/bootloader.git GIT_TAG main) +FetchContent_MakeAvailable(bootloader) +add_subdirectory(${bootloader_SOURCE_DIR}/pureboot pureboot) + +pureboot_add_loader(myboot CLOCK 1000000 SERIAL software TX pb1 RX pb5) +``` + +The function emits the ELF plus `myboot.hex` (the programmer artifact) and +`myboot.bin` (the self-update image), prints the size, and stamps the +resolved deployment on the target as the `PUREBOOT_HZ`, `PUREBOOT_BAUD` +and `PUREBOOT_LINK` properties — what a flashing script or test harness +needs to speak to the build. This exact example deployment runs the full +protocol suite in CI (`pureboot.custom`). + ## Link +The stock builds assume the family's natural deployment; any axis moves +per build (above). + | Chip | Serial | Baud | Clock assumed | |---|---|---|---| | every ATmega | the hardware USART (USART0), RXD/TXD per pinout | 115200 8N1 | 16 MHz crystal | | ATtiny25/45/85 | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 8 MHz internal RC | | ATtiny13/13A | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 9.6 MHz internal RC | -The tiny RX pin has its pull-up enabled; TX idles high. All multi-byte -quantities on the wire are little-endian. +The software-UART RX pin has its pull-up enabled; TX idles high. All +multi-byte quantities on the wire are little-endian. ## Activation @@ -50,9 +99,10 @@ The host then has one activation window per awaited byte to knock: `p` then awaited again (line noise cannot lock the loader, only delay it). A window expiring with an idle line boots the application. -The window length is a compile-time constant — 8 s by default, another value -via the `PUREBOOT_TIMEOUT` CMake cache variable — so the whole EEPROM belongs -to the application; pureboot never uses it for its own state. Re-timing a +The window length is a compile-time constant — 8 s by default, another +value via `pureboot_add_loader(... TIMEOUT )` (the stock target keeps +the `PUREBOOT_TIMEOUT` cache variable) — so the whole EEPROM belongs to +the application; pureboot never uses it for its own state. Re-timing a deployed loader is a self-update with a re-timed build (below). ## Session @@ -237,11 +287,35 @@ read-back unless `--no-verify`; images are raw binary, or Intel HEX by extension. `--force` overrides the refusable safety checks (today: flashing application data into a mega's reset walk region). +Readouts come one fact per line: `--info` prints the decoded info block +field by field, `--fuses` each fuse byte on its own line — plus, on a +boot-sectioned mega, the decoded meaning (where the BOOTSZ section starts, +what BOOTRST does to reset). Transfers that take wire time — programming, +reading, erasing, verifying, the update phases — draw a transient progress +bar on stderr when it is a tty; logs and pipes see only the summary lines. +`-v`/`--verbose` adds the decisions as they happen: knock counts, the +programming plan (vector-surgery targets, skipped blank pages), update +state handling and per-phase page counts. + ## Tests -Per chip preset, `ctest` runs: +`tools/check.sh` runs every chip's workflow (`tools/check.sh --full` adds +the reflect-mode builds of libavr's spot set; `tools/make_presets.py` +regenerates the presets). Per chip preset, `ctest` runs: - `pureboot.size` — the 510-byte (tinies) / 512-byte (mega) budget; +- `pureboot_*.size` — the size matrix: the serial backends × the clock + ladder (1/8/16 MHz; the t13s' own RC menu), plus the USART1 build on the + x4 chips — every configuration axis that could move the image, each + variant against the same slot budget (pins are immediate operands and the + timeout is a constant: size-neutral); +- `pureboot.custom` (328P) — the configured-deployment acceptance test: the + 1 MHz software-serial TX=PB1/RX=PB5 build from the configuration example + drives the full protocol suite through the runner's GPIO bridge, fixture + application included; +- `pureboot.usart1` (644A) — the same protocol suite over the second + hardware USART: instance selection is compile-checked everywhere, but + only a live session proves the loader polls the USART it claims; - `pureboot.pi` — the position-independence lint: no absolute `jmp`/`call` in the image, the info block within its first 256 bytes; - `pureboot.planner` — the host tool's pure logic: programming orders and @@ -249,9 +323,10 @@ Per chip preset, `ctest` runs: boot-fuse decode, and the update preflight's error/warning matrix over synthetic fuse bytes; - `pureboot.protocol` — end to end against a simavr device - (`test/pureboot_device.c` — the mega's USART as a pty; on the tinies a - cycle-timed GPIO⇄pty bridge for the software UART, plus the SPM/NVM module - simavr's tiny cores lack) driven by the real host tool through + (`test/pureboot_device.c` — a hardware USART as a pty, or a cycle-timed + GPIO⇄pty bridge for a software-UART build, selected with `-l` to match + the loader's link; plus the SPM/NVM module simavr's tiny cores lack) + driven by the real host tool through knock-from-reset, program + verify of both memories, session reconnect, an external reset through the patched vector, and the hand-over to a fixture application whose banner proves the launch — cross-checked against the @@ -265,6 +340,6 @@ Per chip preset, `ctest` runs: from its flash dump, and a re-run must complete the update with the application intact throughout. -`size`, `pi`, and `planner` are host logic and run anywhere; the three +`size`, `pi`, and `planner` are host logic and run anywhere; the simulator-driven targets need simavr and a pty, so they are POSIX-only — on Windows the tool is exercised against real hardware. diff --git a/pureboot/pureboot.cpp b/pureboot/pureboot.cpp index 1195191..42f63df 100644 --- a/pureboot/pureboot.cpp +++ b/pureboot/pureboot.cpp @@ -38,22 +38,19 @@ constexpr auto off = avr::irq::guard_policy::unused; constexpr std::uint8_t ack = '+'; -// Per-chip personality: the clocks the dogfood boards run (16 MHz crystal on -// the mega, calibrated RC on the tinies). The device signature comes straight -// from the chip database (avr::hw::db.signature) — compile-time data is the -// only universal source, since the tiny13A cannot even read its signature row -// from code. -consteval avr::hertz_t clock() -{ - auto name = std::string_view{avr::hw::db.name}; - if (name.starts_with("ATtiny13")) - return 9.6_MHz; - if (name.starts_with("ATtiny")) - return 8_MHz; - return 16_MHz; -} +// Per-deployment personality, passed in by the build — pureboot_add_loader() +// (the CMake function next to this file) resolves the defaults: the clock the +// board actually runs, the wire baud, the serial backend and its pins. The +// device signature needs no configuring — it comes from the chip database +// (avr::hw::db.signature), the only universal source, since the tiny13A +// cannot even read its signature row from code. +#if !defined(PUREBOOT_CLOCK_HZ) || !defined(PUREBOOT_BAUD) +#error \ + "PUREBOOT_CLOCK_HZ and PUREBOOT_BAUD select this build's clock and baud — create loader targets with pureboot_add_loader() (README.md)" +#endif -using dev = avr::device<{.clock = clock()}>; +using dev = avr::device<{.clock = avr::hertz_t{PUREBOOT_CLOCK_HZ}}>; +constexpr avr::baud_t wire_baud{PUREBOOT_BAUD}; // The watchdog reset flag's home: MCUSR, or the classic megas' MCUCSR. consteval std::int16_t wdrf_field() @@ -118,34 +115,43 @@ constexpr std::uint8_t timeout_seconds = PUREBOOT_TIMEOUT; static_cast((boot_section ? 0 : 1) | (word_flash ? 2 : 0)), }; -// The serial link: the hardware USART where the chip has one, the polled -// software UART (no vector — the table belongs to the application) on PB0/PB1 -// elsewhere. Both are class templates on the clock so only the selected -// backend is ever instantiated. pending() is the cheap line test the -// activation window polls; rx() then picks the byte up; drain() holds until -// the last transmitted frame is fully on the wire (the jump hand-over must -// not let the target's re-init clip the ack). -template -consteval std::int16_t rxc_field() -{ - return avr::uart::detail::ufield<'0', "UCSR#A", "RXC#">(); -} +// The serial link. PUREBOOT_USART forces a hardware USART instance, +// PUREBOOT_SOFT_SERIAL the polled software UART (no vector — the table +// belongs to the application) on PUREBOOT_RX/PUREBOOT_TX; with neither, the +// chip's first USART where it has one and the software UART elsewhere. Both +// are class templates on the clock so only the selected backend is ever +// instantiated. pending() is the cheap line test the activation window +// polls; rx() then picks the byte up; drain() holds until the last +// transmitted frame is fully on the wire (the jump hand-over must not let +// the target's re-init clip the ack). +#if defined(PUREBOOT_SOFT_SERIAL) && defined(PUREBOOT_USART) +#error "PUREBOOT_SOFT_SERIAL and PUREBOOT_USART select opposing serial backends" +#endif +#if !defined(PUREBOOT_RX) +#define PUREBOOT_RX pb0 +#endif +#if !defined(PUREBOOT_TX) +#define PUREBOOT_TX pb1 +#endif +#if defined(PUREBOOT_USART) +constexpr char usart_digit = '0' + PUREBOOT_USART; +#else +constexpr char usart_digit = '0'; +#endif -template -consteval std::int16_t txc_field() +// Whether the chip carries the selected USART: the suffixed instance name, +// or — for instance 0 — the classic megas' un-numbered block. +consteval bool usart_exists() { - return avr::uart::detail::ufield<'0', "UCSR#A", "TXC#">(); -} - -template -consteval std::int16_t status_reg() -{ - return avr::uart::detail::ureg<'0', "UCSR#A">(); + const char name[] = {'U', 'S', 'A', 'R', 'T', usart_digit}; + if (avr::hw::db.has_instance(std::string_view{name, sizeof(name)})) + return true; + return usart_digit == '0' && avr::hw::db.has_instance("USART"); } template struct hardware_link { - using uart = avr::uart::usart0; + using uart = avr::uart::usart; // The compiled idle poll: lds UCSR0A (2), sbrc skipping the exit (2), // sbiw + sbci + sbci + brne (6). @@ -158,7 +164,7 @@ struct hardware_link { static bool pending() { - return avr::hw::field_impl()>::test(); + return uart::rx_ready(); } static std::uint8_t rx() @@ -173,22 +179,14 @@ struct hardware_link { static void drain() { - // write() leaves the byte draining behind it. Clear a stale TXC0 - // first (W1C by writing the sampled status back — the store a hand - // assembler writes, keeping U2X0), then wait for the fresh - // completion; with a byte still ahead in the shifter TXC0 cannot - // re-set until the last pending byte has fully left. - using status = avr::hw::reg_impl()>; - status::write(status::read()); - while (!avr::hw::field_impl()>::test()) { - } + uart::drain(); } }; template struct software_link { - using rx_t = avr::uart::software_rx_polled; - using tx_t = avr::uart::software_tx; + using rx_t = avr::uart::software_rx_polled; + using tx_t = avr::uart::software_tx; // The compiled idle poll: sbis skipping the exit (2), sbiw + sbci + // sbci + brne (6). @@ -201,7 +199,7 @@ struct software_link { static bool pending() { - return !avr::io::input::read(); // a start bit has begun + return rx_t::start_pending(); } static std::uint8_t rx() @@ -220,8 +218,14 @@ struct software_link { } }; -using link = std::conditional_t, software_link>; +#if defined(PUREBOOT_USART) +static_assert(usart_exists(), "PUREBOOT_USART selects a hardware USART this chip does not have"); +using link = hardware_link; +#elif defined(PUREBOOT_SOFT_SERIAL) +using link = software_link; +#else +using link = std::conditional_t, software_link>; +#endif // The application's entry, an absolute address the linker pins (--defsym in // CMakeLists.txt): 0x0000 on the mega (word 0 stays the application's own @@ -417,10 +421,13 @@ void send_fuses() // return address is a word address, whose high byte is the 256-word slot // index — on byte-addressed chips doubled back into byte terms. // program_flash refuses this one slot and the info block is addressed - // from it, so both follow wherever the code was flashed. + // from it, so both follow wherever the code was flashed. The high byte is + // spelled as byteswap's low byte: the builtin's value is itself built by + // swapping the two stacked bytes, and the double swap folds to the single + // byte pick a hand assembler writes — `>> 8` leaves the swap materialized. const std::uint16_t ra_words = reinterpret_cast(__builtin_return_address(0)); - const std::uint8_t slot_high = - word_flash ? static_cast(ra_words >> 8) & 0xfe : static_cast((ra_words >> 8) << 1); + const std::uint8_t ra_high = static_cast(std::byteswap(ra_words)); + const std::uint8_t slot_high = word_flash ? ra_high & 0xfe : static_cast(ra_high << 1); // The knock: 'p' then 'b', each under a fresh window; any other byte is // line noise and waits again. Falling out of a window runs the app. @@ -439,13 +446,13 @@ void send_fuses() // asserts it), and slots are 512-aligned — so the low byte of its // link address (in wire units: bytes, or words on the large // chips) is its offset in any slot, and the high byte of its - // runtime address is the running slot's. Built as a byte pair so - // no absolute address is ever materialized. + // runtime address is the running slot's. Composed from the two + // bytes — the high half is runtime data, so no absolute address + // is ever materialized. const auto link_low = reinterpret_cast(info_data.data()); const std::uint8_t low = word_flash ? static_cast(link_low >> 1) : static_cast(link_low); - send_flash(std::bit_cast(std::array{low, slot_high}), - static_cast(info_data.size())); + send_flash(static_cast(low | (slot_high << 8)), static_cast(info_data.size())); break; } case 'J': { // jump to a wire word address: hand-over and staging transfer diff --git a/test/pbapp.cpp b/test/pbapp.cpp index a5ef5a0..474a33e 100644 --- a/test/pbapp.cpp +++ b/test/pbapp.cpp @@ -4,11 +4,16 @@ // surgery, actually launched it. Linked normally (crt, vectors at 0); on // the tinies its reset vector is the rjmp the host re-homes. // -// On the mega it then listens, and an 'L' makes it jump into the resident -// loader — the application-owned loader entry a BOOTRST-unprogrammed mega -// relies on (reset always boots the application there), exercised by the -// self-update tests. The tinies idle: reset reaches their loader through -// the patched vector, so the application owes it nothing. +// On the hardware-USART link it then listens, and an 'L' makes it jump into +// the resident loader — the application-owned loader entry a +// BOOTRST-unprogrammed mega relies on (reset always boots the application +// there), exercised by the self-update tests. The software link idles: +// reset reaches those loaders through the patched vector (or the runner +// models BOOTRST), so the application owes them nothing. +// +// The fixture speaks the deployment its loader was built for: the same +// PUREBOOT_* defines configure it, and without them it assumes the stock +// deployment (the crystal/RC clock table below, the chip's natural link). #include using namespace avr::literals; @@ -17,19 +22,44 @@ namespace { consteval avr::hertz_t clock() { +#if defined(PUREBOOT_CLOCK_HZ) + return avr::hertz_t{PUREBOOT_CLOCK_HZ}; +#else auto name = std::string_view{avr::hw::db.name}; if (name.starts_with("ATtiny13")) return 9.6_MHz; if (name.starts_with("ATtiny")) return 8_MHz; return 16_MHz; +#endif +} + +#if !defined(PUREBOOT_TX) +#define PUREBOOT_TX pb1 +#endif +#if !defined(PUREBOOT_USART) +#define PUREBOOT_USART 0 +#endif + +consteval bool use_hardware() +{ +#if defined(PUREBOOT_SOFT_SERIAL) + return false; +#else + return avr::hw::db.has_instance("USART0") || avr::hw::db.has_instance("USART"); +#endif } using dev = avr::device<{.clock = clock()}>; -template +template struct link { - using tx_t = avr::uart::usart0; +#if defined(PUREBOOT_BAUD) + static constexpr avr::baud_t baud{PUREBOOT_BAUD}; +#else + static constexpr avr::baud_t baud{115200}; +#endif + using tx_t = avr::uart::usart<'0' + PUREBOOT_USART, C, {.baud = baud, .max_baud_error = 2.5_pct}>; static void tx(char c) { tx_t::write(static_cast(c)); @@ -47,7 +77,12 @@ struct link { template struct link { - using tx_t = avr::uart::software_tx; +#if defined(PUREBOOT_BAUD) + static constexpr avr::baud_t baud{PUREBOOT_BAUD}; +#else + static constexpr avr::baud_t baud{57600}; +#endif + using tx_t = avr::uart::software_tx; static void tx(char c) { tx_t::write(static_cast(c)); diff --git a/test/pbsim.py b/test/pbsim.py index 1cd77d6..1a3faae 100644 --- a/test/pbsim.py +++ b/test/pbsim.py @@ -8,8 +8,11 @@ import subprocess class Device: - def __init__(self, binary, elf, mcu, hz, base_hex, page, baud, dump, reset_hex=None, resume=None): - cmd = [binary, elf, mcu, hz, base_hex, str(page), str(baud), dump] + def __init__(self, binary, elf, mcu, hz, base_hex, page, baud, dump, reset_hex=None, resume=None, link=None): + cmd = [binary] + if link: + cmd += ["-l", link] + cmd += [elf, mcu, hz, base_hex, str(page), str(baud), dump] if reset_hex is not None or resume is not None: # Chips without a hardware boot section — the tinies and the # m48s — reset to address 0 like silicon; the boot-sectioned diff --git a/test/pbtest.py b/test/pbtest.py index eb7cacf..3d850ac 100644 --- a/test/pbtest.py +++ b/test/pbtest.py @@ -5,15 +5,15 @@ through flash + EEPROM + fuse + hand-over scenarios, and cross-check the tool's view against the simulator's ground-truth memory dumps. Usage: pbtest.py - + [link] + +The optional link is the runner's -l spec (usart1, sw:B5,B1, ...) for a +loader built off the chip's natural serial default. Exits 0 if every scenario passes. """ import os -import signal -import subprocess import sys -import time def fail(message): @@ -33,53 +33,14 @@ def rjmp_decode(word, at, flash_words): return (at + 1 + offset) % flash_words -class Device: - def __init__(self, binary, elf, mcu, hz, base, page, baud, dump): - self.proc = subprocess.Popen( - [binary, elf, mcu, hz, base, str(page), str(baud), dump], - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - ) - self.dump = dump - self.pty = None - deadline = time.time() + 5 - while time.time() < deadline: - line = self.proc.stdout.readline() - if not line: - break - if line.startswith("PB_PTY"): - self.pty = line.split()[1] - break - if not self.pty: - self.stop() - raise RuntimeError("device did not report a pty") - - def stop(self): - self.proc.terminate() - try: - self.proc.wait(timeout=3) - except subprocess.TimeoutExpired: - self.proc.kill() - - -def run_tool(tool, pty, baud, *args): - result = subprocess.run( - [sys.executable, tool, "--port", pty, "--baud", str(baud), "--wait", "20", *args], - capture_output=True, - text=True, - timeout=120, - ) - print(result.stdout, end="") - if result.returncode != 0: - fail(f"tool exited {result.returncode}: {result.stderr.strip()}") - return result.stdout - - def main(): - (device_bin, elf, mcu, hz, base_hex, page, baud, eeprom_size, app_bin, tool, workdir) = sys.argv[1:] + args = sys.argv[1:] + link = args.pop() if len(args) == 12 else None + (device_bin, elf, mcu, hz, base_hex, page, baud, eeprom_size, app_bin, tool, workdir) = args base, page, baud, eeprom_size = int(base_hex, 0), int(page), int(baud), int(eeprom_size) sys.path.insert(0, os.path.dirname(os.path.abspath(tool))) + sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + import pbsim import pureboot as pb os.makedirs(workdir, exist_ok=True) @@ -105,19 +66,19 @@ def main(): + bytes([flags]) ) - device = Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump) + device = pbsim.Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump, link=link) try: # Session 1: knock from reset, identify, program everything, stay. - out = run_tool(tool, device.pty, baud, "--info", "--fuses", "--flash", app_bin, - "--eeprom", ee_path, "--stay") - for needed in ("device: signature", "fuses:", "verify:", "stays"): + out = pbsim.run_tool(tool, device.pty, baud, "--info", "--fuses", "--flash", app_bin, + "--eeprom", ee_path, "--stay") + for needed in ("signature", "fuses", "verify:", "stays"): if needed not in out: fail(f"session 1 output lacks {needed!r}") # Session 2: reconnect into the live session, verify, dump, hand over # is deferred — the pty must be reopened for the APP banner first. - out = run_tool(tool, device.pty, baud, "--verify-flash", app_bin, "--verify-eeprom", ee_path, - "--read-flash", read_flash, "--read-eeprom", read_eeprom, "--stay") + out = pbsim.run_tool(tool, device.pty, baud, "--verify-flash", app_bin, "--verify-eeprom", ee_path, + "--read-flash", read_flash, "--read-eeprom", read_eeprom, "--stay") if out.count("verify:") != 2: fail("session 2 did not verify both memories") @@ -136,7 +97,7 @@ def main(): # runner resets them to address 0 like silicon) or BOOTRST (mega). # The loader must answer a fresh knock, and the 'J' hand-over must # land in the application, which banners on the same link. - device.proc.send_signal(signal.SIGUSR1) + device.reset() port = pb.Port(device.pty, baud) try: loader = pb.Loader(port) diff --git a/test/pureboot_device.c b/test/pureboot_device.c index 67feb8c..e1dc147 100644 --- a/test/pureboot_device.c +++ b/test/pureboot_device.c @@ -1,12 +1,16 @@ -// simavr "device" for the pureboot protocol tests, all three chips. Loads -// the boot-linked ELF at the loader base, starts execution there (BOOTRST / -// the patched vector are not what is under test), and exposes the loader's +// simavr "device" for the pureboot protocol tests, every chip. Loads the +// boot-linked ELF at the loader base, starts execution there (BOOTRST / the +// patched vector are not what is under test), and exposes the loader's // serial link as a pty for the real host tool: // -// - Megas: the hardware USART through simavr's uart_pty. -// - Tinies: an 8N1 bridge between a pty and the GPIO software UART -// (drives PB0, the loader's RX; decodes PB1, its TX), timed against the -// simulated cycle counter. +// - Hardware USART builds: simavr's uart_pty on the selected instance. +// - Software UART builds: an 8N1 bridge between a pty and the GPIO pins, +// timed against the simulated cycle counter (drives the loader's RX, +// decodes its TX). +// +// The link follows the chip's natural default (USART0 on the megas, the +// software UART on PB0/PB1 elsewhere) unless -l overrides it: `-l usart1` +// for the second instance, `-l sw:B5,B1` for a software build's RX,TX pins. // // simavr's tiny cores decode the SPM opcode but attach no NVM module — SPM // is a silent no-op (the mega's boot section has one, avr_flash). The @@ -38,11 +42,31 @@ static avr_t *avr; static uart_pty_t uart_pty; -static int use_uart_pty; +static int link_software; +static char uart_digit = '0'; +static char sw_rx_port = 'B', sw_tx_port = 'B'; +static int sw_rx_bit = 0, sw_tx_bit = 1; static const char *dump_path; static uint32_t reset_pc; static volatile sig_atomic_t reset_requested; +static int parse_link(const char *spec) +{ + if (strcmp(spec, "usart0") == 0 || strcmp(spec, "usart1") == 0) { + link_software = 0; + uart_digit = spec[5]; + return 0; + } + if (strncmp(spec, "sw", 2) == 0) { + link_software = 1; + if (spec[2] == '\0') + return 0; + if (sscanf(spec + 2, ":%c%d,%c%d", &sw_rx_port, &sw_rx_bit, &sw_tx_port, &sw_tx_bit) == 4) + return 0; + } + return -1; +} + // simavr 1.6's avr_flash PGERS handler erases spm_pagesize bytes starting at // Z & ~1 instead of the page containing Z (its PGWRT path masks correctly) — // hardware ignores the in-page bits (§26.8.1), so an erase issued with Z @@ -273,29 +297,42 @@ static void finish(int sig) } } } - if (use_uart_pty) + if (!link_software) uart_pty_stop(&uart_pty); _exit(0); } int main(int argc, char *argv[]) { - if (argc < 8 || argc > 10) { + int link_given = 0; + for (int opt; (opt = getopt(argc, argv, "l:")) != -1;) { + if (opt != 'l' || parse_link(optarg) != 0) { + fprintf(stderr, "device: bad link spec (usart0, usart1, sw, or sw:B0,B1 as RX,TX)\n"); + return 2; + } + link_given = 1; + } + int args = argc - optind; + if (args < 7 || args > 9) { fprintf(stderr, - "usage: %s " + "usage: %s [-l link] " " [reset_hex] [resume_flash]\n" - " reset_hex: reset vector (default: base on the mega, 0 on the tinies)\n" + " -l link: usart0 | usart1 | sw[:B0,B1] (RX,TX); default: the chip's own\n" + " reset_hex: reset vector (default: base with a boot section, else 0)\n" " resume_flash: raw full-flash image loaded instead of the ELF — a prior\n" " run's dump, for power-fail resume tests\n", argv[0]); return 2; } + argv += optind - 1; // argv[1] is the ELF again, whatever was parsed const char *mcu_name = argv[2]; uint32_t base = (uint32_t)strtoul(argv[4], NULL, 0); unsigned page = (unsigned)atoi(argv[5]); unsigned baud = (unsigned)atoi(argv[6]); dump_path = argv[7]; - use_uart_pty = strncmp(mcu_name, "atmega", 6) == 0; // every mega links over its hardware USART + int is_mega = strncmp(mcu_name, "atmega", 6) == 0; + if (!link_given) + link_software = !is_mega; // the chips' natural links: USART0, or PB0/PB1 avr = avr_make_mcu_by_name(mcu_name); if (!avr) { @@ -306,7 +343,7 @@ int main(int argc, char *argv[]) avr->frequency = (uint32_t)strtoul(argv[3], NULL, 0); memset(avr->flash, 0xff, avr->flashend + 1); // real flash powers up erased - if (argc > 9) { + if (args > 8) { // Resume: the full flash image of an interrupted prior run. FILE *f = fopen(argv[9], "rb"); if (!f || fread(avr->flash, 1, avr->flashend + 1, f) == 0) { @@ -327,8 +364,8 @@ int main(int argc, char *argv[]) // and the boot-section-less m48s reset to word 0 like silicon — erased // flash walks up into the loader, and after the host's surgery the // patched vector routes there. - int boot_section = use_uart_pty && strncmp(mcu_name, "atmega48", 8) != 0; - reset_pc = argc > 8 ? (uint32_t)strtoul(argv[8], NULL, 0) : (boot_section ? base : 0); + int boot_section = is_mega && strncmp(mcu_name, "atmega48", 8) != 0; + reset_pc = args > 7 ? (uint32_t)strtoul(argv[8], NULL, 0) : (boot_section ? base : 0); avr->pc = reset_pc; avr->codeend = avr->flashend; @@ -341,27 +378,34 @@ int main(int argc, char *argv[]) avr_ioctl(avr, AVR_IOCTL_EEPROM_SET, &seed); } - if (use_uart_pty) { + // The megas carry simavr's avr_flash module (and its two gaps the wrap + // above fixes); the tinies get the NVM module simavr lacks. Which serial + // bridge runs is the link's business, not the chip class's. + if (is_mega) { fix_mega_flash_erase(); - // POLL_SLEEP paces an idle-polling loader in host real time (a - // no-hardware CPU-saving hack); clear it so cycles run free. - uint32_t flags = 0; - avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS('0'), &flags); - flags &= ~AVR_UART_FLAG_POLL_SLEEP; - avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS('0'), &flags); - uart_pty_init(avr, &uart_pty); - uart_pty_connect(&uart_pty, '0'); - printf("PB_PTY %s\n", uart_pty.pty.slavename); } else { nvm.page = page; memset(nvm.buffer, 0xff, sizeof(nvm.buffer)); nvm.io.kind = "tiny_nvm"; nvm.io.ioctl = nvm_ioctl; avr_register_io(avr, &nvm.io); + } + if (!link_software) { + // POLL_SLEEP paces an idle-polling loader in host real time (a + // no-hardware CPU-saving hack); clear it so cycles run free. + uint32_t flags = 0; + avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS(uart_digit), &flags); + flags &= ~AVR_UART_FLAG_POLL_SLEEP; + avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS(uart_digit), &flags); + uart_pty_init(avr, &uart_pty); + uart_pty_connect(&uart_pty, uart_digit); + printf("PB_PTY %s\n", uart_pty.pty.slavename); + } else { bit_cycles = (avr->frequency + baud / 2) / baud; // matches uart.hpp's own rounding exactly - rx_pin = avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ('B'), 0); - avr_irq_register_notify(avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ('B'), 1), tx_hook, NULL); + rx_pin = avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ(sw_rx_port), (unsigned)sw_rx_bit); + avr_irq_register_notify(avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ(sw_tx_port), (unsigned)sw_tx_bit), tx_hook, + NULL); avr_raise_irq(rx_pin, 1); // idle line int slave; @@ -389,18 +433,27 @@ int main(int argc, char *argv[]) reset_requested = 0; avr_reset(avr); avr->pc = reset_pc; - if (use_uart_pty) { // reset restores the pacing hack; re-clear it + if (!link_software) { // reset restores the pacing hack; re-clear it uint32_t flags = 0; - avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS('0'), &flags); + avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS(uart_digit), &flags); flags &= ~AVR_UART_FLAG_POLL_SLEEP; - avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS('0'), &flags); + avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS(uart_digit), &flags); } else { bridge_reset(); } } - if (!use_uart_pty && ++since_poll >= 2000) { + if (link_software && ++since_poll >= 2000) { since_poll = 0; poll_pty(); + // An unthrottled idle simulation runs the activation window out + // from under the host's real-time knock cadence: a 1 MHz build's + // 8 s window is 8 M cycles — tens of wall milliseconds — so a + // first knock lost to an in-flight reset misses the window + // entirely. Pace the simulation only while the bridge is fully + // quiet (nothing decoding, nothing queued); transfers keep full + // speed, and a quiet window stretches toward real time. + if (!rx_active && !tx_active && rx_head == rx_tail) + usleep(200); } } finish(0);