3 Commits

Author SHA1 Message Date
2e9a50d030 build: generated presets and a check entry point
tools/make_presets.py emits the uniform pipeline the hand-grown file had
drifted from — generated configure/build/test presets and workflows for
all 37 chips, reflect configure/build for libavr's 12-chip spot set —
and tools/check.sh runs every chip's workflow (--full adds the reflect
spot) as the port's gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019553N6pLmaKhxEwkiCs3RX
2026-07-21 23:42:57 +02:00
860e6c1d47 host: one fact per line, progress bars, --verbose
--info prints the decoded info block field by field and --fuses each
byte on its own line plus the BOOTSZ/BOOTRST meaning on boot-sectioned
megas. Transfers that take wire time draw a transient progress bar on
stderr when it is a tty — logs, pipes and the tests see only the
summary lines. -v/--verbose narrates decisions: knock counts, the
programming plan, update state handling and per-phase page counts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019553N6pLmaKhxEwkiCs3RX
2026-07-21 23:42:57 +02:00
21c098978b pureboot: every deployment axis is a build parameter
Clock, baud, serial backend (hardware USART 0/1 or the software UART on
any pins) and the activation window all resolve through one CMake
function, pureboot_add_loader() in pureboot/CMakeLists.txt — the unit a
downstream project consumes. The default baud is the fastest standard
rate within 2.5 % (the same best-divisor search libavr's solver runs),
gated on software builds by the polled receiver's 100-cycles-a-bit
floor; every explicit pick is re-checked by the compile's static asserts.

The size matrix builds each axis that can move the image — backend x
clock ladder x USART instance, per chip — against the slot budget, and
two nondefault deployments run the whole protocol suite live: the 328P
on its shipped 1 MHz fuses over software serial on TX=PB1/RX=PB5
(pureboot.custom), and the 644A over USART1 (pureboot.usart1). The sim
runner takes -l to bridge any link, paces a fully quiet bridge toward
real time (a free-running 8 M-cycle window loses the reset-race knock),
and the fixture application speaks the deployment it is built for.

The loader itself shed bytes on the way: the return-address high byte
spelled through byteswap (the double swap folds to the one-byte pick),
the info-block address composed instead of bit_cast, and libavr's new
polled-UART helpers replacing the port's uart::detail reaches. Every
combination fits: 458-506 B across the megas' whole matrix, 470-484 B
on the tinies, 556-562 B in the 1284s' 1 KiB slot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019553N6pLmaKhxEwkiCs3RX
2026-07-21 23:42:57 +02:00
12 changed files with 2230 additions and 1729 deletions

View File

@@ -126,154 +126,31 @@ if(LIBAVR_MCU STREQUAL "atmega328p")
endif() endif()
# pureboot — the pure-constraint port (see pureboot/README.md): one source, # pureboot — the pure-constraint port (see pureboot/README.md): one source,
# no inline assembly, no global register variables, every libavr chip, 512 # no inline assembly, no global register variables, every libavr chip,
# bytes each. The loader owns the top 512 bytes of flash on every chip; the # fitting each chip's smallest boot sector. The geometry and the
# application entry symbol is address 0 on the mega (reset re-vectors to the # pureboot_add_loader() deployment function live in pureboot/CMakeLists.txt —
# loader through BOOTRST, so word 0 stays the application's own vector) and # the unit a downstream project consumes; everything below is this port's
# the trampoline word just below the loader on the tinies (host-side vector # own build: the stock loaders, their tests, and the size matrix. The
# surgery points it at the application). --pmem-wrap-around models AVR's # distinct binary dir keeps the `pureboot` target's output name free.
# modulo-flash PC where the flash is big enough to need it. add_subdirectory(pureboot pureboot-cmake)
#
# The image is position-independent (check_pi.py asserts the two link-time # The stock loader: the family-default deployment (crystal/RC clock, the
# facts that make it so), and on the tinies its budget is 510, not 512: the # chip's natural link, default pins). The activation window stays a cache
# slot's last word is the trampoline the host composes — the resident slot's # variable — re-timing a deployed loader is a self-update with a re-timed
# holds the application entry, and a staging copy's holds the jump through # build. pureboot9 is that re-timed build, and what the update test installs.
# which it reaches the loader it installed. The activation window is a
# compile-time constant; a different PUREBOOT_TIMEOUT builds the re-timed
# binary a self-update then installs.
set(PUREBOOT_TIMEOUT 8 CACHE STRING "pureboot activation window, seconds") set(PUREBOOT_TIMEOUT 8 CACHE STRING "pureboot activation window, seconds")
# Per-family geometry. The boot-sectioned megas run the loader from the pureboot_add_loader(pureboot TIMEOUT ${PUREBOOT_TIMEOUT})
# hardware boot section and boot the application at word 0; the tinies and
# the boot-section-less m48s get the trampoline surgery. All megas assume a
# 16 MHz crystal at 115200 Bd; the tinies their internal RC at 57600 Bd over
# the software UART. --pmem-wrap-around models AVR's modulo-flash PC where
# the flash is big enough to need it (an rjmp reaches all of 4 KiB by
# itself).
if(LIBAVR_MCU MATCHES "^attiny13a?$")
set(_pb_flash 1024)
set(_pb_wrap "")
set(_pb_page 32)
set(_pb_hz 9600000)
set(_pb_baud 57600)
set(_pb_eeprom 64)
elseif(LIBAVR_MCU STREQUAL "attiny25")
set(_pb_flash 2048)
set(_pb_wrap "")
set(_pb_page 32)
set(_pb_hz 8000000)
set(_pb_baud 57600)
set(_pb_eeprom 128)
elseif(LIBAVR_MCU STREQUAL "attiny45")
set(_pb_flash 4096)
set(_pb_wrap "")
set(_pb_page 64)
set(_pb_hz 8000000)
set(_pb_baud 57600)
set(_pb_eeprom 256)
elseif(LIBAVR_MCU STREQUAL "attiny85")
set(_pb_flash 8192)
set(_pb_wrap -Wl,--pmem-wrap-around=8k)
set(_pb_page 64)
set(_pb_hz 8000000)
set(_pb_baud 57600)
set(_pb_eeprom 512)
elseif(LIBAVR_MCU MATCHES "^atmega48(a|p|pa)?$")
set(_pb_flash 4096)
set(_pb_wrap "")
set(_pb_page 64)
set(_pb_hz 16000000)
set(_pb_baud 115200)
set(_pb_eeprom 256)
elseif(LIBAVR_MCU MATCHES "^atmega8a?$" OR LIBAVR_MCU MATCHES "^atmega88(a|p|pa)?$")
set(_pb_flash 8192)
set(_pb_wrap -Wl,--pmem-wrap-around=8k)
set(_pb_page 64)
set(_pb_hz 16000000)
set(_pb_baud 115200)
set(_pb_eeprom 512)
elseif(LIBAVR_MCU MATCHES "^atmega16a?$" OR LIBAVR_MCU MATCHES "^atmega168(a|p|pa)?$" OR
LIBAVR_MCU MATCHES "^atmega164(a|p|pa)$")
set(_pb_flash 16384)
set(_pb_wrap -Wl,--pmem-wrap-around=16k)
set(_pb_page 128)
set(_pb_hz 16000000)
set(_pb_baud 115200)
set(_pb_eeprom 512)
elseif(LIBAVR_MCU MATCHES "^atmega32a?$" OR LIBAVR_MCU MATCHES "^atmega328p?$" OR
LIBAVR_MCU MATCHES "^atmega324(a|p|pa)$")
set(_pb_flash 32768)
set(_pb_wrap -Wl,--pmem-wrap-around=32k)
set(_pb_page 128)
set(_pb_hz 16000000)
set(_pb_baud 115200)
set(_pb_eeprom 1024)
elseif(LIBAVR_MCU MATCHES "^atmega644(a|p|pa)?$")
# 64 KiB is exactly the 16-bit byte space: plain LPM reaches everything,
# and the smallest boot section (1 KiB) holds the loader and its staging
# slot together (see pureboot/README.md).
set(_pb_flash 65536)
set(_pb_wrap -Wl,--pmem-wrap-around=64k)
set(_pb_page 256)
set(_pb_hz 16000000)
set(_pb_baud 115200)
set(_pb_eeprom 2048)
elseif(LIBAVR_MCU MATCHES "^atmega1284p?$")
# 128 KiB: wire flash addresses are word addresses, reads go through
# ELPM, and the PC's modulo wrap exceeds what --pmem-wrap-around models.
# The slot is 1 KiB — this chip's own smallest boot sector; the far
# machinery cannot fit 512 B (see pureboot/README.md).
set(_pb_flash 131072)
set(_pb_wrap "")
set(_pb_page 256)
set(_pb_hz 16000000)
set(_pb_baud 115200)
set(_pb_eeprom 4096)
set(_pb_slot 1024)
set(_pb_limit 1024)
else()
message(FATAL_ERROR "pureboot: no geometry for ${LIBAVR_MCU}")
endif()
if(NOT DEFINED _pb_slot)
set(_pb_slot 512)
endif()
math(EXPR _pb_base "${_pb_flash} - ${_pb_slot}")
math(EXPR _pb_base_hex "${_pb_base}" OUTPUT_FORMAT HEXADECIMAL)
# Patched-vector chips hand over through the trampoline word below the slot,
# which is also the slot's own last word — their budget is slot 2.
if(LIBAVR_MCU MATCHES "^atmega" AND NOT LIBAVR_MCU MATCHES "^atmega48")
set(_pb_app 0)
if(NOT DEFINED _pb_limit)
set(_pb_limit ${_pb_slot})
endif()
else()
math(EXPR _pb_app "${_pb_base} - 2")
math(EXPR _pb_limit "${_pb_slot} - 2")
endif()
# simavr names its cores after the base dies; the A revisions run on them
# (the 644PA on the 644P core).
set(_pb_sim_mcu ${LIBAVR_MCU})
if(LIBAVR_MCU MATCHES "^atmega(8|16|32|48|88|164|168|644)a$")
string(REGEX REPLACE "a$" "" _pb_sim_mcu ${LIBAVR_MCU})
elseif(LIBAVR_MCU STREQUAL "atmega644pa")
set(_pb_sim_mcu atmega644p)
endif()
add_executable(pureboot pureboot/pureboot.cpp)
target_link_libraries(pureboot PRIVATE libavr)
target_compile_definitions(pureboot PRIVATE PUREBOOT_TIMEOUT=${PUREBOOT_TIMEOUT})
target_link_options(pureboot PRIVATE -nostartfiles -Wl,--section-start=.text=${_pb_base_hex}
-Wl,--defsym=pureboot_app=${_pb_app} ${_pb_wrap})
add_custom_command(TARGET pureboot POST_BUILD COMMAND ${CMAKE_SIZE} $<TARGET_FILE:pureboot>)
add_image_outputs(pureboot)
if(PROJECT_IS_TOP_LEVEL) if(PROJECT_IS_TOP_LEVEL)
get_target_property(_pb_stock_hz pureboot PUREBOOT_HZ)
get_target_property(_pb_stock_baud pureboot PUREBOOT_BAUD)
add_test(NAME pureboot.size add_test(NAME pureboot.size
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:pureboot> COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:pureboot>
-DLIMIT=${_pb_limit} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake) -DLIMIT=${PUREBOOT_LIMIT} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
if(Python3_FOUND) if(Python3_FOUND)
add_test(NAME pureboot.pi add_test(NAME pureboot.pi
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/check_pi.py COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/check_pi.py
${CMAKE_OBJDUMP} ${CMAKE_NM} $<TARGET_FILE:pureboot> ${_pb_base_hex}) ${CMAKE_OBJDUMP} ${CMAKE_NM} $<TARGET_FILE:pureboot> ${PUREBOOT_BASE_HEX})
add_test(NAME pureboot.planner add_test(NAME pureboot.planner
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/test_planner.py COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/test_planner.py
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py) ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py)
@@ -289,9 +166,9 @@ if(PROJECT_IS_TOP_LEVEL)
COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:pbapp> $<TARGET_FILE:pbapp>.bin) COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:pbapp> $<TARGET_FILE:pbapp>.bin)
add_test(NAME pureboot.protocol add_test(NAME pureboot.protocol
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbtest.py COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbtest.py
${PB_DEVICE} $<TARGET_FILE:pureboot> ${_pb_sim_mcu} ${_pb_hz} ${_pb_base_hex} ${PB_DEVICE} $<TARGET_FILE:pureboot> ${PUREBOOT_SIM_MCU} ${_pb_stock_hz}
${_pb_page} ${_pb_baud} ${_pb_eeprom} $<TARGET_FILE:pbapp>.bin ${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_pb_stock_baud} ${PUREBOOT_EEPROM}
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py $<TARGET_FILE:pbapp>.bin ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
${CMAKE_BINARY_DIR}/pbtest-work) ${CMAKE_BINARY_DIR}/pbtest-work)
set_tests_properties(pureboot.protocol PROPERTIES TIMEOUT 180) set_tests_properties(pureboot.protocol PROPERTIES TIMEOUT 180)
@@ -299,43 +176,127 @@ if(PROJECT_IS_TOP_LEVEL)
# installed one slot lower, must serve the full command set. # installed one slot lower, must serve the full command set.
add_test(NAME pureboot.reloc add_test(NAME pureboot.reloc
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbreloc.py COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbreloc.py
${PB_DEVICE} $<TARGET_FILE:pureboot> ${_pb_sim_mcu} ${_pb_hz} ${_pb_base_hex} ${PB_DEVICE} $<TARGET_FILE:pureboot> ${PUREBOOT_SIM_MCU} ${_pb_stock_hz}
${_pb_page} ${_pb_baud} ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py ${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_pb_stock_baud}
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
${CMAKE_BINARY_DIR}/pbreloc-work) ${CMAKE_BINARY_DIR}/pbreloc-work)
set_tests_properties(pureboot.reloc PROPERTIES TIMEOUT 180 set_tests_properties(pureboot.reloc PROPERTIES TIMEOUT 180
ENVIRONMENT "PB_OBJCOPY=${CMAKE_OBJCOPY}") ENVIRONMENT "PB_OBJCOPY=${CMAKE_OBJCOPY}")
# Re-homing: a loader mistakenly programmed at address 0 (a raw .bin # Re-homing: a loader mistakenly programmed at address 0 (a raw .bin
# handed to a programmer) must heal into the canonical slot through # handed to a programmer) or sitting in the staging slot must heal
# the ordinary --update-loader flow. Patched-vector behavior, so one # into the canonical slot through the ordinary --update-loader flow.
# representative chip carries it. # Patched-vector behavior, so one representative chip carries it.
if(LIBAVR_MCU STREQUAL "attiny85") if(LIBAVR_MCU STREQUAL "attiny85")
add_test(NAME pureboot.rehome add_test(NAME pureboot.rehome
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbrehome.py COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbrehome.py
${PB_DEVICE} $<TARGET_FILE:pureboot> $<TARGET_FILE:pureboot9>.bin ${_pb_sim_mcu} ${PB_DEVICE} $<TARGET_FILE:pureboot> $<TARGET_FILE:pureboot9>.bin
${_pb_hz} ${_pb_base_hex} ${_pb_page} ${_pb_baud} $<TARGET_FILE:pbapp>.bin ${PUREBOOT_SIM_MCU} ${_pb_stock_hz} ${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE}
${_pb_stock_baud} $<TARGET_FILE:pbapp>.bin
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
${CMAKE_BINARY_DIR}/pbrehome-work) ${CMAKE_BINARY_DIR}/pbrehome-work)
set_tests_properties(pureboot.rehome PROPERTIES TIMEOUT 180) set_tests_properties(pureboot.rehome PROPERTIES TIMEOUT 180)
endif() endif()
# The self-update end-to-end: the re-timed build (same source, only # The self-update end-to-end: the re-timed build (same source, only
# PUREBOOT_TIMEOUT differs — a byte-different image) replaces the # the timeout differs — a byte-different image) replaces the resident
# resident through --update-loader, with every power-fail phase # through --update-loader, with every power-fail phase rehearsed from
# rehearsed from the runner's flash dumps. # the runner's flash dumps.
add_executable(pureboot9 pureboot/pureboot.cpp) pureboot_add_loader(pureboot9 TIMEOUT 9)
target_link_libraries(pureboot9 PRIVATE libavr)
target_compile_definitions(pureboot9 PRIVATE PUREBOOT_TIMEOUT=9)
target_link_options(pureboot9 PRIVATE -nostartfiles -Wl,--section-start=.text=${_pb_base_hex}
-Wl,--defsym=pureboot_app=${_pb_app} ${_pb_wrap})
add_image_outputs(pureboot9)
add_test(NAME pureboot.update add_test(NAME pureboot.update
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbupdate.py COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbupdate.py
${PB_DEVICE} $<TARGET_FILE:pureboot> $<TARGET_FILE:pureboot9> ${_pb_sim_mcu} ${PB_DEVICE} $<TARGET_FILE:pureboot> $<TARGET_FILE:pureboot9>
${_pb_hz} ${_pb_base_hex} ${_pb_page} ${_pb_baud} $<TARGET_FILE:pbapp>.bin ${PUREBOOT_SIM_MCU} ${_pb_stock_hz} ${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE}
${_pb_stock_baud} $<TARGET_FILE:pbapp>.bin
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
${CMAKE_BINARY_DIR}/pbupdate-work) ${CMAKE_BINARY_DIR}/pbupdate-work)
set_tests_properties(pureboot.update PROPERTIES TIMEOUT 600 set_tests_properties(pureboot.update PROPERTIES TIMEOUT 600
ENVIRONMENT "PB_OBJCOPY=${CMAKE_OBJCOPY}") ENVIRONMENT "PB_OBJCOPY=${CMAKE_OBJCOPY}")
endif() endif()
# The size matrix: every configuration axis that could move the image
# size — the serial backend (different code), the clock and its ladder
# baud (different constants and divisor shapes), the USART instance
# (different register class) — each combination must still fit the
# chip's slot budget. Pins are size-neutral (port and bit are immediate
# operands) and the timeout is a constant, so neither adds an axis. The
# stock build is one point of this matrix and already has its test.
function(pureboot_size_variant name)
pureboot_add_loader(${name} ${ARGN})
add_test(NAME ${name}.size
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:${name}>
-DLIMIT=${PUREBOOT_LIMIT} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
endfunction()
# Clock points: the shipped-fuse floor (CKDIV8), the calibrated RC, and
# the crystal the stock build assumes (the tiny13's ladder is its own RC
# menu — it has no crystal option).
if(LIBAVR_MCU MATCHES "^attiny13")
set(_matrix_clocks 1200000 4800000 9600000)
else()
set(_matrix_clocks 1000000 8000000 16000000)
endif()
foreach(_matrix_hz IN LISTS _matrix_clocks)
math(EXPR _matrix_khz "${_matrix_hz} / 1000")
if(PUREBOOT_HAS_USART OR NOT _matrix_hz EQUAL _pb_stock_hz)
pureboot_size_variant(pureboot_sw_${_matrix_khz}k CLOCK ${_matrix_hz} SERIAL software)
endif()
if(PUREBOOT_HAS_USART AND NOT _matrix_hz EQUAL _pb_stock_hz)
pureboot_size_variant(pureboot_hw_${_matrix_khz}k CLOCK ${_matrix_hz} SERIAL hardware)
endif()
endforeach()
if(PUREBOOT_HAS_USART1)
pureboot_size_variant(pureboot_usart1 USART 1)
endif()
# One configured deployment end to end — a real board's shape rather
# than the stock assumption: the ATmega328P on its shipped 1 MHz fuses,
# the software UART on hand-picked pins (TX = PB1, RX = PB5), the ladder
# baud (9600). The full protocol suite runs against it, fixture
# application included, over the runner's GPIO bridge — proving the
# configuration plumbing produces a working loader, not just one that
# fits.
if(LIBAVR_MCU STREQUAL "atmega328p" AND DEFINED PB_DEVICE)
pureboot_size_variant(pureboot_custom CLOCK 1000000 SERIAL software RX pb5 TX pb1)
get_target_property(_custom_hz pureboot_custom PUREBOOT_HZ)
get_target_property(_custom_baud pureboot_custom PUREBOOT_BAUD)
get_target_property(_custom_link pureboot_custom PUREBOOT_LINK)
add_executable(pbapp_custom test/pbapp.cpp)
target_link_libraries(pbapp_custom PRIVATE libavr)
target_compile_definitions(pbapp_custom PRIVATE PUREBOOT_CLOCK_HZ=${_custom_hz}
PUREBOOT_BAUD=${_custom_baud} PUREBOOT_SOFT_SERIAL PUREBOOT_TX=pb1)
add_custom_command(TARGET pbapp_custom POST_BUILD
COMMAND ${CMAKE_OBJCOPY} -O binary
$<TARGET_FILE:pbapp_custom> $<TARGET_FILE:pbapp_custom>.bin)
add_test(NAME pureboot.custom
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbtest.py
${PB_DEVICE} $<TARGET_FILE:pureboot_custom> ${PUREBOOT_SIM_MCU} ${_custom_hz}
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_custom_baud} ${PUREBOOT_EEPROM}
$<TARGET_FILE:pbapp_custom>.bin ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
${CMAKE_BINARY_DIR}/pbcustom-work ${_custom_link})
set_tests_properties(pureboot.custom PROPERTIES TIMEOUT 180)
endif()
# The second USART, driven for real on one chip: instance selection is
# compile-checked everywhere, but only a live session proves the loader
# initialized and polls the USART it claims to. The fixture application
# banners on the same instance.
if(LIBAVR_MCU STREQUAL "atmega644a" AND DEFINED PB_DEVICE)
get_target_property(_usart1_hz pureboot_usart1 PUREBOOT_HZ)
get_target_property(_usart1_baud pureboot_usart1 PUREBOOT_BAUD)
add_executable(pbapp_usart1 test/pbapp.cpp)
target_link_libraries(pbapp_usart1 PRIVATE libavr)
target_compile_definitions(pbapp_usart1 PRIVATE PUREBOOT_CLOCK_HZ=${_usart1_hz}
PUREBOOT_BAUD=${_usart1_baud} PUREBOOT_USART=1)
add_custom_command(TARGET pbapp_usart1 POST_BUILD
COMMAND ${CMAKE_OBJCOPY} -O binary
$<TARGET_FILE:pbapp_usart1> $<TARGET_FILE:pbapp_usart1>.bin)
add_test(NAME pureboot.usart1
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbtest.py
${PB_DEVICE} $<TARGET_FILE:pureboot_usart1> ${PUREBOOT_SIM_MCU} ${_usart1_hz}
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_usart1_baud} ${PUREBOOT_EEPROM}
$<TARGET_FILE:pbapp_usart1>.bin ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
${CMAKE_BINARY_DIR}/pbusart1-work usart1)
set_tests_properties(pureboot.usart1 PROPERTIES TIMEOUT 180)
endif()
endif() endif()

