Compare commits
50 Commits
9fc28aa89a
...
autobaud
| Author | SHA1 | Date | |
|---|---|---|---|
| f98ed406b8 | |||
| a4da885e36 | |||
| 335e494a31 | |||
| 799709efcf | |||
| 7ae80087b3 | |||
| b477f53ca5 | |||
| 84d3f679c2 | |||
| b5020a1e20 | |||
| 1dbf0089d6 | |||
| 39dfe40dbf | |||
| 8470ce3da0 | |||
| d634741015 | |||
| ab842c8d99 | |||
| 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
|
||||||
399
CMakeLists.txt
399
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,34 @@ 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_OBJCOPY} ${CMAKE_CXX_COMPILER} ${LIBAVR_MCU}
|
||||||
|
$<TARGET_FILE:pureboot>
|
||||||
|
${CMAKE_BINARY_DIR}/CMakeFiles/pureboot.dir/pureboot/pureboot.cpp.obj
|
||||||
|
${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 +172,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 +182,235 @@ 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 USART instance
|
||||||
|
# (different registers), the clock (different constants), and the baud
|
||||||
|
# through the shapes its bit timing takes — 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()
|
||||||
|
|
||||||
|
# The autobaud loader: one clock-agnostic image per chip, so it has no
|
||||||
|
# clock x baud axis of its own — the matrix below sweeps those for the
|
||||||
|
# fixed-baud builds, and this one binary has to serve all of them at run
|
||||||
|
# time. Size-tested against the same per-chip budget as every other variant.
|
||||||
|
pureboot_add_loader(pureboot_autobaud SERIAL autobaud)
|
||||||
|
add_test(NAME pureboot_autobaud.size
|
||||||
|
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:pureboot_autobaud>
|
||||||
|
-DLIMIT=${PUREBOOT_LIMIT} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
|
||||||
|
|
||||||
|
# One point of the exhaustive matrix, named from its resolved parameters
|
||||||
|
# so the enumeration cannot collide with itself. Unreachable rates drop
|
||||||
|
# out here rather than aborting the configure.
|
||||||
|
function(pureboot_matrix_point hz baud link)
|
||||||
|
if(link STREQUAL "software")
|
||||||
|
pureboot_baud_feasible(${hz} ${baud} 1 _ok)
|
||||||
|
set(_args SERIAL software)
|
||||||
|
else()
|
||||||
|
pureboot_baud_feasible(${hz} ${baud} 0 _ok)
|
||||||
|
set(_args USART ${link})
|
||||||
|
endif()
|
||||||
|
if(_ok)
|
||||||
|
pureboot_size_variant(pbm_${hz}_${baud}_${link} CLOCK ${hz} BAUD ${baud} ${_args})
|
||||||
|
endif()
|
||||||
|
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)
|
||||||
|
set(_full_clocks 128000 600000 1200000 4800000 9600000)
|
||||||
|
else()
|
||||||
|
set(_matrix_clocks 1000000 8000000 16000000)
|
||||||
|
set(_full_clocks 128000 1000000 1843200 2000000 3686400 4000000 7372800 8000000
|
||||||
|
11059200 12000000 14745600 16000000 18432000 20000000)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# The exhaustive cross product: every clock a deployment plausibly runs
|
||||||
|
# — the internal oscillators, the shipped CKDIV8 floor, the plain
|
||||||
|
# crystals and the UART crystals — against every rate, against every
|
||||||
|
# backend. Beyond the ladder the list carries the slow rates a
|
||||||
|
# sub-megahertz oscillator is left with, which no ladder rate reaches
|
||||||
|
# (16000 Bd is the only rate the 128 kHz oscillator holds exactly); at
|
||||||
|
# the fast clocks those same rates also select the software UART's
|
||||||
|
# 16-bit _delay_loop_2 bit spin (two words more setup at each of its five
|
||||||
|
# sites), the largest image the space produces and a shape the ladder
|
||||||
|
# default — always the *fastest* rate a clock reaches — never picks.
|
||||||
|
#
|
||||||
|
# Every chip runs the full cross product: the size-bearing classes (flash
|
||||||
|
# addressing, hand-over shape, page size, USART inventory) are what make
|
||||||
|
# the image differ, and a chip outside them is expected to match its class
|
||||||
|
# — but "expected" is what a matrix is for, and the whole sweep is cheap
|
||||||
|
# enough to run rather than reason about. PUREBOOT_FULL_MATRIX is what
|
||||||
|
# selects it; the compact matrix below is the per-commit default.
|
||||||
|
get_property(_full_bauds GLOBAL PROPERTY PUREBOOT_BAUD_LADDER)
|
||||||
|
list(APPEND _full_bauds 16000 4800 2400 1200)
|
||||||
|
if(DEFINED ENV{PUREBOOT_FULL_MATRIX})
|
||||||
|
foreach(_matrix_hz IN LISTS _full_clocks)
|
||||||
|
foreach(_matrix_baud IN LISTS _full_bauds)
|
||||||
|
pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} software)
|
||||||
|
if(PUREBOOT_HAS_USART)
|
||||||
|
pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} 0)
|
||||||
|
endif()
|
||||||
|
if(PUREBOOT_HAS_USART1)
|
||||||
|
pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} 1)
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
endforeach()
|
||||||
|
else()
|
||||||
|
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()
|
||||||
|
if(PUREBOOT_HAS_USART1 AND NOT _matrix_hz EQUAL _pb_stock_hz)
|
||||||
|
pureboot_size_variant(pureboot_usart1_${_matrix_khz}k CLOCK ${_matrix_hz} USART 1)
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
list(GET _matrix_clocks -1 _matrix_top_hz)
|
||||||
|
pureboot_size_variant(pureboot_sw_wide CLOCK ${_matrix_top_hz} BAUD 9600 SERIAL software)
|
||||||
|
endif()
|
||||||
|
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()
|
||||||
|
|
||||||
|
# The autobaud variants driven end to end over the software-UART bridge (both
|
||||||
|
# under review — pureboot/autobaud.md): the host sends the 0xC0 calibration
|
||||||
|
# pulse, the loader times it, locks, and programs. Run on the near-flash 328P
|
||||||
|
# and the word-addressed 1284P — the two flash-addressing classes — and each
|
||||||
|
# at two clocks with the one binary, which is the clock-agnostic property
|
||||||
|
# autobaud exists for (test/pbautobaud.py). The fixture application banners
|
||||||
|
# over the same software link at the first clock's rate.
|
||||||
|
if(LIBAVR_MCU MATCHES "^atmega(328p|1284p)$" AND DEFINED PB_DEVICE)
|
||||||
|
add_executable(pbapp_autobaud test/pbapp.cpp)
|
||||||
|
target_link_libraries(pbapp_autobaud PRIVATE libavr)
|
||||||
|
target_compile_definitions(pbapp_autobaud PRIVATE PUREBOOT_CLOCK_HZ=1000000
|
||||||
|
PUREBOOT_BAUD=9600 PUREBOOT_SOFT_SERIAL PUREBOOT_TX=pb1)
|
||||||
|
add_custom_command(TARGET pbapp_autobaud POST_BUILD
|
||||||
|
COMMAND ${CMAKE_OBJCOPY} -O binary
|
||||||
|
$<TARGET_FILE:pbapp_autobaud> $<TARGET_FILE:pbapp_autobaud>.bin)
|
||||||
|
add_test(NAME pureboot.autobaud
|
||||||
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbautobaud.py
|
||||||
|
${PB_DEVICE} $<TARGET_FILE:pureboot_autobaud> ${PUREBOOT_SIM_MCU}
|
||||||
|
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} $<TARGET_FILE:pbapp_autobaud>.bin
|
||||||
|
1000000 9600 ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
|
${CMAKE_BINARY_DIR}/pbautobaud-work)
|
||||||
|
set_tests_properties(pureboot.autobaud PROPERTIES TIMEOUT 240)
|
||||||
|
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 6cfc7a8eee
329
pureboot/CMakeLists.txt
Normal file
329
pureboot/CMakeLists.txt
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
# 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.
|
||||||
|
# A downstream project brings its usual libavr setup (the `libavr` target and
|
||||||
|
# the LIBAVR_MCU toolchain preset), adds this directory, and states its
|
||||||
|
# deployment; every argument is optional (README.md):
|
||||||
|
#
|
||||||
|
# add_subdirectory(bootloader/pureboot)
|
||||||
|
# pureboot_add_loader(myboot CLOCK 1000000 SERIAL software TX pb1 RX pb5)
|
||||||
|
|
||||||
|
# Per-family geometry, deployment defaults, and the linker wrap the PC modulo
|
||||||
|
# needs. The slot is 512 bytes on every chip. 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, so plain LPM still reaches
|
||||||
|
# everything and the wire stays byte-addressed. 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 addresses are words, reads go through ELPM, and the PC's
|
||||||
|
# modulo wrap exceeds what --pmem-wrap-around models.
|
||||||
|
set(_pb_flash 131072)
|
||||||
|
set(_pb_wrap "")
|
||||||
|
set(_pb_page 256)
|
||||||
|
set(_pb_hz 16000000)
|
||||||
|
set(_pb_eeprom 4096)
|
||||||
|
set(_pb_has_usart1 1)
|
||||||
|
else()
|
||||||
|
message(FATAL_ERROR "pureboot: no geometry for ${LIBAVR_MCU}")
|
||||||
|
endif()
|
||||||
|
set(_pb_slot 512)
|
||||||
|
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)
|
||||||
|
set(_pb_limit ${_pb_slot})
|
||||||
|
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 rates a default may pick, fastest first.
|
||||||
|
set_property(GLOBAL PROPERTY PUREBOOT_BAUD_LADDER 115200 57600 38400 19200 9600)
|
||||||
|
|
||||||
|
# Whether <baud> is reachable from <clock> within 2.5 %, by the same
|
||||||
|
# best-of-U2X-and-plain divisor search libavr's solve_baud runs, so a build
|
||||||
|
# never trips the compile-time error it is checked against. A software build
|
||||||
|
# also needs the polled receiver's 100-cycles-a-bit floor: at low clocks the
|
||||||
|
# U2X divisor reaches rates the bit-banged sampler cannot.
|
||||||
|
function(pureboot_baud_feasible clock baud software outvar)
|
||||||
|
set(${outvar} 0 PARENT_SCOPE)
|
||||||
|
math(EXPR _cycles "${clock} / ${baud}")
|
||||||
|
if(software AND _cycles LESS 100)
|
||||||
|
return()
|
||||||
|
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} 1 PARENT_SCOPE)
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
# The fastest ladder rate the clock reaches.
|
||||||
|
function(pureboot_default_baud clock software outvar)
|
||||||
|
get_property(_ladder GLOBAL PROPERTY PUREBOOT_BAUD_LADDER)
|
||||||
|
foreach(baud ${_ladder})
|
||||||
|
pureboot_baud_feasible(${clock} ${baud} ${software} _ok)
|
||||||
|
if(_ok)
|
||||||
|
set(${outvar} ${baud} PARENT_SCOPE)
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
message(FATAL_ERROR "pureboot: no standard baud rate fits a ${clock} Hz clock within 2.5 % "
|
||||||
|
"— pass BAUD <rate> to deploy a non-standard one")
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
# pureboot_add_loader(<name> [CLOCK <hz>] [BAUD <bd>]
|
||||||
|
# [SERIAL auto|hardware|software|autobaud] [USART <n>]
|
||||||
|
# [RX <pin>] [TX <pin>] [TIMEOUT <s>])
|
||||||
|
#
|
||||||
|
# The loader target plus its flashable images (<name>.hex for a programmer,
|
||||||
|
# <name>.bin for --update-loader). The resolved deployment is stamped on the
|
||||||
|
# target as PUREBOOT_HZ / PUREBOOT_BAUD / PUREBOOT_LINK (the link spelled
|
||||||
|
# usart0, usart1 or sw:<RX>,<TX>) — what a test harness speaks to it with.
|
||||||
|
#
|
||||||
|
# SERIAL autobaud measures the host's bit timing at run time, so the image
|
||||||
|
# carries no clock and no baud: CLOCK and BAUD are not build parameters there,
|
||||||
|
# and one binary per chip serves every F_CPU and every rate. The stamped
|
||||||
|
# PUREBOOT_HZ/PUREBOOT_BAUD then record what a harness should *drive* it at,
|
||||||
|
# not what it was built for.
|
||||||
|
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 NOT PB_SERIAL MATCHES "^(auto|hardware)$")
|
||||||
|
message(FATAL_ERROR "pureboot_add_loader(${name}): USART ${PB_USART} contradicts SERIAL ${PB_SERIAL}")
|
||||||
|
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 MATCHES "^(software|autobaud)$")
|
||||||
|
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()
|
||||||
|
if(PB_SERIAL STREQUAL "autobaud")
|
||||||
|
set(_serial_defines PUREBOOT_AUTOBAUD PUREBOOT_RX=${PB_RX} PUREBOOT_TX=${PB_TX})
|
||||||
|
else()
|
||||||
|
set(_serial_defines PUREBOOT_SOFT_SERIAL PUREBOOT_RX=${PB_RX} PUREBOOT_TX=${PB_TX})
|
||||||
|
endif()
|
||||||
|
# sw:<RX>,<TX> as port letter and bit, 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()
|
||||||
|
|
||||||
|
if(PB_SERIAL STREQUAL "autobaud")
|
||||||
|
# No clock and no baud reach the image; the window is a poll budget.
|
||||||
|
set(_defines ${_serial_defines})
|
||||||
|
else()
|
||||||
|
set(_defines PUREBOOT_CLOCK_HZ=${PB_CLOCK} PUREBOOT_BAUD=${PB_BAUD} PUREBOOT_TIMEOUT=${PB_TIMEOUT}
|
||||||
|
${_serial_defines})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
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. 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 keeps
|
||||||
|
# expression temporaries in registers (-fno-tree-ter) — but every loop body
|
||||||
|
# here contains a call, so a register held across it costs more than the
|
||||||
|
# load-immediate it saves. The set is fitted to the loader's body and has to
|
||||||
|
# be re-measured when that body changes: -fno-move-loop-invariants belonged
|
||||||
|
# here while the command loop carried four transfer bodies and costs bytes
|
||||||
|
# now that it carries one.
|
||||||
|
target_compile_options(${name} PRIVATE
|
||||||
|
-fno-ivopts -fira-algorithm=priority -fno-tree-ter -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, never flashed: .hex for a programmer, .bin (the
|
||||||
|
# slot's bare bytes) for --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,150 +2,289 @@
|
|||||||
|
|
||||||
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), **512 bytes on every chip libavr
|
||||||
fitting each chip's smallest boot sector**: 512 bytes everywhere — 488 B on
|
targets — all 37**. The device speaks primitives; every composite — verify,
|
||||||
the tiny13s, 498–502 B on the tiny25/45/85, 466–504 B across the megas
|
erase, reset-vector surgery, updating the loader itself — lives in the host
|
||||||
(474 B on the boot-section-less m48s, 498 B on the 644s) — except the
|
tool (`pureboot.py`).
|
||||||
ATmega1284/1284P, whose smallest boot sector is 1 KiB and whose far-flash
|
|
||||||
machinery (ELPM reads, RAMPZ page commands, word-addressed wire) lands at
|
|
||||||
558 B in a 1 KiB slot: the 512-byte figure is a hardware boundary those
|
|
||||||
chips simply do not have, and no implementation of this feature set fits it
|
|
||||||
there. The device speaks primitives; every composite — verify, erase,
|
|
||||||
reset-vector surgery, updating the loader itself — lives in the host tool
|
|
||||||
(`pureboot.py`).
|
|
||||||
|
|
||||||
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
|
transfer paths take wire addresses, the write guard protects the slot the code
|
||||||
code is *running* in (from the runtime return address), the info block is
|
is *running* in (from the runtime return address), nothing else is
|
||||||
addressed from that same anchor, and the application jump is an indirect
|
flash-resident to address at all, and the application jump is an indirect call
|
||||||
call to an absolute entry. The identical binary therefore runs from any
|
to an absolute entry. The identical binary therefore runs from any slot with
|
||||||
slot with every command intact — which makes pureboot **its own staging
|
every command intact, which makes pureboot **its own staging loader**: the host
|
||||||
loader**: the host installs the same binary one slot below the resident,
|
installs the same binary one slot below the resident, jumps into it, and lets
|
||||||
jumps into it, and lets it rewrite the resident. The slot is 512 bytes
|
it rewrite the resident. The lint holds it to that literally — the image must
|
||||||
(1 KiB on the word-addressed large chips, matching their boot-sector
|
come out byte-identical linked at a different base.
|
||||||
minimum); on the tinies the budget is 510, not 512: a slot's last word
|
|
||||||
belongs to the host-managed trampoline (below).
|
|
||||||
|
|
||||||
## Link
|
## Chips
|
||||||
|
|
||||||
| Chip | Serial | Baud | Clock assumed |
|
Sizes are the default configuration: the hardware USART0 at 115200 8N1 on a
|
||||||
|---|---|---|---|
|
16 MHz crystal, or the software UART on RX = PB0 / TX = PB1 at 57600 8N1 on
|
||||||
| every ATmega | the hardware USART (USART0), RXD/TXD per pinout | 115200 8N1 | 16 MHz crystal |
|
the tinies' RC oscillator (9.6 MHz on the t13s, 8 MHz above). Every axis moves
|
||||||
| ATtiny25/45/85 | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 8 MHz internal RC |
|
per build — see *Configuration*. The autobaud column is the clock-free build,
|
||||||
| ATtiny13/13A | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 9.6 MHz internal RC |
|
which is the largest the space produces and the tightest fit in the matrix;
|
||||||
|
it carries the calibration machinery and no clock at all.
|
||||||
|
|
||||||
The tiny RX pin has its pull-up enabled; TX idles high. All multi-byte
|
| Chip | Flash | Loader at | Link | Stock | Autobaud |
|
||||||
quantities on the wire are little-endian.
|
|---|---|---|---|---|---|
|
||||||
|
| ATtiny13, ATtiny13A † | 1 KiB | 0x0200 | software | 402 B | 472 B |
|
||||||
|
| ATtiny25 † | 2 KiB | 0x0600 | software | 406 B | 476 B |
|
||||||
|
| ATtiny45 † | 4 KiB | 0x0e00 | software | 410 B | 480 B |
|
||||||
|
| ATtiny85 † | 8 KiB | 0x1e00 | software | 410 B | 480 B |
|
||||||
|
| ATmega8, 8A | 8 KiB | 0x1e00 | USART0 | 372 B | 486 B |
|
||||||
|
| ATmega16, 16A | 16 KiB | 0x3e00 | USART0 | 374 B | 490 B |
|
||||||
|
| ATmega32, 32A | 32 KiB | 0x7e00 | USART0 | 374 B | 490 B |
|
||||||
|
| ATmega48, 48A, 48P, 48PA † | 4 KiB | 0x0e00 | USART0 | 400 B | 476 B |
|
||||||
|
| ATmega88, 88A, 88P, 88PA | 8 KiB | 0x1e00 | USART0 | 410 B | 486 B |
|
||||||
|
| ATmega168, 168A, 168P, 168PA | 16 KiB | 0x3e00 | USART0 | 412 B | 490 B |
|
||||||
|
| ATmega328, 328P | 32 KiB | 0x7e00 | USART0 | 412 B | 490 B |
|
||||||
|
| ATmega164A, 164P, 164PA | 16 KiB | 0x3e00 | USART0 | 412 B | 490 B |
|
||||||
|
| ATmega324A, 324P, 324PA | 32 KiB | 0x7e00 | USART0 | 412 B | 490 B |
|
||||||
|
| ATmega644, 644A, 644P, 644PA | 64 KiB | 0xfe00 | USART0 | 406 B | 484 B |
|
||||||
|
| ATmega1284, 1284P | 128 KiB | 0x1fe00 | USART0 | 432 B | 510 B |
|
||||||
|
|
||||||
|
† No hardware boot section: the host patches the reset vector, and the budget
|
||||||
|
is 510 bytes, since the slot's last word is the trampoline.
|
||||||
|
|
||||||
|
The tightest fit in the whole space is the 1284s' autobaud build, 510 of its
|
||||||
|
512 — they alone carry the far-flash machinery (ELPM reads, RAMPZ page
|
||||||
|
commands) and autobaud alone carries the calibration loop. Everything else has
|
||||||
|
20 B of headroom or more. The flash bank riding in a transfer's selector byte
|
||||||
|
keeps even those chips' addressing the same 16-bit form every other chip uses,
|
||||||
|
which is why they are no longer the outlier they were.
|
||||||
|
|
||||||
|
The software UART enables the RX pull-up; TX idles high. All multi-byte wire
|
||||||
|
quantities are little-endian.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Every deployment axis is a build parameter of `pureboot_add_loader()` (in
|
||||||
|
`pureboot/CMakeLists.txt`) — the one way a loader target is created, by this
|
||||||
|
repo's 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\|autobaud` | 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. Whatever is picked or overridden is
|
||||||
|
re-checked in the compile: an infeasible combination, or a USART the chip does
|
||||||
|
not have, fails with a named static assert.
|
||||||
|
|
||||||
|
`SERIAL autobaud` takes neither: the loader **measures** the host's bit timing
|
||||||
|
at run time, so `CLOCK` and `BAUD` are not build parameters there and one
|
||||||
|
binary per chip serves every clock and every rate. It is for the deployments
|
||||||
|
whose clock is not known at build time and does not hold still — the internal
|
||||||
|
RC oscillator, ±10 % from the factory and moving with supply and temperature —
|
||||||
|
where a fixed-baud software build has to be rebuilt per clock and still drifts
|
||||||
|
out of tolerance. The cost is that it is software-serial only (a hardware USART
|
||||||
|
needs its divisor programmed) and that activation counts poll iterations rather
|
||||||
|
than seconds, since there is no clock to convert them against
|
||||||
|
(`PUREBOOT_AUTOBAUD_POLLS`, default 4,000,000).
|
||||||
|
|
||||||
|
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 — an ATmega328P on its shipped 1 MHz fuses with the
|
||||||
|
software UART on hand-picked pins, say:
|
||||||
|
|
||||||
|
```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 deployment runs the full protocol suite in CI
|
||||||
|
(`pureboot.custom`).
|
||||||
|
|
||||||
## Activation
|
## Activation
|
||||||
|
|
||||||
Reset enters the loader (BOOTRST on the boot-sectioned megas; the patched
|
Reset enters the loader (BOOTRST on the boot-sectioned megas, the patched
|
||||||
reset vector on the tinies and the boot-section-less m48s) — except a
|
reset vector elsewhere) — except a watchdog reset, which hands straight to the
|
||||||
watchdog reset, which hands straight to the application (the application
|
application with no activation window, since the application owns its watchdog.
|
||||||
owns its watchdog; it must clear WDRF itself, which also releases the
|
This is deliberate: it lets an application reboot itself instantly rather than
|
||||||
WDRF-forced WDE).
|
sit through the window. The application must clear WDRF itself (libavr's
|
||||||
|
`watchdog::disable()` does). **Gotcha:** WDRF is sticky (cleared only by
|
||||||
|
software, not by a later reset), so an application that watchdog-resets and
|
||||||
|
never clears it diverts *every* subsequent reset — external ones included —
|
||||||
|
past the window too, and the loader becomes reachable only through an external
|
||||||
|
programmer until the flag is cleared. A serial recovery path therefore assumes
|
||||||
|
the application clears WDRF on its own reset path.
|
||||||
|
|
||||||
The host then has one activation window per awaited byte to knock: `p` then
|
The host then knocks `p` then `b`, each awaited byte under a fresh activation
|
||||||
`b`. Each awaited byte gets a fresh window; any other byte is discarded and
|
window; any other byte is discarded and awaited again, so line noise can delay
|
||||||
awaited again (line noise cannot lock the loader, only delay it). A window
|
the loader but never lock it. A window expiring on an idle line boots the
|
||||||
expiring with an idle line boots the application.
|
application.
|
||||||
|
|
||||||
The window length is a compile-time constant — 8 s by default, another value
|
An autobaud build opens differently, because it has to learn the rate before it
|
||||||
via the `PUREBOOT_TIMEOUT` CMake cache variable — so the whole EEPROM belongs
|
can read a byte at all: the host sends the **calibration byte 0xC0** — a start
|
||||||
to the application; pureboot never uses it for its own state. Re-timing a
|
bit plus six zero data bits form one low pulse of seven bit-times — and the
|
||||||
deployed loader is a self-update with a re-timed build (below).
|
loader times that pulse into its bit period. A single `p` then activates; the
|
||||||
|
pulse has already proven a host is present, which the two-byte knock exists to
|
||||||
|
establish elsewhere. Both waits are bounded, so a stray low pulse with no host
|
||||||
|
behind it costs one window and then boots the application rather than holding
|
||||||
|
the loader.
|
||||||
|
|
||||||
|
The window is a compile-time constant (`TIMEOUT`, 8 s by default), so the whole
|
||||||
|
EEPROM belongs to the application — pureboot keeps no state of its own.
|
||||||
|
Re-timing a deployed loader is a self-update with a re-timed build. An autobaud
|
||||||
|
build counts poll iterations instead (`PUREBOOT_AUTOBAUD_POLLS`), there being
|
||||||
|
no clock to turn into seconds.
|
||||||
|
|
||||||
## Session
|
## Session
|
||||||
|
|
||||||
After the knock the loader stays in its command loop until `J` jumps away or
|
After the knock the loader stays in its command loop until `J` jumps away or
|
||||||
the chip resets. Before reading each command it waits for any pending EEPROM
|
the chip resets. Before reading each command it waits for any pending EEPROM
|
||||||
write to finish and sends the prompt `+` (0x2b) — the prompt is therefore
|
write and sends the prompt `+` (0x2b), which is therefore also the previous
|
||||||
also the completion ack of the previous command. A session is: await `+`,
|
command's completion ack. A session is: await `+`, send a command, read its
|
||||||
send a command, read its reply, repeat.
|
reply, repeat.
|
||||||
|
|
||||||
On chips whose flash exceeds 64 KiB (the 1284s — info-block flag bit 1) the
|
Addresses are **byte addresses within a 64 KiB bank**, and the bank rides in
|
||||||
`R`/`W` flash addresses are **word** addresses; everywhere else they are byte
|
the command's selector byte, so no command has to speak word addresses. `J` is
|
||||||
addresses (the 644s' 64 KiB is exactly the 16-bit byte space and stays
|
the exception: it takes a word address, because that is what the hardware's own
|
||||||
byte-addressed). EEPROM addresses are always bytes, counts always bytes.
|
jump takes. EEPROM and data-space addresses and all counts are bytes.
|
||||||
|
|
||||||
|
The loader trusts the host to keep addresses in range: it does not bound them
|
||||||
|
against the chip. **Gotcha:** a write (or read) that runs past `E2END` wraps —
|
||||||
|
EEAR is only as wide as the array, so an address past the end truncates onto
|
||||||
|
low EEPROM and the write silently overwrites it. Keeping transfers within the
|
||||||
|
real sizes is the host's job (the shipped tool does); the flash budget is
|
||||||
|
better spent on features than on re-checking a bound the host already holds.
|
||||||
|
|
||||||
| Cmd | Arguments | Reply |
|
| Cmd | Arguments | Reply |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `b` | — | the 12-byte info block |
|
| `b` | — | 4 bytes: the pureboot version, then the three signature bytes |
|
||||||
| `R` | addr16, n8 | n flash bytes (n = 0 means 256) |
|
| `G` | sel8, addr16, n8 | n bytes from the selected space (n = 0 means 256) |
|
||||||
| `W` | addr16, then one page of data | — (completion = next prompt) |
|
| `g` | sel8, addr16, n8, then n data bytes | `+` per byte, sent once its write has begun |
|
||||||
| `r` | addr16, n8 | n EEPROM bytes (n = 0 means 256) |
|
| `W` | sel8, addr16, then one page of data | — (completion = next prompt) |
|
||||||
| `w` | addr16, n8, then n data bytes | `+` per byte, sent once its write has begun |
|
|
||||||
| `F` | — | 4 bytes: low fuse, lock, extended fuse, high fuse |
|
|
||||||
| `J` | word address (16-bit) | `+`, then execution continues there |
|
| `J` | word address (16-bit) | `+`, then execution continues there |
|
||||||
| other | — | ignored; the loop re-prompts (send a junk byte, await `+`, to resync) |
|
| other | — | ignored; the loop re-prompts (send a junk byte, await `+`, to resync) |
|
||||||
|
|
||||||
`W` streams exactly one SPM page (size from the info block) into the buffer,
|
`G` and `g` are one letter in two cases, which is the whole command set for
|
||||||
then erases and programs; the address must be page-aligned. Pages inside the
|
every memory: the **selector** byte's low nibble names the space and its high
|
||||||
512-byte slot the loader is *running* in are drained but never programmed — a
|
nibble carries the flash bank.
|
||||||
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
|
|
||||||
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.
|
|
||||||
Fuse *writing* does not exist: SPM reaches flash (and, on the mega, lock
|
|
||||||
bits) only — fuse bytes are external-programming territory by hardware.
|
|
||||||
|
|
||||||
`J` is the one control-transfer primitive: the host uses it to run the
|
| Space | | |
|
||||||
application (word 0 on the mega, the trampoline word on the tinies — both
|
|---|---|---|
|
||||||
known from the info block) and to move between loader copies during a
|
| 0 | flash | read-only here; it is written through `W` and the SPM space |
|
||||||
self-update. A jump to a loader slot's base re-enters that copy's own
|
| 1 | EEPROM | |
|
||||||
startup; it must then be knocked afresh.
|
| 2 | data | SRAM — and with it the register file and every I/O register, which share the data address space on AVR |
|
||||||
|
| 3 | fuse and lock | index 0..3 in the hardware's own Z order: low, lock, extended, high |
|
||||||
|
| 4 | SPM | write-only: the byte goes to SPMCSR and fires the instruction at the address |
|
||||||
|
|
||||||
The info block (`b`):
|
The data space is worth more than it looks. pureboot keeps **zero static RAM**
|
||||||
|
and pushes no register, so at loader entry an application's SRAM is still
|
||||||
|
whatever the application left there, bar the handful of bytes of return-address
|
||||||
|
stack — which makes `G` over space 2 a post-mortem of a running application,
|
||||||
|
not just a poke hole. The same address space carries the register file and the
|
||||||
|
I/O registers, so peripheral state is readable too; reading some of those has
|
||||||
|
side effects (reading UDR clears its flags), which is the host's business to
|
||||||
|
know.
|
||||||
|
|
||||||
| Offset | Content |
|
Programming a page is therefore `W` to fill the buffer, then a `g` to the SPM
|
||||||
|---|---|
|
space for the erase, another for the write, and on a boot-sectioned chip a
|
||||||
| 0–2 | `'P'`, `'B'`, protocol version (1) |
|
third to re-enable the RWW section — `0x03`, `0x05` and `0x11`, the SPMCSR
|
||||||
| 3–5 | device signature |
|
encodings every part pureboot targets shares. The loader carries no page-commit
|
||||||
| 6 | SPM page size in bytes (0 means 256) |
|
logic of its own, and the same primitive reaches every other SPM operation,
|
||||||
| 7–8 | loader base — application flash ends here (a word address when bit 1 is set) |
|
lock bits included.
|
||||||
| 9–10 | EEPROM size |
|
|
||||||
| 11 | bit 0: host must patch the reset vector (no hardware boot section); bit 1: flash wire addresses are word addresses |
|
|
||||||
|
|
||||||
Composites are the host's job: verify = read back and compare, erase =
|
The SPM store and the SPM instruction must issue within four cycles of each
|
||||||
write `0xff` (per page for flash, per byte for EEPROM).
|
other (§26.2), which no host can hit across a serial link — so this one
|
||||||
|
primitive is *fused* rather than being a poke of SPMCSR followed by a poke of
|
||||||
|
something else. That four-cycle window is the floor on how low-level a
|
||||||
|
bootloader's primitives can go; it is not a byte-count decision.
|
||||||
|
|
||||||
|
An SPM command aimed at the 512-byte slot the loader is **running in** is
|
||||||
|
dropped, so a broken host cannot brick the running copy, while a staged copy
|
||||||
|
one slot lower may rewrite the resident — which is what a self-update is.
|
||||||
|
|
||||||
|
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, and —
|
||||||
|
where SPM runs from anywhere, the tinies and the m48s — an application that
|
||||||
|
self-programmed before entering. The next page write takes those stale words
|
||||||
|
and clears them, since 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).
|
||||||
|
|
||||||
|
`g` is host-paced: send the next byte only after the previous byte's `+`. Fuse
|
||||||
|
*writing* does not exist — SPM reaches flash and boot lock bits only.
|
||||||
|
|
||||||
|
`J` is the one control-transfer primitive: it runs the application (word 0 or
|
||||||
|
the trampoline word, both derived from the chip) and moves between loader
|
||||||
|
copies during a self-update. A jump to a slot's base re-enters that copy's own
|
||||||
|
startup, which must then be knocked afresh.
|
||||||
|
|
||||||
|
`b` answers with the loader's identity — its version and the chip's signature —
|
||||||
|
and nothing else. Everything else the host needs (page size, loader base,
|
||||||
|
EEPROM size, whether the reset vector must be patched, how many flash banks)
|
||||||
|
follows from the signature, and the host holds that table; the loader derived
|
||||||
|
the same facts from its own chip database at build time, so nothing is guessed,
|
||||||
|
it is simply not sent twice.
|
||||||
|
|
||||||
|
An update image, though, is a bare 512-byte slot with no device to ask, and
|
||||||
|
installing one built for another chip bricks the target. Every loader image
|
||||||
|
therefore carries a six-byte **stamp** — `'P'`, `'B'`, the version, the three
|
||||||
|
signature bytes — which the loader itself never reads and the host tool refuses
|
||||||
|
to install a mismatch against.
|
||||||
|
|
||||||
|
## Version
|
||||||
|
|
||||||
|
`b`'s first byte is the **pureboot version** — the loader's one identity
|
||||||
|
number, and the only way to tell what a deployed loader is. Nothing else is
|
||||||
|
numbered: the wire protocol has no version, a pureboot version implies it, and
|
||||||
|
the host tool holds that map. The tool states the window of loader versions it
|
||||||
|
speaks (`OLDEST_LOADER`/`NEWEST_LOADER` in `pureboot.py`), and a version that
|
||||||
|
changes the protocol becomes the new floor there. A loader newer than the tool
|
||||||
|
is refused by name rather than decoded on the assumption that nothing moved.
|
||||||
|
|
||||||
|
Two generations exist. **1 through 4** speak one session — a 12-byte info block
|
||||||
|
from `b`, and a command per memory (`R`/`W` flash, `r`/`w` EEPROM, `F` fuses).
|
||||||
|
**5** replaced those with the single `G`/`g` pair over selector-named spaces
|
||||||
|
above; the shipped tool speaks both, choosing on the version it reads, so a
|
||||||
|
deployed pureboot 4 stays drivable and self-updatable to 5.
|
||||||
|
|
||||||
|
Collapsing four command bodies into one transfer loop is what paid for the
|
||||||
|
version: the data space, the host-issued SPM operations and the fuses now share
|
||||||
|
the loop, the cursor and the argument decode that `R`/`r`/`w` each carried a
|
||||||
|
copy of. The loader shrank while gaining all three.
|
||||||
|
|
||||||
|
The tool carries its own version, free to drift; `--version` prints it and the
|
||||||
|
window.
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
The build leaves three artifacts per chip. The ELF is a container for the
|
The build leaves three artifacts per chip. The ELF is a container for the
|
||||||
tests and objcopy — never flashed. The **.hex is the programmer artifact**:
|
tests and objcopy, never flashed. The **.hex is the programmer artifact**: it
|
||||||
it carries its own addresses and lands the loader in its top slot,
|
carries its own addresses and lands the loader in its top slot, touching
|
||||||
touching nothing else. The **.bin is the self-update image** — the slot's
|
nothing else. The **.bin is the self-update image** — the slot's bare bytes.
|
||||||
bare bytes with no addressing, which a programmer would put at address 0.
|
|
||||||
On a boot-sectioned mega a copy at 0 is dead weight (SPM only executes
|
|
||||||
from the boot section, so it cannot even heal itself — reflash the .hex);
|
|
||||||
on the patched-vector chips it *runs* (the image is position-independent
|
|
||||||
and reset enters word 0), reports its canonical geometry, and the ordinary
|
|
||||||
`--update-loader` flow re-homes a build into the top slot — the staging
|
|
||||||
install and the word-0 redirect both execute from copies outside page 0's
|
|
||||||
slot, so the running-slot guard never blocks the flow (`pureboot.rehome`
|
|
||||||
is the acceptance test). Flashing the application afterwards overwrites
|
|
||||||
the stale copy, vector surgery included. The one position that cannot
|
|
||||||
re-home itself is the staging slot: installing the staging copy would hit
|
|
||||||
the running copy's own guard and the update stops at its verify — flash
|
|
||||||
the .hex instead.
|
|
||||||
|
|
||||||
**Boot-sectioned megas**: program the loader at `flash − slot` with an
|
**Boot-sectioned megas**: program the loader at `flash − 512` with an external
|
||||||
external programmer. Every such mega has a BOOTSZ step whose boot section
|
programmer. Every such mega has a BOOTSZ step whose boot section is exactly
|
||||||
is exactly the loader slot — 512 B, the second-smallest step on the 8 KiB
|
the 512-byte slot — the second-smallest step on the 8 KiB and 16 KiB chips,
|
||||||
and 16 KiB chips (m8, m88, m16, m168, m164), the smallest on the 32 KiB
|
the smallest on the 32 KiB ones — so the ATmega328P profiles below apply to
|
||||||
ones (m32, m328, m324); on the 1284s that step is the smallest, 512 words,
|
every one of them with its own addresses; the per-chip BOOTSZ ladders live in
|
||||||
which is why their slot is 1 KiB — so the ATmega328P profiles below apply
|
the host tool (`BOOT_FUSE`).
|
||||||
to every one of them with its own addresses and slot size; the per-chip
|
|
||||||
BOOTSZ ladders live in the host tool (`BOOT_FUSE`). The 1284s' numbers:
|
|
||||||
standalone = BOOTSZ 512 words (reset at the loader base 0x1fc00);
|
|
||||||
self-update = 1024 words, covering both 1 KiB slots, the loader-first
|
|
||||||
reset landing at 0x1f800 — the staging slot, walked across when erased.
|
|
||||||
|
|
||||||
The **644s** are the geometry's sweet spot: their smallest boot section
|
The **644s and 1284s** are the geometry's sweet spot: their smallest boot
|
||||||
(512 words = 1 KiB) is exactly *two* 512-byte slots, so the resident and
|
section (512 words = 1 KiB) is exactly *two* slots, so the resident and its
|
||||||
its staging slot both live inside the minimum section — self-update needs
|
staging slot both live inside the minimum section. Self-update needs no fuse
|
||||||
no fuse step up, and the standalone profile does not exist (reset lands at
|
step up, and the standalone profile does not exist — reset lands one erased
|
||||||
0xfc00, one erased slot below the loader: the loader-first walk built in).
|
slot below the loader (0xfc00 / 0x1fc00) and walks up into it.
|
||||||
|
|
||||||
ATmega328P profiles (addresses for its 32 KiB):
|
ATmega328P profiles (addresses for its 32 KiB):
|
||||||
|
|
||||||
@@ -155,113 +294,160 @@ ATmega328P profiles (addresses for its 32 KiB):
|
|||||||
| 512 words (1 KB) | unprogrammed | *Self-update, app-first*: reset always boots the application, which owns all 31.5 KB and must offer its own jump to 0x7e00 to reach the loader (a virgin chip reaches it by reset across erased flash). Updates are power-fail-safe except mid-rewrite of the resident slot itself (no reset path leads to the staging copy then). |
|
| 512 words (1 KB) | unprogrammed | *Self-update, app-first*: reset always boots the application, which owns all 31.5 KB and must offer its own jump to 0x7e00 to reach the loader (a virgin chip reaches it by reset across erased flash). Updates are power-fail-safe except mid-rewrite of the resident slot itself (no reset path leads to the staging copy then). |
|
||||||
| 512 words (1 KB) | programmed | *Self-update, loader-first*: reset lands at 0x7c00 — the staging slot, normally erased, so execution walks up into the loader; during an update it is the staging copy itself, so a mid-rewrite power loss recovers by reset. The loss windows move to the staging install/retire page writes instead (page-write scale). The host keeps `[0x7c00, 0x7e00)` clear of application data (`--force` overrides). |
|
| 512 words (1 KB) | programmed | *Self-update, loader-first*: reset lands at 0x7c00 — the staging slot, normally erased, so execution walks up into the loader; during an update it is the staging copy itself, so a mid-rewrite power loss recovers by reset. The loss windows move to the staging install/retire page writes instead (page-write scale). The host keeps `[0x7c00, 0x7e00)` clear of application data (`--force` overrides). |
|
||||||
|
|
||||||
Applications are flashed unmodified — word 0 stays the application's own
|
Applications are flashed unmodified here — word 0 stays the application's own
|
||||||
reset vector, and the hand-over jumps to 0.
|
reset vector, and the hand-over jumps to 0.
|
||||||
|
|
||||||
**Patched-vector chips — the tinies and the m48s** (no boot section; the
|
**Patched-vector chips — the tinies and the m48s** (no boot section; the m48s'
|
||||||
m48s' SPM runs from the entire flash, Atmel-8271 §26): program the loader
|
SPM runs from the entire flash, Atmel-8271 §26): program the loader at
|
||||||
at `flash − 512`; erased flash below it walks up into the loader, so a
|
`flash − 512`; erased flash below it walks up into the loader, so a virgin
|
||||||
virgin chip activates. When flashing an application the host performs
|
chip activates. Flashing an application then takes reset-vector surgery: word
|
||||||
reset-vector surgery: word 0 is rewritten to `rjmp` to the loader base, and
|
0 becomes an `rjmp` to the loader base, and the application's own entry is
|
||||||
the application's own entry is re-encoded as a trampoline `rjmp` in the
|
re-encoded as a trampoline `rjmp` in the word just below the loader
|
||||||
word just below the loader (`base − 2`, where the hand-over jumps). Every
|
(`base − 2`, where the hand-over jumps). Every other vector stays the
|
||||||
other vector stays the application's. The patched page 0 and the trampoline
|
application's. The patched page 0 and the trampoline page are written *first*
|
||||||
page are written *first*, so from the first write on an interrupted flash
|
and an erase runs top-down, so from the first write on an interruption still
|
||||||
still resets into the loader; an erase runs top-down for the same reason.
|
resets into the loader.
|
||||||
The m48s speak this profile over their hardware USART — no fuse preflight,
|
|
||||||
BOOTRST does not exist there.
|
A .bin programmed at address 0 by mistake is dead weight on a boot-sectioned
|
||||||
|
mega (SPM only executes from the boot section — reflash the .hex), but *runs*
|
||||||
|
on a patched-vector chip, and the ordinary `--update-loader` flow re-homes it
|
||||||
|
into the top slot from there (`pureboot.rehome`).
|
||||||
|
|
||||||
## Updating the loader
|
## Updating the loader
|
||||||
|
|
||||||
`pureboot.py --update-loader new_pureboot.bin` replaces the resident loader
|
`pureboot.py --update-loader new_pureboot.bin` replaces the resident loader
|
||||||
with any pureboot build — a re-timed window, a newer protocol — using the
|
with any pureboot build — a re-timed window, a newer version — using the
|
||||||
loader itself as its own staging loader. The image is the loader's own 512
|
loader itself as its own staging loader. The image is the loader's own 512
|
||||||
bytes as a raw binary, or the Intel HEX the build emits beside it, which
|
bytes as a raw binary, or the Intel HEX the build emits beside it.
|
||||||
links the loader at its base inside an otherwise blank flash image:
|
|
||||||
|
|
||||||
The preflight refuses an image built for another chip: the info block
|
The preflight refuses an image built for another chip: the stamp every pureboot
|
||||||
embedded in every pureboot binary (signature, page size, loader base,
|
binary carries must resolve to the device's own geometry, and the error names
|
||||||
EEPROM size, flags) must match the device's own, and the error names both.
|
both. Die revisions share their base signature and geometry, so their images
|
||||||
Die revisions share their base signature and geometry, so their images are
|
are interchangeable — as the silicon is.
|
||||||
interchangeable — as the silicon is. `loader_image()` also accepts a
|
|
||||||
padded image (a raw .bin padded from 0, or a whole-flash read-back with
|
|
||||||
the loader resident) and peels it to the slot content by the embedded base.
|
|
||||||
|
|
||||||
1. The staging slot `[base−slot, base)` is saved to a host-side state file
|
1. The staging slot `[base−512, base)` is saved to a host-side state file (on
|
||||||
(on the 1 KB tiny13s that is the whole application, vectors included).
|
the 1 KB tiny13s that is the whole application, vectors included).
|
||||||
2. The resident installs the identical update image there. On the
|
2. The resident installs the update image there. On the patched-vector chips
|
||||||
patched-vector chips the host composes the slot's last word — the same
|
the host composes the slot's last word as a jump to the resident base, so
|
||||||
address as the resident's trampoline — as a jump to the resident base,
|
even an abandoned staging copy times out into a loader. A loader already
|
||||||
so even an abandoned staging copy times out into a loader, never into
|
sitting whole in the staging slot is left as the staging copy instead —
|
||||||
garbage.
|
rewriting it would only meet its own running-slot guard.
|
||||||
3. `J` enters the staging copy, which rewrites the resident slot. On the
|
3. `J` enters the staging copy, which rewrites the resident slot. Where a
|
||||||
patched-vector chips whose staging slot sits away from page 0 the host
|
patched reset vector routes through the resident, the host first re-aims
|
||||||
first re-aims word 0 at the staging copy, so a power loss mid-rewrite
|
word 0 at the staging copy, so a power loss mid-rewrite still resets into a
|
||||||
still resets into a loader; on the tiny13s the staging slot carries the
|
loader; on the tiny13s the staging slot carries the reset vector itself.
|
||||||
reset vector itself.
|
|
||||||
4. `J` enters the new resident, which restores the staging slot's saved
|
4. `J` enters the new resident, which restores the staging slot's saved
|
||||||
content (word 0 and the trampoline with it) and the state file is
|
content, and the state file is discarded.
|
||||||
discarded.
|
|
||||||
|
|
||||||
Every phase is idempotent and keyed off the actual flash state: re-running
|
Every phase is idempotent and keyed off the actual flash state, so re-running
|
||||||
the same command after any interruption resumes and completes. The state
|
the same command after any interruption resumes and completes. The state file
|
||||||
file carries the only bytes not recoverable from the device; if it is lost
|
carries the only bytes not recoverable from the device; losing it mid-update
|
||||||
mid-update the update still completes, and the staging region is restored by
|
still completes the update, and the staging region comes back by reflashing
|
||||||
reflashing the application. A boot-sectioned mega needs its fuses for the
|
the application. A boot-sectioned mega needs its fuses for the preflight — read
|
||||||
preflight (BOOTSZ gate, profile notes) — read from the device, or supplied
|
from the device, or supplied with `--assume-fuses` where reading is impossible
|
||||||
with `--assume-fuses` where reading is impossible (simulators); the
|
(simulators).
|
||||||
patched-vector chips need none.
|
|
||||||
|
|
||||||
## Host tool
|
## Host tool
|
||||||
|
|
||||||
`pureboot.py` — Python 3, standard library only. The port layer is the one
|
`pureboot.py` — Python 3, standard library only. The port layer is the one
|
||||||
platform-specific part: termios drives any tty on POSIX (a USB adapter as
|
platform-specific part: termios drives any tty on POSIX (a USB adapter as well
|
||||||
well as a simavr pty), the Win32 serial API through `ctypes` drives a COM
|
as a simavr pty), the Win32 serial API through `ctypes` drives a COM port on
|
||||||
port on Windows (`--port COM6`; the `\\.\` form for two-digit ports is
|
Windows (`--port COM6`; the `\\.\` form for two-digit ports is supplied by the
|
||||||
supplied by the tool). Opening the port asserts DTR and RTS on both, so a
|
tool). Opening the port asserts DTR and RTS on both, so a board that wires DTR
|
||||||
board that wires DTR to reset gets its reset pulse and opens the activation
|
to reset gets its reset pulse and opens the activation window by itself.
|
||||||
window by itself.
|
|
||||||
|
|
||||||
pureboot.py --port /dev/ttyUSB0 --baud 57600 \
|
pureboot.py --port /dev/ttyUSB0 --baud 57600 \
|
||||||
--info --fuses --flash app.hex
|
--info --fuses --flash app.hex
|
||||||
|
|
||||||
Operations run in a fixed order within one session: info, fuses, loader
|
Operations run in a fixed order within one session: info, fuses, loader
|
||||||
update, flash (erase / program / read / verify), EEPROM (erase / program /
|
update, flash (erase / program / read / verify), EEPROM (the same), then
|
||||||
read / verify) — then the loader hands over to the application; `--stay`
|
`--peek`/`--poke` — then the loader hands over to the application. `--stay` keeps the session alive
|
||||||
keeps the session alive instead, and a later invocation reconnects into it
|
instead, and a later invocation reconnects into it. `--flash` and `--eeprom`
|
||||||
(the knock converges there too). `--flash` and `--eeprom` verify by
|
verify by read-back unless `--no-verify`, and a flash page that reads back
|
||||||
read-back unless `--no-verify`; images are raw binary, or Intel HEX by
|
wrong is rewritten up to three times before the run stops (see `W` above).
|
||||||
extension. `--force` overrides the refusable safety checks (today: flashing
|
`--verify-flash` only reports. Images are raw binary, or Intel HEX by
|
||||||
application data into a mega's reset walk region).
|
extension. `--force` overrides the refusable safety checks — today, flashing
|
||||||
|
application data into a mega's reset walk region.
|
||||||
|
|
||||||
|
`--autobaud` opens with the calibration pulse instead of the plain knock, for a
|
||||||
|
loader built `SERIAL autobaud`; the rest of the session is identical, at
|
||||||
|
whatever `--baud` the host chose.
|
||||||
|
|
||||||
|
`--peek ADDR[:N]` and `--poke ADDR:HEX` reach the data space (pureboot 5) —
|
||||||
|
SRAM, and through the same address space the register file and every I/O
|
||||||
|
register. Reading an I/O register can have side effects (reading UDR clears its
|
||||||
|
flags), which is the caller's business to know.
|
||||||
|
|
||||||
|
Readouts come one fact per line: `--info` prints the device's version and
|
||||||
|
signature and the geometry that follows from them, `--fuses` each fuse byte
|
||||||
|
plus, on a boot-sectioned mega, its decoded meaning. Transfers that take wire time draw a transient progress bar on stderr
|
||||||
|
when it is a tty. `-v`/`--verbose` adds the decisions as they happen: knock
|
||||||
|
counts, the programming plan, update state handling and per-phase page counts.
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
|
`tools/check.sh` runs every chip's workflow (`--full` adds the reflect-mode
|
||||||
|
builds of libavr's spot set; `tools/make_presets.py` regenerates the presets).
|
||||||
Per chip preset, `ctest` runs:
|
Per chip preset, `ctest` runs:
|
||||||
|
|
||||||
- `pureboot.size` — the 510-byte (tinies) / 512-byte (mega) budget;
|
- `pureboot.size` — the 510-byte (patched-vector) / 512-byte budget;
|
||||||
- `pureboot.pi` — the position-independence lint: no absolute `jmp`/`call`
|
- `pureboot_*.size` — the size matrix: the serial backends × the clock ladder
|
||||||
in the image, the info block within its first 256 bytes;
|
(1/8/16 MHz; the t13s' own RC menu), the USART1 instance across that same
|
||||||
- `pureboot.planner` — the host tool's pure logic: programming orders and
|
ladder on the x4 chips, and `pureboot_sw_wide`, the slowest ladder rate at
|
||||||
their recovery properties, the surgery, the staging composition, the
|
the fastest clock — where a software UART's per-bit spin outgrows its
|
||||||
boot-fuse decode, and the update preflight's error/warning matrix over
|
one-register delay loop and takes the 16-bit one. That is the largest image
|
||||||
synthetic fuse bytes;
|
the configuration space produces, and a shape the ladder default (always the
|
||||||
|
*fastest* rate a clock reaches) never picks. Pins are immediate operands and
|
||||||
|
the timeout is a constant: neither is an axis;
|
||||||
|
- `pureboot_autobaud.size` — the clock-free build, which has no clock or baud
|
||||||
|
axis of its own: one binary per chip has to serve every point the matrix
|
||||||
|
below sweeps;
|
||||||
|
- `pbm_*.size` — with `PUREBOOT_FULL_MATRIX=1`, the exhaustive cross product
|
||||||
|
replacing that compact matrix, on **every** chip: every plausible oscillator
|
||||||
|
(the internal ones, the CKDIV8 floor, the plain and the UART crystals) ×
|
||||||
|
every rate reachable from it × every backend, unreachable combinations
|
||||||
|
dropping out rather than aborting the configure. Thousands of points per
|
||||||
|
chip, and cheap enough to run rather than reason about;
|
||||||
|
- `pureboot.pi` — the position-independence lint: no absolute `jmp`/`call`, no
|
||||||
|
flash-resident section but `.text`, and the image byte-identical when linked
|
||||||
|
at a different base — which is position independence itself rather than a
|
||||||
|
proxy for it;
|
||||||
|
- `pureboot.planner` — the host tool's pure logic: programming orders and their
|
||||||
|
recovery properties, the surgery, the staging composition, the boot-fuse
|
||||||
|
decode, the update preflight over synthetic fuse bytes, and the repairing
|
||||||
|
verify against a fake device;
|
||||||
- `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, plus the SPM/NVM module simavr's
|
||||||
simavr's tiny cores lack) driven by the real host tool through
|
tiny cores lack) driven by the real host tool through knock-from-reset,
|
||||||
knock-from-reset, program + verify of both memories, session reconnect, an
|
program + verify of both memories, session reconnect, an external reset
|
||||||
external reset through the patched vector, and the hand-over to a fixture
|
through the patched vector, and the hand-over to a fixture application whose
|
||||||
application whose banner proves the launch — cross-checked against the
|
banner proves the launch — cross-checked against the simulator's
|
||||||
simulator's ground-truth memory dumps and an independent decode of the
|
ground-truth memory dumps and an independent decode of the surgery;
|
||||||
surgery's rjmp words;
|
- `pureboot.reloc` — the identical image one slot below the resident serves the
|
||||||
- `pureboot.reloc` — the identical image installed one slot below the
|
complete command set from there;
|
||||||
resident serves the complete command set from there (the
|
- `pureboot.rehome` (t85) — a loader programmed at address 0 or in the staging
|
||||||
position-independence acceptance test);
|
slot re-homes into the top slot through the ordinary update flow;
|
||||||
- `pureboot.update` — the full `--update-loader` flow to a re-timed build,
|
- `pureboot.custom` (328P) — the configuration example's 1 MHz software-serial
|
||||||
then every power-fail phase: the device is killed mid-write, restarted
|
build driving the full protocol suite, proving the plumbing produces a
|
||||||
from its flash dump, and a re-run must complete the update with the
|
working loader and not just one that fits;
|
||||||
application intact throughout.
|
- `pureboot.usart1` (644A) — the same 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.dirty` (328P) — entering the loader from a running application over
|
||||||
|
an SPM buffer it deliberately dirtied, the case the loader declines to guard:
|
||||||
|
a bare verify must see the corruption and the repairing verify must fix it in
|
||||||
|
one rewrite. Hardware forbids the state here, but simavr dispatches SPM from
|
||||||
|
anywhere, which is what makes the path constructible;
|
||||||
|
- `pureboot.update` — the full `--update-loader` flow, 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 application intact;
|
||||||
|
- `pureboot.autobaud` (328P, 1284P) — the clock-free build over the GPIO⇄pty
|
||||||
|
bridge: the calibration handshake, a flash + EEPROM + fuse round trip against
|
||||||
|
the simulator's own memory, a data-space round trip, the hand-over — then the
|
||||||
|
same binary again at double the clock, which is the property the backend
|
||||||
|
exists for. A lone calibration pulse with no knock behind it must still let
|
||||||
|
the application boot, so no wait in activation can be unbounded.
|
||||||
|
|
||||||
`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.
|
|
||||||
|
|||||||
@@ -1,28 +1,14 @@
|
|||||||
// pureboot — a serial bootloader on libavr, pure by constraint: one C++
|
// pureboot — a serial bootloader on libavr: one C++ source, no inline
|
||||||
// source with no inline assembly and no global register variables, built for
|
// assembly, no global register variables, 512 bytes on every chip libavr
|
||||||
// every chip libavr targets, 512 bytes on each. The device speaks primitives
|
// targets. The device speaks primitives; every composite (verify, erase,
|
||||||
// — read/program flash, read/write EEPROM, fuse bytes, an info block, a jump
|
// reset-vector surgery, self-update) lives in the host tool. Protocol,
|
||||||
// — and everything composite (verify, erase, reset-vector surgery, updating
|
// deployment and configuration: README.md next to this file.
|
||||||
// the loader itself) lives in the host tool. Protocol reference: README.md
|
|
||||||
// next to this file.
|
|
||||||
//
|
//
|
||||||
// The image is position-independent: control flow is PC-relative, the write
|
// The image is position-independent — PC-relative control flow, wire
|
||||||
// and read paths take wire addresses, the write guard refuses the 512-byte
|
// addresses in, the write guard and the info block both anchored on the
|
||||||
// slot the code is *running* in (taken from the runtime return address), the
|
// runtime return address — so the identical binary runs from any slot. That
|
||||||
// info block is read relative to that same anchor, and the application jump
|
// is what makes a copy one slot below able to rewrite the resident one, and
|
||||||
// is an indirect call to an absolute entry. The identical binary therefore
|
// every change here has to keep it (test/check_pi.py).
|
||||||
// runs from any 512-byte slot with every command intact: flashed one slot
|
|
||||||
// below the resident loader it becomes the staging loader that rewrites the
|
|
||||||
// resident — how pureboot updates itself, host-driven, with no other
|
|
||||||
// firmware involved.
|
|
||||||
//
|
|
||||||
// Entry: reset lands in avr::startup::entry below (BOOTRST on the
|
|
||||||
// boot-sectioned megas; the patched reset vector — or erased flash walking
|
|
||||||
// up into the loader — on the tinies and the boot-section-less m48s). A
|
|
||||||
// watchdog reset hands straight to the application. Otherwise the
|
|
||||||
// host has one activation window per awaited knock byte ("pb"); an idle line
|
|
||||||
// boots the application. A session then stays in the command loop until 'J'
|
|
||||||
// jumps away or the chip resets.
|
|
||||||
|
|
||||||
#include <libavr/libavr.hpp>
|
#include <libavr/libavr.hpp>
|
||||||
|
|
||||||
@@ -33,27 +19,24 @@ namespace ee = avr::eeprom;
|
|||||||
namespace pureboot {
|
namespace pureboot {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// Purely polled — interrupts stay off, every guard folds to nothing.
|
// Purely polled: every interrupt guard folds to nothing.
|
||||||
constexpr auto off = avr::irq::guard_policy::unused;
|
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
|
// Deployment parameters come from the build (pureboot_add_loader()). The
|
||||||
// the mega, calibrated RC on the tinies). The device signature comes straight
|
// signature is not one of them: the chip database is the only universal
|
||||||
// from the chip database (avr::hw::db.signature) — compile-time data is the
|
// source — a tiny13A cannot read its own signature row from code. An autobaud
|
||||||
// only universal source, since the tiny13A cannot even read its signature row
|
// build carries no clock and no baud at all; it measures both.
|
||||||
// from code.
|
#if !defined(PUREBOOT_AUTOBAUD) && (!defined(PUREBOOT_CLOCK_HZ) || !defined(PUREBOOT_BAUD))
|
||||||
consteval avr::hertz_t clock()
|
#error \
|
||||||
{
|
"PUREBOOT_CLOCK_HZ and PUREBOOT_BAUD select this build's clock and baud — create loader targets with pureboot_add_loader(), or PUREBOOT_AUTOBAUD for a clock-free one (README.md)"
|
||||||
auto name = std::string_view{avr::hw::db.name};
|
#endif
|
||||||
if (name.starts_with("ATtiny13"))
|
|
||||||
return 9.6_MHz;
|
|
||||||
if (name.starts_with("ATtiny"))
|
|
||||||
return 8_MHz;
|
|
||||||
return 16_MHz;
|
|
||||||
}
|
|
||||||
|
|
||||||
using dev = avr::device<{.clock = clock()}>;
|
#if !defined(PUREBOOT_AUTOBAUD)
|
||||||
|
using dev = avr::device<{.clock = avr::hertz_t{PUREBOOT_CLOCK_HZ}}>;
|
||||||
|
constexpr avr::baud_t wire_baud{PUREBOOT_BAUD};
|
||||||
|
#endif
|
||||||
|
|
||||||
// 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()
|
||||||
@@ -62,90 +45,130 @@ consteval std::int16_t wdrf_field()
|
|||||||
return avr::hw::db.field_index(reg, "WDRF");
|
return avr::hw::db.field_index(reg, "WDRF");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Geometry: the resident loader owns the top slot of flash — 512 bytes,
|
// The loader owns the top 512 bytes; a staging copy goes in the slot below.
|
||||||
// except on the >64 KiB chips whose own smallest boot sector is 1 KiB (the
|
// Chips without a hardware boot section — the tinies and the m48s, whose SPM
|
||||||
// 1284s): there the slot is 1 KiB, matching the hardware boundary the
|
// runs from anywhere (Atmel-8271 §26) — keep the application's relocated
|
||||||
// 512-byte figure comes from everywhere else. The word below the slot is
|
// reset vector in the word under the slot.
|
||||||
// the trampoline (the application's relocated reset vector) on chips
|
constexpr std::uint16_t slot_bytes = 512;
|
||||||
// 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 on while the RWW section programs; everywhere else it halts through
|
|
||||||
// the operation. The m48s still carry RWWSRE as their temporary-buffer
|
|
||||||
// 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::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 one bank of flash does not cover the chip, so a transfer's
|
||||||
// large chips every flash address on the wire — and all slot arithmetic —
|
// selector byte carries the bank and the wire address stays a byte address
|
||||||
// is a word address instead ('J' always was one). A slot spans the same
|
// within it. 'J' is the exception: it is a word address everywhere, because
|
||||||
// wire-high-byte pair in either unit (512 B = 2 x 256 bytes, 1 KiB =
|
// that is what the hardware's own jump takes.
|
||||||
// 2 x 256 words), so the slot index is the high byte with its low bit
|
constexpr bool banked_flash = spm::flash_bytes > 65536;
|
||||||
// dropped everywhere.
|
|
||||||
constexpr bool word_flash = spm::flash_bytes > 65536;
|
|
||||||
constexpr std::uint16_t wire_base =
|
|
||||||
word_flash ? static_cast<std::uint16_t>(base / 2) : static_cast<std::uint16_t>(base);
|
|
||||||
constexpr std::uint16_t wire_page_mask = word_flash ? (page / 2 - 1) : (page - 1);
|
|
||||||
|
|
||||||
// The activation window, in seconds, is a compile-time constant (the build
|
// A compile-time window, so the whole EEPROM belongs to the application;
|
||||||
// may override it): the whole EEPROM belongs to the application, and
|
// re-timing a deployed loader is a self-update with a re-timed build. An
|
||||||
// re-timing the loader is a bootloader self-update with a re-timed binary.
|
// autobaud build has no clock to convert seconds against and counts polls.
|
||||||
#if !defined(PUREBOOT_TIMEOUT)
|
#if !defined(PUREBOOT_TIMEOUT)
|
||||||
#define PUREBOOT_TIMEOUT 8
|
#define PUREBOOT_TIMEOUT 8
|
||||||
#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
|
#if !defined(PUREBOOT_AUTOBAUD_POLLS)
|
||||||
// (there is no crt to copy a .data image), word-aligned so its wire (word)
|
#define PUREBOOT_AUTOBAUD_POLLS 4000000
|
||||||
// address is exact on the large chips. The page byte is the wire count
|
#endif
|
||||||
// convention: 0 means 256.
|
constexpr avr::uint24_t autobaud_budget = PUREBOOT_AUTOBAUD_POLLS;
|
||||||
[[gnu::progmem]] alignas(2) inline constexpr std::array<std::uint8_t, 12> info_data = {
|
|
||||||
'P',
|
// The loader's one identity number. The protocol carries none of its own —
|
||||||
'B',
|
// a version implies it, and the host tool holds that map (README.md).
|
||||||
1, // magic, protocol version
|
constexpr std::uint8_t version = 5;
|
||||||
|
|
||||||
|
// The image's identity stamp, for the host tool rather than for the wire: an
|
||||||
|
// update image is a bare 512-byte slot, and without this nothing in it says
|
||||||
|
// which chip it was built for. The tool refuses to install an image whose
|
||||||
|
// stamp does not match the device — flashing a foreign loader bricks the
|
||||||
|
// target, and the loader itself cannot check what has already replaced it.
|
||||||
|
//
|
||||||
|
// Never read from flash by the loader — 'b' answers out of this array, but at
|
||||||
|
// constant indices, so those fold to immediates and no runtime address of it
|
||||||
|
// is ever formed. `used` keeps the compiler from dropping the copy the host
|
||||||
|
// needs and `retain` keeps --gc-sections from collecting it.
|
||||||
|
// clang-format off
|
||||||
|
[[gnu::used, gnu::retain, gnu::section(".text.stamp")]]
|
||||||
|
inline constexpr std::uint8_t identity_stamp[]{
|
||||||
|
'P', 'B', // the magic the host scans an image for
|
||||||
|
version, // and from here on, exactly what 'b' answers
|
||||||
avr::hw::db.signature[0],
|
avr::hw::db.signature[0],
|
||||||
avr::hw::db.signature[1],
|
avr::hw::db.signature[1],
|
||||||
avr::hw::db.signature[2],
|
avr::hw::db.signature[2],
|
||||||
static_cast<std::uint8_t>(page),
|
|
||||||
wire_base & 0xff,
|
|
||||||
wire_base >> 8, // app flash ends here; resident loader base (a word address on large chips)
|
|
||||||
avr::hw::db.mem.eeprom_size & 0xff,
|
|
||||||
avr::hw::db.mem.eeprom_size >> 8,
|
|
||||||
// bit 0: host must patch the reset vector (no hardware boot section);
|
|
||||||
// bit 1: flash wire addresses are word addresses
|
|
||||||
static_cast<std::uint8_t>((boot_section ? 0 : 1) | (word_flash ? 2 : 0)),
|
|
||||||
};
|
};
|
||||||
|
// clang-format on
|
||||||
|
// Where the identity proper starts: past the magic the host scans for.
|
||||||
|
constexpr std::uint8_t stamp_identity = 2;
|
||||||
|
|
||||||
// The serial link: the hardware USART where the chip has one, the polled
|
// The address spaces a transfer can name, in a selector byte's low nibble.
|
||||||
// software UART (no vector — the table belongs to the application) on PB0/PB1
|
// Flash is 0 so it is the cheapest to select.
|
||||||
// elsewhere. Both are class templates on the clock so only the selected
|
//
|
||||||
// backend is ever instantiated. pending() is the cheap line test the
|
// spm_ops is the one that is not memory: a write there hands its byte to
|
||||||
// activation window polls; rx() then picks the byte up; drain() holds until
|
// SPMCSR and fires the instruction at the transfer's address, which is how
|
||||||
// the last transmitted frame is fully on the wire (the jump hand-over must
|
// page erase, page write and RWW re-enable reach the wire without the loader
|
||||||
// not let the target's re-init clip the ack).
|
// carrying a command for each. The hardware's four-cycle store-to-SPM window
|
||||||
template <avr::hertz_t C>
|
// is why this is one fused primitive and not a poke of SPMCSR — no host can
|
||||||
consteval std::int16_t rxc_field()
|
// hit that window across a serial link.
|
||||||
|
enum : std::uint8_t { sp_flash = 0, sp_eeprom = 1, sp_data = 2, sp_fuse = 3, sp_spm = 4 };
|
||||||
|
|
||||||
|
// A selector's high nibble is the flash bank — the address bits above the
|
||||||
|
// 16-bit wire address, RAMPZ on the chips that have one. Keeping it here
|
||||||
|
// rather than widening the wire address is what lets one 16-bit cursor serve
|
||||||
|
// every space: a 24-bit cursor would pay its extra byte on EEPROM and data
|
||||||
|
// reads that can never need it.
|
||||||
|
[[gnu::always_inline]] inline std::uint8_t space_of(std::uint8_t selector)
|
||||||
{
|
{
|
||||||
return avr::uart::detail::ufield<'0', "UCSR#A", "RXC#">();
|
return selector & 0x0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
template <avr::hertz_t C>
|
[[gnu::always_inline]] inline std::uint8_t bank_of(std::uint8_t selector)
|
||||||
consteval std::int16_t txc_field()
|
|
||||||
{
|
{
|
||||||
return avr::uart::detail::ufield<'0', "UCSR#A", "TXC#">();
|
return static_cast<std::uint8_t>(selector >> 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
template <avr::hertz_t C>
|
// The slot a flash address falls in, as one byte. A slot is half as many words
|
||||||
consteval std::int16_t status_reg()
|
// as bytes, so the word address's high byte is exactly this index — which is
|
||||||
|
// what lets the write guard compare a single byte, and what the running copy's
|
||||||
|
// own return address yields for free.
|
||||||
|
constexpr std::uint8_t slot_shift = std::countr_zero(slot_bytes);
|
||||||
|
constexpr std::uint8_t bank_shift = 16 - slot_shift;
|
||||||
|
|
||||||
|
[[gnu::always_inline]] inline std::uint8_t slot_of([[maybe_unused]] std::uint8_t bank, std::uint16_t at)
|
||||||
{
|
{
|
||||||
return avr::uart::detail::ureg<'0', "UCSR#A">();
|
const auto within = static_cast<std::uint8_t>(at >> slot_shift);
|
||||||
|
if constexpr (banked_flash)
|
||||||
|
return static_cast<std::uint8_t>((bank << bank_shift) | within);
|
||||||
|
else
|
||||||
|
return within;
|
||||||
}
|
}
|
||||||
|
|
||||||
template <avr::hertz_t C>
|
// The serial link, per the build's PUREBOOT_USART / PUREBOOT_SOFT_SERIAL /
|
||||||
|
// PUREBOOT_AUTOBAUD, defaulting to the chip's USART0 where it has one. The
|
||||||
|
// software receiver is the polled one: the vector table belongs to the
|
||||||
|
// application. Templates on the clock, so only the selected backend
|
||||||
|
// instantiates. pending() is the cheap line test the activation window polls;
|
||||||
|
// drain() holds until the last frame is off the wire, so a hand-over cannot
|
||||||
|
// let the target's re-init clip the ack.
|
||||||
|
#if defined(PUREBOOT_SOFT_SERIAL) && defined(PUREBOOT_USART)
|
||||||
|
#error "PUREBOOT_SOFT_SERIAL and PUREBOOT_USART select opposing serial backends"
|
||||||
|
#endif
|
||||||
|
#if defined(PUREBOOT_AUTOBAUD) && defined(PUREBOOT_USART)
|
||||||
|
#error "PUREBOOT_AUTOBAUD measures a software link; it cannot drive a hardware USART"
|
||||||
|
#endif
|
||||||
|
#if !defined(PUREBOOT_RX)
|
||||||
|
#define PUREBOOT_RX pb0
|
||||||
|
#endif
|
||||||
|
#if !defined(PUREBOOT_TX)
|
||||||
|
#define PUREBOOT_TX pb1
|
||||||
|
#endif
|
||||||
|
#if defined(PUREBOOT_USART)
|
||||||
|
constexpr char usart_digit = '0' + PUREBOOT_USART;
|
||||||
|
#else
|
||||||
|
constexpr char usart_digit = '0';
|
||||||
|
#endif
|
||||||
|
|
||||||
|
template <avr::hertz_t C, avr::baud_t B>
|
||||||
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 = B, .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 +181,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 +196,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, avr::baud_t B>
|
||||||
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, B>;
|
||||||
using tx_t = avr::uart::software_tx<C, avr::pb1, 57600_Bd>;
|
using tx_t = avr::uart::software_tx<C, avr::PUREBOOT_TX, B>;
|
||||||
|
|
||||||
// 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 +216,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,15 +235,50 @@ struct software_link {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
using link = std::conditional_t<avr::hw::db.has_instance("USART0") || avr::hw::db.has_instance("USART"),
|
// The clock-free link: the bit period is measured from the host's calibration
|
||||||
hardware_link<dev::clock>, software_link<dev::clock>>;
|
// pulse instead of derived from a clock, so one image serves every F_CPU and
|
||||||
|
// every rate. Activation differs in kind from the other two — there is no
|
||||||
|
// clock to time a window against — so this backend brings its own, below.
|
||||||
|
struct autobaud_link {
|
||||||
|
using uart = avr::uart::software_autobaud<avr::PUREBOOT_RX, avr::PUREBOOT_TX>;
|
||||||
|
|
||||||
// The application's entry, an absolute address the linker pins (--defsym in
|
static void init()
|
||||||
// CMakeLists.txt): 0x0000 on the mega (word 0 stays the application's own
|
{
|
||||||
// vector — BOOTRST re-vectors a reset into the loader in hardware) and the
|
avr::init<uart>();
|
||||||
// trampoline word at base - 2 on the tinies. Reaching it must not depend on
|
}
|
||||||
// where this copy runs, so the jump goes through a pointer: [[gnu::noipa]]
|
|
||||||
// keeps the constant from folding back into a PC-relative call.
|
static std::uint8_t rx()
|
||||||
|
{
|
||||||
|
return uart::template read<off>();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void tx(std::uint8_t byte)
|
||||||
|
{
|
||||||
|
uart::template write<off>(byte);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void drain()
|
||||||
|
{
|
||||||
|
uart::drain();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
#if defined(PUREBOOT_AUTOBAUD)
|
||||||
|
using link = autobaud_link;
|
||||||
|
#elif defined(PUREBOOT_USART)
|
||||||
|
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, wire_baud>;
|
||||||
|
#elif defined(PUREBOOT_SOFT_SERIAL)
|
||||||
|
using link = software_link<dev::clock, wire_baud>;
|
||||||
|
#else
|
||||||
|
using link = std::conditional_t<avr::uart::has_usart<usart_digit>(), hardware_link<dev::clock, wire_baud>,
|
||||||
|
software_link<dev::clock, wire_baud>>;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// The application's entry, pinned by the linker (--defsym): word 0 on a
|
||||||
|
// boot-sectioned mega, the trampoline at base − 2 elsewhere. Reaching it must
|
||||||
|
// not depend on where this copy runs, so the jump goes through a pointer, and
|
||||||
|
// [[gnu::noipa]] keeps the constant from folding back into a relative call.
|
||||||
extern "C" [[noreturn]] void pureboot_app();
|
extern "C" [[noreturn]] void pureboot_app();
|
||||||
|
|
||||||
[[gnu::noipa, noreturn]] void jump(void (*target)())
|
[[gnu::noipa, noreturn]] void jump(void (*target)())
|
||||||
@@ -242,10 +292,29 @@ extern "C" [[noreturn]] void pureboot_app();
|
|||||||
jump(pureboot_app);
|
jump(pureboot_app);
|
||||||
}
|
}
|
||||||
|
|
||||||
// One activation window is a single 32-bit poll countdown. The divisor is
|
// Activation: a bounded wait for the host, then the knock. Both forms boot the
|
||||||
// the backend's counted poll-loop cycles (its own comment reads them off the
|
// application when the window closes on an idle line, and both bound *every*
|
||||||
// compiled loop); whole-second precision is all the window promises, so the
|
// wait — a knock awaited without a deadline would let one stray edge hold an
|
||||||
// nearest cycle count is plenty.
|
// unattended device in the loader forever.
|
||||||
|
#if defined(PUREBOOT_AUTOBAUD)
|
||||||
|
// The window is a fixed poll budget: with no clock, whole seconds cannot be
|
||||||
|
// timed. A uint24_t holds it — a fourth byte would cost two words at every
|
||||||
|
// countdown step for range never used.
|
||||||
|
void await_host()
|
||||||
|
{
|
||||||
|
for (;;) {
|
||||||
|
if (!link::uart::calibrate(autobaud_budget))
|
||||||
|
run_app();
|
||||||
|
// The calibration pulse has already proven a host is there, so one
|
||||||
|
// byte activates. A knock that never arrives falls back to calibrate(),
|
||||||
|
// whose own budget then boots the application.
|
||||||
|
if (link::uart::template read<off>(autobaud_budget) == 'p')
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
// The window as one 32-bit countdown, divided by the backend's counted
|
||||||
|
// poll-loop cycles. Whole seconds is all it promises.
|
||||||
consteval std::uint32_t window_polls()
|
consteval std::uint32_t window_polls()
|
||||||
{
|
{
|
||||||
return timeout_seconds * static_cast<std::uint32_t>(dev::clock.hz / link::poll_cycles);
|
return timeout_seconds * static_cast<std::uint32_t>(dev::clock.hz / link::poll_cycles);
|
||||||
@@ -261,8 +330,8 @@ bool pending_before_deadline()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A knock byte under the activation deadline: an idle line means no host is
|
// A knock byte under the deadline: an idle window means no host, so the
|
||||||
// there, and the application runs.
|
// application runs.
|
||||||
std::uint8_t rx_deadline()
|
std::uint8_t rx_deadline()
|
||||||
{
|
{
|
||||||
if (!pending_before_deadline())
|
if (!pending_before_deadline())
|
||||||
@@ -270,209 +339,187 @@ std::uint8_t rx_deadline()
|
|||||||
return link::rx();
|
return link::rx();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::uint16_t rx16()
|
void await_host()
|
||||||
|
{
|
||||||
|
// 'p' then 'b', each under a fresh window; anything else is line noise.
|
||||||
|
while (rx_deadline() != 'p' || rx_deadline() != 'b') {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Inlined: read across a call, the first byte strands in a call-saved
|
||||||
|
// register the caller has to push and pop.
|
||||||
|
[[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 wire's byte pair as the word it is — AVR is little-endian too, so the
|
||||||
// send_flash stays out of line: its two callers ('b' and 'R') otherwise each
|
// cast is the identity a shift-and-or spelling makes the compiler rediscover.
|
||||||
// inline a private copy of the loop. On the large chips the address is a
|
// Callers read into named variables first: the wire order is a sequence of
|
||||||
// word address and the read goes through ELPM (flash_load_far).
|
// reads, not an argument order.
|
||||||
[[gnu::noinline]] void send_flash(std::uint16_t address, std::uint8_t count)
|
[[gnu::always_inline]] inline std::uint16_t word_of(std::array<std::uint8_t, 2> pair)
|
||||||
{
|
{
|
||||||
if constexpr (word_flash) {
|
return std::bit_cast<std::uint16_t>(pair);
|
||||||
// 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
|
|
||||||
link::tx(avr::flash_load(reinterpret_cast<const std::uint8_t *>(address++)));
|
|
||||||
while (--count);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void send_eeprom(std::uint16_t address, std::uint8_t count)
|
// Out of line: several sites send it, and a call is shorter than a
|
||||||
|
// load-immediate at each.
|
||||||
|
[[gnu::noinline]] void tx_ack()
|
||||||
{
|
{
|
||||||
do
|
|
||||||
link::tx(ee::read(address++));
|
|
||||||
while (--count);
|
|
||||||
}
|
|
||||||
|
|
||||||
// EEPROM write, host-paced: each ack goes out once the byte's write has
|
|
||||||
// begun, so the next byte arrives while it completes and the following
|
|
||||||
// write's own ready-wait sees an idle line. Nothing is ever missed, on
|
|
||||||
// either serial backend, without a buffer.
|
|
||||||
void store_eeprom(std::uint16_t address, std::uint8_t count)
|
|
||||||
{
|
|
||||||
do {
|
|
||||||
ee::write<off>(address++, link::rx());
|
|
||||||
link::tx(ack);
|
link::tx(ack);
|
||||||
} while (--count);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// One flash page: stream the bytes into the SPM buffer as little-endian
|
// A wire address and its selector's bank as the flash address they name.
|
||||||
// words, then erase and program — except the 512-byte slot this code runs
|
[[gnu::always_inline]] inline spm::flash_address_t flash_address([[maybe_unused]] std::uint8_t bank, std::uint16_t at)
|
||||||
// in, which is drained but never programmed, so a copy can never erase
|
|
||||||
// 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
|
|
||||||
// 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
|
|
||||||
// immediately.
|
|
||||||
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
|
if constexpr (banked_flash)
|
||||||
// refused page's drained data must not linger for the next write:
|
return (static_cast<spm::flash_address_t>(bank) << 16) | at;
|
||||||
// discard the buffer up front — CTPB on the tinies; on the megas
|
|
||||||
// writing RWWSRE aborts a pending load (§26.2.2 — on the m48s that
|
|
||||||
// flush is the bit's whole documented job).
|
|
||||||
if constexpr (rww_discard)
|
|
||||||
spm::rww_enable<off>();
|
|
||||||
else
|
else
|
||||||
spm::clear_buffer<off>();
|
return at;
|
||||||
// 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
|
|
||||||
// the word-addressed large chips the wire word address becomes a 32-bit
|
// One byte out of any space. Every accessor shares the transfer's cursor, its
|
||||||
// byte cursor once, and their 256-byte page makes its low byte the whole
|
// loop and its call site, so a space costs only its own instruction rather
|
||||||
// in-page offset. The slot index is one high byte of the wire address —
|
// than a body, a loop and a dispatch arm of its own.
|
||||||
// two values on byte-addressed chips (the & ~1), bits 16:9 re-packed on
|
[[gnu::always_inline]] inline std::uint8_t load(std::uint8_t space, [[maybe_unused]] std::uint8_t bank,
|
||||||
// the large ones.
|
std::uint16_t at)
|
||||||
spm::flash_address_t address;
|
{
|
||||||
std::uint8_t page_high;
|
if (space == sp_eeprom)
|
||||||
if constexpr (word_flash) {
|
return ee::read(at);
|
||||||
// Pages are aligned, so one page never crosses a 64 KiB boundary:
|
if (space == sp_data)
|
||||||
// RAMPZ is a per-page constant and the fill cursor is a 16-bit Z
|
return *reinterpret_cast<volatile std::uint8_t *>(at);
|
||||||
// whose low byte is the whole in-page offset (256-byte pages). The
|
if (space == sp_fuse)
|
||||||
// slot index is simply the wire word address's high byte.
|
return spm::read_fuse<off>(static_cast<spm::fuse>(at));
|
||||||
const std::uint8_t rampz = static_cast<std::uint8_t>(wire_address >> 15);
|
if constexpr (banked_flash)
|
||||||
const std::uint16_t z0 = static_cast<std::uint16_t>(wire_address << 1);
|
return avr::flash_load_far<std::uint8_t>(flash_address(bank, at));
|
||||||
std::uint16_t z = z0;
|
else
|
||||||
do {
|
return avr::flash_load(reinterpret_cast<const std::uint8_t *>(at));
|
||||||
std::uint8_t low = link::rx();
|
}
|
||||||
std::uint8_t high = link::rx();
|
|
||||||
spm::fill<off>((static_cast<spm::flash_address_t>(rampz) << 16) | z,
|
// One byte into a writable space. Flash is not one of them — it arrives a
|
||||||
static_cast<std::uint16_t>(low | (high << 8)));
|
// page at a time through 'W' and is committed through sp_spm — and the fuses
|
||||||
z += 2;
|
// are not writable at all: SPM reaches flash and boot lock bits only.
|
||||||
} while (static_cast<std::uint8_t>(z));
|
[[gnu::always_inline]] inline void store(std::uint8_t space, std::uint8_t bank, std::uint16_t at, std::uint8_t value,
|
||||||
address = (static_cast<spm::flash_address_t>(rampz) << 16) | z0;
|
std::uint8_t slot_high)
|
||||||
page_high = static_cast<std::uint8_t>(wire_address >> 8) & 0xfe;
|
{
|
||||||
} else {
|
if (space == sp_data) {
|
||||||
address = static_cast<spm::flash_address_t>(wire_address);
|
*reinterpret_cast<volatile std::uint8_t *>(at) = value;
|
||||||
do {
|
return;
|
||||||
std::uint8_t low = link::rx();
|
|
||||||
std::uint8_t high = link::rx();
|
|
||||||
spm::fill<off>(address, static_cast<std::uint16_t>(low | (high << 8)));
|
|
||||||
address += 2;
|
|
||||||
} while (static_cast<std::uint8_t>(address) & (page - 1));
|
|
||||||
address -= 2; // back inside the page — erase and write ignore the word bits
|
|
||||||
page_high = static_cast<std::uint8_t>(address >> 8) & 0xfe;
|
|
||||||
}
|
}
|
||||||
if (page_high != slot_high) {
|
if (space == sp_spm) {
|
||||||
// The tinies and the m48s halt the CPU through the erase and the
|
// The running-slot write guard. An SPM command aimed at the slot this
|
||||||
// write, so only the boot-sectioned megas — running on while their
|
// code executes from is dropped, so a broken host cannot brick the
|
||||||
// RWW section programs — wait.
|
// running loader — while a copy one slot lower may still rewrite the
|
||||||
spm::erase_page<off>(address);
|
// resident one, which is what a self-update is. Guarding the commit
|
||||||
|
// rather than the page fill covers erase and write both, and leaves a
|
||||||
|
// refused page's words in the buffer: harmless, since the next page
|
||||||
|
// write auto-erases it (§26.2.1).
|
||||||
|
if (slot_of(bank, at) != slot_high)
|
||||||
|
spm::command<off>(value, flash_address(bank, at));
|
||||||
|
// Only a boot-sectioned mega runs on while its RWW section programs;
|
||||||
|
// everywhere else the CPU halts through erase and write, so the wait
|
||||||
|
// is already over by the time it returns.
|
||||||
if constexpr (boot_section)
|
if constexpr (boot_section)
|
||||||
spm::wait();
|
spm::wait();
|
||||||
spm::write_page<off>(address);
|
return;
|
||||||
if constexpr (boot_section) {
|
|
||||||
spm::wait();
|
|
||||||
spm::rww_enable<off>();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// Host-paced: the ack goes out once the write has begun, so the next byte
|
||||||
|
// arrives while it completes and nothing is missed without a buffer.
|
||||||
|
ee::write<off>(at, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The four fuse/lock bytes in the hardware's own Z order: low, lock,
|
// One page into the SPM buffer, and only that: the erase and the write that
|
||||||
// extended, high. Writing fuses is not a thing self-programming can do on
|
// commit it are host-issued sp_spm stores, which reach the same fused
|
||||||
// AVR — SPM reaches flash (and boot lock bits) only.
|
// store-and-SPM pair through the transfer path's own address and data.
|
||||||
void send_fuses()
|
//
|
||||||
|
// Nothing discards the buffer first: it is write-once per word (§26.2.1), so
|
||||||
|
// filling over a refused page or an application's leavings programs stale
|
||||||
|
// words — but a page write auto-erases it (§26.2.1; §19.2 on the tinies), so
|
||||||
|
// that write clears the condition and the host's read-back rewrites the page.
|
||||||
|
void fill_page(std::uint8_t bank, std::uint16_t at)
|
||||||
{
|
{
|
||||||
std::uint8_t which = 0;
|
// The address names a page, so its in-page bits are dropped and the walk
|
||||||
do
|
// starts at the page base; the low byte of the cursor is the whole in-page
|
||||||
link::tx(spm::read_fuse<off>(static_cast<spm::fuse>(which)));
|
// offset, since a page is aligned and never crosses a bank.
|
||||||
while (++which & 3);
|
std::uint16_t z = at & ~static_cast<std::uint16_t>(page - 1);
|
||||||
|
do {
|
||||||
|
std::uint8_t low = link::rx();
|
||||||
|
std::uint8_t high = link::rx();
|
||||||
|
spm::fill<off>(flash_address(bank, z), word_of({low, high}));
|
||||||
|
z += 2;
|
||||||
|
} while (static_cast<std::uint8_t>(z) & (page - 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
[[noreturn]] void run()
|
[[noreturn]] void run()
|
||||||
{
|
{
|
||||||
// A watchdog reset belongs to the application (whose watchdog stays
|
// A watchdog reset belongs to the application, whose watchdog stays forced
|
||||||
// forced on until it clears WDRF) — no activation window in its way.
|
// on until it clears WDRF — no activation window in its way.
|
||||||
// The flag register is MCUSR, or the classic megas' MCUCSR.
|
|
||||||
if (avr::hw::field_impl<wdrf_field()>::test())
|
if (avr::hw::field_impl<wdrf_field()>::test())
|
||||||
run_app();
|
run_app();
|
||||||
|
|
||||||
link::init();
|
link::init();
|
||||||
|
|
||||||
// The high byte of the 512-byte-aligned base this copy runs at: the
|
// The slot this copy runs in, which the write guard follows: the return
|
||||||
// return address is a word address, whose high byte is the 256-word slot
|
// address is a word address and a slot is half as many words as bytes, so
|
||||||
// index — on byte-addressed chips doubled back into byte terms.
|
// its high byte is the slot index outright. No absolute address is ever
|
||||||
// program_flash refuses this one slot and the info block is addressed
|
// formed, so the image stays position-independent.
|
||||||
// from it, so both follow wherever the code was flashed.
|
const auto return_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 auto slot_high = static_cast<std::uint8_t>(return_words >> 8);
|
||||||
const std::uint8_t slot_high =
|
|
||||||
word_flash ? static_cast<std::uint8_t>(ra_words >> 8) & 0xfe : static_cast<std::uint8_t>((ra_words >> 8) << 1);
|
|
||||||
|
|
||||||
// The knock: 'p' then 'b', each under a fresh window; any other byte is
|
await_host();
|
||||||
// line noise and waits again. Falling out of a window runs the app.
|
|
||||||
while (rx_deadline() != 'p' || rx_deadline() != 'b') {
|
|
||||||
}
|
|
||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
// No prompt while an EEPROM write runs: a pending write blocks SPM
|
// No prompt while an EEPROM write runs: it blocks SPM and fuse reads
|
||||||
// and fuse reads (§26.2.1), and the ack tells the host all is done.
|
// (§26.2.1), and the prompt is the previous command's completion ack.
|
||||||
ee::wait();
|
ee::wait();
|
||||||
link::tx(ack);
|
tx_ack();
|
||||||
const std::uint8_t command = link::rx();
|
const std::uint8_t command = link::rx();
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case 'b': { // info block, read relative to the running slot
|
|
||||||
// The block sits in the image's first 256 bytes (the build lint
|
|
||||||
// asserts it), and slots are 512-aligned — so the low byte of its
|
|
||||||
// link address (in wire units: bytes, or words on the large
|
|
||||||
// chips) is its offset in any slot, and the high byte of its
|
|
||||||
// runtime address is the running slot's. Built as a byte pair so
|
|
||||||
// no absolute address is ever materialized.
|
|
||||||
const auto link_low = reinterpret_cast<std::uint16_t>(info_data.data());
|
|
||||||
const std::uint8_t 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}),
|
|
||||||
static_cast<std::uint8_t>(info_data.size()));
|
|
||||||
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
|
||||||
auto target = reinterpret_cast<void (*)()>(rx16());
|
auto target = reinterpret_cast<void (*)()>(rx16());
|
||||||
link::tx(ack);
|
tx_ack();
|
||||||
link::drain();
|
link::drain();
|
||||||
jump(target);
|
jump(target);
|
||||||
}
|
}
|
||||||
case 'R': // read flash: addr16, n8 (0 = 256)
|
case 'b': // identity: the version, then the three signature bytes
|
||||||
case 'r': // read EEPROM: addr16, n8
|
// Straight out of the stamp, so the wire and the image can never
|
||||||
case 'w': { // write EEPROM: addr16, n8, then n bytes each acked
|
// disagree about what this loader is. The indices are constant and
|
||||||
std::uint16_t address = rx16();
|
// the array is constexpr, so these are immediates, not flash reads:
|
||||||
std::uint8_t count = link::rx();
|
// nothing here needs the stamp's runtime address.
|
||||||
if (command == 'R')
|
for (std::uint8_t at = stamp_identity; at != sizeof identity_stamp; ++at)
|
||||||
send_flash(address, count);
|
link::tx(identity_stamp[at]);
|
||||||
else if (command == 'r')
|
break;
|
||||||
send_eeprom(address, count);
|
case 'W': // fill one flash page buffer: sel8, addr16, then page bytes
|
||||||
else
|
case 'G': // read: sel8, addr16, n8 (0 = 256)
|
||||||
store_eeprom(address, count);
|
case 'g': { // write: sel8, addr16, n8, then n bytes, each acked
|
||||||
|
// One decode, one cursor and one loop for every space and both
|
||||||
|
// directions: a command per memory would carry a copy of all three
|
||||||
|
// each. 'W' joins the same decode rather than keeping an address
|
||||||
|
// form of its own, so flash addressing is uniform across every
|
||||||
|
// command that names it.
|
||||||
|
const std::uint8_t selector = link::rx();
|
||||||
|
const std::uint8_t space = space_of(selector);
|
||||||
|
const std::uint8_t bank = bank_of(selector);
|
||||||
|
std::uint16_t at = rx16();
|
||||||
|
if (command == 'W') {
|
||||||
|
fill_page(bank, at);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'W': // program one flash page: addr16, page bytes
|
std::uint8_t count = link::rx();
|
||||||
program_flash(rx16(), slot_high);
|
do {
|
||||||
break;
|
// Read and write are one letter apart in case, so the direction
|
||||||
case 'F': // fuse and lock bytes
|
// is a single bit and the loop picks it with a one-word skip.
|
||||||
send_fuses();
|
if (command & 0x20) {
|
||||||
|
store(space, bank, at, link::rx(), slot_high);
|
||||||
|
tx_ack();
|
||||||
|
} else
|
||||||
|
link::tx(load(space, bank, at));
|
||||||
|
++at;
|
||||||
|
} while (--count);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
default: // unknown bytes are ignored; the loop re-acks
|
default: // unknown bytes are ignored; the loop re-acks
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,13 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""pureboot host tool — the smart half of the pureboot protocol (README.md).
|
"""pureboot host tool — the smart half of the protocol (README.md).
|
||||||
|
|
||||||
The device exposes primitives; this tool composes them: image loading (raw
|
The device exposes primitives; everything composite is here: HEX/raw images,
|
||||||
binary or Intel HEX), flash programming with read-back verification, erase as
|
programming with repairing read-back verification, the reset-vector surgery
|
||||||
writing 0xff, EEPROM programming, fuse and info readout, the hand-over jump,
|
the boot-section-less chips need, and the self-update that stages the loader
|
||||||
and — on chips without a hardware boot section — the reset-vector surgery
|
one slot lower and lets it rewrite the resident.
|
||||||
that re-homes the application's entry through the trampoline word below the
|
|
||||||
loader. Page 0 and the trampoline are written first, so every interruption
|
|
||||||
point of a flash leaves the chip reset-recoverable into the loader.
|
|
||||||
|
|
||||||
It also updates the loader itself (--update-loader): pureboot's image is
|
Standard library only. The port is termios on POSIX and the Win32 serial API
|
||||||
position-independent, so the tool installs the identical binary one 512-byte
|
through ctypes on Windows, so any tty or COM port works.
|
||||||
slot below the resident loader, jumps into that staging copy, lets it rewrite
|
|
||||||
the resident slot, and restores what the staging slot held — resumable at
|
|
||||||
every phase from the flash state plus a host-side state file carrying the
|
|
||||||
saved bytes.
|
|
||||||
|
|
||||||
Python standard library only; the serial port is driven with termios on POSIX
|
|
||||||
and the Win32 serial API (through ctypes) on Windows, so any tty or COM port
|
|
||||||
works — a USB adapter as well as a simavr pty.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -35,14 +24,122 @@ else:
|
|||||||
import termios
|
import termios
|
||||||
|
|
||||||
PROMPT = b"+"
|
PROMPT = b"+"
|
||||||
PROTOCOL_VERSION = 1
|
VERSION = 4 # this tool's own version — free to drift from a loader's
|
||||||
SLOT = 512 # the loader slot on byte-addressed chips; word-addressed ones (>64 KiB) use 1 KiB — their own smallest boot sector
|
# The loader versions this tool speaks. A pureboot version implies its wire
|
||||||
|
# protocol, which carries no number of its own, so this window is where that
|
||||||
|
# map lives: every version so far speaks the same protocol, and one that
|
||||||
|
# changes it becomes the new floor here.
|
||||||
|
OLDEST_LOADER = 1
|
||||||
|
NEWEST_LOADER = 5
|
||||||
|
SLOT = 512 # the loader slot, on every chip
|
||||||
|
RETRIES = 3 # rewrites of a page that reads back wrong, before the run stops
|
||||||
|
|
||||||
|
# pureboot 5 replaced the four per-memory commands with one pair: 'G' reads and
|
||||||
|
# 'g' writes, each taking a selector byte, a 16-bit address and a count, over
|
||||||
|
# the spaces below. The loader carries one transfer loop instead of four bodies
|
||||||
|
# — which is what buys the data space and the host-issued SPM operations.
|
||||||
|
UNIFIED_LOADER = 5
|
||||||
|
SP_FLASH, SP_EEPROM, SP_RAM, SP_FUSE, SP_SPM = 0, 1, 2, 3, 4
|
||||||
|
|
||||||
|
# A selector's high nibble is the flash bank — the address bits above the 16-bit
|
||||||
|
# wire address — so a transfer names a byte address within one 64 KiB bank and
|
||||||
|
# no command has to speak word addresses. No single transfer may cross a bank
|
||||||
|
# boundary; the host chunks to keep that true.
|
||||||
|
def selector(space, address):
|
||||||
|
return space | ((address >> 16) << 4)
|
||||||
|
|
||||||
|
|
||||||
|
# The SPM operations pureboot 5 leaves to the host: a write to SP_SPM hands its
|
||||||
|
# byte to SPMCSR and fires the instruction at the selected flash address. Every
|
||||||
|
# part pureboot targets agrees on these encodings.
|
||||||
|
SPM_ERASE, SPM_WRITE, SPM_RWWSRE = 0x03, 0x05, 0x11
|
||||||
|
|
||||||
|
# Calibration byte for an autobaud loader: 0xC0 is a start bit plus six zero
|
||||||
|
# data bits — one low pulse of seven bit-times, which the loader times into its
|
||||||
|
# per-bit unit. Sent at whatever baud the host chose; the loader locks to it.
|
||||||
|
CALIBRATE = 0xC0
|
||||||
|
|
||||||
|
# pureboot 5 answers 'b' with its version and the chip signature; the host
|
||||||
|
# derives the rest of the geometry from the signature rather than reading a
|
||||||
|
# table off the device. flash, page, eeprom, patch-vector per distinct
|
||||||
|
# signature, over every chip pureboot targets (the loader computes the same
|
||||||
|
# from its chip database at build time). Die revisions that share a signature
|
||||||
|
# share this row, as they share the silicon.
|
||||||
|
CHIP_GEOMETRY = {
|
||||||
|
# signature : (flash, page, eeprom, patch_vector)
|
||||||
|
(0x1E, 0x90, 0x07): (1024, 32, 64, True), # ATtiny13/13A
|
||||||
|
(0x1E, 0x91, 0x08): (2048, 32, 128, True), # ATtiny25
|
||||||
|
(0x1E, 0x92, 0x06): (4096, 64, 256, True), # ATtiny45
|
||||||
|
(0x1E, 0x93, 0x0B): (8192, 64, 512, True), # ATtiny85
|
||||||
|
(0x1E, 0x92, 0x05): (4096, 64, 256, True), # ATmega48/48A
|
||||||
|
(0x1E, 0x92, 0x0A): (4096, 64, 256, True), # ATmega48P/48PA
|
||||||
|
(0x1E, 0x93, 0x07): (8192, 64, 512, False), # ATmega8/8A
|
||||||
|
(0x1E, 0x93, 0x0A): (8192, 64, 512, False), # ATmega88/88A
|
||||||
|
(0x1E, 0x93, 0x0F): (8192, 64, 512, False), # ATmega88P/88PA
|
||||||
|
(0x1E, 0x94, 0x03): (16384, 128, 512, False), # ATmega16/16A
|
||||||
|
(0x1E, 0x94, 0x06): (16384, 128, 512, False), # ATmega168/168A
|
||||||
|
(0x1E, 0x94, 0x0B): (16384, 128, 512, False), # ATmega168P/168PA
|
||||||
|
(0x1E, 0x94, 0x0A): (16384, 128, 512, False), # ATmega164P/164PA
|
||||||
|
(0x1E, 0x94, 0x0F): (16384, 128, 512, False), # ATmega164A
|
||||||
|
(0x1E, 0x95, 0x02): (32768, 128, 1024, False), # ATmega32/32A
|
||||||
|
(0x1E, 0x95, 0x0F): (32768, 128, 1024, False), # ATmega328P
|
||||||
|
(0x1E, 0x95, 0x14): (32768, 128, 1024, False), # ATmega328
|
||||||
|
(0x1E, 0x95, 0x08): (32768, 128, 1024, False), # ATmega324P
|
||||||
|
(0x1E, 0x95, 0x11): (32768, 128, 1024, False), # ATmega324PA
|
||||||
|
(0x1E, 0x95, 0x15): (32768, 128, 1024, False), # ATmega324A
|
||||||
|
(0x1E, 0x96, 0x09): (65536, 256, 2048, False), # ATmega644/644A
|
||||||
|
(0x1E, 0x96, 0x0A): (65536, 256, 2048, False), # ATmega644P/644PA
|
||||||
|
(0x1E, 0x97, 0x05): (131072, 256, 4096, False),# ATmega1284P
|
||||||
|
(0x1E, 0x97, 0x06): (131072, 256, 4096, False),# ATmega1284
|
||||||
|
}
|
||||||
|
|
||||||
|
VERBOSE = False
|
||||||
|
|
||||||
|
|
||||||
|
def verbose(message):
|
||||||
|
if VERBOSE:
|
||||||
|
print(f" {message}")
|
||||||
|
|
||||||
|
|
||||||
class Error(Exception):
|
class Error(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Progress:
|
||||||
|
"""A transient bar on stderr, drawn only for a tty and erased when done —
|
||||||
|
logs and pipes see only the summary line each operation prints. No label
|
||||||
|
or a zero total disables it, so callers can pass one 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 ---
|
||||||
|
|
||||||
|
|
||||||
@@ -263,27 +360,62 @@ Port = WindowsPort if os.name == "nt" else PosixPort
|
|||||||
class Info:
|
class Info:
|
||||||
"""The 12-byte info block."""
|
"""The 12-byte info block."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_identity(cls, raw):
|
||||||
|
"""pureboot 5's reply: the version and the chip signature. The rest of
|
||||||
|
the geometry is looked up from the signature — the loader derived the
|
||||||
|
same facts from its chip database at build time, so nothing is guessed,
|
||||||
|
it is simply not sent. Reconstructs a block in the older layout, so
|
||||||
|
every derived attribute below is shared with the loaders that do send
|
||||||
|
one.
|
||||||
|
|
||||||
|
The base is where application flash ends, which is a property of the
|
||||||
|
chip and not of the copy answering: a loader staged one slot lower
|
||||||
|
reports the same geometry the resident one does, exactly as the loaders
|
||||||
|
that send a block do. Which slot a copy runs in matters only to its own
|
||||||
|
write guard, which is the loader's business."""
|
||||||
|
if len(raw) != 4:
|
||||||
|
raise Error(f"bad identity reply: {raw.hex()}")
|
||||||
|
version, signature = raw[0], tuple(raw[1:4])
|
||||||
|
geometry = CHIP_GEOMETRY.get(signature)
|
||||||
|
if geometry is None:
|
||||||
|
sig = " ".join(f"{b:02x}" for b in signature)
|
||||||
|
raise Error(f"unknown signature {sig} — this tool has no geometry for it")
|
||||||
|
flash, page, eeprom, patch = geometry
|
||||||
|
base = flash - SLOT
|
||||||
|
word_flash = flash > 0x10000
|
||||||
|
wire_base = base // 2 if word_flash else base
|
||||||
|
flags = (1 if patch else 0) | (2 if word_flash else 0)
|
||||||
|
raw12 = bytes((ord("P"), ord("B"), version, *signature, page & 0xFF,
|
||||||
|
wire_base & 0xFF, wire_base >> 8, eeprom & 0xFF, eeprom >> 8, flags))
|
||||||
|
return cls(raw12)
|
||||||
|
|
||||||
def __init__(self, raw):
|
def __init__(self, raw):
|
||||||
if len(raw) != 12 or raw[0:2] != b"PB":
|
if len(raw) != 12 or raw[0:2] != b"PB":
|
||||||
raise Error(f"bad info block: {raw.hex()}")
|
raise Error(f"bad info block: {raw.hex()}")
|
||||||
if raw[2] != PROTOCOL_VERSION:
|
self.version = raw[2]
|
||||||
raise Error(f"protocol version {raw[2]}, tool speaks {PROTOCOL_VERSION}")
|
if not OLDEST_LOADER <= self.version <= NEWEST_LOADER:
|
||||||
|
raise Error(
|
||||||
|
f"pureboot {self.version}: this tool (version {VERSION}) speaks pureboot "
|
||||||
|
f"{OLDEST_LOADER}..{NEWEST_LOADER} — a newer loader needs a newer tool"
|
||||||
|
)
|
||||||
self.raw = bytes(raw)
|
self.raw = bytes(raw)
|
||||||
self.signature = raw[3:6]
|
self.signature = raw[3:6]
|
||||||
self.page = raw[6] or 256 # the wire count convention: 0 means 256
|
self.page = raw[6] or 256 # the wire count convention: 0 means 256
|
||||||
self.patch_vector = bool(raw[11] & 1)
|
self.patch_vector = bool(raw[11] & 1)
|
||||||
# Large chips speak word addresses for flash (bit 1); the host keeps
|
# Bit 1: the flash runs past what one 16-bit address covers. Through
|
||||||
# every address in bytes and converts at the wire.
|
# pureboot 4 that made flash addresses words on the wire; pureboot 5
|
||||||
|
# keeps them bytes and carries the bank in the selector instead. Every
|
||||||
|
# address in this tool stays a byte address either way and converts at
|
||||||
|
# the wire.
|
||||||
self.word_flash = bool(raw[11] & 2)
|
self.word_flash = bool(raw[11] & 2)
|
||||||
scale = 2 if self.word_flash else 1
|
scale = 2 if self.word_flash else 1
|
||||||
self.base = (raw[7] | (raw[8] << 8)) * scale
|
self.base = (raw[7] | (raw[8] << 8)) * scale
|
||||||
self.eeprom_size = raw[9] | (raw[10] << 8)
|
self.eeprom_size = raw[9] | (raw[10] << 8)
|
||||||
self.slot = 1024 if self.word_flash else SLOT
|
self.flash_size = self.base + SLOT
|
||||||
self.flash_size = self.base + self.slot
|
self.stage = self.base - SLOT # where a staging copy of the loader goes
|
||||||
self.stage = self.base - self.slot # where a staging copy of the loader goes
|
# The hand-over target as 'J' takes it: the trampoline below the
|
||||||
# The hand-over target, as the word address 'J' takes: the trampoline
|
# loader, or word 0 where BOOTRST re-vectors reset in hardware.
|
||||||
# below the loader (tinies), or word 0 (mega — the application's own
|
|
||||||
# reset vector; BOOTRST re-vectors a reset into the loader instead).
|
|
||||||
self.app_entry_word = (self.base - 2) // 2 if self.patch_vector else 0
|
self.app_entry_word = (self.base - 2) // 2 if self.patch_vector else 0
|
||||||
|
|
||||||
def describe(self):
|
def describe(self):
|
||||||
@@ -295,35 +427,96 @@ class Info:
|
|||||||
f"EEPROM {self.eeprom_size} B, {vector}"
|
f"EEPROM {self.eeprom_size} B, {vector}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def lines(self):
|
||||||
|
"""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"version pureboot {self.version}",
|
||||||
|
f"signature {' '.join(f'{b:02x}' for b in self.signature)}",
|
||||||
|
f"flash {self.flash_size} B, {self.page} B pages"
|
||||||
|
+ (", past one 16-bit bank" if self.word_flash else ""),
|
||||||
|
f"application 0x0000..{self.base - 1:#06x} ({self.base} B)",
|
||||||
|
f"loader {self.base:#06x} ({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 session. Between commands the loader has prompted and awaits a
|
||||||
awaits a command byte; every method restores that invariant — except
|
command byte; every method restores that, except jump() — after which the
|
||||||
jump(), after which the target must be knocked afresh."""
|
target must be knocked afresh."""
|
||||||
|
|
||||||
def __init__(self, port):
|
def __init__(self, port):
|
||||||
self.port = port
|
self.port = port
|
||||||
self.info = None
|
self.info = None
|
||||||
|
# Set once a session is established over an autobaud link, so a
|
||||||
|
# re-entry after 'J' repeats the handshake that worked.
|
||||||
|
self.autobaud = False
|
||||||
|
|
||||||
def connect(self, wait):
|
def _read_identity(self):
|
||||||
"""Knock until the activation window answers, then read the info
|
"""The 'b' reply, in either of the two layouts a loader may send.
|
||||||
block. Also converges when the loader already sits in its command
|
pureboot 5 answers with its version and the signature; older loaders
|
||||||
loop: the knock bytes are ignored-or-executed there, and the drain
|
answer with a 12-byte block. The version byte cannot be mistaken for
|
||||||
absorbs whatever they produced."""
|
the older block's 'P', so four bytes are enough to tell them apart."""
|
||||||
self.port.flush_input()
|
head = self.port.read_exact(4, 2.0)
|
||||||
|
if head[0:2] == b"PB":
|
||||||
|
return Info(head + self.port.read_exact(8, 2.0))
|
||||||
|
return Info.from_identity(head)
|
||||||
|
|
||||||
|
def _handshake(self, wait, knock, what):
|
||||||
|
"""One activation, retried until the loader answers or the window
|
||||||
|
closes. The identity reply is what proves the loader is listening — a
|
||||||
|
prompt byte alone does not, since one left over from a previous session
|
||||||
|
can still be in the pipeline while the port opening resets the device
|
||||||
|
into a fresh window, where a command without its knock is discarded.
|
||||||
|
Each attempt is therefore the whole handshake. This also converges into
|
||||||
|
an already-live session: the knock bytes are ignored there and the
|
||||||
|
drain absorbs whatever they produced."""
|
||||||
deadline = time.monotonic() + wait
|
deadline = time.monotonic() + wait
|
||||||
|
knocks = 0
|
||||||
while True:
|
while True:
|
||||||
self.port.write(b"pb")
|
self.port.flush_input()
|
||||||
|
self.port.write(knock)
|
||||||
|
knocks += 1
|
||||||
if PROMPT in self.port.read_available(0.4):
|
if PROMPT in self.port.read_available(0.4):
|
||||||
break
|
|
||||||
if time.monotonic() > deadline:
|
|
||||||
raise Error("no answer — reset the device within its activation window")
|
|
||||||
while self.port.read_available(0.3):
|
while self.port.read_available(0.3):
|
||||||
pass
|
pass
|
||||||
self.port.write(b"b")
|
self.port.write(b"b")
|
||||||
self.info = Info(self.port.read_exact(12, 2.0))
|
try:
|
||||||
|
# A version the tool cannot speak is the loader's own
|
||||||
|
# answer, not a failed knock: Info reports it rather than
|
||||||
|
# sending the tool round the loop again.
|
||||||
|
self.info = self._read_identity()
|
||||||
|
except Error as failed:
|
||||||
|
if "pureboot" in str(failed):
|
||||||
|
raise
|
||||||
|
self.info = None
|
||||||
|
if self.info is not None:
|
||||||
self._expect_prompt()
|
self._expect_prompt()
|
||||||
|
verbose(f"loader answered {what} {knocks}; identity read")
|
||||||
return self.info
|
return self.info
|
||||||
|
if time.monotonic() > deadline:
|
||||||
|
raise Error("no answer — reset the device within its activation window")
|
||||||
|
|
||||||
|
def connect(self, wait):
|
||||||
|
"""Knock 'p' then 'b' and read the identity."""
|
||||||
|
return self._handshake(wait, b"pb", "knock")
|
||||||
|
|
||||||
|
def connect_autobaud(self, wait):
|
||||||
|
"""The autobaud handshake. In place of the p+b knock the host sends the
|
||||||
|
calibration pulse — one seven-bit-time low pulse at the host's chosen
|
||||||
|
baud, which the loader times into its per-bit unit — then a single 'p'
|
||||||
|
the loader decodes at the rate it just measured. A lost pulse, or a
|
||||||
|
knock landing while the loader is mid-frame, simply fails to answer and
|
||||||
|
leaves the measurement loop waiting for the next pulse, so the retry in
|
||||||
|
_handshake covers it."""
|
||||||
|
self.autobaud = True
|
||||||
|
return self._handshake(wait, bytes((CALIBRATE, ord("p"))), "calibration")
|
||||||
|
|
||||||
def _expect_prompt(self, timeout=2.0):
|
def _expect_prompt(self, timeout=2.0):
|
||||||
byte = self.port.read_exact(1, timeout)
|
byte = self.port.read_exact(1, timeout)
|
||||||
@@ -347,7 +540,58 @@ class Loader:
|
|||||||
count -= chunk
|
count -= chunk
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unified(self):
|
||||||
|
"""pureboot 5 and later: one 'G'/'g' pair over selector-named spaces."""
|
||||||
|
return self.info is not None and self.info.version >= UNIFIED_LOADER
|
||||||
|
|
||||||
|
def _read_space(self, space, address, count):
|
||||||
|
"""A run out of any space, chunked to 256 bytes and to bank bounds."""
|
||||||
|
data = b""
|
||||||
|
while count:
|
||||||
|
chunk = min(count, 256, 0x10000 - (address & 0xFFFF))
|
||||||
|
head = bytes((ord("G"), selector(space, address), address & 0xFF,
|
||||||
|
(address >> 8) & 0xFF, chunk & 0xFF))
|
||||||
|
data += self._command(head, chunk, 5.0)
|
||||||
|
address += chunk
|
||||||
|
count -= chunk
|
||||||
|
return data
|
||||||
|
|
||||||
|
def _write_space(self, space, address, data, progress=None):
|
||||||
|
"""A run into any space. Each byte is acked as its write begins — an
|
||||||
|
EEPROM cell and an SPM operation both need that pacing, and the ack is
|
||||||
|
what the loader sends in place of a completion status."""
|
||||||
|
offset = 0
|
||||||
|
while offset < len(data):
|
||||||
|
chunk = data[offset : offset + min(256, 0x10000 - (address & 0xFFFF))]
|
||||||
|
head = bytes((ord("g"), selector(space, address), address & 0xFF,
|
||||||
|
(address >> 8) & 0xFF, len(chunk) & 0xFF))
|
||||||
|
self.port.write(head)
|
||||||
|
for byte in chunk:
|
||||||
|
self.port.write(bytes((byte,)))
|
||||||
|
self._expect_prompt()
|
||||||
|
if progress:
|
||||||
|
progress.step()
|
||||||
|
self._expect_prompt() # the next command prompt
|
||||||
|
address += len(chunk)
|
||||||
|
offset += len(chunk)
|
||||||
|
|
||||||
|
def spm(self, operation, address):
|
||||||
|
"""One SPM operation at a flash address — the erase, write and RWW
|
||||||
|
re-enable that pureboot 4 ran inside 'W' and pureboot 5 leaves here."""
|
||||||
|
self._write_space(SP_SPM, address, bytes((operation,)))
|
||||||
|
|
||||||
|
def read_ram(self, address, count):
|
||||||
|
"""Data space: SRAM, and with it the register file and every I/O
|
||||||
|
register, which share the address space on AVR. New in pureboot 5."""
|
||||||
|
return self._read_space(SP_RAM, address, count)
|
||||||
|
|
||||||
|
def write_ram(self, address, data):
|
||||||
|
self._write_space(SP_RAM, address, data)
|
||||||
|
|
||||||
def read_flash(self, address, count):
|
def read_flash(self, address, count):
|
||||||
|
if self.unified:
|
||||||
|
return self._read_space(SP_FLASH, address, count)
|
||||||
if not self.info.word_flash:
|
if not self.info.word_flash:
|
||||||
return self._stream_read("R", address, count)
|
return self._stream_read("R", address, count)
|
||||||
# Word-addressed wire: widen to even bounds and never let one read
|
# Word-addressed wire: widen to even bounds and never let one read
|
||||||
@@ -365,15 +609,32 @@ class Loader:
|
|||||||
return data[address - start : address - start + count]
|
return data[address - start : address - start + count]
|
||||||
|
|
||||||
def read_eeprom(self, address, count):
|
def read_eeprom(self, address, count):
|
||||||
|
if self.unified:
|
||||||
|
return self._read_space(SP_EEPROM, address, count)
|
||||||
return self._stream_read("r", address, count)
|
return self._stream_read("r", address, count)
|
||||||
|
|
||||||
def write_page(self, address, data):
|
def write_page(self, address, data):
|
||||||
assert len(data) == self.info.page and address % self.info.page == 0
|
assert len(data) == self.info.page and address % self.info.page == 0
|
||||||
|
if self.unified:
|
||||||
|
# 'W' fills the page buffer and stops there; the erase and the write
|
||||||
|
# are host-issued SPM operations. Only a chip with a boot section
|
||||||
|
# has RWW to re-enable — on the others bit 4 of SPMCSR means
|
||||||
|
# something else entirely, so it must not be sent.
|
||||||
|
head = bytes((ord("W"), selector(SP_FLASH, address), address & 0xFF, (address >> 8) & 0xFF))
|
||||||
|
self._command(head + data, 0, 2.0)
|
||||||
|
self.spm(SPM_ERASE, address)
|
||||||
|
self.spm(SPM_WRITE, address)
|
||||||
|
if not self.info.patch_vector:
|
||||||
|
self.spm(SPM_RWWSRE, address)
|
||||||
|
return
|
||||||
wire = address // (2 if self.info.word_flash else 1)
|
wire = address // (2 if self.info.word_flash else 1)
|
||||||
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):
|
||||||
|
if self.unified:
|
||||||
|
self._write_space(SP_EEPROM, address, data, progress)
|
||||||
|
return
|
||||||
offset = 0
|
offset = 0
|
||||||
while offset < len(data):
|
while offset < len(data):
|
||||||
chunk = data[offset : offset + 256]
|
chunk = data[offset : offset + 256]
|
||||||
@@ -382,26 +643,28 @@ 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)
|
||||||
|
|
||||||
def read_fuses(self):
|
def read_fuses(self):
|
||||||
|
if self.unified:
|
||||||
|
return self._read_space(SP_FUSE, 0, 4)
|
||||||
return self._command(b"F", 4, 2.0)
|
return self._command(b"F", 4, 2.0)
|
||||||
|
|
||||||
def jump(self, word_address):
|
def jump(self, word_address):
|
||||||
"""'J': the device acks, then execution continues at the word
|
"""The device acks, then execution continues at the word address."""
|
||||||
address — a loader slot's base (whose copy must then be knocked
|
|
||||||
afresh) or the application entry."""
|
|
||||||
self.port.write(bytes((ord("J"), word_address & 0xFF, word_address >> 8)))
|
self.port.write(bytes((ord("J"), word_address & 0xFF, word_address >> 8)))
|
||||||
self._expect_prompt()
|
self._expect_prompt()
|
||||||
|
|
||||||
def enter_copy(self, byte_address, wait):
|
def enter_copy(self, byte_address, wait):
|
||||||
"""Jump into the loader copy at `byte_address` and knock it. Ending
|
"""Jump into the loader copy at `byte_address` and knock it — a slot
|
||||||
up in the copy addressed is guaranteed by construction: a jump to a
|
base is that copy's entry stub, so it can only land there."""
|
||||||
slot base lands in that slot's entry stub."""
|
autobaud = self.autobaud
|
||||||
self.jump(byte_address // 2)
|
self.jump(byte_address // 2)
|
||||||
return self.connect(wait)
|
return self.connect_autobaud(wait) if autobaud else self.connect(wait)
|
||||||
|
|
||||||
def run_application(self):
|
def run_application(self):
|
||||||
self.jump(self.info.app_entry_word)
|
self.jump(self.info.app_entry_word)
|
||||||
@@ -487,21 +750,22 @@ 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
|
||||||
|
|
||||||
|
|
||||||
def covered(pages, info, skip_blank):
|
def covered(pages, info, skip_blank):
|
||||||
"""Pages in programming order; optionally dropping all-0xff pages (sound
|
"""Pages in programming order, optionally dropping all-0xff ones (sound
|
||||||
only over erased flash) — never a load-bearing one.
|
only over erased flash, and never a load-bearing page).
|
||||||
|
|
||||||
With a patched vector (tinies), the patched page 0 goes first and the
|
A patched vector puts page 0 first and the trampoline page second, so from
|
||||||
trampoline page second: from the first write on, a reset lands in the
|
the first write on a reset lands in the loader and its fall-through on the
|
||||||
loader and the loader's own fall-through lands on the application entry,
|
application entry — every interruption point recoverable. A hardware boot
|
||||||
so every interruption point of the flash is recoverable. With a hardware
|
section re-vectors reset regardless; page 0 goes last there, which
|
||||||
boot section a reset re-vectors to the loader regardless; ascending
|
maximizes what an interrupted image retains."""
|
||||||
order, page 0 last, maximizes what an interrupted image retains."""
|
|
||||||
trampoline_page = info.base - info.page if info.patch_vector else None
|
trampoline_page = info.base - info.page if info.patch_vector else None
|
||||||
first = [0, trampoline_page] if info.patch_vector else []
|
first = [0, trampoline_page] if info.patch_vector else []
|
||||||
rest = [a for a in sorted(pages) if a not in first]
|
rest = [a for a in sorted(pages) if a not in first]
|
||||||
@@ -567,18 +831,35 @@ def mega_boot(info, fuse_bytes):
|
|||||||
|
|
||||||
|
|
||||||
def image_info(image):
|
def image_info(image):
|
||||||
"""The info block embedded in a pureboot binary, or None."""
|
"""What a pureboot binary says about itself, or None.
|
||||||
at = image.find(b"PB" + bytes((PROTOCOL_VERSION,)))
|
|
||||||
return Info(image[at : at + 12]) if 0 <= at <= len(image) - 12 else None
|
An update image is a bare slot: nothing about it names the chip it was
|
||||||
|
built for, and installing a foreign one bricks the target — so every
|
||||||
|
loader carries a stamp for this. Through pureboot 4 the stamp is the
|
||||||
|
12-byte info block the device also serves; pureboot 5 serves its identity
|
||||||
|
from immediates and carries a 6-byte stamp (magic, version, signature)
|
||||||
|
that only this exists for, from which the geometry is looked up exactly as
|
||||||
|
it is for a live device.
|
||||||
|
|
||||||
|
Searched once per known version, so the magic stays three selective bytes
|
||||||
|
rather than two that code could carry by chance."""
|
||||||
|
for version in range(OLDEST_LOADER, NEWEST_LOADER + 1):
|
||||||
|
at = image.find(b"PB" + bytes((version,)))
|
||||||
|
if at < 0:
|
||||||
|
continue
|
||||||
|
if version >= UNIFIED_LOADER:
|
||||||
|
if at <= len(image) - 6:
|
||||||
|
return Info.from_identity(image[at + 2 : at + 6])
|
||||||
|
elif at <= len(image) - 12:
|
||||||
|
return Info(image[at : at + 12])
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def loader_image(path):
|
def loader_image(path):
|
||||||
"""A loader update image, as the slot's own content. A raw binary is that
|
"""An update image as the slot's own content: a raw binary already is,
|
||||||
already; an Intel HEX links the loader at its base inside an otherwise
|
while a HEX carries the blank below the loader's base, which is peeled off
|
||||||
blank flash image, and load_image() anchors every image at zero, so the
|
here. The base comes from the image's own block, not the device's, so a
|
||||||
blank below the base is dropped here. The base comes from the image's own
|
foreign image survives intact for the preflight to reject by name."""
|
||||||
info block rather than the device's, so an image built for somewhere else
|
|
||||||
survives intact and the preflight can say so."""
|
|
||||||
image = load_image(path)
|
image = load_image(path)
|
||||||
embedded = image_info(image)
|
embedded = image_info(image)
|
||||||
if embedded and len(image) > embedded.base:
|
if embedded and len(image) > embedded.base:
|
||||||
@@ -587,18 +868,17 @@ def loader_image(path):
|
|||||||
|
|
||||||
|
|
||||||
def staging_content(image, info):
|
def staging_content(image, info):
|
||||||
"""The 512-byte staging-slot content: the image, padding, and — on
|
"""The staging slot's content: the image, padding, and — where the
|
||||||
chips whose hand-over jumps through the word below the resident loader —
|
hand-over jumps through the word below the resident — that word, which for
|
||||||
that word, which for a staging copy is the slot's own last word: an rjmp
|
a staging copy is its own last one. Composed as an rjmp to the resident,
|
||||||
to the resident base. The staging copy's fall-through and 'J'-free exit
|
so an abandoned staging copy still falls through into a loader."""
|
||||||
both land in a loader instead of garbage."""
|
budget = SLOT - 2 if info.patch_vector else SLOT
|
||||||
slot = info.slot
|
if len(image) > budget:
|
||||||
if len(image) > (slot - 2 if info.patch_vector else slot):
|
raise Error(f"loader image is {len(image)} B, the slot holds {budget}")
|
||||||
raise Error(f"loader image is {len(image)} B, the slot holds {slot - 2 if info.patch_vector else slot}")
|
content = bytearray(image) + bytearray([0xFF] * (SLOT - len(image)))
|
||||||
content = bytearray(image) + bytearray([0xFF] * (slot - len(image)))
|
|
||||||
if info.patch_vector:
|
if info.patch_vector:
|
||||||
through = rjmp_to((info.base - 2) // 2, info.base // 2, info.flash_size // 2)
|
through = rjmp_to((info.base - 2) // 2, info.base // 2, info.flash_size // 2)
|
||||||
content[slot - 2], content[slot - 1] = through & 0xFF, through >> 8
|
content[SLOT - 2], content[SLOT - 1] = through & 0xFF, through >> 8
|
||||||
return bytes(content)
|
return bytes(content)
|
||||||
|
|
||||||
|
|
||||||
@@ -606,7 +886,10 @@ def update_preflight(image, info, fuse_bytes):
|
|||||||
"""Errors and warnings before any flash is touched. Returns warnings."""
|
"""Errors and warnings before any flash is touched. Returns warnings."""
|
||||||
embedded = image_info(image)
|
embedded = image_info(image)
|
||||||
if embedded is None:
|
if embedded is None:
|
||||||
raise Error("no pureboot info block in the update image — not a pureboot binary?")
|
raise Error(
|
||||||
|
"no pureboot info block in the update image — not a pureboot binary, "
|
||||||
|
f"or a version this tool ({VERSION}) does not know"
|
||||||
|
)
|
||||||
if embedded.raw[3:] != info.raw[3:]:
|
if embedded.raw[3:] != info.raw[3:]:
|
||||||
raise Error(
|
raise Error(
|
||||||
f"update image is for another target: it declares "
|
f"update image is for another target: it declares "
|
||||||
@@ -621,7 +904,7 @@ def update_preflight(image, info, fuse_bytes):
|
|||||||
raise Error(
|
raise Error(
|
||||||
f"cannot self-update: the staging slot {info.stage:#06x} lies below the "
|
f"cannot self-update: the staging slot {info.stage:#06x} lies below the "
|
||||||
f"boot section ({bls_start:#06x}) where SPM is disabled "
|
f"boot section ({bls_start:#06x}) where SPM is disabled "
|
||||||
f"— a boot section of at least two slots ({2 * info.slot} B, BOOTSZ) is "
|
f"— a boot section of at least two slots ({2 * SLOT} B, BOOTSZ) is "
|
||||||
f"required, and only an external programmer can change fuses"
|
f"required, and only an external programmer can change fuses"
|
||||||
)
|
)
|
||||||
if not bootrst:
|
if not bootrst:
|
||||||
@@ -663,7 +946,7 @@ class UpdateState:
|
|||||||
self.data = {
|
self.data = {
|
||||||
"signature": info.signature.hex(),
|
"signature": info.signature.hex(),
|
||||||
"base": info.base,
|
"base": info.base,
|
||||||
"staging": loader.read_flash(info.stage, info.slot).hex(),
|
"staging": loader.read_flash(info.stage, SLOT).hex(),
|
||||||
"page0": loader.read_flash(0, info.page).hex() if info.patch_vector else "",
|
"page0": loader.read_flash(0, info.page).hex() if info.patch_vector else "",
|
||||||
}
|
}
|
||||||
with open(self.path, "w") as f:
|
with open(self.path, "w") as f:
|
||||||
@@ -681,26 +964,46 @@ 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, so a
|
||||||
idempotent, so a resumed phase redoes only what an interruption left."""
|
resumed phase redoes only what an interruption left."""
|
||||||
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")
|
||||||
|
# The same bounded repair as verify_pages: here 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
|
||||||
|
|
||||||
|
|
||||||
def patch_word0(loader, page0, target_base):
|
def patch_word0(loader, page0, target_base):
|
||||||
"""Rewrite page 0 with its word 0 re-aimed at `target_base` — the
|
"""Re-aim word 0 at `target_base` — the resume insurance around
|
||||||
resume insurance around rewriting a loader slot the reset path uses."""
|
rewriting a loader slot the reset path goes through."""
|
||||||
info = loader.info
|
info = loader.info
|
||||||
patched = bytearray(page0)
|
patched = bytearray(page0)
|
||||||
word = rjmp_to(0, target_base // 2, info.flash_size // 2)
|
word = rjmp_to(0, target_base // 2, info.flash_size // 2)
|
||||||
@@ -710,62 +1013,82 @@ def patch_word0(loader, page0, target_base):
|
|||||||
|
|
||||||
|
|
||||||
def op_update_loader(loader, wait, path, state_path, fuse_bytes):
|
def op_update_loader(loader, wait, path, state_path, fuse_bytes):
|
||||||
"""Replace the resident loader with `path`, using the loader itself as
|
"""Replace the resident loader with `path`, using the loader as its own
|
||||||
its own staging loader. Every phase is idempotent and keyed off the
|
staging loader. Every phase is idempotent and keyed off the flash state,
|
||||||
actual flash state, so a re-run after any interruption resumes; the
|
so a re-run resumes; the state file carries what the staging slot held."""
|
||||||
state file carries the bytes the staging slot held."""
|
|
||||||
info = loader.info
|
info = loader.info
|
||||||
image = loader_image(path)
|
image = loader_image(path)
|
||||||
for warning in update_preflight(image, info, fuse_bytes):
|
for warning in update_preflight(image, info, fuse_bytes):
|
||||||
print(f"note: {warning}")
|
print(f"note: {warning}")
|
||||||
|
update = image_info(image) # the preflight proved it is there
|
||||||
|
verbose(f"installing pureboot {update.version} over pureboot {info.version}")
|
||||||
staged = staging_content(image, info)
|
staged = staging_content(image, info)
|
||||||
resident = bytes(image) + bytes([0xFF] * (info.slot - len(image)))
|
resident = bytes(image) + bytes([0xFF] * (SLOT - len(image)))
|
||||||
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. On a chip whose staging slot starts at
|
# A loader already sitting whole in the staging slot IS the staging copy:
|
||||||
# address 0 (the 1 KB tiny13A), its first page carries the reset vector:
|
# rewriting it would only meet its own running-slot guard. Any pureboot
|
||||||
# written last, so any earlier interruption still resets into the old
|
# with the device's info block serves, since a staged copy only streams
|
||||||
# resident, and from then on resets enter the staging copy.
|
# pages. "Whole" needs both checks — the block where every image carries
|
||||||
order = list(range(0, info.slot, page))
|
# it and matching byte for byte, and the slot unchanged since this update
|
||||||
|
# began, so a half-written install takes the path below instead.
|
||||||
|
current = loader.read_flash(info.stage, SLOT)
|
||||||
|
# The whole slot is searched: a loader's stamp sits wherever its image put
|
||||||
|
# it, which is the end of the code on pureboot 5 and the front of it
|
||||||
|
# before that.
|
||||||
|
staged_loader = image_info(current)
|
||||||
|
if staged_loader is not None and staged_loader.raw == info.raw and current == state.staging:
|
||||||
|
print("staging slot already holds a loader — left in place")
|
||||||
|
else:
|
||||||
|
# Where the staging slot starts at address 0 (the 1 KB tiny13s) its
|
||||||
|
# first page carries the reset vector, so it goes last: until then a
|
||||||
|
# reset still reaches the old resident.
|
||||||
|
order = list(range(0, 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. Where a patched reset vector
|
||||||
# vector routes through the resident (a tiny with the staging slot away
|
# routes through the resident, word 0 is re-aimed at the staging copy for
|
||||||
# from page 0), word 0 is re-aimed at the staging copy around the
|
# the rewrite, so a power loss 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, 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: pureboot {update.version}, {len(image)} B at {info.base:#06x}, staging region restored")
|
||||||
|
|
||||||
|
|
||||||
def check_walk_region(pages, info, fuse_bytes, force):
|
def check_walk_region(pages, info, fuse_bytes, force):
|
||||||
"""With BOOTRST programmed but targeting below the loader, reset reaches
|
"""BOOTRST programmed below the loader means reset reaches it only by
|
||||||
the loader only by walking across erased flash from the boot-section
|
walking across erased flash; application data in that span would divert
|
||||||
start; application data in that span would divert reset into itself.
|
reset into itself. Needs the fuses (--fuses or --assume-fuses)."""
|
||||||
Only checkable when the fuses are known (--fuses or --assume-fuses)."""
|
|
||||||
if info.patch_vector or fuse_bytes is None:
|
if info.patch_vector or fuse_bytes is None:
|
||||||
return
|
return
|
||||||
bootrst, bls_start = mega_boot(info, fuse_bytes)
|
bootrst, bls_start = mega_boot(info, fuse_bytes)
|
||||||
@@ -784,54 +1107,89 @@ def check_walk_region(pages, info, fuse_bytes, force):
|
|||||||
|
|
||||||
|
|
||||||
def op_erase_flash(loader):
|
def op_erase_flash(loader):
|
||||||
"""0xff over the whole application area. Descending on a patched-vector
|
"""0xff over the application area, descending where the reset vector is
|
||||||
chip: page 0 — the patched reset vector — goes last, so an interrupted
|
patched: page 0 goes last, so an interrupted erase still resets into the
|
||||||
erase still resets into the loader, and once it is gone the whole area
|
loader — and once it is gone, the erased walk reaches it 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 mismatch is
|
||||||
|
rewritten and re-read up to RETRIES times first: 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 is not that."""
|
||||||
|
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}")
|
||||||
|
|
||||||
@@ -842,10 +1200,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}")
|
||||||
@@ -854,7 +1213,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}")
|
||||||
@@ -862,15 +1221,61 @@ 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 _require_unified(loader, what):
|
||||||
|
if not loader.unified:
|
||||||
|
raise Error(f"{what} needs pureboot {UNIFIED_LOADER} or later; this loader is {loader.info.version}")
|
||||||
|
|
||||||
|
|
||||||
|
def _peek_spec(spec):
|
||||||
|
"""ADDR[:N] — addresses and counts in any Python integer base."""
|
||||||
|
address, _, count = spec.partition(":")
|
||||||
|
return int(address, 0), int(count, 0) if count else 1
|
||||||
|
|
||||||
|
|
||||||
|
def op_peek(loader, spec):
|
||||||
|
_require_unified(loader, "--peek")
|
||||||
|
address, count = _peek_spec(spec)
|
||||||
|
data = loader.read_ram(address, count)
|
||||||
|
for offset in range(0, len(data), 16):
|
||||||
|
row = data[offset : offset + 16]
|
||||||
|
text = "".join(chr(b) if 0x20 <= b < 0x7F else "." for b in row)
|
||||||
|
print(f"{address + offset:#06x} {row.hex(' '):<47} {text}")
|
||||||
|
|
||||||
|
|
||||||
|
def op_poke(loader, spec):
|
||||||
|
_require_unified(loader, "--poke")
|
||||||
|
address, _, payload = spec.partition(":")
|
||||||
|
if not payload:
|
||||||
|
raise Error("--poke needs ADDR:HEX, for example 0x200:deadbeef")
|
||||||
|
data = bytes.fromhex(payload.replace(" ", ""))
|
||||||
|
loader.write_ram(int(address, 0), data)
|
||||||
|
print(f"poke: {len(data)} B at {int(address, 0):#06x}")
|
||||||
|
|
||||||
|
|
||||||
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 ---
|
||||||
@@ -880,9 +1285,14 @@ def main():
|
|||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="pureboot host tool", epilog="operations run in the order listed above"
|
description="pureboot host tool", epilog="operations run in the order listed above"
|
||||||
)
|
)
|
||||||
|
parser.add_argument("--version", action="version", version=f"%(prog)s {VERSION} "
|
||||||
|
f"(speaks pureboot {OLDEST_LOADER}..{NEWEST_LOADER})")
|
||||||
parser.add_argument("--port", required=True, help="serial device: COM6, /dev/ttyUSB0, or a simavr pty")
|
parser.add_argument("--port", required=True, help="serial device: COM6, /dev/ttyUSB0, or a simavr pty")
|
||||||
parser.add_argument("--baud", type=int, default=115200, help="115200 mega, 57600 tinies")
|
parser.add_argument("--baud", type=int, default=115200, help="115200 mega, 57600 tinies")
|
||||||
parser.add_argument("--wait", type=float, default=30.0, help="seconds to keep knocking")
|
parser.add_argument("--wait", type=float, default=30.0, help="seconds to keep knocking")
|
||||||
|
parser.add_argument("--autobaud", action="store_true",
|
||||||
|
help="drive an autobaud loader: send the 0xC0 calibration pulse and a single "
|
||||||
|
"knock, and take geometry from the signature (no clock/baud baked in)")
|
||||||
parser.add_argument("--info", action="store_true", help="print the device info block")
|
parser.add_argument("--info", action="store_true", help="print the device info block")
|
||||||
parser.add_argument("--fuses", action="store_true", help="read the fuse and lock bytes")
|
parser.add_argument("--fuses", action="store_true", help="read the fuse and lock bytes")
|
||||||
parser.add_argument("--update-loader", metavar="FILE", help="replace the loader with this pureboot binary")
|
parser.add_argument("--update-loader", metavar="FILE", help="replace the loader with this pureboot binary")
|
||||||
@@ -898,9 +1308,17 @@ def main():
|
|||||||
parser.add_argument("--eeprom", metavar="FILE", help="program the EEPROM (bin or ihex)")
|
parser.add_argument("--eeprom", metavar="FILE", help="program the EEPROM (bin or ihex)")
|
||||||
parser.add_argument("--read-eeprom", metavar="FILE", help="dump the EEPROM")
|
parser.add_argument("--read-eeprom", metavar="FILE", help="dump the EEPROM")
|
||||||
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("--peek", metavar="ADDR[:N]", help="read N bytes of data space (SRAM, registers, "
|
||||||
|
"I/O) — pureboot 5 and later")
|
||||||
|
parser.add_argument("--poke", metavar="ADDR:HEX", help="write hex bytes into data space — "
|
||||||
|
"pureboot 5 and later")
|
||||||
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")
|
||||||
@@ -913,11 +1331,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_autobaud(args.wait) if args.autobaud else 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)
|
||||||
@@ -942,6 +1363,10 @@ def main():
|
|||||||
op_read_eeprom(loader, args.read_eeprom)
|
op_read_eeprom(loader, args.read_eeprom)
|
||||||
if args.verify_eeprom:
|
if args.verify_eeprom:
|
||||||
op_verify_eeprom(loader, args.verify_eeprom)
|
op_verify_eeprom(loader, args.verify_eeprom)
|
||||||
|
if args.poke:
|
||||||
|
op_poke(loader, args.poke)
|
||||||
|
if args.peek:
|
||||||
|
op_peek(loader, args.peek)
|
||||||
if args.stay:
|
if args.stay:
|
||||||
print("loader stays in its session (reset to leave)")
|
print("loader stays in its session (reset to leave)")
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -1,52 +1,75 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Position-independence lint for the pureboot image.
|
"""Position-independence lint: the property that lets the identical image run
|
||||||
|
from any slot, asserted from the built ELF and its object.
|
||||||
|
|
||||||
The self-staging design lets the identical binary run from any 512-byte
|
1. No absolute jmp/call — -mrelax normally guarantees it, but a branch that
|
||||||
slot, which holds only if nothing in the image addresses itself absolutely.
|
grows out of relaxation range would break it silently.
|
||||||
Two link-time facts guarantee it, both asserted here from the built ELF:
|
2. Nothing flash-resident to address: the image is .text alone, so there is
|
||||||
|
no table whose runtime address has to be reconstructed.
|
||||||
|
3. The image is byte-identical when linked at a different base. This is
|
||||||
|
position independence itself rather than a proxy for it — an absolute
|
||||||
|
address anywhere in the image would move with the link and show up as a
|
||||||
|
differing byte.
|
||||||
|
|
||||||
1. No absolute jmp/call opcodes — all control flow is PC-relative
|
Usage: check_pi.py <objdump> <objcopy> <cxx> <mcu> <elf> <object> <text_start_hex>
|
||||||
(rjmp/rcall/ijmp/icall). -mrelax normally guarantees this; a code
|
|
||||||
change that grows a branch out of relaxation range would break it
|
|
||||||
silently.
|
|
||||||
2. The info block sits within the image's first 256 bytes: the 'b'
|
|
||||||
command rebuilds its address as (running slot high byte : low byte of
|
|
||||||
the link address), which needs the offset to fit that low byte.
|
|
||||||
|
|
||||||
Usage: check_pi.py <objdump> <nm> <elf> <text_start_hex>
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
|
def fail(message):
|
||||||
|
print(f"FAIL: {message}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
objdump, nm, elf, text_start = sys.argv[1:]
|
objdump, objcopy, cxx, mcu, elf, obj, text_start = sys.argv[1:]
|
||||||
text_start = int(text_start, 0)
|
text_start = int(text_start, 0)
|
||||||
|
|
||||||
listing = subprocess.run([objdump, "-d", elf], capture_output=True, text=True, check=True).stdout
|
listing = subprocess.run([objdump, "-d", elf], capture_output=True, text=True, check=True).stdout
|
||||||
absolute = [
|
absolute = [line for line in listing.splitlines() if re.search(r"\t(jmp|call)\t", line)]
|
||||||
line
|
|
||||||
for line in listing.splitlines()
|
|
||||||
if re.search(r"\t(jmp|call)\t", line)
|
|
||||||
]
|
|
||||||
if absolute:
|
if absolute:
|
||||||
print("FAIL: absolute control flow in the image:")
|
fail("absolute control flow in the image:\n" + "\n".join(absolute))
|
||||||
print("\n".join(absolute))
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
symbols = subprocess.run([nm, "-C", elf], capture_output=True, text=True, check=True).stdout
|
# Allocated flash beyond .text would be data the running copy has to find.
|
||||||
info = [line for line in symbols.splitlines() if "info_data" in line]
|
# Only ALLOC sections reach the device at all; .comment and the debug
|
||||||
if len(info) != 1:
|
# sections ride along in the ELF container and are never flashed. objdump
|
||||||
print(f"FAIL: expected one info-block storage symbol, found {len(info)}")
|
# prints each section's flags on the line following its header.
|
||||||
sys.exit(1)
|
headers = subprocess.run([objdump, "-h", elf], capture_output=True, text=True, check=True).stdout.splitlines()
|
||||||
offset = int(info[0].split()[0], 16) - text_start
|
for index, line in enumerate(headers):
|
||||||
if not 0 <= offset < 256:
|
fields = line.split()
|
||||||
print(f"FAIL: info block at image offset {offset:#x}, must sit in the first 256 bytes")
|
if len(fields) < 6 or not fields[0].isdigit():
|
||||||
sys.exit(1)
|
continue
|
||||||
|
name, size = fields[1], int(fields[2], 16)
|
||||||
|
flags = headers[index + 1] if index + 1 < len(headers) else ""
|
||||||
|
if "ALLOC" not in flags or not size:
|
||||||
|
continue
|
||||||
|
if name not in (".text", ".noinit", ".bss"):
|
||||||
|
fail(f"flash-resident section {name} ({size} bytes): the image must be .text alone")
|
||||||
|
|
||||||
print(f"PI lint: control flow PC-relative, info block at offset {offset:#x}")
|
# Relink at a different base and compare the bytes.
|
||||||
|
with tempfile.TemporaryDirectory() as work:
|
||||||
|
elsewhere = text_start - 0x200 if text_start >= 0x200 else text_start + 0x200
|
||||||
|
images = []
|
||||||
|
for base, tag in ((text_start, "here"), (elsewhere, "there")):
|
||||||
|
relinked = os.path.join(work, f"{tag}.elf")
|
||||||
|
binary = os.path.join(work, f"{tag}.bin")
|
||||||
|
subprocess.run(
|
||||||
|
[cxx, f"-mmcu={mcu}", "-nostartfiles", f"-Wl,--section-start=.text={base:#x}",
|
||||||
|
"-Wl,--defsym=pureboot_app=0", "-mrelax", obj, "-o", relinked],
|
||||||
|
check=True, capture_output=True)
|
||||||
|
subprocess.run([objcopy, "-O", "binary", relinked, binary], check=True)
|
||||||
|
images.append(open(binary, "rb").read())
|
||||||
|
if images[0] != images[1]:
|
||||||
|
differing = [i for i, (a, b) in enumerate(zip(*images)) if a != b]
|
||||||
|
fail(f"the image changes when linked at {elsewhere:#x} instead of {text_start:#x}: "
|
||||||
|
f"{len(differing)} byte(s) differ, first at offset {differing[0]:#x}")
|
||||||
|
|
||||||
|
print(f"PI lint: control flow PC-relative, .text only, identical linked at {text_start:#x} and {elsewhere:#x}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -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,37 +22,77 @@ 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));
|
||||||
}
|
}
|
||||||
[[noreturn]] static void idle()
|
[[noreturn]] static void idle()
|
||||||
{
|
{
|
||||||
// 'L' hands back to the loader at the top slot — 512 bytes, or the
|
// 'L' hands back to the loader in the top slot — 512 bytes on every
|
||||||
// 1 KiB the >64 KiB chips use.
|
// chip. The jump takes a word address, which is what makes the
|
||||||
constexpr std::uint32_t slot = avr::hw::db.mem.flash_size > 65536 ? 1024 : 512;
|
// >64 KiB chips' entry reachable through a 16-bit pointer at all.
|
||||||
for (;;)
|
constexpr std::uint32_t slot = 512;
|
||||||
if (tx_t::read_blocking() == 'L')
|
for (;;) {
|
||||||
|
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));
|
||||||
|
|||||||
154
test/pbautobaud.py
Normal file
154
test/pbautobaud.py
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""End-to-end autobaud test: drive an autobaud loader in simavr through the
|
||||||
|
calibration handshake and a flash + EEPROM + fuse round-trip, cross-checked
|
||||||
|
against the simulator's ground-truth memory — then repeat at a second F_CPU with
|
||||||
|
the *same* loader binary, which is the property autobaud exists for: one
|
||||||
|
clock-agnostic image that locks onto whatever rate the host sends.
|
||||||
|
|
||||||
|
Usage: pbautobaud.py <device_bin> <loader_elf> <mcu> <base_hex> <page>
|
||||||
|
<app_bin> <app_hz> <app_baud> <tool_py> <workdir>
|
||||||
|
|
||||||
|
The loader is a software-serial build on PB0/PB1 (pureboot_add_autobaud's
|
||||||
|
default), so the runner drives it over the GPIO⇄pty bridge (-l sw:B0,B1). The
|
||||||
|
app fixture is built for (app_hz, app_baud); the hand-over is checked at that
|
||||||
|
point, and a second point at half the clock proves the lock is measured, not
|
||||||
|
baked in.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
def fail(message):
|
||||||
|
print(f"FAIL: {message}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
(device_bin, elf, mcu, base_hex, page, app_bin, app_hz, app_baud, tool, workdir) = sys.argv[1:]
|
||||||
|
base, page, app_hz, app_baud = int(base_hex, 0), int(page), int(app_hz), int(app_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)
|
||||||
|
ee_image = bytes(range(0xA0, 0xB0))
|
||||||
|
ee_path = os.path.join(workdir, "ee.bin")
|
||||||
|
open(ee_path, "wb").write(ee_image)
|
||||||
|
|
||||||
|
# The geometry the surgery planner needs, from the chip class the runner is
|
||||||
|
# told — the same derivation pbtest.py makes: the boot-sectioned megas need
|
||||||
|
# no vector surgery, the tinies and the boot-section-less m48s do, and the
|
||||||
|
# large chips speak word addresses.
|
||||||
|
mega = mcu.startswith("atmega")
|
||||||
|
patch = not mega or mcu.startswith("atmega48")
|
||||||
|
word_flash = base + pb.SLOT > 0x10000
|
||||||
|
wire_base = base // 2 if word_flash else base
|
||||||
|
flags = (1 if patch else 0) | (2 if word_flash else 0)
|
||||||
|
ground_truth = pb.Info(bytes([ord("P"), ord("B"), pb.NEWEST_LOADER, 0, 0, 0, page & 0xFF,
|
||||||
|
wire_base & 0xFF, wire_base >> 8, 0, 0, flags]))
|
||||||
|
|
||||||
|
def round_trip(hz, baud, label, hand_over):
|
||||||
|
"""One clock point: reset, calibrate + knock, program, verify against the
|
||||||
|
simulator's own flash, and (at the app's point) hand over to the fixture."""
|
||||||
|
dump = os.path.join(workdir, f"flash_{label}.bin")
|
||||||
|
device = pbsim.Device(device_bin, elf, mcu, str(hz), base_hex, page, baud, dump, link="sw:B0,B1")
|
||||||
|
try:
|
||||||
|
# The host tool, in autobaud mode, sends the 0xC0 calibration pulse
|
||||||
|
# and a single knock at `baud`; the loader locks to it.
|
||||||
|
out = pbsim.run_tool(tool, device.pty, baud, "--autobaud", "--info", "--fuses",
|
||||||
|
"--flash", app_bin, "--eeprom", ee_path, "--stay")
|
||||||
|
for needed in ("version", "signature", "fuses", "verify:", "stays"):
|
||||||
|
if needed not in out:
|
||||||
|
fail(f"{label}: session output lacks {needed!r}\n{out}")
|
||||||
|
# Read both memories back over the locked link and check them.
|
||||||
|
read_flash = os.path.join(workdir, f"rf_{label}.bin")
|
||||||
|
read_eeprom = os.path.join(workdir, f"re_{label}.bin")
|
||||||
|
out = pbsim.run_tool(tool, device.pty, baud, "--autobaud", "--verify-flash", app_bin,
|
||||||
|
"--verify-eeprom", ee_path, "--read-flash", read_flash,
|
||||||
|
"--read-eeprom", read_eeprom, "--stay")
|
||||||
|
if out.count("verify:") != 2:
|
||||||
|
fail(f"{label}: did not verify both memories\n{out}")
|
||||||
|
if open(read_eeprom, "rb").read()[: len(ee_image)] != ee_image:
|
||||||
|
fail(f"{label}: EEPROM read-back mismatch")
|
||||||
|
|
||||||
|
if hand_over:
|
||||||
|
# Regression: a calibration pulse with no knock behind it must
|
||||||
|
# not wedge the loader. The knock's edge wait used to be
|
||||||
|
# unbudgeted, so one stray low pulse — EMI, or a host that opens
|
||||||
|
# the port and never knocks — held the loader forever and the
|
||||||
|
# application never ran. The whole activation is bounded now, so
|
||||||
|
# the window closes and the app boots; the banner is the proof.
|
||||||
|
# (The pause lets the loader reach its measurement loop, so the
|
||||||
|
# pulse is genuinely seen and the test cannot pass vacuously.)
|
||||||
|
device.reset()
|
||||||
|
port = pb.Port(device.pty, baud)
|
||||||
|
try:
|
||||||
|
time.sleep(0.2)
|
||||||
|
port.write(bytes((pb.CALIBRATE,)))
|
||||||
|
# Accumulate rather than match exactly: the reset leaves the
|
||||||
|
# idle line a framing artefact ahead of the banner, which is
|
||||||
|
# noise here — the question is only whether the app ran.
|
||||||
|
seen = b""
|
||||||
|
deadline = time.monotonic() + 180.0
|
||||||
|
while b"APP" not in seen and time.monotonic() < deadline:
|
||||||
|
seen += port.read_available(1.0)
|
||||||
|
if b"APP" not in seen:
|
||||||
|
fail(f"{label}: lone calibration pulse wedged the loader — app never bannered, saw {seen!r}")
|
||||||
|
print(f" {label}: lone calibration pulse does not wedge the loader")
|
||||||
|
finally:
|
||||||
|
port.close()
|
||||||
|
|
||||||
|
device.reset()
|
||||||
|
port = pb.Port(device.pty, baud)
|
||||||
|
try:
|
||||||
|
loader = pb.Loader(port)
|
||||||
|
live = loader.connect_autobaud(15)
|
||||||
|
if not pb.OLDEST_LOADER <= live.version <= pb.NEWEST_LOADER:
|
||||||
|
fail(f"{label}: loader reports pureboot {live.version}")
|
||||||
|
if loader.unified:
|
||||||
|
# pureboot 5's data space. 0x0200 is clear of the
|
||||||
|
# loader's own .noinit unit at the bottom of SRAM and of
|
||||||
|
# the stack at the top. Reading it back over the same
|
||||||
|
# locked link proves both directions of the new space.
|
||||||
|
probe = bytes(range(0x30, 0x40))
|
||||||
|
loader.write_ram(0x0200, probe)
|
||||||
|
if loader.read_ram(0x0200, len(probe)) != probe:
|
||||||
|
fail(f"{label}: RAM round-trip mismatch")
|
||||||
|
# The register file and the I/O space share the data
|
||||||
|
# address space on AVR, so the same command reaches a
|
||||||
|
# peripheral register. SPMCSR reads back as idle here.
|
||||||
|
verbose_ram = loader.read_ram(0x0200, 4)
|
||||||
|
print(f" {label}: RAM read/write ok ({verbose_ram.hex()})")
|
||||||
|
loader.run_application()
|
||||||
|
banner = port.read_exact(3, 5.0)
|
||||||
|
if banner != b"APP":
|
||||||
|
fail(f"{label}: application banner was {banner!r}")
|
||||||
|
finally:
|
||||||
|
port.close()
|
||||||
|
finally:
|
||||||
|
device.stop()
|
||||||
|
|
||||||
|
# Ground truth (read after the runner exits and writes its dump): what
|
||||||
|
# the tool programmed must be what the simulator actually holds.
|
||||||
|
pages = pb.plan_flash(open(app_bin, "rb").read(), ground_truth)
|
||||||
|
flash_true = open(dump, "rb").read()
|
||||||
|
for address, data in pages.items():
|
||||||
|
if flash_true[address : address + page] != data:
|
||||||
|
fail(f"{label}: simulator flash differs from the programmed image at {address:#06x}")
|
||||||
|
print(f" {label}: locked at {hz} Hz / {baud} Bd, flash+EEPROM verified"
|
||||||
|
+ (", hand-over ok" if hand_over else ""))
|
||||||
|
|
||||||
|
# The app fixture is built for one clock; the hand-over banners there. A
|
||||||
|
# second point at double that clock, same loader binary, proves the lock is
|
||||||
|
# measured, not baked in — the whole point of autobaud. (Doubling keeps the
|
||||||
|
# bit period healthy; halving would drop it below the software UART's floor.)
|
||||||
|
round_trip(app_hz, app_baud, "clock-a", hand_over=True)
|
||||||
|
round_trip(app_hz * 2, app_baud, "clock-b", hand_over=False)
|
||||||
|
print("pbautobaud: calibration lock and flash/EEPROM/fuse round-trip pass at both clocks")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
88
test/pbdirty.py
Normal file
88
test/pbdirty.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Dirty-page-buffer acceptance test: with no discard in the loader, a page
|
||||||
|
filled over words an earlier writer left takes those instead. The whole
|
||||||
|
contract is asserted — a bare verify sees the corruption, the repairing
|
||||||
|
verify fixes it in one rewrite, and it stays fixed.
|
||||||
|
|
||||||
|
The state is reached the one way the loader cannot prevent: an application
|
||||||
|
dirties the buffer and jumps in with no reset between. Boot-sectioned megas
|
||||||
|
forbid that outright (SPM runs only from the boot section, Atmel-8271 §26.2),
|
||||||
|
but simavr dispatches SPM from anywhere, which is what makes it constructible.
|
||||||
|
|
||||||
|
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()
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Re-homing acceptance test: a pureboot image mistakenly programmed at
|
"""Re-homing acceptance test: an image programmed somewhere other than its
|
||||||
address 0 of a patched-vector chip (a raw .bin handed to a programmer, which
|
canonical slot must still be a working loader, and the ordinary
|
||||||
defaults to offset 0) must still be a working loader — position-independent,
|
--update-loader flow must put a build into the top slot from there.
|
||||||
guarding its accidental slot — and the ordinary --update-loader flow must
|
|
||||||
re-home a build into the canonical top slot: the staging install and the
|
Two positions. Address 0, a raw .bin handed to a programmer: the staging
|
||||||
word-0 redirect both run from copies whose slots are not page 0's, so the
|
install and the word-0 redirect run from copies outside page 0's slot, so the
|
||||||
running-slot guard never blocks the flow. Flashing an application through
|
running-slot guard never blocks them. And the staging slot itself, where a
|
||||||
the healed resident then overwrites the stale copy, vector surgery included,
|
loader already sitting there IS the staging copy — recognized by its embedded
|
||||||
and the banner proves the launch.
|
block and left in place, then streaming the new resident like any staged copy.
|
||||||
|
|
||||||
Usage: pbrehome.py <device_bin> <pureboot_elf> <update_bin> <mcu> <hz>
|
Usage: pbrehome.py <device_bin> <pureboot_elf> <update_bin> <mcu> <hz>
|
||||||
<base_hex> <page> <baud> <app_bin> <tool_py> <workdir>
|
<base_hex> <page> <baud> <app_bin> <tool_py> <workdir>
|
||||||
@@ -22,23 +22,15 @@ def fail(message):
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def rehome_from(pbsim, pb, device_bin, elf, place_hex, guard_probe, update_bin, base, page, baud, app_bin, workdir,
|
||||||
(device_bin, elf, update_bin, mcu, hz, base_hex, page, baud, app_bin, tool, workdir) = sys.argv[1:]
|
mcu, hz):
|
||||||
base, baud = int(base_hex, 0), int(baud)
|
"""Place the loader at `place_hex`, heal through --update-loader, flash
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
|
the application, expect the banner."""
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
dump = os.path.join(workdir, f"dump-{place_hex}.bin")
|
||||||
import pbsim
|
state = os.path.join(workdir, f"rehome-{place_hex}.pbstate")
|
||||||
import pureboot as pb
|
|
||||||
|
|
||||||
os.makedirs(workdir, exist_ok=True)
|
|
||||||
dump = os.path.join(workdir, "dump.bin")
|
|
||||||
state = os.path.join(workdir, "rehome.pbstate")
|
|
||||||
if os.path.exists(state):
|
if os.path.exists(state):
|
||||||
os.unlink(state)
|
os.unlink(state)
|
||||||
|
device = pbsim.Device(device_bin, elf, mcu, hz, place_hex, page, baud, dump, reset_hex="0")
|
||||||
# The runner's base argument places the firmware: 0 is the misplaced
|
|
||||||
# flash, and the patched-vector reset at word 0 lands in it directly.
|
|
||||||
device = pbsim.Device(device_bin, elf, mcu, hz, "0x0", int(page), baud, dump, reset_hex="0")
|
|
||||||
try:
|
try:
|
||||||
port = pb.Port(device.pty, baud)
|
port = pb.Port(device.pty, baud)
|
||||||
loader = pb.Loader(port)
|
loader = pb.Loader(port)
|
||||||
@@ -46,14 +38,15 @@ def main():
|
|||||||
if info.base != base:
|
if info.base != base:
|
||||||
fail(f"the misplaced copy reports base {info.base:#06x} — the info block must stay canonical")
|
fail(f"the misplaced copy reports base {info.base:#06x} — the info block must stay canonical")
|
||||||
|
|
||||||
# The accidental slot still guards itself; the canonical base slot
|
# The accidental slot still guards itself; re-homing rides on the
|
||||||
# must be writable from it (that is what re-homing rides on).
|
# canonical slots being writable from it.
|
||||||
before = loader.read_flash(0, info.page)
|
probe = int(guard_probe, 0)
|
||||||
loader.write_page(0, bytes(info.page))
|
before = loader.read_flash(probe, info.page)
|
||||||
if loader.read_flash(0, info.page) != before:
|
loader.write_page(probe, bytes(info.page))
|
||||||
|
if loader.read_flash(probe, info.page) != before:
|
||||||
fail("the misplaced copy's guard let its own slot change")
|
fail("the misplaced copy's guard let its own slot change")
|
||||||
|
|
||||||
# The ordinary update flow re-homes the build into the top slot.
|
# The ordinary update flow puts the build into the top slot.
|
||||||
pb.op_update_loader(loader, 25, update_bin, state, None)
|
pb.op_update_loader(loader, 25, update_bin, state, None)
|
||||||
update = open(update_bin, "rb").read()
|
update = open(update_bin, "rb").read()
|
||||||
if loader.read_flash(base, len(update)) != update:
|
if loader.read_flash(base, len(update)) != update:
|
||||||
@@ -67,10 +60,35 @@ def main():
|
|||||||
pb.verify_pages(loader, pages)
|
pb.verify_pages(loader, pages)
|
||||||
loader.run_application()
|
loader.run_application()
|
||||||
if port.read_exact(3, 5.0) != b"APP":
|
if port.read_exact(3, 5.0) != b"APP":
|
||||||
fail("application does not banner after the re-home")
|
fail(f"application does not banner after the re-home from {place_hex}")
|
||||||
port.close()
|
port.close()
|
||||||
finally:
|
finally:
|
||||||
device.stop()
|
device.stop()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
(device_bin, elf, update_bin, mcu, hz, base_hex, page, baud, app_bin, tool, workdir) = sys.argv[1:]
|
||||||
|
base, page, baud = int(base_hex, 0), 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)
|
||||||
|
|
||||||
|
# Address 0: the raw-.bin-to-a-programmer accident. The guard probe is
|
||||||
|
# the copy's own page 0.
|
||||||
|
rehome_from(pbsim, pb, device_bin, elf, "0x0", "0x0", update_bin, base, page, baud, app_bin, workdir, mcu, hz)
|
||||||
|
print("re-home from address 0: converged")
|
||||||
|
|
||||||
|
# The staging slot: erased flash with the loader sitting exactly where
|
||||||
|
# a staging copy would — the tool must leave it in place and let it
|
||||||
|
# stream the (different) update build into the resident slot.
|
||||||
|
stage = base - pb.SLOT
|
||||||
|
rehome_from(pbsim, pb, device_bin, elf, hex(stage), hex(stage), update_bin, base, page, baud, app_bin, workdir,
|
||||||
|
mcu, hz)
|
||||||
|
print("re-home from the staging slot: converged")
|
||||||
|
|
||||||
print("pbrehome: a misplaced loader re-homes through the ordinary update flow")
|
print("pbrehome: a misplaced loader re-homes through the ordinary update flow")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Position-independence acceptance test: the identical pureboot binary,
|
"""Position-independence acceptance test: the identical binary, flashed one
|
||||||
flashed one slot below the resident loader, must serve the complete command
|
slot below the resident, must serve the complete command set from there. The
|
||||||
set from there. The resident installs it (through-word composed by the host
|
info block must come back byte-identical, the write guard must refuse the
|
||||||
layer), 'J' transfers control, and every command is exercised against the
|
staged copy's own slot and permit the resident's, and the staged copy must be
|
||||||
staged copy — the info block must come back byte-identical, the write guard
|
able to rewrite the resident verbatim.
|
||||||
must protect the staged copy's own slot and permit the resident's, and the
|
|
||||||
staged copy must be able to rewrite the resident slot verbatim.
|
|
||||||
|
|
||||||
Usage: pbreloc.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
|
Usage: pbreloc.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
|
||||||
<baud> <tool_py> <workdir>
|
<baud> <tool_py> <workdir>
|
||||||
@@ -66,8 +64,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,11 +75,13 @@ 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.
|
||||||
resident = image + b"\xff" * (info.slot - len(image))
|
resident = image + b"\xff" * (pb.SLOT - len(image))
|
||||||
pb.write_differing(loader, base, resident)
|
pb.write_differing(loader, base, resident)
|
||||||
back_info = loader.enter_copy(base, 25)
|
back_info = loader.enter_copy(base, 25)
|
||||||
if back_info.raw != resident_info:
|
if back_info.raw != resident_info:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
126
test/pbtest.py
126
test/pbtest.py
@@ -1,19 +1,17 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""End-to-end pureboot protocol test: spawn the simavr device, then drive it
|
"""End-to-end protocol test: drive the simavr device with the real host tool
|
||||||
with the real host tool (pureboot.py, as a subprocess over the device's pty)
|
over its pty through flash, EEPROM, fuse and hand-over scenarios, and
|
||||||
through flash + EEPROM + fuse + hand-over scenarios, and cross-check
|
cross-check the tool's view against the simulator's ground-truth 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]
|
||||||
Exits 0 if every scenario passes.
|
|
||||||
|
The optional link is the runner's -l spec (usart1, sw:B5,B1, ...), for a
|
||||||
|
loader built off the chip's natural serial default.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import signal
|
|
||||||
import subprocess
|
|
||||||
import sys
|
import sys
|
||||||
import time
|
|
||||||
|
|
||||||
|
|
||||||
def fail(message):
|
def fail(message):
|
||||||
@@ -33,53 +31,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)
|
||||||
@@ -96,30 +55,44 @@ def main():
|
|||||||
# the page byte is the wire's 0-means-256.
|
# the page byte is the wire's 0-means-256.
|
||||||
mega = mcu.startswith("atmega")
|
mega = mcu.startswith("atmega")
|
||||||
patch = not mega or mcu.startswith("atmega48")
|
patch = not mega or mcu.startswith("atmega48")
|
||||||
word_flash = base + 512 > 0x10000
|
# Where SRAM begins: the x8 and x4 megas push it past their extended I/O
|
||||||
|
# space, everything else starts right after the plain I/O registers. The
|
||||||
|
# loader keeps no statics and its stack sits at RAMEND, so the first SRAM
|
||||||
|
# byte is free for the data-space probe below.
|
||||||
|
classic = mcu in ("atmega8", "atmega8a", "atmega16", "atmega16a", "atmega32", "atmega32a")
|
||||||
|
ram_base = 0x0100 if mega and not classic else 0x0060
|
||||||
|
word_flash = base + pb.SLOT > 0x10000
|
||||||
wire_base = base // 2 if word_flash else base
|
wire_base = base // 2 if word_flash else base
|
||||||
flags = (1 if patch else 0) | (2 if word_flash else 0)
|
flags = (1 if patch else 0) | (2 if word_flash else 0)
|
||||||
info = pb.Info(
|
info = pb.Info(
|
||||||
bytes([ord("P"), ord("B"), 1, 0, 0, 0, page & 0xFF])
|
bytes([ord("P"), ord("B"), pb.NEWEST_LOADER, 0, 0, 0, page & 0xFF])
|
||||||
+ bytes([wire_base & 0xFF, wire_base >> 8, eeprom_size & 0xFF, eeprom_size >> 8])
|
+ bytes([wire_base & 0xFF, wire_base >> 8, eeprom_size & 0xFF, eeprom_size >> 8])
|
||||||
+ 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 ("version", "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, exercise
|
||||||
# is deferred — the pty must be reopened for the APP banner first.
|
# the data space; hand over is deferred — the pty must be reopened for
|
||||||
out = run_tool(tool, device.pty, baud, "--verify-flash", app_bin, "--verify-eeprom", ee_path,
|
# the APP banner first.
|
||||||
"--read-flash", read_flash, "--read-eeprom", read_eeprom, "--stay")
|
probe = "c0ffee"
|
||||||
|
out = pbsim.run_tool(tool, device.pty, baud, "--verify-flash", app_bin, "--verify-eeprom", ee_path,
|
||||||
|
"--read-flash", read_flash, "--read-eeprom", read_eeprom,
|
||||||
|
"--poke", f"{ram_base:#x}:{probe}", "--peek", f"{ram_base:#x}:3", "--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")
|
||||||
|
# What went into SRAM must come back out of it: the data space is one
|
||||||
|
# more selector on the same transfer as flash and EEPROM, so a wrong
|
||||||
|
# selector decode would show up here and nowhere else.
|
||||||
|
if probe not in out.replace(" ", ""):
|
||||||
|
fail(f"data-space round trip at {ram_base:#x} did not read back {probe}\n{out}")
|
||||||
|
|
||||||
eeprom_back = open(read_eeprom, "rb").read()
|
eeprom_back = open(read_eeprom, "rb").read()
|
||||||
if eeprom_back[: len(ee_image)] != ee_image:
|
if eeprom_back[: len(ee_image)] != ee_image:
|
||||||
@@ -136,11 +109,34 @@ 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)
|
||||||
loader.connect(15)
|
live = loader.connect(15)
|
||||||
|
# The loader built from this tree must report a version the tool
|
||||||
|
# beside it speaks — a bump the tool was never told about is a
|
||||||
|
# loader it would refuse to talk to. Not equality with the newest:
|
||||||
|
# the tool now spans two loader generations, the fixed-baud one
|
||||||
|
# here and the unified autobaud loader that follows it.
|
||||||
|
if not pb.OLDEST_LOADER <= live.version <= pb.NEWEST_LOADER:
|
||||||
|
fail(f"loader reports pureboot {live.version}, the tool speaks "
|
||||||
|
f"{pb.OLDEST_LOADER}..{pb.NEWEST_LOADER}")
|
||||||
|
|
||||||
|
# A W addressed inside a page rather than at its base must still
|
||||||
|
# consume exactly one page and prompt. The loader's own slot is the
|
||||||
|
# target — the guard refuses to commit it — and the payload is
|
||||||
|
# erased-state bytes, so the probe can disturb neither the image nor
|
||||||
|
# the page buffer it leaves behind. Hand-built rather than through
|
||||||
|
# write_page(), which would follow the fill with its erase and
|
||||||
|
# write; the point here is that the fill alone consumes exactly one
|
||||||
|
# page whatever the address's low bits say.
|
||||||
|
wire = base + 1
|
||||||
|
port.write(bytes((ord("W"), pb.selector(pb.SP_FLASH, wire), wire & 0xFF, (wire >> 8) & 0xFF))
|
||||||
|
+ b"\xff" * page)
|
||||||
|
if port.read_exact(1, 5.0) != pb.PROMPT:
|
||||||
|
fail("unaligned W did not return to the prompt")
|
||||||
|
|
||||||
loader.run_application()
|
loader.run_application()
|
||||||
banner = port.read_exact(3, 5.0)
|
banner = port.read_exact(3, 5.0)
|
||||||
if banner != b"APP":
|
if banner != b"APP":
|
||||||
@@ -161,7 +157,7 @@ def main():
|
|||||||
# loader, the trampoline on the application's own entry (patched-vector
|
# loader, the trampoline on the application's own entry (patched-vector
|
||||||
# chips only — a boot-sectioned mega's word 0 stays the application's).
|
# chips only — a boot-sectioned mega's word 0 stays the application's).
|
||||||
if patch:
|
if patch:
|
||||||
flash_words = (base + 512) // 2
|
flash_words = (base + pb.SLOT) // 2
|
||||||
app = open(app_bin, "rb").read()
|
app = open(app_bin, "rb").read()
|
||||||
word0 = flash_true[0] | (flash_true[1] << 8)
|
word0 = flash_true[0] | (flash_true[1] << 8)
|
||||||
if rjmp_decode(word0, 0, flash_words) != base // 2:
|
if rjmp_decode(word0, 0, flash_words) != base // 2:
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Self-update end-to-end: an application is flashed, then the loader
|
"""Self-update end-to-end: an application is flashed, the loader replaces
|
||||||
replaces itself with a re-timed build through the host tool's
|
itself with a re-timed build, and every power-fail phase is rehearsed by
|
||||||
--update-loader — and the power-fail phases of that update are rehearsed by
|
killing the device mid-write, restarting it from its flash dump, and letting
|
||||||
killing the simulated device mid-write, restarting it from its flash dump,
|
a re-run complete the update.
|
||||||
and letting a re-run complete the update.
|
|
||||||
|
|
||||||
The boot-sectioned megas run the BOOTRST-unprogrammed profile (reset boots
|
The boot-sectioned megas run the BOOTRST-unprogrammed profile — reset boots
|
||||||
the application; the fixture application's 'L' jump is the application-owned
|
the application, whose 'L' is the application-owned loader entry — with
|
||||||
loader entry), with --assume-fuses standing in for the fuse read simavr
|
--assume-fuses standing in for the fuse read simavr cannot model.
|
||||||
cannot model. The patched-vector chips — the tinies and the m48s — reset
|
|
||||||
into a loader at every phase by construction: the t13a because its staging
|
|
||||||
slot carries the reset vector itself, the others through the word-0 redirect
|
|
||||||
the tool plants around the resident rewrite.
|
|
||||||
|
|
||||||
Usage: pbupdate.py <device_bin> <pureboot_elf> <update_elf> <mcu> <hz>
|
Usage: pbupdate.py <device_bin> <pureboot_elf> <update_elf> <mcu> <hz>
|
||||||
<base_hex> <page> <baud> <app_bin> <tool_py> <workdir>
|
<base_hex> <page> <baud> <app_bin> <tool_py> <workdir>
|
||||||
@@ -50,7 +45,7 @@ def assumed_fuses(pb, image):
|
|||||||
image's embedded signature."""
|
image's embedded signature."""
|
||||||
info = pb.image_info(image)
|
info = pb.image_info(image)
|
||||||
which, ladder = pb.BOOT_FUSE[bytes(info.signature[1:3])]
|
which, ladder = pb.BOOT_FUSE[bytes(info.signature[1:3])]
|
||||||
bits = min((b for b in ladder if ladder[b] * 2 >= 2 * info.slot), key=lambda b: ladder[b])
|
bits = min((b for b in ladder if ladder[b] * 2 >= 2 * pb.SLOT), key=lambda b: ladder[b])
|
||||||
fuses = bytearray((0xFF, 0xFF, 0xFF, 0xFF))
|
fuses = bytearray((0xFF, 0xFF, 0xFF, 0xFF))
|
||||||
fuses[which] = 0xF8 | (bits << 1) | 1
|
fuses[which] = 0xF8 | (bits << 1) | 1
|
||||||
return bytes(fuses)
|
return bytes(fuses)
|
||||||
@@ -93,16 +88,13 @@ def main():
|
|||||||
# The m48s are megas without a boot section: patched vector, no fuse
|
# The m48s are megas without a boot section: patched vector, no fuse
|
||||||
# preflight, and the same reset-to-0 the tinies get.
|
# preflight, and the same reset-to-0 the tinies get.
|
||||||
patch = not mega or mcu.startswith("atmega48")
|
patch = not mega or mcu.startswith("atmega48")
|
||||||
# Word-addressed (>64 KiB) chips use the 1 KiB slot; their loader base
|
|
||||||
# itself sits beyond the 16-bit byte space — the 644's base + slot only
|
|
||||||
# touches the 64 KiB boundary and stays byte-addressed.
|
|
||||||
slot = 1024 if base >= 0x10000 and mega else 512
|
|
||||||
reset_hex = "0" if mega else None # the boot-sectioned mega runs BOOTRST-unprogrammed here
|
reset_hex = "0" if mega else None # the boot-sectioned mega runs BOOTRST-unprogrammed here
|
||||||
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__)))
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
import pbsim
|
import pbsim
|
||||||
import pureboot as pb
|
import pureboot as pb
|
||||||
|
|
||||||
|
slot = pb.SLOT
|
||||||
os.makedirs(workdir, exist_ok=True)
|
os.makedirs(workdir, exist_ok=True)
|
||||||
objcopy = os.environ.get("PB_OBJCOPY", "avr-objcopy")
|
objcopy = os.environ.get("PB_OBJCOPY", "avr-objcopy")
|
||||||
images = {}
|
images = {}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -158,9 +182,16 @@ static avr_cycle_count_t tx_sample(avr_t *mcu, avr_cycle_count_t when, void *par
|
|||||||
{
|
{
|
||||||
(void)mcu;
|
(void)mcu;
|
||||||
(void)param;
|
(void)param;
|
||||||
|
if (tx_bit < 8) {
|
||||||
tx_shift = (uint8_t)((tx_shift >> 1) | (tx_level ? 0x80 : 0));
|
tx_shift = (uint8_t)((tx_shift >> 1) | (tx_level ? 0x80 : 0));
|
||||||
if (++tx_bit < 8)
|
if (++tx_bit < 8)
|
||||||
return when + bit_cycles;
|
return when + bit_cycles;
|
||||||
|
/* The byte is not delivered until its stop bit has passed. A real
|
||||||
|
* receiver cannot answer sooner, and a host that did would put its
|
||||||
|
* start bit on the wire while the device is still driving the stop
|
||||||
|
* bit — which the device, transmitting, is not watching for. */
|
||||||
|
return when + bit_cycles;
|
||||||
|
}
|
||||||
if (write(pty_master, &tx_shift, 1) != 1)
|
if (write(pty_master, &tx_shift, 1) != 1)
|
||||||
fprintf(stderr, "device: pty write lost a byte\n");
|
fprintf(stderr, "device: pty write lost a byte\n");
|
||||||
tx_active = 0;
|
tx_active = 0;
|
||||||
@@ -273,29 +304,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 +350,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 +371,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 +385,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 +440,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);
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Host-tool unit tests — the pure planning and policy logic, no simulator:
|
"""Host-tool unit tests — the planning and policy logic, no simulator:
|
||||||
the flash-programming orders and their recovery properties, the reset-vector
|
programming orders and their recovery properties, the reset-vector surgery,
|
||||||
surgery, the staging-slot composition, the mega boot-fuse decode, and the
|
the staging composition, the boot-fuse decode, and the update preflight over
|
||||||
update preflight's error/warning matrix (fuse combinations simavr cannot
|
fuse combinations simavr cannot model.
|
||||||
model reach it here as synthetic bytes).
|
|
||||||
|
|
||||||
Usage: test_planner.py <tool_py>
|
Usage: test_planner.py <tool_py>
|
||||||
"""
|
"""
|
||||||
@@ -27,14 +26,21 @@ def expect_error(what, fn, *needles):
|
|||||||
fail(f"{what}: no error raised")
|
fail(f"{what}: no error raised")
|
||||||
|
|
||||||
|
|
||||||
def info_of(pb, base, page, patch, flash, signature=(0x1E, 0x93, 0x0B), word_flash=False):
|
def info_of(pb, base, page, patch, flash, signature=(0x1E, 0x93, 0x0B), word_flash=False, version=None):
|
||||||
scale = 2 if word_flash else 1
|
scale = 2 if word_flash else 1
|
||||||
wire_base = base // scale
|
wire_base = base // scale
|
||||||
flags = (1 if patch else 0) | (2 if word_flash else 0)
|
flags = (1 if patch else 0) | (2 if word_flash else 0)
|
||||||
raw = bytes((0x50, 0x42, 1, *signature, page & 0xFF, wire_base & 0xFF, wire_base >> 8,
|
# The EEPROM size comes from the signature, as it must: pureboot 5 derives
|
||||||
0, 2, flags))
|
# the whole geometry from the signature rather than sending it, so a
|
||||||
|
# synthetic block that disagreed with its own signature would describe a
|
||||||
|
# chip that cannot exist.
|
||||||
|
eeprom = pb.CHIP_GEOMETRY[signature][2]
|
||||||
|
raw = bytes((0x50, 0x42, pb.NEWEST_LOADER if version is None else version,
|
||||||
|
*signature, page & 0xFF, wire_base & 0xFF, wire_base >> 8,
|
||||||
|
eeprom & 0xFF, eeprom >> 8, flags))
|
||||||
info = pb.Info(raw)
|
info = pb.Info(raw)
|
||||||
assert info.flash_size == flash
|
if info.flash_size != flash:
|
||||||
|
fail(f"info_of({base:#x}) decodes to {info.flash_size:#x} of flash, not {flash:#x}")
|
||||||
return info
|
return info
|
||||||
|
|
||||||
|
|
||||||
@@ -55,6 +61,21 @@ def main():
|
|||||||
tiny = info_of(pb, 0x1E00, 64, True, 0x2000)
|
tiny = info_of(pb, 0x1E00, 64, True, 0x2000)
|
||||||
mega = info_of(pb, 0x7E00, 128, False, 0x8000, signature=(0x1E, 0x95, 0x0F))
|
mega = info_of(pb, 0x7E00, 128, False, 0x8000, signature=(0x1E, 0x95, 0x0F))
|
||||||
|
|
||||||
|
# Versioning: the block's third byte is the loader's version, and the tool
|
||||||
|
# speaks a window of them. Every version in the window decodes, so an older
|
||||||
|
# deployed loader stays usable; one above the window is refused by name,
|
||||||
|
# since which version changed the protocol is knowledge only the tool
|
||||||
|
# holds, and it holds none about a version it has never heard of.
|
||||||
|
for version in range(pb.OLDEST_LOADER, pb.NEWEST_LOADER + 1):
|
||||||
|
if info_of(pb, 0x1E00, 64, True, 0x2000, version=version).version != version:
|
||||||
|
fail(f"pureboot {version} does not decode")
|
||||||
|
expect_error(
|
||||||
|
"unknown loader version",
|
||||||
|
lambda: info_of(pb, 0x1E00, 64, True, 0x2000, version=pb.NEWEST_LOADER + 1),
|
||||||
|
f"pureboot {pb.NEWEST_LOADER + 1}",
|
||||||
|
"newer tool",
|
||||||
|
)
|
||||||
|
|
||||||
# mega_boot: BOOTSZ words and the BOOTRST sense per chip — the fuse byte
|
# mega_boot: BOOTSZ words and the BOOTRST sense per chip — the fuse byte
|
||||||
# index (EXTENDED on the x8 line except the m328s' HIGH, HIGH elsewhere)
|
# index (EXTENDED on the x8 line except the m328s' HIGH, HIGH elsewhere)
|
||||||
# and the per-family ladders (Atmel-2486/2466/2503/2545/8271/DS40002065/
|
# and the per-family ladders (Atmel-2486/2466/2503/2545/8271/DS40002065/
|
||||||
@@ -79,9 +100,7 @@ def main():
|
|||||||
((0x1E, 0x97, 0x05), 0x20000, 3, {0b11: 0x1FC00, 0b10: 0x1F800, 0b01: 0x1F000, 0b00: 0x1E000}), # 1284P
|
((0x1E, 0x97, 0x05), 0x20000, 3, {0b11: 0x1FC00, 0b10: 0x1F800, 0b01: 0x1F000, 0b00: 0x1E000}), # 1284P
|
||||||
)
|
)
|
||||||
for signature, flash, which, ladder in cases:
|
for signature, flash, which, ladder in cases:
|
||||||
# Word-addressed chips carry the 1 KiB slot (their smallest boot sector).
|
chip = info_of(pb, flash - pb.SLOT, 128 if flash < 0x20000 else 0, False, flash,
|
||||||
slot = 1024 if flash > 0x10000 else 512
|
|
||||||
chip = info_of(pb, flash - slot, 128 if flash < 0x20000 else 0, False, flash,
|
|
||||||
signature=signature, word_flash=flash > 0x10000)
|
signature=signature, word_flash=flash > 0x10000)
|
||||||
for bits, start in ladder.items():
|
for bits, start in ladder.items():
|
||||||
fuses = bytearray((0xFF, 0xFF, 0xFF, 0xFF))
|
fuses = bytearray((0xFF, 0xFF, 0xFF, 0xFF))
|
||||||
@@ -94,10 +113,12 @@ def main():
|
|||||||
if prog or at != start:
|
if prog or at != start:
|
||||||
fail(f"mega_boot {signature[1]:02x}{signature[2]:02b} unprogrammed: {prog} {at:#07x}")
|
fail(f"mega_boot {signature[1]:02x}{signature[2]:02b} unprogrammed: {prog} {at:#07x}")
|
||||||
|
|
||||||
# Word-addressed info decode: the 1284P's base/page ride the wire scaled,
|
# Word-addressed info decode: the 1284P's base and page ride the wire
|
||||||
# and its slot is 1 KiB.
|
# scaled — a 17-bit base halved into the block's two bytes, a 256-byte page
|
||||||
big = info_of(pb, 0x1FC00, 0, False, 0x20000, signature=(0x1E, 0x97, 0x05), word_flash=True)
|
# spelled 0 — and its slot is the same 512 bytes as everywhere else, so its
|
||||||
if big.page != 256 or big.base != 0x1FC00 or big.stage != 0x1F800 or big.slot != 1024:
|
# staging slot lands inside the 1 KiB minimum boot section.
|
||||||
|
big = info_of(pb, 0x1FE00, 0, False, 0x20000, signature=(0x1E, 0x97, 0x05), word_flash=True)
|
||||||
|
if big.page != 256 or big.base != 0x1FE00 or big.stage != 0x1FC00:
|
||||||
fail(f"word-addressed info decode: page {big.page}, base {big.base:#x}, stage {big.stage:#x}")
|
fail(f"word-addressed info decode: page {big.page}, base {big.base:#x}, stage {big.stage:#x}")
|
||||||
|
|
||||||
# Surgery: word 0 lands on the loader, the trampoline on the original
|
# Surgery: word 0 lands on the loader, the trampoline on the original
|
||||||
@@ -147,13 +168,24 @@ def main():
|
|||||||
fail("mega staging content should be the bare image")
|
fail("mega staging content should be the bare image")
|
||||||
expect_error("mega staging size", lambda: pb.staging_content(image + b"!", mega), "512")
|
expect_error("mega staging size", lambda: pb.staging_content(image + b"!", mega), "512")
|
||||||
|
|
||||||
# The embedded info block: found in a synthetic binary, absent in noise.
|
# The image stamp: found in a synthetic binary, absent in noise. pureboot
|
||||||
binary = bytes((0xAA,)) * 10 + tiny.raw + bytes((0xBB,)) * 10
|
# 5 stamps the magic, its version and the signature, and the geometry is
|
||||||
|
# looked up from there — so what comes back must equal what a live device
|
||||||
|
# of the same chip reports.
|
||||||
|
stamp = bytes((0x50, 0x42, pb.NEWEST_LOADER)) + bytes(tiny.signature)
|
||||||
|
binary = bytes((0xAA,)) * 10 + stamp + bytes((0xBB,)) * 10
|
||||||
found = pb.image_info(binary)
|
found = pb.image_info(binary)
|
||||||
if found is None or found.raw != tiny.raw:
|
if found is None or found.raw != tiny.raw:
|
||||||
fail("image_info misses the embedded block")
|
fail(f"image_info misreads the v{pb.NEWEST_LOADER} stamp: "
|
||||||
|
f"{found.raw.hex() if found else None} != {tiny.raw.hex()}")
|
||||||
if pb.image_info(bytes((0xAA,)) * 40) is not None:
|
if pb.image_info(bytes((0xAA,)) * 40) is not None:
|
||||||
fail("image_info invents a block")
|
fail("image_info invents a block")
|
||||||
|
# An older loader's image stays readable, so a deployed build can be
|
||||||
|
# identified and installed like any other.
|
||||||
|
old = info_of(pb, 0x1E00, 64, True, 0x2000, version=pb.OLDEST_LOADER)
|
||||||
|
found_old = pb.image_info(bytes((0xAA,)) * 10 + old.raw)
|
||||||
|
if found_old is None or found_old.version != pb.OLDEST_LOADER:
|
||||||
|
fail("image_info misses an older loader's block")
|
||||||
|
|
||||||
# loader_image must peel a padded image down to the slot content: a raw
|
# loader_image must peel a padded image down to the slot content: a raw
|
||||||
# .bin padded from address 0 (or a whole-flash read-back with the loader
|
# .bin padded from address 0 (or a whole-flash read-back with the loader
|
||||||
@@ -194,6 +226,15 @@ def main():
|
|||||||
if pb.update_preflight(bytes((0xAA,)) * 8 + tiny.raw, tiny, None) != []:
|
if pb.update_preflight(bytes((0xAA,)) * 8 + tiny.raw, tiny, None) != []:
|
||||||
fail("tiny preflight should pass without fuses")
|
fail("tiny preflight should pass without fuses")
|
||||||
|
|
||||||
|
# The 1284s' smallest boot section (512 words) is exactly the resident
|
||||||
|
# slot plus its staging slot, so self-update is possible at the minimum
|
||||||
|
# BOOTSZ — no fuse step up, the 644's geometry. That holds only while a
|
||||||
|
# slot is 512 B: at 1 KiB the staging slot would fall outside the section
|
||||||
|
# and the preflight would refuse.
|
||||||
|
notes = pb.update_preflight(bytes((0xAA,)) * 8 + big.raw, big, fuses(0xFE))
|
||||||
|
if not any("staging slot" in n for n in notes):
|
||||||
|
fail(f"1284 minimum-BOOTSZ notes: {notes}")
|
||||||
|
|
||||||
# The walk-region refusal: BOOTRST aimed below the loader plus app data
|
# The walk-region refusal: BOOTRST aimed below the loader plus app data
|
||||||
# in the walk span errors without --force; erased spans and unprogrammed
|
# in the walk span errors without --force; erased spans and unprogrammed
|
||||||
# BOOTRST pass.
|
# BOOTRST pass.
|
||||||
@@ -204,6 +245,114 @@ 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}")
|
||||||
|
|
||||||
|
# The knock handshake against a device that is not listening yet — the
|
||||||
|
# state a port open leaves behind: it resets the chip into a fresh
|
||||||
|
# activation window while the previous session's prompt is still in
|
||||||
|
# flight, so the first knock is lost and a prompt arrives anyway.
|
||||||
|
class FakePort:
|
||||||
|
"""A loader in its activation window, plus `lost` leading writes the
|
||||||
|
reset swallows and one stale prompt still on the wire."""
|
||||||
|
|
||||||
|
def __init__(self, info_raw, lost=0, stale=b"", active=False):
|
||||||
|
self.info_raw = info_raw
|
||||||
|
self.lost = lost
|
||||||
|
self.inflight = bytearray(stale)
|
||||||
|
self.rx = bytearray()
|
||||||
|
self.active = active
|
||||||
|
self.last = None
|
||||||
|
|
||||||
|
def flush_input(self):
|
||||||
|
self.rx.clear()
|
||||||
|
|
||||||
|
def write(self, data):
|
||||||
|
if self.lost:
|
||||||
|
self.lost -= 1
|
||||||
|
return
|
||||||
|
for byte in bytes(data):
|
||||||
|
if not self.active:
|
||||||
|
self.active = self.last == ord("p") and byte == ord("b")
|
||||||
|
self.last = byte
|
||||||
|
if self.active:
|
||||||
|
self.rx += pb.PROMPT
|
||||||
|
elif byte == ord("b"):
|
||||||
|
self.rx += self.info_raw + pb.PROMPT
|
||||||
|
else:
|
||||||
|
self.rx += pb.PROMPT
|
||||||
|
|
||||||
|
def read_available(self, wait):
|
||||||
|
self.rx = self.inflight + self.rx # the stale prompt lands late
|
||||||
|
self.inflight.clear()
|
||||||
|
out, self.rx = bytes(self.rx), bytearray()
|
||||||
|
return out
|
||||||
|
|
||||||
|
def read_exact(self, count, timeout):
|
||||||
|
if len(self.rx) < count:
|
||||||
|
raise pb.Error(f"timeout: got {len(self.rx)} of {count} bytes")
|
||||||
|
out, self.rx = bytes(self.rx[:count]), self.rx[count:]
|
||||||
|
return out
|
||||||
|
|
||||||
|
raw = info_of(pb, 0x7E00, 128, False, 0x8000).raw
|
||||||
|
for what, port in (
|
||||||
|
("clean window", FakePort(raw)),
|
||||||
|
("stale prompt over a lost knock", FakePort(raw, lost=1, stale=pb.PROMPT)),
|
||||||
|
("live session", FakePort(raw, active=True)),
|
||||||
|
):
|
||||||
|
info = pb.Loader(port).connect(5)
|
||||||
|
if info.raw != raw:
|
||||||
|
fail(f"connect ({what}) returned {info.raw.hex()}")
|
||||||
|
|
||||||
|
# A device that never answers still says so, and a version the tool cannot
|
||||||
|
# speak is reported as such rather than retried into a timeout.
|
||||||
|
expect_error("dead device", lambda: pb.Loader(FakePort(raw, lost=99)).connect(0), "no answer")
|
||||||
|
old = bytes(raw[:2]) + bytes((pb.NEWEST_LOADER + 1,)) + bytes(raw[3:])
|
||||||
|
expect_error("unspeakable version", lambda: pb.Loader(FakePort(old)).connect(5), "needs a newer tool")
|
||||||
|
|
||||||
print("test_planner: all planner and policy checks pass")
|
print("test_planner: all planner and policy checks pass")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
39
tools/check.sh
Executable file
39
tools/check.sh
Executable file
@@ -0,0 +1,39 @@
|
|||||||
|
#!/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) and swaps the compact size matrix for the exhaustive
|
||||||
|
# clock × baud × backend cross product. LIBAVR_ROOT must point at the libavr
|
||||||
|
# checkout.
|
||||||
|
set -e
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
full=0
|
||||||
|
[[ "$1" == "--full" ]] && { full=1; shift; export PUREBOOT_FULL_MATRIX=1; }
|
||||||
|
|
||||||
|
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