Compare commits
10 Commits
v1
...
445e187722
| Author | SHA1 | Date | |
|---|---|---|---|
| 445e187722 | |||
| 250aba5cfb | |||
| 5c900720e3 | |||
| 7d6ef959b2 | |||
| 11ffbce2e2 | |||
| f32a27ff15 | |||
| 57d94cf631 | |||
| 8203a24f33 | |||
| 2906da3272 | |||
| 64c1e484b5 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -14,7 +14,3 @@ Debug
|
|||||||
/build/
|
/build/
|
||||||
compile_commands.json
|
compile_commands.json
|
||||||
.cache/
|
.cache/
|
||||||
|
|
||||||
# Python
|
|
||||||
__pycache__/
|
|
||||||
*.pyc
|
|
||||||
|
|||||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
|||||||
[submodule "libavr"]
|
|
||||||
path = libavr
|
|
||||||
url = ../libavr.git
|
|
||||||
265
CMakeLists.txt
265
CMakeLists.txt
@@ -8,9 +8,6 @@ include(FetchContent)
|
|||||||
if(NOT LIBAVR_ROOT AND DEFINED ENV{LIBAVR_ROOT})
|
if(NOT LIBAVR_ROOT AND DEFINED ENV{LIBAVR_ROOT})
|
||||||
set(LIBAVR_ROOT $ENV{LIBAVR_ROOT})
|
set(LIBAVR_ROOT $ENV{LIBAVR_ROOT})
|
||||||
endif()
|
endif()
|
||||||
if(NOT LIBAVR_ROOT)
|
|
||||||
set(LIBAVR_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/libavr)
|
|
||||||
endif()
|
|
||||||
if(LIBAVR_ROOT)
|
if(LIBAVR_ROOT)
|
||||||
FetchContent_Declare(libavr SOURCE_DIR ${LIBAVR_ROOT})
|
FetchContent_Declare(libavr SOURCE_DIR ${LIBAVR_ROOT})
|
||||||
else()
|
else()
|
||||||
@@ -22,24 +19,13 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
add_compile_options(-Werror) # warnings are errors for the port's own code
|
add_compile_options(-Werror) # warnings are errors for the port's own code
|
||||||
enable_testing()
|
enable_testing()
|
||||||
|
|
||||||
# The behavioral tests drive the real wire protocols over a simavr pty
|
# The behavioral test drives the real TinySafeBoot wire protocol over a
|
||||||
# (as the host tools do) and actually flash the device. The runners are
|
# simavr pty (as the host tools do) and actually flashes the device. The
|
||||||
# host programs built at configure time against libsimavr; if they or
|
# runner is a host program built at configure time against libsimavr; if it
|
||||||
# Python are missing, only the size tests run.
|
# or Python is missing, only the size tests run.
|
||||||
find_program(_host_cc NAMES cc gcc)
|
find_program(_host_cc NAMES cc gcc)
|
||||||
find_package(Python3 COMPONENTS Interpreter)
|
find_package(Python3 COMPONENTS Interpreter)
|
||||||
if(_host_cc AND Python3_FOUND)
|
if(_host_cc AND Python3_FOUND)
|
||||||
set(PB_DEVICE ${CMAKE_BINARY_DIR}/pureboot_device)
|
|
||||||
execute_process(
|
|
||||||
COMMAND ${_host_cc} -O2 -I/usr/include/simavr -I/usr/include/simavr/parts
|
|
||||||
-o ${PB_DEVICE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pureboot_device.c
|
|
||||||
-lsimavr -lsimavrparts -lelf -lutil
|
|
||||||
RESULT_VARIABLE _pbdev_res ERROR_VARIABLE _pbdev_err)
|
|
||||||
if(NOT _pbdev_res EQUAL 0)
|
|
||||||
message(STATUS "pureboot_device not built (${_pbdev_err}) — protocol tests skipped")
|
|
||||||
unset(PB_DEVICE)
|
|
||||||
endif()
|
|
||||||
if(LIBAVR_MCU STREQUAL "atmega328p")
|
|
||||||
set(TSB_DEVICE ${CMAKE_BINARY_DIR}/tsb_device)
|
set(TSB_DEVICE ${CMAKE_BINARY_DIR}/tsb_device)
|
||||||
execute_process(
|
execute_process(
|
||||||
COMMAND ${_host_cc} -O2 -I/usr/include/simavr -I/usr/include/simavr/parts
|
COMMAND ${_host_cc} -O2 -I/usr/include/simavr -I/usr/include/simavr/parts
|
||||||
@@ -52,51 +38,20 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
endif()
|
|
||||||
|
|
||||||
# The ELF is only a container (symbols, section headers) and is never flashed —
|
|
||||||
# and the host tool's load_image() dispatches on extension, so handing it one
|
|
||||||
# would silently program the header bytes. Every loader image therefore gets
|
|
||||||
# both flashable forms beside it at link time: .hex for avrdude, and .bin for
|
|
||||||
# the host tool's raw path (which is what the reloc and update tests convert to
|
|
||||||
# on the fly). .eeprom is dropped — EEPROM content is its own update.
|
|
||||||
function(add_image_outputs name)
|
|
||||||
add_custom_command(TARGET ${name} POST_BUILD
|
|
||||||
COMMAND ${CMAKE_OBJCOPY} -O ihex -R .eeprom
|
|
||||||
$<TARGET_FILE:${name}> $<TARGET_FILE:${name}>.hex
|
|
||||||
COMMAND ${CMAKE_OBJCOPY} -O binary -R .eeprom
|
|
||||||
$<TARGET_FILE:${name}> $<TARGET_FILE:${name}>.bin)
|
|
||||||
endfunction()
|
|
||||||
|
|
||||||
# The TinySafeBoot protocol reimplemented on libavr in three variants that trade
|
# The TinySafeBoot protocol reimplemented on libavr in three variants that trade
|
||||||
# clarity for size. Each links into the ATmega328P boot section (BOOTSZ selects
|
# clarity for size. Each links into the ATmega328P boot section (BOOTSZ selects
|
||||||
# its size; BOOTRST vectors a reset to its base) with -nostartfiles — a polled
|
# its size; BOOTRST vectors a reset to its base) with -nostartfiles — a polled
|
||||||
# loader has no use for the crt or the vector table. The naked entry sits in
|
# loader has no use for the crt or the vector table. The naked entry sits in
|
||||||
# .vectors, laid first, and runs. The boot base is FLASHEND+1 minus the section
|
# .vectors, laid first, and runs. The boot base is FLASHEND+1 minus the section
|
||||||
# size; the linker section-start and the source's boot_bytes agree. tsb_app is
|
# size; the linker section-start and the source's boot_bytes agree.
|
||||||
# the application's reset vector, pinned to 0 here so the loaders jump to a
|
|
||||||
# named function; --pmem-wrap-around lets relaxation turn that absolute jump
|
|
||||||
# into the wrapped rjmp AVR's modulo-flash PC actually executes.
|
|
||||||
# All three implement the full oracle feature set (see oracle/README.md):
|
# All three implement the full oracle feature set (see oracle/README.md):
|
||||||
# watchdog bail, one-wire half-duplex, config-page activation timeout, password
|
# watchdog bail, one-wire half-duplex, config-page activation timeout, password
|
||||||
# gate, emergency erase, config/flash/EEPROM read-write. They differ only in how,
|
# gate, emergency erase, config/flash/EEPROM read-write. They differ only in how.
|
||||||
# and the size gradient is the cost of that "how" — see dev/lessons.md.
|
# tsb_asm — minimal inline asm, the headline: 502 B in the 512 B section,
|
||||||
# tsb_asm — the tricks tier's C++ with exactly two routines in asm (the
|
# matching the hand-written oracle's size and features.
|
||||||
# bounded rx and the page-store loop — the two whose remaining
|
# tsb_tricks — compiler trickery, no asm: 808 B in the 1 KB section (BOOTSZ=10).
|
||||||
# cost is the C ABI itself): 510 B in the 512 B section the
|
# tsb_pure — pure idiomatic libavr: 950 B in the 1 KB section.
|
||||||
# hand-written 500 B oracle occupies. Everything else, from
|
|
||||||
# bring-up to dispatch, is C++ on libavr.
|
|
||||||
# tsb_tricks — no asm at all: the whole-loader register allocation lives in
|
|
||||||
# global register variables (Y walks the page pointer), every
|
|
||||||
# helper is a tiny noinline primitive placed by the
|
|
||||||
# global-register store rules, pages stream straight to
|
|
||||||
# SPM/EEPROM, and the bring-up is the two reset-non-default
|
|
||||||
# registers only. 526 B in the 1 KB section (BOOTSZ=10) — 14
|
|
||||||
# over the oracle's section, from 168 over at this tier's first
|
|
||||||
# floor.
|
|
||||||
# tsb_pure — pure idiomatic libavr, one function per command, TU-local
|
|
||||||
# (internal linkage), streaming (no SRAM page buffer): 836 B in
|
|
||||||
# the 1 KB section.
|
|
||||||
#
|
#
|
||||||
# add_tsb_variant(<name> <boot-section-bytes>)
|
# add_tsb_variant(<name> <boot-section-bytes>)
|
||||||
function(add_tsb_variant name bytes)
|
function(add_tsb_variant name bytes)
|
||||||
@@ -104,10 +59,8 @@ function(add_tsb_variant name bytes)
|
|||||||
math(EXPR base_hex "${base_dec}" OUTPUT_FORMAT HEXADECIMAL)
|
math(EXPR base_hex "${base_dec}" OUTPUT_FORMAT HEXADECIMAL)
|
||||||
add_executable(${name} tsb/${name}.cpp)
|
add_executable(${name} tsb/${name}.cpp)
|
||||||
target_link_libraries(${name} PRIVATE libavr)
|
target_link_libraries(${name} PRIVATE libavr)
|
||||||
target_link_options(${name} PRIVATE -nostartfiles -Wl,--section-start=.text=${base_hex}
|
target_link_options(${name} PRIVATE -nostartfiles -Wl,--section-start=.text=${base_hex})
|
||||||
-Wl,--defsym=tsb_app=0 -Wl,--pmem-wrap-around=32k)
|
|
||||||
add_custom_command(TARGET ${name} POST_BUILD COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${name}>)
|
add_custom_command(TARGET ${name} POST_BUILD COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${name}>)
|
||||||
add_image_outputs(${name})
|
|
||||||
if(PROJECT_IS_TOP_LEVEL)
|
if(PROJECT_IS_TOP_LEVEL)
|
||||||
add_test(NAME ${name}.size
|
add_test(NAME ${name}.size
|
||||||
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:${name}>
|
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:${name}>
|
||||||
@@ -120,202 +73,6 @@ function(add_tsb_variant name bytes)
|
|||||||
endif()
|
endif()
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
# The tsb tiers reimplement the ATmega328P-only reference protocol; the other
|
|
||||||
# chips build pureboot alone.
|
|
||||||
if(LIBAVR_MCU STREQUAL "atmega328p")
|
|
||||||
add_tsb_variant(tsb_asm 512)
|
add_tsb_variant(tsb_asm 512)
|
||||||
add_tsb_variant(tsb_pure 1024)
|
add_tsb_variant(tsb_pure 1024)
|
||||||
add_tsb_variant(tsb_tricks 1024)
|
add_tsb_variant(tsb_tricks 1024)
|
||||||
endif()
|
|
||||||
|
|
||||||
# pureboot — the pure-constraint port (see pureboot/README.md): one source,
|
|
||||||
# no inline assembly, no global register variables, every libavr chip,
|
|
||||||
# fitting each chip's smallest boot sector. The geometry and the
|
|
||||||
# pureboot_add_loader() deployment function live in pureboot/CMakeLists.txt —
|
|
||||||
# the unit a downstream project consumes; everything below is this port's
|
|
||||||
# own build: the stock loaders, their tests, and the size matrix. The
|
|
||||||
# distinct binary dir keeps the `pureboot` target's output name free.
|
|
||||||
add_subdirectory(pureboot pureboot-cmake)
|
|
||||||
|
|
||||||
# The stock loader: the family-default deployment (crystal/RC clock, the
|
|
||||||
# chip's natural link, default pins). The activation window stays a cache
|
|
||||||
# variable — re-timing a deployed loader is a self-update with a re-timed
|
|
||||||
# build. pureboot9 is that re-timed build, and what the update test installs.
|
|
||||||
set(PUREBOOT_TIMEOUT 8 CACHE STRING "pureboot activation window, seconds")
|
|
||||||
pureboot_add_loader(pureboot TIMEOUT ${PUREBOOT_TIMEOUT})
|
|
||||||
if(PROJECT_IS_TOP_LEVEL)
|
|
||||||
get_target_property(_pb_stock_hz pureboot PUREBOOT_HZ)
|
|
||||||
get_target_property(_pb_stock_baud pureboot PUREBOOT_BAUD)
|
|
||||||
|
|
||||||
add_test(NAME pureboot.size
|
|
||||||
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:pureboot>
|
|
||||||
-DLIMIT=${PUREBOOT_LIMIT} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
|
|
||||||
if(Python3_FOUND)
|
|
||||||
add_test(NAME pureboot.pi
|
|
||||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/check_pi.py
|
|
||||||
${CMAKE_OBJDUMP} ${CMAKE_NM} $<TARGET_FILE:pureboot> ${PUREBOOT_BASE_HEX})
|
|
||||||
add_test(NAME pureboot.planner
|
|
||||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/test_planner.py
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# The protocol test flashes this fixture through the loader with the real
|
|
||||||
# host tool and expects its banner after the hand-over; a normally linked
|
|
||||||
# application whose reset vector is what the tinies' surgery re-homes.
|
|
||||||
if(DEFINED PB_DEVICE)
|
|
||||||
add_executable(pbapp test/pbapp.cpp)
|
|
||||||
target_link_libraries(pbapp PRIVATE libavr)
|
|
||||||
add_custom_command(TARGET pbapp POST_BUILD
|
|
||||||
COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:pbapp> $<TARGET_FILE:pbapp>.bin)
|
|
||||||
add_test(NAME pureboot.protocol
|
|
||||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbtest.py
|
|
||||||
${PB_DEVICE} $<TARGET_FILE:pureboot> ${PUREBOOT_SIM_MCU} ${_pb_stock_hz}
|
|
||||||
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_pb_stock_baud} ${PUREBOOT_EEPROM}
|
|
||||||
$<TARGET_FILE:pbapp>.bin ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
|
||||||
${CMAKE_BINARY_DIR}/pbtest-work)
|
|
||||||
set_tests_properties(pureboot.protocol PROPERTIES TIMEOUT 180)
|
|
||||||
|
|
||||||
# The position-independence acceptance test: the identical image,
|
|
||||||
# installed one slot lower, must serve the full command set.
|
|
||||||
add_test(NAME pureboot.reloc
|
|
||||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbreloc.py
|
|
||||||
${PB_DEVICE} $<TARGET_FILE:pureboot> ${PUREBOOT_SIM_MCU} ${_pb_stock_hz}
|
|
||||||
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_pb_stock_baud}
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
|
||||||
${CMAKE_BINARY_DIR}/pbreloc-work)
|
|
||||||
set_tests_properties(pureboot.reloc PROPERTIES TIMEOUT 180
|
|
||||||
ENVIRONMENT "PB_OBJCOPY=${CMAKE_OBJCOPY}")
|
|
||||||
|
|
||||||
# Entering the loader from a running application with no reset
|
|
||||||
# between, over a page buffer the application dirtied — the case the
|
|
||||||
# loader declines to guard and the host repairs. Hardware forbids the
|
|
||||||
# state here (SPM runs only from the boot section); simavr does not,
|
|
||||||
# which is what makes it constructible.
|
|
||||||
if(LIBAVR_MCU STREQUAL "atmega328p")
|
|
||||||
add_test(NAME pureboot.dirty
|
|
||||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbdirty.py
|
|
||||||
${PB_DEVICE} $<TARGET_FILE:pureboot> ${PUREBOOT_SIM_MCU} ${_pb_stock_hz}
|
|
||||||
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_pb_stock_baud}
|
|
||||||
$<TARGET_FILE:pbapp>.bin
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
|
||||||
${CMAKE_BINARY_DIR}/pbdirty-work)
|
|
||||||
set_tests_properties(pureboot.dirty PROPERTIES TIMEOUT 180)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# Re-homing: a loader mistakenly programmed at address 0 (a raw .bin
|
|
||||||
# handed to a programmer) or sitting in the staging slot must heal
|
|
||||||
# into the canonical slot through the ordinary --update-loader flow.
|
|
||||||
# Patched-vector behavior, so one representative chip carries it.
|
|
||||||
if(LIBAVR_MCU STREQUAL "attiny85")
|
|
||||||
add_test(NAME pureboot.rehome
|
|
||||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbrehome.py
|
|
||||||
${PB_DEVICE} $<TARGET_FILE:pureboot> $<TARGET_FILE:pureboot9>.bin
|
|
||||||
${PUREBOOT_SIM_MCU} ${_pb_stock_hz} ${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE}
|
|
||||||
${_pb_stock_baud} $<TARGET_FILE:pbapp>.bin
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
|
||||||
${CMAKE_BINARY_DIR}/pbrehome-work)
|
|
||||||
set_tests_properties(pureboot.rehome PROPERTIES TIMEOUT 180)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# The self-update end-to-end: the re-timed build (same source, only
|
|
||||||
# the timeout differs — a byte-different image) replaces the resident
|
|
||||||
# through --update-loader, with every power-fail phase rehearsed from
|
|
||||||
# the runner's flash dumps.
|
|
||||||
pureboot_add_loader(pureboot9 TIMEOUT 9)
|
|
||||||
add_test(NAME pureboot.update
|
|
||||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbupdate.py
|
|
||||||
${PB_DEVICE} $<TARGET_FILE:pureboot> $<TARGET_FILE:pureboot9>
|
|
||||||
${PUREBOOT_SIM_MCU} ${_pb_stock_hz} ${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE}
|
|
||||||
${_pb_stock_baud} $<TARGET_FILE:pbapp>.bin
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
|
||||||
${CMAKE_BINARY_DIR}/pbupdate-work)
|
|
||||||
set_tests_properties(pureboot.update PROPERTIES TIMEOUT 600
|
|
||||||
ENVIRONMENT "PB_OBJCOPY=${CMAKE_OBJCOPY}")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# The size matrix: every configuration axis that could move the image
|
|
||||||
# size — the serial backend (different code), the clock and its ladder
|
|
||||||
# baud (different constants and divisor shapes), the USART instance
|
|
||||||
# (different register class) — each combination must still fit the
|
|
||||||
# chip's slot budget. Pins are size-neutral (port and bit are immediate
|
|
||||||
# operands) and the timeout is a constant, so neither adds an axis. The
|
|
||||||
# stock build is one point of this matrix and already has its test.
|
|
||||||
function(pureboot_size_variant name)
|
|
||||||
pureboot_add_loader(${name} ${ARGN})
|
|
||||||
add_test(NAME ${name}.size
|
|
||||||
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:${name}>
|
|
||||||
-DLIMIT=${PUREBOOT_LIMIT} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
|
|
||||||
endfunction()
|
|
||||||
|
|
||||||
# Clock points: the shipped-fuse floor (CKDIV8), the calibrated RC, and
|
|
||||||
# the crystal the stock build assumes (the tiny13's ladder is its own RC
|
|
||||||
# menu — it has no crystal option).
|
|
||||||
if(LIBAVR_MCU MATCHES "^attiny13")
|
|
||||||
set(_matrix_clocks 1200000 4800000 9600000)
|
|
||||||
else()
|
|
||||||
set(_matrix_clocks 1000000 8000000 16000000)
|
|
||||||
endif()
|
|
||||||
foreach(_matrix_hz IN LISTS _matrix_clocks)
|
|
||||||
math(EXPR _matrix_khz "${_matrix_hz} / 1000")
|
|
||||||
if(PUREBOOT_HAS_USART OR NOT _matrix_hz EQUAL _pb_stock_hz)
|
|
||||||
pureboot_size_variant(pureboot_sw_${_matrix_khz}k CLOCK ${_matrix_hz} SERIAL software)
|
|
||||||
endif()
|
|
||||||
if(PUREBOOT_HAS_USART AND NOT _matrix_hz EQUAL _pb_stock_hz)
|
|
||||||
pureboot_size_variant(pureboot_hw_${_matrix_khz}k CLOCK ${_matrix_hz} SERIAL hardware)
|
|
||||||
endif()
|
|
||||||
endforeach()
|
|
||||||
if(PUREBOOT_HAS_USART1)
|
|
||||||
pureboot_size_variant(pureboot_usart1 USART 1)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# One configured deployment end to end — a real board's shape rather
|
|
||||||
# than the stock assumption: the ATmega328P on its shipped 1 MHz fuses,
|
|
||||||
# the software UART on hand-picked pins (TX = PB1, RX = PB5), the ladder
|
|
||||||
# baud (9600). The full protocol suite runs against it, fixture
|
|
||||||
# application included, over the runner's GPIO bridge — proving the
|
|
||||||
# configuration plumbing produces a working loader, not just one that
|
|
||||||
# fits.
|
|
||||||
if(LIBAVR_MCU STREQUAL "atmega328p" AND DEFINED PB_DEVICE)
|
|
||||||
pureboot_size_variant(pureboot_custom CLOCK 1000000 SERIAL software RX pb5 TX pb1)
|
|
||||||
get_target_property(_custom_hz pureboot_custom PUREBOOT_HZ)
|
|
||||||
get_target_property(_custom_baud pureboot_custom PUREBOOT_BAUD)
|
|
||||||
get_target_property(_custom_link pureboot_custom PUREBOOT_LINK)
|
|
||||||
add_executable(pbapp_custom test/pbapp.cpp)
|
|
||||||
target_link_libraries(pbapp_custom PRIVATE libavr)
|
|
||||||
target_compile_definitions(pbapp_custom PRIVATE PUREBOOT_CLOCK_HZ=${_custom_hz}
|
|
||||||
PUREBOOT_BAUD=${_custom_baud} PUREBOOT_SOFT_SERIAL PUREBOOT_TX=pb1)
|
|
||||||
add_custom_command(TARGET pbapp_custom POST_BUILD
|
|
||||||
COMMAND ${CMAKE_OBJCOPY} -O binary
|
|
||||||
$<TARGET_FILE:pbapp_custom> $<TARGET_FILE:pbapp_custom>.bin)
|
|
||||||
add_test(NAME pureboot.custom
|
|
||||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbtest.py
|
|
||||||
${PB_DEVICE} $<TARGET_FILE:pureboot_custom> ${PUREBOOT_SIM_MCU} ${_custom_hz}
|
|
||||||
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_custom_baud} ${PUREBOOT_EEPROM}
|
|
||||||
$<TARGET_FILE:pbapp_custom>.bin ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
|
||||||
${CMAKE_BINARY_DIR}/pbcustom-work ${_custom_link})
|
|
||||||
set_tests_properties(pureboot.custom PROPERTIES TIMEOUT 180)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# The second USART, driven for real on one chip: instance selection is
|
|
||||||
# compile-checked everywhere, but only a live session proves the loader
|
|
||||||
# initialized and polls the USART it claims to. The fixture application
|
|
||||||
# banners on the same instance.
|
|
||||||
if(LIBAVR_MCU STREQUAL "atmega644a" AND DEFINED PB_DEVICE)
|
|
||||||
get_target_property(_usart1_hz pureboot_usart1 PUREBOOT_HZ)
|
|
||||||
get_target_property(_usart1_baud pureboot_usart1 PUREBOOT_BAUD)
|
|
||||||
add_executable(pbapp_usart1 test/pbapp.cpp)
|
|
||||||
target_link_libraries(pbapp_usart1 PRIVATE libavr)
|
|
||||||
target_compile_definitions(pbapp_usart1 PRIVATE PUREBOOT_CLOCK_HZ=${_usart1_hz}
|
|
||||||
PUREBOOT_BAUD=${_usart1_baud} PUREBOOT_USART=1)
|
|
||||||
add_custom_command(TARGET pbapp_usart1 POST_BUILD
|
|
||||||
COMMAND ${CMAKE_OBJCOPY} -O binary
|
|
||||||
$<TARGET_FILE:pbapp_usart1> $<TARGET_FILE:pbapp_usart1>.bin)
|
|
||||||
add_test(NAME pureboot.usart1
|
|
||||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbtest.py
|
|
||||||
${PB_DEVICE} $<TARGET_FILE:pureboot_usart1> ${PUREBOOT_SIM_MCU} ${_usart1_hz}
|
|
||||||
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_usart1_baud} ${PUREBOOT_EEPROM}
|
|
||||||
$<TARGET_FILE:pbapp_usart1>.bin ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
|
||||||
${CMAKE_BINARY_DIR}/pbusart1-work usart1)
|
|
||||||
set_tests_properties(pureboot.usart1 PROPERTIES TIMEOUT 180)
|
|
||||||
endif()
|
|
||||||
endif()
|
|
||||||
|
|||||||
1633
CMakePresets.json
1633
CMakePresets.json
File diff suppressed because it is too large
Load Diff
1
libavr
1
libavr
Submodule libavr deleted from e81dad0131
@@ -38,11 +38,9 @@ avra -I /usr/share/avra tsb-fixedbaud.asm # after uncommenting .include "m328P
|
|||||||
```
|
```
|
||||||
|
|
||||||
**500 bytes with every feature** — the proof that ≤512 B and full feature parity
|
**500 bytes with every feature** — the proof that ≤512 B and full feature parity
|
||||||
are simultaneously reachable. The port's `tsb_asm` tier meets the same bar at
|
are simultaneously reachable. The port's `tsb_asm` tier matches this bar; `tsb_pure`
|
||||||
510 B in the same 512 B section, written in C++ on libavr except the two
|
and `tsb_tricks` implement the same protocol at larger sizes in the 1 KB section,
|
||||||
routines whose remaining cost is the calling convention itself (the bounded rx
|
trading bytes for readability.
|
||||||
and the page-store loop); `tsb_tricks` needs no assembly at all at 526 B, and
|
|
||||||
`tsb_pure` stays fully idiomatic at 836 B, both in the 1 KB section.
|
|
||||||
|
|
||||||
The oracle targets 20 MHz / 33333 baud; the port targets 16 MHz / 115200 baud
|
The oracle targets 20 MHz / 33333 baud; the port targets 16 MHz / 115200 baud
|
||||||
(what the simavr protocol test drives). Baud and geometry differ, code size and
|
(what the simavr protocol test drives). Baud and geometry differ, code size and
|
||||||
|
|||||||
@@ -1,319 +0,0 @@
|
|||||||
# pureboot as a consumable CMake unit: the per-chip geometry, the default
|
|
||||||
# baud ladder, and pureboot_add_loader() — the one way a loader target is
|
|
||||||
# created, both by this port's own build and by a downstream project. A
|
|
||||||
# downstream project brings its usual libavr setup (the `libavr` target and
|
|
||||||
# the LIBAVR_MCU toolchain preset), adds this directory, and states its
|
|
||||||
# deployment:
|
|
||||||
#
|
|
||||||
# add_subdirectory(bootloader/pureboot)
|
|
||||||
# pureboot_add_loader(myboot CLOCK 1000000 SERIAL software TX pb1 RX pb5)
|
|
||||||
#
|
|
||||||
# Every argument is optional — CLOCK defaults to the family assumption
|
|
||||||
# below, BAUD to the fastest standard rate the clock reaches within 2.5 %
|
|
||||||
# (the ladder), SERIAL to the chip's hardware USART where it has one
|
|
||||||
# (`hardware`/`software` force a backend, USART 1 picks the second
|
|
||||||
# instance), RX/TX to pb0/pb1 for the software UART, TIMEOUT to 8 s.
|
|
||||||
# Infeasible picks fail the build by name: libavr's baud-error and
|
|
||||||
# software-UART cycle-floor static asserts re-check whatever is passed.
|
|
||||||
|
|
||||||
# Per-family geometry: flash/page/EEPROM sizes and the linker wrap the PC
|
|
||||||
# modulo needs, the loader slot (each chip's smallest boot sector — 1 KiB on
|
|
||||||
# the word-addressed 1284s), and the deployment defaults (crystal assumption
|
|
||||||
# on the megas, calibrated RC on the tinies). The USART flags mirror the
|
|
||||||
# hardware inventory the loader's own static asserts check (the plain 644 is
|
|
||||||
# the x4 family's one single-USART die, Atmel-2593).
|
|
||||||
set(_pb_has_usart 1)
|
|
||||||
set(_pb_has_usart1 0)
|
|
||||||
if(LIBAVR_MCU MATCHES "^attiny13a?$")
|
|
||||||
set(_pb_flash 1024)
|
|
||||||
set(_pb_wrap "")
|
|
||||||
set(_pb_page 32)
|
|
||||||
set(_pb_hz 9600000)
|
|
||||||
set(_pb_eeprom 64)
|
|
||||||
set(_pb_has_usart 0)
|
|
||||||
elseif(LIBAVR_MCU STREQUAL "attiny25")
|
|
||||||
set(_pb_flash 2048)
|
|
||||||
set(_pb_wrap "")
|
|
||||||
set(_pb_page 32)
|
|
||||||
set(_pb_hz 8000000)
|
|
||||||
set(_pb_eeprom 128)
|
|
||||||
set(_pb_has_usart 0)
|
|
||||||
elseif(LIBAVR_MCU STREQUAL "attiny45")
|
|
||||||
set(_pb_flash 4096)
|
|
||||||
set(_pb_wrap "")
|
|
||||||
set(_pb_page 64)
|
|
||||||
set(_pb_hz 8000000)
|
|
||||||
set(_pb_eeprom 256)
|
|
||||||
set(_pb_has_usart 0)
|
|
||||||
elseif(LIBAVR_MCU STREQUAL "attiny85")
|
|
||||||
set(_pb_flash 8192)
|
|
||||||
set(_pb_wrap -Wl,--pmem-wrap-around=8k)
|
|
||||||
set(_pb_page 64)
|
|
||||||
set(_pb_hz 8000000)
|
|
||||||
set(_pb_eeprom 512)
|
|
||||||
set(_pb_has_usart 0)
|
|
||||||
elseif(LIBAVR_MCU MATCHES "^atmega48(a|p|pa)?$")
|
|
||||||
set(_pb_flash 4096)
|
|
||||||
set(_pb_wrap "")
|
|
||||||
set(_pb_page 64)
|
|
||||||
set(_pb_hz 16000000)
|
|
||||||
set(_pb_eeprom 256)
|
|
||||||
elseif(LIBAVR_MCU MATCHES "^atmega8a?$" OR LIBAVR_MCU MATCHES "^atmega88(a|p|pa)?$")
|
|
||||||
set(_pb_flash 8192)
|
|
||||||
set(_pb_wrap -Wl,--pmem-wrap-around=8k)
|
|
||||||
set(_pb_page 64)
|
|
||||||
set(_pb_hz 16000000)
|
|
||||||
set(_pb_eeprom 512)
|
|
||||||
elseif(LIBAVR_MCU MATCHES "^atmega16a?$" OR LIBAVR_MCU MATCHES "^atmega168(a|p|pa)?$")
|
|
||||||
set(_pb_flash 16384)
|
|
||||||
set(_pb_wrap -Wl,--pmem-wrap-around=16k)
|
|
||||||
set(_pb_page 128)
|
|
||||||
set(_pb_hz 16000000)
|
|
||||||
set(_pb_eeprom 512)
|
|
||||||
elseif(LIBAVR_MCU MATCHES "^atmega164(a|p|pa)$")
|
|
||||||
set(_pb_flash 16384)
|
|
||||||
set(_pb_wrap -Wl,--pmem-wrap-around=16k)
|
|
||||||
set(_pb_page 128)
|
|
||||||
set(_pb_hz 16000000)
|
|
||||||
set(_pb_eeprom 512)
|
|
||||||
set(_pb_has_usart1 1)
|
|
||||||
elseif(LIBAVR_MCU MATCHES "^atmega32a?$" OR LIBAVR_MCU MATCHES "^atmega328p?$")
|
|
||||||
set(_pb_flash 32768)
|
|
||||||
set(_pb_wrap -Wl,--pmem-wrap-around=32k)
|
|
||||||
set(_pb_page 128)
|
|
||||||
set(_pb_hz 16000000)
|
|
||||||
set(_pb_eeprom 1024)
|
|
||||||
elseif(LIBAVR_MCU MATCHES "^atmega324(a|p|pa)$")
|
|
||||||
set(_pb_flash 32768)
|
|
||||||
set(_pb_wrap -Wl,--pmem-wrap-around=32k)
|
|
||||||
set(_pb_page 128)
|
|
||||||
set(_pb_hz 16000000)
|
|
||||||
set(_pb_eeprom 1024)
|
|
||||||
set(_pb_has_usart1 1)
|
|
||||||
elseif(LIBAVR_MCU MATCHES "^atmega644(a|p|pa)?$")
|
|
||||||
# 64 KiB is exactly the 16-bit byte space: plain LPM reaches everything,
|
|
||||||
# and the smallest boot section (1 KiB) holds the loader and its staging
|
|
||||||
# slot together (see README.md). The plain 644 is the family's one
|
|
||||||
# single-USART die.
|
|
||||||
set(_pb_flash 65536)
|
|
||||||
set(_pb_wrap -Wl,--pmem-wrap-around=64k)
|
|
||||||
set(_pb_page 256)
|
|
||||||
set(_pb_hz 16000000)
|
|
||||||
set(_pb_eeprom 2048)
|
|
||||||
if(NOT LIBAVR_MCU STREQUAL "atmega644")
|
|
||||||
set(_pb_has_usart1 1)
|
|
||||||
endif()
|
|
||||||
elseif(LIBAVR_MCU MATCHES "^atmega1284p?$")
|
|
||||||
# 128 KiB: wire flash addresses are word addresses, reads go through
|
|
||||||
# ELPM, and the PC's modulo wrap exceeds what --pmem-wrap-around models.
|
|
||||||
# The slot is 1 KiB — this chip's own smallest boot sector; the far
|
|
||||||
# machinery cannot fit 512 B (see README.md).
|
|
||||||
set(_pb_flash 131072)
|
|
||||||
set(_pb_wrap "")
|
|
||||||
set(_pb_page 256)
|
|
||||||
set(_pb_hz 16000000)
|
|
||||||
set(_pb_eeprom 4096)
|
|
||||||
set(_pb_slot 1024)
|
|
||||||
set(_pb_limit 1024)
|
|
||||||
set(_pb_has_usart1 1)
|
|
||||||
else()
|
|
||||||
message(FATAL_ERROR "pureboot: no geometry for ${LIBAVR_MCU}")
|
|
||||||
endif()
|
|
||||||
if(NOT DEFINED _pb_slot)
|
|
||||||
set(_pb_slot 512)
|
|
||||||
endif()
|
|
||||||
math(EXPR _pb_base "${_pb_flash} - ${_pb_slot}")
|
|
||||||
math(EXPR _pb_base_hex "${_pb_base}" OUTPUT_FORMAT HEXADECIMAL)
|
|
||||||
# Patched-vector chips hand over through the trampoline word below the slot,
|
|
||||||
# which is also the slot's own last word — their budget is slot − 2.
|
|
||||||
if(LIBAVR_MCU MATCHES "^atmega" AND NOT LIBAVR_MCU MATCHES "^atmega48")
|
|
||||||
set(_pb_app 0)
|
|
||||||
if(NOT DEFINED _pb_limit)
|
|
||||||
set(_pb_limit ${_pb_slot})
|
|
||||||
endif()
|
|
||||||
else()
|
|
||||||
math(EXPR _pb_app "${_pb_base} - 2")
|
|
||||||
math(EXPR _pb_limit "${_pb_slot} - 2")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# simavr names its cores after the base dies; the A revisions run on them
|
|
||||||
# (the 644PA on the 644P core).
|
|
||||||
set(_pb_sim_mcu ${LIBAVR_MCU})
|
|
||||||
if(LIBAVR_MCU MATCHES "^atmega(8|16|32|48|88|164|168|644)a$")
|
|
||||||
string(REGEX REPLACE "a$" "" _pb_sim_mcu ${LIBAVR_MCU})
|
|
||||||
elseif(LIBAVR_MCU STREQUAL "atmega644pa")
|
|
||||||
set(_pb_sim_mcu atmega644p)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# The function runs in its caller's scope, so everything it needs crosses
|
|
||||||
# scopes as global properties.
|
|
||||||
set_property(GLOBAL PROPERTY PUREBOOT_BASE_HEX ${_pb_base_hex})
|
|
||||||
set_property(GLOBAL PROPERTY PUREBOOT_APP ${_pb_app})
|
|
||||||
set_property(GLOBAL PROPERTY PUREBOOT_WRAP "${_pb_wrap}")
|
|
||||||
set_property(GLOBAL PROPERTY PUREBOOT_DEFAULT_HZ ${_pb_hz})
|
|
||||||
set_property(GLOBAL PROPERTY PUREBOOT_HAS_USART ${_pb_has_usart})
|
|
||||||
set_property(GLOBAL PROPERTY PUREBOOT_HAS_USART1 ${_pb_has_usart1})
|
|
||||||
|
|
||||||
# The port's own build (tests, the size matrix) reads the geometry from the
|
|
||||||
# parent scope; a downstream consumer gets the same variables for free.
|
|
||||||
set(PUREBOOT_BASE_HEX ${_pb_base_hex} PARENT_SCOPE)
|
|
||||||
set(PUREBOOT_PAGE ${_pb_page} PARENT_SCOPE)
|
|
||||||
set(PUREBOOT_SLOT ${_pb_slot} PARENT_SCOPE)
|
|
||||||
set(PUREBOOT_LIMIT ${_pb_limit} PARENT_SCOPE)
|
|
||||||
set(PUREBOOT_EEPROM ${_pb_eeprom} PARENT_SCOPE)
|
|
||||||
set(PUREBOOT_DEFAULT_HZ ${_pb_hz} PARENT_SCOPE)
|
|
||||||
set(PUREBOOT_HAS_USART ${_pb_has_usart} PARENT_SCOPE)
|
|
||||||
set(PUREBOOT_HAS_USART1 ${_pb_has_usart1} PARENT_SCOPE)
|
|
||||||
set(PUREBOOT_SIM_MCU ${_pb_sim_mcu} PARENT_SCOPE)
|
|
||||||
|
|
||||||
# The fastest standard rate the clock reaches within 2.5 % — the same
|
|
||||||
# best-of-U2X-and-plain divisor search libavr's solve_baud runs, so a
|
|
||||||
# default never trips the compile-time error it is checked against. A
|
|
||||||
# software build additionally requires the polled receiver's 100-cycles-a-bit
|
|
||||||
# floor (its own static assert): at low clocks the U2X divisor still reaches
|
|
||||||
# rates the bit-banged sampler cannot, so the backend gates the ladder.
|
|
||||||
function(pureboot_default_baud clock software outvar)
|
|
||||||
foreach(baud 115200 57600 38400 19200 9600)
|
|
||||||
math(EXPR _cycles "${clock} / ${baud}")
|
|
||||||
if(software AND _cycles LESS 100)
|
|
||||||
continue()
|
|
||||||
endif()
|
|
||||||
foreach(divisor 8 16)
|
|
||||||
math(EXPR _step "${divisor} * ${baud}")
|
|
||||||
math(EXPR _n "(${clock} + ${_step} / 2) / ${_step}")
|
|
||||||
if(_n LESS 1 OR _n GREATER 4096)
|
|
||||||
continue()
|
|
||||||
endif()
|
|
||||||
math(EXPR _actual "${clock} / (${divisor} * ${_n})")
|
|
||||||
math(EXPR _delta "${_actual} - ${baud}")
|
|
||||||
if(_delta LESS 0)
|
|
||||||
math(EXPR _delta "-(${_delta})")
|
|
||||||
endif()
|
|
||||||
math(EXPR _error_bp "${_delta} * 10000 / ${baud}")
|
|
||||||
if(_error_bp LESS_EQUAL 250)
|
|
||||||
set(${outvar} ${baud} PARENT_SCOPE)
|
|
||||||
return()
|
|
||||||
endif()
|
|
||||||
endforeach()
|
|
||||||
endforeach()
|
|
||||||
message(FATAL_ERROR "pureboot: no standard baud rate fits a ${clock} Hz clock within 2.5 %")
|
|
||||||
endfunction()
|
|
||||||
|
|
||||||
# pureboot_add_loader(<name> [CLOCK <hz>] [BAUD <bd>]
|
|
||||||
# [SERIAL auto|hardware|software] [USART <n>]
|
|
||||||
# [RX <pin>] [TX <pin>] [TIMEOUT <s>])
|
|
||||||
#
|
|
||||||
# Creates the loader target plus its flashable images (<name>.hex for a
|
|
||||||
# programmer, <name>.bin for --update-loader) and stamps the resolved
|
|
||||||
# deployment on the target: the PUREBOOT_HZ, PUREBOOT_BAUD and PUREBOOT_LINK
|
|
||||||
# properties (the link as usart0/usart1/sw:<RX>,<TX> — what a test harness
|
|
||||||
# needs to speak to the build).
|
|
||||||
function(pureboot_add_loader name)
|
|
||||||
cmake_parse_arguments(PB "" "CLOCK;BAUD;SERIAL;USART;RX;TX;TIMEOUT" "" ${ARGN})
|
|
||||||
if(PB_UNPARSED_ARGUMENTS)
|
|
||||||
message(FATAL_ERROR "pureboot_add_loader(${name}): unknown arguments ${PB_UNPARSED_ARGUMENTS}")
|
|
||||||
endif()
|
|
||||||
get_property(_hz GLOBAL PROPERTY PUREBOOT_DEFAULT_HZ)
|
|
||||||
get_property(_base_hex GLOBAL PROPERTY PUREBOOT_BASE_HEX)
|
|
||||||
get_property(_app GLOBAL PROPERTY PUREBOOT_APP)
|
|
||||||
get_property(_wrap GLOBAL PROPERTY PUREBOOT_WRAP)
|
|
||||||
get_property(_usart GLOBAL PROPERTY PUREBOOT_HAS_USART)
|
|
||||||
get_property(_usart1 GLOBAL PROPERTY PUREBOOT_HAS_USART1)
|
|
||||||
|
|
||||||
if(NOT PB_CLOCK)
|
|
||||||
set(PB_CLOCK ${_hz})
|
|
||||||
endif()
|
|
||||||
if(NOT PB_TIMEOUT)
|
|
||||||
set(PB_TIMEOUT 8)
|
|
||||||
endif()
|
|
||||||
if(NOT PB_SERIAL)
|
|
||||||
set(PB_SERIAL auto)
|
|
||||||
endif()
|
|
||||||
if(DEFINED PB_USART AND PB_SERIAL STREQUAL "software")
|
|
||||||
message(FATAL_ERROR "pureboot_add_loader(${name}): USART ${PB_USART} contradicts SERIAL software")
|
|
||||||
endif()
|
|
||||||
if(DEFINED PB_USART)
|
|
||||||
set(PB_SERIAL hardware)
|
|
||||||
elseif(PB_SERIAL STREQUAL "hardware")
|
|
||||||
set(PB_USART 0)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
set(_serial_defines "")
|
|
||||||
if(PB_SERIAL STREQUAL "hardware")
|
|
||||||
if(PB_USART EQUAL 1 AND NOT _usart1)
|
|
||||||
message(FATAL_ERROR "pureboot_add_loader(${name}): ${LIBAVR_MCU} has no USART1")
|
|
||||||
elseif(NOT _usart)
|
|
||||||
message(FATAL_ERROR "pureboot_add_loader(${name}): ${LIBAVR_MCU} has no hardware USART")
|
|
||||||
endif()
|
|
||||||
set(_serial_defines PUREBOOT_USART=${PB_USART})
|
|
||||||
set(_link usart${PB_USART})
|
|
||||||
else()
|
|
||||||
if(PB_SERIAL STREQUAL "auto")
|
|
||||||
if(_usart AND (PB_RX OR PB_TX))
|
|
||||||
message(WARNING "pureboot_add_loader(${name}): RX/TX apply to the software UART, "
|
|
||||||
"which auto does not pick on ${LIBAVR_MCU} — SERIAL software to force it")
|
|
||||||
endif()
|
|
||||||
if(_usart)
|
|
||||||
set(_link usart0)
|
|
||||||
else()
|
|
||||||
set(PB_SERIAL software)
|
|
||||||
endif()
|
|
||||||
endif()
|
|
||||||
if(PB_SERIAL STREQUAL "software")
|
|
||||||
if(NOT PB_RX)
|
|
||||||
set(PB_RX pb0)
|
|
||||||
endif()
|
|
||||||
if(NOT PB_TX)
|
|
||||||
set(PB_TX pb1)
|
|
||||||
endif()
|
|
||||||
foreach(_pin ${PB_RX} ${PB_TX})
|
|
||||||
if(NOT _pin MATCHES "^p[a-h][0-7]$")
|
|
||||||
message(FATAL_ERROR "pureboot_add_loader(${name}): pin '${_pin}' is not of the form pb1")
|
|
||||||
endif()
|
|
||||||
endforeach()
|
|
||||||
set(_serial_defines PUREBOOT_SOFT_SERIAL PUREBOOT_RX=${PB_RX} PUREBOOT_TX=${PB_TX})
|
|
||||||
# The link spec a test harness drives a GPIO bridge with: sw:<RX>,<TX>
|
|
||||||
# as the port letter and bit, the loader's own pin naming upcased.
|
|
||||||
string(SUBSTRING ${PB_RX} 1 2 _rx_pin)
|
|
||||||
string(SUBSTRING ${PB_TX} 1 2 _tx_pin)
|
|
||||||
string(TOUPPER "sw:${_rx_pin},${_tx_pin}" _link)
|
|
||||||
string(REPLACE "SW" "sw" _link ${_link})
|
|
||||||
endif()
|
|
||||||
endif()
|
|
||||||
if(NOT PB_BAUD)
|
|
||||||
if(PB_SERIAL STREQUAL "software")
|
|
||||||
pureboot_default_baud(${PB_CLOCK} 1 PB_BAUD)
|
|
||||||
else()
|
|
||||||
pureboot_default_baud(${PB_CLOCK} 0 PB_BAUD)
|
|
||||||
endif()
|
|
||||||
endif()
|
|
||||||
|
|
||||||
set(_defines PUREBOOT_CLOCK_HZ=${PB_CLOCK} PUREBOOT_BAUD=${PB_BAUD} PUREBOOT_TIMEOUT=${PB_TIMEOUT}
|
|
||||||
${_serial_defines})
|
|
||||||
|
|
||||||
add_executable(${name} ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/pureboot.cpp)
|
|
||||||
target_link_libraries(${name} PRIVATE libavr)
|
|
||||||
target_compile_definitions(${name} PRIVATE ${_defines})
|
|
||||||
# Codegen shaping for the loader TU only, worth ~40 B on every chip and
|
|
||||||
# what carries the far-flash 1284 build under 512. At -Os GCC otherwise
|
|
||||||
# rewrites the byte-stream loops' counters into end-pointer forms that
|
|
||||||
# cost registers (-fno-ivopts, -fno-split-wide-types), leaves register
|
|
||||||
# pressure on the table with the default allocator
|
|
||||||
# (-fira-algorithm=priority), and spends bytes on rewrites a
|
|
||||||
# straight-line loader gains nothing from.
|
|
||||||
target_compile_options(${name} PRIVATE
|
|
||||||
-fno-ivopts -fira-algorithm=priority -fno-expensive-optimizations -fno-split-wide-types)
|
|
||||||
target_link_options(${name} PRIVATE -nostartfiles -Wl,--section-start=.text=${_base_hex}
|
|
||||||
-Wl,--defsym=pureboot_app=${_app} ${_wrap})
|
|
||||||
add_custom_command(TARGET ${name} POST_BUILD COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${name}>)
|
|
||||||
# The ELF is a container (symbols, section headers), never flashed; the
|
|
||||||
# flashable forms sit beside it: .hex for a programmer, .bin (the slot's
|
|
||||||
# bare bytes) for the host tool's raw path and --update-loader.
|
|
||||||
add_custom_command(TARGET ${name} POST_BUILD
|
|
||||||
COMMAND ${CMAKE_OBJCOPY} -O ihex -R .eeprom
|
|
||||||
$<TARGET_FILE:${name}> $<TARGET_FILE:${name}>.hex
|
|
||||||
COMMAND ${CMAKE_OBJCOPY} -O binary -R .eeprom
|
|
||||||
$<TARGET_FILE:${name}> $<TARGET_FILE:${name}>.bin)
|
|
||||||
set_target_properties(${name} PROPERTIES PUREBOOT_HZ ${PB_CLOCK} PUREBOOT_BAUD ${PB_BAUD}
|
|
||||||
PUREBOOT_LINK ${_link})
|
|
||||||
endfunction()
|
|
||||||
@@ -1,379 +0,0 @@
|
|||||||
# pureboot
|
|
||||||
|
|
||||||
A serial bootloader on [libavr](https://git.blackmark.me/avr/libavr), pure by
|
|
||||||
constraint: one C++ source, no inline assembly, no global register variables
|
|
||||||
(attributes and compiler flags allowed), built for **every chip libavr
|
|
||||||
targets — all 37 — in 512 bytes each**: 434 B on the tiny13s, 438–442 B on
|
|
||||||
the tiny25/45/85, 412–452 B across the megas, and 506 B on the
|
|
||||||
ATmega1284/1284P, whose far-flash machinery (ELPM reads, RAMPZ page commands,
|
|
||||||
word-addressed wire) is the heaviest. Those are the stock deployments;
|
|
||||||
choosing the software UART where the chip has a USART costs 8–46 B more (a
|
|
||||||
bit-bang against a peripheral), which every chip still absorbs inside its
|
|
||||||
slot — on the 1284s that means their 1 KiB boot sector, where the
|
|
||||||
software-serial image lands at 546 B. Bringing the 1284's default build
|
|
||||||
under 512 at all is what the loop-placement attributes on the byte streamers
|
|
||||||
(`pureboot.cpp`) and the codegen flags on the loader TU (`CMakeLists.txt`)
|
|
||||||
are for; measured against each chip's own budget the tightest is the
|
|
||||||
ATmega328P, 50 B spare. Clock, baud, serial backend and
|
|
||||||
pins are per-build configuration (below); the size matrix in the test suite
|
|
||||||
holds every combination inside its slot. The device speaks primitives; every
|
|
||||||
composite — verify, erase, reset-vector surgery, updating the loader itself —
|
|
||||||
lives in the host tool (`pureboot.py`).
|
|
||||||
|
|
||||||
The 1284s still *deploy* in a 1 KiB slot, their smallest boot sector being
|
|
||||||
512 words; at 506 B the image would also fit the 644's
|
|
||||||
two-512-byte-slots-per-boot-sector geometry.
|
|
||||||
|
|
||||||
The image is **position-independent**: control flow is PC-relative, the
|
|
||||||
read/write paths take wire addresses, the write guard protects the slot the
|
|
||||||
code is *running* in (from the runtime return address), the info block is
|
|
||||||
addressed from that same anchor, and the application jump is an indirect
|
|
||||||
call to an absolute entry. The identical binary therefore runs from any
|
|
||||||
slot with every command intact — which makes pureboot **its own staging
|
|
||||||
loader**: the host installs the same binary one slot below the resident,
|
|
||||||
jumps into it, and lets it rewrite the resident. The slot is 512 bytes
|
|
||||||
(1 KiB on the word-addressed large chips, matching their boot-sector
|
|
||||||
minimum); on the tinies the budget is 510, not 512: a slot's last word
|
|
||||||
belongs to the host-managed trampoline (below).
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
Every deployment axis is a build parameter, resolved by the CMake function
|
|
||||||
`pureboot_add_loader()` (in `pureboot/CMakeLists.txt`) — the one way a
|
|
||||||
loader target is created, by this repo's own build and by a downstream
|
|
||||||
project alike:
|
|
||||||
|
|
||||||
| Argument | Meaning | Default |
|
|
||||||
|---|---|---|
|
|
||||||
| `CLOCK <hz>` | the clock the board runs | 16 MHz megas, 8 MHz t25/45/85, 9.6 MHz t13s |
|
|
||||||
| `BAUD <bd>` | the wire rate | the ladder below |
|
|
||||||
| `SERIAL auto\|hardware\|software` | the link backend | `auto`: the hardware USART where the chip has one |
|
|
||||||
| `USART <n>` | the USART instance (x4 megas carry two) | 0 |
|
|
||||||
| `RX <pin>`, `TX <pin>` | software-UART pins | `pb0`, `pb1` |
|
|
||||||
| `TIMEOUT <s>` | the activation window | 8 |
|
|
||||||
|
|
||||||
The default baud is the fastest of 115200/57600/38400/19200/9600 the clock
|
|
||||||
reaches within 2.5 % — the same U2X-included divisor search libavr's baud
|
|
||||||
solver runs — and on a software build additionally within the polled
|
|
||||||
receiver's 100-cycles-a-bit floor. 16 MHz lands 115200, 8 MHz 57600,
|
|
||||||
1 MHz 9600. Whatever is picked or overridden is re-checked in the compile:
|
|
||||||
an infeasible clock/baud/backend combination, or a USART the chip does not
|
|
||||||
have, fails with a named static assert.
|
|
||||||
|
|
||||||
A downstream project brings its usual libavr setup (the `libavr` target,
|
|
||||||
the chip via the `LIBAVR_MCU` toolchain preset), consumes this directory,
|
|
||||||
and states its deployment — for example an ATmega328P on its shipped
|
|
||||||
1 MHz fuses with the software UART on hand-picked pins:
|
|
||||||
|
|
||||||
```cmake
|
|
||||||
FetchContent_Declare(bootloader GIT_REPOSITORY git@git.blackmark.me:avr/bootloader.git GIT_TAG main)
|
|
||||||
FetchContent_MakeAvailable(bootloader)
|
|
||||||
add_subdirectory(${bootloader_SOURCE_DIR}/pureboot pureboot)
|
|
||||||
|
|
||||||
pureboot_add_loader(myboot CLOCK 1000000 SERIAL software TX pb1 RX pb5)
|
|
||||||
```
|
|
||||||
|
|
||||||
The function emits the ELF plus `myboot.hex` (the programmer artifact) and
|
|
||||||
`myboot.bin` (the self-update image), prints the size, and stamps the
|
|
||||||
resolved deployment on the target as the `PUREBOOT_HZ`, `PUREBOOT_BAUD`
|
|
||||||
and `PUREBOOT_LINK` properties — what a flashing script or test harness
|
|
||||||
needs to speak to the build. This exact example deployment runs the full
|
|
||||||
protocol suite in CI (`pureboot.custom`).
|
|
||||||
|
|
||||||
## Link
|
|
||||||
|
|
||||||
The stock builds assume the family's natural deployment; any axis moves
|
|
||||||
per build (above).
|
|
||||||
|
|
||||||
| Chip | Serial | Baud | Clock assumed |
|
|
||||||
|---|---|---|---|
|
|
||||||
| every ATmega | the hardware USART (USART0), RXD/TXD per pinout | 115200 8N1 | 16 MHz crystal |
|
|
||||||
| ATtiny25/45/85 | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 8 MHz internal RC |
|
|
||||||
| ATtiny13/13A | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 9.6 MHz internal RC |
|
|
||||||
|
|
||||||
The software-UART RX pin has its pull-up enabled; TX idles high. All
|
|
||||||
multi-byte quantities on the wire are little-endian.
|
|
||||||
|
|
||||||
## Activation
|
|
||||||
|
|
||||||
Reset enters the loader (BOOTRST on the boot-sectioned megas; the patched
|
|
||||||
reset vector on the tinies and the boot-section-less m48s) — except a
|
|
||||||
watchdog reset, which hands straight to the application (the application
|
|
||||||
owns its watchdog; it must clear WDRF itself, which also releases the
|
|
||||||
WDRF-forced WDE).
|
|
||||||
|
|
||||||
The host then has one activation window per awaited byte to knock: `p` then
|
|
||||||
`b`. Each awaited byte gets a fresh window; any other byte is discarded and
|
|
||||||
awaited again (line noise cannot lock the loader, only delay it). A window
|
|
||||||
expiring with an idle line boots the application.
|
|
||||||
|
|
||||||
The window length is a compile-time constant — 8 s by default, another
|
|
||||||
value via `pureboot_add_loader(... TIMEOUT <s>)` (the stock target keeps
|
|
||||||
the `PUREBOOT_TIMEOUT` cache variable) — so the whole EEPROM belongs to
|
|
||||||
the application; pureboot never uses it for its own state. Re-timing a
|
|
||||||
deployed loader is a self-update with a re-timed build (below).
|
|
||||||
|
|
||||||
## Session
|
|
||||||
|
|
||||||
After the knock the loader stays in its command loop until `J` jumps away or
|
|
||||||
the chip resets. Before reading each command it waits for any pending EEPROM
|
|
||||||
write to finish and sends the prompt `+` (0x2b) — the prompt is therefore
|
|
||||||
also the completion ack of the previous command. A session is: await `+`,
|
|
||||||
send a command, read its reply, repeat.
|
|
||||||
|
|
||||||
On chips whose flash exceeds 64 KiB (the 1284s — info-block flag bit 1) the
|
|
||||||
`R`/`W` flash addresses are **word** addresses; everywhere else they are byte
|
|
||||||
addresses (the 644s' 64 KiB is exactly the 16-bit byte space and stays
|
|
||||||
byte-addressed). EEPROM addresses are always bytes, counts always bytes.
|
|
||||||
|
|
||||||
| Cmd | Arguments | Reply |
|
|
||||||
|---|---|---|
|
|
||||||
| `b` | — | the 12-byte info block |
|
|
||||||
| `R` | addr16, n8 | n flash bytes (n = 0 means 256) |
|
|
||||||
| `W` | addr16, then one page of data | — (completion = next prompt) |
|
|
||||||
| `r` | addr16, n8 | n EEPROM bytes (n = 0 means 256) |
|
|
||||||
| `w` | addr16, n8, then n data bytes | `+` per byte, sent once its write has begun |
|
|
||||||
| `F` | — | 4 bytes: low fuse, lock, extended fuse, high fuse |
|
|
||||||
| `J` | word address (16-bit) | `+`, then execution continues there |
|
|
||||||
| other | — | ignored; the loop re-prompts (send a junk byte, await `+`, to resync) |
|
|
||||||
|
|
||||||
`W` streams exactly one SPM page (size from the info block) into the buffer,
|
|
||||||
then erases and programs; the address must be page-aligned. Pages inside the
|
|
||||||
512-byte slot the loader is *running* in are drained but never programmed — a
|
|
||||||
broken host cannot brick the running copy, and a staged copy may rewrite the
|
|
||||||
resident slot.
|
|
||||||
|
|
||||||
The loader never clears the SPM buffer before a fill, so **one `W` may
|
|
||||||
program the wrong bytes, and the host is what fixes it**. The buffer is
|
|
||||||
write-once per word until cleared, and two things leave words in it: a
|
|
||||||
refused page (drained, never programmed) and — where SPM runs from anywhere,
|
|
||||||
the tinies and the m48s — an application that self-programmed before
|
|
||||||
entering. The next `W` takes those stale words, and clears them: a page write
|
|
||||||
auto-erases the buffer (§26.2.1; §19.2 on the tinies), so repeating it
|
|
||||||
programs correctly. The host therefore verifies every page it writes and
|
|
||||||
rewrites what comes back wrong (three retries, then it stops); a host that
|
|
||||||
programs without reading back cannot trust the first `W` after either event.
|
|
||||||
|
|
||||||
`w` is host-paced: send the next byte only after the previous
|
|
||||||
byte's `+`. `F` returns the bytes in the hardware's Z order; on a chip
|
|
||||||
without an extended fuse byte (the ATtiny13A) that slot carries no meaning.
|
|
||||||
Fuse *writing* does not exist: SPM reaches flash (and, on the mega, lock
|
|
||||||
bits) only — fuse bytes are external-programming territory by hardware.
|
|
||||||
|
|
||||||
`J` is the one control-transfer primitive: the host uses it to run the
|
|
||||||
application (word 0 on the mega, the trampoline word on the tinies — both
|
|
||||||
known from the info block) and to move between loader copies during a
|
|
||||||
self-update. A jump to a loader slot's base re-enters that copy's own
|
|
||||||
startup; it must then be knocked afresh.
|
|
||||||
|
|
||||||
The info block (`b`):
|
|
||||||
|
|
||||||
| Offset | Content |
|
|
||||||
|---|---|
|
|
||||||
| 0–2 | `'P'`, `'B'`, protocol version (1) |
|
|
||||||
| 3–5 | device signature |
|
|
||||||
| 6 | SPM page size in bytes (0 means 256) |
|
|
||||||
| 7–8 | loader base — application flash ends here (a word address when bit 1 is set) |
|
|
||||||
| 9–10 | EEPROM size |
|
|
||||||
| 11 | bit 0: host must patch the reset vector (no hardware boot section); bit 1: flash wire addresses are word addresses |
|
|
||||||
|
|
||||||
Composites are the host's job: verify = read back and compare, erase =
|
|
||||||
write `0xff` (per page for flash, per byte for EEPROM).
|
|
||||||
|
|
||||||
## Deployment
|
|
||||||
|
|
||||||
The build leaves three artifacts per chip. The ELF is a container for the
|
|
||||||
tests and objcopy — never flashed. The **.hex is the programmer artifact**:
|
|
||||||
it carries its own addresses and lands the loader in its top slot,
|
|
||||||
touching nothing else. The **.bin is the self-update image** — the slot's
|
|
||||||
bare bytes with no addressing, which a programmer would put at address 0.
|
|
||||||
On a boot-sectioned mega a copy at 0 is dead weight (SPM only executes
|
|
||||||
from the boot section, so it cannot even heal itself — reflash the .hex);
|
|
||||||
on the patched-vector chips it *runs* (the image is position-independent
|
|
||||||
and reset enters word 0), reports its canonical geometry, and the ordinary
|
|
||||||
`--update-loader` flow re-homes a build into the top slot from any
|
|
||||||
position — the staging install and the word-0 redirect execute from
|
|
||||||
copies outside page 0's slot, and a copy sitting in the staging slot
|
|
||||||
itself is recognized as the installed staging copy and left in place (it
|
|
||||||
streams the new resident like any staged copy, so an older build installs
|
|
||||||
a newer one). `pureboot.rehome` is the acceptance test for both
|
|
||||||
positions. Flashing the application afterwards overwrites the stale copy,
|
|
||||||
vector surgery included.
|
|
||||||
|
|
||||||
**Boot-sectioned megas**: program the loader at `flash − slot` with an
|
|
||||||
external programmer. Every such mega has a BOOTSZ step whose boot section
|
|
||||||
is exactly the loader slot — 512 B, the second-smallest step on the 8 KiB
|
|
||||||
and 16 KiB chips (m8, m88, m16, m168, m164), the smallest on the 32 KiB
|
|
||||||
ones (m32, m328, m324); on the 1284s that step is the smallest, 512 words,
|
|
||||||
which is why their slot is 1 KiB — so the ATmega328P profiles below apply
|
|
||||||
to every one of them with its own addresses and slot size; the per-chip
|
|
||||||
BOOTSZ ladders live in the host tool (`BOOT_FUSE`). The 1284s' numbers:
|
|
||||||
standalone = BOOTSZ 512 words (reset at the loader base 0x1fc00);
|
|
||||||
self-update = 1024 words, covering both 1 KiB slots, the loader-first
|
|
||||||
reset landing at 0x1f800 — the staging slot, walked across when erased.
|
|
||||||
|
|
||||||
The **644s** are the geometry's sweet spot: their smallest boot section
|
|
||||||
(512 words = 1 KiB) is exactly *two* 512-byte slots, so the resident and
|
|
||||||
its staging slot both live inside the minimum section — self-update needs
|
|
||||||
no fuse step up, and the standalone profile does not exist (reset lands at
|
|
||||||
0xfc00, one erased slot below the loader: the loader-first walk built in).
|
|
||||||
|
|
||||||
ATmega328P profiles (addresses for its 32 KiB):
|
|
||||||
|
|
||||||
| BOOTSZ | BOOTRST | Behavior |
|
|
||||||
|---|---|---|
|
|
||||||
| 256 words (512 B) | programmed | *Standalone*: reset always enters the loader; **self-update impossible** (the staging slot lies outside the boot section, where SPM is disabled). |
|
|
||||||
| 512 words (1 KB) | unprogrammed | *Self-update, app-first*: reset always boots the application, which owns all 31.5 KB and must offer its own jump to 0x7e00 to reach the loader (a virgin chip reaches it by reset across erased flash). Updates are power-fail-safe except mid-rewrite of the resident slot itself (no reset path leads to the staging copy then). |
|
|
||||||
| 512 words (1 KB) | programmed | *Self-update, loader-first*: reset lands at 0x7c00 — the staging slot, normally erased, so execution walks up into the loader; during an update it is the staging copy itself, so a mid-rewrite power loss recovers by reset. The loss windows move to the staging install/retire page writes instead (page-write scale). The host keeps `[0x7c00, 0x7e00)` clear of application data (`--force` overrides). |
|
|
||||||
|
|
||||||
Applications are flashed unmodified — word 0 stays the application's own
|
|
||||||
reset vector, and the hand-over jumps to 0.
|
|
||||||
|
|
||||||
**Patched-vector chips — the tinies and the m48s** (no boot section; the
|
|
||||||
m48s' SPM runs from the entire flash, Atmel-8271 §26): program the loader
|
|
||||||
at `flash − 512`; erased flash below it walks up into the loader, so a
|
|
||||||
virgin chip activates. When flashing an application the host performs
|
|
||||||
reset-vector surgery: word 0 is rewritten to `rjmp` to the loader base, and
|
|
||||||
the application's own entry is re-encoded as a trampoline `rjmp` in the
|
|
||||||
word just below the loader (`base − 2`, where the hand-over jumps). Every
|
|
||||||
other vector stays the application's. The patched page 0 and the trampoline
|
|
||||||
page are written *first*, so from the first write on an interrupted flash
|
|
||||||
still resets into the loader; an erase runs top-down for the same reason.
|
|
||||||
The m48s speak this profile over their hardware USART — no fuse preflight,
|
|
||||||
BOOTRST does not exist there.
|
|
||||||
|
|
||||||
## Updating the loader
|
|
||||||
|
|
||||||
`pureboot.py --update-loader new_pureboot.bin` replaces the resident loader
|
|
||||||
with any pureboot build — a re-timed window, a newer protocol — using the
|
|
||||||
loader itself as its own staging loader. The image is the loader's own 512
|
|
||||||
bytes as a raw binary, or the Intel HEX the build emits beside it, which
|
|
||||||
links the loader at its base inside an otherwise blank flash image:
|
|
||||||
|
|
||||||
The preflight refuses an image built for another chip: the info block
|
|
||||||
embedded in every pureboot binary (signature, page size, loader base,
|
|
||||||
EEPROM size, flags) must match the device's own, and the error names both.
|
|
||||||
Die revisions share their base signature and geometry, so their images are
|
|
||||||
interchangeable — as the silicon is. `loader_image()` also accepts a
|
|
||||||
padded image (a raw .bin padded from 0, or a whole-flash read-back with
|
|
||||||
the loader resident) and peels it to the slot content by the embedded base.
|
|
||||||
|
|
||||||
1. The staging slot `[base−slot, base)` is saved to a host-side state file
|
|
||||||
(on the 1 KB tiny13s that is the whole application, vectors included).
|
|
||||||
2. The resident installs the identical update image there. On the
|
|
||||||
patched-vector chips the host composes the slot's last word — the same
|
|
||||||
address as the resident's trampoline — as a jump to the resident base,
|
|
||||||
so even an abandoned staging copy times out into a loader, never into
|
|
||||||
garbage. A loader already sitting whole in the staging slot (its info
|
|
||||||
block in place, the slot unchanged since the update began) is left as
|
|
||||||
the staging copy instead — rewriting it would only meet its own
|
|
||||||
running-slot guard.
|
|
||||||
3. `J` enters the staging copy, which rewrites the resident slot. On the
|
|
||||||
patched-vector chips whose staging slot sits away from page 0 the host
|
|
||||||
first re-aims word 0 at the staging copy, so a power loss mid-rewrite
|
|
||||||
still resets into a loader; on the tiny13s the staging slot carries the
|
|
||||||
reset vector itself.
|
|
||||||
4. `J` enters the new resident, which restores the staging slot's saved
|
|
||||||
content (word 0 and the trampoline with it) and the state file is
|
|
||||||
discarded.
|
|
||||||
|
|
||||||
Every phase is idempotent and keyed off the actual flash state: re-running
|
|
||||||
the same command after any interruption resumes and completes. The state
|
|
||||||
file carries the only bytes not recoverable from the device; if it is lost
|
|
||||||
mid-update the update still completes, and the staging region is restored by
|
|
||||||
reflashing the application. A boot-sectioned mega needs its fuses for the
|
|
||||||
preflight (BOOTSZ gate, profile notes) — read from the device, or supplied
|
|
||||||
with `--assume-fuses` where reading is impossible (simulators); the
|
|
||||||
patched-vector chips need none.
|
|
||||||
|
|
||||||
## Host tool
|
|
||||||
|
|
||||||
`pureboot.py` — Python 3, standard library only. The port layer is the one
|
|
||||||
platform-specific part: termios drives any tty on POSIX (a USB adapter as
|
|
||||||
well as a simavr pty), the Win32 serial API through `ctypes` drives a COM
|
|
||||||
port on Windows (`--port COM6`; the `\\.\` form for two-digit ports is
|
|
||||||
supplied by the tool). Opening the port asserts DTR and RTS on both, so a
|
|
||||||
board that wires DTR to reset gets its reset pulse and opens the activation
|
|
||||||
window by itself.
|
|
||||||
|
|
||||||
pureboot.py --port /dev/ttyUSB0 --baud 57600 \
|
|
||||||
--info --fuses --flash app.hex
|
|
||||||
|
|
||||||
Operations run in a fixed order within one session: info, fuses, loader
|
|
||||||
update, flash (erase / program / read / verify), EEPROM (erase / program /
|
|
||||||
read / verify) — then the loader hands over to the application; `--stay`
|
|
||||||
keeps the session alive instead, and a later invocation reconnects into it
|
|
||||||
(the knock converges there too). `--flash` and `--eeprom` verify by
|
|
||||||
read-back unless `--no-verify`, and a flash page that reads back wrong is
|
|
||||||
rewritten up to three times before the run stops — the loader leaves one
|
|
||||||
recoverable way for a page to land wrong (see `W` above), and rewriting is
|
|
||||||
what clears it. `--verify-flash` only reports. Images are raw binary, or
|
|
||||||
Intel HEX by extension. `--force` overrides the refusable safety checks (today: flashing
|
|
||||||
application data into a mega's reset walk region).
|
|
||||||
|
|
||||||
Readouts come one fact per line: `--info` prints the decoded info block
|
|
||||||
field by field, `--fuses` each fuse byte on its own line — plus, on a
|
|
||||||
boot-sectioned mega, the decoded meaning (where the BOOTSZ section starts,
|
|
||||||
what BOOTRST does to reset). Transfers that take wire time — programming,
|
|
||||||
reading, erasing, verifying, the update phases — draw a transient progress
|
|
||||||
bar on stderr when it is a tty; logs and pipes see only the summary lines.
|
|
||||||
`-v`/`--verbose` adds the decisions as they happen: knock counts, the
|
|
||||||
programming plan (vector-surgery targets, skipped blank pages), update
|
|
||||||
state handling and per-phase page counts.
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
`tools/check.sh` runs every chip's workflow (`tools/check.sh --full` adds
|
|
||||||
the reflect-mode builds of libavr's spot set; `tools/make_presets.py`
|
|
||||||
regenerates the presets). Per chip preset, `ctest` runs:
|
|
||||||
|
|
||||||
- `pureboot.size` — the 510-byte (tinies) / 512-byte (mega) budget;
|
|
||||||
- `pureboot_*.size` — the size matrix: the serial backends × the clock
|
|
||||||
ladder (1/8/16 MHz; the t13s' own RC menu), plus the USART1 build on the
|
|
||||||
x4 chips — every configuration axis that could move the image, each
|
|
||||||
variant against the same slot budget (pins are immediate operands and the
|
|
||||||
timeout is a constant: size-neutral);
|
|
||||||
- `pureboot.custom` (328P) — the configured-deployment acceptance test: the
|
|
||||||
1 MHz software-serial TX=PB1/RX=PB5 build from the configuration example
|
|
||||||
drives the full protocol suite through the runner's GPIO bridge, fixture
|
|
||||||
application included;
|
|
||||||
- `pureboot.usart1` (644A) — the same protocol suite over the second
|
|
||||||
hardware USART: instance selection is compile-checked everywhere, but
|
|
||||||
only a live session proves the loader polls the USART it claims;
|
|
||||||
- `pureboot.pi` — the position-independence lint: no absolute `jmp`/`call`
|
|
||||||
in the image, the info block within its first 256 bytes;
|
|
||||||
- `pureboot.planner` — the host tool's pure logic: programming orders and
|
|
||||||
their recovery properties, the surgery, the staging composition, the
|
|
||||||
boot-fuse decode, the update preflight's error/warning matrix over
|
|
||||||
synthetic fuse bytes, and the repairing verify against a fake device — one
|
|
||||||
bad write repaired in a single rewrite, a page that never comes good
|
|
||||||
stopping after exactly three;
|
|
||||||
- `pureboot.protocol` — end to end against a simavr device
|
|
||||||
(`test/pureboot_device.c` — a hardware USART as a pty, or a cycle-timed
|
|
||||||
GPIO⇄pty bridge for a software-UART build, selected with `-l` to match
|
|
||||||
the loader's link; plus the SPM/NVM module simavr's tiny cores lack)
|
|
||||||
driven by the real host tool through
|
|
||||||
knock-from-reset, program + verify of both memories, session reconnect, an
|
|
||||||
external reset through the patched vector, and the hand-over to a fixture
|
|
||||||
application whose banner proves the launch — cross-checked against the
|
|
||||||
simulator's ground-truth memory dumps and an independent decode of the
|
|
||||||
surgery's rjmp words;
|
|
||||||
- `pureboot.reloc` — the identical image installed one slot below the
|
|
||||||
resident serves the complete command set from there (the
|
|
||||||
position-independence acceptance test);
|
|
||||||
- `pureboot.dirty` (328P) — entering the loader from a running application
|
|
||||||
with no reset between, over an SPM page buffer the fixture deliberately
|
|
||||||
dirtied: the case the loader declines to guard against. A bare verify must
|
|
||||||
see the corruption, the repairing verify must fix it in one rewrite, and a
|
|
||||||
plain verify afterwards must pass. On the boot-sectioned megas hardware
|
|
||||||
forbids the state outright (SPM runs only from the boot section, and reset
|
|
||||||
erases the buffer), but simavr dispatches SPM from anywhere — which is what
|
|
||||||
makes the path constructible at all;
|
|
||||||
- `pureboot.update` — the full `--update-loader` flow to a re-timed build,
|
|
||||||
then every power-fail phase: the device is killed mid-write, restarted
|
|
||||||
from its flash dump, and a re-run must complete the update with the
|
|
||||||
application intact throughout.
|
|
||||||
|
|
||||||
`size`, `pi`, and `planner` are host logic and run anywhere; the
|
|
||||||
simulator-driven targets need simavr and a pty, so they are POSIX-only —
|
|
||||||
on Windows the tool is exercised against real hardware.
|
|
||||||
@@ -1,496 +0,0 @@
|
|||||||
// pureboot — a serial bootloader on libavr, pure by constraint: one C++
|
|
||||||
// source with no inline assembly and no global register variables, built for
|
|
||||||
// every chip libavr targets, 512 bytes on each. The device speaks primitives
|
|
||||||
// — read/program flash, read/write EEPROM, fuse bytes, an info block, a jump
|
|
||||||
// — and everything composite (verify, erase, reset-vector surgery, updating
|
|
||||||
// the loader itself) lives in the host tool. Protocol reference: README.md
|
|
||||||
// next to this file.
|
|
||||||
//
|
|
||||||
// The image is position-independent: control flow is PC-relative, the write
|
|
||||||
// and read paths take wire addresses, the write guard refuses the 512-byte
|
|
||||||
// slot the code is *running* in (taken from the runtime return address), the
|
|
||||||
// info block is read relative to that same anchor, and the application jump
|
|
||||||
// is an indirect call to an absolute entry. The identical binary therefore
|
|
||||||
// runs from any 512-byte slot with every command intact: flashed one slot
|
|
||||||
// below the resident loader it becomes the staging loader that rewrites the
|
|
||||||
// resident — how pureboot updates itself, host-driven, with no other
|
|
||||||
// firmware involved.
|
|
||||||
//
|
|
||||||
// Entry: reset lands in avr::startup::entry below (BOOTRST on the
|
|
||||||
// boot-sectioned megas; the patched reset vector — or erased flash walking
|
|
||||||
// up into the loader — on the tinies and the boot-section-less m48s). A
|
|
||||||
// watchdog reset hands straight to the application. Otherwise the
|
|
||||||
// host has one activation window per awaited knock byte ("pb"); an idle line
|
|
||||||
// boots the application. A session then stays in the command loop until 'J'
|
|
||||||
// jumps away or the chip resets.
|
|
||||||
|
|
||||||
#include <libavr/libavr.hpp>
|
|
||||||
|
|
||||||
using namespace avr::literals;
|
|
||||||
namespace spm = avr::spm;
|
|
||||||
namespace ee = avr::eeprom;
|
|
||||||
|
|
||||||
namespace pureboot {
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
// Purely polled — interrupts stay off, every guard folds to nothing.
|
|
||||||
constexpr auto off = avr::irq::guard_policy::unused;
|
|
||||||
|
|
||||||
constexpr std::uint8_t ack = '+';
|
|
||||||
|
|
||||||
// Per-deployment personality, passed in by the build — pureboot_add_loader()
|
|
||||||
// (the CMake function next to this file) resolves the defaults: the clock the
|
|
||||||
// board actually runs, the wire baud, the serial backend and its pins. The
|
|
||||||
// device signature needs no configuring — it comes from the chip database
|
|
||||||
// (avr::hw::db.signature), the only universal source, since the tiny13A
|
|
||||||
// cannot even read its signature row from code.
|
|
||||||
#if !defined(PUREBOOT_CLOCK_HZ) || !defined(PUREBOOT_BAUD)
|
|
||||||
#error \
|
|
||||||
"PUREBOOT_CLOCK_HZ and PUREBOOT_BAUD select this build's clock and baud — create loader targets with pureboot_add_loader() (README.md)"
|
|
||||||
#endif
|
|
||||||
|
|
||||||
using dev = avr::device<{.clock = avr::hertz_t{PUREBOOT_CLOCK_HZ}}>;
|
|
||||||
constexpr avr::baud_t wire_baud{PUREBOOT_BAUD};
|
|
||||||
|
|
||||||
// The watchdog reset flag's home: MCUSR, or the classic megas' MCUCSR.
|
|
||||||
consteval std::int16_t wdrf_field()
|
|
||||||
{
|
|
||||||
auto reg = std::string_view{avr::hw::db.regs[static_cast<std::size_t>(avr::power::detail::reset_reg())].name};
|
|
||||||
return avr::hw::db.field_index(reg, "WDRF");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Geometry: the resident loader owns the top slot of flash — 512 bytes,
|
|
||||||
// except on the >64 KiB chips whose own smallest boot sector is 1 KiB (the
|
|
||||||
// 1284s): there the slot is 1 KiB, matching the hardware boundary the
|
|
||||||
// 512-byte figure comes from everywhere else. The word below the slot is
|
|
||||||
// the trampoline (the application's relocated reset vector) on chips
|
|
||||||
// without a hardware boot section — the tinies and the m48s, whose SPM
|
|
||||||
// runs from anywhere (Atmel-8271 §26). A boot section also means the CPU
|
|
||||||
// runs on while the RWW section programs; everywhere else it halts through
|
|
||||||
// the operation.
|
|
||||||
constexpr std::uint16_t slot_bytes = spm::flash_bytes > 65536 ? 1024 : 512;
|
|
||||||
constexpr std::uint32_t base = spm::flash_bytes - slot_bytes;
|
|
||||||
constexpr std::uint16_t page = spm::page_bytes;
|
|
||||||
constexpr bool boot_section = avr::hw::curated::has_boot_section();
|
|
||||||
|
|
||||||
// Past 64 KiB a byte address no longer fits the wire's 16 bits, so on the
|
|
||||||
// large chips every flash address on the wire — and all slot arithmetic —
|
|
||||||
// is a word address instead ('J' always was one). A slot spans the same
|
|
||||||
// wire-high-byte pair in either unit (512 B = 2 x 256 bytes, 1 KiB =
|
|
||||||
// 2 x 256 words), so the slot index is the high byte with its low bit
|
|
||||||
// dropped everywhere.
|
|
||||||
constexpr bool word_flash = spm::flash_bytes > 65536;
|
|
||||||
constexpr std::uint16_t wire_base =
|
|
||||||
word_flash ? static_cast<std::uint16_t>(base / 2) : static_cast<std::uint16_t>(base);
|
|
||||||
constexpr std::uint16_t wire_page_mask = word_flash ? (page / 2 - 1) : (page - 1);
|
|
||||||
|
|
||||||
// The activation window, in seconds, is a compile-time constant (the build
|
|
||||||
// may override it): the whole EEPROM belongs to the application, and
|
|
||||||
// re-timing the loader is a bootloader self-update with a re-timed binary.
|
|
||||||
#if !defined(PUREBOOT_TIMEOUT)
|
|
||||||
#define PUREBOOT_TIMEOUT 8
|
|
||||||
#endif
|
|
||||||
constexpr std::uint8_t timeout_seconds = PUREBOOT_TIMEOUT;
|
|
||||||
|
|
||||||
// The 12-byte info block the host reads with the 'b' command, flash-resident
|
|
||||||
// through flash_table (there is no crt to copy a .data image, and its storage
|
|
||||||
// carries the word alignment 'b' needs to halve the address on the large
|
|
||||||
// chips). The page byte is the wire count convention: 0 means 256.
|
|
||||||
inline constexpr avr::flash_table<std::array<std::uint8_t, 12>{
|
|
||||||
'P',
|
|
||||||
'B',
|
|
||||||
1, // magic, protocol version
|
|
||||||
avr::hw::db.signature[0],
|
|
||||||
avr::hw::db.signature[1],
|
|
||||||
avr::hw::db.signature[2],
|
|
||||||
static_cast<std::uint8_t>(page),
|
|
||||||
wire_base & 0xff,
|
|
||||||
wire_base >> 8, // app flash ends here; resident loader base (a word address on large chips)
|
|
||||||
avr::hw::db.mem.eeprom_size & 0xff,
|
|
||||||
avr::hw::db.mem.eeprom_size >> 8,
|
|
||||||
// bit 0: host must patch the reset vector (no hardware boot section);
|
|
||||||
// bit 1: flash wire addresses are word addresses
|
|
||||||
static_cast<std::uint8_t>((boot_section ? 0 : 1) | (word_flash ? 2 : 0)),
|
|
||||||
}>
|
|
||||||
info_data;
|
|
||||||
|
|
||||||
// The serial link. PUREBOOT_USART forces a hardware USART instance,
|
|
||||||
// PUREBOOT_SOFT_SERIAL the polled software UART (no vector — the table
|
|
||||||
// belongs to the application) on PUREBOOT_RX/PUREBOOT_TX; with neither, the
|
|
||||||
// chip's first USART where it has one and the software UART elsewhere. Both
|
|
||||||
// are class templates on the clock so only the selected backend is ever
|
|
||||||
// instantiated. pending() is the cheap line test the activation window
|
|
||||||
// polls; rx() then picks the byte up; drain() holds until the last
|
|
||||||
// transmitted frame is fully on the wire (the jump hand-over must not let
|
|
||||||
// the target's re-init clip the ack).
|
|
||||||
#if defined(PUREBOOT_SOFT_SERIAL) && defined(PUREBOOT_USART)
|
|
||||||
#error "PUREBOOT_SOFT_SERIAL and PUREBOOT_USART select opposing serial backends"
|
|
||||||
#endif
|
|
||||||
#if !defined(PUREBOOT_RX)
|
|
||||||
#define PUREBOOT_RX pb0
|
|
||||||
#endif
|
|
||||||
#if !defined(PUREBOOT_TX)
|
|
||||||
#define PUREBOOT_TX pb1
|
|
||||||
#endif
|
|
||||||
#if defined(PUREBOOT_USART)
|
|
||||||
constexpr char usart_digit = '0' + PUREBOOT_USART;
|
|
||||||
#else
|
|
||||||
constexpr char usart_digit = '0';
|
|
||||||
#endif
|
|
||||||
|
|
||||||
template <avr::hertz_t C>
|
|
||||||
struct hardware_link {
|
|
||||||
using uart = avr::uart::usart<usart_digit, C, {.baud = wire_baud, .max_baud_error = 2.5_pct}>;
|
|
||||||
|
|
||||||
// The compiled idle poll: lds UCSR0A (2), sbrc skipping the exit (2),
|
|
||||||
// sbiw + sbci + sbci + brne (6).
|
|
||||||
static constexpr std::uint8_t poll_cycles = 10;
|
|
||||||
|
|
||||||
static void init()
|
|
||||||
{
|
|
||||||
avr::init<uart>();
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool pending()
|
|
||||||
{
|
|
||||||
return uart::rx_ready();
|
|
||||||
}
|
|
||||||
|
|
||||||
static std::uint8_t rx()
|
|
||||||
{
|
|
||||||
return uart::read_blocking();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void tx(std::uint8_t byte)
|
|
||||||
{
|
|
||||||
uart::write(byte);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void drain()
|
|
||||||
{
|
|
||||||
uart::drain();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
template <avr::hertz_t C>
|
|
||||||
struct software_link {
|
|
||||||
using rx_t = avr::uart::software_rx_polled<C, avr::PUREBOOT_RX, wire_baud>;
|
|
||||||
using tx_t = avr::uart::software_tx<C, avr::PUREBOOT_TX, wire_baud>;
|
|
||||||
|
|
||||||
// The compiled idle poll: sbis skipping the exit (2), sbiw + sbci +
|
|
||||||
// sbci + brne (6).
|
|
||||||
static constexpr std::uint8_t poll_cycles = 8;
|
|
||||||
|
|
||||||
static void init()
|
|
||||||
{
|
|
||||||
avr::init<rx_t, tx_t>();
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool pending()
|
|
||||||
{
|
|
||||||
return rx_t::start_pending();
|
|
||||||
}
|
|
||||||
|
|
||||||
static std::uint8_t rx()
|
|
||||||
{
|
|
||||||
return rx_t::template read_blocking<off>();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void tx(std::uint8_t byte)
|
|
||||||
{
|
|
||||||
tx_t::template write<off>(byte);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void drain()
|
|
||||||
{
|
|
||||||
// The software transmitter returns only after the stop bit.
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
#if defined(PUREBOOT_USART)
|
|
||||||
static_assert(avr::uart::has_usart<usart_digit>(), "PUREBOOT_USART selects a hardware USART this chip does not have");
|
|
||||||
using link = hardware_link<dev::clock>;
|
|
||||||
#elif defined(PUREBOOT_SOFT_SERIAL)
|
|
||||||
using link = software_link<dev::clock>;
|
|
||||||
#else
|
|
||||||
using link =
|
|
||||||
std::conditional_t<avr::uart::has_usart<usart_digit>(), hardware_link<dev::clock>, software_link<dev::clock>>;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// The application's entry, an absolute address the linker pins (--defsym in
|
|
||||||
// CMakeLists.txt): 0x0000 on the mega (word 0 stays the application's own
|
|
||||||
// vector — BOOTRST re-vectors a reset into the loader in hardware) and the
|
|
||||||
// trampoline word at base - 2 on the tinies. Reaching it must not depend on
|
|
||||||
// where this copy runs, so the jump goes through a pointer: [[gnu::noipa]]
|
|
||||||
// keeps the constant from folding back into a PC-relative call.
|
|
||||||
extern "C" [[noreturn]] void pureboot_app();
|
|
||||||
|
|
||||||
[[gnu::noipa, noreturn]] void jump(void (*target)())
|
|
||||||
{
|
|
||||||
target();
|
|
||||||
__builtin_unreachable();
|
|
||||||
}
|
|
||||||
|
|
||||||
[[gnu::noinline, noreturn]] void run_app()
|
|
||||||
{
|
|
||||||
jump(pureboot_app);
|
|
||||||
}
|
|
||||||
|
|
||||||
// One activation window is a single 32-bit poll countdown. The divisor is
|
|
||||||
// the backend's counted poll-loop cycles (its own comment reads them off the
|
|
||||||
// compiled loop); whole-second precision is all the window promises, so the
|
|
||||||
// nearest cycle count is plenty.
|
|
||||||
consteval std::uint32_t window_polls()
|
|
||||||
{
|
|
||||||
return timeout_seconds * static_cast<std::uint32_t>(dev::clock.hz / link::poll_cycles);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool pending_before_deadline()
|
|
||||||
{
|
|
||||||
std::uint32_t polls = window_polls();
|
|
||||||
do {
|
|
||||||
if (link::pending())
|
|
||||||
return true;
|
|
||||||
} while (--polls);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A knock byte under the activation deadline: an idle line means no host is
|
|
||||||
// there, and the application runs.
|
|
||||||
std::uint8_t rx_deadline()
|
|
||||||
{
|
|
||||||
if (!pending_before_deadline())
|
|
||||||
run_app();
|
|
||||||
return link::rx();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inlined into its call sites: reading two bytes across a call otherwise
|
|
||||||
// strands the first in a call-saved register the caller must push/pop; folded
|
|
||||||
// into the (noreturn) command loop that cost disappears.
|
|
||||||
[[gnu::always_inline]] inline std::uint16_t rx16()
|
|
||||||
{
|
|
||||||
std::uint16_t low = link::rx();
|
|
||||||
return static_cast<std::uint16_t>(low | (link::rx() << 8));
|
|
||||||
}
|
|
||||||
|
|
||||||
// The streamers take the count in the wire's 8-bit form: 0 means 256.
|
|
||||||
//
|
|
||||||
// Two functions, because they want opposite placement and placement is an
|
|
||||||
// attribute: the byte-addressed loop is small enough to inline into both
|
|
||||||
// callers, the word-addressed one stays out of line but flattened — a call to
|
|
||||||
// the transmit inside it would strand the 24-bit cursor in callee-saved
|
|
||||||
// registers. `word_flash` picks at the call site.
|
|
||||||
[[maybe_unused, gnu::always_inline]] inline void send_flash_near(std::uint16_t address, std::uint8_t count)
|
|
||||||
{
|
|
||||||
do
|
|
||||||
link::tx(avr::flash_load(reinterpret_cast<const std::uint8_t *>(address++)));
|
|
||||||
while (--count);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The 24-bit cursor as the machine holds it: the RAMPZ byte and a 16-bit Z,
|
|
||||||
// carried explicitly (the reassembled 32-bit address folds away inside the
|
|
||||||
// inlined far load).
|
|
||||||
[[maybe_unused, gnu::flatten, gnu::noinline]] void send_flash_far(std::uint16_t address, std::uint8_t count)
|
|
||||||
{
|
|
||||||
std::uint8_t rampz = static_cast<std::uint8_t>(address >> 15);
|
|
||||||
std::uint16_t z = static_cast<std::uint16_t>(address << 1);
|
|
||||||
do {
|
|
||||||
link::tx(avr::flash_load_far<std::uint8_t>((static_cast<std::uint32_t>(rampz) << 16) | z));
|
|
||||||
// The protocol never reads across 64 KiB, but carrying the wrap is
|
|
||||||
// smaller than the flat 32-bit cursor GCC builds without it.
|
|
||||||
if (++z == 0)
|
|
||||||
++rampz;
|
|
||||||
} while (--count);
|
|
||||||
}
|
|
||||||
|
|
||||||
[[gnu::always_inline]] inline void send_flash(std::uint16_t address, std::uint8_t count)
|
|
||||||
{
|
|
||||||
if constexpr (word_flash)
|
|
||||||
send_flash_far(address, count);
|
|
||||||
else
|
|
||||||
send_flash_near(address, count);
|
|
||||||
}
|
|
||||||
|
|
||||||
void send_eeprom(std::uint16_t address, std::uint8_t count)
|
|
||||||
{
|
|
||||||
do
|
|
||||||
link::tx(ee::read(address++));
|
|
||||||
while (--count);
|
|
||||||
}
|
|
||||||
|
|
||||||
// EEPROM write, host-paced: each ack goes out once the byte's write has
|
|
||||||
// begun, so the next byte arrives while it completes and the following
|
|
||||||
// write's own ready-wait sees an idle line. Nothing is ever missed, on
|
|
||||||
// either serial backend, without a buffer.
|
|
||||||
void store_eeprom(std::uint16_t address, std::uint8_t count)
|
|
||||||
{
|
|
||||||
do {
|
|
||||||
ee::write<off>(address++, link::rx());
|
|
||||||
link::tx(ack);
|
|
||||||
} while (--count);
|
|
||||||
}
|
|
||||||
|
|
||||||
// One flash page: stream the bytes into the SPM buffer as little-endian
|
|
||||||
// words, then erase and program — except the 512-byte slot this code runs
|
|
||||||
// in, which is drained but never programmed, so a copy can never erase
|
|
||||||
// itself. `slot_high` is the high byte of that running slot's base (run()
|
|
||||||
// derives it); a broken host thus cannot brick the running loader, and a
|
|
||||||
// copy flashed one slot lower may rewrite the slot above it — how pureboot
|
|
||||||
// updates itself.
|
|
||||||
void program_flash(std::uint16_t wire_address, std::uint8_t slot_high)
|
|
||||||
{
|
|
||||||
// No discard before the fill: the buffer is write-once per word
|
|
||||||
// (§26.2.1), so filling over one a refused page or an application left
|
|
||||||
// dirty programs stale words — but a page write auto-erases the buffer
|
|
||||||
// (§26.2.1; §19.2 on the tinies), so that write clears the condition and
|
|
||||||
// the host's read-back rewrites the page.
|
|
||||||
|
|
||||||
// One induction either way. On the byte-addressed chips the wire address
|
|
||||||
// itself walks the page (aligned, so the offset bits wrap to zero); on
|
|
||||||
// the word-addressed large chips the wire word address becomes a 32-bit
|
|
||||||
// byte cursor once, and their 256-byte page makes its low byte the whole
|
|
||||||
// in-page offset. The slot index is one high byte of the wire address —
|
|
||||||
// two values on byte-addressed chips (the & ~1), bits 16:9 re-packed on
|
|
||||||
// the large ones.
|
|
||||||
spm::flash_address_t address;
|
|
||||||
std::uint8_t page_high;
|
|
||||||
if constexpr (word_flash) {
|
|
||||||
// Pages are aligned, so one page never crosses a 64 KiB boundary:
|
|
||||||
// RAMPZ is a per-page constant and the fill cursor is a 16-bit Z
|
|
||||||
// whose low byte is the whole in-page offset (256-byte pages). The
|
|
||||||
// slot index is simply the wire word address's high byte.
|
|
||||||
const std::uint8_t rampz = static_cast<std::uint8_t>(wire_address >> 15);
|
|
||||||
const std::uint16_t z0 = static_cast<std::uint16_t>(wire_address << 1);
|
|
||||||
std::uint16_t z = z0;
|
|
||||||
do {
|
|
||||||
std::uint8_t low = link::rx();
|
|
||||||
std::uint8_t high = link::rx();
|
|
||||||
spm::fill<off>((static_cast<spm::flash_address_t>(rampz) << 16) | z,
|
|
||||||
static_cast<std::uint16_t>(low | (high << 8)));
|
|
||||||
z += 2;
|
|
||||||
} while (static_cast<std::uint8_t>(z));
|
|
||||||
address = (static_cast<spm::flash_address_t>(rampz) << 16) | z0;
|
|
||||||
page_high = static_cast<std::uint8_t>(wire_address >> 8) & 0xfe;
|
|
||||||
} else {
|
|
||||||
address = static_cast<spm::flash_address_t>(wire_address);
|
|
||||||
do {
|
|
||||||
std::uint8_t low = link::rx();
|
|
||||||
std::uint8_t high = link::rx();
|
|
||||||
spm::fill<off>(address, static_cast<std::uint16_t>(low | (high << 8)));
|
|
||||||
address += 2;
|
|
||||||
} while (static_cast<std::uint8_t>(address) & (page - 1));
|
|
||||||
address -= 2; // back inside the page — erase and write ignore the word bits
|
|
||||||
page_high = static_cast<std::uint8_t>(address >> 8) & 0xfe;
|
|
||||||
}
|
|
||||||
if (page_high != slot_high) {
|
|
||||||
// The tinies and the m48s halt the CPU through the erase and the
|
|
||||||
// write, so only the boot-sectioned megas — running on while their
|
|
||||||
// RWW section programs — wait.
|
|
||||||
spm::erase_page<off>(address);
|
|
||||||
if constexpr (boot_section)
|
|
||||||
spm::wait();
|
|
||||||
spm::write_page<off>(address);
|
|
||||||
if constexpr (boot_section)
|
|
||||||
spm::wait();
|
|
||||||
}
|
|
||||||
// The megas program with their RWW section disabled; reads need it back
|
|
||||||
// on. The same store discards the buffer (§26.2.2), so they never meet
|
|
||||||
// the stale-word case above. boot_section implies an RWW section.
|
|
||||||
if constexpr (boot_section)
|
|
||||||
spm::rww_enable<off>();
|
|
||||||
}
|
|
||||||
|
|
||||||
// The four fuse/lock bytes in the hardware's own Z order: low, lock,
|
|
||||||
// extended, high. Writing fuses is not a thing self-programming can do on
|
|
||||||
// AVR — SPM reaches flash (and boot lock bits) only.
|
|
||||||
void send_fuses()
|
|
||||||
{
|
|
||||||
std::uint8_t which = 0;
|
|
||||||
do
|
|
||||||
link::tx(spm::read_fuse<off>(static_cast<spm::fuse>(which)));
|
|
||||||
while (++which & 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
[[noreturn]] void run()
|
|
||||||
{
|
|
||||||
// A watchdog reset belongs to the application (whose watchdog stays
|
|
||||||
// forced on until it clears WDRF) — no activation window in its way.
|
|
||||||
// The flag register is MCUSR, or the classic megas' MCUCSR.
|
|
||||||
if (avr::hw::field_impl<wdrf_field()>::test())
|
|
||||||
run_app();
|
|
||||||
|
|
||||||
link::init();
|
|
||||||
|
|
||||||
// The high byte of the 512-byte-aligned base this copy runs at: the
|
|
||||||
// return address is a word address, whose high byte is the 256-word slot
|
|
||||||
// index — on byte-addressed chips doubled back into byte terms.
|
|
||||||
// program_flash refuses this one slot and the info block is addressed
|
|
||||||
// from it, so both follow wherever the code was flashed. The high byte is
|
|
||||||
// spelled as byteswap's low byte: the builtin's value is itself built by
|
|
||||||
// swapping the two stacked bytes, and the double swap folds to the single
|
|
||||||
// byte pick a hand assembler writes — `>> 8` leaves the swap materialized.
|
|
||||||
const std::uint16_t ra_words = reinterpret_cast<std::uint16_t>(__builtin_return_address(0));
|
|
||||||
const std::uint8_t ra_high = static_cast<std::uint8_t>(std::byteswap(ra_words));
|
|
||||||
const std::uint8_t slot_high = word_flash ? ra_high & 0xfe : static_cast<std::uint8_t>(ra_high << 1);
|
|
||||||
|
|
||||||
// The knock: 'p' then 'b', each under a fresh window; any other byte is
|
|
||||||
// line noise and waits again. Falling out of a window runs the app.
|
|
||||||
while (rx_deadline() != 'p' || rx_deadline() != 'b') {
|
|
||||||
}
|
|
||||||
|
|
||||||
for (;;) {
|
|
||||||
// No prompt while an EEPROM write runs: a pending write blocks SPM
|
|
||||||
// and fuse reads (§26.2.1), and the ack tells the host all is done.
|
|
||||||
ee::wait();
|
|
||||||
link::tx(ack);
|
|
||||||
const std::uint8_t command = link::rx();
|
|
||||||
switch (command) {
|
|
||||||
case 'b': { // info block, read relative to the running slot
|
|
||||||
// The block sits in the image's first 256 bytes (the build lint
|
|
||||||
// asserts it), and slots are 512-aligned — so the low byte of its
|
|
||||||
// link address (in wire units: bytes, or words on the large
|
|
||||||
// chips) is its offset in any slot, and the high byte of its
|
|
||||||
// runtime address is the running slot's. Composed from the two
|
|
||||||
// bytes — the high half is runtime data, so no absolute address
|
|
||||||
// is ever materialized.
|
|
||||||
const auto link_low = reinterpret_cast<std::uint16_t>(info_data.storage.data());
|
|
||||||
const std::uint8_t low =
|
|
||||||
word_flash ? static_cast<std::uint8_t>(link_low >> 1) : static_cast<std::uint8_t>(link_low);
|
|
||||||
send_flash(static_cast<std::uint16_t>(low | (slot_high << 8)), static_cast<std::uint8_t>(info_data.size()));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'J': { // jump to a wire word address: hand-over and staging transfer
|
|
||||||
auto target = reinterpret_cast<void (*)()>(rx16());
|
|
||||||
link::tx(ack);
|
|
||||||
link::drain();
|
|
||||||
jump(target);
|
|
||||||
}
|
|
||||||
case 'R': // read flash: addr16, n8 (0 = 256)
|
|
||||||
case 'r': // read EEPROM: addr16, n8
|
|
||||||
case 'w': { // write EEPROM: addr16, n8, then n bytes each acked
|
|
||||||
std::uint16_t address = rx16();
|
|
||||||
std::uint8_t count = link::rx();
|
|
||||||
if (command == 'R')
|
|
||||||
send_flash(address, count);
|
|
||||||
else if (command == 'r')
|
|
||||||
send_eeprom(address, count);
|
|
||||||
else
|
|
||||||
store_eeprom(address, count);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'W': // program one flash page: addr16, page bytes
|
|
||||||
program_flash(rx16(), slot_high);
|
|
||||||
break;
|
|
||||||
case 'F': // fuse and lock bytes
|
|
||||||
send_fuses();
|
|
||||||
break;
|
|
||||||
default: // unknown bytes are ignored; the loop re-acks
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
} // namespace pureboot
|
|
||||||
|
|
||||||
template struct avr::startup::entry<pureboot::run>;
|
|
||||||
1140
pureboot/pureboot.py
1140
pureboot/pureboot.py
File diff suppressed because it is too large
Load Diff
@@ -1,54 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Position-independence lint for the pureboot image.
|
|
||||||
|
|
||||||
The self-staging design lets the identical binary run from any 512-byte
|
|
||||||
slot, which holds only if nothing in the image addresses itself absolutely.
|
|
||||||
Two link-time facts guarantee it, both asserted here from the built ELF:
|
|
||||||
|
|
||||||
1. No absolute jmp/call opcodes — all control flow is PC-relative
|
|
||||||
(rjmp/rcall/ijmp/icall). -mrelax normally guarantees this; a code
|
|
||||||
change that grows a branch out of relaxation range would break it
|
|
||||||
silently.
|
|
||||||
2. The info block sits within the image's first 256 bytes: the 'b'
|
|
||||||
command rebuilds its address as (running slot high byte : low byte of
|
|
||||||
the link address), which needs the offset to fit that low byte.
|
|
||||||
|
|
||||||
Usage: check_pi.py <objdump> <nm> <elf> <text_start_hex>
|
|
||||||
"""
|
|
||||||
|
|
||||||
import re
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
objdump, nm, elf, text_start = sys.argv[1:]
|
|
||||||
text_start = int(text_start, 0)
|
|
||||||
|
|
||||||
listing = subprocess.run([objdump, "-d", elf], capture_output=True, text=True, check=True).stdout
|
|
||||||
absolute = [
|
|
||||||
line
|
|
||||||
for line in listing.splitlines()
|
|
||||||
if re.search(r"\t(jmp|call)\t", line)
|
|
||||||
]
|
|
||||||
if absolute:
|
|
||||||
print("FAIL: absolute control flow in the image:")
|
|
||||||
print("\n".join(absolute))
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
symbols = subprocess.run([nm, "-C", elf], capture_output=True, text=True, check=True).stdout
|
|
||||||
info = [line for line in symbols.splitlines() if "flash_table" in line and "::storage" in line]
|
|
||||||
if len(info) != 1:
|
|
||||||
print(f"FAIL: expected one info-block storage symbol, found {len(info)}")
|
|
||||||
sys.exit(1)
|
|
||||||
address = int(info[0].split()[0], 16)
|
|
||||||
offset = address - text_start
|
|
||||||
if not 0 <= offset < 256:
|
|
||||||
print(f"FAIL: info block at image offset {offset:#x}, must sit in the first 256 bytes")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
print(f"PI lint: control flow PC-relative, info block at offset {offset:#x}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
115
test/pbapp.cpp
115
test/pbapp.cpp
@@ -1,115 +0,0 @@
|
|||||||
// Test-fixture application for the pureboot protocol tests: prints "APP" on
|
|
||||||
// the chip's serial link (the same link the loader uses) — the proof that
|
|
||||||
// the loader's hand-over, and on the tinies the host's reset-vector
|
|
||||||
// surgery, actually launched it. Linked normally (crt, vectors at 0); on
|
|
||||||
// the tinies its reset vector is the rjmp the host re-homes.
|
|
||||||
//
|
|
||||||
// On the hardware-USART link it then listens, and an 'L' makes it jump into
|
|
||||||
// the resident loader — the application-owned loader entry a
|
|
||||||
// BOOTRST-unprogrammed mega relies on (reset always boots the application
|
|
||||||
// there), exercised by the self-update tests. The software link idles:
|
|
||||||
// reset reaches those loaders through the patched vector (or the runner
|
|
||||||
// models BOOTRST), so the application owes them nothing.
|
|
||||||
//
|
|
||||||
// The fixture speaks the deployment its loader was built for: the same
|
|
||||||
// PUREBOOT_* defines configure it, and without them it assumes the stock
|
|
||||||
// deployment (the crystal/RC clock table below, the chip's natural link).
|
|
||||||
#include <libavr/libavr.hpp>
|
|
||||||
|
|
||||||
using namespace avr::literals;
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
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};
|
|
||||||
if (name.starts_with("ATtiny13"))
|
|
||||||
return 9.6_MHz;
|
|
||||||
if (name.starts_with("ATtiny"))
|
|
||||||
return 8_MHz;
|
|
||||||
return 16_MHz;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
#if !defined(PUREBOOT_TX)
|
|
||||||
#define PUREBOOT_TX pb1
|
|
||||||
#endif
|
|
||||||
#if !defined(PUREBOOT_USART)
|
|
||||||
#define PUREBOOT_USART 0
|
|
||||||
#endif
|
|
||||||
|
|
||||||
consteval bool use_hardware()
|
|
||||||
{
|
|
||||||
#if defined(PUREBOOT_SOFT_SERIAL)
|
|
||||||
return false;
|
|
||||||
#else
|
|
||||||
return avr::hw::db.has_instance("USART0") || avr::hw::db.has_instance("USART");
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
using dev = avr::device<{.clock = clock()}>;
|
|
||||||
|
|
||||||
template <avr::hertz_t C, bool Hardware = use_hardware()>
|
|
||||||
struct link {
|
|
||||||
#if defined(PUREBOOT_BAUD)
|
|
||||||
static constexpr avr::baud_t baud{PUREBOOT_BAUD};
|
|
||||||
#else
|
|
||||||
static constexpr avr::baud_t baud{115200};
|
|
||||||
#endif
|
|
||||||
using tx_t = avr::uart::usart<'0' + PUREBOOT_USART, C, {.baud = baud, .max_baud_error = 2.5_pct}>;
|
|
||||||
static void tx(char c)
|
|
||||||
{
|
|
||||||
tx_t::write(static_cast<std::uint8_t>(c));
|
|
||||||
}
|
|
||||||
[[noreturn]] static void idle()
|
|
||||||
{
|
|
||||||
// 'L' hands back to the loader at the top slot — 512 bytes, or the
|
|
||||||
// 1 KiB the >64 KiB chips use.
|
|
||||||
constexpr std::uint32_t slot = avr::hw::db.mem.flash_size > 65536 ? 1024 : 512;
|
|
||||||
for (;;) {
|
|
||||||
auto command = tx_t::read_blocking();
|
|
||||||
if (command == 'L')
|
|
||||||
reinterpret_cast<void (*)()>(static_cast<std::uint16_t>((avr::hw::db.mem.flash_size - slot) / 2))();
|
|
||||||
// 'D' leaves every word of the SPM page buffer dirty, so that a
|
|
||||||
// following 'L' enters the loader with the buffer it never clears.
|
|
||||||
if (command == 'D') {
|
|
||||||
for (std::uint16_t at = 0; at < avr::spm::page_bytes; at += 2)
|
|
||||||
avr::spm::fill(at, 0xdead);
|
|
||||||
tx('D');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
template <avr::hertz_t C>
|
|
||||||
struct link<C, false> {
|
|
||||||
#if defined(PUREBOOT_BAUD)
|
|
||||||
static constexpr avr::baud_t baud{PUREBOOT_BAUD};
|
|
||||||
#else
|
|
||||||
static constexpr avr::baud_t baud{57600};
|
|
||||||
#endif
|
|
||||||
using tx_t = avr::uart::software_tx<C, avr::PUREBOOT_TX, baud>;
|
|
||||||
static void tx(char c)
|
|
||||||
{
|
|
||||||
tx_t::write(static_cast<std::uint8_t>(c));
|
|
||||||
}
|
|
||||||
[[noreturn]] static void idle()
|
|
||||||
{
|
|
||||||
while (true) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
int main()
|
|
||||||
{
|
|
||||||
avr::init<typename link<dev::clock>::tx_t>();
|
|
||||||
link<dev::clock>::tx('A');
|
|
||||||
link<dev::clock>::tx('P');
|
|
||||||
link<dev::clock>::tx('P');
|
|
||||||
link<dev::clock>::idle();
|
|
||||||
}
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Dirty-page-buffer acceptance test: the loader carries no buffer discard,
|
|
||||||
so a page filled over words an earlier writer left behind programs those
|
|
||||||
instead. This asserts the whole contract — the corruption is real and a bare
|
|
||||||
verify sees it, the repairing verify fixes it in one rewrite (the write that
|
|
||||||
took the stale words auto-erased the buffer), and it stays fixed.
|
|
||||||
|
|
||||||
The state is reached the way the loader cannot prevent: an application
|
|
||||||
dirties the buffer and jumps in with no reset between. Real boot-sectioned
|
|
||||||
megas forbid that outright — SPM executes only from the boot section
|
|
||||||
(Atmel-8271 §26.2) — but simavr dispatches SPM from anywhere, which is what
|
|
||||||
makes the path constructible at all.
|
|
||||||
|
|
||||||
Usage: pbdirty.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
|
|
||||||
<baud> <app_bin> <tool_py> <workdir>
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def fail(message):
|
|
||||||
print(f"FAIL: {message}")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
device_bin, elf, mcu, hz, base_hex, page, baud, app_bin, tool, workdir = sys.argv[1:]
|
|
||||||
page, baud = int(page), int(baud)
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
import pbsim
|
|
||||||
import pureboot as pb
|
|
||||||
|
|
||||||
os.makedirs(workdir, exist_ok=True)
|
|
||||||
dump = os.path.join(workdir, "dump.bin")
|
|
||||||
|
|
||||||
# Reset boots the application on a BOOTRST-unprogrammed mega; its 'L' is
|
|
||||||
# the loader entry this test needs, reached without a reset.
|
|
||||||
device = pbsim.Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump, reset_hex="0")
|
|
||||||
try:
|
|
||||||
port = pb.Port(device.pty, baud)
|
|
||||||
loader = pb.Loader(port)
|
|
||||||
loader.connect(25)
|
|
||||||
|
|
||||||
# Install the application and hand over to it.
|
|
||||||
pb.op_flash(loader, app_bin, erase=False, verify=True)
|
|
||||||
loader.run_application()
|
|
||||||
if port.read_exact(3, 5.0) != b"APP":
|
|
||||||
fail("the application did not start")
|
|
||||||
|
|
||||||
port.write(b"D")
|
|
||||||
if port.read_exact(1, 5.0) != b"D":
|
|
||||||
fail("the application did not acknowledge dirtying the page buffer")
|
|
||||||
port.write(b"L")
|
|
||||||
loader = pb.Loader(port)
|
|
||||||
loader.connect(25)
|
|
||||||
|
|
||||||
# Program by hand, so the corruption is observable before anything
|
|
||||||
# repairs it.
|
|
||||||
pages = pb.plan_flash(open(app_bin, "rb").read(), loader.info)
|
|
||||||
for address in sorted(pages):
|
|
||||||
loader.write_page(address, pages[address])
|
|
||||||
try:
|
|
||||||
pb.verify_pages(loader, pages)
|
|
||||||
except pb.Error as error:
|
|
||||||
if "verify failed" not in str(error):
|
|
||||||
fail(f"the read-back failed, but not at verify: {error}")
|
|
||||||
else:
|
|
||||||
# Either the fixture no longer dirties the buffer, or the loader
|
|
||||||
# clears it again — in which case this test's premise is gone.
|
|
||||||
fail("programming over a dirty page buffer came back clean")
|
|
||||||
|
|
||||||
# What the programming path uses: one rewrite settles it, and it stays
|
|
||||||
# settled.
|
|
||||||
pb.verify_pages(loader, pages, repair=True)
|
|
||||||
pb.verify_pages(loader, pages)
|
|
||||||
|
|
||||||
# Ground truth beyond the loader's own read-back.
|
|
||||||
loader.run_application()
|
|
||||||
if port.read_exact(3, 5.0) != b"APP":
|
|
||||||
fail("the application did not start after the recovered write")
|
|
||||||
port.close()
|
|
||||||
finally:
|
|
||||||
device.stop()
|
|
||||||
print("pbdirty: a dirty page buffer is caught by verify and cleared by the retry")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
102
test/pbrehome.py
102
test/pbrehome.py
@@ -1,102 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Re-homing acceptance test: a pureboot image programmed somewhere other
|
|
||||||
than its canonical top slot must still be a working loader —
|
|
||||||
position-independent, guarding its accidental slot — and the ordinary
|
|
||||||
--update-loader flow must put a build into the top slot from there.
|
|
||||||
|
|
||||||
Two positions are exercised. Address 0 (a raw .bin handed to a programmer,
|
|
||||||
which defaults to offset 0): the staging install and the word-0 redirect
|
|
||||||
both run from copies whose slots are not page 0's, so the running-slot
|
|
||||||
guard never blocks the flow. The staging slot itself: a loader already
|
|
||||||
sitting there IS the installed staging copy — the tool recognizes it by
|
|
||||||
its embedded info block and leaves it in place instead of tripping the
|
|
||||||
copy's own guard on the composed through-word — and that (older) copy
|
|
||||||
streams the new resident like any staged copy. In both cases flashing an
|
|
||||||
application through the healed resident overwrites the stale copy, vector
|
|
||||||
surgery included, and the banner proves the launch.
|
|
||||||
|
|
||||||
Usage: pbrehome.py <device_bin> <pureboot_elf> <update_bin> <mcu> <hz>
|
|
||||||
<base_hex> <page> <baud> <app_bin> <tool_py> <workdir>
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def fail(message):
|
|
||||||
print(f"FAIL: {message}")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def rehome_from(pbsim, pb, device_bin, elf, place_hex, guard_probe, update_bin, base, page, baud, app_bin, workdir,
|
|
||||||
mcu, hz):
|
|
||||||
"""Place the loader at `place_hex`, heal through --update-loader, flash
|
|
||||||
the application, expect the banner."""
|
|
||||||
dump = os.path.join(workdir, f"dump-{place_hex}.bin")
|
|
||||||
state = os.path.join(workdir, f"rehome-{place_hex}.pbstate")
|
|
||||||
if os.path.exists(state):
|
|
||||||
os.unlink(state)
|
|
||||||
device = pbsim.Device(device_bin, elf, mcu, hz, place_hex, page, baud, dump, reset_hex="0")
|
|
||||||
try:
|
|
||||||
port = pb.Port(device.pty, baud)
|
|
||||||
loader = pb.Loader(port)
|
|
||||||
info = loader.connect(25)
|
|
||||||
if info.base != base:
|
|
||||||
fail(f"the misplaced copy reports base {info.base:#06x} — the info block must stay canonical")
|
|
||||||
|
|
||||||
# The accidental slot still guards itself; re-homing rides on the
|
|
||||||
# canonical slots being writable from it.
|
|
||||||
probe = int(guard_probe, 0)
|
|
||||||
before = loader.read_flash(probe, info.page)
|
|
||||||
loader.write_page(probe, bytes(info.page))
|
|
||||||
if loader.read_flash(probe, info.page) != before:
|
|
||||||
fail("the misplaced copy's guard let its own slot change")
|
|
||||||
|
|
||||||
# The ordinary update flow puts the build into the top slot.
|
|
||||||
pb.op_update_loader(loader, 25, update_bin, state, None)
|
|
||||||
update = open(update_bin, "rb").read()
|
|
||||||
if loader.read_flash(base, len(update)) != update:
|
|
||||||
fail("the canonical slot does not hold the update image")
|
|
||||||
|
|
||||||
# An application flashed through the healed resident overwrites the
|
|
||||||
# stale copy (surgery included) and launches.
|
|
||||||
pages = pb.plan_flash(open(app_bin, "rb").read(), loader.info)
|
|
||||||
for address in pb.covered(pages, loader.info, skip_blank=False):
|
|
||||||
loader.write_page(address, pages[address])
|
|
||||||
pb.verify_pages(loader, pages)
|
|
||||||
loader.run_application()
|
|
||||||
if port.read_exact(3, 5.0) != b"APP":
|
|
||||||
fail(f"application does not banner after the re-home from {place_hex}")
|
|
||||||
port.close()
|
|
||||||
finally:
|
|
||||||
device.stop()
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
(device_bin, elf, update_bin, mcu, hz, base_hex, page, baud, app_bin, tool, workdir) = sys.argv[1:]
|
|
||||||
base, page, baud = int(base_hex, 0), int(page), int(baud)
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
import pbsim
|
|
||||||
import pureboot as pb
|
|
||||||
|
|
||||||
os.makedirs(workdir, exist_ok=True)
|
|
||||||
|
|
||||||
# Address 0: the raw-.bin-to-a-programmer accident. The guard probe is
|
|
||||||
# the copy's own page 0.
|
|
||||||
rehome_from(pbsim, pb, device_bin, elf, "0x0", "0x0", update_bin, base, page, baud, app_bin, workdir, mcu, hz)
|
|
||||||
print("re-home from address 0: converged")
|
|
||||||
|
|
||||||
# The staging slot: erased flash with the loader sitting exactly where
|
|
||||||
# a staging copy would — the tool must leave it in place and let it
|
|
||||||
# stream the (different) update build into the resident slot.
|
|
||||||
stage = base - 512
|
|
||||||
rehome_from(pbsim, pb, device_bin, elf, hex(stage), hex(stage), update_bin, base, page, baud, app_bin, workdir,
|
|
||||||
mcu, hz)
|
|
||||||
print("re-home from the staging slot: converged")
|
|
||||||
|
|
||||||
print("pbrehome: a misplaced loader re-homes through the ordinary update flow")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Position-independence acceptance test: the identical pureboot binary,
|
|
||||||
flashed one slot below the resident loader, must serve the complete command
|
|
||||||
set from there. The resident installs it (through-word composed by the host
|
|
||||||
layer), 'J' transfers control, and every command is exercised against the
|
|
||||||
staged copy — the info block must come back byte-identical, the write guard
|
|
||||||
must protect the staged copy's own slot and permit the resident's, and the
|
|
||||||
staged copy must be able to rewrite the resident slot verbatim.
|
|
||||||
|
|
||||||
Usage: pbreloc.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
|
|
||||||
<baud> <tool_py> <workdir>
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def fail(message):
|
|
||||||
print(f"FAIL: {message}")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
device_bin, elf, mcu, hz, base_hex, page, baud, tool, workdir = sys.argv[1:]
|
|
||||||
base, page, baud = int(base_hex, 0), int(page), int(baud)
|
|
||||||
stage = None # derived from the device's own info (slot-sized) below
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
import pbsim
|
|
||||||
import pureboot as pb
|
|
||||||
|
|
||||||
os.makedirs(workdir, exist_ok=True)
|
|
||||||
objcopy = os.environ.get("PB_OBJCOPY", "avr-objcopy")
|
|
||||||
image_path = os.path.join(workdir, "pureboot.bin")
|
|
||||||
subprocess.run([objcopy, "-O", "binary", elf, image_path], check=True)
|
|
||||||
image = open(image_path, "rb").read()
|
|
||||||
|
|
||||||
device = pbsim.Device(device_bin, elf, mcu, hz, base_hex, page, baud, os.path.join(workdir, "dump.bin"))
|
|
||||||
try:
|
|
||||||
port = pb.Port(device.pty, baud)
|
|
||||||
loader = pb.Loader(port)
|
|
||||||
info = loader.connect(25)
|
|
||||||
if info.base != base:
|
|
||||||
fail(f"info reports base {info.base:#06x}")
|
|
||||||
resident_info = info.raw
|
|
||||||
|
|
||||||
# Install the staging copy exactly as the update flow would.
|
|
||||||
stage = info.stage
|
|
||||||
staged = pb.staging_content(image, info)
|
|
||||||
pb.write_differing(loader, stage, staged)
|
|
||||||
|
|
||||||
# Enter it; from here on, every command runs in the relocated copy.
|
|
||||||
staged_info = loader.enter_copy(stage, 25)
|
|
||||||
if staged_info.raw != resident_info:
|
|
||||||
fail(f"staged info {staged_info.raw.hex()} != resident info {resident_info.hex()}")
|
|
||||||
|
|
||||||
# 'R' from the staged copy already proved itself in the install
|
|
||||||
# verify; 'F' must answer 4 bytes (values are unmodeled in simavr).
|
|
||||||
if len(loader.read_fuses()) != 4:
|
|
||||||
fail("fuse read from the staged copy")
|
|
||||||
|
|
||||||
# EEPROM round-trip through the staged copy.
|
|
||||||
pattern = bytes(range(0x50, 0x60))
|
|
||||||
loader.write_eeprom(0, pattern)
|
|
||||||
if loader.read_eeprom(0, len(pattern)) != pattern:
|
|
||||||
fail("EEPROM round-trip through the staged copy")
|
|
||||||
|
|
||||||
# The guard, both ways: its own slot refused (drained, unchanged), the
|
|
||||||
# resident slot writable. The refusal leaves its drained words in the
|
|
||||||
# SPM buffer, so the write that follows may take them — and clears
|
|
||||||
# them by writing, so the retry must not.
|
|
||||||
before = loader.read_flash(stage, page)
|
|
||||||
loader.write_page(stage, bytes(page))
|
|
||||||
if loader.read_flash(stage, page) != before:
|
|
||||||
fail("the staged copy's guard let its own slot change")
|
|
||||||
marker = bytes((i * 3) & 0xFF for i in range(page))
|
|
||||||
loader.write_page(base, marker)
|
|
||||||
if loader.read_flash(base, page) != marker:
|
|
||||||
loader.write_page(base, marker)
|
|
||||||
if loader.read_flash(base, page) != marker:
|
|
||||||
fail("the staged copy could not write the resident slot, even on retry")
|
|
||||||
|
|
||||||
# Restore the resident image through the staged copy, then 'J' back
|
|
||||||
# into it and prove it lives.
|
|
||||||
resident = image + b"\xff" * (info.slot - len(image))
|
|
||||||
pb.write_differing(loader, base, resident)
|
|
||||||
back_info = loader.enter_copy(base, 25)
|
|
||||||
if back_info.raw != resident_info:
|
|
||||||
fail("the restored resident does not serve its info block")
|
|
||||||
port.close()
|
|
||||||
finally:
|
|
||||||
device.stop()
|
|
||||||
print("pbreloc: the relocated copy serves the full command set")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
"""Shared simavr harness for the pureboot tests: spawn the device runner,
|
|
||||||
hand out its pty, restart it from a flash dump (the power-fail path), and
|
|
||||||
keep its chatter out of undrained pipes."""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import signal
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
|
|
||||||
class Device:
|
|
||||||
def __init__(self, binary, elf, mcu, hz, base_hex, page, baud, dump, reset_hex=None, resume=None, link=None):
|
|
||||||
cmd = [binary]
|
|
||||||
if link:
|
|
||||||
cmd += ["-l", link]
|
|
||||||
cmd += [elf, mcu, hz, base_hex, str(page), str(baud), dump]
|
|
||||||
if reset_hex is not None or resume is not None:
|
|
||||||
# Chips without a hardware boot section — the tinies and the
|
|
||||||
# m48s — reset to address 0 like silicon; the boot-sectioned
|
|
||||||
# megas re-vector to the loader base (BOOTRST).
|
|
||||||
patch = not mcu.startswith("atmega") or mcu.startswith("atmega48")
|
|
||||||
cmd.append(reset_hex if reset_hex is not None else ("0" if patch else base_hex))
|
|
||||||
if resume is not None:
|
|
||||||
cmd.append(resume)
|
|
||||||
self.log = open(dump + ".log", "a")
|
|
||||||
self.proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=self.log, text=True)
|
|
||||||
self.dump = dump
|
|
||||||
self.pty = None
|
|
||||||
for _ in range(50):
|
|
||||||
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 reset(self):
|
|
||||||
"""The external reset line: SIGUSR1 re-enters at the reset vector."""
|
|
||||||
self.proc.send_signal(signal.SIGUSR1)
|
|
||||||
|
|
||||||
def power_fail(self):
|
|
||||||
"""SIGTERM: the runner dumps its flash and exits — the image a
|
|
||||||
restart resumes from."""
|
|
||||||
self.stop()
|
|
||||||
return self.dump
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
self.proc.terminate()
|
|
||||||
try:
|
|
||||||
self.proc.wait(timeout=5)
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
self.proc.kill()
|
|
||||||
self.log.close()
|
|
||||||
|
|
||||||
|
|
||||||
def run_tool(tool, pty, baud, *args, timeout=180):
|
|
||||||
result = subprocess.run(
|
|
||||||
[os.environ.get("PYTHON", "python3"), tool, "--port", pty, "--baud", str(baud), "--wait", "25", *args],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=timeout,
|
|
||||||
)
|
|
||||||
print(result.stdout, end="")
|
|
||||||
if result.returncode != 0:
|
|
||||||
raise RuntimeError(f"tool exited {result.returncode}: {result.stderr.strip()}")
|
|
||||||
return result.stdout
|
|
||||||
144
test/pbtest.py
144
test/pbtest.py
@@ -1,144 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""End-to-end pureboot protocol test: spawn the simavr device, then drive it
|
|
||||||
with the real host tool (pureboot.py, as a subprocess over the device's pty)
|
|
||||||
through flash + EEPROM + fuse + hand-over scenarios, and cross-check
|
|
||||||
the tool's view against the simulator's ground-truth memory dumps.
|
|
||||||
|
|
||||||
Usage: pbtest.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
|
|
||||||
<baud> <eeprom_size> <app_bin> <tool_py> <workdir> [link]
|
|
||||||
|
|
||||||
The optional link is the runner's -l spec (usart1, sw:B5,B1, ...) for a
|
|
||||||
loader built off the chip's natural serial default.
|
|
||||||
Exits 0 if every scenario passes.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def fail(message):
|
|
||||||
print(f"FAIL: {message}")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def rjmp_decode(word, at, flash_words):
|
|
||||||
"""Where an rjmp word at word-address `at` lands — deliberately written
|
|
||||||
against the instruction-set definition (12-bit signed offset), not with
|
|
||||||
the host tool's encoder, so an encoding bug cannot verify itself."""
|
|
||||||
if word & 0xF000 != 0xC000:
|
|
||||||
fail(f"word at {at * 2:#06x} is {word:#06x}, not an rjmp")
|
|
||||||
offset = word & 0x0FFF
|
|
||||||
if offset >= 0x800:
|
|
||||||
offset -= 0x1000
|
|
||||||
return (at + 1 + offset) % flash_words
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
args = sys.argv[1:]
|
|
||||||
link = args.pop() if len(args) == 12 else None
|
|
||||||
(device_bin, elf, mcu, hz, base_hex, page, baud, eeprom_size, app_bin, tool, workdir) = args
|
|
||||||
base, page, baud, eeprom_size = int(base_hex, 0), int(page), int(baud), int(eeprom_size)
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
import pbsim
|
|
||||||
import pureboot as pb
|
|
||||||
|
|
||||||
os.makedirs(workdir, exist_ok=True)
|
|
||||||
ee_image = bytes(range(0xA0, 0xB0))
|
|
||||||
ee_path = os.path.join(workdir, "ee.bin")
|
|
||||||
open(ee_path, "wb").write(ee_image)
|
|
||||||
dump = os.path.join(workdir, "flash_dump.bin")
|
|
||||||
read_flash = os.path.join(workdir, "readback_flash.bin")
|
|
||||||
read_eeprom = os.path.join(workdir, "readback_eeprom.bin")
|
|
||||||
|
|
||||||
# The geometry the host will discover, for computing the expected image:
|
|
||||||
# the boot-sectioned megas need no vector surgery (the tinies and the
|
|
||||||
# boot-section-less m48s do), the large chips speak word addresses, and
|
|
||||||
# the page byte is the wire's 0-means-256.
|
|
||||||
mega = mcu.startswith("atmega")
|
|
||||||
patch = not mega or mcu.startswith("atmega48")
|
|
||||||
word_flash = base + 512 > 0x10000
|
|
||||||
wire_base = base // 2 if word_flash else base
|
|
||||||
flags = (1 if patch else 0) | (2 if word_flash else 0)
|
|
||||||
info = pb.Info(
|
|
||||||
bytes([ord("P"), ord("B"), 1, 0, 0, 0, page & 0xFF])
|
|
||||||
+ bytes([wire_base & 0xFF, wire_base >> 8, eeprom_size & 0xFF, eeprom_size >> 8])
|
|
||||||
+ bytes([flags])
|
|
||||||
)
|
|
||||||
|
|
||||||
device = pbsim.Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump, link=link)
|
|
||||||
try:
|
|
||||||
# Session 1: knock from reset, identify, program everything, stay.
|
|
||||||
out = pbsim.run_tool(tool, device.pty, baud, "--info", "--fuses", "--flash", app_bin,
|
|
||||||
"--eeprom", ee_path, "--stay")
|
|
||||||
for needed in ("signature", "fuses", "verify:", "stays"):
|
|
||||||
if needed not in out:
|
|
||||||
fail(f"session 1 output lacks {needed!r}")
|
|
||||||
|
|
||||||
# Session 2: reconnect into the live session, verify, dump, hand over
|
|
||||||
# is deferred — the pty must be reopened for the APP banner first.
|
|
||||||
out = pbsim.run_tool(tool, device.pty, baud, "--verify-flash", app_bin, "--verify-eeprom", ee_path,
|
|
||||||
"--read-flash", read_flash, "--read-eeprom", read_eeprom, "--stay")
|
|
||||||
if out.count("verify:") != 2:
|
|
||||||
fail("session 2 did not verify both memories")
|
|
||||||
|
|
||||||
eeprom_back = open(read_eeprom, "rb").read()
|
|
||||||
if eeprom_back[: len(ee_image)] != ee_image:
|
|
||||||
fail("EEPROM read-back mismatch")
|
|
||||||
|
|
||||||
# The expected post-surgery flash, straight from the tool's planner.
|
|
||||||
pages = pb.plan_flash(open(app_bin, "rb").read(), info)
|
|
||||||
flash_back = open(read_flash, "rb").read()
|
|
||||||
for address, data in pages.items():
|
|
||||||
if flash_back[address : address + page] != data:
|
|
||||||
fail(f"flash read-back mismatch in page {address:#06x}")
|
|
||||||
|
|
||||||
# An external reset re-enters through the patched word 0 (tinies; the
|
|
||||||
# runner resets them to address 0 like silicon) or BOOTRST (mega).
|
|
||||||
# The loader must answer a fresh knock, and the 'J' hand-over must
|
|
||||||
# land in the application, which banners on the same link.
|
|
||||||
device.reset()
|
|
||||||
port = pb.Port(device.pty, baud)
|
|
||||||
try:
|
|
||||||
loader = pb.Loader(port)
|
|
||||||
loader.connect(15)
|
|
||||||
loader.run_application()
|
|
||||||
banner = port.read_exact(3, 5.0)
|
|
||||||
if banner != b"APP":
|
|
||||||
fail(f"application banner was {banner!r}")
|
|
||||||
finally:
|
|
||||||
port.close()
|
|
||||||
finally:
|
|
||||||
device.stop()
|
|
||||||
|
|
||||||
# Ground truth: the simulator's own memories, against the host's view.
|
|
||||||
flash_true = open(dump, "rb").read()
|
|
||||||
if flash_true[:base] != flash_back:
|
|
||||||
fail("host flash read-back differs from the simulator's flash")
|
|
||||||
if flash_true[base] == 0xFF and flash_true[base + 1] == 0xFF:
|
|
||||||
fail("loader region looks erased in the ground-truth dump")
|
|
||||||
|
|
||||||
# The surgery, decoded independently: the patched vector must land on the
|
|
||||||
# loader, the trampoline on the application's own entry (patched-vector
|
|
||||||
# chips only — a boot-sectioned mega's word 0 stays the application's).
|
|
||||||
if patch:
|
|
||||||
flash_words = (base + 512) // 2
|
|
||||||
app = open(app_bin, "rb").read()
|
|
||||||
word0 = flash_true[0] | (flash_true[1] << 8)
|
|
||||||
if rjmp_decode(word0, 0, flash_words) != base // 2:
|
|
||||||
fail("patched reset vector does not land on the loader base")
|
|
||||||
trampoline = flash_true[base - 2] | (flash_true[base - 1] << 8)
|
|
||||||
original = app[0] | (app[1] << 8)
|
|
||||||
if rjmp_decode(trampoline, (base - 2) // 2, flash_words) != rjmp_decode(original, 0, flash_words):
|
|
||||||
fail("trampoline does not land on the application's own entry")
|
|
||||||
ee_true_path = dump + ".eeprom"
|
|
||||||
if os.path.exists(ee_true_path):
|
|
||||||
ee_true = open(ee_true_path, "rb").read()
|
|
||||||
if ee_true[: len(ee_image)] != ee_image:
|
|
||||||
fail("ground-truth EEPROM does not match what was programmed")
|
|
||||||
|
|
||||||
print("pbtest: all scenarios pass")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
230
test/pbupdate.py
230
test/pbupdate.py
@@ -1,230 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Self-update end-to-end: an application is flashed, then the loader
|
|
||||||
replaces itself with a re-timed build through the host tool's
|
|
||||||
--update-loader — and the power-fail phases of that update are rehearsed by
|
|
||||||
killing the simulated device mid-write, restarting it from its flash dump,
|
|
||||||
and letting a re-run complete the update.
|
|
||||||
|
|
||||||
The boot-sectioned megas run the BOOTRST-unprogrammed profile (reset boots
|
|
||||||
the application; the fixture application's 'L' jump is the application-owned
|
|
||||||
loader entry), with --assume-fuses standing in for the fuse read simavr
|
|
||||||
cannot model. The patched-vector chips — the tinies and the m48s — reset
|
|
||||||
into a loader at every phase by construction: the t13a because its staging
|
|
||||||
slot carries the reset vector itself, the others through the word-0 redirect
|
|
||||||
the tool plants around the resident rewrite.
|
|
||||||
|
|
||||||
Usage: pbupdate.py <device_bin> <pureboot_elf> <update_elf> <mcu> <hz>
|
|
||||||
<base_hex> <page> <baud> <app_bin> <tool_py> <workdir>
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def fail(message):
|
|
||||||
print(f"FAIL: {message}")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def rjmp_decode(word, at, flash_words):
|
|
||||||
"""Written against the instruction-set definition, not with the tool's
|
|
||||||
encoder, so an encoding bug cannot verify itself."""
|
|
||||||
if word & 0xF000 != 0xC000:
|
|
||||||
fail(f"word at {at * 2:#06x} is {word:#06x}, not an rjmp")
|
|
||||||
offset = word & 0x0FFF
|
|
||||||
if offset >= 0x800:
|
|
||||||
offset -= 0x1000
|
|
||||||
return (at + 1 + offset) % flash_words
|
|
||||||
|
|
||||||
|
|
||||||
class PowerFail(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def assumed_fuses(pb, image):
|
|
||||||
"""Synthetic 'F' bytes for --assume-fuses: the smallest boot section
|
|
||||||
covering both the resident and the staging slot (two slots — what a
|
|
||||||
self-update needs), BOOTRST unprogrammed — the per-chip BOOTSZ ladder
|
|
||||||
and fuse byte come from the tool's own table, keyed by the update
|
|
||||||
image's embedded signature."""
|
|
||||||
info = pb.image_info(image)
|
|
||||||
which, ladder = pb.BOOT_FUSE[bytes(info.signature[1:3])]
|
|
||||||
bits = min((b for b in ladder if ladder[b] * 2 >= 2 * info.slot), key=lambda b: ladder[b])
|
|
||||||
fuses = bytearray((0xFF, 0xFF, 0xFF, 0xFF))
|
|
||||||
fuses[which] = 0xF8 | (bits << 1) | 1
|
|
||||||
return bytes(fuses)
|
|
||||||
|
|
||||||
|
|
||||||
def make_fault_loader(pb, base, slot, kill_region, kill_hits, device):
|
|
||||||
"""A Loader whose write_page kills the device (or, with device=None,
|
|
||||||
just the host) at the Nth write into a region; the sequence
|
|
||||||
stage->resident->stage distinguishes the install from the restore."""
|
|
||||||
|
|
||||||
class FaultLoader(pb.Loader):
|
|
||||||
def __init__(self, port):
|
|
||||||
super().__init__(port)
|
|
||||||
self.seen_resident = False
|
|
||||||
self.hits = 0
|
|
||||||
|
|
||||||
def write_page(self, address, data):
|
|
||||||
if address >= base:
|
|
||||||
phase = "resident"
|
|
||||||
self.seen_resident = True
|
|
||||||
elif address >= base - slot:
|
|
||||||
phase = "stage_restore" if self.seen_resident else "stage"
|
|
||||||
else:
|
|
||||||
phase = "app"
|
|
||||||
if phase == kill_region:
|
|
||||||
self.hits += 1
|
|
||||||
if self.hits == kill_hits:
|
|
||||||
if device is not None:
|
|
||||||
device.power_fail()
|
|
||||||
raise PowerFail(f"{kill_region} write {kill_hits}")
|
|
||||||
super().write_page(address, data)
|
|
||||||
|
|
||||||
return FaultLoader
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
(device_bin, elf, update_elf, mcu, hz, base_hex, page, baud, app_bin, tool, workdir) = sys.argv[1:]
|
|
||||||
base, page, baud = int(base_hex, 0), int(page), int(baud)
|
|
||||||
mega = mcu.startswith("atmega")
|
|
||||||
# The m48s are megas without a boot section: patched vector, no fuse
|
|
||||||
# preflight, and the same reset-to-0 the tinies get.
|
|
||||||
patch = not mega or mcu.startswith("atmega48")
|
|
||||||
# Word-addressed (>64 KiB) chips use the 1 KiB slot; their loader base
|
|
||||||
# itself sits beyond the 16-bit byte space — the 644's base + slot only
|
|
||||||
# touches the 64 KiB boundary and stays byte-addressed.
|
|
||||||
slot = 1024 if base >= 0x10000 and mega else 512
|
|
||||||
reset_hex = "0" if mega else None # the boot-sectioned mega runs BOOTRST-unprogrammed here
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
import pbsim
|
|
||||||
import pureboot as pb
|
|
||||||
|
|
||||||
os.makedirs(workdir, exist_ok=True)
|
|
||||||
objcopy = os.environ.get("PB_OBJCOPY", "avr-objcopy")
|
|
||||||
images = {}
|
|
||||||
for name, source in (("v0", elf), ("v9", update_elf)):
|
|
||||||
path = os.path.join(workdir, name + ".bin")
|
|
||||||
subprocess.run([objcopy, "-O", "binary", source, path], check=True)
|
|
||||||
images[name] = open(path, "rb").read()
|
|
||||||
if images["v0"] == images["v9"]:
|
|
||||||
fail("the update image is byte-identical to the resident build")
|
|
||||||
dump = os.path.join(workdir, "dump.bin")
|
|
||||||
state = os.path.join(workdir, "update.pbstate")
|
|
||||||
fuses = assumed_fuses(pb, images["v0"]) if mega and not patch else None
|
|
||||||
|
|
||||||
def connect(device):
|
|
||||||
port = pb.Port(device.pty, baud)
|
|
||||||
if mega:
|
|
||||||
# Reset boots the application here; its 'L' is the loader entry.
|
|
||||||
# To a live loader the same byte is an ignored command.
|
|
||||||
port.read_available(0.5)
|
|
||||||
port.write(b"L")
|
|
||||||
loader = pb.Loader(port)
|
|
||||||
loader.connect(25)
|
|
||||||
return port, loader
|
|
||||||
|
|
||||||
def padded(image):
|
|
||||||
return image + b"\xff" * (slot - len(image))
|
|
||||||
|
|
||||||
def resident_bytes(loader):
|
|
||||||
return loader.read_flash(base, slot)
|
|
||||||
|
|
||||||
def assert_state(loader, image, app_pages):
|
|
||||||
if resident_bytes(loader) != padded(image):
|
|
||||||
fail("resident loader does not match the update image")
|
|
||||||
stage = base - slot
|
|
||||||
got = loader.read_flash(stage, slot)
|
|
||||||
for address, data in app_pages.items():
|
|
||||||
if stage <= address < base:
|
|
||||||
if got[address - stage : address - stage + page] != data:
|
|
||||||
fail(f"staging region page {address:#06x} not restored")
|
|
||||||
|
|
||||||
device = pbsim.Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump, reset_hex=reset_hex)
|
|
||||||
final = "v0"
|
|
||||||
try:
|
|
||||||
# The application first — its planner output is the restore truth.
|
|
||||||
pbsim.run_tool(tool, device.pty, baud, "--flash", app_bin, "--stay")
|
|
||||||
port, loader = connect(device)
|
|
||||||
app_pages = pb.plan_flash(open(app_bin, "rb").read(), loader.info)
|
|
||||||
port.close()
|
|
||||||
|
|
||||||
# A clean CLI update, resident -> v9.
|
|
||||||
args = ["--update-loader", os.path.join(workdir, "v9.bin"), "--state", state, "--stay"]
|
|
||||||
if fuses:
|
|
||||||
args += ["--assume-fuses", fuses.hex()]
|
|
||||||
out = pbsim.run_tool(tool, device.pty, baud, *args)
|
|
||||||
if "loader updated" not in out:
|
|
||||||
fail("update did not report success")
|
|
||||||
if os.path.exists(state):
|
|
||||||
fail("state file survived a completed update")
|
|
||||||
port, loader = connect(device)
|
|
||||||
assert_state(loader, images["v9"], app_pages)
|
|
||||||
loader.run_application()
|
|
||||||
if port.read_exact(3, 5.0) != b"APP":
|
|
||||||
fail("application does not banner after the update")
|
|
||||||
port.close()
|
|
||||||
final = "v9"
|
|
||||||
print("clean update: resident replaced, staging restored, application intact")
|
|
||||||
|
|
||||||
# Power-fail rehearsal: kill mid-phase, restart from the dump,
|
|
||||||
# re-run, and the update must still complete. Each round flips the
|
|
||||||
# direction so the flash is never already at its target. The mega's
|
|
||||||
# mid-resident-rewrite loss is exercised as a host crash instead:
|
|
||||||
# with BOOTRST unprogrammed and the resident mid-erase, a power loss
|
|
||||||
# there has no reset path into the staging copy — the documented
|
|
||||||
# cost of that profile (README).
|
|
||||||
for kill_region, kill_hits, kill_device in (
|
|
||||||
("stage", 2, True),
|
|
||||||
("resident", 1, patch),
|
|
||||||
("stage_restore", 2, True),
|
|
||||||
):
|
|
||||||
device.reset() # the previous round left the application running
|
|
||||||
port, loader = connect(device)
|
|
||||||
target = "v9" if resident_bytes(loader) == padded(images["v0"]) else "v0"
|
|
||||||
image_path = os.path.join(workdir, target + ".bin")
|
|
||||||
injected = make_fault_loader(pb, base, slot, kill_region, kill_hits, device if kill_device else None)(port)
|
|
||||||
injected.info = loader.info
|
|
||||||
try:
|
|
||||||
pb.op_update_loader(injected, 25, image_path, state, fuses)
|
|
||||||
fail(f"{kill_region}: fault never triggered")
|
|
||||||
except PowerFail as event:
|
|
||||||
print(f"power fail injected: {event}")
|
|
||||||
port.close()
|
|
||||||
if kill_device:
|
|
||||||
device = pbsim.Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump,
|
|
||||||
reset_hex=reset_hex, resume=dump)
|
|
||||||
port, loader = connect(device)
|
|
||||||
pb.op_update_loader(loader, 25, image_path, state, fuses)
|
|
||||||
assert_state(loader, images[target], app_pages)
|
|
||||||
loader.run_application()
|
|
||||||
if port.read_exact(3, 5.0) != b"APP":
|
|
||||||
fail(f"{kill_region}: application lost after the resumed update")
|
|
||||||
port.close()
|
|
||||||
final = target
|
|
||||||
print(f"resumed after {kill_region} loss: update completed, application intact")
|
|
||||||
finally:
|
|
||||||
device.stop()
|
|
||||||
|
|
||||||
# Ground truth: the simulator's own flash against the final state, and
|
|
||||||
# on the patched-vector chips an independent decode of the reset routing.
|
|
||||||
flash = open(dump, "rb").read()
|
|
||||||
if flash[base : base + slot] != padded(images[final]):
|
|
||||||
fail("ground-truth resident region does not match the final image")
|
|
||||||
if patch:
|
|
||||||
flash_words = (base + slot) // 2
|
|
||||||
word0 = flash[0] | (flash[1] << 8)
|
|
||||||
if rjmp_decode(word0, 0, flash_words) != base // 2:
|
|
||||||
fail("ground-truth reset vector does not land on the loader")
|
|
||||||
app = open(app_bin, "rb").read()
|
|
||||||
trampoline = flash[base - 2] | (flash[base - 1] << 8)
|
|
||||||
if rjmp_decode(trampoline, (base - 2) // 2, flash_words) != rjmp_decode(app[0] | (app[1] << 8), 0, flash_words):
|
|
||||||
fail("ground-truth trampoline does not land on the application entry")
|
|
||||||
print("pbupdate: clean update + all power-fail phases recovered")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,461 +0,0 @@
|
|||||||
// simavr "device" for the pureboot protocol tests, every chip. Loads the
|
|
||||||
// boot-linked ELF at the loader base, starts execution there (BOOTRST / the
|
|
||||||
// patched vector are not what is under test), and exposes the loader's
|
|
||||||
// serial link as a pty for the real host tool:
|
|
||||||
//
|
|
||||||
// - Hardware USART builds: simavr's uart_pty on the selected instance.
|
|
||||||
// - Software UART builds: an 8N1 bridge between a pty and the GPIO pins,
|
|
||||||
// timed against the simulated cycle counter (drives the loader's RX,
|
|
||||||
// decodes its TX).
|
|
||||||
//
|
|
||||||
// The link follows the chip's natural default (USART0 on the megas, the
|
|
||||||
// software UART on PB0/PB1 elsewhere) unless -l overrides it: `-l usart1`
|
|
||||||
// for the second instance, `-l sw:B5,B1` for a software build's RX,TX pins.
|
|
||||||
//
|
|
||||||
// simavr's tiny cores decode the SPM opcode but attach no NVM module — SPM
|
|
||||||
// is a silent no-op (the mega's boot section has one, avr_flash). The
|
|
||||||
// missing module is supplied here: the SPM ioctl reads SPMCSR/Z/r1:r0 and
|
|
||||||
// implements buffer fill, page erase, page write, and CTPB, completing
|
|
||||||
// instantly. RFLB's LPM diversion (fuse readout) stays unmodeled, so the
|
|
||||||
// 'F' command answers with flash bytes — the tests assert transport only.
|
|
||||||
//
|
|
||||||
// On exit (or SIGTERM) the flash and EEPROM are dumped to files for a
|
|
||||||
// ground-truth cross-check against what the host read back.
|
|
||||||
#include <fcntl.h>
|
|
||||||
#include <pty.h>
|
|
||||||
#include <signal.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include <termios.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
|
|
||||||
#include "avr_eeprom.h"
|
|
||||||
#include "avr_flash.h"
|
|
||||||
#include "avr_ioport.h"
|
|
||||||
#include "avr_uart.h"
|
|
||||||
#include "sim_avr.h"
|
|
||||||
#include "sim_elf.h"
|
|
||||||
#include "sim_io.h"
|
|
||||||
#include "uart_pty.h"
|
|
||||||
|
|
||||||
static avr_t *avr;
|
|
||||||
static uart_pty_t uart_pty;
|
|
||||||
static int link_software;
|
|
||||||
static char uart_digit = '0';
|
|
||||||
static char sw_rx_port = 'B', sw_tx_port = 'B';
|
|
||||||
static int sw_rx_bit = 0, sw_tx_bit = 1;
|
|
||||||
static const char *dump_path;
|
|
||||||
static uint32_t reset_pc;
|
|
||||||
static volatile sig_atomic_t reset_requested;
|
|
||||||
|
|
||||||
static int parse_link(const char *spec)
|
|
||||||
{
|
|
||||||
if (strcmp(spec, "usart0") == 0 || strcmp(spec, "usart1") == 0) {
|
|
||||||
link_software = 0;
|
|
||||||
uart_digit = spec[5];
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (strncmp(spec, "sw", 2) == 0) {
|
|
||||||
link_software = 1;
|
|
||||||
if (spec[2] == '\0')
|
|
||||||
return 0;
|
|
||||||
if (sscanf(spec + 2, ":%c%d,%c%d", &sw_rx_port, &sw_rx_bit, &sw_tx_port, &sw_tx_bit) == 4)
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// simavr 1.6's avr_flash PGERS handler erases spm_pagesize bytes starting at
|
|
||||||
// Z & ~1 instead of the page containing Z (its PGWRT path masks correctly) —
|
|
||||||
// hardware ignores the in-page bits (§26.8.1), so an erase issued with Z
|
|
||||||
// anywhere inside the page wipes half the neighbouring page in simulation
|
|
||||||
// only. Wrap the mega's registered flash ioctl and re-dispatch page erases
|
|
||||||
// with Z forced to the page boundary; everything else passes through.
|
|
||||||
//
|
|
||||||
// A second gap on the boot-section-less m48s: their RWWSRE bit is the
|
|
||||||
// temporary-buffer discard (Atmel-8271 §26.2/§26.3.1), but the stock model
|
|
||||||
// gates its RWWSRE branch on AVR_SELFPROG_HAVE_RWW — absent on the m48
|
|
||||||
// core — so the discard store falls through into the buffer-fill branch and
|
|
||||||
// plants whatever Z/R1:R0 happen to hold. Perform the silicon's discard
|
|
||||||
// here instead.
|
|
||||||
static avr_flash_t *mega_flash;
|
|
||||||
static int (*mega_flash_ioctl)(avr_io_t *io, uint32_t ctl, void *param);
|
|
||||||
|
|
||||||
static int fixed_flash_ioctl(avr_io_t *io, uint32_t ctl, void *param)
|
|
||||||
{
|
|
||||||
if (ctl == AVR_IOCTL_FLASH_SPM && avr_regbit_get(io->avr, mega_flash->pgers)) {
|
|
||||||
uint16_t z = (uint16_t)(io->avr->data[30] | (io->avr->data[31] << 8));
|
|
||||||
uint16_t masked = (uint16_t)(z & ~(mega_flash->spm_pagesize - 1));
|
|
||||||
io->avr->data[30] = (uint8_t)masked;
|
|
||||||
io->avr->data[31] = (uint8_t)(masked >> 8);
|
|
||||||
int result = mega_flash_ioctl(io, ctl, param);
|
|
||||||
io->avr->data[30] = (uint8_t)z;
|
|
||||||
io->avr->data[31] = (uint8_t)(z >> 8);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
if (ctl == AVR_IOCTL_FLASH_SPM && !(mega_flash->flags & AVR_SELFPROG_HAVE_RWW) &&
|
|
||||||
(io->avr->data[mega_flash->r_spm] & 0x11) == 0x11) { // RWWSRE|SELFPRGEN: the m48 buffer discard
|
|
||||||
for (int i = 0; i < mega_flash->spm_pagesize / 2; i++) {
|
|
||||||
mega_flash->tmppage[i] = 0xffff;
|
|
||||||
mega_flash->tmppage_used[i] = 0;
|
|
||||||
}
|
|
||||||
avr_regbit_clear(io->avr, mega_flash->selfprgen);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return mega_flash_ioctl(io, ctl, param);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void fix_mega_flash_erase(void)
|
|
||||||
{
|
|
||||||
for (avr_io_t *io = avr->io_port; io; io = io->next) {
|
|
||||||
if (io->kind && strcmp(io->kind, "flash") == 0) {
|
|
||||||
mega_flash = (avr_flash_t *)io;
|
|
||||||
mega_flash_ioctl = io->ioctl;
|
|
||||||
io->ioctl = fixed_flash_ioctl;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fprintf(stderr, "device: no flash module to fix — SPM page erases may misalign\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
static void request_reset(int sig)
|
|
||||||
{
|
|
||||||
(void)sig;
|
|
||||||
reset_requested = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------- tiny NVM ---
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
avr_io_t io;
|
|
||||||
uint8_t buffer[128];
|
|
||||||
uint8_t used[128]; // a buffer word loads once until erased — like silicon
|
|
||||||
unsigned page;
|
|
||||||
} tiny_nvm_t;
|
|
||||||
|
|
||||||
static tiny_nvm_t nvm;
|
|
||||||
|
|
||||||
static int nvm_ioctl(avr_io_t *io, uint32_t ctl, void *param)
|
|
||||||
{
|
|
||||||
(void)param;
|
|
||||||
if (ctl != AVR_IOCTL_FLASH_SPM)
|
|
||||||
return -1;
|
|
||||||
tiny_nvm_t *n = (tiny_nvm_t *)io;
|
|
||||||
avr_t *mcu = io->avr;
|
|
||||||
uint8_t command = mcu->data[0x57] & 0x1f; // SPMCSR, both tinies
|
|
||||||
uint16_t z = (uint16_t)(mcu->data[30] | (mcu->data[31] << 8));
|
|
||||||
uint32_t page_base = (uint32_t)(z & ~(n->page - 1)) % (mcu->flashend + 1);
|
|
||||||
if (command == 0x01) { // SPMEN alone: buffer fill from r1:r0
|
|
||||||
unsigned offset = z & (n->page - 1) & ~1u;
|
|
||||||
if (!n->used[offset]) { // first write wins until the buffer clears
|
|
||||||
n->buffer[offset] = mcu->data[0];
|
|
||||||
n->buffer[offset + 1] = mcu->data[1];
|
|
||||||
n->used[offset] = 1;
|
|
||||||
}
|
|
||||||
} else if (command == 0x03) { // PGERS
|
|
||||||
memset(mcu->flash + page_base, 0xff, n->page);
|
|
||||||
} else if (command == 0x05) { // PGWRT: programming only clears bits
|
|
||||||
for (unsigned i = 0; i < n->page; i++)
|
|
||||||
mcu->flash[page_base + i] &= n->buffer[i];
|
|
||||||
memset(n->buffer, 0xff, n->page);
|
|
||||||
memset(n->used, 0, n->page);
|
|
||||||
} else if (command == 0x11) { // CTPB
|
|
||||||
memset(n->buffer, 0xff, n->page);
|
|
||||||
memset(n->used, 0, n->page);
|
|
||||||
}
|
|
||||||
mcu->data[0x57] &= (uint8_t)~0x1f; // the operation completes instantly
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------- GPIO bridge ---
|
|
||||||
|
|
||||||
static int pty_master = -1;
|
|
||||||
static avr_irq_t *rx_pin; // the loader's RX (PB0), driven from the pty
|
|
||||||
static avr_cycle_count_t bit_cycles;
|
|
||||||
|
|
||||||
static int tx_level = 1, tx_active, tx_bit;
|
|
||||||
static uint8_t tx_shift;
|
|
||||||
|
|
||||||
static avr_cycle_count_t tx_sample(avr_t *mcu, avr_cycle_count_t when, void *param)
|
|
||||||
{
|
|
||||||
(void)mcu;
|
|
||||||
(void)param;
|
|
||||||
tx_shift = (uint8_t)((tx_shift >> 1) | (tx_level ? 0x80 : 0));
|
|
||||||
if (++tx_bit < 8)
|
|
||||||
return when + bit_cycles;
|
|
||||||
if (write(pty_master, &tx_shift, 1) != 1)
|
|
||||||
fprintf(stderr, "device: pty write lost a byte\n");
|
|
||||||
tx_active = 0;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void tx_hook(avr_irq_t *irq, uint32_t value, void *param)
|
|
||||||
{
|
|
||||||
(void)irq;
|
|
||||||
(void)param;
|
|
||||||
int level = value & 1;
|
|
||||||
if (!tx_active && tx_level == 1 && level == 0) { // start edge
|
|
||||||
tx_active = 1;
|
|
||||||
tx_bit = 0;
|
|
||||||
avr_cycle_timer_register(avr, bit_cycles + bit_cycles / 2, tx_sample, NULL);
|
|
||||||
}
|
|
||||||
tx_level = level;
|
|
||||||
}
|
|
||||||
|
|
||||||
static uint8_t rx_queue[8192];
|
|
||||||
static unsigned rx_head, rx_tail; // ring: head = next to send
|
|
||||||
static int rx_active, rx_bit;
|
|
||||||
static uint8_t rx_byte;
|
|
||||||
|
|
||||||
static void rx_start_next(void);
|
|
||||||
|
|
||||||
static avr_cycle_count_t rx_step(avr_t *mcu, avr_cycle_count_t when, void *param)
|
|
||||||
{
|
|
||||||
(void)mcu;
|
|
||||||
(void)param;
|
|
||||||
if (rx_bit < 8) {
|
|
||||||
avr_raise_irq(rx_pin, (rx_byte >> rx_bit) & 1);
|
|
||||||
rx_bit++;
|
|
||||||
return when + bit_cycles;
|
|
||||||
}
|
|
||||||
if (rx_bit == 8) { // stop bit, plus one idle bit of margin
|
|
||||||
avr_raise_irq(rx_pin, 1);
|
|
||||||
rx_bit++;
|
|
||||||
return when + 2 * bit_cycles;
|
|
||||||
}
|
|
||||||
rx_active = 0;
|
|
||||||
rx_start_next();
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void rx_start_next(void)
|
|
||||||
{
|
|
||||||
if (rx_active || rx_head == rx_tail)
|
|
||||||
return;
|
|
||||||
rx_byte = rx_queue[rx_head];
|
|
||||||
rx_head = (rx_head + 1) % sizeof(rx_queue);
|
|
||||||
rx_active = 1;
|
|
||||||
rx_bit = 0;
|
|
||||||
avr_raise_irq(rx_pin, 0); // start bit
|
|
||||||
avr_cycle_timer_register(avr, bit_cycles, rx_step, NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
// A reset abandons whatever the bridge was mid-transfer: bytes still queued
|
|
||||||
// for a chip that no longer has the context to receive them meaningfully,
|
|
||||||
// and a decode in progress on a TX line the reset may have already changed.
|
|
||||||
// The pending cycle timers must go with the state: avr_reset drops the TX
|
|
||||||
// output latch, whose falling edge starts a spurious decode before this
|
|
||||||
// runs, and a stale tx_sample would then interleave with the loader's first
|
|
||||||
// real answer through the shared shift state, corrupting it.
|
|
||||||
static void bridge_reset(void)
|
|
||||||
{
|
|
||||||
avr_cycle_timer_cancel(avr, tx_sample, NULL);
|
|
||||||
avr_cycle_timer_cancel(avr, rx_step, NULL);
|
|
||||||
rx_head = rx_tail = 0;
|
|
||||||
rx_active = 0;
|
|
||||||
tx_active = 0;
|
|
||||||
tx_level = 1;
|
|
||||||
avr_raise_irq(rx_pin, 1); // idle line
|
|
||||||
}
|
|
||||||
|
|
||||||
static void poll_pty(void)
|
|
||||||
{
|
|
||||||
uint8_t chunk[256];
|
|
||||||
ssize_t got = read(pty_master, chunk, sizeof(chunk));
|
|
||||||
for (ssize_t i = 0; i < got; i++) {
|
|
||||||
unsigned next = (rx_tail + 1) % sizeof(rx_queue);
|
|
||||||
if (next == rx_head)
|
|
||||||
break; // full: the host will retry on timeout
|
|
||||||
rx_queue[rx_tail] = chunk[i];
|
|
||||||
rx_tail = next;
|
|
||||||
}
|
|
||||||
if (got > 0)
|
|
||||||
rx_start_next();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------ main ---
|
|
||||||
|
|
||||||
static void finish(int sig)
|
|
||||||
{
|
|
||||||
(void)sig;
|
|
||||||
if (dump_path) {
|
|
||||||
FILE *f = fopen(dump_path, "wb");
|
|
||||||
if (f) {
|
|
||||||
fwrite(avr->flash, 1, avr->flashend + 1, f);
|
|
||||||
fclose(f);
|
|
||||||
}
|
|
||||||
avr_eeprom_desc_t ee = {.ee = NULL, .offset = 0, .size = 0};
|
|
||||||
if (avr_ioctl(avr, AVR_IOCTL_EEPROM_GET, &ee) == 0 && ee.ee && ee.size) {
|
|
||||||
char path[512];
|
|
||||||
snprintf(path, sizeof(path), "%s.eeprom", dump_path);
|
|
||||||
f = fopen(path, "wb");
|
|
||||||
if (f) {
|
|
||||||
fwrite(ee.ee, 1, ee.size, f);
|
|
||||||
fclose(f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!link_software)
|
|
||||||
uart_pty_stop(&uart_pty);
|
|
||||||
_exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
|
||||||
{
|
|
||||||
int link_given = 0;
|
|
||||||
for (int opt; (opt = getopt(argc, argv, "l:")) != -1;) {
|
|
||||||
if (opt != 'l' || parse_link(optarg) != 0) {
|
|
||||||
fprintf(stderr, "device: bad link spec (usart0, usart1, sw, or sw:B0,B1 as RX,TX)\n");
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
link_given = 1;
|
|
||||||
}
|
|
||||||
int args = argc - optind;
|
|
||||||
if (args < 7 || args > 9) {
|
|
||||||
fprintf(stderr,
|
|
||||||
"usage: %s [-l link] <pureboot.elf> <mcu> <hz> <base_hex> <page> <baud> <flash_dump>"
|
|
||||||
" [reset_hex] [resume_flash]\n"
|
|
||||||
" -l link: usart0 | usart1 | sw[:B0,B1] (RX,TX); default: the chip's own\n"
|
|
||||||
" reset_hex: reset vector (default: base with a boot section, else 0)\n"
|
|
||||||
" resume_flash: raw full-flash image loaded instead of the ELF — a prior\n"
|
|
||||||
" run's dump, for power-fail resume tests\n",
|
|
||||||
argv[0]);
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
argv += optind - 1; // argv[1] is the ELF again, whatever was parsed
|
|
||||||
const char *mcu_name = argv[2];
|
|
||||||
uint32_t base = (uint32_t)strtoul(argv[4], NULL, 0);
|
|
||||||
unsigned page = (unsigned)atoi(argv[5]);
|
|
||||||
unsigned baud = (unsigned)atoi(argv[6]);
|
|
||||||
dump_path = argv[7];
|
|
||||||
int is_mega = strncmp(mcu_name, "atmega", 6) == 0;
|
|
||||||
if (!link_given)
|
|
||||||
link_software = !is_mega; // the chips' natural links: USART0, or PB0/PB1
|
|
||||||
|
|
||||||
avr = avr_make_mcu_by_name(mcu_name);
|
|
||||||
if (!avr) {
|
|
||||||
fprintf(stderr, "device: no %s core\n", mcu_name);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
avr_init(avr);
|
|
||||||
avr->frequency = (uint32_t)strtoul(argv[3], NULL, 0);
|
|
||||||
memset(avr->flash, 0xff, avr->flashend + 1); // real flash powers up erased
|
|
||||||
|
|
||||||
if (args > 8) {
|
|
||||||
// Resume: the full flash image of an interrupted prior run.
|
|
||||||
FILE *f = fopen(argv[9], "rb");
|
|
||||||
if (!f || fread(avr->flash, 1, avr->flashend + 1, f) == 0) {
|
|
||||||
fprintf(stderr, "device: cannot read %s\n", argv[9]);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
fclose(f);
|
|
||||||
} else {
|
|
||||||
elf_firmware_t fw = {0};
|
|
||||||
if (elf_read_firmware(argv[1], &fw) != 0) {
|
|
||||||
fprintf(stderr, "device: cannot read %s\n", argv[1]);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
memcpy(avr->flash + base, fw.flash, fw.flashsize);
|
|
||||||
}
|
|
||||||
// The boot-sectioned megas enter the loader in hardware (BOOTRST, not
|
|
||||||
// modeled — the argument picks the modeled fuse's target); the tinies
|
|
||||||
// and the boot-section-less m48s reset to word 0 like silicon — erased
|
|
||||||
// flash walks up into the loader, and after the host's surgery the
|
|
||||||
// patched vector routes there.
|
|
||||||
int boot_section = is_mega && strncmp(mcu_name, "atmega48", 8) != 0;
|
|
||||||
reset_pc = args > 7 ? (uint32_t)strtoul(argv[8], NULL, 0) : (boot_section ? base : 0);
|
|
||||||
avr->pc = reset_pc;
|
|
||||||
avr->codeend = avr->flashend;
|
|
||||||
|
|
||||||
// Erased EEPROM, as hardware powers up (simavr zeroes it).
|
|
||||||
uint8_t blank[1024];
|
|
||||||
memset(blank, 0xff, sizeof(blank));
|
|
||||||
avr_eeprom_desc_t seed = {.ee = blank, .offset = 0, .size = 0};
|
|
||||||
if (avr_ioctl(avr, AVR_IOCTL_EEPROM_GET, &seed) == 0 && seed.size <= sizeof(blank)) {
|
|
||||||
seed.ee = blank;
|
|
||||||
avr_ioctl(avr, AVR_IOCTL_EEPROM_SET, &seed);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
} else {
|
|
||||||
nvm.page = page;
|
|
||||||
memset(nvm.buffer, 0xff, sizeof(nvm.buffer));
|
|
||||||
nvm.io.kind = "tiny_nvm";
|
|
||||||
nvm.io.ioctl = nvm_ioctl;
|
|
||||||
avr_register_io(avr, &nvm.io);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!link_software) {
|
|
||||||
// POLL_SLEEP paces an idle-polling loader in host real time (a
|
|
||||||
// no-hardware CPU-saving hack); clear it so cycles run free.
|
|
||||||
uint32_t flags = 0;
|
|
||||||
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS(uart_digit), &flags);
|
|
||||||
flags &= ~AVR_UART_FLAG_POLL_SLEEP;
|
|
||||||
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS(uart_digit), &flags);
|
|
||||||
uart_pty_init(avr, &uart_pty);
|
|
||||||
uart_pty_connect(&uart_pty, uart_digit);
|
|
||||||
printf("PB_PTY %s\n", uart_pty.pty.slavename);
|
|
||||||
} else {
|
|
||||||
bit_cycles = (avr->frequency + baud / 2) / baud; // matches uart.hpp's own rounding exactly
|
|
||||||
rx_pin = avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ(sw_rx_port), (unsigned)sw_rx_bit);
|
|
||||||
avr_irq_register_notify(avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ(sw_tx_port), (unsigned)sw_tx_bit), tx_hook,
|
|
||||||
NULL);
|
|
||||||
avr_raise_irq(rx_pin, 1); // idle line
|
|
||||||
|
|
||||||
int slave;
|
|
||||||
struct termios raw;
|
|
||||||
cfmakeraw(&raw);
|
|
||||||
if (openpty(&pty_master, &slave, NULL, &raw, NULL) != 0) {
|
|
||||||
fprintf(stderr, "device: openpty failed\n");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
fcntl(pty_master, F_SETFL, O_NONBLOCK);
|
|
||||||
printf("PB_PTY %s\n", ttyname(slave));
|
|
||||||
}
|
|
||||||
fflush(stdout);
|
|
||||||
|
|
||||||
signal(SIGTERM, finish);
|
|
||||||
signal(SIGINT, finish);
|
|
||||||
signal(SIGUSR1, request_reset); // an external reset line, for the tests
|
|
||||||
|
|
||||||
long since_poll = 0;
|
|
||||||
for (;;) {
|
|
||||||
int state = avr_run(avr);
|
|
||||||
if (state == cpu_Done || state == cpu_Crashed)
|
|
||||||
break;
|
|
||||||
if (reset_requested) {
|
|
||||||
reset_requested = 0;
|
|
||||||
avr_reset(avr);
|
|
||||||
avr->pc = reset_pc;
|
|
||||||
if (!link_software) { // reset restores the pacing hack; re-clear it
|
|
||||||
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);
|
|
||||||
} else {
|
|
||||||
bridge_reset();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (link_software && ++since_poll >= 2000) {
|
|
||||||
since_poll = 0;
|
|
||||||
poll_pty();
|
|
||||||
// An unthrottled idle simulation runs the activation window out
|
|
||||||
// from under the host's real-time knock cadence: a 1 MHz build's
|
|
||||||
// 8 s window is 8 M cycles — tens of wall milliseconds — so a
|
|
||||||
// first knock lost to an in-flight reset misses the window
|
|
||||||
// entirely. Pace the simulation only while the bridge is fully
|
|
||||||
// quiet (nothing decoding, nothing queued); transfers keep full
|
|
||||||
// speed, and a quiet window stretches toward real time.
|
|
||||||
if (!rx_active && !tx_active && rx_head == rx_tail)
|
|
||||||
usleep(200);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finish(0);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,257 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Host-tool unit tests — the pure planning and policy logic, no simulator:
|
|
||||||
the flash-programming orders and their recovery properties, the reset-vector
|
|
||||||
surgery, the staging-slot composition, the mega boot-fuse decode, and the
|
|
||||||
update preflight's error/warning matrix (fuse combinations simavr cannot
|
|
||||||
model reach it here as synthetic bytes).
|
|
||||||
|
|
||||||
Usage: test_planner.py <tool_py>
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def fail(message):
|
|
||||||
print(f"FAIL: {message}")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def expect_error(what, fn, *needles):
|
|
||||||
try:
|
|
||||||
fn()
|
|
||||||
except Exception as error:
|
|
||||||
for needle in needles:
|
|
||||||
if needle not in str(error):
|
|
||||||
fail(f"{what}: error lacks {needle!r}: {error}")
|
|
||||||
return
|
|
||||||
fail(f"{what}: no error raised")
|
|
||||||
|
|
||||||
|
|
||||||
def info_of(pb, base, page, patch, flash, signature=(0x1E, 0x93, 0x0B), word_flash=False):
|
|
||||||
scale = 2 if word_flash else 1
|
|
||||||
wire_base = base // scale
|
|
||||||
flags = (1 if patch else 0) | (2 if word_flash else 0)
|
|
||||||
raw = bytes((0x50, 0x42, 1, *signature, page & 0xFF, wire_base & 0xFF, wire_base >> 8,
|
|
||||||
0, 2, flags))
|
|
||||||
info = pb.Info(raw)
|
|
||||||
assert info.flash_size == flash
|
|
||||||
return info
|
|
||||||
|
|
||||||
|
|
||||||
def rjmp_decode(word, at, flash_words):
|
|
||||||
if word & 0xF000 != 0xC000:
|
|
||||||
fail(f"not an rjmp: {word:#06x}")
|
|
||||||
offset = word & 0x0FFF
|
|
||||||
if offset >= 0x800:
|
|
||||||
offset -= 0x1000
|
|
||||||
return (at + 1 + offset) % flash_words
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
import os
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(sys.argv[1])))
|
|
||||||
import pureboot as pb
|
|
||||||
|
|
||||||
tiny = info_of(pb, 0x1E00, 64, True, 0x2000)
|
|
||||||
mega = info_of(pb, 0x7E00, 128, False, 0x8000, signature=(0x1E, 0x95, 0x0F))
|
|
||||||
|
|
||||||
# mega_boot: BOOTSZ words and the BOOTRST sense per chip — the fuse byte
|
|
||||||
# index (EXTENDED on the x8 line except the m328s' HIGH, HIGH elsewhere)
|
|
||||||
# and the per-family ladders (Atmel-2486/2466/2503/2545/8271/DS40002065/
|
|
||||||
# 8272/8011/2593/42719). Synthetic 'F' replies: only the boot byte
|
|
||||||
# carries meaning.
|
|
||||||
cases = (
|
|
||||||
((0x1E, 0x93, 0x07), 0x2000, 3, {0b11: 0x1F00, 0b10: 0x1E00, 0b01: 0x1C00, 0b00: 0x1800}), # m8
|
|
||||||
((0x1E, 0x94, 0x03), 0x4000, 3, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m16
|
|
||||||
((0x1E, 0x95, 0x02), 0x8000, 3, {0b11: 0x7E00, 0b10: 0x7C00, 0b01: 0x7800, 0b00: 0x7000}), # m32
|
|
||||||
((0x1E, 0x93, 0x0A), 0x2000, 2, {0b11: 0x1F00, 0b10: 0x1E00, 0b01: 0x1C00, 0b00: 0x1800}), # m88
|
|
||||||
((0x1E, 0x93, 0x0F), 0x2000, 2, {0b11: 0x1F00, 0b10: 0x1E00, 0b01: 0x1C00, 0b00: 0x1800}), # m88P
|
|
||||||
((0x1E, 0x94, 0x06), 0x4000, 2, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m168/168A
|
|
||||||
((0x1E, 0x94, 0x0B), 0x4000, 2, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m168P
|
|
||||||
((0x1E, 0x95, 0x14), 0x8000, 3, {0b11: 0x7E00, 0b10: 0x7C00, 0b01: 0x7800, 0b00: 0x7000}), # m328
|
|
||||||
((0x1E, 0x95, 0x0F), 0x8000, 3, {0b11: 0x7E00, 0b10: 0x7C00, 0b01: 0x7800, 0b00: 0x7000}), # m328P
|
|
||||||
((0x1E, 0x94, 0x0F), 0x4000, 3, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m164A
|
|
||||||
((0x1E, 0x94, 0x0A), 0x4000, 3, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m164P
|
|
||||||
((0x1E, 0x95, 0x15), 0x8000, 3, {0b11: 0x7E00, 0b10: 0x7C00, 0b01: 0x7800, 0b00: 0x7000}), # m324A
|
|
||||||
((0x1E, 0x96, 0x09), 0x10000, 3, {0b11: 0xFC00, 0b10: 0xF800, 0b01: 0xF000, 0b00: 0xE000}), # m644
|
|
||||||
((0x1E, 0x96, 0x0A), 0x10000, 3, {0b11: 0xFC00, 0b10: 0xF800, 0b01: 0xF000, 0b00: 0xE000}), # m644P
|
|
||||||
((0x1E, 0x97, 0x06), 0x20000, 3, {0b11: 0x1FC00, 0b10: 0x1F800, 0b01: 0x1F000, 0b00: 0x1E000}), # 1284
|
|
||||||
((0x1E, 0x97, 0x05), 0x20000, 3, {0b11: 0x1FC00, 0b10: 0x1F800, 0b01: 0x1F000, 0b00: 0x1E000}), # 1284P
|
|
||||||
)
|
|
||||||
for signature, flash, which, ladder in cases:
|
|
||||||
# Word-addressed chips carry the 1 KiB slot (their smallest boot sector).
|
|
||||||
slot = 1024 if flash > 0x10000 else 512
|
|
||||||
chip = info_of(pb, flash - slot, 128 if flash < 0x20000 else 0, False, flash,
|
|
||||||
signature=signature, word_flash=flash > 0x10000)
|
|
||||||
for bits, start in ladder.items():
|
|
||||||
fuses = bytearray((0xFF, 0xFF, 0xFF, 0xFF))
|
|
||||||
fuses[which] = (0xF8 | (bits << 1)) & ~1
|
|
||||||
prog, at = pb.mega_boot(chip, bytes(fuses))
|
|
||||||
if not prog or at != start:
|
|
||||||
fail(f"mega_boot {signature[1]:02x}{signature[2]:02x} BOOTSZ={bits:02b} programmed: {prog} {at:#07x}")
|
|
||||||
fuses[which] |= 1
|
|
||||||
prog, at = pb.mega_boot(chip, bytes(fuses))
|
|
||||||
if prog or at != start:
|
|
||||||
fail(f"mega_boot {signature[1]:02x}{signature[2]:02b} unprogrammed: {prog} {at:#07x}")
|
|
||||||
|
|
||||||
# Word-addressed info decode: the 1284P's base/page ride the wire scaled,
|
|
||||||
# and its slot is 1 KiB.
|
|
||||||
big = info_of(pb, 0x1FC00, 0, False, 0x20000, signature=(0x1E, 0x97, 0x05), word_flash=True)
|
|
||||||
if big.page != 256 or big.base != 0x1FC00 or big.stage != 0x1F800 or big.slot != 1024:
|
|
||||||
fail(f"word-addressed info decode: page {big.page}, base {big.base:#x}, stage {big.stage:#x}")
|
|
||||||
|
|
||||||
# Surgery: word 0 lands on the loader, the trampoline on the original
|
|
||||||
# entry — checked with an independent decoder.
|
|
||||||
app = bytes((0xC0 | 0x00, 0xC0)) + bytes((0x12,)) * 300 # rjmp .+0x00C0... entry word 0xC0C0
|
|
||||||
entry = rjmp_decode(app[0] | (app[1] << 8), 0, tiny.flash_size // 2)
|
|
||||||
pages = pb.plan_flash(app, tiny)
|
|
||||||
word0 = pages[0][0] | (pages[0][1] << 8)
|
|
||||||
if rjmp_decode(word0, 0, tiny.flash_size // 2) != tiny.base // 2:
|
|
||||||
fail("surgery: patched word 0 misses the loader")
|
|
||||||
tp = pages[tiny.base - 64]
|
|
||||||
tramp = tp[62] | (tp[63] << 8)
|
|
||||||
if rjmp_decode(tramp, (tiny.base - 2) // 2, tiny.flash_size // 2) != entry:
|
|
||||||
fail("surgery: trampoline misses the original entry")
|
|
||||||
expect_error("non-rjmp vector", lambda: pb.plan_flash(bytes((0x0C, 0x94)) + app[2:], tiny), "not an rjmp")
|
|
||||||
looped = bytearray(app)
|
|
||||||
word = pb.rjmp_to(0, tiny.base // 2, tiny.flash_size // 2)
|
|
||||||
looped[0], looped[1] = word & 0xFF, word >> 8
|
|
||||||
expect_error("read-back image", lambda: pb.plan_flash(bytes(looped), tiny), "read-back")
|
|
||||||
expect_error("oversize image", lambda: pb.plan_flash(bytes(0x1DFF), tiny), "application flash ends")
|
|
||||||
|
|
||||||
# Ordering: patched vector puts page 0 first and the trampoline second;
|
|
||||||
# a boot section puts page 0 last. Blank pages drop only when erased.
|
|
||||||
order = pb.covered(pages, tiny, skip_blank=False)
|
|
||||||
if order[0] != 0 or order[1] != tiny.base - 64:
|
|
||||||
fail(f"tiny order starts {order[:2]}, want page 0 then trampoline page")
|
|
||||||
if sorted(order[2:]) != order[2:]:
|
|
||||||
fail("tiny order tail not ascending")
|
|
||||||
mega_pages = pb.plan_flash(bytes((0xFF,)) * 600, mega)
|
|
||||||
morder = pb.covered(mega_pages, mega, skip_blank=False)
|
|
||||||
if morder[-1] != 0 or sorted(morder[:-1]) != morder[:-1]:
|
|
||||||
fail(f"mega order {morder}, want ascending with page 0 last")
|
|
||||||
blanky = {0: pages[0], 64: bytes((0xFF,)) * 64, 128: pages[128], tiny.base - 64: tp}
|
|
||||||
slim = pb.covered(blanky, tiny, skip_blank=True)
|
|
||||||
if 64 in slim or 0 not in slim or tiny.base - 64 not in slim:
|
|
||||||
fail(f"skip_blank order wrong: {slim}")
|
|
||||||
|
|
||||||
# Staging content: the identical image plus the through-word on a
|
|
||||||
# patched-vector chip; hard size clamps either way.
|
|
||||||
image = bytes(range(256)) * 2 # 512 B — too big for a tiny slot
|
|
||||||
expect_error("tiny staging size", lambda: pb.staging_content(image, tiny), "510")
|
|
||||||
staged = pb.staging_content(image[:508], tiny)
|
|
||||||
through = staged[510] | (staged[511] << 8)
|
|
||||||
if rjmp_decode(through, (tiny.base - 2) // 2, tiny.flash_size // 2) != tiny.base // 2:
|
|
||||||
fail("through-word misses the resident base")
|
|
||||||
if pb.staging_content(image, mega) != image:
|
|
||||||
fail("mega staging content should be the bare image")
|
|
||||||
expect_error("mega staging size", lambda: pb.staging_content(image + b"!", mega), "512")
|
|
||||||
|
|
||||||
# The embedded info block: found in a synthetic binary, absent in noise.
|
|
||||||
binary = bytes((0xAA,)) * 10 + tiny.raw + bytes((0xBB,)) * 10
|
|
||||||
found = pb.image_info(binary)
|
|
||||||
if found is None or found.raw != tiny.raw:
|
|
||||||
fail("image_info misses the embedded block")
|
|
||||||
if pb.image_info(bytes((0xAA,)) * 40) is not None:
|
|
||||||
fail("image_info invents a block")
|
|
||||||
|
|
||||||
# loader_image must peel a padded image down to the slot content: a raw
|
|
||||||
# .bin padded from address 0 (or a whole-flash read-back with the loader
|
|
||||||
# resident at base) yields the same bytes as the bare slot image.
|
|
||||||
import tempfile
|
|
||||||
slot_image = bytes((0xAA,)) * 10 + tiny.raw + bytes((0xCC,)) * 40
|
|
||||||
padded = bytes((0xFF,)) * tiny.base + slot_image
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f:
|
|
||||||
f.write(padded)
|
|
||||||
padded_path = f.name
|
|
||||||
try:
|
|
||||||
if pb.loader_image(padded_path) != slot_image:
|
|
||||||
fail("loader_image does not peel a padded image to the slot content")
|
|
||||||
finally:
|
|
||||||
os.unlink(padded_path)
|
|
||||||
|
|
||||||
# Update preflight: the full fuse matrix, plus target mismatch.
|
|
||||||
other = info_of(pb, 0x1E00, 32, True, 0x2000)
|
|
||||||
expect_error("wrong-target image", lambda: pb.update_preflight(binary, other, None), "another target")
|
|
||||||
expect_error("mega needs fuses", lambda: pb.update_preflight(bytes((0xAA,)) * 8 + mega.raw, mega, None),
|
|
||||||
"--assume-fuses")
|
|
||||||
mega_image = bytes((0xAA,)) * 8 + mega.raw
|
|
||||||
|
|
||||||
def fuses(high):
|
|
||||||
return bytes((0xFF, 0xFF, 0xFF, high))
|
|
||||||
|
|
||||||
expect_error("BOOTSZ 512 B", lambda: pb.update_preflight(mega_image, mega, fuses(0xFE)),
|
|
||||||
"cannot self-update", "BOOTSZ")
|
|
||||||
notes = pb.update_preflight(mega_image, mega, fuses(0xFD)) # 1 KB, BOOTRST unprogrammed
|
|
||||||
if not any("BOOTRST unprogrammed" in n for n in notes):
|
|
||||||
fail(f"1K/unprogrammed notes: {notes}")
|
|
||||||
notes = pb.update_preflight(mega_image, mega, fuses(0xFC)) # 1 KB, BOOTRST programmed
|
|
||||||
if not any("staging slot" in n for n in notes):
|
|
||||||
fail(f"1K/programmed notes: {notes}")
|
|
||||||
notes = pb.update_preflight(mega_image, mega, fuses(0xFA)) # 2 KB, BOOTRST programmed
|
|
||||||
if not any("application flash" in n for n in notes):
|
|
||||||
fail(f"2K/programmed notes: {notes}")
|
|
||||||
if pb.update_preflight(bytes((0xAA,)) * 8 + tiny.raw, tiny, None) != []:
|
|
||||||
fail("tiny preflight should pass without fuses")
|
|
||||||
|
|
||||||
# The walk-region refusal: BOOTRST aimed below the loader plus app data
|
|
||||||
# in the walk span errors without --force; erased spans and unprogrammed
|
|
||||||
# BOOTRST pass.
|
|
||||||
deep = {0x7800: bytes((1,)) * 128}
|
|
||||||
expect_error("walk region", lambda: pb.check_walk_region(deep, mega, fuses(0xFA), False), "--force")
|
|
||||||
pb.check_walk_region(deep, mega, fuses(0xFA), True)
|
|
||||||
pb.check_walk_region(deep, mega, fuses(0xFB), False) # BOOTRST unprogrammed
|
|
||||||
pb.check_walk_region({0x7800: bytes((0xFF,)) * 128}, mega, fuses(0xFA), False)
|
|
||||||
pb.check_walk_region(deep, mega, None, False) # fuses unknown: no check
|
|
||||||
|
|
||||||
# The repairing verify: a mismatched page is rewritten rather than raised,
|
|
||||||
# bounded so a fault that is not self-clearing cannot spin.
|
|
||||||
class FakeLoader:
|
|
||||||
"""A device whose first `bad` writes of any page land wrong."""
|
|
||||||
|
|
||||||
def __init__(self, info, bad):
|
|
||||||
self.info = info
|
|
||||||
self.bad = bad
|
|
||||||
self.flash = {}
|
|
||||||
self.writes = 0
|
|
||||||
|
|
||||||
def write_page(self, address, data):
|
|
||||||
self.writes += 1
|
|
||||||
self.flash[address] = bytes(len(data)) if self.bad > 0 else bytes(data)
|
|
||||||
self.bad -= 1
|
|
||||||
|
|
||||||
def read_flash(self, address, count):
|
|
||||||
return self.flash.get(address, bytes(count))
|
|
||||||
|
|
||||||
want = {0: bytes((i * 5) & 0xFF for i in range(128))}
|
|
||||||
|
|
||||||
# One bad write, then good: repaired in place, and the caller never sees
|
|
||||||
# an error. The rewrite is counted, so a silent no-op cannot pass.
|
|
||||||
device = FakeLoader(info_of(pb, 0x7E00, 128, False, 0x8000), bad=1)
|
|
||||||
device.write_page(0, want[0])
|
|
||||||
pb.verify_pages(device, want, repair=True)
|
|
||||||
if device.writes != 2:
|
|
||||||
fail(f"repairing verify made {device.writes} writes, expected 2")
|
|
||||||
|
|
||||||
# Without repair the same state raises, so the repair is what fixed it.
|
|
||||||
device = FakeLoader(info_of(pb, 0x7E00, 128, False, 0x8000), bad=1)
|
|
||||||
device.write_page(0, want[0])
|
|
||||||
expect_error("verify without repair", lambda: pb.verify_pages(device, want), "verify failed")
|
|
||||||
|
|
||||||
# A page that never comes good stops after RETRIES rewrites, and says so.
|
|
||||||
device = FakeLoader(info_of(pb, 0x7E00, 128, False, 0x8000), bad=99)
|
|
||||||
device.write_page(0, want[0])
|
|
||||||
expect_error(
|
|
||||||
"unrepairable page",
|
|
||||||
lambda: pb.verify_pages(device, want, repair=True),
|
|
||||||
"verify failed",
|
|
||||||
f"after {pb.RETRIES} retries",
|
|
||||||
)
|
|
||||||
if device.writes != pb.RETRIES + 1:
|
|
||||||
fail(f"unrepairable page took {device.writes} writes, expected {pb.RETRIES + 1}")
|
|
||||||
|
|
||||||
print("test_planner: all planner and policy checks pass")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# The port's gate: every chip's generated workflow — build, size matrix, and
|
|
||||||
# the simulator-driven protocol suites. --full adds the reflect-spot builds
|
|
||||||
# (libavr's rule: reflect compiles are bounded to its spot set, never the
|
|
||||||
# full matrix). LIBAVR_ROOT must point at the libavr checkout.
|
|
||||||
set -e
|
|
||||||
cd "$(dirname "$0")/.."
|
|
||||||
|
|
||||||
full=0
|
|
||||||
[[ "$1" == "--full" ]] && { full=1; shift; }
|
|
||||||
|
|
||||||
CHIPS=(attiny13 attiny13a attiny25 attiny45 attiny85
|
|
||||||
atmega8 atmega8a atmega16 atmega16a atmega32 atmega32a
|
|
||||||
atmega48 atmega48a atmega48p atmega48pa
|
|
||||||
atmega88 atmega88a atmega88p atmega88pa
|
|
||||||
atmega168 atmega168a atmega168p atmega168pa
|
|
||||||
atmega328 atmega328p
|
|
||||||
atmega164a atmega164p atmega164pa
|
|
||||||
atmega324a atmega324p atmega324pa
|
|
||||||
atmega644 atmega644a atmega644p atmega644pa
|
|
||||||
atmega1284 atmega1284p)
|
|
||||||
REFLECT_SPOT=(attiny13a attiny85 atmega8 atmega16a atmega32a atmega48pa
|
|
||||||
atmega88 atmega168pa atmega328p atmega164a atmega644p atmega1284)
|
|
||||||
|
|
||||||
for chip in "${CHIPS[@]}"; do
|
|
||||||
echo "==== $chip ===="
|
|
||||||
cmake --workflow --preset "$chip-generated" "$@"
|
|
||||||
done
|
|
||||||
|
|
||||||
if ((full)); then
|
|
||||||
for chip in "${REFLECT_SPOT[@]}"; do
|
|
||||||
echo "==== $chip reflect ===="
|
|
||||||
cmake --workflow --preset "$chip-reflect" "$@"
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "check: every chip green"
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Regenerate CMakePresets.json — one uniform pipeline per chip.
|
|
||||||
|
|
||||||
Every chip gets generated-mode configure/build/test presets and a workflow
|
|
||||||
running all three. Reflect-mode presets (configure + build, no tests — the
|
|
||||||
port's TUs compile identically; the sims prove nothing new there) exist for
|
|
||||||
libavr's reflect spot set only, mirroring its rule: the full reflect matrix
|
|
||||||
is never built, one chip per hardware class and pack vintage is.
|
|
||||||
|
|
||||||
Run from the repo root: tools/make_presets.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
|
|
||||||
CHIPS = [
|
|
||||||
"attiny13", "attiny13a", "attiny25", "attiny45", "attiny85",
|
|
||||||
"atmega8", "atmega8a", "atmega16", "atmega16a", "atmega32", "atmega32a",
|
|
||||||
"atmega48", "atmega48a", "atmega48p", "atmega48pa",
|
|
||||||
"atmega88", "atmega88a", "atmega88p", "atmega88pa",
|
|
||||||
"atmega168", "atmega168a", "atmega168p", "atmega168pa",
|
|
||||||
"atmega328", "atmega328p",
|
|
||||||
"atmega164a", "atmega164p", "atmega164pa",
|
|
||||||
"atmega324a", "atmega324p", "atmega324pa",
|
|
||||||
"atmega644", "atmega644a", "atmega644p", "atmega644pa",
|
|
||||||
"atmega1284", "atmega1284p",
|
|
||||||
]
|
|
||||||
|
|
||||||
# libavr's REFLECT_SPOT (tools/check.sh): one chip per hardware class and
|
|
||||||
# pack vintage.
|
|
||||||
REFLECT_SPOT = [
|
|
||||||
"attiny13a", "attiny85", "atmega8", "atmega16a", "atmega32a",
|
|
||||||
"atmega48pa", "atmega88", "atmega168pa", "atmega328p", "atmega164a",
|
|
||||||
"atmega644p", "atmega1284",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
configure = [{
|
|
||||||
"name": "base",
|
|
||||||
"hidden": True,
|
|
||||||
"generator": "Ninja",
|
|
||||||
"binaryDir": "${sourceDir}/build/${presetName}",
|
|
||||||
"toolchainFile": "$env{LIBAVR_ROOT}/cmake/avr-toolchain.cmake",
|
|
||||||
"cacheVariables": {
|
|
||||||
"CMAKE_BUILD_TYPE": "Release",
|
|
||||||
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
|
|
||||||
"CMAKE_COLOR_DIAGNOSTICS": "ON",
|
|
||||||
},
|
|
||||||
}]
|
|
||||||
build, test, workflows = [], [], []
|
|
||||||
|
|
||||||
def add(chip, mode):
|
|
||||||
name = f"{chip}-{mode}"
|
|
||||||
configure.append({
|
|
||||||
"name": name,
|
|
||||||
"inherits": "base",
|
|
||||||
"cacheVariables": {
|
|
||||||
"LIBAVR_MCU": chip,
|
|
||||||
"LIBAVR_REFLECT": "ON" if mode == "reflect" else "OFF",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
build.append({"name": name, "configurePreset": name})
|
|
||||||
steps = [{"type": "configure", "name": name}, {"type": "build", "name": name}]
|
|
||||||
if mode == "generated":
|
|
||||||
test.append({"name": name, "configurePreset": name, "output": {"outputOnFailure": True}})
|
|
||||||
steps.append({"type": "test", "name": name})
|
|
||||||
workflows.append({"name": name, "steps": steps})
|
|
||||||
|
|
||||||
for chip in CHIPS:
|
|
||||||
add(chip, "generated")
|
|
||||||
for chip in REFLECT_SPOT:
|
|
||||||
add(chip, "reflect")
|
|
||||||
|
|
||||||
presets = {
|
|
||||||
"version": 8,
|
|
||||||
"configurePresets": configure,
|
|
||||||
"buildPresets": build,
|
|
||||||
"testPresets": test,
|
|
||||||
"workflowPresets": workflows,
|
|
||||||
}
|
|
||||||
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "CMakePresets.json")
|
|
||||||
with open(path, "w") as f:
|
|
||||||
json.dump(presets, f, indent=1)
|
|
||||||
f.write("\n")
|
|
||||||
print(f"{len(CHIPS)} chips, {len(REFLECT_SPOT)} reflect: {os.path.normpath(path)}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
658
tsb/tsb_asm.cpp
658
tsb/tsb_asm.cpp
@@ -1,73 +1,54 @@
|
|||||||
// TinySafeBoot on libavr — tier 3: full feature parity in the 512-byte boot
|
// TinySafeBoot on libavr — tier 3: full feature parity in ≤512 B.
|
||||||
// section, in C++ except where the C ABI itself is the cost.
|
|
||||||
//
|
//
|
||||||
// The complete TinySafeBoot feature set — watchdog-reset bail, one-wire
|
// The complete TinySafeBoot feature set — watchdog-reset bail, one-wire
|
||||||
// half-duplex UART, a config-page activation timeout, the password gate,
|
// half-duplex UART, a config-page activation timeout, the password gate,
|
||||||
// emergency erase, and config/flash/EEPROM read-write — at 510 bytes in the
|
// emergency erase, and config/flash/EEPROM read-write — reimplemented for the
|
||||||
// 512-byte BOOTSZ=11 section the hand-written oracle occupies (500 B). This tier used to be one
|
// 512-byte ATmega328P boot section. Matching the hand-written assembly oracle's
|
||||||
// monolithic inline-asm routine; it is now the tricks tier's C++ (same
|
// size and features at once is only reachable at assembly density, so the loader
|
||||||
// register protocol, same structure — see tsb_tricks.cpp, including the
|
// body is one cohesive inline-asm routine. libavr still does the datasheet work:
|
||||||
// global-register miscompile rules) with exactly two routines kept in
|
// every geometry, baud and info-block constant below is computed by the library,
|
||||||
// assembly, the two whose remaining cost *is* the calling convention:
|
// never hand-entered, and the loader references them as assembler immediates.
|
||||||
//
|
//
|
||||||
// rx the bounded receive: C++ must re-floor the timeout window on every
|
// The wire protocol is strict request/response, which makes the one-wire
|
||||||
// call (the global-register-store miscompile) and split it across
|
// turn-around safe: the device owns the line whenever it drives a byte and
|
||||||
// call-saved registers; the asm keeps the oracle's X-register nested
|
// releases it (RX-only) whenever it waits for one.
|
||||||
// countdown.
|
|
||||||
// store the page-store loop: C++ cannot hold the receive byte pair and the
|
|
||||||
// walked Z pointer across the rx calls without call-saved staging
|
|
||||||
// (push/pop + a Y→Z copy per word); the asm calls rx knowing exactly
|
|
||||||
// which registers it touches and walks Z live across the whole page.
|
|
||||||
//
|
|
||||||
// Everything else — bring-up, activation, password gate, emergency erase,
|
|
||||||
// dispatch, every SPM/EEPROM/flash primitive, every geometry/baud/info
|
|
||||||
// constant — is C++ on libavr, and the two asm routines splice into the same
|
|
||||||
// global-register protocol the C++ uses (g_addr in Y, g_cnt in r16, g_window
|
|
||||||
// in r7, g_receiving in r6), so calls cross the boundary with no marshalling.
|
|
||||||
//
|
|
||||||
// The wire protocol is strict request/response, which is what makes the shared
|
|
||||||
// line safe: the device drives it only between a received command and its
|
|
||||||
// reply, and releases it (RXEN0 only) whenever it waits.
|
|
||||||
|
|
||||||
#include <libavr/libavr.hpp>
|
#include <libavr/libavr.hpp>
|
||||||
|
|
||||||
#include <avr/io.h> // SP / RAMEND for the crt-free boot entry, SFR addresses for the asm routines
|
#include <avr/boot.h> // __SPM_ENABLE and the SPM page-op bit names
|
||||||
|
#include <avr/io.h> // SFR addresses / bit numbers for the boot entry
|
||||||
|
|
||||||
using namespace avr::literals;
|
using namespace avr::literals;
|
||||||
namespace spm = avr::spm;
|
namespace spm = avr::spm;
|
||||||
namespace ee = avr::eeprom;
|
|
||||||
namespace hw = avr::hw;
|
|
||||||
|
|
||||||
namespace tsb {
|
namespace tsb {
|
||||||
namespace {
|
|
||||||
|
|
||||||
// The loader is purely polled — it never enables interrupts — so every SPM and
|
// Boot geometry — the chip database's to know, not ours.
|
||||||
// EEPROM lock folds to nothing under this posture.
|
constexpr std::uint16_t page = spm::page_bytes; // 128
|
||||||
constexpr auto off = avr::irq::guard_policy::unused;
|
constexpr std::uint16_t boot_bytes = 512; // BOOTSZ=11
|
||||||
|
constexpr std::uint16_t app_end = spm::flash_bytes - boot_bytes - page; // config page base
|
||||||
|
constexpr std::uint16_t eeprom_end = avr::hw::db.mem.eeprom_size - 1;
|
||||||
|
|
||||||
|
// Fixed 115200 8N1; the library solves UBRR + U2X from clock and baud.
|
||||||
|
constexpr auto baud = avr::uart::detail::solve_baud(16_MHz, 115200_Bd);
|
||||||
|
static_assert(baud.u2x && baud.ubrr < 256, "asm bring-up writes UBRR0L only, with U2X0");
|
||||||
|
|
||||||
|
// Activation window: the config page's timeout byte, floored so a corrupt page
|
||||||
|
// can never lock the loader out (at least the clock rate in MHz → ~0.5 s here).
|
||||||
|
constexpr std::uint8_t act_min = 16;
|
||||||
|
// Post-activation communication timeout (~several seconds); the loader bails to
|
||||||
|
// the application if the host falls silent mid-session.
|
||||||
|
constexpr std::uint8_t comm_timeout = 200;
|
||||||
|
|
||||||
constexpr std::uint8_t confirm = '!';
|
constexpr std::uint8_t confirm = '!';
|
||||||
constexpr std::uint8_t request = '?';
|
constexpr std::uint8_t request = '?';
|
||||||
constexpr std::uint8_t knock = '@';
|
constexpr std::uint8_t knock = '@';
|
||||||
|
|
||||||
// Boot geometry for the 512 B boot section (BOOTSZ=11); the page size and the
|
constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 19;
|
||||||
// flash/EEPROM extents are the chip database's to know. app_end is the config
|
|
||||||
// page (TSB's LASTPAGE), one page below the boot section.
|
|
||||||
constexpr std::uint16_t page = spm::page_bytes;
|
|
||||||
constexpr std::uint16_t boot_bytes = 512;
|
|
||||||
constexpr std::uint16_t app_end = spm::flash_bytes - boot_bytes - page;
|
|
||||||
constexpr std::uint16_t eeprom_end = avr::hw::db.mem.eeprom_size - 1;
|
|
||||||
|
|
||||||
// Lockout-proof floor for the activation window (the oracle's F_CPU/1MHz).
|
// The 16-byte device-info block, LPM-read on activation. A plain progmem array:
|
||||||
constexpr std::uint8_t act_min = 16;
|
// the loader streams it straight out with LPM, so a flash_table wrapper would
|
||||||
// Post-activation window: the host gets seconds, not milliseconds, mid-session.
|
// add nothing here.
|
||||||
constexpr std::uint8_t comm_window = 200;
|
|
||||||
|
|
||||||
constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 20;
|
|
||||||
|
|
||||||
// Fixed 115200 8N1; the library solves UBRR + U2X from clock and baud.
|
|
||||||
constexpr auto baud = avr::uart::detail::solve_baud(16_MHz, 115200_Bd);
|
|
||||||
|
|
||||||
// The 16-byte device-info block, streamed out on activation.
|
|
||||||
// clang-format off
|
// clang-format off
|
||||||
[[gnu::progmem]] constexpr std::uint8_t info[16] = {
|
[[gnu::progmem]] constexpr std::uint8_t info[16] = {
|
||||||
'T', 'S', 'B',
|
'T', 'S', 'B',
|
||||||
@@ -81,306 +62,299 @@ constexpr auto baud = avr::uart::detail::solve_baud(16_MHz, 115200_Bd);
|
|||||||
};
|
};
|
||||||
// clang-format on
|
// clang-format on
|
||||||
|
|
||||||
register std::uint16_t g_addr asm("r28");
|
} // namespace tsb
|
||||||
register std::uint8_t g_cnt asm("r16");
|
|
||||||
register std::uint8_t g_window asm("r7");
|
|
||||||
register std::uint8_t g_receiving asm("r6");
|
|
||||||
|
|
||||||
const std::uint8_t *flash_ptr(std::uint16_t addr)
|
// Reset lands here: BOOTRST vectors to the boot base, .vectors is laid first, and
|
||||||
|
// no crt runs. The whole loader is this one naked routine.
|
||||||
|
extern "C" [[gnu::naked, gnu::used, gnu::section(".vectors")]] void __boot_entry()
|
||||||
{
|
{
|
||||||
return reinterpret_cast<const std::uint8_t *>(addr);
|
asm volatile(
|
||||||
}
|
// --- bring-up ------------------------------------------------------
|
||||||
|
" ldi r16, lo8(%[ramend]) \n\t"
|
||||||
// Bounded byte receive (asm 1 of 2): release the one-wire line on a direction
|
" out %[spl], r16 \n\t"
|
||||||
// change, poll RXC0 under the oracle's nested X-register countdown seeded from
|
" ldi r16, hi8(%[ramend]) \n\t"
|
||||||
// g_window (floored against lockout), byte or 0-on-silence in r24. Z survives
|
" out %[sph], r16 \n\t"
|
||||||
// — the property the store's word loop rides on.
|
" in r16, %[mcusr] \n\t" // watchdog reset → hand straight back
|
||||||
[[gnu::noinline, gnu::noclone]] std::uint8_t rx()
|
" sbrc r16, 3 \n\t" // MCUSR bit 3 = WDRF
|
||||||
{
|
" rjmp 9f \n\t" // 9: = appjump
|
||||||
std::uint8_t byte;
|
" ldi r16, %[ubrr] \n\t" // fixed baud, UBRR0L only
|
||||||
asm volatile(" tst %[dir] \n\t" // already receiving? keep the line released
|
" sts %[ubrr0l], r16 \n\t"
|
||||||
" brne 1f \n\t"
|
" ldi r16, 0x02 \n\t" // 1<<U2X0
|
||||||
" ldi %[b], 0x10 \n\t" // RXEN0 alone: release the line and listen
|
" sts %[ucsr0a], r16 \n\t"
|
||||||
" sts %[ucsr0b], %[b] \n\t"
|
" clr r22 \n\t" // direction flag bit0: 0 = receiving, 1 = driving the line
|
||||||
" ser %[b] \n\t"
|
// --- activation: 3×'@' inside a config-page-timed window -----------
|
||||||
" mov %[dir], %[b] \n\t"
|
" ldi r30, lo8(%[appto]) \n\t" // Z = config page + 2
|
||||||
"1: mov r27, %[to] \n\t" // outer countdown high byte = window
|
" ldi r31, hi8(%[appto]) \n\t"
|
||||||
" ori r27, %[actmin] \n\t" // lockout-proof floor
|
" lpm r23, Z+ \n\t" // timeout byte; Z → password
|
||||||
" clr r26 \n\t"
|
" ori r23, %[actmin] \n\t" // lockout-proof floor
|
||||||
"2: ser %[b] \n\t"
|
" clr r17 \n\t" // knock counter
|
||||||
"3: lds %[b], %[ucsr0a] \n\t"
|
"1: rcall tsb_rx \n\t"
|
||||||
" sbrc %[b], 7 \n\t" // RXC0
|
" brcs 9f \n\t" // window elapsed → application
|
||||||
" rjmp 4f \n\t"
|
" cpi r16, %[knock] \n\t"
|
||||||
" dec %[b] \n\t"
|
" brne 9f \n\t" // any non-'@' → application
|
||||||
" brne 3b \n\t"
|
" inc r17 \n\t"
|
||||||
" sbiw r26, 1 \n\t"
|
" cpi r17, 3 \n\t"
|
||||||
" brcc 2b \n\t"
|
" brne 1b \n\t"
|
||||||
" clr %[b] \n\t" // silence → 0, which no compare accepts
|
// --- password / emergency erase (Z at config-page password) --------
|
||||||
" rjmp 5f \n\t"
|
" ldi r23, %[commto] \n\t" // widen the timeout for the session
|
||||||
"4: lds %[b], %[udr0] \n\t"
|
"2: ser r19 \n\t" // r19=0xff → comparison enabled
|
||||||
"5: \n\t"
|
"3: lpm r18, Z+ \n\t"
|
||||||
: [b] "=&d"(byte), [dir] "+r"(g_receiving)
|
" and r18, r19 \n\t" // a prior mismatch (r19=0) blanks the rest
|
||||||
: [to] "r"(g_window), [actmin] "M"(act_min), [ucsr0a] "n"(_SFR_MEM_ADDR(UCSR0A)),
|
" cpi r18, 0xff \n\t"
|
||||||
[ucsr0b] "n"(_SFR_MEM_ADDR(UCSR0B)), [udr0] "n"(_SFR_MEM_ADDR(UDR0))
|
" breq tsb_info \n\t" // 0xff terminator → password satisfied
|
||||||
: "r26", "r27", "cc");
|
" rcall tsb_rx \n\t"
|
||||||
return byte;
|
" cpi r16, 0 \n\t"
|
||||||
}
|
" breq 5f \n\t" // a 0 byte requests emergency erase
|
||||||
|
" cp r16, r18 \n\t"
|
||||||
// One-wire transmit: take the line (TXEN0 alone) on a direction change with a
|
" breq 2b \n\t" // char matched → next, comparison re-armed
|
||||||
// turn-around guard, put the byte out, hold the line until the whole frame is
|
" clr r19 \n\t" // mismatch → drain forever, never erase
|
||||||
// out (TXC0, not UDRE0), W1C TXC0 by storing the sampled status back (keeps
|
" rjmp 3b \n\t"
|
||||||
// U2X0). Plain C++ — it compiles *smaller* than the oracle's routine.
|
"5: cpi r19, 0 \n\t" // only offer erase if not already wrong
|
||||||
[[gnu::noinline, gnu::noclone]] void tx(std::uint8_t byte)
|
" breq 3b \n\t"
|
||||||
{
|
" rcall tsb_rcnf \n\t" // two confirmations guard the wipe
|
||||||
if (g_receiving) {
|
" brts 9f \n\t"
|
||||||
g_receiving = 0;
|
" rcall tsb_rcnf \n\t"
|
||||||
hw::ucsr0b::write(hw::ucsr0b::txen0(1));
|
" brts 9f \n\t"
|
||||||
for (std::uint8_t guard = 46; guard; --guard)
|
" rcall tsb_emerg \n\t"
|
||||||
;
|
" rjmp tsb_main \n\t"
|
||||||
}
|
// --- device info, then the command loop ----------------------------
|
||||||
hw::udr0::write(byte);
|
"tsb_info: \n\t"
|
||||||
std::uint8_t status;
|
" ldi r30, lo8(%[info]) \n\t"
|
||||||
do {
|
" ldi r31, hi8(%[info]) \n\t"
|
||||||
status = hw::ucsr0a::read();
|
" ldi r20, 16 \n\t"
|
||||||
} while (!(status & hw::ucsr0a::txc0(1).value));
|
" rcall tsb_sendf \n\t"
|
||||||
hw::ucsr0a::write(status);
|
"tsb_main: \n\t"
|
||||||
}
|
" clr r30 \n\t" // Z = 0 for the memory commands
|
||||||
|
" clr r31 \n\t"
|
||||||
// '?', then hand back the host's reply for the callers' one-byte compare.
|
" ldi r16, %[cfm] \n\t" // mainloop ready
|
||||||
[[gnu::noinline, gnu::noclone]] std::uint8_t rcnf()
|
" rcall tsb_tx \n\t"
|
||||||
{
|
" rcall tsb_rx \n\t"
|
||||||
tx(request);
|
" rcall tsb_disp \n\t"
|
||||||
return rx();
|
" rjmp tsb_main \n\t"
|
||||||
}
|
"tsb_disp: \n\t"
|
||||||
|
" cpi r16, 'f' \n\t"
|
||||||
// One flash byte ← [g_addr++] (the advance right before ret — the
|
" breq tsb_rflash \n\t"
|
||||||
// global-register rule, see tsb_tricks.cpp).
|
" cpi r16, 'F' \n\t"
|
||||||
[[gnu::noinline, gnu::noclone]] std::uint8_t sflash()
|
" breq tsb_wflash \n\t"
|
||||||
{
|
" cpi r16, 'e' \n\t"
|
||||||
std::uint8_t byte = avr::flash_load(flash_ptr(g_addr));
|
" breq tsb_reep \n\t"
|
||||||
++g_addr;
|
" cpi r16, 'E' \n\t"
|
||||||
return byte;
|
" breq tsb_weep \n\t"
|
||||||
}
|
" cpi r16, 'c' \n\t"
|
||||||
|
" breq tsb_rconf \n\t"
|
||||||
// One EEPROM byte ← [g_addr++].
|
" cpi r16, 'C' \n\t"
|
||||||
[[gnu::noinline, gnu::noclone]] std::uint8_t eerd()
|
" breq tsb_wconf \n\t"
|
||||||
{
|
"9: rcall tsb_spmw \n\t" // appjump: finish any SPM, hand over at 0
|
||||||
std::uint8_t byte = ee::read(g_addr);
|
" jmp 0 \n\t"
|
||||||
++g_addr;
|
// --- 'f' read application flash (host-paced) -----------------------
|
||||||
return byte;
|
"tsb_rflash: \n\t"
|
||||||
}
|
"1: rcall tsb_rwait \n\t"
|
||||||
|
" brts 9f \n\t"
|
||||||
// One EEPROM byte → [g_addr++].
|
" ldi r20, %[page] \n\t"
|
||||||
[[gnu::noinline, gnu::noclone]] void eewr(std::uint8_t byte)
|
" rcall tsb_sendf \n\t"
|
||||||
{
|
" cpi r30, lo8(%[appcfg]) \n\t"
|
||||||
ee::write<off>(g_addr, byte);
|
" ldi r24, hi8(%[appcfg]) \n\t"
|
||||||
++g_addr;
|
" cpc r31, r24 \n\t"
|
||||||
}
|
" brlo 1b \n\t"
|
||||||
|
"9: ret \n\t"
|
||||||
// Stream g_cnt flash bytes from g_addr to the host.
|
// --- 'e' read EEPROM (host-paced) ----------------------------------
|
||||||
[[gnu::noinline, gnu::noclone]] void sendf()
|
"tsb_reep: \n\t"
|
||||||
{
|
"1: rcall tsb_rwait \n\t"
|
||||||
do {
|
" brts 9f \n\t"
|
||||||
tx(sflash());
|
" ldi r20, %[page] \n\t"
|
||||||
} while (--g_cnt);
|
"2: out %[earl], r30 \n\t"
|
||||||
}
|
" out %[earh], r31 \n\t"
|
||||||
|
" sbi %[eecr], 0 \n\t" // EERE
|
||||||
// Wait out a running SPM op, then re-open the RWW section — after every page
|
" in r16, %[eedr] \n\t"
|
||||||
// op and before handing over, as the oracle does.
|
" rcall tsb_tx \n\t"
|
||||||
[[gnu::noinline, gnu::noclone]] void settle()
|
" adiw r30, 1 \n\t"
|
||||||
{
|
" dec r20 \n\t"
|
||||||
spm::wait();
|
" brne 2b \n\t"
|
||||||
spm::rww_enable<off>();
|
" rjmp 1b \n\t"
|
||||||
}
|
"9: ret \n\t"
|
||||||
|
// --- 'F' write application flash -----------------------------------
|
||||||
extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --defsym=tsb_app=0
|
"tsb_wflash: \n\t"
|
||||||
|
" rcall tsb_erapp \n\t" // erase the whole application first (leaves Z=0)
|
||||||
[[noreturn]] void appjump()
|
"1: rcall tsb_rcnf \n\t"
|
||||||
{
|
" brts 9f \n\t"
|
||||||
settle();
|
" rcall tsb_store \n\t"
|
||||||
tsb_app();
|
" cpi r30, lo8(%[appcfg]) \n\t"
|
||||||
}
|
" ldi r24, hi8(%[appcfg]) \n\t"
|
||||||
|
" cpc r31, r24 \n\t"
|
||||||
// Step g_addr one page down and erase that page (the decrement lives here —
|
" brlo 1b \n\t"
|
||||||
// the global-register rule).
|
"9: ret \n\t"
|
||||||
[[gnu::noinline, gnu::noclone]] void erase_below()
|
// --- 'E' write EEPROM ----------------------------------------------
|
||||||
{
|
"tsb_weep: \n\t" // Z already 0 from the mainloop
|
||||||
g_addr -= page;
|
"1: rcall tsb_rcnf \n\t"
|
||||||
spm::erase_page<off>(g_addr);
|
" brts 9f \n\t"
|
||||||
settle();
|
" ldi r20, %[page] \n\t"
|
||||||
}
|
"2: rcall tsb_rx \n\t"
|
||||||
|
" rcall tsb_eewr \n\t"
|
||||||
// Erase the whole application, top-down like the oracle: the loop bound is a
|
" dec r20 \n\t"
|
||||||
// compare with zero, and g_addr = 0 is handed back for free.
|
" brne 2b \n\t"
|
||||||
[[gnu::noinline, gnu::noclone]] void erase_application()
|
" rjmp 1b \n\t"
|
||||||
{
|
"9: ret \n\t"
|
||||||
g_addr = app_end;
|
// --- 'c' read config page, 'C' write config page -------------------
|
||||||
do {
|
"tsb_rconf: \n\t"
|
||||||
erase_below();
|
" ldi r30, lo8(%[appcfg]) \n\t"
|
||||||
} while (g_addr != 0);
|
" ldi r31, hi8(%[appcfg]) \n\t"
|
||||||
}
|
" ldi r20, %[page] \n\t"
|
||||||
|
" rjmp tsb_sendf \n\t"
|
||||||
// Stream one host page into the erased flash page at g_addr (asm 2 of 2): the
|
"tsb_wconf: \n\t"
|
||||||
// word pair stages in r0:r1 straight from rx (whose register set is known —
|
" rcall tsb_rcnf \n\t"
|
||||||
// the cross-call liveness C++ cannot express), Z walks the page and PGWRT
|
" brts 9f \n\t"
|
||||||
// programs it. g_addr is left at the next page base.
|
" ldi r30, lo8(%[appcfg]) \n\t"
|
||||||
[[gnu::noinline, gnu::noclone]] void store_flash()
|
" ldi r31, hi8(%[appcfg]) \n\t"
|
||||||
{
|
" rcall tsb_erpage \n\t" // erase the config page (Z unchanged)
|
||||||
asm volatile(" movw r30, r28 \n\t" // Z = page base; rx leaves Z live
|
" rcall tsb_store \n\t" // program it from the host
|
||||||
|
" rjmp tsb_rconf \n\t" // rewind Z and echo it back
|
||||||
|
"9: ret \n\t"
|
||||||
|
// --- stream one page host→flash at Z, program it (Z → next page) ----
|
||||||
|
"tsb_store: \n\t"
|
||||||
" ldi r20, %[words] \n\t"
|
" ldi r20, %[words] \n\t"
|
||||||
"1: rcall %x[rx] \n\t"
|
"1: rcall tsb_rx \n\t"
|
||||||
" mov r0, r24 \n\t" // word low byte
|
" mov r0, r16 \n\t"
|
||||||
" rcall %x[rx] \n\t"
|
" rcall tsb_rx \n\t"
|
||||||
" mov r1, r24 \n\t" // word high byte
|
" mov r1, r16 \n\t"
|
||||||
" ldi r24, 0x01 \n\t" // SPMEN: buffer the word at Z
|
" ldi r24, %[spm_fill] \n\t"
|
||||||
" out %[spmcsr], r24 \n\t"
|
" out %[spmcsr], r24 \n\t"
|
||||||
" spm \n\t"
|
" spm \n\t"
|
||||||
" clr r1 \n\t"
|
" clr r1 \n\t"
|
||||||
" adiw r30, 2 \n\t"
|
" adiw r30, 2 \n\t"
|
||||||
" dec r20 \n\t"
|
" dec r20 \n\t"
|
||||||
" brne 1b \n\t"
|
" brne 1b \n\t"
|
||||||
" movw %[base], r30 \n\t" // g_addr = the next page base
|
" subi r30, lo8(%[page]) \n\t" // back to the page base for PGWRT
|
||||||
" subi r30, %[pagelo] \n\t" // Z back to this page's base
|
" sbci r31, hi8(%[page]) \n\t"
|
||||||
" sbci r31, %[pagehi] \n\t"
|
" ldi r24, %[spm_wrt] \n\t"
|
||||||
" ldi r24, 0x05 \n\t" // PGWRT | SPMEN: program the page
|
|
||||||
" out %[spmcsr], r24 \n\t"
|
" out %[spmcsr], r24 \n\t"
|
||||||
" spm \n\t"
|
" spm \n\t"
|
||||||
: [base] "+r"(g_addr)
|
" rcall tsb_spmw \n\t"
|
||||||
: [rx] "i"(&rx), [spmcsr] "I"(_SFR_IO_ADDR(SPMCSR)), [words] "M"(page / 2), [pagelo] "M"(page & 0xff),
|
" subi r30, lo8(-%[page]) \n\t" // Z → next page base
|
||||||
[pagehi] "M"(page >> 8)
|
" sbci r31, hi8(-%[page]) \n\t"
|
||||||
: "r0", "r1", "r20", "r24", "r26", "r27", "r30", "r31", "cc", "memory");
|
" ret \n\t"
|
||||||
settle();
|
// --- erase [0, config page) ----------------------------------------
|
||||||
}
|
"tsb_erapp: \n\t"
|
||||||
|
" clr r30 \n\t"
|
||||||
[[noreturn, gnu::noinline]] void run()
|
" clr r31 \n\t"
|
||||||
{
|
"1: rcall tsb_erpage \n\t"
|
||||||
// A watchdog reset hands straight back to the application, as the
|
" subi r30, lo8(-%[page]) \n\t"
|
||||||
// reference loader does, rather than re-entering the bootloader.
|
" sbci r31, hi8(-%[page]) \n\t"
|
||||||
if (hw::mcusr::wdrf.test())
|
" cpi r30, lo8(%[appcfg]) \n\t"
|
||||||
appjump();
|
" ldi r24, hi8(%[appcfg]) \n\t"
|
||||||
|
" cpc r31, r24 \n\t"
|
||||||
// Lean bring-up from reset state: UCSR0C already reads 8N1, UBRR0H reads
|
" brlo 1b \n\t"
|
||||||
// 0, and rx()/tx() raise RXEN0/TXEN0 on first use — only the divisor low
|
" clr r30 \n\t" // hand callers Z=0
|
||||||
// byte and U2X0 need a store. The library still does the datasheet work.
|
" clr r31 \n\t"
|
||||||
static_assert(baud.u2x && baud.ubrr < 256, "lean bring-up writes UBRR0L only, with U2X0");
|
" ret \n\t"
|
||||||
hw::reg<"UBRR0">::write(static_cast<std::uint8_t>(baud.ubrr));
|
// --- erase one flash page at Z (busy-wait + RWW re-enable) ----------
|
||||||
hw::ucsr0a::write(hw::ucsr0a::u2x0(1));
|
"tsb_erpage: \n\t"
|
||||||
// General-purpose registers are undefined at power-on (no crt zeroes them);
|
" ldi r24, %[spm_ers] \n\t"
|
||||||
// the direction latch must start "not receiving" so the first rx() enables
|
" out %[spmcsr], r24 \n\t"
|
||||||
// the receiver. The reference loader clears its shadow register for the
|
" spm \n\t"
|
||||||
// same reason.
|
" rjmp tsb_spmw \n\t" // tail: wait + RWW re-enable, then ret
|
||||||
g_receiving = 0;
|
// --- emergency erase: application flash, EEPROM, config page -------
|
||||||
|
"tsb_emerg: \n\t"
|
||||||
// Activation: 3×'@', each inside the config page's timeout window (rx
|
" rcall tsb_erapp \n\t" // erases the application, leaves Z=0
|
||||||
// floors it so a corrupt page cannot lock the loader out); anything else —
|
" ser r16 \n\t"
|
||||||
// including silence — hands over.
|
"1: rcall tsb_eewr \n\t"
|
||||||
g_window = avr::flash_load(flash_ptr(app_end + 2));
|
" cpi r30, lo8(%[eeend1]) \n\t"
|
||||||
for (std::uint8_t k = 3; k; --k)
|
" ldi r24, hi8(%[eeend1]) \n\t"
|
||||||
if (rx() != knock)
|
" cpc r31, r24 \n\t"
|
||||||
appjump();
|
" brne 1b \n\t"
|
||||||
g_window = comm_window;
|
" ldi r30, lo8(%[appcfg]) \n\t"
|
||||||
|
" ldi r31, hi8(%[appcfg]) \n\t"
|
||||||
// Password gate (config page from app_end+3, 0xff-terminated; a blank
|
" rjmp tsb_erpage \n\t" // erase the config page (tail)
|
||||||
// page is no password). A wrong byte blanks the comparison and drains the
|
// --- one EEPROM byte r16 → [Z], Z++ --------------------------------
|
||||||
// line forever, so a wrong password can never fall through; a 0 requests
|
"tsb_eewr: \n\t"
|
||||||
// emergency erase behind two confirms. On pass the info block goes out;
|
"1: sbic %[eecr], 1 \n\t" // EEPE busy
|
||||||
// the emergency path skips it and drops into the command loop.
|
" rjmp 1b \n\t"
|
||||||
g_addr = app_end + 3;
|
" out %[earl], r30 \n\t"
|
||||||
std::uint8_t mask = 0xff;
|
" out %[earh], r31 \n\t"
|
||||||
for (;;) {
|
" out %[eedr], r16 \n\t"
|
||||||
std::uint8_t expected = avr::flash_load(flash_ptr(g_addr)) & mask;
|
" sbi %[eecr], 2 \n\t" // EEMPE, then EEPE within 4 cycles
|
||||||
++g_addr;
|
" sbi %[eecr], 1 \n\t" // EEPE
|
||||||
if (expected == 0xff) {
|
" adiw r30, 1 \n\t"
|
||||||
g_addr = reinterpret_cast<std::uint16_t>(&info[0]);
|
" ret \n\t"
|
||||||
g_cnt = sizeof(info);
|
// --- stream r20 flash bytes from Z to the host ---------------------
|
||||||
sendf();
|
"tsb_sendf: \n\t"
|
||||||
break;
|
"1: lpm r16, Z+ \n\t"
|
||||||
}
|
" rcall tsb_tx \n\t"
|
||||||
std::uint8_t got = rx();
|
" dec r20 \n\t"
|
||||||
if (got == 0) {
|
" brne 1b \n\t"
|
||||||
if (mask == 0)
|
" ret \n\t"
|
||||||
continue;
|
// --- SPM busy-wait, then re-enable RWW read access -----------------
|
||||||
if (rcnf() != confirm || rcnf() != confirm)
|
"tsb_spmw: \n\t"
|
||||||
appjump();
|
"1: in r24, %[spmcsr] \n\t"
|
||||||
erase_application(); // leaves g_addr = 0 for the EEPROM walk
|
" sbrc r24, 0 \n\t"
|
||||||
do {
|
" rjmp 1b \n\t"
|
||||||
eewr(0xff);
|
" ldi r24, %[spm_rww] \n\t"
|
||||||
} while (g_addr <= eeprom_end);
|
" out %[spmcsr], r24 \n\t"
|
||||||
g_addr = app_end + page;
|
" spm \n\t"
|
||||||
erase_below();
|
" ret \n\t"
|
||||||
break;
|
// --- '?' then await '!' (T=1 ⇒ not confirmed) ----------------------
|
||||||
}
|
"tsb_rcnf: \n\t"
|
||||||
if (got != expected)
|
" ldi r16, %[req] \n\t"
|
||||||
mask = 0;
|
" rcall tsb_tx \n\t"
|
||||||
}
|
"tsb_rwait: \n\t"
|
||||||
|
" rcall tsb_rx \n\t"
|
||||||
for (;;) {
|
" clt \n\t"
|
||||||
tx(confirm); // Mainloop ready
|
" cpi r16, %[cfm] \n\t"
|
||||||
g_addr = 0;
|
" breq 9f \n\t"
|
||||||
switch (rx()) {
|
" set \n\t"
|
||||||
case 'f': // read application flash, one page per host '!'
|
"9: ret \n\t"
|
||||||
for (;;) {
|
// --- one-wire transmit r16 (drive the line + guard, wait TXC) ------
|
||||||
if (rx() != confirm)
|
// One-wire: RX and TX share the line, so only one direction is enabled
|
||||||
break;
|
// at a time. Waiting for TXC (whole frame out) before a caller can
|
||||||
g_cnt = page;
|
// release the line is what makes the shared wiring safe.
|
||||||
sendf();
|
"tsb_tx: \n\t"
|
||||||
if (g_addr >= app_end)
|
" sbrc r22, 0 \n\t" // currently receiving? turn the line around
|
||||||
break;
|
" rjmp 2f \n\t"
|
||||||
}
|
"1: sts %[udr0], r16 \n\t"
|
||||||
break;
|
"3: lds r25, %[ucsr0a] \n\t" // wait for the whole frame out (TXC0)
|
||||||
case 'F': // erase the application, then take pages behind '?'
|
" sbrs r25, 6 \n\t" // UCSR0A bit 6 = TXC0
|
||||||
erase_application(); // leaves g_addr = 0, the write start
|
" rjmp 3b \n\t"
|
||||||
while (rcnf() == confirm)
|
" sts %[ucsr0a], r25 \n\t" // write 1 to clear TXC
|
||||||
store_flash();
|
" ret \n\t"
|
||||||
break;
|
"2: ldi r25, 0x08 \n\t" // TXEN0 only: drive the line (receiver off)
|
||||||
case 'e': // read EEPROM, one page per host '!', until the host stops
|
" sts %[ucsr0b], r25 \n\t"
|
||||||
for (;;) {
|
" clr r22 \n\t"
|
||||||
if (rx() != confirm)
|
" ser r21 \n\t" // turn-around guard for a shorted receiver
|
||||||
break;
|
"4: dec r21 \n\t"
|
||||||
g_cnt = page;
|
" brne 4b \n\t"
|
||||||
do {
|
" rjmp 1b \n\t"
|
||||||
tx(eerd());
|
// --- one-wire receive → r16, C set on timeout ----------------------
|
||||||
} while (--g_cnt);
|
"tsb_rx: \n\t"
|
||||||
}
|
" sbrc r22, 0 \n\t" // already receiving? keep the line released
|
||||||
break;
|
" rjmp 1f \n\t"
|
||||||
case 'E': // take EEPROM pages behind '?'
|
" ldi r25, 0x10 \n\t" // RXEN0 only: release the line and listen
|
||||||
while (rcnf() == confirm) {
|
" sts %[ucsr0b], r25 \n\t"
|
||||||
g_cnt = page;
|
" ser r22 \n\t"
|
||||||
do {
|
"1: mov r27, r23 \n\t" // outer countdown high = timeout byte
|
||||||
eewr(rx());
|
" clr r26 \n\t"
|
||||||
} while (--g_cnt);
|
"2: ser r21 \n\t"
|
||||||
}
|
"3: lds r16, %[ucsr0a] \n\t"
|
||||||
break;
|
" sbrc r16, 7 \n\t" // UCSR0A bit 7 = RXC0
|
||||||
case 'c': // read the config page
|
" rjmp 4f \n\t"
|
||||||
read_config:
|
" dec r21 \n\t"
|
||||||
g_addr = app_end;
|
" brne 3b \n\t"
|
||||||
g_cnt = page;
|
" sbiw r26, 1 \n\t"
|
||||||
sendf();
|
" brcc 2b \n\t"
|
||||||
break;
|
" sec \n\t" // timed out
|
||||||
case 'C': // replace the config page, then echo it back to verify
|
" ret \n\t"
|
||||||
if (rcnf() != confirm)
|
"4: lds r16, %[udr0] \n\t"
|
||||||
break;
|
" clc \n\t"
|
||||||
g_addr = app_end + page;
|
" ret \n\t"
|
||||||
erase_below(); // leaves g_addr = app_end, the store target
|
:
|
||||||
store_flash();
|
: [ramend] "i"(RAMEND), [spl] "I"(_SFR_IO_ADDR(SPL)), [sph] "I"(_SFR_IO_ADDR(SPH)),
|
||||||
goto read_config;
|
[mcusr] "I"(_SFR_IO_ADDR(MCUSR)), [ubrr] "n"(tsb::baud.ubrr), [ubrr0l] "n"(_SFR_MEM_ADDR(UBRR0L)),
|
||||||
default: // 'q' or any other byte runs the application
|
[ucsr0a] "n"(_SFR_MEM_ADDR(UCSR0A)), [ucsr0b] "n"(_SFR_MEM_ADDR(UCSR0B)), [udr0] "n"(_SFR_MEM_ADDR(UDR0)),
|
||||||
appjump();
|
[spmcsr] "I"(_SFR_IO_ADDR(SPMCSR)), [spm_fill] "n"(_BV(__SPM_ENABLE)),
|
||||||
}
|
[spm_ers] "n"(_BV(PGERS) | _BV(__SPM_ENABLE)), [spm_wrt] "n"(_BV(PGWRT) | _BV(__SPM_ENABLE)),
|
||||||
}
|
[spm_rww] "n"(_BV(RWWSRE) | _BV(__SPM_ENABLE)), [eecr] "I"(_SFR_IO_ADDR(EECR)),
|
||||||
}
|
[eedr] "I"(_SFR_IO_ADDR(EEDR)), [earl] "I"(_SFR_IO_ADDR(EEARL)), [earh] "I"(_SFR_IO_ADDR(EEARH)),
|
||||||
|
[appcfg] "i"(tsb::app_end), [appto] "i"(tsb::app_end + 2), [eeend1] "i"(tsb::eeprom_end + 1),
|
||||||
} // namespace
|
[info] "i"(&tsb::info[0]), [page] "n"(tsb::page), [words] "n"(tsb::page / 2), [actmin] "n"(tsb::act_min),
|
||||||
} // namespace tsb
|
[commto] "n"(tsb::comm_timeout), [cfm] "n"(tsb::confirm), [req] "n"(tsb::request), [knock] "n"(tsb::knock)
|
||||||
|
: "r0", "r1", "r16", "r17", "r18", "r19", "r20", "r21", "r22", "r23", "r24", "r25", "r26", "r27", "r30", "r31",
|
||||||
// Reset lands here: BOOTRST vectors to the boot section base and .vectors is
|
"cc", "memory");
|
||||||
// laid first, so this is the first instruction executed. No crt ran, so set
|
|
||||||
// the stack pointer before anything is called.
|
|
||||||
extern "C" [[gnu::naked, gnu::used, gnu::section(".vectors")]] void __boot_entry()
|
|
||||||
{
|
|
||||||
SP = RAMEND;
|
|
||||||
// The one line of crt this loader needs: compiled code assumes
|
|
||||||
// __zero_reg__ (r1) is 0, and power-on registers are undefined.
|
|
||||||
asm volatile("clr __zero_reg__");
|
|
||||||
tsb::run();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ using serial_t = dev::uart0<{.baud = 115200_Bd, .max_baud_error = 3_pct, .half_d
|
|||||||
inline constexpr serial_t serial{};
|
inline constexpr serial_t serial{};
|
||||||
|
|
||||||
namespace tsb {
|
namespace tsb {
|
||||||
namespace {
|
|
||||||
|
|
||||||
// The loader is purely polled — it never enables interrupts — so every SPM and
|
// The loader is purely polled — it never enables interrupts — so every SPM and
|
||||||
// EEPROM lock folds to nothing under this posture.
|
// EEPROM lock folds to nothing under this posture.
|
||||||
@@ -45,7 +44,7 @@ constexpr std::uint16_t app_end = spm::flash_bytes - boot_bytes - page;
|
|||||||
constexpr std::uint16_t eeprom_end = avr::hw::db.mem.eeprom_size - 1;
|
constexpr std::uint16_t eeprom_end = avr::hw::db.mem.eeprom_size - 1;
|
||||||
|
|
||||||
// Firmware version stamp: YY*512 + MM*32 + DD, the encoding the host decodes.
|
// Firmware version stamp: YY*512 + MM*32 + DD, the encoding the host decodes.
|
||||||
constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 20;
|
constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 19;
|
||||||
|
|
||||||
// The 16-byte device-info block the host reads on activation. A flash_table
|
// The 16-byte device-info block the host reads on activation. A flash_table
|
||||||
// keeps it in progmem with no .data image (there is no crt to copy one).
|
// keeps it in progmem with no .data image (there is no crt to copy one).
|
||||||
@@ -63,6 +62,10 @@ inline constexpr std::array<std::uint8_t, 16> info_data = {
|
|||||||
// clang-format on
|
// clang-format on
|
||||||
using info = avr::flash_table<info_data>;
|
using info = avr::flash_table<info_data>;
|
||||||
|
|
||||||
|
// One page staged in SRAM. Scratch that is always filled before it is read, so
|
||||||
|
// it lives in .noinit — no startup clear (there is no crt) and no .text bytes.
|
||||||
|
[[gnu::section(".noinit")]] std::uint8_t buffer[page];
|
||||||
|
|
||||||
// Blocking byte read/write over the one-wire line: read() releases the line to
|
// Blocking byte read/write over the one-wire line: read() releases the line to
|
||||||
// the receiver, write() takes it and holds it until the frame is out.
|
// the receiver, write() takes it and holds it until the frame is out.
|
||||||
std::uint8_t rx()
|
std::uint8_t rx()
|
||||||
@@ -81,18 +84,25 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Stream `count` bytes to the host, from flash (LPM) or from EEPROM.
|
// Stream `count` bytes to the host, from flash (LPM) or from EEPROM.
|
||||||
void send_flash(std::uint16_t addr, std::uint8_t count)
|
void send_flash(std::uint16_t addr, std::uint16_t count)
|
||||||
{
|
{
|
||||||
while (count--)
|
while (count--)
|
||||||
tx(avr::flash_load(flash_ptr(addr++)));
|
tx(avr::flash_load(flash_ptr(addr++)));
|
||||||
}
|
}
|
||||||
|
|
||||||
void send_eeprom(std::uint16_t addr, std::uint8_t count)
|
void send_eeprom(std::uint16_t addr, std::uint16_t count)
|
||||||
{
|
{
|
||||||
while (count--)
|
while (count--)
|
||||||
tx(ee::read(addr++));
|
tx(ee::read(addr++));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Take one page from the host into the SRAM buffer.
|
||||||
|
void get_page()
|
||||||
|
{
|
||||||
|
for (std::uint16_t i = 0; i < page; ++i)
|
||||||
|
buffer[i] = rx();
|
||||||
|
}
|
||||||
|
|
||||||
// Prompt the host with '?' and report whether it answered '!'.
|
// Prompt the host with '?' and report whether it answered '!'.
|
||||||
bool request_confirm()
|
bool request_confirm()
|
||||||
{
|
{
|
||||||
@@ -100,56 +110,39 @@ bool request_confirm()
|
|||||||
return rx() == confirm;
|
return rx() == confirm;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stream one page from the host straight into the already-erased flash page at
|
// Program the SRAM buffer into one already-erased flash page (low byte then
|
||||||
// `addr`, filling the SPM word buffer low byte then high — no SRAM staging, so
|
// high, as the SPM word buffer wants).
|
||||||
// receiving and programming are the same loop.
|
void write_flash_page(std::uint16_t addr)
|
||||||
void store_flash_page(std::uint16_t addr)
|
|
||||||
{
|
{
|
||||||
for (std::uint16_t i = 0; i < page; i += 2) {
|
spm::fill<off>(addr, std::span<const std::uint8_t>{buffer, page});
|
||||||
std::uint8_t lo = rx();
|
|
||||||
std::uint8_t hi = rx();
|
|
||||||
spm::fill<off>(addr + i, static_cast<std::uint16_t>(lo | (hi << 8)));
|
|
||||||
}
|
|
||||||
spm::write_page<off>(addr);
|
spm::write_page<off>(addr);
|
||||||
spm::wait();
|
spm::wait();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stream one page from the host straight into EEPROM, byte by byte.
|
// Write the SRAM buffer into EEPROM byte by byte.
|
||||||
void store_eeprom_page(std::uint16_t addr)
|
void write_eeprom_page(std::uint16_t addr)
|
||||||
{
|
{
|
||||||
for (std::uint16_t i = 0; i < page; ++i)
|
for (std::uint16_t i = 0; i < page; ++i)
|
||||||
ee::write<off>(addr + i, rx());
|
ee::write<off>(addr + i, buffer[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Erase one flash page and wait it out — the erase step shared by the whole-app
|
// Erase the whole application, one page at a time (unwritten pages stay erased).
|
||||||
// erase, the config-page rewrite and the emergency wipe.
|
|
||||||
void erase_page(std::uint16_t addr)
|
|
||||||
{
|
|
||||||
spm::erase_page<off>(addr);
|
|
||||||
spm::wait();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Erase the whole application, one page at a time, top-down as the reference
|
|
||||||
// loader does (unwritten pages stay erased and the host cannot observe the
|
|
||||||
// order; the loop bound becomes a compare with zero).
|
|
||||||
void erase_application()
|
void erase_application()
|
||||||
{
|
{
|
||||||
for (std::uint16_t a = app_end; a != 0;) {
|
for (std::uint16_t a = 0; a < app_end; a += page) {
|
||||||
a -= page;
|
spm::erase_page<off>(a);
|
||||||
erase_page(a);
|
spm::wait();
|
||||||
}
|
}
|
||||||
spm::rww_enable<off>();
|
spm::rww_enable<off>();
|
||||||
}
|
}
|
||||||
|
|
||||||
// The application's reset vector; the linker pins it to 0x0000 (--defsym).
|
// Run the application: reset vector at 0x0000. Any non-command byte, a wrong
|
||||||
extern "C" [[noreturn]] void tsb_app();
|
// password, or an idle programmer port lands here.
|
||||||
|
|
||||||
// Run the application. Any non-command byte, a wrong password, or an idle
|
|
||||||
// programmer port lands here.
|
|
||||||
[[noreturn]] void appjump()
|
[[noreturn]] void appjump()
|
||||||
{
|
{
|
||||||
spm::wait(); // make sure any pending SPM finished before handing over
|
spm::wait(); // make sure any pending SPM finished before handing over
|
||||||
tsb_app();
|
reinterpret_cast<void (*)()>(0)();
|
||||||
|
__builtin_unreachable();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 'f': stream the application flash back, one page per host '!'. Self-terminates
|
// 'f': stream the application flash back, one page per host '!'. Self-terminates
|
||||||
@@ -178,15 +171,19 @@ void read_eeprom()
|
|||||||
void write_flash()
|
void write_flash()
|
||||||
{
|
{
|
||||||
erase_application();
|
erase_application();
|
||||||
for (std::uint16_t a = 0; request_confirm(); a += page)
|
for (std::uint16_t a = 0; request_confirm(); a += page) {
|
||||||
store_flash_page(a);
|
get_page();
|
||||||
|
write_flash_page(a);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 'E': take pages the host offers behind '?' into EEPROM.
|
// 'E': take pages the host offers behind '?' into EEPROM.
|
||||||
void write_eeprom()
|
void write_eeprom()
|
||||||
{
|
{
|
||||||
for (std::uint16_t a = 0; request_confirm(); a += page)
|
for (std::uint16_t a = 0; request_confirm(); a += page) {
|
||||||
store_eeprom_page(a);
|
get_page();
|
||||||
|
write_eeprom_page(a);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 'C': replace the config page, then echo it back for the host to verify.
|
// 'C': replace the config page, then echo it back for the host to verify.
|
||||||
@@ -194,8 +191,10 @@ void write_config()
|
|||||||
{
|
{
|
||||||
if (!request_confirm())
|
if (!request_confirm())
|
||||||
return;
|
return;
|
||||||
erase_page(app_end);
|
get_page();
|
||||||
store_flash_page(app_end);
|
spm::erase_page<off>(app_end);
|
||||||
|
spm::wait();
|
||||||
|
write_flash_page(app_end);
|
||||||
spm::rww_enable<off>();
|
spm::rww_enable<off>();
|
||||||
send_flash(app_end, page);
|
send_flash(app_end, page);
|
||||||
}
|
}
|
||||||
@@ -208,7 +207,8 @@ void emergency_erase()
|
|||||||
erase_application();
|
erase_application();
|
||||||
for (std::uint16_t a = 0; a <= eeprom_end; ++a)
|
for (std::uint16_t a = 0; a <= eeprom_end; ++a)
|
||||||
ee::write<off>(a, 0xff);
|
ee::write<off>(a, 0xff);
|
||||||
erase_page(app_end);
|
spm::erase_page<off>(app_end);
|
||||||
|
spm::wait();
|
||||||
spm::rww_enable<off>();
|
spm::rww_enable<off>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,7 +245,7 @@ gate password_gate()
|
|||||||
// Activation: the host knocks three '@' inside a window whose length is the
|
// Activation: the host knocks three '@' inside a window whose length is the
|
||||||
// config page's timeout byte (floored so a corrupt page can never lock the
|
// config page's timeout byte (floored so a corrupt page can never lock the
|
||||||
// loader out). An idle port times out and boots the application.
|
// loader out). An idle port times out and boots the application.
|
||||||
__uint24 idle = static_cast<__uint24>(avr::flash_load(flash_ptr(app_end + 2)) | 16) << 16;
|
std::uint32_t idle = static_cast<std::uint32_t>(avr::flash_load(flash_ptr(app_end + 2)) | 16) << 16;
|
||||||
std::uint8_t knocks = 0;
|
std::uint8_t knocks = 0;
|
||||||
while (knocks < 3) {
|
while (knocks < 3) {
|
||||||
if (auto byte = serial.read())
|
if (auto byte = serial.read())
|
||||||
@@ -292,7 +292,6 @@ gate password_gate()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
|
||||||
} // namespace tsb
|
} // namespace tsb
|
||||||
|
|
||||||
// Reset lands here: BOOTRST vectors to the boot section base and .vectors is
|
// Reset lands here: BOOTRST vectors to the boot section base and .vectors is
|
||||||
@@ -301,8 +300,5 @@ gate password_gate()
|
|||||||
extern "C" [[gnu::naked, gnu::used, gnu::section(".vectors")]] void __boot_entry()
|
extern "C" [[gnu::naked, gnu::used, gnu::section(".vectors")]] void __boot_entry()
|
||||||
{
|
{
|
||||||
SP = RAMEND;
|
SP = RAMEND;
|
||||||
// The one line of crt this loader needs: compiled code assumes
|
|
||||||
// __zero_reg__ (r1) is 0, and power-on registers are undefined.
|
|
||||||
asm volatile("clr __zero_reg__");
|
|
||||||
tsb::run();
|
tsb::run();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,15 @@
|
|||||||
// TinySafeBoot on libavr — tier 2: C++ with compiler trickery, no assembly.
|
// TinySafeBoot on libavr — tier 2: C++ with compiler trickery.
|
||||||
//
|
//
|
||||||
// The full TinySafeBoot feature set — watchdog bail, one-wire half-duplex,
|
// Same protocol, libavr surface and full feature set as the pure variant
|
||||||
// config-page activation timeout, password gate, emergency erase, and
|
// (tsb_pure.cpp) — watchdog bail, one-wire, config-page timeout, password gate,
|
||||||
// config/flash/EEPROM read-write — in pure C++, 526 bytes: 14 over the 512-byte
|
// emergency erase, config/flash/EEPROM read-write — but the readable
|
||||||
// boot section the hand-written oracle fits, from 168 over at this tier's first
|
// one-handler-per-command shape is traded for size. Flash and EEPROM share a
|
||||||
// floor. The structure mirrors the oracle's: a handful of tiny noinline
|
// single code path selected by a *runtime* flag decoded from the command byte,
|
||||||
// primitives sharing one whole-loader register allocation, expressed as global
|
// so the compiler cannot constant-propagate it into two clones; attributes
|
||||||
// register variables so no helper ever saves, spills, or reloads any of it.
|
// (noinline/noclone) pin that sharing down; the hot page address and byte
|
||||||
//
|
// counter live in call-saved global registers to erase the prologue push/pop
|
||||||
// The register protocol (all call-saved, so calls preserve them by ABI):
|
// that C++ function decomposition otherwise pays; and pages stream straight to
|
||||||
// Y (r28:r29) g_addr the walked flash/EEPROM address — adiw-able
|
// SPM/EEPROM with no SRAM staging. No inline assembly.
|
||||||
// r16 g_cnt byte countdown of the running block — ldi-able
|
|
||||||
// r7 g_window rx timeout, roughly 30 ms units at 16 MHz
|
|
||||||
// r6 g_receiving one-wire direction latch, cleared at bring-up
|
|
||||||
// (power-on registers are undefined)
|
|
||||||
//
|
|
||||||
// GCC 16.1 miscompiles stores into global register variables: an update whose
|
|
||||||
// remaining uses all hide inside callees is deleted whenever a CALL follows it
|
|
||||||
// before any jump/ret (the backend's liveness walk lumps fixed registers with
|
|
||||||
// call-clobbered ones — minimal repro in libavr's
|
|
||||||
// local/scratch/probes/gcc-avr-globalreg-repro.cpp, lessons.md entry). Every
|
|
||||||
// g_* update below therefore sits where a *local* read or a jump/ret follows
|
|
||||||
// it — the helpers advance g_addr immediately before returning, and rx()
|
|
||||||
// re-floors the window on every call instead of storing the floored value
|
|
||||||
// once. The layout is load-bearing; do not "simplify" it.
|
|
||||||
//
|
|
||||||
// The wire protocol is strict request/response, which is what makes the shared
|
|
||||||
// line safe: the device drives it only between a received command and its
|
|
||||||
// reply, and releases it (RXEN0 only) whenever it waits.
|
|
||||||
|
|
||||||
#include <libavr/libavr.hpp>
|
#include <libavr/libavr.hpp>
|
||||||
|
|
||||||
@@ -36,328 +18,255 @@
|
|||||||
using namespace avr::literals;
|
using namespace avr::literals;
|
||||||
namespace spm = avr::spm;
|
namespace spm = avr::spm;
|
||||||
namespace ee = avr::eeprom;
|
namespace ee = avr::eeprom;
|
||||||
namespace hw = avr::hw;
|
|
||||||
|
using dev = avr::device<{.clock = 16_MHz}>;
|
||||||
|
using serial_t = dev::uart0<{.baud = 115200_Bd, .max_baud_error = 3_pct, .half_duplex = true}>;
|
||||||
|
inline constexpr serial_t serial{};
|
||||||
|
|
||||||
namespace tsb {
|
namespace tsb {
|
||||||
namespace {
|
|
||||||
|
|
||||||
// The loader is purely polled — it never enables interrupts — so every SPM and
|
|
||||||
// EEPROM lock folds to nothing under this posture.
|
|
||||||
constexpr auto off = avr::irq::guard_policy::unused;
|
constexpr auto off = avr::irq::guard_policy::unused;
|
||||||
|
|
||||||
constexpr std::uint8_t confirm = '!';
|
constexpr std::uint8_t confirm = '!';
|
||||||
constexpr std::uint8_t request = '?';
|
constexpr std::uint8_t request = '?';
|
||||||
constexpr std::uint8_t knock = '@';
|
constexpr std::uint8_t knock = '@';
|
||||||
|
|
||||||
// Boot geometry for the 1 KB boot section (BOOTSZ=10); the page size and the
|
|
||||||
// flash/EEPROM extents are the chip database's to know. app_end is the config
|
|
||||||
// page (TSB's LASTPAGE), one page below the boot section.
|
|
||||||
constexpr std::uint16_t page = spm::page_bytes;
|
constexpr std::uint16_t page = spm::page_bytes;
|
||||||
constexpr std::uint16_t boot_bytes = 1024;
|
constexpr std::uint16_t boot_bytes = 1024;
|
||||||
constexpr std::uint16_t app_end = spm::flash_bytes - boot_bytes - page;
|
constexpr std::uint16_t app_end = spm::flash_bytes - boot_bytes - page;
|
||||||
constexpr std::uint16_t eeprom_end = avr::hw::db.mem.eeprom_size - 1;
|
constexpr std::uint16_t eeprom_end = avr::hw::db.mem.eeprom_size - 1;
|
||||||
|
|
||||||
// Lockout-proof floor for the activation window (the oracle's F_CPU/1MHz).
|
constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 19;
|
||||||
constexpr std::uint8_t act_min = 16;
|
|
||||||
// Post-activation window: the host gets seconds, not milliseconds, mid-session.
|
|
||||||
constexpr std::uint8_t comm_window = 200;
|
|
||||||
|
|
||||||
constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 20;
|
|
||||||
|
|
||||||
// Fixed 115200 8N1; the library solves UBRR + U2X from clock and baud.
|
|
||||||
constexpr auto baud = avr::uart::detail::solve_baud(16_MHz, 115200_Bd);
|
|
||||||
|
|
||||||
// The 16-byte device-info block, streamed out on activation.
|
|
||||||
// clang-format off
|
// clang-format off
|
||||||
[[gnu::progmem]] constexpr std::uint8_t info[16] = {
|
[[gnu::progmem]] constexpr std::uint8_t info[16] = {
|
||||||
'T', 'S', 'B',
|
'T', 'S', 'B',
|
||||||
build_date & 0xFF, build_date >> 8,
|
build_date & 0xFF, build_date >> 8,
|
||||||
0xF3, // status: native-UART fixed-baud lineage
|
0xF3,
|
||||||
0x1E, 0x95, 0x0F, // ATmega328P signature
|
0x1E, 0x95, 0x0F,
|
||||||
page / 2, // page size in words
|
page / 2,
|
||||||
(app_end / 2) & 0xFF, (app_end / 2) >> 8,
|
(app_end / 2) & 0xFF, (app_end / 2) >> 8,
|
||||||
eeprom_end & 0xFF, eeprom_end >> 8,
|
eeprom_end & 0xFF, eeprom_end >> 8,
|
||||||
0xAA, 0xAA,
|
0xAA, 0xAA,
|
||||||
};
|
};
|
||||||
// clang-format on
|
// clang-format on
|
||||||
|
|
||||||
register std::uint16_t g_addr asm("r28");
|
// The hot page walk lives in call-saved global registers, TSB-style: g_addr is
|
||||||
register std::uint8_t g_cnt asm("r16");
|
// the running flash/EEPROM byte address, g_cnt the byte countdown. Being global
|
||||||
register std::uint8_t g_window asm("r7");
|
// they are never spilled around the rx/tx/spm calls the way a local would be —
|
||||||
register std::uint8_t g_receiving asm("r6");
|
// r4-r7 are call-saved, so the library's UART and SPM helpers preserve them.
|
||||||
|
register std::uint16_t g_addr asm("r4");
|
||||||
|
register std::uint8_t g_cnt asm("r6");
|
||||||
|
|
||||||
|
std::uint8_t rx()
|
||||||
|
{
|
||||||
|
return serial.read_blocking();
|
||||||
|
}
|
||||||
|
|
||||||
|
void tx(std::uint8_t byte)
|
||||||
|
{
|
||||||
|
serial.write(byte);
|
||||||
|
}
|
||||||
|
|
||||||
const std::uint8_t *flash_ptr(std::uint16_t addr)
|
const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||||
{
|
{
|
||||||
return reinterpret_cast<const std::uint8_t *>(addr);
|
return reinterpret_cast<const std::uint8_t *>(addr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bounded byte receive, the oracle's shape: release the one-wire line on a
|
// Stream g_cnt bytes to the host from flash (LPM) or EEPROM, memory chosen at
|
||||||
// direction change, poll RXC0 under nested countdowns, 0 on silence. The 0
|
// run time so the optimiser cannot split the loop into two clones.
|
||||||
// then falls through every compare — not a knock, not a confirm, not a
|
[[gnu::noinline, gnu::noclone]] void send(bool flash)
|
||||||
// command — so a silent host unwinds the loader to the application from
|
|
||||||
// anywhere, and a mid-session cable pull cannot wedge it.
|
|
||||||
[[gnu::noinline, gnu::noclone]] std::uint8_t rx()
|
|
||||||
{
|
{
|
||||||
if (!g_receiving) {
|
|
||||||
g_receiving = 1;
|
|
||||||
hw::ucsr0b::write(hw::ucsr0b::rxen0(1)); // RXEN0 alone: release and listen
|
|
||||||
}
|
|
||||||
// act_min ORs in here, per call, not once into g_window at setup — the
|
|
||||||
// one placement the global-register-store miscompile cannot delete.
|
|
||||||
std::uint16_t outer = static_cast<std::uint16_t>(g_window | act_min) << 8;
|
|
||||||
do {
|
do {
|
||||||
std::uint8_t fine = 0;
|
tx(flash ? avr::flash_load(flash_ptr(g_addr)) : ee::read(g_addr));
|
||||||
do {
|
|
||||||
auto status = hw::ucsr0a::read();
|
|
||||||
if (status & hw::ucsr0a::rxc0(1).value)
|
|
||||||
return hw::udr0::read();
|
|
||||||
} while (--fine);
|
|
||||||
} while (--outer);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// One-wire transmit: take the line (TXEN0 alone — the receiver must be off
|
|
||||||
// while driving) on a direction change, with a turn-around guard so a shorted
|
|
||||||
// peer can switch first; then hold the line until the whole frame is out
|
|
||||||
// (TXC0, not UDRE0 — the stop bit must be on the wire before a caller may
|
|
||||||
// release the line), and W1C TXC0 by storing the sampled status back, which
|
|
||||||
// keeps U2X0.
|
|
||||||
[[gnu::noinline, gnu::noclone]] void tx(std::uint8_t byte)
|
|
||||||
{
|
|
||||||
if (g_receiving) {
|
|
||||||
g_receiving = 0;
|
|
||||||
hw::ucsr0b::write(hw::ucsr0b::txen0(1));
|
|
||||||
for (std::uint8_t guard = 46; guard; --guard)
|
|
||||||
;
|
|
||||||
}
|
|
||||||
hw::udr0::write(byte);
|
|
||||||
std::uint8_t status;
|
|
||||||
do {
|
|
||||||
status = hw::ucsr0a::read();
|
|
||||||
} while (!(status & hw::ucsr0a::txc0(1).value));
|
|
||||||
hw::ucsr0a::write(status);
|
|
||||||
}
|
|
||||||
|
|
||||||
// '?', then hand back the host's reply for the callers' one-byte compare.
|
|
||||||
[[gnu::noinline, gnu::noclone]] std::uint8_t rcnf()
|
|
||||||
{
|
|
||||||
tx(request);
|
|
||||||
return rx();
|
|
||||||
}
|
|
||||||
|
|
||||||
// One flash byte ← [g_addr++] (the advance right before ret — see header).
|
|
||||||
[[gnu::noinline, gnu::noclone]] std::uint8_t sflash()
|
|
||||||
{
|
|
||||||
std::uint8_t byte = avr::flash_load(flash_ptr(g_addr));
|
|
||||||
++g_addr;
|
++g_addr;
|
||||||
return byte;
|
|
||||||
}
|
|
||||||
|
|
||||||
// One EEPROM byte ← [g_addr++].
|
|
||||||
[[gnu::noinline, gnu::noclone]] std::uint8_t eerd()
|
|
||||||
{
|
|
||||||
std::uint8_t byte = ee::read(g_addr);
|
|
||||||
++g_addr;
|
|
||||||
return byte;
|
|
||||||
}
|
|
||||||
|
|
||||||
// One EEPROM byte → [g_addr++].
|
|
||||||
[[gnu::noinline, gnu::noclone]] void eewr(std::uint8_t byte)
|
|
||||||
{
|
|
||||||
ee::write<off>(g_addr, byte);
|
|
||||||
++g_addr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stream g_cnt flash bytes from g_addr to the host.
|
|
||||||
[[gnu::noinline, gnu::noclone]] void sendf()
|
|
||||||
{
|
|
||||||
do {
|
|
||||||
tx(sflash());
|
|
||||||
} while (--g_cnt);
|
} while (--g_cnt);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait out a running SPM op, then re-open the RWW section — after every page
|
[[gnu::noinline]] bool request_confirm()
|
||||||
// op and before handing over, as the oracle does.
|
|
||||||
[[gnu::noinline, gnu::noclone]] void settle()
|
|
||||||
{
|
{
|
||||||
|
tx(request);
|
||||||
|
return rx() == confirm;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stream one page straight from the host into the already-erased flash page at
|
||||||
|
// g_addr (SPM word buffer, low byte then high) or into EEPROM — no SRAM staging,
|
||||||
|
// so receive and store are one loop. The memory is a run-time flag.
|
||||||
|
[[gnu::noinline, gnu::noclone]] void store_page(bool flash)
|
||||||
|
{
|
||||||
|
g_cnt = 0;
|
||||||
|
if (flash) {
|
||||||
|
do {
|
||||||
|
std::uint8_t lo = rx();
|
||||||
|
std::uint8_t hi = rx();
|
||||||
|
spm::fill<off>(g_addr + g_cnt, static_cast<std::uint16_t>(lo | (hi << 8)));
|
||||||
|
g_cnt += 2;
|
||||||
|
} while (g_cnt != page);
|
||||||
|
spm::write_page<off>(g_addr);
|
||||||
|
spm::wait();
|
||||||
|
} else {
|
||||||
|
do {
|
||||||
|
ee::write<off>(g_addr + g_cnt, rx());
|
||||||
|
} while (++g_cnt != page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[[noreturn]] void appjump()
|
||||||
|
{
|
||||||
|
spm::wait();
|
||||||
|
reinterpret_cast<void (*)()>(0)();
|
||||||
|
__builtin_unreachable();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Erase the whole application, one page at a time.
|
||||||
|
[[gnu::noinline]] void erase_application()
|
||||||
|
{
|
||||||
|
g_addr = 0;
|
||||||
|
do {
|
||||||
|
spm::erase_page<off>(g_addr);
|
||||||
|
spm::wait();
|
||||||
|
g_addr += page;
|
||||||
|
} while (g_addr < app_end);
|
||||||
|
spm::rww_enable<off>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 'f'/'e': stream memory back one page per host '!'. send advances g_addr, so
|
||||||
|
// flash self-terminates at the application boundary; EEPROM runs until the host
|
||||||
|
// stops.
|
||||||
|
[[gnu::noinline]] void read_mem(bool flash)
|
||||||
|
{
|
||||||
|
g_addr = 0;
|
||||||
|
for (;;) {
|
||||||
|
if (rx() != confirm)
|
||||||
|
return;
|
||||||
|
g_cnt = page;
|
||||||
|
send(flash);
|
||||||
|
if (flash && g_addr >= app_end)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 'F'/'E': flash erases the whole application first, then both take the pages
|
||||||
|
// the host offers behind '?'.
|
||||||
|
[[gnu::noinline]] void write_mem(bool flash)
|
||||||
|
{
|
||||||
|
if (flash)
|
||||||
|
erase_application();
|
||||||
|
g_addr = 0;
|
||||||
|
while (request_confirm()) {
|
||||||
|
store_page(flash);
|
||||||
|
g_addr += page;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 'C': replace the config page, then echo it back for the host to verify.
|
||||||
|
void write_config()
|
||||||
|
{
|
||||||
|
if (!request_confirm())
|
||||||
|
return;
|
||||||
|
g_addr = app_end;
|
||||||
|
spm::erase_page<off>(g_addr);
|
||||||
|
spm::wait();
|
||||||
|
store_page(true);
|
||||||
|
spm::rww_enable<off>();
|
||||||
|
g_addr = app_end;
|
||||||
|
g_cnt = page;
|
||||||
|
send(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emergency erase: wipe the application flash, the EEPROM and the config page.
|
||||||
|
[[gnu::noinline]] void emergency_erase()
|
||||||
|
{
|
||||||
|
erase_application();
|
||||||
|
g_addr = 0;
|
||||||
|
do {
|
||||||
|
ee::write<off>(g_addr, 0xff);
|
||||||
|
} while (++g_addr <= eeprom_end);
|
||||||
|
spm::erase_page<off>(app_end);
|
||||||
spm::wait();
|
spm::wait();
|
||||||
spm::rww_enable<off>();
|
spm::rww_enable<off>();
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --defsym=tsb_app=0
|
// The password gate. A byte of 0 requests emergency erase; a wrong byte hangs
|
||||||
|
// the loader (still draining the line), so it can never fall through to erase.
|
||||||
|
enum class gate : std::uint8_t { pass, emergency };
|
||||||
|
|
||||||
[[noreturn]] void appjump()
|
[[gnu::noinline]] gate password_gate()
|
||||||
{
|
{
|
||||||
settle();
|
for (const std::uint8_t *pw = flash_ptr(app_end + 3);; ++pw) {
|
||||||
tsb_app();
|
std::uint8_t expected = avr::flash_load(pw);
|
||||||
|
if (expected == 0xff)
|
||||||
|
return gate::pass;
|
||||||
|
std::uint8_t got = rx();
|
||||||
|
if (got == 0)
|
||||||
|
return gate::emergency;
|
||||||
|
if (got != expected)
|
||||||
|
for (;;)
|
||||||
|
rx();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step g_addr one page down and erase that page. The decrement lives in here,
|
[[noreturn]] void run()
|
||||||
// before the erase's own use of it, not in the caller's loop where a following
|
|
||||||
// call would get it deleted (see header).
|
|
||||||
[[gnu::noinline, gnu::noclone]] void erase_below()
|
|
||||||
{
|
{
|
||||||
g_addr -= page;
|
if (avr::hw::mcusr::wdrf.test())
|
||||||
spm::erase_page<off>(g_addr);
|
|
||||||
settle();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Erase the whole application, top-down like the oracle: the loop bound is a
|
|
||||||
// compare with zero, and g_addr = 0 — the value every caller wants next — is
|
|
||||||
// handed back for free.
|
|
||||||
[[gnu::noinline, gnu::noclone]] void erase_application()
|
|
||||||
{
|
|
||||||
g_addr = app_end;
|
|
||||||
do {
|
|
||||||
erase_below();
|
|
||||||
} while (g_addr != 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stream one host page into the erased flash page at g_addr (SPM word buffer,
|
|
||||||
// low byte then high) — no SRAM staging, receive and program are one loop.
|
|
||||||
// g_addr is left at the next page base.
|
|
||||||
[[gnu::noinline, gnu::noclone]] void store_flash()
|
|
||||||
{
|
|
||||||
g_cnt = page / 2;
|
|
||||||
do {
|
|
||||||
std::uint16_t word = rx();
|
|
||||||
word |= static_cast<std::uint16_t>(rx()) << 8;
|
|
||||||
spm::fill<off>(g_addr, word);
|
|
||||||
g_addr += 2;
|
|
||||||
} while (--g_cnt);
|
|
||||||
spm::write_page<off>(g_addr - page);
|
|
||||||
settle();
|
|
||||||
}
|
|
||||||
|
|
||||||
[[noreturn, gnu::noinline]] void run()
|
|
||||||
{
|
|
||||||
// A watchdog reset hands straight back to the application, as the
|
|
||||||
// reference loader does, rather than re-entering the bootloader.
|
|
||||||
if (hw::mcusr::wdrf.test())
|
|
||||||
appjump();
|
appjump();
|
||||||
|
|
||||||
// Lean bring-up from reset state: UCSR0C already reads 8N1, UBRR0H reads
|
avr::init<serial_t>();
|
||||||
// 0, and rx()/tx() raise RXEN0/TXEN0 on first use — only the divisor low
|
|
||||||
// byte and U2X0 need a store. The library still does the datasheet work.
|
|
||||||
static_assert(baud.u2x && baud.ubrr < 256, "lean bring-up writes UBRR0L only, with U2X0");
|
|
||||||
hw::reg<"UBRR0">::write(static_cast<std::uint8_t>(baud.ubrr));
|
|
||||||
hw::ucsr0a::write(hw::ucsr0a::u2x0(1));
|
|
||||||
// General-purpose registers are undefined at power-on (no crt zeroes them);
|
|
||||||
// the direction latch must start "not receiving" so the first rx() enables
|
|
||||||
// the receiver. The reference loader clears its shadow register for the
|
|
||||||
// same reason.
|
|
||||||
g_receiving = 0;
|
|
||||||
|
|
||||||
// Activation: 3×'@', each inside the config page's timeout window (rx
|
std::uint32_t idle = static_cast<std::uint32_t>(avr::flash_load(flash_ptr(app_end + 2)) | 16) << 16;
|
||||||
// floors it so a corrupt page cannot lock the loader out); anything else —
|
std::uint8_t knocks = 0;
|
||||||
// including silence — hands over.
|
while (knocks < 3) {
|
||||||
g_window = avr::flash_load(flash_ptr(app_end + 2));
|
if (auto byte = serial.read())
|
||||||
for (std::uint8_t k = 3; k; --k)
|
knocks = *byte == knock ? knocks + 1 : 0;
|
||||||
if (rx() != knock)
|
else if (--idle == 0)
|
||||||
appjump();
|
appjump();
|
||||||
g_window = comm_window;
|
}
|
||||||
|
|
||||||
// Password gate (config page from app_end+3, 0xff-terminated; a blank
|
switch (password_gate()) {
|
||||||
// page is no password). A wrong byte blanks the comparison and drains the
|
case gate::pass:
|
||||||
// line forever, so a wrong password can never fall through; a 0 requests
|
|
||||||
// emergency erase behind two confirms. On pass the info block goes out;
|
|
||||||
// the emergency path skips it and drops into the command loop.
|
|
||||||
g_addr = app_end + 3;
|
|
||||||
std::uint8_t mask = 0xff;
|
|
||||||
for (;;) {
|
|
||||||
std::uint8_t expected = avr::flash_load(flash_ptr(g_addr)) & mask;
|
|
||||||
++g_addr;
|
|
||||||
if (expected == 0xff) {
|
|
||||||
g_addr = reinterpret_cast<std::uint16_t>(&info[0]);
|
g_addr = reinterpret_cast<std::uint16_t>(&info[0]);
|
||||||
g_cnt = sizeof(info);
|
g_cnt = sizeof(info);
|
||||||
sendf();
|
send(true);
|
||||||
break;
|
break;
|
||||||
}
|
case gate::emergency:
|
||||||
std::uint8_t got = rx();
|
if (!request_confirm() || !request_confirm())
|
||||||
if (got == 0) {
|
|
||||||
if (mask == 0)
|
|
||||||
continue;
|
|
||||||
if (rcnf() != confirm || rcnf() != confirm)
|
|
||||||
appjump();
|
appjump();
|
||||||
erase_application(); // leaves g_addr = 0 for the EEPROM walk
|
emergency_erase();
|
||||||
do {
|
|
||||||
eewr(0xff);
|
|
||||||
} while (g_addr <= eeprom_end);
|
|
||||||
g_addr = app_end + page;
|
|
||||||
erase_below();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (got != expected)
|
|
||||||
mask = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
tx(confirm); // Mainloop ready
|
tx(confirm); // Mainloop ready
|
||||||
g_addr = 0;
|
// Decode the command arithmetically so flash/write stay run-time values:
|
||||||
switch (rx()) {
|
// bit 5 is the case bit (upper = write), the folded-lower letter picks the
|
||||||
case 'f': // read application flash, one page per host '!'
|
// memory. A single unified path serves f/F/e/E.
|
||||||
for (;;) {
|
std::uint8_t cmd = rx();
|
||||||
if (rx() != confirm)
|
std::uint8_t lower = cmd | 0x20;
|
||||||
break;
|
bool write = (cmd & 0x20) == 0;
|
||||||
g_cnt = page;
|
if (lower == 'f' || lower == 'e') {
|
||||||
sendf();
|
bool flash = lower == 'f';
|
||||||
if (g_addr >= app_end)
|
if (write)
|
||||||
break;
|
write_mem(flash);
|
||||||
}
|
else
|
||||||
break;
|
read_mem(flash);
|
||||||
case 'F': // erase the application, then take pages behind '?'
|
} else if (lower == 'c') {
|
||||||
erase_application(); // leaves g_addr = 0, the write start
|
if (write) {
|
||||||
while (rcnf() == confirm)
|
write_config();
|
||||||
store_flash();
|
} else {
|
||||||
break;
|
|
||||||
case 'e': // read EEPROM, one page per host '!', until the host stops
|
|
||||||
for (;;) {
|
|
||||||
if (rx() != confirm)
|
|
||||||
break;
|
|
||||||
g_cnt = page;
|
|
||||||
do {
|
|
||||||
tx(eerd());
|
|
||||||
} while (--g_cnt);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'E': // take EEPROM pages behind '?'
|
|
||||||
while (rcnf() == confirm) {
|
|
||||||
g_cnt = page;
|
|
||||||
do {
|
|
||||||
eewr(rx());
|
|
||||||
} while (--g_cnt);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'c': // read the config page
|
|
||||||
read_config:
|
|
||||||
g_addr = app_end;
|
g_addr = app_end;
|
||||||
g_cnt = page;
|
g_cnt = page;
|
||||||
sendf();
|
send(true);
|
||||||
break;
|
}
|
||||||
case 'C': // replace the config page, then echo it back to verify
|
} else {
|
||||||
if (rcnf() != confirm)
|
|
||||||
break;
|
|
||||||
g_addr = app_end + page;
|
|
||||||
erase_below(); // leaves g_addr = app_end, the store target
|
|
||||||
store_flash();
|
|
||||||
goto read_config;
|
|
||||||
default: // 'q' or any other byte runs the application
|
|
||||||
appjump();
|
appjump();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
|
||||||
} // namespace tsb
|
} // namespace tsb
|
||||||
|
|
||||||
// Reset lands here: BOOTRST vectors to the boot section base and .vectors is
|
|
||||||
// laid first, so this is the first instruction executed. No crt ran, so set
|
|
||||||
// the stack pointer before anything is called.
|
|
||||||
extern "C" [[gnu::naked, gnu::used, gnu::section(".vectors")]] void __boot_entry()
|
extern "C" [[gnu::naked, gnu::used, gnu::section(".vectors")]] void __boot_entry()
|
||||||
{
|
{
|
||||||
SP = RAMEND;
|
SP = RAMEND;
|
||||||
// The one line of crt this loader needs: compiled code assumes
|
|
||||||
// __zero_reg__ (r1) is 0, and power-on registers are undefined.
|
|
||||||
asm volatile("clr __zero_reg__");
|
|
||||||
tsb::run();
|
tsb::run();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user