File diff suppressed because it is too large Load Diff

310
pureboot/CMakeLists.txt Normal file
View File

@@ -0,0 +1,310 @@
# pureboot as a consumable CMake unit: the per-chip geometry, the default
# baud ladder, and pureboot_add_loader() — the one way a loader target is
# created, both by this port's own build and by a downstream project. A
# downstream project brings its usual libavr setup (the `libavr` target and
# the LIBAVR_MCU toolchain preset), adds this directory, and states its
# deployment:
#
# add_subdirectory(bootloader/pureboot)
# pureboot_add_loader(myboot CLOCK 1000000 SERIAL software TX pb1 RX pb5)
#
# Every argument is optional — CLOCK defaults to the family assumption
# below, BAUD to the fastest standard rate the clock reaches within 2.5 %
# (the ladder), SERIAL to the chip's hardware USART where it has one
# (`hardware`/`software` force a backend, USART 1 picks the second
# instance), RX/TX to pb0/pb1 for the software UART, TIMEOUT to 8 s.
# Infeasible picks fail the build by name: libavr's baud-error and
# software-UART cycle-floor static asserts re-check whatever is passed.
# Per-family geometry: flash/page/EEPROM sizes and the linker wrap the PC
# modulo needs, the loader slot (each chip's smallest boot sector — 1 KiB on
# the word-addressed 1284s), and the deployment defaults (crystal assumption
# on the megas, calibrated RC on the tinies). The USART flags mirror the
# hardware inventory the loader's own static asserts check (the plain 644 is
# the x4 family's one single-USART die, Atmel-2593).
set(_pb_has_usart 1)
set(_pb_has_usart1 0)
if(LIBAVR_MCU MATCHES "^attiny13a?$")
set(_pb_flash 1024)
set(_pb_wrap "")
set(_pb_page 32)
set(_pb_hz 9600000)
set(_pb_eeprom 64)
set(_pb_has_usart 0)
elseif(LIBAVR_MCU STREQUAL "attiny25")
set(_pb_flash 2048)
set(_pb_wrap "")
set(_pb_page 32)
set(_pb_hz 8000000)
set(_pb_eeprom 128)
set(_pb_has_usart 0)
elseif(LIBAVR_MCU STREQUAL "attiny45")
set(_pb_flash 4096)
set(_pb_wrap "")
set(_pb_page 64)
set(_pb_hz 8000000)
set(_pb_eeprom 256)
set(_pb_has_usart 0)
elseif(LIBAVR_MCU STREQUAL "attiny85")
set(_pb_flash 8192)
set(_pb_wrap -Wl,--pmem-wrap-around=8k)
set(_pb_page 64)
set(_pb_hz 8000000)
set(_pb_eeprom 512)
set(_pb_has_usart 0)
elseif(LIBAVR_MCU MATCHES "^atmega48(a|p|pa)?$")
set(_pb_flash 4096)
set(_pb_wrap "")
set(_pb_page 64)
set(_pb_hz 16000000)
set(_pb_eeprom 256)
elseif(LIBAVR_MCU MATCHES "^atmega8a?$" OR LIBAVR_MCU MATCHES "^atmega88(a|p|pa)?$")
set(_pb_flash 8192)
set(_pb_wrap -Wl,--pmem-wrap-around=8k)
set(_pb_page 64)
set(_pb_hz 16000000)
set(_pb_eeprom 512)
elseif(LIBAVR_MCU MATCHES "^atmega16a?$" OR LIBAVR_MCU MATCHES "^atmega168(a|p|pa)?$")
set(_pb_flash 16384)
set(_pb_wrap -Wl,--pmem-wrap-around=16k)
set(_pb_page 128)
set(_pb_hz 16000000)
set(_pb_eeprom 512)
elseif(LIBAVR_MCU MATCHES "^atmega164(a|p|pa)$")
set(_pb_flash 16384)
set(_pb_wrap -Wl,--pmem-wrap-around=16k)
set(_pb_page 128)
set(_pb_hz 16000000)
set(_pb_eeprom 512)
set(_pb_has_usart1 1)
elseif(LIBAVR_MCU MATCHES "^atmega32a?$" OR LIBAVR_MCU MATCHES "^atmega328p?$")
set(_pb_flash 32768)
set(_pb_wrap -Wl,--pmem-wrap-around=32k)
set(_pb_page 128)
set(_pb_hz 16000000)
set(_pb_eeprom 1024)
elseif(LIBAVR_MCU MATCHES "^atmega324(a|p|pa)$")
set(_pb_flash 32768)
set(_pb_wrap -Wl,--pmem-wrap-around=32k)
set(_pb_page 128)
set(_pb_hz 16000000)
set(_pb_eeprom 1024)
set(_pb_has_usart1 1)
elseif(LIBAVR_MCU MATCHES "^atmega644(a|p|pa)?$")
# 64 KiB is exactly the 16-bit byte space: plain LPM reaches everything,
# and the smallest boot section (1 KiB) holds the loader and its staging
# slot together (see README.md). The plain 644 is the family's one
# single-USART die.
set(_pb_flash 65536)
set(_pb_wrap -Wl,--pmem-wrap-around=64k)
set(_pb_page 256)
set(_pb_hz 16000000)
set(_pb_eeprom 2048)
if(NOT LIBAVR_MCU STREQUAL "atmega644")
set(_pb_has_usart1 1)
endif()
elseif(LIBAVR_MCU MATCHES "^atmega1284p?$")
# 128 KiB: wire flash addresses are word addresses, reads go through
# ELPM, and the PC's modulo wrap exceeds what --pmem-wrap-around models.
# The slot is 1 KiB — this chip's own smallest boot sector; the far
# machinery cannot fit 512 B (see README.md).
set(_pb_flash 131072)
set(_pb_wrap "")
set(_pb_page 256)
set(_pb_hz 16000000)
set(_pb_eeprom 4096)
set(_pb_slot 1024)
set(_pb_limit 1024)
set(_pb_has_usart1 1)
else()
message(FATAL_ERROR "pureboot: no geometry for ${LIBAVR_MCU}")
endif()
if(NOT DEFINED _pb_slot)
set(_pb_slot 512)
endif()
math(EXPR _pb_base "${_pb_flash} - ${_pb_slot}")
math(EXPR _pb_base_hex "${_pb_base}" OUTPUT_FORMAT HEXADECIMAL)
# Patched-vector chips hand over through the trampoline word below the slot,
# which is also the slot's own last word — their budget is slot 2.
if(LIBAVR_MCU MATCHES "^atmega" AND NOT LIBAVR_MCU MATCHES "^atmega48")
set(_pb_app 0)
if(NOT DEFINED _pb_limit)
set(_pb_limit ${_pb_slot})
endif()
else()
math(EXPR _pb_app "${_pb_base} - 2")
math(EXPR _pb_limit "${_pb_slot} - 2")
endif()
# simavr names its cores after the base dies; the A revisions run on them
# (the 644PA on the 644P core).
set(_pb_sim_mcu ${LIBAVR_MCU})
if(LIBAVR_MCU MATCHES "^atmega(8|16|32|48|88|164|168|644)a$")
string(REGEX REPLACE "a$" "" _pb_sim_mcu ${LIBAVR_MCU})
elseif(LIBAVR_MCU STREQUAL "atmega644pa")
set(_pb_sim_mcu atmega644p)
endif()
# The function runs in its caller's scope, so everything it needs crosses
# scopes as global properties.
set_property(GLOBAL PROPERTY PUREBOOT_BASE_HEX ${_pb_base_hex})
set_property(GLOBAL PROPERTY PUREBOOT_APP ${_pb_app})
set_property(GLOBAL PROPERTY PUREBOOT_WRAP "${_pb_wrap}")
set_property(GLOBAL PROPERTY PUREBOOT_DEFAULT_HZ ${_pb_hz})
set_property(GLOBAL PROPERTY PUREBOOT_HAS_USART ${_pb_has_usart})
set_property(GLOBAL PROPERTY PUREBOOT_HAS_USART1 ${_pb_has_usart1})
# The port's own build (tests, the size matrix) reads the geometry from the
# parent scope; a downstream consumer gets the same variables for free.
set(PUREBOOT_BASE_HEX ${_pb_base_hex} PARENT_SCOPE)
set(PUREBOOT_PAGE ${_pb_page} PARENT_SCOPE)
set(PUREBOOT_SLOT ${_pb_slot} PARENT_SCOPE)
set(PUREBOOT_LIMIT ${_pb_limit} PARENT_SCOPE)
set(PUREBOOT_EEPROM ${_pb_eeprom} PARENT_SCOPE)
set(PUREBOOT_DEFAULT_HZ ${_pb_hz} PARENT_SCOPE)
set(PUREBOOT_HAS_USART ${_pb_has_usart} PARENT_SCOPE)
set(PUREBOOT_HAS_USART1 ${_pb_has_usart1} PARENT_SCOPE)
set(PUREBOOT_SIM_MCU ${_pb_sim_mcu} PARENT_SCOPE)
# The fastest standard rate the clock reaches within 2.5 % — the same
# best-of-U2X-and-plain divisor search libavr's solve_baud runs, so a
# default never trips the compile-time error it is checked against. A
# software build additionally requires the polled receiver's 100-cycles-a-bit
# floor (its own static assert): at low clocks the U2X divisor still reaches
# rates the bit-banged sampler cannot, so the backend gates the ladder.
function(pureboot_default_baud clock software outvar)
foreach(baud 115200 57600 38400 19200 9600)
math(EXPR _cycles "${clock} / ${baud}")
if(software AND _cycles LESS 100)
continue()
endif()
foreach(divisor 8 16)
math(EXPR _step "${divisor} * ${baud}")
math(EXPR _n "(${clock} + ${_step} / 2) / ${_step}")
if(_n LESS 1 OR _n GREATER 4096)
continue()
endif()
math(EXPR _actual "${clock} / (${divisor} * ${_n})")
math(EXPR _delta "${_actual} - ${baud}")
if(_delta LESS 0)
math(EXPR _delta "-(${_delta})")
endif()
math(EXPR _error_bp "${_delta} * 10000 / ${baud}")
if(_error_bp LESS_EQUAL 250)
set(${outvar} ${baud} PARENT_SCOPE)
return()
endif()
endforeach()
endforeach()
message(FATAL_ERROR "pureboot: no standard baud rate fits a ${clock} Hz clock within 2.5 %")
endfunction()
# pureboot_add_loader(<name> [CLOCK <hz>] [BAUD <bd>]
# [SERIAL auto|hardware|software] [USART <n>]
# [RX <pin>] [TX <pin>] [TIMEOUT <s>])
#
# Creates the loader target plus its flashable images (<name>.hex for a
# programmer, <name>.bin for --update-loader) and stamps the resolved
# deployment on the target: the PUREBOOT_HZ, PUREBOOT_BAUD and PUREBOOT_LINK
# properties (the link as usart0/usart1/sw:<RX>,<TX> — what a test harness
# needs to speak to the build).
function(pureboot_add_loader name)
cmake_parse_arguments(PB "" "CLOCK;BAUD;SERIAL;USART;RX;TX;TIMEOUT" "" ${ARGN})
if(PB_UNPARSED_ARGUMENTS)
message(FATAL_ERROR "pureboot_add_loader(${name}): unknown arguments ${PB_UNPARSED_ARGUMENTS}")
endif()
get_property(_hz GLOBAL PROPERTY PUREBOOT_DEFAULT_HZ)
get_property(_base_hex GLOBAL PROPERTY PUREBOOT_BASE_HEX)
get_property(_app GLOBAL PROPERTY PUREBOOT_APP)
get_property(_wrap GLOBAL PROPERTY PUREBOOT_WRAP)
get_property(_usart GLOBAL PROPERTY PUREBOOT_HAS_USART)
get_property(_usart1 GLOBAL PROPERTY PUREBOOT_HAS_USART1)
if(NOT PB_CLOCK)
set(PB_CLOCK ${_hz})
endif()
if(NOT PB_TIMEOUT)
set(PB_TIMEOUT 8)
endif()
if(NOT PB_SERIAL)
set(PB_SERIAL auto)
endif()
if(DEFINED PB_USART AND PB_SERIAL STREQUAL "software")
message(FATAL_ERROR "pureboot_add_loader(${name}): USART ${PB_USART} contradicts SERIAL software")
endif()
if(DEFINED PB_USART)
set(PB_SERIAL hardware)
elseif(PB_SERIAL STREQUAL "hardware")
set(PB_USART 0)
endif()
set(_serial_defines "")
if(PB_SERIAL STREQUAL "hardware")
if(PB_USART EQUAL 1 AND NOT _usart1)
message(FATAL_ERROR "pureboot_add_loader(${name}): ${LIBAVR_MCU} has no USART1")
elseif(NOT _usart)
message(FATAL_ERROR "pureboot_add_loader(${name}): ${LIBAVR_MCU} has no hardware USART")
endif()
set(_serial_defines PUREBOOT_USART=${PB_USART})
set(_link usart${PB_USART})
else()
if(PB_SERIAL STREQUAL "auto")
if(_usart AND (PB_RX OR PB_TX))
message(WARNING "pureboot_add_loader(${name}): RX/TX apply to the software UART, "
"which auto does not pick on ${LIBAVR_MCU} — SERIAL software to force it")
endif()
if(_usart)
set(_link usart0)
else()
set(PB_SERIAL software)
endif()
endif()
if(PB_SERIAL STREQUAL "software")
if(NOT PB_RX)
set(PB_RX pb0)
endif()
if(NOT PB_TX)
set(PB_TX pb1)
endif()
foreach(_pin ${PB_RX} ${PB_TX})
if(NOT _pin MATCHES "^p[a-h][0-7]$")
message(FATAL_ERROR "pureboot_add_loader(${name}): pin '${_pin}' is not of the form pb1")
endif()
endforeach()
set(_serial_defines PUREBOOT_SOFT_SERIAL PUREBOOT_RX=${PB_RX} PUREBOOT_TX=${PB_TX})
# The link spec a test harness drives a GPIO bridge with: sw:<RX>,<TX>
# as the port letter and bit, the loader's own pin naming upcased.
string(SUBSTRING ${PB_RX} 1 2 _rx_pin)
string(SUBSTRING ${PB_TX} 1 2 _tx_pin)
string(TOUPPER "sw:${_rx_pin},${_tx_pin}" _link)
string(REPLACE "SW" "sw" _link ${_link})
endif()
endif()
if(NOT PB_BAUD)
if(PB_SERIAL STREQUAL "software")
pureboot_default_baud(${PB_CLOCK} 1 PB_BAUD)
else()
pureboot_default_baud(${PB_CLOCK} 0 PB_BAUD)
endif()
endif()
set(_defines PUREBOOT_CLOCK_HZ=${PB_CLOCK} PUREBOOT_BAUD=${PB_BAUD} PUREBOOT_TIMEOUT=${PB_TIMEOUT}
${_serial_defines})
add_executable(${name} ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/pureboot.cpp)
target_link_libraries(${name} PRIVATE libavr)
target_compile_definitions(${name} PRIVATE ${_defines})
target_link_options(${name} PRIVATE -nostartfiles -Wl,--section-start=.text=${_base_hex}
-Wl,--defsym=pureboot_app=${_app} ${_wrap})
add_custom_command(TARGET ${name} POST_BUILD COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${name}>)
# The ELF is a container (symbols, section headers), never flashed; the
# flashable forms sit beside it: .hex for a programmer, .bin (the slot's
# bare bytes) for the host tool's raw path and --update-loader.
add_custom_command(TARGET ${name} POST_BUILD
COMMAND ${CMAKE_OBJCOPY} -O ihex -R .eeprom
$<TARGET_FILE:${name}> $<TARGET_FILE:${name}>.hex
COMMAND ${CMAKE_OBJCOPY} -O binary -R .eeprom
$<TARGET_FILE:${name}> $<TARGET_FILE:${name}>.bin)
set_target_properties(${name} PROPERTIES PUREBOOT_HZ ${PB_CLOCK} PUREBOOT_BAUD ${PB_BAUD}
PUREBOOT_LINK ${_link})
endfunction()

