Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f98ed406b8 | |||
| a4da885e36 | |||
| 335e494a31 | |||
| 799709efcf | |||
| 7ae80087b3 | |||
| b477f53ca5 | |||
| 84d3f679c2 | |||
| b5020a1e20 |
@@ -153,7 +153,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)
|
||||||
@@ -248,6 +251,15 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
-DLIMIT=${PUREBOOT_LIMIT} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
|
-DLIMIT=${PUREBOOT_LIMIT} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
|
# The autobaud loader: one clock-agnostic image per chip, so it has no
|
||||||
|
# clock x baud axis of its own — the matrix below sweeps those for the
|
||||||
|
# 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.
|
||||||
|
pureboot_add_loader(pureboot_autobaud SERIAL autobaud)
|
||||||
|
add_test(NAME pureboot_autobaud.size
|
||||||
|
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:pureboot_autobaud>
|
||||||
|
-DLIMIT=${PUREBOOT_LIMIT} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
|
||||||
|
|
||||||
# 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
|
||||||
# out here rather than aborting the configure.
|
# out here rather than aborting the configure.
|
||||||
@@ -287,16 +299,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)
|
||||||
@@ -378,4 +389,28 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
${CMAKE_BINARY_DIR}/pbusart1-work usart1)
|
${CMAKE_BINARY_DIR}/pbusart1-work usart1)
|
||||||
set_tests_properties(pureboot.usart1 PROPERTIES TIMEOUT 180)
|
set_tests_properties(pureboot.usart1 PROPERTIES TIMEOUT 180)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# The autobaud variants driven end to end over the software-UART bridge (both
|
||||||
|
# under review — pureboot/autobaud.md): the host sends the 0xC0 calibration
|
||||||
|
# pulse, the loader times it, locks, and programs. Run on the near-flash 328P
|
||||||
|
# and the word-addressed 1284P — the two flash-addressing classes — and each
|
||||||
|
# at two clocks with the one binary, which is the clock-agnostic property
|
||||||
|
# autobaud exists for (test/pbautobaud.py). The fixture application banners
|
||||||
|
# over the same software link at the first clock's rate.
|
||||||
|
if(LIBAVR_MCU MATCHES "^atmega(328p|1284p)$" AND DEFINED PB_DEVICE)
|
||||||
|
add_executable(pbapp_autobaud test/pbapp.cpp)
|
||||||
|
target_link_libraries(pbapp_autobaud PRIVATE libavr)
|
||||||
|
target_compile_definitions(pbapp_autobaud PRIVATE PUREBOOT_CLOCK_HZ=1000000
|
||||||
|
PUREBOOT_BAUD=9600 PUREBOOT_SOFT_SERIAL PUREBOOT_TX=pb1)
|
||||||
|
add_custom_command(TARGET pbapp_autobaud POST_BUILD
|
||||||
|
COMMAND ${CMAKE_OBJCOPY} -O binary
|
||||||
|
$<TARGET_FILE:pbapp_autobaud> $<TARGET_FILE:pbapp_autobaud>.bin)
|
||||||
|
add_test(NAME pureboot.autobaud
|
||||||
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbautobaud.py
|
||||||
|
${PB_DEVICE} $<TARGET_FILE:pureboot_autobaud> ${PUREBOOT_SIM_MCU}
|
||||||
|
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} $<TARGET_FILE:pbapp_autobaud>.bin
|
||||||
|
1000000 9600 ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
|
${CMAKE_BINARY_DIR}/pbautobaud-work)
|
||||||
|
set_tests_properties(pureboot.autobaud PROPERTIES TIMEOUT 240)
|
||||||
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
2
libavr
2
libavr
Submodule libavr updated: a8ed8c4851...6cfc7a8eee
@@ -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}>)
|
||||||
@@ -310,3 +326,4 @@ function(pureboot_add_loader name)
|
|||||||
set_target_properties(${name} PROPERTIES PUREBOOT_HZ ${PB_CLOCK} PUREBOOT_BAUD ${PB_BAUD}
|
set_target_properties(${name} PROPERTIES PUREBOOT_HZ ${PB_CLOCK} PUREBOOT_BAUD ${PB_BAUD}
|
||||||
PUREBOOT_LINK ${_link})
|
PUREBOOT_LINK ${_link})
|
||||||
endfunction()
|
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
|
||||||
@@ -98,17 +114,35 @@ speak to the build. This exact deployment runs the full protocol suite in CI
|
|||||||
|
|
||||||
Reset enters the loader (BOOTRST on the boot-sectioned megas, the patched
|
Reset enters the loader (BOOTRST on the boot-sectioned megas, the patched
|
||||||
reset vector elsewhere) — except a watchdog reset, which hands straight to the
|
reset vector elsewhere) — except a watchdog reset, which hands straight to the
|
||||||
application, since the application owns its watchdog and must clear WDRF
|
application with no activation window, since the application owns its watchdog.
|
||||||
itself.
|
This is deliberate: it lets an application reboot itself instantly rather than
|
||||||
|
sit through the window. The application must clear WDRF itself (libavr's
|
||||||
|
`watchdog::disable()` does). **Gotcha:** WDRF is sticky (cleared only by
|
||||||
|
software, not by a later reset), so an application that watchdog-resets and
|
||||||
|
never clears it diverts *every* subsequent reset — external ones included —
|
||||||
|
past the window too, and the loader becomes reachable only through an external
|
||||||
|
programmer until the flag is cleared. A serial recovery path therefore assumes
|
||||||
|
the application clears WDRF on its own reset path.
|
||||||
|
|
||||||
The host then knocks `p` then `b`, each awaited byte under a fresh activation
|
The host then knocks `p` then `b`, each awaited byte under a fresh activation
|
||||||
window; any other byte is discarded and awaited again, so line noise can delay
|
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
|
||||||
|
|
||||||
@@ -118,68 +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
|
||||||
|
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
|
||||||
|
low EEPROM and the write silently overwrites it. Keeping transfers within the
|
||||||
|
real sizes is the host's job (the shipped tool does); the flash budget is
|
||||||
|
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.
|
||||||
@@ -238,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).
|
||||||
@@ -279,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).
|
||||||
@@ -288,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.
|
||||||
|
|
||||||
@@ -309,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
|
||||||
@@ -347,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.
|
||||||
|
|||||||
@@ -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,49 +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 = 3;
|
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
|
||||||
inline constexpr avr::flash_table<std::array<std::uint8_t, 12>{
|
// stamp does not match the device — flashing a foreign loader bricks the
|
||||||
'P', 'B', version, avr::hw::db.signature[0], avr::hw::db.signature[1], avr::hw::db.signature[2],
|
// target, and the loader itself cannot check what has already replaced it.
|
||||||
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,
|
// Never read from flash by the loader — 'b' answers out of this array, but at
|
||||||
static_cast<std::uint8_t>((boot_section ? 0 : 1) | (word_flash ? 2 : 0)), // patch-vector, word-addressed
|
// 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
|
||||||
info_data;
|
// needs and `retain` keeps --gc-sections from collecting it.
|
||||||
|
// clang-format off
|
||||||
|
[[gnu::used, gnu::retain, gnu::section(".text.stamp")]]
|
||||||
|
inline constexpr std::uint8_t identity_stamp[]{
|
||||||
|
'P', 'B', // the magic the host scans an image for
|
||||||
|
version, // and from here on, exactly what 'b' answers
|
||||||
|
avr::hw::db.signature[0],
|
||||||
|
avr::hw::db.signature[1],
|
||||||
|
avr::hw::db.signature[2],
|
||||||
|
};
|
||||||
|
// 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
|
||||||
@@ -102,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).
|
||||||
@@ -136,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).
|
||||||
@@ -171,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
|
||||||
@@ -198,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()
|
||||||
@@ -224,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()
|
||||||
@@ -241,133 +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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Host-paced: the ack goes out once the write has begun, so the next byte
|
// One byte out of any space. Every accessor shares the transfer's cursor, its
|
||||||
// arrives while it completes and nothing is missed without a buffer.
|
// loop and its call site, so a space costs only its own instruction rather
|
||||||
void store_eeprom(std::uint16_t address, std::uint8_t count)
|
// 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)
|
||||||
{
|
{
|
||||||
do {
|
if (space == sp_eeprom)
|
||||||
ee::write<off>(address++, link::rx());
|
return ee::read(at);
|
||||||
tx_ack();
|
if (space == sp_data)
|
||||||
} while (--count);
|
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 page into the SPM buffer, then erase and program — except the slot
|
// One byte into a writable space. Flash is not one of them — it arrives a
|
||||||
// this code is running in (`slot_high`, from run()), which is drained and
|
// page at a time through 'W' and is committed through sp_spm — and the fuses
|
||||||
// left alone. A broken host therefore cannot brick the running loader, and a
|
// are not writable at all: SPM reaches flash and boot lock bits only.
|
||||||
// copy one slot lower may rewrite the resident one.
|
[[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
|
||||||
|
// arrives while it completes and nothing is missed without a buffer.
|
||||||
|
ee::write<off>(at, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// One page into the SPM buffer, and only that: the erase and the write that
|
||||||
|
// commit it are host-issued sp_spm stores, which reach the same fused
|
||||||
|
// store-and-SPM pair through the transfer path's own address and data.
|
||||||
//
|
//
|
||||||
// Nothing discards the buffer first: it is write-once per word (§26.2.1), so
|
// Nothing discards the buffer first: it is write-once per word (§26.2.1), so
|
||||||
// filling over a refused page or an application's leavings programs stale
|
// filling over a refused page or an application's leavings programs stale
|
||||||
// words — but a page write auto-erases it (§26.2.1; §19.2 on the tinies), so
|
// words — but a page write auto-erases it (§26.2.1; §19.2 on the tinies), so
|
||||||
// that write clears the condition and the host's read-back rewrites the page.
|
// that write clears the condition and the host's read-back rewrites the page.
|
||||||
void 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()
|
||||||
@@ -379,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
|
||||||
@@ -406,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,16 +24,75 @@ else:
|
|||||||
import termios
|
import termios
|
||||||
|
|
||||||
PROMPT = b"+"
|
PROMPT = b"+"
|
||||||
VERSION = 2 # 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
|
||||||
# changes it becomes the new floor here.
|
# changes it becomes the new floor here.
|
||||||
OLDEST_LOADER = 1
|
OLDEST_LOADER = 1
|
||||||
NEWEST_LOADER = 3
|
NEWEST_LOADER = 5
|
||||||
SLOT = 512 # the loader slot, on every chip
|
SLOT = 512 # the loader slot, on every chip
|
||||||
RETRIES = 3 # rewrites of a page that reads back wrong, before the run stops
|
RETRIES = 3 # rewrites of a page that reads back wrong, before the run stops
|
||||||
|
|
||||||
|
# pureboot 5 replaced the four per-memory commands with one pair: 'G' reads and
|
||||||
|
# 'g' writes, each taking a selector byte, a 16-bit address and a count, over
|
||||||
|
# the spaces below. The loader carries one transfer loop instead of four bodies
|
||||||
|
# — which is what buys the data space and the host-issued SPM operations.
|
||||||
|
UNIFIED_LOADER = 5
|
||||||
|
SP_FLASH, SP_EEPROM, SP_RAM, SP_FUSE, SP_SPM = 0, 1, 2, 3, 4
|
||||||
|
|
||||||
|
# A selector's high nibble is the flash bank — the address bits above the 16-bit
|
||||||
|
# wire address — so a transfer names a byte address within one 64 KiB bank and
|
||||||
|
# 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):
|
||||||
|
return space | ((address >> 16) << 4)
|
||||||
|
|
||||||
|
|
||||||
|
# The SPM operations pureboot 5 leaves to the host: a write to SP_SPM hands its
|
||||||
|
# byte to SPMCSR and fires the instruction at the selected flash address. Every
|
||||||
|
# part pureboot targets agrees on these encodings.
|
||||||
|
SPM_ERASE, SPM_WRITE, SPM_RWWSRE = 0x03, 0x05, 0x11
|
||||||
|
|
||||||
|
# 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
|
||||||
|
# per-bit unit. Sent at whatever baud the host chose; the loader locks to it.
|
||||||
|
CALIBRATE = 0xC0
|
||||||
|
|
||||||
|
# pureboot 5 answers 'b' with its version and the chip signature; the host
|
||||||
|
# derives the rest of the geometry from the signature rather than reading a
|
||||||
|
# table off the device. flash, page, eeprom, patch-vector per distinct
|
||||||
|
# signature, over every chip pureboot targets (the loader computes the same
|
||||||
|
# from its chip database at build time). Die revisions that share a signature
|
||||||
|
# share this row, as they share the silicon.
|
||||||
|
CHIP_GEOMETRY = {
|
||||||
|
# signature : (flash, page, eeprom, patch_vector)
|
||||||
|
(0x1E, 0x90, 0x07): (1024, 32, 64, True), # ATtiny13/13A
|
||||||
|
(0x1E, 0x91, 0x08): (2048, 32, 128, True), # ATtiny25
|
||||||
|
(0x1E, 0x92, 0x06): (4096, 64, 256, True), # ATtiny45
|
||||||
|
(0x1E, 0x93, 0x0B): (8192, 64, 512, True), # ATtiny85
|
||||||
|
(0x1E, 0x92, 0x05): (4096, 64, 256, True), # ATmega48/48A
|
||||||
|
(0x1E, 0x92, 0x0A): (4096, 64, 256, True), # ATmega48P/48PA
|
||||||
|
(0x1E, 0x93, 0x07): (8192, 64, 512, False), # ATmega8/8A
|
||||||
|
(0x1E, 0x93, 0x0A): (8192, 64, 512, False), # ATmega88/88A
|
||||||
|
(0x1E, 0x93, 0x0F): (8192, 64, 512, False), # ATmega88P/88PA
|
||||||
|
(0x1E, 0x94, 0x03): (16384, 128, 512, False), # ATmega16/16A
|
||||||
|
(0x1E, 0x94, 0x06): (16384, 128, 512, False), # ATmega168/168A
|
||||||
|
(0x1E, 0x94, 0x0B): (16384, 128, 512, False), # ATmega168P/168PA
|
||||||
|
(0x1E, 0x94, 0x0A): (16384, 128, 512, False), # ATmega164P/164PA
|
||||||
|
(0x1E, 0x94, 0x0F): (16384, 128, 512, False), # ATmega164A
|
||||||
|
(0x1E, 0x95, 0x02): (32768, 128, 1024, False), # ATmega32/32A
|
||||||
|
(0x1E, 0x95, 0x0F): (32768, 128, 1024, False), # ATmega328P
|
||||||
|
(0x1E, 0x95, 0x14): (32768, 128, 1024, False), # ATmega328
|
||||||
|
(0x1E, 0x95, 0x08): (32768, 128, 1024, False), # ATmega324P
|
||||||
|
(0x1E, 0x95, 0x11): (32768, 128, 1024, False), # ATmega324PA
|
||||||
|
(0x1E, 0x95, 0x15): (32768, 128, 1024, False), # ATmega324A
|
||||||
|
(0x1E, 0x96, 0x09): (65536, 256, 2048, False), # ATmega644/644A
|
||||||
|
(0x1E, 0x96, 0x0A): (65536, 256, 2048, False), # ATmega644P/644PA
|
||||||
|
(0x1E, 0x97, 0x05): (131072, 256, 4096, False),# ATmega1284P
|
||||||
|
(0x1E, 0x97, 0x06): (131072, 256, 4096, False),# ATmega1284
|
||||||
|
}
|
||||||
|
|
||||||
VERBOSE = False
|
VERBOSE = False
|
||||||
|
|
||||||
|
|
||||||
@@ -301,6 +360,36 @@ Port = WindowsPort if os.name == "nt" else PosixPort
|
|||||||
class Info:
|
class Info:
|
||||||
"""The 12-byte info block."""
|
"""The 12-byte info block."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_identity(cls, raw):
|
||||||
|
"""pureboot 5's reply: the version and the chip signature. The rest of
|
||||||
|
the geometry is looked up from the signature — the loader derived the
|
||||||
|
same facts from its chip database at build time, so nothing is guessed,
|
||||||
|
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:
|
||||||
|
raise Error(f"bad identity reply: {raw.hex()}")
|
||||||
|
version, signature = raw[0], tuple(raw[1:4])
|
||||||
|
geometry = CHIP_GEOMETRY.get(signature)
|
||||||
|
if geometry is None:
|
||||||
|
sig = " ".join(f"{b:02x}" for b in signature)
|
||||||
|
raise Error(f"unknown signature {sig} — this tool has no geometry for it")
|
||||||
|
flash, page, eeprom, patch = geometry
|
||||||
|
base = flash - SLOT
|
||||||
|
word_flash = flash > 0x10000
|
||||||
|
wire_base = base // 2 if word_flash else base
|
||||||
|
flags = (1 if patch else 0) | (2 if word_flash else 0)
|
||||||
|
raw12 = bytes((ord("P"), ord("B"), version, *signature, page & 0xFF,
|
||||||
|
wire_base & 0xFF, wire_base >> 8, eeprom & 0xFF, eeprom >> 8, flags))
|
||||||
|
return cls(raw12)
|
||||||
|
|
||||||
def __init__(self, raw):
|
def __init__(self, raw):
|
||||||
if len(raw) != 12 or raw[0:2] != b"PB":
|
if len(raw) != 12 or raw[0:2] != b"PB":
|
||||||
raise Error(f"bad info block: {raw.hex()}")
|
raise Error(f"bad info block: {raw.hex()}")
|
||||||
@@ -314,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
|
||||||
@@ -345,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}",
|
||||||
@@ -362,40 +454,70 @@ class Loader:
|
|||||||
def __init__(self, port):
|
def __init__(self, port):
|
||||||
self.port = port
|
self.port = port
|
||||||
self.info = None
|
self.info = None
|
||||||
|
# Set once a session is established over an autobaud link, so a
|
||||||
|
# re-entry after 'J' repeats the handshake that worked.
|
||||||
|
self.autobaud = False
|
||||||
|
|
||||||
def connect(self, wait):
|
def _read_identity(self):
|
||||||
"""Knock until the info block comes back. The block is what proves the
|
"""The 'b' reply, in either of the two layouts a loader may send.
|
||||||
loader is listening — a prompt byte alone does not, since one left over
|
pureboot 5 answers with its version and the signature; older loaders
|
||||||
from a previous session can still be in the pipeline while the port
|
answer with a 12-byte block. The version byte cannot be mistaken for
|
||||||
opening resets the device into a fresh activation window, where a
|
the older block's 'P', so four bytes are enough to tell them apart."""
|
||||||
command without its knock is discarded. Each attempt is therefore the
|
head = self.port.read_exact(4, 2.0)
|
||||||
whole handshake, retried until it produces the block or the window
|
if head[0:2] == b"PB":
|
||||||
closes. Also converges into a live session: the knock bytes are ignored
|
return Info(head + self.port.read_exact(8, 2.0))
|
||||||
there and the drain absorbs whatever they produced."""
|
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
|
deadline = time.monotonic() + wait
|
||||||
knocks = 0
|
knocks = 0
|
||||||
while True:
|
while True:
|
||||||
self.port.flush_input()
|
self.port.flush_input()
|
||||||
self.port.write(b"pb")
|
self.port.write(knock)
|
||||||
knocks += 1
|
knocks += 1
|
||||||
if PROMPT in self.port.read_available(0.4):
|
if PROMPT in self.port.read_available(0.4):
|
||||||
while self.port.read_available(0.3):
|
while self.port.read_available(0.3):
|
||||||
pass
|
pass
|
||||||
self.port.write(b"b")
|
self.port.write(b"b")
|
||||||
try:
|
try:
|
||||||
block = self.port.read_exact(12, 2.0)
|
# A version the tool cannot speak is the loader's own
|
||||||
except Error:
|
# answer, not a failed knock: Info reports it rather than
|
||||||
block = b""
|
# sending the tool round the loop again.
|
||||||
# A version the tool cannot speak is the loader's own answer,
|
self.info = self._read_identity()
|
||||||
# not a failed knock: Info reports it rather than retrying.
|
except Error as failed:
|
||||||
if block[0:2] == b"PB":
|
if "pureboot" in str(failed):
|
||||||
self.info = Info(block)
|
raise
|
||||||
|
self.info = None
|
||||||
|
if self.info is not None:
|
||||||
self._expect_prompt()
|
self._expect_prompt()
|
||||||
verbose(f"loader answered knock {knocks}; info block read")
|
verbose(f"loader answered {what} {knocks}; identity read")
|
||||||
return self.info
|
return self.info
|
||||||
if time.monotonic() > deadline:
|
if time.monotonic() > deadline:
|
||||||
raise Error("no answer — reset the device within its activation window")
|
raise Error("no answer — reset the device within its activation window")
|
||||||
|
|
||||||
|
def connect(self, wait):
|
||||||
|
"""Knock 'p' then 'b' and read the identity."""
|
||||||
|
return self._handshake(wait, b"pb", "knock")
|
||||||
|
|
||||||
|
def connect_autobaud(self, wait):
|
||||||
|
"""The autobaud handshake. In place of the p+b knock the host sends the
|
||||||
|
calibration pulse — one seven-bit-time low pulse at the host's chosen
|
||||||
|
baud, which the loader times into its per-bit unit — then a single 'p'
|
||||||
|
the loader decodes at the rate it just measured. A lost pulse, or a
|
||||||
|
knock landing while the loader is mid-frame, simply fails to answer and
|
||||||
|
leaves the measurement loop waiting for the next pulse, so the retry in
|
||||||
|
_handshake covers it."""
|
||||||
|
self.autobaud = True
|
||||||
|
return self._handshake(wait, bytes((CALIBRATE, ord("p"))), "calibration")
|
||||||
|
|
||||||
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)
|
||||||
if byte != PROMPT:
|
if byte != PROMPT:
|
||||||
@@ -418,7 +540,58 @@ class Loader:
|
|||||||
count -= chunk
|
count -= chunk
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unified(self):
|
||||||
|
"""pureboot 5 and later: one 'G'/'g' pair over selector-named spaces."""
|
||||||
|
return self.info is not None and self.info.version >= UNIFIED_LOADER
|
||||||
|
|
||||||
|
def _read_space(self, space, address, count):
|
||||||
|
"""A run out of any space, chunked to 256 bytes and to bank bounds."""
|
||||||
|
data = b""
|
||||||
|
while count:
|
||||||
|
chunk = min(count, 256, 0x10000 - (address & 0xFFFF))
|
||||||
|
head = bytes((ord("G"), selector(space, address), address & 0xFF,
|
||||||
|
(address >> 8) & 0xFF, chunk & 0xFF))
|
||||||
|
data += self._command(head, chunk, 5.0)
|
||||||
|
address += chunk
|
||||||
|
count -= chunk
|
||||||
|
return data
|
||||||
|
|
||||||
|
def _write_space(self, space, address, data, progress=None):
|
||||||
|
"""A run into any space. Each byte is acked as its write begins — an
|
||||||
|
EEPROM cell and an SPM operation both need that pacing, and the ack is
|
||||||
|
what the loader sends in place of a completion status."""
|
||||||
|
offset = 0
|
||||||
|
while offset < len(data):
|
||||||
|
chunk = data[offset : offset + min(256, 0x10000 - (address & 0xFFFF))]
|
||||||
|
head = bytes((ord("g"), selector(space, address), address & 0xFF,
|
||||||
|
(address >> 8) & 0xFF, len(chunk) & 0xFF))
|
||||||
|
self.port.write(head)
|
||||||
|
for byte in chunk:
|
||||||
|
self.port.write(bytes((byte,)))
|
||||||
|
self._expect_prompt()
|
||||||
|
if progress:
|
||||||
|
progress.step()
|
||||||
|
self._expect_prompt() # the next command prompt
|
||||||
|
address += len(chunk)
|
||||||
|
offset += len(chunk)
|
||||||
|
|
||||||
|
def spm(self, operation, address):
|
||||||
|
"""One SPM operation at a flash address — the erase, write and RWW
|
||||||
|
re-enable that pureboot 4 ran inside 'W' and pureboot 5 leaves here."""
|
||||||
|
self._write_space(SP_SPM, address, bytes((operation,)))
|
||||||
|
|
||||||
|
def read_ram(self, address, count):
|
||||||
|
"""Data space: SRAM, and with it the register file and every I/O
|
||||||
|
register, which share the address space on AVR. New in pureboot 5."""
|
||||||
|
return self._read_space(SP_RAM, address, count)
|
||||||
|
|
||||||
|
def write_ram(self, address, data):
|
||||||
|
self._write_space(SP_RAM, address, data)
|
||||||
|
|
||||||
def read_flash(self, address, count):
|
def read_flash(self, address, count):
|
||||||
|
if self.unified:
|
||||||
|
return self._read_space(SP_FLASH, address, count)
|
||||||
if not self.info.word_flash:
|
if not self.info.word_flash:
|
||||||
return self._stream_read("R", address, count)
|
return self._stream_read("R", address, count)
|
||||||
# Word-addressed wire: widen to even bounds and never let one read
|
# Word-addressed wire: widen to even bounds and never let one read
|
||||||
@@ -436,15 +609,32 @@ class Loader:
|
|||||||
return data[address - start : address - start + count]
|
return data[address - start : address - start + count]
|
||||||
|
|
||||||
def read_eeprom(self, address, count):
|
def read_eeprom(self, address, count):
|
||||||
|
if self.unified:
|
||||||
|
return self._read_space(SP_EEPROM, address, count)
|
||||||
return self._stream_read("r", address, count)
|
return self._stream_read("r", address, count)
|
||||||
|
|
||||||
def write_page(self, address, data):
|
def write_page(self, address, data):
|
||||||
assert len(data) == self.info.page and address % self.info.page == 0
|
assert len(data) == self.info.page and address % self.info.page == 0
|
||||||
|
if self.unified:
|
||||||
|
# 'W' fills the page buffer and stops there; the erase and the write
|
||||||
|
# are host-issued SPM operations. Only a chip with a boot section
|
||||||
|
# has RWW to re-enable — on the others bit 4 of SPMCSR means
|
||||||
|
# something else entirely, so it must not be sent.
|
||||||
|
head = bytes((ord("W"), selector(SP_FLASH, address), address & 0xFF, (address >> 8) & 0xFF))
|
||||||
|
self._command(head + data, 0, 2.0)
|
||||||
|
self.spm(SPM_ERASE, address)
|
||||||
|
self.spm(SPM_WRITE, address)
|
||||||
|
if not self.info.patch_vector:
|
||||||
|
self.spm(SPM_RWWSRE, address)
|
||||||
|
return
|
||||||
wire = address // (2 if self.info.word_flash else 1)
|
wire = address // (2 if self.info.word_flash else 1)
|
||||||
head = bytes((ord("W"), wire & 0xFF, wire >> 8))
|
head = bytes((ord("W"), wire & 0xFF, wire >> 8))
|
||||||
self._command(head + data, 0, 2.0)
|
self._command(head + data, 0, 2.0)
|
||||||
|
|
||||||
def write_eeprom(self, address, data, progress=None):
|
def write_eeprom(self, address, data, progress=None):
|
||||||
|
if self.unified:
|
||||||
|
self._write_space(SP_EEPROM, address, data, progress)
|
||||||
|
return
|
||||||
offset = 0
|
offset = 0
|
||||||
while offset < len(data):
|
while offset < len(data):
|
||||||
chunk = data[offset : offset + 256]
|
chunk = data[offset : offset + 256]
|
||||||
@@ -460,6 +650,8 @@ class Loader:
|
|||||||
offset += len(chunk)
|
offset += len(chunk)
|
||||||
|
|
||||||
def read_fuses(self):
|
def read_fuses(self):
|
||||||
|
if self.unified:
|
||||||
|
return self._read_space(SP_FUSE, 0, 4)
|
||||||
return self._command(b"F", 4, 2.0)
|
return self._command(b"F", 4, 2.0)
|
||||||
|
|
||||||
def jump(self, word_address):
|
def jump(self, word_address):
|
||||||
@@ -470,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)
|
||||||
@@ -638,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
|
||||||
|
|
||||||
@@ -833,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:
|
||||||
@@ -1016,6 +1226,37 @@ def op_read_eeprom(loader, path):
|
|||||||
print(f"read EEPROM: {len(data)} B -> {path}")
|
print(f"read EEPROM: {len(data)} B -> {path}")
|
||||||
|
|
||||||
|
|
||||||
|
def _require_unified(loader, what):
|
||||||
|
if not loader.unified:
|
||||||
|
raise Error(f"{what} needs pureboot {UNIFIED_LOADER} or later; this loader is {loader.info.version}")
|
||||||
|
|
||||||
|
|
||||||
|
def _peek_spec(spec):
|
||||||
|
"""ADDR[:N] — addresses and counts in any Python integer base."""
|
||||||
|
address, _, count = spec.partition(":")
|
||||||
|
return int(address, 0), int(count, 0) if count else 1
|
||||||
|
|
||||||
|
|
||||||
|
def op_peek(loader, spec):
|
||||||
|
_require_unified(loader, "--peek")
|
||||||
|
address, count = _peek_spec(spec)
|
||||||
|
data = loader.read_ram(address, count)
|
||||||
|
for offset in range(0, len(data), 16):
|
||||||
|
row = data[offset : offset + 16]
|
||||||
|
text = "".join(chr(b) if 0x20 <= b < 0x7F else "." for b in row)
|
||||||
|
print(f"{address + offset:#06x} {row.hex(' '):<47} {text}")
|
||||||
|
|
||||||
|
|
||||||
|
def op_poke(loader, spec):
|
||||||
|
_require_unified(loader, "--poke")
|
||||||
|
address, _, payload = spec.partition(":")
|
||||||
|
if not payload:
|
||||||
|
raise Error("--poke needs ADDR:HEX, for example 0x200:deadbeef")
|
||||||
|
data = bytes.fromhex(payload.replace(" ", ""))
|
||||||
|
loader.write_ram(int(address, 0), data)
|
||||||
|
print(f"poke: {len(data)} B at {int(address, 0):#06x}")
|
||||||
|
|
||||||
|
|
||||||
def op_fuses(loader):
|
def op_fuses(loader):
|
||||||
low, lock, extended, high = loader.read_fuses()
|
low, lock, extended, high = loader.read_fuses()
|
||||||
print("fuses:")
|
print("fuses:")
|
||||||
@@ -1049,6 +1290,9 @@ def main():
|
|||||||
parser.add_argument("--port", required=True, help="serial device: COM6, /dev/ttyUSB0, or a simavr pty")
|
parser.add_argument("--port", required=True, help="serial device: COM6, /dev/ttyUSB0, or a simavr pty")
|
||||||
parser.add_argument("--baud", type=int, default=115200, help="115200 mega, 57600 tinies")
|
parser.add_argument("--baud", type=int, default=115200, help="115200 mega, 57600 tinies")
|
||||||
parser.add_argument("--wait", type=float, default=30.0, help="seconds to keep knocking")
|
parser.add_argument("--wait", type=float, default=30.0, help="seconds to keep knocking")
|
||||||
|
parser.add_argument("--autobaud", action="store_true",
|
||||||
|
help="drive an autobaud loader: send the 0xC0 calibration pulse and a single "
|
||||||
|
"knock, and take geometry from the signature (no clock/baud baked in)")
|
||||||
parser.add_argument("--info", action="store_true", help="print the device info block")
|
parser.add_argument("--info", action="store_true", help="print the device info block")
|
||||||
parser.add_argument("--fuses", action="store_true", help="read the fuse and lock bytes")
|
parser.add_argument("--fuses", action="store_true", help="read the fuse and lock bytes")
|
||||||
parser.add_argument("--update-loader", metavar="FILE", help="replace the loader with this pureboot binary")
|
parser.add_argument("--update-loader", metavar="FILE", help="replace the loader with this pureboot binary")
|
||||||
@@ -1064,6 +1308,10 @@ def main():
|
|||||||
parser.add_argument("--eeprom", metavar="FILE", help="program the EEPROM (bin or ihex)")
|
parser.add_argument("--eeprom", metavar="FILE", help="program the EEPROM (bin or ihex)")
|
||||||
parser.add_argument("--read-eeprom", metavar="FILE", help="dump the EEPROM")
|
parser.add_argument("--read-eeprom", metavar="FILE", help="dump the EEPROM")
|
||||||
parser.add_argument("--verify-eeprom", metavar="FILE", help="compare EEPROM against an image")
|
parser.add_argument("--verify-eeprom", metavar="FILE", help="compare EEPROM against an image")
|
||||||
|
parser.add_argument("--peek", metavar="ADDR[:N]", help="read N bytes of data space (SRAM, registers, "
|
||||||
|
"I/O) — pureboot 5 and later")
|
||||||
|
parser.add_argument("--poke", metavar="ADDR:HEX", help="write hex bytes into data space — "
|
||||||
|
"pureboot 5 and later")
|
||||||
parser.add_argument("--force", action="store_true", help="override refusable safety checks")
|
parser.add_argument("--force", action="store_true", help="override refusable safety checks")
|
||||||
parser.add_argument("--stay", action="store_true", help="leave the loader in its session")
|
parser.add_argument("--stay", action="store_true", help="leave the loader in its session")
|
||||||
parser.add_argument("-v", "--verbose", action="store_true",
|
parser.add_argument("-v", "--verbose", action="store_true",
|
||||||
@@ -1086,7 +1334,7 @@ def main():
|
|||||||
verbose(f"{args.port}: {args.baud} Bd 8N1, DTR/RTS asserted")
|
verbose(f"{args.port}: {args.baud} Bd 8N1, DTR/RTS asserted")
|
||||||
try:
|
try:
|
||||||
loader = Loader(port)
|
loader = Loader(port)
|
||||||
info = loader.connect(args.wait)
|
info = loader.connect_autobaud(args.wait) if args.autobaud else loader.connect(args.wait)
|
||||||
if args.info:
|
if args.info:
|
||||||
print("device:")
|
print("device:")
|
||||||
for line in info.lines():
|
for line in info.lines():
|
||||||
@@ -1115,6 +1363,10 @@ def main():
|
|||||||
op_read_eeprom(loader, args.read_eeprom)
|
op_read_eeprom(loader, args.read_eeprom)
|
||||||
if args.verify_eeprom:
|
if args.verify_eeprom:
|
||||||
op_verify_eeprom(loader, args.verify_eeprom)
|
op_verify_eeprom(loader, args.verify_eeprom)
|
||||||
|
if args.poke:
|
||||||
|
op_poke(loader, args.poke)
|
||||||
|
if args.peek:
|
||||||
|
op_peek(loader, args.peek)
|
||||||
if args.stay:
|
if args.stay:
|
||||||
print("loader stays in its session (reset to leave)")
|
print("loader stays in its session (reset to leave)")
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -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__":
|
||||||
|
|||||||
154
test/pbautobaud.py
Normal file
154
test/pbautobaud.py
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""End-to-end autobaud test: drive an autobaud loader in simavr through the
|
||||||
|
calibration handshake and a flash + EEPROM + fuse round-trip, cross-checked
|
||||||
|
against the simulator's ground-truth memory — then repeat at a second F_CPU with
|
||||||
|
the *same* loader binary, which is the property autobaud exists for: one
|
||||||
|
clock-agnostic image that locks onto whatever rate the host sends.
|
||||||
|
|
||||||
|
Usage: pbautobaud.py <device_bin> <loader_elf> <mcu> <base_hex> <page>
|
||||||
|
<app_bin> <app_hz> <app_baud> <tool_py> <workdir>
|
||||||
|
|
||||||
|
The loader is a software-serial build on PB0/PB1 (pureboot_add_autobaud's
|
||||||
|
default), so the runner drives it over the GPIO⇄pty bridge (-l sw:B0,B1). The
|
||||||
|
app fixture is built for (app_hz, app_baud); the hand-over is checked at that
|
||||||
|
point, and a second point at half the clock proves the lock is measured, not
|
||||||
|
baked in.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
def fail(message):
|
||||||
|
print(f"FAIL: {message}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
(device_bin, elf, mcu, base_hex, page, app_bin, app_hz, app_baud, tool, workdir) = sys.argv[1:]
|
||||||
|
base, page, app_hz, app_baud = int(base_hex, 0), int(page), int(app_hz), int(app_baud)
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
import pbsim
|
||||||
|
import pureboot as pb
|
||||||
|
|
||||||
|
os.makedirs(workdir, exist_ok=True)
|
||||||
|
ee_image = bytes(range(0xA0, 0xB0))
|
||||||
|
ee_path = os.path.join(workdir, "ee.bin")
|
||||||
|
open(ee_path, "wb").write(ee_image)
|
||||||
|
|
||||||
|
# The geometry the surgery planner needs, from the chip class the runner is
|
||||||
|
# told — the same derivation pbtest.py makes: the boot-sectioned megas need
|
||||||
|
# no vector surgery, the tinies and the boot-section-less m48s do, and the
|
||||||
|
# large chips speak word addresses.
|
||||||
|
mega = mcu.startswith("atmega")
|
||||||
|
patch = not mega or mcu.startswith("atmega48")
|
||||||
|
word_flash = base + pb.SLOT > 0x10000
|
||||||
|
wire_base = base // 2 if word_flash else base
|
||||||
|
flags = (1 if patch else 0) | (2 if word_flash else 0)
|
||||||
|
ground_truth = pb.Info(bytes([ord("P"), ord("B"), pb.NEWEST_LOADER, 0, 0, 0, page & 0xFF,
|
||||||
|
wire_base & 0xFF, wire_base >> 8, 0, 0, flags]))
|
||||||
|
|
||||||
|
def round_trip(hz, baud, label, hand_over):
|
||||||
|
"""One clock point: reset, calibrate + knock, program, verify against the
|
||||||
|
simulator's own flash, and (at the app's point) hand over to the fixture."""
|
||||||
|
dump = os.path.join(workdir, f"flash_{label}.bin")
|
||||||
|
device = pbsim.Device(device_bin, elf, mcu, str(hz), base_hex, page, baud, dump, link="sw:B0,B1")
|
||||||
|
try:
|
||||||
|
# The host tool, in autobaud mode, sends the 0xC0 calibration pulse
|
||||||
|
# and a single knock at `baud`; the loader locks to it.
|
||||||
|
out = pbsim.run_tool(tool, device.pty, baud, "--autobaud", "--info", "--fuses",
|
||||||
|
"--flash", app_bin, "--eeprom", ee_path, "--stay")
|
||||||
|
for needed in ("version", "signature", "fuses", "verify:", "stays"):
|
||||||
|
if needed not in out:
|
||||||
|
fail(f"{label}: session output lacks {needed!r}\n{out}")
|
||||||
|
# Read both memories back over the locked link and check them.
|
||||||
|
read_flash = os.path.join(workdir, f"rf_{label}.bin")
|
||||||
|
read_eeprom = os.path.join(workdir, f"re_{label}.bin")
|
||||||
|
out = pbsim.run_tool(tool, device.pty, baud, "--autobaud", "--verify-flash", app_bin,
|
||||||
|
"--verify-eeprom", ee_path, "--read-flash", read_flash,
|
||||||
|
"--read-eeprom", read_eeprom, "--stay")
|
||||||
|
if out.count("verify:") != 2:
|
||||||
|
fail(f"{label}: did not verify both memories\n{out}")
|
||||||
|
if open(read_eeprom, "rb").read()[: len(ee_image)] != ee_image:
|
||||||
|
fail(f"{label}: EEPROM read-back mismatch")
|
||||||
|
|
||||||
|
if hand_over:
|
||||||
|
# Regression: a calibration pulse with no knock behind it must
|
||||||
|
# not wedge the loader. The knock's edge wait used to be
|
||||||
|
# unbudgeted, so one stray low pulse — EMI, or a host that opens
|
||||||
|
# the port and never knocks — held the loader forever and the
|
||||||
|
# application never ran. The whole activation is bounded now, so
|
||||||
|
# the window closes and the app boots; the banner is the proof.
|
||||||
|
# (The pause lets the loader reach its measurement loop, so the
|
||||||
|
# pulse is genuinely seen and the test cannot pass vacuously.)
|
||||||
|
device.reset()
|
||||||
|
port = pb.Port(device.pty, baud)
|
||||||
|
try:
|
||||||
|
time.sleep(0.2)
|
||||||
|
port.write(bytes((pb.CALIBRATE,)))
|
||||||
|
# Accumulate rather than match exactly: the reset leaves the
|
||||||
|
# idle line a framing artefact ahead of the banner, which is
|
||||||
|
# noise here — the question is only whether the app ran.
|
||||||
|
seen = b""
|
||||||
|
deadline = time.monotonic() + 180.0
|
||||||
|
while b"APP" not in seen and time.monotonic() < deadline:
|
||||||
|
seen += port.read_available(1.0)
|
||||||
|
if b"APP" not in seen:
|
||||||
|
fail(f"{label}: lone calibration pulse wedged the loader — app never bannered, saw {seen!r}")
|
||||||
|
print(f" {label}: lone calibration pulse does not wedge the loader")
|
||||||
|
finally:
|
||||||
|
port.close()
|
||||||
|
|
||||||
|
device.reset()
|
||||||
|
port = pb.Port(device.pty, baud)
|
||||||
|
try:
|
||||||
|
loader = pb.Loader(port)
|
||||||
|
live = loader.connect_autobaud(15)
|
||||||
|
if not pb.OLDEST_LOADER <= live.version <= pb.NEWEST_LOADER:
|
||||||
|
fail(f"{label}: loader reports pureboot {live.version}")
|
||||||
|
if loader.unified:
|
||||||
|
# pureboot 5's data space. 0x0200 is clear of the
|
||||||
|
# loader's own .noinit unit at the bottom of SRAM and of
|
||||||
|
# the stack at the top. Reading it back over the same
|
||||||
|
# locked link proves both directions of the new space.
|
||||||
|
probe = bytes(range(0x30, 0x40))
|
||||||
|
loader.write_ram(0x0200, probe)
|
||||||
|
if loader.read_ram(0x0200, len(probe)) != probe:
|
||||||
|
fail(f"{label}: RAM round-trip mismatch")
|
||||||
|
# The register file and the I/O space share the data
|
||||||
|
# address space on AVR, so the same command reaches a
|
||||||
|
# peripheral register. SPMCSR reads back as idle here.
|
||||||
|
verbose_ram = loader.read_ram(0x0200, 4)
|
||||||
|
print(f" {label}: RAM read/write ok ({verbose_ram.hex()})")
|
||||||
|
loader.run_application()
|
||||||
|
banner = port.read_exact(3, 5.0)
|
||||||
|
if banner != b"APP":
|
||||||
|
fail(f"{label}: application banner was {banner!r}")
|
||||||
|
finally:
|
||||||
|
port.close()
|
||||||
|
finally:
|
||||||
|
device.stop()
|
||||||
|
|
||||||
|
# Ground truth (read after the runner exits and writes its dump): what
|
||||||
|
# the tool programmed must be what the simulator actually holds.
|
||||||
|
pages = pb.plan_flash(open(app_bin, "rb").read(), ground_truth)
|
||||||
|
flash_true = open(dump, "rb").read()
|
||||||
|
for address, data in pages.items():
|
||||||
|
if flash_true[address : address + page] != data:
|
||||||
|
fail(f"{label}: simulator flash differs from the programmed image at {address:#06x}")
|
||||||
|
print(f" {label}: locked at {hz} Hz / {baud} Bd, flash+EEPROM verified"
|
||||||
|
+ (", hand-over ok" if hand_over else ""))
|
||||||
|
|
||||||
|
# The app fixture is built for one clock; the hand-over banners there. A
|
||||||
|
# second point at double that clock, same loader binary, proves the lock is
|
||||||
|
# measured, not baked in — the whole point of autobaud. (Doubling keeps the
|
||||||
|
# bit period healthy; halving would drop it below the software UART's floor.)
|
||||||
|
round_trip(app_hz, app_baud, "clock-a", hand_over=True)
|
||||||
|
round_trip(app_hz * 2, app_baud, "clock-b", hand_over=False)
|
||||||
|
print("pbautobaud: calibration lock and flash/EEPROM/fuse round-trip pass at both clocks")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
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:
|
||||||
@@ -100,19 +114,26 @@ def main():
|
|||||||
try:
|
try:
|
||||||
loader = pb.Loader(port)
|
loader = pb.Loader(port)
|
||||||
live = loader.connect(15)
|
live = loader.connect(15)
|
||||||
# The loader built from this tree and the tool beside it must
|
# The loader built from this tree must report a version the tool
|
||||||
# agree on where the version numbering stands: a bump the tool
|
# beside it speaks — a bump the tool was never told about is a
|
||||||
# was never told about is a loader it would refuse to speak to.
|
# loader it would refuse to talk to. Not equality with the newest:
|
||||||
if live.version != pb.NEWEST_LOADER:
|
# the tool now spans two loader generations, the fixed-baud one
|
||||||
fail(f"loader reports pureboot {live.version}, the tool's newest is {pb.NEWEST_LOADER}")
|
# here and the unified autobaud loader that follows it.
|
||||||
|
if not pb.OLDEST_LOADER <= live.version <= pb.NEWEST_LOADER:
|
||||||
|
fail(f"loader reports pureboot {live.version}, the tool speaks "
|
||||||
|
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