Compare commits
22 Commits
v1
...
19662d2386
| Author | SHA1 | Date | |
|---|---|---|---|
| 19662d2386 | |||
| f6598b0511 | |||
| 0604d3a0ad | |||
| 62a548cc09 | |||
| 7d103ca957 | |||
| eca7a41051 | |||
| 7314f7ab3b | |||
| 5b361904ab | |||
| 833e134e01 | |||
| da730b7bb5 | |||
| c351bee257 | |||
| 34e7f1be34 | |||
| 445e187722 | |||
| 250aba5cfb | |||
| 5c900720e3 | |||
| 7d6ef959b2 | |||
| 11ffbce2e2 | |||
| f32a27ff15 | |||
| 57d94cf631 | |||
| 8203a24f33 | |||
| 2906da3272 | |||
| 64c1e484b5 |
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
|||||||
[submodule "libavr"]
|
|
||||||
path = libavr
|
|
||||||
url = ../libavr.git
|
|
||||||
241
CMakeLists.txt
241
CMakeLists.txt
@@ -8,9 +8,6 @@ include(FetchContent)
|
|||||||
if(NOT LIBAVR_ROOT AND DEFINED ENV{LIBAVR_ROOT})
|
if(NOT LIBAVR_ROOT AND DEFINED ENV{LIBAVR_ROOT})
|
||||||
set(LIBAVR_ROOT $ENV{LIBAVR_ROOT})
|
set(LIBAVR_ROOT $ENV{LIBAVR_ROOT})
|
||||||
endif()
|
endif()
|
||||||
if(NOT LIBAVR_ROOT)
|
|
||||||
set(LIBAVR_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/libavr)
|
|
||||||
endif()
|
|
||||||
if(LIBAVR_ROOT)
|
if(LIBAVR_ROOT)
|
||||||
FetchContent_Declare(libavr SOURCE_DIR ${LIBAVR_ROOT})
|
FetchContent_Declare(libavr SOURCE_DIR ${LIBAVR_ROOT})
|
||||||
else()
|
else()
|
||||||
@@ -54,18 +51,13 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# The ELF is only a container (symbols, section headers) and is never flashed —
|
# avrdude programs Intel-HEX; the ELF is only a container (symbols, section
|
||||||
# and the host tool's load_image() dispatches on extension, so handing it one
|
# headers) and is never flashed. Every loader image therefore gets a .hex beside
|
||||||
# would silently program the header bytes. Every loader image therefore gets
|
# it at link time. .eeprom is dropped — EEPROM content is its own avrdude update.
|
||||||
# both flashable forms beside it at link time: .hex for avrdude, and .bin for
|
function(add_hex_output name)
|
||||||
# the host tool's raw path (which is what the reloc and update tests convert to
|
|
||||||
# on the fly). .eeprom is dropped — EEPROM content is its own update.
|
|
||||||
function(add_image_outputs name)
|
|
||||||
add_custom_command(TARGET ${name} POST_BUILD
|
add_custom_command(TARGET ${name} POST_BUILD
|
||||||
COMMAND ${CMAKE_OBJCOPY} -O ihex -R .eeprom
|
COMMAND ${CMAKE_OBJCOPY} -O ihex -R .eeprom
|
||||||
$<TARGET_FILE:${name}> $<TARGET_FILE:${name}>.hex
|
$<TARGET_FILE:${name}> $<TARGET_FILE:${name}>.hex)
|
||||||
COMMAND ${CMAKE_OBJCOPY} -O binary -R .eeprom
|
|
||||||
$<TARGET_FILE:${name}> $<TARGET_FILE:${name}>.bin)
|
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
# The TinySafeBoot protocol reimplemented on libavr in three variants that trade
|
# The TinySafeBoot protocol reimplemented on libavr in three variants that trade
|
||||||
@@ -107,7 +99,7 @@ function(add_tsb_variant name bytes)
|
|||||||
target_link_options(${name} PRIVATE -nostartfiles -Wl,--section-start=.text=${base_hex}
|
target_link_options(${name} PRIVATE -nostartfiles -Wl,--section-start=.text=${base_hex}
|
||||||
-Wl,--defsym=tsb_app=0 -Wl,--pmem-wrap-around=32k)
|
-Wl,--defsym=tsb_app=0 -Wl,--pmem-wrap-around=32k)
|
||||||
add_custom_command(TARGET ${name} POST_BUILD COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${name}>)
|
add_custom_command(TARGET ${name} POST_BUILD COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${name}>)
|
||||||
add_image_outputs(${name})
|
add_hex_output(${name})
|
||||||
if(PROJECT_IS_TOP_LEVEL)
|
if(PROJECT_IS_TOP_LEVEL)
|
||||||
add_test(NAME ${name}.size
|
add_test(NAME ${name}.size
|
||||||
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:${name}>
|
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:${name}>
|
||||||
@@ -129,31 +121,70 @@ 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,
|
# no inline assembly, no global register variables, every libavr chip, 512
|
||||||
# fitting each chip's smallest boot sector. The geometry and the
|
# bytes each. The loader owns the top 512 bytes of flash on every chip; the
|
||||||
# pureboot_add_loader() deployment function live in pureboot/CMakeLists.txt —
|
# application entry symbol is address 0 on the mega (reset re-vectors to the
|
||||||
# the unit a downstream project consumes; everything below is this port's
|
# loader through BOOTRST, so word 0 stays the application's own vector) and
|
||||||
# own build: the stock loaders, their tests, and the size matrix. The
|
# the trampoline word just below the loader on the tinies (host-side vector
|
||||||
# distinct binary dir keeps the `pureboot` target's output name free.
|
# surgery points it at the application). --pmem-wrap-around models AVR's
|
||||||
add_subdirectory(pureboot pureboot-cmake)
|
# modulo-flash PC where the flash is big enough to need it.
|
||||||
|
#
|
||||||
# The stock loader: the family-default deployment (crystal/RC clock, the
|
# The image is position-independent (check_pi.py asserts the two link-time
|
||||||
# chip's natural link, default pins). The activation window stays a cache
|
# facts that make it so), and on the tinies its budget is 510, not 512: the
|
||||||
# variable — re-timing a deployed loader is a self-update with a re-timed
|
# slot's last word is the trampoline the host composes — the resident slot's
|
||||||
# build. pureboot9 is that re-timed build, and what the update test installs.
|
# holds the application entry, and a staging copy's holds the jump through
|
||||||
|
# which it reaches the loader it installed. The activation window is a
|
||||||
|
# compile-time constant; a different PUREBOOT_TIMEOUT builds the re-timed
|
||||||
|
# binary a self-update then installs.
|
||||||
set(PUREBOOT_TIMEOUT 8 CACHE STRING "pureboot activation window, seconds")
|
set(PUREBOOT_TIMEOUT 8 CACHE STRING "pureboot activation window, seconds")
|
||||||
pureboot_add_loader(pureboot TIMEOUT ${PUREBOOT_TIMEOUT})
|
if(LIBAVR_MCU STREQUAL "attiny13a")
|
||||||
if(PROJECT_IS_TOP_LEVEL)
|
set(_pb_flash 1024)
|
||||||
get_target_property(_pb_stock_hz pureboot PUREBOOT_HZ)
|
set(_pb_wrap "")
|
||||||
get_target_property(_pb_stock_baud pureboot PUREBOOT_BAUD)
|
set(_pb_page 32)
|
||||||
|
set(_pb_hz 9600000)
|
||||||
|
set(_pb_baud 57600)
|
||||||
|
set(_pb_eeprom 64)
|
||||||
|
set(_pb_limit 510)
|
||||||
|
elseif(LIBAVR_MCU STREQUAL "attiny85")
|
||||||
|
set(_pb_flash 8192)
|
||||||
|
set(_pb_wrap -Wl,--pmem-wrap-around=8k)
|
||||||
|
set(_pb_page 64)
|
||||||
|
set(_pb_hz 8000000)
|
||||||
|
set(_pb_baud 57600)
|
||||||
|
set(_pb_eeprom 512)
|
||||||
|
set(_pb_limit 510)
|
||||||
|
else()
|
||||||
|
set(_pb_flash 32768)
|
||||||
|
set(_pb_wrap -Wl,--pmem-wrap-around=32k)
|
||||||
|
set(_pb_page 128)
|
||||||
|
set(_pb_hz 16000000)
|
||||||
|
set(_pb_baud 115200)
|
||||||
|
set(_pb_eeprom 1024)
|
||||||
|
set(_pb_limit 512)
|
||||||
|
endif()
|
||||||
|
math(EXPR _pb_base "${_pb_flash} - 512")
|
||||||
|
math(EXPR _pb_base_hex "${_pb_base}" OUTPUT_FORMAT HEXADECIMAL)
|
||||||
|
if(LIBAVR_MCU STREQUAL "atmega328p")
|
||||||
|
set(_pb_app 0)
|
||||||
|
else()
|
||||||
|
math(EXPR _pb_app "${_pb_base} - 2")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_executable(pureboot pureboot/pureboot.cpp)
|
||||||
|
target_link_libraries(pureboot PRIVATE libavr)
|
||||||
|
target_compile_definitions(pureboot PRIVATE PUREBOOT_TIMEOUT=${PUREBOOT_TIMEOUT})
|
||||||
|
target_link_options(pureboot PRIVATE -nostartfiles -Wl,--section-start=.text=${_pb_base_hex}
|
||||||
|
-Wl,--defsym=pureboot_app=${_pb_app} ${_pb_wrap})
|
||||||
|
add_custom_command(TARGET pureboot POST_BUILD COMMAND ${CMAKE_SIZE} $<TARGET_FILE:pureboot>)
|
||||||
|
add_hex_output(pureboot)
|
||||||
|
if(PROJECT_IS_TOP_LEVEL)
|
||||||
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=${PUREBOOT_LIMIT} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
|
-DLIMIT=${_pb_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> ${PUREBOOT_BASE_HEX})
|
${CMAKE_OBJDUMP} ${CMAKE_NM} $<TARGET_FILE:pureboot> ${_pb_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)
|
||||||
@@ -169,9 +200,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> ${PUREBOOT_SIM_MCU} ${_pb_stock_hz}
|
${PB_DEVICE} $<TARGET_FILE:pureboot> ${LIBAVR_MCU} ${_pb_hz} ${_pb_base_hex}
|
||||||
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_pb_stock_baud} ${PUREBOOT_EEPROM}
|
${_pb_page} ${_pb_baud} ${_pb_eeprom} $<TARGET_FILE:pbapp>.bin
|
||||||
$<TARGET_FILE:pbapp>.bin ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
${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)
|
||||||
|
|
||||||
@@ -179,143 +210,29 @@ 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> ${PUREBOOT_SIM_MCU} ${_pb_stock_hz}
|
${PB_DEVICE} $<TARGET_FILE:pureboot> ${LIBAVR_MCU} ${_pb_hz} ${_pb_base_hex}
|
||||||
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_pb_stock_baud}
|
${_pb_page} ${_pb_baud} ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
|
||||||
${CMAKE_BINARY_DIR}/pbreloc-work)
|
${CMAKE_BINARY_DIR}/pbreloc-work)
|
||||||
set_tests_properties(pureboot.reloc PROPERTIES TIMEOUT 180
|
set_tests_properties(pureboot.reloc PROPERTIES TIMEOUT 180
|
||||||
ENVIRONMENT "PB_OBJCOPY=${CMAKE_OBJCOPY}")
|
ENVIRONMENT "PB_OBJCOPY=${CMAKE_OBJCOPY}")
|
||||||
|
|
||||||
# Entering the loader from a running application with no reset
|
|
||||||
# between, over a page buffer the application dirtied — the case the
|
|
||||||
# loader declines to guard and the host repairs. Hardware forbids the
|
|
||||||
# state here (SPM runs only from the boot section); simavr does not,
|
|
||||||
# which is what makes it constructible.
|
|
||||||
if(LIBAVR_MCU STREQUAL "atmega328p")
|
|
||||||
add_test(NAME pureboot.dirty
|
|
||||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbdirty.py
|
|
||||||
${PB_DEVICE} $<TARGET_FILE:pureboot> ${PUREBOOT_SIM_MCU} ${_pb_stock_hz}
|
|
||||||
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_pb_stock_baud}
|
|
||||||
$<TARGET_FILE:pbapp>.bin
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
|
||||||
${CMAKE_BINARY_DIR}/pbdirty-work)
|
|
||||||
set_tests_properties(pureboot.dirty PROPERTIES TIMEOUT 180)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# Re-homing: a loader mistakenly programmed at address 0 (a raw .bin
|
|
||||||
# handed to a programmer) or sitting in the staging slot must heal
|
|
||||||
# into the canonical slot through the ordinary --update-loader flow.
|
|
||||||
# Patched-vector behavior, so one representative chip carries it.
|
|
||||||
if(LIBAVR_MCU STREQUAL "attiny85")
|
|
||||||
add_test(NAME pureboot.rehome
|
|
||||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbrehome.py
|
|
||||||
${PB_DEVICE} $<TARGET_FILE:pureboot> $<TARGET_FILE:pureboot9>.bin
|
|
||||||
${PUREBOOT_SIM_MCU} ${_pb_stock_hz} ${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE}
|
|
||||||
${_pb_stock_baud} $<TARGET_FILE:pbapp>.bin
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
|
||||||
${CMAKE_BINARY_DIR}/pbrehome-work)
|
|
||||||
set_tests_properties(pureboot.rehome PROPERTIES TIMEOUT 180)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# The self-update end-to-end: the re-timed build (same source, only
|
# The self-update end-to-end: the re-timed build (same source, only
|
||||||
# the timeout differs — a byte-different image) replaces the resident
|
# PUREBOOT_TIMEOUT differs — a byte-different image) replaces the
|
||||||
# through --update-loader, with every power-fail phase rehearsed from
|
# resident through --update-loader, with every power-fail phase
|
||||||
# the runner's flash dumps.
|
# rehearsed from the runner's flash dumps.
|
||||||
pureboot_add_loader(pureboot9 TIMEOUT 9)
|
add_executable(pureboot9 pureboot/pureboot.cpp)
|
||||||
|
target_link_libraries(pureboot9 PRIVATE libavr)
|
||||||
|
target_compile_definitions(pureboot9 PRIVATE PUREBOOT_TIMEOUT=9)
|
||||||
|
target_link_options(pureboot9 PRIVATE -nostartfiles -Wl,--section-start=.text=${_pb_base_hex}
|
||||||
|
-Wl,--defsym=pureboot_app=${_pb_app} ${_pb_wrap})
|
||||||
|
add_hex_output(pureboot9)
|
||||||
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_DEVICE} $<TARGET_FILE:pureboot> $<TARGET_FILE:pureboot9> ${LIBAVR_MCU}
|
||||||
${PUREBOOT_SIM_MCU} ${_pb_stock_hz} ${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE}
|
${_pb_hz} ${_pb_base_hex} ${_pb_page} ${_pb_baud} $<TARGET_FILE:pbapp>.bin
|
||||||
${_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()
|
||||||
|
|||||||
1641
CMakePresets.json
1641
CMakePresets.json
File diff suppressed because it is too large
Load Diff
1
libavr
1
libavr
Submodule libavr deleted from e81dad0131
@@ -1,319 +0,0 @@
|
|||||||
# pureboot as a consumable CMake unit: the per-chip geometry, the default
|
|
||||||
# baud ladder, and pureboot_add_loader() — the one way a loader target is
|
|
||||||
# created, both by this port's own build and by a downstream project. A
|
|
||||||
# downstream project brings its usual libavr setup (the `libavr` target and
|
|
||||||
# the LIBAVR_MCU toolchain preset), adds this directory, and states its
|
|
||||||
# deployment:
|
|
||||||
#
|
|
||||||
# add_subdirectory(bootloader/pureboot)
|
|
||||||
# pureboot_add_loader(myboot CLOCK 1000000 SERIAL software TX pb1 RX pb5)
|
|
||||||
#
|
|
||||||
# Every argument is optional — CLOCK defaults to the family assumption
|
|
||||||
# below, BAUD to the fastest standard rate the clock reaches within 2.5 %
|
|
||||||
# (the ladder), SERIAL to the chip's hardware USART where it has one
|
|
||||||
# (`hardware`/`software` force a backend, USART 1 picks the second
|
|
||||||
# instance), RX/TX to pb0/pb1 for the software UART, TIMEOUT to 8 s.
|
|
||||||
# Infeasible picks fail the build by name: libavr's baud-error and
|
|
||||||
# software-UART cycle-floor static asserts re-check whatever is passed.
|
|
||||||
|
|
||||||
# Per-family geometry: flash/page/EEPROM sizes and the linker wrap the PC
|
|
||||||
# modulo needs, the loader slot (each chip's smallest boot sector — 1 KiB on
|
|
||||||
# the word-addressed 1284s), and the deployment defaults (crystal assumption
|
|
||||||
# on the megas, calibrated RC on the tinies). The USART flags mirror the
|
|
||||||
# hardware inventory the loader's own static asserts check (the plain 644 is
|
|
||||||
# the x4 family's one single-USART die, Atmel-2593).
|
|
||||||
set(_pb_has_usart 1)
|
|
||||||
set(_pb_has_usart1 0)
|
|
||||||
if(LIBAVR_MCU MATCHES "^attiny13a?$")
|
|
||||||
set(_pb_flash 1024)
|
|
||||||
set(_pb_wrap "")
|
|
||||||
set(_pb_page 32)
|
|
||||||
set(_pb_hz 9600000)
|
|
||||||
set(_pb_eeprom 64)
|
|
||||||
set(_pb_has_usart 0)
|
|
||||||
elseif(LIBAVR_MCU STREQUAL "attiny25")
|
|
||||||
set(_pb_flash 2048)
|
|
||||||
set(_pb_wrap "")
|
|
||||||
set(_pb_page 32)
|
|
||||||
set(_pb_hz 8000000)
|
|
||||||
set(_pb_eeprom 128)
|
|
||||||
set(_pb_has_usart 0)
|
|
||||||
elseif(LIBAVR_MCU STREQUAL "attiny45")
|
|
||||||
set(_pb_flash 4096)
|
|
||||||
set(_pb_wrap "")
|
|
||||||
set(_pb_page 64)
|
|
||||||
set(_pb_hz 8000000)
|
|
||||||
set(_pb_eeprom 256)
|
|
||||||
set(_pb_has_usart 0)
|
|
||||||
elseif(LIBAVR_MCU STREQUAL "attiny85")
|
|
||||||
set(_pb_flash 8192)
|
|
||||||
set(_pb_wrap -Wl,--pmem-wrap-around=8k)
|
|
||||||
set(_pb_page 64)
|
|
||||||
set(_pb_hz 8000000)
|
|
||||||
set(_pb_eeprom 512)
|
|
||||||
set(_pb_has_usart 0)
|
|
||||||
elseif(LIBAVR_MCU MATCHES "^atmega48(a|p|pa)?$")
|
|
||||||
set(_pb_flash 4096)
|
|
||||||
set(_pb_wrap "")
|
|
||||||
set(_pb_page 64)
|
|
||||||
set(_pb_hz 16000000)
|
|
||||||
set(_pb_eeprom 256)
|
|
||||||
elseif(LIBAVR_MCU MATCHES "^atmega8a?$" OR LIBAVR_MCU MATCHES "^atmega88(a|p|pa)?$")
|
|
||||||
set(_pb_flash 8192)
|
|
||||||
set(_pb_wrap -Wl,--pmem-wrap-around=8k)
|
|
||||||
set(_pb_page 64)
|
|
||||||
set(_pb_hz 16000000)
|
|
||||||
set(_pb_eeprom 512)
|
|
||||||
elseif(LIBAVR_MCU MATCHES "^atmega16a?$" OR LIBAVR_MCU MATCHES "^atmega168(a|p|pa)?$")
|
|
||||||
set(_pb_flash 16384)
|
|
||||||
set(_pb_wrap -Wl,--pmem-wrap-around=16k)
|
|
||||||
set(_pb_page 128)
|
|
||||||
set(_pb_hz 16000000)
|
|
||||||
set(_pb_eeprom 512)
|
|
||||||
elseif(LIBAVR_MCU MATCHES "^atmega164(a|p|pa)$")
|
|
||||||
set(_pb_flash 16384)
|
|
||||||
set(_pb_wrap -Wl,--pmem-wrap-around=16k)
|
|
||||||
set(_pb_page 128)
|
|
||||||
set(_pb_hz 16000000)
|
|
||||||
set(_pb_eeprom 512)
|
|
||||||
set(_pb_has_usart1 1)
|
|
||||||
elseif(LIBAVR_MCU MATCHES "^atmega32a?$" OR LIBAVR_MCU MATCHES "^atmega328p?$")
|
|
||||||
set(_pb_flash 32768)
|
|
||||||
set(_pb_wrap -Wl,--pmem-wrap-around=32k)
|
|
||||||
set(_pb_page 128)
|
|
||||||
set(_pb_hz 16000000)
|
|
||||||
set(_pb_eeprom 1024)
|
|
||||||
elseif(LIBAVR_MCU MATCHES "^atmega324(a|p|pa)$")
|
|
||||||
set(_pb_flash 32768)
|
|
||||||
set(_pb_wrap -Wl,--pmem-wrap-around=32k)
|
|
||||||
set(_pb_page 128)
|
|
||||||
set(_pb_hz 16000000)
|
|
||||||
set(_pb_eeprom 1024)
|
|
||||||
set(_pb_has_usart1 1)
|
|
||||||
elseif(LIBAVR_MCU MATCHES "^atmega644(a|p|pa)?$")
|
|
||||||
# 64 KiB is exactly the 16-bit byte space: plain LPM reaches everything,
|
|
||||||
# and the smallest boot section (1 KiB) holds the loader and its staging
|
|
||||||
# slot together (see README.md). The plain 644 is the family's one
|
|
||||||
# single-USART die.
|
|
||||||
set(_pb_flash 65536)
|
|
||||||
set(_pb_wrap -Wl,--pmem-wrap-around=64k)
|
|
||||||
set(_pb_page 256)
|
|
||||||
set(_pb_hz 16000000)
|
|
||||||
set(_pb_eeprom 2048)
|
|
||||||
if(NOT LIBAVR_MCU STREQUAL "atmega644")
|
|
||||||
set(_pb_has_usart1 1)
|
|
||||||
endif()
|
|
||||||
elseif(LIBAVR_MCU MATCHES "^atmega1284p?$")
|
|
||||||
# 128 KiB: wire flash addresses are word addresses, reads go through
|
|
||||||
# ELPM, and the PC's modulo wrap exceeds what --pmem-wrap-around models.
|
|
||||||
# The slot is 1 KiB — this chip's own smallest boot sector; the far
|
|
||||||
# machinery cannot fit 512 B (see README.md).
|
|
||||||
set(_pb_flash 131072)
|
|
||||||
set(_pb_wrap "")
|
|
||||||
set(_pb_page 256)
|
|
||||||
set(_pb_hz 16000000)
|
|
||||||
set(_pb_eeprom 4096)
|
|
||||||
set(_pb_slot 1024)
|
|
||||||
set(_pb_limit 1024)
|
|
||||||
set(_pb_has_usart1 1)
|
|
||||||
else()
|
|
||||||
message(FATAL_ERROR "pureboot: no geometry for ${LIBAVR_MCU}")
|
|
||||||
endif()
|
|
||||||
if(NOT DEFINED _pb_slot)
|
|
||||||
set(_pb_slot 512)
|
|
||||||
endif()
|
|
||||||
math(EXPR _pb_base "${_pb_flash} - ${_pb_slot}")
|
|
||||||
math(EXPR _pb_base_hex "${_pb_base}" OUTPUT_FORMAT HEXADECIMAL)
|
|
||||||
# Patched-vector chips hand over through the trampoline word below the slot,
|
|
||||||
# which is also the slot's own last word — their budget is slot − 2.
|
|
||||||
if(LIBAVR_MCU MATCHES "^atmega" AND NOT LIBAVR_MCU MATCHES "^atmega48")
|
|
||||||
set(_pb_app 0)
|
|
||||||
if(NOT DEFINED _pb_limit)
|
|
||||||
set(_pb_limit ${_pb_slot})
|
|
||||||
endif()
|
|
||||||
else()
|
|
||||||
math(EXPR _pb_app "${_pb_base} - 2")
|
|
||||||
math(EXPR _pb_limit "${_pb_slot} - 2")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# simavr names its cores after the base dies; the A revisions run on them
|
|
||||||
# (the 644PA on the 644P core).
|
|
||||||
set(_pb_sim_mcu ${LIBAVR_MCU})
|
|
||||||
if(LIBAVR_MCU MATCHES "^atmega(8|16|32|48|88|164|168|644)a$")
|
|
||||||
string(REGEX REPLACE "a$" "" _pb_sim_mcu ${LIBAVR_MCU})
|
|
||||||
elseif(LIBAVR_MCU STREQUAL "atmega644pa")
|
|
||||||
set(_pb_sim_mcu atmega644p)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# The function runs in its caller's scope, so everything it needs crosses
|
|
||||||
# scopes as global properties.
|
|
||||||
set_property(GLOBAL PROPERTY PUREBOOT_BASE_HEX ${_pb_base_hex})
|
|
||||||
set_property(GLOBAL PROPERTY PUREBOOT_APP ${_pb_app})
|
|
||||||
set_property(GLOBAL PROPERTY PUREBOOT_WRAP "${_pb_wrap}")
|
|
||||||
set_property(GLOBAL PROPERTY PUREBOOT_DEFAULT_HZ ${_pb_hz})
|
|
||||||
set_property(GLOBAL PROPERTY PUREBOOT_HAS_USART ${_pb_has_usart})
|
|
||||||
set_property(GLOBAL PROPERTY PUREBOOT_HAS_USART1 ${_pb_has_usart1})
|
|
||||||
|
|
||||||
# The port's own build (tests, the size matrix) reads the geometry from the
|
|
||||||
# parent scope; a downstream consumer gets the same variables for free.
|
|
||||||
set(PUREBOOT_BASE_HEX ${_pb_base_hex} PARENT_SCOPE)
|
|
||||||
set(PUREBOOT_PAGE ${_pb_page} PARENT_SCOPE)
|
|
||||||
set(PUREBOOT_SLOT ${_pb_slot} PARENT_SCOPE)
|
|
||||||
set(PUREBOOT_LIMIT ${_pb_limit} PARENT_SCOPE)
|
|
||||||
set(PUREBOOT_EEPROM ${_pb_eeprom} PARENT_SCOPE)
|
|
||||||
set(PUREBOOT_DEFAULT_HZ ${_pb_hz} PARENT_SCOPE)
|
|
||||||
set(PUREBOOT_HAS_USART ${_pb_has_usart} PARENT_SCOPE)
|
|
||||||
set(PUREBOOT_HAS_USART1 ${_pb_has_usart1} PARENT_SCOPE)
|
|
||||||
set(PUREBOOT_SIM_MCU ${_pb_sim_mcu} PARENT_SCOPE)
|
|
||||||
|
|
||||||
# The fastest standard rate the clock reaches within 2.5 % — the same
|
|
||||||
# best-of-U2X-and-plain divisor search libavr's solve_baud runs, so a
|
|
||||||
# default never trips the compile-time error it is checked against. A
|
|
||||||
# software build additionally requires the polled receiver's 100-cycles-a-bit
|
|
||||||
# floor (its own static assert): at low clocks the U2X divisor still reaches
|
|
||||||
# rates the bit-banged sampler cannot, so the backend gates the ladder.
|
|
||||||
function(pureboot_default_baud clock software outvar)
|
|
||||||
foreach(baud 115200 57600 38400 19200 9600)
|
|
||||||
math(EXPR _cycles "${clock} / ${baud}")
|
|
||||||
if(software AND _cycles LESS 100)
|
|
||||||
continue()
|
|
||||||
endif()
|
|
||||||
foreach(divisor 8 16)
|
|
||||||
math(EXPR _step "${divisor} * ${baud}")
|
|
||||||
math(EXPR _n "(${clock} + ${_step} / 2) / ${_step}")
|
|
||||||
if(_n LESS 1 OR _n GREATER 4096)
|
|
||||||
continue()
|
|
||||||
endif()
|
|
||||||
math(EXPR _actual "${clock} / (${divisor} * ${_n})")
|
|
||||||
math(EXPR _delta "${_actual} - ${baud}")
|
|
||||||
if(_delta LESS 0)
|
|
||||||
math(EXPR _delta "-(${_delta})")
|
|
||||||
endif()
|
|
||||||
math(EXPR _error_bp "${_delta} * 10000 / ${baud}")
|
|
||||||
if(_error_bp LESS_EQUAL 250)
|
|
||||||
set(${outvar} ${baud} PARENT_SCOPE)
|
|
||||||
return()
|
|
||||||
endif()
|
|
||||||
endforeach()
|
|
||||||
endforeach()
|
|
||||||
message(FATAL_ERROR "pureboot: no standard baud rate fits a ${clock} Hz clock within 2.5 %")
|
|
||||||
endfunction()
|
|
||||||
|
|
||||||
# pureboot_add_loader(<name> [CLOCK <hz>] [BAUD <bd>]
|
|
||||||
# [SERIAL auto|hardware|software] [USART <n>]
|
|
||||||
# [RX <pin>] [TX <pin>] [TIMEOUT <s>])
|
|
||||||
#
|
|
||||||
# Creates the loader target plus its flashable images (<name>.hex for a
|
|
||||||
# programmer, <name>.bin for --update-loader) and stamps the resolved
|
|
||||||
# deployment on the target: the PUREBOOT_HZ, PUREBOOT_BAUD and PUREBOOT_LINK
|
|
||||||
# properties (the link as usart0/usart1/sw:<RX>,<TX> — what a test harness
|
|
||||||
# needs to speak to the build).
|
|
||||||
function(pureboot_add_loader name)
|
|
||||||
cmake_parse_arguments(PB "" "CLOCK;BAUD;SERIAL;USART;RX;TX;TIMEOUT" "" ${ARGN})
|
|
||||||
if(PB_UNPARSED_ARGUMENTS)
|
|
||||||
message(FATAL_ERROR "pureboot_add_loader(${name}): unknown arguments ${PB_UNPARSED_ARGUMENTS}")
|
|
||||||
endif()
|
|
||||||
get_property(_hz GLOBAL PROPERTY PUREBOOT_DEFAULT_HZ)
|
|
||||||
get_property(_base_hex GLOBAL PROPERTY PUREBOOT_BASE_HEX)
|
|
||||||
get_property(_app GLOBAL PROPERTY PUREBOOT_APP)
|
|
||||||
get_property(_wrap GLOBAL PROPERTY PUREBOOT_WRAP)
|
|
||||||
get_property(_usart GLOBAL PROPERTY PUREBOOT_HAS_USART)
|
|
||||||
get_property(_usart1 GLOBAL PROPERTY PUREBOOT_HAS_USART1)
|
|
||||||
|
|
||||||
if(NOT PB_CLOCK)
|
|
||||||
set(PB_CLOCK ${_hz})
|
|
||||||
endif()
|
|
||||||
if(NOT PB_TIMEOUT)
|
|
||||||
set(PB_TIMEOUT 8)
|
|
||||||
endif()
|
|
||||||
if(NOT PB_SERIAL)
|
|
||||||
set(PB_SERIAL auto)
|
|
||||||
endif()
|
|
||||||
if(DEFINED PB_USART AND PB_SERIAL STREQUAL "software")
|
|
||||||
message(FATAL_ERROR "pureboot_add_loader(${name}): USART ${PB_USART} contradicts SERIAL software")
|
|
||||||
endif()
|
|
||||||
if(DEFINED PB_USART)
|
|
||||||
set(PB_SERIAL hardware)
|
|
||||||
elseif(PB_SERIAL STREQUAL "hardware")
|
|
||||||
set(PB_USART 0)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
set(_serial_defines "")
|
|
||||||
if(PB_SERIAL STREQUAL "hardware")
|
|
||||||
if(PB_USART EQUAL 1 AND NOT _usart1)
|
|
||||||
message(FATAL_ERROR "pureboot_add_loader(${name}): ${LIBAVR_MCU} has no USART1")
|
|
||||||
elseif(NOT _usart)
|
|
||||||
message(FATAL_ERROR "pureboot_add_loader(${name}): ${LIBAVR_MCU} has no hardware USART")
|
|
||||||
endif()
|
|
||||||
set(_serial_defines PUREBOOT_USART=${PB_USART})
|
|
||||||
set(_link usart${PB_USART})
|
|
||||||
else()
|
|
||||||
if(PB_SERIAL STREQUAL "auto")
|
|
||||||
if(_usart AND (PB_RX OR PB_TX))
|
|
||||||
message(WARNING "pureboot_add_loader(${name}): RX/TX apply to the software UART, "
|
|
||||||
"which auto does not pick on ${LIBAVR_MCU} — SERIAL software to force it")
|
|
||||||
endif()
|
|
||||||
if(_usart)
|
|
||||||
set(_link usart0)
|
|
||||||
else()
|
|
||||||
set(PB_SERIAL software)
|
|
||||||
endif()
|
|
||||||
endif()
|
|
||||||
if(PB_SERIAL STREQUAL "software")
|
|
||||||
if(NOT PB_RX)
|
|
||||||
set(PB_RX pb0)
|
|
||||||
endif()
|
|
||||||
if(NOT PB_TX)
|
|
||||||
set(PB_TX pb1)
|
|
||||||
endif()
|
|
||||||
foreach(_pin ${PB_RX} ${PB_TX})
|
|
||||||
if(NOT _pin MATCHES "^p[a-h][0-7]$")
|
|
||||||
message(FATAL_ERROR "pureboot_add_loader(${name}): pin '${_pin}' is not of the form pb1")
|
|
||||||
endif()
|
|
||||||
endforeach()
|
|
||||||
set(_serial_defines PUREBOOT_SOFT_SERIAL PUREBOOT_RX=${PB_RX} PUREBOOT_TX=${PB_TX})
|
|
||||||
# The link spec a test harness drives a GPIO bridge with: sw:<RX>,<TX>
|
|
||||||
# as the port letter and bit, the loader's own pin naming upcased.
|
|
||||||
string(SUBSTRING ${PB_RX} 1 2 _rx_pin)
|
|
||||||
string(SUBSTRING ${PB_TX} 1 2 _tx_pin)
|
|
||||||
string(TOUPPER "sw:${_rx_pin},${_tx_pin}" _link)
|
|
||||||
string(REPLACE "SW" "sw" _link ${_link})
|
|
||||||
endif()
|
|
||||||
endif()
|
|
||||||
if(NOT PB_BAUD)
|
|
||||||
if(PB_SERIAL STREQUAL "software")
|
|
||||||
pureboot_default_baud(${PB_CLOCK} 1 PB_BAUD)
|
|
||||||
else()
|
|
||||||
pureboot_default_baud(${PB_CLOCK} 0 PB_BAUD)
|
|
||||||
endif()
|
|
||||||
endif()
|
|
||||||
|
|
||||||
set(_defines PUREBOOT_CLOCK_HZ=${PB_CLOCK} PUREBOOT_BAUD=${PB_BAUD} PUREBOOT_TIMEOUT=${PB_TIMEOUT}
|
|
||||||
${_serial_defines})
|
|
||||||
|
|
||||||
add_executable(${name} ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/pureboot.cpp)
|
|
||||||
target_link_libraries(${name} PRIVATE libavr)
|
|
||||||
target_compile_definitions(${name} PRIVATE ${_defines})
|
|
||||||
# Codegen shaping for the loader TU only, worth ~40 B on every chip and
|
|
||||||
# what carries the far-flash 1284 build under 512. At -Os GCC otherwise
|
|
||||||
# rewrites the byte-stream loops' counters into end-pointer forms that
|
|
||||||
# cost registers (-fno-ivopts, -fno-split-wide-types), leaves register
|
|
||||||
# pressure on the table with the default allocator
|
|
||||||
# (-fira-algorithm=priority), and spends bytes on rewrites a
|
|
||||||
# straight-line loader gains nothing from.
|
|
||||||
target_compile_options(${name} PRIVATE
|
|
||||||
-fno-ivopts -fira-algorithm=priority -fno-expensive-optimizations -fno-split-wide-types)
|
|
||||||
target_link_options(${name} PRIVATE -nostartfiles -Wl,--section-start=.text=${_base_hex}
|
|
||||||
-Wl,--defsym=pureboot_app=${_app} ${_wrap})
|
|
||||||
add_custom_command(TARGET ${name} POST_BUILD COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${name}>)
|
|
||||||
# The ELF is a container (symbols, section headers), never flashed; the
|
|
||||||
# flashable forms sit beside it: .hex for a programmer, .bin (the slot's
|
|
||||||
# bare bytes) for the host tool's raw path and --update-loader.
|
|
||||||
add_custom_command(TARGET ${name} POST_BUILD
|
|
||||||
COMMAND ${CMAKE_OBJCOPY} -O ihex -R .eeprom
|
|
||||||
$<TARGET_FILE:${name}> $<TARGET_FILE:${name}>.hex
|
|
||||||
COMMAND ${CMAKE_OBJCOPY} -O binary -R .eeprom
|
|
||||||
$<TARGET_FILE:${name}> $<TARGET_FILE:${name}>.bin)
|
|
||||||
set_target_properties(${name} PROPERTIES PUREBOOT_HZ ${PB_CLOCK} PUREBOOT_BAUD ${PB_BAUD}
|
|
||||||
PUREBOOT_LINK ${_link})
|
|
||||||
endfunction()
|
|
||||||
@@ -2,115 +2,49 @@
|
|||||||
|
|
||||||
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 and compiler flags allowed), built for **every chip libavr
|
(attributes allowed), built for every chip libavr targets, **512 bytes on
|
||||||
targets — all 37 — in 512 bytes each**: 434 B on the tiny13s, 438–442 B on
|
each** — 488 B on the ATtiny13A, 502 B on the ATtiny85, 504 B on the
|
||||||
the tiny25/45/85, 412–452 B across the megas, and 506 B on the
|
ATmega328P. The device speaks primitives; every composite — verify, erase,
|
||||||
ATmega1284/1284P, whose far-flash machinery (ELPM reads, RAMPZ page commands,
|
reset-vector surgery, updating the loader itself — lives in the host tool
|
||||||
word-addressed wire) is the heaviest. Those are the stock deployments;
|
(`pureboot.py`).
|
||||||
choosing the software UART where the chip has a USART costs 8–46 B more (a
|
|
||||||
bit-bang against a peripheral), which every chip still absorbs inside its
|
|
||||||
slot — on the 1284s that means their 1 KiB boot sector, where the
|
|
||||||
software-serial image lands at 546 B. Bringing the 1284's default build
|
|
||||||
under 512 at all is what the loop-placement attributes on the byte streamers
|
|
||||||
(`pureboot.cpp`) and the codegen flags on the loader TU (`CMakeLists.txt`)
|
|
||||||
are for; measured against each chip's own budget the tightest is the
|
|
||||||
ATmega328P, 50 B spare. Clock, baud, serial backend and
|
|
||||||
pins are per-build configuration (below); the size matrix in the test suite
|
|
||||||
holds every combination inside its slot. The device speaks primitives; every
|
|
||||||
composite — verify, erase, reset-vector surgery, updating the loader itself —
|
|
||||||
lives in the host tool (`pureboot.py`).
|
|
||||||
|
|
||||||
The 1284s still *deploy* in a 1 KiB slot, their smallest boot sector being
|
|
||||||
512 words; at 506 B the image would also fit the 644's
|
|
||||||
two-512-byte-slots-per-boot-sector geometry.
|
|
||||||
|
|
||||||
The image is **position-independent**: control flow is PC-relative, the
|
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 512-byte
|
||||||
code is *running* in (from the runtime return address), the info block is
|
slot the code is *running* in (from the runtime return address), the info
|
||||||
addressed from that same anchor, and the application jump is an indirect
|
block is addressed from that same anchor, and the application jump is an
|
||||||
call to an absolute entry. The identical binary therefore runs from any
|
indirect call to an absolute entry. The identical binary therefore runs from
|
||||||
slot with every command intact — which makes pureboot **its own staging
|
any 512-byte slot with every command intact — which makes pureboot **its own
|
||||||
loader**: the host installs the same binary one slot below the resident,
|
staging loader**: the host installs the same binary one slot below the
|
||||||
jumps into it, and lets it rewrite the resident. The slot is 512 bytes
|
resident, jumps into it, and lets it rewrite the resident. On the tinies the
|
||||||
(1 KiB on the word-addressed large chips, matching their boot-sector
|
budget is 510, not 512: a slot's last word belongs to the host-managed
|
||||||
minimum); on the tinies the budget is 510, not 512: a slot's last word
|
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 |
|
| ATmega328P | USART0, RXD/TXD = PD0/PD1 | 115200 8N1 | 16 MHz crystal |
|
||||||
| ATtiny25/45/85 | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 8 MHz internal RC |
|
| ATtiny85 | 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 |
|
| ATtiny13A | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 9.6 MHz internal RC |
|
||||||
|
|
||||||
The software-UART RX pin has its pull-up enabled; TX idles high. All
|
The tiny RX pin has its pull-up enabled; TX idles high. All multi-byte
|
||||||
multi-byte quantities on the wire are little-endian.
|
quantities on the wire are little-endian.
|
||||||
|
|
||||||
## Activation
|
## Activation
|
||||||
|
|
||||||
Reset enters the loader (BOOTRST on the boot-sectioned megas; the patched
|
Reset enters the loader (BOOTRST on the mega, the patched reset vector on the
|
||||||
reset vector on the tinies and the boot-section-less m48s) — except a
|
tinies) — except a watchdog reset, which hands straight to the application
|
||||||
watchdog reset, which hands straight to the application (the application
|
(the application owns its watchdog; it must clear WDRF itself, which also
|
||||||
owns its watchdog; it must clear WDRF itself, which also releases the
|
releases the WDRF-forced WDE).
|
||||||
WDRF-forced WDE).
|
|
||||||
|
|
||||||
The host then has one activation window per awaited byte to knock: `p` then
|
The host then has one activation window per awaited byte to knock: `p` then
|
||||||
`b`. Each awaited byte gets a fresh window; any other byte is discarded and
|
`b`. Each awaited byte gets a fresh window; any other byte is discarded and
|
||||||
awaited again (line noise cannot lock the loader, only delay it). A window
|
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
|
The window length is a compile-time constant — 8 s by default, another value
|
||||||
value via `pureboot_add_loader(... TIMEOUT <s>)` (the stock target keeps
|
via the `PUREBOOT_TIMEOUT` CMake cache variable — so the whole EEPROM belongs
|
||||||
the `PUREBOOT_TIMEOUT` cache variable) — so the whole EEPROM belongs to
|
to the application; pureboot never uses it for its own state. Re-timing a
|
||||||
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
|
||||||
@@ -121,11 +55,6 @@ write to finish and sends the prompt `+` (0x2b) — the prompt is therefore
|
|||||||
also the completion ack of the previous command. A session is: await `+`,
|
also the completion ack of the previous command. A session is: await `+`,
|
||||||
send a command, read its reply, repeat.
|
send a command, read its reply, repeat.
|
||||||
|
|
||||||
On chips whose flash exceeds 64 KiB (the 1284s — info-block flag bit 1) the
|
|
||||||
`R`/`W` flash addresses are **word** addresses; everywhere else they are byte
|
|
||||||
addresses (the 644s' 64 KiB is exactly the 16-bit byte space and stays
|
|
||||||
byte-addressed). EEPROM addresses are always bytes, counts always bytes.
|
|
||||||
|
|
||||||
| Cmd | Arguments | Reply |
|
| Cmd | Arguments | Reply |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `b` | — | the 12-byte info block |
|
| `b` | — | the 12-byte info block |
|
||||||
@@ -141,20 +70,7 @@ byte-addressed). EEPROM addresses are always bytes, counts always bytes.
|
|||||||
then erases and programs; the address must be page-aligned. Pages inside the
|
then erases and programs; the address must be page-aligned. Pages inside the
|
||||||
512-byte slot the loader is *running* in are drained but never programmed — a
|
512-byte slot the loader is *running* in are drained but never programmed — a
|
||||||
broken host cannot brick the running copy, and a staged copy may rewrite the
|
broken host cannot brick the running copy, and a staged copy may rewrite the
|
||||||
resident slot.
|
resident slot. `w` is host-paced: send the next byte only after the previous
|
||||||
|
|
||||||
The loader never clears the SPM buffer before a fill, so **one `W` may
|
|
||||||
program the wrong bytes, and the host is what fixes it**. The buffer is
|
|
||||||
write-once per word until cleared, and two things leave words in it: a
|
|
||||||
refused page (drained, never programmed) and — where SPM runs from anywhere,
|
|
||||||
the tinies and the m48s — an application that self-programmed before
|
|
||||||
entering. The next `W` takes those stale words, and clears them: a page write
|
|
||||||
auto-erases the buffer (§26.2.1; §19.2 on the tinies), so repeating it
|
|
||||||
programs correctly. The host therefore verifies every page it writes and
|
|
||||||
rewrites what comes back wrong (three retries, then it stops); a host that
|
|
||||||
programs without reading back cannot trust the first `W` after either event.
|
|
||||||
|
|
||||||
`w` is host-paced: send the next byte only after the previous
|
|
||||||
byte's `+`. `F` returns the bytes in the hardware's Z order; on a chip
|
byte's `+`. `F` returns the bytes in the hardware's Z order; on a chip
|
||||||
without an extended fuse byte (the ATtiny13A) that slot carries no meaning.
|
without an extended fuse byte (the ATtiny13A) that slot carries no meaning.
|
||||||
Fuse *writing* does not exist: SPM reaches flash (and, on the mega, lock
|
Fuse *writing* does not exist: SPM reaches flash (and, on the mega, lock
|
||||||
@@ -172,53 +88,18 @@ The info block (`b`):
|
|||||||
|---|---|
|
|---|---|
|
||||||
| 0–2 | `'P'`, `'B'`, protocol version (1) |
|
| 0–2 | `'P'`, `'B'`, protocol version (1) |
|
||||||
| 3–5 | device signature |
|
| 3–5 | device signature |
|
||||||
| 6 | SPM page size in bytes (0 means 256) |
|
| 6 | SPM page size in bytes |
|
||||||
| 7–8 | loader base — application flash ends here (a word address when bit 1 is set) |
|
| 7–8 | loader base — application flash ends here |
|
||||||
| 9–10 | EEPROM size |
|
| 9–10 | EEPROM size |
|
||||||
| 11 | bit 0: host must patch the reset vector (no hardware boot section); bit 1: flash wire addresses are word addresses |
|
| 11 | bit 0 set: host must patch the reset vector (no hardware boot section) |
|
||||||
|
|
||||||
Composites are the host's job: verify = read back and compare, erase =
|
Composites are the host's job: verify = read back and compare, erase =
|
||||||
write `0xff` (per page for flash, per byte for EEPROM).
|
write `0xff` (per page for flash, per byte for EEPROM).
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
The build leaves three artifacts per chip. The ELF is a container for the
|
**ATmega328P**: program the loader at 0x7e00 with an external programmer.
|
||||||
tests and objcopy — never flashed. The **.hex is the programmer artifact**:
|
Two fuse profiles, same binary:
|
||||||
it carries its own addresses and lands the loader in its top slot,
|
|
||||||
touching nothing else. The **.bin is the self-update image** — the slot's
|
|
||||||
bare bytes with no addressing, which a programmer would put at address 0.
|
|
||||||
On a boot-sectioned mega a copy at 0 is dead weight (SPM only executes
|
|
||||||
from the boot section, so it cannot even heal itself — reflash the .hex);
|
|
||||||
on the patched-vector chips it *runs* (the image is position-independent
|
|
||||||
and reset enters word 0), reports its canonical geometry, and the ordinary
|
|
||||||
`--update-loader` flow re-homes a build into the top slot from any
|
|
||||||
position — the staging install and the word-0 redirect execute from
|
|
||||||
copies outside page 0's slot, and a copy sitting in the staging slot
|
|
||||||
itself is recognized as the installed staging copy and left in place (it
|
|
||||||
streams the new resident like any staged copy, so an older build installs
|
|
||||||
a newer one). `pureboot.rehome` is the acceptance test for both
|
|
||||||
positions. Flashing the application afterwards overwrites the stale copy,
|
|
||||||
vector surgery included.
|
|
||||||
|
|
||||||
**Boot-sectioned megas**: program the loader at `flash − slot` with an
|
|
||||||
external programmer. Every such mega has a BOOTSZ step whose boot section
|
|
||||||
is exactly the loader slot — 512 B, the second-smallest step on the 8 KiB
|
|
||||||
and 16 KiB chips (m8, m88, m16, m168, m164), the smallest on the 32 KiB
|
|
||||||
ones (m32, m328, m324); on the 1284s that step is the smallest, 512 words,
|
|
||||||
which is why their slot is 1 KiB — so the ATmega328P profiles below apply
|
|
||||||
to every one of them with its own addresses and slot size; the per-chip
|
|
||||||
BOOTSZ ladders live in the host tool (`BOOT_FUSE`). The 1284s' numbers:
|
|
||||||
standalone = BOOTSZ 512 words (reset at the loader base 0x1fc00);
|
|
||||||
self-update = 1024 words, covering both 1 KiB slots, the loader-first
|
|
||||||
reset landing at 0x1f800 — the staging slot, walked across when erased.
|
|
||||||
|
|
||||||
The **644s** are the geometry's sweet spot: their smallest boot section
|
|
||||||
(512 words = 1 KiB) is exactly *two* 512-byte slots, so the resident and
|
|
||||||
its staging slot both live inside the minimum section — self-update needs
|
|
||||||
no fuse step up, and the standalone profile does not exist (reset lands at
|
|
||||||
0xfc00, one erased slot below the loader: the loader-first walk built in).
|
|
||||||
|
|
||||||
ATmega328P profiles (addresses for its 32 KiB):
|
|
||||||
|
|
||||||
| BOOTSZ | BOOTRST | Behavior |
|
| BOOTSZ | BOOTRST | Behavior |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -229,50 +110,32 @@ ATmega328P profiles (addresses for its 32 KiB):
|
|||||||
Applications are flashed unmodified — word 0 stays the application's own
|
Applications are flashed unmodified — word 0 stays the application's own
|
||||||
reset vector, and the hand-over jumps to 0.
|
reset vector, and the hand-over jumps to 0.
|
||||||
|
|
||||||
**Patched-vector chips — the tinies and the m48s** (no boot section; the
|
**Tinies** (no boot section): program the loader at `flash − 512`; erased
|
||||||
m48s' SPM runs from the entire flash, Atmel-8271 §26): program the loader
|
flash below it walks up into the loader, so a virgin chip activates. When
|
||||||
at `flash − 512`; erased flash below it walks up into the loader, so a
|
flashing an application the host performs reset-vector surgery: word 0 is
|
||||||
virgin chip activates. When flashing an application the host performs
|
rewritten to `rjmp` to the loader base, and the application's own entry is
|
||||||
reset-vector surgery: word 0 is rewritten to `rjmp` to the loader base, and
|
re-encoded as a trampoline `rjmp` in the word just below the loader
|
||||||
the application's own entry is re-encoded as a trampoline `rjmp` in the
|
(`base − 2`, where the hand-over jumps). Every other vector stays the
|
||||||
word just below the loader (`base − 2`, where the hand-over jumps). Every
|
application's. The patched page 0 and the trampoline page are written
|
||||||
other vector stays the application's. The patched page 0 and the trampoline
|
*first*, so from the first write on an interrupted flash still resets into
|
||||||
page are written *first*, so from the first write on an interrupted flash
|
the loader; an erase runs top-down for the same reason.
|
||||||
still resets into the loader; an erase runs top-down for the same reason.
|
|
||||||
The m48s speak this profile over their hardware USART — no fuse preflight,
|
|
||||||
BOOTRST does not exist there.
|
|
||||||
|
|
||||||
## Updating the loader
|
## Updating the loader
|
||||||
|
|
||||||
`pureboot.py --update-loader new_pureboot.bin` replaces the resident loader
|
`pureboot.py --update-loader new_pureboot.bin` replaces the resident loader
|
||||||
with any pureboot build — a re-timed window, a newer protocol — using the
|
with any pureboot build — a re-timed window, a newer protocol — using the
|
||||||
loader itself as its own staging loader. The image is the loader's own 512
|
loader itself as its own staging loader:
|
||||||
bytes as a raw binary, or the Intel HEX the build emits beside it, which
|
|
||||||
links the loader at its base inside an otherwise blank flash image:
|
|
||||||
|
|
||||||
The preflight refuses an image built for another chip: the info block
|
1. The staging slot `[base−512, base)` is saved to a host-side state file
|
||||||
embedded in every pureboot binary (signature, page size, loader base,
|
(on the 1 KB tiny13A that is the whole application, vectors included).
|
||||||
EEPROM size, flags) must match the device's own, and the error names both.
|
2. The resident installs the identical update image there. On the tinies the
|
||||||
Die revisions share their base signature and geometry, so their images are
|
host composes the slot's last word — the same address as the resident's
|
||||||
interchangeable — as the silicon is. `loader_image()` also accepts a
|
trampoline — as a jump to the resident base, so even an abandoned staging
|
||||||
padded image (a raw .bin padded from 0, or a whole-flash read-back with
|
copy times out into a loader, never into garbage.
|
||||||
the loader resident) and peels it to the slot content by the embedded base.
|
|
||||||
|
|
||||||
1. The staging slot `[base−slot, base)` is saved to a host-side state file
|
|
||||||
(on the 1 KB tiny13s that is the whole application, vectors included).
|
|
||||||
2. The resident installs the identical update image there. On the
|
|
||||||
patched-vector chips the host composes the slot's last word — the same
|
|
||||||
address as the resident's trampoline — as a jump to the resident base,
|
|
||||||
so even an abandoned staging copy times out into a loader, never into
|
|
||||||
garbage. A loader already sitting whole in the staging slot (its info
|
|
||||||
block in place, the slot unchanged since the update began) is left as
|
|
||||||
the staging copy instead — rewriting it would only meet its own
|
|
||||||
running-slot guard.
|
|
||||||
3. `J` enters the staging copy, which rewrites the resident slot. On the
|
3. `J` enters the staging copy, which rewrites the resident slot. On the
|
||||||
patched-vector chips whose staging slot sits away from page 0 the host
|
t85 the host first re-aims word 0 at the staging copy, so a power loss
|
||||||
first re-aims word 0 at the staging copy, so a power loss mid-rewrite
|
mid-rewrite still resets into a loader; on the t13a the staging slot
|
||||||
still resets into a loader; on the tiny13s the staging slot carries the
|
carries the reset vector itself.
|
||||||
reset vector itself.
|
|
||||||
4. `J` enters the new resident, which restores the staging slot's saved
|
4. `J` enters the new resident, which restores the staging slot's saved
|
||||||
content (word 0 and the trampoline with it) and the state file is
|
content (word 0 and the trampoline with it) and the state file is
|
||||||
discarded.
|
discarded.
|
||||||
@@ -281,20 +144,14 @@ Every phase is idempotent and keyed off the actual flash state: re-running
|
|||||||
the same command after any interruption resumes and completes. The state
|
the same command after any interruption resumes and completes. The state
|
||||||
file carries the only bytes not recoverable from the device; if it is lost
|
file carries the only bytes not recoverable from the device; if it is lost
|
||||||
mid-update the update still completes, and the staging region is restored by
|
mid-update the update still completes, and the staging region is restored by
|
||||||
reflashing the application. A boot-sectioned mega needs its fuses for the
|
reflashing the application. The mega needs its fuses for the preflight
|
||||||
preflight (BOOTSZ gate, profile notes) — read from the device, or supplied
|
(BOOTSZ gate, profile notes) — read from the device, or supplied with
|
||||||
with `--assume-fuses` where reading is impossible (simulators); the
|
`--assume-fuses` where reading is impossible (simulators).
|
||||||
patched-vector chips need none.
|
|
||||||
|
|
||||||
## Host tool
|
## Host tool
|
||||||
|
|
||||||
`pureboot.py` — Python 3, standard library only. The port layer is the one
|
`pureboot.py` — Python 3, standard library only (termios drives any tty,
|
||||||
platform-specific part: termios drives any tty on POSIX (a USB adapter as
|
a USB adapter as well as a simavr pty):
|
||||||
well as a simavr pty), the Win32 serial API through `ctypes` drives a COM
|
|
||||||
port on Windows (`--port COM6`; the `\\.\` form for two-digit ports is
|
|
||||||
supplied by the tool). Opening the port asserts DTR and RTS on both, so a
|
|
||||||
board that wires DTR to reset gets its reset pulse and opens the activation
|
|
||||||
window by itself.
|
|
||||||
|
|
||||||
pureboot.py --port /dev/ttyUSB0 --baud 57600 \
|
pureboot.py --port /dev/ttyUSB0 --baud 57600 \
|
||||||
--info --fuses --flash app.hex
|
--info --fuses --flash app.hex
|
||||||
@@ -304,55 +161,25 @@ update, flash (erase / program / read / verify), EEPROM (erase / program /
|
|||||||
read / verify) — then the loader hands over to the application; `--stay`
|
read / verify) — then the loader hands over to the application; `--stay`
|
||||||
keeps the session alive instead, and a later invocation reconnects into it
|
keeps the session alive instead, and a later invocation reconnects into it
|
||||||
(the knock converges there too). `--flash` and `--eeprom` verify by
|
(the knock converges there too). `--flash` and `--eeprom` verify by
|
||||||
read-back unless `--no-verify`, and a flash page that reads back wrong is
|
read-back unless `--no-verify`; images are raw binary, or Intel HEX by
|
||||||
rewritten up to three times before the run stops — the loader leaves one
|
extension. `--force` overrides the refusable safety checks (today: flashing
|
||||||
recoverable way for a page to land wrong (see `W` above), and rewriting is
|
|
||||||
what clears it. `--verify-flash` only reports. Images are raw binary, or
|
|
||||||
Intel HEX by extension. `--force` overrides the refusable safety checks (today: flashing
|
|
||||||
application data into a mega's reset walk region).
|
application data into a mega's reset walk region).
|
||||||
|
|
||||||
Readouts come one fact per line: `--info` prints the decoded info block
|
|
||||||
field by field, `--fuses` each fuse byte on its own line — plus, on a
|
|
||||||
boot-sectioned mega, the decoded meaning (where the BOOTSZ section starts,
|
|
||||||
what BOOTRST does to reset). Transfers that take wire time — programming,
|
|
||||||
reading, erasing, verifying, the update phases — draw a transient progress
|
|
||||||
bar on stderr when it is a tty; logs and pipes see only the summary lines.
|
|
||||||
`-v`/`--verbose` adds the decisions as they happen: knock counts, the
|
|
||||||
programming plan (vector-surgery targets, skipped blank pages), update
|
|
||||||
state handling and per-phase page counts.
|
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
`tools/check.sh` runs every chip's workflow (`tools/check.sh --full` adds
|
Per chip preset, `ctest` runs:
|
||||||
the reflect-mode builds of libavr's spot set; `tools/make_presets.py`
|
|
||||||
regenerates the presets). Per chip preset, `ctest` runs:
|
|
||||||
|
|
||||||
- `pureboot.size` — the 510-byte (tinies) / 512-byte (mega) budget;
|
- `pureboot.size` — the 510-byte (tinies) / 512-byte (mega) budget;
|
||||||
- `pureboot_*.size` — the size matrix: the serial backends × the clock
|
|
||||||
ladder (1/8/16 MHz; the t13s' own RC menu), plus the USART1 build on the
|
|
||||||
x4 chips — every configuration axis that could move the image, each
|
|
||||||
variant against the same slot budget (pins are immediate operands and the
|
|
||||||
timeout is a constant: size-neutral);
|
|
||||||
- `pureboot.custom` (328P) — the configured-deployment acceptance test: the
|
|
||||||
1 MHz software-serial TX=PB1/RX=PB5 build from the configuration example
|
|
||||||
drives the full protocol suite through the runner's GPIO bridge, fixture
|
|
||||||
application included;
|
|
||||||
- `pureboot.usart1` (644A) — the same protocol suite over the second
|
|
||||||
hardware USART: instance selection is compile-checked everywhere, but
|
|
||||||
only a live session proves the loader polls the USART it claims;
|
|
||||||
- `pureboot.pi` — the position-independence lint: no absolute `jmp`/`call`
|
- `pureboot.pi` — the position-independence lint: no absolute `jmp`/`call`
|
||||||
in the image, the info block within its first 256 bytes;
|
in the image, the info block within its first 256 bytes;
|
||||||
- `pureboot.planner` — the host tool's pure logic: programming orders and
|
- `pureboot.planner` — the host tool's pure logic: programming orders and
|
||||||
their recovery properties, the surgery, the staging composition, the
|
their recovery properties, the surgery, the staging composition, the
|
||||||
boot-fuse decode, the update preflight's error/warning matrix over
|
boot-fuse decode, and the update preflight's error/warning matrix over
|
||||||
synthetic fuse bytes, and the repairing verify against a fake device — one
|
synthetic fuse bytes;
|
||||||
bad write repaired in a single rewrite, a page that never comes good
|
|
||||||
stopping after exactly three;
|
|
||||||
- `pureboot.protocol` — end to end against a simavr device
|
- `pureboot.protocol` — end to end against a simavr device
|
||||||
(`test/pureboot_device.c` — a hardware USART as a pty, or a cycle-timed
|
(`test/pureboot_device.c` — the mega's USART as a pty; on the tinies a
|
||||||
GPIO⇄pty bridge for a software-UART build, selected with `-l` to match
|
cycle-timed GPIO⇄pty bridge for the software UART, plus the SPM/NVM module
|
||||||
the loader's link; plus the SPM/NVM module simavr's tiny cores lack)
|
simavr's tiny cores lack) driven by the real host tool through
|
||||||
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
|
||||||
@@ -361,19 +188,7 @@ regenerates the presets). Per chip preset, `ctest` runs:
|
|||||||
- `pureboot.reloc` — the identical image installed one slot below the
|
- `pureboot.reloc` — the identical image installed one slot below the
|
||||||
resident serves the complete command set from there (the
|
resident serves the complete command set from there (the
|
||||||
position-independence acceptance test);
|
position-independence acceptance test);
|
||||||
- `pureboot.dirty` (328P) — entering the loader from a running application
|
|
||||||
with no reset between, over an SPM page buffer the fixture deliberately
|
|
||||||
dirtied: the case the loader declines to guard against. A bare verify must
|
|
||||||
see the corruption, the repairing verify must fix it in one rewrite, and a
|
|
||||||
plain verify afterwards must pass. On the boot-sectioned megas hardware
|
|
||||||
forbids the state outright (SPM runs only from the boot section, and reset
|
|
||||||
erases the buffer), but simavr dispatches SPM from anywhere — which is what
|
|
||||||
makes the path constructible at all;
|
|
||||||
- `pureboot.update` — the full `--update-loader` flow to a re-timed build,
|
- `pureboot.update` — the full `--update-loader` flow to a re-timed build,
|
||||||
then every power-fail phase: the device is killed mid-write, restarted
|
then every power-fail phase: the device is killed mid-write, restarted
|
||||||
from its flash dump, and a re-run must complete the update with the
|
from its flash dump, and a re-run must complete the update with the
|
||||||
application intact throughout.
|
application intact throughout.
|
||||||
|
|
||||||
`size`, `pi`, and `planner` are host logic and run anywhere; the
|
|
||||||
simulator-driven targets need simavr and a pty, so they are POSIX-only —
|
|
||||||
on Windows the tool is exercised against real hardware.
|
|
||||||
|
|||||||
@@ -16,10 +16,9 @@
|
|||||||
// resident — how pureboot updates itself, host-driven, with no other
|
// resident — how pureboot updates itself, host-driven, with no other
|
||||||
// firmware involved.
|
// firmware involved.
|
||||||
//
|
//
|
||||||
// Entry: reset lands in avr::startup::entry below (BOOTRST on the
|
// Entry: reset lands in avr::startup::entry below (BOOTRST on the mega; the
|
||||||
// boot-sectioned megas; the patched reset vector — or erased flash walking
|
// patched reset vector — or erased flash walking up into the loader — on the
|
||||||
// up into the loader — on the tinies and the boot-section-less m48s). A
|
// tinies). A watchdog reset hands straight to the application. Otherwise the
|
||||||
// watchdog reset hands straight to the application. Otherwise the
|
|
||||||
// host has one activation window per awaited knock byte ("pb"); an idle line
|
// host has one activation window per awaited knock byte ("pb"); an idle line
|
||||||
// boots the application. A session then stays in the command loop until 'J'
|
// boots the application. A session then stays in the command loop until 'J'
|
||||||
// jumps away or the chip resets.
|
// jumps away or the chip resets.
|
||||||
@@ -38,51 +37,38 @@ constexpr auto off = avr::irq::guard_policy::unused;
|
|||||||
|
|
||||||
constexpr std::uint8_t ack = '+';
|
constexpr std::uint8_t ack = '+';
|
||||||
|
|
||||||
// Per-deployment personality, passed in by the build — pureboot_add_loader()
|
// Per-chip personality, from the chip database: the clocks the dogfood
|
||||||
// (the CMake function next to this file) resolves the defaults: the clock the
|
// boards run (16 MHz crystal on the mega, calibrated RC on the tinies) and
|
||||||
// board actually runs, the wire baud, the serial backend and its pins. The
|
// the device signature (compile-time data — the tiny13A cannot even read its
|
||||||
// device signature needs no configuring — it comes from the chip database
|
// signature row 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)
|
|
||||||
#error \
|
|
||||||
"PUREBOOT_CLOCK_HZ and PUREBOOT_BAUD select this build's clock and baud — create loader targets with pureboot_add_loader() (README.md)"
|
|
||||||
#endif
|
|
||||||
|
|
||||||
using dev = avr::device<{.clock = avr::hertz_t{PUREBOOT_CLOCK_HZ}}>;
|
|
||||||
constexpr avr::baud_t wire_baud{PUREBOOT_BAUD};
|
|
||||||
|
|
||||||
// The watchdog reset flag's home: MCUSR, or the classic megas' MCUCSR.
|
|
||||||
consteval std::int16_t wdrf_field()
|
|
||||||
{
|
{
|
||||||
auto reg = std::string_view{avr::hw::db.regs[static_cast<std::size_t>(avr::power::detail::reset_reg())].name};
|
if (avr::hw::db.name == "ATtiny13A")
|
||||||
return avr::hw::db.field_index(reg, "WDRF");
|
return 9.6_MHz;
|
||||||
|
if (avr::hw::db.name == "ATtiny85")
|
||||||
|
return 8_MHz;
|
||||||
|
return 16_MHz;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Geometry: the resident loader owns the top slot of flash — 512 bytes,
|
consteval std::array<std::uint8_t, 3> signature()
|
||||||
// except on the >64 KiB chips whose own smallest boot sector is 1 KiB (the
|
{
|
||||||
// 1284s): there the slot is 1 KiB, matching the hardware boundary the
|
if (avr::hw::db.name == "ATtiny13A")
|
||||||
// 512-byte figure comes from everywhere else. The word below the slot is
|
return {0x1e, 0x90, 0x07};
|
||||||
// the trampoline (the application's relocated reset vector) on chips
|
if (avr::hw::db.name == "ATtiny85")
|
||||||
// without a hardware boot section — the tinies and the m48s, whose SPM
|
return {0x1e, 0x93, 0x0b};
|
||||||
// runs from anywhere (Atmel-8271 §26). A boot section also means the CPU
|
return {0x1e, 0x95, 0x0f};
|
||||||
// runs on while the RWW section programs; everywhere else it halts through
|
}
|
||||||
// the operation.
|
|
||||||
constexpr std::uint16_t slot_bytes = spm::flash_bytes > 65536 ? 1024 : 512;
|
|
||||||
constexpr std::uint32_t base = spm::flash_bytes - slot_bytes;
|
|
||||||
constexpr std::uint16_t page = spm::page_bytes;
|
|
||||||
constexpr bool boot_section = avr::hw::curated::has_boot_section();
|
|
||||||
|
|
||||||
// Past 64 KiB a byte address no longer fits the wire's 16 bits, so on the
|
using dev = avr::device<{.clock = clock()}>;
|
||||||
// large chips every flash address on the wire — and all slot arithmetic —
|
|
||||||
// is a word address instead ('J' always was one). A slot spans the same
|
// Geometry: the resident loader owns the top 512 bytes of flash; the word
|
||||||
// wire-high-byte pair in either unit (512 B = 2 x 256 bytes, 1 KiB =
|
// below it is the trampoline (the application's relocated reset vector) on
|
||||||
// 2 x 256 words), so the slot index is the high byte with its low bit
|
// chips without a hardware boot section. The RWWSRE bit marks a separate
|
||||||
// dropped everywhere.
|
// boot section — on classic AVR the two capabilities coincide.
|
||||||
constexpr bool word_flash = spm::flash_bytes > 65536;
|
constexpr std::uint16_t boot_bytes = 512;
|
||||||
constexpr std::uint16_t wire_base =
|
constexpr std::uint16_t base = static_cast<std::uint16_t>(spm::flash_bytes - boot_bytes);
|
||||||
word_flash ? static_cast<std::uint16_t>(base / 2) : static_cast<std::uint16_t>(base);
|
constexpr std::uint16_t page = spm::page_bytes;
|
||||||
constexpr std::uint16_t wire_page_mask = word_flash ? (page / 2 - 1) : (page - 1);
|
constexpr bool boot_section = avr::hw::db.field_index("SPMCSR", "RWWSRE") >= 0;
|
||||||
|
|
||||||
// The activation window, in seconds, is a compile-time constant (the build
|
// The activation window, in seconds, is a compile-time constant (the build
|
||||||
// may override it): the whole EEPROM belongs to the application, and
|
// may override it): the whole EEPROM belongs to the application, and
|
||||||
@@ -92,55 +78,52 @@ constexpr std::uint16_t wire_page_mask = word_flash ? (page / 2 - 1) : (page - 1
|
|||||||
#endif
|
#endif
|
||||||
constexpr std::uint8_t timeout_seconds = PUREBOOT_TIMEOUT;
|
constexpr std::uint8_t timeout_seconds = PUREBOOT_TIMEOUT;
|
||||||
|
|
||||||
// The 12-byte info block the host reads with the 'b' command, flash-resident
|
// The 12-byte info block the host reads with the 'b' command; flash-resident
|
||||||
// through flash_table (there is no crt to copy a .data image, and its storage
|
// (there is no crt to copy a .data image).
|
||||||
// carries the word alignment 'b' needs to halve the address on the large
|
inline constexpr std::array<std::uint8_t, 12> info_data = {
|
||||||
// chips). The page byte is the wire count convention: 0 means 256.
|
|
||||||
inline constexpr avr::flash_table<std::array<std::uint8_t, 12>{
|
|
||||||
'P',
|
'P',
|
||||||
'B',
|
'B',
|
||||||
1, // magic, protocol version
|
1, // magic, protocol version
|
||||||
avr::hw::db.signature[0],
|
signature()[0],
|
||||||
avr::hw::db.signature[1],
|
signature()[1],
|
||||||
avr::hw::db.signature[2],
|
signature()[2],
|
||||||
static_cast<std::uint8_t>(page),
|
static_cast<std::uint8_t>(page),
|
||||||
wire_base & 0xff,
|
base & 0xff,
|
||||||
wire_base >> 8, // app flash ends here; resident loader base (a word address on large chips)
|
base >> 8, // app flash ends here; resident loader base
|
||||||
avr::hw::db.mem.eeprom_size & 0xff,
|
avr::hw::db.mem.eeprom_size & 0xff,
|
||||||
avr::hw::db.mem.eeprom_size >> 8,
|
avr::hw::db.mem.eeprom_size >> 8,
|
||||||
// bit 0: host must patch the reset vector (no hardware boot section);
|
boot_section ? 0 : 1, // bit 0: host must patch the reset vector (no hardware boot section)
|
||||||
// bit 1: flash wire addresses are word addresses
|
};
|
||||||
static_cast<std::uint8_t>((boot_section ? 0 : 1) | (word_flash ? 2 : 0)),
|
using info = avr::flash_table<info_data>;
|
||||||
}>
|
|
||||||
info_data;
|
|
||||||
|
|
||||||
// The serial link. PUREBOOT_USART forces a hardware USART instance,
|
// The serial link: the hardware USART where the chip has one, the polled
|
||||||
// PUREBOOT_SOFT_SERIAL the polled software UART (no vector — the table
|
// software UART (no vector — the table belongs to the application) on PB0/PB1
|
||||||
// belongs to the application) on PUREBOOT_RX/PUREBOOT_TX; with neither, the
|
// elsewhere. Both are class templates on the clock so only the selected
|
||||||
// chip's first USART where it has one and the software UART elsewhere. Both
|
// backend is ever instantiated. pending() is the cheap line test the
|
||||||
// are class templates on the clock so only the selected backend is ever
|
// activation window polls; rx() then picks the byte up; drain() holds until
|
||||||
// instantiated. pending() is the cheap line test the activation window
|
// the last transmitted frame is fully on the wire (the jump hand-over must
|
||||||
// polls; rx() then picks the byte up; drain() holds until the last
|
// not let the target's re-init clip the ack).
|
||||||
// transmitted frame is fully on the wire (the jump hand-over must not let
|
template <avr::hertz_t C>
|
||||||
// the target's re-init clip the ack).
|
consteval std::int16_t rxc_field()
|
||||||
#if defined(PUREBOOT_SOFT_SERIAL) && defined(PUREBOOT_USART)
|
{
|
||||||
#error "PUREBOOT_SOFT_SERIAL and PUREBOOT_USART select opposing serial backends"
|
return avr::hw::db.field_index("UCSR0A", "RXC0");
|
||||||
#endif
|
}
|
||||||
#if !defined(PUREBOOT_RX)
|
|
||||||
#define PUREBOOT_RX pb0
|
template <avr::hertz_t C>
|
||||||
#endif
|
consteval std::int16_t txc_field()
|
||||||
#if !defined(PUREBOOT_TX)
|
{
|
||||||
#define PUREBOOT_TX pb1
|
return avr::hw::db.field_index("UCSR0A", "TXC0");
|
||||||
#endif
|
}
|
||||||
#if defined(PUREBOOT_USART)
|
|
||||||
constexpr char usart_digit = '0' + PUREBOOT_USART;
|
template <avr::hertz_t C>
|
||||||
#else
|
consteval std::int16_t status_reg()
|
||||||
constexpr char usart_digit = '0';
|
{
|
||||||
#endif
|
return avr::hw::db.reg_index("UCSR0A");
|
||||||
|
}
|
||||||
|
|
||||||
template <avr::hertz_t C>
|
template <avr::hertz_t C>
|
||||||
struct hardware_link {
|
struct hardware_link {
|
||||||
using uart = avr::uart::usart<usart_digit, C, {.baud = wire_baud, .max_baud_error = 2.5_pct}>;
|
using uart = avr::uart::usart0<C, {.baud = 115200_Bd, .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).
|
||||||
@@ -153,7 +136,7 @@ struct hardware_link {
|
|||||||
|
|
||||||
static bool pending()
|
static bool pending()
|
||||||
{
|
{
|
||||||
return uart::rx_ready();
|
return avr::hw::field_impl<rxc_field<C>()>::test();
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::uint8_t rx()
|
static std::uint8_t rx()
|
||||||
@@ -168,14 +151,22 @@ struct hardware_link {
|
|||||||
|
|
||||||
static void drain()
|
static void drain()
|
||||||
{
|
{
|
||||||
uart::drain();
|
// write() leaves the byte draining behind it. Clear a stale TXC0
|
||||||
|
// first (W1C by writing the sampled status back — the store a hand
|
||||||
|
// assembler writes, keeping U2X0), then wait for the fresh
|
||||||
|
// completion; with a byte still ahead in the shifter TXC0 cannot
|
||||||
|
// re-set until the last pending byte has fully left.
|
||||||
|
using status = avr::hw::reg_impl<status_reg<C>()>;
|
||||||
|
status::write(status::read());
|
||||||
|
while (!avr::hw::field_impl<txc_field<C>()>::test()) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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::PUREBOOT_RX, wire_baud>;
|
using rx_t = avr::uart::software_rx_polled<C, avr::pb0, 57600_Bd>;
|
||||||
using tx_t = avr::uart::software_tx<C, avr::PUREBOOT_TX, wire_baud>;
|
using tx_t = avr::uart::software_tx<C, avr::pb1, 57600_Bd>;
|
||||||
|
|
||||||
// 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).
|
||||||
@@ -188,7 +179,7 @@ struct software_link {
|
|||||||
|
|
||||||
static bool pending()
|
static bool pending()
|
||||||
{
|
{
|
||||||
return rx_t::start_pending();
|
return !avr::io::input<avr::pb0>::read(); // a start bit has begun
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::uint8_t rx()
|
static std::uint8_t rx()
|
||||||
@@ -207,15 +198,7 @@ struct software_link {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
#if defined(PUREBOOT_USART)
|
using link = std::conditional_t<avr::hw::db.has_reg("UDR0"), hardware_link<dev::clock>, software_link<dev::clock>>;
|
||||||
static_assert(avr::uart::has_usart<usart_digit>(), "PUREBOOT_USART selects a hardware USART this chip does not have");
|
|
||||||
using link = hardware_link<dev::clock>;
|
|
||||||
#elif defined(PUREBOOT_SOFT_SERIAL)
|
|
||||||
using link = software_link<dev::clock>;
|
|
||||||
#else
|
|
||||||
using link =
|
|
||||||
std::conditional_t<avr::uart::has_usart<usart_digit>(), hardware_link<dev::clock>, software_link<dev::clock>>;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// The application's entry, an absolute address the linker pins (--defsym in
|
// The application's entry, an absolute address the linker pins (--defsym in
|
||||||
// CMakeLists.txt): 0x0000 on the mega (word 0 stays the application's own
|
// CMakeLists.txt): 0x0000 on the mega (word 0 stays the application's own
|
||||||
@@ -264,53 +247,27 @@ std::uint8_t rx_deadline()
|
|||||||
return link::rx();
|
return link::rx();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inlined into its call sites: reading two bytes across a call otherwise
|
std::uint16_t rx16()
|
||||||
// strands the first in a call-saved register the caller must push/pop; folded
|
|
||||||
// into the (noreturn) command loop that cost disappears.
|
|
||||||
[[gnu::always_inline]] inline std::uint16_t rx16()
|
|
||||||
{
|
{
|
||||||
std::uint16_t low = link::rx();
|
std::uint16_t low = link::rx();
|
||||||
return static_cast<std::uint16_t>(low | (link::rx() << 8));
|
return static_cast<std::uint16_t>(low | (link::rx() << 8));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const std::uint8_t *flash_ptr(std::uint16_t address)
|
||||||
|
{
|
||||||
|
return reinterpret_cast<const std::uint8_t *>(address);
|
||||||
|
}
|
||||||
|
|
||||||
// The streamers take the count in the wire's 8-bit form: 0 means 256.
|
// The streamers take the count in the wire's 8-bit form: 0 means 256.
|
||||||
//
|
// send_flash stays out of line: its two callers ('b' and 'R') otherwise each
|
||||||
// Two functions, because they want opposite placement and placement is an
|
// inline a private copy of the loop.
|
||||||
// attribute: the byte-addressed loop is small enough to inline into both
|
[[gnu::noinline]] void send_flash(std::uint16_t address, std::uint8_t count)
|
||||||
// callers, the word-addressed one stays out of line but flattened — a call to
|
|
||||||
// the transmit inside it would strand the 24-bit cursor in callee-saved
|
|
||||||
// registers. `word_flash` picks at the call site.
|
|
||||||
[[maybe_unused, gnu::always_inline]] inline void send_flash_near(std::uint16_t address, std::uint8_t count)
|
|
||||||
{
|
{
|
||||||
do
|
do
|
||||||
link::tx(avr::flash_load(reinterpret_cast<const std::uint8_t *>(address++)));
|
link::tx(avr::flash_load(flash_ptr(address++)));
|
||||||
while (--count);
|
while (--count);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The 24-bit cursor as the machine holds it: the RAMPZ byte and a 16-bit Z,
|
|
||||||
// carried explicitly (the reassembled 32-bit address folds away inside the
|
|
||||||
// inlined far load).
|
|
||||||
[[maybe_unused, gnu::flatten, gnu::noinline]] void send_flash_far(std::uint16_t address, std::uint8_t count)
|
|
||||||
{
|
|
||||||
std::uint8_t rampz = static_cast<std::uint8_t>(address >> 15);
|
|
||||||
std::uint16_t z = static_cast<std::uint16_t>(address << 1);
|
|
||||||
do {
|
|
||||||
link::tx(avr::flash_load_far<std::uint8_t>((static_cast<std::uint32_t>(rampz) << 16) | z));
|
|
||||||
// The protocol never reads across 64 KiB, but carrying the wrap is
|
|
||||||
// smaller than the flat 32-bit cursor GCC builds without it.
|
|
||||||
if (++z == 0)
|
|
||||||
++rampz;
|
|
||||||
} while (--count);
|
|
||||||
}
|
|
||||||
|
|
||||||
[[gnu::always_inline]] inline void send_flash(std::uint16_t address, std::uint8_t count)
|
|
||||||
{
|
|
||||||
if constexpr (word_flash)
|
|
||||||
send_flash_far(address, count);
|
|
||||||
else
|
|
||||||
send_flash_near(address, count);
|
|
||||||
}
|
|
||||||
|
|
||||||
void send_eeprom(std::uint16_t address, std::uint8_t count)
|
void send_eeprom(std::uint16_t address, std::uint8_t count)
|
||||||
{
|
{
|
||||||
do
|
do
|
||||||
@@ -336,43 +293,20 @@ void store_eeprom(std::uint16_t address, std::uint8_t count)
|
|||||||
// itself. `slot_high` is the high byte of that running slot's base (run()
|
// itself. `slot_high` is the high byte of that running slot's base (run()
|
||||||
// derives it); a broken host thus cannot brick the running loader, and a
|
// derives it); a broken host thus cannot brick the running loader, and a
|
||||||
// copy flashed one slot lower may rewrite the slot above it — how pureboot
|
// copy flashed one slot lower may rewrite the slot above it — how pureboot
|
||||||
// updates itself.
|
// updates itself. On the mega the RWW section is re-enabled so reads work
|
||||||
void program_flash(std::uint16_t wire_address, std::uint8_t slot_high)
|
// immediately.
|
||||||
|
void program_flash(std::uint16_t address, std::uint8_t slot_high)
|
||||||
{
|
{
|
||||||
// No discard before the fill: the buffer is write-once per word
|
// A buffer word cannot be loaded twice without an erase (§26.2.1), so a
|
||||||
// (§26.2.1), so filling over one a refused page or an application left
|
// refused page's drained data must not linger for the next write:
|
||||||
// dirty programs stale words — but a page write auto-erases the buffer
|
// discard the buffer up front — CTPB on the tinies; on the mega writing
|
||||||
// (§26.2.1; §19.2 on the tinies), so that write clears the condition and
|
// RWWSRE aborts a pending load (§26.2.2).
|
||||||
// the host's read-back rewrites the page.
|
if constexpr (boot_section)
|
||||||
|
spm::rww_enable<off>();
|
||||||
// One induction either way. On the byte-addressed chips the wire address
|
else
|
||||||
// itself walks the page (aligned, so the offset bits wrap to zero); on
|
spm::clear_buffer<off>();
|
||||||
// the word-addressed large chips the wire word address becomes a 32-bit
|
// The address is the loop's only state: pages are aligned, so the walk
|
||||||
// byte cursor once, and their 256-byte page makes its low byte the whole
|
// ends when the offset bits wrap back to zero.
|
||||||
// in-page offset. The slot index is one high byte of the wire address —
|
|
||||||
// two values on byte-addressed chips (the & ~1), bits 16:9 re-packed on
|
|
||||||
// the large ones.
|
|
||||||
spm::flash_address_t address;
|
|
||||||
std::uint8_t page_high;
|
|
||||||
if constexpr (word_flash) {
|
|
||||||
// Pages are aligned, so one page never crosses a 64 KiB boundary:
|
|
||||||
// RAMPZ is a per-page constant and the fill cursor is a 16-bit Z
|
|
||||||
// whose low byte is the whole in-page offset (256-byte pages). The
|
|
||||||
// slot index is simply the wire word address's high byte.
|
|
||||||
const std::uint8_t rampz = static_cast<std::uint8_t>(wire_address >> 15);
|
|
||||||
const std::uint16_t z0 = static_cast<std::uint16_t>(wire_address << 1);
|
|
||||||
std::uint16_t z = z0;
|
|
||||||
do {
|
|
||||||
std::uint8_t low = link::rx();
|
|
||||||
std::uint8_t high = link::rx();
|
|
||||||
spm::fill<off>((static_cast<spm::flash_address_t>(rampz) << 16) | z,
|
|
||||||
static_cast<std::uint16_t>(low | (high << 8)));
|
|
||||||
z += 2;
|
|
||||||
} while (static_cast<std::uint8_t>(z));
|
|
||||||
address = (static_cast<spm::flash_address_t>(rampz) << 16) | z0;
|
|
||||||
page_high = static_cast<std::uint8_t>(wire_address >> 8) & 0xfe;
|
|
||||||
} else {
|
|
||||||
address = static_cast<spm::flash_address_t>(wire_address);
|
|
||||||
do {
|
do {
|
||||||
std::uint8_t low = link::rx();
|
std::uint8_t low = link::rx();
|
||||||
std::uint8_t high = link::rx();
|
std::uint8_t high = link::rx();
|
||||||
@@ -380,25 +314,20 @@ void program_flash(std::uint16_t wire_address, std::uint8_t slot_high)
|
|||||||
address += 2;
|
address += 2;
|
||||||
} while (static_cast<std::uint8_t>(address) & (page - 1));
|
} while (static_cast<std::uint8_t>(address) & (page - 1));
|
||||||
address -= 2; // back inside the page — erase and write ignore the word bits
|
address -= 2; // back inside the page — erase and write ignore the word bits
|
||||||
page_high = static_cast<std::uint8_t>(address >> 8) & 0xfe;
|
const std::uint8_t page_high = static_cast<std::uint8_t>(address >> 8) & 0xfe;
|
||||||
}
|
|
||||||
if (page_high != slot_high) {
|
if (page_high != slot_high) {
|
||||||
// The tinies and the m48s halt the CPU through the erase and the
|
// The tinies halt the CPU through the erase and the write, so only
|
||||||
// write, so only the boot-sectioned megas — running on while their
|
// the mega — running on while its RWW section programs — waits.
|
||||||
// RWW section programs — wait.
|
|
||||||
spm::erase_page<off>(address);
|
spm::erase_page<off>(address);
|
||||||
if constexpr (boot_section)
|
if constexpr (boot_section)
|
||||||
spm::wait();
|
spm::wait();
|
||||||
spm::write_page<off>(address);
|
spm::write_page<off>(address);
|
||||||
if constexpr (boot_section)
|
if constexpr (boot_section) {
|
||||||
spm::wait();
|
spm::wait();
|
||||||
}
|
|
||||||
// The megas program with their RWW section disabled; reads need it back
|
|
||||||
// on. The same store discards the buffer (§26.2.2), so they never meet
|
|
||||||
// the stale-word case above. boot_section implies an RWW section.
|
|
||||||
if constexpr (boot_section)
|
|
||||||
spm::rww_enable<off>();
|
spm::rww_enable<off>();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// The four fuse/lock bytes in the hardware's own Z order: low, lock,
|
// The four fuse/lock bytes in the hardware's own Z order: low, lock,
|
||||||
// extended, high. Writing fuses is not a thing self-programming can do on
|
// extended, high. Writing fuses is not a thing self-programming can do on
|
||||||
@@ -415,23 +344,18 @@ void send_fuses()
|
|||||||
{
|
{
|
||||||
// A watchdog reset belongs to the application (whose watchdog stays
|
// A watchdog reset belongs to the application (whose watchdog stays
|
||||||
// forced on until it clears WDRF) — no activation window in its way.
|
// forced on until it clears WDRF) — no activation window in its way.
|
||||||
// The flag register is MCUSR, or the classic megas' MCUCSR.
|
if (avr::hw::mcusr::wdrf.test())
|
||||||
if (avr::hw::field_impl<wdrf_field()>::test())
|
|
||||||
run_app();
|
run_app();
|
||||||
|
|
||||||
link::init();
|
link::init();
|
||||||
|
|
||||||
// The high byte of the 512-byte-aligned base this copy runs at: the
|
// The high byte of the 512-byte-aligned base this copy runs at: the word
|
||||||
// return address is a word address, whose high byte is the 256-word slot
|
// return address's high byte is the byte address >> 9 (the slot index),
|
||||||
// index — on byte-addressed chips doubled back into byte terms.
|
// doubled back into address terms. program_flash refuses this one slot
|
||||||
// program_flash refuses this one slot and the info block is addressed
|
// and the info block is addressed from it, so both follow wherever the
|
||||||
// from it, so both follow wherever the code was flashed. The high byte is
|
// code was flashed.
|
||||||
// spelled as byteswap's low byte: the builtin's value is itself built by
|
const std::uint8_t slot_high =
|
||||||
// swapping the two stacked bytes, and the double swap folds to the single
|
static_cast<std::uint8_t>((reinterpret_cast<std::uint16_t>(__builtin_return_address(0)) >> 8) << 1);
|
||||||
// byte pick a hand assembler writes — `>> 8` leaves the swap materialized.
|
|
||||||
const std::uint16_t ra_words = reinterpret_cast<std::uint16_t>(__builtin_return_address(0));
|
|
||||||
const std::uint8_t ra_high = static_cast<std::uint8_t>(std::byteswap(ra_words));
|
|
||||||
const std::uint8_t slot_high = word_flash ? ra_high & 0xfe : static_cast<std::uint8_t>(ra_high << 1);
|
|
||||||
|
|
||||||
// The knock: 'p' then 'b', each under a fresh window; any other byte is
|
// 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.
|
||||||
@@ -448,15 +372,11 @@ void send_fuses()
|
|||||||
case 'b': { // info block, read relative to the running slot
|
case 'b': { // info block, read relative to the running slot
|
||||||
// The block sits in the image's first 256 bytes (the build lint
|
// The block sits in the image's first 256 bytes (the build lint
|
||||||
// asserts it), and slots are 512-aligned — so the low byte of its
|
// 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 is its offset in any slot, and the high byte of
|
||||||
// chips) is its offset in any slot, and the high byte of its
|
// its runtime address is the running slot's. Built as a byte
|
||||||
// runtime address is the running slot's. Composed from the two
|
// pair so no absolute 16-bit address is ever materialized.
|
||||||
// bytes — the high half is runtime data, so no absolute address
|
const std::uint8_t low = static_cast<std::uint8_t>(reinterpret_cast<std::uint16_t>(info::storage.data()));
|
||||||
// is ever materialized.
|
send_flash(std::bit_cast<std::uint16_t>(std::array{low, slot_high}), info::size());
|
||||||
const auto link_low = reinterpret_cast<std::uint16_t>(info_data.storage.data());
|
|
||||||
const std::uint8_t low =
|
|
||||||
word_flash ? static_cast<std::uint8_t>(link_low >> 1) : static_cast<std::uint8_t>(link_low);
|
|
||||||
send_flash(static_cast<std::uint16_t>(low | (slot_high << 8)), static_cast<std::uint8_t>(info_data.size()));
|
|
||||||
break;
|
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
|
||||||
|
|||||||
@@ -16,85 +16,32 @@ the resident slot, and restores what the staging slot held — resumable at
|
|||||||
every phase from the flash state plus a host-side state file carrying the
|
every phase from the flash state plus a host-side state file carrying the
|
||||||
saved bytes.
|
saved bytes.
|
||||||
|
|
||||||
Python standard library only; the serial port is driven with termios on POSIX
|
Python standard library only; the serial port is driven with termios, so any
|
||||||
and the Win32 serial API (through ctypes) on Windows, so any tty or COM port
|
tty works — a USB adapter as well as a simavr pty.
|
||||||
works — a USB adapter as well as a simavr pty.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
|
|
||||||
if os.name == "nt":
|
|
||||||
import ctypes
|
|
||||||
from ctypes import wintypes
|
|
||||||
else:
|
|
||||||
import select
|
import select
|
||||||
|
import sys
|
||||||
import termios
|
import termios
|
||||||
|
import time
|
||||||
|
|
||||||
PROMPT = b"+"
|
PROMPT = b"+"
|
||||||
PROTOCOL_VERSION = 1
|
PROTOCOL_VERSION = 1
|
||||||
SLOT = 512 # the loader slot on byte-addressed chips; word-addressed ones (>64 KiB) use 1 KiB — their own smallest boot sector
|
SLOT = 512 # the loader slot size; also the self-update staging distance
|
||||||
RETRIES = 3 # rewrites of a page that reads back wrong, before the run stops
|
|
||||||
|
|
||||||
VERBOSE = False
|
|
||||||
|
|
||||||
|
|
||||||
def verbose(message):
|
|
||||||
"""Detail printed only under --verbose: decisions and derived facts, not
|
|
||||||
per-byte chatter — the progress bar carries the bulk transfers."""
|
|
||||||
if VERBOSE:
|
|
||||||
print(f" {message}")
|
|
||||||
|
|
||||||
|
|
||||||
class Error(Exception):
|
class Error(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Progress:
|
|
||||||
"""A transient in-place bar on stderr for the operations that take wire
|
|
||||||
time. Drawn only when stderr is a tty — logs, pipes and the test harness
|
|
||||||
see nothing — and erased once done; the summary line each operation
|
|
||||||
prints afterwards is the persistent record. A zero total (or no label)
|
|
||||||
disables it, so callers can pass one through unconditionally."""
|
|
||||||
|
|
||||||
def __init__(self, label, total, unit="pages"):
|
|
||||||
self.label, self.total, self.unit = label, total, unit
|
|
||||||
self.done = 0
|
|
||||||
self.width = 0
|
|
||||||
self.live = bool(label) and total > 0 and sys.stderr.isatty()
|
|
||||||
self._draw()
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(self, *exc):
|
|
||||||
if self.live:
|
|
||||||
sys.stderr.write("\r" + " " * self.width + "\r")
|
|
||||||
sys.stderr.flush()
|
|
||||||
|
|
||||||
def step(self, n=1):
|
|
||||||
self.done += n
|
|
||||||
self._draw()
|
|
||||||
|
|
||||||
def _draw(self):
|
|
||||||
if not self.live:
|
|
||||||
return
|
|
||||||
bar = 24 * self.done // self.total
|
|
||||||
line = (f"{self.label:<16} [{'#' * bar}{'-' * (24 - bar)}] "
|
|
||||||
f"{100 * self.done // self.total:3d}% {self.done}/{self.total} {self.unit}")
|
|
||||||
self.width = max(self.width, len(line))
|
|
||||||
sys.stderr.write("\r" + line)
|
|
||||||
sys.stderr.flush()
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------- serial ---
|
# ---------------------------------------------------------------- serial ---
|
||||||
|
|
||||||
|
|
||||||
class PosixPort:
|
class Port:
|
||||||
"""A raw serial port with deadline-based reads, over termios."""
|
"""A raw serial port with deadline-based reads."""
|
||||||
|
|
||||||
def __init__(self, path, baud):
|
def __init__(self, path, baud):
|
||||||
self.fd = os.open(path, os.O_RDWR | os.O_NOCTTY)
|
self.fd = os.open(path, os.O_RDWR | os.O_NOCTTY)
|
||||||
@@ -139,171 +86,6 @@ class PosixPort:
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
if os.name == "nt":
|
|
||||||
# The same port, over the Win32 serial API — kernel32 through ctypes, so
|
|
||||||
# the tool stays standard-library only. Timeouts live in the driver
|
|
||||||
# (COMMTIMEOUTS) rather than in a readiness call: Windows has no select()
|
|
||||||
# for a COM handle, so each read asks the driver for its own deadline.
|
|
||||||
|
|
||||||
_GENERIC_READ, _GENERIC_WRITE = 0x80000000, 0x40000000
|
|
||||||
_OPEN_EXISTING, _PURGE_RXCLEAR = 3, 0x0008
|
|
||||||
_INVALID_HANDLE = wintypes.HANDLE(-1).value
|
|
||||||
# A gap this long ends a read_available(): longer than the coalescing a
|
|
||||||
# USB-serial adapter's latency timer imposes (16 ms on FTDI parts), so a
|
|
||||||
# burst is not split, short enough to stay responsive.
|
|
||||||
_GAP_MS = 30
|
|
||||||
|
|
||||||
class _DCB(ctypes.Structure):
|
|
||||||
_fields_ = [
|
|
||||||
("DCBlength", wintypes.DWORD),
|
|
||||||
("BaudRate", wintypes.DWORD),
|
|
||||||
("fBits", wintypes.DWORD), # the packed flag bitfield, set below
|
|
||||||
("wReserved", wintypes.WORD),
|
|
||||||
("XonLim", wintypes.WORD),
|
|
||||||
("XoffLim", wintypes.WORD),
|
|
||||||
("ByteSize", wintypes.BYTE),
|
|
||||||
("Parity", wintypes.BYTE),
|
|
||||||
("StopBits", wintypes.BYTE),
|
|
||||||
("XonChar", ctypes.c_char),
|
|
||||||
("XoffChar", ctypes.c_char),
|
|
||||||
("ErrorChar", ctypes.c_char),
|
|
||||||
("EofChar", ctypes.c_char),
|
|
||||||
("EvtChar", ctypes.c_char),
|
|
||||||
("wReserved1", wintypes.WORD),
|
|
||||||
]
|
|
||||||
|
|
||||||
class _COMMTIMEOUTS(ctypes.Structure):
|
|
||||||
_fields_ = [
|
|
||||||
("ReadIntervalTimeout", wintypes.DWORD),
|
|
||||||
("ReadTotalTimeoutMultiplier", wintypes.DWORD),
|
|
||||||
("ReadTotalTimeoutConstant", wintypes.DWORD),
|
|
||||||
("WriteTotalTimeoutMultiplier", wintypes.DWORD),
|
|
||||||
("WriteTotalTimeoutConstant", wintypes.DWORD),
|
|
||||||
]
|
|
||||||
|
|
||||||
_k32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
||||||
_LPDWORD = ctypes.POINTER(wintypes.DWORD)
|
|
||||||
# Declared, not inferred: a HANDLE is a pointer, and a defaulted int
|
|
||||||
# return would truncate it on 64-bit.
|
|
||||||
_k32.CreateFileW.restype = wintypes.HANDLE
|
|
||||||
_k32.CreateFileW.argtypes = [wintypes.LPCWSTR, wintypes.DWORD, wintypes.DWORD,
|
|
||||||
wintypes.LPVOID, wintypes.DWORD, wintypes.DWORD, wintypes.HANDLE]
|
|
||||||
_k32.ReadFile.argtypes = [wintypes.HANDLE, wintypes.LPVOID, wintypes.DWORD, _LPDWORD, wintypes.LPVOID]
|
|
||||||
_k32.WriteFile.argtypes = [wintypes.HANDLE, wintypes.LPCVOID, wintypes.DWORD, _LPDWORD, wintypes.LPVOID]
|
|
||||||
_k32.GetCommState.argtypes = [wintypes.HANDLE, ctypes.POINTER(_DCB)]
|
|
||||||
_k32.SetCommState.argtypes = [wintypes.HANDLE, ctypes.POINTER(_DCB)]
|
|
||||||
_k32.SetCommTimeouts.argtypes = [wintypes.HANDLE, ctypes.POINTER(_COMMTIMEOUTS)]
|
|
||||||
_k32.PurgeComm.argtypes = [wintypes.HANDLE, wintypes.DWORD]
|
|
||||||
_k32.CloseHandle.argtypes = [wintypes.HANDLE]
|
|
||||||
|
|
||||||
def _fail(what):
|
|
||||||
code = ctypes.get_last_error()
|
|
||||||
raise Error(f"{what}: {ctypes.FormatError(code).strip()} (Windows error {code})")
|
|
||||||
|
|
||||||
class WindowsPort:
|
|
||||||
"""A raw serial port with deadline-based reads, over Win32."""
|
|
||||||
|
|
||||||
def __init__(self, path, baud):
|
|
||||||
# Win32 takes the rate as a plain integer, so unlike termios any
|
|
||||||
# rate the hardware can divide down to is available — but a driver
|
|
||||||
# may also accept one it cannot produce (an FT232R takes a baud of
|
|
||||||
# 3, reports it back, and goes on using the previous divisor).
|
|
||||||
# Only obvious nonsense is refusable; the rest is the driver's word.
|
|
||||||
if baud < 50:
|
|
||||||
raise Error(f"unsupported baud rate {baud}")
|
|
||||||
# \\.\COM6: the device-namespace form. A bare COMn resolves only
|
|
||||||
# for n < 10, and double-digit ports are routine on Windows.
|
|
||||||
if path.lower().startswith("com") and path[3:].isdigit():
|
|
||||||
path = rf"\\.\{path}"
|
|
||||||
self.handle = _k32.CreateFileW(
|
|
||||||
path, _GENERIC_READ | _GENERIC_WRITE, 0, None, _OPEN_EXISTING, 0, None
|
|
||||||
)
|
|
||||||
if self.handle == _INVALID_HANDLE:
|
|
||||||
_fail(f"cannot open {path}")
|
|
||||||
self.timeouts = None
|
|
||||||
try:
|
|
||||||
dcb = _DCB()
|
|
||||||
dcb.DCBlength = ctypes.sizeof(_DCB)
|
|
||||||
if not _k32.GetCommState(self.handle, ctypes.byref(dcb)):
|
|
||||||
_fail(f"cannot read the state of {path}")
|
|
||||||
dcb.BaudRate, dcb.ByteSize, dcb.Parity, dcb.StopBits = baud, 8, 0, 0 # 8N1
|
|
||||||
# fBinary, and DTR/RTS asserted (fDtrControl and fRtsControl,
|
|
||||||
# two bits each, = _ENABLE); every other flag clear, so no
|
|
||||||
# parity and no flow control. Raising both matches what opening
|
|
||||||
# a POSIX tty does — including the reset pulse on the boards
|
|
||||||
# that wire DTR to it.
|
|
||||||
dcb.fBits = 0x1 | (1 << 4) | (1 << 12)
|
|
||||||
if not _k32.SetCommState(self.handle, ctypes.byref(dcb)):
|
|
||||||
_fail(f"cannot configure {path} for {baud} baud 8N1")
|
|
||||||
# Arm them once here too: reads re-arm per call, but the write
|
|
||||||
# timeout would otherwise stay at the driver's default — which
|
|
||||||
# may be "wait forever" — until the first read.
|
|
||||||
self._deadline(_GAP_MS, 1000)
|
|
||||||
except Error:
|
|
||||||
# An open port outlives the exception otherwise, and a COM
|
|
||||||
# handle is exclusive: the next attempt would meet its own
|
|
||||||
# leftover as "Access is denied".
|
|
||||||
self.close()
|
|
||||||
raise
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
_k32.CloseHandle(self.handle)
|
|
||||||
|
|
||||||
def _deadline(self, interval, total):
|
|
||||||
"""Arm the driver's read timeouts: `interval` ms of quiet ends a
|
|
||||||
read once bytes have arrived, `total` ms ends it regardless."""
|
|
||||||
if self.timeouts == (interval, total):
|
|
||||||
return
|
|
||||||
spec = _COMMTIMEOUTS()
|
|
||||||
spec.ReadIntervalTimeout = interval
|
|
||||||
spec.ReadTotalTimeoutConstant = total
|
|
||||||
spec.WriteTotalTimeoutConstant = 5000
|
|
||||||
if not _k32.SetCommTimeouts(self.handle, ctypes.byref(spec)):
|
|
||||||
_fail("cannot set the port timeouts")
|
|
||||||
self.timeouts = (interval, total)
|
|
||||||
|
|
||||||
def _read(self, count):
|
|
||||||
buffer = ctypes.create_string_buffer(count)
|
|
||||||
got = wintypes.DWORD()
|
|
||||||
if not _k32.ReadFile(self.handle, buffer, count, ctypes.byref(got), None):
|
|
||||||
_fail("read failed")
|
|
||||||
return buffer.raw[: got.value]
|
|
||||||
|
|
||||||
def write(self, data):
|
|
||||||
written = wintypes.DWORD()
|
|
||||||
if not _k32.WriteFile(self.handle, data, len(data), ctypes.byref(written), None):
|
|
||||||
_fail("write failed")
|
|
||||||
if written.value != len(data):
|
|
||||||
raise Error(f"short write: {written.value} of {len(data)} bytes")
|
|
||||||
|
|
||||||
def flush_input(self):
|
|
||||||
if not _k32.PurgeComm(self.handle, _PURGE_RXCLEAR):
|
|
||||||
_fail("cannot flush the input buffer")
|
|
||||||
|
|
||||||
def read_available(self, wait):
|
|
||||||
"""Everything that arrives within `wait` seconds of quiet start."""
|
|
||||||
# A zero total means *no* timeout to the driver, so never round
|
|
||||||
# down to it — the same trap on the deadline below.
|
|
||||||
self._deadline(_GAP_MS, max(1, round(wait * 1000)))
|
|
||||||
return self._read(4096)
|
|
||||||
|
|
||||||
def read_exact(self, count, timeout):
|
|
||||||
data = b""
|
|
||||||
deadline = time.monotonic() + timeout
|
|
||||||
while len(data) < count:
|
|
||||||
remaining = deadline - time.monotonic()
|
|
||||||
if remaining <= 0:
|
|
||||||
raise Error(f"timeout: got {len(data)} of {count} bytes")
|
|
||||||
# No interval timeout here: only the count or the deadline
|
|
||||||
# ends the read, so a gap mid-reply is simply waited out.
|
|
||||||
self._deadline(0, max(1, round(remaining * 1000)))
|
|
||||||
data += self._read(count - len(data))
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
Port = WindowsPort if os.name == "nt" else PosixPort
|
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------------------- protocol ---
|
# -------------------------------------------------------------- protocol ---
|
||||||
|
|
||||||
|
|
||||||
@@ -317,17 +99,12 @@ class Info:
|
|||||||
raise Error(f"protocol version {raw[2]}, tool speaks {PROTOCOL_VERSION}")
|
raise Error(f"protocol version {raw[2]}, tool speaks {PROTOCOL_VERSION}")
|
||||||
self.raw = bytes(raw)
|
self.raw = bytes(raw)
|
||||||
self.signature = raw[3:6]
|
self.signature = raw[3:6]
|
||||||
self.page = raw[6] or 256 # the wire count convention: 0 means 256
|
self.page = raw[6]
|
||||||
self.patch_vector = bool(raw[11] & 1)
|
self.base = raw[7] | (raw[8] << 8)
|
||||||
# Large chips speak word addresses for flash (bit 1); the host keeps
|
|
||||||
# every address in bytes and converts at the wire.
|
|
||||||
self.word_flash = bool(raw[11] & 2)
|
|
||||||
scale = 2 if self.word_flash else 1
|
|
||||||
self.base = (raw[7] | (raw[8] << 8)) * scale
|
|
||||||
self.eeprom_size = raw[9] | (raw[10] << 8)
|
self.eeprom_size = raw[9] | (raw[10] << 8)
|
||||||
self.slot = 1024 if self.word_flash else SLOT
|
self.patch_vector = bool(raw[11] & 1)
|
||||||
self.flash_size = self.base + self.slot
|
self.flash_size = self.base + SLOT
|
||||||
self.stage = self.base - self.slot # where a staging copy of the loader goes
|
self.stage = self.base - SLOT # where a staging copy of the loader goes
|
||||||
# The hand-over target, as the word address 'J' takes: the trampoline
|
# The hand-over target, as the word address 'J' takes: the trampoline
|
||||||
# below the loader (tinies), or word 0 (mega — the application's own
|
# below the loader (tinies), or word 0 (mega — the application's own
|
||||||
# reset vector; BOOTRST re-vectors a reset into the loader instead).
|
# reset vector; BOOTRST re-vectors a reset into the loader instead).
|
||||||
@@ -342,23 +119,6 @@ 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
|
||||||
@@ -376,10 +136,8 @@ 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:
|
||||||
@@ -389,7 +147,6 @@ 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):
|
||||||
@@ -403,44 +160,28 @@ class Loader:
|
|||||||
self._expect_prompt(timeout)
|
self._expect_prompt(timeout)
|
||||||
return reply
|
return reply
|
||||||
|
|
||||||
def _stream_read(self, command, address, count, address_scale=1):
|
def _stream_read(self, command, address, count):
|
||||||
data = b""
|
data = b""
|
||||||
while count:
|
while count:
|
||||||
chunk = min(count, 256)
|
chunk = min(count, 256)
|
||||||
wire = address // address_scale
|
head = bytes((ord(command), address & 0xFF, address >> 8, chunk & 0xFF))
|
||||||
head = bytes((ord(command), wire & 0xFF, wire >> 8, chunk & 0xFF))
|
|
||||||
data += self._command(head, chunk, 5.0)
|
data += self._command(head, chunk, 5.0)
|
||||||
address += chunk
|
address += chunk
|
||||||
count -= chunk
|
count -= chunk
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def read_flash(self, address, count):
|
def read_flash(self, address, count):
|
||||||
if not self.info.word_flash:
|
|
||||||
return self._stream_read("R", address, count)
|
return self._stream_read("R", address, count)
|
||||||
# Word-addressed wire: widen to even bounds and never let one read
|
|
||||||
# cross a 64 KiB boundary (the device holds RAMPZ for a whole run).
|
|
||||||
start = address & ~1
|
|
||||||
span = (address + count + 1 & ~1) - start
|
|
||||||
data = b""
|
|
||||||
at = start
|
|
||||||
remaining = span
|
|
||||||
while remaining:
|
|
||||||
chunk = min(remaining, 0x10000 - (at & 0xFFFF))
|
|
||||||
data += self._stream_read("R", at, chunk, address_scale=2)
|
|
||||||
at += chunk
|
|
||||||
remaining -= chunk
|
|
||||||
return data[address - start : address - start + count]
|
|
||||||
|
|
||||||
def read_eeprom(self, address, count):
|
def read_eeprom(self, address, count):
|
||||||
return self._stream_read("r", address, count)
|
return self._stream_read("r", address, count)
|
||||||
|
|
||||||
def write_page(self, address, data):
|
def write_page(self, address, data):
|
||||||
assert len(data) == self.info.page and address % self.info.page == 0
|
assert len(data) == self.info.page and address % self.info.page == 0
|
||||||
wire = address // (2 if self.info.word_flash else 1)
|
head = bytes((ord("W"), address & 0xFF, address >> 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, progress=None):
|
def write_eeprom(self, address, data):
|
||||||
offset = 0
|
offset = 0
|
||||||
while offset < len(data):
|
while offset < len(data):
|
||||||
chunk = data[offset : offset + 256]
|
chunk = data[offset : offset + 256]
|
||||||
@@ -449,8 +190,6 @@ 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)
|
||||||
@@ -556,8 +295,6 @@ 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
|
||||||
@@ -587,51 +324,14 @@ def covered(pages, info, skip_blank):
|
|||||||
# ----------------------------------------------------------------- fuses ---
|
# ----------------------------------------------------------------- fuses ---
|
||||||
|
|
||||||
|
|
||||||
# Per-chip boot fuse geometry, keyed by the signature's family/part bytes:
|
def mega_boot(high_fuse):
|
||||||
# which byte of the 'F' reply (low, lock, extended, high) carries BOOTSZ/
|
"""Decode the ATmega328P high fuse's boot configuration (DS40002061B
|
||||||
# BOOTRST, and the BOOTSZ->words ladder. A die revision shares its base
|
§27.3, Table 27-13/27-16): BOOTSZ1:0 in bits 2:1 select the boot-section
|
||||||
# signature, so one row covers it. The m48s have no boot section and no
|
words, BOOTRST in bit 0 (programmed = 0) re-vectors reset to its start.
|
||||||
# row — their info block says patch-vector and this table is never
|
Returns (bootrst_programmed, boot_section_start_byte)."""
|
||||||
# consulted. Sources: Atmel-2486/2466/2503 (HIGH fuse), Atmel-2545/8271/
|
bootsz = (high_fuse >> 1) & 0x03
|
||||||
# DS40002065 (x8: EXTENDED, except the m328s' HIGH), Atmel-8272/8011/2593/
|
words = {0b11: 256, 0b10: 512, 0b01: 1024, 0b00: 2048}[bootsz]
|
||||||
# 42719 (x4: HIGH).
|
return (high_fuse & 1) == 0, 0x8000 - words * 2
|
||||||
_LADDER_128 = {0b11: 128, 0b10: 256, 0b01: 512, 0b00: 1024}
|
|
||||||
_LADDER_256 = {0b11: 256, 0b10: 512, 0b01: 1024, 0b00: 2048}
|
|
||||||
_LADDER_512 = {0b11: 512, 0b10: 1024, 0b01: 2048, 0b00: 4096}
|
|
||||||
BOOT_FUSE = {
|
|
||||||
bytes((0x93, 0x07)): (3, _LADDER_128), # m8/8A
|
|
||||||
bytes((0x94, 0x03)): (3, _LADDER_128), # m16/16A
|
|
||||||
bytes((0x95, 0x02)): (3, _LADDER_256), # m32/32A
|
|
||||||
bytes((0x93, 0x0A)): (2, _LADDER_128), # m88/88A
|
|
||||||
bytes((0x93, 0x0F)): (2, _LADDER_128), # m88P/88PA
|
|
||||||
bytes((0x94, 0x06)): (2, _LADDER_128), # m168/168A
|
|
||||||
bytes((0x94, 0x0B)): (2, _LADDER_128), # m168P/168PA
|
|
||||||
bytes((0x95, 0x14)): (3, _LADDER_256), # m328
|
|
||||||
bytes((0x95, 0x0F)): (3, _LADDER_256), # m328P
|
|
||||||
bytes((0x94, 0x0F)): (3, _LADDER_128), # m164A
|
|
||||||
bytes((0x94, 0x0A)): (3, _LADDER_128), # m164P/164PA
|
|
||||||
bytes((0x95, 0x15)): (3, _LADDER_256), # m324A
|
|
||||||
bytes((0x95, 0x08)): (3, _LADDER_256), # m324P
|
|
||||||
bytes((0x95, 0x11)): (3, _LADDER_256), # m324PA
|
|
||||||
bytes((0x96, 0x09)): (3, _LADDER_512), # m644/644A
|
|
||||||
bytes((0x96, 0x0A)): (3, _LADDER_512), # m644P/644PA
|
|
||||||
bytes((0x97, 0x06)): (3, _LADDER_512), # m1284
|
|
||||||
bytes((0x97, 0x05)): (3, _LADDER_512), # m1284P
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def mega_boot(info, fuse_bytes):
|
|
||||||
"""Decode a mega's boot configuration from its fuses (the byte and the
|
|
||||||
BOOTSZ ladder are per chip): BOOTSZ1:0 in bits 2:1 select the
|
|
||||||
boot-section words, BOOTRST in bit 0 (programmed = 0) re-vectors reset
|
|
||||||
to its start. Returns (bootrst_programmed, boot_section_start_byte)."""
|
|
||||||
entry = BOOT_FUSE.get(bytes(info.signature[1:3]))
|
|
||||||
if entry is None:
|
|
||||||
raise Error(f"unknown mega signature {info.signature.hex()} — no boot fuse map")
|
|
||||||
which, ladder = entry
|
|
||||||
fuse = fuse_bytes[which]
|
|
||||||
words = ladder[(fuse >> 1) & 0x03]
|
|
||||||
return (fuse & 1) == 0, info.flash_size - words * 2
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------- loader update ---
|
# ---------------------------------------------------------- loader update ---
|
||||||
@@ -643,33 +343,18 @@ def image_info(image):
|
|||||||
return Info(image[at : at + 12]) if 0 <= at <= len(image) - 12 else None
|
return Info(image[at : at + 12]) if 0 <= at <= len(image) - 12 else None
|
||||||
|
|
||||||
|
|
||||||
def loader_image(path):
|
|
||||||
"""A loader update image, as the slot's own content. A raw binary is that
|
|
||||||
already; an Intel HEX links the loader at its base inside an otherwise
|
|
||||||
blank flash image, and load_image() anchors every image at zero, so the
|
|
||||||
blank below the base is dropped here. The base comes from the image's own
|
|
||||||
info block rather than the device's, so an image built for somewhere else
|
|
||||||
survives intact and the preflight can say so."""
|
|
||||||
image = load_image(path)
|
|
||||||
embedded = image_info(image)
|
|
||||||
if embedded and len(image) > embedded.base:
|
|
||||||
image = image[embedded.base :]
|
|
||||||
return image
|
|
||||||
|
|
||||||
|
|
||||||
def staging_content(image, info):
|
def staging_content(image, info):
|
||||||
"""The 512-byte staging-slot content: the image, padding, and — on
|
"""The 512-byte staging-slot content: the image, padding, and — on
|
||||||
chips whose hand-over jumps through the word below the resident loader —
|
chips whose hand-over jumps through the word below the resident loader —
|
||||||
that word, which for a staging copy is the slot's own last word: an rjmp
|
that word, which for a staging copy is the slot's own last word: an rjmp
|
||||||
to the resident base. The staging copy's fall-through and 'J'-free exit
|
to the resident base. The staging copy's fall-through and 'J'-free exit
|
||||||
both land in a loader instead of garbage."""
|
both land in a loader instead of garbage."""
|
||||||
slot = info.slot
|
if len(image) > (SLOT - 2 if info.patch_vector else SLOT):
|
||||||
if len(image) > (slot - 2 if info.patch_vector else slot):
|
raise Error(f"loader image is {len(image)} B, the slot holds {SLOT - 2 if info.patch_vector else SLOT}")
|
||||||
raise Error(f"loader image is {len(image)} B, the slot holds {slot - 2 if info.patch_vector else slot}")
|
content = bytearray(image) + bytearray([0xFF] * (SLOT - len(image)))
|
||||||
content = bytearray(image) + bytearray([0xFF] * (slot - len(image)))
|
|
||||||
if info.patch_vector:
|
if info.patch_vector:
|
||||||
through = rjmp_to((info.base - 2) // 2, info.base // 2, info.flash_size // 2)
|
through = rjmp_to((info.base - 2) // 2, info.base // 2, info.flash_size // 2)
|
||||||
content[slot - 2], content[slot - 1] = through & 0xFF, through >> 8
|
content[SLOT - 2], content[SLOT - 1] = through & 0xFF, through >> 8
|
||||||
return bytes(content)
|
return bytes(content)
|
||||||
|
|
||||||
|
|
||||||
@@ -687,13 +372,14 @@ def update_preflight(image, info, fuse_bytes):
|
|||||||
if not info.patch_vector:
|
if not info.patch_vector:
|
||||||
if fuse_bytes is None:
|
if fuse_bytes is None:
|
||||||
raise Error("a loader update on this chip needs its fuses — unreadable? pass --assume-fuses")
|
raise Error("a loader update on this chip needs its fuses — unreadable? pass --assume-fuses")
|
||||||
bootrst, bls_start = mega_boot(info, fuse_bytes)
|
high = fuse_bytes[3]
|
||||||
|
bootrst, bls_start = mega_boot(high)
|
||||||
if info.stage < bls_start:
|
if info.stage < bls_start:
|
||||||
raise Error(
|
raise Error(
|
||||||
f"cannot self-update: the staging slot {info.stage:#06x} lies below the "
|
f"cannot self-update: the staging slot {info.stage:#06x} lies below the "
|
||||||
f"boot section ({bls_start:#06x}) where SPM is disabled "
|
f"boot section ({bls_start:#06x}, high fuse {high:#04x}) where SPM is disabled "
|
||||||
f"— a boot section of at least two slots ({2 * info.slot} B, BOOTSZ) is "
|
f"— a boot section of at least 1 KB (BOOTSZ) is required, and only an "
|
||||||
f"required, and only an external programmer can change fuses"
|
f"external programmer can change fuses"
|
||||||
)
|
)
|
||||||
if not bootrst:
|
if not bootrst:
|
||||||
warnings.append(
|
warnings.append(
|
||||||
@@ -734,7 +420,7 @@ class UpdateState:
|
|||||||
self.data = {
|
self.data = {
|
||||||
"signature": info.signature.hex(),
|
"signature": info.signature.hex(),
|
||||||
"base": info.base,
|
"base": info.base,
|
||||||
"staging": loader.read_flash(info.stage, info.slot).hex(),
|
"staging": loader.read_flash(info.stage, SLOT).hex(),
|
||||||
"page0": loader.read_flash(0, info.page).hex() if info.patch_vector else "",
|
"page0": loader.read_flash(0, info.page).hex() if info.patch_vector else "",
|
||||||
}
|
}
|
||||||
with open(self.path, "w") as f:
|
with open(self.path, "w") as f:
|
||||||
@@ -752,42 +438,20 @@ class UpdateState:
|
|||||||
os.unlink(self.path)
|
os.unlink(self.path)
|
||||||
|
|
||||||
|
|
||||||
def write_differing(loader, base, content, order=None, label=None):
|
def write_differing(loader, base, content, order=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 = list(order) if order is not None else list(range(0, len(content), page))
|
offsets = order if order is not None else range(0, len(content), page)
|
||||||
written = 0
|
written = 0
|
||||||
with Progress(label, len(offsets)) as bar:
|
|
||||||
for offset in offsets:
|
for offset in offsets:
|
||||||
want = content[offset : offset + page]
|
want = content[offset : offset + page]
|
||||||
if loader.read_flash(base + offset, page) != want:
|
if loader.read_flash(base + offset, page) != want:
|
||||||
loader.write_page(base + offset, want)
|
loader.write_page(base + offset, want)
|
||||||
written += 1
|
written += 1
|
||||||
bar.step()
|
for at in range(0, len(content), 256):
|
||||||
if label:
|
if loader.read_flash(base + at, min(256, len(content) - at)) != content[at : at + 256]:
|
||||||
verbose(f"{label}: {written} of {len(offsets)} pages differed")
|
raise Error(f"verify failed at {base + at:#06x} after programming")
|
||||||
# Page-wise read-back with the same bounded repair as verify_pages: this
|
|
||||||
# is the loader-update path, where a page left wrong is a half-written
|
|
||||||
# loader slot.
|
|
||||||
for retry in range(RETRIES + 1):
|
|
||||||
bad = [
|
|
||||||
offset
|
|
||||||
for offset in range(0, len(content), page)
|
|
||||||
if loader.read_flash(base + offset, len(content[offset : offset + page])) != content[offset : offset + page]
|
|
||||||
]
|
|
||||||
if not bad:
|
|
||||||
break
|
|
||||||
if retry == RETRIES:
|
|
||||||
raise Error(
|
|
||||||
f"verify failed at {base + bad[0]:#06x} after programming "
|
|
||||||
f"(still wrong after {RETRIES} retries)"
|
|
||||||
)
|
|
||||||
for offset in bad:
|
|
||||||
verbose(f"rewriting page {base + offset:#06x} (retry {retry + 1})")
|
|
||||||
loader.write_page(base + offset, content[offset : offset + page])
|
|
||||||
written += 1
|
|
||||||
return written
|
return written
|
||||||
|
|
||||||
|
|
||||||
@@ -808,73 +472,47 @@ def op_update_loader(loader, wait, path, state_path, fuse_bytes):
|
|||||||
actual flash state, so a re-run after any interruption resumes; the
|
actual flash state, so a re-run after any interruption resumes; the
|
||||||
state file carries the bytes the staging slot held."""
|
state file carries the bytes the staging slot held."""
|
||||||
info = loader.info
|
info = loader.info
|
||||||
image = loader_image(path)
|
image = load_image(path)
|
||||||
for warning in update_preflight(image, info, fuse_bytes):
|
for warning in update_preflight(image, info, fuse_bytes):
|
||||||
print(f"note: {warning}")
|
print(f"note: {warning}")
|
||||||
staged = staging_content(image, info)
|
staged = staging_content(image, info)
|
||||||
resident = bytes(image) + bytes([0xFF] * (info.slot - len(image)))
|
resident = bytes(image) + bytes([0xFF] * (SLOT - len(image)))
|
||||||
page = info.page
|
page = info.page
|
||||||
|
|
||||||
state = UpdateState(state_path)
|
state = UpdateState(state_path)
|
||||||
if os.path.exists(state_path):
|
|
||||||
verbose(f"resuming the update recorded in {state_path}")
|
|
||||||
else:
|
|
||||||
verbose(f"saving the staging slot to {state_path}")
|
|
||||||
state.load_or_save(loader)
|
state.load_or_save(loader)
|
||||||
|
|
||||||
# Install the staging copy — unless a loader already sits whole in the
|
# Install the staging copy. On a chip whose staging slot starts at
|
||||||
# staging slot (a build programmed there by hand): that copy IS the
|
# address 0 (the 1 KB tiny13A), its first page carries the reset vector:
|
||||||
# installed staging copy, and rewriting it would only trip its own
|
# written last, so any earlier interruption still resets into the old
|
||||||
# running-slot guard on the composed through-word. Any pureboot with
|
# resident, and from then on resets enter the staging copy.
|
||||||
# the device's own info block serves — the staged copy just streams
|
order = list(range(0, SLOT, page))
|
||||||
# pages, so an older build installs a newer resident all the same. Two
|
|
||||||
# checks make "already a loader" mean a *complete* one: the block must
|
|
||||||
# sit where every image carries it (within the slot's first 256 bytes
|
|
||||||
# — the build's position lint), matching the device's block byte for
|
|
||||||
# byte, and the slot must be unchanged since this update began (the
|
|
||||||
# state file's snapshot) — a resumed, half-written install differs
|
|
||||||
# from its snapshot and takes the install path below, which completes
|
|
||||||
# it page by page.
|
|
||||||
current = loader.read_flash(info.stage, info.slot)
|
|
||||||
staged_loader = image_info(current[:268])
|
|
||||||
if staged_loader is not None and staged_loader.raw == info.raw and current == state.staging:
|
|
||||||
print("staging slot already holds a loader — left in place")
|
|
||||||
else:
|
|
||||||
# On a chip whose staging slot starts at address 0 (the 1 KB
|
|
||||||
# tiny13s), its first page carries the reset vector: written last,
|
|
||||||
# so any earlier interruption still resets into the old resident,
|
|
||||||
# and from then on resets enter the staging copy.
|
|
||||||
order = list(range(0, info.slot, page))
|
|
||||||
if info.stage == 0:
|
if info.stage == 0:
|
||||||
order = order[1:] + [0]
|
order = order[1:] + [0]
|
||||||
if write_differing(loader, info.stage, staged, order, label="staging copy"):
|
if write_differing(loader, info.stage, staged, order):
|
||||||
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, label="resident"):
|
if write_differing(loader, info.base, resident):
|
||||||
print(f"resident loader rewritten at {info.base:#06x}")
|
print(f"resident loader rewritten at {info.base:#06x}")
|
||||||
|
|
||||||
# Enter the new resident and put the staging region back: page 0 first
|
# Enter the new resident and put the staging region back: page 0 first
|
||||||
# where it lives in that region (word 0 then points at the new resident
|
# where it lives in that region (word 0 then points at the new resident
|
||||||
# for the rest of the restore), the saved trampoline with the rest.
|
# for the rest of the restore), the saved trampoline with the rest.
|
||||||
verbose(f"entering the new resident at {info.base:#06x}")
|
|
||||||
loader.enter_copy(info.base, wait)
|
loader.enter_copy(info.base, wait)
|
||||||
if redirect:
|
if redirect:
|
||||||
verbose("word 0 restored")
|
|
||||||
write_differing(loader, 0, state.page0)
|
write_differing(loader, 0, state.page0)
|
||||||
order = list(range(0, info.slot, page))
|
order = list(range(0, SLOT, page))
|
||||||
if info.stage == 0:
|
if info.stage == 0:
|
||||||
order = [0] + order[1:]
|
order = [0] + order[1:]
|
||||||
write_differing(loader, info.stage, state.staging, order, label="staging restore")
|
write_differing(loader, info.stage, state.staging, order)
|
||||||
|
|
||||||
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")
|
||||||
@@ -887,7 +525,7 @@ def check_walk_region(pages, info, fuse_bytes, force):
|
|||||||
Only checkable when the fuses are known (--fuses or --assume-fuses)."""
|
Only checkable when the fuses are known (--fuses or --assume-fuses)."""
|
||||||
if info.patch_vector or fuse_bytes is None:
|
if info.patch_vector or fuse_bytes is None:
|
||||||
return
|
return
|
||||||
bootrst, bls_start = mega_boot(info, fuse_bytes)
|
bootrst, bls_start = mega_boot(fuse_bytes[3])
|
||||||
if not bootrst or bls_start >= info.base:
|
if not bootrst or bls_start >= info.base:
|
||||||
return
|
return
|
||||||
overlap = [a for a in sorted(pages) if a >= bls_start and pages[a].count(0xFF) != len(pages[a])]
|
overlap = [a for a in sorted(pages) if a >= bls_start and pages[a].count(0xFF) != len(pages[a])]
|
||||||
@@ -909,85 +547,48 @@ def op_erase_flash(loader):
|
|||||||
is erased and the reset walk reaches the loader anyway."""
|
is erased and the reset walk reaches the loader anyway."""
|
||||||
blank = bytes([0xFF] * loader.info.page)
|
blank = bytes([0xFF] * loader.info.page)
|
||||||
addresses = range(0, loader.info.base, loader.info.page)
|
addresses = range(0, loader.info.base, loader.info.page)
|
||||||
with Progress("erase", len(addresses)) as bar:
|
|
||||||
for address in reversed(addresses) if loader.info.patch_vector else addresses:
|
for address in reversed(addresses) if loader.info.patch_vector else addresses:
|
||||||
loader.write_page(address, blank)
|
loader.write_page(address, blank)
|
||||||
bar.step()
|
|
||||||
print(f"erase: {loader.info.base // loader.info.page} pages")
|
print(f"erase: {loader.info.base // loader.info.page} pages")
|
||||||
|
|
||||||
|
|
||||||
def op_erase_eeprom(loader):
|
def op_erase_eeprom(loader):
|
||||||
with Progress("erase EEPROM", loader.info.eeprom_size, "B") as bar:
|
loader.write_eeprom(0, bytes([0xFF] * loader.info.eeprom_size))
|
||||||
loader.write_eeprom(0, bytes([0xFF] * loader.info.eeprom_size), progress=bar)
|
|
||||||
print(f"erase: {loader.info.eeprom_size} B of EEPROM")
|
print(f"erase: {loader.info.eeprom_size} B of EEPROM")
|
||||||
|
|
||||||
|
|
||||||
def op_flash(loader, path, erase, verify, fuse_bytes=None, force=False):
|
def op_flash(loader, path, erase, verify, fuse_bytes=None, force=False):
|
||||||
image = load_image(path)
|
image = load_image(path)
|
||||||
verbose(f"{path}: {len(image)} B image")
|
|
||||||
pages = plan_flash(image, loader.info)
|
pages = plan_flash(image, loader.info)
|
||||||
check_walk_region(pages, loader.info, fuse_bytes, force)
|
check_walk_region(pages, loader.info, fuse_bytes, force)
|
||||||
if erase:
|
if erase:
|
||||||
op_erase_flash(loader)
|
op_erase_flash(loader)
|
||||||
order = covered(pages, loader.info, skip_blank=erase)
|
order = covered(pages, loader.info, skip_blank=erase)
|
||||||
if len(order) != len(pages):
|
|
||||||
verbose(f"{len(pages) - len(order)} blank pages skipped (erased flash underneath)")
|
|
||||||
with Progress("flash", len(order)) as bar:
|
|
||||||
for address in order:
|
for address in order:
|
||||||
loader.write_page(address, pages[address])
|
loader.write_page(address, pages[address])
|
||||||
bar.step()
|
|
||||||
print(f"flash: {path}: {len(order)} pages")
|
print(f"flash: {path}: {len(order)} pages")
|
||||||
if verify:
|
if verify:
|
||||||
verify_pages(loader, pages, repair=True)
|
verify_pages(loader, pages)
|
||||||
|
|
||||||
|
|
||||||
def verify_pages(loader, pages, repair=False):
|
def verify_pages(loader, pages):
|
||||||
"""Read every page back and compare. With `repair`, a mismatched page is
|
|
||||||
rewritten and re-read, up to RETRIES times before it is raised: a page
|
|
||||||
filled over a dirty SPM buffer takes stale words, and the write that took
|
|
||||||
them cleared the buffer, so one rewrite settles it. Anything still wrong
|
|
||||||
after three is not that, and stops the run."""
|
|
||||||
repaired = 0
|
|
||||||
with Progress("verify", len(pages)) as bar:
|
|
||||||
for address in sorted(pages):
|
for address in sorted(pages):
|
||||||
for retry in range(RETRIES + 1):
|
|
||||||
got = loader.read_flash(address, loader.info.page)
|
got = loader.read_flash(address, loader.info.page)
|
||||||
if got == pages[address]:
|
if got != pages[address]:
|
||||||
break
|
|
||||||
first = next(i for i in range(len(got)) if got[i] != pages[address][i])
|
first = next(i for i in range(len(got)) if got[i] != pages[address][i])
|
||||||
detail = (
|
raise Error(
|
||||||
f"verify failed at {address + first:#06x}: "
|
f"verify failed at {address + first:#06x}: "
|
||||||
f"wrote {pages[address][first]:02x}, read {got[first]:02x}"
|
f"wrote {pages[address][first]:02x}, read {got[first]:02x}"
|
||||||
)
|
)
|
||||||
if not repair:
|
print(f"verify: {len(pages)} pages ok")
|
||||||
raise Error(detail)
|
|
||||||
if retry == RETRIES:
|
|
||||||
raise Error(f"{detail} (still wrong after {RETRIES} retries)")
|
|
||||||
verbose(f"{detail} — rewriting page {address:#06x} (retry {retry + 1})")
|
|
||||||
loader.write_page(address, pages[address])
|
|
||||||
repaired += 1
|
|
||||||
bar.step()
|
|
||||||
note = f", {repaired} page rewrite(s)" if repaired else ""
|
|
||||||
print(f"verify: {len(pages)} pages ok{note}")
|
|
||||||
|
|
||||||
|
|
||||||
def op_verify_flash(loader, path):
|
def op_verify_flash(loader, path):
|
||||||
verify_pages(loader, plan_flash(load_image(path), loader.info))
|
verify_pages(loader, plan_flash(load_image(path), loader.info))
|
||||||
|
|
||||||
|
|
||||||
def read_progress(reader, total, label):
|
|
||||||
"""A bulk read in 256-byte wire chunks under a progress bar."""
|
|
||||||
data = b""
|
|
||||||
with Progress(label, total, "B") as bar:
|
|
||||||
while len(data) < total:
|
|
||||||
chunk = min(256, total - len(data))
|
|
||||||
data += reader(len(data), chunk)
|
|
||||||
bar.step(chunk)
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def op_read_flash(loader, path):
|
def op_read_flash(loader, path):
|
||||||
data = read_progress(loader.read_flash, loader.info.base, "read flash")
|
data = loader.read_flash(0, loader.info.base)
|
||||||
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}")
|
||||||
|
|
||||||
@@ -998,11 +599,10 @@ 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)
|
||||||
with Progress("eeprom", len(image), "B") as bar:
|
loader.write_eeprom(0, image)
|
||||||
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 = read_progress(loader.read_eeprom, len(image), "verify EEPROM")
|
got = loader.read_eeprom(0, len(image))
|
||||||
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}")
|
||||||
@@ -1011,7 +611,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 = read_progress(loader.read_eeprom, len(image), "verify EEPROM")
|
got = loader.read_eeprom(0, len(image))
|
||||||
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}")
|
||||||
@@ -1019,30 +619,15 @@ def op_verify_eeprom(loader, path):
|
|||||||
|
|
||||||
|
|
||||||
def op_read_eeprom(loader, path):
|
def op_read_eeprom(loader, path):
|
||||||
data = read_progress(loader.read_eeprom, loader.info.eeprom_size, "read EEPROM")
|
data = loader.read_eeprom(0, loader.info.eeprom_size)
|
||||||
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("fuses:")
|
print(f"fuses: low {low:02x} high {high:02x} extended {extended:02x} lock {lock:02x}")
|
||||||
print(f" low 0x{low:02x}")
|
return bytes((low, lock, extended, high))
|
||||||
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 ---
|
||||||
@@ -1052,7 +637,7 @@ def main():
|
|||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="pureboot host tool", epilog="operations run in the order listed above"
|
description="pureboot host tool", epilog="operations run in the order listed above"
|
||||||
)
|
)
|
||||||
parser.add_argument("--port", required=True, help="serial device: COM6, /dev/ttyUSB0, or a simavr pty")
|
parser.add_argument("--port", required=True, help="serial device (or simavr pty)")
|
||||||
parser.add_argument("--baud", type=int, default=115200, help="115200 mega, 57600 tinies")
|
parser.add_argument("--baud", type=int, default=115200, help="115200 mega, 57600 tinies")
|
||||||
parser.add_argument("--wait", type=float, default=30.0, help="seconds to keep knocking")
|
parser.add_argument("--wait", type=float, default=30.0, help="seconds to keep knocking")
|
||||||
parser.add_argument("--info", action="store_true", help="print the device info block")
|
parser.add_argument("--info", action="store_true", help="print the device info block")
|
||||||
@@ -1072,11 +657,7 @@ 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")
|
||||||
@@ -1089,14 +670,11 @@ 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("device:")
|
print(f"device: {info.describe()}")
|
||||||
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)
|
||||||
|
|||||||
@@ -41,8 +41,7 @@ def main():
|
|||||||
if len(info) != 1:
|
if len(info) != 1:
|
||||||
print(f"FAIL: expected one info-block storage symbol, found {len(info)}")
|
print(f"FAIL: expected one info-block storage symbol, found {len(info)}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
address = int(info[0].split()[0], 16)
|
offset = int(info[0].split()[0], 16) - text_start
|
||||||
offset = address - text_start
|
|
||||||
if not 0 <= offset < 256:
|
if not 0 <= offset < 256:
|
||||||
print(f"FAIL: info block at image offset {offset:#x}, must sit in the first 256 bytes")
|
print(f"FAIL: info block at image offset {offset:#x}, must sit in the first 256 bytes")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|||||||
@@ -4,16 +4,11 @@
|
|||||||
// 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 hardware-USART link it then listens, and an 'L' makes it jump into
|
// On the mega it then listens, and an 'L' makes it jump into the resident
|
||||||
// the resident loader — the application-owned loader entry a
|
// loader — the application-owned loader entry a BOOTRST-unprogrammed mega
|
||||||
// BOOTRST-unprogrammed mega relies on (reset always boots the application
|
// relies on (reset always boots the application there), exercised by the
|
||||||
// there), exercised by the self-update tests. The software link idles:
|
// self-update tests. The tinies idle: reset reaches their loader through
|
||||||
// reset reaches those loaders through the patched vector (or the runner
|
// the patched vector, so the application owes it nothing.
|
||||||
// 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;
|
||||||
@@ -22,76 +17,33 @@ namespace {
|
|||||||
|
|
||||||
consteval avr::hertz_t clock()
|
consteval avr::hertz_t clock()
|
||||||
{
|
{
|
||||||
#if defined(PUREBOOT_CLOCK_HZ)
|
if (avr::hw::db.name == "ATtiny13A")
|
||||||
return avr::hertz_t{PUREBOOT_CLOCK_HZ};
|
|
||||||
#else
|
|
||||||
auto name = std::string_view{avr::hw::db.name};
|
|
||||||
if (name.starts_with("ATtiny13"))
|
|
||||||
return 9.6_MHz;
|
return 9.6_MHz;
|
||||||
if (name.starts_with("ATtiny"))
|
if (avr::hw::db.name == "ATtiny85")
|
||||||
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 = use_hardware()>
|
template <avr::hertz_t C, bool Hardware = avr::hw::db.has_reg("UDR0")>
|
||||||
struct link {
|
struct link {
|
||||||
#if defined(PUREBOOT_BAUD)
|
using tx_t = avr::uart::usart0<C, {.baud = 115200_Bd, .max_baud_error = 2.5_pct}>;
|
||||||
static constexpr avr::baud_t baud{PUREBOOT_BAUD};
|
|
||||||
#else
|
|
||||||
static constexpr avr::baud_t baud{115200};
|
|
||||||
#endif
|
|
||||||
using tx_t = avr::uart::usart<'0' + PUREBOOT_USART, C, {.baud = baud, .max_baud_error = 2.5_pct}>;
|
|
||||||
static void tx(char c)
|
static void tx(char c)
|
||||||
{
|
{
|
||||||
tx_t::write(static_cast<std::uint8_t>(c));
|
tx_t::write(static_cast<std::uint8_t>(c));
|
||||||
}
|
}
|
||||||
[[noreturn]] static void idle()
|
[[noreturn]] static void idle()
|
||||||
{
|
{
|
||||||
// 'L' hands back to the loader at the top slot — 512 bytes, or the
|
for (;;)
|
||||||
// 1 KiB the >64 KiB chips use.
|
if (tx_t::read_blocking() == 'L')
|
||||||
constexpr std::uint32_t slot = avr::hw::db.mem.flash_size > 65536 ? 1024 : 512;
|
reinterpret_cast<void (*)()>((avr::hw::db.mem.flash_size - 512) / 2)();
|
||||||
for (;;) {
|
|
||||||
auto command = tx_t::read_blocking();
|
|
||||||
if (command == 'L')
|
|
||||||
reinterpret_cast<void (*)()>(static_cast<std::uint16_t>((avr::hw::db.mem.flash_size - slot) / 2))();
|
|
||||||
// 'D' leaves every word of the SPM page buffer dirty, so that a
|
|
||||||
// following 'L' enters the loader with the buffer it never clears.
|
|
||||||
if (command == 'D') {
|
|
||||||
for (std::uint16_t at = 0; at < avr::spm::page_bytes; at += 2)
|
|
||||||
avr::spm::fill(at, 0xdead);
|
|
||||||
tx('D');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
template <avr::hertz_t C>
|
template <avr::hertz_t C>
|
||||||
struct link<C, false> {
|
struct link<C, false> {
|
||||||
#if defined(PUREBOOT_BAUD)
|
using tx_t = avr::uart::software_tx<C, avr::pb1, 57600_Bd>;
|
||||||
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));
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Dirty-page-buffer acceptance test: the loader carries no buffer discard,
|
|
||||||
so a page filled over words an earlier writer left behind programs those
|
|
||||||
instead. This asserts the whole contract — the corruption is real and a bare
|
|
||||||
verify sees it, the repairing verify fixes it in one rewrite (the write that
|
|
||||||
took the stale words auto-erased the buffer), and it stays fixed.
|
|
||||||
|
|
||||||
The state is reached the way the loader cannot prevent: an application
|
|
||||||
dirties the buffer and jumps in with no reset between. Real boot-sectioned
|
|
||||||
megas forbid that outright — SPM executes only from the boot section
|
|
||||||
(Atmel-8271 §26.2) — but simavr dispatches SPM from anywhere, which is what
|
|
||||||
makes the path constructible at all.
|
|
||||||
|
|
||||||
Usage: pbdirty.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
|
|
||||||
<baud> <app_bin> <tool_py> <workdir>
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def fail(message):
|
|
||||||
print(f"FAIL: {message}")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
device_bin, elf, mcu, hz, base_hex, page, baud, app_bin, tool, workdir = sys.argv[1:]
|
|
||||||
page, baud = int(page), int(baud)
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
import pbsim
|
|
||||||
import pureboot as pb
|
|
||||||
|
|
||||||
os.makedirs(workdir, exist_ok=True)
|
|
||||||
dump = os.path.join(workdir, "dump.bin")
|
|
||||||
|
|
||||||
# Reset boots the application on a BOOTRST-unprogrammed mega; its 'L' is
|
|
||||||
# the loader entry this test needs, reached without a reset.
|
|
||||||
device = pbsim.Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump, reset_hex="0")
|
|
||||||
try:
|
|
||||||
port = pb.Port(device.pty, baud)
|
|
||||||
loader = pb.Loader(port)
|
|
||||||
loader.connect(25)
|
|
||||||
|
|
||||||
# Install the application and hand over to it.
|
|
||||||
pb.op_flash(loader, app_bin, erase=False, verify=True)
|
|
||||||
loader.run_application()
|
|
||||||
if port.read_exact(3, 5.0) != b"APP":
|
|
||||||
fail("the application did not start")
|
|
||||||
|
|
||||||
port.write(b"D")
|
|
||||||
if port.read_exact(1, 5.0) != b"D":
|
|
||||||
fail("the application did not acknowledge dirtying the page buffer")
|
|
||||||
port.write(b"L")
|
|
||||||
loader = pb.Loader(port)
|
|
||||||
loader.connect(25)
|
|
||||||
|
|
||||||
# Program by hand, so the corruption is observable before anything
|
|
||||||
# repairs it.
|
|
||||||
pages = pb.plan_flash(open(app_bin, "rb").read(), loader.info)
|
|
||||||
for address in sorted(pages):
|
|
||||||
loader.write_page(address, pages[address])
|
|
||||||
try:
|
|
||||||
pb.verify_pages(loader, pages)
|
|
||||||
except pb.Error as error:
|
|
||||||
if "verify failed" not in str(error):
|
|
||||||
fail(f"the read-back failed, but not at verify: {error}")
|
|
||||||
else:
|
|
||||||
# Either the fixture no longer dirties the buffer, or the loader
|
|
||||||
# clears it again — in which case this test's premise is gone.
|
|
||||||
fail("programming over a dirty page buffer came back clean")
|
|
||||||
|
|
||||||
# What the programming path uses: one rewrite settles it, and it stays
|
|
||||||
# settled.
|
|
||||||
pb.verify_pages(loader, pages, repair=True)
|
|
||||||
pb.verify_pages(loader, pages)
|
|
||||||
|
|
||||||
# Ground truth beyond the loader's own read-back.
|
|
||||||
loader.run_application()
|
|
||||||
if port.read_exact(3, 5.0) != b"APP":
|
|
||||||
fail("the application did not start after the recovered write")
|
|
||||||
port.close()
|
|
||||||
finally:
|
|
||||||
device.stop()
|
|
||||||
print("pbdirty: a dirty page buffer is caught by verify and cleared by the retry")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
102
test/pbrehome.py
102
test/pbrehome.py
@@ -1,102 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Re-homing acceptance test: a pureboot image programmed somewhere other
|
|
||||||
than its canonical top slot must still be a working loader —
|
|
||||||
position-independent, guarding its accidental slot — and the ordinary
|
|
||||||
--update-loader flow must put a build into the top slot from there.
|
|
||||||
|
|
||||||
Two positions are exercised. Address 0 (a raw .bin handed to a programmer,
|
|
||||||
which defaults to offset 0): the staging install and the word-0 redirect
|
|
||||||
both run from copies whose slots are not page 0's, so the running-slot
|
|
||||||
guard never blocks the flow. The staging slot itself: a loader already
|
|
||||||
sitting there IS the installed staging copy — the tool recognizes it by
|
|
||||||
its embedded info block and leaves it in place instead of tripping the
|
|
||||||
copy's own guard on the composed through-word — and that (older) copy
|
|
||||||
streams the new resident like any staged copy. In both cases flashing an
|
|
||||||
application through the healed resident overwrites the stale copy, vector
|
|
||||||
surgery included, and the banner proves the launch.
|
|
||||||
|
|
||||||
Usage: pbrehome.py <device_bin> <pureboot_elf> <update_bin> <mcu> <hz>
|
|
||||||
<base_hex> <page> <baud> <app_bin> <tool_py> <workdir>
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def fail(message):
|
|
||||||
print(f"FAIL: {message}")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def rehome_from(pbsim, pb, device_bin, elf, place_hex, guard_probe, update_bin, base, page, baud, app_bin, workdir,
|
|
||||||
mcu, hz):
|
|
||||||
"""Place the loader at `place_hex`, heal through --update-loader, flash
|
|
||||||
the application, expect the banner."""
|
|
||||||
dump = os.path.join(workdir, f"dump-{place_hex}.bin")
|
|
||||||
state = os.path.join(workdir, f"rehome-{place_hex}.pbstate")
|
|
||||||
if os.path.exists(state):
|
|
||||||
os.unlink(state)
|
|
||||||
device = pbsim.Device(device_bin, elf, mcu, hz, place_hex, page, baud, dump, reset_hex="0")
|
|
||||||
try:
|
|
||||||
port = pb.Port(device.pty, baud)
|
|
||||||
loader = pb.Loader(port)
|
|
||||||
info = loader.connect(25)
|
|
||||||
if info.base != base:
|
|
||||||
fail(f"the misplaced copy reports base {info.base:#06x} — the info block must stay canonical")
|
|
||||||
|
|
||||||
# The accidental slot still guards itself; re-homing rides on the
|
|
||||||
# canonical slots being writable from it.
|
|
||||||
probe = int(guard_probe, 0)
|
|
||||||
before = loader.read_flash(probe, info.page)
|
|
||||||
loader.write_page(probe, bytes(info.page))
|
|
||||||
if loader.read_flash(probe, info.page) != before:
|
|
||||||
fail("the misplaced copy's guard let its own slot change")
|
|
||||||
|
|
||||||
# The ordinary update flow puts the build into the top slot.
|
|
||||||
pb.op_update_loader(loader, 25, update_bin, state, None)
|
|
||||||
update = open(update_bin, "rb").read()
|
|
||||||
if loader.read_flash(base, len(update)) != update:
|
|
||||||
fail("the canonical slot does not hold the update image")
|
|
||||||
|
|
||||||
# An application flashed through the healed resident overwrites the
|
|
||||||
# stale copy (surgery included) and launches.
|
|
||||||
pages = pb.plan_flash(open(app_bin, "rb").read(), loader.info)
|
|
||||||
for address in pb.covered(pages, loader.info, skip_blank=False):
|
|
||||||
loader.write_page(address, pages[address])
|
|
||||||
pb.verify_pages(loader, pages)
|
|
||||||
loader.run_application()
|
|
||||||
if port.read_exact(3, 5.0) != b"APP":
|
|
||||||
fail(f"application does not banner after the re-home from {place_hex}")
|
|
||||||
port.close()
|
|
||||||
finally:
|
|
||||||
device.stop()
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
(device_bin, elf, update_bin, mcu, hz, base_hex, page, baud, app_bin, tool, workdir) = sys.argv[1:]
|
|
||||||
base, page, baud = int(base_hex, 0), int(page), int(baud)
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
import pbsim
|
|
||||||
import pureboot as pb
|
|
||||||
|
|
||||||
os.makedirs(workdir, exist_ok=True)
|
|
||||||
|
|
||||||
# Address 0: the raw-.bin-to-a-programmer accident. The guard probe is
|
|
||||||
# the copy's own page 0.
|
|
||||||
rehome_from(pbsim, pb, device_bin, elf, "0x0", "0x0", update_bin, base, page, baud, app_bin, workdir, mcu, hz)
|
|
||||||
print("re-home from address 0: converged")
|
|
||||||
|
|
||||||
# The staging slot: erased flash with the loader sitting exactly where
|
|
||||||
# a staging copy would — the tool must leave it in place and let it
|
|
||||||
# stream the (different) update build into the resident slot.
|
|
||||||
stage = base - 512
|
|
||||||
rehome_from(pbsim, pb, device_bin, elf, hex(stage), hex(stage), update_bin, base, page, baud, app_bin, workdir,
|
|
||||||
mcu, hz)
|
|
||||||
print("re-home from the staging slot: converged")
|
|
||||||
|
|
||||||
print("pbrehome: a misplaced loader re-homes through the ordinary update flow")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -24,7 +24,7 @@ def fail(message):
|
|||||||
def main():
|
def main():
|
||||||
device_bin, elf, mcu, hz, base_hex, page, baud, tool, workdir = sys.argv[1:]
|
device_bin, elf, mcu, hz, base_hex, page, baud, tool, workdir = sys.argv[1:]
|
||||||
base, page, baud = int(base_hex, 0), int(page), int(baud)
|
base, page, baud = int(base_hex, 0), int(page), int(baud)
|
||||||
stage = None # derived from the device's own info (slot-sized) below
|
stage = base - 512
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
|
sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
import pbsim
|
import pbsim
|
||||||
@@ -46,7 +46,6 @@ def main():
|
|||||||
resident_info = info.raw
|
resident_info = info.raw
|
||||||
|
|
||||||
# Install the staging copy exactly as the update flow would.
|
# Install the staging copy exactly as the update flow would.
|
||||||
stage = info.stage
|
|
||||||
staged = pb.staging_content(image, info)
|
staged = pb.staging_content(image, info)
|
||||||
pb.write_differing(loader, stage, staged)
|
pb.write_differing(loader, stage, staged)
|
||||||
|
|
||||||
@@ -66,10 +65,8 @@ def main():
|
|||||||
if loader.read_eeprom(0, len(pattern)) != pattern:
|
if loader.read_eeprom(0, len(pattern)) != pattern:
|
||||||
fail("EEPROM round-trip through the staged copy")
|
fail("EEPROM round-trip through the staged copy")
|
||||||
|
|
||||||
# The guard, both ways: its own slot refused (drained, unchanged), the
|
# The guard, both ways: its own slot refused (drained, unchanged),
|
||||||
# resident slot writable. The refusal leaves its drained words in the
|
# the resident slot writable.
|
||||||
# SPM buffer, so the write that follows may take them — and clears
|
|
||||||
# them by writing, so the retry must not.
|
|
||||||
before = loader.read_flash(stage, page)
|
before = loader.read_flash(stage, page)
|
||||||
loader.write_page(stage, bytes(page))
|
loader.write_page(stage, bytes(page))
|
||||||
if loader.read_flash(stage, page) != before:
|
if loader.read_flash(stage, page) != before:
|
||||||
@@ -77,13 +74,11 @@ def main():
|
|||||||
marker = bytes((i * 3) & 0xFF for i in range(page))
|
marker = bytes((i * 3) & 0xFF for i in range(page))
|
||||||
loader.write_page(base, marker)
|
loader.write_page(base, marker)
|
||||||
if loader.read_flash(base, page) != marker:
|
if loader.read_flash(base, page) != marker:
|
||||||
loader.write_page(base, marker)
|
fail("the staged copy could not write the resident slot")
|
||||||
if loader.read_flash(base, page) != marker:
|
|
||||||
fail("the staged copy could not write the resident slot, even on retry")
|
|
||||||
|
|
||||||
# Restore the resident image through the staged copy, then 'J' back
|
# Restore the resident image through the staged copy, then 'J' back
|
||||||
# into it and prove it lives.
|
# into it and prove it lives.
|
||||||
resident = image + b"\xff" * (info.slot - len(image))
|
resident = image + b"\xff" * (512 - len(image))
|
||||||
pb.write_differing(loader, base, resident)
|
pb.write_differing(loader, base, resident)
|
||||||
back_info = loader.enter_copy(base, 25)
|
back_info = loader.enter_copy(base, 25)
|
||||||
if back_info.raw != resident_info:
|
if back_info.raw != resident_info:
|
||||||
|
|||||||
@@ -8,17 +8,10 @@ import subprocess
|
|||||||
|
|
||||||
|
|
||||||
class Device:
|
class Device:
|
||||||
def __init__(self, binary, elf, mcu, hz, base_hex, page, baud, dump, reset_hex=None, resume=None, link=None):
|
def __init__(self, binary, elf, mcu, hz, base_hex, page, baud, dump, reset_hex=None, resume=None):
|
||||||
cmd = [binary]
|
cmd = [binary, elf, mcu, hz, base_hex, str(page), str(baud), dump]
|
||||||
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
|
cmd.append(reset_hex if reset_hex is not None else ("0" if mcu != "atmega328p" else base_hex))
|
||||||
# m48s — reset to address 0 like silicon; the boot-sectioned
|
|
||||||
# megas re-vector to the loader base (BOOTRST).
|
|
||||||
patch = not mcu.startswith("atmega") or mcu.startswith("atmega48")
|
|
||||||
cmd.append(reset_hex if reset_hex is not None else ("0" if patch else base_hex))
|
|
||||||
if resume is not None:
|
if resume is not None:
|
||||||
cmd.append(resume)
|
cmd.append(resume)
|
||||||
self.log = open(dump + ".log", "a")
|
self.log = open(dump + ".log", "a")
|
||||||
|
|||||||
@@ -5,15 +5,15 @@ through flash + EEPROM + fuse + hand-over scenarios, and cross-check
|
|||||||
the tool's view against the simulator's ground-truth memory dumps.
|
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> [link]
|
<baud> <eeprom_size> <app_bin> <tool_py> <workdir>
|
||||||
|
|
||||||
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,14 +33,53 @@ 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():
|
||||||
args = sys.argv[1:]
|
(device_bin, elf, mcu, hz, base_hex, page, baud, eeprom_size, app_bin, tool, workdir) = 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)
|
||||||
@@ -51,33 +90,25 @@ def main():
|
|||||||
read_flash = os.path.join(workdir, "readback_flash.bin")
|
read_flash = os.path.join(workdir, "readback_flash.bin")
|
||||||
read_eeprom = os.path.join(workdir, "readback_eeprom.bin")
|
read_eeprom = os.path.join(workdir, "readback_eeprom.bin")
|
||||||
|
|
||||||
# The geometry the host will discover, for computing the expected image:
|
# The geometry the host will discover, for computing the expected image.
|
||||||
# the boot-sectioned megas need no vector surgery (the tinies and the
|
|
||||||
# boot-section-less m48s do), the large chips speak word addresses, and
|
|
||||||
# the page byte is the wire's 0-means-256.
|
|
||||||
mega = mcu.startswith("atmega")
|
|
||||||
patch = not mega or mcu.startswith("atmega48")
|
|
||||||
word_flash = base + 512 > 0x10000
|
|
||||||
wire_base = base // 2 if word_flash else base
|
|
||||||
flags = (1 if patch else 0) | (2 if word_flash else 0)
|
|
||||||
info = pb.Info(
|
info = pb.Info(
|
||||||
bytes([ord("P"), ord("B"), 1, 0, 0, 0, page & 0xFF])
|
bytes([ord("P"), ord("B"), 1, 0, 0, 0, page])
|
||||||
+ bytes([wire_base & 0xFF, wire_base >> 8, eeprom_size & 0xFF, eeprom_size >> 8])
|
+ bytes([base & 0xFF, base >> 8, eeprom_size & 0xFF, eeprom_size >> 8])
|
||||||
+ bytes([flags])
|
+ bytes([0 if mcu == "atmega328p" else 1])
|
||||||
)
|
)
|
||||||
|
|
||||||
device = pbsim.Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump, link=link)
|
device = Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump)
|
||||||
try:
|
try:
|
||||||
# Session 1: knock from reset, identify, program everything, stay.
|
# Session 1: knock from reset, identify, program everything, stay.
|
||||||
out = pbsim.run_tool(tool, device.pty, baud, "--info", "--fuses", "--flash", app_bin,
|
out = run_tool(tool, device.pty, baud, "--info", "--fuses", "--flash", app_bin,
|
||||||
"--eeprom", ee_path, "--stay")
|
"--eeprom", ee_path, "--stay")
|
||||||
for needed in ("signature", "fuses", "verify:", "stays"):
|
for needed in ("device: 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 = pbsim.run_tool(tool, device.pty, baud, "--verify-flash", app_bin, "--verify-eeprom", ee_path,
|
out = 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")
|
||||||
@@ -97,7 +128,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.reset()
|
device.proc.send_signal(signal.SIGUSR1)
|
||||||
port = pb.Port(device.pty, baud)
|
port = pb.Port(device.pty, baud)
|
||||||
try:
|
try:
|
||||||
loader = pb.Loader(port)
|
loader = pb.Loader(port)
|
||||||
@@ -119,9 +150,8 @@ def main():
|
|||||||
fail("loader region looks erased in the ground-truth dump")
|
fail("loader region looks erased in the ground-truth dump")
|
||||||
|
|
||||||
# The surgery, decoded independently: the patched vector must land on the
|
# The surgery, decoded independently: the patched vector must land on the
|
||||||
# loader, the trampoline on the application's own entry (patched-vector
|
# loader, the trampoline on the application's own entry.
|
||||||
# chips only — a boot-sectioned mega's word 0 stays the application's).
|
if mcu != "atmega328p":
|
||||||
if patch:
|
|
||||||
flash_words = (base + 512) // 2
|
flash_words = (base + 512) // 2
|
||||||
app = open(app_bin, "rb").read()
|
app = open(app_bin, "rb").read()
|
||||||
word0 = flash_true[0] | (flash_true[1] << 8)
|
word0 = flash_true[0] | (flash_true[1] << 8)
|
||||||
|
|||||||
@@ -5,13 +5,12 @@ replaces itself with a re-timed build through the host tool's
|
|||||||
killing the simulated device mid-write, restarting it from its flash dump,
|
killing the simulated device mid-write, restarting it from its flash dump,
|
||||||
and letting a re-run complete the update.
|
and letting a re-run complete the update.
|
||||||
|
|
||||||
The boot-sectioned megas run the BOOTRST-unprogrammed profile (reset boots
|
The mega runs the BOOTRST-unprogrammed profile (reset boots the application;
|
||||||
the application; the fixture application's 'L' jump is the application-owned
|
the fixture application's 'L' jump is the application-owned loader entry),
|
||||||
loader entry), with --assume-fuses standing in for the fuse read simavr
|
with --assume-fuses standing in for the fuse read simavr cannot model. The
|
||||||
cannot model. The patched-vector chips — the tinies and the m48s — reset
|
tinies reset into a loader at every phase by construction — the t13a because
|
||||||
into a loader at every phase by construction: the t13a because its staging
|
its staging slot carries the reset vector itself, the t85 through the word-0
|
||||||
slot carries the reset vector itself, the others through the word-0 redirect
|
redirect the tool plants around the resident rewrite.
|
||||||
the tool plants around the resident rewrite.
|
|
||||||
|
|
||||||
Usage: pbupdate.py <device_bin> <pureboot_elf> <update_elf> <mcu> <hz>
|
Usage: pbupdate.py <device_bin> <pureboot_elf> <update_elf> <mcu> <hz>
|
||||||
<base_hex> <page> <baud> <app_bin> <tool_py> <workdir>
|
<base_hex> <page> <baud> <app_bin> <tool_py> <workdir>
|
||||||
@@ -42,21 +41,10 @@ class PowerFail(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def assumed_fuses(pb, image):
|
MEGA_FUSES = "ffffffdd" # high 0xdd: BOOTSZ = 1 KB, BOOTRST unprogrammed
|
||||||
"""Synthetic 'F' bytes for --assume-fuses: the smallest boot section
|
|
||||||
covering both the resident and the staging slot (two slots — what a
|
|
||||||
self-update needs), BOOTRST unprogrammed — the per-chip BOOTSZ ladder
|
|
||||||
and fuse byte come from the tool's own table, keyed by the update
|
|
||||||
image's embedded signature."""
|
|
||||||
info = pb.image_info(image)
|
|
||||||
which, ladder = pb.BOOT_FUSE[bytes(info.signature[1:3])]
|
|
||||||
bits = min((b for b in ladder if ladder[b] * 2 >= 2 * info.slot), key=lambda b: ladder[b])
|
|
||||||
fuses = bytearray((0xFF, 0xFF, 0xFF, 0xFF))
|
|
||||||
fuses[which] = 0xF8 | (bits << 1) | 1
|
|
||||||
return bytes(fuses)
|
|
||||||
|
|
||||||
|
|
||||||
def make_fault_loader(pb, base, slot, kill_region, kill_hits, device):
|
def make_fault_loader(pb, base, kill_region, kill_hits, device):
|
||||||
"""A Loader whose write_page kills the device (or, with device=None,
|
"""A Loader whose write_page kills the device (or, with device=None,
|
||||||
just the host) at the Nth write into a region; the sequence
|
just the host) at the Nth write into a region; the sequence
|
||||||
stage->resident->stage distinguishes the install from the restore."""
|
stage->resident->stage distinguishes the install from the restore."""
|
||||||
@@ -71,7 +59,7 @@ def make_fault_loader(pb, base, slot, kill_region, kill_hits, device):
|
|||||||
if address >= base:
|
if address >= base:
|
||||||
phase = "resident"
|
phase = "resident"
|
||||||
self.seen_resident = True
|
self.seen_resident = True
|
||||||
elif address >= base - slot:
|
elif address >= base - 512:
|
||||||
phase = "stage_restore" if self.seen_resident else "stage"
|
phase = "stage_restore" if self.seen_resident else "stage"
|
||||||
else:
|
else:
|
||||||
phase = "app"
|
phase = "app"
|
||||||
@@ -89,15 +77,8 @@ def make_fault_loader(pb, base, slot, kill_region, kill_hits, device):
|
|||||||
def main():
|
def main():
|
||||||
(device_bin, elf, update_elf, mcu, hz, base_hex, page, baud, app_bin, tool, workdir) = sys.argv[1:]
|
(device_bin, elf, update_elf, mcu, hz, base_hex, page, baud, app_bin, tool, workdir) = sys.argv[1:]
|
||||||
base, page, baud = int(base_hex, 0), int(page), int(baud)
|
base, page, baud = int(base_hex, 0), int(page), int(baud)
|
||||||
mega = mcu.startswith("atmega")
|
mega = mcu == "atmega328p"
|
||||||
# The m48s are megas without a boot section: patched vector, no fuse
|
reset_hex = "0" if mega else None # the mega runs BOOTRST-unprogrammed here
|
||||||
# preflight, and the same reset-to-0 the tinies get.
|
|
||||||
patch = not mega or mcu.startswith("atmega48")
|
|
||||||
# Word-addressed (>64 KiB) chips use the 1 KiB slot; their loader base
|
|
||||||
# itself sits beyond the 16-bit byte space — the 644's base + slot only
|
|
||||||
# touches the 64 KiB boundary and stays byte-addressed.
|
|
||||||
slot = 1024 if base >= 0x10000 and mega else 512
|
|
||||||
reset_hex = "0" if mega else None # the boot-sectioned mega runs BOOTRST-unprogrammed here
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
|
sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
import pbsim
|
import pbsim
|
||||||
@@ -114,7 +95,7 @@ def main():
|
|||||||
fail("the update image is byte-identical to the resident build")
|
fail("the update image is byte-identical to the resident build")
|
||||||
dump = os.path.join(workdir, "dump.bin")
|
dump = os.path.join(workdir, "dump.bin")
|
||||||
state = os.path.join(workdir, "update.pbstate")
|
state = os.path.join(workdir, "update.pbstate")
|
||||||
fuses = assumed_fuses(pb, images["v0"]) if mega and not patch else None
|
fuses = bytes.fromhex(MEGA_FUSES) if mega else None
|
||||||
|
|
||||||
def connect(device):
|
def connect(device):
|
||||||
port = pb.Port(device.pty, baud)
|
port = pb.Port(device.pty, baud)
|
||||||
@@ -128,16 +109,16 @@ def main():
|
|||||||
return port, loader
|
return port, loader
|
||||||
|
|
||||||
def padded(image):
|
def padded(image):
|
||||||
return image + b"\xff" * (slot - len(image))
|
return image + b"\xff" * (512 - len(image))
|
||||||
|
|
||||||
def resident_bytes(loader):
|
def resident_bytes(loader):
|
||||||
return loader.read_flash(base, slot)
|
return loader.read_flash(base, 256) + loader.read_flash(base + 256, 256)
|
||||||
|
|
||||||
def assert_state(loader, image, app_pages):
|
def assert_state(loader, image, app_pages):
|
||||||
if resident_bytes(loader) != padded(image):
|
if resident_bytes(loader) != padded(image):
|
||||||
fail("resident loader does not match the update image")
|
fail("resident loader does not match the update image")
|
||||||
stage = base - slot
|
stage = base - 512
|
||||||
got = loader.read_flash(stage, slot)
|
got = loader.read_flash(stage, 256) + loader.read_flash(stage + 256, 256)
|
||||||
for address, data in app_pages.items():
|
for address, data in app_pages.items():
|
||||||
if stage <= address < base:
|
if stage <= address < base:
|
||||||
if got[address - stage : address - stage + page] != data:
|
if got[address - stage : address - stage + page] != data:
|
||||||
@@ -154,8 +135,8 @@ def main():
|
|||||||
|
|
||||||
# A clean CLI update, resident -> v9.
|
# A clean CLI update, resident -> v9.
|
||||||
args = ["--update-loader", os.path.join(workdir, "v9.bin"), "--state", state, "--stay"]
|
args = ["--update-loader", os.path.join(workdir, "v9.bin"), "--state", state, "--stay"]
|
||||||
if fuses:
|
if mega:
|
||||||
args += ["--assume-fuses", fuses.hex()]
|
args += ["--assume-fuses", MEGA_FUSES]
|
||||||
out = pbsim.run_tool(tool, device.pty, baud, *args)
|
out = pbsim.run_tool(tool, device.pty, baud, *args)
|
||||||
if "loader updated" not in out:
|
if "loader updated" not in out:
|
||||||
fail("update did not report success")
|
fail("update did not report success")
|
||||||
@@ -179,14 +160,14 @@ def main():
|
|||||||
# cost of that profile (README).
|
# cost of that profile (README).
|
||||||
for kill_region, kill_hits, kill_device in (
|
for kill_region, kill_hits, kill_device in (
|
||||||
("stage", 2, True),
|
("stage", 2, True),
|
||||||
("resident", 1, patch),
|
("resident", 1, not mega),
|
||||||
("stage_restore", 2, True),
|
("stage_restore", 2, True),
|
||||||
):
|
):
|
||||||
device.reset() # the previous round left the application running
|
device.reset() # the previous round left the application running
|
||||||
port, loader = connect(device)
|
port, loader = connect(device)
|
||||||
target = "v9" if resident_bytes(loader) == padded(images["v0"]) else "v0"
|
target = "v9" if resident_bytes(loader) == padded(images["v0"]) else "v0"
|
||||||
image_path = os.path.join(workdir, target + ".bin")
|
image_path = os.path.join(workdir, target + ".bin")
|
||||||
injected = make_fault_loader(pb, base, slot, kill_region, kill_hits, device if kill_device else None)(port)
|
injected = make_fault_loader(pb, base, kill_region, kill_hits, device if kill_device else None)(port)
|
||||||
injected.info = loader.info
|
injected.info = loader.info
|
||||||
try:
|
try:
|
||||||
pb.op_update_loader(injected, 25, image_path, state, fuses)
|
pb.op_update_loader(injected, 25, image_path, state, fuses)
|
||||||
@@ -210,12 +191,12 @@ def main():
|
|||||||
device.stop()
|
device.stop()
|
||||||
|
|
||||||
# Ground truth: the simulator's own flash against the final state, and
|
# Ground truth: the simulator's own flash against the final state, and
|
||||||
# on the patched-vector chips an independent decode of the reset routing.
|
# on the tinies an independent decode of the reset routing.
|
||||||
flash = open(dump, "rb").read()
|
flash = open(dump, "rb").read()
|
||||||
if flash[base : base + slot] != padded(images[final]):
|
if flash[base : base + 512] != padded(images[final]):
|
||||||
fail("ground-truth resident region does not match the final image")
|
fail("ground-truth resident region does not match the final image")
|
||||||
if patch:
|
if not mega:
|
||||||
flash_words = (base + slot) // 2
|
flash_words = (base + 512) // 2
|
||||||
word0 = flash[0] | (flash[1] << 8)
|
word0 = flash[0] | (flash[1] << 8)
|
||||||
if rjmp_decode(word0, 0, flash_words) != base // 2:
|
if rjmp_decode(word0, 0, flash_words) != base // 2:
|
||||||
fail("ground-truth reset vector does not land on the loader")
|
fail("ground-truth reset vector does not land on the loader")
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
// simavr "device" for the pureboot protocol tests, every chip. Loads the
|
// simavr "device" for the pureboot protocol tests, all three chips. Loads
|
||||||
// boot-linked ELF at the loader base, starts execution there (BOOTRST / the
|
// the boot-linked ELF at the loader base, starts execution there (BOOTRST /
|
||||||
// patched vector are not what is under test), and exposes the loader's
|
// the 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:
|
||||||
//
|
//
|
||||||
// - Hardware USART builds: simavr's uart_pty on the selected instance.
|
// - ATmega328P: the hardware USART0 through simavr's uart_pty.
|
||||||
// - Software UART builds: an 8N1 bridge between a pty and the GPIO pins,
|
// - Tinies: an 8N1 bridge between a pty and the GPIO software UART
|
||||||
// timed against the simulated cycle counter (drives the loader's RX,
|
// (drives PB0, the loader's RX; decodes PB1, its TX), timed against the
|
||||||
// decodes its TX).
|
// simulated cycle counter.
|
||||||
//
|
|
||||||
// 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
|
||||||
@@ -42,44 +38,17 @@
|
|||||||
|
|
||||||
static avr_t *avr;
|
static avr_t *avr;
|
||||||
static uart_pty_t uart_pty;
|
static uart_pty_t uart_pty;
|
||||||
static int link_software;
|
static int use_uart_pty;
|
||||||
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
|
||||||
// anywhere inside the page wipes half the neighbouring page in simulation
|
// anywhere inside the page wipes half the neighbouring page in simulation
|
||||||
// only. Wrap the mega's registered flash ioctl and re-dispatch page erases
|
// only. Wrap the mega's registered flash ioctl and re-dispatch page erases
|
||||||
// with Z forced to the page boundary; everything else passes through.
|
// with Z forced to the page boundary; everything else passes through.
|
||||||
//
|
|
||||||
// A second gap on the boot-section-less m48s: their RWWSRE bit is the
|
|
||||||
// temporary-buffer discard (Atmel-8271 §26.2/§26.3.1), but the stock model
|
|
||||||
// gates its RWWSRE branch on AVR_SELFPROG_HAVE_RWW — absent on the m48
|
|
||||||
// core — so the discard store falls through into the buffer-fill branch and
|
|
||||||
// plants whatever Z/R1:R0 happen to hold. Perform the silicon's discard
|
|
||||||
// here instead.
|
|
||||||
static avr_flash_t *mega_flash;
|
static avr_flash_t *mega_flash;
|
||||||
static int (*mega_flash_ioctl)(avr_io_t *io, uint32_t ctl, void *param);
|
static int (*mega_flash_ioctl)(avr_io_t *io, uint32_t ctl, void *param);
|
||||||
|
|
||||||
@@ -95,15 +64,6 @@ static int fixed_flash_ioctl(avr_io_t *io, uint32_t ctl, void *param)
|
|||||||
io->avr->data[31] = (uint8_t)(z >> 8);
|
io->avr->data[31] = (uint8_t)(z >> 8);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
if (ctl == AVR_IOCTL_FLASH_SPM && !(mega_flash->flags & AVR_SELFPROG_HAVE_RWW) &&
|
|
||||||
(io->avr->data[mega_flash->r_spm] & 0x11) == 0x11) { // RWWSRE|SELFPRGEN: the m48 buffer discard
|
|
||||||
for (int i = 0; i < mega_flash->spm_pagesize / 2; i++) {
|
|
||||||
mega_flash->tmppage[i] = 0xffff;
|
|
||||||
mega_flash->tmppage_used[i] = 0;
|
|
||||||
}
|
|
||||||
avr_regbit_clear(io->avr, mega_flash->selfprgen);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return mega_flash_ioctl(io, ctl, param);
|
return mega_flash_ioctl(io, ctl, param);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,42 +257,29 @@ static void finish(int sig)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!link_software)
|
if (use_uart_pty)
|
||||||
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[])
|
||||||
{
|
{
|
||||||
int link_given = 0;
|
if (argc < 8 || argc > 10) {
|
||||||
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 [-l link] <pureboot.elf> <mcu> <hz> <base_hex> <page> <baud> <flash_dump>"
|
"usage: %s <pureboot.elf> <mcu> <hz> <base_hex> <page> <baud> <flash_dump>"
|
||||||
" [reset_hex] [resume_flash]\n"
|
" [reset_hex] [resume_flash]\n"
|
||||||
" -l link: usart0 | usart1 | sw[:B0,B1] (RX,TX); default: the chip's own\n"
|
" reset_hex: reset vector (default: base on the mega, 0 on the tinies)\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];
|
||||||
int is_mega = strncmp(mcu_name, "atmega", 6) == 0;
|
use_uart_pty = strcmp(mcu_name, "atmega328p") == 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) {
|
||||||
@@ -343,7 +290,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 (args > 8) {
|
if (argc > 9) {
|
||||||
// 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) {
|
||||||
@@ -359,13 +306,11 @@ int main(int argc, char *argv[])
|
|||||||
}
|
}
|
||||||
memcpy(avr->flash + base, fw.flash, fw.flashsize);
|
memcpy(avr->flash + base, fw.flash, fw.flashsize);
|
||||||
}
|
}
|
||||||
// The boot-sectioned megas enter the loader in hardware (BOOTRST, not
|
// The mega enters the loader in hardware (BOOTRST, not modeled — the
|
||||||
// modeled — the argument picks the modeled fuse's target); the tinies
|
// argument picks the modeled fuse's target); the tinies reset to word 0
|
||||||
// and the boot-section-less m48s reset to word 0 like silicon — erased
|
// like silicon — erased flash walks up into the loader, and after the
|
||||||
// flash walks up into the loader, and after the host's surgery the
|
// host's surgery the patched vector routes there.
|
||||||
// patched vector routes there.
|
reset_pc = argc > 8 ? (uint32_t)strtoul(argv[8], NULL, 0) : (use_uart_pty ? base : 0);
|
||||||
int boot_section = is_mega && strncmp(mcu_name, "atmega48", 8) != 0;
|
|
||||||
reset_pc = args > 7 ? (uint32_t)strtoul(argv[8], NULL, 0) : (boot_section ? base : 0);
|
|
||||||
avr->pc = reset_pc;
|
avr->pc = reset_pc;
|
||||||
avr->codeend = avr->flashend;
|
avr->codeend = avr->flashend;
|
||||||
|
|
||||||
@@ -378,34 +323,27 @@ int main(int argc, char *argv[])
|
|||||||
avr_ioctl(avr, AVR_IOCTL_EEPROM_SET, &seed);
|
avr_ioctl(avr, AVR_IOCTL_EEPROM_SET, &seed);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The megas carry simavr's avr_flash module (and its two gaps the wrap
|
if (use_uart_pty) {
|
||||||
// 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(sw_rx_port), (unsigned)sw_rx_bit);
|
rx_pin = avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ('B'), 0);
|
||||||
avr_irq_register_notify(avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ(sw_tx_port), (unsigned)sw_tx_bit), tx_hook,
|
avr_irq_register_notify(avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ('B'), 1), tx_hook, NULL);
|
||||||
NULL);
|
|
||||||
avr_raise_irq(rx_pin, 1); // idle line
|
avr_raise_irq(rx_pin, 1); // idle line
|
||||||
|
|
||||||
int slave;
|
int slave;
|
||||||
@@ -433,27 +371,18 @@ 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 (!link_software) { // reset restores the pacing hack; re-clear it
|
if (use_uart_pty) { // reset restores the pacing hack; re-clear it
|
||||||
uint32_t flags = 0;
|
uint32_t flags = 0;
|
||||||
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS(uart_digit), &flags);
|
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS('0'), &flags);
|
||||||
flags &= ~AVR_UART_FLAG_POLL_SLEEP;
|
flags &= ~AVR_UART_FLAG_POLL_SLEEP;
|
||||||
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS(uart_digit), &flags);
|
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS('0'), &flags);
|
||||||
} else {
|
} else {
|
||||||
bridge_reset();
|
bridge_reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (link_software && ++since_poll >= 2000) {
|
if (!use_uart_pty && ++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);
|
||||||
|
|||||||
@@ -27,12 +27,9 @@ def expect_error(what, fn, *needles):
|
|||||||
fail(f"{what}: no error raised")
|
fail(f"{what}: no error raised")
|
||||||
|
|
||||||
|
|
||||||
def info_of(pb, base, page, patch, flash, signature=(0x1E, 0x93, 0x0B), word_flash=False):
|
def info_of(pb, base, page, patch, flash):
|
||||||
scale = 2 if word_flash else 1
|
raw = bytes((0x50, 0x42, 1, 0x1E, 0x93, 0x0B, page, base & 0xFF, base >> 8,
|
||||||
wire_base = base // scale
|
0, 2, 1 if patch else 0))
|
||||||
flags = (1 if patch else 0) | (2 if word_flash else 0)
|
|
||||||
raw = bytes((0x50, 0x42, 1, *signature, page & 0xFF, wire_base & 0xFF, wire_base >> 8,
|
|
||||||
0, 2, flags))
|
|
||||||
info = pb.Info(raw)
|
info = pb.Info(raw)
|
||||||
assert info.flash_size == flash
|
assert info.flash_size == flash
|
||||||
return info
|
return info
|
||||||
@@ -53,52 +50,16 @@ def main():
|
|||||||
import pureboot as pb
|
import pureboot as pb
|
||||||
|
|
||||||
tiny = info_of(pb, 0x1E00, 64, True, 0x2000)
|
tiny = info_of(pb, 0x1E00, 64, True, 0x2000)
|
||||||
mega = info_of(pb, 0x7E00, 128, False, 0x8000, signature=(0x1E, 0x95, 0x0F))
|
mega = info_of(pb, 0x7E00, 128, False, 0x8000)
|
||||||
|
|
||||||
# mega_boot: BOOTSZ words and the BOOTRST sense per chip — the fuse byte
|
# mega_boot: BOOTSZ words and the BOOTRST sense, DS40002061B §27.
|
||||||
# index (EXTENDED on the x8 line except the m328s' HIGH, HIGH elsewhere)
|
for bits, start in ((0b11, 0x7E00), (0b10, 0x7C00), (0b01, 0x7800), (0b00, 0x7000)):
|
||||||
# and the per-family ladders (Atmel-2486/2466/2503/2545/8271/DS40002065/
|
prog, at = pb.mega_boot((0xF8 | (bits << 1)) & ~1)
|
||||||
# 8272/8011/2593/42719). Synthetic 'F' replies: only the boot byte
|
|
||||||
# carries meaning.
|
|
||||||
cases = (
|
|
||||||
((0x1E, 0x93, 0x07), 0x2000, 3, {0b11: 0x1F00, 0b10: 0x1E00, 0b01: 0x1C00, 0b00: 0x1800}), # m8
|
|
||||||
((0x1E, 0x94, 0x03), 0x4000, 3, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m16
|
|
||||||
((0x1E, 0x95, 0x02), 0x8000, 3, {0b11: 0x7E00, 0b10: 0x7C00, 0b01: 0x7800, 0b00: 0x7000}), # m32
|
|
||||||
((0x1E, 0x93, 0x0A), 0x2000, 2, {0b11: 0x1F00, 0b10: 0x1E00, 0b01: 0x1C00, 0b00: 0x1800}), # m88
|
|
||||||
((0x1E, 0x93, 0x0F), 0x2000, 2, {0b11: 0x1F00, 0b10: 0x1E00, 0b01: 0x1C00, 0b00: 0x1800}), # m88P
|
|
||||||
((0x1E, 0x94, 0x06), 0x4000, 2, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m168/168A
|
|
||||||
((0x1E, 0x94, 0x0B), 0x4000, 2, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m168P
|
|
||||||
((0x1E, 0x95, 0x14), 0x8000, 3, {0b11: 0x7E00, 0b10: 0x7C00, 0b01: 0x7800, 0b00: 0x7000}), # m328
|
|
||||||
((0x1E, 0x95, 0x0F), 0x8000, 3, {0b11: 0x7E00, 0b10: 0x7C00, 0b01: 0x7800, 0b00: 0x7000}), # m328P
|
|
||||||
((0x1E, 0x94, 0x0F), 0x4000, 3, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m164A
|
|
||||||
((0x1E, 0x94, 0x0A), 0x4000, 3, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m164P
|
|
||||||
((0x1E, 0x95, 0x15), 0x8000, 3, {0b11: 0x7E00, 0b10: 0x7C00, 0b01: 0x7800, 0b00: 0x7000}), # m324A
|
|
||||||
((0x1E, 0x96, 0x09), 0x10000, 3, {0b11: 0xFC00, 0b10: 0xF800, 0b01: 0xF000, 0b00: 0xE000}), # m644
|
|
||||||
((0x1E, 0x96, 0x0A), 0x10000, 3, {0b11: 0xFC00, 0b10: 0xF800, 0b01: 0xF000, 0b00: 0xE000}), # m644P
|
|
||||||
((0x1E, 0x97, 0x06), 0x20000, 3, {0b11: 0x1FC00, 0b10: 0x1F800, 0b01: 0x1F000, 0b00: 0x1E000}), # 1284
|
|
||||||
((0x1E, 0x97, 0x05), 0x20000, 3, {0b11: 0x1FC00, 0b10: 0x1F800, 0b01: 0x1F000, 0b00: 0x1E000}), # 1284P
|
|
||||||
)
|
|
||||||
for signature, flash, which, ladder in cases:
|
|
||||||
# Word-addressed chips carry the 1 KiB slot (their smallest boot sector).
|
|
||||||
slot = 1024 if flash > 0x10000 else 512
|
|
||||||
chip = info_of(pb, flash - slot, 128 if flash < 0x20000 else 0, False, flash,
|
|
||||||
signature=signature, word_flash=flash > 0x10000)
|
|
||||||
for bits, start in ladder.items():
|
|
||||||
fuses = bytearray((0xFF, 0xFF, 0xFF, 0xFF))
|
|
||||||
fuses[which] = (0xF8 | (bits << 1)) & ~1
|
|
||||||
prog, at = pb.mega_boot(chip, bytes(fuses))
|
|
||||||
if not prog or at != start:
|
if not prog or at != start:
|
||||||
fail(f"mega_boot {signature[1]:02x}{signature[2]:02x} BOOTSZ={bits:02b} programmed: {prog} {at:#07x}")
|
fail(f"mega_boot BOOTSZ={bits:02b} programmed: {prog} {at:#06x}")
|
||||||
fuses[which] |= 1
|
prog, at = pb.mega_boot(0xF8 | (bits << 1) | 1)
|
||||||
prog, at = pb.mega_boot(chip, bytes(fuses))
|
|
||||||
if prog or at != start:
|
if prog or at != start:
|
||||||
fail(f"mega_boot {signature[1]:02x}{signature[2]:02b} unprogrammed: {prog} {at:#07x}")
|
fail(f"mega_boot BOOTSZ={bits:02b} unprogrammed: {prog} {at:#06x}")
|
||||||
|
|
||||||
# Word-addressed info decode: the 1284P's base/page ride the wire scaled,
|
|
||||||
# and its slot is 1 KiB.
|
|
||||||
big = info_of(pb, 0x1FC00, 0, False, 0x20000, signature=(0x1E, 0x97, 0x05), word_flash=True)
|
|
||||||
if big.page != 256 or big.base != 0x1FC00 or big.stage != 0x1F800 or big.slot != 1024:
|
|
||||||
fail(f"word-addressed info decode: page {big.page}, base {big.base:#x}, stage {big.stage:#x}")
|
|
||||||
|
|
||||||
# Surgery: word 0 lands on the loader, the trampoline on the original
|
# Surgery: word 0 lands on the loader, the trampoline on the original
|
||||||
# entry — checked with an independent decoder.
|
# entry — checked with an independent decoder.
|
||||||
@@ -155,21 +116,6 @@ def main():
|
|||||||
if pb.image_info(bytes((0xAA,)) * 40) is not None:
|
if pb.image_info(bytes((0xAA,)) * 40) is not None:
|
||||||
fail("image_info invents a block")
|
fail("image_info invents a block")
|
||||||
|
|
||||||
# loader_image must peel a padded image down to the slot content: a raw
|
|
||||||
# .bin padded from address 0 (or a whole-flash read-back with the loader
|
|
||||||
# resident at base) yields the same bytes as the bare slot image.
|
|
||||||
import tempfile
|
|
||||||
slot_image = bytes((0xAA,)) * 10 + tiny.raw + bytes((0xCC,)) * 40
|
|
||||||
padded = bytes((0xFF,)) * tiny.base + slot_image
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f:
|
|
||||||
f.write(padded)
|
|
||||||
padded_path = f.name
|
|
||||||
try:
|
|
||||||
if pb.loader_image(padded_path) != slot_image:
|
|
||||||
fail("loader_image does not peel a padded image to the slot content")
|
|
||||||
finally:
|
|
||||||
os.unlink(padded_path)
|
|
||||||
|
|
||||||
# Update preflight: the full fuse matrix, plus target mismatch.
|
# Update preflight: the full fuse matrix, plus target mismatch.
|
||||||
other = info_of(pb, 0x1E00, 32, True, 0x2000)
|
other = info_of(pb, 0x1E00, 32, True, 0x2000)
|
||||||
expect_error("wrong-target image", lambda: pb.update_preflight(binary, other, None), "another target")
|
expect_error("wrong-target image", lambda: pb.update_preflight(binary, other, None), "another target")
|
||||||
@@ -204,52 +150,6 @@ def main():
|
|||||||
pb.check_walk_region({0x7800: bytes((0xFF,)) * 128}, mega, fuses(0xFA), False)
|
pb.check_walk_region({0x7800: bytes((0xFF,)) * 128}, mega, fuses(0xFA), False)
|
||||||
pb.check_walk_region(deep, mega, None, False) # fuses unknown: no check
|
pb.check_walk_region(deep, mega, None, False) # fuses unknown: no check
|
||||||
|
|
||||||
# The repairing verify: a mismatched page is rewritten rather than raised,
|
|
||||||
# bounded so a fault that is not self-clearing cannot spin.
|
|
||||||
class FakeLoader:
|
|
||||||
"""A device whose first `bad` writes of any page land wrong."""
|
|
||||||
|
|
||||||
def __init__(self, info, bad):
|
|
||||||
self.info = info
|
|
||||||
self.bad = bad
|
|
||||||
self.flash = {}
|
|
||||||
self.writes = 0
|
|
||||||
|
|
||||||
def write_page(self, address, data):
|
|
||||||
self.writes += 1
|
|
||||||
self.flash[address] = bytes(len(data)) if self.bad > 0 else bytes(data)
|
|
||||||
self.bad -= 1
|
|
||||||
|
|
||||||
def read_flash(self, address, count):
|
|
||||||
return self.flash.get(address, bytes(count))
|
|
||||||
|
|
||||||
want = {0: bytes((i * 5) & 0xFF for i in range(128))}
|
|
||||||
|
|
||||||
# One bad write, then good: repaired in place, and the caller never sees
|
|
||||||
# an error. The rewrite is counted, so a silent no-op cannot pass.
|
|
||||||
device = FakeLoader(info_of(pb, 0x7E00, 128, False, 0x8000), bad=1)
|
|
||||||
device.write_page(0, want[0])
|
|
||||||
pb.verify_pages(device, want, repair=True)
|
|
||||||
if device.writes != 2:
|
|
||||||
fail(f"repairing verify made {device.writes} writes, expected 2")
|
|
||||||
|
|
||||||
# Without repair the same state raises, so the repair is what fixed it.
|
|
||||||
device = FakeLoader(info_of(pb, 0x7E00, 128, False, 0x8000), bad=1)
|
|
||||||
device.write_page(0, want[0])
|
|
||||||
expect_error("verify without repair", lambda: pb.verify_pages(device, want), "verify failed")
|
|
||||||
|
|
||||||
# A page that never comes good stops after RETRIES rewrites, and says so.
|
|
||||||
device = FakeLoader(info_of(pb, 0x7E00, 128, False, 0x8000), bad=99)
|
|
||||||
device.write_page(0, want[0])
|
|
||||||
expect_error(
|
|
||||||
"unrepairable page",
|
|
||||||
lambda: pb.verify_pages(device, want, repair=True),
|
|
||||||
"verify failed",
|
|
||||||
f"after {pb.RETRIES} retries",
|
|
||||||
)
|
|
||||||
if device.writes != pb.RETRIES + 1:
|
|
||||||
fail(f"unrepairable page took {device.writes} writes, expected {pb.RETRIES + 1}")
|
|
||||||
|
|
||||||
print("test_planner: all planner and policy checks pass")
|
print("test_planner: all planner and policy checks pass")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
#!/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"
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
#!/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