Compare commits
49 Commits
a285347e5f
...
v4
| Author | SHA1 | Date | |
|---|---|---|---|
| a4da885e36 | |||
| 335e494a31 | |||
| 799709efcf | |||
| 7ae80087b3 | |||
| b477f53ca5 | |||
| 84d3f679c2 | |||
| b5020a1e20 | |||
| 1dbf0089d6 | |||
| 39dfe40dbf | |||
| 8470ce3da0 | |||
| d634741015 | |||
| ab842c8d99 | |||
| a58b59c60f | |||
| ceacb61ba1 | |||
| dd7490a3df | |||
| bc24b63e65 | |||
| 332b244ac3 | |||
| 61b687120c | |||
| ef58d5d363 | |||
| 03bb1f56cf | |||
| 6a5e313530 | |||
| a28fccf475 | |||
| 92503fbb2d | |||
| 8ba023ed6b | |||
| 443fd39d31 | |||
| 08a0d77109 | |||
| 260ab2e4e3 | |||
| e4aaf5bc62 | |||
| 653602058a | |||
| 57d1c009cc | |||
| 06bb06d994 | |||
| 9a9a7ef0e8 | |||
| 4acf358dda | |||
| 11c10f986d | |||
| f587f0a26e | |||
| 27c768a102 | |||
| d989b5b8cd | |||
| 314e422b19 | |||
| 3e0717ef06 | |||
| 812be910c1 | |||
| 34529c8031 | |||
| 06bd56af50 | |||
| 75bb84fa3e | |||
| 778b8a0100 | |||
| 48d11fe8e3 | |||
| bb4784ae0c | |||
| d07586652d | |||
| 3a2f2a23c2 | |||
| 4fa058653e |
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "libavr"]
|
||||||
|
path = libavr
|
||||||
|
url = ../libavr.git
|
||||||
@@ -8,6 +8,9 @@ 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()
|
||||||
@@ -150,10 +153,7 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
if(Python3_FOUND)
|
if(Python3_FOUND)
|
||||||
add_test(NAME pureboot.pi
|
add_test(NAME pureboot.pi
|
||||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/check_pi.py
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/check_pi.py
|
||||||
${CMAKE_OBJDUMP} ${CMAKE_OBJCOPY} ${CMAKE_CXX_COMPILER} ${LIBAVR_MCU}
|
${CMAKE_OBJDUMP} ${CMAKE_NM} $<TARGET_FILE:pureboot> ${PUREBOOT_BASE_HEX})
|
||||||
$<TARGET_FILE:pureboot>
|
|
||||||
${CMAKE_BINARY_DIR}/CMakeFiles/pureboot.dir/pureboot/pureboot.cpp.obj
|
|
||||||
${PUREBOOT_BASE_HEX})
|
|
||||||
add_test(NAME pureboot.planner
|
add_test(NAME pureboot.planner
|
||||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/test_planner.py
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/test_planner.py
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py)
|
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py)
|
||||||
@@ -248,14 +248,23 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
-DLIMIT=${PUREBOOT_LIMIT} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
|
-DLIMIT=${PUREBOOT_LIMIT} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
# The autobaud loader: one clock-agnostic image per chip, so it has no
|
# The autobaud loader (pureboot/autobaud.md): one clock-agnostic image per
|
||||||
# clock x baud axis of its own — the matrix below sweeps those for the
|
# chip, no clock × baud axis, size-tested against the same per-chip budget on
|
||||||
# fixed-baud builds, and this one binary has to serve all of them at run
|
# every chip.
|
||||||
# time. Size-tested against the same per-chip budget as every other variant.
|
#
|
||||||
pureboot_add_loader(pureboot_autobaud SERIAL autobaud)
|
# Only the unified version is built. Its two predecessors —
|
||||||
add_test(NAME pureboot_autobaud.size
|
# pureboot_autobaud_pure.cpp at 508 B and pureboot_autobaud_reg.cpp at 512 —
|
||||||
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:pureboot_autobaud>
|
# had 4 B and 0 B of margin on the 1284P, and the fix for the activation hang
|
||||||
-DLIMIT=${PUREBOOT_LIMIT} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
|
# costs ~22, which puts them at 530 and 534. Neither can ship, so the choice
|
||||||
|
# the branch existed to offer is settled by measurement rather than taste.
|
||||||
|
# The sources stay for the record; autobaud.md carries the numbers.
|
||||||
|
function(pureboot_autobaud_variant name source)
|
||||||
|
pureboot_add_autobaud(${name} ${source})
|
||||||
|
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()
|
||||||
|
pureboot_autobaud_variant(pureboot_autobaud_uni pureboot_autobaud_uni.cpp)
|
||||||
|
|
||||||
# One point of the exhaustive matrix, named from its resolved parameters
|
# One point of the exhaustive matrix, named from its resolved parameters
|
||||||
# so the enumeration cannot collide with itself. Unreachable rates drop
|
# so the enumeration cannot collide with itself. Unreachable rates drop
|
||||||
@@ -296,15 +305,16 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
# sites), the largest image the space produces and a shape the ladder
|
# sites), the largest image the space produces and a shape the ladder
|
||||||
# default — always the *fastest* rate a clock reaches — never picks.
|
# default — always the *fastest* rate a clock reaches — never picks.
|
||||||
#
|
#
|
||||||
# Every chip runs the full cross product: the size-bearing classes (flash
|
# Bounded to one chip per size-bearing class: flash addressing (the
|
||||||
# addressing, hand-over shape, page size, USART inventory) are what make
|
# word-addressed 1284), hand-over shape (the patched vector on the tinies
|
||||||
# the image differ, and a chip outside them is expected to match its class
|
# and m48s), page size, and USART inventory. Everything else in the image
|
||||||
# — but "expected" is what a matrix is for, and the whole sweep is cheap
|
# is chip-independent code, so a further chip buys builds and no
|
||||||
# enough to run rather than reason about. PUREBOOT_FULL_MATRIX is what
|
# coverage; every chip outside the set carries the compact matrix.
|
||||||
# selects it; the compact matrix below is the per-commit default.
|
|
||||||
get_property(_full_bauds GLOBAL PROPERTY PUREBOOT_BAUD_LADDER)
|
get_property(_full_bauds GLOBAL PROPERTY PUREBOOT_BAUD_LADDER)
|
||||||
list(APPEND _full_bauds 16000 4800 2400 1200)
|
list(APPEND _full_bauds 16000 4800 2400 1200)
|
||||||
if(DEFINED ENV{PUREBOOT_FULL_MATRIX})
|
set(_matrix_spot attiny13a attiny85 atmega48pa atmega8a atmega168pa
|
||||||
|
atmega328p atmega164a atmega644a atmega1284p)
|
||||||
|
if(DEFINED ENV{PUREBOOT_FULL_MATRIX} AND LIBAVR_MCU IN_LIST _matrix_spot)
|
||||||
foreach(_matrix_hz IN LISTS _full_clocks)
|
foreach(_matrix_hz IN LISTS _full_clocks)
|
||||||
foreach(_matrix_baud IN LISTS _full_bauds)
|
foreach(_matrix_baud IN LISTS _full_bauds)
|
||||||
pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} software)
|
pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} software)
|
||||||
@@ -402,12 +412,14 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
add_custom_command(TARGET pbapp_autobaud POST_BUILD
|
add_custom_command(TARGET pbapp_autobaud POST_BUILD
|
||||||
COMMAND ${CMAKE_OBJCOPY} -O binary
|
COMMAND ${CMAKE_OBJCOPY} -O binary
|
||||||
$<TARGET_FILE:pbapp_autobaud> $<TARGET_FILE:pbapp_autobaud>.bin)
|
$<TARGET_FILE:pbapp_autobaud> $<TARGET_FILE:pbapp_autobaud>.bin)
|
||||||
add_test(NAME pureboot.autobaud
|
foreach(_variant uni)
|
||||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbautobaud.py
|
add_test(NAME pureboot.autobaud_${_variant}
|
||||||
${PB_DEVICE} $<TARGET_FILE:pureboot_autobaud> ${PUREBOOT_SIM_MCU}
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbautobaud.py
|
||||||
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} $<TARGET_FILE:pbapp_autobaud>.bin
|
${PB_DEVICE} $<TARGET_FILE:pureboot_autobaud_${_variant}> ${PUREBOOT_SIM_MCU}
|
||||||
1000000 9600 ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} $<TARGET_FILE:pbapp_autobaud>.bin
|
||||||
${CMAKE_BINARY_DIR}/pbautobaud-work)
|
1000000 9600 ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
set_tests_properties(pureboot.autobaud PROPERTIES TIMEOUT 240)
|
${CMAKE_BINARY_DIR}/pbautobaud-${_variant}-work)
|
||||||
|
set_tests_properties(pureboot.autobaud_${_variant} PROPERTIES TIMEOUT 240)
|
||||||
|
endforeach()
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"hidden": true,
|
"hidden": true,
|
||||||
"generator": "Ninja",
|
"generator": "Ninja",
|
||||||
"binaryDir": "${sourceDir}/build/${presetName}",
|
"binaryDir": "${sourceDir}/build/${presetName}",
|
||||||
"toolchainFile": "$env{LIBAVR_ROOT}/cmake/avr-toolchain.cmake",
|
"toolchainFile": "${sourceDir}/libavr/cmake/avr-toolchain.cmake",
|
||||||
"cacheVariables": {
|
"cacheVariables": {
|
||||||
"CMAKE_BUILD_TYPE": "Release",
|
"CMAKE_BUILD_TYPE": "Release",
|
||||||
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
|
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
|
||||||
|
|||||||
1
libavr
Submodule
1
libavr
Submodule
Submodule libavr added at edc77ca43f
@@ -194,19 +194,13 @@ function(pureboot_default_baud clock software outvar)
|
|||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
# pureboot_add_loader(<name> [CLOCK <hz>] [BAUD <bd>]
|
# pureboot_add_loader(<name> [CLOCK <hz>] [BAUD <bd>]
|
||||||
# [SERIAL auto|hardware|software|autobaud] [USART <n>]
|
# [SERIAL auto|hardware|software] [USART <n>]
|
||||||
# [RX <pin>] [TX <pin>] [TIMEOUT <s>])
|
# [RX <pin>] [TX <pin>] [TIMEOUT <s>])
|
||||||
#
|
#
|
||||||
# The loader target plus its flashable images (<name>.hex for a programmer,
|
# The loader target plus its flashable images (<name>.hex for a programmer,
|
||||||
# <name>.bin for --update-loader). The resolved deployment is stamped on the
|
# <name>.bin for --update-loader). The resolved deployment is stamped on the
|
||||||
# target as PUREBOOT_HZ / PUREBOOT_BAUD / PUREBOOT_LINK (the link spelled
|
# target as PUREBOOT_HZ / PUREBOOT_BAUD / PUREBOOT_LINK (the link spelled
|
||||||
# usart0, usart1 or sw:<RX>,<TX>) — what a test harness speaks to it with.
|
# usart0, usart1 or sw:<RX>,<TX>) — what a test harness speaks to it with.
|
||||||
#
|
|
||||||
# SERIAL autobaud measures the host's bit timing at run time, so the image
|
|
||||||
# carries no clock and no baud: CLOCK and BAUD are not build parameters there,
|
|
||||||
# and one binary per chip serves every F_CPU and every rate. The stamped
|
|
||||||
# PUREBOOT_HZ/PUREBOOT_BAUD then record what a harness should *drive* it at,
|
|
||||||
# not what it was built for.
|
|
||||||
function(pureboot_add_loader name)
|
function(pureboot_add_loader name)
|
||||||
cmake_parse_arguments(PB "" "CLOCK;BAUD;SERIAL;USART;RX;TX;TIMEOUT" "" ${ARGN})
|
cmake_parse_arguments(PB "" "CLOCK;BAUD;SERIAL;USART;RX;TX;TIMEOUT" "" ${ARGN})
|
||||||
if(PB_UNPARSED_ARGUMENTS)
|
if(PB_UNPARSED_ARGUMENTS)
|
||||||
@@ -228,8 +222,8 @@ function(pureboot_add_loader name)
|
|||||||
if(NOT PB_SERIAL)
|
if(NOT PB_SERIAL)
|
||||||
set(PB_SERIAL auto)
|
set(PB_SERIAL auto)
|
||||||
endif()
|
endif()
|
||||||
if(DEFINED PB_USART AND NOT PB_SERIAL MATCHES "^(auto|hardware)$")
|
if(DEFINED PB_USART AND PB_SERIAL STREQUAL "software")
|
||||||
message(FATAL_ERROR "pureboot_add_loader(${name}): USART ${PB_USART} contradicts SERIAL ${PB_SERIAL}")
|
message(FATAL_ERROR "pureboot_add_loader(${name}): USART ${PB_USART} contradicts SERIAL software")
|
||||||
endif()
|
endif()
|
||||||
if(DEFINED PB_USART)
|
if(DEFINED PB_USART)
|
||||||
set(PB_SERIAL hardware)
|
set(PB_SERIAL hardware)
|
||||||
@@ -258,7 +252,7 @@ function(pureboot_add_loader name)
|
|||||||
set(PB_SERIAL software)
|
set(PB_SERIAL software)
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
if(PB_SERIAL MATCHES "^(software|autobaud)$")
|
if(PB_SERIAL STREQUAL "software")
|
||||||
if(NOT PB_RX)
|
if(NOT PB_RX)
|
||||||
set(PB_RX pb0)
|
set(PB_RX pb0)
|
||||||
endif()
|
endif()
|
||||||
@@ -270,11 +264,7 @@ function(pureboot_add_loader name)
|
|||||||
message(FATAL_ERROR "pureboot_add_loader(${name}): pin '${_pin}' is not of the form pb1")
|
message(FATAL_ERROR "pureboot_add_loader(${name}): pin '${_pin}' is not of the form pb1")
|
||||||
endif()
|
endif()
|
||||||
endforeach()
|
endforeach()
|
||||||
if(PB_SERIAL STREQUAL "autobaud")
|
set(_serial_defines PUREBOOT_SOFT_SERIAL PUREBOOT_RX=${PB_RX} PUREBOOT_TX=${PB_TX})
|
||||||
set(_serial_defines PUREBOOT_AUTOBAUD PUREBOOT_RX=${PB_RX} PUREBOOT_TX=${PB_TX})
|
|
||||||
else()
|
|
||||||
set(_serial_defines PUREBOOT_SOFT_SERIAL PUREBOOT_RX=${PB_RX} PUREBOOT_TX=${PB_TX})
|
|
||||||
endif()
|
|
||||||
# sw:<RX>,<TX> as port letter and bit, upcased.
|
# sw:<RX>,<TX> as port letter and bit, upcased.
|
||||||
string(SUBSTRING ${PB_RX} 1 2 _rx_pin)
|
string(SUBSTRING ${PB_RX} 1 2 _rx_pin)
|
||||||
string(SUBSTRING ${PB_TX} 1 2 _tx_pin)
|
string(SUBSTRING ${PB_TX} 1 2 _tx_pin)
|
||||||
@@ -290,29 +280,23 @@ function(pureboot_add_loader name)
|
|||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(PB_SERIAL STREQUAL "autobaud")
|
set(_defines PUREBOOT_CLOCK_HZ=${PB_CLOCK} PUREBOOT_BAUD=${PB_BAUD} PUREBOOT_TIMEOUT=${PB_TIMEOUT}
|
||||||
# No clock and no baud reach the image; the window is a poll budget.
|
${_serial_defines})
|
||||||
set(_defines ${_serial_defines})
|
|
||||||
else()
|
|
||||||
set(_defines PUREBOOT_CLOCK_HZ=${PB_CLOCK} PUREBOOT_BAUD=${PB_BAUD} PUREBOOT_TIMEOUT=${PB_TIMEOUT}
|
|
||||||
${_serial_defines})
|
|
||||||
endif()
|
|
||||||
|
|
||||||
add_executable(${name} ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/pureboot.cpp)
|
add_executable(${name} ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/pureboot.cpp)
|
||||||
target_link_libraries(${name} PRIVATE libavr)
|
target_link_libraries(${name} PRIVATE libavr)
|
||||||
target_compile_definitions(${name} PRIVATE ${_defines})
|
target_compile_definitions(${name} PRIVATE ${_defines})
|
||||||
# Codegen shaping for the loader TU only. At -Os GCC otherwise rewrites the
|
# Codegen shaping for the loader TU only, worth 14–36 B depending on the
|
||||||
# byte-stream loops' counters into end-pointer forms that cost registers
|
# chip. At -Os GCC otherwise rewrites the byte-stream loops' counters into
|
||||||
# (-fno-ivopts, -fno-split-wide-types), leaves register pressure on the
|
# end-pointer forms that cost registers (-fno-ivopts,
|
||||||
# table with the default allocator (-fira-algorithm=priority), and keeps
|
# -fno-split-wide-types), leaves register pressure on the table with the
|
||||||
# expression temporaries in registers (-fno-tree-ter) — but every loop body
|
# default allocator (-fira-algorithm=priority), and keeps loop-invariant
|
||||||
# here contains a call, so a register held across it costs more than the
|
# immediates and expression temporaries in registers
|
||||||
# load-immediate it saves. The set is fitted to the loader's body and has to
|
# (-fno-move-loop-invariants, -fno-tree-ter) — but every loop body here
|
||||||
# be re-measured when that body changes: -fno-move-loop-invariants belonged
|
# contains a call, so a register held across it costs more than the
|
||||||
# here while the command loop carried four transfer bodies and costs bytes
|
# load-immediate it saves.
|
||||||
# now that it carries one.
|
|
||||||
target_compile_options(${name} PRIVATE
|
target_compile_options(${name} PRIVATE
|
||||||
-fno-ivopts -fira-algorithm=priority -fno-tree-ter -fno-split-wide-types)
|
-fno-ivopts -fira-algorithm=priority -fno-move-loop-invariants -fno-tree-ter -fno-split-wide-types)
|
||||||
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=pureboot_app=${_app} ${_wrap})
|
-Wl,--defsym=pureboot_app=${_app} ${_wrap})
|
||||||
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}>)
|
||||||
@@ -327,3 +311,37 @@ function(pureboot_add_loader name)
|
|||||||
PUREBOOT_LINK ${_link})
|
PUREBOOT_LINK ${_link})
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
|
# pureboot_add_autobaud(<name> <source> [RX <pin>] [TX <pin>])
|
||||||
|
#
|
||||||
|
# An autobaud software-serial loader from <source> (pureboot_autobaud_*.cpp).
|
||||||
|
# Autobaud measures the host's bit timing at runtime, so the image carries no
|
||||||
|
# clock and no baud — one binary per chip runs at any F_CPU. Same per-chip
|
||||||
|
# geometry, link and codegen flags as pureboot_add_loader(); only the clock and
|
||||||
|
# baud axes fall away. Two source files are under review (autobaud.md):
|
||||||
|
# pureboot_autobaud_pure.cpp and pureboot_autobaud_reg.cpp.
|
||||||
|
function(pureboot_add_autobaud name source)
|
||||||
|
cmake_parse_arguments(PB "" "RX;TX" "" ${ARGN})
|
||||||
|
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_autobaud(${name}): pin '${_pin}' is not of the form pb1")
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
get_property(_base_hex GLOBAL PROPERTY PUREBOOT_BASE_HEX)
|
||||||
|
get_property(_app GLOBAL PROPERTY PUREBOOT_APP)
|
||||||
|
get_property(_wrap GLOBAL PROPERTY PUREBOOT_WRAP)
|
||||||
|
|
||||||
|
add_executable(${name} ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/${source})
|
||||||
|
target_link_libraries(${name} PRIVATE libavr)
|
||||||
|
target_compile_definitions(${name} PRIVATE PUREBOOT_RX=${PB_RX} PUREBOOT_TX=${PB_TX})
|
||||||
|
target_compile_options(${name} PRIVATE
|
||||||
|
-fno-ivopts -fira-algorithm=priority -fno-move-loop-invariants -fno-tree-ter -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}>)
|
||||||
|
endfunction()
|
||||||
|
|||||||
@@ -8,51 +8,46 @@ erase, reset-vector surgery, updating the loader itself — lives in the host
|
|||||||
tool (`pureboot.py`).
|
tool (`pureboot.py`).
|
||||||
|
|
||||||
The image is **position-independent**: control flow is PC-relative, the
|
The image is **position-independent**: control flow is PC-relative, the
|
||||||
transfer paths take wire addresses, the write guard protects the slot the code
|
read/write paths take wire addresses, the write guard protects the slot the
|
||||||
is *running* in (from the runtime return address), nothing else is
|
code is *running* in (from the runtime return address), the info block is
|
||||||
flash-resident to address at all, and the application jump is an indirect call
|
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
|
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
|
every command intact, which makes pureboot **its own staging loader**: the
|
||||||
installs the same binary one slot below the resident, jumps into it, and lets
|
host installs the same binary one slot below the resident, jumps into it, and
|
||||||
it rewrite the resident. The lint holds it to that literally — the image must
|
lets it rewrite the resident.
|
||||||
come out byte-identical linked at a different base.
|
|
||||||
|
|
||||||
## Chips
|
## Chips
|
||||||
|
|
||||||
Sizes are the default configuration: the hardware USART0 at 115200 8N1 on a
|
Sizes are the default configuration: the hardware USART0 at 115200 8N1 on a
|
||||||
16 MHz crystal, or the software UART on RX = PB0 / TX = PB1 at 57600 8N1 on
|
16 MHz crystal, or the software UART on RX = PB0 / TX = PB1 at 57600 8N1 on
|
||||||
the tinies' RC oscillator (9.6 MHz on the t13s, 8 MHz above). Every axis moves
|
the tinies' RC oscillator (9.6 MHz on the t13s, 8 MHz above). Every axis moves
|
||||||
per build — see *Configuration*. The autobaud column is the clock-free build,
|
per build — see *Configuration*; the largest image any of them produces is a
|
||||||
which is the largest the space produces and the tightest fit in the matrix;
|
software UART at a slow baud, which on the 1284s is 494 B, the tightest fit in
|
||||||
it carries the calibration machinery and no clock at all.
|
the whole matrix at 18 B spare.
|
||||||
|
|
||||||
| Chip | Flash | Loader at | Link | Stock | Autobaud |
|
| Chip | Flash | Loader at | Link | Size |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| ATtiny13, ATtiny13A † | 1 KiB | 0x0200 | software | 402 B | 472 B |
|
| ATtiny13, ATtiny13A † | 1 KiB | 0x0200 | software | 416 B |
|
||||||
| ATtiny25 † | 2 KiB | 0x0600 | software | 406 B | 476 B |
|
| ATtiny25 † | 2 KiB | 0x0600 | software | 420 B |
|
||||||
| ATtiny45 † | 4 KiB | 0x0e00 | software | 410 B | 480 B |
|
| ATtiny45 † | 4 KiB | 0x0e00 | software | 424 B |
|
||||||
| ATtiny85 † | 8 KiB | 0x1e00 | software | 410 B | 480 B |
|
| ATtiny85 † | 8 KiB | 0x1e00 | software | 424 B |
|
||||||
| ATmega8, 8A | 8 KiB | 0x1e00 | USART0 | 372 B | 486 B |
|
| ATmega8, 8A | 8 KiB | 0x1e00 | USART0 | 396 B |
|
||||||
| ATmega16, 16A | 16 KiB | 0x3e00 | USART0 | 374 B | 490 B |
|
| ATmega16, 16A | 16 KiB | 0x3e00 | USART0 | 400 B |
|
||||||
| ATmega32, 32A | 32 KiB | 0x7e00 | USART0 | 374 B | 490 B |
|
| ATmega32, 32A | 32 KiB | 0x7e00 | USART0 | 400 B |
|
||||||
| ATmega48, 48A, 48P, 48PA † | 4 KiB | 0x0e00 | USART0 | 400 B | 476 B |
|
| ATmega48, 48A, 48P, 48PA † | 4 KiB | 0x0e00 | USART0 | 414 B |
|
||||||
| ATmega88, 88A, 88P, 88PA | 8 KiB | 0x1e00 | USART0 | 410 B | 486 B |
|
| ATmega88, 88A, 88P, 88PA | 8 KiB | 0x1e00 | USART0 | 434 B |
|
||||||
| ATmega168, 168A, 168P, 168PA | 16 KiB | 0x3e00 | USART0 | 412 B | 490 B |
|
| ATmega168, 168A, 168P, 168PA | 16 KiB | 0x3e00 | USART0 | 438 B |
|
||||||
| ATmega328, 328P | 32 KiB | 0x7e00 | USART0 | 412 B | 490 B |
|
| ATmega328, 328P | 32 KiB | 0x7e00 | USART0 | 438 B |
|
||||||
| ATmega164A, 164P, 164PA | 16 KiB | 0x3e00 | USART0 | 412 B | 490 B |
|
| ATmega164A, 164P, 164PA | 16 KiB | 0x3e00 | USART0 | 438 B |
|
||||||
| ATmega324A, 324P, 324PA | 32 KiB | 0x7e00 | USART0 | 412 B | 490 B |
|
| ATmega324A, 324P, 324PA | 32 KiB | 0x7e00 | USART0 | 438 B |
|
||||||
| ATmega644, 644A, 644P, 644PA | 64 KiB | 0xfe00 | USART0 | 406 B | 484 B |
|
| ATmega644, 644A, 644P, 644PA | 64 KiB | 0xfe00 | USART0 | 432 B |
|
||||||
| ATmega1284, 1284P | 128 KiB | 0x1fe00 | USART0 | 432 B | 510 B |
|
| ATmega1284, 1284P | 128 KiB | 0x1fe00 | USART0 | 478 B |
|
||||||
|
|
||||||
† No hardware boot section: the host patches the reset vector, and the budget
|
† No hardware boot section: the host patches the reset vector, and the budget
|
||||||
is 510 bytes, since the slot's last word is the trampoline.
|
is 510 bytes, since the slot's last word is the trampoline.
|
||||||
|
|
||||||
The tightest fit in the whole space is the 1284s' autobaud build, 510 of its
|
The 1284s are the heaviest because they alone carry the far-flash machinery —
|
||||||
512 — they alone carry the far-flash machinery (ELPM reads, RAMPZ page
|
ELPM reads, RAMPZ page commands, a word-addressed wire.
|
||||||
commands) and autobaud alone carries the calibration loop. Everything else has
|
|
||||||
20 B of headroom or more. The flash bank riding in a transfer's selector byte
|
|
||||||
keeps even those chips' addressing the same 16-bit form every other chip uses,
|
|
||||||
which is why they are no longer the outlier they were.
|
|
||||||
|
|
||||||
The software UART enables the RX pull-up; TX idles high. All multi-byte wire
|
The software UART enables the RX pull-up; TX idles high. All multi-byte wire
|
||||||
quantities are little-endian.
|
quantities are little-endian.
|
||||||
@@ -67,7 +62,7 @@ repo's build and by a downstream project alike:
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `CLOCK <hz>` | the clock the board runs | 16 MHz megas, 8 MHz t25/45/85, 9.6 MHz t13s |
|
| `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 |
|
| `BAUD <bd>` | the wire rate | the ladder below |
|
||||||
| `SERIAL auto\|hardware\|software\|autobaud` | the link backend | `auto`: the hardware USART where the chip has one |
|
| `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 |
|
| `USART <n>` | the USART instance (x4 megas carry two) | 0 |
|
||||||
| `RX <pin>`, `TX <pin>` | software-UART pins | `pb0`, `pb1` |
|
| `RX <pin>`, `TX <pin>` | software-UART pins | `pb0`, `pb1` |
|
||||||
| `TIMEOUT <s>` | the activation window | 8 |
|
| `TIMEOUT <s>` | the activation window | 8 |
|
||||||
@@ -79,17 +74,6 @@ receiver's 100-cycles-a-bit floor. Whatever is picked or overridden is
|
|||||||
re-checked in the compile: an infeasible combination, or a USART the chip does
|
re-checked in the compile: an infeasible combination, or a USART the chip does
|
||||||
not have, fails with a named static assert.
|
not have, fails with a named static assert.
|
||||||
|
|
||||||
`SERIAL autobaud` takes neither: the loader **measures** the host's bit timing
|
|
||||||
at run time, so `CLOCK` and `BAUD` are not build parameters there and one
|
|
||||||
binary per chip serves every clock and every rate. It is for the deployments
|
|
||||||
whose clock is not known at build time and does not hold still — the internal
|
|
||||||
RC oscillator, ±10 % from the factory and moving with supply and temperature —
|
|
||||||
where a fixed-baud software build has to be rebuilt per clock and still drifts
|
|
||||||
out of tolerance. The cost is that it is software-serial only (a hardware USART
|
|
||||||
needs its divisor programmed) and that activation counts poll iterations rather
|
|
||||||
than seconds, since there is no clock to convert them against
|
|
||||||
(`PUREBOOT_AUTOBAUD_POLLS`, default 4,000,000).
|
|
||||||
|
|
||||||
A downstream project brings its usual libavr setup (the `libavr` target, the
|
A downstream project brings its usual libavr setup (the `libavr` target, the
|
||||||
chip via the `LIBAVR_MCU` toolchain preset), consumes this directory, and
|
chip via the `LIBAVR_MCU` toolchain preset), consumes this directory, and
|
||||||
states its deployment — an ATmega328P on its shipped 1 MHz fuses with the
|
states its deployment — an ATmega328P on its shipped 1 MHz fuses with the
|
||||||
@@ -129,20 +113,9 @@ window; any other byte is discarded and awaited again, so line noise can delay
|
|||||||
the loader but never lock it. A window expiring on an idle line boots the
|
the loader but never lock it. A window expiring on an idle line boots the
|
||||||
application.
|
application.
|
||||||
|
|
||||||
An autobaud build opens differently, because it has to learn the rate before it
|
|
||||||
can read a byte at all: the host sends the **calibration byte 0xC0** — a start
|
|
||||||
bit plus six zero data bits form one low pulse of seven bit-times — and the
|
|
||||||
loader times that pulse into its bit period. A single `p` then activates; the
|
|
||||||
pulse has already proven a host is present, which the two-byte knock exists to
|
|
||||||
establish elsewhere. Both waits are bounded, so a stray low pulse with no host
|
|
||||||
behind it costs one window and then boots the application rather than holding
|
|
||||||
the loader.
|
|
||||||
|
|
||||||
The window is a compile-time constant (`TIMEOUT`, 8 s by default), so the whole
|
The window is a compile-time constant (`TIMEOUT`, 8 s by default), so the whole
|
||||||
EEPROM belongs to the application — pureboot keeps no state of its own.
|
EEPROM belongs to the application — pureboot keeps no state of its own.
|
||||||
Re-timing a deployed loader is a self-update with a re-timed build. An autobaud
|
Re-timing a deployed loader is a self-update with a re-timed build.
|
||||||
build counts poll iterations instead (`PUREBOOT_AUTOBAUD_POLLS`), there being
|
|
||||||
no clock to turn into seconds.
|
|
||||||
|
|
||||||
## Session
|
## Session
|
||||||
|
|
||||||
@@ -152,116 +125,75 @@ write and sends the prompt `+` (0x2b), which is therefore also the previous
|
|||||||
command's completion ack. A session is: await `+`, send a command, read its
|
command's completion ack. A session is: await `+`, send a command, read its
|
||||||
reply, repeat.
|
reply, repeat.
|
||||||
|
|
||||||
Addresses are **byte addresses within a 64 KiB bank**, and the bank rides in
|
On chips whose flash exceeds 64 KiB (the 1284s — info-block flag bit 1) the
|
||||||
the command's selector byte, so no command has to speak word addresses. `J` is
|
`R`/`W` flash addresses are **word** addresses; everywhere else they are byte
|
||||||
the exception: it takes a word address, because that is what the hardware's own
|
addresses (the 644s' 64 KiB is exactly the 16-bit byte space). EEPROM
|
||||||
jump takes. EEPROM and data-space addresses and all counts are bytes.
|
addresses and all counts are bytes.
|
||||||
|
|
||||||
The loader trusts the host to keep addresses in range: it does not bound them
|
The loader trusts the host to keep addresses in range: it does not bound them
|
||||||
against the chip. **Gotcha:** a write (or read) that runs past `E2END` wraps —
|
against the info block. **Gotcha:** a `w` (or `r`) that runs past `E2END` wraps
|
||||||
EEAR is only as wide as the array, so an address past the end truncates onto
|
— EEAR is only as wide as the array, so an address past the end truncates onto
|
||||||
low EEPROM and the write silently overwrites it. Keeping transfers within the
|
low EEPROM and the write silently overwrites it. Keeping writes within the
|
||||||
real sizes is the host's job (the shipped tool does); the flash budget is
|
advertised sizes is the host's job (the shipped tool does); the flash budget
|
||||||
better spent on features than on re-checking a bound the host already holds.
|
is better spent on features than on re-checking a bound the host already holds.
|
||||||
|
|
||||||
| Cmd | Arguments | Reply |
|
| Cmd | Arguments | Reply |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `b` | — | 4 bytes: the pureboot version, then the three signature bytes |
|
| `b` | — | the 12-byte info block |
|
||||||
| `G` | sel8, addr16, n8 | n bytes from the selected space (n = 0 means 256) |
|
| `R` | addr16, n8 | n flash bytes (n = 0 means 256) |
|
||||||
| `g` | sel8, addr16, n8, then n data bytes | `+` per byte, sent once its write has begun |
|
| `W` | addr16 (any address in the page), then one page of data | — (completion = next prompt) |
|
||||||
| `W` | sel8, 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 |
|
| `J` | word address (16-bit) | `+`, then execution continues there |
|
||||||
| other | — | ignored; the loop re-prompts (send a junk byte, await `+`, to resync) |
|
| other | — | ignored; the loop re-prompts (send a junk byte, await `+`, to resync) |
|
||||||
|
|
||||||
`G` and `g` are one letter in two cases, which is the whole command set for
|
`W` streams exactly one SPM page (size from the info block) into the buffer,
|
||||||
every memory: the **selector** byte's low nibble names the space and its high
|
then erases and programs — except pages inside the 512-byte slot
|
||||||
nibble carries the flash bank.
|
the loader is *running* in, which are drained and left alone, so a broken host
|
||||||
|
cannot brick the running copy and a staged copy may rewrite the resident.
|
||||||
| Space | | |
|
|
||||||
|---|---|---|
|
|
||||||
| 0 | flash | read-only here; it is written through `W` and the SPM space |
|
|
||||||
| 1 | EEPROM | |
|
|
||||||
| 2 | data | SRAM — and with it the register file and every I/O register, which share the data address space on AVR |
|
|
||||||
| 3 | fuse and lock | index 0..3 in the hardware's own Z order: low, lock, extended, high |
|
|
||||||
| 4 | SPM | write-only: the byte goes to SPMCSR and fires the instruction at the address |
|
|
||||||
|
|
||||||
The data space is worth more than it looks. pureboot keeps **zero static RAM**
|
|
||||||
and pushes no register, so at loader entry an application's SRAM is still
|
|
||||||
whatever the application left there, bar the handful of bytes of return-address
|
|
||||||
stack — which makes `G` over space 2 a post-mortem of a running application,
|
|
||||||
not just a poke hole. The same address space carries the register file and the
|
|
||||||
I/O registers, so peripheral state is readable too; reading some of those has
|
|
||||||
side effects (reading UDR clears its flags), which is the host's business to
|
|
||||||
know.
|
|
||||||
|
|
||||||
Programming a page is therefore `W` to fill the buffer, then a `g` to the SPM
|
|
||||||
space for the erase, another for the write, and on a boot-sectioned chip a
|
|
||||||
third to re-enable the RWW section — `0x03`, `0x05` and `0x11`, the SPMCSR
|
|
||||||
encodings every part pureboot targets shares. The loader carries no page-commit
|
|
||||||
logic of its own, and the same primitive reaches every other SPM operation,
|
|
||||||
lock bits included.
|
|
||||||
|
|
||||||
The SPM store and the SPM instruction must issue within four cycles of each
|
|
||||||
other (§26.2), which no host can hit across a serial link — so this one
|
|
||||||
primitive is *fused* rather than being a poke of SPMCSR followed by a poke of
|
|
||||||
something else. That four-cycle window is the floor on how low-level a
|
|
||||||
bootloader's primitives can go; it is not a byte-count decision.
|
|
||||||
|
|
||||||
An SPM command aimed at the 512-byte slot the loader is **running in** is
|
|
||||||
dropped, so a broken host cannot brick the running copy, while a staged copy
|
|
||||||
one slot lower may rewrite the resident — which is what a self-update is.
|
|
||||||
|
|
||||||
The loader never clears the SPM buffer before a fill, so **one `W` may program
|
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
|
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, and —
|
word until cleared, and two things leave words in it: a refused page, and —
|
||||||
where SPM runs from anywhere, the tinies and the m48s — an application that
|
where SPM runs from anywhere, the tinies and the m48s — an application that
|
||||||
self-programmed before entering. The next page write takes those stale words
|
self-programmed before entering. The next `W` takes those stale words and
|
||||||
and clears them, since a page write auto-erases the buffer (§26.2.1; §19.2 on
|
clears them, since a page write auto-erases the buffer (§26.2.1; §19.2 on the
|
||||||
the tinies), so repeating it programs correctly. The host therefore verifies
|
tinies), so repeating it programs correctly. The host therefore verifies every
|
||||||
every page it writes and rewrites what comes back wrong (three retries, then it
|
page it writes and rewrites what comes back wrong (three retries, then it
|
||||||
stops).
|
stops).
|
||||||
|
|
||||||
`g` is host-paced: send the next byte only after the previous byte's `+`. Fuse
|
`w` is host-paced: send the next byte only after the previous byte's `+`. `F`
|
||||||
*writing* does not exist — SPM reaches flash and boot lock bits only.
|
returns the bytes in the hardware's Z order; on a chip without an extended
|
||||||
|
fuse byte that slot carries no meaning. Fuse *writing* does not exist — SPM
|
||||||
|
reaches flash and boot lock bits only.
|
||||||
|
|
||||||
`J` is the one control-transfer primitive: it runs the application (word 0 or
|
`J` is the one control-transfer primitive: it runs the application (word 0 or
|
||||||
the trampoline word, both derived from the chip) and moves between loader
|
the trampoline word, both known from the info block) and moves between loader
|
||||||
copies during a self-update. A jump to a slot's base re-enters that copy's own
|
copies during a self-update. A jump to a slot's base re-enters that copy's own
|
||||||
startup, which must then be knocked afresh.
|
startup, which must then be knocked afresh.
|
||||||
|
|
||||||
`b` answers with the loader's identity — its version and the chip's signature —
|
The info block (`b`):
|
||||||
and nothing else. Everything else the host needs (page size, loader base,
|
|
||||||
EEPROM size, whether the reset vector must be patched, how many flash banks)
|
|
||||||
follows from the signature, and the host holds that table; the loader derived
|
|
||||||
the same facts from its own chip database at build time, so nothing is guessed,
|
|
||||||
it is simply not sent twice.
|
|
||||||
|
|
||||||
An update image, though, is a bare 512-byte slot with no device to ask, and
|
| Offset | Content |
|
||||||
installing one built for another chip bricks the target. Every loader image
|
|---|---|
|
||||||
therefore carries a six-byte **stamp** — `'P'`, `'B'`, the version, the three
|
| 0–2 | `'P'`, `'B'`, pureboot version (3) |
|
||||||
signature bytes — which the loader itself never reads and the host tool refuses
|
| 3–5 | device signature |
|
||||||
to install a mismatch against.
|
| 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 |
|
||||||
|
|
||||||
## Version
|
## Version
|
||||||
|
|
||||||
`b`'s first byte is the **pureboot version** — the loader's one identity
|
The info block's third byte is the **pureboot version** — the loader's one
|
||||||
number, and the only way to tell what a deployed loader is. Nothing else is
|
identity number, and the only way to tell what a deployed loader is. Nothing
|
||||||
numbered: the wire protocol has no version, a pureboot version implies it, and
|
else is numbered: the wire protocol has no version, a pureboot version implies
|
||||||
the host tool holds that map. The tool states the window of loader versions it
|
it, and the host tool holds that map. The tool states the window of loader
|
||||||
speaks (`OLDEST_LOADER`/`NEWEST_LOADER` in `pureboot.py`), and a version that
|
versions it speaks (`OLDEST_LOADER`/`NEWEST_LOADER` in `pureboot.py`), and a
|
||||||
changes the protocol becomes the new floor there. A loader newer than the tool
|
version that changes the protocol becomes the new floor there. None has so
|
||||||
is refused by name rather than decoded on the assumption that nothing moved.
|
far: 1 through 3 speak the identical session. A loader newer than the tool is
|
||||||
|
refused by name rather than decoded on the assumption that nothing moved.
|
||||||
Two generations exist. **1 through 4** speak one session — a 12-byte info block
|
|
||||||
from `b`, and a command per memory (`R`/`W` flash, `r`/`w` EEPROM, `F` fuses).
|
|
||||||
**5** replaced those with the single `G`/`g` pair over selector-named spaces
|
|
||||||
above; the shipped tool speaks both, choosing on the version it reads, so a
|
|
||||||
deployed pureboot 4 stays drivable and self-updatable to 5.
|
|
||||||
|
|
||||||
Collapsing four command bodies into one transfer loop is what paid for the
|
|
||||||
version: the data space, the host-issued SPM operations and the fuses now share
|
|
||||||
the loop, the cursor and the argument decode that `R`/`r`/`w` each carried a
|
|
||||||
copy of. The loader shrank while gaining all three.
|
|
||||||
|
|
||||||
The tool carries its own version, free to drift; `--version` prints it and the
|
The tool carries its own version, free to drift; `--version` prints it and the
|
||||||
window.
|
window.
|
||||||
@@ -320,10 +252,11 @@ with any pureboot build — a re-timed window, a newer version — using the
|
|||||||
loader itself as its own staging loader. The image is the loader's own 512
|
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.
|
bytes as a raw binary, or the Intel HEX the build emits beside it.
|
||||||
|
|
||||||
The preflight refuses an image built for another chip: the stamp every pureboot
|
The preflight refuses an image built for another chip: the info block embedded
|
||||||
binary carries must resolve to the device's own geometry, and the error names
|
in every pureboot binary (signature, page size, loader base, EEPROM size,
|
||||||
both. Die revisions share their base signature and geometry, so their images
|
flags) must match the device's own, and the error names both. Die revisions
|
||||||
are interchangeable — as the silicon is.
|
share their base signature and geometry, so their images are interchangeable —
|
||||||
|
as the silicon is.
|
||||||
|
|
||||||
1. The staging slot `[base−512, base)` is saved to a host-side state file (on
|
1. The staging slot `[base−512, base)` is saved to a host-side state file (on
|
||||||
the 1 KB tiny13s that is the whole application, vectors included).
|
the 1 KB tiny13s that is the whole application, vectors included).
|
||||||
@@ -360,8 +293,8 @@ to reset gets its reset pulse and opens the activation window by itself.
|
|||||||
--info --fuses --flash app.hex
|
--info --fuses --flash app.hex
|
||||||
|
|
||||||
Operations run in a fixed order within one session: info, fuses, loader
|
Operations run in a fixed order within one session: info, fuses, loader
|
||||||
update, flash (erase / program / read / verify), EEPROM (the same), then
|
update, flash (erase / program / read / verify), EEPROM (the same) — then the
|
||||||
`--peek`/`--poke` — then the loader hands over to the application. `--stay` keeps the session alive
|
loader hands over to the application. `--stay` keeps the session alive
|
||||||
instead, and a later invocation reconnects into it. `--flash` and `--eeprom`
|
instead, and a later invocation reconnects into it. `--flash` and `--eeprom`
|
||||||
verify by read-back unless `--no-verify`, and a flash page that reads back
|
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 (see `W` above).
|
wrong is rewritten up to three times before the run stops (see `W` above).
|
||||||
@@ -369,18 +302,9 @@ wrong is rewritten up to three times before the run stops (see `W` above).
|
|||||||
extension. `--force` overrides the refusable safety checks — today, flashing
|
extension. `--force` overrides the refusable safety checks — today, flashing
|
||||||
application data into a mega's reset walk region.
|
application data into a mega's reset walk region.
|
||||||
|
|
||||||
`--autobaud` opens with the calibration pulse instead of the plain knock, for a
|
Readouts come one fact per line: `--info` decodes the info block field by
|
||||||
loader built `SERIAL autobaud`; the rest of the session is identical, at
|
field, `--fuses` each fuse byte plus, on a boot-sectioned mega, its decoded
|
||||||
whatever `--baud` the host chose.
|
meaning. Transfers that take wire time draw a transient progress bar on stderr
|
||||||
|
|
||||||
`--peek ADDR[:N]` and `--poke ADDR:HEX` reach the data space (pureboot 5) —
|
|
||||||
SRAM, and through the same address space the register file and every I/O
|
|
||||||
register. Reading an I/O register can have side effects (reading UDR clears its
|
|
||||||
flags), which is the caller's business to know.
|
|
||||||
|
|
||||||
Readouts come one fact per line: `--info` prints the device's version and
|
|
||||||
signature and the geometry that follows from them, `--fuses` each fuse byte
|
|
||||||
plus, on a boot-sectioned mega, its decoded meaning. Transfers that take wire time draw a transient progress bar on stderr
|
|
||||||
when it is a tty. `-v`/`--verbose` adds the decisions as they happen: knock
|
when it is a tty. `-v`/`--verbose` adds the decisions as they happen: knock
|
||||||
counts, the programming plan, update state handling and per-phase page counts.
|
counts, the programming plan, update state handling and per-phase page counts.
|
||||||
|
|
||||||
@@ -399,19 +323,15 @@ Per chip preset, `ctest` runs:
|
|||||||
the configuration space produces, and a shape the ladder default (always the
|
the configuration space produces, and a shape the ladder default (always the
|
||||||
*fastest* rate a clock reaches) never picks. Pins are immediate operands and
|
*fastest* rate a clock reaches) never picks. Pins are immediate operands and
|
||||||
the timeout is a constant: neither is an axis;
|
the timeout is a constant: neither is an axis;
|
||||||
- `pureboot_autobaud.size` — the clock-free build, which has no clock or baud
|
- `pbm_*.size` — under `--full`, the exhaustive cross product replacing that
|
||||||
axis of its own: one binary per chip has to serve every point the matrix
|
compact matrix: every plausible oscillator (the internal ones, the CKDIV8
|
||||||
below sweeps;
|
floor, the plain and the UART crystals) × every rate reachable from it ×
|
||||||
- `pbm_*.size` — with `PUREBOOT_FULL_MATRIX=1`, the exhaustive cross product
|
every backend, unreachable combinations dropping out rather than aborting
|
||||||
replacing that compact matrix, on **every** chip: every plausible oscillator
|
the configure. Bounded to one chip per size-bearing class — flash
|
||||||
(the internal ones, the CKDIV8 floor, the plain and the UART crystals) ×
|
addressing, hand-over shape, page size, USART inventory — since everything
|
||||||
every rate reachable from it × every backend, unreachable combinations
|
else in the image is chip-independent code;
|
||||||
dropping out rather than aborting the configure. Thousands of points per
|
- `pureboot.pi` — the position-independence lint: no absolute `jmp`/`call`, the
|
||||||
chip, and cheap enough to run rather than reason about;
|
info block within the image's first 256 bytes;
|
||||||
- `pureboot.pi` — the position-independence lint: no absolute `jmp`/`call`, no
|
|
||||||
flash-resident section but `.text`, and the image byte-identical when linked
|
|
||||||
at a different base — which is position independence itself rather than a
|
|
||||||
proxy for it;
|
|
||||||
- `pureboot.planner` — the host tool's pure logic: programming orders and their
|
- `pureboot.planner` — the host tool's pure logic: programming orders and their
|
||||||
recovery properties, the surgery, the staging composition, the boot-fuse
|
recovery properties, the surgery, the staging composition, the boot-fuse
|
||||||
decode, the update preflight over synthetic fuse bytes, and the repairing
|
decode, the update preflight over synthetic fuse bytes, and the repairing
|
||||||
@@ -441,13 +361,7 @@ Per chip preset, `ctest` runs:
|
|||||||
anywhere, which is what makes the path constructible;
|
anywhere, which is what makes the path constructible;
|
||||||
- `pureboot.update` — the full `--update-loader` flow, then every power-fail
|
- `pureboot.update` — the full `--update-loader` flow, then every power-fail
|
||||||
phase: the device is killed mid-write, restarted from its flash dump, and a
|
phase: the device is killed mid-write, restarted from its flash dump, and a
|
||||||
re-run must complete the update with the application intact;
|
re-run must complete the update with the application intact.
|
||||||
- `pureboot.autobaud` (328P, 1284P) — the clock-free build over the GPIO⇄pty
|
|
||||||
bridge: the calibration handshake, a flash + EEPROM + fuse round trip against
|
|
||||||
the simulator's own memory, a data-space round trip, the hand-over — then the
|
|
||||||
same binary again at double the clock, which is the property the backend
|
|
||||||
exists for. A lone calibration pulse with no knock behind it must still let
|
|
||||||
the application boot, so no wait in activation can be unbounded.
|
|
||||||
|
|
||||||
`size`, `pi` and `planner` are host logic and run anywhere; the
|
`size`, `pi` and `planner` are host logic and run anywhere; the
|
||||||
simulator-driven targets need simavr and a pty, so they are POSIX-only.
|
simulator-driven targets need simavr and a pty, so they are POSIX-only.
|
||||||
|
|||||||
251
pureboot/autobaud.md
Normal file
251
pureboot/autobaud.md
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
# pureboot autobaud — findings, and how the version question settled itself
|
||||||
|
|
||||||
|
The autobaud loader measures the host's bit timing **at runtime** from a
|
||||||
|
calibration pulse, so the image carries no clock: one clock-agnostic binary per
|
||||||
|
chip runs at any F_CPU and locks onto whatever baud the host sends. It exists
|
||||||
|
for the software-serial deployments — the RC-oscillator parts (the tinies,
|
||||||
|
internal-oscillator megas) whose exact clock is uncertain and drifts, so today
|
||||||
|
each needs a per-clock build. Autobaud erases that axis. That is the win —
|
||||||
|
deployment, not bytes.
|
||||||
|
|
||||||
|
This document records how the fit was established, why the two versions that
|
||||||
|
were under review are both dead, and what the loader looks like now.
|
||||||
|
|
||||||
|
## The decision, settled by measurement
|
||||||
|
|
||||||
|
Two autobaud loaders were built for review, differing in one tradeoff — the
|
||||||
|
running-slot write guard against strict purity. `pureboot_autobaud_pure.cpp`
|
||||||
|
landed at **508 B** on the 1284P (4 B spare) and `pureboot_autobaud_reg.cpp` at
|
||||||
|
**512** (zero spare).
|
||||||
|
|
||||||
|
Then hardware testing found a defect that neither could absorb.
|
||||||
|
|
||||||
|
**A single spurious calibration pulse wedged the loader.** `run()` budgeted only
|
||||||
|
the start-edge wait inside `measure()`; the `rx()` that read the knock behind it
|
||||||
|
was unbudgeted and blocked forever. One stray low pulse on an unattended device
|
||||||
|
— EMI, or a host that opens the port and never knocks — held the loader in its
|
||||||
|
activation loop and the application never ran. On a field device that is a hang,
|
||||||
|
not a hiccup, and it is exactly the deployment autobaud is for.
|
||||||
|
|
||||||
|
The fix is to bound the whole activation: an expired knock budget returns a byte
|
||||||
|
that cannot be the knock, so control falls back into the budgeted `measure()`,
|
||||||
|
and a line that stays idle boots the application there. It costs about 22 bytes.
|
||||||
|
|
||||||
|
| 1284P, with the activation fix | size | 512 B budget |
|
||||||
|
|---|---|---|
|
||||||
|
| `pureboot_autobaud_pure.cpp` | 530 | **over by 18** |
|
||||||
|
| `pureboot_autobaud_reg.cpp` | 534 | **over by 22** |
|
||||||
|
| `pureboot_autobaud_uni.cpp` | **464** | **48 B spare** |
|
||||||
|
|
||||||
|
Both candidates were unshippable, and the margin they were competing over was
|
||||||
|
never real — it was the space the missing fix should have occupied. So the
|
||||||
|
choice is not between them. It is the third loader below, which fits with room
|
||||||
|
to spare *and* carries features neither had. The two sources stay in the tree
|
||||||
|
for the record; only the unified one is built.
|
||||||
|
|
||||||
|
## The unified loader
|
||||||
|
|
||||||
|
The insight that paid was the one that had already paid once: **merging command
|
||||||
|
bodies removes cost that moving them around only redistributes.** Folding `R`,
|
||||||
|
`r` and `w` into a single address-and-count path had been worth 14 B earlier.
|
||||||
|
Pushed further — one read command and one write command over *named spaces* —
|
||||||
|
it is worth far more, because four transfer loops collapse into one.
|
||||||
|
|
||||||
|
`pureboot_autobaud_uni.cpp` is pureboot 5. It is strictly pure: no inline
|
||||||
|
assembly, no global register variable, and **no GPIOR either** — the measured
|
||||||
|
unit lives in a plain static, so the loader claims no chip resource an
|
||||||
|
application might want, and the GPIOR-versus-static question disappears along
|
||||||
|
with the chips that have no GPIOR.
|
||||||
|
|
||||||
|
### The protocol
|
||||||
|
|
||||||
|
| command | arguments | |
|
||||||
|
|---|---|---|
|
||||||
|
| `b` | — | version, then the three signature bytes |
|
||||||
|
| `J` | addr16 | ack, then jump (word address) |
|
||||||
|
| `W` | sel8, addr16, page bytes | fill the flash page buffer |
|
||||||
|
| `G` | sel8, addr16, n8 | read n bytes (0 means 256) |
|
||||||
|
| `g` | sel8, addr16, n8, then n bytes | write, each byte acked |
|
||||||
|
|
||||||
|
`sel` is `space | bank << 4`. The low nibble names the space; the high nibble is
|
||||||
|
flash's third address byte, so every transfer speaks a **byte** address inside a
|
||||||
|
64 KiB bank and no command has to carry word addresses. The host must not span a
|
||||||
|
bank boundary in one transfer — it already chunks by page, so nothing it does
|
||||||
|
comes close.
|
||||||
|
|
||||||
|
| space | | |
|
||||||
|
|---|---|---|
|
||||||
|
| 0 | flash | `lpm`/`elpm` |
|
||||||
|
| 1 | EEPROM | |
|
||||||
|
| 2 | data | SRAM — and with it the register file and every I/O register, which share the data address space on AVR |
|
||||||
|
| 3 | fuse and lock | |
|
||||||
|
| 4 | SPM | write-only: the data byte goes to SPMCSR and fires the instruction at the selected address |
|
||||||
|
|
||||||
|
Three things follow from that table that the loader never had:
|
||||||
|
|
||||||
|
- **RAM read and write**, the missing feature. In a per-command design it would
|
||||||
|
have cost a fresh dispatch arm and a fresh loop, ~30 B, on a loader with 4 B
|
||||||
|
spare. As one more space on a shared loop it is a single `ld`/`st`. It also
|
||||||
|
hands the host arbitrary **I/O register access** for free, because AVR maps
|
||||||
|
the peripherals into the same address space.
|
||||||
|
- **Host-driven SPM.** `W` used to end with a hardcoded erase, write and RWW
|
||||||
|
re-enable — 42 B. Those are now three writes to the SPM space, reusing the
|
||||||
|
store path's own address, data byte and ack. The host pays three extra
|
||||||
|
round-trips per page (18 wire bytes against 256 of data) and gains the ability
|
||||||
|
to issue *any* SPM operation, lock bits included.
|
||||||
|
- **`W` on the same footing as everything else.** It takes the same selector and
|
||||||
|
the same byte address instead of a word address of its own, which made flash
|
||||||
|
addressing uniform across the protocol *and* was 20 B cheaper than keeping its
|
||||||
|
private convention.
|
||||||
|
|
||||||
|
Read and write are `G` and `g` — the same letter, one bit apart — so the
|
||||||
|
transfer loop picks its direction with a one-word skip rather than a compare.
|
||||||
|
|
||||||
|
### Why the SPM space has to be one primitive
|
||||||
|
|
||||||
|
It is tempting to go further and expose a generic "poke this I/O register", from
|
||||||
|
which the host could drive SPM itself. The hardware forbids it: `out SPMCSR, x`
|
||||||
|
and the `spm` that follows must issue within four cycles, and EEPROM's
|
||||||
|
EEMPE→EEPE window is the same shape. A host cannot hit a four-cycle window
|
||||||
|
across a serial link. **Atomicity is the floor, and the atomic unit must be
|
||||||
|
resident.** That is the real limit on how low-level a bootloader's primitives
|
||||||
|
can go — not the byte count.
|
||||||
|
|
||||||
|
## Where the bytes went
|
||||||
|
|
||||||
|
The starting point was the 508 B pure build, disassembled and attributed:
|
||||||
|
|
||||||
|
| phase | bytes |
|
||||||
|
|---|---|
|
||||||
|
| `link::rx` + `link::tx` (bit-banged UART) | 102 |
|
||||||
|
| autobaud measure + knock | 62 |
|
||||||
|
| reset vector, pin init, WDRF check, jump, ack, EEPROM wait | 50 |
|
||||||
|
| command loop head + dispatch tree | 42 |
|
||||||
|
| `W` program flash page | 98 |
|
||||||
|
| `R` / `r` / `w` / `F` bodies, plus the shared address decode | 122 |
|
||||||
|
| `b` info, `J` jump | 32 |
|
||||||
|
|
||||||
|
**208 B — 41% — is physical layer and activation**, which no protocol change can
|
||||||
|
touch. The command bodies were the entire addressable surface, and they were
|
||||||
|
four copies of one idea.
|
||||||
|
|
||||||
|
The route from a first attempt to the final loader, all on the 1284P:
|
||||||
|
|
||||||
|
| step | size |
|
||||||
|
|---|---|
|
||||||
|
| unified `G`/`P`, `load`/`store` outlined, `__uint24` cursor | 600 |
|
||||||
|
| …`load`/`store` inlined; unit in `.noinit`, not `.bss` | 534 |
|
||||||
|
| …16-bit cursor with the bank in the selector; direction as a command bit | 510 |
|
||||||
|
| …erase/write/RWW moved out to the SPM space | 484 |
|
||||||
|
| …`W` sharing the selector-and-address decode | **464** |
|
||||||
|
|
||||||
|
Three of those steps are worth keeping as lessons:
|
||||||
|
|
||||||
|
- **Outlining `load`/`store` cost more than the four bodies they replaced.** As
|
||||||
|
functions they were 110 B against the 106 B of inline bodies — the AVR ABI's
|
||||||
|
argument marshalling plus prologue ate the entire saving. Inlined into the one
|
||||||
|
shared loop they cost only their own instructions. The call-site lesson cuts
|
||||||
|
both ways: *merging* call sites pays, *creating* one does not.
|
||||||
|
- **A `.bss` static drags in `__do_clear_bss`** — 18 B of startup code to zero a
|
||||||
|
variable that is always measured before it is read. `.noinit` is correct here
|
||||||
|
and free.
|
||||||
|
- **A three-byte cursor taxes every space.** Widening the shared cursor so flash
|
||||||
|
could reach past 64 KiB put an extra increment on EEPROM and RAM reads that
|
||||||
|
never need it. Moving the bank into the selector byte kept the cursor at
|
||||||
|
sixteen bits and cost nothing on the wire.
|
||||||
|
|
||||||
|
### What did not work
|
||||||
|
|
||||||
|
- **Encoding the space in the command byte** (so dispatch becomes masking rather
|
||||||
|
than a compare tree) cannot carry the bank's four bits alongside a space. The
|
||||||
|
cheap half of the idea survived as the direction bit; the rest lost to the
|
||||||
|
selector byte, which is also more extensible.
|
||||||
|
- **A generic primitive interpreter** — a loader with no logic at all, driven
|
||||||
|
entirely by the host — is not reachable on AVR. Harvard architecture means the
|
||||||
|
program counter cannot fetch from data space, so the classic "upload a flash
|
||||||
|
algorithm into RAM and jump to it" bootstrap is impossible, and on every
|
||||||
|
boot-sectioned part SPM only takes effect from the boot section anyway. What
|
||||||
|
remains is a fixed primitive set: still a protocol, still logic, only at a
|
||||||
|
different granularity. debugWIRE reaches that design point only because its
|
||||||
|
interpreter is *in silicon*; it costs the loader nothing because it is not in
|
||||||
|
the loader.
|
||||||
|
- Below 512 B the saved bytes are largely unspendable on the boot-sectioned
|
||||||
|
chips: the 328P's smallest boot section is exactly 512 B, and the 1284P's is
|
||||||
|
1024 B, of which pureboot already occupies only the top half. The margin
|
||||||
|
matters as headroom for correctness fixes — as this defect showed — not as
|
||||||
|
flash returned to the application. On the patch-vector parts, which have no
|
||||||
|
boot section, it *is* returned: on the ATtiny13 the loader is 43% of a 1 KiB
|
||||||
|
part, and every byte is real.
|
||||||
|
|
||||||
|
## The codegen coupling, still load-bearing
|
||||||
|
|
||||||
|
`count >> 2` is exact only because the calibration pulse's bit-count (7, from
|
||||||
|
the 0xC0 byte) equals the poll loop's cycles per iteration (7 — `sbis` 1,
|
||||||
|
`rjmp` 2, `adiw` 2, `rjmp` 2). The loop shape survived every restructuring here,
|
||||||
|
verified in the disassembly, but a toolchain bump that reshapes it would break
|
||||||
|
the lock silently. `test/pbautobaud.py` is what pins it: a wrong unit fails the
|
||||||
|
flash verify.
|
||||||
|
|
||||||
|
## Sizes — every chip
|
||||||
|
|
||||||
|
Budget 510 B on the patch-vector parts, 512 elsewhere. The 1284P is no longer
|
||||||
|
the tight one: the bank nibble made far flash *cheaper* than the near-flash
|
||||||
|
arithmetic it replaced.
|
||||||
|
|
||||||
|
| size | chips | budget | spare |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 444 | ATtiny13, 13A | 510 | 66 |
|
||||||
|
| 448 | ATmega48, 48A, 48P, 48PA; ATtiny25 | 510 | 62 |
|
||||||
|
| 452 | ATtiny45, 85 | 510 | 58 |
|
||||||
|
| 460 | ATmega644, 644A, 644P, 644PA | 512 | 52 |
|
||||||
|
| 464 | **ATmega1284, 1284P**; ATmega8, 8A, 88, 88A, 88P, 88PA | 512 | 48 |
|
||||||
|
| 466 | ATmega16, 16A, 32, 32A; 164A/P/PA, 168/A/P/PA, 324A/P/PA, 328, 328P | 512 | 46 |
|
||||||
|
|
||||||
|
All 37 chips build and size-test green, plus the 12-preset reflect spot set
|
||||||
|
(guidance rule 4 — the reflect matrix is never run in full), which matches its
|
||||||
|
generated counterpart byte for byte on every chip in the set. Worst case across
|
||||||
|
the whole set is **466 B, 46 under budget**.
|
||||||
|
|
||||||
|
## Host tool and simulation
|
||||||
|
|
||||||
|
- **`pureboot.py`** speaks both generations. `Info.version >= 5` selects the
|
||||||
|
unified path; everything below it keeps the four-command protocol, so the
|
||||||
|
fixed-baud loader is untouched. `--autobaud` sends the 0xC0 pulse and one
|
||||||
|
knock, then derives full geometry from the signature. New: `--peek ADDR[:N]`
|
||||||
|
and `--poke ADDR:HEX` reach the data space.
|
||||||
|
- **`test/pbautobaud.py`** drives the loader over the GPIO⇄pty software-UART
|
||||||
|
bridge through the calibration handshake, a flash + EEPROM + fuse round-trip
|
||||||
|
cross-checked against the simulator's own memory, a RAM read/write round-trip,
|
||||||
|
and a hand-over to the fixture application — then repeats at double the F_CPU
|
||||||
|
with the same binary, which is the clock-agnostic property autobaud exists
|
||||||
|
for. Run on the near-flash 328P and the word-addressed 1284P.
|
||||||
|
- It also **pins the activation hang**: the test sends a lone calibration pulse
|
||||||
|
with no knock behind it and requires the application to boot. Against the
|
||||||
|
unfixed loader that assertion never returns.
|
||||||
|
|
||||||
|
## What remains
|
||||||
|
|
||||||
|
- **Real-hardware acceptance.** A cycle-exact simulator cannot produce what
|
||||||
|
autobaud exists for: a real RC oscillator at ±10% with drift and jitter.
|
||||||
|
simavr proves the arithmetic and the fit at exact clocks; only silicon proves
|
||||||
|
the feature. Drive an internal-oscillator ATtiny at a fixed host baud and
|
||||||
|
confirm lock plus a full flash and verify.
|
||||||
|
- **A generic `spm::command()` in libavr.** The SPM space issues a runtime
|
||||||
|
command through `spm::detail::page_command` where RAMPZ exists, and falls back
|
||||||
|
to a dispatch over the known operations where it does not — the one
|
||||||
|
preprocessor branch in the file. A two-line library addition would make it
|
||||||
|
uniform and save a few bytes on the 36 non-RAMPZ chips, none of which are
|
||||||
|
tight.
|
||||||
|
- **Retire or revive the two dead variants.** They are kept only as the record
|
||||||
|
of the measurement; nothing builds them.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `pureboot_autobaud_uni.cpp` — the loader. pureboot 5.
|
||||||
|
- `pureboot_autobaud_pure.cpp`, `pureboot_autobaud_reg.cpp` — superseded, not
|
||||||
|
built; 530 and 534 B on the 1284P once the activation hang is fixed.
|
||||||
|
- `pureboot.py` — `--autobaud`, the unified transfer path, `--peek`/`--poke`.
|
||||||
|
- `test/pbautobaud.py` — the end-to-end sim test and the hang regression.
|
||||||
|
- `local/scratch/autobaud/floor_1284.S` (libavr checkout) — the hand-asm floor
|
||||||
|
probe at 506 B, off-tree and gitignored; a size reference only. The unified
|
||||||
|
loader is 42 B under it, with features the probe never had.
|
||||||
@@ -26,17 +26,14 @@ constexpr std::uint8_t ack = '+';
|
|||||||
|
|
||||||
// Deployment parameters come from the build (pureboot_add_loader()). The
|
// Deployment parameters come from the build (pureboot_add_loader()). The
|
||||||
// signature is not one of them: the chip database is the only universal
|
// signature is not one of them: the chip database is the only universal
|
||||||
// source — a tiny13A cannot read its own signature row from code. An autobaud
|
// source — a tiny13A cannot read its own signature row from code.
|
||||||
// build carries no clock and no baud at all; it measures both.
|
#if !defined(PUREBOOT_CLOCK_HZ) || !defined(PUREBOOT_BAUD)
|
||||||
#if !defined(PUREBOOT_AUTOBAUD) && (!defined(PUREBOOT_CLOCK_HZ) || !defined(PUREBOOT_BAUD))
|
|
||||||
#error \
|
#error \
|
||||||
"PUREBOOT_CLOCK_HZ and PUREBOOT_BAUD select this build's clock and baud — create loader targets with pureboot_add_loader(), or PUREBOOT_AUTOBAUD for a clock-free one (README.md)"
|
"PUREBOOT_CLOCK_HZ and PUREBOOT_BAUD select this build's clock and baud — create loader targets with pureboot_add_loader() (README.md)"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if !defined(PUREBOOT_AUTOBAUD)
|
|
||||||
using dev = avr::device<{.clock = avr::hertz_t{PUREBOOT_CLOCK_HZ}}>;
|
using dev = avr::device<{.clock = avr::hertz_t{PUREBOOT_CLOCK_HZ}}>;
|
||||||
constexpr avr::baud_t wire_baud{PUREBOOT_BAUD};
|
constexpr avr::baud_t wire_baud{PUREBOOT_BAUD};
|
||||||
#endif
|
|
||||||
|
|
||||||
// The watchdog reset flag's home: MCUSR, or the classic megas' MCUCSR.
|
// The watchdog reset flag's home: MCUSR, or the classic megas' MCUCSR.
|
||||||
consteval std::int16_t wdrf_field()
|
consteval std::int16_t wdrf_field()
|
||||||
@@ -50,110 +47,60 @@ consteval std::int16_t wdrf_field()
|
|||||||
// runs from anywhere (Atmel-8271 §26) — keep the application's relocated
|
// runs from anywhere (Atmel-8271 §26) — keep the application's relocated
|
||||||
// reset vector in the word under the slot.
|
// reset vector in the word under the slot.
|
||||||
constexpr std::uint16_t slot_bytes = 512;
|
constexpr std::uint16_t slot_bytes = 512;
|
||||||
|
constexpr std::uint32_t base = spm::flash_bytes - slot_bytes;
|
||||||
constexpr std::uint16_t page = spm::page_bytes;
|
constexpr std::uint16_t page = spm::page_bytes;
|
||||||
constexpr bool boot_section = avr::hw::curated::has_boot_section();
|
constexpr bool boot_section = avr::hw::curated::has_boot_section();
|
||||||
|
|
||||||
// Past 64 KiB one bank of flash does not cover the chip, so a transfer's
|
// Past 64 KiB a byte address no longer fits the wire's 16 bits, so flash
|
||||||
// selector byte carries the bank and the wire address stays a byte address
|
// addresses there are word addresses ('J' always was one). A slot is 256 of
|
||||||
// within it. 'J' is the exception: it is a word address everywhere, because
|
// those — one value of a wire address's high byte, where 512 bytes span two.
|
||||||
// that is what the hardware's own jump takes.
|
constexpr bool word_flash = spm::flash_bytes > 65536;
|
||||||
constexpr bool banked_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);
|
||||||
|
|
||||||
// A compile-time window, so the whole EEPROM belongs to the application;
|
// A compile-time window, so the whole EEPROM belongs to the application;
|
||||||
// re-timing a deployed loader is a self-update with a re-timed build. An
|
// re-timing a deployed loader is a self-update with a re-timed build.
|
||||||
// autobaud build has no clock to convert seconds against and counts polls.
|
|
||||||
#if !defined(PUREBOOT_TIMEOUT)
|
#if !defined(PUREBOOT_TIMEOUT)
|
||||||
#define PUREBOOT_TIMEOUT 8
|
#define PUREBOOT_TIMEOUT 8
|
||||||
#endif
|
#endif
|
||||||
constexpr std::uint8_t timeout_seconds = PUREBOOT_TIMEOUT;
|
constexpr std::uint8_t timeout_seconds = PUREBOOT_TIMEOUT;
|
||||||
|
|
||||||
#if !defined(PUREBOOT_AUTOBAUD_POLLS)
|
|
||||||
#define PUREBOOT_AUTOBAUD_POLLS 4000000
|
|
||||||
#endif
|
|
||||||
constexpr avr::uint24_t autobaud_budget = PUREBOOT_AUTOBAUD_POLLS;
|
|
||||||
|
|
||||||
// The loader's one identity number. The protocol carries none of its own —
|
// The loader's one identity number. The protocol carries none of its own —
|
||||||
// a version implies it, and the host tool holds that map (README.md).
|
// a version implies it, and the host tool holds that map (README.md).
|
||||||
constexpr std::uint8_t version = 5;
|
constexpr std::uint8_t version = 4;
|
||||||
|
|
||||||
// The image's identity stamp, for the host tool rather than for the wire: an
|
// The 'b' reply, byte for byte (layout: README.md). Flash-resident because
|
||||||
// update image is a bare 512-byte slot, and without this nothing in it says
|
// no crt copies a .data image — and flash_table's storage carries the word
|
||||||
// which chip it was built for. The tool refuses to install an image whose
|
// alignment 'b' needs to halve the address on the large chips.
|
||||||
// stamp does not match the device — flashing a foreign loader bricks the
|
// One wire byte per line: this is the reply's layout, not a list.
|
||||||
// target, and the loader itself cannot check what has already replaced it.
|
|
||||||
//
|
|
||||||
// Never read from flash by the loader — 'b' answers out of this array, but at
|
|
||||||
// constant indices, so those fold to immediates and no runtime address of it
|
|
||||||
// is ever formed. `used` keeps the compiler from dropping the copy the host
|
|
||||||
// needs and `retain` keeps --gc-sections from collecting it.
|
|
||||||
// clang-format off
|
// clang-format off
|
||||||
[[gnu::used, gnu::retain, gnu::section(".text.stamp")]]
|
inline constexpr avr::flash_table<std::array<std::uint8_t, 12>{
|
||||||
inline constexpr std::uint8_t identity_stamp[]{
|
'P',
|
||||||
'P', 'B', // the magic the host scans an image for
|
'B',
|
||||||
version, // and from here on, exactly what 'b' answers
|
version,
|
||||||
avr::hw::db.signature[0],
|
avr::hw::db.signature[0],
|
||||||
avr::hw::db.signature[1],
|
avr::hw::db.signature[1],
|
||||||
avr::hw::db.signature[2],
|
avr::hw::db.signature[2],
|
||||||
};
|
static_cast<std::uint8_t>(page), // 0 means 256
|
||||||
|
wire_base & 0xff,
|
||||||
|
wire_base >> 8,
|
||||||
|
avr::hw::db.mem.eeprom_size & 0xff,
|
||||||
|
avr::hw::db.mem.eeprom_size >> 8,
|
||||||
|
static_cast<std::uint8_t>((boot_section ? 0 : 1) | (word_flash ? 2 : 0)), // patch-vector, word-addressed
|
||||||
|
}>
|
||||||
|
info_data;
|
||||||
// clang-format on
|
// clang-format on
|
||||||
// Where the identity proper starts: past the magic the host scans for.
|
|
||||||
constexpr std::uint8_t stamp_identity = 2;
|
|
||||||
|
|
||||||
// The address spaces a transfer can name, in a selector byte's low nibble.
|
// The serial link, per the build's PUREBOOT_USART / PUREBOOT_SOFT_SERIAL,
|
||||||
// Flash is 0 so it is the cheapest to select.
|
// defaulting to the chip's USART0 where it has one. The software receiver is
|
||||||
//
|
// the polled one: the vector table belongs to the application. Templates on
|
||||||
// spm_ops is the one that is not memory: a write there hands its byte to
|
// the clock, so only the selected backend instantiates. pending() is the
|
||||||
// SPMCSR and fires the instruction at the transfer's address, which is how
|
// cheap line test the activation window polls; drain() holds until the last
|
||||||
// page erase, page write and RWW re-enable reach the wire without the loader
|
// frame is off the wire, so a hand-over cannot let the target's re-init clip
|
||||||
// carrying a command for each. The hardware's four-cycle store-to-SPM window
|
// the ack.
|
||||||
// is why this is one fused primitive and not a poke of SPMCSR — no host can
|
|
||||||
// hit that window across a serial link.
|
|
||||||
enum : std::uint8_t { sp_flash = 0, sp_eeprom = 1, sp_data = 2, sp_fuse = 3, sp_spm = 4 };
|
|
||||||
|
|
||||||
// A selector's high nibble is the flash bank — the address bits above the
|
|
||||||
// 16-bit wire address, RAMPZ on the chips that have one. Keeping it here
|
|
||||||
// rather than widening the wire address is what lets one 16-bit cursor serve
|
|
||||||
// every space: a 24-bit cursor would pay its extra byte on EEPROM and data
|
|
||||||
// reads that can never need it.
|
|
||||||
[[gnu::always_inline]] inline std::uint8_t space_of(std::uint8_t selector)
|
|
||||||
{
|
|
||||||
return selector & 0x0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
[[gnu::always_inline]] inline std::uint8_t bank_of(std::uint8_t selector)
|
|
||||||
{
|
|
||||||
return static_cast<std::uint8_t>(selector >> 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The slot a flash address falls in, as one byte. A slot is half as many words
|
|
||||||
// as bytes, so the word address's high byte is exactly this index — which is
|
|
||||||
// what lets the write guard compare a single byte, and what the running copy's
|
|
||||||
// own return address yields for free.
|
|
||||||
constexpr std::uint8_t slot_shift = std::countr_zero(slot_bytes);
|
|
||||||
constexpr std::uint8_t bank_shift = 16 - slot_shift;
|
|
||||||
|
|
||||||
[[gnu::always_inline]] inline std::uint8_t slot_of([[maybe_unused]] std::uint8_t bank, std::uint16_t at)
|
|
||||||
{
|
|
||||||
const auto within = static_cast<std::uint8_t>(at >> slot_shift);
|
|
||||||
if constexpr (banked_flash)
|
|
||||||
return static_cast<std::uint8_t>((bank << bank_shift) | within);
|
|
||||||
else
|
|
||||||
return within;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The serial link, per the build's PUREBOOT_USART / PUREBOOT_SOFT_SERIAL /
|
|
||||||
// PUREBOOT_AUTOBAUD, defaulting to the chip's USART0 where it has one. The
|
|
||||||
// software receiver is the polled one: the vector table belongs to the
|
|
||||||
// application. Templates on the clock, so only the selected backend
|
|
||||||
// instantiates. pending() is the cheap line test the activation window polls;
|
|
||||||
// drain() holds until the last frame is off the wire, so a hand-over cannot
|
|
||||||
// let the target's re-init clip the ack.
|
|
||||||
#if defined(PUREBOOT_SOFT_SERIAL) && defined(PUREBOOT_USART)
|
#if defined(PUREBOOT_SOFT_SERIAL) && defined(PUREBOOT_USART)
|
||||||
#error "PUREBOOT_SOFT_SERIAL and PUREBOOT_USART select opposing serial backends"
|
#error "PUREBOOT_SOFT_SERIAL and PUREBOOT_USART select opposing serial backends"
|
||||||
#endif
|
#endif
|
||||||
#if defined(PUREBOOT_AUTOBAUD) && defined(PUREBOOT_USART)
|
|
||||||
#error "PUREBOOT_AUTOBAUD measures a software link; it cannot drive a hardware USART"
|
|
||||||
#endif
|
|
||||||
#if !defined(PUREBOOT_RX)
|
#if !defined(PUREBOOT_RX)
|
||||||
#define PUREBOOT_RX pb0
|
#define PUREBOOT_RX pb0
|
||||||
#endif
|
#endif
|
||||||
@@ -166,9 +113,9 @@ constexpr char usart_digit = '0' + PUREBOOT_USART;
|
|||||||
constexpr char usart_digit = '0';
|
constexpr char usart_digit = '0';
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
template <avr::hertz_t C, avr::baud_t B>
|
template <avr::hertz_t C>
|
||||||
struct hardware_link {
|
struct hardware_link {
|
||||||
using uart = avr::uart::usart<usart_digit, C, {.baud = B, .max_baud_error = 2.5_pct}>;
|
using uart = avr::uart::usart<usart_digit, C, {.baud = wire_baud, .max_baud_error = 2.5_pct}>;
|
||||||
|
|
||||||
// The compiled idle poll: lds UCSR0A (2), sbrc skipping the exit (2),
|
// The compiled idle poll: lds UCSR0A (2), sbrc skipping the exit (2),
|
||||||
// sbiw + sbci + sbci + brne (6).
|
// sbiw + sbci + sbci + brne (6).
|
||||||
@@ -200,10 +147,10 @@ struct hardware_link {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
template <avr::hertz_t C, avr::baud_t B>
|
template <avr::hertz_t C>
|
||||||
struct software_link {
|
struct software_link {
|
||||||
using rx_t = avr::uart::software_rx_polled<C, avr::PUREBOOT_RX, B>;
|
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, B>;
|
using tx_t = avr::uart::software_tx<C, avr::PUREBOOT_TX, wire_baud>;
|
||||||
|
|
||||||
// The compiled idle poll: sbis skipping the exit (2), sbiw + sbci +
|
// The compiled idle poll: sbis skipping the exit (2), sbiw + sbci +
|
||||||
// sbci + brne (6).
|
// sbci + brne (6).
|
||||||
@@ -235,44 +182,14 @@ struct software_link {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// The clock-free link: the bit period is measured from the host's calibration
|
#if defined(PUREBOOT_USART)
|
||||||
// pulse instead of derived from a clock, so one image serves every F_CPU and
|
|
||||||
// every rate. Activation differs in kind from the other two — there is no
|
|
||||||
// clock to time a window against — so this backend brings its own, below.
|
|
||||||
struct autobaud_link {
|
|
||||||
using uart = avr::uart::software_autobaud<avr::PUREBOOT_RX, avr::PUREBOOT_TX>;
|
|
||||||
|
|
||||||
static void init()
|
|
||||||
{
|
|
||||||
avr::init<uart>();
|
|
||||||
}
|
|
||||||
|
|
||||||
static std::uint8_t rx()
|
|
||||||
{
|
|
||||||
return uart::template read<off>();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void tx(std::uint8_t byte)
|
|
||||||
{
|
|
||||||
uart::template write<off>(byte);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void drain()
|
|
||||||
{
|
|
||||||
uart::drain();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
#if defined(PUREBOOT_AUTOBAUD)
|
|
||||||
using link = autobaud_link;
|
|
||||||
#elif defined(PUREBOOT_USART)
|
|
||||||
static_assert(avr::uart::has_usart<usart_digit>(), "PUREBOOT_USART selects a hardware USART this chip does not have");
|
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, wire_baud>;
|
using link = hardware_link<dev::clock>;
|
||||||
#elif defined(PUREBOOT_SOFT_SERIAL)
|
#elif defined(PUREBOOT_SOFT_SERIAL)
|
||||||
using link = software_link<dev::clock, wire_baud>;
|
using link = software_link<dev::clock>;
|
||||||
#else
|
#else
|
||||||
using link = std::conditional_t<avr::uart::has_usart<usart_digit>(), hardware_link<dev::clock, wire_baud>,
|
using link =
|
||||||
software_link<dev::clock, wire_baud>>;
|
std::conditional_t<avr::uart::has_usart<usart_digit>(), hardware_link<dev::clock>, software_link<dev::clock>>;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// The application's entry, pinned by the linker (--defsym): word 0 on a
|
// The application's entry, pinned by the linker (--defsym): word 0 on a
|
||||||
@@ -292,27 +209,6 @@ extern "C" [[noreturn]] void pureboot_app();
|
|||||||
jump(pureboot_app);
|
jump(pureboot_app);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Activation: a bounded wait for the host, then the knock. Both forms boot the
|
|
||||||
// application when the window closes on an idle line, and both bound *every*
|
|
||||||
// wait — a knock awaited without a deadline would let one stray edge hold an
|
|
||||||
// unattended device in the loader forever.
|
|
||||||
#if defined(PUREBOOT_AUTOBAUD)
|
|
||||||
// The window is a fixed poll budget: with no clock, whole seconds cannot be
|
|
||||||
// timed. A uint24_t holds it — a fourth byte would cost two words at every
|
|
||||||
// countdown step for range never used.
|
|
||||||
void await_host()
|
|
||||||
{
|
|
||||||
for (;;) {
|
|
||||||
if (!link::uart::calibrate(autobaud_budget))
|
|
||||||
run_app();
|
|
||||||
// The calibration pulse has already proven a host is there, so one
|
|
||||||
// byte activates. A knock that never arrives falls back to calibrate(),
|
|
||||||
// whose own budget then boots the application.
|
|
||||||
if (link::uart::template read<off>(autobaud_budget) == 'p')
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
// The window as one 32-bit countdown, divided by the backend's counted
|
// The window as one 32-bit countdown, divided by the backend's counted
|
||||||
// poll-loop cycles. Whole seconds is all it promises.
|
// poll-loop cycles. Whole seconds is all it promises.
|
||||||
consteval std::uint32_t window_polls()
|
consteval std::uint32_t window_polls()
|
||||||
@@ -339,14 +235,6 @@ std::uint8_t rx_deadline()
|
|||||||
return link::rx();
|
return link::rx();
|
||||||
}
|
}
|
||||||
|
|
||||||
void await_host()
|
|
||||||
{
|
|
||||||
// 'p' then 'b', each under a fresh window; anything else is line noise.
|
|
||||||
while (rx_deadline() != 'p' || rx_deadline() != 'b') {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Inlined: read across a call, the first byte strands in a call-saved
|
// Inlined: read across a call, the first byte strands in a call-saved
|
||||||
// register the caller has to push and pop.
|
// register the caller has to push and pop.
|
||||||
[[gnu::always_inline]] inline std::uint16_t rx16()
|
[[gnu::always_inline]] inline std::uint16_t rx16()
|
||||||
@@ -364,92 +252,132 @@ void await_host()
|
|||||||
return std::bit_cast<std::uint16_t>(pair);
|
return std::bit_cast<std::uint16_t>(pair);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Out of line: several sites send it, and a call is shorter than a
|
// Counts arrive in the wire's 8-bit form: 0 means 256. Both streamers fold
|
||||||
// load-immediate at each.
|
// into the one command that reads flash, which is what lets the far one's
|
||||||
|
// 24-bit cursor sit in the command loop's own call-saved registers.
|
||||||
|
[[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 apart; the reassembled address folds away inside the far load.
|
||||||
|
[[maybe_unused, gnu::always_inline]] inline 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));
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Out of line: three sites send it, and a call is shorter than three
|
||||||
|
// load-immediates.
|
||||||
[[gnu::noinline]] void tx_ack()
|
[[gnu::noinline]] void tx_ack()
|
||||||
{
|
{
|
||||||
link::tx(ack);
|
link::tx(ack);
|
||||||
}
|
}
|
||||||
|
|
||||||
// A wire address and its selector's bank as the flash address they name.
|
void send_eeprom(std::uint16_t address, std::uint8_t count)
|
||||||
[[gnu::always_inline]] inline spm::flash_address_t flash_address([[maybe_unused]] std::uint8_t bank, std::uint16_t at)
|
|
||||||
{
|
{
|
||||||
if constexpr (banked_flash)
|
do
|
||||||
return (static_cast<spm::flash_address_t>(bank) << 16) | at;
|
link::tx(ee::read(address++));
|
||||||
else
|
while (--count);
|
||||||
return at;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// One byte out of any space. Every accessor shares the transfer's cursor, its
|
// Host-paced: the ack goes out once the write has begun, so the next byte
|
||||||
// loop and its call site, so a space costs only its own instruction rather
|
// arrives while it completes and nothing is missed without a buffer.
|
||||||
// than a body, a loop and a dispatch arm of its own.
|
void store_eeprom(std::uint16_t address, std::uint8_t count)
|
||||||
[[gnu::always_inline]] inline std::uint8_t load(std::uint8_t space, [[maybe_unused]] std::uint8_t bank,
|
|
||||||
std::uint16_t at)
|
|
||||||
{
|
{
|
||||||
if (space == sp_eeprom)
|
do {
|
||||||
return ee::read(at);
|
ee::write<off>(address++, link::rx());
|
||||||
if (space == sp_data)
|
tx_ack();
|
||||||
return *reinterpret_cast<volatile std::uint8_t *>(at);
|
} while (--count);
|
||||||
if (space == sp_fuse)
|
|
||||||
return spm::read_fuse<off>(static_cast<spm::fuse>(at));
|
|
||||||
if constexpr (banked_flash)
|
|
||||||
return avr::flash_load_far<std::uint8_t>(flash_address(bank, at));
|
|
||||||
else
|
|
||||||
return avr::flash_load(reinterpret_cast<const std::uint8_t *>(at));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// One byte into a writable space. Flash is not one of them — it arrives a
|
// One page into the SPM buffer, then erase and program — except the slot
|
||||||
// page at a time through 'W' and is committed through sp_spm — and the fuses
|
// this code is running in (`slot_high`, from run()), which is drained and
|
||||||
// are not writable at all: SPM reaches flash and boot lock bits only.
|
// left alone. A broken host therefore cannot brick the running loader, and a
|
||||||
[[gnu::always_inline]] inline void store(std::uint8_t space, std::uint8_t bank, std::uint16_t at, std::uint8_t value,
|
// copy one slot lower may rewrite the resident one.
|
||||||
std::uint8_t slot_high)
|
|
||||||
{
|
|
||||||
if (space == sp_data) {
|
|
||||||
*reinterpret_cast<volatile std::uint8_t *>(at) = value;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (space == sp_spm) {
|
|
||||||
// The running-slot write guard. An SPM command aimed at the slot this
|
|
||||||
// code executes from is dropped, so a broken host cannot brick the
|
|
||||||
// running loader — while a copy one slot lower may still rewrite the
|
|
||||||
// resident one, which is what a self-update is. Guarding the commit
|
|
||||||
// rather than the page fill covers erase and write both, and leaves a
|
|
||||||
// refused page's words in the buffer: harmless, since the next page
|
|
||||||
// write auto-erases it (§26.2.1).
|
|
||||||
if (slot_of(bank, at) != slot_high)
|
|
||||||
spm::command<off>(value, flash_address(bank, at));
|
|
||||||
// Only a boot-sectioned mega runs on while its RWW section programs;
|
|
||||||
// everywhere else the CPU halts through erase and write, so the wait
|
|
||||||
// is already over by the time it returns.
|
|
||||||
if constexpr (boot_section)
|
|
||||||
spm::wait();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Host-paced: the ack goes out once the write has begun, so the next byte
|
|
||||||
// arrives while it completes and nothing is missed without a buffer.
|
|
||||||
ee::write<off>(at, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// One page into the SPM buffer, and only that: the erase and the write that
|
|
||||||
// commit it are host-issued sp_spm stores, which reach the same fused
|
|
||||||
// store-and-SPM pair through the transfer path's own address and data.
|
|
||||||
//
|
//
|
||||||
// Nothing discards the buffer first: it is write-once per word (§26.2.1), so
|
// Nothing discards the buffer first: it is write-once per word (§26.2.1), so
|
||||||
// filling over a refused page or an application's leavings programs stale
|
// filling over a refused page or an application's leavings programs stale
|
||||||
// words — but a page write auto-erases it (§26.2.1; §19.2 on the tinies), so
|
// words — but a page write auto-erases it (§26.2.1; §19.2 on the tinies), so
|
||||||
// that write clears the condition and the host's read-back rewrites the page.
|
// that write clears the condition and the host's read-back rewrites the page.
|
||||||
void fill_page(std::uint8_t bank, std::uint16_t at)
|
void program_flash(std::uint16_t wire_address, std::uint8_t slot_high)
|
||||||
{
|
{
|
||||||
// The address names a page, so its in-page bits are dropped and the walk
|
// The address names a page, so its in-page bits are dropped and the walk
|
||||||
// starts at the page base; the low byte of the cursor is the whole in-page
|
// starts at the page base — one induction either way: a byte-addressed
|
||||||
// offset, since a page is aligned and never crosses a bank.
|
// wire address walks the page itself (the offset bits wrap back to zero),
|
||||||
std::uint16_t z = at & ~static_cast<std::uint16_t>(page - 1);
|
// while a word one becomes a byte cursor once. The slot index is the wire
|
||||||
do {
|
// address's high byte — on byte-addressed chips the byte address's, with
|
||||||
std::uint8_t low = link::rx();
|
// the low bit dropped, since a slot is two of those.
|
||||||
std::uint8_t high = link::rx();
|
spm::flash_address_t address;
|
||||||
spm::fill<off>(flash_address(bank, z), word_of({low, high}));
|
std::uint8_t page_high;
|
||||||
z += 2;
|
if constexpr (word_flash) {
|
||||||
} while (static_cast<std::uint8_t>(z) & (page - 1));
|
// A page is aligned, so it never crosses 64 KiB: RAMPZ is a per-page
|
||||||
|
// constant and the 16-bit Z's low byte is the whole in-page offset.
|
||||||
|
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) & ~static_cast<std::uint16_t>(page - 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, word_of({low, high}));
|
||||||
|
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);
|
||||||
|
} else {
|
||||||
|
address = static_cast<spm::flash_address_t>(wire_address & ~static_cast<std::uint16_t>(page - 1));
|
||||||
|
do {
|
||||||
|
std::uint8_t low = link::rx();
|
||||||
|
std::uint8_t high = link::rx();
|
||||||
|
spm::fill<off>(address, word_of({low, high}));
|
||||||
|
address += 2;
|
||||||
|
} while (static_cast<std::uint8_t>(address) & (page - 1));
|
||||||
|
address -= 2; // back inside the page — erase and write ignore the word bits
|
||||||
|
page_high = static_cast<std::uint8_t>(address >> 8) & 0xfe;
|
||||||
|
}
|
||||||
|
if (page_high != slot_high) {
|
||||||
|
// Only a boot-sectioned mega runs on while its RWW section programs;
|
||||||
|
// everywhere else the CPU halts through erase and write.
|
||||||
|
spm::erase_page<off>(address);
|
||||||
|
if constexpr (boot_section)
|
||||||
|
spm::wait();
|
||||||
|
spm::write_page<off>(address);
|
||||||
|
if constexpr (boot_section)
|
||||||
|
spm::wait();
|
||||||
|
}
|
||||||
|
// Programming leaves the RWW section disabled; reads need it back on. The
|
||||||
|
// same store discards the buffer (§26.2.2), so a boot-sectioned mega never
|
||||||
|
// meets the stale-word case above.
|
||||||
|
if constexpr (boot_section)
|
||||||
|
spm::rww_enable<off>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The four fuse and lock bytes in the hardware's own Z order: low, lock,
|
||||||
|
// extended, high.
|
||||||
|
void send_fuses()
|
||||||
|
{
|
||||||
|
std::uint8_t which = 0;
|
||||||
|
do
|
||||||
|
link::tx(spm::read_fuse<off>(static_cast<spm::fuse>(which)));
|
||||||
|
while (++which != 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
[[noreturn]] void run()
|
[[noreturn]] void run()
|
||||||
@@ -461,14 +389,19 @@ void fill_page(std::uint8_t bank, std::uint16_t at)
|
|||||||
|
|
||||||
link::init();
|
link::init();
|
||||||
|
|
||||||
// The slot this copy runs in, which the write guard follows: the return
|
// The high byte of the slot this copy runs at, which the write guard and
|
||||||
// address is a word address and a slot is half as many words as bytes, so
|
// the info block both follow: the return address is a word address, so its
|
||||||
// its high byte is the slot index outright. No absolute address is ever
|
// high byte is the 256-word slot index, doubled back into byte terms where
|
||||||
// formed, so the image stays position-independent.
|
// the wire counts bytes. Taken as byteswap's low byte — the builtin already
|
||||||
const auto return_words = reinterpret_cast<std::uint16_t>(__builtin_return_address(0));
|
// swaps the two stacked bytes, and the double swap folds away, where `>> 8`
|
||||||
const auto slot_high = static_cast<std::uint8_t>(return_words >> 8);
|
// would leave 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 : static_cast<std::uint8_t>(ra_high << 1);
|
||||||
|
|
||||||
await_host();
|
// 'p' then 'b', each under a fresh window; anything else is line noise.
|
||||||
|
while (rx_deadline() != 'p' || rx_deadline() != 'b') {
|
||||||
|
}
|
||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
// No prompt while an EEPROM write runs: it blocks SPM and fuse reads
|
// No prompt while an EEPROM write runs: it blocks SPM and fuse reads
|
||||||
@@ -483,43 +416,47 @@ void fill_page(std::uint8_t bank, std::uint16_t at)
|
|||||||
link::drain();
|
link::drain();
|
||||||
jump(target);
|
jump(target);
|
||||||
}
|
}
|
||||||
case 'b': // identity: the version, then the three signature bytes
|
case 'b': // info block, read relative to the running slot
|
||||||
// Straight out of the stamp, so the wire and the image can never
|
case 'R': // read flash: addr16, n8 (0 = 256)
|
||||||
// disagree about what this loader is. The indices are constant and
|
case 'r': // read EEPROM: addr16, n8
|
||||||
// the array is constexpr, so these are immediates, not flash reads:
|
case 'w': { // write EEPROM: addr16, n8, then n bytes each acked
|
||||||
// nothing here needs the stamp's runtime address.
|
// One address-and-count path for all four: 'b' is a flash read
|
||||||
for (std::uint8_t at = stamp_identity; at != sizeof identity_stamp; ++at)
|
// whose arguments the loader already knows, so it joins the
|
||||||
link::tx(identity_stamp[at]);
|
// wire-argument three rather than streaming from a call site of its
|
||||||
break;
|
// own. That leaves one flash streamer in the image, and lets its
|
||||||
case 'W': // fill one flash page buffer: sel8, addr16, then page bytes
|
// cursor live in this never-returning loop's own call-saved
|
||||||
case 'G': // read: sel8, addr16, n8 (0 = 256)
|
// registers instead of being saved and restored around a call.
|
||||||
case 'g': { // write: sel8, addr16, n8, then n bytes, each acked
|
std::uint16_t address;
|
||||||
// One decode, one cursor and one loop for every space and both
|
std::uint8_t count;
|
||||||
// directions: a command per memory would carry a copy of all three
|
if (command == 'b') {
|
||||||
// each. 'W' joins the same decode rather than keeping an address
|
// The block sits in the image's first 256 bytes (check_pi.py
|
||||||
// form of its own, so flash addressing is uniform across every
|
// asserts it) and slots are 512-aligned, so the low byte of its
|
||||||
// command that names it.
|
// link address is its offset in any slot — halved where wire
|
||||||
const std::uint8_t selector = link::rx();
|
// units are words. The high byte is runtime data, so no
|
||||||
const std::uint8_t space = space_of(selector);
|
// absolute address is ever materialized.
|
||||||
const std::uint8_t bank = bank_of(selector);
|
const auto link_byte =
|
||||||
std::uint16_t at = rx16();
|
static_cast<std::uint8_t>(reinterpret_cast<std::uint16_t>(info_data.storage.data()));
|
||||||
if (command == 'W') {
|
const std::uint8_t low = word_flash ? static_cast<std::uint8_t>(link_byte >> 1) : link_byte;
|
||||||
fill_page(bank, at);
|
address = static_cast<std::uint16_t>(low | (slot_high << 8));
|
||||||
break;
|
count = static_cast<std::uint8_t>(info_data.size());
|
||||||
|
} else {
|
||||||
|
address = rx16();
|
||||||
|
count = link::rx();
|
||||||
}
|
}
|
||||||
std::uint8_t count = link::rx();
|
if (command == 'r')
|
||||||
do {
|
send_eeprom(address, count);
|
||||||
// Read and write are one letter apart in case, so the direction
|
else if (command == 'w')
|
||||||
// is a single bit and the loop picks it with a one-word skip.
|
store_eeprom(address, count);
|
||||||
if (command & 0x20) {
|
else
|
||||||
store(space, bank, at, link::rx(), slot_high);
|
send_flash(address, count);
|
||||||
tx_ack();
|
|
||||||
} else
|
|
||||||
link::tx(load(space, bank, at));
|
|
||||||
++at;
|
|
||||||
} while (--count);
|
|
||||||
break;
|
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
|
default: // unknown bytes are ignored; the loop re-acks
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ else:
|
|||||||
import termios
|
import termios
|
||||||
|
|
||||||
PROMPT = b"+"
|
PROMPT = b"+"
|
||||||
VERSION = 4 # this tool's own version — free to drift from a loader's
|
VERSION = 3 # this tool's own version — free to drift from a loader's
|
||||||
# The loader versions this tool speaks. A pureboot version implies its wire
|
# The loader versions this tool speaks. A pureboot version implies its wire
|
||||||
# protocol, which carries no number of its own, so this window is where that
|
# protocol, which carries no number of its own, so this window is where that
|
||||||
# map lives: every version so far speaks the same protocol, and one that
|
# map lives: every version so far speaks the same protocol, and one that
|
||||||
@@ -34,38 +34,36 @@ NEWEST_LOADER = 5
|
|||||||
SLOT = 512 # the loader slot, on every chip
|
SLOT = 512 # the loader slot, on every chip
|
||||||
RETRIES = 3 # rewrites of a page that reads back wrong, before the run stops
|
RETRIES = 3 # rewrites of a page that reads back wrong, before the run stops
|
||||||
|
|
||||||
# pureboot 5 replaced the four per-memory commands with one pair: 'G' reads and
|
# pureboot 5 replaced the four per-memory commands with one unified pair: 'G'
|
||||||
# 'g' writes, each taking a selector byte, a 16-bit address and a count, over
|
# reads and 'g' writes, both taking a selector byte, a 16-bit address and a
|
||||||
# the spaces below. The loader carries one transfer loop instead of four bodies
|
# count, over the spaces below. The loader carries one transfer loop instead of
|
||||||
# — which is what buys the data space and the host-issued SPM operations.
|
# four bodies, which is what paid for RAM access and for the calibration fix.
|
||||||
UNIFIED_LOADER = 5
|
UNIFIED_LOADER = 5
|
||||||
SP_FLASH, SP_EEPROM, SP_RAM, SP_FUSE, SP_SPM = 0, 1, 2, 3, 4
|
SP_FLASH, SP_EEPROM, SP_RAM, SP_FUSE, SP_SPM = 0, 1, 2, 3, 4
|
||||||
|
|
||||||
# A selector's high nibble is the flash bank — the address bits above the 16-bit
|
# A selector's high nibble is flash's third address byte, so a transfer names a
|
||||||
# wire address — so a transfer names a byte address within one 64 KiB bank and
|
# byte address within a 64 KiB bank and never has to speak word addresses. No
|
||||||
# no command has to speak word addresses. No single transfer may cross a bank
|
# single transfer may cross a bank boundary — the host chunks to keep that true.
|
||||||
# boundary; the host chunks to keep that true.
|
|
||||||
def selector(space, address):
|
def selector(space, address):
|
||||||
return space | ((address >> 16) << 4)
|
return space | ((address >> 16) << 4)
|
||||||
|
|
||||||
|
|
||||||
# The SPM operations pureboot 5 leaves to the host: a write to SP_SPM hands its
|
# The SPM operations pureboot 5 leaves to the host: a write to SP_SPM hands its
|
||||||
# byte to SPMCSR and fires the instruction at the selected flash address. Every
|
# data byte to SPMCSR and fires the instruction at the selected flash address.
|
||||||
# part pureboot targets agrees on these encodings.
|
# Every part pureboot targets agrees on these encodings.
|
||||||
SPM_ERASE, SPM_WRITE, SPM_RWWSRE = 0x03, 0x05, 0x11
|
SPM_ERASE, SPM_WRITE, SPM_RWWSRE = 0x03, 0x05, 0x11
|
||||||
|
|
||||||
# Calibration byte for an autobaud loader: 0xC0 is a start bit plus six zero
|
# Calibration byte for the autobaud loader: 0xC0 is a start bit plus six zero
|
||||||
# data bits — one low pulse of seven bit-times, which the loader times into its
|
# data bits — one low pulse of seven bit-times, which the loader times into its
|
||||||
# per-bit unit. Sent at whatever baud the host chose; the loader locks to it.
|
# per-bit unit. Sent at whatever baud the host chose; the loader locks to it.
|
||||||
CALIBRATE = 0xC0
|
CALIBRATE = 0xC0
|
||||||
|
|
||||||
# pureboot 5 answers 'b' with its version and the chip signature; the host
|
# The autobaud loader slims its info block to the version and signature; the host
|
||||||
# derives the rest of the geometry from the signature rather than reading a
|
# derives the rest of the geometry from the signature. flash, page, eeprom,
|
||||||
# table off the device. flash, page, eeprom, patch-vector per distinct
|
# patch-vector per distinct signature, over every chip pureboot targets (the
|
||||||
# signature, over every chip pureboot targets (the loader computes the same
|
# loader computes the same from its chip database at build time). Die revisions
|
||||||
# from its chip database at build time). Die revisions that share a signature
|
# that share a signature share this row, as they share the silicon.
|
||||||
# share this row, as they share the silicon.
|
AUTOBAUD_GEOMETRY = {
|
||||||
CHIP_GEOMETRY = {
|
|
||||||
# signature : (flash, page, eeprom, patch_vector)
|
# signature : (flash, page, eeprom, patch_vector)
|
||||||
(0x1E, 0x90, 0x07): (1024, 32, 64, True), # ATtiny13/13A
|
(0x1E, 0x90, 0x07): (1024, 32, 64, True), # ATtiny13/13A
|
||||||
(0x1E, 0x91, 0x08): (2048, 32, 128, True), # ATtiny25
|
(0x1E, 0x91, 0x08): (2048, 32, 128, True), # ATtiny25
|
||||||
@@ -361,26 +359,18 @@ class Info:
|
|||||||
"""The 12-byte info block."""
|
"""The 12-byte info block."""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_identity(cls, raw):
|
def from_slim(cls, raw):
|
||||||
"""pureboot 5's reply: the version and the chip signature. The rest of
|
"""The autobaud loader's slimmed reply — version and signature only —
|
||||||
the geometry is looked up from the signature — the loader derived the
|
with the rest of the geometry looked up from the signature (the loader
|
||||||
same facts from its chip database at build time, so nothing is guessed,
|
derived it from the same chip facts at build time). Reconstructs the full
|
||||||
it is simply not sent. Reconstructs a block in the older layout, so
|
block so every derived attribute matches the fixed-baud path exactly."""
|
||||||
every derived attribute below is shared with the loaders that do send
|
|
||||||
one.
|
|
||||||
|
|
||||||
The base is where application flash ends, which is a property of the
|
|
||||||
chip and not of the copy answering: a loader staged one slot lower
|
|
||||||
reports the same geometry the resident one does, exactly as the loaders
|
|
||||||
that send a block do. Which slot a copy runs in matters only to its own
|
|
||||||
write guard, which is the loader's business."""
|
|
||||||
if len(raw) != 4:
|
if len(raw) != 4:
|
||||||
raise Error(f"bad identity reply: {raw.hex()}")
|
raise Error(f"bad slim info block: {raw.hex()}")
|
||||||
version, signature = raw[0], tuple(raw[1:4])
|
version, signature = raw[0], tuple(raw[1:4])
|
||||||
geometry = CHIP_GEOMETRY.get(signature)
|
geometry = AUTOBAUD_GEOMETRY.get(signature)
|
||||||
if geometry is None:
|
if geometry is None:
|
||||||
sig = " ".join(f"{b:02x}" for b in signature)
|
sig = " ".join(f"{b:02x}" for b in signature)
|
||||||
raise Error(f"unknown signature {sig} — this tool has no geometry for it")
|
raise Error(f"unknown signature {sig} — this tool has no autobaud geometry for it")
|
||||||
flash, page, eeprom, patch = geometry
|
flash, page, eeprom, patch = geometry
|
||||||
base = flash - SLOT
|
base = flash - SLOT
|
||||||
word_flash = flash > 0x10000
|
word_flash = flash > 0x10000
|
||||||
@@ -403,11 +393,8 @@ class Info:
|
|||||||
self.signature = raw[3:6]
|
self.signature = raw[3:6]
|
||||||
self.page = raw[6] or 256 # the wire count convention: 0 means 256
|
self.page = raw[6] or 256 # the wire count convention: 0 means 256
|
||||||
self.patch_vector = bool(raw[11] & 1)
|
self.patch_vector = bool(raw[11] & 1)
|
||||||
# Bit 1: the flash runs past what one 16-bit address covers. Through
|
# Bit 1: flash addresses are words on the wire. Every address here
|
||||||
# pureboot 4 that made flash addresses words on the wire; pureboot 5
|
# stays a byte address and converts at the wire.
|
||||||
# keeps them bytes and carries the bank in the selector instead. Every
|
|
||||||
# address in this tool stays a byte address either way and converts at
|
|
||||||
# the wire.
|
|
||||||
self.word_flash = bool(raw[11] & 2)
|
self.word_flash = bool(raw[11] & 2)
|
||||||
scale = 2 if self.word_flash else 1
|
scale = 2 if self.word_flash else 1
|
||||||
self.base = (raw[7] | (raw[8] << 8)) * scale
|
self.base = (raw[7] | (raw[8] << 8)) * scale
|
||||||
@@ -437,7 +424,7 @@ class Info:
|
|||||||
f"version pureboot {self.version}",
|
f"version pureboot {self.version}",
|
||||||
f"signature {' '.join(f'{b:02x}' for b in self.signature)}",
|
f"signature {' '.join(f'{b:02x}' for b in self.signature)}",
|
||||||
f"flash {self.flash_size} B, {self.page} B pages"
|
f"flash {self.flash_size} B, {self.page} B pages"
|
||||||
+ (", past one 16-bit bank" if self.word_flash else ""),
|
+ (", word-addressed wire" if self.word_flash else ""),
|
||||||
f"application 0x0000..{self.base - 1:#06x} ({self.base} B)",
|
f"application 0x0000..{self.base - 1:#06x} ({self.base} B)",
|
||||||
f"loader {self.base:#06x} ({SLOT} B slot)",
|
f"loader {self.base:#06x} ({SLOT} B slot)",
|
||||||
f"staging {self.stage:#06x}",
|
f"staging {self.stage:#06x}",
|
||||||
@@ -454,69 +441,70 @@ class Loader:
|
|||||||
def __init__(self, port):
|
def __init__(self, port):
|
||||||
self.port = port
|
self.port = port
|
||||||
self.info = None
|
self.info = None
|
||||||
# Set once a session is established over an autobaud link, so a
|
|
||||||
# re-entry after 'J' repeats the handshake that worked.
|
|
||||||
self.autobaud = False
|
|
||||||
|
|
||||||
def _read_identity(self):
|
def connect(self, wait):
|
||||||
"""The 'b' reply, in either of the two layouts a loader may send.
|
"""Knock until the info block comes back. The block is what proves the
|
||||||
pureboot 5 answers with its version and the signature; older loaders
|
loader is listening — a prompt byte alone does not, since one left over
|
||||||
answer with a 12-byte block. The version byte cannot be mistaken for
|
from a previous session can still be in the pipeline while the port
|
||||||
the older block's 'P', so four bytes are enough to tell them apart."""
|
opening resets the device into a fresh activation window, where a
|
||||||
head = self.port.read_exact(4, 2.0)
|
command without its knock is discarded. Each attempt is therefore the
|
||||||
if head[0:2] == b"PB":
|
whole handshake, retried until it produces the block or the window
|
||||||
return Info(head + self.port.read_exact(8, 2.0))
|
closes. Also converges into a live session: the knock bytes are ignored
|
||||||
return Info.from_identity(head)
|
there and the drain absorbs whatever they produced."""
|
||||||
|
|
||||||
def _handshake(self, wait, knock, what):
|
|
||||||
"""One activation, retried until the loader answers or the window
|
|
||||||
closes. The identity reply is what proves the loader is listening — a
|
|
||||||
prompt byte alone does not, since one left over from a previous session
|
|
||||||
can still be in the pipeline while the port opening resets the device
|
|
||||||
into a fresh window, where a command without its knock is discarded.
|
|
||||||
Each attempt is therefore the whole handshake. This also converges into
|
|
||||||
an already-live session: the knock bytes are ignored there and the
|
|
||||||
drain absorbs whatever they produced."""
|
|
||||||
deadline = time.monotonic() + wait
|
deadline = time.monotonic() + wait
|
||||||
knocks = 0
|
knocks = 0
|
||||||
while True:
|
while True:
|
||||||
self.port.flush_input()
|
self.port.flush_input()
|
||||||
self.port.write(knock)
|
self.port.write(b"pb")
|
||||||
knocks += 1
|
knocks += 1
|
||||||
if PROMPT in self.port.read_available(0.4):
|
if PROMPT in self.port.read_available(0.4):
|
||||||
while self.port.read_available(0.3):
|
while self.port.read_available(0.3):
|
||||||
pass
|
pass
|
||||||
self.port.write(b"b")
|
self.port.write(b"b")
|
||||||
try:
|
try:
|
||||||
# A version the tool cannot speak is the loader's own
|
block = self.port.read_exact(12, 2.0)
|
||||||
# answer, not a failed knock: Info reports it rather than
|
except Error:
|
||||||
# sending the tool round the loop again.
|
block = b""
|
||||||
self.info = self._read_identity()
|
# A version the tool cannot speak is the loader's own answer,
|
||||||
except Error as failed:
|
# not a failed knock: Info reports it rather than retrying.
|
||||||
if "pureboot" in str(failed):
|
if block[0:2] == b"PB":
|
||||||
raise
|
self.info = Info(block)
|
||||||
self.info = None
|
|
||||||
if self.info is not None:
|
|
||||||
self._expect_prompt()
|
self._expect_prompt()
|
||||||
verbose(f"loader answered {what} {knocks}; identity read")
|
verbose(f"loader answered knock {knocks}; info block read")
|
||||||
return self.info
|
return self.info
|
||||||
if time.monotonic() > deadline:
|
if time.monotonic() > deadline:
|
||||||
raise Error("no answer — reset the device within its activation window")
|
raise Error("no answer — reset the device within its activation window")
|
||||||
|
|
||||||
def connect(self, wait):
|
|
||||||
"""Knock 'p' then 'b' and read the identity."""
|
|
||||||
return self._handshake(wait, b"pb", "knock")
|
|
||||||
|
|
||||||
def connect_autobaud(self, wait):
|
def connect_autobaud(self, wait):
|
||||||
"""The autobaud handshake. In place of the p+b knock the host sends the
|
"""The autobaud handshake. Instead of the p+b knock, the host sends the
|
||||||
calibration pulse — one seven-bit-time low pulse at the host's chosen
|
0xC0 calibration pulse — a single seven-bit-time low pulse at the host's
|
||||||
baud, which the loader times into its per-bit unit — then a single 'p'
|
chosen baud — which the loader times into its per-bit unit, then a single
|
||||||
the loader decodes at the rate it just measured. A lost pulse, or a
|
'p' knock the loader decodes at the rate it just measured. As with
|
||||||
knock landing while the loader is mid-frame, simply fails to answer and
|
connect(), each attempt is the whole handshake, retried until the slim
|
||||||
leaves the measurement loop waiting for the next pulse, so the retry in
|
info block comes back or the window closes: a lost pulse or a knock that
|
||||||
_handshake covers it."""
|
lands while the loader is mid-frame simply fails to answer, and the
|
||||||
self.autobaud = True
|
loader's measurement loop is back waiting for the next pulse."""
|
||||||
return self._handshake(wait, bytes((CALIBRATE, ord("p"))), "calibration")
|
deadline = time.monotonic() + wait
|
||||||
|
knocks = 0
|
||||||
|
while True:
|
||||||
|
self.port.flush_input()
|
||||||
|
self.port.write(bytes((CALIBRATE, ord("p"))))
|
||||||
|
knocks += 1
|
||||||
|
if PROMPT in self.port.read_available(0.4):
|
||||||
|
while self.port.read_available(0.3):
|
||||||
|
pass
|
||||||
|
self.port.write(b"b")
|
||||||
|
try:
|
||||||
|
block = self.port.read_exact(4, 2.0)
|
||||||
|
except Error:
|
||||||
|
block = b""
|
||||||
|
if len(block) == 4:
|
||||||
|
self.info = Info.from_slim(block)
|
||||||
|
self._expect_prompt()
|
||||||
|
verbose(f"loader locked on knock {knocks}; slim info read")
|
||||||
|
return self.info
|
||||||
|
if time.monotonic() > deadline:
|
||||||
|
raise Error("no answer — reset the device within its activation window")
|
||||||
|
|
||||||
def _expect_prompt(self, timeout=2.0):
|
def _expect_prompt(self, timeout=2.0):
|
||||||
byte = self.port.read_exact(1, timeout)
|
byte = self.port.read_exact(1, timeout)
|
||||||
@@ -662,9 +650,8 @@ class Loader:
|
|||||||
def enter_copy(self, byte_address, wait):
|
def enter_copy(self, byte_address, wait):
|
||||||
"""Jump into the loader copy at `byte_address` and knock it — a slot
|
"""Jump into the loader copy at `byte_address` and knock it — a slot
|
||||||
base is that copy's entry stub, so it can only land there."""
|
base is that copy's entry stub, so it can only land there."""
|
||||||
autobaud = self.autobaud
|
|
||||||
self.jump(byte_address // 2)
|
self.jump(byte_address // 2)
|
||||||
return self.connect_autobaud(wait) if autobaud else self.connect(wait)
|
return self.connect(wait)
|
||||||
|
|
||||||
def run_application(self):
|
def run_application(self):
|
||||||
self.jump(self.info.app_entry_word)
|
self.jump(self.info.app_entry_word)
|
||||||
@@ -831,26 +818,12 @@ def mega_boot(info, fuse_bytes):
|
|||||||
|
|
||||||
|
|
||||||
def image_info(image):
|
def image_info(image):
|
||||||
"""What a pureboot binary says about itself, or None.
|
"""The info block embedded in a pureboot binary, or None. Searched once
|
||||||
|
per known version, so the magic stays three selective bytes rather than
|
||||||
An update image is a bare slot: nothing about it names the chip it was
|
two that code could carry by chance."""
|
||||||
built for, and installing a foreign one bricks the target — so every
|
|
||||||
loader carries a stamp for this. Through pureboot 4 the stamp is the
|
|
||||||
12-byte info block the device also serves; pureboot 5 serves its identity
|
|
||||||
from immediates and carries a 6-byte stamp (magic, version, signature)
|
|
||||||
that only this exists for, from which the geometry is looked up exactly as
|
|
||||||
it is for a live device.
|
|
||||||
|
|
||||||
Searched once per known version, so the magic stays three selective bytes
|
|
||||||
rather than two that code could carry by chance."""
|
|
||||||
for version in range(OLDEST_LOADER, NEWEST_LOADER + 1):
|
for version in range(OLDEST_LOADER, NEWEST_LOADER + 1):
|
||||||
at = image.find(b"PB" + bytes((version,)))
|
at = image.find(b"PB" + bytes((version,)))
|
||||||
if at < 0:
|
if 0 <= at <= len(image) - 12:
|
||||||
continue
|
|
||||||
if version >= UNIFIED_LOADER:
|
|
||||||
if at <= len(image) - 6:
|
|
||||||
return Info.from_identity(image[at + 2 : at + 6])
|
|
||||||
elif at <= len(image) - 12:
|
|
||||||
return Info(image[at : at + 12])
|
return Info(image[at : at + 12])
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -1040,10 +1013,7 @@ def op_update_loader(loader, wait, path, state_path, fuse_bytes):
|
|||||||
# it and matching byte for byte, and the slot unchanged since this update
|
# it and matching byte for byte, and the slot unchanged since this update
|
||||||
# began, so a half-written install takes the path below instead.
|
# began, so a half-written install takes the path below instead.
|
||||||
current = loader.read_flash(info.stage, SLOT)
|
current = loader.read_flash(info.stage, SLOT)
|
||||||
# The whole slot is searched: a loader's stamp sits wherever its image put
|
staged_loader = image_info(current[:268])
|
||||||
# it, which is the end of the code on pureboot 5 and the front of it
|
|
||||||
# before that.
|
|
||||||
staged_loader = image_info(current)
|
|
||||||
if staged_loader is not None and staged_loader.raw == info.raw and current == state.staging:
|
if staged_loader is not None and staged_loader.raw == info.raw and current == state.staging:
|
||||||
print("staging slot already holds a loader — left in place")
|
print("staging slot already holds a loader — left in place")
|
||||||
else:
|
else:
|
||||||
|
|||||||
396
pureboot/pureboot_autobaud_pure.cpp
Normal file
396
pureboot/pureboot_autobaud_pure.cpp
Normal file
@@ -0,0 +1,396 @@
|
|||||||
|
// SUPERSEDED — kept for the record, not built. With the activation hang fixed
|
||||||
|
// (a lone calibration pulse used to wedge the loader, see autobaud.md) this
|
||||||
|
// version is 530 B on the 1284P against a 512 B slot. Its 4 B of margin was
|
||||||
|
// never spare capacity; it was the space the missing fix should have occupied.
|
||||||
|
// pureboot_autobaud_uni.cpp replaces it at 464 B with more features.
|
||||||
|
//
|
||||||
|
// pureboot autobaud — the software-serial variant that measures the host's bit
|
||||||
|
// timing at runtime, so one binary runs at any F_CPU: the image carries no
|
||||||
|
// clock. THE PURE VERSION — no inline assembly, no global register variables,
|
||||||
|
// exactly the constraints the fixed-baud loader keeps.
|
||||||
|
//
|
||||||
|
// Two source files exist for review (autobaud.md):
|
||||||
|
// this one, pure, and pureboot_autobaud_reg.cpp, which keeps the running-slot
|
||||||
|
// write guard at the cost of one global register variable. They differ only in
|
||||||
|
// where the measured unit lives and whether the guard is present.
|
||||||
|
//
|
||||||
|
// What this version trades to fit 512 B in pure C++ (the 1284 at 508), each
|
||||||
|
// licensed by "the host guarantees safety" (README.md) and the owner's approval
|
||||||
|
// to simplify the info block:
|
||||||
|
// - the measured per-bit unit lives in the two general-purpose I/O scratch
|
||||||
|
// registers (GPIOR) where the chip has them, in a static otherwise —
|
||||||
|
// reached through libavr's named register surface, so no asm and no global
|
||||||
|
// register variable; the loader stays pure;
|
||||||
|
// - the info block is slimmed to the version and the signature — the chip's
|
||||||
|
// identity — from which the host derives page size, loader base, EEPROM
|
||||||
|
// size and the addressing flags via its own chip database;
|
||||||
|
// - no running-slot write guard: the host never programs the loader's own
|
||||||
|
// slot, and a broken host bricking the target is the host's bug;
|
||||||
|
// - a single-byte activation knock: the calibration pulse already proves a
|
||||||
|
// host is present.
|
||||||
|
//
|
||||||
|
// Position independence is kept and is in fact total here: control flow is
|
||||||
|
// PC-relative, the wire carries addresses, and with the slimmed info block and
|
||||||
|
// no write guard nothing anchors on the runtime address at all.
|
||||||
|
|
||||||
|
#include <libavr/libavr.hpp>
|
||||||
|
|
||||||
|
#include <util/delay_basic.h>
|
||||||
|
|
||||||
|
using namespace avr::literals;
|
||||||
|
namespace spm = avr::spm;
|
||||||
|
namespace ee = avr::eeprom;
|
||||||
|
namespace hw = avr::hw;
|
||||||
|
|
||||||
|
namespace pureboot {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Purely polled: every interrupt guard folds to nothing.
|
||||||
|
constexpr auto off = avr::irq::guard_policy::unused;
|
||||||
|
|
||||||
|
constexpr std::uint8_t ack = '+';
|
||||||
|
|
||||||
|
// Autobaud carries no clock, so PUREBOOT_CLOCK_HZ / PUREBOOT_BAUD are not
|
||||||
|
// consulted; only the software-UART pins are a deployment parameter.
|
||||||
|
#if !defined(PUREBOOT_RX)
|
||||||
|
#define PUREBOOT_RX pb0
|
||||||
|
#endif
|
||||||
|
#if !defined(PUREBOOT_TX)
|
||||||
|
#define PUREBOOT_TX pb1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// The watchdog reset flag's home: MCUSR, or the classic megas' MCUCSR.
|
||||||
|
consteval std::int16_t wdrf_field()
|
||||||
|
{
|
||||||
|
auto reg = std::string_view{hw::db.regs[static_cast<std::size_t>(avr::power::detail::reset_reg())].name};
|
||||||
|
return hw::db.field_index(reg, "WDRF");
|
||||||
|
}
|
||||||
|
|
||||||
|
// The loader owns the top 512 bytes; a staging copy goes in the slot below.
|
||||||
|
constexpr std::uint16_t slot_bytes = 512;
|
||||||
|
constexpr std::uint32_t base = spm::flash_bytes - slot_bytes;
|
||||||
|
constexpr std::uint16_t page = spm::page_bytes;
|
||||||
|
constexpr bool boot_section = hw::curated::has_boot_section();
|
||||||
|
|
||||||
|
// Past 64 KiB a byte address no longer fits the wire's 16 bits, so flash
|
||||||
|
// addresses there are word addresses ('J' always was one).
|
||||||
|
constexpr bool word_flash = spm::flash_bytes > 65536;
|
||||||
|
|
||||||
|
// The loader's one identity number (README.md); the slimmed info block carries
|
||||||
|
// it and the signature, and the host maps a version to its protocol.
|
||||||
|
constexpr std::uint8_t version = 4;
|
||||||
|
|
||||||
|
// The activation window as a fixed poll budget: with no clock, whole seconds
|
||||||
|
// cannot be timed. A __uint24 (AVR's three-byte type) holds it — a fourth byte
|
||||||
|
// would cost two words at each countdown step for range never used.
|
||||||
|
#if !defined(PUREBOOT_AUTOBAUD_POLLS)
|
||||||
|
#define PUREBOOT_AUTOBAUD_POLLS 4000000
|
||||||
|
#endif
|
||||||
|
constexpr __uint24 autobaud_budget = PUREBOOT_AUTOBAUD_POLLS;
|
||||||
|
|
||||||
|
// The measured per-bit delay (in _delay_loop_2 four-cycle iterations). It lives
|
||||||
|
// in the two adjacent general-purpose I/O scratch registers (GPIOR1:GPIOR2)
|
||||||
|
// where the chip has them — in/out reach them in one word where a static's
|
||||||
|
// lds/sts take two, and there is no .bss to clear — and in a plain static
|
||||||
|
// otherwise (the t13, m8 and m16/32 have no GPIOR). Both are pure: the named
|
||||||
|
// register surface, no inline asm, no global register variable.
|
||||||
|
constexpr bool have_gpior = hw::db.reg_index("GPIOR1") >= 0 && hw::db.reg_index("GPIOR2") >= 0;
|
||||||
|
|
||||||
|
std::uint16_t unit_backing;
|
||||||
|
|
||||||
|
template <bool Gpior = have_gpior>
|
||||||
|
[[gnu::always_inline]] inline std::uint16_t get_unit()
|
||||||
|
{
|
||||||
|
if constexpr (Gpior)
|
||||||
|
return static_cast<std::uint16_t>(hw::reg_impl<hw::db.reg_index("GPIOR1")>::read() |
|
||||||
|
(hw::reg_impl<hw::db.reg_index("GPIOR2")>::read() << 8));
|
||||||
|
else
|
||||||
|
return unit_backing;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <bool Gpior = have_gpior>
|
||||||
|
[[gnu::always_inline]] inline void put_unit(std::uint16_t u)
|
||||||
|
{
|
||||||
|
if constexpr (Gpior) {
|
||||||
|
hw::reg_impl<hw::db.reg_index("GPIOR1")>::write(static_cast<std::uint8_t>(u));
|
||||||
|
hw::reg_impl<hw::db.reg_index("GPIOR2")>::write(static_cast<std::uint8_t>(u >> 8));
|
||||||
|
} else
|
||||||
|
unit_backing = u;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The autobaud software link: bit-banged with cycle-counted delays like the
|
||||||
|
// fixed-baud software backend, but the per-bit delay is the measured unit, not
|
||||||
|
// a consteval constant. rx/tx load the unit once into a local, so each bit
|
||||||
|
// spins from a register with no per-bit reload.
|
||||||
|
struct link {
|
||||||
|
using in_t = avr::io::input<avr::PUREBOOT_RX, avr::io::pull::up>;
|
||||||
|
using out_t = avr::io::output<avr::PUREBOOT_TX>;
|
||||||
|
|
||||||
|
static void init()
|
||||||
|
{
|
||||||
|
avr::init<in_t, out_t>();
|
||||||
|
out_t::set(); // idle high
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time the calibration pulse into the unit. The host sends 0xC0 — a start
|
||||||
|
// bit plus six zero data bits are one low pulse of seven bit-times — and the
|
||||||
|
// counted poll loop is seven cycles an iteration, so the count is the pulse
|
||||||
|
// length in cycles ÷ 7 × 7 = one bit period in cycles, and count >> 2 is that
|
||||||
|
// period in _delay_loop_2's four-cycle iterations. Waits for the start edge
|
||||||
|
// under the poll budget; 0 (returned, and stored) means the budget expired.
|
||||||
|
static std::uint16_t measure(__uint24 budget)
|
||||||
|
{
|
||||||
|
while (in_t::read())
|
||||||
|
if (!--budget)
|
||||||
|
return 0;
|
||||||
|
std::uint16_t count = 0;
|
||||||
|
while (!in_t::read())
|
||||||
|
++count;
|
||||||
|
const std::uint16_t u = static_cast<std::uint16_t>(count >> 2);
|
||||||
|
put_unit(u);
|
||||||
|
return u;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The eight data bits, entered just past the falling edge of the start bit.
|
||||||
|
// Split out of rx() so the activation path can wait for that edge under a
|
||||||
|
// budget while the command loop waits for it indefinitely.
|
||||||
|
static std::uint8_t sample()
|
||||||
|
{
|
||||||
|
const std::uint16_t unit = get_unit();
|
||||||
|
_delay_loop_2(static_cast<std::uint16_t>(unit + (unit >> 1))); // 1.5 bits to the LSB centre
|
||||||
|
std::uint8_t value = 0;
|
||||||
|
for (std::uint8_t bit = 0; bit < 8; ++bit) {
|
||||||
|
value >>= 1;
|
||||||
|
if (in_t::read())
|
||||||
|
value |= 0x80;
|
||||||
|
_delay_loop_2(unit);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::uint8_t rx()
|
||||||
|
{
|
||||||
|
while (in_t::read()) // await the start edge
|
||||||
|
;
|
||||||
|
return sample();
|
||||||
|
}
|
||||||
|
|
||||||
|
// A byte under the poll budget, for the activation knock. An expired budget
|
||||||
|
// returns 0, which is not the knock, so the caller falls back into the
|
||||||
|
// budgeted measure() — and a line that stays idle boots the application
|
||||||
|
// there. Without this the knock's edge wait was unbounded, so a single
|
||||||
|
// spurious calibration pulse (EMI, or a host that opens the port and never
|
||||||
|
// knocks) wedged the loader and the application never ran.
|
||||||
|
static std::uint8_t rx_bounded(__uint24 budget)
|
||||||
|
{
|
||||||
|
while (in_t::read())
|
||||||
|
if (!--budget)
|
||||||
|
return 0;
|
||||||
|
return sample();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void tx(std::uint8_t value)
|
||||||
|
{
|
||||||
|
const std::uint16_t unit = get_unit();
|
||||||
|
out_t::clear(); // start bit
|
||||||
|
_delay_loop_2(unit);
|
||||||
|
for (std::uint8_t bit = 0; bit < 8; ++bit) {
|
||||||
|
out_t::write(value & 1);
|
||||||
|
value >>= 1;
|
||||||
|
_delay_loop_2(unit);
|
||||||
|
}
|
||||||
|
out_t::set(); // stop bit
|
||||||
|
_delay_loop_2(unit);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void drain()
|
||||||
|
{
|
||||||
|
// The software transmitter returns only after the stop bit.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inlined: read across a call, the first byte strands in a call-saved register
|
||||||
|
// the caller has to push and pop.
|
||||||
|
[[gnu::always_inline]] inline std::uint16_t rx16()
|
||||||
|
{
|
||||||
|
std::uint16_t low = link::rx();
|
||||||
|
return static_cast<std::uint16_t>(low | (link::rx() << 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
[[gnu::always_inline]] inline std::uint16_t word_of(std::array<std::uint8_t, 2> pair)
|
||||||
|
{
|
||||||
|
return std::bit_cast<std::uint16_t>(pair);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[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);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[maybe_unused, gnu::always_inline]] inline 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));
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[gnu::noinline]] void tx_ack()
|
||||||
|
{
|
||||||
|
link::tx(ack);
|
||||||
|
}
|
||||||
|
|
||||||
|
void send_eeprom(std::uint16_t address, std::uint8_t count)
|
||||||
|
{
|
||||||
|
do
|
||||||
|
link::tx(ee::read(address++));
|
||||||
|
while (--count);
|
||||||
|
}
|
||||||
|
|
||||||
|
void store_eeprom(std::uint16_t address, std::uint8_t count)
|
||||||
|
{
|
||||||
|
do {
|
||||||
|
ee::write<off>(address++, link::rx());
|
||||||
|
tx_ack();
|
||||||
|
} while (--count);
|
||||||
|
}
|
||||||
|
|
||||||
|
// One page into the SPM buffer, then erase and program. No running-slot write
|
||||||
|
// guard: the host guarantees it never targets the loader's own slot (the pure
|
||||||
|
// version's one dropped safety net, licensed — README.md).
|
||||||
|
void program_flash(std::uint16_t wire_address)
|
||||||
|
{
|
||||||
|
spm::flash_address_t address;
|
||||||
|
if constexpr (word_flash) {
|
||||||
|
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) & ~static_cast<std::uint16_t>(page - 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, word_of({low, high}));
|
||||||
|
z += 2;
|
||||||
|
} while (static_cast<std::uint8_t>(z));
|
||||||
|
address = (static_cast<spm::flash_address_t>(rampz) << 16) | z0;
|
||||||
|
} else {
|
||||||
|
address = static_cast<spm::flash_address_t>(wire_address & ~static_cast<std::uint16_t>(page - 1));
|
||||||
|
do {
|
||||||
|
std::uint8_t low = link::rx();
|
||||||
|
std::uint8_t high = link::rx();
|
||||||
|
spm::fill<off>(address, word_of({low, high}));
|
||||||
|
address += 2;
|
||||||
|
} while (static_cast<std::uint8_t>(address) & (page - 1));
|
||||||
|
address -= 2; // back inside the page — erase and write ignore the word bits
|
||||||
|
}
|
||||||
|
spm::erase_page<off>(address);
|
||||||
|
if constexpr (boot_section)
|
||||||
|
spm::wait();
|
||||||
|
spm::write_page<off>(address);
|
||||||
|
if constexpr (boot_section)
|
||||||
|
spm::wait();
|
||||||
|
if constexpr (boot_section)
|
||||||
|
spm::rww_enable<off>();
|
||||||
|
}
|
||||||
|
|
||||||
|
void send_fuses()
|
||||||
|
{
|
||||||
|
std::uint8_t which = 0;
|
||||||
|
do
|
||||||
|
link::tx(spm::read_fuse<off>(static_cast<spm::fuse>(which)));
|
||||||
|
while (++which != 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[noreturn]] void run()
|
||||||
|
{
|
||||||
|
if (hw::field_impl<wdrf_field()>::test())
|
||||||
|
run_app();
|
||||||
|
|
||||||
|
link::init();
|
||||||
|
|
||||||
|
// Measure the calibration pulse into the unit, then take one 'p' knock. An
|
||||||
|
// expired budget (no host) boots the application; a pulse that decodes to
|
||||||
|
// anything but 'p' re-measures.
|
||||||
|
for (;;) {
|
||||||
|
if (link::measure(autobaud_budget) == 0)
|
||||||
|
run_app();
|
||||||
|
if (link::rx_bounded(autobaud_budget) == 'p')
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
ee::wait();
|
||||||
|
tx_ack();
|
||||||
|
const std::uint8_t command = link::rx();
|
||||||
|
switch (command) {
|
||||||
|
case 'J': { // jump to a wire word address: hand-over and staging transfer
|
||||||
|
auto target = reinterpret_cast<void (*)()>(rx16());
|
||||||
|
tx_ack();
|
||||||
|
link::drain();
|
||||||
|
jump(target);
|
||||||
|
}
|
||||||
|
case 'b': // chip identity: version then the three signature bytes
|
||||||
|
link::tx(version);
|
||||||
|
link::tx(hw::db.signature[0]);
|
||||||
|
link::tx(hw::db.signature[1]);
|
||||||
|
link::tx(hw::db.signature[2]);
|
||||||
|
break;
|
||||||
|
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
|
||||||
|
// One address-and-count path for the three, so the flash streamer
|
||||||
|
// keeps a single call site and inlines into this never-returning
|
||||||
|
// loop — its cursor then lives in the loop's own call-saved
|
||||||
|
// registers instead of being saved and restored around a call
|
||||||
|
// (the call-site-count lesson, autobaud.md).
|
||||||
|
const std::uint16_t address = rx16();
|
||||||
|
const std::uint8_t count = link::rx();
|
||||||
|
if (command == 'r')
|
||||||
|
send_eeprom(address, count);
|
||||||
|
else if (command == 'w')
|
||||||
|
store_eeprom(address, count);
|
||||||
|
else
|
||||||
|
send_flash(address, count);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'W': // program one flash page: addr16, page bytes
|
||||||
|
program_flash(rx16());
|
||||||
|
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>;
|
||||||
371
pureboot/pureboot_autobaud_reg.cpp
Normal file
371
pureboot/pureboot_autobaud_reg.cpp
Normal file
@@ -0,0 +1,371 @@
|
|||||||
|
// SUPERSEDED — kept for the record, not built. With the activation hang fixed
|
||||||
|
// (a lone calibration pulse used to wedge the loader, see autobaud.md) this
|
||||||
|
// version is 534 B on the 1284P against a 512 B slot, so the global register
|
||||||
|
// variable it broke purity for buys nothing. pureboot_autobaud_uni.cpp replaces
|
||||||
|
// it at 464 B, strictly pure and with more features.
|
||||||
|
//
|
||||||
|
// pureboot autobaud — the software-serial variant that measures the host's bit
|
||||||
|
// timing at runtime, so one binary runs at any F_CPU: the image carries no
|
||||||
|
// clock. THE REGISTER VERSION — keeps the running-slot write guard, at the cost
|
||||||
|
// of one global register variable (r4) holding the measured unit. That variable
|
||||||
|
// is pureboot's single, deliberate break from its no-global-register-variable
|
||||||
|
// rule, present only in this variant; everything else stays pure C++.
|
||||||
|
//
|
||||||
|
// Two source files exist for review (autobaud.md):
|
||||||
|
// pureboot_autobaud_pure.cpp, fully pure but dropping the write guard, and this
|
||||||
|
// one. They differ only in where the measured unit lives (a call-saved register
|
||||||
|
// here, GPIOR/RAM there) and whether the guard is present.
|
||||||
|
//
|
||||||
|
// The register buys ~32 B over a RAM home — an outlined rx/tx reads it with one
|
||||||
|
// move where a static costs an lds — and that is what lets the write guard stay
|
||||||
|
// while the image still fits 512 B (the 1284 at 512, exactly). The unit is
|
||||||
|
// written once through a noinline setter so the store lands immediately before a
|
||||||
|
// ret: GCC otherwise deletes a global-register store whose only readers are
|
||||||
|
// callees (autobaud.md, upstream bug 6).
|
||||||
|
//
|
||||||
|
// Simplifications shared with the pure version, each licensed: a slimmed info
|
||||||
|
// block (version + signature; the host derives geometry from its chip database)
|
||||||
|
// and a single-byte activation knock (the calibration pulse already proves a
|
||||||
|
// host). Position independence is kept: control flow is PC-relative and the
|
||||||
|
// write guard anchors on the runtime return address, as the fixed-baud loader.
|
||||||
|
|
||||||
|
#include <libavr/libavr.hpp>
|
||||||
|
|
||||||
|
#include <util/delay_basic.h>
|
||||||
|
|
||||||
|
using namespace avr::literals;
|
||||||
|
namespace spm = avr::spm;
|
||||||
|
namespace ee = avr::eeprom;
|
||||||
|
namespace hw = avr::hw;
|
||||||
|
|
||||||
|
// The measured per-bit delay (in _delay_loop_2 four-cycle iterations) in a
|
||||||
|
// call-saved register that the serial callees read directly, so no wire path
|
||||||
|
// threads it and rx/tx reach it with a move, not a load. Written only through
|
||||||
|
// set_unit() below.
|
||||||
|
register std::uint16_t g_unit asm("r4");
|
||||||
|
|
||||||
|
namespace pureboot {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Purely polled: every interrupt guard folds to nothing.
|
||||||
|
constexpr auto off = avr::irq::guard_policy::unused;
|
||||||
|
|
||||||
|
constexpr std::uint8_t ack = '+';
|
||||||
|
|
||||||
|
// Autobaud carries no clock; only the software-UART pins are a parameter.
|
||||||
|
#if !defined(PUREBOOT_RX)
|
||||||
|
#define PUREBOOT_RX pb0
|
||||||
|
#endif
|
||||||
|
#if !defined(PUREBOOT_TX)
|
||||||
|
#define PUREBOOT_TX pb1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
consteval std::int16_t wdrf_field()
|
||||||
|
{
|
||||||
|
auto reg = std::string_view{hw::db.regs[static_cast<std::size_t>(avr::power::detail::reset_reg())].name};
|
||||||
|
return hw::db.field_index(reg, "WDRF");
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr std::uint16_t slot_bytes = 512;
|
||||||
|
constexpr std::uint32_t base = spm::flash_bytes - slot_bytes;
|
||||||
|
constexpr std::uint16_t page = spm::page_bytes;
|
||||||
|
constexpr bool boot_section = hw::curated::has_boot_section();
|
||||||
|
constexpr bool word_flash = spm::flash_bytes > 65536;
|
||||||
|
|
||||||
|
constexpr std::uint8_t version = 4;
|
||||||
|
|
||||||
|
#if !defined(PUREBOOT_AUTOBAUD_POLLS)
|
||||||
|
#define PUREBOOT_AUTOBAUD_POLLS 4000000
|
||||||
|
#endif
|
||||||
|
constexpr __uint24 autobaud_budget = PUREBOOT_AUTOBAUD_POLLS;
|
||||||
|
|
||||||
|
// The one store into g_unit, isolated so it lands right before the ret: a
|
||||||
|
// global-register store whose only later readers are callees is dropped
|
||||||
|
// otherwise (autobaud.md, upstream bug 6).
|
||||||
|
[[gnu::noinline]] void set_unit(std::uint16_t v)
|
||||||
|
{
|
||||||
|
g_unit = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The autobaud software link: bit-banged with cycle-counted delays, but the
|
||||||
|
// per-bit delay is g_unit, measured from the host's calibration pulse.
|
||||||
|
struct link {
|
||||||
|
using in_t = avr::io::input<avr::PUREBOOT_RX, avr::io::pull::up>;
|
||||||
|
using out_t = avr::io::output<avr::PUREBOOT_TX>;
|
||||||
|
|
||||||
|
static void init()
|
||||||
|
{
|
||||||
|
avr::init<in_t, out_t>();
|
||||||
|
out_t::set(); // idle high
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time the calibration pulse into g_unit. The host sends 0xC0 — a start bit
|
||||||
|
// plus six zero data bits are one low pulse of seven bit-times — and the
|
||||||
|
// counted poll loop is seven cycles an iteration, so count >> 2 is the bit
|
||||||
|
// period in _delay_loop_2's four-cycle iterations. 0 means the budget
|
||||||
|
// expired.
|
||||||
|
static std::uint16_t measure(__uint24 budget)
|
||||||
|
{
|
||||||
|
while (in_t::read())
|
||||||
|
if (!--budget)
|
||||||
|
return 0;
|
||||||
|
std::uint16_t count = 0;
|
||||||
|
while (!in_t::read())
|
||||||
|
++count;
|
||||||
|
const std::uint16_t u = static_cast<std::uint16_t>(count >> 2);
|
||||||
|
set_unit(u);
|
||||||
|
return u;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The eight data bits, entered just past the falling edge of the start bit.
|
||||||
|
// Split out of rx() so the activation path can wait for that edge under a
|
||||||
|
// budget while the command loop waits for it indefinitely.
|
||||||
|
static std::uint8_t sample()
|
||||||
|
{
|
||||||
|
_delay_loop_2(static_cast<std::uint16_t>(g_unit + (g_unit >> 1))); // 1.5 bits to the LSB centre
|
||||||
|
std::uint8_t value = 0;
|
||||||
|
for (std::uint8_t bit = 0; bit < 8; ++bit) {
|
||||||
|
value >>= 1;
|
||||||
|
if (in_t::read())
|
||||||
|
value |= 0x80;
|
||||||
|
_delay_loop_2(g_unit);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::uint8_t rx()
|
||||||
|
{
|
||||||
|
while (in_t::read()) // await the start edge
|
||||||
|
;
|
||||||
|
return sample();
|
||||||
|
}
|
||||||
|
|
||||||
|
// A byte under the poll budget, for the activation knock. An expired budget
|
||||||
|
// returns 0, which is not the knock, so the caller falls back into the
|
||||||
|
// budgeted measure() — and a line that stays idle boots the application
|
||||||
|
// there. Without this the knock's edge wait was unbounded, so a single
|
||||||
|
// spurious calibration pulse (EMI, or a host that opens the port and never
|
||||||
|
// knocks) wedged the loader and the application never ran.
|
||||||
|
static std::uint8_t rx_bounded(__uint24 budget)
|
||||||
|
{
|
||||||
|
while (in_t::read())
|
||||||
|
if (!--budget)
|
||||||
|
return 0;
|
||||||
|
return sample();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void tx(std::uint8_t value)
|
||||||
|
{
|
||||||
|
out_t::clear(); // start bit
|
||||||
|
_delay_loop_2(g_unit);
|
||||||
|
for (std::uint8_t bit = 0; bit < 8; ++bit) {
|
||||||
|
out_t::write(value & 1);
|
||||||
|
value >>= 1;
|
||||||
|
_delay_loop_2(g_unit);
|
||||||
|
}
|
||||||
|
out_t::set(); // stop bit
|
||||||
|
_delay_loop_2(g_unit);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void drain()
|
||||||
|
{
|
||||||
|
// The software transmitter returns only after the stop bit.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[gnu::always_inline]] inline std::uint16_t rx16()
|
||||||
|
{
|
||||||
|
std::uint16_t low = link::rx();
|
||||||
|
return static_cast<std::uint16_t>(low | (link::rx() << 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
[[gnu::always_inline]] inline std::uint16_t word_of(std::array<std::uint8_t, 2> pair)
|
||||||
|
{
|
||||||
|
return std::bit_cast<std::uint16_t>(pair);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[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);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[maybe_unused, gnu::always_inline]] inline 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));
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[gnu::noinline]] void tx_ack()
|
||||||
|
{
|
||||||
|
link::tx(ack);
|
||||||
|
}
|
||||||
|
|
||||||
|
void send_eeprom(std::uint16_t address, std::uint8_t count)
|
||||||
|
{
|
||||||
|
do
|
||||||
|
link::tx(ee::read(address++));
|
||||||
|
while (--count);
|
||||||
|
}
|
||||||
|
|
||||||
|
void store_eeprom(std::uint16_t address, std::uint8_t count)
|
||||||
|
{
|
||||||
|
do {
|
||||||
|
ee::write<off>(address++, link::rx());
|
||||||
|
tx_ack();
|
||||||
|
} while (--count);
|
||||||
|
}
|
||||||
|
|
||||||
|
// One page into the SPM buffer, then erase and program — except the slot this
|
||||||
|
// code is running in (`slot_high`, from run()), which is drained and left
|
||||||
|
// alone. A broken host therefore cannot brick the running loader, and a copy one
|
||||||
|
// slot lower may rewrite the resident one.
|
||||||
|
void program_flash(std::uint16_t wire_address, std::uint8_t slot_high)
|
||||||
|
{
|
||||||
|
spm::flash_address_t address;
|
||||||
|
std::uint8_t page_high;
|
||||||
|
if constexpr (word_flash) {
|
||||||
|
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) & ~static_cast<std::uint16_t>(page - 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, word_of({low, high}));
|
||||||
|
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);
|
||||||
|
} else {
|
||||||
|
address = static_cast<spm::flash_address_t>(wire_address & ~static_cast<std::uint16_t>(page - 1));
|
||||||
|
do {
|
||||||
|
std::uint8_t low = link::rx();
|
||||||
|
std::uint8_t high = link::rx();
|
||||||
|
spm::fill<off>(address, word_of({low, high}));
|
||||||
|
address += 2;
|
||||||
|
} while (static_cast<std::uint8_t>(address) & (page - 1));
|
||||||
|
address -= 2; // back inside the page — erase and write ignore the word bits
|
||||||
|
page_high = static_cast<std::uint8_t>(address >> 8) & 0xfe;
|
||||||
|
}
|
||||||
|
if (page_high != slot_high) {
|
||||||
|
spm::erase_page<off>(address);
|
||||||
|
if constexpr (boot_section)
|
||||||
|
spm::wait();
|
||||||
|
spm::write_page<off>(address);
|
||||||
|
if constexpr (boot_section)
|
||||||
|
spm::wait();
|
||||||
|
}
|
||||||
|
if constexpr (boot_section)
|
||||||
|
spm::rww_enable<off>();
|
||||||
|
}
|
||||||
|
|
||||||
|
void send_fuses()
|
||||||
|
{
|
||||||
|
std::uint8_t which = 0;
|
||||||
|
do
|
||||||
|
link::tx(spm::read_fuse<off>(static_cast<spm::fuse>(which)));
|
||||||
|
while (++which != 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[noreturn]] void run()
|
||||||
|
{
|
||||||
|
if (hw::field_impl<wdrf_field()>::test())
|
||||||
|
run_app();
|
||||||
|
|
||||||
|
link::init();
|
||||||
|
|
||||||
|
// The high byte of the slot this copy runs at, which the write guard
|
||||||
|
// follows: the return address is a word address, so its high byte is the
|
||||||
|
// 256-word slot index, doubled back into byte terms on a byte-addressed
|
||||||
|
// chip. Taken as byteswap's low byte — the builtin already swaps the two
|
||||||
|
// stacked bytes, and the double swap folds away.
|
||||||
|
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 : static_cast<std::uint8_t>(ra_high << 1);
|
||||||
|
|
||||||
|
// Measure the calibration pulse into g_unit, then take one 'p' knock. An
|
||||||
|
// expired budget (no host) boots the application; a pulse that decodes to
|
||||||
|
// anything but 'p' re-measures.
|
||||||
|
for (;;) {
|
||||||
|
if (link::measure(autobaud_budget) == 0)
|
||||||
|
run_app();
|
||||||
|
if (link::rx_bounded(autobaud_budget) == 'p')
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
ee::wait();
|
||||||
|
tx_ack();
|
||||||
|
const std::uint8_t command = link::rx();
|
||||||
|
switch (command) {
|
||||||
|
case 'J': { // jump to a wire word address: hand-over and staging transfer
|
||||||
|
auto target = reinterpret_cast<void (*)()>(rx16());
|
||||||
|
tx_ack();
|
||||||
|
link::drain();
|
||||||
|
jump(target);
|
||||||
|
}
|
||||||
|
case 'b': // chip identity: version then the three signature bytes
|
||||||
|
link::tx(version);
|
||||||
|
link::tx(hw::db.signature[0]);
|
||||||
|
link::tx(hw::db.signature[1]);
|
||||||
|
link::tx(hw::db.signature[2]);
|
||||||
|
break;
|
||||||
|
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
|
||||||
|
// One address-and-count path for the three, so the flash streamer
|
||||||
|
// keeps a single call site and inlines into this never-returning
|
||||||
|
// loop (autobaud.md).
|
||||||
|
const std::uint16_t address = rx16();
|
||||||
|
const std::uint8_t count = link::rx();
|
||||||
|
if (command == 'r')
|
||||||
|
send_eeprom(address, count);
|
||||||
|
else if (command == 'w')
|
||||||
|
store_eeprom(address, count);
|
||||||
|
else
|
||||||
|
send_flash(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>;
|
||||||
411
pureboot/pureboot_autobaud_uni.cpp
Normal file
411
pureboot/pureboot_autobaud_uni.cpp
Normal file
@@ -0,0 +1,411 @@
|
|||||||
|
// pureboot autobaud, the unified-primitive version — VARIANT A, an explicit
|
||||||
|
// space byte. One read command and one write command carry a space selector, so
|
||||||
|
// flash, EEPROM, RAM and the fuses share a single cursor, a single transfer loop
|
||||||
|
// and a single argument decode instead of one command body each.
|
||||||
|
//
|
||||||
|
// Strictly pure: no inline assembly, no global register variables, and no GPIOR
|
||||||
|
// either — the measured unit lives in a plain static, so the loader claims no
|
||||||
|
// chip resource an application might want. (PUREBOOT_UNIT_GPIOR=1 puts it back
|
||||||
|
// in the I/O scratch registers, kept only as a measurement axis.)
|
||||||
|
//
|
||||||
|
// Against pureboot_autobaud_pure.cpp this version:
|
||||||
|
// - adds RAM read and write, which the loader has never had. Because AVR maps
|
||||||
|
// the register file and the whole I/O space into the data address space,
|
||||||
|
// that one space also gives the host arbitrary peripheral access for free;
|
||||||
|
// - collapses 'R' (read flash), 'r' (read EEPROM), 'w' (write EEPROM) and 'F'
|
||||||
|
// (fuses) — four bodies, four loops — into 'G' and 'P' over four spaces;
|
||||||
|
// - fixes the activation hang: a lone calibration pulse used to leave the
|
||||||
|
// loader blocked forever in the knock's rx(), so a stray edge on an
|
||||||
|
// unattended device wedged it in the loader and the application never ran.
|
||||||
|
//
|
||||||
|
// Position independence is kept and is total: control flow is PC-relative, the
|
||||||
|
// wire carries addresses, and nothing anchors on the runtime address.
|
||||||
|
|
||||||
|
#include <libavr/libavr.hpp>
|
||||||
|
|
||||||
|
#include <util/delay_basic.h>
|
||||||
|
|
||||||
|
using namespace avr::literals;
|
||||||
|
namespace spm = avr::spm;
|
||||||
|
namespace ee = avr::eeprom;
|
||||||
|
namespace hw = avr::hw;
|
||||||
|
|
||||||
|
namespace pureboot {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Purely polled: every interrupt guard folds to nothing.
|
||||||
|
constexpr auto off = avr::irq::guard_policy::unused;
|
||||||
|
|
||||||
|
constexpr std::uint8_t ack = '+';
|
||||||
|
|
||||||
|
// Autobaud carries no clock, so PUREBOOT_CLOCK_HZ / PUREBOOT_BAUD are not
|
||||||
|
// consulted; only the software-UART pins are a deployment parameter.
|
||||||
|
#if !defined(PUREBOOT_RX)
|
||||||
|
#define PUREBOOT_RX pb0
|
||||||
|
#endif
|
||||||
|
#if !defined(PUREBOOT_TX)
|
||||||
|
#define PUREBOOT_TX pb1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// The unit's home. A plain static by default — pureboot claims no GPIOR, so the
|
||||||
|
// application keeps both scratch registers. The GPIOR spelling is retained
|
||||||
|
// behind a macro purely so the two can be measured against each other.
|
||||||
|
#if !defined(PUREBOOT_UNIT_GPIOR)
|
||||||
|
#define PUREBOOT_UNIT_GPIOR 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// The watchdog reset flag's home: MCUSR, or the classic megas' MCUCSR.
|
||||||
|
consteval std::int16_t wdrf_field()
|
||||||
|
{
|
||||||
|
auto reg = std::string_view{hw::db.regs[static_cast<std::size_t>(avr::power::detail::reset_reg())].name};
|
||||||
|
return hw::db.field_index(reg, "WDRF");
|
||||||
|
}
|
||||||
|
|
||||||
|
// The loader owns the top 512 bytes; a staging copy goes in the slot below.
|
||||||
|
constexpr std::uint16_t slot_bytes = 512;
|
||||||
|
constexpr std::uint32_t base = spm::flash_bytes - slot_bytes;
|
||||||
|
constexpr std::uint16_t page = spm::page_bytes;
|
||||||
|
constexpr bool boot_section = hw::curated::has_boot_section();
|
||||||
|
|
||||||
|
// Past 64 KiB a byte address no longer fits the wire's 16 bits, so flash
|
||||||
|
// addresses there are word addresses ('J' always was one).
|
||||||
|
constexpr bool word_flash = spm::flash_bytes > 65536;
|
||||||
|
|
||||||
|
// The loader's one identity number (README.md); the slimmed info block carries
|
||||||
|
// it and the signature, and the host maps a version to its protocol.
|
||||||
|
constexpr std::uint8_t version = 5;
|
||||||
|
|
||||||
|
// The activation window as a fixed poll budget: with no clock, whole seconds
|
||||||
|
// cannot be timed. A __uint24 (AVR's three-byte type) holds it — a fourth byte
|
||||||
|
// would cost two words at each countdown step for range never used.
|
||||||
|
#if !defined(PUREBOOT_AUTOBAUD_POLLS)
|
||||||
|
#define PUREBOOT_AUTOBAUD_POLLS 4000000
|
||||||
|
#endif
|
||||||
|
constexpr __uint24 autobaud_budget = PUREBOOT_AUTOBAUD_POLLS;
|
||||||
|
|
||||||
|
// The two SPM commands the loader still has to recognise by value, on the chips
|
||||||
|
// where it cannot issue a runtime one generically. Taken from the chip's own
|
||||||
|
// definitions rather than spelled 3 and 5 — though every part pureboot targets
|
||||||
|
// agrees on those, which is what lets the host send the raw SPMCSR byte.
|
||||||
|
constexpr std::uint8_t spm_erase = __BOOT_PAGE_ERASE;
|
||||||
|
constexpr std::uint8_t spm_write = __BOOT_PAGE_WRITE;
|
||||||
|
|
||||||
|
// The spaces a transfer can name. Flash is 0 so it is the cheap default.
|
||||||
|
//
|
||||||
|
// sp_spm is the one that is not memory: a write there hands its data byte to
|
||||||
|
// SPMCSR and fires the instruction at the given flash address, so page erase,
|
||||||
|
// page write and RWW re-enable become host-issued commands instead of a
|
||||||
|
// hardcoded tail inside 'W'. The store side already owns an address, a data
|
||||||
|
// byte and an ack, so the whole sequence costs only the fused out/spm pair. It
|
||||||
|
// also lets the host reach every other SPM operation — lock bits included —
|
||||||
|
// which the loader previously had no way to expose.
|
||||||
|
enum : std::uint8_t { sp_flash = 0, sp_eeprom = 1, sp_ram = 2, sp_fuse = 3, sp_spm = 4 };
|
||||||
|
|
||||||
|
// A transfer's selector byte is `space | bank << 4`: the low nibble names the
|
||||||
|
// space, the high nibble carries flash's third address byte (RAMPZ) on the
|
||||||
|
// chips that have one. Putting the bank here rather than widening the address
|
||||||
|
// keeps the shared cursor sixteen bits for every space — a three-byte cursor
|
||||||
|
// costs its extra increment on EEPROM and RAM reads too, which never need it.
|
||||||
|
// The host must not span a bank boundary in one transfer; it already chunks by
|
||||||
|
// page, so nothing it does today comes close.
|
||||||
|
|
||||||
|
// .noinit, not .bss: the unit is always measured before it is read, so it needs
|
||||||
|
// no zeroing — and a zeroed .bss would drag in __do_clear_bss, 18 bytes of
|
||||||
|
// startup code for a variable that is written before its first use.
|
||||||
|
[[gnu::section(".noinit")]] std::uint16_t unit_backing;
|
||||||
|
|
||||||
|
[[gnu::always_inline]] inline std::uint16_t get_unit()
|
||||||
|
{
|
||||||
|
#if PUREBOOT_UNIT_GPIOR
|
||||||
|
return static_cast<std::uint16_t>(hw::reg_impl<hw::db.reg_index("GPIOR1")>::read() |
|
||||||
|
(hw::reg_impl<hw::db.reg_index("GPIOR2")>::read() << 8));
|
||||||
|
#else
|
||||||
|
return unit_backing;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
[[gnu::always_inline]] inline void put_unit(std::uint16_t u)
|
||||||
|
{
|
||||||
|
#if PUREBOOT_UNIT_GPIOR
|
||||||
|
hw::reg_impl<hw::db.reg_index("GPIOR1")>::write(static_cast<std::uint8_t>(u));
|
||||||
|
hw::reg_impl<hw::db.reg_index("GPIOR2")>::write(static_cast<std::uint8_t>(u >> 8));
|
||||||
|
#else
|
||||||
|
unit_backing = u;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// The autobaud software link: bit-banged with cycle-counted delays like the
|
||||||
|
// fixed-baud software backend, but the per-bit delay is the measured unit, not
|
||||||
|
// a consteval constant. rx/tx load the unit once into a local, so each bit
|
||||||
|
// spins from a register with no per-bit reload.
|
||||||
|
struct link {
|
||||||
|
using in_t = avr::io::input<avr::PUREBOOT_RX, avr::io::pull::up>;
|
||||||
|
using out_t = avr::io::output<avr::PUREBOOT_TX>;
|
||||||
|
|
||||||
|
static void init()
|
||||||
|
{
|
||||||
|
avr::init<in_t, out_t>();
|
||||||
|
out_t::set(); // idle high
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time the calibration pulse into the unit. The host sends 0xC0 — a start
|
||||||
|
// bit plus six zero data bits are one low pulse of seven bit-times — and the
|
||||||
|
// counted poll loop is seven cycles an iteration, so the count is the pulse
|
||||||
|
// length in cycles ÷ 7 × 7 = one bit period in cycles, and count >> 2 is that
|
||||||
|
// period in _delay_loop_2's four-cycle iterations. Waits for the start edge
|
||||||
|
// under the poll budget; 0 (returned, and stored) means the budget expired.
|
||||||
|
//
|
||||||
|
// The shape of the counting loop is load-bearing, not incidental: the >> 2
|
||||||
|
// is exact only while the pulse's bit-count equals the loop's cycles per
|
||||||
|
// iteration. Both are 7 here (sbis 1 + rjmp 2 + adiw 2 + rjmp 2). Reshaping
|
||||||
|
// this loop silently changes the lock; test/pbautobaud.py is what pins it.
|
||||||
|
static std::uint16_t measure(__uint24 budget)
|
||||||
|
{
|
||||||
|
while (in_t::read())
|
||||||
|
if (!--budget)
|
||||||
|
return 0;
|
||||||
|
std::uint16_t count = 0;
|
||||||
|
while (!in_t::read())
|
||||||
|
++count;
|
||||||
|
const std::uint16_t u = static_cast<std::uint16_t>(count >> 2);
|
||||||
|
put_unit(u);
|
||||||
|
return u;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The eight data bits, entered just past the falling edge of the start bit.
|
||||||
|
// Split out of rx() so the activation path can wait for that edge under a
|
||||||
|
// budget while the command loop waits for it indefinitely.
|
||||||
|
static std::uint8_t sample()
|
||||||
|
{
|
||||||
|
const std::uint16_t unit = get_unit();
|
||||||
|
_delay_loop_2(static_cast<std::uint16_t>(unit + (unit >> 1))); // 1.5 bits to the LSB centre
|
||||||
|
std::uint8_t value = 0;
|
||||||
|
for (std::uint8_t bit = 0; bit < 8; ++bit) {
|
||||||
|
value >>= 1;
|
||||||
|
if (in_t::read())
|
||||||
|
value |= 0x80;
|
||||||
|
_delay_loop_2(unit);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::uint8_t rx()
|
||||||
|
{
|
||||||
|
while (in_t::read()) // await the start edge
|
||||||
|
;
|
||||||
|
return sample();
|
||||||
|
}
|
||||||
|
|
||||||
|
// A byte under the poll budget, for the activation knock. An expired budget
|
||||||
|
// returns 0, which is not the knock, so the caller falls back into the
|
||||||
|
// budgeted measure() — and a line that stays idle boots the application
|
||||||
|
// there. That is the whole hang fix: no wait during activation is unbounded,
|
||||||
|
// so a stray calibration pulse can no longer wedge the loader.
|
||||||
|
static std::uint8_t rx_bounded(__uint24 budget)
|
||||||
|
{
|
||||||
|
while (in_t::read())
|
||||||
|
if (!--budget)
|
||||||
|
return 0;
|
||||||
|
return sample();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void tx(std::uint8_t value)
|
||||||
|
{
|
||||||
|
const std::uint16_t unit = get_unit();
|
||||||
|
out_t::clear(); // start bit
|
||||||
|
_delay_loop_2(unit);
|
||||||
|
for (std::uint8_t bit = 0; bit < 8; ++bit) {
|
||||||
|
out_t::write(value & 1);
|
||||||
|
value >>= 1;
|
||||||
|
_delay_loop_2(unit);
|
||||||
|
}
|
||||||
|
out_t::set(); // stop bit
|
||||||
|
_delay_loop_2(unit);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void drain()
|
||||||
|
{
|
||||||
|
// The software transmitter returns only after the stop bit.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inlined: read across a call, the first byte strands in a call-saved register
|
||||||
|
// the caller has to push and pop.
|
||||||
|
[[gnu::always_inline]] inline std::uint16_t rx16()
|
||||||
|
{
|
||||||
|
std::uint16_t low = link::rx();
|
||||||
|
return static_cast<std::uint16_t>(low | (link::rx() << 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
[[gnu::always_inline]] inline std::uint16_t word_of(std::array<std::uint8_t, 2> pair)
|
||||||
|
{
|
||||||
|
return std::bit_cast<std::uint16_t>(pair);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[gnu::noinline]] void tx_ack()
|
||||||
|
{
|
||||||
|
link::tx(ack);
|
||||||
|
}
|
||||||
|
|
||||||
|
// One byte out of any space. The four accessors share the cursor, the loop and
|
||||||
|
// the call site — the whole point of the unified commands — so each costs only
|
||||||
|
// its own instruction rather than a body, a loop and a dispatch arm.
|
||||||
|
[[gnu::always_inline]] inline std::uint8_t load(std::uint8_t space, std::uint8_t bank, std::uint16_t at)
|
||||||
|
{
|
||||||
|
if (space == sp_eeprom)
|
||||||
|
return ee::read(at);
|
||||||
|
if (space == sp_ram)
|
||||||
|
return *reinterpret_cast<volatile std::uint8_t *>(at);
|
||||||
|
if (space == sp_fuse)
|
||||||
|
return spm::read_fuse<off>(static_cast<spm::fuse>(at));
|
||||||
|
if constexpr (word_flash)
|
||||||
|
return avr::flash_load_far<std::uint8_t>((static_cast<std::uint32_t>(bank) << 16) | at);
|
||||||
|
else
|
||||||
|
return avr::flash_load(reinterpret_cast<const std::uint8_t *>(at));
|
||||||
|
}
|
||||||
|
|
||||||
|
// One byte into a writable space. Flash is not one of them — it arrives a page
|
||||||
|
// at a time through 'W' — and the fuses are not writable at all.
|
||||||
|
[[gnu::always_inline]] inline void store(std::uint8_t space, [[maybe_unused]] std::uint8_t bank, std::uint16_t at,
|
||||||
|
std::uint8_t value)
|
||||||
|
{
|
||||||
|
if (space == sp_ram) {
|
||||||
|
*reinterpret_cast<volatile std::uint8_t *>(at) = value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (space == sp_spm) {
|
||||||
|
// The fused store-and-fire: SPMCSR takes the data byte and the SPM
|
||||||
|
// issues against Z in the same unscheduled pair the hardware's
|
||||||
|
// four-cycle window demands, which is exactly why this is one
|
||||||
|
// primitive and not a poke of SPMCSR followed by a poke of anything
|
||||||
|
// else. A host cannot hit that window across a serial link.
|
||||||
|
// The preprocessor rather than `if constexpr` only because
|
||||||
|
// spm::detail::page_command does not exist at all where there is no
|
||||||
|
// RAMPZ, and a discarded constexpr branch outside a template is still
|
||||||
|
// name-checked. A generic spm::command() in libavr would let the
|
||||||
|
// runtime command through on every chip and retire the dispatch below.
|
||||||
|
#if defined(RAMPZ)
|
||||||
|
spm::detail::page_command(value, (static_cast<spm::flash_address_t>(bank) << 16) | at);
|
||||||
|
#else
|
||||||
|
if (value == spm_erase)
|
||||||
|
spm::erase_page<off>(at);
|
||||||
|
else if (value == spm_write)
|
||||||
|
spm::write_page<off>(at);
|
||||||
|
else if constexpr (boot_section)
|
||||||
|
spm::rww_enable<off>();
|
||||||
|
#endif
|
||||||
|
if constexpr (boot_section)
|
||||||
|
spm::wait();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ee::write<off>(at, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// One page into the SPM buffer, and only that: the erase, the write and the RWW
|
||||||
|
// re-enable that used to follow are now three host-issued writes to sp_spm,
|
||||||
|
// which reach the same fused out/spm pair through the store path's own address
|
||||||
|
// and data. No running-slot write guard: the host guarantees it never targets
|
||||||
|
// the loader's own slot (licensed — README.md).
|
||||||
|
void program_flash([[maybe_unused]] std::uint8_t bank, std::uint16_t at)
|
||||||
|
{
|
||||||
|
std::uint16_t z = at & ~static_cast<std::uint16_t>(page - 1);
|
||||||
|
do {
|
||||||
|
std::uint8_t low = link::rx();
|
||||||
|
std::uint8_t high = link::rx();
|
||||||
|
#if defined(RAMPZ)
|
||||||
|
spm::fill<off>((static_cast<spm::flash_address_t>(bank) << 16) | z, word_of({low, high}));
|
||||||
|
#else
|
||||||
|
spm::fill<off>(z, word_of({low, high}));
|
||||||
|
#endif
|
||||||
|
z += 2;
|
||||||
|
} while (static_cast<std::uint8_t>(z) & (page - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
[[noreturn]] void run()
|
||||||
|
{
|
||||||
|
if (hw::field_impl<wdrf_field()>::test())
|
||||||
|
run_app();
|
||||||
|
|
||||||
|
link::init();
|
||||||
|
|
||||||
|
// Measure the calibration pulse into the unit, then take one 'p' knock —
|
||||||
|
// both under the poll budget. An expired budget (no host) boots the
|
||||||
|
// application; anything but 'p', including the knock timing out, re-measures
|
||||||
|
// and so returns to the budgeted wait that boots it.
|
||||||
|
for (;;) {
|
||||||
|
if (link::measure(autobaud_budget) == 0)
|
||||||
|
run_app();
|
||||||
|
if (link::rx_bounded(autobaud_budget) == 'p')
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
ee::wait();
|
||||||
|
tx_ack();
|
||||||
|
const std::uint8_t command = link::rx();
|
||||||
|
switch (command) {
|
||||||
|
case 'J': { // jump to a wire word address: hand-over and staging transfer
|
||||||
|
auto target = reinterpret_cast<void (*)()>(rx16());
|
||||||
|
tx_ack();
|
||||||
|
link::drain();
|
||||||
|
jump(target);
|
||||||
|
}
|
||||||
|
case 'b': // chip identity: version then the three signature bytes
|
||||||
|
link::tx(version);
|
||||||
|
link::tx(hw::db.signature[0]);
|
||||||
|
link::tx(hw::db.signature[1]);
|
||||||
|
link::tx(hw::db.signature[2]);
|
||||||
|
break;
|
||||||
|
case 'W': // program one flash page: sel8, addr16, then page bytes
|
||||||
|
case 'G': // read: sel8, addr16, n8 (0 = 256)
|
||||||
|
case 'g': { // write: sel8, addr16, n8, then n bytes each acked
|
||||||
|
// The unified transfer. One decode, one cursor, one loop for every
|
||||||
|
// space and both directions — the four command bodies this replaces
|
||||||
|
// each carried their own copy of all three. Read and write are the
|
||||||
|
// same letter in the two cases, so the direction is bit 5 of the
|
||||||
|
// command and the loop tests it with a one-word skip. 'W' joins the
|
||||||
|
// same selector-and-address decode rather than keeping a word
|
||||||
|
// address of its own, which makes flash addressing uniform across
|
||||||
|
// every command that names it and costs nothing to share.
|
||||||
|
const std::uint8_t sel = link::rx();
|
||||||
|
const std::uint8_t space = sel & 0x0f;
|
||||||
|
const std::uint8_t bank = static_cast<std::uint8_t>(sel >> 4);
|
||||||
|
std::uint16_t at = rx16();
|
||||||
|
if (command == 'W') {
|
||||||
|
program_flash(bank, at);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
std::uint8_t count = link::rx();
|
||||||
|
do {
|
||||||
|
if (command & 0x20) {
|
||||||
|
store(space, bank, at, link::rx());
|
||||||
|
tx_ack();
|
||||||
|
} else
|
||||||
|
link::tx(load(space, bank, at));
|
||||||
|
++at;
|
||||||
|
} while (--count);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: // unknown bytes are ignored; the loop re-acks
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace pureboot
|
||||||
|
|
||||||
|
template struct avr::startup::entry<pureboot::run>;
|
||||||
@@ -1,75 +1,47 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Position-independence lint: the property that lets the identical image run
|
"""Position-independence lint: the two link-time facts that let the identical
|
||||||
from any slot, asserted from the built ELF and its object.
|
image run from any slot, asserted from the built ELF.
|
||||||
|
|
||||||
1. No absolute jmp/call — -mrelax normally guarantees it, but a branch that
|
1. No absolute jmp/call — -mrelax normally guarantees it, but a branch that
|
||||||
grows out of relaxation range would break it silently.
|
grows out of relaxation range would break it silently.
|
||||||
2. Nothing flash-resident to address: the image is .text alone, so there is
|
2. The info block within the image's first 256 bytes: 'b' rebuilds its
|
||||||
no table whose runtime address has to be reconstructed.
|
address as (running slot high byte : link address low byte).
|
||||||
3. The image is byte-identical when linked at a different base. This is
|
|
||||||
position independence itself rather than a proxy for it — an absolute
|
|
||||||
address anywhere in the image would move with the link and show up as a
|
|
||||||
differing byte.
|
|
||||||
|
|
||||||
Usage: check_pi.py <objdump> <objcopy> <cxx> <mcu> <elf> <object> <text_start_hex>
|
Usage: check_pi.py <objdump> <nm> <elf> <text_start_hex>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
|
||||||
|
|
||||||
|
|
||||||
def fail(message):
|
|
||||||
print(f"FAIL: {message}")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
objdump, objcopy, cxx, mcu, elf, obj, text_start = sys.argv[1:]
|
objdump, nm, elf, text_start = sys.argv[1:]
|
||||||
text_start = int(text_start, 0)
|
text_start = int(text_start, 0)
|
||||||
|
|
||||||
listing = subprocess.run([objdump, "-d", elf], capture_output=True, text=True, check=True).stdout
|
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)]
|
absolute = [
|
||||||
|
line
|
||||||
|
for line in listing.splitlines()
|
||||||
|
if re.search(r"\t(jmp|call)\t", line)
|
||||||
|
]
|
||||||
if absolute:
|
if absolute:
|
||||||
fail("absolute control flow in the image:\n" + "\n".join(absolute))
|
print("FAIL: absolute control flow in the image:")
|
||||||
|
print("\n".join(absolute))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
# Allocated flash beyond .text would be data the running copy has to find.
|
symbols = subprocess.run([nm, "-C", elf], capture_output=True, text=True, check=True).stdout
|
||||||
# Only ALLOC sections reach the device at all; .comment and the debug
|
info = [line for line in symbols.splitlines() if "flash_table" in line and "::storage" in line]
|
||||||
# sections ride along in the ELF container and are never flashed. objdump
|
if len(info) != 1:
|
||||||
# prints each section's flags on the line following its header.
|
print(f"FAIL: expected one info-block storage symbol, found {len(info)}")
|
||||||
headers = subprocess.run([objdump, "-h", elf], capture_output=True, text=True, check=True).stdout.splitlines()
|
sys.exit(1)
|
||||||
for index, line in enumerate(headers):
|
address = int(info[0].split()[0], 16)
|
||||||
fields = line.split()
|
offset = address - text_start
|
||||||
if len(fields) < 6 or not fields[0].isdigit():
|
if not 0 <= offset < 256:
|
||||||
continue
|
print(f"FAIL: info block at image offset {offset:#x}, must sit in the first 256 bytes")
|
||||||
name, size = fields[1], int(fields[2], 16)
|
sys.exit(1)
|
||||||
flags = headers[index + 1] if index + 1 < len(headers) else ""
|
|
||||||
if "ALLOC" not in flags or not size:
|
|
||||||
continue
|
|
||||||
if name not in (".text", ".noinit", ".bss"):
|
|
||||||
fail(f"flash-resident section {name} ({size} bytes): the image must be .text alone")
|
|
||||||
|
|
||||||
# Relink at a different base and compare the bytes.
|
print(f"PI lint: control flow PC-relative, info block at offset {offset:#x}")
|
||||||
with tempfile.TemporaryDirectory() as work:
|
|
||||||
elsewhere = text_start - 0x200 if text_start >= 0x200 else text_start + 0x200
|
|
||||||
images = []
|
|
||||||
for base, tag in ((text_start, "here"), (elsewhere, "there")):
|
|
||||||
relinked = os.path.join(work, f"{tag}.elf")
|
|
||||||
binary = os.path.join(work, f"{tag}.bin")
|
|
||||||
subprocess.run(
|
|
||||||
[cxx, f"-mmcu={mcu}", "-nostartfiles", f"-Wl,--section-start=.text={base:#x}",
|
|
||||||
"-Wl,--defsym=pureboot_app=0", "-mrelax", obj, "-o", relinked],
|
|
||||||
check=True, capture_output=True)
|
|
||||||
subprocess.run([objcopy, "-O", "binary", relinked, binary], check=True)
|
|
||||||
images.append(open(binary, "rb").read())
|
|
||||||
if images[0] != images[1]:
|
|
||||||
differing = [i for i, (a, b) in enumerate(zip(*images)) if a != b]
|
|
||||||
fail(f"the image changes when linked at {elsewhere:#x} instead of {text_start:#x}: "
|
|
||||||
f"{len(differing)} byte(s) differ, first at offset {differing[0]:#x}")
|
|
||||||
|
|
||||||
print(f"PI lint: control flow PC-relative, .text only, identical linked at {text_start:#x} and {elsewhere:#x}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -55,12 +55,6 @@ def main():
|
|||||||
# the page byte is the wire's 0-means-256.
|
# the page byte is the wire's 0-means-256.
|
||||||
mega = mcu.startswith("atmega")
|
mega = mcu.startswith("atmega")
|
||||||
patch = not mega or mcu.startswith("atmega48")
|
patch = not mega or mcu.startswith("atmega48")
|
||||||
# Where SRAM begins: the x8 and x4 megas push it past their extended I/O
|
|
||||||
# space, everything else starts right after the plain I/O registers. The
|
|
||||||
# loader keeps no statics and its stack sits at RAMEND, so the first SRAM
|
|
||||||
# byte is free for the data-space probe below.
|
|
||||||
classic = mcu in ("atmega8", "atmega8a", "atmega16", "atmega16a", "atmega32", "atmega32a")
|
|
||||||
ram_base = 0x0100 if mega and not classic else 0x0060
|
|
||||||
word_flash = base + pb.SLOT > 0x10000
|
word_flash = base + pb.SLOT > 0x10000
|
||||||
wire_base = base // 2 if word_flash else base
|
wire_base = base // 2 if word_flash else base
|
||||||
flags = (1 if patch else 0) | (2 if word_flash else 0)
|
flags = (1 if patch else 0) | (2 if word_flash else 0)
|
||||||
@@ -79,20 +73,12 @@ def main():
|
|||||||
if needed not in out:
|
if needed not in out:
|
||||||
fail(f"session 1 output lacks {needed!r}")
|
fail(f"session 1 output lacks {needed!r}")
|
||||||
|
|
||||||
# Session 2: reconnect into the live session, verify, dump, exercise
|
# Session 2: reconnect into the live session, verify, dump, hand over
|
||||||
# the data space; hand over is deferred — the pty must be reopened for
|
# is deferred — the pty must be reopened for the APP banner first.
|
||||||
# the APP banner first.
|
|
||||||
probe = "c0ffee"
|
|
||||||
out = pbsim.run_tool(tool, device.pty, baud, "--verify-flash", app_bin, "--verify-eeprom", ee_path,
|
out = pbsim.run_tool(tool, device.pty, baud, "--verify-flash", app_bin, "--verify-eeprom", ee_path,
|
||||||
"--read-flash", read_flash, "--read-eeprom", read_eeprom,
|
"--read-flash", read_flash, "--read-eeprom", read_eeprom, "--stay")
|
||||||
"--poke", f"{ram_base:#x}:{probe}", "--peek", f"{ram_base:#x}:3", "--stay")
|
|
||||||
if out.count("verify:") != 2:
|
if out.count("verify:") != 2:
|
||||||
fail("session 2 did not verify both memories")
|
fail("session 2 did not verify both memories")
|
||||||
# What went into SRAM must come back out of it: the data space is one
|
|
||||||
# more selector on the same transfer as flash and EEPROM, so a wrong
|
|
||||||
# selector decode would show up here and nowhere else.
|
|
||||||
if probe not in out.replace(" ", ""):
|
|
||||||
fail(f"data-space round trip at {ram_base:#x} did not read back {probe}\n{out}")
|
|
||||||
|
|
||||||
eeprom_back = open(read_eeprom, "rb").read()
|
eeprom_back = open(read_eeprom, "rb").read()
|
||||||
if eeprom_back[: len(ee_image)] != ee_image:
|
if eeprom_back[: len(ee_image)] != ee_image:
|
||||||
@@ -124,16 +110,12 @@ def main():
|
|||||||
f"{pb.OLDEST_LOADER}..{pb.NEWEST_LOADER}")
|
f"{pb.OLDEST_LOADER}..{pb.NEWEST_LOADER}")
|
||||||
|
|
||||||
# A W addressed inside a page rather than at its base must still
|
# A W addressed inside a page rather than at its base must still
|
||||||
# consume exactly one page and prompt. The loader's own slot is the
|
# consume exactly one page and prompt. The loader's own slot is
|
||||||
# target — the guard refuses to commit it — and the payload is
|
# the target — it is drained and never programmed — and the
|
||||||
# erased-state bytes, so the probe can disturb neither the image nor
|
# payload is erased-state bytes, so the probe can disturb neither
|
||||||
# the page buffer it leaves behind. Hand-built rather than through
|
# the image nor the page buffer it leaves behind.
|
||||||
# write_page(), which would follow the fill with its erase and
|
wire = wire_base + 1
|
||||||
# write; the point here is that the fill alone consumes exactly one
|
port.write(bytes((ord("W"), wire & 0xFF, wire >> 8)) + b"\xff" * page)
|
||||||
# page whatever the address's low bits say.
|
|
||||||
wire = base + 1
|
|
||||||
port.write(bytes((ord("W"), pb.selector(pb.SP_FLASH, wire), wire & 0xFF, (wire >> 8) & 0xFF))
|
|
||||||
+ b"\xff" * page)
|
|
||||||
if port.read_exact(1, 5.0) != pb.PROMPT:
|
if port.read_exact(1, 5.0) != pb.PROMPT:
|
||||||
fail("unaligned W did not return to the prompt")
|
fail("unaligned W did not return to the prompt")
|
||||||
|
|
||||||
|
|||||||
@@ -182,16 +182,9 @@ static avr_cycle_count_t tx_sample(avr_t *mcu, avr_cycle_count_t when, void *par
|
|||||||
{
|
{
|
||||||
(void)mcu;
|
(void)mcu;
|
||||||
(void)param;
|
(void)param;
|
||||||
if (tx_bit < 8) {
|
tx_shift = (uint8_t)((tx_shift >> 1) | (tx_level ? 0x80 : 0));
|
||||||
tx_shift = (uint8_t)((tx_shift >> 1) | (tx_level ? 0x80 : 0));
|
if (++tx_bit < 8)
|
||||||
if (++tx_bit < 8)
|
|
||||||
return when + bit_cycles;
|
|
||||||
/* The byte is not delivered until its stop bit has passed. A real
|
|
||||||
* receiver cannot answer sooner, and a host that did would put its
|
|
||||||
* start bit on the wire while the device is still driving the stop
|
|
||||||
* bit — which the device, transmitting, is not watching for. */
|
|
||||||
return when + bit_cycles;
|
return when + bit_cycles;
|
||||||
}
|
|
||||||
if (write(pty_master, &tx_shift, 1) != 1)
|
if (write(pty_master, &tx_shift, 1) != 1)
|
||||||
fprintf(stderr, "device: pty write lost a byte\n");
|
fprintf(stderr, "device: pty write lost a byte\n");
|
||||||
tx_active = 0;
|
tx_active = 0;
|
||||||
|
|||||||
@@ -30,14 +30,8 @@ def info_of(pb, base, page, patch, flash, signature=(0x1E, 0x93, 0x0B), word_fla
|
|||||||
scale = 2 if word_flash else 1
|
scale = 2 if word_flash else 1
|
||||||
wire_base = base // scale
|
wire_base = base // scale
|
||||||
flags = (1 if patch else 0) | (2 if word_flash else 0)
|
flags = (1 if patch else 0) | (2 if word_flash else 0)
|
||||||
# The EEPROM size comes from the signature, as it must: pureboot 5 derives
|
|
||||||
# the whole geometry from the signature rather than sending it, so a
|
|
||||||
# synthetic block that disagreed with its own signature would describe a
|
|
||||||
# chip that cannot exist.
|
|
||||||
eeprom = pb.CHIP_GEOMETRY[signature][2]
|
|
||||||
raw = bytes((0x50, 0x42, pb.NEWEST_LOADER if version is None else version,
|
raw = bytes((0x50, 0x42, pb.NEWEST_LOADER if version is None else version,
|
||||||
*signature, page & 0xFF, wire_base & 0xFF, wire_base >> 8,
|
*signature, page & 0xFF, wire_base & 0xFF, wire_base >> 8, 0, 2, flags))
|
||||||
eeprom & 0xFF, eeprom >> 8, flags))
|
|
||||||
info = pb.Info(raw)
|
info = pb.Info(raw)
|
||||||
if info.flash_size != flash:
|
if info.flash_size != flash:
|
||||||
fail(f"info_of({base:#x}) decodes to {info.flash_size:#x} of flash, not {flash:#x}")
|
fail(f"info_of({base:#x}) decodes to {info.flash_size:#x} of flash, not {flash:#x}")
|
||||||
@@ -168,16 +162,11 @@ def main():
|
|||||||
fail("mega staging content should be the bare image")
|
fail("mega staging content should be the bare image")
|
||||||
expect_error("mega staging size", lambda: pb.staging_content(image + b"!", mega), "512")
|
expect_error("mega staging size", lambda: pb.staging_content(image + b"!", mega), "512")
|
||||||
|
|
||||||
# The image stamp: found in a synthetic binary, absent in noise. pureboot
|
# The embedded info block: found in a synthetic binary, absent in noise.
|
||||||
# 5 stamps the magic, its version and the signature, and the geometry is
|
binary = bytes((0xAA,)) * 10 + tiny.raw + bytes((0xBB,)) * 10
|
||||||
# looked up from there — so what comes back must equal what a live device
|
|
||||||
# of the same chip reports.
|
|
||||||
stamp = bytes((0x50, 0x42, pb.NEWEST_LOADER)) + bytes(tiny.signature)
|
|
||||||
binary = bytes((0xAA,)) * 10 + stamp + bytes((0xBB,)) * 10
|
|
||||||
found = pb.image_info(binary)
|
found = pb.image_info(binary)
|
||||||
if found is None or found.raw != tiny.raw:
|
if found is None or found.raw != tiny.raw:
|
||||||
fail(f"image_info misreads the v{pb.NEWEST_LOADER} stamp: "
|
fail("image_info misses the embedded block")
|
||||||
f"{found.raw.hex() if found else None} != {tiny.raw.hex()}")
|
|
||||||
if pb.image_info(bytes((0xAA,)) * 40) is not None:
|
if pb.image_info(bytes((0xAA,)) * 40) is not None:
|
||||||
fail("image_info invents a block")
|
fail("image_info invents a block")
|
||||||
# An older loader's image stays readable, so a deployed build can be
|
# An older loader's image stays readable, so a deployed build can be
|
||||||
|
|||||||
Reference in New Issue
Block a user