View File

@@ -3,16 +3,18 @@
A serial bootloader on [libavr](https://git.blackmark.me/avr/libavr), pure by A serial bootloader on [libavr](https://git.blackmark.me/avr/libavr), pure by
constraint: one C++ source, no inline assembly, no global register variables constraint: one C++ source, no inline assembly, no global register variables
(attributes allowed), built for **every chip libavr targets — all 37 — (attributes allowed), built for **every chip libavr targets — all 37 —
fitting each chip's smallest boot sector**: 512 bytes everywhere — 488 B on fitting each chip's smallest boot sector**: 512 bytes everywhere — 470 B on
the tiny13s, 498502 B on the tiny25/45/85, 466504 B across the megas the tiny13s, ~484 B on the tiny25/45/85, 458506 B across the megas and
(474 B on the boot-section-less m48s, 498 B on the 644s) — except the every configured variant of them (the m8's software-serial build is the
ATmega1284/1284P, whose smallest boot sector is 1 KiB and whose far-flash fattest) — except the ATmega1284/1284P, whose smallest boot sector is 1 KiB
machinery (ELPM reads, RAMPZ page commands, word-addressed wire) lands at and whose far-flash machinery (ELPM reads, RAMPZ page commands,
558 B in a 1 KiB slot: the 512-byte figure is a hardware boundary those word-addressed wire) lands at 556562 B in a 1 KiB slot: the 512-byte
chips simply do not have, and no implementation of this feature set fits it figure is a hardware boundary those chips simply do not have, and no
there. The device speaks primitives; every composite — verify, erase, implementation of this feature set fits it there. Clock, baud, serial
reset-vector surgery, updating the loader itself — lives in the host tool backend and pins are per-build configuration (below); the size matrix in
(`pureboot.py`). the test suite holds every combination inside its slot. The device speaks
primitives; every composite — verify, erase, reset-vector surgery, updating
the loader itself — lives in the host tool (`pureboot.py`).
The image is **position-independent**: control flow is PC-relative, the The image is **position-independent**: control flow is PC-relative, the
read/write paths take wire addresses, the write guard protects the slot the read/write paths take wire addresses, the write guard protects the slot the
@@ -26,16 +28,63 @@ jumps into it, and lets it rewrite the resident. The slot is 512 bytes
minimum); on the tinies the budget is 510, not 512: a slot's last word minimum); on the tinies the budget is 510, not 512: a slot's last word
belongs to the host-managed trampoline (below). belongs to the host-managed trampoline (below).
## Configuration
Every deployment axis is a build parameter, resolved by the CMake function
`pureboot_add_loader()` (in `pureboot/CMakeLists.txt`) — the one way a
loader target is created, by this repo's own build and by a downstream
project alike:
| Argument | Meaning | Default |
|---|---|---|
| `CLOCK <hz>` | the clock the board runs | 16 MHz megas, 8 MHz t25/45/85, 9.6 MHz t13s |
| `BAUD <bd>` | the wire rate | the ladder below |
| `SERIAL auto\|hardware\|software` | the link backend | `auto`: the hardware USART where the chip has one |
| `USART <n>` | the USART instance (x4 megas carry two) | 0 |
| `RX <pin>`, `TX <pin>` | software-UART pins | `pb0`, `pb1` |
| `TIMEOUT <s>` | the activation window | 8 |
The default baud is the fastest of 115200/57600/38400/19200/9600 the clock
reaches within 2.5 % — the same U2X-included divisor search libavr's baud
solver runs — and on a software build additionally within the polled
receiver's 100-cycles-a-bit floor. 16 MHz lands 115200, 8 MHz 57600,
1 MHz 9600. Whatever is picked or overridden is re-checked in the compile:
an infeasible clock/baud/backend combination, or a USART the chip does not
have, fails with a named static assert.
A downstream project brings its usual libavr setup (the `libavr` target,
the chip via the `LIBAVR_MCU` toolchain preset), consumes this directory,
and states its deployment — for example an ATmega328P on its shipped
1 MHz fuses with the software UART on hand-picked pins:
```cmake
FetchContent_Declare(bootloader GIT_REPOSITORY git@git.blackmark.me:avr/bootloader.git GIT_TAG main)
FetchContent_MakeAvailable(bootloader)
add_subdirectory(${bootloader_SOURCE_DIR}/pureboot pureboot)
pureboot_add_loader(myboot CLOCK 1000000 SERIAL software TX pb1 RX pb5)
```
The function emits the ELF plus `myboot.hex` (the programmer artifact) and
`myboot.bin` (the self-update image), prints the size, and stamps the
resolved deployment on the target as the `PUREBOOT_HZ`, `PUREBOOT_BAUD`
and `PUREBOOT_LINK` properties — what a flashing script or test harness
needs to speak to the build. This exact example deployment runs the full
protocol suite in CI (`pureboot.custom`).
## Link ## Link
The stock builds assume the family's natural deployment; any axis moves
per build (above).
| Chip | Serial | Baud | Clock assumed | | Chip | Serial | Baud | Clock assumed |
|---|---|---|---| |---|---|---|---|
| every ATmega | the hardware USART (USART0), RXD/TXD per pinout | 115200 8N1 | 16 MHz crystal | | every ATmega | the hardware USART (USART0), RXD/TXD per pinout | 115200 8N1 | 16 MHz crystal |
| ATtiny25/45/85 | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 8 MHz internal RC | | ATtiny25/45/85 | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 8 MHz internal RC |
| ATtiny13/13A | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 9.6 MHz internal RC | | ATtiny13/13A | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 9.6 MHz internal RC |
The tiny RX pin has its pull-up enabled; TX idles high. All multi-byte The software-UART RX pin has its pull-up enabled; TX idles high. All
quantities on the wire are little-endian. multi-byte quantities on the wire are little-endian.
## Activation ## Activation
@@ -50,9 +99,10 @@ The host then has one activation window per awaited byte to knock: `p` then
awaited again (line noise cannot lock the loader, only delay it). A window awaited again (line noise cannot lock the loader, only delay it). A window
expiring with an idle line boots the application. expiring with an idle line boots the application.
The window length is a compile-time constant — 8 s by default, another value The window length is a compile-time constant — 8 s by default, another
via the `PUREBOOT_TIMEOUT` CMake cache variable — so the whole EEPROM belongs value via `pureboot_add_loader(... TIMEOUT <s>)` (the stock target keeps
to the application; pureboot never uses it for its own state. Re-timing a the `PUREBOOT_TIMEOUT` cache variable) — so the whole EEPROM belongs to
the application; pureboot never uses it for its own state. Re-timing a
deployed loader is a self-update with a re-timed build (below). deployed loader is a self-update with a re-timed build (below).
## Session ## Session
@@ -237,11 +287,35 @@ read-back unless `--no-verify`; images are raw binary, or Intel HEX by
extension. `--force` overrides the refusable safety checks (today: flashing extension. `--force` overrides the refusable safety checks (today: flashing
application data into a mega's reset walk region). application data into a mega's reset walk region).
Readouts come one fact per line: `--info` prints the decoded info block
field by field, `--fuses` each fuse byte on its own line — plus, on a
boot-sectioned mega, the decoded meaning (where the BOOTSZ section starts,
what BOOTRST does to reset). Transfers that take wire time — programming,
reading, erasing, verifying, the update phases — draw a transient progress
bar on stderr when it is a tty; logs and pipes see only the summary lines.
`-v`/`--verbose` adds the decisions as they happen: knock counts, the
programming plan (vector-surgery targets, skipped blank pages), update
state handling and per-phase page counts.
## Tests ## Tests
Per chip preset, `ctest` runs: `tools/check.sh` runs every chip's workflow (`tools/check.sh --full` adds
the reflect-mode builds of libavr's spot set; `tools/make_presets.py`
regenerates the presets). Per chip preset, `ctest` runs:
- `pureboot.size` — the 510-byte (tinies) / 512-byte (mega) budget; - `pureboot.size` — the 510-byte (tinies) / 512-byte (mega) budget;
- `pureboot_*.size` — the size matrix: the serial backends × the clock
ladder (1/8/16 MHz; the t13s' own RC menu), plus the USART1 build on the
x4 chips — every configuration axis that could move the image, each
variant against the same slot budget (pins are immediate operands and the
timeout is a constant: size-neutral);
- `pureboot.custom` (328P) — the configured-deployment acceptance test: the
1 MHz software-serial TX=PB1/RX=PB5 build from the configuration example
drives the full protocol suite through the runner's GPIO bridge, fixture
application included;
- `pureboot.usart1` (644A) — the same protocol suite over the second
hardware USART: instance selection is compile-checked everywhere, but
only a live session proves the loader polls the USART it claims;
- `pureboot.pi` — the position-independence lint: no absolute `jmp`/`call` - `pureboot.pi` — the position-independence lint: no absolute `jmp`/`call`
in the image, the info block within its first 256 bytes; in the image, the info block within its first 256 bytes;
- `pureboot.planner` — the host tool's pure logic: programming orders and - `pureboot.planner` — the host tool's pure logic: programming orders and
@@ -249,9 +323,10 @@ Per chip preset, `ctest` runs:
boot-fuse decode, and the update preflight's error/warning matrix over boot-fuse decode, and the update preflight's error/warning matrix over
synthetic fuse bytes; synthetic fuse bytes;
- `pureboot.protocol` — end to end against a simavr device - `pureboot.protocol` — end to end against a simavr device
(`test/pureboot_device.c`the mega's USART as a pty; on the tinies a (`test/pureboot_device.c`a hardware USART as a pty, or a cycle-timed
cycle-timed GPIO⇄pty bridge for the software UART, plus the SPM/NVM module GPIO⇄pty bridge for a software-UART build, selected with `-l` to match
simavr's tiny cores lack) driven by the real host tool through the loader's link; plus the SPM/NVM module simavr's tiny cores lack)
driven by the real host tool through
knock-from-reset, program + verify of both memories, session reconnect, an knock-from-reset, program + verify of both memories, session reconnect, an
external reset through the patched vector, and the hand-over to a fixture external reset through the patched vector, and the hand-over to a fixture
application whose banner proves the launch — cross-checked against the application whose banner proves the launch — cross-checked against the
@@ -265,6 +340,6 @@ Per chip preset, `ctest` runs:
from its flash dump, and a re-run must complete the update with the from its flash dump, and a re-run must complete the update with the
application intact throughout. application intact throughout.
`size`, `pi`, and `planner` are host logic and run anywhere; the three `size`, `pi`, and `planner` are host logic and run anywhere; the
simulator-driven targets need simavr and a pty, so they are POSIX-only — simulator-driven targets need simavr and a pty, so they are POSIX-only —
on Windows the tool is exercised against real hardware. on Windows the tool is exercised against real hardware.

View File

@@ -38,22 +38,19 @@ constexpr auto off = avr::irq::guard_policy::unused;
constexpr std::uint8_t ack = '+'; constexpr std::uint8_t ack = '+';
// Per-chip personality: the clocks the dogfood boards run (16 MHz crystal on // Per-deployment personality, passed in by the build — pureboot_add_loader()
// the mega, calibrated RC on the tinies). The device signature comes straight // (the CMake function next to this file) resolves the defaults: the clock the
// from the chip database (avr::hw::db.signature) — compile-time data is the // board actually runs, the wire baud, the serial backend and its pins. The
// only universal source, since the tiny13A cannot even read its signature row // device signature needs no configuring — it comes from the chip database
// from code. // (avr::hw::db.signature), the only universal source, since the tiny13A
consteval avr::hertz_t clock() // cannot even read its signature row from code.
{ #if !defined(PUREBOOT_CLOCK_HZ) || !defined(PUREBOOT_BAUD)
auto name = std::string_view{avr::hw::db.name}; #error \
if (name.starts_with("ATtiny13")) "PUREBOOT_CLOCK_HZ and PUREBOOT_BAUD select this build's clock and baud — create loader targets with pureboot_add_loader() (README.md)"
return 9.6_MHz; #endif
if (name.starts_with("ATtiny"))
return 8_MHz;
return 16_MHz;
}
using dev = avr::device<{.clock = clock()}>; using dev = avr::device<{.clock = avr::hertz_t{PUREBOOT_CLOCK_HZ}}>;
constexpr avr::baud_t wire_baud{PUREBOOT_BAUD};
// The watchdog reset flag's home: MCUSR, or the classic megas' MCUCSR. // The watchdog reset flag's home: MCUSR, or the classic megas' MCUCSR.
consteval std::int16_t wdrf_field() consteval std::int16_t wdrf_field()
@@ -118,34 +115,43 @@ constexpr std::uint8_t timeout_seconds = PUREBOOT_TIMEOUT;
static_cast<std::uint8_t>((boot_section ? 0 : 1) | (word_flash ? 2 : 0)), static_cast<std::uint8_t>((boot_section ? 0 : 1) | (word_flash ? 2 : 0)),
}; };
// The serial link: the hardware USART where the chip has one, the polled // The serial link. PUREBOOT_USART forces a hardware USART instance,
// software UART (no vector — the table belongs to the application) on PB0/PB1 // PUREBOOT_SOFT_SERIAL the polled software UART (no vector — the table
// elsewhere. Both are class templates on the clock so only the selected // belongs to the application) on PUREBOOT_RX/PUREBOOT_TX; with neither, the
// backend is ever instantiated. pending() is the cheap line test the // chip's first USART where it has one and the software UART elsewhere. Both
// activation window polls; rx() then picks the byte up; drain() holds until // are class templates on the clock so only the selected backend is ever
// the last transmitted frame is fully on the wire (the jump hand-over must // instantiated. pending() is the cheap line test the activation window
// not let the target's re-init clip the ack). // polls; rx() then picks the byte up; drain() holds until the last
template <avr::hertz_t C> // transmitted frame is fully on the wire (the jump hand-over must not let
consteval std::int16_t rxc_field() // the target's re-init clip the ack).
{ #if defined(PUREBOOT_SOFT_SERIAL) && defined(PUREBOOT_USART)
return avr::uart::detail::ufield<'0', "UCSR#A", "RXC#">(); #error "PUREBOOT_SOFT_SERIAL and PUREBOOT_USART select opposing serial backends"
} #endif
#if !defined(PUREBOOT_RX)
#define PUREBOOT_RX pb0
#endif
#if !defined(PUREBOOT_TX)
#define PUREBOOT_TX pb1
#endif
#if defined(PUREBOOT_USART)
constexpr char usart_digit = '0' + PUREBOOT_USART;
#else
constexpr char usart_digit = '0';
#endif
template <avr::hertz_t C> // Whether the chip carries the selected USART: the suffixed instance name,
consteval std::int16_t txc_field() // or — for instance 0 — the classic megas' un-numbered block.
consteval bool usart_exists()
{ {
return avr::uart::detail::ufield<'0', "UCSR#A", "TXC#">(); const char name[] = {'U', 'S', 'A', 'R', 'T', usart_digit};
} if (avr::hw::db.has_instance(std::string_view{name, sizeof(name)}))
return true;
template <avr::hertz_t C> return usart_digit == '0' && avr::hw::db.has_instance("USART");
consteval std::int16_t status_reg()
{
return avr::uart::detail::ureg<'0', "UCSR#A">();
} }
template <avr::hertz_t C> template <avr::hertz_t C>
struct hardware_link { struct hardware_link {
using uart = avr::uart::usart0<C, {.baud = 115200_Bd, .max_baud_error = 2.5_pct}>; using uart = avr::uart::usart<usart_digit, C, {.baud = wire_baud, .max_baud_error = 2.5_pct}>;
// The compiled idle poll: lds UCSR0A (2), sbrc skipping the exit (2), // The compiled idle poll: lds UCSR0A (2), sbrc skipping the exit (2),
// sbiw + sbci + sbci + brne (6). // sbiw + sbci + sbci + brne (6).
@@ -158,7 +164,7 @@ struct hardware_link {
static bool pending() static bool pending()
{ {
return avr::hw::field_impl<rxc_field<C>()>::test(); return uart::rx_ready();
} }
static std::uint8_t rx() static std::uint8_t rx()
@@ -173,22 +179,14 @@ struct hardware_link {
static void drain() static void drain()
{ {
// write() leaves the byte draining behind it. Clear a stale TXC0 uart::drain();
// first (W1C by writing the sampled status back — the store a hand
// assembler writes, keeping U2X0), then wait for the fresh
// completion; with a byte still ahead in the shifter TXC0 cannot
// re-set until the last pending byte has fully left.
using status = avr::hw::reg_impl<status_reg<C>()>;
status::write(status::read());
while (!avr::hw::field_impl<txc_field<C>()>::test()) {
}
} }
}; };
template <avr::hertz_t C> template <avr::hertz_t C>
struct software_link { struct software_link {
using rx_t = avr::uart::software_rx_polled<C, avr::pb0, 57600_Bd>; using rx_t = avr::uart::software_rx_polled<C, avr::PUREBOOT_RX, wire_baud>;
using tx_t = avr::uart::software_tx<C, avr::pb1, 57600_Bd>; using tx_t = avr::uart::software_tx<C, avr::PUREBOOT_TX, wire_baud>;
// The compiled idle poll: sbis skipping the exit (2), sbiw + sbci + // The compiled idle poll: sbis skipping the exit (2), sbiw + sbci +
// sbci + brne (6). // sbci + brne (6).
@@ -201,7 +199,7 @@ struct software_link {
static bool pending() static bool pending()
{ {
return !avr::io::input<avr::pb0>::read(); // a start bit has begun return rx_t::start_pending();
} }
static std::uint8_t rx() static std::uint8_t rx()
@@ -220,8 +218,14 @@ struct software_link {
} }
}; };
using link = std::conditional_t<avr::hw::db.has_instance("USART0") || avr::hw::db.has_instance("USART"), #if defined(PUREBOOT_USART)
hardware_link<dev::clock>, software_link<dev::clock>>; static_assert(usart_exists(), "PUREBOOT_USART selects a hardware USART this chip does not have");
using link = hardware_link<dev::clock>;
#elif defined(PUREBOOT_SOFT_SERIAL)
using link = software_link<dev::clock>;
#else
using link = std::conditional_t<usart_exists(), hardware_link<dev::clock>, software_link<dev::clock>>;
#endif
// The application's entry, an absolute address the linker pins (--defsym in // The application's entry, an absolute address the linker pins (--defsym in
// CMakeLists.txt): 0x0000 on the mega (word 0 stays the application's own // CMakeLists.txt): 0x0000 on the mega (word 0 stays the application's own
@@ -417,10 +421,13 @@ void send_fuses()
// return address is a word address, whose high byte is the 256-word slot // return address is a word address, whose high byte is the 256-word slot
// index — on byte-addressed chips doubled back into byte terms. // index — on byte-addressed chips doubled back into byte terms.
// program_flash refuses this one slot and the info block is addressed // program_flash refuses this one slot and the info block is addressed
// from it, so both follow wherever the code was flashed. // from it, so both follow wherever the code was flashed. The high byte is
// spelled as byteswap's low byte: the builtin's value is itself built by
// swapping the two stacked bytes, and the double swap folds to the single
// byte pick a hand assembler writes — `>> 8` leaves the swap materialized.
const std::uint16_t ra_words = reinterpret_cast<std::uint16_t>(__builtin_return_address(0)); const std::uint16_t ra_words = reinterpret_cast<std::uint16_t>(__builtin_return_address(0));
const std::uint8_t slot_high = const std::uint8_t ra_high = static_cast<std::uint8_t>(std::byteswap(ra_words));
word_flash ? static_cast<std::uint8_t>(ra_words >> 8) & 0xfe : static_cast<std::uint8_t>((ra_words >> 8) << 1); const std::uint8_t slot_high = word_flash ? ra_high & 0xfe : static_cast<std::uint8_t>(ra_high << 1);
// The knock: 'p' then 'b', each under a fresh window; any other byte is // The knock: 'p' then 'b', each under a fresh window; any other byte is
// line noise and waits again. Falling out of a window runs the app. // line noise and waits again. Falling out of a window runs the app.
@@ -439,13 +446,13 @@ void send_fuses()
// asserts it), and slots are 512-aligned — so the low byte of its // asserts it), and slots are 512-aligned — so the low byte of its
// link address (in wire units: bytes, or words on the large // link address (in wire units: bytes, or words on the large
// chips) is its offset in any slot, and the high byte of its // chips) is its offset in any slot, and the high byte of its
// runtime address is the running slot's. Built as a byte pair so // runtime address is the running slot's. Composed from the two
// no absolute address is ever materialized. // bytes — the high half is runtime data, so no absolute address
// is ever materialized.
const auto link_low = reinterpret_cast<std::uint16_t>(info_data.data()); const auto link_low = reinterpret_cast<std::uint16_t>(info_data.data());
const std::uint8_t low = const std::uint8_t low =
word_flash ? static_cast<std::uint8_t>(link_low >> 1) : static_cast<std::uint8_t>(link_low); word_flash ? static_cast<std::uint8_t>(link_low >> 1) : static_cast<std::uint8_t>(link_low);
send_flash(std::bit_cast<std::uint16_t>(std::array{low, slot_high}), send_flash(static_cast<std::uint16_t>(low | (slot_high << 8)), static_cast<std::uint8_t>(info_data.size()));
static_cast<std::uint8_t>(info_data.size()));
break; break;
} }
case 'J': { // jump to a wire word address: hand-over and staging transfer case 'J': { // jump to a wire word address: hand-over and staging transfer

View File

@@ -38,11 +38,57 @@ PROMPT = b"+"
PROTOCOL_VERSION = 1 PROTOCOL_VERSION = 1
SLOT = 512 # the loader slot on byte-addressed chips; word-addressed ones (>64 KiB) use 1 KiB — their own smallest boot sector SLOT = 512 # the loader slot on byte-addressed chips; word-addressed ones (>64 KiB) use 1 KiB — their own smallest boot sector
VERBOSE = False
def verbose(message):
"""Detail printed only under --verbose: decisions and derived facts, not
per-byte chatter — the progress bar carries the bulk transfers."""
if VERBOSE:
print(f" {message}")
class Error(Exception): class Error(Exception):
pass pass
class Progress:
"""A transient in-place bar on stderr for the operations that take wire
time. Drawn only when stderr is a tty — logs, pipes and the test harness
see nothing — and erased once done; the summary line each operation
prints afterwards is the persistent record. A zero total (or no label)
disables it, so callers can pass one through unconditionally."""
def __init__(self, label, total, unit="pages"):
self.label, self.total, self.unit = label, total, unit
self.done = 0
self.width = 0
self.live = bool(label) and total > 0 and sys.stderr.isatty()
self._draw()
def __enter__(self):
return self
def __exit__(self, *exc):
if self.live:
sys.stderr.write("\r" + " " * self.width + "\r")
sys.stderr.flush()
def step(self, n=1):
self.done += n
self._draw()
def _draw(self):
if not self.live:
return
bar = 24 * self.done // self.total
line = (f"{self.label:<16} [{'#' * bar}{'-' * (24 - bar)}] "
f"{100 * self.done // self.total:3d}% {self.done}/{self.total} {self.unit}")
self.width = max(self.width, len(line))
sys.stderr.write("\r" + line)
sys.stderr.flush()
# ---------------------------------------------------------------- serial --- # ---------------------------------------------------------------- serial ---
@@ -295,6 +341,23 @@ class Info:
f"EEPROM {self.eeprom_size} B, {vector}" f"EEPROM {self.eeprom_size} B, {vector}"
) )
def lines(self):
"""The info block as one fact per line — what --info prints."""
if self.patch_vector:
hand_over = f"host-patched reset vector, trampoline at {self.base - 2:#06x}"
else:
hand_over = "hardware boot section, jump to word 0"
return (
f"signature {' '.join(f'{b:02x}' for b in self.signature)}",
f"flash {self.flash_size} B, {self.page} B pages"
+ (", word-addressed wire" if self.word_flash else ""),
f"application 0x0000..{self.base - 1:#06x} ({self.base} B)",
f"loader {self.base:#06x} ({self.slot} B slot)",
f"staging {self.stage:#06x}",
f"EEPROM {self.eeprom_size} B",
f"hand-over {hand_over}",
)
class Loader: class Loader:
"""A pureboot session. Between commands the loader has prompted `+` and """A pureboot session. Between commands the loader has prompted `+` and
@@ -312,8 +375,10 @@ class Loader:
absorbs whatever they produced.""" absorbs whatever they produced."""
self.port.flush_input() self.port.flush_input()
deadline = time.monotonic() + wait deadline = time.monotonic() + wait
knocks = 0
while True: while True:
self.port.write(b"pb") self.port.write(b"pb")
knocks += 1
if PROMPT in self.port.read_available(0.4): if PROMPT in self.port.read_available(0.4):
break break
if time.monotonic() > deadline: if time.monotonic() > deadline:
@@ -323,6 +388,7 @@ class Loader:
self.port.write(b"b") self.port.write(b"b")
self.info = Info(self.port.read_exact(12, 2.0)) self.info = Info(self.port.read_exact(12, 2.0))
self._expect_prompt() self._expect_prompt()
verbose(f"loader answered knock {knocks}; info block read")
return self.info return self.info
def _expect_prompt(self, timeout=2.0): def _expect_prompt(self, timeout=2.0):
@@ -373,7 +439,7 @@ class Loader:
head = bytes((ord("W"), wire & 0xFF, wire >> 8)) head = bytes((ord("W"), wire & 0xFF, wire >> 8))
self._command(head + data, 0, 2.0) self._command(head + data, 0, 2.0)
def write_eeprom(self, address, data): def write_eeprom(self, address, data, progress=None):
offset = 0 offset = 0
while offset < len(data): while offset < len(data):
chunk = data[offset : offset + 256] chunk = data[offset : offset + 256]
@@ -382,6 +448,8 @@ class Loader:
for byte in chunk: for byte in chunk:
self.port.write(bytes((byte,))) self.port.write(bytes((byte,)))
self._expect_prompt() # per-byte ack: the write has begun self._expect_prompt() # per-byte ack: the write has begun
if progress:
progress.step()
self._expect_prompt() # the next command prompt self._expect_prompt() # the next command prompt
address += len(chunk) address += len(chunk)
offset += len(chunk) offset += len(chunk)
@@ -487,6 +555,8 @@ def plan_flash(image, info):
final += bytearray([0xFF] * (trampoline_page + page - len(final))) final += bytearray([0xFF] * (trampoline_page + page - len(final)))
jump = rjmp_to(trampoline_word, entry, flash_words) jump = rjmp_to(trampoline_word, entry, flash_words)
final[info.base - 2], final[info.base - 1] = jump & 0xFF, jump >> 8 final[info.base - 2], final[info.base - 1] = jump & 0xFF, jump >> 8
verbose(f"vector surgery: word 0 -> loader {info.base:#06x}, "
f"trampoline {info.base - 2:#06x} -> entry word {entry:#06x}")
pages = {a: bytes(final[a : a + page]) for a in range(0, len(final), page)} pages = {a: bytes(final[a : a + page]) for a in range(0, len(final), page)}
return pages return pages
@@ -681,17 +751,22 @@ class UpdateState:
os.unlink(self.path) os.unlink(self.path)
def write_differing(loader, base, content, order=None): def write_differing(loader, base, content, order=None, label=None):
"""Program the pages of `content` at `base` that differ from flash — """Program the pages of `content` at `base` that differ from flash —
idempotent, so a resumed phase redoes only what an interruption left.""" idempotent, so a resumed phase redoes only what an interruption left.
A label puts the compare-and-program loop on the progress bar."""
page = loader.info.page page = loader.info.page
offsets = order if order is not None else range(0, len(content), page) offsets = list(order) if order is not None else list(range(0, len(content), page))
written = 0 written = 0
for offset in offsets: with Progress(label, len(offsets)) as bar:
want = content[offset : offset + page] for offset in offsets:
if loader.read_flash(base + offset, page) != want: want = content[offset : offset + page]
loader.write_page(base + offset, want) if loader.read_flash(base + offset, page) != want:
written += 1 loader.write_page(base + offset, want)
written += 1
bar.step()
if label:
verbose(f"{label}: {written} of {len(offsets)} pages differed")
for at in range(0, len(content), 256): for at in range(0, len(content), 256):
if loader.read_flash(base + at, min(256, len(content) - at)) != content[at : at + 256]: if loader.read_flash(base + at, min(256, len(content) - at)) != content[at : at + 256]:
raise Error(f"verify failed at {base + at:#06x} after programming") raise Error(f"verify failed at {base + at:#06x} after programming")
@@ -723,6 +798,10 @@ def op_update_loader(loader, wait, path, state_path, fuse_bytes):
page = info.page page = info.page
state = UpdateState(state_path) state = UpdateState(state_path)
if os.path.exists(state_path):
verbose(f"resuming the update recorded in {state_path}")
else:
verbose(f"saving the staging slot to {state_path}")
state.load_or_save(loader) state.load_or_save(loader)
# Install the staging copy — unless a loader already sits whole in the # Install the staging copy — unless a loader already sits whole in the
@@ -741,7 +820,7 @@ def op_update_loader(loader, wait, path, state_path, fuse_bytes):
current = loader.read_flash(info.stage, info.slot) current = loader.read_flash(info.stage, info.slot)
staged_loader = image_info(current[:268]) staged_loader = image_info(current[:268])
if staged_loader is not None and staged_loader.raw == info.raw and current == state.staging: if staged_loader is not None and staged_loader.raw == info.raw and current == state.staging:
print(f"staging slot already holds a loader — left in place") print("staging slot already holds a loader — left in place")
else: else:
# On a chip whose staging slot starts at address 0 (the 1 KB # On a chip whose staging slot starts at address 0 (the 1 KB
# tiny13s), its first page carries the reset vector: written last, # tiny13s), its first page carries the reset vector: written last,
@@ -750,30 +829,34 @@ def op_update_loader(loader, wait, path, state_path, fuse_bytes):
order = list(range(0, info.slot, page)) order = list(range(0, info.slot, page))
if info.stage == 0: if info.stage == 0:
order = order[1:] + [0] order = order[1:] + [0]
if write_differing(loader, info.stage, staged, order): if write_differing(loader, info.stage, staged, order, label="staging copy"):
print(f"staging copy installed at {info.stage:#06x}") print(f"staging copy installed at {info.stage:#06x}")
# Enter it and let it rewrite the resident slot. Where a patched reset # Enter it and let it rewrite the resident slot. Where a patched reset
# vector routes through the resident (a tiny with the staging slot away # vector routes through the resident (a tiny with the staging slot away
# from page 0), word 0 is re-aimed at the staging copy around the # from page 0), word 0 is re-aimed at the staging copy around the
# rewrite, so a power failure mid-rewrite still resets into a loader. # rewrite, so a power failure mid-rewrite still resets into a loader.
verbose(f"entering the staging copy at {info.stage:#06x}")
loader.enter_copy(info.stage, wait) loader.enter_copy(info.stage, wait)
redirect = info.patch_vector and info.stage != 0 redirect = info.patch_vector and info.stage != 0
if redirect: if redirect:
verbose("word 0 re-aimed at the staging copy for the rewrite")
patch_word0(loader, state.page0, info.stage) patch_word0(loader, state.page0, info.stage)
if write_differing(loader, info.base, resident): if write_differing(loader, info.base, resident, label="resident"):
print(f"resident loader rewritten at {info.base:#06x}") print(f"resident loader rewritten at {info.base:#06x}")
# Enter the new resident and put the staging region back: page 0 first # Enter the new resident and put the staging region back: page 0 first
# where it lives in that region (word 0 then points at the new resident # where it lives in that region (word 0 then points at the new resident
# for the rest of the restore), the saved trampoline with the rest. # for the rest of the restore), the saved trampoline with the rest.
verbose(f"entering the new resident at {info.base:#06x}")
loader.enter_copy(info.base, wait) loader.enter_copy(info.base, wait)
if redirect: if redirect:
verbose("word 0 restored")
write_differing(loader, 0, state.page0) write_differing(loader, 0, state.page0)
order = list(range(0, info.slot, page)) order = list(range(0, info.slot, page))
if info.stage == 0: if info.stage == 0:
order = [0] + order[1:] order = [0] + order[1:]
write_differing(loader, info.stage, state.staging, order) write_differing(loader, info.stage, state.staging, order, label="staging restore")
state.discard() state.discard()
print(f"loader updated: {len(image)} B at {info.base:#06x}, staging region restored") print(f"loader updated: {len(image)} B at {info.base:#06x}, staging region restored")
@@ -808,39 +891,49 @@ def op_erase_flash(loader):
is erased and the reset walk reaches the loader anyway.""" is erased and the reset walk reaches the loader anyway."""
blank = bytes([0xFF] * loader.info.page) blank = bytes([0xFF] * loader.info.page)
addresses = range(0, loader.info.base, loader.info.page) addresses = range(0, loader.info.base, loader.info.page)
for address in reversed(addresses) if loader.info.patch_vector else addresses: with Progress("erase", len(addresses)) as bar:
loader.write_page(address, blank) for address in reversed(addresses) if loader.info.patch_vector else addresses:
loader.write_page(address, blank)
bar.step()
print(f"erase: {loader.info.base // loader.info.page} pages") print(f"erase: {loader.info.base // loader.info.page} pages")
def op_erase_eeprom(loader): def op_erase_eeprom(loader):
loader.write_eeprom(0, bytes([0xFF] * loader.info.eeprom_size)) with Progress("erase EEPROM", loader.info.eeprom_size, "B") as bar:
loader.write_eeprom(0, bytes([0xFF] * loader.info.eeprom_size), progress=bar)
print(f"erase: {loader.info.eeprom_size} B of EEPROM") print(f"erase: {loader.info.eeprom_size} B of EEPROM")
def op_flash(loader, path, erase, verify, fuse_bytes=None, force=False): def op_flash(loader, path, erase, verify, fuse_bytes=None, force=False):
image = load_image(path) image = load_image(path)
verbose(f"{path}: {len(image)} B image")
pages = plan_flash(image, loader.info) pages = plan_flash(image, loader.info)
check_walk_region(pages, loader.info, fuse_bytes, force) check_walk_region(pages, loader.info, fuse_bytes, force)
if erase: if erase:
op_erase_flash(loader) op_erase_flash(loader)
order = covered(pages, loader.info, skip_blank=erase) order = covered(pages, loader.info, skip_blank=erase)
for address in order: if len(order) != len(pages):
loader.write_page(address, pages[address]) verbose(f"{len(pages) - len(order)} blank pages skipped (erased flash underneath)")
with Progress("flash", len(order)) as bar:
for address in order:
loader.write_page(address, pages[address])
bar.step()
print(f"flash: {path}: {len(order)} pages") print(f"flash: {path}: {len(order)} pages")
if verify: if verify:
verify_pages(loader, pages) verify_pages(loader, pages)
def verify_pages(loader, pages): def verify_pages(loader, pages):
for address in sorted(pages): with Progress("verify", len(pages)) as bar:
got = loader.read_flash(address, loader.info.page) for address in sorted(pages):
if got != pages[address]: got = loader.read_flash(address, loader.info.page)
first = next(i for i in range(len(got)) if got[i] != pages[address][i]) if got != pages[address]:
raise Error( first = next(i for i in range(len(got)) if got[i] != pages[address][i])
f"verify failed at {address + first:#06x}: " raise Error(
f"wrote {pages[address][first]:02x}, read {got[first]:02x}" f"verify failed at {address + first:#06x}: "
) f"wrote {pages[address][first]:02x}, read {got[first]:02x}"
)
bar.step()
print(f"verify: {len(pages)} pages ok") print(f"verify: {len(pages)} pages ok")
@@ -848,8 +941,19 @@ def op_verify_flash(loader, path):
verify_pages(loader, plan_flash(load_image(path), loader.info)) verify_pages(loader, plan_flash(load_image(path), loader.info))
def read_progress(reader, total, label):
"""A bulk read in 256-byte wire chunks under a progress bar."""
data = b""
with Progress(label, total, "B") as bar:
while len(data) < total:
chunk = min(256, total - len(data))
data += reader(len(data), chunk)
bar.step(chunk)
return data
def op_read_flash(loader, path): def op_read_flash(loader, path):
data = loader.read_flash(0, loader.info.base) data = read_progress(loader.read_flash, loader.info.base, "read flash")
open(path, "wb").write(data) open(path, "wb").write(data)
print(f"read flash: {len(data)} B -> {path}") print(f"read flash: {len(data)} B -> {path}")
@@ -860,10 +964,11 @@ def op_eeprom(loader, path, erase, verify):
raise Error(f"EEPROM image is {len(image)} B, device has {loader.info.eeprom_size}") raise Error(f"EEPROM image is {len(image)} B, device has {loader.info.eeprom_size}")
if erase: if erase:
op_erase_eeprom(loader) op_erase_eeprom(loader)
loader.write_eeprom(0, image) with Progress("eeprom", len(image), "B") as bar:
loader.write_eeprom(0, image, progress=bar)
print(f"eeprom: {path}: {len(image)} B") print(f"eeprom: {path}: {len(image)} B")
if verify: if verify:
got = loader.read_eeprom(0, len(image)) got = read_progress(loader.read_eeprom, len(image), "verify EEPROM")
if got != image: if got != image:
first = next(i for i in range(len(got)) if got[i] != image[i]) first = next(i for i in range(len(got)) if got[i] != image[i])
raise Error(f"verify failed at EEPROM {first:#06x}: wrote {image[first]:02x}, read {got[first]:02x}") raise Error(f"verify failed at EEPROM {first:#06x}: wrote {image[first]:02x}, read {got[first]:02x}")
@@ -872,7 +977,7 @@ def op_eeprom(loader, path, erase, verify):
def op_verify_eeprom(loader, path): def op_verify_eeprom(loader, path):
image = load_image(path) image = load_image(path)
got = loader.read_eeprom(0, len(image)) got = read_progress(loader.read_eeprom, len(image), "verify EEPROM")
if got != image: if got != image:
first = next(i for i in range(len(got)) if got[i] != image[i]) first = next(i for i in range(len(got)) if got[i] != image[i])
raise Error(f"verify failed at EEPROM {first:#06x}: expected {image[first]:02x}, read {got[first]:02x}") raise Error(f"verify failed at EEPROM {first:#06x}: expected {image[first]:02x}, read {got[first]:02x}")
@@ -880,15 +985,30 @@ def op_verify_eeprom(loader, path):
def op_read_eeprom(loader, path): def op_read_eeprom(loader, path):
data = loader.read_eeprom(0, loader.info.eeprom_size) data = read_progress(loader.read_eeprom, loader.info.eeprom_size, "read EEPROM")
open(path, "wb").write(data) open(path, "wb").write(data)
print(f"read EEPROM: {len(data)} B -> {path}") print(f"read EEPROM: {len(data)} B -> {path}")
def op_fuses(loader): def op_fuses(loader):
low, lock, extended, high = loader.read_fuses() low, lock, extended, high = loader.read_fuses()
print(f"fuses: low {low:02x} high {high:02x} extended {extended:02x} lock {lock:02x}") print("fuses:")
return bytes((low, lock, extended, high)) print(f" low 0x{low:02x}")
print(f" high 0x{high:02x}")
print(f" extended 0x{extended:02x}")
print(f" lock 0x{lock:02x}")
fuse_bytes = bytes((low, lock, extended, high))
# On a boot-sectioned mega the BOOTSZ/BOOTRST decode is the fuse fact the
# loader's whole deployment hangs on — say it in words.
if not loader.info.patch_vector:
try:
bootrst, bls_start = mega_boot(loader.info, fuse_bytes)
reset = "reset enters it" if bootrst else "reset boots the application"
print(f" boot section at {bls_start:#06x} ({loader.info.flash_size - bls_start} B), "
f"BOOTRST {'programmed' if bootrst else 'unprogrammed'}{reset}")
except Error:
pass # unknown signature: the raw bytes above still stand
return fuse_bytes
# -------------------------------------------------------------------- cli --- # -------------------------------------------------------------------- cli ---
@@ -918,7 +1038,11 @@ def main():
parser.add_argument("--verify-eeprom", metavar="FILE", help="compare EEPROM against an image") parser.add_argument("--verify-eeprom", metavar="FILE", help="compare EEPROM against an image")
parser.add_argument("--force", action="store_true", help="override refusable safety checks") parser.add_argument("--force", action="store_true", help="override refusable safety checks")
parser.add_argument("--stay", action="store_true", help="leave the loader in its session") parser.add_argument("--stay", action="store_true", help="leave the loader in its session")
parser.add_argument("-v", "--verbose", action="store_true",
help="print decisions and derived facts as operations run")
args = parser.parse_args() args = parser.parse_args()
global VERBOSE
VERBOSE = args.verbose
if args.update_loader and (args.flash or args.erase_flash): if args.update_loader and (args.flash or args.erase_flash):
parser.error("--update-loader does not combine with application flash operations") parser.error("--update-loader does not combine with application flash operations")
@@ -931,11 +1055,14 @@ def main():
parser.error("--assume-fuses takes 8 hex digits: low,lock,extended,high") parser.error("--assume-fuses takes 8 hex digits: low,lock,extended,high")
port = Port(args.port, args.baud) port = Port(args.port, args.baud)
verbose(f"{args.port}: {args.baud} Bd 8N1, DTR/RTS asserted")
try: try:
loader = Loader(port) loader = Loader(port)
info = loader.connect(args.wait) info = loader.connect(args.wait)
if args.info: if args.info:
print(f"device: {info.describe()}") print("device:")
for line in info.lines():
print(f" {line}")
fuse_bytes = fuse_override fuse_bytes = fuse_override
if args.fuses or (args.update_loader and not info.patch_vector and fuse_bytes is None): if args.fuses or (args.update_loader and not info.patch_vector and fuse_bytes is None):
read = op_fuses(loader) read = op_fuses(loader)

