Compare commits
37 Commits
4e5322b1df
...
v1
| Author | SHA1 | Date | |
|---|---|---|---|
| a58b59c60f | |||
| ceacb61ba1 | |||
| dd7490a3df | |||
| bc24b63e65 | |||
| 332b244ac3 | |||
| 61b687120c | |||
| ef58d5d363 | |||
| 03bb1f56cf | |||
| 6a5e313530 | |||
| a28fccf475 | |||
| 92503fbb2d | |||
| 8ba023ed6b | |||
| 443fd39d31 | |||
| 08a0d77109 | |||
| 260ab2e4e3 | |||
| e4aaf5bc62 | |||
| 653602058a | |||
| 57d1c009cc | |||
| 06bb06d994 | |||
| 9a9a7ef0e8 | |||
| 4acf358dda | |||
| 11c10f986d | |||
| f587f0a26e | |||
| 27c768a102 | |||
| d989b5b8cd | |||
| 314e422b19 | |||
| 3e0717ef06 | |||
| 812be910c1 | |||
| 34529c8031 | |||
| 06bd56af50 | |||
| 75bb84fa3e | |||
| 778b8a0100 | |||
| 48d11fe8e3 | |||
| bb4784ae0c | |||
| d07586652d | |||
| 3a2f2a23c2 | |||
| 4fa058653e |
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "libavr"]
|
||||||
|
path = libavr
|
||||||
|
url = ../libavr.git
|
||||||
304
CMakeLists.txt
304
CMakeLists.txt
@@ -8,6 +8,9 @@ include(FetchContent)
|
|||||||
if(NOT LIBAVR_ROOT AND DEFINED ENV{LIBAVR_ROOT})
|
if(NOT LIBAVR_ROOT AND DEFINED ENV{LIBAVR_ROOT})
|
||||||
set(LIBAVR_ROOT $ENV{LIBAVR_ROOT})
|
set(LIBAVR_ROOT $ENV{LIBAVR_ROOT})
|
||||||
endif()
|
endif()
|
||||||
|
if(NOT LIBAVR_ROOT)
|
||||||
|
set(LIBAVR_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/libavr)
|
||||||
|
endif()
|
||||||
if(LIBAVR_ROOT)
|
if(LIBAVR_ROOT)
|
||||||
FetchContent_Declare(libavr SOURCE_DIR ${LIBAVR_ROOT})
|
FetchContent_Declare(libavr SOURCE_DIR ${LIBAVR_ROOT})
|
||||||
else()
|
else()
|
||||||
@@ -126,154 +129,31 @@ if(LIBAVR_MCU STREQUAL "atmega328p")
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
# pureboot — the pure-constraint port (see pureboot/README.md): one source,
|
# pureboot — the pure-constraint port (see pureboot/README.md): one source,
|
||||||
# no inline assembly, no global register variables, every libavr chip, 512
|
# no inline assembly, no global register variables, every libavr chip,
|
||||||
# bytes each. The loader owns the top 512 bytes of flash on every chip; the
|
# fitting each chip's smallest boot sector. The geometry and the
|
||||||
# application entry symbol is address 0 on the mega (reset re-vectors to the
|
# pureboot_add_loader() deployment function live in pureboot/CMakeLists.txt —
|
||||||
# loader through BOOTRST, so word 0 stays the application's own vector) and
|
# the unit a downstream project consumes; everything below is this port's
|
||||||
# the trampoline word just below the loader on the tinies (host-side vector
|
# own build: the stock loaders, their tests, and the size matrix. The
|
||||||
# surgery points it at the application). --pmem-wrap-around models AVR's
|
# distinct binary dir keeps the `pureboot` target's output name free.
|
||||||
# modulo-flash PC where the flash is big enough to need it.
|
add_subdirectory(pureboot pureboot-cmake)
|
||||||
#
|
|
||||||
# The image is position-independent (check_pi.py asserts the two link-time
|
# The stock loader: the family-default deployment (crystal/RC clock, the
|
||||||
# facts that make it so), and on the tinies its budget is 510, not 512: the
|
# chip's natural link, default pins). The activation window stays a cache
|
||||||
# slot's last word is the trampoline the host composes — the resident slot's
|
# variable — re-timing a deployed loader is a self-update with a re-timed
|
||||||
# holds the application entry, and a staging copy's holds the jump through
|
# build. pureboot9 is that re-timed build, and what the update test installs.
|
||||||
# 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")
|
set(PUREBOOT_TIMEOUT 8 CACHE STRING "pureboot activation window, seconds")
|
||||||
# Per-family geometry. The boot-sectioned megas run the loader from the
|
pureboot_add_loader(pureboot TIMEOUT ${PUREBOOT_TIMEOUT})
|
||||||
# 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} $<TARGET_FILE:pureboot>)
|
|
||||||
add_image_outputs(pureboot)
|
|
||||||
if(PROJECT_IS_TOP_LEVEL)
|
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
|
add_test(NAME pureboot.size
|
||||||
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:pureboot>
|
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:pureboot>
|
||||||
-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)
|
if(Python3_FOUND)
|
||||||
add_test(NAME pureboot.pi
|
add_test(NAME pureboot.pi
|
||||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/check_pi.py
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/check_pi.py
|
||||||
${CMAKE_OBJDUMP} ${CMAKE_NM} $<TARGET_FILE:pureboot> ${_pb_base_hex})
|
${CMAKE_OBJDUMP} ${CMAKE_NM} $<TARGET_FILE:pureboot> ${PUREBOOT_BASE_HEX})
|
||||||
add_test(NAME pureboot.planner
|
add_test(NAME pureboot.planner
|
||||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/test_planner.py
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/test_planner.py
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py)
|
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py)
|
||||||
@@ -289,9 +169,9 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:pbapp> $<TARGET_FILE:pbapp>.bin)
|
COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:pbapp> $<TARGET_FILE:pbapp>.bin)
|
||||||
add_test(NAME pureboot.protocol
|
add_test(NAME pureboot.protocol
|
||||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbtest.py
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbtest.py
|
||||||
${PB_DEVICE} $<TARGET_FILE:pureboot> ${_pb_sim_mcu} ${_pb_hz} ${_pb_base_hex}
|
${PB_DEVICE} $<TARGET_FILE:pureboot> ${PUREBOOT_SIM_MCU} ${_pb_stock_hz}
|
||||||
${_pb_page} ${_pb_baud} ${_pb_eeprom} $<TARGET_FILE:pbapp>.bin
|
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_pb_stock_baud} ${PUREBOOT_EEPROM}
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
$<TARGET_FILE:pbapp>.bin ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
${CMAKE_BINARY_DIR}/pbtest-work)
|
${CMAKE_BINARY_DIR}/pbtest-work)
|
||||||
set_tests_properties(pureboot.protocol PROPERTIES TIMEOUT 180)
|
set_tests_properties(pureboot.protocol PROPERTIES TIMEOUT 180)
|
||||||
|
|
||||||
@@ -299,43 +179,143 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
# installed one slot lower, must serve the full command set.
|
# installed one slot lower, must serve the full command set.
|
||||||
add_test(NAME pureboot.reloc
|
add_test(NAME pureboot.reloc
|
||||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbreloc.py
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbreloc.py
|
||||||
${PB_DEVICE} $<TARGET_FILE:pureboot> ${_pb_sim_mcu} ${_pb_hz} ${_pb_base_hex}
|
${PB_DEVICE} $<TARGET_FILE:pureboot> ${PUREBOOT_SIM_MCU} ${_pb_stock_hz}
|
||||||
${_pb_page} ${_pb_baud} ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_pb_stock_baud}
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
${CMAKE_BINARY_DIR}/pbreloc-work)
|
${CMAKE_BINARY_DIR}/pbreloc-work)
|
||||||
set_tests_properties(pureboot.reloc PROPERTIES TIMEOUT 180
|
set_tests_properties(pureboot.reloc PROPERTIES TIMEOUT 180
|
||||||
ENVIRONMENT "PB_OBJCOPY=${CMAKE_OBJCOPY}")
|
ENVIRONMENT "PB_OBJCOPY=${CMAKE_OBJCOPY}")
|
||||||
|
|
||||||
|
# Entering the loader from a running application with no reset
|
||||||
|
# between, over a page buffer the application dirtied — the case the
|
||||||
|
# loader declines to guard and the host repairs. Hardware forbids the
|
||||||
|
# state here (SPM runs only from the boot section); simavr does not,
|
||||||
|
# which is what makes it constructible.
|
||||||
|
if(LIBAVR_MCU STREQUAL "atmega328p")
|
||||||
|
add_test(NAME pureboot.dirty
|
||||||
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbdirty.py
|
||||||
|
${PB_DEVICE} $<TARGET_FILE:pureboot> ${PUREBOOT_SIM_MCU} ${_pb_stock_hz}
|
||||||
|
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_pb_stock_baud}
|
||||||
|
$<TARGET_FILE:pbapp>.bin
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
|
${CMAKE_BINARY_DIR}/pbdirty-work)
|
||||||
|
set_tests_properties(pureboot.dirty PROPERTIES TIMEOUT 180)
|
||||||
|
endif()
|
||||||
|
|
||||||
# Re-homing: a loader mistakenly programmed at address 0 (a raw .bin
|
# Re-homing: a loader mistakenly programmed at address 0 (a raw .bin
|
||||||
# handed to a programmer) must heal into the canonical slot through
|
# handed to a programmer) or sitting in the staging slot must heal
|
||||||
# the ordinary --update-loader flow. Patched-vector behavior, so one
|
# into the canonical slot through the ordinary --update-loader flow.
|
||||||
# representative chip carries it.
|
# Patched-vector behavior, so one representative chip carries it.
|
||||||
if(LIBAVR_MCU STREQUAL "attiny85")
|
if(LIBAVR_MCU STREQUAL "attiny85")
|
||||||
add_test(NAME pureboot.rehome
|
add_test(NAME pureboot.rehome
|
||||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbrehome.py
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbrehome.py
|
||||||
${PB_DEVICE} $<TARGET_FILE:pureboot> $<TARGET_FILE:pureboot9>.bin ${_pb_sim_mcu}
|
${PB_DEVICE} $<TARGET_FILE:pureboot> $<TARGET_FILE:pureboot9>.bin
|
||||||
${_pb_hz} ${_pb_base_hex} ${_pb_page} ${_pb_baud} $<TARGET_FILE:pbapp>.bin
|
${PUREBOOT_SIM_MCU} ${_pb_stock_hz} ${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE}
|
||||||
|
${_pb_stock_baud} $<TARGET_FILE:pbapp>.bin
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
${CMAKE_BINARY_DIR}/pbrehome-work)
|
${CMAKE_BINARY_DIR}/pbrehome-work)
|
||||||
set_tests_properties(pureboot.rehome PROPERTIES TIMEOUT 180)
|
set_tests_properties(pureboot.rehome PROPERTIES TIMEOUT 180)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# The self-update end-to-end: the re-timed build (same source, only
|
# The self-update end-to-end: the re-timed build (same source, only
|
||||||
# PUREBOOT_TIMEOUT differs — a byte-different image) replaces the
|
# the timeout differs — a byte-different image) replaces the resident
|
||||||
# resident through --update-loader, with every power-fail phase
|
# through --update-loader, with every power-fail phase rehearsed from
|
||||||
# rehearsed from the runner's flash dumps.
|
# the runner's flash dumps.
|
||||||
add_executable(pureboot9 pureboot/pureboot.cpp)
|
pureboot_add_loader(pureboot9 TIMEOUT 9)
|
||||||
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
|
add_test(NAME pureboot.update
|
||||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbupdate.py
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbupdate.py
|
||||||
${PB_DEVICE} $<TARGET_FILE:pureboot> $<TARGET_FILE:pureboot9> ${_pb_sim_mcu}
|
${PB_DEVICE} $<TARGET_FILE:pureboot> $<TARGET_FILE:pureboot9>
|
||||||
${_pb_hz} ${_pb_base_hex} ${_pb_page} ${_pb_baud} $<TARGET_FILE:pbapp>.bin
|
${PUREBOOT_SIM_MCU} ${_pb_stock_hz} ${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE}
|
||||||
|
${_pb_stock_baud} $<TARGET_FILE:pbapp>.bin
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
${CMAKE_BINARY_DIR}/pbupdate-work)
|
${CMAKE_BINARY_DIR}/pbupdate-work)
|
||||||
set_tests_properties(pureboot.update PROPERTIES TIMEOUT 600
|
set_tests_properties(pureboot.update PROPERTIES TIMEOUT 600
|
||||||
ENVIRONMENT "PB_OBJCOPY=${CMAKE_OBJCOPY}")
|
ENVIRONMENT "PB_OBJCOPY=${CMAKE_OBJCOPY}")
|
||||||
endif()
|
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=$<TARGET_FILE:${name}>
|
||||||
|
-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
|
||||||
|
$<TARGET_FILE:pbapp_custom> $<TARGET_FILE:pbapp_custom>.bin)
|
||||||
|
add_test(NAME pureboot.custom
|
||||||
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbtest.py
|
||||||
|
${PB_DEVICE} $<TARGET_FILE:pureboot_custom> ${PUREBOOT_SIM_MCU} ${_custom_hz}
|
||||||
|
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_custom_baud} ${PUREBOOT_EEPROM}
|
||||||
|
$<TARGET_FILE:pbapp_custom>.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
|
||||||
|
$<TARGET_FILE:pbapp_usart1> $<TARGET_FILE:pbapp_usart1>.bin)
|
||||||
|
add_test(NAME pureboot.usart1
|
||||||
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbtest.py
|
||||||
|
${PB_DEVICE} $<TARGET_FILE:pureboot_usart1> ${PUREBOOT_SIM_MCU} ${_usart1_hz}
|
||||||
|
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_usart1_baud} ${PUREBOOT_EEPROM}
|
||||||
|
$<TARGET_FILE:pbapp_usart1>.bin ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
|
${CMAKE_BINARY_DIR}/pbusart1-work usart1)
|
||||||
|
set_tests_properties(pureboot.usart1 PROPERTIES TIMEOUT 180)
|
||||||
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
2558
CMakePresets.json
2558
CMakePresets.json
File diff suppressed because it is too large
Load Diff
1
libavr
Submodule
1
libavr
Submodule
Submodule libavr added at e81dad0131
319
pureboot/CMakeLists.txt
Normal file
319
pureboot/CMakeLists.txt
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
# 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(<name> [CLOCK <hz>] [BAUD <bd>]
|
||||||
|
# [SERIAL auto|hardware|software] [USART <n>]
|
||||||
|
# [RX <pin>] [TX <pin>] [TIMEOUT <s>])
|
||||||
|
#
|
||||||
|
# Creates the loader target plus its flashable images (<name>.hex for a
|
||||||
|
# programmer, <name>.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:<RX>,<TX> — 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:<RX>,<TX>
|
||||||
|
# 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})
|
||||||
|
# Codegen shaping for the loader TU only, worth ~40 B on every chip and
|
||||||
|
# what carries the far-flash 1284 build under 512. At -Os GCC otherwise
|
||||||
|
# rewrites the byte-stream loops' counters into end-pointer forms that
|
||||||
|
# cost registers (-fno-ivopts, -fno-split-wide-types), leaves register
|
||||||
|
# pressure on the table with the default allocator
|
||||||
|
# (-fira-algorithm=priority), and spends bytes on rewrites a
|
||||||
|
# straight-line loader gains nothing from.
|
||||||
|
target_compile_options(${name} PRIVATE
|
||||||
|
-fno-ivopts -fira-algorithm=priority -fno-expensive-optimizations -fno-split-wide-types)
|
||||||
|
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} $<TARGET_FILE:${name}>)
|
||||||
|
# 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
|
||||||
|
$<TARGET_FILE:${name}> $<TARGET_FILE:${name}>.hex
|
||||||
|
COMMAND ${CMAKE_OBJCOPY} -O binary -R .eeprom
|
||||||
|
$<TARGET_FILE:${name}> $<TARGET_FILE:${name}>.bin)
|
||||||
|
set_target_properties(${name} PROPERTIES PUREBOOT_HZ ${PB_CLOCK} PUREBOOT_BAUD ${PB_BAUD}
|
||||||
|
PUREBOOT_LINK ${_link})
|
||||||
|
endfunction()
|
||||||
@@ -2,17 +2,27 @@
|
|||||||
|
|
||||||
A serial bootloader on [libavr](https://git.blackmark.me/avr/libavr), pure by
|
A serial bootloader on [libavr](https://git.blackmark.me/avr/libavr), pure by
|
||||||
constraint: one C++ source, no inline assembly, no global register variables
|
constraint: one C++ source, no inline assembly, no global register variables
|
||||||
(attributes allowed), built for **every chip libavr targets — all 37 —
|
(attributes and compiler flags allowed), built for **every chip libavr
|
||||||
fitting each chip's smallest boot sector**: 512 bytes everywhere — 488 B on
|
targets — all 37 — in 512 bytes each**: 434 B on the tiny13s, 438–442 B on
|
||||||
the tiny13s, 498–502 B on the tiny25/45/85, 466–504 B across the megas
|
the tiny25/45/85, 412–452 B across the megas, and 506 B on the
|
||||||
(474 B on the boot-section-less m48s, 498 B on the 644s) — except the
|
ATmega1284/1284P, whose far-flash machinery (ELPM reads, RAMPZ page commands,
|
||||||
ATmega1284/1284P, whose smallest boot sector is 1 KiB and whose far-flash
|
word-addressed wire) is the heaviest. Those are the stock deployments;
|
||||||
machinery (ELPM reads, RAMPZ page commands, word-addressed wire) lands at
|
choosing the software UART where the chip has a USART costs 8–46 B more (a
|
||||||
558 B in a 1 KiB slot: the 512-byte figure is a hardware boundary those
|
bit-bang against a peripheral), which every chip still absorbs inside its
|
||||||
chips simply do not have, and no implementation of this feature set fits it
|
slot — on the 1284s that means their 1 KiB boot sector, where the
|
||||||
there. The device speaks primitives; every composite — verify, erase,
|
software-serial image lands at 546 B. Bringing the 1284's default build
|
||||||
reset-vector surgery, updating the loader itself — lives in the host tool
|
under 512 at all is what the loop-placement attributes on the byte streamers
|
||||||
(`pureboot.py`).
|
(`pureboot.cpp`) and the codegen flags on the loader TU (`CMakeLists.txt`)
|
||||||
|
are for; measured against each chip's own budget the tightest is the
|
||||||
|
ATmega328P, 50 B spare. 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 1284s still *deploy* in a 1 KiB slot, their smallest boot sector being
|
||||||
|
512 words; at 506 B the image would also fit the 644's
|
||||||
|
two-512-byte-slots-per-boot-sector geometry.
|
||||||
|
|
||||||
The image is **position-independent**: control flow is PC-relative, the
|
The image is **position-independent**: control flow is PC-relative, the
|
||||||
read/write paths take wire addresses, the write guard protects the slot the
|
read/write paths take wire addresses, the write guard protects the slot the
|
||||||
@@ -26,16 +36,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
|
minimum); on the tinies the budget is 510, not 512: a slot's last word
|
||||||
belongs to the host-managed trampoline (below).
|
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 <hz>` | the clock the board runs | 16 MHz megas, 8 MHz t25/45/85, 9.6 MHz t13s |
|
||||||
|
| `BAUD <bd>` | the wire rate | the ladder below |
|
||||||
|
| `SERIAL auto\|hardware\|software` | the link backend | `auto`: the hardware USART where the chip has one |
|
||||||
|
| `USART <n>` | the USART instance (x4 megas carry two) | 0 |
|
||||||
|
| `RX <pin>`, `TX <pin>` | software-UART pins | `pb0`, `pb1` |
|
||||||
|
| `TIMEOUT <s>` | 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
|
## Link
|
||||||
|
|
||||||
|
The stock builds assume the family's natural deployment; any axis moves
|
||||||
|
per build (above).
|
||||||
|
|
||||||
| Chip | Serial | Baud | Clock assumed |
|
| Chip | Serial | Baud | Clock assumed |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| every ATmega | the hardware USART (USART0), RXD/TXD per pinout | 115200 8N1 | 16 MHz crystal |
|
| 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 |
|
| 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 |
|
| 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
|
The software-UART RX pin has its pull-up enabled; TX idles high. All
|
||||||
quantities on the wire are little-endian.
|
multi-byte quantities on the wire are little-endian.
|
||||||
|
|
||||||
## Activation
|
## Activation
|
||||||
|
|
||||||
@@ -50,9 +107,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
|
awaited again (line noise cannot lock the loader, only delay it). A window
|
||||||
expiring with an idle line boots the application.
|
expiring with an idle line boots the application.
|
||||||
|
|
||||||
The window length is a compile-time constant — 8 s by default, another value
|
The window length is a compile-time constant — 8 s by default, another
|
||||||
via the `PUREBOOT_TIMEOUT` CMake cache variable — so the whole EEPROM belongs
|
value via `pureboot_add_loader(... TIMEOUT <s>)` (the stock target keeps
|
||||||
to the application; pureboot never uses it for its own state. Re-timing a
|
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).
|
deployed loader is a self-update with a re-timed build (below).
|
||||||
|
|
||||||
## Session
|
## Session
|
||||||
@@ -83,7 +141,20 @@ byte-addressed). EEPROM addresses are always bytes, counts always bytes.
|
|||||||
then erases and programs; the address must be page-aligned. Pages inside the
|
then erases and programs; the address must be page-aligned. Pages inside the
|
||||||
512-byte slot the loader is *running* in are drained but never programmed — a
|
512-byte slot the loader is *running* in are drained but never programmed — a
|
||||||
broken host cannot brick the running copy, and a staged copy may rewrite the
|
broken host cannot brick the running copy, and a staged copy may rewrite the
|
||||||
resident slot. `w` is host-paced: send the next byte only after the previous
|
resident slot.
|
||||||
|
|
||||||
|
The loader never clears the SPM buffer before a fill, so **one `W` may
|
||||||
|
program the wrong bytes, and the host is what fixes it**. The buffer is
|
||||||
|
write-once per word until cleared, and two things leave words in it: a
|
||||||
|
refused page (drained, never programmed) and — where SPM runs from anywhere,
|
||||||
|
the tinies and the m48s — an application that self-programmed before
|
||||||
|
entering. The next `W` takes those stale words, and clears them: a page write
|
||||||
|
auto-erases the buffer (§26.2.1; §19.2 on the tinies), so repeating it
|
||||||
|
programs correctly. The host therefore verifies every page it writes and
|
||||||
|
rewrites what comes back wrong (three retries, then it stops); a host that
|
||||||
|
programs without reading back cannot trust the first `W` after either event.
|
||||||
|
|
||||||
|
`w` is host-paced: send the next byte only after the previous
|
||||||
byte's `+`. `F` returns the bytes in the hardware's Z order; on a chip
|
byte's `+`. `F` returns the bytes in the hardware's Z order; on a chip
|
||||||
without an extended fuse byte (the ATtiny13A) that slot carries no meaning.
|
without an extended fuse byte (the ATtiny13A) that slot carries no meaning.
|
||||||
Fuse *writing* does not exist: SPM reaches flash (and, on the mega, lock
|
Fuse *writing* does not exist: SPM reaches flash (and, on the mega, lock
|
||||||
@@ -233,25 +304,55 @@ update, flash (erase / program / read / verify), EEPROM (erase / program /
|
|||||||
read / verify) — then the loader hands over to the application; `--stay`
|
read / verify) — then the loader hands over to the application; `--stay`
|
||||||
keeps the session alive instead, and a later invocation reconnects into it
|
keeps the session alive instead, and a later invocation reconnects into it
|
||||||
(the knock converges there too). `--flash` and `--eeprom` verify by
|
(the knock converges there too). `--flash` and `--eeprom` verify by
|
||||||
read-back unless `--no-verify`; images are raw binary, or Intel HEX by
|
read-back unless `--no-verify`, and a flash page that reads back wrong is
|
||||||
extension. `--force` overrides the refusable safety checks (today: flashing
|
rewritten up to three times before the run stops — the loader leaves one
|
||||||
|
recoverable way for a page to land wrong (see `W` above), and rewriting is
|
||||||
|
what clears it. `--verify-flash` only reports. 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).
|
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
|
## 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 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`
|
- `pureboot.pi` — the position-independence lint: no absolute `jmp`/`call`
|
||||||
in the image, the info block within its first 256 bytes;
|
in the image, the info block within its first 256 bytes;
|
||||||
- `pureboot.planner` — the host tool's pure logic: programming orders and
|
- `pureboot.planner` — the host tool's pure logic: programming orders and
|
||||||
their recovery properties, the surgery, the staging composition, the
|
their recovery properties, the surgery, the staging composition, the
|
||||||
boot-fuse decode, and the update preflight's error/warning matrix over
|
boot-fuse decode, the update preflight's error/warning matrix over
|
||||||
synthetic fuse bytes;
|
synthetic fuse bytes, and the repairing verify against a fake device — one
|
||||||
|
bad write repaired in a single rewrite, a page that never comes good
|
||||||
|
stopping after exactly three;
|
||||||
- `pureboot.protocol` — end to end against a simavr device
|
- `pureboot.protocol` — end to end against a simavr device
|
||||||
(`test/pureboot_device.c` — the mega's USART as a pty; on the tinies a
|
(`test/pureboot_device.c` — a hardware USART as a pty, or a cycle-timed
|
||||||
cycle-timed GPIO⇄pty bridge for the software UART, plus the SPM/NVM module
|
GPIO⇄pty bridge for a software-UART build, selected with `-l` to match
|
||||||
simavr's tiny cores lack) driven by the real host tool through
|
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
|
knock-from-reset, program + verify of both memories, session reconnect, an
|
||||||
external reset through the patched vector, and the hand-over to a fixture
|
external reset through the patched vector, and the hand-over to a fixture
|
||||||
application whose banner proves the launch — cross-checked against the
|
application whose banner proves the launch — cross-checked against the
|
||||||
@@ -260,11 +361,19 @@ Per chip preset, `ctest` runs:
|
|||||||
- `pureboot.reloc` — the identical image installed one slot below the
|
- `pureboot.reloc` — the identical image installed one slot below the
|
||||||
resident serves the complete command set from there (the
|
resident serves the complete command set from there (the
|
||||||
position-independence acceptance test);
|
position-independence acceptance test);
|
||||||
|
- `pureboot.dirty` (328P) — entering the loader from a running application
|
||||||
|
with no reset between, over an SPM page buffer the fixture deliberately
|
||||||
|
dirtied: the case the loader declines to guard against. A bare verify must
|
||||||
|
see the corruption, the repairing verify must fix it in one rewrite, and a
|
||||||
|
plain verify afterwards must pass. On the boot-sectioned megas hardware
|
||||||
|
forbids the state outright (SPM runs only from the boot section, and reset
|
||||||
|
erases the buffer), but simavr dispatches SPM from anywhere — which is what
|
||||||
|
makes the path constructible at all;
|
||||||
- `pureboot.update` — the full `--update-loader` flow to a re-timed build,
|
- `pureboot.update` — the full `--update-loader` flow to a re-timed build,
|
||||||
then every power-fail phase: the device is killed mid-write, restarted
|
then every power-fail phase: the device is killed mid-write, restarted
|
||||||
from its flash dump, and a re-run must complete the update with the
|
from its flash dump, and a re-run must complete the update with the
|
||||||
application intact throughout.
|
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 —
|
simulator-driven targets need simavr and a pty, so they are POSIX-only —
|
||||||
on Windows the tool is exercised against real hardware.
|
on Windows the tool is exercised against real hardware.
|
||||||
|
|||||||
@@ -38,22 +38,19 @@ constexpr auto off = avr::irq::guard_policy::unused;
|
|||||||
|
|
||||||
constexpr std::uint8_t ack = '+';
|
constexpr std::uint8_t ack = '+';
|
||||||
|
|
||||||
// Per-chip personality: the clocks the dogfood boards run (16 MHz crystal on
|
// Per-deployment personality, passed in by the build — pureboot_add_loader()
|
||||||
// the mega, calibrated RC on the tinies). The device signature comes straight
|
// (the CMake function next to this file) resolves the defaults: the clock the
|
||||||
// from the chip database (avr::hw::db.signature) — compile-time data is the
|
// board actually runs, the wire baud, the serial backend and its pins. The
|
||||||
// only universal source, since the tiny13A cannot even read its signature row
|
// device signature needs no configuring — it comes from the chip database
|
||||||
// from code.
|
// (avr::hw::db.signature), the only universal source, since the tiny13A
|
||||||
consteval avr::hertz_t clock()
|
// cannot even read its signature row from code.
|
||||||
{
|
#if !defined(PUREBOOT_CLOCK_HZ) || !defined(PUREBOOT_BAUD)
|
||||||
auto name = std::string_view{avr::hw::db.name};
|
#error \
|
||||||
if (name.starts_with("ATtiny13"))
|
"PUREBOOT_CLOCK_HZ and PUREBOOT_BAUD select this build's clock and baud — create loader targets with pureboot_add_loader() (README.md)"
|
||||||
return 9.6_MHz;
|
#endif
|
||||||
if (name.starts_with("ATtiny"))
|
|
||||||
return 8_MHz;
|
|
||||||
return 16_MHz;
|
|
||||||
}
|
|
||||||
|
|
||||||
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.
|
// The watchdog reset flag's home: MCUSR, or the classic megas' MCUCSR.
|
||||||
consteval std::int16_t wdrf_field()
|
consteval std::int16_t wdrf_field()
|
||||||
@@ -70,13 +67,11 @@ consteval std::int16_t wdrf_field()
|
|||||||
// without a hardware boot section — the tinies and the m48s, whose SPM
|
// without a hardware boot section — the tinies and the m48s, whose SPM
|
||||||
// runs from anywhere (Atmel-8271 §26). A boot section also means the CPU
|
// runs from anywhere (Atmel-8271 §26). A boot section also means the CPU
|
||||||
// runs on while the RWW section programs; everywhere else it halts through
|
// runs on while the RWW section programs; everywhere else it halts through
|
||||||
// the operation. The m48s still carry RWWSRE as their temporary-buffer
|
// the operation.
|
||||||
// discard (§26.2), so the discard picks by that bit, not by the section.
|
|
||||||
constexpr std::uint16_t slot_bytes = spm::flash_bytes > 65536 ? 1024 : 512;
|
constexpr std::uint16_t slot_bytes = spm::flash_bytes > 65536 ? 1024 : 512;
|
||||||
constexpr std::uint32_t base = spm::flash_bytes - slot_bytes;
|
constexpr std::uint32_t base = spm::flash_bytes - slot_bytes;
|
||||||
constexpr std::uint16_t page = spm::page_bytes;
|
constexpr std::uint16_t page = spm::page_bytes;
|
||||||
constexpr bool boot_section = avr::hw::curated::has_boot_section();
|
constexpr bool boot_section = avr::hw::curated::has_boot_section();
|
||||||
constexpr bool rww_discard = spm::detail::has_rww();
|
|
||||||
|
|
||||||
// Past 64 KiB a byte address no longer fits the wire's 16 bits, so on the
|
// Past 64 KiB a byte address no longer fits the wire's 16 bits, so on the
|
||||||
// large chips every flash address on the wire — and all slot arithmetic —
|
// large chips every flash address on the wire — and all slot arithmetic —
|
||||||
@@ -97,11 +92,11 @@ constexpr std::uint16_t wire_page_mask = word_flash ? (page / 2 - 1) : (page - 1
|
|||||||
#endif
|
#endif
|
||||||
constexpr std::uint8_t timeout_seconds = PUREBOOT_TIMEOUT;
|
constexpr std::uint8_t timeout_seconds = PUREBOOT_TIMEOUT;
|
||||||
|
|
||||||
// The 12-byte info block the host reads with the 'b' command; flash-resident
|
// The 12-byte info block the host reads with the 'b' command, flash-resident
|
||||||
// (there is no crt to copy a .data image), word-aligned so its wire (word)
|
// through flash_table (there is no crt to copy a .data image, and its storage
|
||||||
// address is exact on the large chips. The page byte is the wire count
|
// carries the word alignment 'b' needs to halve the address on the large
|
||||||
// convention: 0 means 256.
|
// chips). The page byte is the wire count convention: 0 means 256.
|
||||||
[[gnu::progmem]] alignas(2) inline constexpr std::array<std::uint8_t, 12> info_data = {
|
inline constexpr avr::flash_table<std::array<std::uint8_t, 12>{
|
||||||
'P',
|
'P',
|
||||||
'B',
|
'B',
|
||||||
1, // magic, protocol version
|
1, // magic, protocol version
|
||||||
@@ -116,36 +111,36 @@ constexpr std::uint8_t timeout_seconds = PUREBOOT_TIMEOUT;
|
|||||||
// bit 0: host must patch the reset vector (no hardware boot section);
|
// bit 0: host must patch the reset vector (no hardware boot section);
|
||||||
// bit 1: flash wire addresses are word addresses
|
// bit 1: flash wire addresses are word addresses
|
||||||
static_cast<std::uint8_t>((boot_section ? 0 : 1) | (word_flash ? 2 : 0)),
|
static_cast<std::uint8_t>((boot_section ? 0 : 1) | (word_flash ? 2 : 0)),
|
||||||
};
|
}>
|
||||||
|
info_data;
|
||||||
|
|
||||||
// The serial link: the hardware USART where the chip has one, the polled
|
// The serial link. PUREBOOT_USART forces a hardware USART instance,
|
||||||
// software UART (no vector — the table belongs to the application) on PB0/PB1
|
// PUREBOOT_SOFT_SERIAL the polled software UART (no vector — the table
|
||||||
// elsewhere. Both are class templates on the clock so only the selected
|
// belongs to the application) on PUREBOOT_RX/PUREBOOT_TX; with neither, the
|
||||||
// backend is ever instantiated. pending() is the cheap line test the
|
// chip's first USART where it has one and the software UART elsewhere. Both
|
||||||
// activation window polls; rx() then picks the byte up; drain() holds until
|
// are class templates on the clock so only the selected backend is ever
|
||||||
// the last transmitted frame is fully on the wire (the jump hand-over must
|
// instantiated. pending() is the cheap line test the activation window
|
||||||
// not let the target's re-init clip the ack).
|
// polls; rx() then picks the byte up; drain() holds until the last
|
||||||
template <avr::hertz_t C>
|
// transmitted frame is fully on the wire (the jump hand-over must not let
|
||||||
consteval std::int16_t rxc_field()
|
// the target's re-init clip the ack).
|
||||||
{
|
#if defined(PUREBOOT_SOFT_SERIAL) && defined(PUREBOOT_USART)
|
||||||
return avr::uart::detail::ufield<'0', "UCSR#A", "RXC#">();
|
#error "PUREBOOT_SOFT_SERIAL and PUREBOOT_USART select opposing serial backends"
|
||||||
}
|
#endif
|
||||||
|
#if !defined(PUREBOOT_RX)
|
||||||
template <avr::hertz_t C>
|
#define PUREBOOT_RX pb0
|
||||||
consteval std::int16_t txc_field()
|
#endif
|
||||||
{
|
#if !defined(PUREBOOT_TX)
|
||||||
return avr::uart::detail::ufield<'0', "UCSR#A", "TXC#">();
|
#define PUREBOOT_TX pb1
|
||||||
}
|
#endif
|
||||||
|
#if defined(PUREBOOT_USART)
|
||||||
template <avr::hertz_t C>
|
constexpr char usart_digit = '0' + PUREBOOT_USART;
|
||||||
consteval std::int16_t status_reg()
|
#else
|
||||||
{
|
constexpr char usart_digit = '0';
|
||||||
return avr::uart::detail::ureg<'0', "UCSR#A">();
|
#endif
|
||||||
}
|
|
||||||
|
|
||||||
template <avr::hertz_t C>
|
template <avr::hertz_t C>
|
||||||
struct hardware_link {
|
struct hardware_link {
|
||||||
using uart = avr::uart::usart0<C, {.baud = 115200_Bd, .max_baud_error = 2.5_pct}>;
|
using uart = avr::uart::usart<usart_digit, C, {.baud = wire_baud, .max_baud_error = 2.5_pct}>;
|
||||||
|
|
||||||
// The compiled idle poll: lds UCSR0A (2), sbrc skipping the exit (2),
|
// The compiled idle poll: lds UCSR0A (2), sbrc skipping the exit (2),
|
||||||
// sbiw + sbci + sbci + brne (6).
|
// sbiw + sbci + sbci + brne (6).
|
||||||
@@ -158,7 +153,7 @@ struct hardware_link {
|
|||||||
|
|
||||||
static bool pending()
|
static bool pending()
|
||||||
{
|
{
|
||||||
return avr::hw::field_impl<rxc_field<C>()>::test();
|
return uart::rx_ready();
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::uint8_t rx()
|
static std::uint8_t rx()
|
||||||
@@ -173,22 +168,14 @@ struct hardware_link {
|
|||||||
|
|
||||||
static void drain()
|
static void drain()
|
||||||
{
|
{
|
||||||
// write() leaves the byte draining behind it. Clear a stale TXC0
|
uart::drain();
|
||||||
// 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_reg<C>()>;
|
|
||||||
status::write(status::read());
|
|
||||||
while (!avr::hw::field_impl<txc_field<C>()>::test()) {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
template <avr::hertz_t C>
|
template <avr::hertz_t C>
|
||||||
struct software_link {
|
struct software_link {
|
||||||
using rx_t = avr::uart::software_rx_polled<C, avr::pb0, 57600_Bd>;
|
using rx_t = avr::uart::software_rx_polled<C, avr::PUREBOOT_RX, wire_baud>;
|
||||||
using tx_t = avr::uart::software_tx<C, avr::pb1, 57600_Bd>;
|
using tx_t = avr::uart::software_tx<C, avr::PUREBOOT_TX, wire_baud>;
|
||||||
|
|
||||||
// The compiled idle poll: sbis skipping the exit (2), sbiw + sbci +
|
// The compiled idle poll: sbis skipping the exit (2), sbiw + sbci +
|
||||||
// sbci + brne (6).
|
// sbci + brne (6).
|
||||||
@@ -201,7 +188,7 @@ struct software_link {
|
|||||||
|
|
||||||
static bool pending()
|
static bool pending()
|
||||||
{
|
{
|
||||||
return !avr::io::input<avr::pb0>::read(); // a start bit has begun
|
return rx_t::start_pending();
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::uint8_t rx()
|
static std::uint8_t rx()
|
||||||
@@ -220,8 +207,15 @@ struct software_link {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
using link = std::conditional_t<avr::hw::db.has_instance("USART0") || avr::hw::db.has_instance("USART"),
|
#if defined(PUREBOOT_USART)
|
||||||
hardware_link<dev::clock>, software_link<dev::clock>>;
|
static_assert(avr::uart::has_usart<usart_digit>(), "PUREBOOT_USART selects a hardware USART this chip does not have");
|
||||||
|
using link = hardware_link<dev::clock>;
|
||||||
|
#elif defined(PUREBOOT_SOFT_SERIAL)
|
||||||
|
using link = software_link<dev::clock>;
|
||||||
|
#else
|
||||||
|
using link =
|
||||||
|
std::conditional_t<avr::uart::has_usart<usart_digit>(), hardware_link<dev::clock>, software_link<dev::clock>>;
|
||||||
|
#endif
|
||||||
|
|
||||||
// The application's entry, an absolute address the linker pins (--defsym in
|
// 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
|
// CMakeLists.txt): 0x0000 on the mega (word 0 stays the application's own
|
||||||
@@ -270,36 +264,51 @@ std::uint8_t rx_deadline()
|
|||||||
return link::rx();
|
return link::rx();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::uint16_t rx16()
|
// Inlined into its call sites: reading two bytes across a call otherwise
|
||||||
|
// strands the first in a call-saved register the caller must push/pop; folded
|
||||||
|
// into the (noreturn) command loop that cost disappears.
|
||||||
|
[[gnu::always_inline]] inline std::uint16_t rx16()
|
||||||
{
|
{
|
||||||
std::uint16_t low = link::rx();
|
std::uint16_t low = link::rx();
|
||||||
return static_cast<std::uint16_t>(low | (link::rx() << 8));
|
return static_cast<std::uint16_t>(low | (link::rx() << 8));
|
||||||
}
|
}
|
||||||
|
|
||||||
// The streamers take the count in the wire's 8-bit form: 0 means 256.
|
// The streamers take the count in the wire's 8-bit form: 0 means 256.
|
||||||
// send_flash stays out of line: its two callers ('b' and 'R') otherwise each
|
//
|
||||||
// inline a private copy of the loop. On the large chips the address is a
|
// Two functions, because they want opposite placement and placement is an
|
||||||
// word address and the read goes through ELPM (flash_load_far).
|
// attribute: the byte-addressed loop is small enough to inline into both
|
||||||
[[gnu::noinline]] void send_flash(std::uint16_t address, std::uint8_t count)
|
// callers, the word-addressed one stays out of line but flattened — a call to
|
||||||
|
// the transmit inside it would strand the 24-bit cursor in callee-saved
|
||||||
|
// registers. `word_flash` picks at the call site.
|
||||||
|
[[maybe_unused, gnu::always_inline]] inline void send_flash_near(std::uint16_t address, std::uint8_t count)
|
||||||
{
|
{
|
||||||
if constexpr (word_flash) {
|
|
||||||
// The 24-bit cursor as the machine holds it: the RAMPZ byte and a
|
|
||||||
// 16-bit Z, carried explicitly (the reassembled 32-bit address
|
|
||||||
// folds away inside the inlined far load). A single read never
|
|
||||||
// crosses a 64 KiB boundary — the protocol forbids it and the host
|
|
||||||
// splits its chunks there — so RAMPZ holds for the whole run.
|
|
||||||
std::uint8_t rampz = static_cast<std::uint8_t>(address >> 15);
|
|
||||||
std::uint16_t z = static_cast<std::uint16_t>(address << 1);
|
|
||||||
do {
|
|
||||||
link::tx(avr::flash_load_far<std::uint8_t>((static_cast<std::uint32_t>(rampz) << 16) | z));
|
|
||||||
if (++z == 0)
|
|
||||||
++rampz; // robustness for a host that reads across 64 KiB
|
|
||||||
} while (--count);
|
|
||||||
} else {
|
|
||||||
do
|
do
|
||||||
link::tx(avr::flash_load(reinterpret_cast<const std::uint8_t *>(address++)));
|
link::tx(avr::flash_load(reinterpret_cast<const std::uint8_t *>(address++)));
|
||||||
while (--count);
|
while (--count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The 24-bit cursor as the machine holds it: the RAMPZ byte and a 16-bit Z,
|
||||||
|
// carried explicitly (the reassembled 32-bit address folds away inside the
|
||||||
|
// inlined far load).
|
||||||
|
[[maybe_unused, gnu::flatten, gnu::noinline]] void send_flash_far(std::uint16_t address, std::uint8_t count)
|
||||||
|
{
|
||||||
|
std::uint8_t rampz = static_cast<std::uint8_t>(address >> 15);
|
||||||
|
std::uint16_t z = static_cast<std::uint16_t>(address << 1);
|
||||||
|
do {
|
||||||
|
link::tx(avr::flash_load_far<std::uint8_t>((static_cast<std::uint32_t>(rampz) << 16) | z));
|
||||||
|
// The protocol never reads across 64 KiB, but carrying the wrap is
|
||||||
|
// smaller than the flat 32-bit cursor GCC builds without it.
|
||||||
|
if (++z == 0)
|
||||||
|
++rampz;
|
||||||
|
} while (--count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[gnu::always_inline]] inline void send_flash(std::uint16_t address, std::uint8_t count)
|
||||||
|
{
|
||||||
|
if constexpr (word_flash)
|
||||||
|
send_flash_far(address, count);
|
||||||
|
else
|
||||||
|
send_flash_near(address, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
void send_eeprom(std::uint16_t address, std::uint8_t count)
|
void send_eeprom(std::uint16_t address, std::uint8_t count)
|
||||||
@@ -327,19 +336,15 @@ void store_eeprom(std::uint16_t address, std::uint8_t count)
|
|||||||
// itself. `slot_high` is the high byte of that running slot's base (run()
|
// itself. `slot_high` is the high byte of that running slot's base (run()
|
||||||
// derives it); a broken host thus cannot brick the running loader, and a
|
// derives it); a broken host thus cannot brick the running loader, and a
|
||||||
// copy flashed one slot lower may rewrite the slot above it — how pureboot
|
// copy flashed one slot lower may rewrite the slot above it — how pureboot
|
||||||
// updates itself. On the mega the RWW section is re-enabled so reads work
|
// updates itself.
|
||||||
// immediately.
|
|
||||||
void program_flash(std::uint16_t wire_address, std::uint8_t slot_high)
|
void program_flash(std::uint16_t wire_address, std::uint8_t slot_high)
|
||||||
{
|
{
|
||||||
// A buffer word cannot be loaded twice without an erase (§26.2.1), so a
|
// No discard before the fill: the buffer is write-once per word
|
||||||
// refused page's drained data must not linger for the next write:
|
// (§26.2.1), so filling over one a refused page or an application left
|
||||||
// discard the buffer up front — CTPB on the tinies; on the megas
|
// dirty programs stale words — but a page write auto-erases the buffer
|
||||||
// writing RWWSRE aborts a pending load (§26.2.2 — on the m48s that
|
// (§26.2.1; §19.2 on the tinies), so that write clears the condition and
|
||||||
// flush is the bit's whole documented job).
|
// the host's read-back rewrites the page.
|
||||||
if constexpr (rww_discard)
|
|
||||||
spm::rww_enable<off>();
|
|
||||||
else
|
|
||||||
spm::clear_buffer<off>();
|
|
||||||
// One induction either way. On the byte-addressed chips the wire address
|
// One induction either way. On the byte-addressed chips the wire address
|
||||||
// itself walks the page (aligned, so the offset bits wrap to zero); on
|
// itself walks the page (aligned, so the offset bits wrap to zero); on
|
||||||
// the word-addressed large chips the wire word address becomes a 32-bit
|
// the word-addressed large chips the wire word address becomes a 32-bit
|
||||||
@@ -385,12 +390,15 @@ void program_flash(std::uint16_t wire_address, std::uint8_t slot_high)
|
|||||||
if constexpr (boot_section)
|
if constexpr (boot_section)
|
||||||
spm::wait();
|
spm::wait();
|
||||||
spm::write_page<off>(address);
|
spm::write_page<off>(address);
|
||||||
if constexpr (boot_section) {
|
if constexpr (boot_section)
|
||||||
spm::wait();
|
spm::wait();
|
||||||
|
}
|
||||||
|
// The megas program with their RWW section disabled; reads need it back
|
||||||
|
// on. The same store discards the buffer (§26.2.2), so they never meet
|
||||||
|
// the stale-word case above. boot_section implies an RWW section.
|
||||||
|
if constexpr (boot_section)
|
||||||
spm::rww_enable<off>();
|
spm::rww_enable<off>();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The four fuse/lock bytes in the hardware's own Z order: low, lock,
|
// The four fuse/lock bytes in the hardware's own Z order: low, lock,
|
||||||
// extended, high. Writing fuses is not a thing self-programming can do on
|
// extended, high. Writing fuses is not a thing self-programming can do on
|
||||||
@@ -417,10 +425,13 @@ void send_fuses()
|
|||||||
// return address is a word address, whose high byte is the 256-word slot
|
// return address is a word address, whose high byte is the 256-word slot
|
||||||
// index — on byte-addressed chips doubled back into byte terms.
|
// index — on byte-addressed chips doubled back into byte terms.
|
||||||
// program_flash refuses this one slot and the info block is addressed
|
// 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<std::uint16_t>(__builtin_return_address(0));
|
const std::uint16_t ra_words = reinterpret_cast<std::uint16_t>(__builtin_return_address(0));
|
||||||
const std::uint8_t slot_high =
|
const std::uint8_t ra_high = static_cast<std::uint8_t>(std::byteswap(ra_words));
|
||||||
word_flash ? static_cast<std::uint8_t>(ra_words >> 8) & 0xfe : static_cast<std::uint8_t>((ra_words >> 8) << 1);
|
const std::uint8_t slot_high = word_flash ? ra_high & 0xfe : static_cast<std::uint8_t>(ra_high << 1);
|
||||||
|
|
||||||
// The knock: 'p' then 'b', each under a fresh window; any other byte is
|
// 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.
|
// line noise and waits again. Falling out of a window runs the app.
|
||||||
@@ -439,13 +450,13 @@ void send_fuses()
|
|||||||
// asserts it), and slots are 512-aligned — so the low byte of its
|
// asserts it), and slots are 512-aligned — so the low byte of its
|
||||||
// link address (in wire units: bytes, or words on the large
|
// link address (in wire units: bytes, or words on the large
|
||||||
// chips) is its offset in any slot, and the high byte of its
|
// 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
|
// runtime address is the running slot's. Composed from the two
|
||||||
// no absolute address is ever materialized.
|
// bytes — the high half is runtime data, so no absolute address
|
||||||
const auto link_low = reinterpret_cast<std::uint16_t>(info_data.data());
|
// is ever materialized.
|
||||||
|
const auto link_low = reinterpret_cast<std::uint16_t>(info_data.storage.data());
|
||||||
const std::uint8_t low =
|
const std::uint8_t low =
|
||||||
word_flash ? static_cast<std::uint8_t>(link_low >> 1) : static_cast<std::uint8_t>(link_low);
|
word_flash ? static_cast<std::uint8_t>(link_low >> 1) : static_cast<std::uint8_t>(link_low);
|
||||||
send_flash(std::bit_cast<std::uint16_t>(std::array{low, slot_high}),
|
send_flash(static_cast<std::uint16_t>(low | (slot_high << 8)), static_cast<std::uint8_t>(info_data.size()));
|
||||||
static_cast<std::uint8_t>(info_data.size()));
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'J': { // jump to a wire word address: hand-over and staging transfer
|
case 'J': { // jump to a wire word address: hand-over and staging transfer
|
||||||
|
|||||||
@@ -37,12 +37,59 @@ else:
|
|||||||
PROMPT = b"+"
|
PROMPT = b"+"
|
||||||
PROTOCOL_VERSION = 1
|
PROTOCOL_VERSION = 1
|
||||||
SLOT = 512 # the loader slot on byte-addressed chips; word-addressed ones (>64 KiB) use 1 KiB — their own smallest boot sector
|
SLOT = 512 # the loader slot on byte-addressed chips; word-addressed ones (>64 KiB) use 1 KiB — their own smallest boot sector
|
||||||
|
RETRIES = 3 # rewrites of a page that reads back wrong, before the run stops
|
||||||
|
|
||||||
|
VERBOSE = False
|
||||||
|
|
||||||
|
|
||||||
|
def verbose(message):
|
||||||
|
"""Detail printed only under --verbose: decisions and derived facts, not
|
||||||
|
per-byte chatter — the progress bar carries the bulk transfers."""
|
||||||
|
if VERBOSE:
|
||||||
|
print(f" {message}")
|
||||||
|
|
||||||
|
|
||||||
class Error(Exception):
|
class Error(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Progress:
|
||||||
|
"""A transient in-place bar on stderr for the operations that take wire
|
||||||
|
time. Drawn only when stderr is a tty — logs, pipes and the test harness
|
||||||
|
see nothing — and erased once done; the summary line each operation
|
||||||
|
prints afterwards is the persistent record. A zero total (or no label)
|
||||||
|
disables it, so callers can pass one through unconditionally."""
|
||||||
|
|
||||||
|
def __init__(self, label, total, unit="pages"):
|
||||||
|
self.label, self.total, self.unit = label, total, unit
|
||||||
|
self.done = 0
|
||||||
|
self.width = 0
|
||||||
|
self.live = bool(label) and total > 0 and sys.stderr.isatty()
|
||||||
|
self._draw()
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *exc):
|
||||||
|
if self.live:
|
||||||
|
sys.stderr.write("\r" + " " * self.width + "\r")
|
||||||
|
sys.stderr.flush()
|
||||||
|
|
||||||
|
def step(self, n=1):
|
||||||
|
self.done += n
|
||||||
|
self._draw()
|
||||||
|
|
||||||
|
def _draw(self):
|
||||||
|
if not self.live:
|
||||||
|
return
|
||||||
|
bar = 24 * self.done // self.total
|
||||||
|
line = (f"{self.label:<16} [{'#' * bar}{'-' * (24 - bar)}] "
|
||||||
|
f"{100 * self.done // self.total:3d}% {self.done}/{self.total} {self.unit}")
|
||||||
|
self.width = max(self.width, len(line))
|
||||||
|
sys.stderr.write("\r" + line)
|
||||||
|
sys.stderr.flush()
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------- serial ---
|
# ---------------------------------------------------------------- serial ---
|
||||||
|
|
||||||
|
|
||||||
@@ -295,6 +342,23 @@ class Info:
|
|||||||
f"EEPROM {self.eeprom_size} B, {vector}"
|
f"EEPROM {self.eeprom_size} B, {vector}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def lines(self):
|
||||||
|
"""The info block as one fact per line — what --info prints."""
|
||||||
|
if self.patch_vector:
|
||||||
|
hand_over = f"host-patched reset vector, trampoline at {self.base - 2:#06x}"
|
||||||
|
else:
|
||||||
|
hand_over = "hardware boot section, jump to word 0"
|
||||||
|
return (
|
||||||
|
f"signature {' '.join(f'{b:02x}' for b in self.signature)}",
|
||||||
|
f"flash {self.flash_size} B, {self.page} B pages"
|
||||||
|
+ (", word-addressed wire" if self.word_flash else ""),
|
||||||
|
f"application 0x0000..{self.base - 1:#06x} ({self.base} B)",
|
||||||
|
f"loader {self.base:#06x} ({self.slot} B slot)",
|
||||||
|
f"staging {self.stage:#06x}",
|
||||||
|
f"EEPROM {self.eeprom_size} B",
|
||||||
|
f"hand-over {hand_over}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Loader:
|
class Loader:
|
||||||
"""A pureboot session. Between commands the loader has prompted `+` and
|
"""A pureboot session. Between commands the loader has prompted `+` and
|
||||||
@@ -312,8 +376,10 @@ class Loader:
|
|||||||
absorbs whatever they produced."""
|
absorbs whatever they produced."""
|
||||||
self.port.flush_input()
|
self.port.flush_input()
|
||||||
deadline = time.monotonic() + wait
|
deadline = time.monotonic() + wait
|
||||||
|
knocks = 0
|
||||||
while True:
|
while True:
|
||||||
self.port.write(b"pb")
|
self.port.write(b"pb")
|
||||||
|
knocks += 1
|
||||||
if PROMPT in self.port.read_available(0.4):
|
if PROMPT in self.port.read_available(0.4):
|
||||||
break
|
break
|
||||||
if time.monotonic() > deadline:
|
if time.monotonic() > deadline:
|
||||||
@@ -323,6 +389,7 @@ class Loader:
|
|||||||
self.port.write(b"b")
|
self.port.write(b"b")
|
||||||
self.info = Info(self.port.read_exact(12, 2.0))
|
self.info = Info(self.port.read_exact(12, 2.0))
|
||||||
self._expect_prompt()
|
self._expect_prompt()
|
||||||
|
verbose(f"loader answered knock {knocks}; info block read")
|
||||||
return self.info
|
return self.info
|
||||||
|
|
||||||
def _expect_prompt(self, timeout=2.0):
|
def _expect_prompt(self, timeout=2.0):
|
||||||
@@ -373,7 +440,7 @@ class Loader:
|
|||||||
head = bytes((ord("W"), wire & 0xFF, wire >> 8))
|
head = bytes((ord("W"), wire & 0xFF, wire >> 8))
|
||||||
self._command(head + data, 0, 2.0)
|
self._command(head + data, 0, 2.0)
|
||||||
|
|
||||||
def write_eeprom(self, address, data):
|
def write_eeprom(self, address, data, progress=None):
|
||||||
offset = 0
|
offset = 0
|
||||||
while offset < len(data):
|
while offset < len(data):
|
||||||
chunk = data[offset : offset + 256]
|
chunk = data[offset : offset + 256]
|
||||||
@@ -382,6 +449,8 @@ class Loader:
|
|||||||
for byte in chunk:
|
for byte in chunk:
|
||||||
self.port.write(bytes((byte,)))
|
self.port.write(bytes((byte,)))
|
||||||
self._expect_prompt() # per-byte ack: the write has begun
|
self._expect_prompt() # per-byte ack: the write has begun
|
||||||
|
if progress:
|
||||||
|
progress.step()
|
||||||
self._expect_prompt() # the next command prompt
|
self._expect_prompt() # the next command prompt
|
||||||
address += len(chunk)
|
address += len(chunk)
|
||||||
offset += len(chunk)
|
offset += len(chunk)
|
||||||
@@ -487,6 +556,8 @@ def plan_flash(image, info):
|
|||||||
final += bytearray([0xFF] * (trampoline_page + page - len(final)))
|
final += bytearray([0xFF] * (trampoline_page + page - len(final)))
|
||||||
jump = rjmp_to(trampoline_word, entry, flash_words)
|
jump = rjmp_to(trampoline_word, entry, flash_words)
|
||||||
final[info.base - 2], final[info.base - 1] = jump & 0xFF, jump >> 8
|
final[info.base - 2], final[info.base - 1] = jump & 0xFF, jump >> 8
|
||||||
|
verbose(f"vector surgery: word 0 -> loader {info.base:#06x}, "
|
||||||
|
f"trampoline {info.base - 2:#06x} -> entry word {entry:#06x}")
|
||||||
|
|
||||||
pages = {a: bytes(final[a : a + page]) for a in range(0, len(final), page)}
|
pages = {a: bytes(final[a : a + page]) for a in range(0, len(final), page)}
|
||||||
return pages
|
return pages
|
||||||
@@ -681,20 +752,42 @@ class UpdateState:
|
|||||||
os.unlink(self.path)
|
os.unlink(self.path)
|
||||||
|
|
||||||
|
|
||||||
def write_differing(loader, base, content, order=None):
|
def write_differing(loader, base, content, order=None, label=None):
|
||||||
"""Program the pages of `content` at `base` that differ from flash —
|
"""Program the pages of `content` at `base` that differ from flash —
|
||||||
idempotent, so a resumed phase redoes only what an interruption left."""
|
idempotent, so a resumed phase redoes only what an interruption left.
|
||||||
|
A label puts the compare-and-program loop on the progress bar."""
|
||||||
page = loader.info.page
|
page = loader.info.page
|
||||||
offsets = order if order is not None else range(0, len(content), page)
|
offsets = list(order) if order is not None else list(range(0, len(content), page))
|
||||||
written = 0
|
written = 0
|
||||||
|
with Progress(label, len(offsets)) as bar:
|
||||||
for offset in offsets:
|
for offset in offsets:
|
||||||
want = content[offset : offset + page]
|
want = content[offset : offset + page]
|
||||||
if loader.read_flash(base + offset, page) != want:
|
if loader.read_flash(base + offset, page) != want:
|
||||||
loader.write_page(base + offset, want)
|
loader.write_page(base + offset, want)
|
||||||
written += 1
|
written += 1
|
||||||
for at in range(0, len(content), 256):
|
bar.step()
|
||||||
if loader.read_flash(base + at, min(256, len(content) - at)) != content[at : at + 256]:
|
if label:
|
||||||
raise Error(f"verify failed at {base + at:#06x} after programming")
|
verbose(f"{label}: {written} of {len(offsets)} pages differed")
|
||||||
|
# Page-wise read-back with the same bounded repair as verify_pages: this
|
||||||
|
# is the loader-update path, where a page left wrong is a half-written
|
||||||
|
# loader slot.
|
||||||
|
for retry in range(RETRIES + 1):
|
||||||
|
bad = [
|
||||||
|
offset
|
||||||
|
for offset in range(0, len(content), page)
|
||||||
|
if loader.read_flash(base + offset, len(content[offset : offset + page])) != content[offset : offset + page]
|
||||||
|
]
|
||||||
|
if not bad:
|
||||||
|
break
|
||||||
|
if retry == RETRIES:
|
||||||
|
raise Error(
|
||||||
|
f"verify failed at {base + bad[0]:#06x} after programming "
|
||||||
|
f"(still wrong after {RETRIES} retries)"
|
||||||
|
)
|
||||||
|
for offset in bad:
|
||||||
|
verbose(f"rewriting page {base + offset:#06x} (retry {retry + 1})")
|
||||||
|
loader.write_page(base + offset, content[offset : offset + page])
|
||||||
|
written += 1
|
||||||
return written
|
return written
|
||||||
|
|
||||||
|
|
||||||
@@ -723,6 +816,10 @@ def op_update_loader(loader, wait, path, state_path, fuse_bytes):
|
|||||||
page = info.page
|
page = info.page
|
||||||
|
|
||||||
state = UpdateState(state_path)
|
state = UpdateState(state_path)
|
||||||
|
if os.path.exists(state_path):
|
||||||
|
verbose(f"resuming the update recorded in {state_path}")
|
||||||
|
else:
|
||||||
|
verbose(f"saving the staging slot to {state_path}")
|
||||||
state.load_or_save(loader)
|
state.load_or_save(loader)
|
||||||
|
|
||||||
# Install the staging copy — unless a loader already sits whole in the
|
# Install the staging copy — unless a loader already sits whole in the
|
||||||
@@ -741,7 +838,7 @@ def op_update_loader(loader, wait, path, state_path, fuse_bytes):
|
|||||||
current = loader.read_flash(info.stage, info.slot)
|
current = loader.read_flash(info.stage, info.slot)
|
||||||
staged_loader = image_info(current[:268])
|
staged_loader = image_info(current[:268])
|
||||||
if staged_loader is not None and staged_loader.raw == info.raw and current == state.staging:
|
if staged_loader is not None and staged_loader.raw == info.raw and current == state.staging:
|
||||||
print(f"staging slot already holds a loader — left in place")
|
print("staging slot already holds a loader — left in place")
|
||||||
else:
|
else:
|
||||||
# On a chip whose staging slot starts at address 0 (the 1 KB
|
# On a chip whose staging slot starts at address 0 (the 1 KB
|
||||||
# tiny13s), its first page carries the reset vector: written last,
|
# tiny13s), its first page carries the reset vector: written last,
|
||||||
@@ -750,30 +847,34 @@ def op_update_loader(loader, wait, path, state_path, fuse_bytes):
|
|||||||
order = list(range(0, info.slot, page))
|
order = list(range(0, info.slot, page))
|
||||||
if info.stage == 0:
|
if info.stage == 0:
|
||||||
order = order[1:] + [0]
|
order = order[1:] + [0]
|
||||||
if write_differing(loader, info.stage, staged, order):
|
if write_differing(loader, info.stage, staged, order, label="staging copy"):
|
||||||
print(f"staging copy installed at {info.stage:#06x}")
|
print(f"staging copy installed at {info.stage:#06x}")
|
||||||
|
|
||||||
# Enter it and let it rewrite the resident slot. Where a patched reset
|
# Enter it and let it rewrite the resident slot. Where a patched reset
|
||||||
# vector routes through the resident (a tiny with the staging slot away
|
# vector routes through the resident (a tiny with the staging slot away
|
||||||
# from page 0), word 0 is re-aimed at the staging copy around the
|
# from page 0), word 0 is re-aimed at the staging copy around the
|
||||||
# rewrite, so a power failure mid-rewrite still resets into a loader.
|
# rewrite, so a power failure mid-rewrite still resets into a loader.
|
||||||
|
verbose(f"entering the staging copy at {info.stage:#06x}")
|
||||||
loader.enter_copy(info.stage, wait)
|
loader.enter_copy(info.stage, wait)
|
||||||
redirect = info.patch_vector and info.stage != 0
|
redirect = info.patch_vector and info.stage != 0
|
||||||
if redirect:
|
if redirect:
|
||||||
|
verbose("word 0 re-aimed at the staging copy for the rewrite")
|
||||||
patch_word0(loader, state.page0, info.stage)
|
patch_word0(loader, state.page0, info.stage)
|
||||||
if write_differing(loader, info.base, resident):
|
if write_differing(loader, info.base, resident, label="resident"):
|
||||||
print(f"resident loader rewritten at {info.base:#06x}")
|
print(f"resident loader rewritten at {info.base:#06x}")
|
||||||
|
|
||||||
# Enter the new resident and put the staging region back: page 0 first
|
# Enter the new resident and put the staging region back: page 0 first
|
||||||
# where it lives in that region (word 0 then points at the new resident
|
# where it lives in that region (word 0 then points at the new resident
|
||||||
# for the rest of the restore), the saved trampoline with the rest.
|
# for the rest of the restore), the saved trampoline with the rest.
|
||||||
|
verbose(f"entering the new resident at {info.base:#06x}")
|
||||||
loader.enter_copy(info.base, wait)
|
loader.enter_copy(info.base, wait)
|
||||||
if redirect:
|
if redirect:
|
||||||
|
verbose("word 0 restored")
|
||||||
write_differing(loader, 0, state.page0)
|
write_differing(loader, 0, state.page0)
|
||||||
order = list(range(0, info.slot, page))
|
order = list(range(0, info.slot, page))
|
||||||
if info.stage == 0:
|
if info.stage == 0:
|
||||||
order = [0] + order[1:]
|
order = [0] + order[1:]
|
||||||
write_differing(loader, info.stage, state.staging, order)
|
write_differing(loader, info.stage, state.staging, order, label="staging restore")
|
||||||
|
|
||||||
state.discard()
|
state.discard()
|
||||||
print(f"loader updated: {len(image)} B at {info.base:#06x}, staging region restored")
|
print(f"loader updated: {len(image)} B at {info.base:#06x}, staging region restored")
|
||||||
@@ -808,48 +909,85 @@ def op_erase_flash(loader):
|
|||||||
is erased and the reset walk reaches the loader anyway."""
|
is erased and the reset walk reaches the loader anyway."""
|
||||||
blank = bytes([0xFF] * loader.info.page)
|
blank = bytes([0xFF] * loader.info.page)
|
||||||
addresses = range(0, loader.info.base, loader.info.page)
|
addresses = range(0, loader.info.base, loader.info.page)
|
||||||
|
with Progress("erase", len(addresses)) as bar:
|
||||||
for address in reversed(addresses) if loader.info.patch_vector else addresses:
|
for address in reversed(addresses) if loader.info.patch_vector else addresses:
|
||||||
loader.write_page(address, blank)
|
loader.write_page(address, blank)
|
||||||
|
bar.step()
|
||||||
print(f"erase: {loader.info.base // loader.info.page} pages")
|
print(f"erase: {loader.info.base // loader.info.page} pages")
|
||||||
|
|
||||||
|
|
||||||
def op_erase_eeprom(loader):
|
def op_erase_eeprom(loader):
|
||||||
loader.write_eeprom(0, bytes([0xFF] * loader.info.eeprom_size))
|
with Progress("erase EEPROM", loader.info.eeprom_size, "B") as bar:
|
||||||
|
loader.write_eeprom(0, bytes([0xFF] * loader.info.eeprom_size), progress=bar)
|
||||||
print(f"erase: {loader.info.eeprom_size} B of EEPROM")
|
print(f"erase: {loader.info.eeprom_size} B of EEPROM")
|
||||||
|
|
||||||
|
|
||||||
def op_flash(loader, path, erase, verify, fuse_bytes=None, force=False):
|
def op_flash(loader, path, erase, verify, fuse_bytes=None, force=False):
|
||||||
image = load_image(path)
|
image = load_image(path)
|
||||||
|
verbose(f"{path}: {len(image)} B image")
|
||||||
pages = plan_flash(image, loader.info)
|
pages = plan_flash(image, loader.info)
|
||||||
check_walk_region(pages, loader.info, fuse_bytes, force)
|
check_walk_region(pages, loader.info, fuse_bytes, force)
|
||||||
if erase:
|
if erase:
|
||||||
op_erase_flash(loader)
|
op_erase_flash(loader)
|
||||||
order = covered(pages, loader.info, skip_blank=erase)
|
order = covered(pages, loader.info, skip_blank=erase)
|
||||||
|
if len(order) != len(pages):
|
||||||
|
verbose(f"{len(pages) - len(order)} blank pages skipped (erased flash underneath)")
|
||||||
|
with Progress("flash", len(order)) as bar:
|
||||||
for address in order:
|
for address in order:
|
||||||
loader.write_page(address, pages[address])
|
loader.write_page(address, pages[address])
|
||||||
|
bar.step()
|
||||||
print(f"flash: {path}: {len(order)} pages")
|
print(f"flash: {path}: {len(order)} pages")
|
||||||
if verify:
|
if verify:
|
||||||
verify_pages(loader, pages)
|
verify_pages(loader, pages, repair=True)
|
||||||
|
|
||||||
|
|
||||||
def verify_pages(loader, pages):
|
def verify_pages(loader, pages, repair=False):
|
||||||
|
"""Read every page back and compare. With `repair`, a mismatched page is
|
||||||
|
rewritten and re-read, up to RETRIES times before it is raised: a page
|
||||||
|
filled over a dirty SPM buffer takes stale words, and the write that took
|
||||||
|
them cleared the buffer, so one rewrite settles it. Anything still wrong
|
||||||
|
after three is not that, and stops the run."""
|
||||||
|
repaired = 0
|
||||||
|
with Progress("verify", len(pages)) as bar:
|
||||||
for address in sorted(pages):
|
for address in sorted(pages):
|
||||||
|
for retry in range(RETRIES + 1):
|
||||||
got = loader.read_flash(address, loader.info.page)
|
got = loader.read_flash(address, loader.info.page)
|
||||||
if got != pages[address]:
|
if got == pages[address]:
|
||||||
|
break
|
||||||
first = next(i for i in range(len(got)) if got[i] != pages[address][i])
|
first = next(i for i in range(len(got)) if got[i] != pages[address][i])
|
||||||
raise Error(
|
detail = (
|
||||||
f"verify failed at {address + first:#06x}: "
|
f"verify failed at {address + first:#06x}: "
|
||||||
f"wrote {pages[address][first]:02x}, read {got[first]:02x}"
|
f"wrote {pages[address][first]:02x}, read {got[first]:02x}"
|
||||||
)
|
)
|
||||||
print(f"verify: {len(pages)} pages ok")
|
if not repair:
|
||||||
|
raise Error(detail)
|
||||||
|
if retry == RETRIES:
|
||||||
|
raise Error(f"{detail} (still wrong after {RETRIES} retries)")
|
||||||
|
verbose(f"{detail} — rewriting page {address:#06x} (retry {retry + 1})")
|
||||||
|
loader.write_page(address, pages[address])
|
||||||
|
repaired += 1
|
||||||
|
bar.step()
|
||||||
|
note = f", {repaired} page rewrite(s)" if repaired else ""
|
||||||
|
print(f"verify: {len(pages)} pages ok{note}")
|
||||||
|
|
||||||
|
|
||||||
def op_verify_flash(loader, path):
|
def op_verify_flash(loader, path):
|
||||||
verify_pages(loader, plan_flash(load_image(path), loader.info))
|
verify_pages(loader, plan_flash(load_image(path), loader.info))
|
||||||
|
|
||||||
|
|
||||||
|
def read_progress(reader, total, label):
|
||||||
|
"""A bulk read in 256-byte wire chunks under a progress bar."""
|
||||||
|
data = b""
|
||||||
|
with Progress(label, total, "B") as bar:
|
||||||
|
while len(data) < total:
|
||||||
|
chunk = min(256, total - len(data))
|
||||||
|
data += reader(len(data), chunk)
|
||||||
|
bar.step(chunk)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
def op_read_flash(loader, path):
|
def op_read_flash(loader, path):
|
||||||
data = loader.read_flash(0, loader.info.base)
|
data = read_progress(loader.read_flash, loader.info.base, "read flash")
|
||||||
open(path, "wb").write(data)
|
open(path, "wb").write(data)
|
||||||
print(f"read flash: {len(data)} B -> {path}")
|
print(f"read flash: {len(data)} B -> {path}")
|
||||||
|
|
||||||
@@ -860,10 +998,11 @@ def op_eeprom(loader, path, erase, verify):
|
|||||||
raise Error(f"EEPROM image is {len(image)} B, device has {loader.info.eeprom_size}")
|
raise Error(f"EEPROM image is {len(image)} B, device has {loader.info.eeprom_size}")
|
||||||
if erase:
|
if erase:
|
||||||
op_erase_eeprom(loader)
|
op_erase_eeprom(loader)
|
||||||
loader.write_eeprom(0, image)
|
with Progress("eeprom", len(image), "B") as bar:
|
||||||
|
loader.write_eeprom(0, image, progress=bar)
|
||||||
print(f"eeprom: {path}: {len(image)} B")
|
print(f"eeprom: {path}: {len(image)} B")
|
||||||
if verify:
|
if verify:
|
||||||
got = loader.read_eeprom(0, len(image))
|
got = read_progress(loader.read_eeprom, len(image), "verify EEPROM")
|
||||||
if got != image:
|
if got != image:
|
||||||
first = next(i for i in range(len(got)) if got[i] != image[i])
|
first = next(i for i in range(len(got)) if got[i] != image[i])
|
||||||
raise Error(f"verify failed at EEPROM {first:#06x}: wrote {image[first]:02x}, read {got[first]:02x}")
|
raise Error(f"verify failed at EEPROM {first:#06x}: wrote {image[first]:02x}, read {got[first]:02x}")
|
||||||
@@ -872,7 +1011,7 @@ def op_eeprom(loader, path, erase, verify):
|
|||||||
|
|
||||||
def op_verify_eeprom(loader, path):
|
def op_verify_eeprom(loader, path):
|
||||||
image = load_image(path)
|
image = load_image(path)
|
||||||
got = loader.read_eeprom(0, len(image))
|
got = read_progress(loader.read_eeprom, len(image), "verify EEPROM")
|
||||||
if got != image:
|
if got != image:
|
||||||
first = next(i for i in range(len(got)) if got[i] != image[i])
|
first = next(i for i in range(len(got)) if got[i] != image[i])
|
||||||
raise Error(f"verify failed at EEPROM {first:#06x}: expected {image[first]:02x}, read {got[first]:02x}")
|
raise Error(f"verify failed at EEPROM {first:#06x}: expected {image[first]:02x}, read {got[first]:02x}")
|
||||||
@@ -880,15 +1019,30 @@ def op_verify_eeprom(loader, path):
|
|||||||
|
|
||||||
|
|
||||||
def op_read_eeprom(loader, path):
|
def op_read_eeprom(loader, path):
|
||||||
data = loader.read_eeprom(0, loader.info.eeprom_size)
|
data = read_progress(loader.read_eeprom, loader.info.eeprom_size, "read EEPROM")
|
||||||
open(path, "wb").write(data)
|
open(path, "wb").write(data)
|
||||||
print(f"read EEPROM: {len(data)} B -> {path}")
|
print(f"read EEPROM: {len(data)} B -> {path}")
|
||||||
|
|
||||||
|
|
||||||
def op_fuses(loader):
|
def op_fuses(loader):
|
||||||
low, lock, extended, high = loader.read_fuses()
|
low, lock, extended, high = loader.read_fuses()
|
||||||
print(f"fuses: low {low:02x} high {high:02x} extended {extended:02x} lock {lock:02x}")
|
print("fuses:")
|
||||||
return bytes((low, lock, extended, high))
|
print(f" low 0x{low:02x}")
|
||||||
|
print(f" high 0x{high:02x}")
|
||||||
|
print(f" extended 0x{extended:02x}")
|
||||||
|
print(f" lock 0x{lock:02x}")
|
||||||
|
fuse_bytes = bytes((low, lock, extended, high))
|
||||||
|
# On a boot-sectioned mega the BOOTSZ/BOOTRST decode is the fuse fact the
|
||||||
|
# loader's whole deployment hangs on — say it in words.
|
||||||
|
if not loader.info.patch_vector:
|
||||||
|
try:
|
||||||
|
bootrst, bls_start = mega_boot(loader.info, fuse_bytes)
|
||||||
|
reset = "reset enters it" if bootrst else "reset boots the application"
|
||||||
|
print(f" boot section at {bls_start:#06x} ({loader.info.flash_size - bls_start} B), "
|
||||||
|
f"BOOTRST {'programmed' if bootrst else 'unprogrammed'} — {reset}")
|
||||||
|
except Error:
|
||||||
|
pass # unknown signature: the raw bytes above still stand
|
||||||
|
return fuse_bytes
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------- cli ---
|
# -------------------------------------------------------------------- cli ---
|
||||||
@@ -918,7 +1072,11 @@ def main():
|
|||||||
parser.add_argument("--verify-eeprom", metavar="FILE", help="compare EEPROM against an image")
|
parser.add_argument("--verify-eeprom", metavar="FILE", help="compare EEPROM against an image")
|
||||||
parser.add_argument("--force", action="store_true", help="override refusable safety checks")
|
parser.add_argument("--force", action="store_true", help="override refusable safety checks")
|
||||||
parser.add_argument("--stay", action="store_true", help="leave the loader in its session")
|
parser.add_argument("--stay", action="store_true", help="leave the loader in its session")
|
||||||
|
parser.add_argument("-v", "--verbose", action="store_true",
|
||||||
|
help="print decisions and derived facts as operations run")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
global VERBOSE
|
||||||
|
VERBOSE = args.verbose
|
||||||
|
|
||||||
if args.update_loader and (args.flash or args.erase_flash):
|
if args.update_loader and (args.flash or args.erase_flash):
|
||||||
parser.error("--update-loader does not combine with application flash operations")
|
parser.error("--update-loader does not combine with application flash operations")
|
||||||
@@ -931,11 +1089,14 @@ def main():
|
|||||||
parser.error("--assume-fuses takes 8 hex digits: low,lock,extended,high")
|
parser.error("--assume-fuses takes 8 hex digits: low,lock,extended,high")
|
||||||
|
|
||||||
port = Port(args.port, args.baud)
|
port = Port(args.port, args.baud)
|
||||||
|
verbose(f"{args.port}: {args.baud} Bd 8N1, DTR/RTS asserted")
|
||||||
try:
|
try:
|
||||||
loader = Loader(port)
|
loader = Loader(port)
|
||||||
info = loader.connect(args.wait)
|
info = loader.connect(args.wait)
|
||||||
if args.info:
|
if args.info:
|
||||||
print(f"device: {info.describe()}")
|
print("device:")
|
||||||
|
for line in info.lines():
|
||||||
|
print(f" {line}")
|
||||||
fuse_bytes = fuse_override
|
fuse_bytes = fuse_override
|
||||||
if args.fuses or (args.update_loader and not info.patch_vector and fuse_bytes is None):
|
if args.fuses or (args.update_loader and not info.patch_vector and fuse_bytes is None):
|
||||||
read = op_fuses(loader)
|
read = op_fuses(loader)
|
||||||
|
|||||||
@@ -37,11 +37,12 @@ def main():
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
symbols = subprocess.run([nm, "-C", elf], capture_output=True, text=True, check=True).stdout
|
symbols = subprocess.run([nm, "-C", elf], capture_output=True, text=True, check=True).stdout
|
||||||
info = [line for line in symbols.splitlines() if "info_data" in line]
|
info = [line for line in symbols.splitlines() if "flash_table" in line and "::storage" in line]
|
||||||
if len(info) != 1:
|
if len(info) != 1:
|
||||||
print(f"FAIL: expected one info-block storage symbol, found {len(info)}")
|
print(f"FAIL: expected one info-block storage symbol, found {len(info)}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
offset = int(info[0].split()[0], 16) - text_start
|
address = int(info[0].split()[0], 16)
|
||||||
|
offset = address - text_start
|
||||||
if not 0 <= offset < 256:
|
if not 0 <= offset < 256:
|
||||||
print(f"FAIL: info block at image offset {offset:#x}, must sit in the first 256 bytes")
|
print(f"FAIL: info block at image offset {offset:#x}, must sit in the first 256 bytes")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|||||||
@@ -4,11 +4,16 @@
|
|||||||
// surgery, actually launched it. Linked normally (crt, vectors at 0); on
|
// surgery, actually launched it. Linked normally (crt, vectors at 0); on
|
||||||
// the tinies its reset vector is the rjmp the host re-homes.
|
// 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
|
// On the hardware-USART link it then listens, and an 'L' makes it jump into
|
||||||
// loader — the application-owned loader entry a BOOTRST-unprogrammed mega
|
// the resident loader — the application-owned loader entry a
|
||||||
// relies on (reset always boots the application there), exercised by the
|
// BOOTRST-unprogrammed mega relies on (reset always boots the application
|
||||||
// self-update tests. The tinies idle: reset reaches their loader through
|
// there), exercised by the self-update tests. The software link idles:
|
||||||
// the patched vector, so the application owes it nothing.
|
// 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 <libavr/libavr.hpp>
|
#include <libavr/libavr.hpp>
|
||||||
|
|
||||||
using namespace avr::literals;
|
using namespace avr::literals;
|
||||||
@@ -17,19 +22,44 @@ namespace {
|
|||||||
|
|
||||||
consteval avr::hertz_t clock()
|
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};
|
auto name = std::string_view{avr::hw::db.name};
|
||||||
if (name.starts_with("ATtiny13"))
|
if (name.starts_with("ATtiny13"))
|
||||||
return 9.6_MHz;
|
return 9.6_MHz;
|
||||||
if (name.starts_with("ATtiny"))
|
if (name.starts_with("ATtiny"))
|
||||||
return 8_MHz;
|
return 8_MHz;
|
||||||
return 16_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()}>;
|
using dev = avr::device<{.clock = clock()}>;
|
||||||
|
|
||||||
template <avr::hertz_t C, bool Hardware = avr::hw::db.has_instance("USART0") || avr::hw::db.has_instance("USART")>
|
template <avr::hertz_t C, bool Hardware = use_hardware()>
|
||||||
struct link {
|
struct link {
|
||||||
using tx_t = avr::uart::usart0<C, {.baud = 115200_Bd, .max_baud_error = 2.5_pct}>;
|
#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)
|
static void tx(char c)
|
||||||
{
|
{
|
||||||
tx_t::write(static_cast<std::uint8_t>(c));
|
tx_t::write(static_cast<std::uint8_t>(c));
|
||||||
@@ -39,15 +69,29 @@ struct link {
|
|||||||
// 'L' hands back to the loader at the top slot — 512 bytes, or the
|
// 'L' hands back to the loader at the top slot — 512 bytes, or the
|
||||||
// 1 KiB the >64 KiB chips use.
|
// 1 KiB the >64 KiB chips use.
|
||||||
constexpr std::uint32_t slot = avr::hw::db.mem.flash_size > 65536 ? 1024 : 512;
|
constexpr std::uint32_t slot = avr::hw::db.mem.flash_size > 65536 ? 1024 : 512;
|
||||||
for (;;)
|
for (;;) {
|
||||||
if (tx_t::read_blocking() == 'L')
|
auto command = tx_t::read_blocking();
|
||||||
|
if (command == 'L')
|
||||||
reinterpret_cast<void (*)()>(static_cast<std::uint16_t>((avr::hw::db.mem.flash_size - slot) / 2))();
|
reinterpret_cast<void (*)()>(static_cast<std::uint16_t>((avr::hw::db.mem.flash_size - slot) / 2))();
|
||||||
|
// 'D' leaves every word of the SPM page buffer dirty, so that a
|
||||||
|
// following 'L' enters the loader with the buffer it never clears.
|
||||||
|
if (command == 'D') {
|
||||||
|
for (std::uint16_t at = 0; at < avr::spm::page_bytes; at += 2)
|
||||||
|
avr::spm::fill(at, 0xdead);
|
||||||
|
tx('D');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
template <avr::hertz_t C>
|
template <avr::hertz_t C>
|
||||||
struct link<C, false> {
|
struct link<C, false> {
|
||||||
using tx_t = avr::uart::software_tx<C, avr::pb1, 57600_Bd>;
|
#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<C, avr::PUREBOOT_TX, baud>;
|
||||||
static void tx(char c)
|
static void tx(char c)
|
||||||
{
|
{
|
||||||
tx_t::write(static_cast<std::uint8_t>(c));
|
tx_t::write(static_cast<std::uint8_t>(c));
|
||||||
|
|||||||
90
test/pbdirty.py
Normal file
90
test/pbdirty.py
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Dirty-page-buffer acceptance test: the loader carries no buffer discard,
|
||||||
|
so a page filled over words an earlier writer left behind programs those
|
||||||
|
instead. This asserts the whole contract — the corruption is real and a bare
|
||||||
|
verify sees it, the repairing verify fixes it in one rewrite (the write that
|
||||||
|
took the stale words auto-erased the buffer), and it stays fixed.
|
||||||
|
|
||||||
|
The state is reached the way the loader cannot prevent: an application
|
||||||
|
dirties the buffer and jumps in with no reset between. Real boot-sectioned
|
||||||
|
megas forbid that outright — SPM executes only from the boot section
|
||||||
|
(Atmel-8271 §26.2) — but simavr dispatches SPM from anywhere, which is what
|
||||||
|
makes the path constructible at all.
|
||||||
|
|
||||||
|
Usage: pbdirty.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
|
||||||
|
<baud> <app_bin> <tool_py> <workdir>
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def fail(message):
|
||||||
|
print(f"FAIL: {message}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
device_bin, elf, mcu, hz, base_hex, page, baud, app_bin, tool, workdir = sys.argv[1:]
|
||||||
|
page, baud = int(page), int(baud)
|
||||||
|
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)
|
||||||
|
dump = os.path.join(workdir, "dump.bin")
|
||||||
|
|
||||||
|
# Reset boots the application on a BOOTRST-unprogrammed mega; its 'L' is
|
||||||
|
# the loader entry this test needs, reached without a reset.
|
||||||
|
device = pbsim.Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump, reset_hex="0")
|
||||||
|
try:
|
||||||
|
port = pb.Port(device.pty, baud)
|
||||||
|
loader = pb.Loader(port)
|
||||||
|
loader.connect(25)
|
||||||
|
|
||||||
|
# Install the application and hand over to it.
|
||||||
|
pb.op_flash(loader, app_bin, erase=False, verify=True)
|
||||||
|
loader.run_application()
|
||||||
|
if port.read_exact(3, 5.0) != b"APP":
|
||||||
|
fail("the application did not start")
|
||||||
|
|
||||||
|
port.write(b"D")
|
||||||
|
if port.read_exact(1, 5.0) != b"D":
|
||||||
|
fail("the application did not acknowledge dirtying the page buffer")
|
||||||
|
port.write(b"L")
|
||||||
|
loader = pb.Loader(port)
|
||||||
|
loader.connect(25)
|
||||||
|
|
||||||
|
# Program by hand, so the corruption is observable before anything
|
||||||
|
# repairs it.
|
||||||
|
pages = pb.plan_flash(open(app_bin, "rb").read(), loader.info)
|
||||||
|
for address in sorted(pages):
|
||||||
|
loader.write_page(address, pages[address])
|
||||||
|
try:
|
||||||
|
pb.verify_pages(loader, pages)
|
||||||
|
except pb.Error as error:
|
||||||
|
if "verify failed" not in str(error):
|
||||||
|
fail(f"the read-back failed, but not at verify: {error}")
|
||||||
|
else:
|
||||||
|
# Either the fixture no longer dirties the buffer, or the loader
|
||||||
|
# clears it again — in which case this test's premise is gone.
|
||||||
|
fail("programming over a dirty page buffer came back clean")
|
||||||
|
|
||||||
|
# What the programming path uses: one rewrite settles it, and it stays
|
||||||
|
# settled.
|
||||||
|
pb.verify_pages(loader, pages, repair=True)
|
||||||
|
pb.verify_pages(loader, pages)
|
||||||
|
|
||||||
|
# Ground truth beyond the loader's own read-back.
|
||||||
|
loader.run_application()
|
||||||
|
if port.read_exact(3, 5.0) != b"APP":
|
||||||
|
fail("the application did not start after the recovered write")
|
||||||
|
port.close()
|
||||||
|
finally:
|
||||||
|
device.stop()
|
||||||
|
print("pbdirty: a dirty page buffer is caught by verify and cleared by the retry")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -66,8 +66,10 @@ def main():
|
|||||||
if loader.read_eeprom(0, len(pattern)) != pattern:
|
if loader.read_eeprom(0, len(pattern)) != pattern:
|
||||||
fail("EEPROM round-trip through the staged copy")
|
fail("EEPROM round-trip through the staged copy")
|
||||||
|
|
||||||
# The guard, both ways: its own slot refused (drained, unchanged),
|
# The guard, both ways: its own slot refused (drained, unchanged), the
|
||||||
# the resident slot writable.
|
# resident slot writable. The refusal leaves its drained words in the
|
||||||
|
# SPM buffer, so the write that follows may take them — and clears
|
||||||
|
# them by writing, so the retry must not.
|
||||||
before = loader.read_flash(stage, page)
|
before = loader.read_flash(stage, page)
|
||||||
loader.write_page(stage, bytes(page))
|
loader.write_page(stage, bytes(page))
|
||||||
if loader.read_flash(stage, page) != before:
|
if loader.read_flash(stage, page) != before:
|
||||||
@@ -75,7 +77,9 @@ def main():
|
|||||||
marker = bytes((i * 3) & 0xFF for i in range(page))
|
marker = bytes((i * 3) & 0xFF for i in range(page))
|
||||||
loader.write_page(base, marker)
|
loader.write_page(base, marker)
|
||||||
if loader.read_flash(base, page) != marker:
|
if loader.read_flash(base, page) != marker:
|
||||||
fail("the staged copy could not write the resident slot")
|
loader.write_page(base, marker)
|
||||||
|
if loader.read_flash(base, page) != marker:
|
||||||
|
fail("the staged copy could not write the resident slot, even on retry")
|
||||||
|
|
||||||
# Restore the resident image through the staged copy, then 'J' back
|
# Restore the resident image through the staged copy, then 'J' back
|
||||||
# into it and prove it lives.
|
# into it and prove it lives.
|
||||||
|
|||||||
@@ -8,8 +8,11 @@ import subprocess
|
|||||||
|
|
||||||
|
|
||||||
class Device:
|
class Device:
|
||||||
def __init__(self, binary, elf, mcu, hz, base_hex, page, baud, dump, reset_hex=None, resume=None):
|
def __init__(self, binary, elf, mcu, hz, base_hex, page, baud, dump, reset_hex=None, resume=None, link=None):
|
||||||
cmd = [binary, elf, mcu, hz, base_hex, str(page), str(baud), dump]
|
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:
|
if reset_hex is not None or resume is not None:
|
||||||
# Chips without a hardware boot section — the tinies and the
|
# Chips without a hardware boot section — the tinies and the
|
||||||
# m48s — reset to address 0 like silicon; the boot-sectioned
|
# m48s — reset to address 0 like silicon; the boot-sectioned
|
||||||
|
|||||||
@@ -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.
|
the tool's view against the simulator's ground-truth memory dumps.
|
||||||
|
|
||||||
Usage: pbtest.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
|
Usage: pbtest.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
|
||||||
<baud> <eeprom_size> <app_bin> <tool_py> <workdir>
|
<baud> <eeprom_size> <app_bin> <tool_py> <workdir> [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.
|
Exits 0 if every scenario passes.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import signal
|
|
||||||
import subprocess
|
|
||||||
import sys
|
import sys
|
||||||
import time
|
|
||||||
|
|
||||||
|
|
||||||
def fail(message):
|
def fail(message):
|
||||||
@@ -33,53 +33,14 @@ def rjmp_decode(word, at, flash_words):
|
|||||||
return (at + 1 + offset) % 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():
|
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)
|
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(tool)))
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
import pbsim
|
||||||
import pureboot as pb
|
import pureboot as pb
|
||||||
|
|
||||||
os.makedirs(workdir, exist_ok=True)
|
os.makedirs(workdir, exist_ok=True)
|
||||||
@@ -105,18 +66,18 @@ def main():
|
|||||||
+ bytes([flags])
|
+ 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:
|
try:
|
||||||
# Session 1: knock from reset, identify, program everything, stay.
|
# Session 1: knock from reset, identify, program everything, stay.
|
||||||
out = run_tool(tool, device.pty, baud, "--info", "--fuses", "--flash", app_bin,
|
out = pbsim.run_tool(tool, device.pty, baud, "--info", "--fuses", "--flash", app_bin,
|
||||||
"--eeprom", ee_path, "--stay")
|
"--eeprom", ee_path, "--stay")
|
||||||
for needed in ("device: signature", "fuses:", "verify:", "stays"):
|
for needed in ("signature", "fuses", "verify:", "stays"):
|
||||||
if needed not in out:
|
if needed not in out:
|
||||||
fail(f"session 1 output lacks {needed!r}")
|
fail(f"session 1 output lacks {needed!r}")
|
||||||
|
|
||||||
# Session 2: reconnect into the live session, verify, dump, hand over
|
# Session 2: reconnect into the live session, verify, dump, hand over
|
||||||
# is deferred — the pty must be reopened for the APP banner first.
|
# 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,
|
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")
|
"--read-flash", read_flash, "--read-eeprom", read_eeprom, "--stay")
|
||||||
if out.count("verify:") != 2:
|
if out.count("verify:") != 2:
|
||||||
fail("session 2 did not verify both memories")
|
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).
|
# runner resets them to address 0 like silicon) or BOOTRST (mega).
|
||||||
# The loader must answer a fresh knock, and the 'J' hand-over must
|
# The loader must answer a fresh knock, and the 'J' hand-over must
|
||||||
# land in the application, which banners on the same link.
|
# land in the application, which banners on the same link.
|
||||||
device.proc.send_signal(signal.SIGUSR1)
|
device.reset()
|
||||||
port = pb.Port(device.pty, baud)
|
port = pb.Port(device.pty, baud)
|
||||||
try:
|
try:
|
||||||
loader = pb.Loader(port)
|
loader = pb.Loader(port)
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
// simavr "device" for the pureboot protocol tests, all three chips. Loads
|
// simavr "device" for the pureboot protocol tests, every chip. Loads the
|
||||||
// the boot-linked ELF at the loader base, starts execution there (BOOTRST /
|
// boot-linked ELF at the loader base, starts execution there (BOOTRST / the
|
||||||
// the patched vector are not what is under test), and exposes the loader's
|
// patched vector are not what is under test), and exposes the loader's
|
||||||
// serial link as a pty for the real host tool:
|
// serial link as a pty for the real host tool:
|
||||||
//
|
//
|
||||||
// - Megas: the hardware USART through simavr's uart_pty.
|
// - Hardware USART builds: simavr's uart_pty on the selected instance.
|
||||||
// - Tinies: an 8N1 bridge between a pty and the GPIO software UART
|
// - Software UART builds: an 8N1 bridge between a pty and the GPIO pins,
|
||||||
// (drives PB0, the loader's RX; decodes PB1, its TX), timed against the
|
// timed against the simulated cycle counter (drives the loader's RX,
|
||||||
// simulated cycle counter.
|
// 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
|
// 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
|
// is a silent no-op (the mega's boot section has one, avr_flash). The
|
||||||
@@ -38,11 +42,31 @@
|
|||||||
|
|
||||||
static avr_t *avr;
|
static avr_t *avr;
|
||||||
static uart_pty_t uart_pty;
|
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 const char *dump_path;
|
||||||
static uint32_t reset_pc;
|
static uint32_t reset_pc;
|
||||||
static volatile sig_atomic_t reset_requested;
|
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
|
// 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) —
|
// 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
|
// 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);
|
uart_pty_stop(&uart_pty);
|
||||||
_exit(0);
|
_exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
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,
|
fprintf(stderr,
|
||||||
"usage: %s <pureboot.elf> <mcu> <hz> <base_hex> <page> <baud> <flash_dump>"
|
"usage: %s [-l link] <pureboot.elf> <mcu> <hz> <base_hex> <page> <baud> <flash_dump>"
|
||||||
" [reset_hex] [resume_flash]\n"
|
" [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"
|
" resume_flash: raw full-flash image loaded instead of the ELF — a prior\n"
|
||||||
" run's dump, for power-fail resume tests\n",
|
" run's dump, for power-fail resume tests\n",
|
||||||
argv[0]);
|
argv[0]);
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
|
argv += optind - 1; // argv[1] is the ELF again, whatever was parsed
|
||||||
const char *mcu_name = argv[2];
|
const char *mcu_name = argv[2];
|
||||||
uint32_t base = (uint32_t)strtoul(argv[4], NULL, 0);
|
uint32_t base = (uint32_t)strtoul(argv[4], NULL, 0);
|
||||||
unsigned page = (unsigned)atoi(argv[5]);
|
unsigned page = (unsigned)atoi(argv[5]);
|
||||||
unsigned baud = (unsigned)atoi(argv[6]);
|
unsigned baud = (unsigned)atoi(argv[6]);
|
||||||
dump_path = argv[7];
|
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);
|
avr = avr_make_mcu_by_name(mcu_name);
|
||||||
if (!avr) {
|
if (!avr) {
|
||||||
@@ -306,7 +343,7 @@ int main(int argc, char *argv[])
|
|||||||
avr->frequency = (uint32_t)strtoul(argv[3], NULL, 0);
|
avr->frequency = (uint32_t)strtoul(argv[3], NULL, 0);
|
||||||
memset(avr->flash, 0xff, avr->flashend + 1); // real flash powers up erased
|
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.
|
// Resume: the full flash image of an interrupted prior run.
|
||||||
FILE *f = fopen(argv[9], "rb");
|
FILE *f = fopen(argv[9], "rb");
|
||||||
if (!f || fread(avr->flash, 1, avr->flashend + 1, f) == 0) {
|
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
|
// 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
|
// flash walks up into the loader, and after the host's surgery the
|
||||||
// patched vector routes there.
|
// patched vector routes there.
|
||||||
int boot_section = use_uart_pty && strncmp(mcu_name, "atmega48", 8) != 0;
|
int boot_section = is_mega && strncmp(mcu_name, "atmega48", 8) != 0;
|
||||||
reset_pc = argc > 8 ? (uint32_t)strtoul(argv[8], NULL, 0) : (boot_section ? base : 0);
|
reset_pc = args > 7 ? (uint32_t)strtoul(argv[8], NULL, 0) : (boot_section ? base : 0);
|
||||||
avr->pc = reset_pc;
|
avr->pc = reset_pc;
|
||||||
avr->codeend = avr->flashend;
|
avr->codeend = avr->flashend;
|
||||||
|
|
||||||
@@ -341,27 +378,34 @@ int main(int argc, char *argv[])
|
|||||||
avr_ioctl(avr, AVR_IOCTL_EEPROM_SET, &seed);
|
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();
|
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 {
|
} else {
|
||||||
nvm.page = page;
|
nvm.page = page;
|
||||||
memset(nvm.buffer, 0xff, sizeof(nvm.buffer));
|
memset(nvm.buffer, 0xff, sizeof(nvm.buffer));
|
||||||
nvm.io.kind = "tiny_nvm";
|
nvm.io.kind = "tiny_nvm";
|
||||||
nvm.io.ioctl = nvm_ioctl;
|
nvm.io.ioctl = nvm_ioctl;
|
||||||
avr_register_io(avr, &nvm.io);
|
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
|
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);
|
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('B'), 1), tx_hook, NULL);
|
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
|
avr_raise_irq(rx_pin, 1); // idle line
|
||||||
|
|
||||||
int slave;
|
int slave;
|
||||||
@@ -389,18 +433,27 @@ int main(int argc, char *argv[])
|
|||||||
reset_requested = 0;
|
reset_requested = 0;
|
||||||
avr_reset(avr);
|
avr_reset(avr);
|
||||||
avr->pc = reset_pc;
|
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;
|
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;
|
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 {
|
} else {
|
||||||
bridge_reset();
|
bridge_reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!use_uart_pty && ++since_poll >= 2000) {
|
if (link_software && ++since_poll >= 2000) {
|
||||||
since_poll = 0;
|
since_poll = 0;
|
||||||
poll_pty();
|
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);
|
finish(0);
|
||||||
|
|||||||
@@ -204,6 +204,52 @@ def main():
|
|||||||
pb.check_walk_region({0x7800: bytes((0xFF,)) * 128}, mega, fuses(0xFA), False)
|
pb.check_walk_region({0x7800: bytes((0xFF,)) * 128}, mega, fuses(0xFA), False)
|
||||||
pb.check_walk_region(deep, mega, None, False) # fuses unknown: no check
|
pb.check_walk_region(deep, mega, None, False) # fuses unknown: no check
|
||||||
|
|
||||||
|
# The repairing verify: a mismatched page is rewritten rather than raised,
|
||||||
|
# bounded so a fault that is not self-clearing cannot spin.
|
||||||
|
class FakeLoader:
|
||||||
|
"""A device whose first `bad` writes of any page land wrong."""
|
||||||
|
|
||||||
|
def __init__(self, info, bad):
|
||||||
|
self.info = info
|
||||||
|
self.bad = bad
|
||||||
|
self.flash = {}
|
||||||
|
self.writes = 0
|
||||||
|
|
||||||
|
def write_page(self, address, data):
|
||||||
|
self.writes += 1
|
||||||
|
self.flash[address] = bytes(len(data)) if self.bad > 0 else bytes(data)
|
||||||
|
self.bad -= 1
|
||||||
|
|
||||||
|
def read_flash(self, address, count):
|
||||||
|
return self.flash.get(address, bytes(count))
|
||||||
|
|
||||||
|
want = {0: bytes((i * 5) & 0xFF for i in range(128))}
|
||||||
|
|
||||||
|
# One bad write, then good: repaired in place, and the caller never sees
|
||||||
|
# an error. The rewrite is counted, so a silent no-op cannot pass.
|
||||||
|
device = FakeLoader(info_of(pb, 0x7E00, 128, False, 0x8000), bad=1)
|
||||||
|
device.write_page(0, want[0])
|
||||||
|
pb.verify_pages(device, want, repair=True)
|
||||||
|
if device.writes != 2:
|
||||||
|
fail(f"repairing verify made {device.writes} writes, expected 2")
|
||||||
|
|
||||||
|
# Without repair the same state raises, so the repair is what fixed it.
|
||||||
|
device = FakeLoader(info_of(pb, 0x7E00, 128, False, 0x8000), bad=1)
|
||||||
|
device.write_page(0, want[0])
|
||||||
|
expect_error("verify without repair", lambda: pb.verify_pages(device, want), "verify failed")
|
||||||
|
|
||||||
|
# A page that never comes good stops after RETRIES rewrites, and says so.
|
||||||
|
device = FakeLoader(info_of(pb, 0x7E00, 128, False, 0x8000), bad=99)
|
||||||
|
device.write_page(0, want[0])
|
||||||
|
expect_error(
|
||||||
|
"unrepairable page",
|
||||||
|
lambda: pb.verify_pages(device, want, repair=True),
|
||||||
|
"verify failed",
|
||||||
|
f"after {pb.RETRIES} retries",
|
||||||
|
)
|
||||||
|
if device.writes != pb.RETRIES + 1:
|
||||||
|
fail(f"unrepairable page took {device.writes} writes, expected {pb.RETRIES + 1}")
|
||||||
|
|
||||||
print("test_planner: all planner and policy checks pass")
|
print("test_planner: all planner and policy checks pass")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
37
tools/check.sh
Executable file
37
tools/check.sh
Executable file
@@ -0,0 +1,37 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# The port's gate: every chip's generated workflow — build, size matrix, and
|
||||||
|
# the simulator-driven protocol suites. --full adds the reflect-spot builds
|
||||||
|
# (libavr's rule: reflect compiles are bounded to its spot set, never the
|
||||||
|
# full matrix). LIBAVR_ROOT must point at the libavr checkout.
|
||||||
|
set -e
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
full=0
|
||||||
|
[[ "$1" == "--full" ]] && { full=1; shift; }
|
||||||
|
|
||||||
|
CHIPS=(attiny13 attiny13a attiny25 attiny45 attiny85
|
||||||
|
atmega8 atmega8a atmega16 atmega16a atmega32 atmega32a
|
||||||
|
atmega48 atmega48a atmega48p atmega48pa
|
||||||
|
atmega88 atmega88a atmega88p atmega88pa
|
||||||
|
atmega168 atmega168a atmega168p atmega168pa
|
||||||
|
atmega328 atmega328p
|
||||||
|
atmega164a atmega164p atmega164pa
|
||||||
|
atmega324a atmega324p atmega324pa
|
||||||
|
atmega644 atmega644a atmega644p atmega644pa
|
||||||
|
atmega1284 atmega1284p)
|
||||||
|
REFLECT_SPOT=(attiny13a attiny85 atmega8 atmega16a atmega32a atmega48pa
|
||||||
|
atmega88 atmega168pa atmega328p atmega164a atmega644p atmega1284)
|
||||||
|
|
||||||
|
for chip in "${CHIPS[@]}"; do
|
||||||
|
echo "==== $chip ===="
|
||||||
|
cmake --workflow --preset "$chip-generated" "$@"
|
||||||
|
done
|
||||||
|
|
||||||
|
if ((full)); then
|
||||||
|
for chip in "${REFLECT_SPOT[@]}"; do
|
||||||
|
echo "==== $chip reflect ===="
|
||||||
|
cmake --workflow --preset "$chip-reflect" "$@"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "check: every chip green"
|
||||||
90
tools/make_presets.py
Executable file
90
tools/make_presets.py
Executable file
@@ -0,0 +1,90 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Regenerate CMakePresets.json — one uniform pipeline per chip.
|
||||||
|
|
||||||
|
Every chip gets generated-mode configure/build/test presets and a workflow
|
||||||
|
running all three. Reflect-mode presets (configure + build, no tests — the
|
||||||
|
port's TUs compile identically; the sims prove nothing new there) exist for
|
||||||
|
libavr's reflect spot set only, mirroring its rule: the full reflect matrix
|
||||||
|
is never built, one chip per hardware class and pack vintage is.
|
||||||
|
|
||||||
|
Run from the repo root: tools/make_presets.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
CHIPS = [
|
||||||
|
"attiny13", "attiny13a", "attiny25", "attiny45", "attiny85",
|
||||||
|
"atmega8", "atmega8a", "atmega16", "atmega16a", "atmega32", "atmega32a",
|
||||||
|
"atmega48", "atmega48a", "atmega48p", "atmega48pa",
|
||||||
|
"atmega88", "atmega88a", "atmega88p", "atmega88pa",
|
||||||
|
"atmega168", "atmega168a", "atmega168p", "atmega168pa",
|
||||||
|
"atmega328", "atmega328p",
|
||||||
|
"atmega164a", "atmega164p", "atmega164pa",
|
||||||
|
"atmega324a", "atmega324p", "atmega324pa",
|
||||||
|
"atmega644", "atmega644a", "atmega644p", "atmega644pa",
|
||||||
|
"atmega1284", "atmega1284p",
|
||||||
|
]
|
||||||
|
|
||||||
|
# libavr's REFLECT_SPOT (tools/check.sh): one chip per hardware class and
|
||||||
|
# pack vintage.
|
||||||
|
REFLECT_SPOT = [
|
||||||
|
"attiny13a", "attiny85", "atmega8", "atmega16a", "atmega32a",
|
||||||
|
"atmega48pa", "atmega88", "atmega168pa", "atmega328p", "atmega164a",
|
||||||
|
"atmega644p", "atmega1284",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
configure = [{
|
||||||
|
"name": "base",
|
||||||
|
"hidden": True,
|
||||||
|
"generator": "Ninja",
|
||||||
|
"binaryDir": "${sourceDir}/build/${presetName}",
|
||||||
|
"toolchainFile": "$env{LIBAVR_ROOT}/cmake/avr-toolchain.cmake",
|
||||||
|
"cacheVariables": {
|
||||||
|
"CMAKE_BUILD_TYPE": "Release",
|
||||||
|
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
|
||||||
|
"CMAKE_COLOR_DIAGNOSTICS": "ON",
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
build, test, workflows = [], [], []
|
||||||
|
|
||||||
|
def add(chip, mode):
|
||||||
|
name = f"{chip}-{mode}"
|
||||||
|
configure.append({
|
||||||
|
"name": name,
|
||||||
|
"inherits": "base",
|
||||||
|
"cacheVariables": {
|
||||||
|
"LIBAVR_MCU": chip,
|
||||||
|
"LIBAVR_REFLECT": "ON" if mode == "reflect" else "OFF",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
build.append({"name": name, "configurePreset": name})
|
||||||
|
steps = [{"type": "configure", "name": name}, {"type": "build", "name": name}]
|
||||||
|
if mode == "generated":
|
||||||
|
test.append({"name": name, "configurePreset": name, "output": {"outputOnFailure": True}})
|
||||||
|
steps.append({"type": "test", "name": name})
|
||||||
|
workflows.append({"name": name, "steps": steps})
|
||||||
|
|
||||||
|
for chip in CHIPS:
|
||||||
|
add(chip, "generated")
|
||||||
|
for chip in REFLECT_SPOT:
|
||||||
|
add(chip, "reflect")
|
||||||
|
|
||||||
|
presets = {
|
||||||
|
"version": 8,
|
||||||
|
"configurePresets": configure,
|
||||||
|
"buildPresets": build,
|
||||||
|
"testPresets": test,
|
||||||
|
"workflowPresets": workflows,
|
||||||
|
}
|
||||||
|
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "CMakePresets.json")
|
||||||
|
with open(path, "w") as f:
|
||||||
|
json.dump(presets, f, indent=1)
|
||||||
|
f.write("\n")
|
||||||
|
print(f"{len(CHIPS)} chips, {len(REFLECT_SPOT)} reflect: {os.path.normpath(path)}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user