pureboot 5: one command pair for every memory, and a clock-free backend
R/r/w/F collapse into G and g over a selector byte naming the space — flash,
EEPROM, data, fuse, SPM — with the flash bank in its high nibble. Four command
bodies, four transfer loops and four argument decodes become one of each, and
W joins the same decode instead of keeping an address form of its own. The
loader shrinks while gaining everything below: on the 1284P the stock build
goes 480 -> 432 B and the software one 496 -> 450.
What the freed space buys:
- Data space. On AVR one pointer spans SRAM, the register file and the whole
I/O space, so G over space 2 reads all three. pureboot keeps zero static
RAM and pushes no register, so at loader entry an application's SRAM is
still what the application left there — this is a post-mortem, not just a
poke hole. As its own command it needed a dispatch arm and a loop; as one
more space it is a single ld/st.
- Host-issued SPM. W fills the page buffer and stops; erase, write and RWW
re-enable are writes to space 4, which reach the same fused store-and-SPM
pair through the transfer's own address and data. Any SPM operation, lock
bits included, is now reachable and the loader carries no page-commit logic.
The four-cycle SPMCSR-to-SPM window is why that primitive stays fused: no
host can hit it across a serial link, and that — not the byte count — is
the floor on how low-level a bootloader's primitives can go.
- Byte addresses everywhere. The bank in the selector retires the
word-addressed wire the >64 KiB parts needed, so the 1284s stop being the
outlier.
SERIAL autobaud is a third backend on the same loader, over libavr's
software_autobaud: no clock, no baud, one binary per chip for every F_CPU and
every rate. Activation counts poll iterations rather than seconds and bounds
every wait, so a stray pulse cannot hold an unattended device.
b answers with the version and signature only; the host derives geometry from
the signature, which is what an autobaud build requires anyway. An update image
is a bare slot with no device to ask, so every image carries a six-byte stamp —
the same bytes b answers with, and the source of both — that the loader never
reads from flash and the host refuses to install a mismatch against. The
running-slot write guard moved onto the SPM commit, which covers erase and
write both where guarding W covered neither directly.
The position-independence lint now proves the property instead of a proxy for
it: the image must come out byte-identical linked at a different base.
-fno-move-loop-invariants left the tuned flag set — it was fitted to a command
loop carrying four transfer bodies and costs bytes now that it carries one.
Verified: the exhaustive matrix on all 37 chips (every clock x every baud x
every backend, non-standard rates included, plus the autobaud build) —
8174 size checks, no failures, tightest fit the 1284s' autobaud at 510 of 512.
Behavioral suites green on every chip class: t13a 10/10, t85 11/11, m8 13/13,
m16a 13/13, m48pa 13/13, 328P 23/23, 644A 17/17, 1284P 17/17. Data-space
round trip through --peek/--poke and the autobaud handshake are both red-green
proven.
The two prototype sources and their findings file go; the README carries the
protocol and dev/done.md in libavr carries the reasoning.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -150,7 +150,10 @@ 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_NM} $<TARGET_FILE:pureboot> ${PUREBOOT_BASE_HEX})
|
${CMAKE_OBJDUMP} ${CMAKE_OBJCOPY} ${CMAKE_CXX_COMPILER} ${LIBAVR_MCU}
|
||||||
|
$<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)
|
||||||
@@ -245,23 +248,14 @@ 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 (pureboot/autobaud.md): one clock-agnostic image per
|
# The autobaud loader: one clock-agnostic image per chip, so it has no
|
||||||
# chip, no clock × baud axis, size-tested against the same per-chip budget on
|
# clock x baud axis of its own — the matrix below sweeps those for the
|
||||||
# every chip.
|
# fixed-baud builds, and this one binary has to serve all of them at run
|
||||||
#
|
# time. Size-tested against the same per-chip budget as every other variant.
|
||||||
# Only the unified version is built. Its two predecessors —
|
pureboot_add_loader(pureboot_autobaud SERIAL autobaud)
|
||||||
# pureboot_autobaud_pure.cpp at 508 B and pureboot_autobaud_reg.cpp at 512 —
|
add_test(NAME pureboot_autobaud.size
|
||||||
# had 4 B and 0 B of margin on the 1284P, and the fix for the activation hang
|
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:pureboot_autobaud>
|
||||||
# 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)
|
-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
|
||||||
@@ -302,16 +296,15 @@ 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.
|
||||||
#
|
#
|
||||||
# Bounded to one chip per size-bearing class: flash addressing (the
|
# Every chip runs the full cross product: the size-bearing classes (flash
|
||||||
# word-addressed 1284), hand-over shape (the patched vector on the tinies
|
# addressing, hand-over shape, page size, USART inventory) are what make
|
||||||
# and m48s), page size, and USART inventory. Everything else in the image
|
# the image differ, and a chip outside them is expected to match its class
|
||||||
# is chip-independent code, so a further chip buys builds and no
|
# — but "expected" is what a matrix is for, and the whole sweep is cheap
|
||||||
# coverage; every chip outside the set carries the compact matrix.
|
# enough to run rather than reason about. PUREBOOT_FULL_MATRIX is what
|
||||||
|
# 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)
|
||||||
set(_matrix_spot attiny13a attiny85 atmega48pa atmega8a atmega168pa
|
if(DEFINED ENV{PUREBOOT_FULL_MATRIX})
|
||||||
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)
|
||||||
@@ -409,14 +402,12 @@ 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)
|
||||||
foreach(_variant uni)
|
add_test(NAME pureboot.autobaud
|
||||||
add_test(NAME pureboot.autobaud_${_variant}
|
|
||||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbautobaud.py
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbautobaud.py
|
||||||
${PB_DEVICE} $<TARGET_FILE:pureboot_autobaud_${_variant}> ${PUREBOOT_SIM_MCU}
|
${PB_DEVICE} $<TARGET_FILE:pureboot_autobaud> ${PUREBOOT_SIM_MCU}
|
||||||
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} $<TARGET_FILE:pbapp_autobaud>.bin
|
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} $<TARGET_FILE:pbapp_autobaud>.bin
|
||||||
1000000 9600 ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
1000000 9600 ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
${CMAKE_BINARY_DIR}/pbautobaud-${_variant}-work)
|
${CMAKE_BINARY_DIR}/pbautobaud-work)
|
||||||
set_tests_properties(pureboot.autobaud_${_variant} PROPERTIES TIMEOUT 240)
|
set_tests_properties(pureboot.autobaud PROPERTIES TIMEOUT 240)
|
||||||
endforeach()
|
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -194,13 +194,19 @@ 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] [USART <n>]
|
# [SERIAL auto|hardware|software|autobaud] [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)
|
||||||
@@ -222,8 +228,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 PB_SERIAL STREQUAL "software")
|
if(DEFINED PB_USART AND NOT PB_SERIAL MATCHES "^(auto|hardware)$")
|
||||||
message(FATAL_ERROR "pureboot_add_loader(${name}): USART ${PB_USART} contradicts SERIAL software")
|
message(FATAL_ERROR "pureboot_add_loader(${name}): USART ${PB_USART} contradicts SERIAL ${PB_SERIAL}")
|
||||||
endif()
|
endif()
|
||||||
if(DEFINED PB_USART)
|
if(DEFINED PB_USART)
|
||||||
set(PB_SERIAL hardware)
|
set(PB_SERIAL hardware)
|
||||||
@@ -252,7 +258,7 @@ function(pureboot_add_loader name)
|
|||||||
set(PB_SERIAL software)
|
set(PB_SERIAL software)
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
if(PB_SERIAL STREQUAL "software")
|
if(PB_SERIAL MATCHES "^(software|autobaud)$")
|
||||||
if(NOT PB_RX)
|
if(NOT PB_RX)
|
||||||
set(PB_RX pb0)
|
set(PB_RX pb0)
|
||||||
endif()
|
endif()
|
||||||
@@ -264,7 +270,11 @@ 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_AUTOBAUD PUREBOOT_RX=${PB_RX} PUREBOOT_TX=${PB_TX})
|
||||||
|
else()
|
||||||
set(_serial_defines PUREBOOT_SOFT_SERIAL PUREBOOT_RX=${PB_RX} PUREBOOT_TX=${PB_TX})
|
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)
|
||||||
@@ -280,23 +290,29 @@ function(pureboot_add_loader name)
|
|||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if(PB_SERIAL STREQUAL "autobaud")
|
||||||
|
# No clock and no baud reach the image; the window is a poll budget.
|
||||||
|
set(_defines ${_serial_defines})
|
||||||
|
else()
|
||||||
set(_defines PUREBOOT_CLOCK_HZ=${PB_CLOCK} PUREBOOT_BAUD=${PB_BAUD} PUREBOOT_TIMEOUT=${PB_TIMEOUT}
|
set(_defines PUREBOOT_CLOCK_HZ=${PB_CLOCK} PUREBOOT_BAUD=${PB_BAUD} PUREBOOT_TIMEOUT=${PB_TIMEOUT}
|
||||||
${_serial_defines})
|
${_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, worth 14–36 B depending on the
|
# Codegen shaping for the loader TU only. At -Os GCC otherwise rewrites the
|
||||||
# chip. At -Os GCC otherwise rewrites the byte-stream loops' counters into
|
# byte-stream loops' counters into end-pointer forms that cost registers
|
||||||
# end-pointer forms that cost registers (-fno-ivopts,
|
# (-fno-ivopts, -fno-split-wide-types), leaves register pressure on the
|
||||||
# -fno-split-wide-types), leaves register pressure on the table with the
|
# table with the default allocator (-fira-algorithm=priority), and keeps
|
||||||
# default allocator (-fira-algorithm=priority), and keeps loop-invariant
|
# expression temporaries in registers (-fno-tree-ter) — but every loop body
|
||||||
# immediates and expression temporaries in registers
|
# here contains a call, so a register held across it costs more than the
|
||||||
# (-fno-move-loop-invariants, -fno-tree-ter) — but every loop body here
|
# load-immediate it saves. The set is fitted to the loader's body and has to
|
||||||
# contains a call, so a register held across it costs more than the
|
# be re-measured when that body changes: -fno-move-loop-invariants belonged
|
||||||
# load-immediate it saves.
|
# here while the command loop carried four transfer bodies and costs bytes
|
||||||
|
# now that it carries one.
|
||||||
target_compile_options(${name} PRIVATE
|
target_compile_options(${name} PRIVATE
|
||||||
-fno-ivopts -fira-algorithm=priority -fno-move-loop-invariants -fno-tree-ter -fno-split-wide-types)
|
-fno-ivopts -fira-algorithm=priority -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}>)
|
||||||
@@ -311,37 +327,3 @@ 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,46 +8,51 @@ 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
|
||||||
read/write paths take wire addresses, the write guard protects the slot the
|
transfer paths take wire addresses, the write guard protects the slot the code
|
||||||
code is *running* in (from the runtime return address), the info block is
|
is *running* in (from the runtime return address), nothing else is
|
||||||
addressed from that same anchor, and the application jump is an indirect call
|
flash-resident to address at all, 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
|
every command intact, which makes pureboot **its own staging loader**: the host
|
||||||
host installs the same binary one slot below the resident, jumps into it, and
|
installs the same binary one slot below the resident, jumps into it, and lets
|
||||||
lets it rewrite the resident.
|
it rewrite the resident. The lint holds it to that literally — the image must
|
||||||
|
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 largest image any of them produces is a
|
per build — see *Configuration*. The autobaud column is the clock-free build,
|
||||||
software UART at a slow baud, which on the 1284s is 494 B, the tightest fit in
|
which is the largest the space produces and the tightest fit in the matrix;
|
||||||
the whole matrix at 18 B spare.
|
it carries the calibration machinery and no clock at all.
|
||||||
|
|
||||||
| Chip | Flash | Loader at | Link | Size |
|
| Chip | Flash | Loader at | Link | Stock | Autobaud |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| ATtiny13, ATtiny13A † | 1 KiB | 0x0200 | software | 416 B |
|
| ATtiny13, ATtiny13A † | 1 KiB | 0x0200 | software | 402 B | 472 B |
|
||||||
| ATtiny25 † | 2 KiB | 0x0600 | software | 420 B |
|
| ATtiny25 † | 2 KiB | 0x0600 | software | 406 B | 476 B |
|
||||||
| ATtiny45 † | 4 KiB | 0x0e00 | software | 424 B |
|
| ATtiny45 † | 4 KiB | 0x0e00 | software | 410 B | 480 B |
|
||||||
| ATtiny85 † | 8 KiB | 0x1e00 | software | 424 B |
|
| ATtiny85 † | 8 KiB | 0x1e00 | software | 410 B | 480 B |
|
||||||
| ATmega8, 8A | 8 KiB | 0x1e00 | USART0 | 396 B |
|
| ATmega8, 8A | 8 KiB | 0x1e00 | USART0 | 372 B | 486 B |
|
||||||
| ATmega16, 16A | 16 KiB | 0x3e00 | USART0 | 400 B |
|
| ATmega16, 16A | 16 KiB | 0x3e00 | USART0 | 374 B | 490 B |
|
||||||
| ATmega32, 32A | 32 KiB | 0x7e00 | USART0 | 400 B |
|
| ATmega32, 32A | 32 KiB | 0x7e00 | USART0 | 374 B | 490 B |
|
||||||
| ATmega48, 48A, 48P, 48PA † | 4 KiB | 0x0e00 | USART0 | 414 B |
|
| ATmega48, 48A, 48P, 48PA † | 4 KiB | 0x0e00 | USART0 | 400 B | 476 B |
|
||||||
| ATmega88, 88A, 88P, 88PA | 8 KiB | 0x1e00 | USART0 | 434 B |
|
| ATmega88, 88A, 88P, 88PA | 8 KiB | 0x1e00 | USART0 | 410 B | 486 B |
|
||||||
| ATmega168, 168A, 168P, 168PA | 16 KiB | 0x3e00 | USART0 | 438 B |
|
| ATmega168, 168A, 168P, 168PA | 16 KiB | 0x3e00 | USART0 | 412 B | 490 B |
|
||||||
| ATmega328, 328P | 32 KiB | 0x7e00 | USART0 | 438 B |
|
| ATmega328, 328P | 32 KiB | 0x7e00 | USART0 | 412 B | 490 B |
|
||||||
| ATmega164A, 164P, 164PA | 16 KiB | 0x3e00 | USART0 | 438 B |
|
| ATmega164A, 164P, 164PA | 16 KiB | 0x3e00 | USART0 | 412 B | 490 B |
|
||||||
| ATmega324A, 324P, 324PA | 32 KiB | 0x7e00 | USART0 | 438 B |
|
| ATmega324A, 324P, 324PA | 32 KiB | 0x7e00 | USART0 | 412 B | 490 B |
|
||||||
| ATmega644, 644A, 644P, 644PA | 64 KiB | 0xfe00 | USART0 | 432 B |
|
| ATmega644, 644A, 644P, 644PA | 64 KiB | 0xfe00 | USART0 | 406 B | 484 B |
|
||||||
| ATmega1284, 1284P | 128 KiB | 0x1fe00 | USART0 | 478 B |
|
| ATmega1284, 1284P | 128 KiB | 0x1fe00 | USART0 | 432 B | 510 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 1284s are the heaviest because they alone carry the far-flash machinery —
|
The tightest fit in the whole space is the 1284s' autobaud build, 510 of its
|
||||||
ELPM reads, RAMPZ page commands, a word-addressed wire.
|
512 — they alone carry the far-flash machinery (ELPM reads, RAMPZ page
|
||||||
|
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.
|
||||||
@@ -62,7 +67,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` | the link backend | `auto`: the hardware USART where the chip has one |
|
| `SERIAL auto\|hardware\|software\|autobaud` | 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 |
|
||||||
@@ -74,6 +79,17 @@ 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
|
||||||
@@ -113,9 +129,20 @@ 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.
|
Re-timing a deployed loader is a self-update with a re-timed build. An autobaud
|
||||||
|
build counts poll iterations instead (`PUREBOOT_AUTOBAUD_POLLS`), there being
|
||||||
|
no clock to turn into seconds.
|
||||||
|
|
||||||
## Session
|
## Session
|
||||||
|
|
||||||
@@ -125,75 +152,116 @@ 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.
|
||||||
|
|
||||||
On chips whose flash exceeds 64 KiB (the 1284s — info-block flag bit 1) the
|
Addresses are **byte addresses within a 64 KiB bank**, and the bank rides in
|
||||||
`R`/`W` flash addresses are **word** addresses; everywhere else they are byte
|
the command's selector byte, so no command has to speak word addresses. `J` is
|
||||||
addresses (the 644s' 64 KiB is exactly the 16-bit byte space). EEPROM
|
the exception: it takes a word address, because that is what the hardware's own
|
||||||
addresses and all counts are bytes.
|
jump takes. EEPROM and data-space 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 info block. **Gotcha:** a `w` (or `r`) that runs past `E2END` wraps
|
against the chip. **Gotcha:** a write (or read) 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 writes within the
|
low EEPROM and the write silently overwrites it. Keeping transfers within the
|
||||||
advertised sizes is the host's job (the shipped tool does); the flash budget
|
real sizes is the host's job (the shipped tool does); the flash budget is
|
||||||
is better spent on features than on re-checking a bound the host already holds.
|
better spent on features than on re-checking a bound the host already holds.
|
||||||
|
|
||||||
| Cmd | Arguments | Reply |
|
| Cmd | Arguments | Reply |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `b` | — | the 12-byte info block |
|
| `b` | — | 4 bytes: the pureboot version, then the three signature bytes |
|
||||||
| `R` | addr16, n8 | n flash bytes (n = 0 means 256) |
|
| `G` | sel8, addr16, n8 | n bytes from the selected space (n = 0 means 256) |
|
||||||
| `W` | addr16 (any address in the page), then one page of data | — (completion = next prompt) |
|
| `g` | sel8, addr16, n8, then n data bytes | `+` per byte, sent once its write has begun |
|
||||||
| `r` | addr16, n8 | n EEPROM bytes (n = 0 means 256) |
|
| `W` | sel8, addr16, then one page of data | — (completion = next prompt) |
|
||||||
| `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) |
|
||||||
|
|
||||||
`W` streams exactly one SPM page (size from the info block) into the buffer,
|
`G` and `g` are one letter in two cases, which is the whole command set for
|
||||||
then erases and programs — except pages inside the 512-byte slot
|
every memory: the **selector** byte's low nibble names the space and its high
|
||||||
the loader is *running* in, which are drained and left alone, so a broken host
|
nibble carries the flash bank.
|
||||||
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 `W` takes those stale words and
|
self-programmed before entering. The next page write takes those stale words
|
||||||
clears them, since a page write auto-erases the buffer (§26.2.1; §19.2 on the
|
and clears them, since a page write auto-erases the buffer (§26.2.1; §19.2 on
|
||||||
tinies), so repeating it programs correctly. The host therefore verifies every
|
the tinies), so repeating it programs correctly. The host therefore verifies
|
||||||
page it writes and rewrites what comes back wrong (three retries, then it
|
every page it writes and rewrites what comes back wrong (three retries, then it
|
||||||
stops).
|
stops).
|
||||||
|
|
||||||
`w` is host-paced: send the next byte only after the previous byte's `+`. `F`
|
`g` is host-paced: send the next byte only after the previous byte's `+`. Fuse
|
||||||
returns the bytes in the hardware's Z order; on a chip without an extended
|
*writing* does not exist — SPM reaches flash and boot lock bits only.
|
||||||
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 known from the info block) and moves between loader
|
the trampoline word, both derived from the chip) 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.
|
||||||
|
|
||||||
The info block (`b`):
|
`b` answers with the loader's identity — its version and the chip's signature —
|
||||||
|
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.
|
||||||
|
|
||||||
| Offset | Content |
|
An update image, though, is a bare 512-byte slot with no device to ask, and
|
||||||
|---|---|
|
installing one built for another chip bricks the target. Every loader image
|
||||||
| 0–2 | `'P'`, `'B'`, pureboot version (3) |
|
therefore carries a six-byte **stamp** — `'P'`, `'B'`, the version, the three
|
||||||
| 3–5 | device signature |
|
signature bytes — which the loader itself never reads and the host tool refuses
|
||||||
| 6 | SPM page size in bytes (0 means 256) |
|
to install a mismatch against.
|
||||||
| 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
|
||||||
|
|
||||||
The info block's third byte is the **pureboot version** — the loader's one
|
`b`'s first byte is the **pureboot version** — the loader's one identity
|
||||||
identity number, and the only way to tell what a deployed loader is. Nothing
|
number, and the only way to tell what a deployed loader is. Nothing else is
|
||||||
else is numbered: the wire protocol has no version, a pureboot version implies
|
numbered: the wire protocol has no version, a pureboot version implies it, and
|
||||||
it, and the host tool holds that map. The tool states the window of loader
|
the host tool holds that map. The tool states the window of loader versions it
|
||||||
versions it speaks (`OLDEST_LOADER`/`NEWEST_LOADER` in `pureboot.py`), and a
|
speaks (`OLDEST_LOADER`/`NEWEST_LOADER` in `pureboot.py`), and a version that
|
||||||
version that changes the protocol becomes the new floor there. None has so
|
changes the protocol becomes the new floor there. A loader newer than the tool
|
||||||
far: 1 through 3 speak the identical session. A loader newer than the tool is
|
is refused by name rather than decoded on the assumption that nothing moved.
|
||||||
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.
|
||||||
@@ -252,11 +320,10 @@ 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 info block embedded
|
The preflight refuses an image built for another chip: the stamp every pureboot
|
||||||
in every pureboot binary (signature, page size, loader base, EEPROM size,
|
binary carries must resolve to the device's own geometry, and the error names
|
||||||
flags) must match the device's own, and the error names both. Die revisions
|
both. Die revisions share their base signature and geometry, so their images
|
||||||
share their base signature and geometry, so their images are interchangeable —
|
are interchangeable — as the silicon is.
|
||||||
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).
|
||||||
@@ -293,8 +360,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 the
|
update, flash (erase / program / read / verify), EEPROM (the same), then
|
||||||
loader hands over to the application. `--stay` keeps the session alive
|
`--peek`/`--poke` — then the 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).
|
||||||
@@ -302,9 +369,18 @@ 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.
|
||||||
|
|
||||||
Readouts come one fact per line: `--info` decodes the info block field by
|
`--autobaud` opens with the calibration pulse instead of the plain knock, for a
|
||||||
field, `--fuses` each fuse byte plus, on a boot-sectioned mega, its decoded
|
loader built `SERIAL autobaud`; the rest of the session is identical, at
|
||||||
meaning. Transfers that take wire time draw a transient progress bar on stderr
|
whatever `--baud` the host chose.
|
||||||
|
|
||||||
|
`--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.
|
||||||
|
|
||||||
@@ -323,15 +399,19 @@ 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;
|
||||||
- `pbm_*.size` — under `--full`, the exhaustive cross product replacing that
|
- `pureboot_autobaud.size` — the clock-free build, which has no clock or baud
|
||||||
compact matrix: every plausible oscillator (the internal ones, the CKDIV8
|
axis of its own: one binary per chip has to serve every point the matrix
|
||||||
floor, the plain and the UART crystals) × every rate reachable from it ×
|
below sweeps;
|
||||||
every backend, unreachable combinations dropping out rather than aborting
|
- `pbm_*.size` — with `PUREBOOT_FULL_MATRIX=1`, the exhaustive cross product
|
||||||
the configure. Bounded to one chip per size-bearing class — flash
|
replacing that compact matrix, on **every** chip: every plausible oscillator
|
||||||
addressing, hand-over shape, page size, USART inventory — since everything
|
(the internal ones, the CKDIV8 floor, the plain and the UART crystals) ×
|
||||||
else in the image is chip-independent code;
|
every rate reachable from it × every backend, unreachable combinations
|
||||||
- `pureboot.pi` — the position-independence lint: no absolute `jmp`/`call`, the
|
dropping out rather than aborting the configure. Thousands of points per
|
||||||
info block within the image's first 256 bytes;
|
chip, and cheap enough to run rather than reason about;
|
||||||
|
- `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
|
||||||
@@ -361,7 +441,13 @@ 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.
|
||||||
|
|||||||
@@ -1,251 +0,0 @@
|
|||||||
# 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,14 +26,17 @@ 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.
|
// source — a tiny13A cannot read its own signature row from code. An autobaud
|
||||||
#if !defined(PUREBOOT_CLOCK_HZ) || !defined(PUREBOOT_BAUD)
|
// build carries no clock and no baud at all; it measures both.
|
||||||
|
#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() (README.md)"
|
"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)"
|
||||||
#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()
|
||||||
@@ -47,60 +50,110 @@ 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 a byte address no longer fits the wire's 16 bits, so flash
|
// Past 64 KiB one bank of flash does not cover the chip, so a transfer's
|
||||||
// addresses there are word addresses ('J' always was one). A slot is 256 of
|
// selector byte carries the bank and the wire address stays a byte address
|
||||||
// those — one value of a wire address's high byte, where 512 bytes span two.
|
// within it. 'J' is the exception: it is a word address everywhere, because
|
||||||
constexpr bool word_flash = spm::flash_bytes > 65536;
|
// that is what the hardware's own jump takes.
|
||||||
constexpr std::uint16_t wire_base =
|
constexpr bool banked_flash = spm::flash_bytes > 65536;
|
||||||
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.
|
// re-timing a deployed loader is a self-update with a re-timed build. An
|
||||||
|
// 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 = 4;
|
constexpr std::uint8_t version = 5;
|
||||||
|
|
||||||
// The 'b' reply, byte for byte (layout: README.md). Flash-resident because
|
// The image's identity stamp, for the host tool rather than for the wire: an
|
||||||
// no crt copies a .data image — and flash_table's storage carries the word
|
// update image is a bare 512-byte slot, and without this nothing in it says
|
||||||
// alignment 'b' needs to halve the address on the large chips.
|
// which chip it was built for. The tool refuses to install an image whose
|
||||||
// One wire byte per line: this is the reply's layout, not a list.
|
// stamp does not match the device — flashing a foreign loader bricks the
|
||||||
|
// 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
|
||||||
inline constexpr avr::flash_table<std::array<std::uint8_t, 12>{
|
[[gnu::used, gnu::retain, gnu::section(".text.stamp")]]
|
||||||
'P',
|
inline constexpr std::uint8_t identity_stamp[]{
|
||||||
'B',
|
'P', 'B', // the magic the host scans an image for
|
||||||
version,
|
version, // and from here on, exactly what 'b' answers
|
||||||
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 serial link, per the build's PUREBOOT_USART / PUREBOOT_SOFT_SERIAL,
|
// The address spaces a transfer can name, in a selector byte's low nibble.
|
||||||
// defaulting to the chip's USART0 where it has one. The software receiver is
|
// Flash is 0 so it is the cheapest to select.
|
||||||
// the polled one: the vector table belongs to the application. Templates on
|
//
|
||||||
// the clock, so only the selected backend instantiates. pending() is the
|
// spm_ops is the one that is not memory: a write there hands its byte to
|
||||||
// cheap line test the activation window polls; drain() holds until the last
|
// SPMCSR and fires the instruction at the transfer's address, which is how
|
||||||
// frame is off the wire, so a hand-over cannot let the target's re-init clip
|
// page erase, page write and RWW re-enable reach the wire without the loader
|
||||||
// the ack.
|
// carrying a command for each. The hardware's four-cycle store-to-SPM window
|
||||||
|
// 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
|
||||||
@@ -113,9 +166,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>
|
template <avr::hertz_t C, avr::baud_t B>
|
||||||
struct hardware_link {
|
struct hardware_link {
|
||||||
using uart = avr::uart::usart<usart_digit, C, {.baud = wire_baud, .max_baud_error = 2.5_pct}>;
|
using uart = avr::uart::usart<usart_digit, C, {.baud = B, .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).
|
||||||
@@ -147,10 +200,10 @@ struct hardware_link {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
template <avr::hertz_t C>
|
template <avr::hertz_t C, avr::baud_t B>
|
||||||
struct software_link {
|
struct software_link {
|
||||||
using rx_t = avr::uart::software_rx_polled<C, avr::PUREBOOT_RX, wire_baud>;
|
using rx_t = avr::uart::software_rx_polled<C, avr::PUREBOOT_RX, B>;
|
||||||
using tx_t = avr::uart::software_tx<C, avr::PUREBOOT_TX, wire_baud>;
|
using tx_t = avr::uart::software_tx<C, avr::PUREBOOT_TX, B>;
|
||||||
|
|
||||||
// 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).
|
||||||
@@ -182,14 +235,44 @@ struct software_link {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
#if defined(PUREBOOT_USART)
|
// The clock-free link: the bit period is measured from the host's calibration
|
||||||
|
// 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>;
|
using link = hardware_link<dev::clock, wire_baud>;
|
||||||
#elif defined(PUREBOOT_SOFT_SERIAL)
|
#elif defined(PUREBOOT_SOFT_SERIAL)
|
||||||
using link = software_link<dev::clock>;
|
using link = software_link<dev::clock, wire_baud>;
|
||||||
#else
|
#else
|
||||||
using link =
|
using link = std::conditional_t<avr::uart::has_usart<usart_digit>(), hardware_link<dev::clock, wire_baud>,
|
||||||
std::conditional_t<avr::uart::has_usart<usart_digit>(), hardware_link<dev::clock>, software_link<dev::clock>>;
|
software_link<dev::clock, wire_baud>>;
|
||||||
#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
|
||||||
@@ -209,6 +292,27 @@ 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()
|
||||||
@@ -235,6 +339,14 @@ 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()
|
||||||
@@ -252,132 +364,92 @@ std::uint8_t rx_deadline()
|
|||||||
return std::bit_cast<std::uint16_t>(pair);
|
return std::bit_cast<std::uint16_t>(pair);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Counts arrive in the wire's 8-bit form: 0 means 256. Both streamers fold
|
// Out of line: several sites send it, and a call is shorter than a
|
||||||
// into the one command that reads flash, which is what lets the far one's
|
// load-immediate at each.
|
||||||
// 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
void send_eeprom(std::uint16_t address, std::uint8_t count)
|
// A wire address and its selector's bank as the flash address they name.
|
||||||
|
[[gnu::always_inline]] inline spm::flash_address_t flash_address([[maybe_unused]] std::uint8_t bank, std::uint16_t at)
|
||||||
{
|
{
|
||||||
do
|
if constexpr (banked_flash)
|
||||||
link::tx(ee::read(address++));
|
return (static_cast<spm::flash_address_t>(bank) << 16) | at;
|
||||||
while (--count);
|
else
|
||||||
|
return at;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// One byte out of any space. Every accessor shares the transfer's cursor, its
|
||||||
|
// loop and its call site, so a space costs only its own instruction rather
|
||||||
|
// than a body, a loop and a dispatch arm of its own.
|
||||||
|
[[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)
|
||||||
|
return ee::read(at);
|
||||||
|
if (space == sp_data)
|
||||||
|
return *reinterpret_cast<volatile std::uint8_t *>(at);
|
||||||
|
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
|
||||||
|
// page at a time through 'W' and is committed through sp_spm — and the fuses
|
||||||
|
// are not writable at all: SPM reaches flash and boot lock bits only.
|
||||||
|
[[gnu::always_inline]] inline void store(std::uint8_t space, std::uint8_t bank, std::uint16_t at, std::uint8_t value,
|
||||||
|
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
|
// 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.
|
// arrives while it completes and nothing is missed without a buffer.
|
||||||
void store_eeprom(std::uint16_t address, std::uint8_t count)
|
ee::write<off>(at, value);
|
||||||
{
|
|
||||||
do {
|
|
||||||
ee::write<off>(address++, link::rx());
|
|
||||||
tx_ack();
|
|
||||||
} while (--count);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// One page into the SPM buffer, then erase and program — except the slot
|
// One page into the SPM buffer, and only that: the erase and the write that
|
||||||
// this code is running in (`slot_high`, from run()), which is drained and
|
// commit it are host-issued sp_spm stores, which reach the same fused
|
||||||
// left alone. A broken host therefore cannot brick the running loader, and a
|
// store-and-SPM pair through the transfer path's own address and data.
|
||||||
// copy one slot lower may rewrite the resident one.
|
|
||||||
//
|
//
|
||||||
// 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 program_flash(std::uint16_t wire_address, std::uint8_t slot_high)
|
void fill_page(std::uint8_t bank, std::uint16_t at)
|
||||||
{
|
{
|
||||||
// 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 — one induction either way: a byte-addressed
|
// starts at the page base; the low byte of the cursor is the whole in-page
|
||||||
// wire address walks the page itself (the offset bits wrap back to zero),
|
// offset, since a page is aligned and never crosses a bank.
|
||||||
// while a word one becomes a byte cursor once. The slot index is the wire
|
std::uint16_t z = at & ~static_cast<std::uint16_t>(page - 1);
|
||||||
// address's high byte — on byte-addressed chips the byte address's, with
|
|
||||||
// the low bit dropped, since a slot is two of those.
|
|
||||||
spm::flash_address_t address;
|
|
||||||
std::uint8_t page_high;
|
|
||||||
if constexpr (word_flash) {
|
|
||||||
// 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 {
|
do {
|
||||||
std::uint8_t low = link::rx();
|
std::uint8_t low = link::rx();
|
||||||
std::uint8_t high = link::rx();
|
std::uint8_t high = link::rx();
|
||||||
spm::fill<off>((static_cast<spm::flash_address_t>(rampz) << 16) | z, word_of({low, high}));
|
spm::fill<off>(flash_address(bank, z), word_of({low, high}));
|
||||||
z += 2;
|
z += 2;
|
||||||
} while (static_cast<std::uint8_t>(z));
|
} while (static_cast<std::uint8_t>(z) & (page - 1));
|
||||||
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()
|
||||||
@@ -389,19 +461,14 @@ void send_fuses()
|
|||||||
|
|
||||||
link::init();
|
link::init();
|
||||||
|
|
||||||
// The high byte of the slot this copy runs at, which the write guard and
|
// The slot this copy runs in, which the write guard follows: the return
|
||||||
// the info block both follow: the return address is a word address, so its
|
// address is a word address and a slot is half as many words as bytes, so
|
||||||
// high byte is the 256-word slot index, doubled back into byte terms where
|
// its high byte is the slot index outright. No absolute address is ever
|
||||||
// the wire counts bytes. Taken as byteswap's low byte — the builtin already
|
// formed, so the image stays position-independent.
|
||||||
// swaps the two stacked bytes, and the double swap folds away, where `>> 8`
|
const auto return_words = reinterpret_cast<std::uint16_t>(__builtin_return_address(0));
|
||||||
// would leave the swap materialized.
|
const auto slot_high = static_cast<std::uint8_t>(return_words >> 8);
|
||||||
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);
|
|
||||||
|
|
||||||
// 'p' then 'b', each under a fresh window; anything else is line noise.
|
await_host();
|
||||||
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
|
||||||
@@ -416,47 +483,43 @@ void send_fuses()
|
|||||||
link::drain();
|
link::drain();
|
||||||
jump(target);
|
jump(target);
|
||||||
}
|
}
|
||||||
case 'b': // info block, read relative to the running slot
|
case 'b': // identity: the version, then the three signature bytes
|
||||||
case 'R': // read flash: addr16, n8 (0 = 256)
|
// Straight out of the stamp, so the wire and the image can never
|
||||||
case 'r': // read EEPROM: addr16, n8
|
// disagree about what this loader is. The indices are constant and
|
||||||
case 'w': { // write EEPROM: addr16, n8, then n bytes each acked
|
// the array is constexpr, so these are immediates, not flash reads:
|
||||||
// One address-and-count path for all four: 'b' is a flash read
|
// nothing here needs the stamp's runtime address.
|
||||||
// whose arguments the loader already knows, so it joins the
|
for (std::uint8_t at = stamp_identity; at != sizeof identity_stamp; ++at)
|
||||||
// wire-argument three rather than streaming from a call site of its
|
link::tx(identity_stamp[at]);
|
||||||
// own. That leaves one flash streamer in the image, and lets its
|
break;
|
||||||
// cursor live in this never-returning loop's own call-saved
|
case 'W': // fill one flash page buffer: sel8, addr16, then page bytes
|
||||||
// registers instead of being saved and restored around a call.
|
case 'G': // read: sel8, addr16, n8 (0 = 256)
|
||||||
std::uint16_t address;
|
case 'g': { // write: sel8, addr16, n8, then n bytes, each acked
|
||||||
std::uint8_t count;
|
// One decode, one cursor and one loop for every space and both
|
||||||
if (command == 'b') {
|
// directions: a command per memory would carry a copy of all three
|
||||||
// The block sits in the image's first 256 bytes (check_pi.py
|
// each. 'W' joins the same decode rather than keeping an address
|
||||||
// asserts it) and slots are 512-aligned, so the low byte of its
|
// form of its own, so flash addressing is uniform across every
|
||||||
// link address is its offset in any slot — halved where wire
|
// command that names it.
|
||||||
// units are words. The high byte is runtime data, so no
|
const std::uint8_t selector = link::rx();
|
||||||
// absolute address is ever materialized.
|
const std::uint8_t space = space_of(selector);
|
||||||
const auto link_byte =
|
const std::uint8_t bank = bank_of(selector);
|
||||||
static_cast<std::uint8_t>(reinterpret_cast<std::uint16_t>(info_data.storage.data()));
|
std::uint16_t at = rx16();
|
||||||
const std::uint8_t low = word_flash ? static_cast<std::uint8_t>(link_byte >> 1) : link_byte;
|
if (command == 'W') {
|
||||||
address = static_cast<std::uint16_t>(low | (slot_high << 8));
|
fill_page(bank, at);
|
||||||
count = static_cast<std::uint8_t>(info_data.size());
|
|
||||||
} else {
|
|
||||||
address = rx16();
|
|
||||||
count = link::rx();
|
|
||||||
}
|
|
||||||
if (command == 'r')
|
|
||||||
send_eeprom(address, count);
|
|
||||||
else if (command == 'w')
|
|
||||||
store_eeprom(address, count);
|
|
||||||
else
|
|
||||||
send_flash(address, count);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'W': // program one flash page: addr16, page bytes
|
std::uint8_t count = link::rx();
|
||||||
program_flash(rx16(), slot_high);
|
do {
|
||||||
break;
|
// Read and write are one letter apart in case, so the direction
|
||||||
case 'F': // fuse and lock bytes
|
// is a single bit and the loop picks it with a one-word skip.
|
||||||
send_fuses();
|
if (command & 0x20) {
|
||||||
|
store(space, bank, at, link::rx(), slot_high);
|
||||||
|
tx_ack();
|
||||||
|
} else
|
||||||
|
link::tx(load(space, bank, at));
|
||||||
|
++at;
|
||||||
|
} while (--count);
|
||||||
break;
|
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 = 3 # this tool's own version — free to drift from a loader's
|
VERSION = 4 # 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,36 +34,38 @@ 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 unified pair: 'G'
|
# pureboot 5 replaced the four per-memory commands with one pair: 'G' reads and
|
||||||
# reads and 'g' writes, both taking a selector byte, a 16-bit address and a
|
# 'g' writes, each taking a selector byte, a 16-bit address and a count, over
|
||||||
# count, over the spaces below. The loader carries one transfer loop instead of
|
# the spaces below. The loader carries one transfer loop instead of four bodies
|
||||||
# four bodies, which is what paid for RAM access and for the calibration fix.
|
# — which is what buys the data space and the host-issued SPM operations.
|
||||||
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 flash's third address byte, so a transfer names a
|
# A selector's high nibble is the flash bank — the address bits above the 16-bit
|
||||||
# byte address within a 64 KiB bank and never has to speak word addresses. No
|
# wire address — so a transfer names a byte address within one 64 KiB bank and
|
||||||
# single transfer may cross a bank boundary — the host chunks to keep that true.
|
# no command has to speak word addresses. No single transfer may cross a bank
|
||||||
|
# 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
|
||||||
# data byte to SPMCSR and fires the instruction at the selected flash address.
|
# byte to SPMCSR and fires the instruction at the selected flash address. Every
|
||||||
# Every part pureboot targets agrees on these encodings.
|
# 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 the autobaud loader: 0xC0 is a start bit plus six zero
|
# Calibration byte for an 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
|
||||||
|
|
||||||
# The autobaud loader slims its info block to the version and signature; the host
|
# pureboot 5 answers 'b' with its version and the chip signature; the host
|
||||||
# derives the rest of the geometry from the signature. flash, page, eeprom,
|
# derives the rest of the geometry from the signature rather than reading a
|
||||||
# patch-vector per distinct signature, over every chip pureboot targets (the
|
# table off the device. flash, page, eeprom, patch-vector per distinct
|
||||||
# loader computes the same from its chip database at build time). Die revisions
|
# signature, over every chip pureboot targets (the loader computes the same
|
||||||
# that share a signature share this row, as they share the silicon.
|
# from its chip database at build time). Die revisions that share a signature
|
||||||
AUTOBAUD_GEOMETRY = {
|
# share this row, as they share the silicon.
|
||||||
|
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
|
||||||
@@ -359,18 +361,26 @@ class Info:
|
|||||||
"""The 12-byte info block."""
|
"""The 12-byte info block."""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_slim(cls, raw):
|
def from_identity(cls, raw):
|
||||||
"""The autobaud loader's slimmed reply — version and signature only —
|
"""pureboot 5's reply: the version and the chip signature. The rest of
|
||||||
with the rest of the geometry looked up from the signature (the loader
|
the geometry is looked up from the signature — the loader derived the
|
||||||
derived it from the same chip facts at build time). Reconstructs the full
|
same facts from its chip database at build time, so nothing is guessed,
|
||||||
block so every derived attribute matches the fixed-baud path exactly."""
|
it is simply not sent. Reconstructs a block in the older layout, so
|
||||||
|
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 slim info block: {raw.hex()}")
|
raise Error(f"bad identity reply: {raw.hex()}")
|
||||||
version, signature = raw[0], tuple(raw[1:4])
|
version, signature = raw[0], tuple(raw[1:4])
|
||||||
geometry = AUTOBAUD_GEOMETRY.get(signature)
|
geometry = CHIP_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 autobaud geometry for it")
|
raise Error(f"unknown signature {sig} — this tool has no 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
|
||||||
@@ -393,8 +403,11 @@ 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: flash addresses are words on the wire. Every address here
|
# Bit 1: the flash runs past what one 16-bit address covers. Through
|
||||||
# stays a byte address and converts at the wire.
|
# pureboot 4 that made flash addresses words on the wire; pureboot 5
|
||||||
|
# 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
|
||||||
@@ -424,7 +437,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"
|
||||||
+ (", word-addressed wire" if self.word_flash else ""),
|
+ (", past one 16-bit bank" 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}",
|
||||||
@@ -441,70 +454,69 @@ 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):
|
||||||
|
"""The 'b' reply, in either of the two layouts a loader may send.
|
||||||
|
pureboot 5 answers with its version and the signature; older loaders
|
||||||
|
answer with a 12-byte block. The version byte cannot be mistaken for
|
||||||
|
the older block's 'P', so four bytes are enough to tell them apart."""
|
||||||
|
head = self.port.read_exact(4, 2.0)
|
||||||
|
if head[0:2] == b"PB":
|
||||||
|
return Info(head + self.port.read_exact(8, 2.0))
|
||||||
|
return Info.from_identity(head)
|
||||||
|
|
||||||
|
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
|
||||||
|
knocks = 0
|
||||||
|
while True:
|
||||||
|
self.port.flush_input()
|
||||||
|
self.port.write(knock)
|
||||||
|
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:
|
||||||
|
# A version the tool cannot speak is the loader's own
|
||||||
|
# answer, not a failed knock: Info reports it rather than
|
||||||
|
# sending the tool round the loop again.
|
||||||
|
self.info = self._read_identity()
|
||||||
|
except Error as failed:
|
||||||
|
if "pureboot" in str(failed):
|
||||||
|
raise
|
||||||
|
self.info = None
|
||||||
|
if self.info is not None:
|
||||||
|
self._expect_prompt()
|
||||||
|
verbose(f"loader answered {what} {knocks}; identity read")
|
||||||
|
return self.info
|
||||||
|
if time.monotonic() > deadline:
|
||||||
|
raise Error("no answer — reset the device within its activation window")
|
||||||
|
|
||||||
def connect(self, wait):
|
def connect(self, wait):
|
||||||
"""Knock until the info block comes back. The block is what proves the
|
"""Knock 'p' then 'b' and read the identity."""
|
||||||
loader is listening — a prompt byte alone does not, since one left over
|
return self._handshake(wait, b"pb", "knock")
|
||||||
from a previous session can still be in the pipeline while the port
|
|
||||||
opening resets the device into a fresh activation window, where a
|
|
||||||
command without its knock is discarded. Each attempt is therefore the
|
|
||||||
whole handshake, retried until it produces the block or the window
|
|
||||||
closes. Also converges into a live session: the knock bytes are ignored
|
|
||||||
there and the drain absorbs whatever they produced."""
|
|
||||||
deadline = time.monotonic() + wait
|
|
||||||
knocks = 0
|
|
||||||
while True:
|
|
||||||
self.port.flush_input()
|
|
||||||
self.port.write(b"pb")
|
|
||||||
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(12, 2.0)
|
|
||||||
except Error:
|
|
||||||
block = b""
|
|
||||||
# A version the tool cannot speak is the loader's own answer,
|
|
||||||
# not a failed knock: Info reports it rather than retrying.
|
|
||||||
if block[0:2] == b"PB":
|
|
||||||
self.info = Info(block)
|
|
||||||
self._expect_prompt()
|
|
||||||
verbose(f"loader answered knock {knocks}; info block read")
|
|
||||||
return self.info
|
|
||||||
if time.monotonic() > deadline:
|
|
||||||
raise Error("no answer — reset the device within its activation window")
|
|
||||||
|
|
||||||
def connect_autobaud(self, wait):
|
def connect_autobaud(self, wait):
|
||||||
"""The autobaud handshake. Instead of the p+b knock, the host sends the
|
"""The autobaud handshake. In place of the p+b knock the host sends the
|
||||||
0xC0 calibration pulse — a single seven-bit-time low pulse at the host's
|
calibration pulse — one seven-bit-time low pulse at the host's chosen
|
||||||
chosen baud — which the loader times into its per-bit unit, then a single
|
baud, which the loader times into its per-bit unit — then a single 'p'
|
||||||
'p' knock the loader decodes at the rate it just measured. As with
|
the loader decodes at the rate it just measured. A lost pulse, or a
|
||||||
connect(), each attempt is the whole handshake, retried until the slim
|
knock landing while the loader is mid-frame, simply fails to answer and
|
||||||
info block comes back or the window closes: a lost pulse or a knock that
|
leaves the measurement loop waiting for the next pulse, so the retry in
|
||||||
lands while the loader is mid-frame simply fails to answer, and the
|
_handshake covers it."""
|
||||||
loader's measurement loop is back waiting for the next pulse."""
|
self.autobaud = True
|
||||||
deadline = time.monotonic() + wait
|
return self._handshake(wait, bytes((CALIBRATE, ord("p"))), "calibration")
|
||||||
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)
|
||||||
@@ -650,8 +662,9 @@ 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(wait)
|
return self.connect_autobaud(wait) if autobaud else 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)
|
||||||
@@ -818,12 +831,26 @@ def mega_boot(info, fuse_bytes):
|
|||||||
|
|
||||||
|
|
||||||
def image_info(image):
|
def image_info(image):
|
||||||
"""The info block embedded in a pureboot binary, or None. Searched once
|
"""What a pureboot binary says about itself, or None.
|
||||||
per known version, so the magic stays three selective bytes rather than
|
|
||||||
two that code could carry by chance."""
|
An update image is a bare slot: nothing about it names the chip it was
|
||||||
|
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 0 <= at <= len(image) - 12:
|
if at < 0:
|
||||||
|
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
|
||||||
|
|
||||||
@@ -1013,7 +1040,10 @@ 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)
|
||||||
staged_loader = image_info(current[:268])
|
# The whole slot is searched: a loader's stamp sits wherever its image put
|
||||||
|
# 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:
|
||||||
|
|||||||
@@ -1,396 +0,0 @@
|
|||||||
// 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>;
|
|
||||||
@@ -1,371 +0,0 @@
|
|||||||
// 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>;
|
|
||||||
@@ -1,411 +0,0 @@
|
|||||||
// 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,47 +1,75 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Position-independence lint: the two link-time facts that let the identical
|
"""Position-independence lint: the property that lets the identical image run
|
||||||
image run from any slot, asserted from the built ELF.
|
from any slot, asserted from the built ELF and its object.
|
||||||
|
|
||||||
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. The info block within the image's first 256 bytes: 'b' rebuilds its
|
2. Nothing flash-resident to address: the image is .text alone, so there is
|
||||||
address as (running slot high byte : link address low byte).
|
no table whose runtime address has to be reconstructed.
|
||||||
|
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> <nm> <elf> <text_start_hex>
|
Usage: check_pi.py <objdump> <objcopy> <cxx> <mcu> <elf> <object> <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, nm, elf, text_start = sys.argv[1:]
|
objdump, objcopy, cxx, mcu, elf, obj, 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 = [
|
absolute = [line for line in listing.splitlines() if re.search(r"\t(jmp|call)\t", line)]
|
||||||
line
|
|
||||||
for line in listing.splitlines()
|
|
||||||
if re.search(r"\t(jmp|call)\t", line)
|
|
||||||
]
|
|
||||||
if absolute:
|
if absolute:
|
||||||
print("FAIL: absolute control flow in the image:")
|
fail("absolute control flow in the image:\n" + "\n".join(absolute))
|
||||||
print("\n".join(absolute))
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
symbols = subprocess.run([nm, "-C", elf], capture_output=True, text=True, check=True).stdout
|
# Allocated flash beyond .text would be data the running copy has to find.
|
||||||
info = [line for line in symbols.splitlines() if "flash_table" in line and "::storage" in line]
|
# Only ALLOC sections reach the device at all; .comment and the debug
|
||||||
if len(info) != 1:
|
# sections ride along in the ELF container and are never flashed. objdump
|
||||||
print(f"FAIL: expected one info-block storage symbol, found {len(info)}")
|
# prints each section's flags on the line following its header.
|
||||||
sys.exit(1)
|
headers = subprocess.run([objdump, "-h", elf], capture_output=True, text=True, check=True).stdout.splitlines()
|
||||||
address = int(info[0].split()[0], 16)
|
for index, line in enumerate(headers):
|
||||||
offset = address - text_start
|
fields = line.split()
|
||||||
if not 0 <= offset < 256:
|
if len(fields) < 6 or not fields[0].isdigit():
|
||||||
print(f"FAIL: info block at image offset {offset:#x}, must sit in the first 256 bytes")
|
continue
|
||||||
sys.exit(1)
|
name, size = fields[1], int(fields[2], 16)
|
||||||
|
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")
|
||||||
|
|
||||||
print(f"PI lint: control flow PC-relative, info block at offset {offset:#x}")
|
# Relink at a different base and compare the bytes.
|
||||||
|
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,6 +55,12 @@ 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)
|
||||||
@@ -73,12 +79,20 @@ 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, hand over
|
# Session 2: reconnect into the live session, verify, dump, exercise
|
||||||
# is deferred — the pty must be reopened for the APP banner first.
|
# the data space; hand over is deferred — the pty must be reopened for
|
||||||
|
# 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, "--stay")
|
"--read-flash", read_flash, "--read-eeprom", read_eeprom,
|
||||||
|
"--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:
|
||||||
@@ -110,12 +124,16 @@ 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
|
# consume exactly one page and prompt. The loader's own slot is the
|
||||||
# the target — it is drained and never programmed — and the
|
# target — the guard refuses to commit it — and the payload is
|
||||||
# payload is erased-state bytes, so the probe can disturb neither
|
# erased-state bytes, so the probe can disturb neither the image nor
|
||||||
# the image nor the page buffer it leaves behind.
|
# the page buffer it leaves behind. Hand-built rather than through
|
||||||
wire = wire_base + 1
|
# write_page(), which would follow the fill with its erase and
|
||||||
port.write(bytes((ord("W"), wire & 0xFF, wire >> 8)) + b"\xff" * page)
|
# write; the point here is that the fill alone consumes exactly one
|
||||||
|
# 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,9 +182,16 @@ 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;
|
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;
|
||||||
|
}
|
||||||
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,8 +30,14 @@ 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, 0, 2, flags))
|
*signature, page & 0xFF, wire_base & 0xFF, wire_base >> 8,
|
||||||
|
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}")
|
||||||
@@ -162,11 +168,16 @@ 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 embedded info block: found in a synthetic binary, absent in noise.
|
# The image stamp: found in a synthetic binary, absent in noise. pureboot
|
||||||
binary = bytes((0xAA,)) * 10 + tiny.raw + bytes((0xBB,)) * 10
|
# 5 stamps the magic, its version and the signature, and the geometry is
|
||||||
|
# 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("image_info misses the embedded block")
|
fail(f"image_info misreads the v{pb.NEWEST_LOADER} stamp: "
|
||||||
|
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