View File

@@ -4,11 +4,16 @@
// surgery, actually launched it. Linked normally (crt, vectors at 0); on // surgery, actually launched it. Linked normally (crt, vectors at 0); on
// the tinies its reset vector is the rjmp the host re-homes. // the tinies its reset vector is the rjmp the host re-homes.
// //
// On the mega it then listens, and an 'L' makes it jump into the resident // On the hardware-USART link it then listens, and an 'L' makes it jump into
// loader — the application-owned loader entry a BOOTRST-unprogrammed mega // the resident loader — the application-owned loader entry a
// relies on (reset always boots the application there), exercised by the // BOOTRST-unprogrammed mega relies on (reset always boots the application
// self-update tests. The tinies idle: reset reaches their loader through // there), exercised by the self-update tests. The software link idles:
// the patched vector, so the application owes it nothing. // reset reaches those loaders through the patched vector (or the runner
// models BOOTRST), so the application owes them nothing.
//
// The fixture speaks the deployment its loader was built for: the same
// PUREBOOT_* defines configure it, and without them it assumes the stock
// deployment (the crystal/RC clock table below, the chip's natural link).
#include <libavr/libavr.hpp> #include <libavr/libavr.hpp>
using namespace avr::literals; using namespace avr::literals;
@@ -17,19 +22,44 @@ namespace {
consteval avr::hertz_t clock() consteval avr::hertz_t clock()
{ {
#if defined(PUREBOOT_CLOCK_HZ)
return avr::hertz_t{PUREBOOT_CLOCK_HZ};
#else
auto name = std::string_view{avr::hw::db.name}; auto name = std::string_view{avr::hw::db.name};
if (name.starts_with("ATtiny13")) if (name.starts_with("ATtiny13"))
return 9.6_MHz; return 9.6_MHz;
if (name.starts_with("ATtiny")) if (name.starts_with("ATtiny"))
return 8_MHz; return 8_MHz;
return 16_MHz; return 16_MHz;
#endif
}
#if !defined(PUREBOOT_TX)
#define PUREBOOT_TX pb1
#endif
#if !defined(PUREBOOT_USART)
#define PUREBOOT_USART 0
#endif
consteval bool use_hardware()
{
#if defined(PUREBOOT_SOFT_SERIAL)
return false;
#else
return avr::hw::db.has_instance("USART0") || avr::hw::db.has_instance("USART");
#endif
} }
using dev = avr::device<{.clock = clock()}>; using dev = avr::device<{.clock = clock()}>;
template <avr::hertz_t C, bool Hardware = avr::hw::db.has_instance("USART0") || avr::hw::db.has_instance("USART")> template <avr::hertz_t C, bool Hardware = use_hardware()>
struct link { struct link {
using tx_t = avr::uart::usart0<C, {.baud = 115200_Bd, .max_baud_error = 2.5_pct}>; #if defined(PUREBOOT_BAUD)
static constexpr avr::baud_t baud{PUREBOOT_BAUD};
#else
static constexpr avr::baud_t baud{115200};
#endif
using tx_t = avr::uart::usart<'0' + PUREBOOT_USART, C, {.baud = baud, .max_baud_error = 2.5_pct}>;
static void tx(char c) static void tx(char c)
{ {
tx_t::write(static_cast<std::uint8_t>(c)); tx_t::write(static_cast<std::uint8_t>(c));
@@ -47,7 +77,12 @@ struct link {
template <avr::hertz_t C> template <avr::hertz_t C>
struct link<C, false> { struct link<C, false> {
using tx_t = avr::uart::software_tx<C, avr::pb1, 57600_Bd>; #if defined(PUREBOOT_BAUD)
static constexpr avr::baud_t baud{PUREBOOT_BAUD};
#else
static constexpr avr::baud_t baud{57600};
#endif
using tx_t = avr::uart::software_tx<C, avr::PUREBOOT_TX, baud>;
static void tx(char c) static void tx(char c)
{ {
tx_t::write(static_cast<std::uint8_t>(c)); tx_t::write(static_cast<std::uint8_t>(c));

View File

@@ -8,8 +8,11 @@ import subprocess
class Device: class Device:
def __init__(self, binary, elf, mcu, hz, base_hex, page, baud, dump, reset_hex=None, resume=None): def __init__(self, binary, elf, mcu, hz, base_hex, page, baud, dump, reset_hex=None, resume=None, link=None):
cmd = [binary, elf, mcu, hz, base_hex, str(page), str(baud), dump] cmd = [binary]
if link:
cmd += ["-l", link]
cmd += [elf, mcu, hz, base_hex, str(page), str(baud), dump]
if reset_hex is not None or resume is not None: if reset_hex is not None or resume is not None:
# Chips without a hardware boot section — the tinies and the # Chips without a hardware boot section — the tinies and the
# m48s — reset to address 0 like silicon; the boot-sectioned # m48s — reset to address 0 like silicon; the boot-sectioned

View File

@@ -5,15 +5,15 @@ through flash + EEPROM + fuse + hand-over scenarios, and cross-check
the tool's view against the simulator's ground-truth memory dumps. the tool's view against the simulator's ground-truth memory dumps.
Usage: pbtest.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page> Usage: pbtest.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
<baud> <eeprom_size> <app_bin> <tool_py> <workdir> <baud> <eeprom_size> <app_bin> <tool_py> <workdir> [link]
The optional link is the runner's -l spec (usart1, sw:B5,B1, ...) for a
loader built off the chip's natural serial default.
Exits 0 if every scenario passes. Exits 0 if every scenario passes.
""" """
import os import os
import signal
import subprocess
import sys import sys
import time
def fail(message): def fail(message):
@@ -33,53 +33,14 @@ def rjmp_decode(word, at, flash_words):
return (at + 1 + offset) % flash_words return (at + 1 + offset) % flash_words
class Device:
def __init__(self, binary, elf, mcu, hz, base, page, baud, dump):
self.proc = subprocess.Popen(
[binary, elf, mcu, hz, base, str(page), str(baud), dump],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)
self.dump = dump
self.pty = None
deadline = time.time() + 5
while time.time() < deadline:
line = self.proc.stdout.readline()
if not line:
break
if line.startswith("PB_PTY"):
self.pty = line.split()[1]
break
if not self.pty:
self.stop()
raise RuntimeError("device did not report a pty")
def stop(self):
self.proc.terminate()
try:
self.proc.wait(timeout=3)
except subprocess.TimeoutExpired:
self.proc.kill()
def run_tool(tool, pty, baud, *args):
result = subprocess.run(
[sys.executable, tool, "--port", pty, "--baud", str(baud), "--wait", "20", *args],
capture_output=True,
text=True,
timeout=120,
)
print(result.stdout, end="")
if result.returncode != 0:
fail(f"tool exited {result.returncode}: {result.stderr.strip()}")
return result.stdout
def main(): def main():
(device_bin, elf, mcu, hz, base_hex, page, baud, eeprom_size, app_bin, tool, workdir) = sys.argv[1:] args = sys.argv[1:]
link = args.pop() if len(args) == 12 else None
(device_bin, elf, mcu, hz, base_hex, page, baud, eeprom_size, app_bin, tool, workdir) = args
base, page, baud, eeprom_size = int(base_hex, 0), int(page), int(baud), int(eeprom_size) base, page, baud, eeprom_size = int(base_hex, 0), int(page), int(baud), int(eeprom_size)
sys.path.insert(0, os.path.dirname(os.path.abspath(tool))) sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import pbsim
import pureboot as pb import pureboot as pb
os.makedirs(workdir, exist_ok=True) os.makedirs(workdir, exist_ok=True)
@@ -105,19 +66,19 @@ def main():
+ bytes([flags]) + bytes([flags])
) )
device = Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump) device = pbsim.Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump, link=link)
try: try:
# Session 1: knock from reset, identify, program everything, stay. # Session 1: knock from reset, identify, program everything, stay.
out = run_tool(tool, device.pty, baud, "--info", "--fuses", "--flash", app_bin, out = pbsim.run_tool(tool, device.pty, baud, "--info", "--fuses", "--flash", app_bin,
"--eeprom", ee_path, "--stay") "--eeprom", ee_path, "--stay")
for needed in ("device: signature", "fuses:", "verify:", "stays"): for needed in ("signature", "fuses", "verify:", "stays"):
if needed not in out: if needed not in out:
fail(f"session 1 output lacks {needed!r}") fail(f"session 1 output lacks {needed!r}")
# Session 2: reconnect into the live session, verify, dump, hand over # Session 2: reconnect into the live session, verify, dump, hand over
# is deferred — the pty must be reopened for the APP banner first. # is deferred — the pty must be reopened for the APP banner first.
out = run_tool(tool, device.pty, baud, "--verify-flash", app_bin, "--verify-eeprom", ee_path, out = pbsim.run_tool(tool, device.pty, baud, "--verify-flash", app_bin, "--verify-eeprom", ee_path,
"--read-flash", read_flash, "--read-eeprom", read_eeprom, "--stay") "--read-flash", read_flash, "--read-eeprom", read_eeprom, "--stay")
if out.count("verify:") != 2: if out.count("verify:") != 2:
fail("session 2 did not verify both memories") fail("session 2 did not verify both memories")
@@ -136,7 +97,7 @@ def main():
# runner resets them to address 0 like silicon) or BOOTRST (mega). # runner resets them to address 0 like silicon) or BOOTRST (mega).
# The loader must answer a fresh knock, and the 'J' hand-over must # The loader must answer a fresh knock, and the 'J' hand-over must
# land in the application, which banners on the same link. # land in the application, which banners on the same link.
device.proc.send_signal(signal.SIGUSR1) device.reset()
port = pb.Port(device.pty, baud) port = pb.Port(device.pty, baud)
try: try:
loader = pb.Loader(port) loader = pb.Loader(port)

View File

@@ -1,12 +1,16 @@
// simavr "device" for the pureboot protocol tests, all three chips. Loads // simavr "device" for the pureboot protocol tests, every chip. Loads the
// the boot-linked ELF at the loader base, starts execution there (BOOTRST / // boot-linked ELF at the loader base, starts execution there (BOOTRST / the
// the patched vector are not what is under test), and exposes the loader's // patched vector are not what is under test), and exposes the loader's
// serial link as a pty for the real host tool: // serial link as a pty for the real host tool:
// //
// - Megas: the hardware USART through simavr's uart_pty. // - Hardware USART builds: simavr's uart_pty on the selected instance.
// - Tinies: an 8N1 bridge between a pty and the GPIO software UART // - Software UART builds: an 8N1 bridge between a pty and the GPIO pins,
// (drives PB0, the loader's RX; decodes PB1, its TX), timed against the // timed against the simulated cycle counter (drives the loader's RX,
// simulated cycle counter. // decodes its TX).
//
// The link follows the chip's natural default (USART0 on the megas, the
// software UART on PB0/PB1 elsewhere) unless -l overrides it: `-l usart1`
// for the second instance, `-l sw:B5,B1` for a software build's RX,TX pins.
// //
// simavr's tiny cores decode the SPM opcode but attach no NVM module — SPM // simavr's tiny cores decode the SPM opcode but attach no NVM module — SPM
// is a silent no-op (the mega's boot section has one, avr_flash). The // is a silent no-op (the mega's boot section has one, avr_flash). The
@@ -38,11 +42,31 @@
static avr_t *avr; static avr_t *avr;
static uart_pty_t uart_pty; static uart_pty_t uart_pty;
static int use_uart_pty; static int link_software;
static char uart_digit = '0';
static char sw_rx_port = 'B', sw_tx_port = 'B';
static int sw_rx_bit = 0, sw_tx_bit = 1;
static const char *dump_path; static const char *dump_path;
static uint32_t reset_pc; static uint32_t reset_pc;
static volatile sig_atomic_t reset_requested; static volatile sig_atomic_t reset_requested;
static int parse_link(const char *spec)
{
if (strcmp(spec, "usart0") == 0 || strcmp(spec, "usart1") == 0) {
link_software = 0;
uart_digit = spec[5];
return 0;
}
if (strncmp(spec, "sw", 2) == 0) {
link_software = 1;
if (spec[2] == '\0')
return 0;
if (sscanf(spec + 2, ":%c%d,%c%d", &sw_rx_port, &sw_rx_bit, &sw_tx_port, &sw_tx_bit) == 4)
return 0;
}
return -1;
}
// simavr 1.6's avr_flash PGERS handler erases spm_pagesize bytes starting at // simavr 1.6's avr_flash PGERS handler erases spm_pagesize bytes starting at
// Z & ~1 instead of the page containing Z (its PGWRT path masks correctly) — // Z & ~1 instead of the page containing Z (its PGWRT path masks correctly) —
// hardware ignores the in-page bits (§26.8.1), so an erase issued with Z // hardware ignores the in-page bits (§26.8.1), so an erase issued with Z
@@ -273,29 +297,42 @@ static void finish(int sig)
} }
} }
} }
if (use_uart_pty) if (!link_software)
uart_pty_stop(&uart_pty); uart_pty_stop(&uart_pty);
_exit(0); _exit(0);
} }
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
if (argc < 8 || argc > 10) { int link_given = 0;
for (int opt; (opt = getopt(argc, argv, "l:")) != -1;) {
if (opt != 'l' || parse_link(optarg) != 0) {
fprintf(stderr, "device: bad link spec (usart0, usart1, sw, or sw:B0,B1 as RX,TX)\n");
return 2;
}
link_given = 1;
}
int args = argc - optind;
if (args < 7 || args > 9) {
fprintf(stderr, fprintf(stderr,
"usage: %s <pureboot.elf> <mcu> <hz> <base_hex> <page> <baud> <flash_dump>" "usage: %s [-l link] <pureboot.elf> <mcu> <hz> <base_hex> <page> <baud> <flash_dump>"
" [reset_hex] [resume_flash]\n" " [reset_hex] [resume_flash]\n"
" reset_hex: reset vector (default: base on the mega, 0 on the tinies)\n" " -l link: usart0 | usart1 | sw[:B0,B1] (RX,TX); default: the chip's own\n"
" reset_hex: reset vector (default: base with a boot section, else 0)\n"
" resume_flash: raw full-flash image loaded instead of the ELF — a prior\n" " resume_flash: raw full-flash image loaded instead of the ELF — a prior\n"
" run's dump, for power-fail resume tests\n", " run's dump, for power-fail resume tests\n",
argv[0]); argv[0]);
return 2; return 2;
} }
argv += optind - 1; // argv[1] is the ELF again, whatever was parsed
const char *mcu_name = argv[2]; const char *mcu_name = argv[2];
uint32_t base = (uint32_t)strtoul(argv[4], NULL, 0); uint32_t base = (uint32_t)strtoul(argv[4], NULL, 0);
unsigned page = (unsigned)atoi(argv[5]); unsigned page = (unsigned)atoi(argv[5]);
unsigned baud = (unsigned)atoi(argv[6]); unsigned baud = (unsigned)atoi(argv[6]);
dump_path = argv[7]; dump_path = argv[7];
use_uart_pty = strncmp(mcu_name, "atmega", 6) == 0; // every mega links over its hardware USART int is_mega = strncmp(mcu_name, "atmega", 6) == 0;
if (!link_given)
link_software = !is_mega; // the chips' natural links: USART0, or PB0/PB1
avr = avr_make_mcu_by_name(mcu_name); avr = avr_make_mcu_by_name(mcu_name);
if (!avr) { if (!avr) {
@@ -306,7 +343,7 @@ int main(int argc, char *argv[])
avr->frequency = (uint32_t)strtoul(argv[3], NULL, 0); avr->frequency = (uint32_t)strtoul(argv[3], NULL, 0);
memset(avr->flash, 0xff, avr->flashend + 1); // real flash powers up erased memset(avr->flash, 0xff, avr->flashend + 1); // real flash powers up erased
if (argc > 9) { if (args > 8) {
// Resume: the full flash image of an interrupted prior run. // Resume: the full flash image of an interrupted prior run.
FILE *f = fopen(argv[9], "rb"); FILE *f = fopen(argv[9], "rb");
if (!f || fread(avr->flash, 1, avr->flashend + 1, f) == 0) { if (!f || fread(avr->flash, 1, avr->flashend + 1, f) == 0) {
@@ -327,8 +364,8 @@ int main(int argc, char *argv[])
// and the boot-section-less m48s reset to word 0 like silicon — erased // and the boot-section-less m48s reset to word 0 like silicon — erased
// flash walks up into the loader, and after the host's surgery the // flash walks up into the loader, and after the host's surgery the
// patched vector routes there. // patched vector routes there.
int boot_section = use_uart_pty && strncmp(mcu_name, "atmega48", 8) != 0; int boot_section = is_mega && strncmp(mcu_name, "atmega48", 8) != 0;
reset_pc = argc > 8 ? (uint32_t)strtoul(argv[8], NULL, 0) : (boot_section ? base : 0); reset_pc = args > 7 ? (uint32_t)strtoul(argv[8], NULL, 0) : (boot_section ? base : 0);
avr->pc = reset_pc; avr->pc = reset_pc;
avr->codeend = avr->flashend; avr->codeend = avr->flashend;
@@ -341,27 +378,34 @@ int main(int argc, char *argv[])
avr_ioctl(avr, AVR_IOCTL_EEPROM_SET, &seed); avr_ioctl(avr, AVR_IOCTL_EEPROM_SET, &seed);
} }
if (use_uart_pty) { // The megas carry simavr's avr_flash module (and its two gaps the wrap
// above fixes); the tinies get the NVM module simavr lacks. Which serial
// bridge runs is the link's business, not the chip class's.
if (is_mega) {
fix_mega_flash_erase(); fix_mega_flash_erase();
// POLL_SLEEP paces an idle-polling loader in host real time (a
// no-hardware CPU-saving hack); clear it so cycles run free.
uint32_t flags = 0;
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS('0'), &flags);
flags &= ~AVR_UART_FLAG_POLL_SLEEP;
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS('0'), &flags);
uart_pty_init(avr, &uart_pty);
uart_pty_connect(&uart_pty, '0');
printf("PB_PTY %s\n", uart_pty.pty.slavename);
} else { } else {
nvm.page = page; nvm.page = page;
memset(nvm.buffer, 0xff, sizeof(nvm.buffer)); memset(nvm.buffer, 0xff, sizeof(nvm.buffer));
nvm.io.kind = "tiny_nvm"; nvm.io.kind = "tiny_nvm";
nvm.io.ioctl = nvm_ioctl; nvm.io.ioctl = nvm_ioctl;
avr_register_io(avr, &nvm.io); avr_register_io(avr, &nvm.io);
}
if (!link_software) {
// POLL_SLEEP paces an idle-polling loader in host real time (a
// no-hardware CPU-saving hack); clear it so cycles run free.
uint32_t flags = 0;
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS(uart_digit), &flags);
flags &= ~AVR_UART_FLAG_POLL_SLEEP;
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS(uart_digit), &flags);
uart_pty_init(avr, &uart_pty);
uart_pty_connect(&uart_pty, uart_digit);
printf("PB_PTY %s\n", uart_pty.pty.slavename);
} else {
bit_cycles = (avr->frequency + baud / 2) / baud; // matches uart.hpp's own rounding exactly bit_cycles = (avr->frequency + baud / 2) / baud; // matches uart.hpp's own rounding exactly
rx_pin = avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ('B'), 0); rx_pin = avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ(sw_rx_port), (unsigned)sw_rx_bit);
avr_irq_register_notify(avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ('B'), 1), tx_hook, NULL); avr_irq_register_notify(avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ(sw_tx_port), (unsigned)sw_tx_bit), tx_hook,
NULL);
avr_raise_irq(rx_pin, 1); // idle line avr_raise_irq(rx_pin, 1); // idle line
int slave; int slave;
@@ -389,18 +433,27 @@ int main(int argc, char *argv[])
reset_requested = 0; reset_requested = 0;
avr_reset(avr); avr_reset(avr);
avr->pc = reset_pc; avr->pc = reset_pc;
if (use_uart_pty) { // reset restores the pacing hack; re-clear it if (!link_software) { // reset restores the pacing hack; re-clear it
uint32_t flags = 0; uint32_t flags = 0;
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS('0'), &flags); avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS(uart_digit), &flags);
flags &= ~AVR_UART_FLAG_POLL_SLEEP; flags &= ~AVR_UART_FLAG_POLL_SLEEP;
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS('0'), &flags); avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS(uart_digit), &flags);
} else { } else {
bridge_reset(); bridge_reset();
} }
} }
if (!use_uart_pty && ++since_poll >= 2000) { if (link_software && ++since_poll >= 2000) {
since_poll = 0; since_poll = 0;
poll_pty(); poll_pty();
// An unthrottled idle simulation runs the activation window out
// from under the host's real-time knock cadence: a 1 MHz build's
// 8 s window is 8 M cycles — tens of wall milliseconds — so a
// first knock lost to an in-flight reset misses the window
// entirely. Pace the simulation only while the bridge is fully
// quiet (nothing decoding, nothing queued); transfers keep full
// speed, and a quiet window stretches toward real time.
if (!rx_active && !tx_active && rx_head == rx_tail)
usleep(200);
} }
} }
finish(0); finish(0);

