Compare commits
37 Commits
e85840fa39
...
v1
| Author | SHA1 | Date | |
|---|---|---|---|
| a58b59c60f | |||
| ceacb61ba1 | |||
| dd7490a3df | |||
| bc24b63e65 | |||
| 332b244ac3 | |||
| 61b687120c | |||
| ef58d5d363 | |||
| 03bb1f56cf | |||
| 6a5e313530 | |||
| a28fccf475 | |||
| 92503fbb2d | |||
| 8ba023ed6b | |||
| 443fd39d31 | |||
| 08a0d77109 | |||
| 260ab2e4e3 | |||
| e4aaf5bc62 | |||
| 653602058a | |||
| 57d1c009cc | |||
| 06bb06d994 | |||
| 9a9a7ef0e8 | |||
| 4acf358dda | |||
| 11c10f986d | |||
| f587f0a26e | |||
| 27c768a102 | |||
| d989b5b8cd | |||
| 314e422b19 | |||
| 3e0717ef06 | |||
| 812be910c1 | |||
| 34529c8031 | |||
| 06bd56af50 | |||
| 75bb84fa3e | |||
| 778b8a0100 | |||
| 48d11fe8e3 | |||
| bb4784ae0c | |||
| d07586652d | |||
| 3a2f2a23c2 | |||
| 4fa058653e |
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "libavr"]
|
||||
path = libavr
|
||||
url = ../libavr.git
|
||||
241
CMakeLists.txt
241
CMakeLists.txt
@@ -8,6 +8,9 @@ include(FetchContent)
|
||||
if(NOT LIBAVR_ROOT AND DEFINED ENV{LIBAVR_ROOT})
|
||||
set(LIBAVR_ROOT $ENV{LIBAVR_ROOT})
|
||||
endif()
|
||||
if(NOT LIBAVR_ROOT)
|
||||
set(LIBAVR_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/libavr)
|
||||
endif()
|
||||
if(LIBAVR_ROOT)
|
||||
FetchContent_Declare(libavr SOURCE_DIR ${LIBAVR_ROOT})
|
||||
else()
|
||||
@@ -51,13 +54,18 @@ if(PROJECT_IS_TOP_LEVEL)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# avrdude programs Intel-HEX; the ELF is only a container (symbols, section
|
||||
# headers) and is never flashed. Every loader image therefore gets a .hex beside
|
||||
# it at link time. .eeprom is dropped — EEPROM content is its own avrdude update.
|
||||
function(add_hex_output name)
|
||||
# The ELF is only a container (symbols, section headers) and is never flashed —
|
||||
# and the host tool's load_image() dispatches on extension, so handing it one
|
||||
# would silently program the header bytes. Every loader image therefore gets
|
||||
# both flashable forms beside it at link time: .hex for avrdude, and .bin for
|
||||
# the host tool's raw path (which is what the reloc and update tests convert to
|
||||
# on the fly). .eeprom is dropped — EEPROM content is its own update.
|
||||
function(add_image_outputs name)
|
||||
add_custom_command(TARGET ${name} POST_BUILD
|
||||
COMMAND ${CMAKE_OBJCOPY} -O ihex -R .eeprom
|
||||
$<TARGET_FILE:${name}> $<TARGET_FILE:${name}>.hex)
|
||||
$<TARGET_FILE:${name}> $<TARGET_FILE:${name}>.hex
|
||||
COMMAND ${CMAKE_OBJCOPY} -O binary -R .eeprom
|
||||
$<TARGET_FILE:${name}> $<TARGET_FILE:${name}>.bin)
|
||||
endfunction()
|
||||
|
||||
# The TinySafeBoot protocol reimplemented on libavr in three variants that trade
|
||||
@@ -99,7 +107,7 @@ function(add_tsb_variant name bytes)
|
||||
target_link_options(${name} PRIVATE -nostartfiles -Wl,--section-start=.text=${base_hex}
|
||||
-Wl,--defsym=tsb_app=0 -Wl,--pmem-wrap-around=32k)
|
||||
add_custom_command(TARGET ${name} POST_BUILD COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${name}>)
|
||||
add_hex_output(${name})
|
||||
add_image_outputs(${name})
|
||||
if(PROJECT_IS_TOP_LEVEL)
|
||||
add_test(NAME ${name}.size
|
||||
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:${name}>
|
||||
@@ -121,70 +129,31 @@ if(LIBAVR_MCU STREQUAL "atmega328p")
|
||||
endif()
|
||||
|
||||
# pureboot — the pure-constraint port (see pureboot/README.md): one source,
|
||||
# no inline assembly, no global register variables, every libavr chip, 512
|
||||
# bytes each. The loader owns the top 512 bytes of flash on every chip; the
|
||||
# application entry symbol is address 0 on the mega (reset re-vectors to the
|
||||
# loader through BOOTRST, so word 0 stays the application's own vector) and
|
||||
# the trampoline word just below the loader on the tinies (host-side vector
|
||||
# surgery points it at the application). --pmem-wrap-around models AVR's
|
||||
# modulo-flash PC where the flash is big enough to need it.
|
||||
#
|
||||
# The image is position-independent (check_pi.py asserts the two link-time
|
||||
# facts that make it so), and on the tinies its budget is 510, not 512: the
|
||||
# slot's last word is the trampoline the host composes — the resident slot's
|
||||
# holds the application entry, and a staging copy's holds the jump through
|
||||
# which it reaches the loader it installed. The activation window is a
|
||||
# compile-time constant; a different PUREBOOT_TIMEOUT builds the re-timed
|
||||
# binary a self-update then installs.
|
||||
set(PUREBOOT_TIMEOUT 8 CACHE STRING "pureboot activation window, seconds")
|
||||
if(LIBAVR_MCU STREQUAL "attiny13a")
|
||||
set(_pb_flash 1024)
|
||||
set(_pb_wrap "")
|
||||
set(_pb_page 32)
|
||||
set(_pb_hz 9600000)
|
||||
set(_pb_baud 57600)
|
||||
set(_pb_eeprom 64)
|
||||
set(_pb_limit 510)
|
||||
elseif(LIBAVR_MCU STREQUAL "attiny85")
|
||||
set(_pb_flash 8192)
|
||||
set(_pb_wrap -Wl,--pmem-wrap-around=8k)
|
||||
set(_pb_page 64)
|
||||
set(_pb_hz 8000000)
|
||||
set(_pb_baud 57600)
|
||||
set(_pb_eeprom 512)
|
||||
set(_pb_limit 510)
|
||||
else()
|
||||
set(_pb_flash 32768)
|
||||
set(_pb_wrap -Wl,--pmem-wrap-around=32k)
|
||||
set(_pb_page 128)
|
||||
set(_pb_hz 16000000)
|
||||
set(_pb_baud 115200)
|
||||
set(_pb_eeprom 1024)
|
||||
set(_pb_limit 512)
|
||||
endif()
|
||||
math(EXPR _pb_base "${_pb_flash} - 512")
|
||||
math(EXPR _pb_base_hex "${_pb_base}" OUTPUT_FORMAT HEXADECIMAL)
|
||||
if(LIBAVR_MCU STREQUAL "atmega328p")
|
||||
set(_pb_app 0)
|
||||
else()
|
||||
math(EXPR _pb_app "${_pb_base} - 2")
|
||||
endif()
|
||||
# no inline assembly, no global register variables, every libavr chip,
|
||||
# fitting each chip's smallest boot sector. The geometry and the
|
||||
# pureboot_add_loader() deployment function live in pureboot/CMakeLists.txt —
|
||||
# the unit a downstream project consumes; everything below is this port's
|
||||
# own build: the stock loaders, their tests, and the size matrix. The
|
||||
# distinct binary dir keeps the `pureboot` target's output name free.
|
||||
add_subdirectory(pureboot pureboot-cmake)
|
||||
|
||||
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_hex_output(pureboot)
|
||||
# The stock loader: the family-default deployment (crystal/RC clock, the
|
||||
# chip's natural link, default pins). The activation window stays a cache
|
||||
# variable — re-timing a deployed loader is a self-update with a re-timed
|
||||
# build. pureboot9 is that re-timed build, and what the update test installs.
|
||||
set(PUREBOOT_TIMEOUT 8 CACHE STRING "pureboot activation window, seconds")
|
||||
pureboot_add_loader(pureboot TIMEOUT ${PUREBOOT_TIMEOUT})
|
||||
if(PROJECT_IS_TOP_LEVEL)
|
||||
get_target_property(_pb_stock_hz pureboot PUREBOOT_HZ)
|
||||
get_target_property(_pb_stock_baud pureboot PUREBOOT_BAUD)
|
||||
|
||||
add_test(NAME pureboot.size
|
||||
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<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)
|
||||
add_test(NAME pureboot.pi
|
||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/check_pi.py
|
||||
${CMAKE_OBJDUMP} ${CMAKE_NM} $<TARGET_FILE:pureboot> ${_pb_base_hex})
|
||||
${CMAKE_OBJDUMP} ${CMAKE_NM} $<TARGET_FILE:pureboot> ${PUREBOOT_BASE_HEX})
|
||||
add_test(NAME pureboot.planner
|
||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/test_planner.py
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py)
|
||||
@@ -200,9 +169,9 @@ if(PROJECT_IS_TOP_LEVEL)
|
||||
COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:pbapp> $<TARGET_FILE:pbapp>.bin)
|
||||
add_test(NAME pureboot.protocol
|
||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbtest.py
|
||||
${PB_DEVICE} $<TARGET_FILE:pureboot> ${LIBAVR_MCU} ${_pb_hz} ${_pb_base_hex}
|
||||
${_pb_page} ${_pb_baud} ${_pb_eeprom} $<TARGET_FILE:pbapp>.bin
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||
${PB_DEVICE} $<TARGET_FILE:pureboot> ${PUREBOOT_SIM_MCU} ${_pb_stock_hz}
|
||||
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_pb_stock_baud} ${PUREBOOT_EEPROM}
|
||||
$<TARGET_FILE:pbapp>.bin ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||
${CMAKE_BINARY_DIR}/pbtest-work)
|
||||
set_tests_properties(pureboot.protocol PROPERTIES TIMEOUT 180)
|
||||
|
||||
@@ -210,29 +179,143 @@ if(PROJECT_IS_TOP_LEVEL)
|
||||
# installed one slot lower, must serve the full command set.
|
||||
add_test(NAME pureboot.reloc
|
||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbreloc.py
|
||||
${PB_DEVICE} $<TARGET_FILE:pureboot> ${LIBAVR_MCU} ${_pb_hz} ${_pb_base_hex}
|
||||
${_pb_page} ${_pb_baud} ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||
${PB_DEVICE} $<TARGET_FILE:pureboot> ${PUREBOOT_SIM_MCU} ${_pb_stock_hz}
|
||||
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_pb_stock_baud}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||
${CMAKE_BINARY_DIR}/pbreloc-work)
|
||||
set_tests_properties(pureboot.reloc PROPERTIES TIMEOUT 180
|
||||
ENVIRONMENT "PB_OBJCOPY=${CMAKE_OBJCOPY}")
|
||||
|
||||
# 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
|
||||
# handed to a programmer) or sitting in the staging slot must heal
|
||||
# into the canonical slot through the ordinary --update-loader flow.
|
||||
# Patched-vector behavior, so one representative chip carries it.
|
||||
if(LIBAVR_MCU STREQUAL "attiny85")
|
||||
add_test(NAME pureboot.rehome
|
||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbrehome.py
|
||||
${PB_DEVICE} $<TARGET_FILE:pureboot> $<TARGET_FILE:pureboot9>.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_BINARY_DIR}/pbrehome-work)
|
||||
set_tests_properties(pureboot.rehome PROPERTIES TIMEOUT 180)
|
||||
endif()
|
||||
|
||||
# The self-update end-to-end: the re-timed build (same source, only
|
||||
# PUREBOOT_TIMEOUT differs — a byte-different image) replaces the
|
||||
# resident through --update-loader, with every power-fail phase
|
||||
# rehearsed from the runner's flash dumps.
|
||||
add_executable(pureboot9 pureboot/pureboot.cpp)
|
||||
target_link_libraries(pureboot9 PRIVATE libavr)
|
||||
target_compile_definitions(pureboot9 PRIVATE PUREBOOT_TIMEOUT=9)
|
||||
target_link_options(pureboot9 PRIVATE -nostartfiles -Wl,--section-start=.text=${_pb_base_hex}
|
||||
-Wl,--defsym=pureboot_app=${_pb_app} ${_pb_wrap})
|
||||
add_hex_output(pureboot9)
|
||||
# the timeout differs — a byte-different image) replaces the resident
|
||||
# through --update-loader, with every power-fail phase rehearsed from
|
||||
# the runner's flash dumps.
|
||||
pureboot_add_loader(pureboot9 TIMEOUT 9)
|
||||
add_test(NAME pureboot.update
|
||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbupdate.py
|
||||
${PB_DEVICE} $<TARGET_FILE:pureboot> $<TARGET_FILE:pureboot9> ${LIBAVR_MCU}
|
||||
${_pb_hz} ${_pb_base_hex} ${_pb_page} ${_pb_baud} $<TARGET_FILE:pbapp>.bin
|
||||
${PB_DEVICE} $<TARGET_FILE:pureboot> $<TARGET_FILE:pureboot9>
|
||||
${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}/pbupdate-work)
|
||||
set_tests_properties(pureboot.update PROPERTIES TIMEOUT 600
|
||||
ENVIRONMENT "PB_OBJCOPY=${CMAKE_OBJCOPY}")
|
||||
endif()
|
||||
|
||||
# The size matrix: every configuration axis that could move the image
|
||||
# size — the serial backend (different code), the clock and its ladder
|
||||
# baud (different constants and divisor shapes), the USART instance
|
||||
# (different register class) — each combination must still fit the
|
||||
# chip's slot budget. Pins are size-neutral (port and bit are immediate
|
||||
# operands) and the timeout is a constant, so neither adds an axis. The
|
||||
# stock build is one point of this matrix and already has its test.
|
||||
function(pureboot_size_variant name)
|
||||
pureboot_add_loader(${name} ${ARGN})
|
||||
add_test(NAME ${name}.size
|
||||
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:${name}>
|
||||
-DLIMIT=${PUREBOOT_LIMIT} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
|
||||
endfunction()
|
||||
|
||||
# Clock points: the shipped-fuse floor (CKDIV8), the calibrated RC, and
|
||||
# the crystal the stock build assumes (the tiny13's ladder is its own RC
|
||||
# menu — it has no crystal option).
|
||||
if(LIBAVR_MCU MATCHES "^attiny13")
|
||||
set(_matrix_clocks 1200000 4800000 9600000)
|
||||
else()
|
||||
set(_matrix_clocks 1000000 8000000 16000000)
|
||||
endif()
|
||||
foreach(_matrix_hz IN LISTS _matrix_clocks)
|
||||
math(EXPR _matrix_khz "${_matrix_hz} / 1000")
|
||||
if(PUREBOOT_HAS_USART OR NOT _matrix_hz EQUAL _pb_stock_hz)
|
||||
pureboot_size_variant(pureboot_sw_${_matrix_khz}k CLOCK ${_matrix_hz} SERIAL software)
|
||||
endif()
|
||||
if(PUREBOOT_HAS_USART AND NOT _matrix_hz EQUAL _pb_stock_hz)
|
||||
pureboot_size_variant(pureboot_hw_${_matrix_khz}k CLOCK ${_matrix_hz} SERIAL hardware)
|
||||
endif()
|
||||
endforeach()
|
||||
if(PUREBOOT_HAS_USART1)
|
||||
pureboot_size_variant(pureboot_usart1 USART 1)
|
||||
endif()
|
||||
|
||||
# One configured deployment end to end — a real board's shape rather
|
||||
# than the stock assumption: the ATmega328P on its shipped 1 MHz fuses,
|
||||
# the software UART on hand-picked pins (TX = PB1, RX = PB5), the ladder
|
||||
# baud (9600). The full protocol suite runs against it, fixture
|
||||
# application included, over the runner's GPIO bridge — proving the
|
||||
# configuration plumbing produces a working loader, not just one that
|
||||
# fits.
|
||||
if(LIBAVR_MCU STREQUAL "atmega328p" AND DEFINED PB_DEVICE)
|
||||
pureboot_size_variant(pureboot_custom CLOCK 1000000 SERIAL software RX pb5 TX pb1)
|
||||
get_target_property(_custom_hz pureboot_custom PUREBOOT_HZ)
|
||||
get_target_property(_custom_baud pureboot_custom PUREBOOT_BAUD)
|
||||
get_target_property(_custom_link pureboot_custom PUREBOOT_LINK)
|
||||
add_executable(pbapp_custom test/pbapp.cpp)
|
||||
target_link_libraries(pbapp_custom PRIVATE libavr)
|
||||
target_compile_definitions(pbapp_custom PRIVATE PUREBOOT_CLOCK_HZ=${_custom_hz}
|
||||
PUREBOOT_BAUD=${_custom_baud} PUREBOOT_SOFT_SERIAL PUREBOOT_TX=pb1)
|
||||
add_custom_command(TARGET pbapp_custom POST_BUILD
|
||||
COMMAND ${CMAKE_OBJCOPY} -O binary
|
||||
$<TARGET_FILE:pbapp_custom> $<TARGET_FILE:pbapp_custom>.bin)
|
||||
add_test(NAME pureboot.custom
|
||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbtest.py
|
||||
${PB_DEVICE} $<TARGET_FILE:pureboot_custom> ${PUREBOOT_SIM_MCU} ${_custom_hz}
|
||||
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_custom_baud} ${PUREBOOT_EEPROM}
|
||||
$<TARGET_FILE:pbapp_custom>.bin ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||
${CMAKE_BINARY_DIR}/pbcustom-work ${_custom_link})
|
||||
set_tests_properties(pureboot.custom PROPERTIES TIMEOUT 180)
|
||||
endif()
|
||||
|
||||
# The second USART, driven for real on one chip: instance selection is
|
||||
# compile-checked everywhere, but only a live session proves the loader
|
||||
# initialized and polls the USART it claims to. The fixture application
|
||||
# banners on the same instance.
|
||||
if(LIBAVR_MCU STREQUAL "atmega644a" AND DEFINED PB_DEVICE)
|
||||
get_target_property(_usart1_hz pureboot_usart1 PUREBOOT_HZ)
|
||||
get_target_property(_usart1_baud pureboot_usart1 PUREBOOT_BAUD)
|
||||
add_executable(pbapp_usart1 test/pbapp.cpp)
|
||||
target_link_libraries(pbapp_usart1 PRIVATE libavr)
|
||||
target_compile_definitions(pbapp_usart1 PRIVATE PUREBOOT_CLOCK_HZ=${_usart1_hz}
|
||||
PUREBOOT_BAUD=${_usart1_baud} PUREBOOT_USART=1)
|
||||
add_custom_command(TARGET pbapp_usart1 POST_BUILD
|
||||
COMMAND ${CMAKE_OBJCOPY} -O binary
|
||||
$<TARGET_FILE:pbapp_usart1> $<TARGET_FILE:pbapp_usart1>.bin)
|
||||
add_test(NAME pureboot.usart1
|
||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbtest.py
|
||||
${PB_DEVICE} $<TARGET_FILE:pureboot_usart1> ${PUREBOOT_SIM_MCU} ${_usart1_hz}
|
||||
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_usart1_baud} ${PUREBOOT_EEPROM}
|
||||
$<TARGET_FILE:pbapp_usart1>.bin ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||
${CMAKE_BINARY_DIR}/pbusart1-work usart1)
|
||||
set_tests_properties(pureboot.usart1 PROPERTIES TIMEOUT 180)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
1659
CMakePresets.json
1659
CMakePresets.json
File diff suppressed because it is too large
Load Diff
1
libavr
Submodule
1
libavr
Submodule
Submodule libavr added at e81dad0131
319
pureboot/CMakeLists.txt
Normal file
319
pureboot/CMakeLists.txt
Normal file
@@ -0,0 +1,319 @@
|
||||
# pureboot as a consumable CMake unit: the per-chip geometry, the default
|
||||
# baud ladder, and pureboot_add_loader() — the one way a loader target is
|
||||
# created, both by this port's own build and by a downstream project. A
|
||||
# downstream project brings its usual libavr setup (the `libavr` target and
|
||||
# the LIBAVR_MCU toolchain preset), adds this directory, and states its
|
||||
# deployment:
|
||||
#
|
||||
# add_subdirectory(bootloader/pureboot)
|
||||
# pureboot_add_loader(myboot CLOCK 1000000 SERIAL software TX pb1 RX pb5)
|
||||
#
|
||||
# Every argument is optional — CLOCK defaults to the family assumption
|
||||
# below, BAUD to the fastest standard rate the clock reaches within 2.5 %
|
||||
# (the ladder), SERIAL to the chip's hardware USART where it has one
|
||||
# (`hardware`/`software` force a backend, USART 1 picks the second
|
||||
# instance), RX/TX to pb0/pb1 for the software UART, TIMEOUT to 8 s.
|
||||
# Infeasible picks fail the build by name: libavr's baud-error and
|
||||
# software-UART cycle-floor static asserts re-check whatever is passed.
|
||||
|
||||
# Per-family geometry: flash/page/EEPROM sizes and the linker wrap the PC
|
||||
# modulo needs, the loader slot (each chip's smallest boot sector — 1 KiB on
|
||||
# the word-addressed 1284s), and the deployment defaults (crystal assumption
|
||||
# on the megas, calibrated RC on the tinies). The USART flags mirror the
|
||||
# hardware inventory the loader's own static asserts check (the plain 644 is
|
||||
# the x4 family's one single-USART die, Atmel-2593).
|
||||
set(_pb_has_usart 1)
|
||||
set(_pb_has_usart1 0)
|
||||
if(LIBAVR_MCU MATCHES "^attiny13a?$")
|
||||
set(_pb_flash 1024)
|
||||
set(_pb_wrap "")
|
||||
set(_pb_page 32)
|
||||
set(_pb_hz 9600000)
|
||||
set(_pb_eeprom 64)
|
||||
set(_pb_has_usart 0)
|
||||
elseif(LIBAVR_MCU STREQUAL "attiny25")
|
||||
set(_pb_flash 2048)
|
||||
set(_pb_wrap "")
|
||||
set(_pb_page 32)
|
||||
set(_pb_hz 8000000)
|
||||
set(_pb_eeprom 128)
|
||||
set(_pb_has_usart 0)
|
||||
elseif(LIBAVR_MCU STREQUAL "attiny45")
|
||||
set(_pb_flash 4096)
|
||||
set(_pb_wrap "")
|
||||
set(_pb_page 64)
|
||||
set(_pb_hz 8000000)
|
||||
set(_pb_eeprom 256)
|
||||
set(_pb_has_usart 0)
|
||||
elseif(LIBAVR_MCU STREQUAL "attiny85")
|
||||
set(_pb_flash 8192)
|
||||
set(_pb_wrap -Wl,--pmem-wrap-around=8k)
|
||||
set(_pb_page 64)
|
||||
set(_pb_hz 8000000)
|
||||
set(_pb_eeprom 512)
|
||||
set(_pb_has_usart 0)
|
||||
elseif(LIBAVR_MCU MATCHES "^atmega48(a|p|pa)?$")
|
||||
set(_pb_flash 4096)
|
||||
set(_pb_wrap "")
|
||||
set(_pb_page 64)
|
||||
set(_pb_hz 16000000)
|
||||
set(_pb_eeprom 256)
|
||||
elseif(LIBAVR_MCU MATCHES "^atmega8a?$" OR LIBAVR_MCU MATCHES "^atmega88(a|p|pa)?$")
|
||||
set(_pb_flash 8192)
|
||||
set(_pb_wrap -Wl,--pmem-wrap-around=8k)
|
||||
set(_pb_page 64)
|
||||
set(_pb_hz 16000000)
|
||||
set(_pb_eeprom 512)
|
||||
elseif(LIBAVR_MCU MATCHES "^atmega16a?$" OR LIBAVR_MCU MATCHES "^atmega168(a|p|pa)?$")
|
||||
set(_pb_flash 16384)
|
||||
set(_pb_wrap -Wl,--pmem-wrap-around=16k)
|
||||
set(_pb_page 128)
|
||||
set(_pb_hz 16000000)
|
||||
set(_pb_eeprom 512)
|
||||
elseif(LIBAVR_MCU MATCHES "^atmega164(a|p|pa)$")
|
||||
set(_pb_flash 16384)
|
||||
set(_pb_wrap -Wl,--pmem-wrap-around=16k)
|
||||
set(_pb_page 128)
|
||||
set(_pb_hz 16000000)
|
||||
set(_pb_eeprom 512)
|
||||
set(_pb_has_usart1 1)
|
||||
elseif(LIBAVR_MCU MATCHES "^atmega32a?$" OR LIBAVR_MCU MATCHES "^atmega328p?$")
|
||||
set(_pb_flash 32768)
|
||||
set(_pb_wrap -Wl,--pmem-wrap-around=32k)
|
||||
set(_pb_page 128)
|
||||
set(_pb_hz 16000000)
|
||||
set(_pb_eeprom 1024)
|
||||
elseif(LIBAVR_MCU MATCHES "^atmega324(a|p|pa)$")
|
||||
set(_pb_flash 32768)
|
||||
set(_pb_wrap -Wl,--pmem-wrap-around=32k)
|
||||
set(_pb_page 128)
|
||||
set(_pb_hz 16000000)
|
||||
set(_pb_eeprom 1024)
|
||||
set(_pb_has_usart1 1)
|
||||
elseif(LIBAVR_MCU MATCHES "^atmega644(a|p|pa)?$")
|
||||
# 64 KiB is exactly the 16-bit byte space: plain LPM reaches everything,
|
||||
# and the smallest boot section (1 KiB) holds the loader and its staging
|
||||
# slot together (see README.md). The plain 644 is the family's one
|
||||
# single-USART die.
|
||||
set(_pb_flash 65536)
|
||||
set(_pb_wrap -Wl,--pmem-wrap-around=64k)
|
||||
set(_pb_page 256)
|
||||
set(_pb_hz 16000000)
|
||||
set(_pb_eeprom 2048)
|
||||
if(NOT LIBAVR_MCU STREQUAL "atmega644")
|
||||
set(_pb_has_usart1 1)
|
||||
endif()
|
||||
elseif(LIBAVR_MCU MATCHES "^atmega1284p?$")
|
||||
# 128 KiB: wire flash addresses are word addresses, reads go through
|
||||
# ELPM, and the PC's modulo wrap exceeds what --pmem-wrap-around models.
|
||||
# The slot is 1 KiB — this chip's own smallest boot sector; the far
|
||||
# machinery cannot fit 512 B (see README.md).
|
||||
set(_pb_flash 131072)
|
||||
set(_pb_wrap "")
|
||||
set(_pb_page 256)
|
||||
set(_pb_hz 16000000)
|
||||
set(_pb_eeprom 4096)
|
||||
set(_pb_slot 1024)
|
||||
set(_pb_limit 1024)
|
||||
set(_pb_has_usart1 1)
|
||||
else()
|
||||
message(FATAL_ERROR "pureboot: no geometry for ${LIBAVR_MCU}")
|
||||
endif()
|
||||
if(NOT DEFINED _pb_slot)
|
||||
set(_pb_slot 512)
|
||||
endif()
|
||||
math(EXPR _pb_base "${_pb_flash} - ${_pb_slot}")
|
||||
math(EXPR _pb_base_hex "${_pb_base}" OUTPUT_FORMAT HEXADECIMAL)
|
||||
# Patched-vector chips hand over through the trampoline word below the slot,
|
||||
# which is also the slot's own last word — their budget is slot − 2.
|
||||
if(LIBAVR_MCU MATCHES "^atmega" AND NOT LIBAVR_MCU MATCHES "^atmega48")
|
||||
set(_pb_app 0)
|
||||
if(NOT DEFINED _pb_limit)
|
||||
set(_pb_limit ${_pb_slot})
|
||||
endif()
|
||||
else()
|
||||
math(EXPR _pb_app "${_pb_base} - 2")
|
||||
math(EXPR _pb_limit "${_pb_slot} - 2")
|
||||
endif()
|
||||
|
||||
# simavr names its cores after the base dies; the A revisions run on them
|
||||
# (the 644PA on the 644P core).
|
||||
set(_pb_sim_mcu ${LIBAVR_MCU})
|
||||
if(LIBAVR_MCU MATCHES "^atmega(8|16|32|48|88|164|168|644)a$")
|
||||
string(REGEX REPLACE "a$" "" _pb_sim_mcu ${LIBAVR_MCU})
|
||||
elseif(LIBAVR_MCU STREQUAL "atmega644pa")
|
||||
set(_pb_sim_mcu atmega644p)
|
||||
endif()
|
||||
|
||||
# The function runs in its caller's scope, so everything it needs crosses
|
||||
# scopes as global properties.
|
||||
set_property(GLOBAL PROPERTY PUREBOOT_BASE_HEX ${_pb_base_hex})
|
||||
set_property(GLOBAL PROPERTY PUREBOOT_APP ${_pb_app})
|
||||
set_property(GLOBAL PROPERTY PUREBOOT_WRAP "${_pb_wrap}")
|
||||
set_property(GLOBAL PROPERTY PUREBOOT_DEFAULT_HZ ${_pb_hz})
|
||||
set_property(GLOBAL PROPERTY PUREBOOT_HAS_USART ${_pb_has_usart})
|
||||
set_property(GLOBAL PROPERTY PUREBOOT_HAS_USART1 ${_pb_has_usart1})
|
||||
|
||||
# The port's own build (tests, the size matrix) reads the geometry from the
|
||||
# parent scope; a downstream consumer gets the same variables for free.
|
||||
set(PUREBOOT_BASE_HEX ${_pb_base_hex} PARENT_SCOPE)
|
||||
set(PUREBOOT_PAGE ${_pb_page} PARENT_SCOPE)
|
||||
set(PUREBOOT_SLOT ${_pb_slot} PARENT_SCOPE)
|
||||
set(PUREBOOT_LIMIT ${_pb_limit} PARENT_SCOPE)
|
||||
set(PUREBOOT_EEPROM ${_pb_eeprom} PARENT_SCOPE)
|
||||
set(PUREBOOT_DEFAULT_HZ ${_pb_hz} PARENT_SCOPE)
|
||||
set(PUREBOOT_HAS_USART ${_pb_has_usart} PARENT_SCOPE)
|
||||
set(PUREBOOT_HAS_USART1 ${_pb_has_usart1} PARENT_SCOPE)
|
||||
set(PUREBOOT_SIM_MCU ${_pb_sim_mcu} PARENT_SCOPE)
|
||||
|
||||
# The fastest standard rate the clock reaches within 2.5 % — the same
|
||||
# best-of-U2X-and-plain divisor search libavr's solve_baud runs, so a
|
||||
# default never trips the compile-time error it is checked against. A
|
||||
# software build additionally requires the polled receiver's 100-cycles-a-bit
|
||||
# floor (its own static assert): at low clocks the U2X divisor still reaches
|
||||
# rates the bit-banged sampler cannot, so the backend gates the ladder.
|
||||
function(pureboot_default_baud clock software outvar)
|
||||
foreach(baud 115200 57600 38400 19200 9600)
|
||||
math(EXPR _cycles "${clock} / ${baud}")
|
||||
if(software AND _cycles LESS 100)
|
||||
continue()
|
||||
endif()
|
||||
foreach(divisor 8 16)
|
||||
math(EXPR _step "${divisor} * ${baud}")
|
||||
math(EXPR _n "(${clock} + ${_step} / 2) / ${_step}")
|
||||
if(_n LESS 1 OR _n GREATER 4096)
|
||||
continue()
|
||||
endif()
|
||||
math(EXPR _actual "${clock} / (${divisor} * ${_n})")
|
||||
math(EXPR _delta "${_actual} - ${baud}")
|
||||
if(_delta LESS 0)
|
||||
math(EXPR _delta "-(${_delta})")
|
||||
endif()
|
||||
math(EXPR _error_bp "${_delta} * 10000 / ${baud}")
|
||||
if(_error_bp LESS_EQUAL 250)
|
||||
set(${outvar} ${baud} PARENT_SCOPE)
|
||||
return()
|
||||
endif()
|
||||
endforeach()
|
||||
endforeach()
|
||||
message(FATAL_ERROR "pureboot: no standard baud rate fits a ${clock} Hz clock within 2.5 %")
|
||||
endfunction()
|
||||
|
||||
# pureboot_add_loader(<name> [CLOCK <hz>] [BAUD <bd>]
|
||||
# [SERIAL auto|hardware|software] [USART <n>]
|
||||
# [RX <pin>] [TX <pin>] [TIMEOUT <s>])
|
||||
#
|
||||
# Creates the loader target plus its flashable images (<name>.hex for a
|
||||
# programmer, <name>.bin for --update-loader) and stamps the resolved
|
||||
# deployment on the target: the PUREBOOT_HZ, PUREBOOT_BAUD and PUREBOOT_LINK
|
||||
# properties (the link as usart0/usart1/sw:<RX>,<TX> — what a test harness
|
||||
# needs to speak to the build).
|
||||
function(pureboot_add_loader name)
|
||||
cmake_parse_arguments(PB "" "CLOCK;BAUD;SERIAL;USART;RX;TX;TIMEOUT" "" ${ARGN})
|
||||
if(PB_UNPARSED_ARGUMENTS)
|
||||
message(FATAL_ERROR "pureboot_add_loader(${name}): unknown arguments ${PB_UNPARSED_ARGUMENTS}")
|
||||
endif()
|
||||
get_property(_hz GLOBAL PROPERTY PUREBOOT_DEFAULT_HZ)
|
||||
get_property(_base_hex GLOBAL PROPERTY PUREBOOT_BASE_HEX)
|
||||
get_property(_app GLOBAL PROPERTY PUREBOOT_APP)
|
||||
get_property(_wrap GLOBAL PROPERTY PUREBOOT_WRAP)
|
||||
get_property(_usart GLOBAL PROPERTY PUREBOOT_HAS_USART)
|
||||
get_property(_usart1 GLOBAL PROPERTY PUREBOOT_HAS_USART1)
|
||||
|
||||
if(NOT PB_CLOCK)
|
||||
set(PB_CLOCK ${_hz})
|
||||
endif()
|
||||
if(NOT PB_TIMEOUT)
|
||||
set(PB_TIMEOUT 8)
|
||||
endif()
|
||||
if(NOT PB_SERIAL)
|
||||
set(PB_SERIAL auto)
|
||||
endif()
|
||||
if(DEFINED PB_USART AND PB_SERIAL STREQUAL "software")
|
||||
message(FATAL_ERROR "pureboot_add_loader(${name}): USART ${PB_USART} contradicts SERIAL software")
|
||||
endif()
|
||||
if(DEFINED PB_USART)
|
||||
set(PB_SERIAL hardware)
|
||||
elseif(PB_SERIAL STREQUAL "hardware")
|
||||
set(PB_USART 0)
|
||||
endif()
|
||||
|
||||
set(_serial_defines "")
|
||||
if(PB_SERIAL STREQUAL "hardware")
|
||||
if(PB_USART EQUAL 1 AND NOT _usart1)
|
||||
message(FATAL_ERROR "pureboot_add_loader(${name}): ${LIBAVR_MCU} has no USART1")
|
||||
elseif(NOT _usart)
|
||||
message(FATAL_ERROR "pureboot_add_loader(${name}): ${LIBAVR_MCU} has no hardware USART")
|
||||
endif()
|
||||
set(_serial_defines PUREBOOT_USART=${PB_USART})
|
||||
set(_link usart${PB_USART})
|
||||
else()
|
||||
if(PB_SERIAL STREQUAL "auto")
|
||||
if(_usart AND (PB_RX OR PB_TX))
|
||||
message(WARNING "pureboot_add_loader(${name}): RX/TX apply to the software UART, "
|
||||
"which auto does not pick on ${LIBAVR_MCU} — SERIAL software to force it")
|
||||
endif()
|
||||
if(_usart)
|
||||
set(_link usart0)
|
||||
else()
|
||||
set(PB_SERIAL software)
|
||||
endif()
|
||||
endif()
|
||||
if(PB_SERIAL STREQUAL "software")
|
||||
if(NOT PB_RX)
|
||||
set(PB_RX pb0)
|
||||
endif()
|
||||
if(NOT PB_TX)
|
||||
set(PB_TX pb1)
|
||||
endif()
|
||||
foreach(_pin ${PB_RX} ${PB_TX})
|
||||
if(NOT _pin MATCHES "^p[a-h][0-7]$")
|
||||
message(FATAL_ERROR "pureboot_add_loader(${name}): pin '${_pin}' is not of the form pb1")
|
||||
endif()
|
||||
endforeach()
|
||||
set(_serial_defines PUREBOOT_SOFT_SERIAL PUREBOOT_RX=${PB_RX} PUREBOOT_TX=${PB_TX})
|
||||
# The link spec a test harness drives a GPIO bridge with: sw:<RX>,<TX>
|
||||
# as the port letter and bit, the loader's own pin naming upcased.
|
||||
string(SUBSTRING ${PB_RX} 1 2 _rx_pin)
|
||||
string(SUBSTRING ${PB_TX} 1 2 _tx_pin)
|
||||
string(TOUPPER "sw:${_rx_pin},${_tx_pin}" _link)
|
||||
string(REPLACE "SW" "sw" _link ${_link})
|
||||
endif()
|
||||
endif()
|
||||
if(NOT PB_BAUD)
|
||||
if(PB_SERIAL STREQUAL "software")
|
||||
pureboot_default_baud(${PB_CLOCK} 1 PB_BAUD)
|
||||
else()
|
||||
pureboot_default_baud(${PB_CLOCK} 0 PB_BAUD)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set(_defines PUREBOOT_CLOCK_HZ=${PB_CLOCK} PUREBOOT_BAUD=${PB_BAUD} PUREBOOT_TIMEOUT=${PB_TIMEOUT}
|
||||
${_serial_defines})
|
||||
|
||||
add_executable(${name} ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/pureboot.cpp)
|
||||
target_link_libraries(${name} PRIVATE libavr)
|
||||
target_compile_definitions(${name} PRIVATE ${_defines})
|
||||
# Codegen shaping for the loader TU only, worth ~40 B on every chip and
|
||||
# what carries the far-flash 1284 build under 512. At -Os GCC otherwise
|
||||
# rewrites the byte-stream loops' counters into end-pointer forms that
|
||||
# cost registers (-fno-ivopts, -fno-split-wide-types), leaves register
|
||||
# pressure on the table with the default allocator
|
||||
# (-fira-algorithm=priority), and spends bytes on rewrites a
|
||||
# straight-line loader gains nothing from.
|
||||
target_compile_options(${name} PRIVATE
|
||||
-fno-ivopts -fira-algorithm=priority -fno-expensive-optimizations -fno-split-wide-types)
|
||||
target_link_options(${name} PRIVATE -nostartfiles -Wl,--section-start=.text=${_base_hex}
|
||||
-Wl,--defsym=pureboot_app=${_app} ${_wrap})
|
||||
add_custom_command(TARGET ${name} POST_BUILD COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${name}>)
|
||||
# The ELF is a container (symbols, section headers), never flashed; the
|
||||
# flashable forms sit beside it: .hex for a programmer, .bin (the slot's
|
||||
# bare bytes) for the host tool's raw path and --update-loader.
|
||||
add_custom_command(TARGET ${name} POST_BUILD
|
||||
COMMAND ${CMAKE_OBJCOPY} -O ihex -R .eeprom
|
||||
$<TARGET_FILE:${name}> $<TARGET_FILE:${name}>.hex
|
||||
COMMAND ${CMAKE_OBJCOPY} -O binary -R .eeprom
|
||||
$<TARGET_FILE:${name}> $<TARGET_FILE:${name}>.bin)
|
||||
set_target_properties(${name} PROPERTIES PUREBOOT_HZ ${PB_CLOCK} PUREBOOT_BAUD ${PB_BAUD}
|
||||
PUREBOOT_LINK ${_link})
|
||||
endfunction()
|
||||
@@ -2,49 +2,115 @@
|
||||
|
||||
A serial bootloader on [libavr](https://git.blackmark.me/avr/libavr), pure by
|
||||
constraint: one C++ source, no inline assembly, no global register variables
|
||||
(attributes allowed), built for every chip libavr targets, **512 bytes on
|
||||
each** — 488 B on the ATtiny13A, 502 B on the ATtiny85, 504 B on the
|
||||
ATmega328P. The device speaks primitives; every composite — verify, erase,
|
||||
reset-vector surgery, updating the loader itself — lives in the host tool
|
||||
(`pureboot.py`).
|
||||
(attributes and compiler flags allowed), built for **every chip libavr
|
||||
targets — all 37 — in 512 bytes each**: 434 B on the tiny13s, 438–442 B on
|
||||
the tiny25/45/85, 412–452 B across the megas, and 506 B on the
|
||||
ATmega1284/1284P, whose far-flash machinery (ELPM reads, RAMPZ page commands,
|
||||
word-addressed wire) is the heaviest. Those are the stock deployments;
|
||||
choosing the software UART where the chip has a USART costs 8–46 B more (a
|
||||
bit-bang against a peripheral), which every chip still absorbs inside its
|
||||
slot — on the 1284s that means their 1 KiB boot sector, where the
|
||||
software-serial image lands at 546 B. Bringing the 1284's default build
|
||||
under 512 at all is what the loop-placement attributes on the byte streamers
|
||||
(`pureboot.cpp`) and the codegen flags on the loader TU (`CMakeLists.txt`)
|
||||
are for; measured against each chip's own budget the tightest is the
|
||||
ATmega328P, 50 B spare. Clock, baud, serial backend and
|
||||
pins are per-build configuration (below); the size matrix in the test suite
|
||||
holds every combination inside its slot. The device speaks primitives; every
|
||||
composite — verify, erase, reset-vector surgery, updating the loader itself —
|
||||
lives in the host tool (`pureboot.py`).
|
||||
|
||||
The 1284s still *deploy* in a 1 KiB slot, their smallest boot sector being
|
||||
512 words; at 506 B the image would also fit the 644's
|
||||
two-512-byte-slots-per-boot-sector geometry.
|
||||
|
||||
The image is **position-independent**: control flow is PC-relative, the
|
||||
read/write paths take wire addresses, the write guard protects the 512-byte
|
||||
slot the code is *running* in (from the runtime return address), the info
|
||||
block is addressed from that same anchor, and the application jump is an
|
||||
indirect call to an absolute entry. The identical binary therefore runs from
|
||||
any 512-byte slot with every command intact — which makes pureboot **its own
|
||||
staging loader**: the host installs the same binary one slot below the
|
||||
resident, jumps into it, and lets it rewrite the resident. On the tinies the
|
||||
budget is 510, not 512: a slot's last word belongs to the host-managed
|
||||
trampoline (below).
|
||||
read/write paths take wire addresses, the write guard protects the slot the
|
||||
code is *running* in (from the runtime return address), the info block is
|
||||
addressed from that same anchor, and the application jump is an indirect
|
||||
call to an absolute entry. The identical binary therefore runs from any
|
||||
slot with every command intact — which makes pureboot **its own staging
|
||||
loader**: the host installs the same binary one slot below the resident,
|
||||
jumps into it, and lets it rewrite the resident. The slot is 512 bytes
|
||||
(1 KiB on the word-addressed large chips, matching their boot-sector
|
||||
minimum); on the tinies the budget is 510, not 512: a slot's last word
|
||||
belongs to the host-managed trampoline (below).
|
||||
|
||||
## Configuration
|
||||
|
||||
Every deployment axis is a build parameter, resolved by the CMake function
|
||||
`pureboot_add_loader()` (in `pureboot/CMakeLists.txt`) — the one way a
|
||||
loader target is created, by this repo's own build and by a downstream
|
||||
project alike:
|
||||
|
||||
| Argument | Meaning | Default |
|
||||
|---|---|---|
|
||||
| `CLOCK <hz>` | the clock the board runs | 16 MHz megas, 8 MHz t25/45/85, 9.6 MHz t13s |
|
||||
| `BAUD <bd>` | the wire rate | the ladder below |
|
||||
| `SERIAL auto\|hardware\|software` | the link backend | `auto`: the hardware USART where the chip has one |
|
||||
| `USART <n>` | the USART instance (x4 megas carry two) | 0 |
|
||||
| `RX <pin>`, `TX <pin>` | software-UART pins | `pb0`, `pb1` |
|
||||
| `TIMEOUT <s>` | the activation window | 8 |
|
||||
|
||||
The default baud is the fastest of 115200/57600/38400/19200/9600 the clock
|
||||
reaches within 2.5 % — the same U2X-included divisor search libavr's baud
|
||||
solver runs — and on a software build additionally within the polled
|
||||
receiver's 100-cycles-a-bit floor. 16 MHz lands 115200, 8 MHz 57600,
|
||||
1 MHz 9600. Whatever is picked or overridden is re-checked in the compile:
|
||||
an infeasible clock/baud/backend combination, or a USART the chip does not
|
||||
have, fails with a named static assert.
|
||||
|
||||
A downstream project brings its usual libavr setup (the `libavr` target,
|
||||
the chip via the `LIBAVR_MCU` toolchain preset), consumes this directory,
|
||||
and states its deployment — for example an ATmega328P on its shipped
|
||||
1 MHz fuses with the software UART on hand-picked pins:
|
||||
|
||||
```cmake
|
||||
FetchContent_Declare(bootloader GIT_REPOSITORY git@git.blackmark.me:avr/bootloader.git GIT_TAG main)
|
||||
FetchContent_MakeAvailable(bootloader)
|
||||
add_subdirectory(${bootloader_SOURCE_DIR}/pureboot pureboot)
|
||||
|
||||
pureboot_add_loader(myboot CLOCK 1000000 SERIAL software TX pb1 RX pb5)
|
||||
```
|
||||
|
||||
The function emits the ELF plus `myboot.hex` (the programmer artifact) and
|
||||
`myboot.bin` (the self-update image), prints the size, and stamps the
|
||||
resolved deployment on the target as the `PUREBOOT_HZ`, `PUREBOOT_BAUD`
|
||||
and `PUREBOOT_LINK` properties — what a flashing script or test harness
|
||||
needs to speak to the build. This exact example deployment runs the full
|
||||
protocol suite in CI (`pureboot.custom`).
|
||||
|
||||
## Link
|
||||
|
||||
The stock builds assume the family's natural deployment; any axis moves
|
||||
per build (above).
|
||||
|
||||
| Chip | Serial | Baud | Clock assumed |
|
||||
|---|---|---|---|
|
||||
| ATmega328P | USART0, RXD/TXD = PD0/PD1 | 115200 8N1 | 16 MHz crystal |
|
||||
| ATtiny85 | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 8 MHz internal RC |
|
||||
| ATtiny13A | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 9.6 MHz internal RC |
|
||||
| every ATmega | the hardware USART (USART0), RXD/TXD per pinout | 115200 8N1 | 16 MHz crystal |
|
||||
| ATtiny25/45/85 | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 8 MHz internal RC |
|
||||
| ATtiny13/13A | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 9.6 MHz internal RC |
|
||||
|
||||
The tiny RX pin has its pull-up enabled; TX idles high. All multi-byte
|
||||
quantities on the wire are little-endian.
|
||||
The software-UART RX pin has its pull-up enabled; TX idles high. All
|
||||
multi-byte quantities on the wire are little-endian.
|
||||
|
||||
## Activation
|
||||
|
||||
Reset enters the loader (BOOTRST on the mega, the patched reset vector on the
|
||||
tinies) — except a watchdog reset, which hands straight to the application
|
||||
(the application owns its watchdog; it must clear WDRF itself, which also
|
||||
releases the WDRF-forced WDE).
|
||||
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
|
||||
watchdog reset, which hands straight to the application (the application
|
||||
owns its watchdog; it must clear WDRF itself, which also releases the
|
||||
WDRF-forced WDE).
|
||||
|
||||
The host then has one activation window per awaited byte to knock: `p` then
|
||||
`b`. Each awaited byte gets a fresh window; any other byte is discarded and
|
||||
awaited again (line noise cannot lock the loader, only delay it). A window
|
||||
expiring with an idle line boots the application.
|
||||
|
||||
The window length is a compile-time constant — 8 s by default, another value
|
||||
via the `PUREBOOT_TIMEOUT` CMake cache variable — so the whole EEPROM belongs
|
||||
to the application; pureboot never uses it for its own state. Re-timing a
|
||||
The window length is a compile-time constant — 8 s by default, another
|
||||
value via `pureboot_add_loader(... TIMEOUT <s>)` (the stock target keeps
|
||||
the `PUREBOOT_TIMEOUT` cache variable) — so the whole EEPROM belongs to
|
||||
the application; pureboot never uses it for its own state. Re-timing a
|
||||
deployed loader is a self-update with a re-timed build (below).
|
||||
|
||||
## Session
|
||||
@@ -55,6 +121,11 @@ write to finish and sends the prompt `+` (0x2b) — the prompt is therefore
|
||||
also the completion ack of the previous command. A session is: await `+`,
|
||||
send a command, read its reply, repeat.
|
||||
|
||||
On chips whose flash exceeds 64 KiB (the 1284s — info-block flag bit 1) the
|
||||
`R`/`W` flash addresses are **word** addresses; everywhere else they are byte
|
||||
addresses (the 644s' 64 KiB is exactly the 16-bit byte space and stays
|
||||
byte-addressed). EEPROM addresses are always bytes, counts always bytes.
|
||||
|
||||
| Cmd | Arguments | Reply |
|
||||
|---|---|---|
|
||||
| `b` | — | the 12-byte info block |
|
||||
@@ -70,7 +141,20 @@ send a command, read its reply, repeat.
|
||||
then erases and programs; the address must be page-aligned. Pages inside the
|
||||
512-byte slot the loader is *running* in are drained but never programmed — a
|
||||
broken host cannot brick the running copy, and a staged copy may rewrite the
|
||||
resident slot. `w` is host-paced: send the next byte only after the previous
|
||||
resident slot.
|
||||
|
||||
The loader never clears the SPM buffer before a fill, so **one `W` may
|
||||
program the wrong bytes, and the host is what fixes it**. The buffer is
|
||||
write-once per word until cleared, and two things leave words in it: a
|
||||
refused page (drained, never programmed) and — where SPM runs from anywhere,
|
||||
the tinies and the m48s — an application that self-programmed before
|
||||
entering. The next `W` takes those stale words, and clears them: a page write
|
||||
auto-erases the buffer (§26.2.1; §19.2 on the tinies), so repeating it
|
||||
programs correctly. The host therefore verifies every page it writes and
|
||||
rewrites what comes back wrong (three retries, then it stops); a host that
|
||||
programs without reading back cannot trust the first `W` after either event.
|
||||
|
||||
`w` is host-paced: send the next byte only after the previous
|
||||
byte's `+`. `F` returns the bytes in the hardware's Z order; on a chip
|
||||
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
|
||||
@@ -88,18 +172,53 @@ The info block (`b`):
|
||||
|---|---|
|
||||
| 0–2 | `'P'`, `'B'`, protocol version (1) |
|
||||
| 3–5 | device signature |
|
||||
| 6 | SPM page size in bytes |
|
||||
| 7–8 | loader base — application flash ends here |
|
||||
| 6 | SPM page size in bytes (0 means 256) |
|
||||
| 7–8 | loader base — application flash ends here (a word address when bit 1 is set) |
|
||||
| 9–10 | EEPROM size |
|
||||
| 11 | bit 0 set: host must patch the reset vector (no hardware boot section) |
|
||||
| 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 =
|
||||
write `0xff` (per page for flash, per byte for EEPROM).
|
||||
|
||||
## Deployment
|
||||
|
||||
**ATmega328P**: program the loader at 0x7e00 with an external programmer.
|
||||
Two fuse profiles, same binary:
|
||||
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**:
|
||||
it carries its own addresses and lands the loader in its top slot,
|
||||
touching nothing else. The **.bin is the self-update image** — the slot's
|
||||
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 from any
|
||||
position — the staging install and the word-0 redirect execute from
|
||||
copies outside page 0's slot, and a copy sitting in the staging slot
|
||||
itself is recognized as the installed staging copy and left in place (it
|
||||
streams the new resident like any staged copy, so an older build installs
|
||||
a newer one). `pureboot.rehome` is the acceptance test for both
|
||||
positions. Flashing the application afterwards overwrites the stale copy,
|
||||
vector surgery included.
|
||||
|
||||
**Boot-sectioned megas**: program the loader at `flash − slot` with an
|
||||
external programmer. Every such mega has a BOOTSZ step whose boot section
|
||||
is exactly the loader slot — 512 B, the second-smallest step on the 8 KiB
|
||||
and 16 KiB chips (m8, m88, m16, m168, m164), the smallest on the 32 KiB
|
||||
ones (m32, m328, m324); on the 1284s that step is the smallest, 512 words,
|
||||
which is why their slot is 1 KiB — so the ATmega328P profiles below apply
|
||||
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
|
||||
(512 words = 1 KiB) is exactly *two* 512-byte slots, so the resident and
|
||||
its staging slot both live inside the minimum section — self-update needs
|
||||
no fuse step up, and the standalone profile does not exist (reset lands at
|
||||
0xfc00, one erased slot below the loader: the loader-first walk built in).
|
||||
|
||||
ATmega328P profiles (addresses for its 32 KiB):
|
||||
|
||||
| BOOTSZ | BOOTRST | Behavior |
|
||||
|---|---|---|
|
||||
@@ -110,15 +229,18 @@ Two fuse profiles, same binary:
|
||||
Applications are flashed unmodified — word 0 stays the application's own
|
||||
reset vector, and the hand-over jumps to 0.
|
||||
|
||||
**Tinies** (no boot section): program the loader at `flash − 512`; erased
|
||||
flash below it walks up into the loader, so a virgin chip activates. When
|
||||
flashing an application the host performs reset-vector surgery: word 0 is
|
||||
rewritten to `rjmp` to the loader base, and the application's own entry is
|
||||
re-encoded as a trampoline `rjmp` in the word just below the loader
|
||||
(`base − 2`, where the hand-over jumps). Every other vector stays the
|
||||
application's. The patched page 0 and the trampoline page are written
|
||||
*first*, so from the first write on an interrupted flash still resets into
|
||||
the loader; an erase runs top-down for the same reason.
|
||||
**Patched-vector chips — the tinies and the m48s** (no boot section; the
|
||||
m48s' SPM runs from the entire flash, Atmel-8271 §26): program the loader
|
||||
at `flash − 512`; erased flash below it walks up into the loader, so a
|
||||
virgin chip activates. When flashing an application the host performs
|
||||
reset-vector surgery: word 0 is rewritten to `rjmp` to the loader base, and
|
||||
the application's own entry is re-encoded as a trampoline `rjmp` in the
|
||||
word just below the loader (`base − 2`, where the hand-over jumps). Every
|
||||
other vector stays the application's. The patched page 0 and the trampoline
|
||||
page are written *first*, so from the first write on an interrupted flash
|
||||
still resets into the loader; an erase runs top-down for the same reason.
|
||||
The m48s speak this profile over their hardware USART — no fuse preflight,
|
||||
BOOTRST does not exist there.
|
||||
|
||||
## Updating the loader
|
||||
|
||||
@@ -128,16 +250,29 @@ 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
|
||||
links the loader at its base inside an otherwise blank flash image:
|
||||
|
||||
1. The staging slot `[base−512, base)` is saved to a host-side state file
|
||||
(on the 1 KB tiny13A that is the whole application, vectors included).
|
||||
2. The resident installs the identical update image there. On the tinies the
|
||||
host composes the slot's last word — the same address as the resident's
|
||||
trampoline — as a jump to the resident base, so even an abandoned staging
|
||||
copy times out into a loader, never into garbage.
|
||||
The preflight refuses an image built for another chip: the info block
|
||||
embedded in every pureboot binary (signature, page size, loader base,
|
||||
EEPROM size, flags) must match the device's own, and the error names both.
|
||||
Die revisions share their base signature and geometry, so their images are
|
||||
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
|
||||
(on the 1 KB tiny13s that is the whole application, vectors included).
|
||||
2. The resident installs the identical update image there. On the
|
||||
patched-vector chips the host composes the slot's last word — the same
|
||||
address as the resident's trampoline — as a jump to the resident base,
|
||||
so even an abandoned staging copy times out into a loader, never into
|
||||
garbage. A loader already sitting whole in the staging slot (its info
|
||||
block in place, the slot unchanged since the update began) is left as
|
||||
the staging copy instead — rewriting it would only meet its own
|
||||
running-slot guard.
|
||||
3. `J` enters the staging copy, which rewrites the resident slot. On the
|
||||
t85 the host first re-aims word 0 at the staging copy, so a power loss
|
||||
mid-rewrite still resets into a loader; on the t13a the staging slot
|
||||
carries the reset vector itself.
|
||||
patched-vector chips whose staging slot sits away from page 0 the host
|
||||
first re-aims word 0 at the staging copy, so a power loss mid-rewrite
|
||||
still resets into a loader; on the tiny13s the staging slot carries the
|
||||
reset vector itself.
|
||||
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
|
||||
discarded.
|
||||
@@ -146,9 +281,10 @@ Every phase is idempotent and keyed off the actual flash state: re-running
|
||||
the same command after any interruption resumes and completes. The state
|
||||
file carries the only bytes not recoverable from the device; if it is lost
|
||||
mid-update the update still completes, and the staging region is restored by
|
||||
reflashing the application. The mega needs its fuses for the preflight
|
||||
(BOOTSZ gate, profile notes) — read from the device, or supplied with
|
||||
`--assume-fuses` where reading is impossible (simulators).
|
||||
reflashing the application. A boot-sectioned mega needs its fuses for the
|
||||
preflight (BOOTSZ gate, profile notes) — read from the device, or supplied
|
||||
with `--assume-fuses` where reading is impossible (simulators); the
|
||||
patched-vector chips need none.
|
||||
|
||||
## Host tool
|
||||
|
||||
@@ -168,25 +304,55 @@ update, flash (erase / program / read / verify), EEPROM (erase / program /
|
||||
read / verify) — then the loader hands over to the application; `--stay`
|
||||
keeps the session alive instead, and a later invocation reconnects into it
|
||||
(the knock converges there too). `--flash` and `--eeprom` verify by
|
||||
read-back unless `--no-verify`; images are raw binary, or Intel HEX by
|
||||
extension. `--force` overrides the refusable safety checks (today: flashing
|
||||
read-back unless `--no-verify`, and a flash page that reads back wrong is
|
||||
rewritten up to three times before the run stops — the loader leaves one
|
||||
recoverable way for a page to land wrong (see `W` above), and rewriting is
|
||||
what clears it. `--verify-flash` only reports. Images are raw binary, or
|
||||
Intel HEX by extension. `--force` overrides the refusable safety checks (today: flashing
|
||||
application data into a mega's reset walk region).
|
||||
|
||||
Readouts come one fact per line: `--info` prints the decoded info block
|
||||
field by field, `--fuses` each fuse byte on its own line — plus, on a
|
||||
boot-sectioned mega, the decoded meaning (where the BOOTSZ section starts,
|
||||
what BOOTRST does to reset). Transfers that take wire time — programming,
|
||||
reading, erasing, verifying, the update phases — draw a transient progress
|
||||
bar on stderr when it is a tty; logs and pipes see only the summary lines.
|
||||
`-v`/`--verbose` adds the decisions as they happen: knock counts, the
|
||||
programming plan (vector-surgery targets, skipped blank pages), update
|
||||
state handling and per-phase page counts.
|
||||
|
||||
## Tests
|
||||
|
||||
Per chip preset, `ctest` runs:
|
||||
`tools/check.sh` runs every chip's workflow (`tools/check.sh --full` adds
|
||||
the reflect-mode builds of libavr's spot set; `tools/make_presets.py`
|
||||
regenerates the presets). Per chip preset, `ctest` runs:
|
||||
|
||||
- `pureboot.size` — the 510-byte (tinies) / 512-byte (mega) budget;
|
||||
- `pureboot_*.size` — the size matrix: the serial backends × the clock
|
||||
ladder (1/8/16 MHz; the t13s' own RC menu), plus the USART1 build on the
|
||||
x4 chips — every configuration axis that could move the image, each
|
||||
variant against the same slot budget (pins are immediate operands and the
|
||||
timeout is a constant: size-neutral);
|
||||
- `pureboot.custom` (328P) — the configured-deployment acceptance test: the
|
||||
1 MHz software-serial TX=PB1/RX=PB5 build from the configuration example
|
||||
drives the full protocol suite through the runner's GPIO bridge, fixture
|
||||
application included;
|
||||
- `pureboot.usart1` (644A) — the same protocol suite over the second
|
||||
hardware USART: instance selection is compile-checked everywhere, but
|
||||
only a live session proves the loader polls the USART it claims;
|
||||
- `pureboot.pi` — the position-independence lint: no absolute `jmp`/`call`
|
||||
in the image, the info block within its first 256 bytes;
|
||||
- `pureboot.planner` — the host tool's pure logic: programming orders and
|
||||
their recovery properties, the surgery, the staging composition, the
|
||||
boot-fuse decode, and the update preflight's error/warning matrix over
|
||||
synthetic fuse bytes;
|
||||
boot-fuse decode, the update preflight's error/warning matrix over
|
||||
synthetic fuse bytes, and the repairing verify against a fake device — one
|
||||
bad write repaired in a single rewrite, a page that never comes good
|
||||
stopping after exactly three;
|
||||
- `pureboot.protocol` — end to end against a simavr device
|
||||
(`test/pureboot_device.c` — the mega's USART as a pty; on the tinies a
|
||||
cycle-timed GPIO⇄pty bridge for the software UART, plus the SPM/NVM module
|
||||
simavr's tiny cores lack) driven by the real host tool through
|
||||
(`test/pureboot_device.c` — a hardware USART as a pty, or a cycle-timed
|
||||
GPIO⇄pty bridge for a software-UART build, selected with `-l` to match
|
||||
the loader's link; plus the SPM/NVM module simavr's tiny cores lack)
|
||||
driven by the real host tool through
|
||||
knock-from-reset, program + verify of both memories, session reconnect, an
|
||||
external reset through the patched vector, and the hand-over to a fixture
|
||||
application whose banner proves the launch — cross-checked against the
|
||||
@@ -195,11 +361,19 @@ Per chip preset, `ctest` runs:
|
||||
- `pureboot.reloc` — the identical image installed one slot below the
|
||||
resident serves the complete command set from there (the
|
||||
position-independence acceptance test);
|
||||
- `pureboot.dirty` (328P) — entering the loader from a running application
|
||||
with no reset between, over an SPM page buffer the fixture deliberately
|
||||
dirtied: the case the loader declines to guard against. A bare verify must
|
||||
see the corruption, the repairing verify must fix it in one rewrite, and a
|
||||
plain verify afterwards must pass. On the boot-sectioned megas hardware
|
||||
forbids the state outright (SPM runs only from the boot section, and reset
|
||||
erases the buffer), but simavr dispatches SPM from anywhere — which is what
|
||||
makes the path constructible at all;
|
||||
- `pureboot.update` — the full `--update-loader` flow to a re-timed build,
|
||||
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 throughout.
|
||||
|
||||
`size`, `pi`, and `planner` are host logic and run anywhere; the three
|
||||
`size`, `pi`, and `planner` are host logic and run anywhere; the
|
||||
simulator-driven targets need simavr and a pty, so they are POSIX-only —
|
||||
on Windows the tool is exercised against real hardware.
|
||||
|
||||
@@ -16,9 +16,10 @@
|
||||
// resident — how pureboot updates itself, host-driven, with no other
|
||||
// firmware involved.
|
||||
//
|
||||
// Entry: reset lands in avr::startup::entry below (BOOTRST on the mega; the
|
||||
// patched reset vector — or erased flash walking up into the loader — on the
|
||||
// tinies). A watchdog reset hands straight to the application. Otherwise the
|
||||
// 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.
|
||||
@@ -37,38 +38,51 @@ constexpr auto off = avr::irq::guard_policy::unused;
|
||||
|
||||
constexpr std::uint8_t ack = '+';
|
||||
|
||||
// Per-chip personality, from the chip database: the clocks the dogfood
|
||||
// boards run (16 MHz crystal on the mega, calibrated RC on the tinies) and
|
||||
// the device signature (compile-time data — the tiny13A cannot even read its
|
||||
// signature row from code).
|
||||
consteval avr::hertz_t clock()
|
||||
// Per-deployment personality, passed in by the build — pureboot_add_loader()
|
||||
// (the CMake function next to this file) resolves the defaults: the clock the
|
||||
// board actually runs, the wire baud, the serial backend and its pins. The
|
||||
// device signature needs no configuring — it comes from the chip database
|
||||
// (avr::hw::db.signature), the only universal source, since the tiny13A
|
||||
// cannot even read its signature row from code.
|
||||
#if !defined(PUREBOOT_CLOCK_HZ) || !defined(PUREBOOT_BAUD)
|
||||
#error \
|
||||
"PUREBOOT_CLOCK_HZ and PUREBOOT_BAUD select this build's clock and baud — create loader targets with pureboot_add_loader() (README.md)"
|
||||
#endif
|
||||
|
||||
using dev = avr::device<{.clock = avr::hertz_t{PUREBOOT_CLOCK_HZ}}>;
|
||||
constexpr avr::baud_t wire_baud{PUREBOOT_BAUD};
|
||||
|
||||
// The watchdog reset flag's home: MCUSR, or the classic megas' MCUCSR.
|
||||
consteval std::int16_t wdrf_field()
|
||||
{
|
||||
if (avr::hw::db.name == "ATtiny13A")
|
||||
return 9.6_MHz;
|
||||
if (avr::hw::db.name == "ATtiny85")
|
||||
return 8_MHz;
|
||||
return 16_MHz;
|
||||
auto reg = std::string_view{avr::hw::db.regs[static_cast<std::size_t>(avr::power::detail::reset_reg())].name};
|
||||
return avr::hw::db.field_index(reg, "WDRF");
|
||||
}
|
||||
|
||||
consteval std::array<std::uint8_t, 3> signature()
|
||||
{
|
||||
if (avr::hw::db.name == "ATtiny13A")
|
||||
return {0x1e, 0x90, 0x07};
|
||||
if (avr::hw::db.name == "ATtiny85")
|
||||
return {0x1e, 0x93, 0x0b};
|
||||
return {0x1e, 0x95, 0x0f};
|
||||
}
|
||||
|
||||
using dev = avr::device<{.clock = clock()}>;
|
||||
|
||||
// Geometry: the resident loader owns the top 512 bytes of flash; the word
|
||||
// below it is the trampoline (the application's relocated reset vector) on
|
||||
// chips without a hardware boot section. The RWWSRE bit marks a separate
|
||||
// boot section — on classic AVR the two capabilities coincide.
|
||||
constexpr std::uint16_t boot_bytes = 512;
|
||||
constexpr std::uint16_t base = static_cast<std::uint16_t>(spm::flash_bytes - boot_bytes);
|
||||
// Geometry: the resident loader owns the top slot of flash — 512 bytes,
|
||||
// except on the >64 KiB chips whose own smallest boot sector is 1 KiB (the
|
||||
// 1284s): there the slot is 1 KiB, matching the hardware boundary the
|
||||
// 512-byte figure comes from everywhere else. The word below the slot is
|
||||
// the trampoline (the application's relocated reset vector) on chips
|
||||
// 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.
|
||||
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 bool boot_section = avr::hw::db.field_index("SPMCSR", "RWWSRE") >= 0;
|
||||
constexpr bool boot_section = avr::hw::curated::has_boot_section();
|
||||
|
||||
// Past 64 KiB a byte address no longer fits the wire's 16 bits, so on the
|
||||
// large chips every flash address on the wire — and all slot arithmetic —
|
||||
// is a word address instead ('J' always was one). A slot spans the same
|
||||
// wire-high-byte pair in either unit (512 B = 2 x 256 bytes, 1 KiB =
|
||||
// 2 x 256 words), so the slot index is the high byte with its low bit
|
||||
// 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
|
||||
// may override it): the whole EEPROM belongs to the application, and
|
||||
@@ -78,52 +92,55 @@ constexpr bool boot_section = avr::hw::db.field_index("SPMCSR", "RWWSRE") >= 0;
|
||||
#endif
|
||||
constexpr std::uint8_t timeout_seconds = PUREBOOT_TIMEOUT;
|
||||
|
||||
// The 12-byte info block the host reads with the 'b' command; flash-resident
|
||||
// (there is no crt to copy a .data image).
|
||||
inline constexpr std::array<std::uint8_t, 12> info_data = {
|
||||
// The 12-byte info block the host reads with the 'b' command, flash-resident
|
||||
// through flash_table (there is no crt to copy a .data image, and its storage
|
||||
// carries the word alignment 'b' needs to halve the address on the large
|
||||
// chips). The page byte is the wire count convention: 0 means 256.
|
||||
inline constexpr avr::flash_table<std::array<std::uint8_t, 12>{
|
||||
'P',
|
||||
'B',
|
||||
1, // magic, protocol version
|
||||
signature()[0],
|
||||
signature()[1],
|
||||
signature()[2],
|
||||
avr::hw::db.signature[0],
|
||||
avr::hw::db.signature[1],
|
||||
avr::hw::db.signature[2],
|
||||
static_cast<std::uint8_t>(page),
|
||||
base & 0xff,
|
||||
base >> 8, // app flash ends here; resident loader base
|
||||
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,
|
||||
boot_section ? 0 : 1, // bit 0: host must patch the reset vector (no hardware boot section)
|
||||
};
|
||||
using info = avr::flash_table<info_data>;
|
||||
// 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)),
|
||||
}>
|
||||
info_data;
|
||||
|
||||
// The serial link: the hardware USART where the chip has one, the polled
|
||||
// software UART (no vector — the table belongs to the application) on PB0/PB1
|
||||
// elsewhere. Both are class templates on the clock so only the selected
|
||||
// backend is ever instantiated. pending() is the cheap line test the
|
||||
// activation window polls; rx() then picks the byte up; drain() holds until
|
||||
// the last transmitted frame is fully on the wire (the jump hand-over must
|
||||
// not let the target's re-init clip the ack).
|
||||
template <avr::hertz_t C>
|
||||
consteval std::int16_t rxc_field()
|
||||
{
|
||||
return avr::hw::db.field_index("UCSR0A", "RXC0");
|
||||
}
|
||||
|
||||
template <avr::hertz_t C>
|
||||
consteval std::int16_t txc_field()
|
||||
{
|
||||
return avr::hw::db.field_index("UCSR0A", "TXC0");
|
||||
}
|
||||
|
||||
template <avr::hertz_t C>
|
||||
consteval std::int16_t status_reg()
|
||||
{
|
||||
return avr::hw::db.reg_index("UCSR0A");
|
||||
}
|
||||
// The serial link. PUREBOOT_USART forces a hardware USART instance,
|
||||
// PUREBOOT_SOFT_SERIAL the polled software UART (no vector — the table
|
||||
// belongs to the application) on PUREBOOT_RX/PUREBOOT_TX; with neither, the
|
||||
// chip's first USART where it has one and the software UART elsewhere. Both
|
||||
// are class templates on the clock so only the selected backend is ever
|
||||
// instantiated. pending() is the cheap line test the activation window
|
||||
// polls; rx() then picks the byte up; drain() holds until the last
|
||||
// transmitted frame is fully on the wire (the jump hand-over must not let
|
||||
// the target's re-init clip the ack).
|
||||
#if defined(PUREBOOT_SOFT_SERIAL) && defined(PUREBOOT_USART)
|
||||
#error "PUREBOOT_SOFT_SERIAL and PUREBOOT_USART select opposing serial backends"
|
||||
#endif
|
||||
#if !defined(PUREBOOT_RX)
|
||||
#define PUREBOOT_RX pb0
|
||||
#endif
|
||||
#if !defined(PUREBOOT_TX)
|
||||
#define PUREBOOT_TX pb1
|
||||
#endif
|
||||
#if defined(PUREBOOT_USART)
|
||||
constexpr char usart_digit = '0' + PUREBOOT_USART;
|
||||
#else
|
||||
constexpr char usart_digit = '0';
|
||||
#endif
|
||||
|
||||
template <avr::hertz_t C>
|
||||
struct hardware_link {
|
||||
using uart = avr::uart::usart0<C, {.baud = 115200_Bd, .max_baud_error = 2.5_pct}>;
|
||||
using uart = avr::uart::usart<usart_digit, C, {.baud = wire_baud, .max_baud_error = 2.5_pct}>;
|
||||
|
||||
// The compiled idle poll: lds UCSR0A (2), sbrc skipping the exit (2),
|
||||
// sbiw + sbci + sbci + brne (6).
|
||||
@@ -136,7 +153,7 @@ struct hardware_link {
|
||||
|
||||
static bool pending()
|
||||
{
|
||||
return avr::hw::field_impl<rxc_field<C>()>::test();
|
||||
return uart::rx_ready();
|
||||
}
|
||||
|
||||
static std::uint8_t rx()
|
||||
@@ -151,22 +168,14 @@ struct hardware_link {
|
||||
|
||||
static void drain()
|
||||
{
|
||||
// write() leaves the byte draining behind it. Clear a stale TXC0
|
||||
// first (W1C by writing the sampled status back — the store a hand
|
||||
// assembler writes, keeping U2X0), then wait for the fresh
|
||||
// completion; with a byte still ahead in the shifter TXC0 cannot
|
||||
// re-set until the last pending byte has fully left.
|
||||
using status = avr::hw::reg_impl<status_reg<C>()>;
|
||||
status::write(status::read());
|
||||
while (!avr::hw::field_impl<txc_field<C>()>::test()) {
|
||||
}
|
||||
uart::drain();
|
||||
}
|
||||
};
|
||||
|
||||
template <avr::hertz_t C>
|
||||
struct software_link {
|
||||
using rx_t = avr::uart::software_rx_polled<C, avr::pb0, 57600_Bd>;
|
||||
using tx_t = avr::uart::software_tx<C, avr::pb1, 57600_Bd>;
|
||||
using rx_t = avr::uart::software_rx_polled<C, avr::PUREBOOT_RX, wire_baud>;
|
||||
using tx_t = avr::uart::software_tx<C, avr::PUREBOOT_TX, wire_baud>;
|
||||
|
||||
// The compiled idle poll: sbis skipping the exit (2), sbiw + sbci +
|
||||
// sbci + brne (6).
|
||||
@@ -179,7 +188,7 @@ struct software_link {
|
||||
|
||||
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()
|
||||
@@ -198,7 +207,15 @@ struct software_link {
|
||||
}
|
||||
};
|
||||
|
||||
using link = std::conditional_t<avr::hw::db.has_reg("UDR0"), hardware_link<dev::clock>, software_link<dev::clock>>;
|
||||
#if 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>;
|
||||
#elif defined(PUREBOOT_SOFT_SERIAL)
|
||||
using link = software_link<dev::clock>;
|
||||
#else
|
||||
using link =
|
||||
std::conditional_t<avr::uart::has_usart<usart_digit>(), hardware_link<dev::clock>, software_link<dev::clock>>;
|
||||
#endif
|
||||
|
||||
// The application's entry, an absolute address the linker pins (--defsym in
|
||||
// CMakeLists.txt): 0x0000 on the mega (word 0 stays the application's own
|
||||
@@ -247,27 +264,53 @@ std::uint8_t rx_deadline()
|
||||
return link::rx();
|
||||
}
|
||||
|
||||
std::uint16_t rx16()
|
||||
// Inlined into its call sites: reading two bytes across a call otherwise
|
||||
// strands the first in a call-saved register the caller must push/pop; folded
|
||||
// into the (noreturn) command loop that cost disappears.
|
||||
[[gnu::always_inline]] inline std::uint16_t rx16()
|
||||
{
|
||||
std::uint16_t low = link::rx();
|
||||
return static_cast<std::uint16_t>(low | (link::rx() << 8));
|
||||
}
|
||||
|
||||
const std::uint8_t *flash_ptr(std::uint16_t address)
|
||||
{
|
||||
return reinterpret_cast<const std::uint8_t *>(address);
|
||||
}
|
||||
|
||||
// The streamers take the count in the wire's 8-bit form: 0 means 256.
|
||||
// send_flash stays out of line: its two callers ('b' and 'R') otherwise each
|
||||
// inline a private copy of the loop.
|
||||
[[gnu::noinline]] void send_flash(std::uint16_t address, std::uint8_t count)
|
||||
//
|
||||
// Two functions, because they want opposite placement and placement is an
|
||||
// attribute: the byte-addressed loop is small enough to inline into both
|
||||
// callers, the word-addressed one stays out of line but flattened — a call to
|
||||
// the transmit inside it would strand the 24-bit cursor in callee-saved
|
||||
// registers. `word_flash` picks at the call site.
|
||||
[[maybe_unused, gnu::always_inline]] inline void send_flash_near(std::uint16_t address, std::uint8_t count)
|
||||
{
|
||||
do
|
||||
link::tx(avr::flash_load(flash_ptr(address++)));
|
||||
link::tx(avr::flash_load(reinterpret_cast<const std::uint8_t *>(address++)));
|
||||
while (--count);
|
||||
}
|
||||
|
||||
// The 24-bit cursor as the machine holds it: the RAMPZ byte and a 16-bit Z,
|
||||
// carried explicitly (the reassembled 32-bit address folds away inside the
|
||||
// inlined far load).
|
||||
[[maybe_unused, gnu::flatten, gnu::noinline]] void send_flash_far(std::uint16_t address, std::uint8_t count)
|
||||
{
|
||||
std::uint8_t rampz = static_cast<std::uint8_t>(address >> 15);
|
||||
std::uint16_t z = static_cast<std::uint16_t>(address << 1);
|
||||
do {
|
||||
link::tx(avr::flash_load_far<std::uint8_t>((static_cast<std::uint32_t>(rampz) << 16) | z));
|
||||
// The protocol never reads across 64 KiB, but carrying the wrap is
|
||||
// smaller than the flat 32-bit cursor GCC builds without it.
|
||||
if (++z == 0)
|
||||
++rampz;
|
||||
} while (--count);
|
||||
}
|
||||
|
||||
[[gnu::always_inline]] inline void send_flash(std::uint16_t address, std::uint8_t count)
|
||||
{
|
||||
if constexpr (word_flash)
|
||||
send_flash_far(address, count);
|
||||
else
|
||||
send_flash_near(address, count);
|
||||
}
|
||||
|
||||
void send_eeprom(std::uint16_t address, std::uint8_t count)
|
||||
{
|
||||
do
|
||||
@@ -293,20 +336,43 @@ void store_eeprom(std::uint16_t address, std::uint8_t count)
|
||||
// itself. `slot_high` is the high byte of that running slot's base (run()
|
||||
// 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 address, std::uint8_t slot_high)
|
||||
// updates itself.
|
||||
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
|
||||
// refused page's drained data must not linger for the next write:
|
||||
// discard the buffer up front — CTPB on the tinies; on the mega writing
|
||||
// RWWSRE aborts a pending load (§26.2.2).
|
||||
if constexpr (boot_section)
|
||||
spm::rww_enable<off>();
|
||||
else
|
||||
spm::clear_buffer<off>();
|
||||
// The address is the loop's only state: pages are aligned, so the walk
|
||||
// ends when the offset bits wrap back to zero.
|
||||
// No discard before the fill: the buffer is write-once per word
|
||||
// (§26.2.1), so filling over one a refused page or an application left
|
||||
// dirty programs stale words — but a page write auto-erases the buffer
|
||||
// (§26.2.1; §19.2 on the tinies), so that write clears the condition and
|
||||
// the host's read-back rewrites the page.
|
||||
|
||||
// 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
|
||||
// byte cursor once, and their 256-byte page makes its low byte the whole
|
||||
// in-page offset. The slot index is one high byte of the wire address —
|
||||
// two values on byte-addressed chips (the & ~1), bits 16:9 re-packed on
|
||||
// the large ones.
|
||||
spm::flash_address_t address;
|
||||
std::uint8_t page_high;
|
||||
if constexpr (word_flash) {
|
||||
// Pages are aligned, so one page never crosses a 64 KiB boundary:
|
||||
// RAMPZ is a per-page constant and the fill cursor is a 16-bit Z
|
||||
// whose low byte is the whole in-page offset (256-byte pages). The
|
||||
// slot index is simply the wire word address's high byte.
|
||||
const std::uint8_t rampz = static_cast<std::uint8_t>(wire_address >> 15);
|
||||
const std::uint16_t z0 = static_cast<std::uint16_t>(wire_address << 1);
|
||||
std::uint16_t z = z0;
|
||||
do {
|
||||
std::uint8_t low = link::rx();
|
||||
std::uint8_t high = link::rx();
|
||||
spm::fill<off>((static_cast<spm::flash_address_t>(rampz) << 16) | z,
|
||||
static_cast<std::uint16_t>(low | (high << 8)));
|
||||
z += 2;
|
||||
} while (static_cast<std::uint8_t>(z));
|
||||
address = (static_cast<spm::flash_address_t>(rampz) << 16) | z0;
|
||||
page_high = static_cast<std::uint8_t>(wire_address >> 8) & 0xfe;
|
||||
} else {
|
||||
address = static_cast<spm::flash_address_t>(wire_address);
|
||||
do {
|
||||
std::uint8_t low = link::rx();
|
||||
std::uint8_t high = link::rx();
|
||||
@@ -314,19 +380,24 @@ void program_flash(std::uint16_t address, std::uint8_t slot_high)
|
||||
address += 2;
|
||||
} while (static_cast<std::uint8_t>(address) & (page - 1));
|
||||
address -= 2; // back inside the page — erase and write ignore the word bits
|
||||
const std::uint8_t page_high = static_cast<std::uint8_t>(address >> 8) & 0xfe;
|
||||
page_high = static_cast<std::uint8_t>(address >> 8) & 0xfe;
|
||||
}
|
||||
if (page_high != slot_high) {
|
||||
// The tinies halt the CPU through the erase and the write, so only
|
||||
// the mega — running on while its RWW section programs — waits.
|
||||
// The tinies and the m48s halt the CPU through the erase and the
|
||||
// write, so only the boot-sectioned megas — running on while their
|
||||
// RWW section programs — wait.
|
||||
spm::erase_page<off>(address);
|
||||
if constexpr (boot_section)
|
||||
spm::wait();
|
||||
spm::write_page<off>(address);
|
||||
if constexpr (boot_section) {
|
||||
if constexpr (boot_section)
|
||||
spm::wait();
|
||||
}
|
||||
// The megas program with their RWW section disabled; reads need it back
|
||||
// on. The same store discards the buffer (§26.2.2), so they never meet
|
||||
// the stale-word case above. boot_section implies an RWW section.
|
||||
if constexpr (boot_section)
|
||||
spm::rww_enable<off>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The four fuse/lock bytes in the hardware's own Z order: low, lock,
|
||||
@@ -344,18 +415,23 @@ void send_fuses()
|
||||
{
|
||||
// A watchdog reset belongs to the application (whose watchdog stays
|
||||
// forced on until it clears WDRF) — no activation window in its way.
|
||||
if (avr::hw::mcusr::wdrf.test())
|
||||
// The flag register is MCUSR, or the classic megas' MCUCSR.
|
||||
if (avr::hw::field_impl<wdrf_field()>::test())
|
||||
run_app();
|
||||
|
||||
link::init();
|
||||
|
||||
// The high byte of the 512-byte-aligned base this copy runs at: the word
|
||||
// return address's high byte is the byte address >> 9 (the slot index),
|
||||
// doubled back into address terms. program_flash refuses this one slot
|
||||
// and the info block is addressed from it, so both follow wherever the
|
||||
// code was flashed.
|
||||
const std::uint8_t slot_high =
|
||||
static_cast<std::uint8_t>((reinterpret_cast<std::uint16_t>(__builtin_return_address(0)) >> 8) << 1);
|
||||
// The high byte of the 512-byte-aligned base this copy runs at: the
|
||||
// return address is a word address, whose high byte is the 256-word slot
|
||||
// index — on byte-addressed chips doubled back into byte terms.
|
||||
// program_flash refuses this one slot and the info block is addressed
|
||||
// from it, so both follow wherever the code was flashed. The high byte is
|
||||
// spelled as byteswap's low byte: the builtin's value is itself built by
|
||||
// swapping the two stacked bytes, and the double swap folds to the single
|
||||
// byte pick a hand assembler writes — `>> 8` leaves the swap materialized.
|
||||
const std::uint16_t ra_words = reinterpret_cast<std::uint16_t>(__builtin_return_address(0));
|
||||
const std::uint8_t ra_high = static_cast<std::uint8_t>(std::byteswap(ra_words));
|
||||
const std::uint8_t slot_high = word_flash ? ra_high & 0xfe : static_cast<std::uint8_t>(ra_high << 1);
|
||||
|
||||
// The knock: 'p' then 'b', each under a fresh window; any other byte is
|
||||
// line noise and waits again. Falling out of a window runs the app.
|
||||
@@ -372,11 +448,15 @@ void send_fuses()
|
||||
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 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 16-bit address is ever materialized.
|
||||
const std::uint8_t low = static_cast<std::uint8_t>(reinterpret_cast<std::uint16_t>(info::storage.data()));
|
||||
send_flash(std::bit_cast<std::uint16_t>(std::array{low, slot_high}), info::size());
|
||||
// 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. Composed from the two
|
||||
// bytes — the high half is runtime data, so no absolute address
|
||||
// is ever materialized.
|
||||
const auto link_low = reinterpret_cast<std::uint16_t>(info_data.storage.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(static_cast<std::uint16_t>(low | (slot_high << 8)), static_cast<std::uint8_t>(info_data.size()));
|
||||
break;
|
||||
}
|
||||
case 'J': { // jump to a wire word address: hand-over and staging transfer
|
||||
|
||||
@@ -36,13 +36,60 @@ else:
|
||||
|
||||
PROMPT = b"+"
|
||||
PROTOCOL_VERSION = 1
|
||||
SLOT = 512 # the loader slot size; also the self-update staging distance
|
||||
SLOT = 512 # the loader slot on byte-addressed chips; word-addressed ones (>64 KiB) use 1 KiB — their own smallest boot sector
|
||||
RETRIES = 3 # rewrites of a page that reads back wrong, before the run stops
|
||||
|
||||
VERBOSE = False
|
||||
|
||||
|
||||
def verbose(message):
|
||||
"""Detail printed only under --verbose: decisions and derived facts, not
|
||||
per-byte chatter — the progress bar carries the bulk transfers."""
|
||||
if VERBOSE:
|
||||
print(f" {message}")
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Progress:
|
||||
"""A transient in-place bar on stderr for the operations that take wire
|
||||
time. Drawn only when stderr is a tty — logs, pipes and the test harness
|
||||
see nothing — and erased once done; the summary line each operation
|
||||
prints afterwards is the persistent record. A zero total (or no label)
|
||||
disables it, so callers can pass one through unconditionally."""
|
||||
|
||||
def __init__(self, label, total, unit="pages"):
|
||||
self.label, self.total, self.unit = label, total, unit
|
||||
self.done = 0
|
||||
self.width = 0
|
||||
self.live = bool(label) and total > 0 and sys.stderr.isatty()
|
||||
self._draw()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc):
|
||||
if self.live:
|
||||
sys.stderr.write("\r" + " " * self.width + "\r")
|
||||
sys.stderr.flush()
|
||||
|
||||
def step(self, n=1):
|
||||
self.done += n
|
||||
self._draw()
|
||||
|
||||
def _draw(self):
|
||||
if not self.live:
|
||||
return
|
||||
bar = 24 * self.done // self.total
|
||||
line = (f"{self.label:<16} [{'#' * bar}{'-' * (24 - bar)}] "
|
||||
f"{100 * self.done // self.total:3d}% {self.done}/{self.total} {self.unit}")
|
||||
self.width = max(self.width, len(line))
|
||||
sys.stderr.write("\r" + line)
|
||||
sys.stderr.flush()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- serial ---
|
||||
|
||||
|
||||
@@ -270,12 +317,17 @@ class Info:
|
||||
raise Error(f"protocol version {raw[2]}, tool speaks {PROTOCOL_VERSION}")
|
||||
self.raw = bytes(raw)
|
||||
self.signature = raw[3:6]
|
||||
self.page = raw[6]
|
||||
self.base = raw[7] | (raw[8] << 8)
|
||||
self.eeprom_size = raw[9] | (raw[10] << 8)
|
||||
self.page = raw[6] or 256 # the wire count convention: 0 means 256
|
||||
self.patch_vector = bool(raw[11] & 1)
|
||||
self.flash_size = self.base + SLOT
|
||||
self.stage = self.base - SLOT # where a staging copy of the loader goes
|
||||
# Large chips speak word addresses for flash (bit 1); the host keeps
|
||||
# every address in bytes and converts at the wire.
|
||||
self.word_flash = bool(raw[11] & 2)
|
||||
scale = 2 if self.word_flash else 1
|
||||
self.base = (raw[7] | (raw[8] << 8)) * scale
|
||||
self.eeprom_size = raw[9] | (raw[10] << 8)
|
||||
self.slot = 1024 if self.word_flash else SLOT
|
||||
self.flash_size = self.base + self.slot
|
||||
self.stage = self.base - self.slot # where a staging copy of the loader goes
|
||||
# The hand-over target, as the word address 'J' takes: the trampoline
|
||||
# below the loader (tinies), or word 0 (mega — the application's own
|
||||
# reset vector; BOOTRST re-vectors a reset into the loader instead).
|
||||
@@ -290,6 +342,23 @@ class Info:
|
||||
f"EEPROM {self.eeprom_size} B, {vector}"
|
||||
)
|
||||
|
||||
def lines(self):
|
||||
"""The info block as one fact per line — what --info prints."""
|
||||
if self.patch_vector:
|
||||
hand_over = f"host-patched reset vector, trampoline at {self.base - 2:#06x}"
|
||||
else:
|
||||
hand_over = "hardware boot section, jump to word 0"
|
||||
return (
|
||||
f"signature {' '.join(f'{b:02x}' for b in self.signature)}",
|
||||
f"flash {self.flash_size} B, {self.page} B pages"
|
||||
+ (", word-addressed wire" if self.word_flash else ""),
|
||||
f"application 0x0000..{self.base - 1:#06x} ({self.base} B)",
|
||||
f"loader {self.base:#06x} ({self.slot} B slot)",
|
||||
f"staging {self.stage:#06x}",
|
||||
f"EEPROM {self.eeprom_size} B",
|
||||
f"hand-over {hand_over}",
|
||||
)
|
||||
|
||||
|
||||
class Loader:
|
||||
"""A pureboot session. Between commands the loader has prompted `+` and
|
||||
@@ -307,8 +376,10 @@ class Loader:
|
||||
absorbs whatever they produced."""
|
||||
self.port.flush_input()
|
||||
deadline = time.monotonic() + wait
|
||||
knocks = 0
|
||||
while True:
|
||||
self.port.write(b"pb")
|
||||
knocks += 1
|
||||
if PROMPT in self.port.read_available(0.4):
|
||||
break
|
||||
if time.monotonic() > deadline:
|
||||
@@ -318,6 +389,7 @@ class Loader:
|
||||
self.port.write(b"b")
|
||||
self.info = Info(self.port.read_exact(12, 2.0))
|
||||
self._expect_prompt()
|
||||
verbose(f"loader answered knock {knocks}; info block read")
|
||||
return self.info
|
||||
|
||||
def _expect_prompt(self, timeout=2.0):
|
||||
@@ -331,28 +403,44 @@ class Loader:
|
||||
self._expect_prompt(timeout)
|
||||
return reply
|
||||
|
||||
def _stream_read(self, command, address, count):
|
||||
def _stream_read(self, command, address, count, address_scale=1):
|
||||
data = b""
|
||||
while count:
|
||||
chunk = min(count, 256)
|
||||
head = bytes((ord(command), address & 0xFF, address >> 8, chunk & 0xFF))
|
||||
wire = address // address_scale
|
||||
head = bytes((ord(command), wire & 0xFF, wire >> 8, chunk & 0xFF))
|
||||
data += self._command(head, chunk, 5.0)
|
||||
address += chunk
|
||||
count -= chunk
|
||||
return data
|
||||
|
||||
def read_flash(self, address, count):
|
||||
if not self.info.word_flash:
|
||||
return self._stream_read("R", address, count)
|
||||
# Word-addressed wire: widen to even bounds and never let one read
|
||||
# cross a 64 KiB boundary (the device holds RAMPZ for a whole run).
|
||||
start = address & ~1
|
||||
span = (address + count + 1 & ~1) - start
|
||||
data = b""
|
||||
at = start
|
||||
remaining = span
|
||||
while remaining:
|
||||
chunk = min(remaining, 0x10000 - (at & 0xFFFF))
|
||||
data += self._stream_read("R", at, chunk, address_scale=2)
|
||||
at += chunk
|
||||
remaining -= chunk
|
||||
return data[address - start : address - start + count]
|
||||
|
||||
def read_eeprom(self, address, count):
|
||||
return self._stream_read("r", address, count)
|
||||
|
||||
def write_page(self, address, data):
|
||||
assert len(data) == self.info.page and address % self.info.page == 0
|
||||
head = bytes((ord("W"), address & 0xFF, address >> 8))
|
||||
wire = address // (2 if self.info.word_flash else 1)
|
||||
head = bytes((ord("W"), wire & 0xFF, wire >> 8))
|
||||
self._command(head + data, 0, 2.0)
|
||||
|
||||
def write_eeprom(self, address, data):
|
||||
def write_eeprom(self, address, data, progress=None):
|
||||
offset = 0
|
||||
while offset < len(data):
|
||||
chunk = data[offset : offset + 256]
|
||||
@@ -361,6 +449,8 @@ class Loader:
|
||||
for byte in chunk:
|
||||
self.port.write(bytes((byte,)))
|
||||
self._expect_prompt() # per-byte ack: the write has begun
|
||||
if progress:
|
||||
progress.step()
|
||||
self._expect_prompt() # the next command prompt
|
||||
address += len(chunk)
|
||||
offset += len(chunk)
|
||||
@@ -466,6 +556,8 @@ def plan_flash(image, info):
|
||||
final += bytearray([0xFF] * (trampoline_page + page - len(final)))
|
||||
jump = rjmp_to(trampoline_word, entry, flash_words)
|
||||
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)}
|
||||
return pages
|
||||
@@ -495,14 +587,51 @@ def covered(pages, info, skip_blank):
|
||||
# ----------------------------------------------------------------- fuses ---
|
||||
|
||||
|
||||
def mega_boot(high_fuse):
|
||||
"""Decode the ATmega328P high fuse's boot configuration (DS40002061B
|
||||
§27.3, Table 27-13/27-16): BOOTSZ1:0 in bits 2:1 select the boot-section
|
||||
words, BOOTRST in bit 0 (programmed = 0) re-vectors reset to its start.
|
||||
Returns (bootrst_programmed, boot_section_start_byte)."""
|
||||
bootsz = (high_fuse >> 1) & 0x03
|
||||
words = {0b11: 256, 0b10: 512, 0b01: 1024, 0b00: 2048}[bootsz]
|
||||
return (high_fuse & 1) == 0, 0x8000 - words * 2
|
||||
# Per-chip boot fuse geometry, keyed by the signature's family/part bytes:
|
||||
# which byte of the 'F' reply (low, lock, extended, high) carries BOOTSZ/
|
||||
# BOOTRST, and the BOOTSZ->words ladder. A die revision shares its base
|
||||
# signature, so one row covers it. The m48s have no boot section and no
|
||||
# row — their info block says patch-vector and this table is never
|
||||
# consulted. Sources: Atmel-2486/2466/2503 (HIGH fuse), Atmel-2545/8271/
|
||||
# DS40002065 (x8: EXTENDED, except the m328s' HIGH), Atmel-8272/8011/2593/
|
||||
# 42719 (x4: HIGH).
|
||||
_LADDER_128 = {0b11: 128, 0b10: 256, 0b01: 512, 0b00: 1024}
|
||||
_LADDER_256 = {0b11: 256, 0b10: 512, 0b01: 1024, 0b00: 2048}
|
||||
_LADDER_512 = {0b11: 512, 0b10: 1024, 0b01: 2048, 0b00: 4096}
|
||||
BOOT_FUSE = {
|
||||
bytes((0x93, 0x07)): (3, _LADDER_128), # m8/8A
|
||||
bytes((0x94, 0x03)): (3, _LADDER_128), # m16/16A
|
||||
bytes((0x95, 0x02)): (3, _LADDER_256), # m32/32A
|
||||
bytes((0x93, 0x0A)): (2, _LADDER_128), # m88/88A
|
||||
bytes((0x93, 0x0F)): (2, _LADDER_128), # m88P/88PA
|
||||
bytes((0x94, 0x06)): (2, _LADDER_128), # m168/168A
|
||||
bytes((0x94, 0x0B)): (2, _LADDER_128), # m168P/168PA
|
||||
bytes((0x95, 0x14)): (3, _LADDER_256), # m328
|
||||
bytes((0x95, 0x0F)): (3, _LADDER_256), # m328P
|
||||
bytes((0x94, 0x0F)): (3, _LADDER_128), # m164A
|
||||
bytes((0x94, 0x0A)): (3, _LADDER_128), # m164P/164PA
|
||||
bytes((0x95, 0x15)): (3, _LADDER_256), # m324A
|
||||
bytes((0x95, 0x08)): (3, _LADDER_256), # m324P
|
||||
bytes((0x95, 0x11)): (3, _LADDER_256), # m324PA
|
||||
bytes((0x96, 0x09)): (3, _LADDER_512), # m644/644A
|
||||
bytes((0x96, 0x0A)): (3, _LADDER_512), # m644P/644PA
|
||||
bytes((0x97, 0x06)): (3, _LADDER_512), # m1284
|
||||
bytes((0x97, 0x05)): (3, _LADDER_512), # m1284P
|
||||
}
|
||||
|
||||
|
||||
def mega_boot(info, fuse_bytes):
|
||||
"""Decode a mega's boot configuration from its fuses (the byte and the
|
||||
BOOTSZ ladder are per chip): BOOTSZ1:0 in bits 2:1 select the
|
||||
boot-section words, BOOTRST in bit 0 (programmed = 0) re-vectors reset
|
||||
to its start. Returns (bootrst_programmed, boot_section_start_byte)."""
|
||||
entry = BOOT_FUSE.get(bytes(info.signature[1:3]))
|
||||
if entry is None:
|
||||
raise Error(f"unknown mega signature {info.signature.hex()} — no boot fuse map")
|
||||
which, ladder = entry
|
||||
fuse = fuse_bytes[which]
|
||||
words = ladder[(fuse >> 1) & 0x03]
|
||||
return (fuse & 1) == 0, info.flash_size - words * 2
|
||||
|
||||
|
||||
# ---------------------------------------------------------- loader update ---
|
||||
@@ -534,12 +663,13 @@ def staging_content(image, info):
|
||||
that word, which for a staging copy is the slot's own last word: an rjmp
|
||||
to the resident base. The staging copy's fall-through and 'J'-free exit
|
||||
both land in a loader instead of garbage."""
|
||||
if len(image) > (SLOT - 2 if info.patch_vector else SLOT):
|
||||
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)))
|
||||
slot = info.slot
|
||||
if len(image) > (slot - 2 if info.patch_vector else slot):
|
||||
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)))
|
||||
if info.patch_vector:
|
||||
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)
|
||||
|
||||
|
||||
@@ -557,14 +687,13 @@ def update_preflight(image, info, fuse_bytes):
|
||||
if not info.patch_vector:
|
||||
if fuse_bytes is None:
|
||||
raise Error("a loader update on this chip needs its fuses — unreadable? pass --assume-fuses")
|
||||
high = fuse_bytes[3]
|
||||
bootrst, bls_start = mega_boot(high)
|
||||
bootrst, bls_start = mega_boot(info, fuse_bytes)
|
||||
if info.stage < bls_start:
|
||||
raise Error(
|
||||
f"cannot self-update: the staging slot {info.stage:#06x} lies below the "
|
||||
f"boot section ({bls_start:#06x}, high fuse {high:#04x}) where SPM is disabled "
|
||||
f"— a boot section of at least 1 KB (BOOTSZ) is required, and only an "
|
||||
f"external programmer can change fuses"
|
||||
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"required, and only an external programmer can change fuses"
|
||||
)
|
||||
if not bootrst:
|
||||
warnings.append(
|
||||
@@ -605,7 +734,7 @@ class UpdateState:
|
||||
self.data = {
|
||||
"signature": info.signature.hex(),
|
||||
"base": info.base,
|
||||
"staging": loader.read_flash(info.stage, SLOT).hex(),
|
||||
"staging": loader.read_flash(info.stage, info.slot).hex(),
|
||||
"page0": loader.read_flash(0, info.page).hex() if info.patch_vector else "",
|
||||
}
|
||||
with open(self.path, "w") as f:
|
||||
@@ -623,20 +752,42 @@ class UpdateState:
|
||||
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 —
|
||||
idempotent, so a resumed phase redoes only what an interruption left."""
|
||||
idempotent, so a resumed phase redoes only what an interruption left.
|
||||
A label puts the compare-and-program loop on the progress bar."""
|
||||
page = loader.info.page
|
||||
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
|
||||
with Progress(label, len(offsets)) as bar:
|
||||
for offset in offsets:
|
||||
want = content[offset : offset + page]
|
||||
if loader.read_flash(base + offset, page) != want:
|
||||
loader.write_page(base + offset, want)
|
||||
written += 1
|
||||
for at in range(0, len(content), 256):
|
||||
if loader.read_flash(base + at, min(256, len(content) - at)) != content[at : at + 256]:
|
||||
raise Error(f"verify failed at {base + at:#06x} after programming")
|
||||
bar.step()
|
||||
if label:
|
||||
verbose(f"{label}: {written} of {len(offsets)} pages differed")
|
||||
# Page-wise read-back with the same bounded repair as verify_pages: this
|
||||
# is the loader-update path, where a page left wrong is a half-written
|
||||
# loader slot.
|
||||
for retry in range(RETRIES + 1):
|
||||
bad = [
|
||||
offset
|
||||
for offset in range(0, len(content), page)
|
||||
if loader.read_flash(base + offset, len(content[offset : offset + page])) != content[offset : offset + page]
|
||||
]
|
||||
if not bad:
|
||||
break
|
||||
if retry == RETRIES:
|
||||
raise Error(
|
||||
f"verify failed at {base + bad[0]:#06x} after programming "
|
||||
f"(still wrong after {RETRIES} retries)"
|
||||
)
|
||||
for offset in bad:
|
||||
verbose(f"rewriting page {base + offset:#06x} (retry {retry + 1})")
|
||||
loader.write_page(base + offset, content[offset : offset + page])
|
||||
written += 1
|
||||
return written
|
||||
|
||||
|
||||
@@ -661,43 +812,69 @@ def op_update_loader(loader, wait, path, state_path, fuse_bytes):
|
||||
for warning in update_preflight(image, info, fuse_bytes):
|
||||
print(f"note: {warning}")
|
||||
staged = staging_content(image, info)
|
||||
resident = bytes(image) + bytes([0xFF] * (SLOT - len(image)))
|
||||
resident = bytes(image) + bytes([0xFF] * (info.slot - len(image)))
|
||||
page = info.page
|
||||
|
||||
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)
|
||||
|
||||
# Install the staging copy. On a chip whose staging slot starts at
|
||||
# address 0 (the 1 KB tiny13A), its first page carries the reset vector:
|
||||
# written last, so any earlier interruption still resets into the old
|
||||
# resident, and from then on resets enter the staging copy.
|
||||
order = list(range(0, SLOT, page))
|
||||
# Install the staging copy — unless a loader already sits whole in the
|
||||
# staging slot (a build programmed there by hand): that copy IS the
|
||||
# installed staging copy, and rewriting it would only trip its own
|
||||
# running-slot guard on the composed through-word. Any pureboot with
|
||||
# the device's own info block serves — the staged copy just streams
|
||||
# pages, so an older build installs a newer resident all the same. Two
|
||||
# checks make "already a loader" mean a *complete* one: the block must
|
||||
# sit where every image carries it (within the slot's first 256 bytes
|
||||
# — the build's position lint), matching the device's block byte for
|
||||
# byte, and the slot must be unchanged since this update began (the
|
||||
# state file's snapshot) — a resumed, half-written install differs
|
||||
# from its snapshot and takes the install path below, which completes
|
||||
# it page by page.
|
||||
current = loader.read_flash(info.stage, info.slot)
|
||||
staged_loader = image_info(current[:268])
|
||||
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:
|
||||
# On a chip whose staging slot starts at address 0 (the 1 KB
|
||||
# tiny13s), its first page carries the reset vector: written last,
|
||||
# so any earlier interruption still resets into the old resident,
|
||||
# and from then on resets enter the staging copy.
|
||||
order = list(range(0, info.slot, page))
|
||||
if info.stage == 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}")
|
||||
|
||||
# Enter it and let it rewrite the resident slot. Where a patched reset
|
||||
# vector routes through the resident (a tiny with the staging slot away
|
||||
# from page 0), word 0 is re-aimed at the staging copy around the
|
||||
# rewrite, so a power failure mid-rewrite still resets into a loader.
|
||||
verbose(f"entering the staging copy at {info.stage:#06x}")
|
||||
loader.enter_copy(info.stage, wait)
|
||||
redirect = info.patch_vector and info.stage != 0
|
||||
if redirect:
|
||||
verbose("word 0 re-aimed at the staging copy for the rewrite")
|
||||
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}")
|
||||
|
||||
# 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
|
||||
# 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)
|
||||
if redirect:
|
||||
verbose("word 0 restored")
|
||||
write_differing(loader, 0, state.page0)
|
||||
order = list(range(0, SLOT, page))
|
||||
order = list(range(0, info.slot, page))
|
||||
if info.stage == 0:
|
||||
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()
|
||||
print(f"loader updated: {len(image)} B at {info.base:#06x}, staging region restored")
|
||||
@@ -710,7 +887,7 @@ def check_walk_region(pages, info, fuse_bytes, force):
|
||||
Only checkable when the fuses are known (--fuses or --assume-fuses)."""
|
||||
if info.patch_vector or fuse_bytes is None:
|
||||
return
|
||||
bootrst, bls_start = mega_boot(fuse_bytes[3])
|
||||
bootrst, bls_start = mega_boot(info, fuse_bytes)
|
||||
if not bootrst or bls_start >= info.base:
|
||||
return
|
||||
overlap = [a for a in sorted(pages) if a >= bls_start and pages[a].count(0xFF) != len(pages[a])]
|
||||
@@ -732,48 +909,85 @@ def op_erase_flash(loader):
|
||||
is erased and the reset walk reaches the loader anyway."""
|
||||
blank = bytes([0xFF] * 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:
|
||||
loader.write_page(address, blank)
|
||||
bar.step()
|
||||
print(f"erase: {loader.info.base // loader.info.page} pages")
|
||||
|
||||
|
||||
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")
|
||||
|
||||
|
||||
def op_flash(loader, path, erase, verify, fuse_bytes=None, force=False):
|
||||
image = load_image(path)
|
||||
verbose(f"{path}: {len(image)} B image")
|
||||
pages = plan_flash(image, loader.info)
|
||||
check_walk_region(pages, loader.info, fuse_bytes, force)
|
||||
if erase:
|
||||
op_erase_flash(loader)
|
||||
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:
|
||||
loader.write_page(address, pages[address])
|
||||
bar.step()
|
||||
print(f"flash: {path}: {len(order)} pages")
|
||||
if verify:
|
||||
verify_pages(loader, pages)
|
||||
verify_pages(loader, pages, repair=True)
|
||||
|
||||
|
||||
def verify_pages(loader, pages):
|
||||
def verify_pages(loader, pages, repair=False):
|
||||
"""Read every page back and compare. With `repair`, a mismatched page is
|
||||
rewritten and re-read, up to RETRIES times before it is raised: a page
|
||||
filled over a dirty SPM buffer takes stale words, and the write that took
|
||||
them cleared the buffer, so one rewrite settles it. Anything still wrong
|
||||
after three is not that, and stops the run."""
|
||||
repaired = 0
|
||||
with Progress("verify", len(pages)) as bar:
|
||||
for address in sorted(pages):
|
||||
for retry in range(RETRIES + 1):
|
||||
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])
|
||||
raise Error(
|
||||
detail = (
|
||||
f"verify failed at {address + first:#06x}: "
|
||||
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):
|
||||
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):
|
||||
data = loader.read_flash(0, loader.info.base)
|
||||
data = read_progress(loader.read_flash, loader.info.base, "read flash")
|
||||
open(path, "wb").write(data)
|
||||
print(f"read flash: {len(data)} B -> {path}")
|
||||
|
||||
@@ -784,10 +998,11 @@ def op_eeprom(loader, path, erase, verify):
|
||||
raise Error(f"EEPROM image is {len(image)} B, device has {loader.info.eeprom_size}")
|
||||
if erase:
|
||||
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")
|
||||
if verify:
|
||||
got = loader.read_eeprom(0, len(image))
|
||||
got = read_progress(loader.read_eeprom, len(image), "verify EEPROM")
|
||||
if got != image:
|
||||
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}")
|
||||
@@ -796,7 +1011,7 @@ def op_eeprom(loader, path, erase, verify):
|
||||
|
||||
def op_verify_eeprom(loader, 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:
|
||||
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}")
|
||||
@@ -804,15 +1019,30 @@ def op_verify_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)
|
||||
print(f"read EEPROM: {len(data)} B -> {path}")
|
||||
|
||||
|
||||
def op_fuses(loader):
|
||||
low, lock, extended, high = loader.read_fuses()
|
||||
print(f"fuses: low {low:02x} high {high:02x} extended {extended:02x} lock {lock:02x}")
|
||||
return bytes((low, lock, extended, high))
|
||||
print("fuses:")
|
||||
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 ---
|
||||
@@ -842,7 +1072,11 @@ def main():
|
||||
parser.add_argument("--verify-eeprom", metavar="FILE", help="compare EEPROM against an image")
|
||||
parser.add_argument("--force", action="store_true", help="override refusable safety checks")
|
||||
parser.add_argument("--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()
|
||||
global VERBOSE
|
||||
VERBOSE = args.verbose
|
||||
|
||||
if args.update_loader and (args.flash or args.erase_flash):
|
||||
parser.error("--update-loader does not combine with application flash operations")
|
||||
@@ -855,11 +1089,14 @@ def main():
|
||||
parser.error("--assume-fuses takes 8 hex digits: low,lock,extended,high")
|
||||
|
||||
port = Port(args.port, args.baud)
|
||||
verbose(f"{args.port}: {args.baud} Bd 8N1, DTR/RTS asserted")
|
||||
try:
|
||||
loader = Loader(port)
|
||||
info = loader.connect(args.wait)
|
||||
if args.info:
|
||||
print(f"device: {info.describe()}")
|
||||
print("device:")
|
||||
for line in info.lines():
|
||||
print(f" {line}")
|
||||
fuse_bytes = fuse_override
|
||||
if args.fuses or (args.update_loader and not info.patch_vector and fuse_bytes is None):
|
||||
read = op_fuses(loader)
|
||||
|
||||
@@ -41,7 +41,8 @@ def main():
|
||||
if len(info) != 1:
|
||||
print(f"FAIL: expected one info-block storage symbol, found {len(info)}")
|
||||
sys.exit(1)
|
||||
offset = int(info[0].split()[0], 16) - text_start
|
||||
address = int(info[0].split()[0], 16)
|
||||
offset = address - text_start
|
||||
if not 0 <= offset < 256:
|
||||
print(f"FAIL: info block at image offset {offset:#x}, must sit in the first 256 bytes")
|
||||
sys.exit(1)
|
||||
|
||||
@@ -4,11 +4,16 @@
|
||||
// surgery, actually launched it. Linked normally (crt, vectors at 0); on
|
||||
// the tinies its reset vector is the rjmp the host re-homes.
|
||||
//
|
||||
// On the mega it then listens, and an 'L' makes it jump into the resident
|
||||
// loader — the application-owned loader entry a BOOTRST-unprogrammed mega
|
||||
// relies on (reset always boots the application there), exercised by the
|
||||
// self-update tests. The tinies idle: reset reaches their loader through
|
||||
// the patched vector, so the application owes it nothing.
|
||||
// On the hardware-USART link it then listens, and an 'L' makes it jump into
|
||||
// the resident loader — the application-owned loader entry a
|
||||
// BOOTRST-unprogrammed mega relies on (reset always boots the application
|
||||
// there), exercised by the self-update tests. The software link idles:
|
||||
// reset reaches those loaders through the patched vector (or the runner
|
||||
// models BOOTRST), so the application owes them nothing.
|
||||
//
|
||||
// The fixture speaks the deployment its loader was built for: the same
|
||||
// PUREBOOT_* defines configure it, and without them it assumes the stock
|
||||
// deployment (the crystal/RC clock table below, the chip's natural link).
|
||||
#include <libavr/libavr.hpp>
|
||||
|
||||
using namespace avr::literals;
|
||||
@@ -17,33 +22,76 @@ namespace {
|
||||
|
||||
consteval avr::hertz_t clock()
|
||||
{
|
||||
if (avr::hw::db.name == "ATtiny13A")
|
||||
#if defined(PUREBOOT_CLOCK_HZ)
|
||||
return avr::hertz_t{PUREBOOT_CLOCK_HZ};
|
||||
#else
|
||||
auto name = std::string_view{avr::hw::db.name};
|
||||
if (name.starts_with("ATtiny13"))
|
||||
return 9.6_MHz;
|
||||
if (avr::hw::db.name == "ATtiny85")
|
||||
if (name.starts_with("ATtiny"))
|
||||
return 8_MHz;
|
||||
return 16_MHz;
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !defined(PUREBOOT_TX)
|
||||
#define PUREBOOT_TX pb1
|
||||
#endif
|
||||
#if !defined(PUREBOOT_USART)
|
||||
#define PUREBOOT_USART 0
|
||||
#endif
|
||||
|
||||
consteval bool use_hardware()
|
||||
{
|
||||
#if defined(PUREBOOT_SOFT_SERIAL)
|
||||
return false;
|
||||
#else
|
||||
return avr::hw::db.has_instance("USART0") || avr::hw::db.has_instance("USART");
|
||||
#endif
|
||||
}
|
||||
|
||||
using dev = avr::device<{.clock = clock()}>;
|
||||
|
||||
template <avr::hertz_t C, bool Hardware = avr::hw::db.has_reg("UDR0")>
|
||||
template <avr::hertz_t C, bool Hardware = use_hardware()>
|
||||
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)
|
||||
{
|
||||
tx_t::write(static_cast<std::uint8_t>(c));
|
||||
}
|
||||
[[noreturn]] static void idle()
|
||||
{
|
||||
for (;;)
|
||||
if (tx_t::read_blocking() == 'L')
|
||||
reinterpret_cast<void (*)()>((avr::hw::db.mem.flash_size - 512) / 2)();
|
||||
// 'L' hands back to the loader at the top slot — 512 bytes, or the
|
||||
// 1 KiB the >64 KiB chips use.
|
||||
constexpr std::uint32_t slot = avr::hw::db.mem.flash_size > 65536 ? 1024 : 512;
|
||||
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))();
|
||||
// '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>
|
||||
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)
|
||||
{
|
||||
tx_t::write(static_cast<std::uint8_t>(c));
|
||||
|
||||
90
test/pbdirty.py
Normal file
90
test/pbdirty.py
Normal file
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Dirty-page-buffer acceptance test: the loader carries no buffer discard,
|
||||
so a page filled over words an earlier writer left behind programs those
|
||||
instead. This asserts the whole contract — the corruption is real and a bare
|
||||
verify sees it, the repairing verify fixes it in one rewrite (the write that
|
||||
took the stale words auto-erased the buffer), and it stays fixed.
|
||||
|
||||
The state is reached the way the loader cannot prevent: an application
|
||||
dirties the buffer and jumps in with no reset between. Real boot-sectioned
|
||||
megas forbid that outright — SPM executes only from the boot section
|
||||
(Atmel-8271 §26.2) — but simavr dispatches SPM from anywhere, which is what
|
||||
makes the path constructible at all.
|
||||
|
||||
Usage: pbdirty.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
|
||||
<baud> <app_bin> <tool_py> <workdir>
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def fail(message):
|
||||
print(f"FAIL: {message}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
device_bin, elf, mcu, hz, base_hex, page, baud, app_bin, tool, workdir = sys.argv[1:]
|
||||
page, baud = int(page), int(baud)
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import pbsim
|
||||
import pureboot as pb
|
||||
|
||||
os.makedirs(workdir, exist_ok=True)
|
||||
dump = os.path.join(workdir, "dump.bin")
|
||||
|
||||
# Reset boots the application on a BOOTRST-unprogrammed mega; its 'L' is
|
||||
# the loader entry this test needs, reached without a reset.
|
||||
device = pbsim.Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump, reset_hex="0")
|
||||
try:
|
||||
port = pb.Port(device.pty, baud)
|
||||
loader = pb.Loader(port)
|
||||
loader.connect(25)
|
||||
|
||||
# Install the application and hand over to it.
|
||||
pb.op_flash(loader, app_bin, erase=False, verify=True)
|
||||
loader.run_application()
|
||||
if port.read_exact(3, 5.0) != b"APP":
|
||||
fail("the application did not start")
|
||||
|
||||
port.write(b"D")
|
||||
if port.read_exact(1, 5.0) != b"D":
|
||||
fail("the application did not acknowledge dirtying the page buffer")
|
||||
port.write(b"L")
|
||||
loader = pb.Loader(port)
|
||||
loader.connect(25)
|
||||
|
||||
# Program by hand, so the corruption is observable before anything
|
||||
# repairs it.
|
||||
pages = pb.plan_flash(open(app_bin, "rb").read(), loader.info)
|
||||
for address in sorted(pages):
|
||||
loader.write_page(address, pages[address])
|
||||
try:
|
||||
pb.verify_pages(loader, pages)
|
||||
except pb.Error as error:
|
||||
if "verify failed" not in str(error):
|
||||
fail(f"the read-back failed, but not at verify: {error}")
|
||||
else:
|
||||
# Either the fixture no longer dirties the buffer, or the loader
|
||||
# clears it again — in which case this test's premise is gone.
|
||||
fail("programming over a dirty page buffer came back clean")
|
||||
|
||||
# What the programming path uses: one rewrite settles it, and it stays
|
||||
# settled.
|
||||
pb.verify_pages(loader, pages, repair=True)
|
||||
pb.verify_pages(loader, pages)
|
||||
|
||||
# Ground truth beyond the loader's own read-back.
|
||||
loader.run_application()
|
||||
if port.read_exact(3, 5.0) != b"APP":
|
||||
fail("the application did not start after the recovered write")
|
||||
port.close()
|
||||
finally:
|
||||
device.stop()
|
||||
print("pbdirty: a dirty page buffer is caught by verify and cleared by the retry")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
102
test/pbrehome.py
Normal file
102
test/pbrehome.py
Normal file
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Re-homing acceptance test: a pureboot image programmed somewhere other
|
||||
than its canonical top slot must still be a working loader —
|
||||
position-independent, guarding its accidental slot — and the ordinary
|
||||
--update-loader flow must put a build into the top slot from there.
|
||||
|
||||
Two positions are exercised. Address 0 (a raw .bin handed to a programmer,
|
||||
which defaults to offset 0): the staging install and the word-0 redirect
|
||||
both run from copies whose slots are not page 0's, so the running-slot
|
||||
guard never blocks the flow. The staging slot itself: a loader already
|
||||
sitting there IS the installed staging copy — the tool recognizes it by
|
||||
its embedded info block and leaves it in place instead of tripping the
|
||||
copy's own guard on the composed through-word — and that (older) copy
|
||||
streams the new resident like any staged copy. In both cases flashing an
|
||||
application through the healed resident overwrites the stale copy, vector
|
||||
surgery included, and the banner proves the launch.
|
||||
|
||||
Usage: pbrehome.py <device_bin> <pureboot_elf> <update_bin> <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 rehome_from(pbsim, pb, device_bin, elf, place_hex, guard_probe, update_bin, base, page, baud, app_bin, workdir,
|
||||
mcu, hz):
|
||||
"""Place the loader at `place_hex`, heal through --update-loader, flash
|
||||
the application, expect the banner."""
|
||||
dump = os.path.join(workdir, f"dump-{place_hex}.bin")
|
||||
state = os.path.join(workdir, f"rehome-{place_hex}.pbstate")
|
||||
if os.path.exists(state):
|
||||
os.unlink(state)
|
||||
device = pbsim.Device(device_bin, elf, mcu, hz, place_hex, page, baud, dump, reset_hex="0")
|
||||
try:
|
||||
port = pb.Port(device.pty, baud)
|
||||
loader = pb.Loader(port)
|
||||
info = loader.connect(25)
|
||||
if info.base != base:
|
||||
fail(f"the misplaced copy reports base {info.base:#06x} — the info block must stay canonical")
|
||||
|
||||
# The accidental slot still guards itself; re-homing rides on the
|
||||
# canonical slots being writable from it.
|
||||
probe = int(guard_probe, 0)
|
||||
before = loader.read_flash(probe, info.page)
|
||||
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")
|
||||
|
||||
# The ordinary update flow puts the build into the top slot.
|
||||
pb.op_update_loader(loader, 25, update_bin, state, None)
|
||||
update = open(update_bin, "rb").read()
|
||||
if loader.read_flash(base, len(update)) != update:
|
||||
fail("the canonical slot does not hold the update image")
|
||||
|
||||
# An application flashed through the healed resident overwrites the
|
||||
# stale copy (surgery included) and launches.
|
||||
pages = pb.plan_flash(open(app_bin, "rb").read(), loader.info)
|
||||
for address in pb.covered(pages, loader.info, skip_blank=False):
|
||||
loader.write_page(address, pages[address])
|
||||
pb.verify_pages(loader, pages)
|
||||
loader.run_application()
|
||||
if port.read_exact(3, 5.0) != b"APP":
|
||||
fail(f"application does not banner after the re-home from {place_hex}")
|
||||
port.close()
|
||||
finally:
|
||||
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 - 512
|
||||
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")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -24,7 +24,7 @@ def fail(message):
|
||||
def main():
|
||||
device_bin, elf, mcu, hz, base_hex, page, baud, tool, workdir = sys.argv[1:]
|
||||
base, page, baud = int(base_hex, 0), int(page), int(baud)
|
||||
stage = base - 512
|
||||
stage = None # derived from the device's own info (slot-sized) below
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import pbsim
|
||||
@@ -46,6 +46,7 @@ def main():
|
||||
resident_info = info.raw
|
||||
|
||||
# Install the staging copy exactly as the update flow would.
|
||||
stage = info.stage
|
||||
staged = pb.staging_content(image, info)
|
||||
pb.write_differing(loader, stage, staged)
|
||||
|
||||
@@ -65,8 +66,10 @@ def main():
|
||||
if loader.read_eeprom(0, len(pattern)) != pattern:
|
||||
fail("EEPROM round-trip through the staged copy")
|
||||
|
||||
# The guard, both ways: its own slot refused (drained, unchanged),
|
||||
# the resident slot writable.
|
||||
# The guard, both ways: its own slot refused (drained, unchanged), the
|
||||
# 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)
|
||||
loader.write_page(stage, bytes(page))
|
||||
if loader.read_flash(stage, page) != before:
|
||||
@@ -74,11 +77,13 @@ def main():
|
||||
marker = bytes((i * 3) & 0xFF for i in range(page))
|
||||
loader.write_page(base, 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
|
||||
# into it and prove it lives.
|
||||
resident = image + b"\xff" * (512 - len(image))
|
||||
resident = image + b"\xff" * (info.slot - len(image))
|
||||
pb.write_differing(loader, base, resident)
|
||||
back_info = loader.enter_copy(base, 25)
|
||||
if back_info.raw != resident_info:
|
||||
|
||||
@@ -8,10 +8,17 @@ import subprocess
|
||||
|
||||
|
||||
class Device:
|
||||
def __init__(self, binary, elf, mcu, hz, base_hex, page, baud, dump, reset_hex=None, resume=None):
|
||||
cmd = [binary, elf, mcu, hz, base_hex, str(page), str(baud), dump]
|
||||
def __init__(self, binary, elf, mcu, hz, base_hex, page, baud, dump, reset_hex=None, resume=None, link=None):
|
||||
cmd = [binary]
|
||||
if link:
|
||||
cmd += ["-l", link]
|
||||
cmd += [elf, mcu, hz, base_hex, str(page), str(baud), dump]
|
||||
if reset_hex is not None or resume is not None:
|
||||
cmd.append(reset_hex if reset_hex is not None else ("0" if mcu != "atmega328p" else base_hex))
|
||||
# Chips without a hardware boot section — the tinies and the
|
||||
# m48s — reset to address 0 like silicon; the boot-sectioned
|
||||
# megas re-vector to the loader base (BOOTRST).
|
||||
patch = not mcu.startswith("atmega") or mcu.startswith("atmega48")
|
||||
cmd.append(reset_hex if reset_hex is not None else ("0" if patch else base_hex))
|
||||
if resume is not None:
|
||||
cmd.append(resume)
|
||||
self.log = open(dump + ".log", "a")
|
||||
|
||||
@@ -5,15 +5,15 @@ through flash + EEPROM + fuse + hand-over scenarios, and cross-check
|
||||
the tool's view against the simulator's ground-truth memory dumps.
|
||||
|
||||
Usage: pbtest.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
|
||||
<baud> <eeprom_size> <app_bin> <tool_py> <workdir>
|
||||
<baud> <eeprom_size> <app_bin> <tool_py> <workdir> [link]
|
||||
|
||||
The optional link is the runner's -l spec (usart1, sw:B5,B1, ...) for a
|
||||
loader built off the chip's natural serial default.
|
||||
Exits 0 if every scenario passes.
|
||||
"""
|
||||
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
def fail(message):
|
||||
@@ -33,53 +33,14 @@ def rjmp_decode(word, at, flash_words):
|
||||
return (at + 1 + offset) % flash_words
|
||||
|
||||
|
||||
class Device:
|
||||
def __init__(self, binary, elf, mcu, hz, base, page, baud, dump):
|
||||
self.proc = subprocess.Popen(
|
||||
[binary, elf, mcu, hz, base, str(page), str(baud), dump],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
)
|
||||
self.dump = dump
|
||||
self.pty = None
|
||||
deadline = time.time() + 5
|
||||
while time.time() < deadline:
|
||||
line = self.proc.stdout.readline()
|
||||
if not line:
|
||||
break
|
||||
if line.startswith("PB_PTY"):
|
||||
self.pty = line.split()[1]
|
||||
break
|
||||
if not self.pty:
|
||||
self.stop()
|
||||
raise RuntimeError("device did not report a pty")
|
||||
|
||||
def stop(self):
|
||||
self.proc.terminate()
|
||||
try:
|
||||
self.proc.wait(timeout=3)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.proc.kill()
|
||||
|
||||
|
||||
def run_tool(tool, pty, baud, *args):
|
||||
result = subprocess.run(
|
||||
[sys.executable, tool, "--port", pty, "--baud", str(baud), "--wait", "20", *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120,
|
||||
)
|
||||
print(result.stdout, end="")
|
||||
if result.returncode != 0:
|
||||
fail(f"tool exited {result.returncode}: {result.stderr.strip()}")
|
||||
return result.stdout
|
||||
|
||||
|
||||
def main():
|
||||
(device_bin, elf, mcu, hz, base_hex, page, baud, eeprom_size, app_bin, tool, workdir) = sys.argv[1:]
|
||||
args = sys.argv[1:]
|
||||
link = args.pop() if len(args) == 12 else None
|
||||
(device_bin, elf, mcu, hz, base_hex, page, baud, eeprom_size, app_bin, tool, workdir) = args
|
||||
base, page, baud, eeprom_size = int(base_hex, 0), int(page), int(baud), int(eeprom_size)
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import pbsim
|
||||
import pureboot as pb
|
||||
|
||||
os.makedirs(workdir, exist_ok=True)
|
||||
@@ -90,25 +51,33 @@ def main():
|
||||
read_flash = os.path.join(workdir, "readback_flash.bin")
|
||||
read_eeprom = os.path.join(workdir, "readback_eeprom.bin")
|
||||
|
||||
# The geometry the host will discover, for computing the expected image.
|
||||
# The geometry the host will discover, for computing the expected image:
|
||||
# the boot-sectioned megas need no vector surgery (the tinies and the
|
||||
# boot-section-less m48s do), the large chips speak word addresses, and
|
||||
# the page byte is the wire's 0-means-256.
|
||||
mega = mcu.startswith("atmega")
|
||||
patch = not mega or mcu.startswith("atmega48")
|
||||
word_flash = base + 512 > 0x10000
|
||||
wire_base = base // 2 if word_flash else base
|
||||
flags = (1 if patch else 0) | (2 if word_flash else 0)
|
||||
info = pb.Info(
|
||||
bytes([ord("P"), ord("B"), 1, 0, 0, 0, page])
|
||||
+ bytes([base & 0xFF, base >> 8, eeprom_size & 0xFF, eeprom_size >> 8])
|
||||
+ bytes([0 if mcu == "atmega328p" else 1])
|
||||
bytes([ord("P"), ord("B"), 1, 0, 0, 0, page & 0xFF])
|
||||
+ bytes([wire_base & 0xFF, wire_base >> 8, eeprom_size & 0xFF, eeprom_size >> 8])
|
||||
+ bytes([flags])
|
||||
)
|
||||
|
||||
device = Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump)
|
||||
device = pbsim.Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump, link=link)
|
||||
try:
|
||||
# Session 1: knock from reset, identify, program everything, stay.
|
||||
out = run_tool(tool, device.pty, baud, "--info", "--fuses", "--flash", app_bin,
|
||||
out = pbsim.run_tool(tool, device.pty, baud, "--info", "--fuses", "--flash", app_bin,
|
||||
"--eeprom", ee_path, "--stay")
|
||||
for needed in ("device: signature", "fuses:", "verify:", "stays"):
|
||||
for needed in ("signature", "fuses", "verify:", "stays"):
|
||||
if needed not in out:
|
||||
fail(f"session 1 output lacks {needed!r}")
|
||||
|
||||
# Session 2: reconnect into the live session, verify, dump, hand over
|
||||
# is deferred — the pty must be reopened for the APP banner first.
|
||||
out = run_tool(tool, device.pty, baud, "--verify-flash", app_bin, "--verify-eeprom", ee_path,
|
||||
out = pbsim.run_tool(tool, device.pty, baud, "--verify-flash", app_bin, "--verify-eeprom", ee_path,
|
||||
"--read-flash", read_flash, "--read-eeprom", read_eeprom, "--stay")
|
||||
if out.count("verify:") != 2:
|
||||
fail("session 2 did not verify both memories")
|
||||
@@ -128,7 +97,7 @@ def main():
|
||||
# runner resets them to address 0 like silicon) or BOOTRST (mega).
|
||||
# The loader must answer a fresh knock, and the 'J' hand-over must
|
||||
# land in the application, which banners on the same link.
|
||||
device.proc.send_signal(signal.SIGUSR1)
|
||||
device.reset()
|
||||
port = pb.Port(device.pty, baud)
|
||||
try:
|
||||
loader = pb.Loader(port)
|
||||
@@ -150,8 +119,9 @@ def main():
|
||||
fail("loader region looks erased in the ground-truth dump")
|
||||
|
||||
# The surgery, decoded independently: the patched vector must land on the
|
||||
# loader, the trampoline on the application's own entry.
|
||||
if mcu != "atmega328p":
|
||||
# loader, the trampoline on the application's own entry (patched-vector
|
||||
# chips only — a boot-sectioned mega's word 0 stays the application's).
|
||||
if patch:
|
||||
flash_words = (base + 512) // 2
|
||||
app = open(app_bin, "rb").read()
|
||||
word0 = flash_true[0] | (flash_true[1] << 8)
|
||||
|
||||
@@ -5,12 +5,13 @@ replaces itself with a re-timed build through the host tool's
|
||||
killing the simulated device mid-write, restarting it from its flash dump,
|
||||
and letting a re-run complete the update.
|
||||
|
||||
The mega runs the BOOTRST-unprogrammed profile (reset boots the application;
|
||||
the fixture application's 'L' jump is the application-owned loader entry),
|
||||
with --assume-fuses standing in for the fuse read simavr cannot model. The
|
||||
tinies reset into a loader at every phase by construction — the t13a because
|
||||
its staging slot carries the reset vector itself, the t85 through the word-0
|
||||
redirect the tool plants around the resident rewrite.
|
||||
The boot-sectioned megas run the BOOTRST-unprogrammed profile (reset boots
|
||||
the application; the fixture application's 'L' jump is the application-owned
|
||||
loader entry), with --assume-fuses standing in for the fuse read simavr
|
||||
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>
|
||||
<base_hex> <page> <baud> <app_bin> <tool_py> <workdir>
|
||||
@@ -41,10 +42,21 @@ class PowerFail(Exception):
|
||||
pass
|
||||
|
||||
|
||||
MEGA_FUSES = "ffffffdd" # high 0xdd: BOOTSZ = 1 KB, BOOTRST unprogrammed
|
||||
def assumed_fuses(pb, image):
|
||||
"""Synthetic 'F' bytes for --assume-fuses: the smallest boot section
|
||||
covering both the resident and the staging slot (two slots — what a
|
||||
self-update needs), BOOTRST unprogrammed — the per-chip BOOTSZ ladder
|
||||
and fuse byte come from the tool's own table, keyed by the update
|
||||
image's embedded signature."""
|
||||
info = pb.image_info(image)
|
||||
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])
|
||||
fuses = bytearray((0xFF, 0xFF, 0xFF, 0xFF))
|
||||
fuses[which] = 0xF8 | (bits << 1) | 1
|
||||
return bytes(fuses)
|
||||
|
||||
|
||||
def make_fault_loader(pb, base, kill_region, kill_hits, device):
|
||||
def make_fault_loader(pb, base, slot, kill_region, kill_hits, device):
|
||||
"""A Loader whose write_page kills the device (or, with device=None,
|
||||
just the host) at the Nth write into a region; the sequence
|
||||
stage->resident->stage distinguishes the install from the restore."""
|
||||
@@ -59,7 +71,7 @@ def make_fault_loader(pb, base, kill_region, kill_hits, device):
|
||||
if address >= base:
|
||||
phase = "resident"
|
||||
self.seen_resident = True
|
||||
elif address >= base - 512:
|
||||
elif address >= base - slot:
|
||||
phase = "stage_restore" if self.seen_resident else "stage"
|
||||
else:
|
||||
phase = "app"
|
||||
@@ -77,8 +89,15 @@ def make_fault_loader(pb, base, kill_region, kill_hits, device):
|
||||
def main():
|
||||
(device_bin, elf, update_elf, mcu, hz, base_hex, page, baud, app_bin, tool, workdir) = sys.argv[1:]
|
||||
base, page, baud = int(base_hex, 0), int(page), int(baud)
|
||||
mega = mcu == "atmega328p"
|
||||
reset_hex = "0" if mega else None # the mega runs BOOTRST-unprogrammed here
|
||||
mega = mcu.startswith("atmega")
|
||||
# The m48s are megas without a boot section: patched vector, no fuse
|
||||
# preflight, and the same reset-to-0 the tinies get.
|
||||
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
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import pbsim
|
||||
@@ -95,7 +114,7 @@ def main():
|
||||
fail("the update image is byte-identical to the resident build")
|
||||
dump = os.path.join(workdir, "dump.bin")
|
||||
state = os.path.join(workdir, "update.pbstate")
|
||||
fuses = bytes.fromhex(MEGA_FUSES) if mega else None
|
||||
fuses = assumed_fuses(pb, images["v0"]) if mega and not patch else None
|
||||
|
||||
def connect(device):
|
||||
port = pb.Port(device.pty, baud)
|
||||
@@ -109,16 +128,16 @@ def main():
|
||||
return port, loader
|
||||
|
||||
def padded(image):
|
||||
return image + b"\xff" * (512 - len(image))
|
||||
return image + b"\xff" * (slot - len(image))
|
||||
|
||||
def resident_bytes(loader):
|
||||
return loader.read_flash(base, 256) + loader.read_flash(base + 256, 256)
|
||||
return loader.read_flash(base, slot)
|
||||
|
||||
def assert_state(loader, image, app_pages):
|
||||
if resident_bytes(loader) != padded(image):
|
||||
fail("resident loader does not match the update image")
|
||||
stage = base - 512
|
||||
got = loader.read_flash(stage, 256) + loader.read_flash(stage + 256, 256)
|
||||
stage = base - slot
|
||||
got = loader.read_flash(stage, slot)
|
||||
for address, data in app_pages.items():
|
||||
if stage <= address < base:
|
||||
if got[address - stage : address - stage + page] != data:
|
||||
@@ -135,8 +154,8 @@ def main():
|
||||
|
||||
# A clean CLI update, resident -> v9.
|
||||
args = ["--update-loader", os.path.join(workdir, "v9.bin"), "--state", state, "--stay"]
|
||||
if mega:
|
||||
args += ["--assume-fuses", MEGA_FUSES]
|
||||
if fuses:
|
||||
args += ["--assume-fuses", fuses.hex()]
|
||||
out = pbsim.run_tool(tool, device.pty, baud, *args)
|
||||
if "loader updated" not in out:
|
||||
fail("update did not report success")
|
||||
@@ -160,14 +179,14 @@ def main():
|
||||
# cost of that profile (README).
|
||||
for kill_region, kill_hits, kill_device in (
|
||||
("stage", 2, True),
|
||||
("resident", 1, not mega),
|
||||
("resident", 1, patch),
|
||||
("stage_restore", 2, True),
|
||||
):
|
||||
device.reset() # the previous round left the application running
|
||||
port, loader = connect(device)
|
||||
target = "v9" if resident_bytes(loader) == padded(images["v0"]) else "v0"
|
||||
image_path = os.path.join(workdir, target + ".bin")
|
||||
injected = make_fault_loader(pb, base, kill_region, kill_hits, device if kill_device else None)(port)
|
||||
injected = make_fault_loader(pb, base, slot, kill_region, kill_hits, device if kill_device else None)(port)
|
||||
injected.info = loader.info
|
||||
try:
|
||||
pb.op_update_loader(injected, 25, image_path, state, fuses)
|
||||
@@ -191,12 +210,12 @@ def main():
|
||||
device.stop()
|
||||
|
||||
# Ground truth: the simulator's own flash against the final state, and
|
||||
# on the tinies an independent decode of the reset routing.
|
||||
# on the patched-vector chips an independent decode of the reset routing.
|
||||
flash = open(dump, "rb").read()
|
||||
if flash[base : base + 512] != padded(images[final]):
|
||||
if flash[base : base + slot] != padded(images[final]):
|
||||
fail("ground-truth resident region does not match the final image")
|
||||
if not mega:
|
||||
flash_words = (base + 512) // 2
|
||||
if patch:
|
||||
flash_words = (base + slot) // 2
|
||||
word0 = flash[0] | (flash[1] << 8)
|
||||
if rjmp_decode(word0, 0, flash_words) != base // 2:
|
||||
fail("ground-truth reset vector does not land on the loader")
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
// simavr "device" for the pureboot protocol tests, all three chips. Loads
|
||||
// the boot-linked ELF at the loader base, starts execution there (BOOTRST /
|
||||
// the patched vector are not what is under test), and exposes the loader's
|
||||
// simavr "device" for the pureboot protocol tests, every chip. Loads the
|
||||
// boot-linked ELF at the loader base, starts execution there (BOOTRST / the
|
||||
// patched vector are not what is under test), and exposes the loader's
|
||||
// serial link as a pty for the real host tool:
|
||||
//
|
||||
// - ATmega328P: the hardware USART0 through simavr's uart_pty.
|
||||
// - Tinies: an 8N1 bridge between a pty and the GPIO software UART
|
||||
// (drives PB0, the loader's RX; decodes PB1, its TX), timed against the
|
||||
// simulated cycle counter.
|
||||
// - Hardware USART builds: simavr's uart_pty on the selected instance.
|
||||
// - Software UART builds: an 8N1 bridge between a pty and the GPIO pins,
|
||||
// timed against the simulated cycle counter (drives the loader's RX,
|
||||
// decodes its TX).
|
||||
//
|
||||
// The link follows the chip's natural default (USART0 on the megas, the
|
||||
// software UART on PB0/PB1 elsewhere) unless -l overrides it: `-l usart1`
|
||||
// for the second instance, `-l sw:B5,B1` for a software build's RX,TX pins.
|
||||
//
|
||||
// simavr's tiny cores decode the SPM opcode but attach no NVM module — SPM
|
||||
// is a silent no-op (the mega's boot section has one, avr_flash). The
|
||||
@@ -38,17 +42,44 @@
|
||||
|
||||
static avr_t *avr;
|
||||
static uart_pty_t uart_pty;
|
||||
static int use_uart_pty;
|
||||
static int link_software;
|
||||
static char uart_digit = '0';
|
||||
static char sw_rx_port = 'B', sw_tx_port = 'B';
|
||||
static int sw_rx_bit = 0, sw_tx_bit = 1;
|
||||
static const char *dump_path;
|
||||
static uint32_t reset_pc;
|
||||
static volatile sig_atomic_t reset_requested;
|
||||
|
||||
static int parse_link(const char *spec)
|
||||
{
|
||||
if (strcmp(spec, "usart0") == 0 || strcmp(spec, "usart1") == 0) {
|
||||
link_software = 0;
|
||||
uart_digit = spec[5];
|
||||
return 0;
|
||||
}
|
||||
if (strncmp(spec, "sw", 2) == 0) {
|
||||
link_software = 1;
|
||||
if (spec[2] == '\0')
|
||||
return 0;
|
||||
if (sscanf(spec + 2, ":%c%d,%c%d", &sw_rx_port, &sw_rx_bit, &sw_tx_port, &sw_tx_bit) == 4)
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// simavr 1.6's avr_flash PGERS handler erases spm_pagesize bytes starting at
|
||||
// Z & ~1 instead of the page containing Z (its PGWRT path masks correctly) —
|
||||
// hardware ignores the in-page bits (§26.8.1), so an erase issued with Z
|
||||
// anywhere inside the page wipes half the neighbouring page in simulation
|
||||
// only. Wrap the mega's registered flash ioctl and re-dispatch page erases
|
||||
// with Z forced to the page boundary; everything else passes through.
|
||||
//
|
||||
// A second gap on the boot-section-less m48s: their RWWSRE bit is the
|
||||
// temporary-buffer discard (Atmel-8271 §26.2/§26.3.1), but the stock model
|
||||
// gates its RWWSRE branch on AVR_SELFPROG_HAVE_RWW — absent on the m48
|
||||
// core — so the discard store falls through into the buffer-fill branch and
|
||||
// plants whatever Z/R1:R0 happen to hold. Perform the silicon's discard
|
||||
// here instead.
|
||||
static avr_flash_t *mega_flash;
|
||||
static int (*mega_flash_ioctl)(avr_io_t *io, uint32_t ctl, void *param);
|
||||
|
||||
@@ -64,6 +95,15 @@ static int fixed_flash_ioctl(avr_io_t *io, uint32_t ctl, void *param)
|
||||
io->avr->data[31] = (uint8_t)(z >> 8);
|
||||
return result;
|
||||
}
|
||||
if (ctl == AVR_IOCTL_FLASH_SPM && !(mega_flash->flags & AVR_SELFPROG_HAVE_RWW) &&
|
||||
(io->avr->data[mega_flash->r_spm] & 0x11) == 0x11) { // RWWSRE|SELFPRGEN: the m48 buffer discard
|
||||
for (int i = 0; i < mega_flash->spm_pagesize / 2; i++) {
|
||||
mega_flash->tmppage[i] = 0xffff;
|
||||
mega_flash->tmppage_used[i] = 0;
|
||||
}
|
||||
avr_regbit_clear(io->avr, mega_flash->selfprgen);
|
||||
return 0;
|
||||
}
|
||||
return mega_flash_ioctl(io, ctl, param);
|
||||
}
|
||||
|
||||
@@ -257,29 +297,42 @@ static void finish(int sig)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (use_uart_pty)
|
||||
if (!link_software)
|
||||
uart_pty_stop(&uart_pty);
|
||||
_exit(0);
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
if (argc < 8 || argc > 10) {
|
||||
int link_given = 0;
|
||||
for (int opt; (opt = getopt(argc, argv, "l:")) != -1;) {
|
||||
if (opt != 'l' || parse_link(optarg) != 0) {
|
||||
fprintf(stderr, "device: bad link spec (usart0, usart1, sw, or sw:B0,B1 as RX,TX)\n");
|
||||
return 2;
|
||||
}
|
||||
link_given = 1;
|
||||
}
|
||||
int args = argc - optind;
|
||||
if (args < 7 || args > 9) {
|
||||
fprintf(stderr,
|
||||
"usage: %s <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: reset vector (default: base on the mega, 0 on the tinies)\n"
|
||||
" -l link: usart0 | usart1 | sw[:B0,B1] (RX,TX); default: the chip's own\n"
|
||||
" reset_hex: reset vector (default: base with a boot section, else 0)\n"
|
||||
" resume_flash: raw full-flash image loaded instead of the ELF — a prior\n"
|
||||
" run's dump, for power-fail resume tests\n",
|
||||
argv[0]);
|
||||
return 2;
|
||||
}
|
||||
argv += optind - 1; // argv[1] is the ELF again, whatever was parsed
|
||||
const char *mcu_name = argv[2];
|
||||
uint32_t base = (uint32_t)strtoul(argv[4], NULL, 0);
|
||||
unsigned page = (unsigned)atoi(argv[5]);
|
||||
unsigned baud = (unsigned)atoi(argv[6]);
|
||||
dump_path = argv[7];
|
||||
use_uart_pty = strcmp(mcu_name, "atmega328p") == 0;
|
||||
int is_mega = strncmp(mcu_name, "atmega", 6) == 0;
|
||||
if (!link_given)
|
||||
link_software = !is_mega; // the chips' natural links: USART0, or PB0/PB1
|
||||
|
||||
avr = avr_make_mcu_by_name(mcu_name);
|
||||
if (!avr) {
|
||||
@@ -290,7 +343,7 @@ int main(int argc, char *argv[])
|
||||
avr->frequency = (uint32_t)strtoul(argv[3], NULL, 0);
|
||||
memset(avr->flash, 0xff, avr->flashend + 1); // real flash powers up erased
|
||||
|
||||
if (argc > 9) {
|
||||
if (args > 8) {
|
||||
// Resume: the full flash image of an interrupted prior run.
|
||||
FILE *f = fopen(argv[9], "rb");
|
||||
if (!f || fread(avr->flash, 1, avr->flashend + 1, f) == 0) {
|
||||
@@ -306,11 +359,13 @@ int main(int argc, char *argv[])
|
||||
}
|
||||
memcpy(avr->flash + base, fw.flash, fw.flashsize);
|
||||
}
|
||||
// The mega enters the loader in hardware (BOOTRST, not modeled — the
|
||||
// argument picks the modeled fuse's target); the tinies reset to word 0
|
||||
// like silicon — erased flash walks up into the loader, and after the
|
||||
// host's surgery the patched vector routes there.
|
||||
reset_pc = argc > 8 ? (uint32_t)strtoul(argv[8], NULL, 0) : (use_uart_pty ? base : 0);
|
||||
// The boot-sectioned megas enter the loader in hardware (BOOTRST, not
|
||||
// modeled — the argument picks the modeled fuse's target); the tinies
|
||||
// and the boot-section-less m48s reset to word 0 like silicon — erased
|
||||
// flash walks up into the loader, and after the host's surgery the
|
||||
// patched vector routes there.
|
||||
int boot_section = is_mega && strncmp(mcu_name, "atmega48", 8) != 0;
|
||||
reset_pc = args > 7 ? (uint32_t)strtoul(argv[8], NULL, 0) : (boot_section ? base : 0);
|
||||
avr->pc = reset_pc;
|
||||
avr->codeend = avr->flashend;
|
||||
|
||||
@@ -323,27 +378,34 @@ int main(int argc, char *argv[])
|
||||
avr_ioctl(avr, AVR_IOCTL_EEPROM_SET, &seed);
|
||||
}
|
||||
|
||||
if (use_uart_pty) {
|
||||
// The megas carry simavr's avr_flash module (and its two gaps the wrap
|
||||
// above fixes); the tinies get the NVM module simavr lacks. Which serial
|
||||
// bridge runs is the link's business, not the chip class's.
|
||||
if (is_mega) {
|
||||
fix_mega_flash_erase();
|
||||
// POLL_SLEEP paces an idle-polling loader in host real time (a
|
||||
// no-hardware CPU-saving hack); clear it so cycles run free.
|
||||
uint32_t flags = 0;
|
||||
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS('0'), &flags);
|
||||
flags &= ~AVR_UART_FLAG_POLL_SLEEP;
|
||||
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS('0'), &flags);
|
||||
uart_pty_init(avr, &uart_pty);
|
||||
uart_pty_connect(&uart_pty, '0');
|
||||
printf("PB_PTY %s\n", uart_pty.pty.slavename);
|
||||
} else {
|
||||
nvm.page = page;
|
||||
memset(nvm.buffer, 0xff, sizeof(nvm.buffer));
|
||||
nvm.io.kind = "tiny_nvm";
|
||||
nvm.io.ioctl = nvm_ioctl;
|
||||
avr_register_io(avr, &nvm.io);
|
||||
}
|
||||
|
||||
if (!link_software) {
|
||||
// POLL_SLEEP paces an idle-polling loader in host real time (a
|
||||
// no-hardware CPU-saving hack); clear it so cycles run free.
|
||||
uint32_t flags = 0;
|
||||
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS(uart_digit), &flags);
|
||||
flags &= ~AVR_UART_FLAG_POLL_SLEEP;
|
||||
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS(uart_digit), &flags);
|
||||
uart_pty_init(avr, &uart_pty);
|
||||
uart_pty_connect(&uart_pty, uart_digit);
|
||||
printf("PB_PTY %s\n", uart_pty.pty.slavename);
|
||||
} else {
|
||||
bit_cycles = (avr->frequency + baud / 2) / baud; // matches uart.hpp's own rounding exactly
|
||||
rx_pin = avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ('B'), 0);
|
||||
avr_irq_register_notify(avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ('B'), 1), tx_hook, NULL);
|
||||
rx_pin = avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ(sw_rx_port), (unsigned)sw_rx_bit);
|
||||
avr_irq_register_notify(avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ(sw_tx_port), (unsigned)sw_tx_bit), tx_hook,
|
||||
NULL);
|
||||
avr_raise_irq(rx_pin, 1); // idle line
|
||||
|
||||
int slave;
|
||||
@@ -371,18 +433,27 @@ int main(int argc, char *argv[])
|
||||
reset_requested = 0;
|
||||
avr_reset(avr);
|
||||
avr->pc = reset_pc;
|
||||
if (use_uart_pty) { // reset restores the pacing hack; re-clear it
|
||||
if (!link_software) { // reset restores the pacing hack; re-clear it
|
||||
uint32_t flags = 0;
|
||||
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS('0'), &flags);
|
||||
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS(uart_digit), &flags);
|
||||
flags &= ~AVR_UART_FLAG_POLL_SLEEP;
|
||||
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS('0'), &flags);
|
||||
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS(uart_digit), &flags);
|
||||
} else {
|
||||
bridge_reset();
|
||||
}
|
||||
}
|
||||
if (!use_uart_pty && ++since_poll >= 2000) {
|
||||
if (link_software && ++since_poll >= 2000) {
|
||||
since_poll = 0;
|
||||
poll_pty();
|
||||
// An unthrottled idle simulation runs the activation window out
|
||||
// from under the host's real-time knock cadence: a 1 MHz build's
|
||||
// 8 s window is 8 M cycles — tens of wall milliseconds — so a
|
||||
// first knock lost to an in-flight reset misses the window
|
||||
// entirely. Pace the simulation only while the bridge is fully
|
||||
// quiet (nothing decoding, nothing queued); transfers keep full
|
||||
// speed, and a quiet window stretches toward real time.
|
||||
if (!rx_active && !tx_active && rx_head == rx_tail)
|
||||
usleep(200);
|
||||
}
|
||||
}
|
||||
finish(0);
|
||||
|
||||
@@ -27,9 +27,12 @@ def expect_error(what, fn, *needles):
|
||||
fail(f"{what}: no error raised")
|
||||
|
||||
|
||||
def info_of(pb, base, page, patch, flash):
|
||||
raw = bytes((0x50, 0x42, 1, 0x1E, 0x93, 0x0B, page, base & 0xFF, base >> 8,
|
||||
0, 2, 1 if patch else 0))
|
||||
def info_of(pb, base, page, patch, flash, signature=(0x1E, 0x93, 0x0B), word_flash=False):
|
||||
scale = 2 if word_flash else 1
|
||||
wire_base = base // scale
|
||||
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,
|
||||
0, 2, flags))
|
||||
info = pb.Info(raw)
|
||||
assert info.flash_size == flash
|
||||
return info
|
||||
@@ -50,16 +53,52 @@ def main():
|
||||
import pureboot as pb
|
||||
|
||||
tiny = info_of(pb, 0x1E00, 64, True, 0x2000)
|
||||
mega = info_of(pb, 0x7E00, 128, False, 0x8000)
|
||||
mega = info_of(pb, 0x7E00, 128, False, 0x8000, signature=(0x1E, 0x95, 0x0F))
|
||||
|
||||
# mega_boot: BOOTSZ words and the BOOTRST sense, DS40002061B §27.
|
||||
for bits, start in ((0b11, 0x7E00), (0b10, 0x7C00), (0b01, 0x7800), (0b00, 0x7000)):
|
||||
prog, at = pb.mega_boot((0xF8 | (bits << 1)) & ~1)
|
||||
# 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)
|
||||
# and the per-family ladders (Atmel-2486/2466/2503/2545/8271/DS40002065/
|
||||
# 8272/8011/2593/42719). Synthetic 'F' replies: only the boot byte
|
||||
# carries meaning.
|
||||
cases = (
|
||||
((0x1E, 0x93, 0x07), 0x2000, 3, {0b11: 0x1F00, 0b10: 0x1E00, 0b01: 0x1C00, 0b00: 0x1800}), # m8
|
||||
((0x1E, 0x94, 0x03), 0x4000, 3, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m16
|
||||
((0x1E, 0x95, 0x02), 0x8000, 3, {0b11: 0x7E00, 0b10: 0x7C00, 0b01: 0x7800, 0b00: 0x7000}), # m32
|
||||
((0x1E, 0x93, 0x0A), 0x2000, 2, {0b11: 0x1F00, 0b10: 0x1E00, 0b01: 0x1C00, 0b00: 0x1800}), # m88
|
||||
((0x1E, 0x93, 0x0F), 0x2000, 2, {0b11: 0x1F00, 0b10: 0x1E00, 0b01: 0x1C00, 0b00: 0x1800}), # m88P
|
||||
((0x1E, 0x94, 0x06), 0x4000, 2, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m168/168A
|
||||
((0x1E, 0x94, 0x0B), 0x4000, 2, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m168P
|
||||
((0x1E, 0x95, 0x14), 0x8000, 3, {0b11: 0x7E00, 0b10: 0x7C00, 0b01: 0x7800, 0b00: 0x7000}), # m328
|
||||
((0x1E, 0x95, 0x0F), 0x8000, 3, {0b11: 0x7E00, 0b10: 0x7C00, 0b01: 0x7800, 0b00: 0x7000}), # m328P
|
||||
((0x1E, 0x94, 0x0F), 0x4000, 3, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m164A
|
||||
((0x1E, 0x94, 0x0A), 0x4000, 3, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m164P
|
||||
((0x1E, 0x95, 0x15), 0x8000, 3, {0b11: 0x7E00, 0b10: 0x7C00, 0b01: 0x7800, 0b00: 0x7000}), # m324A
|
||||
((0x1E, 0x96, 0x09), 0x10000, 3, {0b11: 0xFC00, 0b10: 0xF800, 0b01: 0xF000, 0b00: 0xE000}), # m644
|
||||
((0x1E, 0x96, 0x0A), 0x10000, 3, {0b11: 0xFC00, 0b10: 0xF800, 0b01: 0xF000, 0b00: 0xE000}), # m644P
|
||||
((0x1E, 0x97, 0x06), 0x20000, 3, {0b11: 0x1FC00, 0b10: 0x1F800, 0b01: 0x1F000, 0b00: 0x1E000}), # 1284
|
||||
((0x1E, 0x97, 0x05), 0x20000, 3, {0b11: 0x1FC00, 0b10: 0x1F800, 0b01: 0x1F000, 0b00: 0x1E000}), # 1284P
|
||||
)
|
||||
for signature, flash, which, ladder in cases:
|
||||
# Word-addressed chips carry the 1 KiB slot (their smallest boot sector).
|
||||
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)
|
||||
for bits, start in ladder.items():
|
||||
fuses = bytearray((0xFF, 0xFF, 0xFF, 0xFF))
|
||||
fuses[which] = (0xF8 | (bits << 1)) & ~1
|
||||
prog, at = pb.mega_boot(chip, bytes(fuses))
|
||||
if not prog or at != start:
|
||||
fail(f"mega_boot BOOTSZ={bits:02b} programmed: {prog} {at:#06x}")
|
||||
prog, at = pb.mega_boot(0xF8 | (bits << 1) | 1)
|
||||
fail(f"mega_boot {signature[1]:02x}{signature[2]:02x} BOOTSZ={bits:02b} programmed: {prog} {at:#07x}")
|
||||
fuses[which] |= 1
|
||||
prog, at = pb.mega_boot(chip, bytes(fuses))
|
||||
if prog or at != start:
|
||||
fail(f"mega_boot BOOTSZ={bits:02b} unprogrammed: {prog} {at:#06x}")
|
||||
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,
|
||||
# and its slot is 1 KiB.
|
||||
big = info_of(pb, 0x1FC00, 0, False, 0x20000, signature=(0x1E, 0x97, 0x05), word_flash=True)
|
||||
if big.page != 256 or big.base != 0x1FC00 or big.stage != 0x1F800 or big.slot != 1024:
|
||||
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
|
||||
# entry — checked with an independent decoder.
|
||||
@@ -116,6 +155,21 @@ def main():
|
||||
if pb.image_info(bytes((0xAA,)) * 40) is not None:
|
||||
fail("image_info invents a block")
|
||||
|
||||
# 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
|
||||
# resident at base) yields the same bytes as the bare slot image.
|
||||
import tempfile
|
||||
slot_image = bytes((0xAA,)) * 10 + tiny.raw + bytes((0xCC,)) * 40
|
||||
padded = bytes((0xFF,)) * tiny.base + slot_image
|
||||
with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f:
|
||||
f.write(padded)
|
||||
padded_path = f.name
|
||||
try:
|
||||
if pb.loader_image(padded_path) != slot_image:
|
||||
fail("loader_image does not peel a padded image to the slot content")
|
||||
finally:
|
||||
os.unlink(padded_path)
|
||||
|
||||
# Update preflight: the full fuse matrix, plus target mismatch.
|
||||
other = info_of(pb, 0x1E00, 32, True, 0x2000)
|
||||
expect_error("wrong-target image", lambda: pb.update_preflight(binary, other, None), "another target")
|
||||
@@ -150,6 +204,52 @@ def main():
|
||||
pb.check_walk_region({0x7800: bytes((0xFF,)) * 128}, mega, fuses(0xFA), False)
|
||||
pb.check_walk_region(deep, mega, None, False) # fuses unknown: no check
|
||||
|
||||
# The repairing verify: a mismatched page is rewritten rather than raised,
|
||||
# bounded so a fault that is not self-clearing cannot spin.
|
||||
class FakeLoader:
|
||||
"""A device whose first `bad` writes of any page land wrong."""
|
||||
|
||||
def __init__(self, info, bad):
|
||||
self.info = info
|
||||
self.bad = bad
|
||||
self.flash = {}
|
||||
self.writes = 0
|
||||
|
||||
def write_page(self, address, data):
|
||||
self.writes += 1
|
||||
self.flash[address] = bytes(len(data)) if self.bad > 0 else bytes(data)
|
||||
self.bad -= 1
|
||||
|
||||
def read_flash(self, address, count):
|
||||
return self.flash.get(address, bytes(count))
|
||||
|
||||
want = {0: bytes((i * 5) & 0xFF for i in range(128))}
|
||||
|
||||
# One bad write, then good: repaired in place, and the caller never sees
|
||||
# an error. The rewrite is counted, so a silent no-op cannot pass.
|
||||
device = FakeLoader(info_of(pb, 0x7E00, 128, False, 0x8000), bad=1)
|
||||
device.write_page(0, want[0])
|
||||
pb.verify_pages(device, want, repair=True)
|
||||
if device.writes != 2:
|
||||
fail(f"repairing verify made {device.writes} writes, expected 2")
|
||||
|
||||
# Without repair the same state raises, so the repair is what fixed it.
|
||||
device = FakeLoader(info_of(pb, 0x7E00, 128, False, 0x8000), bad=1)
|
||||
device.write_page(0, want[0])
|
||||
expect_error("verify without repair", lambda: pb.verify_pages(device, want), "verify failed")
|
||||
|
||||
# A page that never comes good stops after RETRIES rewrites, and says so.
|
||||
device = FakeLoader(info_of(pb, 0x7E00, 128, False, 0x8000), bad=99)
|
||||
device.write_page(0, want[0])
|
||||
expect_error(
|
||||
"unrepairable page",
|
||||
lambda: pb.verify_pages(device, want, repair=True),
|
||||
"verify failed",
|
||||
f"after {pb.RETRIES} retries",
|
||||
)
|
||||
if device.writes != pb.RETRIES + 1:
|
||||
fail(f"unrepairable page took {device.writes} writes, expected {pb.RETRIES + 1}")
|
||||
|
||||
print("test_planner: all planner and policy checks pass")
|
||||
|
||||
|
||||
|
||||
37
tools/check.sh
Executable file
37
tools/check.sh
Executable file
@@ -0,0 +1,37 @@
|
||||
#!/bin/bash
|
||||
# The port's gate: every chip's generated workflow — build, size matrix, and
|
||||
# the simulator-driven protocol suites. --full adds the reflect-spot builds
|
||||
# (libavr's rule: reflect compiles are bounded to its spot set, never the
|
||||
# full matrix). LIBAVR_ROOT must point at the libavr checkout.
|
||||
set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
full=0
|
||||
[[ "$1" == "--full" ]] && { full=1; shift; }
|
||||
|
||||
CHIPS=(attiny13 attiny13a attiny25 attiny45 attiny85
|
||||
atmega8 atmega8a atmega16 atmega16a atmega32 atmega32a
|
||||
atmega48 atmega48a atmega48p atmega48pa
|
||||
atmega88 atmega88a atmega88p atmega88pa
|
||||
atmega168 atmega168a atmega168p atmega168pa
|
||||
atmega328 atmega328p
|
||||
atmega164a atmega164p atmega164pa
|
||||
atmega324a atmega324p atmega324pa
|
||||
atmega644 atmega644a atmega644p atmega644pa
|
||||
atmega1284 atmega1284p)
|
||||
REFLECT_SPOT=(attiny13a attiny85 atmega8 atmega16a atmega32a atmega48pa
|
||||
atmega88 atmega168pa atmega328p atmega164a atmega644p atmega1284)
|
||||
|
||||
for chip in "${CHIPS[@]}"; do
|
||||
echo "==== $chip ===="
|
||||
cmake --workflow --preset "$chip-generated" "$@"
|
||||
done
|
||||
|
||||
if ((full)); then
|
||||
for chip in "${REFLECT_SPOT[@]}"; do
|
||||
echo "==== $chip reflect ===="
|
||||
cmake --workflow --preset "$chip-reflect" "$@"
|
||||
done
|
||||
fi
|
||||
|
||||
echo "check: every chip green"
|
||||
90
tools/make_presets.py
Executable file
90
tools/make_presets.py
Executable file
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regenerate CMakePresets.json — one uniform pipeline per chip.
|
||||
|
||||
Every chip gets generated-mode configure/build/test presets and a workflow
|
||||
running all three. Reflect-mode presets (configure + build, no tests — the
|
||||
port's TUs compile identically; the sims prove nothing new there) exist for
|
||||
libavr's reflect spot set only, mirroring its rule: the full reflect matrix
|
||||
is never built, one chip per hardware class and pack vintage is.
|
||||
|
||||
Run from the repo root: tools/make_presets.py
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
CHIPS = [
|
||||
"attiny13", "attiny13a", "attiny25", "attiny45", "attiny85",
|
||||
"atmega8", "atmega8a", "atmega16", "atmega16a", "atmega32", "atmega32a",
|
||||
"atmega48", "atmega48a", "atmega48p", "atmega48pa",
|
||||
"atmega88", "atmega88a", "atmega88p", "atmega88pa",
|
||||
"atmega168", "atmega168a", "atmega168p", "atmega168pa",
|
||||
"atmega328", "atmega328p",
|
||||
"atmega164a", "atmega164p", "atmega164pa",
|
||||
"atmega324a", "atmega324p", "atmega324pa",
|
||||
"atmega644", "atmega644a", "atmega644p", "atmega644pa",
|
||||
"atmega1284", "atmega1284p",
|
||||
]
|
||||
|
||||
# libavr's REFLECT_SPOT (tools/check.sh): one chip per hardware class and
|
||||
# pack vintage.
|
||||
REFLECT_SPOT = [
|
||||
"attiny13a", "attiny85", "atmega8", "atmega16a", "atmega32a",
|
||||
"atmega48pa", "atmega88", "atmega168pa", "atmega328p", "atmega164a",
|
||||
"atmega644p", "atmega1284",
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
configure = [{
|
||||
"name": "base",
|
||||
"hidden": True,
|
||||
"generator": "Ninja",
|
||||
"binaryDir": "${sourceDir}/build/${presetName}",
|
||||
"toolchainFile": "$env{LIBAVR_ROOT}/cmake/avr-toolchain.cmake",
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "Release",
|
||||
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
|
||||
"CMAKE_COLOR_DIAGNOSTICS": "ON",
|
||||
},
|
||||
}]
|
||||
build, test, workflows = [], [], []
|
||||
|
||||
def add(chip, mode):
|
||||
name = f"{chip}-{mode}"
|
||||
configure.append({
|
||||
"name": name,
|
||||
"inherits": "base",
|
||||
"cacheVariables": {
|
||||
"LIBAVR_MCU": chip,
|
||||
"LIBAVR_REFLECT": "ON" if mode == "reflect" else "OFF",
|
||||
},
|
||||
})
|
||||
build.append({"name": name, "configurePreset": name})
|
||||
steps = [{"type": "configure", "name": name}, {"type": "build", "name": name}]
|
||||
if mode == "generated":
|
||||
test.append({"name": name, "configurePreset": name, "output": {"outputOnFailure": True}})
|
||||
steps.append({"type": "test", "name": name})
|
||||
workflows.append({"name": name, "steps": steps})
|
||||
|
||||
for chip in CHIPS:
|
||||
add(chip, "generated")
|
||||
for chip in REFLECT_SPOT:
|
||||
add(chip, "reflect")
|
||||
|
||||
presets = {
|
||||
"version": 8,
|
||||
"configurePresets": configure,
|
||||
"buildPresets": build,
|
||||
"testPresets": test,
|
||||
"workflowPresets": workflows,
|
||||
}
|
||||
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "CMakePresets.json")
|
||||
with open(path, "w") as f:
|
||||
json.dump(presets, f, indent=1)
|
||||
f.write("\n")
|
||||
print(f"{len(CHIPS)} chips, {len(REFLECT_SPOT)} reflect: {os.path.normpath(path)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user