Compare commits
3 Commits
4e5322b1df
...
2e9a50d030
| Author | SHA1 | Date | |
|---|---|---|---|
| 2e9a50d030 | |||
| 860e6c1d47 | |||
| 21c098978b |
285
CMakeLists.txt
285
CMakeLists.txt
@@ -126,154 +126,31 @@ if(LIBAVR_MCU STREQUAL "atmega328p")
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
# pureboot — the pure-constraint port (see pureboot/README.md): one source,
|
# pureboot — the pure-constraint port (see pureboot/README.md): one source,
|
||||||
# no inline assembly, no global register variables, every libavr chip, 512
|
# no inline assembly, no global register variables, every libavr chip,
|
||||||
# bytes each. The loader owns the top 512 bytes of flash on every chip; the
|
# fitting each chip's smallest boot sector. The geometry and the
|
||||||
# application entry symbol is address 0 on the mega (reset re-vectors to the
|
# pureboot_add_loader() deployment function live in pureboot/CMakeLists.txt —
|
||||||
# loader through BOOTRST, so word 0 stays the application's own vector) and
|
# the unit a downstream project consumes; everything below is this port's
|
||||||
# the trampoline word just below the loader on the tinies (host-side vector
|
# own build: the stock loaders, their tests, and the size matrix. The
|
||||||
# surgery points it at the application). --pmem-wrap-around models AVR's
|
# distinct binary dir keeps the `pureboot` target's output name free.
|
||||||
# modulo-flash PC where the flash is big enough to need it.
|
add_subdirectory(pureboot pureboot-cmake)
|
||||||
#
|
|
||||||
# The image is position-independent (check_pi.py asserts the two link-time
|
# The stock loader: the family-default deployment (crystal/RC clock, the
|
||||||
# facts that make it so), and on the tinies its budget is 510, not 512: the
|
# chip's natural link, default pins). The activation window stays a cache
|
||||||
# slot's last word is the trampoline the host composes — the resident slot's
|
# variable — re-timing a deployed loader is a self-update with a re-timed
|
||||||
# holds the application entry, and a staging copy's holds the jump through
|
# build. pureboot9 is that re-timed build, and what the update test installs.
|
||||||
# which it reaches the loader it installed. The activation window is a
|
|
||||||
# compile-time constant; a different PUREBOOT_TIMEOUT builds the re-timed
|
|
||||||
# binary a self-update then installs.
|
|
||||||
set(PUREBOOT_TIMEOUT 8 CACHE STRING "pureboot activation window, seconds")
|
set(PUREBOOT_TIMEOUT 8 CACHE STRING "pureboot activation window, seconds")
|
||||||
# Per-family geometry. The boot-sectioned megas run the loader from the
|
pureboot_add_loader(pureboot TIMEOUT ${PUREBOOT_TIMEOUT})
|
||||||
# hardware boot section and boot the application at word 0; the tinies and
|
|
||||||
# the boot-section-less m48s get the trampoline surgery. All megas assume a
|
|
||||||
# 16 MHz crystal at 115200 Bd; the tinies their internal RC at 57600 Bd over
|
|
||||||
# the software UART. --pmem-wrap-around models AVR's modulo-flash PC where
|
|
||||||
# the flash is big enough to need it (an rjmp reaches all of 4 KiB by
|
|
||||||
# itself).
|
|
||||||
if(LIBAVR_MCU MATCHES "^attiny13a?$")
|
|
||||||
set(_pb_flash 1024)
|
|
||||||
set(_pb_wrap "")
|
|
||||||
set(_pb_page 32)
|
|
||||||
set(_pb_hz 9600000)
|
|
||||||
set(_pb_baud 57600)
|
|
||||||
set(_pb_eeprom 64)
|
|
||||||
elseif(LIBAVR_MCU STREQUAL "attiny25")
|
|
||||||
set(_pb_flash 2048)
|
|
||||||
set(_pb_wrap "")
|
|
||||||
set(_pb_page 32)
|
|
||||||
set(_pb_hz 8000000)
|
|
||||||
set(_pb_baud 57600)
|
|
||||||
set(_pb_eeprom 128)
|
|
||||||
elseif(LIBAVR_MCU STREQUAL "attiny45")
|
|
||||||
set(_pb_flash 4096)
|
|
||||||
set(_pb_wrap "")
|
|
||||||
set(_pb_page 64)
|
|
||||||
set(_pb_hz 8000000)
|
|
||||||
set(_pb_baud 57600)
|
|
||||||
set(_pb_eeprom 256)
|
|
||||||
elseif(LIBAVR_MCU STREQUAL "attiny85")
|
|
||||||
set(_pb_flash 8192)
|
|
||||||
set(_pb_wrap -Wl,--pmem-wrap-around=8k)
|
|
||||||
set(_pb_page 64)
|
|
||||||
set(_pb_hz 8000000)
|
|
||||||
set(_pb_baud 57600)
|
|
||||||
set(_pb_eeprom 512)
|
|
||||||
elseif(LIBAVR_MCU MATCHES "^atmega48(a|p|pa)?$")
|
|
||||||
set(_pb_flash 4096)
|
|
||||||
set(_pb_wrap "")
|
|
||||||
set(_pb_page 64)
|
|
||||||
set(_pb_hz 16000000)
|
|
||||||
set(_pb_baud 115200)
|
|
||||||
set(_pb_eeprom 256)
|
|
||||||
elseif(LIBAVR_MCU MATCHES "^atmega8a?$" OR LIBAVR_MCU MATCHES "^atmega88(a|p|pa)?$")
|
|
||||||
set(_pb_flash 8192)
|
|
||||||
set(_pb_wrap -Wl,--pmem-wrap-around=8k)
|
|
||||||
set(_pb_page 64)
|
|
||||||
set(_pb_hz 16000000)
|
|
||||||
set(_pb_baud 115200)
|
|
||||||
set(_pb_eeprom 512)
|
|
||||||
elseif(LIBAVR_MCU MATCHES "^atmega16a?$" OR LIBAVR_MCU MATCHES "^atmega168(a|p|pa)?$" OR
|
|
||||||
LIBAVR_MCU MATCHES "^atmega164(a|p|pa)$")
|
|
||||||
set(_pb_flash 16384)
|
|
||||||
set(_pb_wrap -Wl,--pmem-wrap-around=16k)
|
|
||||||
set(_pb_page 128)
|
|
||||||
set(_pb_hz 16000000)
|
|
||||||
set(_pb_baud 115200)
|
|
||||||
set(_pb_eeprom 512)
|
|
||||||
elseif(LIBAVR_MCU MATCHES "^atmega32a?$" OR LIBAVR_MCU MATCHES "^atmega328p?$" OR
|
|
||||||
LIBAVR_MCU MATCHES "^atmega324(a|p|pa)$")
|
|
||||||
set(_pb_flash 32768)
|
|
||||||
set(_pb_wrap -Wl,--pmem-wrap-around=32k)
|
|
||||||
set(_pb_page 128)
|
|
||||||
set(_pb_hz 16000000)
|
|
||||||
set(_pb_baud 115200)
|
|
||||||
set(_pb_eeprom 1024)
|
|
||||||
elseif(LIBAVR_MCU MATCHES "^atmega644(a|p|pa)?$")
|
|
||||||
# 64 KiB is exactly the 16-bit byte space: plain LPM reaches everything,
|
|
||||||
# and the smallest boot section (1 KiB) holds the loader and its staging
|
|
||||||
# slot together (see pureboot/README.md).
|
|
||||||
set(_pb_flash 65536)
|
|
||||||
set(_pb_wrap -Wl,--pmem-wrap-around=64k)
|
|
||||||
set(_pb_page 256)
|
|
||||||
set(_pb_hz 16000000)
|
|
||||||
set(_pb_baud 115200)
|
|
||||||
set(_pb_eeprom 2048)
|
|
||||||
elseif(LIBAVR_MCU MATCHES "^atmega1284p?$")
|
|
||||||
# 128 KiB: wire flash addresses are word addresses, reads go through
|
|
||||||
# ELPM, and the PC's modulo wrap exceeds what --pmem-wrap-around models.
|
|
||||||
# The slot is 1 KiB — this chip's own smallest boot sector; the far
|
|
||||||
# machinery cannot fit 512 B (see pureboot/README.md).
|
|
||||||
set(_pb_flash 131072)
|
|
||||||
set(_pb_wrap "")
|
|
||||||
set(_pb_page 256)
|
|
||||||
set(_pb_hz 16000000)
|
|
||||||
set(_pb_baud 115200)
|
|
||||||
set(_pb_eeprom 4096)
|
|
||||||
set(_pb_slot 1024)
|
|
||||||
set(_pb_limit 1024)
|
|
||||||
else()
|
|
||||||
message(FATAL_ERROR "pureboot: no geometry for ${LIBAVR_MCU}")
|
|
||||||
endif()
|
|
||||||
if(NOT DEFINED _pb_slot)
|
|
||||||
set(_pb_slot 512)
|
|
||||||
endif()
|
|
||||||
math(EXPR _pb_base "${_pb_flash} - ${_pb_slot}")
|
|
||||||
math(EXPR _pb_base_hex "${_pb_base}" OUTPUT_FORMAT HEXADECIMAL)
|
|
||||||
# Patched-vector chips hand over through the trampoline word below the slot,
|
|
||||||
# which is also the slot's own last word — their budget is slot − 2.
|
|
||||||
if(LIBAVR_MCU MATCHES "^atmega" AND NOT LIBAVR_MCU MATCHES "^atmega48")
|
|
||||||
set(_pb_app 0)
|
|
||||||
if(NOT DEFINED _pb_limit)
|
|
||||||
set(_pb_limit ${_pb_slot})
|
|
||||||
endif()
|
|
||||||
else()
|
|
||||||
math(EXPR _pb_app "${_pb_base} - 2")
|
|
||||||
math(EXPR _pb_limit "${_pb_slot} - 2")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# simavr names its cores after the base dies; the A revisions run on them
|
|
||||||
# (the 644PA on the 644P core).
|
|
||||||
set(_pb_sim_mcu ${LIBAVR_MCU})
|
|
||||||
if(LIBAVR_MCU MATCHES "^atmega(8|16|32|48|88|164|168|644)a$")
|
|
||||||
string(REGEX REPLACE "a$" "" _pb_sim_mcu ${LIBAVR_MCU})
|
|
||||||
elseif(LIBAVR_MCU STREQUAL "atmega644pa")
|
|
||||||
set(_pb_sim_mcu atmega644p)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
add_executable(pureboot pureboot/pureboot.cpp)
|
|
||||||
target_link_libraries(pureboot PRIVATE libavr)
|
|
||||||
target_compile_definitions(pureboot PRIVATE PUREBOOT_TIMEOUT=${PUREBOOT_TIMEOUT})
|
|
||||||
target_link_options(pureboot PRIVATE -nostartfiles -Wl,--section-start=.text=${_pb_base_hex}
|
|
||||||
-Wl,--defsym=pureboot_app=${_pb_app} ${_pb_wrap})
|
|
||||||
add_custom_command(TARGET pureboot POST_BUILD COMMAND ${CMAKE_SIZE} $<TARGET_FILE:pureboot>)
|
|
||||||
add_image_outputs(pureboot)
|
|
||||||
if(PROJECT_IS_TOP_LEVEL)
|
if(PROJECT_IS_TOP_LEVEL)
|
||||||
|
get_target_property(_pb_stock_hz pureboot PUREBOOT_HZ)
|
||||||
|
get_target_property(_pb_stock_baud pureboot PUREBOOT_BAUD)
|
||||||
|
|
||||||
add_test(NAME pureboot.size
|
add_test(NAME pureboot.size
|
||||||
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:pureboot>
|
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:pureboot>
|
||||||
-DLIMIT=${_pb_limit} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
|
-DLIMIT=${PUREBOOT_LIMIT} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
|
||||||
if(Python3_FOUND)
|
if(Python3_FOUND)
|
||||||
add_test(NAME pureboot.pi
|
add_test(NAME pureboot.pi
|
||||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/check_pi.py
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/check_pi.py
|
||||||
${CMAKE_OBJDUMP} ${CMAKE_NM} $<TARGET_FILE:pureboot> ${_pb_base_hex})
|
${CMAKE_OBJDUMP} ${CMAKE_NM} $<TARGET_FILE:pureboot> ${PUREBOOT_BASE_HEX})
|
||||||
add_test(NAME pureboot.planner
|
add_test(NAME pureboot.planner
|
||||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/test_planner.py
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/test_planner.py
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py)
|
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py)
|
||||||
@@ -289,9 +166,9 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:pbapp> $<TARGET_FILE:pbapp>.bin)
|
COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:pbapp> $<TARGET_FILE:pbapp>.bin)
|
||||||
add_test(NAME pureboot.protocol
|
add_test(NAME pureboot.protocol
|
||||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbtest.py
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbtest.py
|
||||||
${PB_DEVICE} $<TARGET_FILE:pureboot> ${_pb_sim_mcu} ${_pb_hz} ${_pb_base_hex}
|
${PB_DEVICE} $<TARGET_FILE:pureboot> ${PUREBOOT_SIM_MCU} ${_pb_stock_hz}
|
||||||
${_pb_page} ${_pb_baud} ${_pb_eeprom} $<TARGET_FILE:pbapp>.bin
|
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_pb_stock_baud} ${PUREBOOT_EEPROM}
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
$<TARGET_FILE:pbapp>.bin ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
${CMAKE_BINARY_DIR}/pbtest-work)
|
${CMAKE_BINARY_DIR}/pbtest-work)
|
||||||
set_tests_properties(pureboot.protocol PROPERTIES TIMEOUT 180)
|
set_tests_properties(pureboot.protocol PROPERTIES TIMEOUT 180)
|
||||||
|
|
||||||
@@ -299,43 +176,127 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
# installed one slot lower, must serve the full command set.
|
# installed one slot lower, must serve the full command set.
|
||||||
add_test(NAME pureboot.reloc
|
add_test(NAME pureboot.reloc
|
||||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbreloc.py
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbreloc.py
|
||||||
${PB_DEVICE} $<TARGET_FILE:pureboot> ${_pb_sim_mcu} ${_pb_hz} ${_pb_base_hex}
|
${PB_DEVICE} $<TARGET_FILE:pureboot> ${PUREBOOT_SIM_MCU} ${_pb_stock_hz}
|
||||||
${_pb_page} ${_pb_baud} ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_pb_stock_baud}
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
${CMAKE_BINARY_DIR}/pbreloc-work)
|
${CMAKE_BINARY_DIR}/pbreloc-work)
|
||||||
set_tests_properties(pureboot.reloc PROPERTIES TIMEOUT 180
|
set_tests_properties(pureboot.reloc PROPERTIES TIMEOUT 180
|
||||||
ENVIRONMENT "PB_OBJCOPY=${CMAKE_OBJCOPY}")
|
ENVIRONMENT "PB_OBJCOPY=${CMAKE_OBJCOPY}")
|
||||||
|
|
||||||
# Re-homing: a loader mistakenly programmed at address 0 (a raw .bin
|
# Re-homing: a loader mistakenly programmed at address 0 (a raw .bin
|
||||||
# handed to a programmer) must heal into the canonical slot through
|
# handed to a programmer) or sitting in the staging slot must heal
|
||||||
# the ordinary --update-loader flow. Patched-vector behavior, so one
|
# into the canonical slot through the ordinary --update-loader flow.
|
||||||
# representative chip carries it.
|
# Patched-vector behavior, so one representative chip carries it.
|
||||||
if(LIBAVR_MCU STREQUAL "attiny85")
|
if(LIBAVR_MCU STREQUAL "attiny85")
|
||||||
add_test(NAME pureboot.rehome
|
add_test(NAME pureboot.rehome
|
||||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbrehome.py
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbrehome.py
|
||||||
${PB_DEVICE} $<TARGET_FILE:pureboot> $<TARGET_FILE:pureboot9>.bin ${_pb_sim_mcu}
|
${PB_DEVICE} $<TARGET_FILE:pureboot> $<TARGET_FILE:pureboot9>.bin
|
||||||
${_pb_hz} ${_pb_base_hex} ${_pb_page} ${_pb_baud} $<TARGET_FILE:pbapp>.bin
|
${PUREBOOT_SIM_MCU} ${_pb_stock_hz} ${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE}
|
||||||
|
${_pb_stock_baud} $<TARGET_FILE:pbapp>.bin
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
${CMAKE_BINARY_DIR}/pbrehome-work)
|
${CMAKE_BINARY_DIR}/pbrehome-work)
|
||||||
set_tests_properties(pureboot.rehome PROPERTIES TIMEOUT 180)
|
set_tests_properties(pureboot.rehome PROPERTIES TIMEOUT 180)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# The self-update end-to-end: the re-timed build (same source, only
|
# The self-update end-to-end: the re-timed build (same source, only
|
||||||
# PUREBOOT_TIMEOUT differs — a byte-different image) replaces the
|
# the timeout differs — a byte-different image) replaces the resident
|
||||||
# resident through --update-loader, with every power-fail phase
|
# through --update-loader, with every power-fail phase rehearsed from
|
||||||
# rehearsed from the runner's flash dumps.
|
# the runner's flash dumps.
|
||||||
add_executable(pureboot9 pureboot/pureboot.cpp)
|
pureboot_add_loader(pureboot9 TIMEOUT 9)
|
||||||
target_link_libraries(pureboot9 PRIVATE libavr)
|
|
||||||
target_compile_definitions(pureboot9 PRIVATE PUREBOOT_TIMEOUT=9)
|
|
||||||
target_link_options(pureboot9 PRIVATE -nostartfiles -Wl,--section-start=.text=${_pb_base_hex}
|
|
||||||
-Wl,--defsym=pureboot_app=${_pb_app} ${_pb_wrap})
|
|
||||||
add_image_outputs(pureboot9)
|
|
||||||
add_test(NAME pureboot.update
|
add_test(NAME pureboot.update
|
||||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbupdate.py
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbupdate.py
|
||||||
${PB_DEVICE} $<TARGET_FILE:pureboot> $<TARGET_FILE:pureboot9> ${_pb_sim_mcu}
|
${PB_DEVICE} $<TARGET_FILE:pureboot> $<TARGET_FILE:pureboot9>
|
||||||
${_pb_hz} ${_pb_base_hex} ${_pb_page} ${_pb_baud} $<TARGET_FILE:pbapp>.bin
|
${PUREBOOT_SIM_MCU} ${_pb_stock_hz} ${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE}
|
||||||
|
${_pb_stock_baud} $<TARGET_FILE:pbapp>.bin
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
${CMAKE_BINARY_DIR}/pbupdate-work)
|
${CMAKE_BINARY_DIR}/pbupdate-work)
|
||||||
set_tests_properties(pureboot.update PROPERTIES TIMEOUT 600
|
set_tests_properties(pureboot.update PROPERTIES TIMEOUT 600
|
||||||
ENVIRONMENT "PB_OBJCOPY=${CMAKE_OBJCOPY}")
|
ENVIRONMENT "PB_OBJCOPY=${CMAKE_OBJCOPY}")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# The size matrix: every configuration axis that could move the image
|
||||||
|
# size — the serial backend (different code), the clock and its ladder
|
||||||
|
# baud (different constants and divisor shapes), the USART instance
|
||||||
|
# (different register class) — each combination must still fit the
|
||||||
|
# chip's slot budget. Pins are size-neutral (port and bit are immediate
|
||||||
|
# operands) and the timeout is a constant, so neither adds an axis. The
|
||||||
|
# stock build is one point of this matrix and already has its test.
|
||||||
|
function(pureboot_size_variant name)
|
||||||
|
pureboot_add_loader(${name} ${ARGN})
|
||||||
|
add_test(NAME ${name}.size
|
||||||
|
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:${name}>
|
||||||
|
-DLIMIT=${PUREBOOT_LIMIT} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
# Clock points: the shipped-fuse floor (CKDIV8), the calibrated RC, and
|
||||||
|
# the crystal the stock build assumes (the tiny13's ladder is its own RC
|
||||||
|
# menu — it has no crystal option).
|
||||||
|
if(LIBAVR_MCU MATCHES "^attiny13")
|
||||||
|
set(_matrix_clocks 1200000 4800000 9600000)
|
||||||
|
else()
|
||||||
|
set(_matrix_clocks 1000000 8000000 16000000)
|
||||||
|
endif()
|
||||||
|
foreach(_matrix_hz IN LISTS _matrix_clocks)
|
||||||
|
math(EXPR _matrix_khz "${_matrix_hz} / 1000")
|
||||||
|
if(PUREBOOT_HAS_USART OR NOT _matrix_hz EQUAL _pb_stock_hz)
|
||||||
|
pureboot_size_variant(pureboot_sw_${_matrix_khz}k CLOCK ${_matrix_hz} SERIAL software)
|
||||||
|
endif()
|
||||||
|
if(PUREBOOT_HAS_USART AND NOT _matrix_hz EQUAL _pb_stock_hz)
|
||||||
|
pureboot_size_variant(pureboot_hw_${_matrix_khz}k CLOCK ${_matrix_hz} SERIAL hardware)
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
if(PUREBOOT_HAS_USART1)
|
||||||
|
pureboot_size_variant(pureboot_usart1 USART 1)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# One configured deployment end to end — a real board's shape rather
|
||||||
|
# than the stock assumption: the ATmega328P on its shipped 1 MHz fuses,
|
||||||
|
# the software UART on hand-picked pins (TX = PB1, RX = PB5), the ladder
|
||||||
|
# baud (9600). The full protocol suite runs against it, fixture
|
||||||
|
# application included, over the runner's GPIO bridge — proving the
|
||||||
|
# configuration plumbing produces a working loader, not just one that
|
||||||
|
# fits.
|
||||||
|
if(LIBAVR_MCU STREQUAL "atmega328p" AND DEFINED PB_DEVICE)
|
||||||
|
pureboot_size_variant(pureboot_custom CLOCK 1000000 SERIAL software RX pb5 TX pb1)
|
||||||
|
get_target_property(_custom_hz pureboot_custom PUREBOOT_HZ)
|
||||||
|
get_target_property(_custom_baud pureboot_custom PUREBOOT_BAUD)
|
||||||
|
get_target_property(_custom_link pureboot_custom PUREBOOT_LINK)
|
||||||
|
add_executable(pbapp_custom test/pbapp.cpp)
|
||||||
|
target_link_libraries(pbapp_custom PRIVATE libavr)
|
||||||
|
target_compile_definitions(pbapp_custom PRIVATE PUREBOOT_CLOCK_HZ=${_custom_hz}
|
||||||
|
PUREBOOT_BAUD=${_custom_baud} PUREBOOT_SOFT_SERIAL PUREBOOT_TX=pb1)
|
||||||
|
add_custom_command(TARGET pbapp_custom POST_BUILD
|
||||||
|
COMMAND ${CMAKE_OBJCOPY} -O binary
|
||||||
|
$<TARGET_FILE:pbapp_custom> $<TARGET_FILE:pbapp_custom>.bin)
|
||||||
|
add_test(NAME pureboot.custom
|
||||||
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbtest.py
|
||||||
|
${PB_DEVICE} $<TARGET_FILE:pureboot_custom> ${PUREBOOT_SIM_MCU} ${_custom_hz}
|
||||||
|
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_custom_baud} ${PUREBOOT_EEPROM}
|
||||||
|
$<TARGET_FILE:pbapp_custom>.bin ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
|
${CMAKE_BINARY_DIR}/pbcustom-work ${_custom_link})
|
||||||
|
set_tests_properties(pureboot.custom PROPERTIES TIMEOUT 180)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# The second USART, driven for real on one chip: instance selection is
|
||||||
|
# compile-checked everywhere, but only a live session proves the loader
|
||||||
|
# initialized and polls the USART it claims to. The fixture application
|
||||||
|
# banners on the same instance.
|
||||||
|
if(LIBAVR_MCU STREQUAL "atmega644a" AND DEFINED PB_DEVICE)
|
||||||
|
get_target_property(_usart1_hz pureboot_usart1 PUREBOOT_HZ)
|
||||||
|
get_target_property(_usart1_baud pureboot_usart1 PUREBOOT_BAUD)
|
||||||
|
add_executable(pbapp_usart1 test/pbapp.cpp)
|
||||||
|
target_link_libraries(pbapp_usart1 PRIVATE libavr)
|
||||||
|
target_compile_definitions(pbapp_usart1 PRIVATE PUREBOOT_CLOCK_HZ=${_usart1_hz}
|
||||||
|
PUREBOOT_BAUD=${_usart1_baud} PUREBOOT_USART=1)
|
||||||
|
add_custom_command(TARGET pbapp_usart1 POST_BUILD
|
||||||
|
COMMAND ${CMAKE_OBJCOPY} -O binary
|
||||||
|
$<TARGET_FILE:pbapp_usart1> $<TARGET_FILE:pbapp_usart1>.bin)
|
||||||
|
add_test(NAME pureboot.usart1
|
||||||
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbtest.py
|
||||||
|
${PB_DEVICE} $<TARGET_FILE:pureboot_usart1> ${PUREBOOT_SIM_MCU} ${_usart1_hz}
|
||||||
|
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_usart1_baud} ${PUREBOOT_EEPROM}
|
||||||
|
$<TARGET_FILE:pbapp_usart1>.bin ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
|
${CMAKE_BINARY_DIR}/pbusart1-work usart1)
|
||||||
|
set_tests_properties(pureboot.usart1 PROPERTIES TIMEOUT 180)
|
||||||
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
2556
CMakePresets.json
2556
CMakePresets.json
File diff suppressed because it is too large
Load Diff
310
pureboot/CMakeLists.txt
Normal file
310
pureboot/CMakeLists.txt
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
# pureboot as a consumable CMake unit: the per-chip geometry, the default
|
||||||
|
# baud ladder, and pureboot_add_loader() — the one way a loader target is
|
||||||
|
# created, both by this port's own build and by a downstream project. A
|
||||||
|
# downstream project brings its usual libavr setup (the `libavr` target and
|
||||||
|
# the LIBAVR_MCU toolchain preset), adds this directory, and states its
|
||||||
|
# deployment:
|
||||||
|
#
|
||||||
|
# add_subdirectory(bootloader/pureboot)
|
||||||
|
# pureboot_add_loader(myboot CLOCK 1000000 SERIAL software TX pb1 RX pb5)
|
||||||
|
#
|
||||||
|
# Every argument is optional — CLOCK defaults to the family assumption
|
||||||
|
# below, BAUD to the fastest standard rate the clock reaches within 2.5 %
|
||||||
|
# (the ladder), SERIAL to the chip's hardware USART where it has one
|
||||||
|
# (`hardware`/`software` force a backend, USART 1 picks the second
|
||||||
|
# instance), RX/TX to pb0/pb1 for the software UART, TIMEOUT to 8 s.
|
||||||
|
# Infeasible picks fail the build by name: libavr's baud-error and
|
||||||
|
# software-UART cycle-floor static asserts re-check whatever is passed.
|
||||||
|
|
||||||
|
# Per-family geometry: flash/page/EEPROM sizes and the linker wrap the PC
|
||||||
|
# modulo needs, the loader slot (each chip's smallest boot sector — 1 KiB on
|
||||||
|
# the word-addressed 1284s), and the deployment defaults (crystal assumption
|
||||||
|
# on the megas, calibrated RC on the tinies). The USART flags mirror the
|
||||||
|
# hardware inventory the loader's own static asserts check (the plain 644 is
|
||||||
|
# the x4 family's one single-USART die, Atmel-2593).
|
||||||
|
set(_pb_has_usart 1)
|
||||||
|
set(_pb_has_usart1 0)
|
||||||
|
if(LIBAVR_MCU MATCHES "^attiny13a?$")
|
||||||
|
set(_pb_flash 1024)
|
||||||
|
set(_pb_wrap "")
|
||||||
|
set(_pb_page 32)
|
||||||
|
set(_pb_hz 9600000)
|
||||||
|
set(_pb_eeprom 64)
|
||||||
|
set(_pb_has_usart 0)
|
||||||
|
elseif(LIBAVR_MCU STREQUAL "attiny25")
|
||||||
|
set(_pb_flash 2048)
|
||||||
|
set(_pb_wrap "")
|
||||||
|
set(_pb_page 32)
|
||||||
|
set(_pb_hz 8000000)
|
||||||
|
set(_pb_eeprom 128)
|
||||||
|
set(_pb_has_usart 0)
|
||||||
|
elseif(LIBAVR_MCU STREQUAL "attiny45")
|
||||||
|
set(_pb_flash 4096)
|
||||||
|
set(_pb_wrap "")
|
||||||
|
set(_pb_page 64)
|
||||||
|
set(_pb_hz 8000000)
|
||||||
|
set(_pb_eeprom 256)
|
||||||
|
set(_pb_has_usart 0)
|
||||||
|
elseif(LIBAVR_MCU STREQUAL "attiny85")
|
||||||
|
set(_pb_flash 8192)
|
||||||
|
set(_pb_wrap -Wl,--pmem-wrap-around=8k)
|
||||||
|
set(_pb_page 64)
|
||||||
|
set(_pb_hz 8000000)
|
||||||
|
set(_pb_eeprom 512)
|
||||||
|
set(_pb_has_usart 0)
|
||||||
|
elseif(LIBAVR_MCU MATCHES "^atmega48(a|p|pa)?$")
|
||||||
|
set(_pb_flash 4096)
|
||||||
|
set(_pb_wrap "")
|
||||||
|
set(_pb_page 64)
|
||||||
|
set(_pb_hz 16000000)
|
||||||
|
set(_pb_eeprom 256)
|
||||||
|
elseif(LIBAVR_MCU MATCHES "^atmega8a?$" OR LIBAVR_MCU MATCHES "^atmega88(a|p|pa)?$")
|
||||||
|
set(_pb_flash 8192)
|
||||||
|
set(_pb_wrap -Wl,--pmem-wrap-around=8k)
|
||||||
|
set(_pb_page 64)
|
||||||
|
set(_pb_hz 16000000)
|
||||||
|
set(_pb_eeprom 512)
|
||||||
|
elseif(LIBAVR_MCU MATCHES "^atmega16a?$" OR LIBAVR_MCU MATCHES "^atmega168(a|p|pa)?$")
|
||||||
|
set(_pb_flash 16384)
|
||||||
|
set(_pb_wrap -Wl,--pmem-wrap-around=16k)
|
||||||
|
set(_pb_page 128)
|
||||||
|
set(_pb_hz 16000000)
|
||||||
|
set(_pb_eeprom 512)
|
||||||
|
elseif(LIBAVR_MCU MATCHES "^atmega164(a|p|pa)$")
|
||||||
|
set(_pb_flash 16384)
|
||||||
|
set(_pb_wrap -Wl,--pmem-wrap-around=16k)
|
||||||
|
set(_pb_page 128)
|
||||||
|
set(_pb_hz 16000000)
|
||||||
|
set(_pb_eeprom 512)
|
||||||
|
set(_pb_has_usart1 1)
|
||||||
|
elseif(LIBAVR_MCU MATCHES "^atmega32a?$" OR LIBAVR_MCU MATCHES "^atmega328p?$")
|
||||||
|
set(_pb_flash 32768)
|
||||||
|
set(_pb_wrap -Wl,--pmem-wrap-around=32k)
|
||||||
|
set(_pb_page 128)
|
||||||
|
set(_pb_hz 16000000)
|
||||||
|
set(_pb_eeprom 1024)
|
||||||
|
elseif(LIBAVR_MCU MATCHES "^atmega324(a|p|pa)$")
|
||||||
|
set(_pb_flash 32768)
|
||||||
|
set(_pb_wrap -Wl,--pmem-wrap-around=32k)
|
||||||
|
set(_pb_page 128)
|
||||||
|
set(_pb_hz 16000000)
|
||||||
|
set(_pb_eeprom 1024)
|
||||||
|
set(_pb_has_usart1 1)
|
||||||
|
elseif(LIBAVR_MCU MATCHES "^atmega644(a|p|pa)?$")
|
||||||
|
# 64 KiB is exactly the 16-bit byte space: plain LPM reaches everything,
|
||||||
|
# and the smallest boot section (1 KiB) holds the loader and its staging
|
||||||
|
# slot together (see README.md). The plain 644 is the family's one
|
||||||
|
# single-USART die.
|
||||||
|
set(_pb_flash 65536)
|
||||||
|
set(_pb_wrap -Wl,--pmem-wrap-around=64k)
|
||||||
|
set(_pb_page 256)
|
||||||
|
set(_pb_hz 16000000)
|
||||||
|
set(_pb_eeprom 2048)
|
||||||
|
if(NOT LIBAVR_MCU STREQUAL "atmega644")
|
||||||
|
set(_pb_has_usart1 1)
|
||||||
|
endif()
|
||||||
|
elseif(LIBAVR_MCU MATCHES "^atmega1284p?$")
|
||||||
|
# 128 KiB: wire flash addresses are word addresses, reads go through
|
||||||
|
# ELPM, and the PC's modulo wrap exceeds what --pmem-wrap-around models.
|
||||||
|
# The slot is 1 KiB — this chip's own smallest boot sector; the far
|
||||||
|
# machinery cannot fit 512 B (see README.md).
|
||||||
|
set(_pb_flash 131072)
|
||||||
|
set(_pb_wrap "")
|
||||||
|
set(_pb_page 256)
|
||||||
|
set(_pb_hz 16000000)
|
||||||
|
set(_pb_eeprom 4096)
|
||||||
|
set(_pb_slot 1024)
|
||||||
|
set(_pb_limit 1024)
|
||||||
|
set(_pb_has_usart1 1)
|
||||||
|
else()
|
||||||
|
message(FATAL_ERROR "pureboot: no geometry for ${LIBAVR_MCU}")
|
||||||
|
endif()
|
||||||
|
if(NOT DEFINED _pb_slot)
|
||||||
|
set(_pb_slot 512)
|
||||||
|
endif()
|
||||||
|
math(EXPR _pb_base "${_pb_flash} - ${_pb_slot}")
|
||||||
|
math(EXPR _pb_base_hex "${_pb_base}" OUTPUT_FORMAT HEXADECIMAL)
|
||||||
|
# Patched-vector chips hand over through the trampoline word below the slot,
|
||||||
|
# which is also the slot's own last word — their budget is slot − 2.
|
||||||
|
if(LIBAVR_MCU MATCHES "^atmega" AND NOT LIBAVR_MCU MATCHES "^atmega48")
|
||||||
|
set(_pb_app 0)
|
||||||
|
if(NOT DEFINED _pb_limit)
|
||||||
|
set(_pb_limit ${_pb_slot})
|
||||||
|
endif()
|
||||||
|
else()
|
||||||
|
math(EXPR _pb_app "${_pb_base} - 2")
|
||||||
|
math(EXPR _pb_limit "${_pb_slot} - 2")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# simavr names its cores after the base dies; the A revisions run on them
|
||||||
|
# (the 644PA on the 644P core).
|
||||||
|
set(_pb_sim_mcu ${LIBAVR_MCU})
|
||||||
|
if(LIBAVR_MCU MATCHES "^atmega(8|16|32|48|88|164|168|644)a$")
|
||||||
|
string(REGEX REPLACE "a$" "" _pb_sim_mcu ${LIBAVR_MCU})
|
||||||
|
elseif(LIBAVR_MCU STREQUAL "atmega644pa")
|
||||||
|
set(_pb_sim_mcu atmega644p)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# The function runs in its caller's scope, so everything it needs crosses
|
||||||
|
# scopes as global properties.
|
||||||
|
set_property(GLOBAL PROPERTY PUREBOOT_BASE_HEX ${_pb_base_hex})
|
||||||
|
set_property(GLOBAL PROPERTY PUREBOOT_APP ${_pb_app})
|
||||||
|
set_property(GLOBAL PROPERTY PUREBOOT_WRAP "${_pb_wrap}")
|
||||||
|
set_property(GLOBAL PROPERTY PUREBOOT_DEFAULT_HZ ${_pb_hz})
|
||||||
|
set_property(GLOBAL PROPERTY PUREBOOT_HAS_USART ${_pb_has_usart})
|
||||||
|
set_property(GLOBAL PROPERTY PUREBOOT_HAS_USART1 ${_pb_has_usart1})
|
||||||
|
|
||||||
|
# The port's own build (tests, the size matrix) reads the geometry from the
|
||||||
|
# parent scope; a downstream consumer gets the same variables for free.
|
||||||
|
set(PUREBOOT_BASE_HEX ${_pb_base_hex} PARENT_SCOPE)
|
||||||
|
set(PUREBOOT_PAGE ${_pb_page} PARENT_SCOPE)
|
||||||
|
set(PUREBOOT_SLOT ${_pb_slot} PARENT_SCOPE)
|
||||||
|
set(PUREBOOT_LIMIT ${_pb_limit} PARENT_SCOPE)
|
||||||
|
set(PUREBOOT_EEPROM ${_pb_eeprom} PARENT_SCOPE)
|
||||||
|
set(PUREBOOT_DEFAULT_HZ ${_pb_hz} PARENT_SCOPE)
|
||||||
|
set(PUREBOOT_HAS_USART ${_pb_has_usart} PARENT_SCOPE)
|
||||||
|
set(PUREBOOT_HAS_USART1 ${_pb_has_usart1} PARENT_SCOPE)
|
||||||
|
set(PUREBOOT_SIM_MCU ${_pb_sim_mcu} PARENT_SCOPE)
|
||||||
|
|
||||||
|
# The fastest standard rate the clock reaches within 2.5 % — the same
|
||||||
|
# best-of-U2X-and-plain divisor search libavr's solve_baud runs, so a
|
||||||
|
# default never trips the compile-time error it is checked against. A
|
||||||
|
# software build additionally requires the polled receiver's 100-cycles-a-bit
|
||||||
|
# floor (its own static assert): at low clocks the U2X divisor still reaches
|
||||||
|
# rates the bit-banged sampler cannot, so the backend gates the ladder.
|
||||||
|
function(pureboot_default_baud clock software outvar)
|
||||||
|
foreach(baud 115200 57600 38400 19200 9600)
|
||||||
|
math(EXPR _cycles "${clock} / ${baud}")
|
||||||
|
if(software AND _cycles LESS 100)
|
||||||
|
continue()
|
||||||
|
endif()
|
||||||
|
foreach(divisor 8 16)
|
||||||
|
math(EXPR _step "${divisor} * ${baud}")
|
||||||
|
math(EXPR _n "(${clock} + ${_step} / 2) / ${_step}")
|
||||||
|
if(_n LESS 1 OR _n GREATER 4096)
|
||||||
|
continue()
|
||||||
|
endif()
|
||||||
|
math(EXPR _actual "${clock} / (${divisor} * ${_n})")
|
||||||
|
math(EXPR _delta "${_actual} - ${baud}")
|
||||||
|
if(_delta LESS 0)
|
||||||
|
math(EXPR _delta "-(${_delta})")
|
||||||
|
endif()
|
||||||
|
math(EXPR _error_bp "${_delta} * 10000 / ${baud}")
|
||||||
|
if(_error_bp LESS_EQUAL 250)
|
||||||
|
set(${outvar} ${baud} PARENT_SCOPE)
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
endforeach()
|
||||||
|
message(FATAL_ERROR "pureboot: no standard baud rate fits a ${clock} Hz clock within 2.5 %")
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
# pureboot_add_loader(<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})
|
||||||
|
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()
|
||||||
@@ -3,16 +3,18 @@
|
|||||||
A serial bootloader on [libavr](https://git.blackmark.me/avr/libavr), pure by
|
A serial bootloader on [libavr](https://git.blackmark.me/avr/libavr), pure by
|
||||||
constraint: one C++ source, no inline assembly, no global register variables
|
constraint: one C++ source, no inline assembly, no global register variables
|
||||||
(attributes allowed), built for **every chip libavr targets — all 37 —
|
(attributes allowed), built for **every chip libavr targets — all 37 —
|
||||||
fitting each chip's smallest boot sector**: 512 bytes everywhere — 488 B on
|
fitting each chip's smallest boot sector**: 512 bytes everywhere — 470 B on
|
||||||
the tiny13s, 498–502 B on the tiny25/45/85, 466–504 B across the megas
|
the tiny13s, ~484 B on the tiny25/45/85, 458–506 B across the megas and
|
||||||
(474 B on the boot-section-less m48s, 498 B on the 644s) — except the
|
every configured variant of them (the m8's software-serial build is the
|
||||||
ATmega1284/1284P, whose smallest boot sector is 1 KiB and whose far-flash
|
fattest) — except the ATmega1284/1284P, whose smallest boot sector is 1 KiB
|
||||||
machinery (ELPM reads, RAMPZ page commands, word-addressed wire) lands at
|
and whose far-flash machinery (ELPM reads, RAMPZ page commands,
|
||||||
558 B in a 1 KiB slot: the 512-byte figure is a hardware boundary those
|
word-addressed wire) lands at 556–562 B in a 1 KiB slot: the 512-byte
|
||||||
chips simply do not have, and no implementation of this feature set fits it
|
figure is a hardware boundary those chips simply do not have, and no
|
||||||
there. The device speaks primitives; every composite — verify, erase,
|
implementation of this feature set fits it there. Clock, baud, serial
|
||||||
reset-vector surgery, updating the loader itself — lives in the host tool
|
backend and pins are per-build configuration (below); the size matrix in
|
||||||
(`pureboot.py`).
|
the test suite holds every combination inside its slot. The device speaks
|
||||||
|
primitives; every composite — verify, erase, reset-vector surgery, updating
|
||||||
|
the loader itself — lives in the host tool (`pureboot.py`).
|
||||||
|
|
||||||
The image is **position-independent**: control flow is PC-relative, the
|
The image is **position-independent**: control flow is PC-relative, the
|
||||||
read/write paths take wire addresses, the write guard protects the slot the
|
read/write paths take wire addresses, the write guard protects the slot the
|
||||||
@@ -26,16 +28,63 @@ jumps into it, and lets it rewrite the resident. The slot is 512 bytes
|
|||||||
minimum); on the tinies the budget is 510, not 512: a slot's last word
|
minimum); on the tinies the budget is 510, not 512: a slot's last word
|
||||||
belongs to the host-managed trampoline (below).
|
belongs to the host-managed trampoline (below).
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Every deployment axis is a build parameter, resolved by the CMake function
|
||||||
|
`pureboot_add_loader()` (in `pureboot/CMakeLists.txt`) — the one way a
|
||||||
|
loader target is created, by this repo's own build and by a downstream
|
||||||
|
project alike:
|
||||||
|
|
||||||
|
| Argument | Meaning | Default |
|
||||||
|
|---|---|---|
|
||||||
|
| `CLOCK <hz>` | the clock the board runs | 16 MHz megas, 8 MHz t25/45/85, 9.6 MHz t13s |
|
||||||
|
| `BAUD <bd>` | the wire rate | the ladder below |
|
||||||
|
| `SERIAL auto\|hardware\|software` | the link backend | `auto`: the hardware USART where the chip has one |
|
||||||
|
| `USART <n>` | the USART instance (x4 megas carry two) | 0 |
|
||||||
|
| `RX <pin>`, `TX <pin>` | software-UART pins | `pb0`, `pb1` |
|
||||||
|
| `TIMEOUT <s>` | the activation window | 8 |
|
||||||
|
|
||||||
|
The default baud is the fastest of 115200/57600/38400/19200/9600 the clock
|
||||||
|
reaches within 2.5 % — the same U2X-included divisor search libavr's baud
|
||||||
|
solver runs — and on a software build additionally within the polled
|
||||||
|
receiver's 100-cycles-a-bit floor. 16 MHz lands 115200, 8 MHz 57600,
|
||||||
|
1 MHz 9600. Whatever is picked or overridden is re-checked in the compile:
|
||||||
|
an infeasible clock/baud/backend combination, or a USART the chip does not
|
||||||
|
have, fails with a named static assert.
|
||||||
|
|
||||||
|
A downstream project brings its usual libavr setup (the `libavr` target,
|
||||||
|
the chip via the `LIBAVR_MCU` toolchain preset), consumes this directory,
|
||||||
|
and states its deployment — for example an ATmega328P on its shipped
|
||||||
|
1 MHz fuses with the software UART on hand-picked pins:
|
||||||
|
|
||||||
|
```cmake
|
||||||
|
FetchContent_Declare(bootloader GIT_REPOSITORY git@git.blackmark.me:avr/bootloader.git GIT_TAG main)
|
||||||
|
FetchContent_MakeAvailable(bootloader)
|
||||||
|
add_subdirectory(${bootloader_SOURCE_DIR}/pureboot pureboot)
|
||||||
|
|
||||||
|
pureboot_add_loader(myboot CLOCK 1000000 SERIAL software TX pb1 RX pb5)
|
||||||
|
```
|
||||||
|
|
||||||
|
The function emits the ELF plus `myboot.hex` (the programmer artifact) and
|
||||||
|
`myboot.bin` (the self-update image), prints the size, and stamps the
|
||||||
|
resolved deployment on the target as the `PUREBOOT_HZ`, `PUREBOOT_BAUD`
|
||||||
|
and `PUREBOOT_LINK` properties — what a flashing script or test harness
|
||||||
|
needs to speak to the build. This exact example deployment runs the full
|
||||||
|
protocol suite in CI (`pureboot.custom`).
|
||||||
|
|
||||||
## Link
|
## Link
|
||||||
|
|
||||||
|
The stock builds assume the family's natural deployment; any axis moves
|
||||||
|
per build (above).
|
||||||
|
|
||||||
| Chip | Serial | Baud | Clock assumed |
|
| Chip | Serial | Baud | Clock assumed |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| every ATmega | the hardware USART (USART0), RXD/TXD per pinout | 115200 8N1 | 16 MHz crystal |
|
| every ATmega | the hardware USART (USART0), RXD/TXD per pinout | 115200 8N1 | 16 MHz crystal |
|
||||||
| ATtiny25/45/85 | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 8 MHz internal RC |
|
| ATtiny25/45/85 | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 8 MHz internal RC |
|
||||||
| ATtiny13/13A | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 9.6 MHz internal RC |
|
| ATtiny13/13A | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 9.6 MHz internal RC |
|
||||||
|
|
||||||
The tiny RX pin has its pull-up enabled; TX idles high. All multi-byte
|
The software-UART RX pin has its pull-up enabled; TX idles high. All
|
||||||
quantities on the wire are little-endian.
|
multi-byte quantities on the wire are little-endian.
|
||||||
|
|
||||||
## Activation
|
## Activation
|
||||||
|
|
||||||
@@ -50,9 +99,10 @@ The host then has one activation window per awaited byte to knock: `p` then
|
|||||||
awaited again (line noise cannot lock the loader, only delay it). A window
|
awaited again (line noise cannot lock the loader, only delay it). A window
|
||||||
expiring with an idle line boots the application.
|
expiring with an idle line boots the application.
|
||||||
|
|
||||||
The window length is a compile-time constant — 8 s by default, another value
|
The window length is a compile-time constant — 8 s by default, another
|
||||||
via the `PUREBOOT_TIMEOUT` CMake cache variable — so the whole EEPROM belongs
|
value via `pureboot_add_loader(... TIMEOUT <s>)` (the stock target keeps
|
||||||
to the application; pureboot never uses it for its own state. Re-timing a
|
the `PUREBOOT_TIMEOUT` cache variable) — so the whole EEPROM belongs to
|
||||||
|
the application; pureboot never uses it for its own state. Re-timing a
|
||||||
deployed loader is a self-update with a re-timed build (below).
|
deployed loader is a self-update with a re-timed build (below).
|
||||||
|
|
||||||
## Session
|
## Session
|
||||||
@@ -237,11 +287,35 @@ read-back unless `--no-verify`; images are raw binary, or Intel HEX by
|
|||||||
extension. `--force` overrides the refusable safety checks (today: flashing
|
extension. `--force` overrides the refusable safety checks (today: flashing
|
||||||
application data into a mega's reset walk region).
|
application data into a mega's reset walk region).
|
||||||
|
|
||||||
|
Readouts come one fact per line: `--info` prints the decoded info block
|
||||||
|
field by field, `--fuses` each fuse byte on its own line — plus, on a
|
||||||
|
boot-sectioned mega, the decoded meaning (where the BOOTSZ section starts,
|
||||||
|
what BOOTRST does to reset). Transfers that take wire time — programming,
|
||||||
|
reading, erasing, verifying, the update phases — draw a transient progress
|
||||||
|
bar on stderr when it is a tty; logs and pipes see only the summary lines.
|
||||||
|
`-v`/`--verbose` adds the decisions as they happen: knock counts, the
|
||||||
|
programming plan (vector-surgery targets, skipped blank pages), update
|
||||||
|
state handling and per-phase page counts.
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
Per chip preset, `ctest` runs:
|
`tools/check.sh` runs every chip's workflow (`tools/check.sh --full` adds
|
||||||
|
the reflect-mode builds of libavr's spot set; `tools/make_presets.py`
|
||||||
|
regenerates the presets). Per chip preset, `ctest` runs:
|
||||||
|
|
||||||
- `pureboot.size` — the 510-byte (tinies) / 512-byte (mega) budget;
|
- `pureboot.size` — the 510-byte (tinies) / 512-byte (mega) budget;
|
||||||
|
- `pureboot_*.size` — the size matrix: the serial backends × the clock
|
||||||
|
ladder (1/8/16 MHz; the t13s' own RC menu), plus the USART1 build on the
|
||||||
|
x4 chips — every configuration axis that could move the image, each
|
||||||
|
variant against the same slot budget (pins are immediate operands and the
|
||||||
|
timeout is a constant: size-neutral);
|
||||||
|
- `pureboot.custom` (328P) — the configured-deployment acceptance test: the
|
||||||
|
1 MHz software-serial TX=PB1/RX=PB5 build from the configuration example
|
||||||
|
drives the full protocol suite through the runner's GPIO bridge, fixture
|
||||||
|
application included;
|
||||||
|
- `pureboot.usart1` (644A) — the same protocol suite over the second
|
||||||
|
hardware USART: instance selection is compile-checked everywhere, but
|
||||||
|
only a live session proves the loader polls the USART it claims;
|
||||||
- `pureboot.pi` — the position-independence lint: no absolute `jmp`/`call`
|
- `pureboot.pi` — the position-independence lint: no absolute `jmp`/`call`
|
||||||
in the image, the info block within its first 256 bytes;
|
in the image, the info block within its first 256 bytes;
|
||||||
- `pureboot.planner` — the host tool's pure logic: programming orders and
|
- `pureboot.planner` — the host tool's pure logic: programming orders and
|
||||||
@@ -249,9 +323,10 @@ Per chip preset, `ctest` runs:
|
|||||||
boot-fuse decode, and the update preflight's error/warning matrix over
|
boot-fuse decode, and the update preflight's error/warning matrix over
|
||||||
synthetic fuse bytes;
|
synthetic fuse bytes;
|
||||||
- `pureboot.protocol` — end to end against a simavr device
|
- `pureboot.protocol` — end to end against a simavr device
|
||||||
(`test/pureboot_device.c` — the mega's USART as a pty; on the tinies a
|
(`test/pureboot_device.c` — a hardware USART as a pty, or a cycle-timed
|
||||||
cycle-timed GPIO⇄pty bridge for the software UART, plus the SPM/NVM module
|
GPIO⇄pty bridge for a software-UART build, selected with `-l` to match
|
||||||
simavr's tiny cores lack) driven by the real host tool through
|
the loader's link; plus the SPM/NVM module simavr's tiny cores lack)
|
||||||
|
driven by the real host tool through
|
||||||
knock-from-reset, program + verify of both memories, session reconnect, an
|
knock-from-reset, program + verify of both memories, session reconnect, an
|
||||||
external reset through the patched vector, and the hand-over to a fixture
|
external reset through the patched vector, and the hand-over to a fixture
|
||||||
application whose banner proves the launch — cross-checked against the
|
application whose banner proves the launch — cross-checked against the
|
||||||
@@ -265,6 +340,6 @@ Per chip preset, `ctest` runs:
|
|||||||
from its flash dump, and a re-run must complete the update with the
|
from its flash dump, and a re-run must complete the update with the
|
||||||
application intact throughout.
|
application intact throughout.
|
||||||
|
|
||||||
`size`, `pi`, and `planner` are host logic and run anywhere; the three
|
`size`, `pi`, and `planner` are host logic and run anywhere; the
|
||||||
simulator-driven targets need simavr and a pty, so they are POSIX-only —
|
simulator-driven targets need simavr and a pty, so they are POSIX-only —
|
||||||
on Windows the tool is exercised against real hardware.
|
on Windows the tool is exercised against real hardware.
|
||||||
|
|||||||
@@ -38,22 +38,19 @@ constexpr auto off = avr::irq::guard_policy::unused;
|
|||||||
|
|
||||||
constexpr std::uint8_t ack = '+';
|
constexpr std::uint8_t ack = '+';
|
||||||
|
|
||||||
// Per-chip personality: the clocks the dogfood boards run (16 MHz crystal on
|
// Per-deployment personality, passed in by the build — pureboot_add_loader()
|
||||||
// the mega, calibrated RC on the tinies). The device signature comes straight
|
// (the CMake function next to this file) resolves the defaults: the clock the
|
||||||
// from the chip database (avr::hw::db.signature) — compile-time data is the
|
// board actually runs, the wire baud, the serial backend and its pins. The
|
||||||
// only universal source, since the tiny13A cannot even read its signature row
|
// device signature needs no configuring — it comes from the chip database
|
||||||
// from code.
|
// (avr::hw::db.signature), the only universal source, since the tiny13A
|
||||||
consteval avr::hertz_t clock()
|
// cannot even read its signature row from code.
|
||||||
{
|
#if !defined(PUREBOOT_CLOCK_HZ) || !defined(PUREBOOT_BAUD)
|
||||||
auto name = std::string_view{avr::hw::db.name};
|
#error \
|
||||||
if (name.starts_with("ATtiny13"))
|
"PUREBOOT_CLOCK_HZ and PUREBOOT_BAUD select this build's clock and baud — create loader targets with pureboot_add_loader() (README.md)"
|
||||||
return 9.6_MHz;
|
#endif
|
||||||
if (name.starts_with("ATtiny"))
|
|
||||||
return 8_MHz;
|
|
||||||
return 16_MHz;
|
|
||||||
}
|
|
||||||
|
|
||||||
using dev = avr::device<{.clock = clock()}>;
|
using dev = avr::device<{.clock = avr::hertz_t{PUREBOOT_CLOCK_HZ}}>;
|
||||||
|
constexpr avr::baud_t wire_baud{PUREBOOT_BAUD};
|
||||||
|
|
||||||
// The watchdog reset flag's home: MCUSR, or the classic megas' MCUCSR.
|
// The watchdog reset flag's home: MCUSR, or the classic megas' MCUCSR.
|
||||||
consteval std::int16_t wdrf_field()
|
consteval std::int16_t wdrf_field()
|
||||||
@@ -118,34 +115,43 @@ constexpr std::uint8_t timeout_seconds = PUREBOOT_TIMEOUT;
|
|||||||
static_cast<std::uint8_t>((boot_section ? 0 : 1) | (word_flash ? 2 : 0)),
|
static_cast<std::uint8_t>((boot_section ? 0 : 1) | (word_flash ? 2 : 0)),
|
||||||
};
|
};
|
||||||
|
|
||||||
// The serial link: the hardware USART where the chip has one, the polled
|
// The serial link. PUREBOOT_USART forces a hardware USART instance,
|
||||||
// software UART (no vector — the table belongs to the application) on PB0/PB1
|
// PUREBOOT_SOFT_SERIAL the polled software UART (no vector — the table
|
||||||
// elsewhere. Both are class templates on the clock so only the selected
|
// belongs to the application) on PUREBOOT_RX/PUREBOOT_TX; with neither, the
|
||||||
// backend is ever instantiated. pending() is the cheap line test the
|
// chip's first USART where it has one and the software UART elsewhere. Both
|
||||||
// activation window polls; rx() then picks the byte up; drain() holds until
|
// are class templates on the clock so only the selected backend is ever
|
||||||
// the last transmitted frame is fully on the wire (the jump hand-over must
|
// instantiated. pending() is the cheap line test the activation window
|
||||||
// not let the target's re-init clip the ack).
|
// polls; rx() then picks the byte up; drain() holds until the last
|
||||||
template <avr::hertz_t C>
|
// transmitted frame is fully on the wire (the jump hand-over must not let
|
||||||
consteval std::int16_t rxc_field()
|
// the target's re-init clip the ack).
|
||||||
{
|
#if defined(PUREBOOT_SOFT_SERIAL) && defined(PUREBOOT_USART)
|
||||||
return avr::uart::detail::ufield<'0', "UCSR#A", "RXC#">();
|
#error "PUREBOOT_SOFT_SERIAL and PUREBOOT_USART select opposing serial backends"
|
||||||
}
|
#endif
|
||||||
|
#if !defined(PUREBOOT_RX)
|
||||||
|
#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>
|
// Whether the chip carries the selected USART: the suffixed instance name,
|
||||||
consteval std::int16_t txc_field()
|
// or — for instance 0 — the classic megas' un-numbered block.
|
||||||
|
consteval bool usart_exists()
|
||||||
{
|
{
|
||||||
return avr::uart::detail::ufield<'0', "UCSR#A", "TXC#">();
|
const char name[] = {'U', 'S', 'A', 'R', 'T', usart_digit};
|
||||||
}
|
if (avr::hw::db.has_instance(std::string_view{name, sizeof(name)}))
|
||||||
|
return true;
|
||||||
template <avr::hertz_t C>
|
return usart_digit == '0' && avr::hw::db.has_instance("USART");
|
||||||
consteval std::int16_t status_reg()
|
|
||||||
{
|
|
||||||
return avr::uart::detail::ureg<'0', "UCSR#A">();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
template <avr::hertz_t C>
|
template <avr::hertz_t C>
|
||||||
struct hardware_link {
|
struct hardware_link {
|
||||||
using uart = avr::uart::usart0<C, {.baud = 115200_Bd, .max_baud_error = 2.5_pct}>;
|
using uart = avr::uart::usart<usart_digit, C, {.baud = wire_baud, .max_baud_error = 2.5_pct}>;
|
||||||
|
|
||||||
// The compiled idle poll: lds UCSR0A (2), sbrc skipping the exit (2),
|
// The compiled idle poll: lds UCSR0A (2), sbrc skipping the exit (2),
|
||||||
// sbiw + sbci + sbci + brne (6).
|
// sbiw + sbci + sbci + brne (6).
|
||||||
@@ -158,7 +164,7 @@ struct hardware_link {
|
|||||||
|
|
||||||
static bool pending()
|
static bool pending()
|
||||||
{
|
{
|
||||||
return avr::hw::field_impl<rxc_field<C>()>::test();
|
return uart::rx_ready();
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::uint8_t rx()
|
static std::uint8_t rx()
|
||||||
@@ -173,22 +179,14 @@ struct hardware_link {
|
|||||||
|
|
||||||
static void drain()
|
static void drain()
|
||||||
{
|
{
|
||||||
// write() leaves the byte draining behind it. Clear a stale TXC0
|
uart::drain();
|
||||||
// first (W1C by writing the sampled status back — the store a hand
|
|
||||||
// assembler writes, keeping U2X0), then wait for the fresh
|
|
||||||
// completion; with a byte still ahead in the shifter TXC0 cannot
|
|
||||||
// re-set until the last pending byte has fully left.
|
|
||||||
using status = avr::hw::reg_impl<status_reg<C>()>;
|
|
||||||
status::write(status::read());
|
|
||||||
while (!avr::hw::field_impl<txc_field<C>()>::test()) {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
template <avr::hertz_t C>
|
template <avr::hertz_t C>
|
||||||
struct software_link {
|
struct software_link {
|
||||||
using rx_t = avr::uart::software_rx_polled<C, avr::pb0, 57600_Bd>;
|
using rx_t = avr::uart::software_rx_polled<C, avr::PUREBOOT_RX, wire_baud>;
|
||||||
using tx_t = avr::uart::software_tx<C, avr::pb1, 57600_Bd>;
|
using tx_t = avr::uart::software_tx<C, avr::PUREBOOT_TX, wire_baud>;
|
||||||
|
|
||||||
// The compiled idle poll: sbis skipping the exit (2), sbiw + sbci +
|
// The compiled idle poll: sbis skipping the exit (2), sbiw + sbci +
|
||||||
// sbci + brne (6).
|
// sbci + brne (6).
|
||||||
@@ -201,7 +199,7 @@ struct software_link {
|
|||||||
|
|
||||||
static bool pending()
|
static bool pending()
|
||||||
{
|
{
|
||||||
return !avr::io::input<avr::pb0>::read(); // a start bit has begun
|
return rx_t::start_pending();
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::uint8_t rx()
|
static std::uint8_t rx()
|
||||||
@@ -220,8 +218,14 @@ struct software_link {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
using link = std::conditional_t<avr::hw::db.has_instance("USART0") || avr::hw::db.has_instance("USART"),
|
#if defined(PUREBOOT_USART)
|
||||||
hardware_link<dev::clock>, software_link<dev::clock>>;
|
static_assert(usart_exists(), "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<usart_exists(), hardware_link<dev::clock>, software_link<dev::clock>>;
|
||||||
|
#endif
|
||||||
|
|
||||||
// The application's entry, an absolute address the linker pins (--defsym in
|
// The application's entry, an absolute address the linker pins (--defsym in
|
||||||
// CMakeLists.txt): 0x0000 on the mega (word 0 stays the application's own
|
// CMakeLists.txt): 0x0000 on the mega (word 0 stays the application's own
|
||||||
@@ -417,10 +421,13 @@ void send_fuses()
|
|||||||
// return address is a word address, whose high byte is the 256-word slot
|
// return address is a word address, whose high byte is the 256-word slot
|
||||||
// index — on byte-addressed chips doubled back into byte terms.
|
// index — on byte-addressed chips doubled back into byte terms.
|
||||||
// program_flash refuses this one slot and the info block is addressed
|
// program_flash refuses this one slot and the info block is addressed
|
||||||
// from it, so both follow wherever the code was flashed.
|
// from it, so both follow wherever the code was flashed. The high byte is
|
||||||
|
// spelled as byteswap's low byte: the builtin's value is itself built by
|
||||||
|
// swapping the two stacked bytes, and the double swap folds to the single
|
||||||
|
// byte pick a hand assembler writes — `>> 8` leaves the swap materialized.
|
||||||
const std::uint16_t ra_words = reinterpret_cast<std::uint16_t>(__builtin_return_address(0));
|
const std::uint16_t ra_words = reinterpret_cast<std::uint16_t>(__builtin_return_address(0));
|
||||||
const std::uint8_t slot_high =
|
const std::uint8_t ra_high = static_cast<std::uint8_t>(std::byteswap(ra_words));
|
||||||
word_flash ? static_cast<std::uint8_t>(ra_words >> 8) & 0xfe : static_cast<std::uint8_t>((ra_words >> 8) << 1);
|
const std::uint8_t slot_high = word_flash ? ra_high & 0xfe : static_cast<std::uint8_t>(ra_high << 1);
|
||||||
|
|
||||||
// The knock: 'p' then 'b', each under a fresh window; any other byte is
|
// The knock: 'p' then 'b', each under a fresh window; any other byte is
|
||||||
// line noise and waits again. Falling out of a window runs the app.
|
// line noise and waits again. Falling out of a window runs the app.
|
||||||
@@ -439,13 +446,13 @@ void send_fuses()
|
|||||||
// asserts it), and slots are 512-aligned — so the low byte of its
|
// asserts it), and slots are 512-aligned — so the low byte of its
|
||||||
// link address (in wire units: bytes, or words on the large
|
// link address (in wire units: bytes, or words on the large
|
||||||
// chips) is its offset in any slot, and the high byte of its
|
// chips) is its offset in any slot, and the high byte of its
|
||||||
// runtime address is the running slot's. Built as a byte pair so
|
// runtime address is the running slot's. Composed from the two
|
||||||
// no absolute address is ever materialized.
|
// bytes — the high half is runtime data, so no absolute address
|
||||||
|
// is ever materialized.
|
||||||
const auto link_low = reinterpret_cast<std::uint16_t>(info_data.data());
|
const auto link_low = reinterpret_cast<std::uint16_t>(info_data.data());
|
||||||
const std::uint8_t low =
|
const std::uint8_t low =
|
||||||
word_flash ? static_cast<std::uint8_t>(link_low >> 1) : static_cast<std::uint8_t>(link_low);
|
word_flash ? static_cast<std::uint8_t>(link_low >> 1) : static_cast<std::uint8_t>(link_low);
|
||||||
send_flash(std::bit_cast<std::uint16_t>(std::array{low, slot_high}),
|
send_flash(static_cast<std::uint16_t>(low | (slot_high << 8)), static_cast<std::uint8_t>(info_data.size()));
|
||||||
static_cast<std::uint8_t>(info_data.size()));
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'J': { // jump to a wire word address: hand-over and staging transfer
|
case 'J': { // jump to a wire word address: hand-over and staging transfer
|
||||||
|
|||||||
@@ -38,11 +38,57 @@ PROMPT = b"+"
|
|||||||
PROTOCOL_VERSION = 1
|
PROTOCOL_VERSION = 1
|
||||||
SLOT = 512 # the loader slot on byte-addressed chips; word-addressed ones (>64 KiB) use 1 KiB — their own smallest boot sector
|
SLOT = 512 # the loader slot on byte-addressed chips; word-addressed ones (>64 KiB) use 1 KiB — their own smallest boot sector
|
||||||
|
|
||||||
|
VERBOSE = False
|
||||||
|
|
||||||
|
|
||||||
|
def verbose(message):
|
||||||
|
"""Detail printed only under --verbose: decisions and derived facts, not
|
||||||
|
per-byte chatter — the progress bar carries the bulk transfers."""
|
||||||
|
if VERBOSE:
|
||||||
|
print(f" {message}")
|
||||||
|
|
||||||
|
|
||||||
class Error(Exception):
|
class Error(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Progress:
|
||||||
|
"""A transient in-place bar on stderr for the operations that take wire
|
||||||
|
time. Drawn only when stderr is a tty — logs, pipes and the test harness
|
||||||
|
see nothing — and erased once done; the summary line each operation
|
||||||
|
prints afterwards is the persistent record. A zero total (or no label)
|
||||||
|
disables it, so callers can pass one through unconditionally."""
|
||||||
|
|
||||||
|
def __init__(self, label, total, unit="pages"):
|
||||||
|
self.label, self.total, self.unit = label, total, unit
|
||||||
|
self.done = 0
|
||||||
|
self.width = 0
|
||||||
|
self.live = bool(label) and total > 0 and sys.stderr.isatty()
|
||||||
|
self._draw()
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *exc):
|
||||||
|
if self.live:
|
||||||
|
sys.stderr.write("\r" + " " * self.width + "\r")
|
||||||
|
sys.stderr.flush()
|
||||||
|
|
||||||
|
def step(self, n=1):
|
||||||
|
self.done += n
|
||||||
|
self._draw()
|
||||||
|
|
||||||
|
def _draw(self):
|
||||||
|
if not self.live:
|
||||||
|
return
|
||||||
|
bar = 24 * self.done // self.total
|
||||||
|
line = (f"{self.label:<16} [{'#' * bar}{'-' * (24 - bar)}] "
|
||||||
|
f"{100 * self.done // self.total:3d}% {self.done}/{self.total} {self.unit}")
|
||||||
|
self.width = max(self.width, len(line))
|
||||||
|
sys.stderr.write("\r" + line)
|
||||||
|
sys.stderr.flush()
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------- serial ---
|
# ---------------------------------------------------------------- serial ---
|
||||||
|
|
||||||
|
|
||||||
@@ -295,6 +341,23 @@ class Info:
|
|||||||
f"EEPROM {self.eeprom_size} B, {vector}"
|
f"EEPROM {self.eeprom_size} B, {vector}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def lines(self):
|
||||||
|
"""The info block as one fact per line — what --info prints."""
|
||||||
|
if self.patch_vector:
|
||||||
|
hand_over = f"host-patched reset vector, trampoline at {self.base - 2:#06x}"
|
||||||
|
else:
|
||||||
|
hand_over = "hardware boot section, jump to word 0"
|
||||||
|
return (
|
||||||
|
f"signature {' '.join(f'{b:02x}' for b in self.signature)}",
|
||||||
|
f"flash {self.flash_size} B, {self.page} B pages"
|
||||||
|
+ (", word-addressed wire" if self.word_flash else ""),
|
||||||
|
f"application 0x0000..{self.base - 1:#06x} ({self.base} B)",
|
||||||
|
f"loader {self.base:#06x} ({self.slot} B slot)",
|
||||||
|
f"staging {self.stage:#06x}",
|
||||||
|
f"EEPROM {self.eeprom_size} B",
|
||||||
|
f"hand-over {hand_over}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Loader:
|
class Loader:
|
||||||
"""A pureboot session. Between commands the loader has prompted `+` and
|
"""A pureboot session. Between commands the loader has prompted `+` and
|
||||||
@@ -312,8 +375,10 @@ class Loader:
|
|||||||
absorbs whatever they produced."""
|
absorbs whatever they produced."""
|
||||||
self.port.flush_input()
|
self.port.flush_input()
|
||||||
deadline = time.monotonic() + wait
|
deadline = time.monotonic() + wait
|
||||||
|
knocks = 0
|
||||||
while True:
|
while True:
|
||||||
self.port.write(b"pb")
|
self.port.write(b"pb")
|
||||||
|
knocks += 1
|
||||||
if PROMPT in self.port.read_available(0.4):
|
if PROMPT in self.port.read_available(0.4):
|
||||||
break
|
break
|
||||||
if time.monotonic() > deadline:
|
if time.monotonic() > deadline:
|
||||||
@@ -323,6 +388,7 @@ class Loader:
|
|||||||
self.port.write(b"b")
|
self.port.write(b"b")
|
||||||
self.info = Info(self.port.read_exact(12, 2.0))
|
self.info = Info(self.port.read_exact(12, 2.0))
|
||||||
self._expect_prompt()
|
self._expect_prompt()
|
||||||
|
verbose(f"loader answered knock {knocks}; info block read")
|
||||||
return self.info
|
return self.info
|
||||||
|
|
||||||
def _expect_prompt(self, timeout=2.0):
|
def _expect_prompt(self, timeout=2.0):
|
||||||
@@ -373,7 +439,7 @@ class Loader:
|
|||||||
head = bytes((ord("W"), wire & 0xFF, wire >> 8))
|
head = bytes((ord("W"), wire & 0xFF, wire >> 8))
|
||||||
self._command(head + data, 0, 2.0)
|
self._command(head + data, 0, 2.0)
|
||||||
|
|
||||||
def write_eeprom(self, address, data):
|
def write_eeprom(self, address, data, progress=None):
|
||||||
offset = 0
|
offset = 0
|
||||||
while offset < len(data):
|
while offset < len(data):
|
||||||
chunk = data[offset : offset + 256]
|
chunk = data[offset : offset + 256]
|
||||||
@@ -382,6 +448,8 @@ class Loader:
|
|||||||
for byte in chunk:
|
for byte in chunk:
|
||||||
self.port.write(bytes((byte,)))
|
self.port.write(bytes((byte,)))
|
||||||
self._expect_prompt() # per-byte ack: the write has begun
|
self._expect_prompt() # per-byte ack: the write has begun
|
||||||
|
if progress:
|
||||||
|
progress.step()
|
||||||
self._expect_prompt() # the next command prompt
|
self._expect_prompt() # the next command prompt
|
||||||
address += len(chunk)
|
address += len(chunk)
|
||||||
offset += len(chunk)
|
offset += len(chunk)
|
||||||
@@ -487,6 +555,8 @@ def plan_flash(image, info):
|
|||||||
final += bytearray([0xFF] * (trampoline_page + page - len(final)))
|
final += bytearray([0xFF] * (trampoline_page + page - len(final)))
|
||||||
jump = rjmp_to(trampoline_word, entry, flash_words)
|
jump = rjmp_to(trampoline_word, entry, flash_words)
|
||||||
final[info.base - 2], final[info.base - 1] = jump & 0xFF, jump >> 8
|
final[info.base - 2], final[info.base - 1] = jump & 0xFF, jump >> 8
|
||||||
|
verbose(f"vector surgery: word 0 -> loader {info.base:#06x}, "
|
||||||
|
f"trampoline {info.base - 2:#06x} -> entry word {entry:#06x}")
|
||||||
|
|
||||||
pages = {a: bytes(final[a : a + page]) for a in range(0, len(final), page)}
|
pages = {a: bytes(final[a : a + page]) for a in range(0, len(final), page)}
|
||||||
return pages
|
return pages
|
||||||
@@ -681,17 +751,22 @@ class UpdateState:
|
|||||||
os.unlink(self.path)
|
os.unlink(self.path)
|
||||||
|
|
||||||
|
|
||||||
def write_differing(loader, base, content, order=None):
|
def write_differing(loader, base, content, order=None, label=None):
|
||||||
"""Program the pages of `content` at `base` that differ from flash —
|
"""Program the pages of `content` at `base` that differ from flash —
|
||||||
idempotent, so a resumed phase redoes only what an interruption left."""
|
idempotent, so a resumed phase redoes only what an interruption left.
|
||||||
|
A label puts the compare-and-program loop on the progress bar."""
|
||||||
page = loader.info.page
|
page = loader.info.page
|
||||||
offsets = order if order is not None else range(0, len(content), page)
|
offsets = list(order) if order is not None else list(range(0, len(content), page))
|
||||||
written = 0
|
written = 0
|
||||||
for offset in offsets:
|
with Progress(label, len(offsets)) as bar:
|
||||||
want = content[offset : offset + page]
|
for offset in offsets:
|
||||||
if loader.read_flash(base + offset, page) != want:
|
want = content[offset : offset + page]
|
||||||
loader.write_page(base + offset, want)
|
if loader.read_flash(base + offset, page) != want:
|
||||||
written += 1
|
loader.write_page(base + offset, want)
|
||||||
|
written += 1
|
||||||
|
bar.step()
|
||||||
|
if label:
|
||||||
|
verbose(f"{label}: {written} of {len(offsets)} pages differed")
|
||||||
for at in range(0, len(content), 256):
|
for at in range(0, len(content), 256):
|
||||||
if loader.read_flash(base + at, min(256, len(content) - at)) != content[at : at + 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")
|
raise Error(f"verify failed at {base + at:#06x} after programming")
|
||||||
@@ -723,6 +798,10 @@ def op_update_loader(loader, wait, path, state_path, fuse_bytes):
|
|||||||
page = info.page
|
page = info.page
|
||||||
|
|
||||||
state = UpdateState(state_path)
|
state = UpdateState(state_path)
|
||||||
|
if os.path.exists(state_path):
|
||||||
|
verbose(f"resuming the update recorded in {state_path}")
|
||||||
|
else:
|
||||||
|
verbose(f"saving the staging slot to {state_path}")
|
||||||
state.load_or_save(loader)
|
state.load_or_save(loader)
|
||||||
|
|
||||||
# Install the staging copy — unless a loader already sits whole in the
|
# Install the staging copy — unless a loader already sits whole in the
|
||||||
@@ -741,7 +820,7 @@ def op_update_loader(loader, wait, path, state_path, fuse_bytes):
|
|||||||
current = loader.read_flash(info.stage, info.slot)
|
current = loader.read_flash(info.stage, info.slot)
|
||||||
staged_loader = image_info(current[:268])
|
staged_loader = image_info(current[:268])
|
||||||
if staged_loader is not None and staged_loader.raw == info.raw and current == state.staging:
|
if staged_loader is not None and staged_loader.raw == info.raw and current == state.staging:
|
||||||
print(f"staging slot already holds a loader — left in place")
|
print("staging slot already holds a loader — left in place")
|
||||||
else:
|
else:
|
||||||
# On a chip whose staging slot starts at address 0 (the 1 KB
|
# On a chip whose staging slot starts at address 0 (the 1 KB
|
||||||
# tiny13s), its first page carries the reset vector: written last,
|
# tiny13s), its first page carries the reset vector: written last,
|
||||||
@@ -750,30 +829,34 @@ def op_update_loader(loader, wait, path, state_path, fuse_bytes):
|
|||||||
order = list(range(0, info.slot, page))
|
order = list(range(0, info.slot, page))
|
||||||
if info.stage == 0:
|
if info.stage == 0:
|
||||||
order = order[1:] + [0]
|
order = order[1:] + [0]
|
||||||
if write_differing(loader, info.stage, staged, order):
|
if write_differing(loader, info.stage, staged, order, label="staging copy"):
|
||||||
print(f"staging copy installed at {info.stage:#06x}")
|
print(f"staging copy installed at {info.stage:#06x}")
|
||||||
|
|
||||||
# Enter it and let it rewrite the resident slot. Where a patched reset
|
# Enter it and let it rewrite the resident slot. Where a patched reset
|
||||||
# vector routes through the resident (a tiny with the staging slot away
|
# vector routes through the resident (a tiny with the staging slot away
|
||||||
# from page 0), word 0 is re-aimed at the staging copy around the
|
# from page 0), word 0 is re-aimed at the staging copy around the
|
||||||
# rewrite, so a power failure mid-rewrite still resets into a loader.
|
# rewrite, so a power failure mid-rewrite still resets into a loader.
|
||||||
|
verbose(f"entering the staging copy at {info.stage:#06x}")
|
||||||
loader.enter_copy(info.stage, wait)
|
loader.enter_copy(info.stage, wait)
|
||||||
redirect = info.patch_vector and info.stage != 0
|
redirect = info.patch_vector and info.stage != 0
|
||||||
if redirect:
|
if redirect:
|
||||||
|
verbose("word 0 re-aimed at the staging copy for the rewrite")
|
||||||
patch_word0(loader, state.page0, info.stage)
|
patch_word0(loader, state.page0, info.stage)
|
||||||
if write_differing(loader, info.base, resident):
|
if write_differing(loader, info.base, resident, label="resident"):
|
||||||
print(f"resident loader rewritten at {info.base:#06x}")
|
print(f"resident loader rewritten at {info.base:#06x}")
|
||||||
|
|
||||||
# Enter the new resident and put the staging region back: page 0 first
|
# Enter the new resident and put the staging region back: page 0 first
|
||||||
# where it lives in that region (word 0 then points at the new resident
|
# where it lives in that region (word 0 then points at the new resident
|
||||||
# for the rest of the restore), the saved trampoline with the rest.
|
# for the rest of the restore), the saved trampoline with the rest.
|
||||||
|
verbose(f"entering the new resident at {info.base:#06x}")
|
||||||
loader.enter_copy(info.base, wait)
|
loader.enter_copy(info.base, wait)
|
||||||
if redirect:
|
if redirect:
|
||||||
|
verbose("word 0 restored")
|
||||||
write_differing(loader, 0, state.page0)
|
write_differing(loader, 0, state.page0)
|
||||||
order = list(range(0, info.slot, page))
|
order = list(range(0, info.slot, page))
|
||||||
if info.stage == 0:
|
if info.stage == 0:
|
||||||
order = [0] + order[1:]
|
order = [0] + order[1:]
|
||||||
write_differing(loader, info.stage, state.staging, order)
|
write_differing(loader, info.stage, state.staging, order, label="staging restore")
|
||||||
|
|
||||||
state.discard()
|
state.discard()
|
||||||
print(f"loader updated: {len(image)} B at {info.base:#06x}, staging region restored")
|
print(f"loader updated: {len(image)} B at {info.base:#06x}, staging region restored")
|
||||||
@@ -808,39 +891,49 @@ def op_erase_flash(loader):
|
|||||||
is erased and the reset walk reaches the loader anyway."""
|
is erased and the reset walk reaches the loader anyway."""
|
||||||
blank = bytes([0xFF] * loader.info.page)
|
blank = bytes([0xFF] * loader.info.page)
|
||||||
addresses = range(0, loader.info.base, loader.info.page)
|
addresses = range(0, loader.info.base, loader.info.page)
|
||||||
for address in reversed(addresses) if loader.info.patch_vector else addresses:
|
with Progress("erase", len(addresses)) as bar:
|
||||||
loader.write_page(address, blank)
|
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")
|
print(f"erase: {loader.info.base // loader.info.page} pages")
|
||||||
|
|
||||||
|
|
||||||
def op_erase_eeprom(loader):
|
def op_erase_eeprom(loader):
|
||||||
loader.write_eeprom(0, bytes([0xFF] * loader.info.eeprom_size))
|
with Progress("erase EEPROM", loader.info.eeprom_size, "B") as bar:
|
||||||
|
loader.write_eeprom(0, bytes([0xFF] * loader.info.eeprom_size), progress=bar)
|
||||||
print(f"erase: {loader.info.eeprom_size} B of EEPROM")
|
print(f"erase: {loader.info.eeprom_size} B of EEPROM")
|
||||||
|
|
||||||
|
|
||||||
def op_flash(loader, path, erase, verify, fuse_bytes=None, force=False):
|
def op_flash(loader, path, erase, verify, fuse_bytes=None, force=False):
|
||||||
image = load_image(path)
|
image = load_image(path)
|
||||||
|
verbose(f"{path}: {len(image)} B image")
|
||||||
pages = plan_flash(image, loader.info)
|
pages = plan_flash(image, loader.info)
|
||||||
check_walk_region(pages, loader.info, fuse_bytes, force)
|
check_walk_region(pages, loader.info, fuse_bytes, force)
|
||||||
if erase:
|
if erase:
|
||||||
op_erase_flash(loader)
|
op_erase_flash(loader)
|
||||||
order = covered(pages, loader.info, skip_blank=erase)
|
order = covered(pages, loader.info, skip_blank=erase)
|
||||||
for address in order:
|
if len(order) != len(pages):
|
||||||
loader.write_page(address, pages[address])
|
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")
|
print(f"flash: {path}: {len(order)} pages")
|
||||||
if verify:
|
if verify:
|
||||||
verify_pages(loader, pages)
|
verify_pages(loader, pages)
|
||||||
|
|
||||||
|
|
||||||
def verify_pages(loader, pages):
|
def verify_pages(loader, pages):
|
||||||
for address in sorted(pages):
|
with Progress("verify", len(pages)) as bar:
|
||||||
got = loader.read_flash(address, loader.info.page)
|
for address in sorted(pages):
|
||||||
if got != pages[address]:
|
got = loader.read_flash(address, loader.info.page)
|
||||||
first = next(i for i in range(len(got)) if got[i] != pages[address][i])
|
if got != pages[address]:
|
||||||
raise Error(
|
first = next(i for i in range(len(got)) if got[i] != pages[address][i])
|
||||||
f"verify failed at {address + first:#06x}: "
|
raise Error(
|
||||||
f"wrote {pages[address][first]:02x}, read {got[first]:02x}"
|
f"verify failed at {address + first:#06x}: "
|
||||||
)
|
f"wrote {pages[address][first]:02x}, read {got[first]:02x}"
|
||||||
|
)
|
||||||
|
bar.step()
|
||||||
print(f"verify: {len(pages)} pages ok")
|
print(f"verify: {len(pages)} pages ok")
|
||||||
|
|
||||||
|
|
||||||
@@ -848,8 +941,19 @@ def op_verify_flash(loader, path):
|
|||||||
verify_pages(loader, plan_flash(load_image(path), loader.info))
|
verify_pages(loader, plan_flash(load_image(path), loader.info))
|
||||||
|
|
||||||
|
|
||||||
|
def read_progress(reader, total, label):
|
||||||
|
"""A bulk read in 256-byte wire chunks under a progress bar."""
|
||||||
|
data = b""
|
||||||
|
with Progress(label, total, "B") as bar:
|
||||||
|
while len(data) < total:
|
||||||
|
chunk = min(256, total - len(data))
|
||||||
|
data += reader(len(data), chunk)
|
||||||
|
bar.step(chunk)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
def op_read_flash(loader, path):
|
def op_read_flash(loader, path):
|
||||||
data = loader.read_flash(0, loader.info.base)
|
data = read_progress(loader.read_flash, loader.info.base, "read flash")
|
||||||
open(path, "wb").write(data)
|
open(path, "wb").write(data)
|
||||||
print(f"read flash: {len(data)} B -> {path}")
|
print(f"read flash: {len(data)} B -> {path}")
|
||||||
|
|
||||||
@@ -860,10 +964,11 @@ def op_eeprom(loader, path, erase, verify):
|
|||||||
raise Error(f"EEPROM image is {len(image)} B, device has {loader.info.eeprom_size}")
|
raise Error(f"EEPROM image is {len(image)} B, device has {loader.info.eeprom_size}")
|
||||||
if erase:
|
if erase:
|
||||||
op_erase_eeprom(loader)
|
op_erase_eeprom(loader)
|
||||||
loader.write_eeprom(0, image)
|
with Progress("eeprom", len(image), "B") as bar:
|
||||||
|
loader.write_eeprom(0, image, progress=bar)
|
||||||
print(f"eeprom: {path}: {len(image)} B")
|
print(f"eeprom: {path}: {len(image)} B")
|
||||||
if verify:
|
if verify:
|
||||||
got = loader.read_eeprom(0, len(image))
|
got = read_progress(loader.read_eeprom, len(image), "verify EEPROM")
|
||||||
if got != image:
|
if got != image:
|
||||||
first = next(i for i in range(len(got)) if got[i] != image[i])
|
first = next(i for i in range(len(got)) if got[i] != image[i])
|
||||||
raise Error(f"verify failed at EEPROM {first:#06x}: wrote {image[first]:02x}, read {got[first]:02x}")
|
raise Error(f"verify failed at EEPROM {first:#06x}: wrote {image[first]:02x}, read {got[first]:02x}")
|
||||||
@@ -872,7 +977,7 @@ def op_eeprom(loader, path, erase, verify):
|
|||||||
|
|
||||||
def op_verify_eeprom(loader, path):
|
def op_verify_eeprom(loader, path):
|
||||||
image = load_image(path)
|
image = load_image(path)
|
||||||
got = loader.read_eeprom(0, len(image))
|
got = read_progress(loader.read_eeprom, len(image), "verify EEPROM")
|
||||||
if got != image:
|
if got != image:
|
||||||
first = next(i for i in range(len(got)) if got[i] != image[i])
|
first = next(i for i in range(len(got)) if got[i] != image[i])
|
||||||
raise Error(f"verify failed at EEPROM {first:#06x}: expected {image[first]:02x}, read {got[first]:02x}")
|
raise Error(f"verify failed at EEPROM {first:#06x}: expected {image[first]:02x}, read {got[first]:02x}")
|
||||||
@@ -880,15 +985,30 @@ def op_verify_eeprom(loader, path):
|
|||||||
|
|
||||||
|
|
||||||
def op_read_eeprom(loader, path):
|
def op_read_eeprom(loader, path):
|
||||||
data = loader.read_eeprom(0, loader.info.eeprom_size)
|
data = read_progress(loader.read_eeprom, loader.info.eeprom_size, "read EEPROM")
|
||||||
open(path, "wb").write(data)
|
open(path, "wb").write(data)
|
||||||
print(f"read EEPROM: {len(data)} B -> {path}")
|
print(f"read EEPROM: {len(data)} B -> {path}")
|
||||||
|
|
||||||
|
|
||||||
def op_fuses(loader):
|
def op_fuses(loader):
|
||||||
low, lock, extended, high = loader.read_fuses()
|
low, lock, extended, high = loader.read_fuses()
|
||||||
print(f"fuses: low {low:02x} high {high:02x} extended {extended:02x} lock {lock:02x}")
|
print("fuses:")
|
||||||
return bytes((low, lock, extended, high))
|
print(f" low 0x{low:02x}")
|
||||||
|
print(f" high 0x{high:02x}")
|
||||||
|
print(f" extended 0x{extended:02x}")
|
||||||
|
print(f" lock 0x{lock:02x}")
|
||||||
|
fuse_bytes = bytes((low, lock, extended, high))
|
||||||
|
# On a boot-sectioned mega the BOOTSZ/BOOTRST decode is the fuse fact the
|
||||||
|
# loader's whole deployment hangs on — say it in words.
|
||||||
|
if not loader.info.patch_vector:
|
||||||
|
try:
|
||||||
|
bootrst, bls_start = mega_boot(loader.info, fuse_bytes)
|
||||||
|
reset = "reset enters it" if bootrst else "reset boots the application"
|
||||||
|
print(f" boot section at {bls_start:#06x} ({loader.info.flash_size - bls_start} B), "
|
||||||
|
f"BOOTRST {'programmed' if bootrst else 'unprogrammed'} — {reset}")
|
||||||
|
except Error:
|
||||||
|
pass # unknown signature: the raw bytes above still stand
|
||||||
|
return fuse_bytes
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------- cli ---
|
# -------------------------------------------------------------------- cli ---
|
||||||
@@ -918,7 +1038,11 @@ def main():
|
|||||||
parser.add_argument("--verify-eeprom", metavar="FILE", help="compare EEPROM against an image")
|
parser.add_argument("--verify-eeprom", metavar="FILE", help="compare EEPROM against an image")
|
||||||
parser.add_argument("--force", action="store_true", help="override refusable safety checks")
|
parser.add_argument("--force", action="store_true", help="override refusable safety checks")
|
||||||
parser.add_argument("--stay", action="store_true", help="leave the loader in its session")
|
parser.add_argument("--stay", action="store_true", help="leave the loader in its session")
|
||||||
|
parser.add_argument("-v", "--verbose", action="store_true",
|
||||||
|
help="print decisions and derived facts as operations run")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
global VERBOSE
|
||||||
|
VERBOSE = args.verbose
|
||||||
|
|
||||||
if args.update_loader and (args.flash or args.erase_flash):
|
if args.update_loader and (args.flash or args.erase_flash):
|
||||||
parser.error("--update-loader does not combine with application flash operations")
|
parser.error("--update-loader does not combine with application flash operations")
|
||||||
@@ -931,11 +1055,14 @@ def main():
|
|||||||
parser.error("--assume-fuses takes 8 hex digits: low,lock,extended,high")
|
parser.error("--assume-fuses takes 8 hex digits: low,lock,extended,high")
|
||||||
|
|
||||||
port = Port(args.port, args.baud)
|
port = Port(args.port, args.baud)
|
||||||
|
verbose(f"{args.port}: {args.baud} Bd 8N1, DTR/RTS asserted")
|
||||||
try:
|
try:
|
||||||
loader = Loader(port)
|
loader = Loader(port)
|
||||||
info = loader.connect(args.wait)
|
info = loader.connect(args.wait)
|
||||||
if args.info:
|
if args.info:
|
||||||
print(f"device: {info.describe()}")
|
print("device:")
|
||||||
|
for line in info.lines():
|
||||||
|
print(f" {line}")
|
||||||
fuse_bytes = fuse_override
|
fuse_bytes = fuse_override
|
||||||
if args.fuses or (args.update_loader and not info.patch_vector and fuse_bytes is None):
|
if args.fuses or (args.update_loader and not info.patch_vector and fuse_bytes is None):
|
||||||
read = op_fuses(loader)
|
read = op_fuses(loader)
|
||||||
|
|||||||
@@ -4,11 +4,16 @@
|
|||||||
// surgery, actually launched it. Linked normally (crt, vectors at 0); on
|
// surgery, actually launched it. Linked normally (crt, vectors at 0); on
|
||||||
// the tinies its reset vector is the rjmp the host re-homes.
|
// the tinies its reset vector is the rjmp the host re-homes.
|
||||||
//
|
//
|
||||||
// On the mega it then listens, and an 'L' makes it jump into the resident
|
// On the hardware-USART link it then listens, and an 'L' makes it jump into
|
||||||
// loader — the application-owned loader entry a BOOTRST-unprogrammed mega
|
// the resident loader — the application-owned loader entry a
|
||||||
// relies on (reset always boots the application there), exercised by the
|
// BOOTRST-unprogrammed mega relies on (reset always boots the application
|
||||||
// self-update tests. The tinies idle: reset reaches their loader through
|
// there), exercised by the self-update tests. The software link idles:
|
||||||
// the patched vector, so the application owes it nothing.
|
// reset reaches those loaders through the patched vector (or the runner
|
||||||
|
// models BOOTRST), so the application owes them nothing.
|
||||||
|
//
|
||||||
|
// The fixture speaks the deployment its loader was built for: the same
|
||||||
|
// PUREBOOT_* defines configure it, and without them it assumes the stock
|
||||||
|
// deployment (the crystal/RC clock table below, the chip's natural link).
|
||||||
#include <libavr/libavr.hpp>
|
#include <libavr/libavr.hpp>
|
||||||
|
|
||||||
using namespace avr::literals;
|
using namespace avr::literals;
|
||||||
@@ -17,19 +22,44 @@ namespace {
|
|||||||
|
|
||||||
consteval avr::hertz_t clock()
|
consteval avr::hertz_t clock()
|
||||||
{
|
{
|
||||||
|
#if defined(PUREBOOT_CLOCK_HZ)
|
||||||
|
return avr::hertz_t{PUREBOOT_CLOCK_HZ};
|
||||||
|
#else
|
||||||
auto name = std::string_view{avr::hw::db.name};
|
auto name = std::string_view{avr::hw::db.name};
|
||||||
if (name.starts_with("ATtiny13"))
|
if (name.starts_with("ATtiny13"))
|
||||||
return 9.6_MHz;
|
return 9.6_MHz;
|
||||||
if (name.starts_with("ATtiny"))
|
if (name.starts_with("ATtiny"))
|
||||||
return 8_MHz;
|
return 8_MHz;
|
||||||
return 16_MHz;
|
return 16_MHz;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
#if !defined(PUREBOOT_TX)
|
||||||
|
#define PUREBOOT_TX pb1
|
||||||
|
#endif
|
||||||
|
#if !defined(PUREBOOT_USART)
|
||||||
|
#define PUREBOOT_USART 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
consteval bool use_hardware()
|
||||||
|
{
|
||||||
|
#if defined(PUREBOOT_SOFT_SERIAL)
|
||||||
|
return false;
|
||||||
|
#else
|
||||||
|
return avr::hw::db.has_instance("USART0") || avr::hw::db.has_instance("USART");
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
using dev = avr::device<{.clock = clock()}>;
|
using dev = avr::device<{.clock = clock()}>;
|
||||||
|
|
||||||
template <avr::hertz_t C, bool Hardware = avr::hw::db.has_instance("USART0") || avr::hw::db.has_instance("USART")>
|
template <avr::hertz_t C, bool Hardware = use_hardware()>
|
||||||
struct link {
|
struct link {
|
||||||
using tx_t = avr::uart::usart0<C, {.baud = 115200_Bd, .max_baud_error = 2.5_pct}>;
|
#if defined(PUREBOOT_BAUD)
|
||||||
|
static constexpr avr::baud_t baud{PUREBOOT_BAUD};
|
||||||
|
#else
|
||||||
|
static constexpr avr::baud_t baud{115200};
|
||||||
|
#endif
|
||||||
|
using tx_t = avr::uart::usart<'0' + PUREBOOT_USART, C, {.baud = baud, .max_baud_error = 2.5_pct}>;
|
||||||
static void tx(char c)
|
static void tx(char c)
|
||||||
{
|
{
|
||||||
tx_t::write(static_cast<std::uint8_t>(c));
|
tx_t::write(static_cast<std::uint8_t>(c));
|
||||||
@@ -47,7 +77,12 @@ struct link {
|
|||||||
|
|
||||||
template <avr::hertz_t C>
|
template <avr::hertz_t C>
|
||||||
struct link<C, false> {
|
struct link<C, false> {
|
||||||
using tx_t = avr::uart::software_tx<C, avr::pb1, 57600_Bd>;
|
#if defined(PUREBOOT_BAUD)
|
||||||
|
static constexpr avr::baud_t baud{PUREBOOT_BAUD};
|
||||||
|
#else
|
||||||
|
static constexpr avr::baud_t baud{57600};
|
||||||
|
#endif
|
||||||
|
using tx_t = avr::uart::software_tx<C, avr::PUREBOOT_TX, baud>;
|
||||||
static void tx(char c)
|
static void tx(char c)
|
||||||
{
|
{
|
||||||
tx_t::write(static_cast<std::uint8_t>(c));
|
tx_t::write(static_cast<std::uint8_t>(c));
|
||||||
|
|||||||
@@ -8,8 +8,11 @@ import subprocess
|
|||||||
|
|
||||||
|
|
||||||
class Device:
|
class Device:
|
||||||
def __init__(self, binary, elf, mcu, hz, base_hex, page, baud, dump, reset_hex=None, resume=None):
|
def __init__(self, binary, elf, mcu, hz, base_hex, page, baud, dump, reset_hex=None, resume=None, link=None):
|
||||||
cmd = [binary, elf, mcu, hz, base_hex, str(page), str(baud), dump]
|
cmd = [binary]
|
||||||
|
if link:
|
||||||
|
cmd += ["-l", link]
|
||||||
|
cmd += [elf, mcu, hz, base_hex, str(page), str(baud), dump]
|
||||||
if reset_hex is not None or resume is not None:
|
if reset_hex is not None or resume is not None:
|
||||||
# Chips without a hardware boot section — the tinies and the
|
# Chips without a hardware boot section — the tinies and the
|
||||||
# m48s — reset to address 0 like silicon; the boot-sectioned
|
# m48s — reset to address 0 like silicon; the boot-sectioned
|
||||||
|
|||||||
@@ -5,15 +5,15 @@ through flash + EEPROM + fuse + hand-over scenarios, and cross-check
|
|||||||
the tool's view against the simulator's ground-truth memory dumps.
|
the tool's view against the simulator's ground-truth memory dumps.
|
||||||
|
|
||||||
Usage: pbtest.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
|
Usage: pbtest.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
|
||||||
<baud> <eeprom_size> <app_bin> <tool_py> <workdir>
|
<baud> <eeprom_size> <app_bin> <tool_py> <workdir> [link]
|
||||||
|
|
||||||
|
The optional link is the runner's -l spec (usart1, sw:B5,B1, ...) for a
|
||||||
|
loader built off the chip's natural serial default.
|
||||||
Exits 0 if every scenario passes.
|
Exits 0 if every scenario passes.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import signal
|
|
||||||
import subprocess
|
|
||||||
import sys
|
import sys
|
||||||
import time
|
|
||||||
|
|
||||||
|
|
||||||
def fail(message):
|
def fail(message):
|
||||||
@@ -33,53 +33,14 @@ def rjmp_decode(word, at, flash_words):
|
|||||||
return (at + 1 + offset) % flash_words
|
return (at + 1 + offset) % flash_words
|
||||||
|
|
||||||
|
|
||||||
class Device:
|
|
||||||
def __init__(self, binary, elf, mcu, hz, base, page, baud, dump):
|
|
||||||
self.proc = subprocess.Popen(
|
|
||||||
[binary, elf, mcu, hz, base, str(page), str(baud), dump],
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.STDOUT,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
self.dump = dump
|
|
||||||
self.pty = None
|
|
||||||
deadline = time.time() + 5
|
|
||||||
while time.time() < deadline:
|
|
||||||
line = self.proc.stdout.readline()
|
|
||||||
if not line:
|
|
||||||
break
|
|
||||||
if line.startswith("PB_PTY"):
|
|
||||||
self.pty = line.split()[1]
|
|
||||||
break
|
|
||||||
if not self.pty:
|
|
||||||
self.stop()
|
|
||||||
raise RuntimeError("device did not report a pty")
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
self.proc.terminate()
|
|
||||||
try:
|
|
||||||
self.proc.wait(timeout=3)
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
self.proc.kill()
|
|
||||||
|
|
||||||
|
|
||||||
def run_tool(tool, pty, baud, *args):
|
|
||||||
result = subprocess.run(
|
|
||||||
[sys.executable, tool, "--port", pty, "--baud", str(baud), "--wait", "20", *args],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=120,
|
|
||||||
)
|
|
||||||
print(result.stdout, end="")
|
|
||||||
if result.returncode != 0:
|
|
||||||
fail(f"tool exited {result.returncode}: {result.stderr.strip()}")
|
|
||||||
return result.stdout
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
(device_bin, elf, mcu, hz, base_hex, page, baud, eeprom_size, app_bin, tool, workdir) = sys.argv[1:]
|
args = sys.argv[1:]
|
||||||
|
link = args.pop() if len(args) == 12 else None
|
||||||
|
(device_bin, elf, mcu, hz, base_hex, page, baud, eeprom_size, app_bin, tool, workdir) = args
|
||||||
base, page, baud, eeprom_size = int(base_hex, 0), int(page), int(baud), int(eeprom_size)
|
base, page, baud, eeprom_size = int(base_hex, 0), int(page), int(baud), int(eeprom_size)
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
|
sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
import pbsim
|
||||||
import pureboot as pb
|
import pureboot as pb
|
||||||
|
|
||||||
os.makedirs(workdir, exist_ok=True)
|
os.makedirs(workdir, exist_ok=True)
|
||||||
@@ -105,19 +66,19 @@ def main():
|
|||||||
+ bytes([flags])
|
+ bytes([flags])
|
||||||
)
|
)
|
||||||
|
|
||||||
device = Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump)
|
device = pbsim.Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump, link=link)
|
||||||
try:
|
try:
|
||||||
# Session 1: knock from reset, identify, program everything, stay.
|
# Session 1: knock from reset, identify, program everything, stay.
|
||||||
out = run_tool(tool, device.pty, baud, "--info", "--fuses", "--flash", app_bin,
|
out = pbsim.run_tool(tool, device.pty, baud, "--info", "--fuses", "--flash", app_bin,
|
||||||
"--eeprom", ee_path, "--stay")
|
"--eeprom", ee_path, "--stay")
|
||||||
for needed in ("device: signature", "fuses:", "verify:", "stays"):
|
for needed in ("signature", "fuses", "verify:", "stays"):
|
||||||
if needed not in out:
|
if needed not in out:
|
||||||
fail(f"session 1 output lacks {needed!r}")
|
fail(f"session 1 output lacks {needed!r}")
|
||||||
|
|
||||||
# Session 2: reconnect into the live session, verify, dump, hand over
|
# Session 2: reconnect into the live session, verify, dump, hand over
|
||||||
# is deferred — the pty must be reopened for the APP banner first.
|
# is deferred — the pty must be reopened for the APP banner first.
|
||||||
out = run_tool(tool, device.pty, baud, "--verify-flash", app_bin, "--verify-eeprom", ee_path,
|
out = pbsim.run_tool(tool, device.pty, baud, "--verify-flash", app_bin, "--verify-eeprom", ee_path,
|
||||||
"--read-flash", read_flash, "--read-eeprom", read_eeprom, "--stay")
|
"--read-flash", read_flash, "--read-eeprom", read_eeprom, "--stay")
|
||||||
if out.count("verify:") != 2:
|
if out.count("verify:") != 2:
|
||||||
fail("session 2 did not verify both memories")
|
fail("session 2 did not verify both memories")
|
||||||
|
|
||||||
@@ -136,7 +97,7 @@ def main():
|
|||||||
# runner resets them to address 0 like silicon) or BOOTRST (mega).
|
# runner resets them to address 0 like silicon) or BOOTRST (mega).
|
||||||
# The loader must answer a fresh knock, and the 'J' hand-over must
|
# The loader must answer a fresh knock, and the 'J' hand-over must
|
||||||
# land in the application, which banners on the same link.
|
# land in the application, which banners on the same link.
|
||||||
device.proc.send_signal(signal.SIGUSR1)
|
device.reset()
|
||||||
port = pb.Port(device.pty, baud)
|
port = pb.Port(device.pty, baud)
|
||||||
try:
|
try:
|
||||||
loader = pb.Loader(port)
|
loader = pb.Loader(port)
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
// simavr "device" for the pureboot protocol tests, all three chips. Loads
|
// simavr "device" for the pureboot protocol tests, every chip. Loads the
|
||||||
// the boot-linked ELF at the loader base, starts execution there (BOOTRST /
|
// boot-linked ELF at the loader base, starts execution there (BOOTRST / the
|
||||||
// the patched vector are not what is under test), and exposes the loader's
|
// patched vector are not what is under test), and exposes the loader's
|
||||||
// serial link as a pty for the real host tool:
|
// serial link as a pty for the real host tool:
|
||||||
//
|
//
|
||||||
// - Megas: the hardware USART through simavr's uart_pty.
|
// - Hardware USART builds: simavr's uart_pty on the selected instance.
|
||||||
// - Tinies: an 8N1 bridge between a pty and the GPIO software UART
|
// - Software UART builds: an 8N1 bridge between a pty and the GPIO pins,
|
||||||
// (drives PB0, the loader's RX; decodes PB1, its TX), timed against the
|
// timed against the simulated cycle counter (drives the loader's RX,
|
||||||
// simulated cycle counter.
|
// decodes its TX).
|
||||||
|
//
|
||||||
|
// The link follows the chip's natural default (USART0 on the megas, the
|
||||||
|
// software UART on PB0/PB1 elsewhere) unless -l overrides it: `-l usart1`
|
||||||
|
// for the second instance, `-l sw:B5,B1` for a software build's RX,TX pins.
|
||||||
//
|
//
|
||||||
// simavr's tiny cores decode the SPM opcode but attach no NVM module — SPM
|
// simavr's tiny cores decode the SPM opcode but attach no NVM module — SPM
|
||||||
// is a silent no-op (the mega's boot section has one, avr_flash). The
|
// is a silent no-op (the mega's boot section has one, avr_flash). The
|
||||||
@@ -38,11 +42,31 @@
|
|||||||
|
|
||||||
static avr_t *avr;
|
static avr_t *avr;
|
||||||
static uart_pty_t uart_pty;
|
static uart_pty_t uart_pty;
|
||||||
static int use_uart_pty;
|
static int link_software;
|
||||||
|
static char uart_digit = '0';
|
||||||
|
static char sw_rx_port = 'B', sw_tx_port = 'B';
|
||||||
|
static int sw_rx_bit = 0, sw_tx_bit = 1;
|
||||||
static const char *dump_path;
|
static const char *dump_path;
|
||||||
static uint32_t reset_pc;
|
static uint32_t reset_pc;
|
||||||
static volatile sig_atomic_t reset_requested;
|
static volatile sig_atomic_t reset_requested;
|
||||||
|
|
||||||
|
static int parse_link(const char *spec)
|
||||||
|
{
|
||||||
|
if (strcmp(spec, "usart0") == 0 || strcmp(spec, "usart1") == 0) {
|
||||||
|
link_software = 0;
|
||||||
|
uart_digit = spec[5];
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (strncmp(spec, "sw", 2) == 0) {
|
||||||
|
link_software = 1;
|
||||||
|
if (spec[2] == '\0')
|
||||||
|
return 0;
|
||||||
|
if (sscanf(spec + 2, ":%c%d,%c%d", &sw_rx_port, &sw_rx_bit, &sw_tx_port, &sw_tx_bit) == 4)
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
// simavr 1.6's avr_flash PGERS handler erases spm_pagesize bytes starting at
|
// simavr 1.6's avr_flash PGERS handler erases spm_pagesize bytes starting at
|
||||||
// Z & ~1 instead of the page containing Z (its PGWRT path masks correctly) —
|
// Z & ~1 instead of the page containing Z (its PGWRT path masks correctly) —
|
||||||
// hardware ignores the in-page bits (§26.8.1), so an erase issued with Z
|
// hardware ignores the in-page bits (§26.8.1), so an erase issued with Z
|
||||||
@@ -273,29 +297,42 @@ static void finish(int sig)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (use_uart_pty)
|
if (!link_software)
|
||||||
uart_pty_stop(&uart_pty);
|
uart_pty_stop(&uart_pty);
|
||||||
_exit(0);
|
_exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
if (argc < 8 || argc > 10) {
|
int link_given = 0;
|
||||||
|
for (int opt; (opt = getopt(argc, argv, "l:")) != -1;) {
|
||||||
|
if (opt != 'l' || parse_link(optarg) != 0) {
|
||||||
|
fprintf(stderr, "device: bad link spec (usart0, usart1, sw, or sw:B0,B1 as RX,TX)\n");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
link_given = 1;
|
||||||
|
}
|
||||||
|
int args = argc - optind;
|
||||||
|
if (args < 7 || args > 9) {
|
||||||
fprintf(stderr,
|
fprintf(stderr,
|
||||||
"usage: %s <pureboot.elf> <mcu> <hz> <base_hex> <page> <baud> <flash_dump>"
|
"usage: %s [-l link] <pureboot.elf> <mcu> <hz> <base_hex> <page> <baud> <flash_dump>"
|
||||||
" [reset_hex] [resume_flash]\n"
|
" [reset_hex] [resume_flash]\n"
|
||||||
" reset_hex: reset vector (default: base on the mega, 0 on the tinies)\n"
|
" -l link: usart0 | usart1 | sw[:B0,B1] (RX,TX); default: the chip's own\n"
|
||||||
|
" reset_hex: reset vector (default: base with a boot section, else 0)\n"
|
||||||
" resume_flash: raw full-flash image loaded instead of the ELF — a prior\n"
|
" resume_flash: raw full-flash image loaded instead of the ELF — a prior\n"
|
||||||
" run's dump, for power-fail resume tests\n",
|
" run's dump, for power-fail resume tests\n",
|
||||||
argv[0]);
|
argv[0]);
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
|
argv += optind - 1; // argv[1] is the ELF again, whatever was parsed
|
||||||
const char *mcu_name = argv[2];
|
const char *mcu_name = argv[2];
|
||||||
uint32_t base = (uint32_t)strtoul(argv[4], NULL, 0);
|
uint32_t base = (uint32_t)strtoul(argv[4], NULL, 0);
|
||||||
unsigned page = (unsigned)atoi(argv[5]);
|
unsigned page = (unsigned)atoi(argv[5]);
|
||||||
unsigned baud = (unsigned)atoi(argv[6]);
|
unsigned baud = (unsigned)atoi(argv[6]);
|
||||||
dump_path = argv[7];
|
dump_path = argv[7];
|
||||||
use_uart_pty = strncmp(mcu_name, "atmega", 6) == 0; // every mega links over its hardware USART
|
int is_mega = strncmp(mcu_name, "atmega", 6) == 0;
|
||||||
|
if (!link_given)
|
||||||
|
link_software = !is_mega; // the chips' natural links: USART0, or PB0/PB1
|
||||||
|
|
||||||
avr = avr_make_mcu_by_name(mcu_name);
|
avr = avr_make_mcu_by_name(mcu_name);
|
||||||
if (!avr) {
|
if (!avr) {
|
||||||
@@ -306,7 +343,7 @@ int main(int argc, char *argv[])
|
|||||||
avr->frequency = (uint32_t)strtoul(argv[3], NULL, 0);
|
avr->frequency = (uint32_t)strtoul(argv[3], NULL, 0);
|
||||||
memset(avr->flash, 0xff, avr->flashend + 1); // real flash powers up erased
|
memset(avr->flash, 0xff, avr->flashend + 1); // real flash powers up erased
|
||||||
|
|
||||||
if (argc > 9) {
|
if (args > 8) {
|
||||||
// Resume: the full flash image of an interrupted prior run.
|
// Resume: the full flash image of an interrupted prior run.
|
||||||
FILE *f = fopen(argv[9], "rb");
|
FILE *f = fopen(argv[9], "rb");
|
||||||
if (!f || fread(avr->flash, 1, avr->flashend + 1, f) == 0) {
|
if (!f || fread(avr->flash, 1, avr->flashend + 1, f) == 0) {
|
||||||
@@ -327,8 +364,8 @@ int main(int argc, char *argv[])
|
|||||||
// and the boot-section-less m48s reset to word 0 like silicon — erased
|
// and the boot-section-less m48s reset to word 0 like silicon — erased
|
||||||
// flash walks up into the loader, and after the host's surgery the
|
// flash walks up into the loader, and after the host's surgery the
|
||||||
// patched vector routes there.
|
// patched vector routes there.
|
||||||
int boot_section = use_uart_pty && strncmp(mcu_name, "atmega48", 8) != 0;
|
int boot_section = is_mega && strncmp(mcu_name, "atmega48", 8) != 0;
|
||||||
reset_pc = argc > 8 ? (uint32_t)strtoul(argv[8], NULL, 0) : (boot_section ? base : 0);
|
reset_pc = args > 7 ? (uint32_t)strtoul(argv[8], NULL, 0) : (boot_section ? base : 0);
|
||||||
avr->pc = reset_pc;
|
avr->pc = reset_pc;
|
||||||
avr->codeend = avr->flashend;
|
avr->codeend = avr->flashend;
|
||||||
|
|
||||||
@@ -341,27 +378,34 @@ int main(int argc, char *argv[])
|
|||||||
avr_ioctl(avr, AVR_IOCTL_EEPROM_SET, &seed);
|
avr_ioctl(avr, AVR_IOCTL_EEPROM_SET, &seed);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (use_uart_pty) {
|
// The megas carry simavr's avr_flash module (and its two gaps the wrap
|
||||||
|
// above fixes); the tinies get the NVM module simavr lacks. Which serial
|
||||||
|
// bridge runs is the link's business, not the chip class's.
|
||||||
|
if (is_mega) {
|
||||||
fix_mega_flash_erase();
|
fix_mega_flash_erase();
|
||||||
// POLL_SLEEP paces an idle-polling loader in host real time (a
|
|
||||||
// no-hardware CPU-saving hack); clear it so cycles run free.
|
|
||||||
uint32_t flags = 0;
|
|
||||||
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS('0'), &flags);
|
|
||||||
flags &= ~AVR_UART_FLAG_POLL_SLEEP;
|
|
||||||
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS('0'), &flags);
|
|
||||||
uart_pty_init(avr, &uart_pty);
|
|
||||||
uart_pty_connect(&uart_pty, '0');
|
|
||||||
printf("PB_PTY %s\n", uart_pty.pty.slavename);
|
|
||||||
} else {
|
} else {
|
||||||
nvm.page = page;
|
nvm.page = page;
|
||||||
memset(nvm.buffer, 0xff, sizeof(nvm.buffer));
|
memset(nvm.buffer, 0xff, sizeof(nvm.buffer));
|
||||||
nvm.io.kind = "tiny_nvm";
|
nvm.io.kind = "tiny_nvm";
|
||||||
nvm.io.ioctl = nvm_ioctl;
|
nvm.io.ioctl = nvm_ioctl;
|
||||||
avr_register_io(avr, &nvm.io);
|
avr_register_io(avr, &nvm.io);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!link_software) {
|
||||||
|
// POLL_SLEEP paces an idle-polling loader in host real time (a
|
||||||
|
// no-hardware CPU-saving hack); clear it so cycles run free.
|
||||||
|
uint32_t flags = 0;
|
||||||
|
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS(uart_digit), &flags);
|
||||||
|
flags &= ~AVR_UART_FLAG_POLL_SLEEP;
|
||||||
|
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS(uart_digit), &flags);
|
||||||
|
uart_pty_init(avr, &uart_pty);
|
||||||
|
uart_pty_connect(&uart_pty, uart_digit);
|
||||||
|
printf("PB_PTY %s\n", uart_pty.pty.slavename);
|
||||||
|
} else {
|
||||||
bit_cycles = (avr->frequency + baud / 2) / baud; // matches uart.hpp's own rounding exactly
|
bit_cycles = (avr->frequency + baud / 2) / baud; // matches uart.hpp's own rounding exactly
|
||||||
rx_pin = avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ('B'), 0);
|
rx_pin = avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ(sw_rx_port), (unsigned)sw_rx_bit);
|
||||||
avr_irq_register_notify(avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ('B'), 1), tx_hook, NULL);
|
avr_irq_register_notify(avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ(sw_tx_port), (unsigned)sw_tx_bit), tx_hook,
|
||||||
|
NULL);
|
||||||
avr_raise_irq(rx_pin, 1); // idle line
|
avr_raise_irq(rx_pin, 1); // idle line
|
||||||
|
|
||||||
int slave;
|
int slave;
|
||||||
@@ -389,18 +433,27 @@ int main(int argc, char *argv[])
|
|||||||
reset_requested = 0;
|
reset_requested = 0;
|
||||||
avr_reset(avr);
|
avr_reset(avr);
|
||||||
avr->pc = reset_pc;
|
avr->pc = reset_pc;
|
||||||
if (use_uart_pty) { // reset restores the pacing hack; re-clear it
|
if (!link_software) { // reset restores the pacing hack; re-clear it
|
||||||
uint32_t flags = 0;
|
uint32_t flags = 0;
|
||||||
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS('0'), &flags);
|
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS(uart_digit), &flags);
|
||||||
flags &= ~AVR_UART_FLAG_POLL_SLEEP;
|
flags &= ~AVR_UART_FLAG_POLL_SLEEP;
|
||||||
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS('0'), &flags);
|
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS(uart_digit), &flags);
|
||||||
} else {
|
} else {
|
||||||
bridge_reset();
|
bridge_reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!use_uart_pty && ++since_poll >= 2000) {
|
if (link_software && ++since_poll >= 2000) {
|
||||||
since_poll = 0;
|
since_poll = 0;
|
||||||
poll_pty();
|
poll_pty();
|
||||||
|
// An unthrottled idle simulation runs the activation window out
|
||||||
|
// from under the host's real-time knock cadence: a 1 MHz build's
|
||||||
|
// 8 s window is 8 M cycles — tens of wall milliseconds — so a
|
||||||
|
// first knock lost to an in-flight reset misses the window
|
||||||
|
// entirely. Pace the simulation only while the bridge is fully
|
||||||
|
// quiet (nothing decoding, nothing queued); transfers keep full
|
||||||
|
// speed, and a quiet window stretches toward real time.
|
||||||
|
if (!rx_active && !tx_active && rx_head == rx_tail)
|
||||||
|
usleep(200);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finish(0);
|
finish(0);
|
||||||
|
|||||||
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