37
tools/check.sh Executable file
View File

@@ -0,0 +1,37 @@
#!/bin/bash
# The port's gate: every chip's generated workflow — build, size matrix, and
# the simulator-driven protocol suites. --full adds the reflect-spot builds
# (libavr's rule: reflect compiles are bounded to its spot set, never the
# full matrix). LIBAVR_ROOT must point at the libavr checkout.
set -e
cd "$(dirname "$0")/.."
full=0
[[ "$1" == "--full" ]] && { full=1; shift; }
CHIPS=(attiny13 attiny13a attiny25 attiny45 attiny85
atmega8 atmega8a atmega16 atmega16a atmega32 atmega32a
atmega48 atmega48a atmega48p atmega48pa
atmega88 atmega88a atmega88p atmega88pa
atmega168 atmega168a atmega168p atmega168pa
atmega328 atmega328p
atmega164a atmega164p atmega164pa
atmega324a atmega324p atmega324pa
atmega644 atmega644a atmega644p atmega644pa
atmega1284 atmega1284p)
REFLECT_SPOT=(attiny13a attiny85 atmega8 atmega16a atmega32a atmega48pa
atmega88 atmega168pa atmega328p atmega164a atmega644p atmega1284)
for chip in "${CHIPS[@]}"; do
echo "==== $chip ===="
cmake --workflow --preset "$chip-generated" "$@"
done
if ((full)); then
for chip in "${REFLECT_SPOT[@]}"; do
echo "==== $chip reflect ===="
cmake --workflow --preset "$chip-reflect" "$@"
done
fi
echo "check: every chip green"

90
tools/make_presets.py Executable file
View File

@@ -0,0 +1,90 @@
#!/usr/bin/env python3
"""Regenerate CMakePresets.json — one uniform pipeline per chip.
Every chip gets generated-mode configure/build/test presets and a workflow
running all three. Reflect-mode presets (configure + build, no tests — the
port's TUs compile identically; the sims prove nothing new there) exist for
libavr's reflect spot set only, mirroring its rule: the full reflect matrix
is never built, one chip per hardware class and pack vintage is.
Run from the repo root: tools/make_presets.py
"""
import json
import os
CHIPS = [
"attiny13", "attiny13a", "attiny25", "attiny45", "attiny85",
"atmega8", "atmega8a", "atmega16", "atmega16a", "atmega32", "atmega32a",
"atmega48", "atmega48a", "atmega48p", "atmega48pa",
"atmega88", "atmega88a", "atmega88p", "atmega88pa",
"atmega168", "atmega168a", "atmega168p", "atmega168pa",
"atmega328", "atmega328p",
"atmega164a", "atmega164p", "atmega164pa",
"atmega324a", "atmega324p", "atmega324pa",
"atmega644", "atmega644a", "atmega644p", "atmega644pa",
"atmega1284", "atmega1284p",
]
# libavr's REFLECT_SPOT (tools/check.sh): one chip per hardware class and
# pack vintage.
REFLECT_SPOT = [
"attiny13a", "attiny85", "atmega8", "atmega16a", "atmega32a",
"atmega48pa", "atmega88", "atmega168pa", "atmega328p", "atmega164a",
"atmega644p", "atmega1284",
]
def main():
configure = [{
"name": "base",
"hidden": True,
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/${presetName}",
"toolchainFile": "$env{LIBAVR_ROOT}/cmake/avr-toolchain.cmake",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
"CMAKE_COLOR_DIAGNOSTICS": "ON",
},
}]
build, test, workflows = [], [], []
def add(chip, mode):
name = f"{chip}-{mode}"
configure.append({
"name": name,
"inherits": "base",
"cacheVariables": {
"LIBAVR_MCU": chip,
"LIBAVR_REFLECT": "ON" if mode == "reflect" else "OFF",
},
})
build.append({"name": name, "configurePreset": name})
steps = [{"type": "configure", "name": name}, {"type": "build", "name": name}]
if mode == "generated":
test.append({"name": name, "configurePreset": name, "output": {"outputOnFailure": True}})
steps.append({"type": "test", "name": name})
workflows.append({"name": name, "steps": steps})
for chip in CHIPS:
add(chip, "generated")
for chip in REFLECT_SPOT:
add(chip, "reflect")
presets = {
"version": 8,
"configurePresets": configure,
"buildPresets": build,
"testPresets": test,
"workflowPresets": workflows,
}
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "CMakePresets.json")
with open(path, "w") as f:
json.dump(presets, f, indent=1)
f.write("\n")
print(f"{len(CHIPS)} chips, {len(REFLECT_SPOT)} reflect: {os.path.normpath(path)}")
if __name__ == "__main__":
main()