Compare commits
48 Commits
autobaud
...
0cb83ff36f
| Author | SHA1 | Date | |
|---|---|---|---|
| 0cb83ff36f | |||
| 8aa1721709 | |||
| 2655d058f4 | |||
| e9c3897d4f | |||
| 546b1589a3 | |||
| eb213e1025 | |||
| 3e4bfbaf48 | |||
| 579ca81b27 | |||
| 5d520a1ff9 | |||
| f71d76a815 | |||
| 47419400f6 | |||
| bad8b6b43e | |||
| 5d1b4497d4 | |||
| a743ea64a3 | |||
| aa66cfccff | |||
| 459d463283 | |||
| 8e7cc86fb3 | |||
| c8ac61779e | |||
| 4362886c39 | |||
| 0513d07e87 | |||
| d4ab28aa17 | |||
| b60f182105 | |||
| 5bc35a9733 | |||
| 8f6319c068 | |||
| a3ea099105 | |||
| c535c4756c | |||
| 321ff8a4ee | |||
| 531ae6c8dc | |||
| b3f41caf6e | |||
| 7716e1e291 | |||
| 1b18f10f4e | |||
| 52c4cdab32 | |||
| 69f089e53a | |||
| fe0d9f8790 | |||
| 3ce817ea03 | |||
| af0dd15a77 | |||
| a54075e526 | |||
| aec430c2e1 | |||
| 9a8a5b0082 | |||
| 7702c6b700 | |||
| 77dd45aeca | |||
| 433bec3e58 | |||
| e89000f73e | |||
| b0737f7cc0 | |||
| 07f93caba8 | |||
| 45f10f843a | |||
| 34b47048ca | |||
| 392035923f |
394
CMakeLists.txt
394
CMakeLists.txt
@@ -2,21 +2,19 @@ cmake_minimum_required(VERSION 3.28)
|
|||||||
|
|
||||||
project(tsb_libavr LANGUAGES CXX)
|
project(tsb_libavr LANGUAGES CXX)
|
||||||
|
|
||||||
# libavr from a local checkout (LIBAVR_ROOT) or the forge; the toolchain file
|
# libavr rides as the pinned submodule; LIBAVR_ROOT (cache or environment)
|
||||||
# comes from the same checkout via CMakePresets.json.
|
# overrides it for tandem development against a working tree. The toolchain
|
||||||
include(FetchContent)
|
# file comes from the submodule via CMakePresets.json either way.
|
||||||
if(NOT LIBAVR_ROOT AND DEFINED ENV{LIBAVR_ROOT})
|
if(NOT LIBAVR_ROOT AND DEFINED ENV{LIBAVR_ROOT})
|
||||||
set(LIBAVR_ROOT $ENV{LIBAVR_ROOT})
|
set(LIBAVR_ROOT $ENV{LIBAVR_ROOT})
|
||||||
endif()
|
endif()
|
||||||
if(NOT LIBAVR_ROOT)
|
if(NOT LIBAVR_ROOT)
|
||||||
set(LIBAVR_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/libavr)
|
set(LIBAVR_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/libavr)
|
||||||
endif()
|
endif()
|
||||||
if(LIBAVR_ROOT)
|
if(NOT EXISTS ${LIBAVR_ROOT}/CMakeLists.txt)
|
||||||
FetchContent_Declare(libavr SOURCE_DIR ${LIBAVR_ROOT})
|
message(FATAL_ERROR "libavr not found at ${LIBAVR_ROOT} — run: git submodule update --init libavr")
|
||||||
else()
|
|
||||||
FetchContent_Declare(libavr GIT_REPOSITORY git@git.blackmark.me:avr/libavr.git GIT_TAG main)
|
|
||||||
endif()
|
endif()
|
||||||
FetchContent_MakeAvailable(libavr)
|
add_subdirectory(${LIBAVR_ROOT} libavr-build)
|
||||||
|
|
||||||
if(PROJECT_IS_TOP_LEVEL)
|
if(PROJECT_IS_TOP_LEVEL)
|
||||||
add_compile_options(-Werror) # warnings are errors for the port's own code
|
add_compile_options(-Werror) # warnings are errors for the port's own code
|
||||||
@@ -24,15 +22,17 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
|
|
||||||
# The behavioral tests drive the real wire protocols over a simavr pty
|
# The behavioral tests drive the real wire protocols over a simavr pty
|
||||||
# (as the host tools do) and actually flash the device. The runners are
|
# (as the host tools do) and actually flash the device. The runners are
|
||||||
# host programs built at configure time against libsimavr; if they or
|
# host programs built at configure time against libsimavr (C++23 — what
|
||||||
# Python are missing, only the size tests run.
|
# the distribution's compiler speaks in full); if they or Python are
|
||||||
find_program(_host_cc NAMES cc gcc)
|
# missing, only the size tests run.
|
||||||
|
find_program(_host_cxx NAMES c++ g++)
|
||||||
find_package(Python3 COMPONENTS Interpreter)
|
find_package(Python3 COMPONENTS Interpreter)
|
||||||
if(_host_cc AND Python3_FOUND)
|
if(_host_cxx AND Python3_FOUND)
|
||||||
set(PB_DEVICE ${CMAKE_BINARY_DIR}/pureboot_device)
|
set(PB_DEVICE ${CMAKE_BINARY_DIR}/pureboot_device)
|
||||||
execute_process(
|
execute_process(
|
||||||
COMMAND ${_host_cc} -O2 -I/usr/include/simavr -I/usr/include/simavr/parts
|
COMMAND ${_host_cxx} -std=c++23 -Wall -Wextra -O2
|
||||||
-o ${PB_DEVICE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pureboot_device.c
|
-I/usr/include/simavr -I/usr/include/simavr/parts
|
||||||
|
-o ${PB_DEVICE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pureboot_device.cpp
|
||||||
-lsimavr -lsimavrparts -lelf -lutil
|
-lsimavr -lsimavrparts -lelf -lutil
|
||||||
RESULT_VARIABLE _pbdev_res ERROR_VARIABLE _pbdev_err)
|
RESULT_VARIABLE _pbdev_res ERROR_VARIABLE _pbdev_err)
|
||||||
if(NOT _pbdev_res EQUAL 0)
|
if(NOT _pbdev_res EQUAL 0)
|
||||||
@@ -42,8 +42,9 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
if(LIBAVR_MCU STREQUAL "atmega328p")
|
if(LIBAVR_MCU STREQUAL "atmega328p")
|
||||||
set(TSB_DEVICE ${CMAKE_BINARY_DIR}/tsb_device)
|
set(TSB_DEVICE ${CMAKE_BINARY_DIR}/tsb_device)
|
||||||
execute_process(
|
execute_process(
|
||||||
COMMAND ${_host_cc} -O2 -I/usr/include/simavr -I/usr/include/simavr/parts
|
COMMAND ${_host_cxx} -std=c++23 -Wall -Wextra -O2
|
||||||
-o ${TSB_DEVICE} ${CMAKE_CURRENT_SOURCE_DIR}/test/device.c
|
-I/usr/include/simavr -I/usr/include/simavr/parts
|
||||||
|
-o ${TSB_DEVICE} ${CMAKE_CURRENT_SOURCE_DIR}/test/device.cpp
|
||||||
-lsimavr -lsimavrparts -lelf
|
-lsimavr -lsimavrparts -lelf
|
||||||
RESULT_VARIABLE _dev_res ERROR_VARIABLE _dev_err)
|
RESULT_VARIABLE _dev_res ERROR_VARIABLE _dev_err)
|
||||||
if(NOT _dev_res EQUAL 0)
|
if(NOT _dev_res EQUAL 0)
|
||||||
@@ -68,16 +69,18 @@ function(add_image_outputs name)
|
|||||||
$<TARGET_FILE:${name}> $<TARGET_FILE:${name}>.bin)
|
$<TARGET_FILE:${name}> $<TARGET_FILE:${name}>.bin)
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
# The TinySafeBoot protocol reimplemented on libavr in three variants that trade
|
# The TinySafeBoot protocol reimplemented on libavr in variants that trade
|
||||||
# clarity for size. Each links into the ATmega328P boot section (BOOTSZ selects
|
# clarity for size. Each links into the ATmega328P boot section (BOOTSZ selects
|
||||||
# its size; BOOTRST vectors a reset to its base) with -nostartfiles — a polled
|
# its size; BOOTRST vectors a reset to its base) with -nostartfiles — a polled
|
||||||
# loader has no use for the crt or the vector table. The naked entry sits in
|
# loader has no use for the crt or the vector table. The entry sits in
|
||||||
# .vectors, laid first, and runs. The boot base is FLASHEND+1 minus the section
|
# .vectors, laid first, and runs — avr::startup::entry on the policy tier,
|
||||||
# size; the linker section-start and the source's boot_bytes agree. tsb_app is
|
# the experiment tiers' own naked stubs elsewhere, each documented in its
|
||||||
|
# source. The boot base is FLASHEND+1 minus the section size; the linker
|
||||||
|
# section-start and the source's boot_bytes agree. tsb_app is
|
||||||
# the application's reset vector, pinned to 0 here so the loaders jump to a
|
# the application's reset vector, pinned to 0 here so the loaders jump to a
|
||||||
# named function; --pmem-wrap-around lets relaxation turn that absolute jump
|
# named function; --pmem-wrap-around lets relaxation turn that absolute jump
|
||||||
# into the wrapped rjmp AVR's modulo-flash PC actually executes.
|
# into the wrapped rjmp AVR's modulo-flash PC actually executes.
|
||||||
# All three implement the full oracle feature set (see oracle/README.md):
|
# All four implement the full oracle feature set (see oracle/README.md):
|
||||||
# watchdog bail, one-wire half-duplex, config-page activation timeout, password
|
# watchdog bail, one-wire half-duplex, config-page activation timeout, password
|
||||||
# gate, emergency erase, config/flash/EEPROM read-write. They differ only in how,
|
# gate, emergency erase, config/flash/EEPROM read-write. They differ only in how,
|
||||||
# and the size gradient is the cost of that "how" — see dev/lessons.md.
|
# and the size gradient is the cost of that "how" — see dev/lessons.md.
|
||||||
@@ -97,6 +100,10 @@ endfunction()
|
|||||||
# tsb_pure — pure idiomatic libavr, one function per command, TU-local
|
# tsb_pure — pure idiomatic libavr, one function per command, TU-local
|
||||||
# (internal linkage), streaming (no SRAM page buffer): 836 B in
|
# (internal linkage), streaming (no SRAM page buffer): 836 B in
|
||||||
# the 1 KB section.
|
# the 1 KB section.
|
||||||
|
# tsb_policy — the policy floor: pureboot's rules (no asm, no register
|
||||||
|
# variables) with every pureboot lesson applied. 638 B in the
|
||||||
|
# 1 KB section — the measured evidence that the 512 B fit is a
|
||||||
|
# property of the mechanisms philosophy #5 bans.
|
||||||
#
|
#
|
||||||
# add_tsb_variant(<name> <boot-section-bytes>)
|
# add_tsb_variant(<name> <boot-section-bytes>)
|
||||||
function(add_tsb_variant name bytes)
|
function(add_tsb_variant name bytes)
|
||||||
@@ -124,8 +131,13 @@ endfunction()
|
|||||||
# chips build pureboot alone.
|
# chips build pureboot alone.
|
||||||
if(LIBAVR_MCU STREQUAL "atmega328p")
|
if(LIBAVR_MCU STREQUAL "atmega328p")
|
||||||
add_tsb_variant(tsb_asm 512)
|
add_tsb_variant(tsb_asm 512)
|
||||||
|
add_tsb_variant(tsb_policy 1024)
|
||||||
add_tsb_variant(tsb_pure 1024)
|
add_tsb_variant(tsb_pure 1024)
|
||||||
add_tsb_variant(tsb_tricks 1024)
|
add_tsb_variant(tsb_tricks 1024)
|
||||||
|
# The policy tier's floor is measured with the loop flags pureboot's size
|
||||||
|
# work found (a loader's loop bodies all contain calls); the other tiers
|
||||||
|
# keep the flag set their recorded floors were measured with — none.
|
||||||
|
target_compile_options(tsb_policy PRIVATE -fno-move-loop-invariants -fno-tree-ter)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# pureboot — the pure-constraint port (see pureboot/README.md): one source,
|
# pureboot — the pure-constraint port (see pureboot/README.md): one source,
|
||||||
@@ -160,6 +172,18 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
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)
|
||||||
|
add_test(NAME pureboot.scan
|
||||||
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/test_scan.py
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py)
|
||||||
|
# CMakePresets.json is generated; hand edits drift the moment the
|
||||||
|
# generator reruns, so the gate holds the pair together.
|
||||||
|
add_test(NAME presets.generated
|
||||||
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/make_presets.py
|
||||||
|
--check)
|
||||||
|
add_test(NAME pureboot.handshake
|
||||||
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/test_handshake.py)
|
||||||
|
add_test(NAME pureboot.updatelink
|
||||||
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/test_update_link.py)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# The protocol test flashes this fixture through the loader with the real
|
# The protocol test flashes this fixture through the loader with the real
|
||||||
@@ -178,6 +202,38 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
${CMAKE_BINARY_DIR}/pbtest-work)
|
${CMAKE_BINARY_DIR}/pbtest-work)
|
||||||
set_tests_properties(pureboot.protocol PROPERTIES TIMEOUT 180)
|
set_tests_properties(pureboot.protocol PROPERTIES TIMEOUT 180)
|
||||||
|
|
||||||
|
# The activation window as a measured duration: application installed,
|
||||||
|
# line idle, the first transmit is the application's banner — its
|
||||||
|
# cycle is the window the source declares, held to ±2 % (one
|
||||||
|
# mis-counted cycle per poll is a 10 % shift).
|
||||||
|
add_test(NAME pureboot.window
|
||||||
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbwindow.py
|
||||||
|
--device ${PB_DEVICE} --loader $<TARGET_FILE:pureboot>
|
||||||
|
--mcu ${PUREBOOT_SIM_MCU} --hz ${_pb_stock_hz}
|
||||||
|
--base ${PUREBOOT_BASE_HEX} --page ${PUREBOOT_PAGE}
|
||||||
|
--baud ${_pb_stock_baud} --app $<TARGET_FILE:pbapp>.bin
|
||||||
|
--seconds ${PUREBOOT_TIMEOUT}
|
||||||
|
--tool ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
|
--workdir ${CMAKE_BINARY_DIR}/pbwindow-work)
|
||||||
|
set_tests_properties(pureboot.window PROPERTIES TIMEOUT 300)
|
||||||
|
|
||||||
|
# The half-duplex loader's window, same gate: its poll runs through
|
||||||
|
# rx_ready()'s release-line test, whose outlined call re-shapes the
|
||||||
|
# whole loop — a per-class cycle count (poll_cost() in pureboot.cpp)
|
||||||
|
# that only the built image can prove, chip by chip.
|
||||||
|
if(PUREBOOT_HAS_USART)
|
||||||
|
add_test(NAME pureboot.window.halfduplex
|
||||||
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbwindow.py
|
||||||
|
--device ${PB_DEVICE} --loader $<TARGET_FILE:pureboot_hd>
|
||||||
|
--mcu ${PUREBOOT_SIM_MCU} --hz ${_pb_stock_hz}
|
||||||
|
--base ${PUREBOOT_BASE_HEX} --page ${PUREBOOT_PAGE}
|
||||||
|
--baud ${_pb_stock_baud} --app $<TARGET_FILE:pbapp>.bin
|
||||||
|
--seconds ${PUREBOOT_TIMEOUT}
|
||||||
|
--tool ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
|
--workdir ${CMAKE_BINARY_DIR}/pbwindow-hd-work)
|
||||||
|
set_tests_properties(pureboot.window.halfduplex PROPERTIES TIMEOUT 300)
|
||||||
|
endif()
|
||||||
|
|
||||||
# The position-independence acceptance test: the identical image,
|
# The position-independence acceptance test: the identical image,
|
||||||
# installed one slot lower, must serve the full command set.
|
# installed one slot lower, must serve the full command set.
|
||||||
add_test(NAME pureboot.reloc
|
add_test(NAME pureboot.reloc
|
||||||
@@ -189,6 +245,28 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
set_tests_properties(pureboot.reloc PROPERTIES TIMEOUT 180
|
set_tests_properties(pureboot.reloc PROPERTIES TIMEOUT 180
|
||||||
ENVIRONMENT "PB_OBJCOPY=${CMAKE_OBJCOPY}")
|
ENVIRONMENT "PB_OBJCOPY=${CMAKE_OBJCOPY}")
|
||||||
|
|
||||||
|
# The seal against the one command that proves it: erasing the page the
|
||||||
|
# loader runs from, refused unsealed and honoured sealed. Destroys the
|
||||||
|
# loader by design, so it gets a device of its own.
|
||||||
|
add_test(NAME pureboot.selfwrite
|
||||||
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbselfwrite.py
|
||||||
|
${PB_DEVICE} $<TARGET_FILE:pureboot> ${PUREBOOT_SIM_MCU} ${_pb_stock_hz}
|
||||||
|
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_pb_stock_baud}
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
|
${CMAKE_BINARY_DIR}/pbselfwrite-work)
|
||||||
|
set_tests_properties(pureboot.selfwrite PROPERTIES TIMEOUT 180)
|
||||||
|
|
||||||
|
# The seal against a link that damages bytes on purpose: every header
|
||||||
|
# field flipped after sealing must be refused, and the identical flip
|
||||||
|
# applied before sealing must be obeyed.
|
||||||
|
add_test(NAME pureboot.glitch
|
||||||
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbglitch.py
|
||||||
|
${PB_DEVICE} $<TARGET_FILE:pureboot> ${PUREBOOT_SIM_MCU} ${_pb_stock_hz}
|
||||||
|
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_pb_stock_baud}
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
|
${CMAKE_BINARY_DIR}/pbglitch-work)
|
||||||
|
set_tests_properties(pureboot.glitch PROPERTIES TIMEOUT 180)
|
||||||
|
|
||||||
# Entering the loader from a running application with no reset
|
# Entering the loader from a running application with no reset
|
||||||
# between, over a page buffer the application dirtied — the case the
|
# between, over a page buffer the application dirtied — the case the
|
||||||
# loader declines to guard and the host repairs. Hardware forbids the
|
# loader declines to guard and the host repairs. Hardware forbids the
|
||||||
@@ -238,12 +316,12 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
|
|
||||||
# The size matrix: every configuration axis that could move the image
|
# The size matrix: every configuration axis that could move the image
|
||||||
# size — the serial backend (different code), the USART instance
|
# size — the serial backend (different code), the USART instance
|
||||||
# (different registers), the clock (different constants), and the baud
|
# (different registers), the clock (different constants), the baud
|
||||||
# through the shapes its bit timing takes — each combination must still
|
# through the shapes its bit timing takes, and the pins through the one
|
||||||
# fit the chip's slot budget. Pins are size-neutral (port and bit are
|
# thing they decide (whether a bit-banged link has to release the USART
|
||||||
# immediate operands) and the timeout is a constant, so neither adds an
|
# that owns them) — each combination must still fit the chip's slot
|
||||||
# axis. The stock build is one point of this matrix and already has its
|
# budget. The timeout is a constant and adds no axis. The stock build is
|
||||||
# test.
|
# one point of this matrix and already has its test.
|
||||||
function(pureboot_size_variant name)
|
function(pureboot_size_variant name)
|
||||||
pureboot_add_loader(${name} ${ARGN})
|
pureboot_add_loader(${name} ${ARGN})
|
||||||
add_test(NAME ${name}.size
|
add_test(NAME ${name}.size
|
||||||
@@ -259,20 +337,50 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
add_test(NAME pureboot_autobaud.size
|
add_test(NAME pureboot_autobaud.size
|
||||||
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:pureboot_autobaud>
|
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:pureboot_autobaud>
|
||||||
-DLIMIT=${PUREBOOT_LIMIT} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
|
-DLIMIT=${PUREBOOT_LIMIT} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
|
||||||
|
# The measured unit's home is wire contract, not layout accident: the
|
||||||
|
# host reads the bit period from it (--info's measured clock). In the
|
||||||
|
# GPIOR home the image must carry no RAM copy at all; in the RAM home it
|
||||||
|
# is the loader's only RAM object, at the very start of SRAM.
|
||||||
|
add_test(NAME pureboot_autobaud.unit
|
||||||
|
COMMAND ${CMAKE_COMMAND} -DOBJDUMP=${CMAKE_OBJDUMP} -DELF=$<TARGET_FILE:pureboot_autobaud>
|
||||||
|
-DRAM_START=${PUREBOOT_RAM_START} -DGPIOR=${PUREBOOT_UNIT_GPIOR}
|
||||||
|
-P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_unit.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. `pins` is empty for the
|
||||||
# out here rather than aborting the configure.
|
# default pair, or the index of the USART whose own pins a bit-banged
|
||||||
function(pureboot_matrix_point hz baud link)
|
# link sits on. Unreachable rates drop out here rather than aborting the
|
||||||
|
# configure.
|
||||||
|
# The optional trailing argument is the one-wire shape of the same link:
|
||||||
|
# ONE_WIRE folds a software point onto its RX pin (the default, or the
|
||||||
|
# named USART's RXD), HALF_DUPLEX is the hardware USART's turn-around.
|
||||||
|
function(pureboot_matrix_point hz baud link pins)
|
||||||
|
set(_name pbm_${hz}_${baud}_${link})
|
||||||
if(link STREQUAL "software")
|
if(link STREQUAL "software")
|
||||||
pureboot_baud_feasible(${hz} ${baud} 1 _ok)
|
pureboot_baud_feasible(${hz} ${baud} 1 _ok)
|
||||||
set(_args SERIAL software)
|
set(_args SERIAL software)
|
||||||
|
if(NOT pins STREQUAL "")
|
||||||
|
list(APPEND _args RX ${PUREBOOT_USART${pins}_RX} TX ${PUREBOOT_USART${pins}_TX})
|
||||||
|
set(_name ${_name}_on${pins})
|
||||||
|
endif()
|
||||||
|
if(ARGC GREATER 4 AND ARGV4 STREQUAL "ONE_WIRE")
|
||||||
|
if(NOT pins STREQUAL "")
|
||||||
|
set(_args SERIAL software RX ${PUREBOOT_USART${pins}_RX} TX ${PUREBOOT_USART${pins}_RX})
|
||||||
|
else()
|
||||||
|
list(APPEND _args RX pb0 TX pb0)
|
||||||
|
endif()
|
||||||
|
set(_name ${_name}_1w)
|
||||||
|
endif()
|
||||||
else()
|
else()
|
||||||
pureboot_baud_feasible(${hz} ${baud} 0 _ok)
|
pureboot_baud_feasible(${hz} ${baud} 0 _ok)
|
||||||
set(_args USART ${link})
|
set(_args USART ${link})
|
||||||
|
if(ARGC GREATER 4 AND ARGV4 STREQUAL "HALF_DUPLEX")
|
||||||
|
list(APPEND _args HALF_DUPLEX)
|
||||||
|
set(_name ${_name}_hd)
|
||||||
|
endif()
|
||||||
endif()
|
endif()
|
||||||
if(_ok)
|
if(_ok)
|
||||||
pureboot_size_variant(pbm_${hz}_${baud}_${link} CLOCK ${hz} BAUD ${baud} ${_args})
|
pureboot_size_variant(${_name} CLOCK ${hz} BAUD ${baud} ${_args})
|
||||||
endif()
|
endif()
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
@@ -310,12 +418,19 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
if(DEFINED ENV{PUREBOOT_FULL_MATRIX})
|
if(DEFINED ENV{PUREBOOT_FULL_MATRIX})
|
||||||
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 "")
|
||||||
|
pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} software "" ONE_WIRE)
|
||||||
if(PUREBOOT_HAS_USART)
|
if(PUREBOOT_HAS_USART)
|
||||||
pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} 0)
|
pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} software 0)
|
||||||
|
pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} software 0 ONE_WIRE)
|
||||||
|
pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} 0 "")
|
||||||
|
pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} 0 "" HALF_DUPLEX)
|
||||||
endif()
|
endif()
|
||||||
if(PUREBOOT_HAS_USART1)
|
if(PUREBOOT_HAS_USART1)
|
||||||
pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} 1)
|
pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} software 1)
|
||||||
|
pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} software 1 ONE_WIRE)
|
||||||
|
pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} 1 "")
|
||||||
|
pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} 1 "" HALF_DUPLEX)
|
||||||
endif()
|
endif()
|
||||||
endforeach()
|
endforeach()
|
||||||
endforeach()
|
endforeach()
|
||||||
@@ -334,11 +449,98 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
endforeach()
|
endforeach()
|
||||||
list(GET _matrix_clocks -1 _matrix_top_hz)
|
list(GET _matrix_clocks -1 _matrix_top_hz)
|
||||||
pureboot_size_variant(pureboot_sw_wide CLOCK ${_matrix_top_hz} BAUD 9600 SERIAL software)
|
pureboot_size_variant(pureboot_sw_wide CLOCK ${_matrix_top_hz} BAUD 9600 SERIAL software)
|
||||||
|
# The pin axis at the widest software image — the slowest ladder rate
|
||||||
|
# against the fastest clock, whose bit spin needs the 16-bit delay
|
||||||
|
# loop — with the USART release on top of it. The exhaustive sweep
|
||||||
|
# above carries the same axis across its whole cross product.
|
||||||
|
if(PUREBOOT_HAS_USART)
|
||||||
|
pureboot_size_variant(pureboot_sw_wide_on_usart0 CLOCK ${_matrix_top_hz} BAUD 9600
|
||||||
|
SERIAL software RX ${PUREBOOT_USART0_RX} TX ${PUREBOOT_USART0_TX})
|
||||||
|
endif()
|
||||||
|
if(PUREBOOT_HAS_USART1)
|
||||||
|
pureboot_size_variant(pureboot_sw_wide_on_usart1 CLOCK ${_matrix_top_hz} BAUD 9600
|
||||||
|
SERIAL software RX ${PUREBOOT_USART1_RX} TX ${PUREBOOT_USART1_TX})
|
||||||
|
endif()
|
||||||
endif()
|
endif()
|
||||||
if(PUREBOOT_HAS_USART1)
|
if(PUREBOOT_HAS_USART1)
|
||||||
pureboot_size_variant(pureboot_usart1 USART 1)
|
pureboot_size_variant(pureboot_usart1 USART 1)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# The pin axis at its fixed points, in both matrix modes. The autobaud
|
||||||
|
# loader carries no clock and no baud, so the sweep has nothing to vary
|
||||||
|
# for it — yet it is the tightest image in the space, and on a USART's
|
||||||
|
# own pins it pays the release too: that combination is the one that
|
||||||
|
# overflowed the 1284's slot. The software build on those pins is the
|
||||||
|
# same deployment the mute test drives.
|
||||||
|
if(PUREBOOT_HAS_USART)
|
||||||
|
pureboot_size_variant(pureboot_sw_on_usart0 SERIAL software
|
||||||
|
RX ${PUREBOOT_USART0_RX} TX ${PUREBOOT_USART0_TX})
|
||||||
|
pureboot_size_variant(pureboot_autobaud_on_usart0 SERIAL autobaud
|
||||||
|
RX ${PUREBOOT_USART0_RX} TX ${PUREBOOT_USART0_TX})
|
||||||
|
endif()
|
||||||
|
if(PUREBOOT_HAS_USART1)
|
||||||
|
pureboot_size_variant(pureboot_sw_on_usart1 SERIAL software
|
||||||
|
RX ${PUREBOOT_USART1_RX} TX ${PUREBOOT_USART1_TX})
|
||||||
|
pureboot_size_variant(pureboot_autobaud_on_usart1 SERIAL autobaud
|
||||||
|
RX ${PUREBOOT_USART1_RX} TX ${PUREBOOT_USART1_TX})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# The OSCCAL axis at its fixed points: the stock shape, and the tightest
|
||||||
|
# image in the space with the trim on top — the axis adds one register
|
||||||
|
# write, and these points hold both of its addressing encodings to every
|
||||||
|
# chip's budget.
|
||||||
|
pureboot_size_variant(pureboot_osccal OSCCAL 0x9c)
|
||||||
|
pureboot_size_variant(pureboot_autobaud_osccal SERIAL autobaud OSCCAL 0x9c)
|
||||||
|
if(PUREBOOT_HAS_USART)
|
||||||
|
pureboot_size_variant(pureboot_autobaud_osccal_on_usart0 SERIAL autobaud OSCCAL 0x9c
|
||||||
|
RX ${PUREBOOT_USART0_RX} TX ${PUREBOOT_USART0_TX})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# The one-wire axis at its fixed points, in both matrix modes (the
|
||||||
|
# exhaustive sweep carries the same shapes across its cross product):
|
||||||
|
# the software link folded onto one pin, the tightest autobaud image
|
||||||
|
# likewise — on the default pin and on the USART's own RXD, whose
|
||||||
|
# release the now-driven shared pin needs where a receive-only link
|
||||||
|
# would not — and the hardware USART's half-duplex turn-around, stock
|
||||||
|
# and at the widest fixed-baud shape.
|
||||||
|
# The two spellings deliberately split across the two points: HALF_DUPLEX
|
||||||
|
# folds TX onto RX, RX == TX states the same thing directly.
|
||||||
|
pureboot_size_variant(pureboot_1w SERIAL software RX pb0 HALF_DUPLEX)
|
||||||
|
pureboot_size_variant(pureboot_1w_autobaud_osccal SERIAL autobaud OSCCAL 0x9c RX pb0 TX pb0)
|
||||||
|
if(PUREBOOT_HAS_USART)
|
||||||
|
pureboot_size_variant(pureboot_1w_on_usart0 SERIAL software
|
||||||
|
RX ${PUREBOOT_USART0_RX} TX ${PUREBOOT_USART0_RX})
|
||||||
|
pureboot_size_variant(pureboot_1w_autobaud_osccal_on_usart0 SERIAL autobaud OSCCAL 0x9c
|
||||||
|
RX ${PUREBOOT_USART0_RX} TX ${PUREBOOT_USART0_RX})
|
||||||
|
pureboot_size_variant(pureboot_hd HALF_DUPLEX)
|
||||||
|
list(GET _matrix_clocks -1 _hd_top_hz)
|
||||||
|
pureboot_size_variant(pureboot_hd_wide CLOCK ${_hd_top_hz} BAUD 9600 HALF_DUPLEX)
|
||||||
|
endif()
|
||||||
|
if(PUREBOOT_HAS_USART1)
|
||||||
|
pureboot_size_variant(pureboot_usart1_hd USART 1 HALF_DUPLEX)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# The trim byte, observed through the wire from the first prompt — one
|
||||||
|
# chip per OSCCAL addressing class: extended I/O on the 328P (data 0x66,
|
||||||
|
# an sts — DS40002061B §36), plain I/O on the 85 (data 0x51, an out —
|
||||||
|
# Atmel-2586 §21).
|
||||||
|
if(LIBAVR_MCU MATCHES "^(atmega328p|attiny85)$" AND DEFINED PB_DEVICE)
|
||||||
|
if(LIBAVR_MCU STREQUAL "atmega328p")
|
||||||
|
set(_osccal_addr 0x66)
|
||||||
|
else()
|
||||||
|
set(_osccal_addr 0x51)
|
||||||
|
endif()
|
||||||
|
get_target_property(_osccal_hz pureboot_osccal PUREBOOT_HZ)
|
||||||
|
get_target_property(_osccal_baud pureboot_osccal PUREBOOT_BAUD)
|
||||||
|
add_test(NAME pureboot.osccal
|
||||||
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbosccal.py
|
||||||
|
${PB_DEVICE} $<TARGET_FILE:pureboot_osccal> ${PUREBOOT_SIM_MCU}
|
||||||
|
${_osccal_hz} ${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_osccal_baud}
|
||||||
|
${_osccal_addr} 0x9c ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
|
${CMAKE_BINARY_DIR}/pbosccal-work)
|
||||||
|
set_tests_properties(pureboot.osccal PROPERTIES TIMEOUT 120)
|
||||||
|
endif()
|
||||||
|
|
||||||
# One configured deployment end to end — a real board's shape rather
|
# One configured deployment end to end — a real board's shape rather
|
||||||
# than the stock assumption: the ATmega328P on its shipped 1 MHz fuses,
|
# than the stock assumption: the ATmega328P on its shipped 1 MHz fuses,
|
||||||
# the software UART on hand-picked pins (TX = PB1, RX = PB5), the ladder
|
# the software UART on hand-picked pins (TX = PB1, RX = PB5), the ladder
|
||||||
@@ -367,6 +569,85 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
set_tests_properties(pureboot.custom PROPERTIES TIMEOUT 180)
|
set_tests_properties(pureboot.custom PROPERTIES TIMEOUT 180)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# Hand-over with the USART that owns the loader's pins left enabled — the
|
||||||
|
# state an application reaches by jumping in without a reset, and the one
|
||||||
|
# that made a bit-banged loader on PD0/PD1 (where the Uno's USB bridge
|
||||||
|
# lands) receive and obey while answering nothing. Run where it was found
|
||||||
|
# on silicon; the runner supplies the pin ownership simavr has no model
|
||||||
|
# for, which is what lets this fail when the release is gone.
|
||||||
|
if(LIBAVR_MCU STREQUAL "atmega328p" AND DEFINED PB_DEVICE)
|
||||||
|
get_target_property(_mute_hz pureboot_sw_on_usart0 PUREBOOT_HZ)
|
||||||
|
get_target_property(_mute_baud pureboot_sw_on_usart0 PUREBOOT_BAUD)
|
||||||
|
get_target_property(_mute_link pureboot_sw_on_usart0 PUREBOOT_LINK)
|
||||||
|
add_executable(pbapp_handover test/pbapp.cpp)
|
||||||
|
target_link_libraries(pbapp_handover PRIVATE libavr)
|
||||||
|
target_compile_definitions(pbapp_handover PRIVATE PUREBOOT_CLOCK_HZ=${_mute_hz}
|
||||||
|
PUREBOOT_BAUD=${_mute_baud} PUREBOOT_HANDOVER)
|
||||||
|
add_custom_command(TARGET pbapp_handover POST_BUILD
|
||||||
|
COMMAND ${CMAKE_OBJCOPY} -O binary
|
||||||
|
$<TARGET_FILE:pbapp_handover> $<TARGET_FILE:pbapp_handover>.bin)
|
||||||
|
add_test(NAME pureboot.mute
|
||||||
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbmute.py
|
||||||
|
${PB_DEVICE} $<TARGET_FILE:pureboot_sw_on_usart0> ${PUREBOOT_SIM_MCU} ${_mute_hz}
|
||||||
|
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_mute_baud}
|
||||||
|
$<TARGET_FILE:pbapp_handover>.bin
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
|
${CMAKE_BINARY_DIR}/pbmute-work ${_mute_link})
|
||||||
|
set_tests_properties(pureboot.mute PROPERTIES TIMEOUT 180)
|
||||||
|
|
||||||
|
# The same hand-over against the one-wire deployment on that USART's
|
||||||
|
# RXD: RXEN forces the shared pin's direction, so a loader that only
|
||||||
|
# released the transmit-side hold would read the wire and answer into
|
||||||
|
# a pin it cannot drive. The host runs with the --one-wire echo
|
||||||
|
# discard, which the bridge's shared-line model feeds for real.
|
||||||
|
get_target_property(_mute1w_link pureboot_1w_on_usart0 PUREBOOT_LINK)
|
||||||
|
add_test(NAME pureboot.mute.onewire
|
||||||
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbmute.py
|
||||||
|
${PB_DEVICE} $<TARGET_FILE:pureboot_1w_on_usart0> ${PUREBOOT_SIM_MCU} ${_mute_hz}
|
||||||
|
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_mute_baud}
|
||||||
|
$<TARGET_FILE:pbapp_handover>.bin
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
|
${CMAKE_BINARY_DIR}/pbmute-1w-work ${_mute1w_link})
|
||||||
|
set_tests_properties(pureboot.mute.onewire PROPERTIES TIMEOUT 180)
|
||||||
|
|
||||||
|
# The full protocol suite over one shared pin: the loader folded onto
|
||||||
|
# PB0, the bridge following the pin's direction, the fixture
|
||||||
|
# bannering as a guest on the same line, and the host discarding its
|
||||||
|
# own echo throughout.
|
||||||
|
get_target_property(_1w_hz pureboot_1w PUREBOOT_HZ)
|
||||||
|
get_target_property(_1w_baud pureboot_1w PUREBOOT_BAUD)
|
||||||
|
get_target_property(_1w_link pureboot_1w PUREBOOT_LINK)
|
||||||
|
add_executable(pbapp_1w test/pbapp.cpp)
|
||||||
|
target_link_libraries(pbapp_1w PRIVATE libavr)
|
||||||
|
target_compile_definitions(pbapp_1w PRIVATE PUREBOOT_CLOCK_HZ=${_1w_hz}
|
||||||
|
PUREBOOT_BAUD=${_1w_baud} PUREBOOT_SOFT_SERIAL
|
||||||
|
PUREBOOT_RX=pb0 PUREBOOT_TX=pb0)
|
||||||
|
add_custom_command(TARGET pbapp_1w POST_BUILD
|
||||||
|
COMMAND ${CMAKE_OBJCOPY} -O binary
|
||||||
|
$<TARGET_FILE:pbapp_1w> $<TARGET_FILE:pbapp_1w>.bin)
|
||||||
|
add_test(NAME pureboot.onewire
|
||||||
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbtest.py
|
||||||
|
${PB_DEVICE} $<TARGET_FILE:pureboot_1w> ${PUREBOOT_SIM_MCU} ${_1w_hz}
|
||||||
|
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_1w_baud} ${PUREBOOT_EEPROM}
|
||||||
|
$<TARGET_FILE:pbapp_1w>.bin ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
|
${CMAKE_BINARY_DIR}/pb1w-work ${_1w_link})
|
||||||
|
set_tests_properties(pureboot.onewire PROPERTIES TIMEOUT 180)
|
||||||
|
|
||||||
|
# The hardware USART's half-duplex turn-around, end to end: every
|
||||||
|
# reply byte runs drive-line, TXC-hold, release — against simavr's
|
||||||
|
# RXEN-gated receiver, which drops input to a disabled receiver the
|
||||||
|
# way silicon does. The pty is a two-wire transport, so the host
|
||||||
|
# needs no echo discard here; the off-chip tie itself is the
|
||||||
|
# hardware bench's item.
|
||||||
|
add_test(NAME pureboot.halfduplex
|
||||||
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbtest.py
|
||||||
|
${PB_DEVICE} $<TARGET_FILE:pureboot_hd> ${PUREBOOT_SIM_MCU} ${_pb_stock_hz}
|
||||||
|
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_pb_stock_baud} ${PUREBOOT_EEPROM}
|
||||||
|
$<TARGET_FILE:pbapp>.bin ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
|
${CMAKE_BINARY_DIR}/pbhd-work)
|
||||||
|
set_tests_properties(pureboot.halfduplex PROPERTIES TIMEOUT 180)
|
||||||
|
endif()
|
||||||
|
|
||||||
# The second USART, driven for real on one chip: instance selection is
|
# The second USART, driven for real on one chip: instance selection is
|
||||||
# compile-checked everywhere, but only a live session proves the loader
|
# compile-checked everywhere, but only a live session proves the loader
|
||||||
# initialized and polls the USART it claims to. The fixture application
|
# initialized and polls the USART it claims to. The fixture application
|
||||||
@@ -390,9 +671,9 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
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
|
# The autobaud loader driven end to end over the software-UART bridge:
|
||||||
# under review — pureboot/autobaud.md): the host sends the 0xC0 calibration
|
# the host sends the 0xC0 calibration pulse, the loader times it, locks,
|
||||||
# pulse, the loader times it, locks, and programs. Run on the near-flash 328P
|
# and programs. Run on the near-flash 328P
|
||||||
# and the word-addressed 1284P — the two flash-addressing classes — and each
|
# 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
|
# at two clocks with the one binary, which is the clock-agnostic property
|
||||||
# autobaud exists for (test/pbautobaud.py). The fixture application banners
|
# autobaud exists for (test/pbautobaud.py). The fixture application banners
|
||||||
@@ -412,5 +693,44 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
1000000 9600 ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
1000000 9600 ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
${CMAKE_BINARY_DIR}/pbautobaud-work)
|
${CMAKE_BINARY_DIR}/pbautobaud-work)
|
||||||
set_tests_properties(pureboot.autobaud PROPERTIES TIMEOUT 240)
|
set_tests_properties(pureboot.autobaud PROPERTIES TIMEOUT 240)
|
||||||
|
|
||||||
|
# The tightest deployment in the space, end to end: the autobaud
|
||||||
|
# loader folded onto the USART's own RXD with the OSCCAL trim baked
|
||||||
|
# — one-wire calibration, the receive-side release, and the host's
|
||||||
|
# echo discard, over the same two-clock sweep. One chip carries it;
|
||||||
|
# the shape is chip-independent.
|
||||||
|
if(LIBAVR_MCU STREQUAL "atmega328p")
|
||||||
|
get_target_property(_ab1w_link pureboot_1w_autobaud_osccal_on_usart0 PUREBOOT_LINK)
|
||||||
|
add_executable(pbapp_autobaud_1w test/pbapp.cpp)
|
||||||
|
target_link_libraries(pbapp_autobaud_1w PRIVATE libavr)
|
||||||
|
target_compile_definitions(pbapp_autobaud_1w PRIVATE PUREBOOT_CLOCK_HZ=1000000
|
||||||
|
PUREBOOT_BAUD=9600 PUREBOOT_SOFT_SERIAL
|
||||||
|
PUREBOOT_RX=pd0 PUREBOOT_TX=pd0)
|
||||||
|
add_custom_command(TARGET pbapp_autobaud_1w POST_BUILD
|
||||||
|
COMMAND ${CMAKE_OBJCOPY} -O binary
|
||||||
|
$<TARGET_FILE:pbapp_autobaud_1w> $<TARGET_FILE:pbapp_autobaud_1w>.bin)
|
||||||
|
add_test(NAME pureboot.autobaud.onewire
|
||||||
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbautobaud.py
|
||||||
|
${PB_DEVICE} $<TARGET_FILE:pureboot_1w_autobaud_osccal_on_usart0>
|
||||||
|
${PUREBOOT_SIM_MCU} ${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE}
|
||||||
|
$<TARGET_FILE:pbapp_autobaud_1w>.bin
|
||||||
|
1000000 9600 ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
|
${CMAKE_BINARY_DIR}/pbautobaud-1w-work ${_ab1w_link})
|
||||||
|
set_tests_properties(pureboot.autobaud.onewire PROPERTIES TIMEOUT 240)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# The autobaud window: the calibration poll budget, at the measured
|
||||||
|
# 10 cycles a poll (pbwindow.py pins the constant the README's
|
||||||
|
# seconds arithmetic uses; the budget itself is the clock-free knob).
|
||||||
|
add_test(NAME pureboot.window.autobaud
|
||||||
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbwindow.py
|
||||||
|
--device ${PB_DEVICE} --loader $<TARGET_FILE:pureboot_autobaud>
|
||||||
|
--mcu ${PUREBOOT_SIM_MCU} --hz 1000000
|
||||||
|
--base ${PUREBOOT_BASE_HEX} --page ${PUREBOOT_PAGE}
|
||||||
|
--baud 9600 --app $<TARGET_FILE:pbapp_autobaud>.bin
|
||||||
|
--autobaud-polls 4000000 --link sw
|
||||||
|
--tool ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
|
--workdir ${CMAKE_BINARY_DIR}/pbwindow-autobaud-work)
|
||||||
|
set_tests_properties(pureboot.window.autobaud PROPERTIES TIMEOUT 300)
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
77
ide/README.md
Normal file
77
ide/README.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# Atmel Studio
|
||||||
|
|
||||||
|
`master` carries `bootloader.atsln`, so this branch does too: `ide/bootloader.atsln`
|
||||||
|
builds the loaders from the same sources Ninja does, to a **byte-identical
|
||||||
|
`.text`** — 390 B for the 328P pureboot loader, 510 B for the `tsb_asm` tier in
|
||||||
|
its 512-byte section. CMake remains the build system; the solution is here so the
|
||||||
|
port opens in Studio as its predecessor did.
|
||||||
|
|
||||||
|
## The two projects, and why two
|
||||||
|
|
||||||
|
pureboot is a chip × backend × clock × baud matrix — `pureboot_add_loader()`
|
||||||
|
resolves a deployment into compile definitions — and a `.cppproj` is one binary
|
||||||
|
at one set of flags, so a project can only ever be one point of it. `pureboot`
|
||||||
|
is that point: the stock 328P deployment, USART0 at 115200 on a 16 MHz crystal,
|
||||||
|
an 8-second activation window. `tsb_asm` is the TinySafeBoot tier that occupies
|
||||||
|
the same 512-byte section `master`'s `tsb` project targeted.
|
||||||
|
|
||||||
|
The other three tsb tiers (`tsb_pure`, `tsb_tricks`, `tsb_policy`) are not here.
|
||||||
|
They differ from `tsb_asm` in their source file, their section size, and — for
|
||||||
|
`tsb_policy` — two loop flags; nothing about that is a Studio concern, and what
|
||||||
|
they exist to demonstrate is a size gradient only the CMake size tests measure.
|
||||||
|
Adding one is a copy of `tsb_asm/tsb_asm.cppproj` in its own directory, with its
|
||||||
|
name, its GUID, its source path and its `--section-start` changed (`0x7c00` for
|
||||||
|
the 1 KiB tiers), plus four lines in the solution.
|
||||||
|
|
||||||
|
`avrdevice` is a project property, so each project gets its own directory:
|
||||||
|
Studio builds into `<project dir>/<Configuration>` whatever `OutputDirectory`
|
||||||
|
says, and two projects sharing a directory would share one object file.
|
||||||
|
|
||||||
|
## Debug keeps `-Os`
|
||||||
|
|
||||||
|
Both configurations compile at `-Os`; Debug adds only `-gdwarf-4`. The `.text`
|
||||||
|
is therefore identical in both, which is the point — a loader's section is a
|
||||||
|
**correctness** bound and not a budget. `-Og` builds this same source to 590 B,
|
||||||
|
and linking it at `--section-start=.text=0x7e00` on a 32 KiB part puts 78 bytes
|
||||||
|
past flash end **without a diagnostic**: `rcall`/`rjmp` targets there wrap
|
||||||
|
modulo flash size, so the image dies right after activation. A debug
|
||||||
|
configuration that silently produces that is worse than none, and DWARF costs no
|
||||||
|
flash, so the optimisation level stays where correctness needs it.
|
||||||
|
|
||||||
|
## What Studio needs from the machine
|
||||||
|
|
||||||
|
libavr from the **submodule**, found at
|
||||||
|
`$(MSBuildProjectDirectory)\..\..\libavr\include` — correct by construction, and
|
||||||
|
anchored to the project because a plain relative path resolves against the
|
||||||
|
generated makefile's directory (the configuration's output directory), not the
|
||||||
|
project's. There is no `LIBAVR_ROOT` escape hatch: a variable exported in a
|
||||||
|
shell is invisible to Studio launched from the Start menu, and the failure reads
|
||||||
|
as a missing `libavr/libavr.hpp` — which is what the submodule answers.
|
||||||
|
|
||||||
|
A GCC 16.1 toolchain registered as flavour `avr-g++-16.1.0`, nothing older
|
||||||
|
reaching `-std=c++26`.
|
||||||
|
|
||||||
|
## Generating and gating
|
||||||
|
|
||||||
|
One generated file is required before a project will load at all, and one command
|
||||||
|
per project checks the flags have not drifted (both from libavr's
|
||||||
|
`tools/atmelstudio/`):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
for name in pureboot tsb_asm; do
|
||||||
|
python libavr/tools/atmelstudio/componentinfo.py \
|
||||||
|
"ide/$name/$name.componentinfo.xml" --device ATmega328P
|
||||||
|
python libavr/tools/atmelstudio/check-flags.py \
|
||||||
|
--solution ide/bootloader.atsln --project "$name" --target "$name" \
|
||||||
|
--compile-commands build/atmega328p-generated/compile_commands.json \
|
||||||
|
--log "build/as-$name.log"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
`--project` because one reference describes one binary; `--target` because
|
||||||
|
`pureboot.cpp` is compiled by every point of the size matrix and the flags
|
||||||
|
differ per point, so the basename alone does not name a reference. Release is
|
||||||
|
what the gate compares — the presets define no debug build, and Debug differs
|
||||||
|
from Release only in `-gdwarf-4`.
|
||||||
|
|
||||||
|
Legacy (the yazoalfa-era submodules) stays on `master`.
|
||||||
28
ide/bootloader.atsln
Normal file
28
ide/bootloader.atsln
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Atmel Studio Solution File, Format Version 11.00
|
||||||
|
VisualStudioVersion = 14.0.23107.0
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{E66E83B9-2572-4076-B26E-6BE79FF3018A}") = "pureboot", "pureboot\pureboot.cppproj", "{99067222-32D5-49E3-B4F8-5ABA0F7722B7}"
|
||||||
|
EndProject
|
||||||
|
Project("{E66E83B9-2572-4076-B26E-6BE79FF3018A}") = "tsb_asm", "tsb_asm\tsb_asm.cppproj", "{6618D3BE-7EB3-49A2-9113-F128E396FF06}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|AVR = Debug|AVR
|
||||||
|
Release|AVR = Release|AVR
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{99067222-32D5-49E3-B4F8-5ABA0F7722B7}.Debug|AVR.ActiveCfg = Debug|AVR
|
||||||
|
{99067222-32D5-49E3-B4F8-5ABA0F7722B7}.Debug|AVR.Build.0 = Debug|AVR
|
||||||
|
{99067222-32D5-49E3-B4F8-5ABA0F7722B7}.Release|AVR.ActiveCfg = Release|AVR
|
||||||
|
{99067222-32D5-49E3-B4F8-5ABA0F7722B7}.Release|AVR.Build.0 = Release|AVR
|
||||||
|
{6618D3BE-7EB3-49A2-9113-F128E396FF06}.Debug|AVR.ActiveCfg = Debug|AVR
|
||||||
|
{6618D3BE-7EB3-49A2-9113-F128E396FF06}.Debug|AVR.Build.0 = Debug|AVR
|
||||||
|
{6618D3BE-7EB3-49A2-9113-F128E396FF06}.Release|AVR.ActiveCfg = Release|AVR
|
||||||
|
{6618D3BE-7EB3-49A2-9113-F128E396FF06}.Release|AVR.Build.0 = Release|AVR
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
118
ide/pureboot/pureboot.cppproj
Normal file
118
ide/pureboot/pureboot.cppproj
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="14.0">
|
||||||
|
<PropertyGroup>
|
||||||
|
<SchemaVersion>2.0</SchemaVersion>
|
||||||
|
<ProjectVersion>7.0</ProjectVersion>
|
||||||
|
<ToolchainName>com.Atmel.AVRGCC8.CPP</ToolchainName>
|
||||||
|
<ProjectGuid>99067222-32d5-49e3-b4f8-5aba0f7722b7</ProjectGuid>
|
||||||
|
<avrdevice>ATmega328P</avrdevice>
|
||||||
|
<avrdeviceseries>none</avrdeviceseries>
|
||||||
|
<OutputType>Executable</OutputType>
|
||||||
|
<Language>CPP</Language>
|
||||||
|
<OutputFileName>$(MSBuildProjectName)</OutputFileName>
|
||||||
|
<OutputFileExtension>.elf</OutputFileExtension>
|
||||||
|
<OutputDirectory>$(MSBuildProjectDirectory)\$(Configuration)</OutputDirectory>
|
||||||
|
<AssemblyName>pureboot</AssemblyName>
|
||||||
|
<Name>pureboot</Name>
|
||||||
|
<RootNamespace>pureboot</RootNamespace>
|
||||||
|
<ToolchainFlavour>avr-g++-16.1.0</ToolchainFlavour>
|
||||||
|
<KeepTimersRunning>true</KeepTimersRunning>
|
||||||
|
<OverrideVtor>false</OverrideVtor>
|
||||||
|
<CacheFlash>true</CacheFlash>
|
||||||
|
<ProgFlashFromRam>true</ProgFlashFromRam>
|
||||||
|
<RamSnippetAddress>0x20000000</RamSnippetAddress>
|
||||||
|
<UncachedRange />
|
||||||
|
<preserveEEPROM>true</preserveEEPROM>
|
||||||
|
<OverrideVtorValue>exception_table</OverrideVtorValue>
|
||||||
|
<BootSegment>2</BootSegment>
|
||||||
|
<ResetRule>0</ResetRule>
|
||||||
|
<eraseonlaunchrule>0</eraseonlaunchrule>
|
||||||
|
<EraseKey />
|
||||||
|
<AsfFrameworkConfig>
|
||||||
|
<framework-data xmlns="">
|
||||||
|
<options />
|
||||||
|
<configurations />
|
||||||
|
<files />
|
||||||
|
<documentation help="" />
|
||||||
|
<offline-documentation help="" />
|
||||||
|
<dependencies>
|
||||||
|
<content-extension eid="atmel.asf" uuidref="Atmel.ASF" version="3.52.0" />
|
||||||
|
</dependencies>
|
||||||
|
</framework-data>
|
||||||
|
</AsfFrameworkConfig>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||||
|
<ToolchainSettings>
|
||||||
|
<AvrGccCpp>
|
||||||
|
<avrgcc.common.Device>-mmcu=atmega328p</avrgcc.common.Device>
|
||||||
|
<avrgcc.common.outputfiles.hex>True</avrgcc.common.outputfiles.hex>
|
||||||
|
<avrgcc.common.outputfiles.lss>True</avrgcc.common.outputfiles.lss>
|
||||||
|
<avrgcc.common.outputfiles.eep>True</avrgcc.common.outputfiles.eep>
|
||||||
|
<avrgcc.common.outputfiles.srec>True</avrgcc.common.outputfiles.srec>
|
||||||
|
<avrgcc.common.outputfiles.usersignatures>False</avrgcc.common.outputfiles.usersignatures>
|
||||||
|
<avrgcccpp.compiler.symbols.DefSymbols>
|
||||||
|
<ListValues>
|
||||||
|
<Value>NDEBUG</Value>
|
||||||
|
<Value>PUREBOOT_CLOCK_HZ=16000000</Value>
|
||||||
|
<Value>PUREBOOT_BAUD=115200</Value>
|
||||||
|
<Value>PUREBOOT_TIMEOUT=8</Value>
|
||||||
|
</ListValues>
|
||||||
|
</avrgcccpp.compiler.symbols.DefSymbols>
|
||||||
|
<avrgcccpp.compiler.directories.IncludePaths>
|
||||||
|
<ListValues>
|
||||||
|
<Value>$(MSBuildProjectDirectory)\..\..\libavr\include</Value>
|
||||||
|
</ListValues>
|
||||||
|
</avrgcccpp.compiler.directories.IncludePaths>
|
||||||
|
<avrgcccpp.compiler.optimization.level>Optimize for size (-Os)</avrgcccpp.compiler.optimization.level>
|
||||||
|
<avrgcccpp.compiler.optimization.PrepareFunctionsForGarbageCollection>True</avrgcccpp.compiler.optimization.PrepareFunctionsForGarbageCollection>
|
||||||
|
<avrgcccpp.compiler.optimization.PrepareDataForGarbageCollection>True</avrgcccpp.compiler.optimization.PrepareDataForGarbageCollection>
|
||||||
|
<avrgcccpp.compiler.warnings.AllWarnings>True</avrgcccpp.compiler.warnings.AllWarnings>
|
||||||
|
<avrgcccpp.compiler.miscellaneous.OtherFlags>-std=c++26 -Wextra -Werror -mrelax -fno-exceptions -fno-rtti -fno-threadsafe-statics -fno-ivopts -fira-algorithm=priority -fno-tree-ter -fno-split-wide-types</avrgcccpp.compiler.miscellaneous.OtherFlags>
|
||||||
|
<avrgcccpp.linker.optimization.GarbageCollectUnusedSections>True</avrgcccpp.linker.optimization.GarbageCollectUnusedSections>
|
||||||
|
<avrgcccpp.linker.miscellaneous.LinkerFlags>-mrelax -nostartfiles -Wl,--section-start=.text=0x7e00 -Wl,--defsym=pureboot_app=0 -Wl,--pmem-wrap-around=32k</avrgcccpp.linker.miscellaneous.LinkerFlags>
|
||||||
|
</AvrGccCpp>
|
||||||
|
</ToolchainSettings>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
|
<ToolchainSettings>
|
||||||
|
<AvrGccCpp>
|
||||||
|
<avrgcc.common.Device>-mmcu=atmega328p</avrgcc.common.Device>
|
||||||
|
<avrgcc.common.outputfiles.hex>True</avrgcc.common.outputfiles.hex>
|
||||||
|
<avrgcc.common.outputfiles.lss>True</avrgcc.common.outputfiles.lss>
|
||||||
|
<avrgcc.common.outputfiles.eep>True</avrgcc.common.outputfiles.eep>
|
||||||
|
<avrgcc.common.outputfiles.srec>True</avrgcc.common.outputfiles.srec>
|
||||||
|
<avrgcc.common.outputfiles.usersignatures>False</avrgcc.common.outputfiles.usersignatures>
|
||||||
|
<avrgcccpp.compiler.symbols.DefSymbols>
|
||||||
|
<ListValues>
|
||||||
|
<Value>DEBUG</Value>
|
||||||
|
<Value>PUREBOOT_CLOCK_HZ=16000000</Value>
|
||||||
|
<Value>PUREBOOT_BAUD=115200</Value>
|
||||||
|
<Value>PUREBOOT_TIMEOUT=8</Value>
|
||||||
|
</ListValues>
|
||||||
|
</avrgcccpp.compiler.symbols.DefSymbols>
|
||||||
|
<avrgcccpp.compiler.directories.IncludePaths>
|
||||||
|
<ListValues>
|
||||||
|
<Value>$(MSBuildProjectDirectory)\..\..\libavr\include</Value>
|
||||||
|
</ListValues>
|
||||||
|
</avrgcccpp.compiler.directories.IncludePaths>
|
||||||
|
<avrgcccpp.compiler.optimization.level>Optimize for size (-Os)</avrgcccpp.compiler.optimization.level>
|
||||||
|
<avrgcccpp.compiler.optimization.PrepareFunctionsForGarbageCollection>True</avrgcccpp.compiler.optimization.PrepareFunctionsForGarbageCollection>
|
||||||
|
<avrgcccpp.compiler.optimization.PrepareDataForGarbageCollection>True</avrgcccpp.compiler.optimization.PrepareDataForGarbageCollection>
|
||||||
|
<avrgcccpp.compiler.warnings.AllWarnings>True</avrgcccpp.compiler.warnings.AllWarnings>
|
||||||
|
<avrgcccpp.compiler.miscellaneous.OtherFlags>-std=c++26 -Wextra -Werror -mrelax -fno-exceptions -fno-rtti -fno-threadsafe-statics -fno-ivopts -fira-algorithm=priority -fno-tree-ter -fno-split-wide-types -gdwarf-4</avrgcccpp.compiler.miscellaneous.OtherFlags>
|
||||||
|
<avrgcccpp.linker.optimization.GarbageCollectUnusedSections>True</avrgcccpp.linker.optimization.GarbageCollectUnusedSections>
|
||||||
|
<avrgcccpp.linker.miscellaneous.LinkerFlags>-mrelax -nostartfiles -Wl,--section-start=.text=0x7e00 -Wl,--defsym=pureboot_app=0 -Wl,--pmem-wrap-around=32k</avrgcccpp.linker.miscellaneous.LinkerFlags>
|
||||||
|
</AvrGccCpp>
|
||||||
|
</ToolchainSettings>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="..\..\pureboot\pureboot.cpp">
|
||||||
|
<SubType>compile</SubType>
|
||||||
|
<Link>pureboot\pureboot.cpp</Link>
|
||||||
|
</Compile>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="pureboot" />
|
||||||
|
</ItemGroup>
|
||||||
|
<Import Project="$(AVRSTUDIO_EXE_PATH)\Vs\Compiler.targets" />
|
||||||
|
</Project>
|
||||||
112
ide/tsb_asm/tsb_asm.cppproj
Normal file
112
ide/tsb_asm/tsb_asm.cppproj
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="14.0">
|
||||||
|
<PropertyGroup>
|
||||||
|
<SchemaVersion>2.0</SchemaVersion>
|
||||||
|
<ProjectVersion>7.0</ProjectVersion>
|
||||||
|
<ToolchainName>com.Atmel.AVRGCC8.CPP</ToolchainName>
|
||||||
|
<ProjectGuid>6618d3be-7eb3-49a2-9113-f128e396ff06</ProjectGuid>
|
||||||
|
<avrdevice>ATmega328P</avrdevice>
|
||||||
|
<avrdeviceseries>none</avrdeviceseries>
|
||||||
|
<OutputType>Executable</OutputType>
|
||||||
|
<Language>CPP</Language>
|
||||||
|
<OutputFileName>$(MSBuildProjectName)</OutputFileName>
|
||||||
|
<OutputFileExtension>.elf</OutputFileExtension>
|
||||||
|
<OutputDirectory>$(MSBuildProjectDirectory)\$(Configuration)</OutputDirectory>
|
||||||
|
<AssemblyName>tsb_asm</AssemblyName>
|
||||||
|
<Name>tsb_asm</Name>
|
||||||
|
<RootNamespace>tsb_asm</RootNamespace>
|
||||||
|
<ToolchainFlavour>avr-g++-16.1.0</ToolchainFlavour>
|
||||||
|
<KeepTimersRunning>true</KeepTimersRunning>
|
||||||
|
<OverrideVtor>false</OverrideVtor>
|
||||||
|
<CacheFlash>true</CacheFlash>
|
||||||
|
<ProgFlashFromRam>true</ProgFlashFromRam>
|
||||||
|
<RamSnippetAddress>0x20000000</RamSnippetAddress>
|
||||||
|
<UncachedRange />
|
||||||
|
<preserveEEPROM>true</preserveEEPROM>
|
||||||
|
<OverrideVtorValue>exception_table</OverrideVtorValue>
|
||||||
|
<BootSegment>2</BootSegment>
|
||||||
|
<ResetRule>0</ResetRule>
|
||||||
|
<eraseonlaunchrule>0</eraseonlaunchrule>
|
||||||
|
<EraseKey />
|
||||||
|
<AsfFrameworkConfig>
|
||||||
|
<framework-data xmlns="">
|
||||||
|
<options />
|
||||||
|
<configurations />
|
||||||
|
<files />
|
||||||
|
<documentation help="" />
|
||||||
|
<offline-documentation help="" />
|
||||||
|
<dependencies>
|
||||||
|
<content-extension eid="atmel.asf" uuidref="Atmel.ASF" version="3.52.0" />
|
||||||
|
</dependencies>
|
||||||
|
</framework-data>
|
||||||
|
</AsfFrameworkConfig>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||||
|
<ToolchainSettings>
|
||||||
|
<AvrGccCpp>
|
||||||
|
<avrgcc.common.Device>-mmcu=atmega328p</avrgcc.common.Device>
|
||||||
|
<avrgcc.common.outputfiles.hex>True</avrgcc.common.outputfiles.hex>
|
||||||
|
<avrgcc.common.outputfiles.lss>True</avrgcc.common.outputfiles.lss>
|
||||||
|
<avrgcc.common.outputfiles.eep>True</avrgcc.common.outputfiles.eep>
|
||||||
|
<avrgcc.common.outputfiles.srec>True</avrgcc.common.outputfiles.srec>
|
||||||
|
<avrgcc.common.outputfiles.usersignatures>False</avrgcc.common.outputfiles.usersignatures>
|
||||||
|
<avrgcccpp.compiler.symbols.DefSymbols>
|
||||||
|
<ListValues>
|
||||||
|
<Value>NDEBUG</Value>
|
||||||
|
</ListValues>
|
||||||
|
</avrgcccpp.compiler.symbols.DefSymbols>
|
||||||
|
<avrgcccpp.compiler.directories.IncludePaths>
|
||||||
|
<ListValues>
|
||||||
|
<Value>$(MSBuildProjectDirectory)\..\..\libavr\include</Value>
|
||||||
|
</ListValues>
|
||||||
|
</avrgcccpp.compiler.directories.IncludePaths>
|
||||||
|
<avrgcccpp.compiler.optimization.level>Optimize for size (-Os)</avrgcccpp.compiler.optimization.level>
|
||||||
|
<avrgcccpp.compiler.optimization.PrepareFunctionsForGarbageCollection>True</avrgcccpp.compiler.optimization.PrepareFunctionsForGarbageCollection>
|
||||||
|
<avrgcccpp.compiler.optimization.PrepareDataForGarbageCollection>True</avrgcccpp.compiler.optimization.PrepareDataForGarbageCollection>
|
||||||
|
<avrgcccpp.compiler.warnings.AllWarnings>True</avrgcccpp.compiler.warnings.AllWarnings>
|
||||||
|
<avrgcccpp.compiler.miscellaneous.OtherFlags>-std=c++26 -Wextra -Werror -mrelax -fno-exceptions -fno-rtti -fno-threadsafe-statics</avrgcccpp.compiler.miscellaneous.OtherFlags>
|
||||||
|
<avrgcccpp.linker.optimization.GarbageCollectUnusedSections>True</avrgcccpp.linker.optimization.GarbageCollectUnusedSections>
|
||||||
|
<avrgcccpp.linker.miscellaneous.LinkerFlags>-mrelax -nostartfiles -Wl,--section-start=.text=0x7e00 -Wl,--defsym=tsb_app=0 -Wl,--pmem-wrap-around=32k</avrgcccpp.linker.miscellaneous.LinkerFlags>
|
||||||
|
</AvrGccCpp>
|
||||||
|
</ToolchainSettings>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
|
<ToolchainSettings>
|
||||||
|
<AvrGccCpp>
|
||||||
|
<avrgcc.common.Device>-mmcu=atmega328p</avrgcc.common.Device>
|
||||||
|
<avrgcc.common.outputfiles.hex>True</avrgcc.common.outputfiles.hex>
|
||||||
|
<avrgcc.common.outputfiles.lss>True</avrgcc.common.outputfiles.lss>
|
||||||
|
<avrgcc.common.outputfiles.eep>True</avrgcc.common.outputfiles.eep>
|
||||||
|
<avrgcc.common.outputfiles.srec>True</avrgcc.common.outputfiles.srec>
|
||||||
|
<avrgcc.common.outputfiles.usersignatures>False</avrgcc.common.outputfiles.usersignatures>
|
||||||
|
<avrgcccpp.compiler.symbols.DefSymbols>
|
||||||
|
<ListValues>
|
||||||
|
<Value>DEBUG</Value>
|
||||||
|
</ListValues>
|
||||||
|
</avrgcccpp.compiler.symbols.DefSymbols>
|
||||||
|
<avrgcccpp.compiler.directories.IncludePaths>
|
||||||
|
<ListValues>
|
||||||
|
<Value>$(MSBuildProjectDirectory)\..\..\libavr\include</Value>
|
||||||
|
</ListValues>
|
||||||
|
</avrgcccpp.compiler.directories.IncludePaths>
|
||||||
|
<avrgcccpp.compiler.optimization.level>Optimize for size (-Os)</avrgcccpp.compiler.optimization.level>
|
||||||
|
<avrgcccpp.compiler.optimization.PrepareFunctionsForGarbageCollection>True</avrgcccpp.compiler.optimization.PrepareFunctionsForGarbageCollection>
|
||||||
|
<avrgcccpp.compiler.optimization.PrepareDataForGarbageCollection>True</avrgcccpp.compiler.optimization.PrepareDataForGarbageCollection>
|
||||||
|
<avrgcccpp.compiler.warnings.AllWarnings>True</avrgcccpp.compiler.warnings.AllWarnings>
|
||||||
|
<avrgcccpp.compiler.miscellaneous.OtherFlags>-std=c++26 -Wextra -Werror -mrelax -fno-exceptions -fno-rtti -fno-threadsafe-statics -gdwarf-4</avrgcccpp.compiler.miscellaneous.OtherFlags>
|
||||||
|
<avrgcccpp.linker.optimization.GarbageCollectUnusedSections>True</avrgcccpp.linker.optimization.GarbageCollectUnusedSections>
|
||||||
|
<avrgcccpp.linker.miscellaneous.LinkerFlags>-mrelax -nostartfiles -Wl,--section-start=.text=0x7e00 -Wl,--defsym=tsb_app=0 -Wl,--pmem-wrap-around=32k</avrgcccpp.linker.miscellaneous.LinkerFlags>
|
||||||
|
</AvrGccCpp>
|
||||||
|
</ToolchainSettings>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="..\..\tsb\tsb_asm.cpp">
|
||||||
|
<SubType>compile</SubType>
|
||||||
|
<Link>tsb\tsb_asm.cpp</Link>
|
||||||
|
</Compile>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="tsb" />
|
||||||
|
</ItemGroup>
|
||||||
|
<Import Project="$(AVRSTUDIO_EXE_PATH)\Vs\Compiler.targets" />
|
||||||
|
</Project>
|
||||||
2
libavr
2
libavr
Submodule libavr updated: 6cfc7a8eee...b719ed74d8
@@ -116,6 +116,17 @@ else()
|
|||||||
math(EXPR _pb_limit "${_pb_slot} - 2")
|
math(EXPR _pb_limit "${_pb_slot} - 2")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# The pins each USART owns. A bit-banged link deployed on them has to release
|
||||||
|
# that USART before it can drive the line, and those instructions are the one
|
||||||
|
# way the choice of pins moves the image — so a size matrix needs them as an
|
||||||
|
# axis even though pins are otherwise immediate operands. Uniform across every
|
||||||
|
# mega libavr covers: USART0 (the classics' un-numbered USART included) on
|
||||||
|
# PD0/PD1, USART1 on PD2/PD3.
|
||||||
|
set(_pb_usart0_rx pd0)
|
||||||
|
set(_pb_usart0_tx pd1)
|
||||||
|
set(_pb_usart1_rx pd2)
|
||||||
|
set(_pb_usart1_tx pd3)
|
||||||
|
|
||||||
# simavr names its cores after the base dies; the A revisions run on them
|
# simavr names its cores after the base dies; the A revisions run on them
|
||||||
# (the 644PA on the 644P core).
|
# (the 644PA on the 644P core).
|
||||||
set(_pb_sim_mcu ${LIBAVR_MCU})
|
set(_pb_sim_mcu ${LIBAVR_MCU})
|
||||||
@@ -125,6 +136,28 @@ elseif(LIBAVR_MCU STREQUAL "atmega644pa")
|
|||||||
set(_pb_sim_mcu atmega644p)
|
set(_pb_sim_mcu atmega644p)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# Where SRAM begins: the classic megas keep it right after the plain I/O
|
||||||
|
# registers, the x8/x4 generations push it past their extended I/O file, and
|
||||||
|
# the tinies match the classics. An autobaud loader keeps its measured unit
|
||||||
|
# in GPIOR2:GPIOR1 wherever the chip has the pair (data 0x32 on the
|
||||||
|
# t25/45/85, 0x4A from the x8 generation on) and as the first RAM object at
|
||||||
|
# SRAM start where it does not (the t13s and classic megas). The host reads
|
||||||
|
# whichever home applies (pureboot.py's geometry), and the unit-position
|
||||||
|
# test holds the image to the same split.
|
||||||
|
if(LIBAVR_MCU MATCHES "^atmega(8|16|32)a?$")
|
||||||
|
set(_pb_ram 0x60)
|
||||||
|
set(_pb_unit_gpior "")
|
||||||
|
elseif(LIBAVR_MCU MATCHES "^atmega")
|
||||||
|
set(_pb_ram 0x100)
|
||||||
|
set(_pb_unit_gpior 0x4A)
|
||||||
|
elseif(LIBAVR_MCU MATCHES "^attiny13")
|
||||||
|
set(_pb_ram 0x60)
|
||||||
|
set(_pb_unit_gpior "")
|
||||||
|
else()
|
||||||
|
set(_pb_ram 0x60)
|
||||||
|
set(_pb_unit_gpior 0x32)
|
||||||
|
endif()
|
||||||
|
|
||||||
# The function runs in its caller's scope, so everything it needs crosses
|
# The function runs in its caller's scope, so everything it needs crosses
|
||||||
# scopes as global properties.
|
# scopes as global properties.
|
||||||
set_property(GLOBAL PROPERTY PUREBOOT_BASE_HEX ${_pb_base_hex})
|
set_property(GLOBAL PROPERTY PUREBOOT_BASE_HEX ${_pb_base_hex})
|
||||||
@@ -133,6 +166,10 @@ set_property(GLOBAL PROPERTY PUREBOOT_WRAP "${_pb_wrap}")
|
|||||||
set_property(GLOBAL PROPERTY PUREBOOT_DEFAULT_HZ ${_pb_hz})
|
set_property(GLOBAL PROPERTY PUREBOOT_DEFAULT_HZ ${_pb_hz})
|
||||||
set_property(GLOBAL PROPERTY PUREBOOT_HAS_USART ${_pb_has_usart})
|
set_property(GLOBAL PROPERTY PUREBOOT_HAS_USART ${_pb_has_usart})
|
||||||
set_property(GLOBAL PROPERTY PUREBOOT_HAS_USART1 ${_pb_has_usart1})
|
set_property(GLOBAL PROPERTY PUREBOOT_HAS_USART1 ${_pb_has_usart1})
|
||||||
|
set_property(GLOBAL PROPERTY PUREBOOT_USART0_RX ${_pb_usart0_rx})
|
||||||
|
set_property(GLOBAL PROPERTY PUREBOOT_USART0_TX ${_pb_usart0_tx})
|
||||||
|
set_property(GLOBAL PROPERTY PUREBOOT_USART1_RX ${_pb_usart1_rx})
|
||||||
|
set_property(GLOBAL PROPERTY PUREBOOT_USART1_TX ${_pb_usart1_tx})
|
||||||
|
|
||||||
# The port's own build (tests, the size matrix) reads the geometry from the
|
# The port's own build (tests, the size matrix) reads the geometry from the
|
||||||
# parent scope; a downstream consumer gets the same variables for free.
|
# parent scope; a downstream consumer gets the same variables for free.
|
||||||
@@ -142,9 +179,15 @@ set(PUREBOOT_SLOT ${_pb_slot} PARENT_SCOPE)
|
|||||||
set(PUREBOOT_LIMIT ${_pb_limit} PARENT_SCOPE)
|
set(PUREBOOT_LIMIT ${_pb_limit} PARENT_SCOPE)
|
||||||
set(PUREBOOT_EEPROM ${_pb_eeprom} PARENT_SCOPE)
|
set(PUREBOOT_EEPROM ${_pb_eeprom} PARENT_SCOPE)
|
||||||
set(PUREBOOT_DEFAULT_HZ ${_pb_hz} PARENT_SCOPE)
|
set(PUREBOOT_DEFAULT_HZ ${_pb_hz} PARENT_SCOPE)
|
||||||
|
set(PUREBOOT_RAM_START ${_pb_ram} PARENT_SCOPE)
|
||||||
|
set(PUREBOOT_UNIT_GPIOR "${_pb_unit_gpior}" PARENT_SCOPE)
|
||||||
set(PUREBOOT_HAS_USART ${_pb_has_usart} PARENT_SCOPE)
|
set(PUREBOOT_HAS_USART ${_pb_has_usart} PARENT_SCOPE)
|
||||||
set(PUREBOOT_HAS_USART1 ${_pb_has_usart1} PARENT_SCOPE)
|
set(PUREBOOT_HAS_USART1 ${_pb_has_usart1} PARENT_SCOPE)
|
||||||
set(PUREBOOT_SIM_MCU ${_pb_sim_mcu} PARENT_SCOPE)
|
set(PUREBOOT_SIM_MCU ${_pb_sim_mcu} PARENT_SCOPE)
|
||||||
|
set(PUREBOOT_USART0_RX ${_pb_usart0_rx} PARENT_SCOPE)
|
||||||
|
set(PUREBOOT_USART0_TX ${_pb_usart0_tx} PARENT_SCOPE)
|
||||||
|
set(PUREBOOT_USART1_RX ${_pb_usart1_rx} PARENT_SCOPE)
|
||||||
|
set(PUREBOOT_USART1_TX ${_pb_usart1_tx} PARENT_SCOPE)
|
||||||
|
|
||||||
# The rates a default may pick, fastest first.
|
# The rates a default may pick, fastest first.
|
||||||
set_property(GLOBAL PROPERTY PUREBOOT_BAUD_LADDER 115200 57600 38400 19200 9600)
|
set_property(GLOBAL PROPERTY PUREBOOT_BAUD_LADDER 115200 57600 38400 19200 9600)
|
||||||
@@ -195,20 +238,34 @@ endfunction()
|
|||||||
|
|
||||||
# pureboot_add_loader(<name> [CLOCK <hz>] [BAUD <bd>]
|
# pureboot_add_loader(<name> [CLOCK <hz>] [BAUD <bd>]
|
||||||
# [SERIAL auto|hardware|software|autobaud] [USART <n>]
|
# [SERIAL auto|hardware|software|autobaud] [USART <n>]
|
||||||
# [RX <pin>] [TX <pin>] [TIMEOUT <s>])
|
# [RX <pin>] [TX <pin>] [TIMEOUT <s>] [OSCCAL <byte>]
|
||||||
|
# [HALF_DUPLEX])
|
||||||
#
|
#
|
||||||
# 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> with a trailing @<n> where those pins are a
|
||||||
|
# USART's own) — what a test harness speaks to it with.
|
||||||
|
#
|
||||||
|
# HALF_DUPLEX is the one-wire deployment, per backend: on the hardware USART
|
||||||
|
# it enables the library's .half_duplex turn-around (RXD and TXD tied
|
||||||
|
# together off-chip); on a software or autobaud link it puts both directions
|
||||||
|
# on the RX pin — the same thing RX == TX spells directly.
|
||||||
#
|
#
|
||||||
# SERIAL autobaud measures the host's bit timing at run time, so the image
|
# 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,
|
# 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
|
# 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,
|
# PUREBOOT_HZ/PUREBOOT_BAUD then record what a harness should *drive* it at,
|
||||||
# not what it was built for.
|
# not what it was built for.
|
||||||
|
#
|
||||||
|
# OSCCAL bakes a measured oscillator trim into the loader (README.md: the
|
||||||
|
# RC-oscillator deployment answer): the byte is written at the top of run(),
|
||||||
|
# so every reset path — the watchdog hand-over included — runs on the
|
||||||
|
# corrected clock. Orthogonal to the backend: an autobaud build may carry it
|
||||||
|
# purely for the application's benefit, its own link being clock-free. No
|
||||||
|
# value, no code.
|
||||||
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 "HALF_DUPLEX" "CLOCK;BAUD;SERIAL;USART;RX;TX;TIMEOUT;OSCCAL" "" ${ARGN})
|
||||||
if(PB_UNPARSED_ARGUMENTS)
|
if(PB_UNPARSED_ARGUMENTS)
|
||||||
message(FATAL_ERROR "pureboot_add_loader(${name}): unknown arguments ${PB_UNPARSED_ARGUMENTS}")
|
message(FATAL_ERROR "pureboot_add_loader(${name}): unknown arguments ${PB_UNPARSED_ARGUMENTS}")
|
||||||
endif()
|
endif()
|
||||||
@@ -245,6 +302,9 @@ function(pureboot_add_loader name)
|
|||||||
message(FATAL_ERROR "pureboot_add_loader(${name}): ${LIBAVR_MCU} has no hardware USART")
|
message(FATAL_ERROR "pureboot_add_loader(${name}): ${LIBAVR_MCU} has no hardware USART")
|
||||||
endif()
|
endif()
|
||||||
set(_serial_defines PUREBOOT_USART=${PB_USART})
|
set(_serial_defines PUREBOOT_USART=${PB_USART})
|
||||||
|
if(PB_HALF_DUPLEX)
|
||||||
|
list(APPEND _serial_defines PUREBOOT_HALF_DUPLEX)
|
||||||
|
endif()
|
||||||
set(_link usart${PB_USART})
|
set(_link usart${PB_USART})
|
||||||
else()
|
else()
|
||||||
if(PB_SERIAL STREQUAL "auto")
|
if(PB_SERIAL STREQUAL "auto")
|
||||||
@@ -254,6 +314,9 @@ function(pureboot_add_loader name)
|
|||||||
endif()
|
endif()
|
||||||
if(_usart)
|
if(_usart)
|
||||||
set(_link usart0)
|
set(_link usart0)
|
||||||
|
if(PB_HALF_DUPLEX)
|
||||||
|
set(_serial_defines PUREBOOT_HALF_DUPLEX)
|
||||||
|
endif()
|
||||||
else()
|
else()
|
||||||
set(PB_SERIAL software)
|
set(PB_SERIAL software)
|
||||||
endif()
|
endif()
|
||||||
@@ -262,6 +325,15 @@ function(pureboot_add_loader name)
|
|||||||
if(NOT PB_RX)
|
if(NOT PB_RX)
|
||||||
set(PB_RX pb0)
|
set(PB_RX pb0)
|
||||||
endif()
|
endif()
|
||||||
|
if(PB_HALF_DUPLEX)
|
||||||
|
# One-wire: both directions on the RX pin. RX == TX spells
|
||||||
|
# the same deployment directly.
|
||||||
|
if(PB_TX AND NOT PB_TX STREQUAL PB_RX)
|
||||||
|
message(FATAL_ERROR "pureboot_add_loader(${name}): HALF_DUPLEX puts both "
|
||||||
|
"directions on RX (${PB_RX}); TX ${PB_TX} contradicts it")
|
||||||
|
endif()
|
||||||
|
set(PB_TX ${PB_RX})
|
||||||
|
endif()
|
||||||
if(NOT PB_TX)
|
if(NOT PB_TX)
|
||||||
set(PB_TX pb1)
|
set(PB_TX pb1)
|
||||||
endif()
|
endif()
|
||||||
@@ -275,11 +347,30 @@ function(pureboot_add_loader name)
|
|||||||
else()
|
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()
|
endif()
|
||||||
# sw:<RX>,<TX> as port letter and bit, upcased.
|
# sw:<RX>,<TX> as port letter and bit, upcased — with @<n> where
|
||||||
|
# the TX pin is a USART's own TXD, since a harness driving that
|
||||||
|
# link has to know the USART owns the pin until the loader
|
||||||
|
# releases it.
|
||||||
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)
|
||||||
string(TOUPPER "sw:${_rx_pin},${_tx_pin}" _link)
|
string(TOUPPER "sw:${_rx_pin},${_tx_pin}" _link)
|
||||||
string(REPLACE "SW" "sw" _link ${_link})
|
string(REPLACE "SW" "sw" _link ${_link})
|
||||||
|
get_property(_tx0 GLOBAL PROPERTY PUREBOOT_USART0_TX)
|
||||||
|
get_property(_tx1 GLOBAL PROPERTY PUREBOOT_USART1_TX)
|
||||||
|
get_property(_rx0 GLOBAL PROPERTY PUREBOOT_USART0_RX)
|
||||||
|
get_property(_rx1 GLOBAL PROPERTY PUREBOOT_USART1_RX)
|
||||||
|
if(_usart AND PB_TX STREQUAL _tx0)
|
||||||
|
set(_link "${_link}@0")
|
||||||
|
elseif(_usart1 AND PB_TX STREQUAL _tx1)
|
||||||
|
set(_link "${_link}@1")
|
||||||
|
elseif(PB_TX STREQUAL PB_RX AND _usart AND PB_RX STREQUAL _rx0)
|
||||||
|
# One-wire on a USART's RXD: RXEN forces that pin's direction
|
||||||
|
# (§20.7.3), so the driven shared pin is held exactly like a
|
||||||
|
# TXD — the harness models the hold either way.
|
||||||
|
set(_link "${_link}@0")
|
||||||
|
elseif(PB_TX STREQUAL PB_RX AND _usart1 AND PB_RX STREQUAL _rx1)
|
||||||
|
set(_link "${_link}@1")
|
||||||
|
endif()
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
if(NOT PB_BAUD)
|
if(NOT PB_BAUD)
|
||||||
@@ -297,6 +388,13 @@ function(pureboot_add_loader name)
|
|||||||
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()
|
endif()
|
||||||
|
if(DEFINED PB_OSCCAL)
|
||||||
|
math(EXPR _osccal "${PB_OSCCAL}" OUTPUT_FORMAT DECIMAL)
|
||||||
|
if(_osccal LESS 0 OR _osccal GREATER 255)
|
||||||
|
message(FATAL_ERROR "pureboot_add_loader(${name}): OSCCAL ${PB_OSCCAL} is not one byte")
|
||||||
|
endif()
|
||||||
|
list(APPEND _defines PUREBOOT_OSCCAL=${_osccal})
|
||||||
|
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)
|
||||||
@@ -310,9 +408,18 @@ function(pureboot_add_loader name)
|
|||||||
# load-immediate it saves. The set is fitted to the loader's body and has to
|
# load-immediate it saves. The set is fitted to the loader's body and has to
|
||||||
# be re-measured when that body changes: -fno-move-loop-invariants belonged
|
# be re-measured when that body changes: -fno-move-loop-invariants belonged
|
||||||
# here while the command loop carried four transfer bodies and costs bytes
|
# here while the command loop carried four transfer bodies and costs bytes
|
||||||
# now that it carries one.
|
# now that it carries one, and -fno-ivopts is fitted per backend — an
|
||||||
|
# autobaud body needs ivopts to keep the calibration countdown a single
|
||||||
|
# induction variable (without it the counter is duplicated and the
|
||||||
|
# measurement loop runs 9 cycles instead of its contracted 7), while the
|
||||||
|
# fixed-baud bodies still measure smaller with it off.
|
||||||
|
if(PB_SERIAL STREQUAL "autobaud")
|
||||||
|
target_compile_options(${name} PRIVATE
|
||||||
|
-fira-algorithm=priority -fno-tree-ter -fno-split-wide-types)
|
||||||
|
else()
|
||||||
target_compile_options(${name} PRIVATE
|
target_compile_options(${name} PRIVATE
|
||||||
-fno-ivopts -fira-algorithm=priority -fno-tree-ter -fno-split-wide-types)
|
-fno-ivopts -fira-algorithm=priority -fno-tree-ter -fno-split-wide-types)
|
||||||
|
endif()
|
||||||
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}>)
|
||||||
|
|||||||
@@ -2,16 +2,16 @@
|
|||||||
|
|
||||||
A serial bootloader on [libavr](https://git.blackmark.me/avr/libavr), pure by
|
A serial bootloader on [libavr](https://git.blackmark.me/avr/libavr), pure by
|
||||||
constraint: one C++ source, no inline assembly, no global register variables
|
constraint: one C++ source, no inline assembly, no global register variables
|
||||||
(attributes and compiler flags allowed), **512 bytes on every chip libavr
|
(attributes and compiler flags allowed), **a 512-byte slot on every chip
|
||||||
targets — all 37**. The device speaks primitives; every composite — verify,
|
libavr targets — all 37**. The device speaks primitives; every composite — verify,
|
||||||
erase, reset-vector surgery, updating the loader itself — lives in the host
|
erase, reset-vector surgery, updating the loader itself — lives in the host
|
||||||
tool (`pureboot.py`).
|
tool (`pureboot.py`).
|
||||||
|
|
||||||
The image is **position-independent**: control flow is PC-relative, the
|
The image is **position-independent**: control flow is PC-relative, the
|
||||||
transfer paths take wire addresses, the write guard protects the slot the code
|
transfer paths take wire addresses, nothing is flash-resident to address at
|
||||||
is *running* in (from the runtime return address), nothing else is
|
all, and the application jump is an indirect call to an absolute entry. It does
|
||||||
flash-resident to address at all, and the application jump is an indirect call
|
not know which slot it occupies and does not need to. The identical binary
|
||||||
to an absolute entry. The identical binary therefore runs from any slot with
|
therefore runs from any slot with
|
||||||
every command intact, which makes pureboot **its own staging loader**: the host
|
every command intact, which makes pureboot **its own staging loader**: the host
|
||||||
installs the same binary one slot below the resident, jumps into it, and lets
|
installs the same binary one slot below the resident, jumps into it, and lets
|
||||||
it rewrite the resident. The lint holds it to that literally — the image must
|
it rewrite the resident. The lint holds it to that literally — the image must
|
||||||
@@ -19,40 +19,50 @@ come out byte-identical linked at a different base.
|
|||||||
|
|
||||||
## Chips
|
## Chips
|
||||||
|
|
||||||
Sizes are the default configuration: the hardware USART0 at 115200 8N1 on a
|
The Stock column is the default configuration: the hardware USART0 at 115200
|
||||||
16 MHz crystal, or the software UART on RX = PB0 / TX = PB1 at 57600 8N1 on
|
8N1 on a 16 MHz crystal, or the software UART on RX = PB0 / TX = PB1 at
|
||||||
the tinies' RC oscillator (9.6 MHz on the t13s, 8 MHz above). Every axis moves
|
57600 8N1 on the tinies' RC oscillator (9.6 MHz on the t13s, 8 MHz above).
|
||||||
per build — see *Configuration*. The autobaud column is the clock-free build,
|
Every axis moves per build — see *Configuration*. The Autobaud column is the
|
||||||
which is the largest the space produces and the tightest fit in the matrix;
|
worst configuration the space produces for the chip: the clock-free build —
|
||||||
it carries the calibration machinery and no clock at all.
|
it alone carries the calibration machinery — with the `OSCCAL` trim baked
|
||||||
|
and, where the chip has a USART, the link deployed on that USART's own pins,
|
||||||
|
which the loader then has to release (*Pin ownership*). Folding the same
|
||||||
|
build onto a single pin (*One-wire*) measures identically on every chip, so
|
||||||
|
the column covers that twin too. On default pins without the trim the same
|
||||||
|
loaders run 4–10 B smaller.
|
||||||
|
|
||||||
| Chip | Flash | Loader at | Link | Stock | Autobaud |
|
| Chip | Flash | Loader at | Link | Stock | Autobaud |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| ATtiny13, ATtiny13A † | 1 KiB | 0x0200 | software | 402 B | 472 B |
|
| ATtiny13, ATtiny13A † | 1 KiB | 0x0200 | software | 372 B | 460 B |
|
||||||
| ATtiny25 † | 2 KiB | 0x0600 | software | 406 B | 476 B |
|
| ATtiny25 † | 2 KiB | 0x0600 | software | 376 B | 452 B |
|
||||||
| ATtiny45 † | 4 KiB | 0x0e00 | software | 410 B | 480 B |
|
| ATtiny45 † | 4 KiB | 0x0e00 | software | 376 B | 452 B |
|
||||||
| ATtiny85 † | 8 KiB | 0x1e00 | software | 410 B | 480 B |
|
| ATtiny85 † | 8 KiB | 0x1e00 | software | 376 B | 452 B |
|
||||||
| ATmega8, 8A | 8 KiB | 0x1e00 | USART0 | 372 B | 486 B |
|
| ATmega8, 8A | 8 KiB | 0x1e00 | USART0 | 350 B | 480 B |
|
||||||
| ATmega16, 16A | 16 KiB | 0x3e00 | USART0 | 374 B | 490 B |
|
| ATmega16, 16A | 16 KiB | 0x3e00 | USART0 | 352 B | 484 B |
|
||||||
| ATmega32, 32A | 32 KiB | 0x7e00 | USART0 | 374 B | 490 B |
|
| ATmega32, 32A | 32 KiB | 0x7e00 | USART0 | 352 B | 484 B |
|
||||||
| ATmega48, 48A, 48P, 48PA † | 4 KiB | 0x0e00 | USART0 | 400 B | 476 B |
|
| ATmega48, 48A, 48P, 48PA † | 4 KiB | 0x0e00 | USART0 | 366 B | 454 B |
|
||||||
| ATmega88, 88A, 88P, 88PA | 8 KiB | 0x1e00 | USART0 | 410 B | 486 B |
|
| ATmega88, 88A, 88P, 88PA | 8 KiB | 0x1e00 | USART0 | 376 B | 464 B |
|
||||||
| ATmega168, 168A, 168P, 168PA | 16 KiB | 0x3e00 | USART0 | 412 B | 490 B |
|
| ATmega168, 168A, 168P, 168PA | 16 KiB | 0x3e00 | USART0 | 378 B | 468 B |
|
||||||
| ATmega328, 328P | 32 KiB | 0x7e00 | USART0 | 412 B | 490 B |
|
| ATmega328, 328P | 32 KiB | 0x7e00 | USART0 | 378 B | 468 B |
|
||||||
| ATmega164A, 164P, 164PA | 16 KiB | 0x3e00 | USART0 | 412 B | 490 B |
|
| ATmega164A, 164P, 164PA | 16 KiB | 0x3e00 | USART0 | 378 B | 468 B |
|
||||||
| ATmega324A, 324P, 324PA | 32 KiB | 0x7e00 | USART0 | 412 B | 490 B |
|
| ATmega324A, 324P, 324PA | 32 KiB | 0x7e00 | USART0 | 378 B | 468 B |
|
||||||
| ATmega644, 644A, 644P, 644PA | 64 KiB | 0xfe00 | USART0 | 406 B | 484 B |
|
| ATmega644, 644A, 644P, 644PA | 64 KiB | 0xfe00 | USART0 | 372 B | 462 B |
|
||||||
| ATmega1284, 1284P | 128 KiB | 0x1fe00 | USART0 | 432 B | 510 B |
|
| ATmega1284, 1284P | 128 KiB | 0x1fe00 | USART0 | 390 B | 480 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 an image's
|
||||||
is 510 bytes, since the slot's last word is the trampoline.
|
budget is 510 bytes. The last word of the **lower** slot belongs to the host —
|
||||||
|
it is the trampoline holding the application's relocated reset vector, and it
|
||||||
|
is a staging copy's own last word during a self-update — so an image must fit
|
||||||
|
below it. The resident loader has all 512 bytes of the slot it runs in; 510 is
|
||||||
|
what an image must fit so that a copy of it staged one slot down leaves that
|
||||||
|
word alone.
|
||||||
|
|
||||||
The tightest fit in the whole space is the 1284s' autobaud build, 510 of its
|
The tightest fit in the whole space is therefore the 1284s' 480 of their
|
||||||
512 — they alone carry the far-flash machinery (ELPM reads, RAMPZ page
|
512: they alone carry the far-flash machinery (ELPM reads, RAMPZ page
|
||||||
commands) and autobaud alone carries the calibration loop. Everything else has
|
commands) on top of everything the column already stacks. The flash bank
|
||||||
20 B of headroom or more. The flash bank riding in a transfer's selector byte
|
riding in a transfer's selector byte keeps even those chips' addressing the
|
||||||
keeps even those chips' addressing the same 16-bit form every other chip uses,
|
same 16-bit form every other chip uses, which is why they are no longer the
|
||||||
which is why they are no longer the outlier they were.
|
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.
|
||||||
@@ -71,6 +81,8 @@ repo's build and by a downstream project alike:
|
|||||||
| `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 |
|
||||||
|
| `OSCCAL <byte>` | a measured oscillator trim, applied before anything runs | none — no value, no code |
|
||||||
|
| `HALF_DUPLEX` | one-wire: both directions on one line (*One-wire* below) | off |
|
||||||
|
|
||||||
The default baud is the fastest of 115200/57600/38400/19200/9600 the clock
|
The default baud is the fastest of 115200/57600/38400/19200/9600 the clock
|
||||||
reaches within 2.5 % — the same U2X-included divisor search libavr's baud
|
reaches within 2.5 % — the same U2X-included divisor search libavr's baud
|
||||||
@@ -79,6 +91,14 @@ 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.
|
||||||
|
|
||||||
|
Putting a bit-banged link on a USART's own pins is a supported deployment, and
|
||||||
|
the usual one where a board's USB bridge is wired to RXD/TXD: the link's `init`
|
||||||
|
clears that USART's `UCSRnB` first, because while its `TXEN` is set the USART —
|
||||||
|
not the port register — owns the TX pin, and a loader entered from an
|
||||||
|
application that left it enabled would receive and obey while answering nothing
|
||||||
|
(§20.6.3). It costs one store — four bytes on the extended-I/O chips, two on
|
||||||
|
the classic megas — and only on those pins.
|
||||||
|
|
||||||
`SERIAL autobaud` takes neither: the loader **measures** the host's bit timing
|
`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
|
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
|
binary per chip serves every clock and every rate. It is for the deployments
|
||||||
@@ -88,17 +108,79 @@ 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
|
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
|
needs its divisor programmed) and that activation counts poll iterations rather
|
||||||
than seconds, since there is no clock to convert them against
|
than seconds, since there is no clock to convert them against
|
||||||
(`PUREBOOT_AUTOBAUD_POLLS`, default 4,000,000).
|
(`PUREBOOT_AUTOBAUD_POLLS`, default 4,000,000). The wait spends nine cycles a
|
||||||
|
poll (measured, and held by the `pureboot.window.autobaud` gate), so the
|
||||||
|
default window is 36 M cycles: 4.5 s at 8 MHz, 3.75 s at 9.6 MHz, 36 s at
|
||||||
|
1 MHz.
|
||||||
|
|
||||||
|
**Pick the rate by cycles a bit, and leave the oscillator room.** What the
|
||||||
|
calibration can measure is bounded by how many clock cycles one bit lasts, so a
|
||||||
|
rate is only ever sensible relative to the clock. Two different floors matter:
|
||||||
|
|
||||||
|
| | cycles a bit |
|
||||||
|
|---|---|
|
||||||
|
| the logic's floor — exact clock, simulated | solid to ~36, fails outright by ~31 (`pureboot.autobaud` gates a point here) |
|
||||||
|
| a factory-trimmed internal RC, measured on an ATtiny13A | reliable at ~118; already locking 1 attempt in 5 by ~59 |
|
||||||
|
|
||||||
|
The gap is the oscillator's own jitter, and no exact-clock simulation shows it.
|
||||||
|
So on an RC part, **budget about 100 cycles a bit** — the same order as the
|
||||||
|
fixed-baud software receiver's floor — rather than the logic's ~36. Measured
|
||||||
|
envelope on that ATtiny13A, with an application resident: 9.6 and 4.8 MHz reach
|
||||||
|
115200, 1.2 MHz reaches 9600, 600 kHz reaches 4800, 128 kHz reaches 2400.
|
||||||
|
|
||||||
|
One trap in testing this: on a patched-vector chip an **erased** application
|
||||||
|
region walks straight back up into the loader, so every expired window opens
|
||||||
|
another one and the host's retries eventually catch the pulse. That reads as far
|
||||||
|
more reliable than the same part with an application resident, which gets one
|
||||||
|
window per reset. Measure with an application in place.
|
||||||
|
|
||||||
|
## One-wire
|
||||||
|
|
||||||
|
`HALF_DUPLEX` puts both directions on one line — the deployment for a board
|
||||||
|
with a single spare pin, or a native-UART bootloader's shared-line wiring.
|
||||||
|
Each backend has its shape:
|
||||||
|
|
||||||
|
- **Software and autobaud links** fold onto the RX pin (`RX == TX` spells
|
||||||
|
the same deployment directly). The pin idles as the receiver's pull-up
|
||||||
|
input; each transmitted frame takes the pin's direction and hands it back
|
||||||
|
with the stop bit's level already on the pull-up, so neither flip makes
|
||||||
|
an edge. This costs nothing: the frame's direction wrap is exactly what
|
||||||
|
the dropped second-pin init paid, and the tightest image in the space —
|
||||||
|
the 1284s' autobaud + `OSCCAL` on their USART's RXD — measures the same
|
||||||
|
502 bytes one-wire as two-wire. On a USART's own pin the release applies
|
||||||
|
as ever, RXD included: `RXEN` forces that pin's direction (§20.7.3),
|
||||||
|
which a receive-only link could live with and a driven shared pin cannot.
|
||||||
|
- **The hardware USART** (`SERIAL hardware`/`auto` + `HALF_DUPLEX`) uses
|
||||||
|
libavr's `.half_duplex` turn-around — exactly one direction enabled at a
|
||||||
|
time, each written byte held to transmit-complete before the line can be
|
||||||
|
released — and needs RXD and TXD tied together off-chip. It costs
|
||||||
|
+42…50 B over the stock loader (m8 404, m328P 440, 1284P 460 — all far
|
||||||
|
inside the slot); the activation window is unchanged, its poll merely
|
||||||
|
runs through the release-line test (18 cycles a poll in bit-addressable
|
||||||
|
I/O, 22 in extended — measured, and held per chip by
|
||||||
|
`pureboot.window.halfduplex`).
|
||||||
|
|
||||||
|
Host wiring, for an FTDI-style adapter: **adapter TX through ~1 kΩ to the
|
||||||
|
line, adapter RX and the MCU pin directly on it.** The resistor lets the MCU
|
||||||
|
win the line while it answers; the price is that the adapter reads back every
|
||||||
|
byte it transmits. `pureboot.py --one-wire` consumes that echo byte for byte
|
||||||
|
— a missing echo is reported as the wiring fault it is, and a device reply
|
||||||
|
that lands between the echoes of the knock (a loader already in session
|
||||||
|
re-prompts mid-knock) is held for the reader. The knock is the protocol's
|
||||||
|
one blind multi-byte write, so on real wiring its second byte can be lost to
|
||||||
|
that collision outright; the tool's knock retries absorb it. Everything else
|
||||||
|
is ack-paced and cannot collide.
|
||||||
|
|
||||||
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
|
||||||
software UART on hand-picked pins, say:
|
software UART on hand-picked pins, say. A submodule pins the loader version
|
||||||
|
(the tags name them; this repo pins its own libavr the same way), where
|
||||||
|
FetchContent tracks whatever `main` is:
|
||||||
|
|
||||||
```cmake
|
```cmake
|
||||||
FetchContent_Declare(bootloader GIT_REPOSITORY git@git.blackmark.me:avr/bootloader.git GIT_TAG main)
|
# git submodule add <forge>/avr/bootloader.git bootloader — or FetchContent
|
||||||
FetchContent_MakeAvailable(bootloader)
|
add_subdirectory(bootloader/pureboot pureboot)
|
||||||
add_subdirectory(${bootloader_SOURCE_DIR}/pureboot pureboot)
|
|
||||||
|
|
||||||
pureboot_add_loader(myboot CLOCK 1000000 SERIAL software TX pb1 RX pb5)
|
pureboot_add_loader(myboot CLOCK 1000000 SERIAL software TX pb1 RX pb5)
|
||||||
```
|
```
|
||||||
@@ -144,18 +226,79 @@ 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
|
build counts poll iterations instead (`PUREBOOT_AUTOBAUD_POLLS`), there being
|
||||||
no clock to turn into seconds.
|
no clock to turn into seconds.
|
||||||
|
|
||||||
|
### Entering from a running application
|
||||||
|
|
||||||
|
A window opens at a reset and nowhere else, which assumes a reset edge exists —
|
||||||
|
a button, or DTR wired to it. On a board with neither, the way in has to come
|
||||||
|
from the application, by jumping to the loader base.
|
||||||
|
|
||||||
|
Deciding *when* to jump is the application's business and deliberately not
|
||||||
|
pureboot's: a console command, a held pin, a magic byte, an idle timeout — each
|
||||||
|
board's answer differs, and none of them belongs in a 512-byte loader. pureboot
|
||||||
|
offers no knock detector and no consumable header for this. What follows is the
|
||||||
|
handful of non-obvious mechanics.
|
||||||
|
|
||||||
|
Give the linker the base as a symbol rather than casting a literal to a
|
||||||
|
function pointer, so the address is stated once and in the same units the
|
||||||
|
loader is linked at:
|
||||||
|
|
||||||
|
```cmake
|
||||||
|
target_link_options(app PRIVATE "LINKER:--defsym=pureboot_loader=0x7e00")
|
||||||
|
```
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
extern "C" [[noreturn]] void pureboot_loader();
|
||||||
|
|
||||||
|
// ... then, with interrupts off and the watchdog disabled:
|
||||||
|
pureboot_loader();
|
||||||
|
```
|
||||||
|
|
||||||
|
That is the whole jump: an ordinary call to an absolute symbol, which the
|
||||||
|
linker resolves and `-mrelax` shortens where it can — `call 0x7e00` in four
|
||||||
|
bytes, or an `rjmp` on a part small enough for one.
|
||||||
|
|
||||||
|
It is worth saying what *not* to copy here, because pureboot's own hand-over
|
||||||
|
(`run_app()`) looks different: it launders its target through a
|
||||||
|
`[[gnu::noipa]]` indirect call. That is a position-independence measure, and it
|
||||||
|
belongs to the loader alone — the same image runs from either slot, so it must
|
||||||
|
never bake an absolute address. An application is linked at a fixed base and
|
||||||
|
has no such problem; using the indirect form costs two `ldi`s and a helper call
|
||||||
|
to reach the same place a plain call reaches in four bytes.
|
||||||
|
|
||||||
|
Three things must be true before the jump:
|
||||||
|
|
||||||
|
- **Interrupts off and the watchdog disabled.** The loader is polled and
|
||||||
|
vector-less; an ISR landing in it vectors into the application's table.
|
||||||
|
- **WDRF clear.** pureboot hands straight back to the application on a watchdog
|
||||||
|
reset (above), and it only *peeks* MCUSR — so a jump arriving with WDRF still
|
||||||
|
set opens no window at all.
|
||||||
|
- **Release any peripheral holding the link.** A loader entered by a jump
|
||||||
|
inherits the application's registers rather than reset values: with `TXEN0`
|
||||||
|
still set the USART owns TxD, and a bit-banging loader then receives
|
||||||
|
perfectly and answers into a pin it does not control. Clearing `UCSR0B`
|
||||||
|
before jumping is the whole fix, and the symptom without it is a loader that
|
||||||
|
is mute rather than deaf, which reads as a dead board.
|
||||||
|
|
||||||
## Session
|
## Session
|
||||||
|
|
||||||
After the knock the loader stays in its command loop until `J` jumps away or
|
After the knock the loader stays in its command loop until a jump takes it away
|
||||||
the chip resets. Before reading each command it waits for any pending EEPROM
|
or the chip resets. Before reading each command it waits for any pending EEPROM
|
||||||
write and sends the prompt `+` (0x2b), which is therefore also the previous
|
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.
|
verdict, then its reply, repeat.
|
||||||
|
|
||||||
|
There is no invalid opcode: every byte begins a command, and it is the seal
|
||||||
|
rather than a table of known letters that rejects noise. The knock is the one
|
||||||
|
thing that must survive being sent into a loader already in session, which is
|
||||||
|
why both its bytes — `p` and `b` — carry the identify bit: they answer the
|
||||||
|
identity and consume nothing else.
|
||||||
|
|
||||||
Addresses are **byte addresses within a 64 KiB bank**, and the bank rides in
|
Addresses are **byte addresses within a 64 KiB bank**, and the bank rides in
|
||||||
the command's selector byte, so no command has to speak word addresses. `J` is
|
the command's selector byte, so no command has to speak word addresses. The jump
|
||||||
the exception: it takes a word address, because that is what the hardware's own
|
is the exception: its address is a word address, because that is what the
|
||||||
jump takes. EEPROM and data-space addresses and all counts are bytes.
|
hardware's own jump takes — it still carries a selector byte (reserved,
|
||||||
|
ignored) so its decode is the same three reads as every other command's.
|
||||||
|
EEPROM and data-space addresses and all counts are bytes.
|
||||||
|
|
||||||
The loader trusts the host to keep addresses in range: it does not bound them
|
The loader trusts the host to keep addresses in range: it does not bound them
|
||||||
against the chip. **Gotcha:** a write (or read) that runs past `E2END` wraps —
|
against the chip. **Gotcha:** a write (or read) that runs past `E2END` wraps —
|
||||||
@@ -164,22 +307,38 @@ 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
|
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.
|
better spent on features than on re-checking a bound the host already holds.
|
||||||
|
|
||||||
| Cmd | Arguments | Reply |
|
Every command but the identity has **one header shape** — six bytes, the last
|
||||||
|---|---|---|
|
of them a seal over the other five:
|
||||||
| `b` | — | 4 bytes: the pureboot version, then the three signature bytes |
|
|
||||||
| `G` | sel8, addr16, n8 | n bytes from the selected space (n = 0 means 256) |
|
|
||||||
| `g` | sel8, addr16, n8, then n data bytes | `+` per byte, sent once its write has begun |
|
|
||||||
| `W` | sel8, addr16, then one page of data | — (completion = next prompt) |
|
|
||||||
| `J` | word address (16-bit) | `+`, then execution continues there |
|
|
||||||
| other | — | ignored; the loop re-prompts (send a junk byte, await `+`, to resync) |
|
|
||||||
|
|
||||||
`G` and `g` are one letter in two cases, which is the whole command set for
|
```text
|
||||||
every memory: the **selector** byte's low nibble names the space and its high
|
op8 sel8 addr_lo8 addr_hi8 n8 seal8
|
||||||
nibble carries the flash bank.
|
seal = op ^ sel ^ addr_lo ^ addr_hi ^ n ^ 0x5A
|
||||||
|
```
|
||||||
|
|
||||||
|
The loader folds those fields and compares before it decodes what the command
|
||||||
|
is, then **answers the seal**: `+` accepts, `0xD4` refuses and nothing has
|
||||||
|
happened. The verdict comes before any payload, which is what keeps a refusal
|
||||||
|
local — a fill or a write burst sends its data only once the header is in, so
|
||||||
|
a rejected header never leaves the host pushing bytes into a loader that has
|
||||||
|
gone back to reading commands.
|
||||||
|
|
||||||
|
The opcode is bits, not a letter. A transfer is the absence of the other three,
|
||||||
|
and its direction is the low bit.
|
||||||
|
|
||||||
|
| Opcode | | Arguments after the header | Reply |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 0x00 | read | none | verdict, then n bytes from the selected space (n = 0 means 256), then `+` |
|
||||||
|
| 0x01 | write | n data bytes | verdict, then `+` per byte once its write has begun, then `+` |
|
||||||
|
| 0x04 | fill | one page of data | verdict, then `+` when the page is in |
|
||||||
|
| 0x08 | jump | none | verdict, then execution continues at the word address |
|
||||||
|
| 0x20 | identify | *unsealed, one byte on its own* | 4 bytes: the version, then the three signature bytes, then `+` |
|
||||||
|
|
||||||
|
The **selector** byte's low nibble names the space and its high nibble carries
|
||||||
|
the flash bank.
|
||||||
|
|
||||||
| Space | | |
|
| Space | | |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 0 | flash | read-only here; it is written through `W` and the SPM space |
|
| 0 | flash | read-only here; it is written through the fill and the SPM command |
|
||||||
| 1 | EEPROM | |
|
| 1 | EEPROM | |
|
||||||
| 2 | data | SRAM — and with it the register file and every I/O register, which share the data address space on AVR |
|
| 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 |
|
| 3 | fuse and lock | index 0..3 in the hardware's own Z order: low, lock, extended, high |
|
||||||
@@ -188,18 +347,26 @@ nibble carries the flash bank.
|
|||||||
The data space is worth more than it looks. pureboot keeps **zero static RAM**
|
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
|
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
|
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,
|
stack — which makes a read 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
|
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
|
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
|
side effects (reading UDR clears its flags), which is the host's business to
|
||||||
know.
|
know.
|
||||||
|
|
||||||
Programming a page is therefore `W` to fill the buffer, then a `g` to the SPM
|
Programming a page is therefore a fill to load the buffer, then an SPM command
|
||||||
space for the erase, another for the write, and on a boot-sectioned chip a
|
for the erase, another for the write, and on a boot-sectioned chip a third to
|
||||||
third to re-enable the RWW section — `0x03`, `0x05` and `0x11`, the SPMCSR
|
re-enable the RWW section — `0x03`, `0x05` and `0x11`, the SPMCSR encodings
|
||||||
encodings every part pureboot targets shares. The loader carries no page-commit
|
every part pureboot targets shares. The loader carries no page-commit logic of
|
||||||
logic of its own, and the same primitive reaches every other SPM operation,
|
its own, and the same primitive reaches every other SPM operation, lock bits
|
||||||
lock bits included.
|
included.
|
||||||
|
|
||||||
|
An SPM command has **no data phase**: its SPMCSR byte rides the header's count
|
||||||
|
field, where the seal covers it. That is the whole reason the field is
|
||||||
|
overloaded — a byte arriving behind the header would arrive after the seal had
|
||||||
|
been checked, and the one command that cannot be taken back is exactly the one
|
||||||
|
that must not be decided by an unchecked byte. Setting the lock bits is
|
||||||
|
therefore an ordinary sealed command (`0x09`) rather than something the loader
|
||||||
|
refuses: deliberate is expressible, accidental is not reachable.
|
||||||
|
|
||||||
The SPM store and the SPM instruction must issue within four cycles of each
|
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
|
other (§26.2), which no host can hit across a serial link — so this one
|
||||||
@@ -207,11 +374,21 @@ 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
|
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.
|
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
|
Nothing refuses an address, the loader's own slot included. Through pureboot 8
|
||||||
dropped, so a broken host cannot brick the running copy, while a staged copy
|
a running-slot write was dropped; the seal replaced that guard, because what
|
||||||
one slot lower may rewrite the resident — which is what a self-update is.
|
the guard defended against was a wire fault naming an address, and a wire fault
|
||||||
|
can no longer name one. What it costs is that a host bug aimed at the running
|
||||||
|
slot now lands. What it buys is that a resident copy can write its own slot —
|
||||||
|
which is the only route a self-update has on a chip whose boot section *is* the
|
||||||
|
512-byte slot, where no staged copy can run SPM at all: the resident plants a
|
||||||
|
primitive in its own spare space and an application-side installer drives it.
|
||||||
|
A copy that erases the page it is executing from does not come back, so which
|
||||||
|
page matters; erasing any other page of its own slot it survives. That needs a
|
||||||
|
page the image does not reach into, which the stock builds have and the biggest
|
||||||
|
do not: a 378 B loader on a 128-byte-page mega leaves 384..511 entirely free,
|
||||||
|
while the 480 B autobaud build reaches into it and has none.
|
||||||
|
|
||||||
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 fill 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
|
||||||
@@ -221,15 +398,15 @@ the tinies), so repeating it programs correctly. The host therefore verifies
|
|||||||
every page it writes and rewrites what comes back wrong (three retries, then it
|
every page it writes and rewrites what comes back wrong (three retries, then it
|
||||||
stops).
|
stops).
|
||||||
|
|
||||||
`g` is host-paced: send the next byte only after the previous byte's `+`. Fuse
|
A write is host-paced: send the next byte only after the previous byte's `+`. Fuse
|
||||||
*writing* does not exist — SPM reaches flash and boot lock bits only.
|
*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
|
The jump is the one control-transfer primitive: it runs the application (word 0 or
|
||||||
the trampoline word, both derived from the chip) and moves between loader
|
the trampoline word, both 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.
|
||||||
|
|
||||||
`b` answers with the loader's identity — its version and the chip's signature —
|
Identify 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,
|
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)
|
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
|
follows from the signature, and the host holds that table; the loader derived
|
||||||
@@ -244,7 +421,7 @@ to install a mismatch against.
|
|||||||
|
|
||||||
## Version
|
## Version
|
||||||
|
|
||||||
`b`'s first byte is the **pureboot version** — the loader's one identity
|
The identity's first byte is the **pureboot version** — the loader's one identity
|
||||||
number, and the only way to tell what a deployed loader is. Nothing else is
|
number, and the only way to tell what a deployed loader is. Nothing else is
|
||||||
numbered: the wire protocol has no version, a pureboot version implies it, and
|
numbered: the wire protocol has no version, a pureboot version implies it, and
|
||||||
the host tool holds that map. The tool states the window of loader versions it
|
the host tool holds that map. The tool states the window of loader versions it
|
||||||
@@ -256,7 +433,34 @@ Two generations exist. **1 through 4** speak one session — a 12-byte info bloc
|
|||||||
from `b`, and a command per memory (`R`/`W` flash, `r`/`w` EEPROM, `F` fuses).
|
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
|
**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
|
above; the shipped tool speaks both, choosing on the version it reads, so a
|
||||||
deployed pureboot 4 stays drivable and self-updatable to 5.
|
deployed pureboot 4 stays drivable and self-updatable to 5. **6** changes
|
||||||
|
nothing on the wire: it marks the builds that may carry a baked `OSCCAL` trim
|
||||||
|
(Configuration), so a tool driving an update knows such images exist. **7**
|
||||||
|
moves `J` onto the unified decode — it gains the selector byte the table
|
||||||
|
shows, which older loaders do not read, so the tool sends each form to the
|
||||||
|
version that speaks it — and re-homes the autobaud unit into the GPIOR pair
|
||||||
|
on the chips that have one (Session: what must not be written), which is
|
||||||
|
where `--info`'s measured clock now reads it on those parts. **8** changes
|
||||||
|
nothing on the wire either: it marks the builds whose deployment may be
|
||||||
|
one-wire (*One-wire* above) — the hardware USART's half-duplex turn-around,
|
||||||
|
or a software link folded onto a single pin. The host-side trace is
|
||||||
|
`--one-wire`, the echo discard a shared line requires of any tool driving
|
||||||
|
it. **9** is the third wire change and the largest: bit opcodes in place of
|
||||||
|
the letters, one sealed header shape for every command, and a verdict on that
|
||||||
|
seal before the command runs (*Session* above). It also drops the
|
||||||
|
running-slot write guard, which the seal makes redundant and which was the
|
||||||
|
only thing standing between a resident copy and its own slot. Identify is
|
||||||
|
answered by both knock bytes so version discovery works before the version is
|
||||||
|
known, which is what keeps a deployed pureboot 8 drivable and self-updatable
|
||||||
|
to 9.
|
||||||
|
|
||||||
|
Every closed generation is tagged in this repo at its era's last commit — the
|
||||||
|
commit just before the next version bump, so a tag holds everything its
|
||||||
|
version ever gained — and each tag carries the `libavr/` submodule pinned to
|
||||||
|
the libavr that loader was built against, as the whole libavr era does commit
|
||||||
|
by commit. `git checkout v3 && git submodule update --init libavr` followed by
|
||||||
|
the usual preset build therefore reproduces the v3 loader exactly; the open
|
||||||
|
generation is `main`.
|
||||||
|
|
||||||
Collapsing four command bodies into one transfer loop is what paid for the
|
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
|
version: the data space, the host-issued SPM operations and the fuses now share
|
||||||
@@ -291,7 +495,7 @@ ATmega328P profiles (addresses for its 32 KiB):
|
|||||||
| BOOTSZ | BOOTRST | Behavior |
|
| BOOTSZ | BOOTRST | Behavior |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 256 words (512 B) | programmed | *Standalone*: reset always enters the loader; **self-update impossible** (the staging slot lies outside the boot section, where SPM is disabled). |
|
| 256 words (512 B) | programmed | *Standalone*: reset always enters the loader; **self-update impossible** (the staging slot lies outside the boot section, where SPM is disabled). |
|
||||||
| 512 words (1 KB) | unprogrammed | *Self-update, app-first*: reset always boots the application, which owns all 31.5 KB and must offer its own jump to 0x7e00 to reach the loader (a virgin chip reaches it by reset across erased flash). Updates are power-fail-safe except mid-rewrite of the resident slot itself (no reset path leads to the staging copy then). |
|
| 512 words (1 KB) | unprogrammed | *Self-update, app-first*: reset always boots the application, which owns all 31.5 KB and must offer its own jump to 0x7e00 to reach the loader (a virgin chip reaches it by reset across erased flash; *Entering from a running application* under Activation is how that jump is written). Updates are power-fail-safe except mid-rewrite of the resident slot itself (no reset path leads to the staging copy then). |
|
||||||
| 512 words (1 KB) | programmed | *Self-update, loader-first*: reset lands at 0x7c00 — the staging slot, normally erased, so execution walks up into the loader; during an update it is the staging copy itself, so a mid-rewrite power loss recovers by reset. The loss windows move to the staging install/retire page writes instead (page-write scale). The host keeps `[0x7c00, 0x7e00)` clear of application data (`--force` overrides). |
|
| 512 words (1 KB) | programmed | *Self-update, loader-first*: reset lands at 0x7c00 — the staging slot, normally erased, so execution walks up into the loader; during an update it is the staging copy itself, so a mid-rewrite power loss recovers by reset. The loss windows move to the staging install/retire page writes instead (page-write scale). The host keeps `[0x7c00, 0x7e00)` clear of application data (`--force` overrides). |
|
||||||
|
|
||||||
Applications are flashed unmodified here — word 0 stays the application's own
|
Applications are flashed unmodified here — word 0 stays the application's own
|
||||||
@@ -313,6 +517,18 @@ mega (SPM only executes from the boot section — reflash the .hex), but *runs*
|
|||||||
on a patched-vector chip, and the ordinary `--update-loader` flow re-homes it
|
on a patched-vector chip, and the ordinary `--update-loader` flow re-homes it
|
||||||
into the top slot from there (`pureboot.rehome`).
|
into the top slot from there (`pureboot.rehome`).
|
||||||
|
|
||||||
|
**Fixed-baud on an internal RC oscillator is a deployment risk the build
|
||||||
|
cannot see.** The factory trim is ±10 % where an 8N1 frame survives about
|
||||||
|
±4: a part at the edge answers nothing at the built rate, and the symptom —
|
||||||
|
silence — reads as a wiring fault (a real ATtiny13A measured −5.5 %, outside
|
||||||
|
every standard rate at its own documented default). The **autobaud build is
|
||||||
|
the deployment-proof backend**: it has no rate to miss. Where fixed-baud on
|
||||||
|
RC is wanted anyway, measure first and bake the trim: an autobaud session's
|
||||||
|
`--info` prints the part's true clock from the loader's own measured bit
|
||||||
|
period, OSCCAL moves the oscillator about 1 % per step, and `OSCCAL <byte>`
|
||||||
|
builds the correction in — one build–measure iteration converges. A loader
|
||||||
|
already deployed and silent is diagnosed with `--scan` (Host tool).
|
||||||
|
|
||||||
## Updating the loader
|
## Updating the loader
|
||||||
|
|
||||||
`pureboot.py --update-loader new_pureboot.bin` replaces the resident loader
|
`pureboot.py --update-loader new_pureboot.bin` replaces the resident loader
|
||||||
@@ -320,6 +536,28 @@ 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.
|
||||||
|
|
||||||
|
One thing the image cannot tell the host: **which link it speaks.** The update
|
||||||
|
works by entering copies of the *new* image (steps 3 and 4 below), so a build
|
||||||
|
made for another baud or another backend answers on that one and not on the
|
||||||
|
session's — and 512 bytes of position-independent code carry no header to read
|
||||||
|
it from. Where the new image's link differs, name it:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# a 57600 fixed-baud resident, replaced by an autobaud build
|
||||||
|
pureboot.py --port … --baud 57600 --update-loader ab.bin --staged-autobaud
|
||||||
|
# …or by a 38400 build of the same backend
|
||||||
|
pureboot.py --port … --baud 57600 --update-loader sw38400.bin --staged-baud 38400
|
||||||
|
```
|
||||||
|
|
||||||
|
The host retunes on the open port, so no DTR pulse resets the copy it is talking
|
||||||
|
to. Omit them against a changed link and the update stops after installing the
|
||||||
|
staging copy, saying so and naming this as the cause.
|
||||||
|
|
||||||
|
An `OSCCAL`-baked image is a link change in effect even at an unchanged rate
|
||||||
|
on paper: the staging copy shifts the physical clock the moment its `run()`
|
||||||
|
starts, and from then on speaks exactly what it was built for. Declare it
|
||||||
|
like any other link change — `--staged-baud` with the new build's rate.
|
||||||
|
|
||||||
The preflight refuses an image built for another chip: the stamp every pureboot
|
The preflight refuses an image built for another chip: the stamp every pureboot
|
||||||
binary carries must resolve to the device's own geometry, and the error names
|
binary carries must resolve to the device's own geometry, and the error names
|
||||||
both. Die revisions share their base signature and geometry, so their images
|
both. Die revisions share their base signature and geometry, so their images
|
||||||
@@ -331,7 +569,7 @@ are interchangeable — as the silicon is.
|
|||||||
the host composes the slot's last word as a jump to the resident base, so
|
the host composes the slot's last word as a jump to the resident base, so
|
||||||
even an abandoned staging copy times out into a loader. A loader already
|
even an abandoned staging copy times out into a loader. A loader already
|
||||||
sitting whole in the staging slot is left as the staging copy instead —
|
sitting whole in the staging slot is left as the staging copy instead —
|
||||||
rewriting it would only meet its own running-slot guard.
|
rewriting it in place would be a copy overwriting itself as it runs.
|
||||||
3. `J` enters the staging copy, which rewrites the resident slot. Where a
|
3. `J` enters the staging copy, which rewrites the resident slot. Where a
|
||||||
patched reset vector routes through the resident, the host first re-aims
|
patched reset vector routes through the resident, the host first re-aims
|
||||||
word 0 at the staging copy, so a power loss mid-rewrite still resets into a
|
word 0 at the staging copy, so a power loss mid-rewrite still resets into a
|
||||||
@@ -340,10 +578,15 @@ are interchangeable — as the silicon is.
|
|||||||
content, and the state file is discarded.
|
content, and the state file is discarded.
|
||||||
|
|
||||||
Every phase is idempotent and keyed off the actual flash state, so re-running
|
Every phase is idempotent and keyed off the actual flash state, so re-running
|
||||||
the same command after any interruption resumes and completes. The state file
|
the same command after any interruption resumes and completes — with one
|
||||||
carries the only bytes not recoverable from the device; losing it mid-update
|
qualification, which is the link again: from step 2 on, the copy the re-run has
|
||||||
still completes the update, and the staging region comes back by reflashing
|
to reach is the *new* image, so a resumed run needs the same `--staged-*` as the
|
||||||
the application. A boot-sectioned mega needs its fuses for the preflight — read
|
first one. On a patched-vector part step 3 also re-aims word 0 at the staging
|
||||||
|
copy, so after that point a reset reaches the new image's link and **only** that
|
||||||
|
one; a re-run on the resident's link finds nothing at all. The state file carries
|
||||||
|
the only bytes not recoverable from the device; losing it mid-update still
|
||||||
|
completes the update, and the staging region comes back by reflashing the
|
||||||
|
application. A boot-sectioned mega needs its fuses for the preflight — read
|
||||||
from the device, or supplied with `--assume-fuses` where reading is impossible
|
from the device, or supplied with `--assume-fuses` where reading is impossible
|
||||||
(simulators).
|
(simulators).
|
||||||
|
|
||||||
@@ -364,20 +607,55 @@ update, flash (erase / program / read / verify), EEPROM (the same), then
|
|||||||
`--peek`/`--poke` — then the 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 the fill above).
|
||||||
`--verify-flash` only reports. Images are raw binary, or Intel HEX by
|
`--verify-flash` only reports. Images are raw binary, or Intel HEX by
|
||||||
extension. `--force` overrides the refusable safety checks — today, flashing
|
extension. `--force` overrides the refusable safety checks — today, flashing
|
||||||
application data into a mega's reset walk region.
|
application data into a mega's reset walk region.
|
||||||
|
|
||||||
`--autobaud` opens with the calibration pulse instead of the plain knock, for a
|
`--autobaud` opens with the calibration pulse instead of the plain knock, for a
|
||||||
loader built `SERIAL autobaud`; the rest of the session is identical, at
|
loader built `SERIAL autobaud`; the rest of the session is identical, at
|
||||||
whatever `--baud` the host chose.
|
whatever `--baud` the host chose. Its `--info` adds the **measured clock** —
|
||||||
|
the loader's bit-period unit, decoded and multiplied by the session rate —
|
||||||
|
which is the number an `OSCCAL` bake or a fixed-baud build for the part is
|
||||||
|
held against; `--clock <hz>` states the drift against a nominal.
|
||||||
|
|
||||||
|
`--one-wire` marks the link as a shared line (*One-wire* above): the tool
|
||||||
|
reads back and verifies its own echoed bytes, whatever the backend.
|
||||||
|
It combines with everything, `--scan` included — undiscarded echoes would
|
||||||
|
answer every rate a scan probes.
|
||||||
|
|
||||||
|
`--scan` is the diagnosis once a fixed-baud loader has gone silent: it walks
|
||||||
|
±10 % around `--baud` in 2 % steps, nearest first, one probe per activation
|
||||||
|
window — reset the target as each probe announces itself (a board with DTR
|
||||||
|
wired to reset is pulsed by the probe's own port-open). A loader
|
||||||
|
off-frequency answers at its oscillator's ratio, and the report gives the
|
||||||
|
found rate as the session workaround, the offset, the OSCCAL correction's
|
||||||
|
direction at ~1 % per step, and the autobaud way out. Standalone — no other
|
||||||
|
operation combines with it.
|
||||||
|
|
||||||
`--peek ADDR[:N]` and `--poke ADDR:HEX` reach the data space (pureboot 5) —
|
`--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
|
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
|
register. Reading an I/O register can have side effects (reading UDR clears its
|
||||||
flags), which is the caller's business to know.
|
flags), which is the caller's business to know.
|
||||||
|
|
||||||
|
Reads are safe anywhere; **two small regions cannot be written without ending the
|
||||||
|
session,** because they are what the loader is standing on:
|
||||||
|
|
||||||
|
- the **top of SRAM**, where its stack lives — a handful of bytes below RAMEND;
|
||||||
|
- on an **autobaud** build, the **measured bit period**: two bytes in
|
||||||
|
GPIOR2:GPIOR1 where the chip has the pair (data `0x32..0x33` on the
|
||||||
|
t25/45/85, `0x4A..0x4B` from the x8 generation on — such a loader has *no*
|
||||||
|
static RAM at all), and the two bytes at RAMSTART on the chips without one
|
||||||
|
(the t13s and classic megas), where they are the whole of the loader's
|
||||||
|
static RAM. Overwrite either home and the next reply is timed against
|
||||||
|
garbage — the symptom is a mangled prompt byte rather than any error; the
|
||||||
|
loader is fine, it simply is no longer speaking the agreed rate.
|
||||||
|
|
||||||
|
Both are self-inflicted rather than defects, and a reset clears them. Note also
|
||||||
|
that `--poke` can write OSCCAL, which does take effect — but a session can only
|
||||||
|
survive a step or two of it before the clock walks the link out of the rate
|
||||||
|
autobaud locked to, and OSCCAL reverts on reset regardless.
|
||||||
|
|
||||||
Readouts come one fact per line: `--info` prints the device's version and
|
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
|
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
|
plus, on a boot-sectioned mega, its decoded meaning. Transfers that take wire time draw a transient progress bar on stderr
|
||||||
@@ -386,8 +664,11 @@ counts, the programming plan, update state handling and per-phase page counts.
|
|||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
`tools/check.sh` runs every chip's workflow (`--full` adds the reflect-mode
|
libavr rides as the `libavr/` submodule (`git submodule update --init libavr`);
|
||||||
builds of libavr's spot set; `tools/make_presets.py` regenerates the presets).
|
`LIBAVR_ROOT` (cache or environment) overrides it for tandem development
|
||||||
|
against a working tree. `tools/check.sh` runs every chip's workflow (`--full`
|
||||||
|
adds the reflect-mode builds of libavr's spot set; `tools/make_presets.py`
|
||||||
|
regenerates the presets).
|
||||||
Per chip preset, `ctest` runs:
|
Per chip preset, `ctest` runs:
|
||||||
|
|
||||||
- `pureboot.size` — the 510-byte (patched-vector) / 512-byte budget;
|
- `pureboot.size` — the 510-byte (patched-vector) / 512-byte budget;
|
||||||
@@ -397,11 +678,19 @@ Per chip preset, `ctest` runs:
|
|||||||
the fastest clock — where a software UART's per-bit spin outgrows its
|
the fastest clock — where a software UART's per-bit spin outgrows its
|
||||||
one-register delay loop and takes the 16-bit one. That is the largest image
|
one-register delay loop and takes the 16-bit one. That is the largest image
|
||||||
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 an axis for one reason
|
||||||
the timeout is a constant: neither is an axis;
|
only, and it is enough: a bit-banged link on a USART's own pins has to
|
||||||
|
release that USART, so `pureboot_{sw,autobaud}_on_usart{0,1}` build there
|
||||||
|
too. The timeout is a constant and is no axis;
|
||||||
- `pureboot_autobaud.size` — the clock-free build, which has no clock or baud
|
- `pureboot_autobaud.size` — the clock-free build, which has no clock or baud
|
||||||
axis of its own: one binary per chip has to serve every point the matrix
|
axis of its own: one binary per chip has to serve every point the matrix
|
||||||
below sweeps;
|
below sweeps. `pureboot*osccal*.size` add the `OSCCAL` trim on the stock
|
||||||
|
shape and on the tightest image in the space (autobaud on a USART's own
|
||||||
|
pins), holding both of the trim write's addressing encodings to the budget;
|
||||||
|
- `pureboot_autobaud.unit` — the measured bit period sits where `--info`
|
||||||
|
reads it (wire contract, not layout accident): in the GPIOR pair, with no
|
||||||
|
RAM object at all, on the chips that have one; as the loader's only RAM
|
||||||
|
object at exactly ram_start elsewhere;
|
||||||
- `pbm_*.size` — with `PUREBOOT_FULL_MATRIX=1`, the exhaustive cross product
|
- `pbm_*.size` — with `PUREBOOT_FULL_MATRIX=1`, the exhaustive cross product
|
||||||
replacing that compact matrix, on **every** chip: every plausible oscillator
|
replacing that compact matrix, on **every** chip: every plausible oscillator
|
||||||
(the internal ones, the CKDIV8 floor, the plain and the UART crystals) ×
|
(the internal ones, the CKDIV8 floor, the plain and the UART crystals) ×
|
||||||
@@ -412,12 +701,27 @@ Per chip preset, `ctest` runs:
|
|||||||
flash-resident section but `.text`, and the image byte-identical when linked
|
flash-resident section but `.text`, and the image byte-identical when linked
|
||||||
at a different base — which is position independence itself rather than a
|
at a different base — which is position independence itself rather than a
|
||||||
proxy for it;
|
proxy for it;
|
||||||
|
- `pureboot.handshake` — the host tool's activation must not hang on a target
|
||||||
|
that never falls quiet: the drain after a prompt is bounded by the handshake
|
||||||
|
deadline, and a well-behaved loader still connects;
|
||||||
|
- `pureboot.updatelink` — an update whose image changes the baud or the backend
|
||||||
|
must follow the staging copy onto *its* link, since that copy is the new image;
|
||||||
|
and where nothing was declared, the failure must name the link rather than
|
||||||
|
report a bare activation timeout, because by then the staging slot is written
|
||||||
|
and on a 1 KiB tiny that was the application;
|
||||||
- `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
|
||||||
verify against a fake device;
|
verify against a fake device;
|
||||||
|
- `pureboot.scan` — `--scan`'s walk and report logic: the probe order, the
|
||||||
|
rate arithmetic, and the trim advice's direction. A pty carries bytes at
|
||||||
|
any termios rate, so the rate physics itself belongs to the hardware
|
||||||
|
harness, and what the wire would arbitrate is pinned as logic;
|
||||||
|
- `presets.generated` — CMakePresets.json matches its generator
|
||||||
|
(`tools/make_presets.py --check`), so a hand edit or a generator change
|
||||||
|
cannot drift the pair apart;
|
||||||
- `pureboot.protocol` — end to end against a simavr device
|
- `pureboot.protocol` — end to end against a simavr device
|
||||||
(`test/pureboot_device.c`: a hardware USART as a pty, or a cycle-timed
|
(`test/pureboot_device.cpp`: a hardware USART as a pty, or a cycle-timed
|
||||||
GPIO⇄pty bridge for a software-UART build, plus the SPM/NVM module simavr's
|
GPIO⇄pty bridge for a software-UART build, plus the SPM/NVM module simavr's
|
||||||
tiny cores lack) driven by the real host tool through knock-from-reset,
|
tiny cores lack) driven by the real host tool through knock-from-reset,
|
||||||
program + verify of both memories, session reconnect, an external reset
|
program + verify of both memories, session reconnect, an external reset
|
||||||
@@ -434,6 +738,12 @@ Per chip preset, `ctest` runs:
|
|||||||
- `pureboot.usart1` (644A) — the same suite over the second hardware USART:
|
- `pureboot.usart1` (644A) — the same suite over the second hardware USART:
|
||||||
instance selection is compile-checked everywhere, but only a live session
|
instance selection is compile-checked everywhere, but only a live session
|
||||||
proves the loader polls the USART it claims;
|
proves the loader polls the USART it claims;
|
||||||
|
- `pureboot.mute` (328P) — a software link on USART0's own pins, entered from an
|
||||||
|
application that handed over with that USART still enabled: the loader must
|
||||||
|
still answer, which it does only because it releases it. The pin ownership is
|
||||||
|
the runner's, not simavr's — simavr wires a USART through IRQs and never takes
|
||||||
|
the pin from the port, so without that model the state under test could not
|
||||||
|
arise at all;
|
||||||
- `pureboot.dirty` (328P) — entering the loader from a running application over
|
- `pureboot.dirty` (328P) — entering the loader from a running application over
|
||||||
an SPM buffer it deliberately dirtied, the case the loader declines to guard:
|
an SPM buffer it deliberately dirtied, the case the loader declines to guard:
|
||||||
a bare verify must see the corruption and the repairing verify must fix it in
|
a bare verify must see the corruption and the repairing verify must fix it in
|
||||||
@@ -442,12 +752,58 @@ Per chip preset, `ctest` runs:
|
|||||||
- `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.osccal` (328P, t85) — a loader built with the `OSCCAL` axis holds
|
||||||
|
the trim register at the built byte from its first prompt, observed through
|
||||||
|
the wire on one chip per addressing encoding (`sts` and low-I/O `out`);
|
||||||
- `pureboot.autobaud` (328P, 1284P) — the clock-free build over the GPIO⇄pty
|
- `pureboot.autobaud` (328P, 1284P) — the clock-free build over the GPIO⇄pty
|
||||||
bridge: the calibration handshake, a flash + EEPROM + fuse round trip against
|
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
|
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
|
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
|
exists for. The measured clock `--info` prints is asserted against the
|
||||||
|
simulator's exact clock, inside the unit encoding's own envelope, at both
|
||||||
|
points. A lone calibration pulse with no knock behind it must still let
|
||||||
the application boot, so no wait in activation can be unbounded.
|
the application boot, so no wait in activation can be unbounded.
|
||||||
|
|
||||||
`size`, `pi` and `planner` are host logic and run anywhere; the
|
`size`, `unit`, `pi`, `planner`, `scan` and `handshake` are host logic and run
|
||||||
simulator-driven targets need simavr and a pty, so they are POSIX-only.
|
anywhere; the simulator-driven targets need simavr and a pty, so they are
|
||||||
|
POSIX-only.
|
||||||
|
|
||||||
|
## Hardware
|
||||||
|
|
||||||
|
The suite above proves the protocol on every chip; it cannot prove a *board*.
|
||||||
|
Two things live only on silicon: an RC oscillator that is not on its nominal, and
|
||||||
|
a reset edge that has to come from somewhere. `tools/pbrig.py` and
|
||||||
|
`tools/pbhw.py` cover that, and know nothing per-board — every deployment fact
|
||||||
|
is a flag or a `PUREBOOT_*` environment variable.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
export PUREBOOT_PROGRAMMER=atmelice_isp PUREBOOT_PART=t13 PUREBOOT_PORT=COM6
|
||||||
|
tools/pbrig.py backup rig-backup/ # verified, before anything is written
|
||||||
|
tools/pbhw.py --autobaud --loader build/ab.bin --app build/pbapp.hex --marker APP
|
||||||
|
```
|
||||||
|
|
||||||
|
`pbrig.py` is the primitives — `signature`, `reset`, `flash`, `fuses`, `backup`,
|
||||||
|
`rate` — and the module `pbhw.py` builds on. Two rig facts are encoded in it
|
||||||
|
because neither is guessable: an **ISP access is the reset edge** (the part runs
|
||||||
|
the moment the programmer releases it, which is the only edge available when the
|
||||||
|
adapter's DTR is not wired to reset, so a session begins with an ISP touch and
|
||||||
|
knocks immediately after), and **avrdude splits `-U` on colons**, so a Windows
|
||||||
|
path's drive letter breaks the spec and every file is passed as a bare name with
|
||||||
|
avrdude run in its own directory.
|
||||||
|
|
||||||
|
`pbrig.py rate` is the one that turns "the loader is silent, so the wiring must
|
||||||
|
be wrong" into a number. Against a fixture built with `PUREBOOT_HEARTBEAT` — a
|
||||||
|
*fixed* cycles-per-bit transmitter — it sweeps the host rate, and the band where
|
||||||
|
the marker still decodes brackets the part's true bit rate; with the clock the
|
||||||
|
image was built for, that is the clock the part is really running at. No
|
||||||
|
instrument beyond the adapter already attached. An ATtiny13A measured this way
|
||||||
|
came out at 9.072 MHz against its 9.6 MHz nominal, −5.5 % — inside the
|
||||||
|
datasheet's ±10 % and outside what an 8N1 frame survives, which is the whole
|
||||||
|
case for the autobaud backend on such a part.
|
||||||
|
|
||||||
|
`pbhw.py` takes its bounds from the info block the loader reports, so one run
|
||||||
|
covers a 1 KiB tiny and a 128 KiB mega alike: identity, the EEPROM round trip
|
||||||
|
and erase, an application flashed and verified and then *seen running*, the
|
||||||
|
application region read and erased, the loader slot proven intact across that
|
||||||
|
erase by an independent ISP read, and an oversized image refused. It overwrites
|
||||||
|
the application flash and EEPROM, which is why `backup` comes first.
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
// pureboot — a serial bootloader on libavr: one C++ source, no inline
|
// pureboot — a serial bootloader on libavr: one C++ source, no inline
|
||||||
// assembly, no global register variables, 512 bytes on every chip libavr
|
// assembly, no global register variables, a 512-byte slot on every chip
|
||||||
// targets. The device speaks primitives; every composite (verify, erase,
|
// libavr targets. The device speaks primitives; every composite (verify, erase,
|
||||||
// reset-vector surgery, self-update) lives in the host tool. Protocol,
|
// reset-vector surgery, self-update) lives in the host tool. Protocol,
|
||||||
// deployment and configuration: README.md next to this file.
|
// deployment and configuration: README.md next to this file.
|
||||||
//
|
//
|
||||||
// The image is position-independent — PC-relative control flow, wire
|
// The image is position-independent — PC-relative control flow and wire
|
||||||
// addresses in, the write guard and the info block both anchored on the
|
// addresses in, no absolute address formed anywhere — so the identical binary
|
||||||
// runtime return address — so the identical binary runs from any slot. That
|
// runs from any slot. That is what makes a copy one slot below able to rewrite
|
||||||
// is what makes a copy one slot below able to rewrite the resident one, and
|
// the resident one, and every change here has to keep it (test/check_pi.py).
|
||||||
// every change here has to keep it (test/check_pi.py).
|
// It does not need to know *which* slot it is in: nothing here refuses an
|
||||||
|
// address, so there is no running-slot comparison to anchor.
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
#include <libavr/libavr.hpp>
|
#include <libavr/libavr.hpp>
|
||||||
|
|
||||||
@@ -24,6 +27,37 @@ constexpr auto off = avr::irq::guard_policy::unused;
|
|||||||
|
|
||||||
constexpr std::uint8_t ack = '+';
|
constexpr std::uint8_t ack = '+';
|
||||||
|
|
||||||
|
// The refusal, which is the ack inverted: on a link whose whole problem is
|
||||||
|
// flipped bits, the byte saying "nothing happened" should be as far as a byte
|
||||||
|
// can be from the one saying "it did", and the complement is all eight bits.
|
||||||
|
// It is also the only spelling that needs no justifying — every other value
|
||||||
|
// would be a choice.
|
||||||
|
constexpr std::uint8_t nak = static_cast<std::uint8_t>(~ack);
|
||||||
|
|
||||||
|
// What a command's fields must fold to. Any non-zero constant does: zero is
|
||||||
|
// what a run of one repeated byte folds to, and a repeated byte is the shape of
|
||||||
|
// both a line stuck at a level and a page of erased flash arriving where a
|
||||||
|
// header belongs.
|
||||||
|
constexpr std::uint8_t seal = 0x5a;
|
||||||
|
|
||||||
|
// The opcode, as bits rather than letters. Each is a one-instruction skip,
|
||||||
|
// where a set of arbitrary values costs a compare and a branch apiece — and
|
||||||
|
// with the seal deciding what is a command at all, there is nothing left for a
|
||||||
|
// readable spelling to buy. A transfer is the absence of the other three, and
|
||||||
|
// its direction is the low bit.
|
||||||
|
//
|
||||||
|
// Identify is bit 5 for one reason: 'p' and 'b' both carry it, and those are
|
||||||
|
// the knock. Two things follow that no other assignment gives. A host cannot
|
||||||
|
// know which generation it is talking to until something has answered, so the
|
||||||
|
// command reporting the version has to mean the same thing before the version
|
||||||
|
// is known — 'b' still asks it. And a knock aimed at a loader that is
|
||||||
|
// *already* in session has to stay harmless: with no opcode reserved as
|
||||||
|
// invalid, every byte now starts a command, so a knock that meant nothing to
|
||||||
|
// earlier generations would otherwise consume the five header bytes behind it
|
||||||
|
// and put the stream out of step. Answering both knock bytes with the identity
|
||||||
|
// keeps the reconnect exactly as cheap as it was.
|
||||||
|
enum : std::uint8_t { op_write = 1, op_fill = 4, op_jump = 8, op_identify = 0x20 };
|
||||||
|
|
||||||
// Deployment parameters come from the build (pureboot_add_loader()). The
|
// Deployment parameters come from the build (pureboot_add_loader()). The
|
||||||
// signature is not one of them: the chip database is the only universal
|
// signature is not one of them: the chip database is the only universal
|
||||||
// source — a tiny13A cannot read its own signature row from code. An autobaud
|
// source — a tiny13A cannot read its own signature row from code. An autobaud
|
||||||
@@ -38,18 +72,11 @@ 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
|
#endif
|
||||||
|
|
||||||
// The watchdog reset flag's home: MCUSR, or the classic megas' MCUCSR.
|
|
||||||
consteval std::int16_t wdrf_field()
|
|
||||||
{
|
|
||||||
auto reg = std::string_view{avr::hw::db.regs[static_cast<std::size_t>(avr::power::detail::reset_reg())].name};
|
|
||||||
return avr::hw::db.field_index(reg, "WDRF");
|
|
||||||
}
|
|
||||||
|
|
||||||
// The loader owns the top 512 bytes; a staging copy goes in the slot below.
|
// The loader owns the top 512 bytes; a staging copy goes in the slot below.
|
||||||
// Chips without a hardware boot section — the tinies and the m48s, whose SPM
|
// Chips without a hardware boot section — the tinies and the m48s, whose SPM
|
||||||
// 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. The size itself is the linker's and
|
||||||
constexpr std::uint16_t slot_bytes = 512;
|
// the host's business: nothing in here needs to know where the slot ends.
|
||||||
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();
|
||||||
|
|
||||||
@@ -72,9 +99,17 @@ constexpr std::uint8_t timeout_seconds = PUREBOOT_TIMEOUT;
|
|||||||
#endif
|
#endif
|
||||||
constexpr avr::uint24_t autobaud_budget = PUREBOOT_AUTOBAUD_POLLS;
|
constexpr avr::uint24_t autobaud_budget = PUREBOOT_AUTOBAUD_POLLS;
|
||||||
|
|
||||||
|
// A build may bake a measured oscillator trim (README.md: the RC-oscillator
|
||||||
|
// deployment answer); the byte is applied at the top of run(). Orthogonal to
|
||||||
|
// the serial backend — an autobaud build may carry it for the application's
|
||||||
|
// benefit alone.
|
||||||
|
#if defined(PUREBOOT_OSCCAL)
|
||||||
|
static_assert(PUREBOOT_OSCCAL >= 0 && PUREBOOT_OSCCAL <= 0xff, "PUREBOOT_OSCCAL is one OSCCAL byte");
|
||||||
|
#endif
|
||||||
|
|
||||||
// The loader's one identity number. The protocol carries none of its own —
|
// The loader's one identity number. The protocol carries none of its own —
|
||||||
// a version implies it, and the host tool holds that map (README.md).
|
// a version implies it, and the host tool holds that map (README.md).
|
||||||
constexpr std::uint8_t version = 5;
|
constexpr std::uint8_t version = 9;
|
||||||
|
|
||||||
// The image's identity stamp, for the host tool rather than for the wire: an
|
// The image's identity stamp, for the host tool rather than for the wire: an
|
||||||
// update image is a bare 512-byte slot, and without this nothing in it says
|
// update image is a bare 512-byte slot, and without this nothing in it says
|
||||||
@@ -102,12 +137,18 @@ constexpr std::uint8_t stamp_identity = 2;
|
|||||||
// The address spaces a transfer can name, in a selector byte's low nibble.
|
// The address spaces a transfer can name, in a selector byte's low nibble.
|
||||||
// Flash is 0 so it is the cheapest to select.
|
// Flash is 0 so it is the cheapest to select.
|
||||||
//
|
//
|
||||||
// spm_ops is the one that is not memory: a write there hands its byte to
|
// spm_ops is the one that is not memory: naming it hands the count field to
|
||||||
// SPMCSR and fires the instruction at the transfer's address, which is how
|
// SPMCSR and fires the instruction at the transfer's address, which is how
|
||||||
// page erase, page write and RWW re-enable reach the wire without the loader
|
// page erase, page write and RWW re-enable reach the wire without the loader
|
||||||
// carrying a command for each. The hardware's four-cycle store-to-SPM window
|
// 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
|
// is why this is one fused primitive and not a poke of SPMCSR — no host can
|
||||||
// hit that window across a serial link.
|
// hit that window across a serial link.
|
||||||
|
//
|
||||||
|
// It is the one space with no direction: the opcode's write bit is not
|
||||||
|
// consulted, because a sealed command naming this space says what it means and
|
||||||
|
// there is nothing for the other direction to denote. Testing the bit anyway
|
||||||
|
// would cost six bytes to catch a host contradicting itself, which is the same
|
||||||
|
// trade the running-slot guard lost.
|
||||||
enum : std::uint8_t { sp_flash = 0, sp_eeprom = 1, sp_data = 2, sp_fuse = 3, sp_spm = 4 };
|
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
|
// A selector's high nibble is the flash bank — the address bits above the
|
||||||
@@ -125,22 +166,6 @@ enum : std::uint8_t { sp_flash = 0, sp_eeprom = 1, sp_data = 2, sp_fuse = 3, sp_
|
|||||||
return static_cast<std::uint8_t>(selector >> 4);
|
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 /
|
// 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
|
// 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
|
// software receiver is the polled one: the vector table belongs to the
|
||||||
@@ -154,6 +179,9 @@ constexpr std::uint8_t bank_shift = 16 - slot_shift;
|
|||||||
#if defined(PUREBOOT_AUTOBAUD) && defined(PUREBOOT_USART)
|
#if defined(PUREBOOT_AUTOBAUD) && defined(PUREBOOT_USART)
|
||||||
#error "PUREBOOT_AUTOBAUD measures a software link; it cannot drive a hardware USART"
|
#error "PUREBOOT_AUTOBAUD measures a software link; it cannot drive a hardware USART"
|
||||||
#endif
|
#endif
|
||||||
|
#if defined(PUREBOOT_HALF_DUPLEX) && (defined(PUREBOOT_SOFT_SERIAL) || defined(PUREBOOT_AUTOBAUD))
|
||||||
|
#error "PUREBOOT_HALF_DUPLEX is the hardware USART's one-wire mode; a software link goes one-wire by RX == TX"
|
||||||
|
#endif
|
||||||
#if !defined(PUREBOOT_RX)
|
#if !defined(PUREBOOT_RX)
|
||||||
#define PUREBOOT_RX pb0
|
#define PUREBOOT_RX pb0
|
||||||
#endif
|
#endif
|
||||||
@@ -161,18 +189,50 @@ constexpr std::uint8_t bank_shift = 16 - slot_shift;
|
|||||||
#define PUREBOOT_TX pb1
|
#define PUREBOOT_TX pb1
|
||||||
#endif
|
#endif
|
||||||
#if defined(PUREBOOT_USART)
|
#if defined(PUREBOOT_USART)
|
||||||
constexpr char usart_digit = '0' + PUREBOOT_USART;
|
constexpr int usart_unit = PUREBOOT_USART;
|
||||||
#else
|
#else
|
||||||
constexpr char usart_digit = '0';
|
constexpr int usart_unit = 0;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// One-wire on the hardware USART (PUREBOOT_HALF_DUPLEX): RXD and TXD tied
|
||||||
|
// together off-chip, exactly one direction enabled at a time — the library's
|
||||||
|
// .half_duplex turn-around. The activation window is unchanged; only its
|
||||||
|
// poll grows the release-line test rx_ready() carries in this mode.
|
||||||
|
constexpr bool hw_half_duplex =
|
||||||
|
#if defined(PUREBOOT_HALF_DUPLEX)
|
||||||
|
true;
|
||||||
|
#else
|
||||||
|
false;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
template <avr::hertz_t C, avr::baud_t B>
|
template <avr::hertz_t C, avr::baud_t B>
|
||||||
struct hardware_link {
|
struct hardware_link {
|
||||||
using uart = avr::uart::usart<usart_digit, C, {.baud = B, .max_baud_error = 2.5_pct}>;
|
using uart = avr::uart::usart<usart_unit, C, {.baud = B, .max_baud_error = 2.5_pct, .half_duplex = hw_half_duplex}>;
|
||||||
|
|
||||||
// The compiled idle poll: lds UCSR0A (2), sbrc skipping the exit (2),
|
// The compiled idle poll around the window's narrow (uint24_t) countdown:
|
||||||
// sbiw + sbci + sbci + brne (6).
|
// the RXC test, then sbiw + sbci + brne (5). The test's cost follows the
|
||||||
static constexpr std::uint8_t poll_cycles = 10;
|
// status register's home — a 2-cycle bit-skip where UCSRnA sits in
|
||||||
|
// bit-addressable I/O (the classic megas), lds + skip (4) in extended
|
||||||
|
// I/O. Half-duplex polls through rx_ready()'s release-line test, which
|
||||||
|
// -Os outlines: the rcall (3), the UCSR#B read and not-taken skip with
|
||||||
|
// the jump over the write (I/O 3, extended 5), the ret (4) — and the
|
||||||
|
// call in the loop body pushes the countdown into call-saved registers,
|
||||||
|
// where the uint24_t step is ldi+sub+sbc+sbc (4) instead of sbiw+sbci
|
||||||
|
// (3). Measured off the built loops: 18 a poll in bit-addressable I/O,
|
||||||
|
// 22 in extended. A uint32_t countdown pays one more sbci —
|
||||||
|
// window_polls() adds it where the count forces the wide type. Held per
|
||||||
|
// chip by the pureboot.window gates. The lookup rides the baud parameter
|
||||||
|
// so it stays dependent: the trait is an incomplete type on the
|
||||||
|
// USART-less chips, which parse this template without ever instantiating
|
||||||
|
// it.
|
||||||
|
template <avr::baud_t Baud, typename U = avr::hw::usart_of<usart_unit>>
|
||||||
|
static consteval std::uint8_t poll_cost()
|
||||||
|
{
|
||||||
|
if (hw_half_duplex)
|
||||||
|
return U::ucsra::addr < 0x40 ? 18 : 22;
|
||||||
|
return U::ucsra::addr < 0x40 ? 7 : 9;
|
||||||
|
}
|
||||||
|
static constexpr std::uint8_t poll_cycles = poll_cost<B>();
|
||||||
|
|
||||||
static void init()
|
static void init()
|
||||||
{
|
{
|
||||||
@@ -196,18 +256,26 @@ struct hardware_link {
|
|||||||
|
|
||||||
static void drain()
|
static void drain()
|
||||||
{
|
{
|
||||||
uart::drain();
|
// A drain here always follows this link's own write — the frame is
|
||||||
|
// in flight by construction, so the completion the wait needs is
|
||||||
|
// guaranteed and the bounded default's countdown would be dead bytes.
|
||||||
|
uart::drain_unbounded();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
template <avr::hertz_t C, avr::baud_t B>
|
template <avr::hertz_t C, avr::baud_t B>
|
||||||
struct software_link {
|
struct software_link {
|
||||||
|
// RX == TX is the one-wire deployment: the transmitter becomes a guest
|
||||||
|
// on the receiver's pull-up line, taking the pin's direction for exactly
|
||||||
|
// one frame per byte.
|
||||||
using rx_t = avr::uart::software_rx_polled<C, avr::PUREBOOT_RX, B>;
|
using rx_t = avr::uart::software_rx_polled<C, avr::PUREBOOT_RX, B>;
|
||||||
using tx_t = avr::uart::software_tx<C, avr::PUREBOOT_TX, B>;
|
using tx_t = avr::uart::software_tx<C, avr::PUREBOOT_TX, B, avr::PUREBOOT_RX == avr::PUREBOOT_TX>;
|
||||||
|
|
||||||
// The compiled idle poll: sbis skipping the exit (2), sbiw + sbci +
|
// The compiled idle poll around the window's narrow (uint24_t) countdown:
|
||||||
// sbci + brne (6).
|
// sbis skipping the exit (2), sbiw + sbci + brne (5). A uint32_t
|
||||||
static constexpr std::uint8_t poll_cycles = 8;
|
// countdown pays one more sbci — window_polls() adds it where the count
|
||||||
|
// forces the wide type. Held by the pureboot.window gate.
|
||||||
|
static constexpr std::uint8_t poll_cycles = 7;
|
||||||
|
|
||||||
static void init()
|
static void init()
|
||||||
{
|
{
|
||||||
@@ -240,7 +308,10 @@ struct software_link {
|
|||||||
// every rate. Activation differs in kind from the other two — there is no
|
// 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.
|
// clock to time a window against — so this backend brings its own, below.
|
||||||
struct autobaud_link {
|
struct autobaud_link {
|
||||||
using uart = avr::uart::software_autobaud<avr::PUREBOOT_RX, avr::PUREBOOT_TX>;
|
// The unit in GPIOR2:GPIOR1 where the chip has them: the loader owns the
|
||||||
|
// whole chip while it runs, and the pair costs one word per access where
|
||||||
|
// the RAM word costs two — six words across the image.
|
||||||
|
using uart = avr::uart::software_autobaud<avr::PUREBOOT_RX, avr::PUREBOOT_TX, avr::uart::unit_home::gpior>;
|
||||||
|
|
||||||
static void init()
|
static void init()
|
||||||
{
|
{
|
||||||
@@ -259,19 +330,22 @@ struct autobaud_link {
|
|||||||
|
|
||||||
static void drain()
|
static void drain()
|
||||||
{
|
{
|
||||||
uart::drain();
|
// A drain here always follows this link's own write — the frame is
|
||||||
|
// in flight by construction, so the completion the wait needs is
|
||||||
|
// guaranteed and the bounded default's countdown would be dead bytes.
|
||||||
|
uart::drain_unbounded();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
#if defined(PUREBOOT_AUTOBAUD)
|
#if defined(PUREBOOT_AUTOBAUD)
|
||||||
using link = autobaud_link;
|
using link = autobaud_link;
|
||||||
#elif defined(PUREBOOT_USART)
|
#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_unit>(), "PUREBOOT_USART selects a hardware USART this chip does not have");
|
||||||
using link = hardware_link<dev::clock, wire_baud>;
|
using link = hardware_link<dev::clock, wire_baud>;
|
||||||
#elif defined(PUREBOOT_SOFT_SERIAL)
|
#elif defined(PUREBOOT_SOFT_SERIAL)
|
||||||
using link = software_link<dev::clock, wire_baud>;
|
using link = software_link<dev::clock, wire_baud>;
|
||||||
#else
|
#else
|
||||||
using link = std::conditional_t<avr::uart::has_usart<usart_digit>(), hardware_link<dev::clock, wire_baud>,
|
using link = std::conditional_t<avr::uart::has_usart<usart_unit>(), hardware_link<dev::clock, wire_baud>,
|
||||||
software_link<dev::clock, wire_baud>>;
|
software_link<dev::clock, wire_baud>>;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -287,7 +361,7 @@ extern "C" [[noreturn]] void pureboot_app();
|
|||||||
__builtin_unreachable();
|
__builtin_unreachable();
|
||||||
}
|
}
|
||||||
|
|
||||||
[[gnu::noinline, noreturn]] void run_app()
|
[[noreturn]] void run_app()
|
||||||
{
|
{
|
||||||
jump(pureboot_app);
|
jump(pureboot_app);
|
||||||
}
|
}
|
||||||
@@ -313,16 +387,39 @@ void await_host()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
// The window as one 32-bit countdown, divided by the backend's counted
|
// The window as one countdown, divided by the backend's counted poll-loop
|
||||||
// poll-loop cycles. Whole seconds is all it promises.
|
// cycles. Whole seconds is all it promises. The per-poll cost depends on the
|
||||||
|
// countdown's own width (a uint32_t decrement chain is one sbci longer), and
|
||||||
|
// the width depends on the poll count — solved narrow-first: a count that
|
||||||
|
// fits 24 bits at the narrow cost keeps the narrow loop, anything else takes
|
||||||
|
// the wide loop at its own cost. A count fitting 24 bits only at the wide
|
||||||
|
// cost stays wide, so the choice cannot oscillate on the boundary.
|
||||||
|
consteval std::uint32_t polls_at(std::uint32_t per_poll)
|
||||||
|
{
|
||||||
|
// Whole-window cycles first, then the per-poll division: one truncation
|
||||||
|
// instead of one per second. Same instructions either way — only the
|
||||||
|
// countdown's immediate moves.
|
||||||
|
return static_cast<std::uint32_t>(dev::cycles_for<std::chrono::seconds{timeout_seconds}>() / per_poll);
|
||||||
|
}
|
||||||
|
|
||||||
|
consteval bool narrow_window()
|
||||||
|
{
|
||||||
|
return polls_at(link::poll_cycles) <= 0xffffff;
|
||||||
|
}
|
||||||
|
|
||||||
consteval std::uint32_t window_polls()
|
consteval std::uint32_t window_polls()
|
||||||
{
|
{
|
||||||
return timeout_seconds * static_cast<std::uint32_t>(dev::clock.hz / link::poll_cycles);
|
return polls_at(narrow_window() ? link::poll_cycles : link::poll_cycles + 1u);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The countdown in the narrowest type that holds it: a fourth byte would
|
||||||
|
// cost a wider decrement chain at every poll for range most windows never
|
||||||
|
// use (the autobaud budget makes the same choice).
|
||||||
|
using window_t = std::conditional_t<narrow_window(), avr::uint24_t, std::uint32_t>;
|
||||||
|
|
||||||
bool pending_before_deadline()
|
bool pending_before_deadline()
|
||||||
{
|
{
|
||||||
std::uint32_t polls = window_polls();
|
window_t polls = window_polls();
|
||||||
do {
|
do {
|
||||||
if (link::pending())
|
if (link::pending())
|
||||||
return true;
|
return true;
|
||||||
@@ -399,35 +496,40 @@ void await_host()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// One byte into a writable space. Flash is not one of them — it arrives a
|
// One byte into a writable space. Flash is not one of them — it arrives a
|
||||||
// page at a time through 'W' and is committed through sp_spm — and the fuses
|
// page at a time through 'W' and is committed by the sealed SPM command — and
|
||||||
// are not writable at all: SPM reaches flash and boot lock bits only.
|
// the fuses are not writable at all: SPM reaches flash and boot lock bits only.
|
||||||
[[gnu::always_inline]] inline void store(std::uint8_t space, std::uint8_t bank, std::uint16_t at, std::uint8_t value,
|
[[gnu::always_inline]] inline void store(std::uint8_t space, std::uint16_t at, std::uint8_t value)
|
||||||
std::uint8_t slot_high)
|
|
||||||
{
|
{
|
||||||
if (space == sp_data) {
|
if (space == sp_data) {
|
||||||
*reinterpret_cast<volatile std::uint8_t *>(at) = value;
|
*reinterpret_cast<volatile std::uint8_t *>(at) = value;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (space == sp_spm) {
|
// Host-paced: the ack goes out once the write has begun, so the next byte
|
||||||
// The running-slot write guard. An SPM command aimed at the slot this
|
// arrives while it completes and nothing is missed without a buffer.
|
||||||
// code executes from is dropped, so a broken host cannot brick the
|
ee::write<off>(at, value);
|
||||||
// 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
|
// The irreversible half of the protocol, and the whole of it: page erase, page
|
||||||
// refused page's words in the buffer: harmless, since the next page
|
// write and the lock bits are one SPM command each, and nothing else the loader
|
||||||
// write auto-erases it (§26.2.1).
|
// does outlasts being done again. Reached only from a sealed command (run()),
|
||||||
if (slot_of(bank, at) != slot_high)
|
// so both the byte handed to SPMCSR and the address it fires at are the ones
|
||||||
|
// the host computed its seal over.
|
||||||
|
//
|
||||||
|
// Nothing here refuses an address. A loader that will not write its own slot
|
||||||
|
// cannot plant anything in it either, and a resident copy able to rewrite its
|
||||||
|
// own trailing page is what lets a 512-byte boot section — where no staging
|
||||||
|
// copy can run SPM at all — carry an SPM primitive for an application-side
|
||||||
|
// installer to drive. The protection that made the guard look necessary is the
|
||||||
|
// seal: a wire fault can no longer name an address, only a host can, and a host
|
||||||
|
// that names this one means it.
|
||||||
|
[[gnu::always_inline]] inline void commit(std::uint8_t bank, std::uint16_t at, std::uint8_t value)
|
||||||
|
{
|
||||||
spm::command<off>(value, flash_address(bank, at));
|
spm::command<off>(value, flash_address(bank, at));
|
||||||
// Only a boot-sectioned mega runs on while its RWW section programs;
|
// Only a boot-sectioned mega runs on while its RWW section programs;
|
||||||
// everywhere else the CPU halts through erase and write, so the wait
|
// everywhere else the CPU halts through erase and write, so the wait
|
||||||
// is already over by the time it returns.
|
// is already over by the time it returns.
|
||||||
if constexpr (boot_section)
|
if constexpr (boot_section)
|
||||||
spm::wait();
|
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
|
// One page into the SPM buffer, and only that: the erase and the write that
|
||||||
@@ -454,20 +556,18 @@ void fill_page(std::uint8_t bank, std::uint16_t at)
|
|||||||
|
|
||||||
[[noreturn]] void run()
|
[[noreturn]] void run()
|
||||||
{
|
{
|
||||||
|
#if defined(PUREBOOT_OSCCAL)
|
||||||
|
// The build's oscillator trim, ahead of everything — the WDRF bail
|
||||||
|
// included — so every path out of reset, the watchdog hand-over to the
|
||||||
|
// application first among them, runs on the corrected clock.
|
||||||
|
avr::clock::calibrate(PUREBOOT_OSCCAL);
|
||||||
|
#endif
|
||||||
// A watchdog reset belongs to the application, whose watchdog stays forced
|
// A watchdog reset belongs to the application, whose watchdog stays forced
|
||||||
// on until it clears WDRF — no activation window in its way.
|
// on until it clears WDRF — no activation window in its way.
|
||||||
if (avr::hw::field_impl<wdrf_field()>::test())
|
if (avr::power::peek_reset_cause().watchdog)
|
||||||
run_app();
|
run_app();
|
||||||
|
|
||||||
link::init();
|
link::init();
|
||||||
|
|
||||||
// The slot this copy runs in, which the write guard follows: the return
|
|
||||||
// address is a word address and a slot is half as many words as bytes, so
|
|
||||||
// its high byte is the slot index outright. No absolute address is ever
|
|
||||||
// formed, so the image stays position-independent.
|
|
||||||
const auto return_words = reinterpret_cast<std::uint16_t>(__builtin_return_address(0));
|
|
||||||
const auto slot_high = static_cast<std::uint8_t>(return_words >> 8);
|
|
||||||
|
|
||||||
await_host();
|
await_host();
|
||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
@@ -476,52 +576,99 @@ void fill_page(std::uint8_t bank, std::uint16_t at)
|
|||||||
ee::wait();
|
ee::wait();
|
||||||
tx_ack();
|
tx_ack();
|
||||||
const std::uint8_t command = link::rx();
|
const std::uint8_t command = link::rx();
|
||||||
switch (command) {
|
if (command & op_identify) {
|
||||||
case 'J': { // jump to a wire word address: hand-over and staging transfer
|
// Identity: the version, then the three signature bytes. Straight
|
||||||
auto target = reinterpret_cast<void (*)()>(rx16());
|
// out of the stamp, so the wire and the image can never disagree
|
||||||
tx_ack();
|
// about what this loader is. The indices are constant and the array
|
||||||
link::drain();
|
// is constexpr, so these are immediates, not flash reads: nothing
|
||||||
jump(target);
|
// here needs the stamp's runtime address. Unsealed, because it
|
||||||
}
|
// takes no argument and changes nothing — and because a command
|
||||||
case 'b': // identity: the version, then the three signature bytes
|
// that cannot be got wrong is what a lost host resynchronises on.
|
||||||
// Straight out of the stamp, so the wire and the image can never
|
|
||||||
// disagree about what this loader is. The indices are constant and
|
|
||||||
// the array is constexpr, so these are immediates, not flash reads:
|
|
||||||
// nothing here needs the stamp's runtime address.
|
|
||||||
for (std::uint8_t at = stamp_identity; at != sizeof identity_stamp; ++at)
|
for (std::uint8_t at = stamp_identity; at != sizeof identity_stamp; ++at)
|
||||||
link::tx(identity_stamp[at]);
|
link::tx(identity_stamp[at]);
|
||||||
break;
|
} else {
|
||||||
case 'W': // fill one flash page buffer: sel8, addr16, then page bytes
|
// One decode, one cursor and one loop for every space, both
|
||||||
case 'G': // read: sel8, addr16, n8 (0 = 256)
|
// directions and the jump: a command per memory would carry a copy
|
||||||
case 'g': { // write: sel8, addr16, n8, then n bytes, each acked
|
// of all three each. The jump — the hand-over and staging transfer
|
||||||
// One decode, one cursor and one loop for every space and both
|
// — carries a selector it ignores so its address rides the same two
|
||||||
// directions: a command per memory would carry a copy of all three
|
// reads as everything else; the page fill joins the same decode
|
||||||
// each. 'W' joins the same decode rather than keeping an address
|
// rather than keeping an address form of its own, so flash
|
||||||
// form of its own, so flash addressing is uniform across every
|
// addressing is uniform across every command that names it. Both
|
||||||
// command that names it.
|
// carry the count they do not use for the same reason: one header
|
||||||
|
// shape is one decode, and one seal covers a fixed set of bytes.
|
||||||
const std::uint8_t selector = link::rx();
|
const std::uint8_t selector = link::rx();
|
||||||
const std::uint8_t space = space_of(selector);
|
const std::uint8_t space = space_of(selector);
|
||||||
const std::uint8_t bank = bank_of(selector);
|
const std::uint8_t bank = bank_of(selector);
|
||||||
std::uint16_t at = rx16();
|
std::uint16_t at = rx16();
|
||||||
if (command == 'W') {
|
|
||||||
fill_page(bank, at);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
std::uint8_t count = link::rx();
|
std::uint8_t count = link::rx();
|
||||||
|
const std::uint8_t sealed = link::rx();
|
||||||
|
|
||||||
|
// The seal: every field that decides what this command does folded
|
||||||
|
// into one byte the host chose, tested before any of it happens.
|
||||||
|
//
|
||||||
|
// Checked here rather than acknowledged afterwards, which is the
|
||||||
|
// whole point. An ack reports a command that has already run, and
|
||||||
|
// for the one command that cannot be taken back a report is not a
|
||||||
|
// defence. Once the running-slot guard is gone the address is as
|
||||||
|
// fatal as the command byte — a wrong one reaches the loader's own
|
||||||
|
// page — so the seal covers the act and the place together, and a
|
||||||
|
// stream that lost or mangled either cannot produce it.
|
||||||
|
//
|
||||||
|
// Folded here, after the last read, and never accumulated across
|
||||||
|
// the reads: every field is still live at this point because the
|
||||||
|
// command needs it anyway, so the fold costs one xor each and no
|
||||||
|
// register. An accumulator would have to survive four calls, and
|
||||||
|
// paying for that in call-saved registers costs more than the whole
|
||||||
|
// check costs in arithmetic — measured at fourteen bytes, on a
|
||||||
|
// budget of ten.
|
||||||
|
std::uint8_t fold = command;
|
||||||
|
fold ^= selector;
|
||||||
|
fold ^= static_cast<std::uint8_t>(at);
|
||||||
|
fold ^= static_cast<std::uint8_t>(at >> 8);
|
||||||
|
fold ^= count;
|
||||||
|
fold ^= sealed;
|
||||||
|
// The verdict, and it is not a courtesy. Every command whose
|
||||||
|
// payload the host sends without waiting — a page fill, a write
|
||||||
|
// burst — would otherwise be handed to a loader that has already
|
||||||
|
// gone back to reading commands, so a *detected* error would
|
||||||
|
// become the desync the seal exists to prevent: a 128-byte page
|
||||||
|
// read as command headers is twenty-one more chances at the one in
|
||||||
|
// two hundred and fifty-six. Answering the seal before the payload
|
||||||
|
// is what keeps a refusal local to the command that earned it.
|
||||||
|
//
|
||||||
|
// An unknown opcode lands here too — every bit pattern is now some
|
||||||
|
// command, so it is the seal, not a table of valid letters, that
|
||||||
|
// rejects noise, and the host hears about it either way.
|
||||||
|
if (fold != seal) {
|
||||||
|
link::tx(nak);
|
||||||
|
} else {
|
||||||
|
tx_ack();
|
||||||
|
if (command & op_jump) {
|
||||||
|
link::drain();
|
||||||
|
jump(reinterpret_cast<void (*)()>(at));
|
||||||
|
} else if (command & op_fill) {
|
||||||
|
fill_page(bank, at);
|
||||||
|
} else if (space == sp_spm) {
|
||||||
|
// An SPM command is the whole of what this loader can do
|
||||||
|
// that doing again will not undo, and it is one byte — so
|
||||||
|
// it rides the count field, inside the seal, rather than
|
||||||
|
// arriving as data after the seal has been checked. Which
|
||||||
|
// is also what makes a deliberate lock-bit write
|
||||||
|
// expressible, where refusing it outright did not.
|
||||||
|
commit(bank, at, count);
|
||||||
|
} else {
|
||||||
do {
|
do {
|
||||||
// Read and write are one letter apart in case, so the direction
|
// Direction is one bit of the opcode, so the loop
|
||||||
// is a single bit and the loop picks it with a one-word skip.
|
// picks it with a one-word skip.
|
||||||
if (command & 0x20) {
|
if (command & op_write) {
|
||||||
store(space, bank, at, link::rx(), slot_high);
|
store(space, at, link::rx());
|
||||||
tx_ack();
|
tx_ack();
|
||||||
} else
|
} else
|
||||||
link::tx(load(space, bank, at));
|
link::tx(load(space, bank, at));
|
||||||
++at;
|
++at;
|
||||||
} while (--count);
|
} while (--count);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
default: // unknown bytes are ignored; the loop re-acks
|
}
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -529,4 +676,7 @@ void fill_page(std::uint8_t bank, std::uint16_t at)
|
|||||||
} // namespace
|
} // namespace
|
||||||
} // namespace pureboot
|
} // namespace pureboot
|
||||||
|
|
||||||
template struct avr::startup::entry<pureboot::run>;
|
// stack::hardware: activation is reset-only, so the reset logic's own
|
||||||
|
// SP = RAMEND stands wherever the datasheet guarantees it (the classic
|
||||||
|
// megas still get the write); a 'J' entry runs on the caller's live stack.
|
||||||
|
template struct avr::startup::entry<pureboot::run, avr::startup::stack::hardware>;
|
||||||
|
|||||||
@@ -20,17 +20,26 @@ if os.name == "nt":
|
|||||||
import ctypes
|
import ctypes
|
||||||
from ctypes import wintypes
|
from ctypes import wintypes
|
||||||
else:
|
else:
|
||||||
|
import array
|
||||||
|
import fcntl
|
||||||
import select
|
import select
|
||||||
import termios
|
import termios
|
||||||
|
|
||||||
PROMPT = b"+"
|
PROMPT = b"+"
|
||||||
VERSION = 4 # this tool's own version — free to drift from a loader's
|
# The refusal, from pureboot 9: the prompt inverted, so no single flipped bit
|
||||||
# The loader versions this tool speaks. A pureboot version implies its wire
|
# turns "nothing happened" into "it did".
|
||||||
|
NAK = bytes((~PROMPT[0] & 0xFF,))
|
||||||
|
VERSION = 10 # this tool's own version — free to drift from a loader's
|
||||||
|
# The loader versions this tool can drive. 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: the tool keeps a decoder for every generation in it (1–4 speak
|
||||||
# changes it becomes the new floor here.
|
# the per-memory commands, 5 the unified pair; 6 marks the OSCCAL-carrying
|
||||||
|
# builds and changes nothing on the wire; 8 the one-wire deployments, whose
|
||||||
|
# only host-side trace is the --one-wire echo discard; 9 seals every command
|
||||||
|
# and answers each seal before acting), and a version it has no decoder for
|
||||||
|
# moves the floor.
|
||||||
OLDEST_LOADER = 1
|
OLDEST_LOADER = 1
|
||||||
NEWEST_LOADER = 5
|
NEWEST_LOADER = 9
|
||||||
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
|
||||||
|
|
||||||
@@ -38,9 +47,49 @@ RETRIES = 3 # rewrites of a page that reads back wrong, before the run stops
|
|||||||
# 'g' writes, each taking a selector byte, a 16-bit address and a count, over
|
# '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
|
# 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.
|
# — which is what buys the data space and the host-issued SPM operations.
|
||||||
|
# 6 marks the builds that may carry a baked OSCCAL trim, nothing on the wire;
|
||||||
|
# 7 gives 'J' a selector byte (older loaders take the bare address — jump()
|
||||||
|
# sends each form to the version that speaks it) and re-homes the autobaud
|
||||||
|
# unit into the GPIOR pair where the chip has one; 8 marks the builds whose
|
||||||
|
# deployment may be one-wire (hardware half-duplex, or a software link folded
|
||||||
|
# onto one pin) — nothing on the wire either, but a shared line makes the
|
||||||
|
# host read its own bytes back, which is what --one-wire consumes.
|
||||||
UNIFIED_LOADER = 5
|
UNIFIED_LOADER = 5
|
||||||
SP_FLASH, SP_EEPROM, SP_RAM, SP_FUSE, SP_SPM = 0, 1, 2, 3, 4
|
SP_FLASH, SP_EEPROM, SP_RAM, SP_FUSE, SP_SPM = 0, 1, 2, 3, 4
|
||||||
|
|
||||||
|
# pureboot 9 replaces the command letters with bits and seals every command.
|
||||||
|
# The header is one shape for all of them — opcode, selector, address, count,
|
||||||
|
# seal — and the loader answers the seal *before* it acts: PROMPT accepts, NAK
|
||||||
|
# refuses and nothing happened. That verdict is what lets a refusal stay local
|
||||||
|
# to its own command: the payload of a fill or a write burst only goes out
|
||||||
|
# after the header has been accepted, so a rejected header never leaves the
|
||||||
|
# host pushing bytes into a loader that has gone back to reading commands.
|
||||||
|
#
|
||||||
|
# A transfer is the absence of the other three opcodes, and its direction is
|
||||||
|
# the low bit. Identify is bit 5 because both knock bytes carry it: 'b' has to
|
||||||
|
# still ask the version (the host cannot know which generation it is talking to
|
||||||
|
# until something answers), and 'p' has to stay harmless against a loader
|
||||||
|
# already in session — pureboot 9 reserves no invalid opcode, so a knock that
|
||||||
|
# meant nothing before would otherwise eat the five bytes behind it.
|
||||||
|
SEALED_LOADER = 9
|
||||||
|
OP_WRITE, OP_FILL, OP_JUMP, OP_IDENTIFY = 1, 4, 8, 0x20
|
||||||
|
# What a sealed header's fields must fold to. Non-zero, so a run of one
|
||||||
|
# repeated byte — a stuck line, a page of erased flash read as a header —
|
||||||
|
# cannot satisfy it.
|
||||||
|
SEAL = 0x5A
|
||||||
|
# An SPM command carries its SPMCSR byte in the count field, where the seal
|
||||||
|
# covers it. There is no data phase: a byte after the header would arrive after
|
||||||
|
# the seal had already been checked, which is the hole the seal exists to close.
|
||||||
|
|
||||||
|
# An autobaud loader keeps its measured bit period readable, encoded as
|
||||||
|
# delay-loop counts: (bit cycles − UNIT_DISCOUNT) / UNIT_LOOP_CYCLES,
|
||||||
|
# floored — the spin granule and per-bit overhead of libavr's software UART.
|
||||||
|
# v5/v6 keep it at ram_start; v7 moves it into GPIOR2:GPIOR1 on the chips
|
||||||
|
# that have the pair (their data addresses are in the geometry) and keeps
|
||||||
|
# ram_start only where they do not exist. --info undoes the encoding to
|
||||||
|
# report the true clock, which therefore sits within one granule below it.
|
||||||
|
UNIT_LOOP_CYCLES, UNIT_DISCOUNT = 4, 8
|
||||||
|
|
||||||
# A selector's high nibble is the flash bank — the address bits above the 16-bit
|
# 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
|
# 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
|
# no command has to speak word addresses. No single transfer may cross a bank
|
||||||
@@ -66,31 +115,40 @@ CALIBRATE = 0xC0
|
|||||||
# from its chip database at build time). Die revisions that share a signature
|
# from its chip database at build time). Die revisions that share a signature
|
||||||
# share this row, as they share the silicon.
|
# share this row, as they share the silicon.
|
||||||
CHIP_GEOMETRY = {
|
CHIP_GEOMETRY = {
|
||||||
# signature : (flash, page, eeprom, patch_vector)
|
# signature : (flash, page, eeprom, patch_vector, ram_start, gpior1)
|
||||||
(0x1E, 0x90, 0x07): (1024, 32, 64, True), # ATtiny13/13A
|
# ram_start is where SRAM begins in data space: the classic megas and the
|
||||||
(0x1E, 0x91, 0x08): (2048, 32, 128, True), # ATtiny25
|
# tinies keep it right after the plain I/O registers (0x60), the x8/x4
|
||||||
(0x1E, 0x92, 0x06): (4096, 64, 256, True), # ATtiny45
|
# generations past their extended I/O file (0x100). gpior1 is GPIOR1's
|
||||||
(0x1E, 0x93, 0x0B): (8192, 64, 512, True), # ATtiny85
|
# data address — 0x32 on the t25/45/85, 0x4A from the x8 generation on,
|
||||||
(0x1E, 0x92, 0x05): (4096, 64, 256, True), # ATmega48/48A
|
# None where the chip has no pair (t13, classic megas). A v7 autobaud
|
||||||
(0x1E, 0x92, 0x0A): (4096, 64, 256, True), # ATmega48P/48PA
|
# loader's measured bit period lives in GPIOR2:GPIOR1 where they exist
|
||||||
(0x1E, 0x93, 0x07): (8192, 64, 512, False), # ATmega8/8A
|
# and at exactly ram_start elsewhere (its only RAM object; the loader's
|
||||||
(0x1E, 0x93, 0x0A): (8192, 64, 512, False), # ATmega88/88A
|
# own build pins the layout); v5/v6 always used ram_start. --info reads
|
||||||
(0x1E, 0x93, 0x0F): (8192, 64, 512, False), # ATmega88P/88PA
|
# whichever home the answering version implies.
|
||||||
(0x1E, 0x94, 0x03): (16384, 128, 512, False), # ATmega16/16A
|
(0x1E, 0x90, 0x07): (1024, 32, 64, True, 0x60, None), # ATtiny13/13A
|
||||||
(0x1E, 0x94, 0x06): (16384, 128, 512, False), # ATmega168/168A
|
(0x1E, 0x91, 0x08): (2048, 32, 128, True, 0x60, 0x32), # ATtiny25
|
||||||
(0x1E, 0x94, 0x0B): (16384, 128, 512, False), # ATmega168P/168PA
|
(0x1E, 0x92, 0x06): (4096, 64, 256, True, 0x60, 0x32), # ATtiny45
|
||||||
(0x1E, 0x94, 0x0A): (16384, 128, 512, False), # ATmega164P/164PA
|
(0x1E, 0x93, 0x0B): (8192, 64, 512, True, 0x60, 0x32), # ATtiny85
|
||||||
(0x1E, 0x94, 0x0F): (16384, 128, 512, False), # ATmega164A
|
(0x1E, 0x92, 0x05): (4096, 64, 256, True, 0x100, 0x4A), # ATmega48/48A
|
||||||
(0x1E, 0x95, 0x02): (32768, 128, 1024, False), # ATmega32/32A
|
(0x1E, 0x92, 0x0A): (4096, 64, 256, True, 0x100, 0x4A), # ATmega48P/48PA
|
||||||
(0x1E, 0x95, 0x0F): (32768, 128, 1024, False), # ATmega328P
|
(0x1E, 0x93, 0x07): (8192, 64, 512, False, 0x60, None), # ATmega8/8A
|
||||||
(0x1E, 0x95, 0x14): (32768, 128, 1024, False), # ATmega328
|
(0x1E, 0x93, 0x0A): (8192, 64, 512, False, 0x100, 0x4A), # ATmega88/88A
|
||||||
(0x1E, 0x95, 0x08): (32768, 128, 1024, False), # ATmega324P
|
(0x1E, 0x93, 0x0F): (8192, 64, 512, False, 0x100, 0x4A), # ATmega88P/88PA
|
||||||
(0x1E, 0x95, 0x11): (32768, 128, 1024, False), # ATmega324PA
|
(0x1E, 0x94, 0x03): (16384, 128, 512, False, 0x60, None), # ATmega16/16A
|
||||||
(0x1E, 0x95, 0x15): (32768, 128, 1024, False), # ATmega324A
|
(0x1E, 0x94, 0x06): (16384, 128, 512, False, 0x100, 0x4A), # ATmega168/168A
|
||||||
(0x1E, 0x96, 0x09): (65536, 256, 2048, False), # ATmega644/644A
|
(0x1E, 0x94, 0x0B): (16384, 128, 512, False, 0x100, 0x4A), # ATmega168P/168PA
|
||||||
(0x1E, 0x96, 0x0A): (65536, 256, 2048, False), # ATmega644P/644PA
|
(0x1E, 0x94, 0x0A): (16384, 128, 512, False, 0x100, 0x4A), # ATmega164P/164PA
|
||||||
(0x1E, 0x97, 0x05): (131072, 256, 4096, False),# ATmega1284P
|
(0x1E, 0x94, 0x0F): (16384, 128, 512, False, 0x100, 0x4A), # ATmega164A
|
||||||
(0x1E, 0x97, 0x06): (131072, 256, 4096, False),# ATmega1284
|
(0x1E, 0x95, 0x02): (32768, 128, 1024, False, 0x60, None), # ATmega32/32A
|
||||||
|
(0x1E, 0x95, 0x0F): (32768, 128, 1024, False, 0x100, 0x4A), # ATmega328P
|
||||||
|
(0x1E, 0x95, 0x14): (32768, 128, 1024, False, 0x100, 0x4A), # ATmega328
|
||||||
|
(0x1E, 0x95, 0x08): (32768, 128, 1024, False, 0x100, 0x4A), # ATmega324P
|
||||||
|
(0x1E, 0x95, 0x11): (32768, 128, 1024, False, 0x100, 0x4A), # ATmega324PA
|
||||||
|
(0x1E, 0x95, 0x15): (32768, 128, 1024, False, 0x100, 0x4A), # ATmega324A
|
||||||
|
(0x1E, 0x96, 0x09): (65536, 256, 2048, False, 0x100, 0x4A), # ATmega644/644A
|
||||||
|
(0x1E, 0x96, 0x0A): (65536, 256, 2048, False, 0x100, 0x4A), # ATmega644P/644PA
|
||||||
|
(0x1E, 0x97, 0x05): (131072, 256, 4096, False, 0x100, 0x4A),# ATmega1284P
|
||||||
|
(0x1E, 0x97, 0x06): (131072, 256, 4096, False, 0x100, 0x4A),# ATmega1284
|
||||||
}
|
}
|
||||||
|
|
||||||
VERBOSE = False
|
VERBOSE = False
|
||||||
@@ -144,23 +202,60 @@ class Progress:
|
|||||||
|
|
||||||
|
|
||||||
class PosixPort:
|
class PosixPort:
|
||||||
"""A raw serial port with deadline-based reads, over termios."""
|
"""A raw serial port with deadline-based reads, over termios. A rate with
|
||||||
|
no B-constant — the off-nominal probes `--scan` walks — goes through
|
||||||
|
Linux's termios2 BOTHER; a platform without that ioctl refuses the rate
|
||||||
|
by name."""
|
||||||
|
|
||||||
|
# The termios2 ioctl pair and cflag bits, and the struct's ispeed/ospeed
|
||||||
|
# word offsets: four flag words, then a line-discipline byte and 19
|
||||||
|
# control chars padded to word 9 (include/uapi/asm-generic/termbits.h).
|
||||||
|
_TCGETS2, _TCSETS2 = 0x802C542A, 0x402C542B
|
||||||
|
_BOTHER, _CBAUD = 0o010000, 0o010017
|
||||||
|
_ISPEED, _OSPEED = 9, 10
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _speed(baud):
|
||||||
|
return getattr(termios, f"B{baud}", None)
|
||||||
|
|
||||||
|
def _set_arbitrary(self, baud):
|
||||||
|
buf = array.array("i", [0] * (self._OSPEED + 1))
|
||||||
|
try:
|
||||||
|
fcntl.ioctl(self.fd, self._TCGETS2, buf, True)
|
||||||
|
buf[2] = (buf[2] & ~self._CBAUD) | self._BOTHER
|
||||||
|
buf[self._ISPEED] = buf[self._OSPEED] = baud
|
||||||
|
fcntl.ioctl(self.fd, self._TCSETS2, buf)
|
||||||
|
except OSError:
|
||||||
|
raise Error(f"this platform cannot set {baud} Bd (no termios2)") from None
|
||||||
|
|
||||||
|
def _apply_baud(self, attrs, baud):
|
||||||
|
speed = self._speed(baud)
|
||||||
|
attrs[4] = attrs[5] = speed if speed is not None else termios.B38400
|
||||||
|
termios.tcsetattr(self.fd, termios.TCSANOW, attrs)
|
||||||
|
if speed is None:
|
||||||
|
self._set_arbitrary(baud)
|
||||||
|
self.baud = baud
|
||||||
|
|
||||||
def __init__(self, path, baud):
|
def __init__(self, path, baud):
|
||||||
self.fd = os.open(path, os.O_RDWR | os.O_NOCTTY)
|
self.fd = os.open(path, os.O_RDWR | os.O_NOCTTY)
|
||||||
|
try:
|
||||||
attrs = termios.tcgetattr(self.fd)
|
attrs = termios.tcgetattr(self.fd)
|
||||||
attrs[0] = 0 # iflag
|
attrs[0] = 0 # iflag
|
||||||
attrs[1] = 0 # oflag
|
attrs[1] = 0 # oflag
|
||||||
attrs[2] = termios.CREAD | termios.CLOCAL | termios.CS8 # cflag
|
attrs[2] = termios.CREAD | termios.CLOCAL | termios.CS8 # cflag
|
||||||
attrs[3] = 0 # lflag
|
attrs[3] = 0 # lflag
|
||||||
try:
|
|
||||||
speed = getattr(termios, f"B{baud}")
|
|
||||||
except AttributeError:
|
|
||||||
raise Error(f"unsupported baud rate {baud}") from None
|
|
||||||
attrs[4] = attrs[5] = speed
|
|
||||||
attrs[6][termios.VMIN] = 0
|
attrs[6][termios.VMIN] = 0
|
||||||
attrs[6][termios.VTIME] = 0
|
attrs[6][termios.VTIME] = 0
|
||||||
termios.tcsetattr(self.fd, termios.TCSANOW, attrs)
|
self._apply_baud(attrs, baud)
|
||||||
|
except BaseException:
|
||||||
|
os.close(self.fd)
|
||||||
|
raise
|
||||||
|
|
||||||
|
def set_baud(self, baud):
|
||||||
|
"""Retune the port without closing it — the fd stays open, so no DTR
|
||||||
|
pulse and no reset. That matters: the only caller is mid-session with a
|
||||||
|
loader copy that a reset would throw away."""
|
||||||
|
self._apply_baud(termios.tcgetattr(self.fd), baud)
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
os.close(self.fd)
|
os.close(self.fd)
|
||||||
@@ -289,6 +384,7 @@ if os.name == "nt":
|
|||||||
# timeout would otherwise stay at the driver's default — which
|
# timeout would otherwise stay at the driver's default — which
|
||||||
# may be "wait forever" — until the first read.
|
# may be "wait forever" — until the first read.
|
||||||
self._deadline(_GAP_MS, 1000)
|
self._deadline(_GAP_MS, 1000)
|
||||||
|
self.baud = baud
|
||||||
except Error:
|
except Error:
|
||||||
# An open port outlives the exception otherwise, and a COM
|
# An open port outlives the exception otherwise, and a COM
|
||||||
# handle is exclusive: the next attempt would meet its own
|
# handle is exclusive: the next attempt would meet its own
|
||||||
@@ -296,6 +392,21 @@ if os.name == "nt":
|
|||||||
self.close()
|
self.close()
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
def set_baud(self, baud):
|
||||||
|
"""Retune the port on its live handle — SetCommState only, so the
|
||||||
|
handle is never reopened and DTR never drops. That matters: the only
|
||||||
|
caller is mid-session with a loader copy a reset would throw away."""
|
||||||
|
if baud < 50:
|
||||||
|
raise Error(f"unsupported baud rate {baud}")
|
||||||
|
dcb = _DCB()
|
||||||
|
dcb.DCBlength = ctypes.sizeof(_DCB)
|
||||||
|
if not _k32.GetCommState(self.handle, ctypes.byref(dcb)):
|
||||||
|
_fail("cannot read the port state")
|
||||||
|
dcb.BaudRate = baud
|
||||||
|
if not _k32.SetCommState(self.handle, ctypes.byref(dcb)):
|
||||||
|
_fail(f"cannot retune the port to {baud} baud")
|
||||||
|
self.baud = baud
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
_k32.CloseHandle(self.handle)
|
_k32.CloseHandle(self.handle)
|
||||||
|
|
||||||
@@ -354,6 +465,99 @@ if os.name == "nt":
|
|||||||
Port = WindowsPort if os.name == "nt" else PosixPort
|
Port = WindowsPort if os.name == "nt" else PosixPort
|
||||||
|
|
||||||
|
|
||||||
|
class OneWirePort:
|
||||||
|
"""The host side of a shared line (--one-wire): an FTDI-style adapter on
|
||||||
|
a one-wire link reads back every byte it transmits — its RX is tied to
|
||||||
|
its own TX through the line. Consume that echo at each write and verify
|
||||||
|
it, which doubles as a wiring check: an echo that never comes is an RX
|
||||||
|
not on the line, and is reported as itself instead of decoding as a
|
||||||
|
device reply.
|
||||||
|
|
||||||
|
The device's reply may interleave with the echo of a multi-byte write —
|
||||||
|
a loader already in session re-prompts after the knock's first byte
|
||||||
|
while the second is still queued behind that reply — so the echo is
|
||||||
|
matched byte for byte and anything else arriving in between is device
|
||||||
|
traffic, held for the next read."""
|
||||||
|
|
||||||
|
def __init__(self, port):
|
||||||
|
self._port = port
|
||||||
|
self._pending = b""
|
||||||
|
self.lost_echoes = 0
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
return getattr(self._port, name)
|
||||||
|
|
||||||
|
def write(self, data, blind=False):
|
||||||
|
"""Put `data` on the line and consume its echo.
|
||||||
|
|
||||||
|
`blind` marks the protocol's one multi-byte write with no ack between
|
||||||
|
its bytes — the knock. Aimed at a loader already in session, its first
|
||||||
|
byte draws a prompt while the second is still going out, and on real
|
||||||
|
wiring the device's push-pull ack **wins the line** against the host's
|
||||||
|
1 k series resistor: that second byte is *destroyed, not delayed*, and
|
||||||
|
its echo never comes. Measured on an ATtiny13A at 57600 — the loader
|
||||||
|
answers a single byte perfectly and loses the knock's second every
|
||||||
|
time. So on a blind write a missing echo is a property of the wiring
|
||||||
|
rather than a fault in it, and the caller's retry is what deals with
|
||||||
|
it. Every other write is ack-paced and cannot collide, so a missing
|
||||||
|
echo there really is an RX that is not on the line.
|
||||||
|
"""
|
||||||
|
data = bytes(data)
|
||||||
|
self._port.write(data)
|
||||||
|
# The echo arrives at line rate — 10 bits a byte — plus adapter
|
||||||
|
# latency; a generous floor keeps slow rates and USB scheduling out
|
||||||
|
# of the error path.
|
||||||
|
deadline = time.monotonic() + 10 * len(data) / self._port.baud + 0.5
|
||||||
|
remaining = data
|
||||||
|
while remaining and time.monotonic() < deadline:
|
||||||
|
# Speculative, so it cannot be read_exact, whose contract is to
|
||||||
|
# raise: doing that made the diagnosis below unreachable on every
|
||||||
|
# quiet line and surfaced a bare "timeout: got 0 of 1 bytes" in
|
||||||
|
# its place — the one message this class exists to replace.
|
||||||
|
for byte in self._port.read_available(0.02):
|
||||||
|
if remaining and byte == remaining[0]:
|
||||||
|
remaining = remaining[1:]
|
||||||
|
else:
|
||||||
|
self._pending += bytes((byte,))
|
||||||
|
if not remaining:
|
||||||
|
return
|
||||||
|
if not blind:
|
||||||
|
raise Error(f"one-wire echo missing after {len(data) - len(remaining)} of "
|
||||||
|
f"{len(data)} byte(s) — is the adapter's RX tied to the line?")
|
||||||
|
self.lost_echoes += len(remaining)
|
||||||
|
# Which loss this is matters, and the count says it. *Some* bytes lost is
|
||||||
|
# the device's ack winning the line against the host's series resistor —
|
||||||
|
# ordinary, and what the retry absorbs. *Every* byte lost is nothing
|
||||||
|
# coming back at all, which is a line that is not free: an application
|
||||||
|
# holding the shared pin low (this rig's LED demo ends that way), a
|
||||||
|
# wedge, or an RX that is not on the line. Same retry either way, but
|
||||||
|
# blaming an ack that never happened sends the reader to the wrong place.
|
||||||
|
if len(remaining) == len(data):
|
||||||
|
verbose(f"one-wire: none of {len(data)} byte(s) echoed — the line is not "
|
||||||
|
f"coming back. Held low by something? (a pin driven low, a wedge, "
|
||||||
|
f"or an RX not on the line)")
|
||||||
|
else:
|
||||||
|
verbose(f"one-wire: {len(remaining)} of {len(data)} knock byte(s) lost to the "
|
||||||
|
f"device's ack; retrying")
|
||||||
|
|
||||||
|
def write_blind(self, data):
|
||||||
|
self.write(data, blind=True)
|
||||||
|
|
||||||
|
def read_exact(self, count, timeout):
|
||||||
|
taken, self._pending = self._pending[:count], self._pending[count:]
|
||||||
|
if len(taken) == count:
|
||||||
|
return taken
|
||||||
|
return taken + self._port.read_exact(count - len(taken), timeout)
|
||||||
|
|
||||||
|
def read_available(self, wait):
|
||||||
|
taken, self._pending = self._pending, b""
|
||||||
|
return taken + self._port.read_available(0 if taken else wait)
|
||||||
|
|
||||||
|
def flush_input(self):
|
||||||
|
self._pending = b""
|
||||||
|
self._port.flush_input()
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------------------- protocol ---
|
# -------------------------------------------------------------- protocol ---
|
||||||
|
|
||||||
|
|
||||||
@@ -372,8 +576,8 @@ class Info:
|
|||||||
The base is where application flash ends, which is a property of the
|
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
|
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
|
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
|
that send a block do. Which slot a copy runs in is not something the
|
||||||
write guard, which is the loader's business."""
|
identity reports, and from pureboot 9 nothing on the device cares."""
|
||||||
if len(raw) != 4:
|
if len(raw) != 4:
|
||||||
raise Error(f"bad identity reply: {raw.hex()}")
|
raise Error(f"bad identity reply: {raw.hex()}")
|
||||||
version, signature = raw[0], tuple(raw[1:4])
|
version, signature = raw[0], tuple(raw[1:4])
|
||||||
@@ -381,7 +585,7 @@ class Info:
|
|||||||
if geometry is None:
|
if geometry is None:
|
||||||
sig = " ".join(f"{b:02x}" for b in signature)
|
sig = " ".join(f"{b:02x}" for b in signature)
|
||||||
raise Error(f"unknown signature {sig} — this tool has no geometry for it")
|
raise Error(f"unknown signature {sig} — this tool has no geometry for it")
|
||||||
flash, page, eeprom, patch = geometry
|
flash, page, eeprom, patch, _, _ = geometry
|
||||||
base = flash - SLOT
|
base = flash - SLOT
|
||||||
word_flash = flash > 0x10000
|
word_flash = flash > 0x10000
|
||||||
wire_base = base // 2 if word_flash else base
|
wire_base = base // 2 if word_flash else base
|
||||||
@@ -417,6 +621,16 @@ class Info:
|
|||||||
# The hand-over target as 'J' takes it: the trampoline below the
|
# The hand-over target as 'J' takes it: the trampoline below the
|
||||||
# loader, or word 0 where BOOTRST re-vectors reset in hardware.
|
# loader, or word 0 where BOOTRST re-vectors reset in hardware.
|
||||||
self.app_entry_word = (self.base - 2) // 2 if self.patch_vector else 0
|
self.app_entry_word = (self.base - 2) // 2 if self.patch_vector else 0
|
||||||
|
# Where SRAM begins, from the signature — None only for a chip this
|
||||||
|
# tool has no geometry row for, which the wire-block path (v1–4)
|
||||||
|
# permits where from_identity refuses.
|
||||||
|
geometry = CHIP_GEOMETRY.get(tuple(self.signature))
|
||||||
|
self.ram = geometry[4] if geometry else None
|
||||||
|
# Where this loader keeps the measured bit period (None when a fixed
|
||||||
|
# signature row is missing): the GPIOR pair from v7 where the chip
|
||||||
|
# has one, ram_start before that and everywhere without the pair.
|
||||||
|
gpior1 = geometry[5] if geometry else None
|
||||||
|
self.unit_home = gpior1 if self.version >= 7 and gpior1 is not None else self.ram
|
||||||
|
|
||||||
def describe(self):
|
def describe(self):
|
||||||
sig = " ".join(f"{b:02x}" for b in self.signature)
|
sig = " ".join(f"{b:02x}" for b in self.signature)
|
||||||
@@ -457,17 +671,44 @@ class Loader:
|
|||||||
# Set once a session is established over an autobaud link, so a
|
# Set once a session is established over an autobaud link, so a
|
||||||
# re-entry after 'J' repeats the handshake that worked.
|
# re-entry after 'J' repeats the handshake that worked.
|
||||||
self.autobaud = False
|
self.autobaud = False
|
||||||
|
# The pre-knock drain runs once per port: the bytes it exists for are
|
||||||
|
# leftovers from before this process opened the port. Re-knocks later
|
||||||
|
# in the same session must not pay it — a fresh activation window is
|
||||||
|
# already burning while they wait.
|
||||||
|
self._line_drained = False
|
||||||
|
# The link this session is speaking. It moves when the host follows a
|
||||||
|
# staging copy built for another one (enter_copy).
|
||||||
|
self.baud = getattr(port, "baud", None)
|
||||||
|
self._link_declared = False
|
||||||
|
|
||||||
def _read_identity(self):
|
def _read_identity(self):
|
||||||
"""The 'b' reply, in either of the two layouts a loader may send.
|
"""The 'b' reply, in either of the two layouts a loader may send.
|
||||||
pureboot 5 answers with its version and the signature; older loaders
|
pureboot 5 answers with its version and the signature; older loaders
|
||||||
answer with a 12-byte block. The version byte cannot be mistaken for
|
answer with a 12-byte block. The version byte cannot be mistaken for
|
||||||
the older block's 'P', so four bytes are enough to tell them apart."""
|
the older block's 'P', so four bytes are enough to tell them apart.
|
||||||
head = self.port.read_exact(4, 2.0)
|
|
||||||
|
The timeout is short on purpose: a real answer follows the prompt
|
||||||
|
within a frame time or two, so half a second is dozens of times the
|
||||||
|
worst case — while a *false* prompt match (a stale byte, reset
|
||||||
|
garbage) makes this read collect noise, and every second spent on it
|
||||||
|
comes out of the activation window the retry needs."""
|
||||||
|
head = self.port.read_exact(4, 0.5)
|
||||||
if head[0:2] == b"PB":
|
if head[0:2] == b"PB":
|
||||||
return Info(head + self.port.read_exact(8, 2.0))
|
return Info(head + self.port.read_exact(8, 0.5))
|
||||||
return Info.from_identity(head)
|
return Info.from_identity(head)
|
||||||
|
|
||||||
|
def identity(self):
|
||||||
|
"""Ask a live session who it is.
|
||||||
|
|
||||||
|
The identity command takes no argument and changes nothing, which makes
|
||||||
|
it the one question whose answer is known in advance — so it doubles as
|
||||||
|
the host's check that the stream is still in step, and as the byte a
|
||||||
|
lost host resynchronises on."""
|
||||||
|
self.port.write(b"b")
|
||||||
|
answer = self._read_identity()
|
||||||
|
self._expect_prompt()
|
||||||
|
return answer
|
||||||
|
|
||||||
def _handshake(self, wait, knock, what):
|
def _handshake(self, wait, knock, what):
|
||||||
"""One activation, retried until the loader answers or the window
|
"""One activation, retried until the loader answers or the window
|
||||||
closes. The identity reply is what proves the loader is listening — a
|
closes. The identity reply is what proves the loader is listening — a
|
||||||
@@ -476,16 +717,44 @@ class Loader:
|
|||||||
into a fresh window, where a command without its knock is discarded.
|
into a fresh window, where a command without its knock is discarded.
|
||||||
Each attempt is therefore the whole handshake. This also converges into
|
Each attempt is therefore the whole handshake. This also converges into
|
||||||
an already-live session: the knock bytes are ignored there and the
|
an already-live session: the knock bytes are ignored there and the
|
||||||
drain absorbs whatever they produced."""
|
drain absorbs whatever they produced.
|
||||||
|
|
||||||
|
Before the port's first knock ever, the line is drained until quiet: a
|
||||||
|
prompt from a previous session (`--stay`) can still be in the USB
|
||||||
|
pipeline when the port opens, where a flush cannot clear what has not
|
||||||
|
arrived yet — and on a board that resets when its port opens, trusting
|
||||||
|
that stale byte would spend the fresh activation window reading noise
|
||||||
|
from a device that never heard the knock. Once only, and bounded:
|
||||||
|
later re-knocks in this session face no foreign leftovers, and their
|
||||||
|
own window is already burning."""
|
||||||
deadline = time.monotonic() + wait
|
deadline = time.monotonic() + wait
|
||||||
|
if not self._line_drained:
|
||||||
|
self._line_drained = True
|
||||||
|
drain = time.monotonic() + 0.25
|
||||||
|
while self.port.read_available(0.05):
|
||||||
|
if time.monotonic() > drain:
|
||||||
|
break
|
||||||
knocks = 0
|
knocks = 0
|
||||||
|
refusal = None
|
||||||
|
# The knock is the only write in the protocol with no ack between its
|
||||||
|
# bytes, so on a shared line it is the only one whose echo may
|
||||||
|
# legitimately not come back — the device's ack collides with it and
|
||||||
|
# wins (OneWirePort.write). Losing a byte here is what the retry below
|
||||||
|
# is for; raising instead aborted the loop before it ever ran, which on
|
||||||
|
# real wiring made every reconnect into a live session fail.
|
||||||
|
knock_out = getattr(self.port, "write_blind", self.port.write)
|
||||||
while True:
|
while True:
|
||||||
self.port.flush_input()
|
self.port.flush_input()
|
||||||
self.port.write(knock)
|
knock_out(knock)
|
||||||
knocks += 1
|
knocks += 1
|
||||||
if PROMPT in self.port.read_available(0.4):
|
if PROMPT in self.port.read_available(0.4):
|
||||||
|
# Settle: absorb a real loader's trailing bytes before asking
|
||||||
|
# for the identity. Bounded by the deadline so a target that
|
||||||
|
# never falls quiet — a board stuck in a reset loop, whose
|
||||||
|
# garbage carries a stray prompt — cannot spin here forever.
|
||||||
while self.port.read_available(0.3):
|
while self.port.read_available(0.3):
|
||||||
pass
|
if time.monotonic() > deadline:
|
||||||
|
break
|
||||||
self.port.write(b"b")
|
self.port.write(b"b")
|
||||||
try:
|
try:
|
||||||
# A version the tool cannot speak is the loader's own
|
# A version the tool cannot speak is the loader's own
|
||||||
@@ -495,12 +764,18 @@ class Loader:
|
|||||||
except Error as failed:
|
except Error as failed:
|
||||||
if "pureboot" in str(failed):
|
if "pureboot" in str(failed):
|
||||||
raise
|
raise
|
||||||
|
# A malformed or unknown identity is retried as noise, but
|
||||||
|
# it was an answer: if nothing better ever arrives, naming
|
||||||
|
# it beats reporting silence.
|
||||||
|
refusal = failed
|
||||||
self.info = None
|
self.info = None
|
||||||
if self.info is not None:
|
if self.info is not None:
|
||||||
self._expect_prompt()
|
self._expect_prompt()
|
||||||
verbose(f"loader answered {what} {knocks}; identity read")
|
verbose(f"loader answered {what} {knocks}; identity read")
|
||||||
return self.info
|
return self.info
|
||||||
if time.monotonic() > deadline:
|
if time.monotonic() > deadline:
|
||||||
|
if refusal is not None:
|
||||||
|
raise Error(f"no usable answer — the last identity reply failed: {refusal}")
|
||||||
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):
|
def connect(self, wait):
|
||||||
@@ -523,6 +798,32 @@ class Loader:
|
|||||||
if byte != PROMPT:
|
if byte != PROMPT:
|
||||||
raise Error(f"expected prompt, got {byte.hex()}")
|
raise Error(f"expected prompt, got {byte.hex()}")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sealed(self):
|
||||||
|
"""pureboot 9 and later: bit opcodes, a sealed header, and a verdict
|
||||||
|
on that seal before the command runs."""
|
||||||
|
return self.info is not None and self.info.version >= SEALED_LOADER
|
||||||
|
|
||||||
|
def _header(self, op, space, address, count, timeout=2.0):
|
||||||
|
"""Send a sealed header and take the loader's verdict on it.
|
||||||
|
|
||||||
|
Returning normally means the loader has accepted the command and not
|
||||||
|
yet done it — which is the whole point of the verdict, and why the
|
||||||
|
payload of a fill or a write burst is sent only after this returns."""
|
||||||
|
head = bytes((op, selector(space, address), address & 0xFF, (address >> 8) & 0xFF, count & 0xFF))
|
||||||
|
seal = SEAL
|
||||||
|
for byte in head:
|
||||||
|
seal ^= byte
|
||||||
|
self.port.write(head + bytes((seal,)))
|
||||||
|
answer = self.port.read_exact(1, timeout)
|
||||||
|
if answer == NAK:
|
||||||
|
raise Error(
|
||||||
|
f"the loader refused the command (opcode {op:#04x}, {address:#06x}): the "
|
||||||
|
f"seal did not match, so nothing was done — the link mangled the header"
|
||||||
|
)
|
||||||
|
if answer != PROMPT:
|
||||||
|
raise Error(f"expected a verdict on the seal, got {answer.hex()}")
|
||||||
|
|
||||||
def _command(self, tx, reply_len=0, timeout=2.0):
|
def _command(self, tx, reply_len=0, timeout=2.0):
|
||||||
self.port.write(tx)
|
self.port.write(tx)
|
||||||
reply = self.port.read_exact(reply_len, timeout) if reply_len else b""
|
reply = self.port.read_exact(reply_len, timeout) if reply_len else b""
|
||||||
@@ -550,6 +851,11 @@ class Loader:
|
|||||||
data = b""
|
data = b""
|
||||||
while count:
|
while count:
|
||||||
chunk = min(count, 256, 0x10000 - (address & 0xFFFF))
|
chunk = min(count, 256, 0x10000 - (address & 0xFFFF))
|
||||||
|
if self.sealed:
|
||||||
|
self._header(0, space, address, chunk, 5.0)
|
||||||
|
data += self.port.read_exact(chunk, 5.0)
|
||||||
|
self._expect_prompt(5.0)
|
||||||
|
else:
|
||||||
head = bytes((ord("G"), selector(space, address), address & 0xFF,
|
head = bytes((ord("G"), selector(space, address), address & 0xFF,
|
||||||
(address >> 8) & 0xFF, chunk & 0xFF))
|
(address >> 8) & 0xFF, chunk & 0xFF))
|
||||||
data += self._command(head, chunk, 5.0)
|
data += self._command(head, chunk, 5.0)
|
||||||
@@ -564,9 +870,11 @@ class Loader:
|
|||||||
offset = 0
|
offset = 0
|
||||||
while offset < len(data):
|
while offset < len(data):
|
||||||
chunk = data[offset : offset + min(256, 0x10000 - (address & 0xFFFF))]
|
chunk = data[offset : offset + min(256, 0x10000 - (address & 0xFFFF))]
|
||||||
head = bytes((ord("g"), selector(space, address), address & 0xFF,
|
if self.sealed:
|
||||||
(address >> 8) & 0xFF, len(chunk) & 0xFF))
|
self._header(OP_WRITE, space, address, len(chunk))
|
||||||
self.port.write(head)
|
else:
|
||||||
|
self.port.write(bytes((ord("g"), selector(space, address), address & 0xFF,
|
||||||
|
(address >> 8) & 0xFF, len(chunk) & 0xFF)))
|
||||||
for byte in chunk:
|
for byte in chunk:
|
||||||
self.port.write(bytes((byte,)))
|
self.port.write(bytes((byte,)))
|
||||||
self._expect_prompt()
|
self._expect_prompt()
|
||||||
@@ -578,7 +886,16 @@ class Loader:
|
|||||||
|
|
||||||
def spm(self, operation, address):
|
def spm(self, operation, address):
|
||||||
"""One SPM operation at a flash address — the erase, write and RWW
|
"""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."""
|
re-enable that pureboot 4 ran inside 'W' and pureboot 5 leaves here.
|
||||||
|
|
||||||
|
From pureboot 9 the operation rides the header's count field instead of
|
||||||
|
arriving as data behind it, which is what puts it inside the seal: the
|
||||||
|
loader will not hand a byte to SPMCSR that the host did not seal, and
|
||||||
|
the verdict says whether it did."""
|
||||||
|
if self.sealed:
|
||||||
|
self._header(OP_WRITE, SP_SPM, address, operation, 5.0)
|
||||||
|
self._expect_prompt(5.0)
|
||||||
|
return
|
||||||
self._write_space(SP_SPM, address, bytes((operation,)))
|
self._write_space(SP_SPM, address, bytes((operation,)))
|
||||||
|
|
||||||
def read_ram(self, address, count):
|
def read_ram(self, address, count):
|
||||||
@@ -616,10 +933,20 @@ class Loader:
|
|||||||
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:
|
if self.unified:
|
||||||
# 'W' fills the page buffer and stops there; the erase and the write
|
# The fill loads the page buffer and stops there; the erase and the
|
||||||
# are host-issued SPM operations. Only a chip with a boot section
|
# write are host-issued SPM operations. Only a chip with a boot
|
||||||
# has RWW to re-enable — on the others bit 4 of SPMCSR means
|
# section has RWW to re-enable — on the others bit 4 of SPMCSR
|
||||||
# something else entirely, so it must not be sent.
|
# means something else entirely, so it must not be sent.
|
||||||
|
if self.sealed:
|
||||||
|
# The page goes out only once the header is accepted. It is the
|
||||||
|
# protocol's one unacked burst, so a header refused after the
|
||||||
|
# host had already started sending it would leave the page
|
||||||
|
# being read as commands — which is exactly what the verdict
|
||||||
|
# is placed here to prevent.
|
||||||
|
self._header(OP_FILL, SP_FLASH, address, len(data) & 0xFF)
|
||||||
|
self.port.write(data)
|
||||||
|
self._expect_prompt(2.0)
|
||||||
|
else:
|
||||||
head = bytes((ord("W"), selector(SP_FLASH, address), address & 0xFF, (address >> 8) & 0xFF))
|
head = bytes((ord("W"), selector(SP_FLASH, address), address & 0xFF, (address >> 8) & 0xFF))
|
||||||
self._command(head + data, 0, 2.0)
|
self._command(head + data, 0, 2.0)
|
||||||
self.spm(SPM_ERASE, address)
|
self.spm(SPM_ERASE, address)
|
||||||
@@ -655,16 +982,55 @@ class Loader:
|
|||||||
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):
|
||||||
"""The device acks, then execution continues at the word address."""
|
"""The device acks, then execution continues at the word address.
|
||||||
|
From v7 the jump rides the unified decode, so it carries a selector
|
||||||
|
byte the loader ignores; older loaders take the bare address. From v9
|
||||||
|
the ack is the verdict on its seal — a jump to a mangled address is a
|
||||||
|
jump into arbitrary code, so it is sealed like everything else."""
|
||||||
|
if self.sealed:
|
||||||
|
self._header(OP_JUMP, 0, word_address, 0)
|
||||||
|
return
|
||||||
|
if self.info.version >= 7:
|
||||||
|
self.port.write(bytes((ord("J"), 0, word_address & 0xFF, word_address >> 8)))
|
||||||
|
else:
|
||||||
self.port.write(bytes((ord("J"), word_address & 0xFF, word_address >> 8)))
|
self.port.write(bytes((ord("J"), word_address & 0xFF, word_address >> 8)))
|
||||||
self._expect_prompt()
|
self._expect_prompt()
|
||||||
|
|
||||||
def enter_copy(self, byte_address, wait):
|
def enter_copy(self, byte_address, wait, link=None):
|
||||||
"""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
|
|
||||||
|
`link` is that copy's own `(baud, autobaud)`, for when it is not this
|
||||||
|
session's. A staging copy *is* the new image, so it speaks the rate and
|
||||||
|
backend it was built for; the host has to be told which, because 512
|
||||||
|
bytes of position-independent code carry no header to read it from.
|
||||||
|
Retuning goes through the open port, so no DTR pulse resets the copy that
|
||||||
|
is now running — and the session keeps the new link afterwards, since
|
||||||
|
every later jump lands in the same image.
|
||||||
|
"""
|
||||||
|
baud, autobaud = link if link is not None else (self.baud, self.autobaud)
|
||||||
|
if link is not None:
|
||||||
|
self._link_declared = True
|
||||||
self.jump(byte_address // 2)
|
self.jump(byte_address // 2)
|
||||||
|
if baud is not None and baud != self.baud:
|
||||||
|
self.port.set_baud(baud)
|
||||||
|
self.baud = baud
|
||||||
|
self.autobaud = autobaud
|
||||||
|
try:
|
||||||
return self.connect_autobaud(wait) if autobaud else self.connect(wait)
|
return self.connect_autobaud(wait) if autobaud else self.connect(wait)
|
||||||
|
except Error as unheard:
|
||||||
|
if self._link_declared:
|
||||||
|
raise
|
||||||
|
# The bare activation timeout sends the operator to look at wiring,
|
||||||
|
# while on a patched-vector part the application region is already
|
||||||
|
# gone. Name the one cause that fits: the copy answers on its own
|
||||||
|
# link, not the resident's.
|
||||||
|
raise Error(
|
||||||
|
f"the copy at {byte_address:#06x} did not answer on this session's "
|
||||||
|
f"link ({baud} Bd, {'autobaud' if autobaud else 'fixed baud'}). An "
|
||||||
|
f"image built for another baud or backend speaks that one instead — "
|
||||||
|
f"say which with --staged-baud / --staged-autobaud"
|
||||||
|
) from unheard
|
||||||
|
|
||||||
def run_application(self):
|
def run_application(self):
|
||||||
self.jump(self.info.app_entry_word)
|
self.jump(self.info.app_entry_word)
|
||||||
@@ -1012,10 +1378,16 @@ def patch_word0(loader, page0, target_base):
|
|||||||
return bytes(patched)
|
return bytes(patched)
|
||||||
|
|
||||||
|
|
||||||
def op_update_loader(loader, wait, path, state_path, fuse_bytes):
|
def op_update_loader(loader, wait, path, state_path, fuse_bytes, staged_link=None):
|
||||||
"""Replace the resident loader with `path`, using the loader as its own
|
"""Replace the resident loader with `path`, using the loader as its own
|
||||||
staging loader. Every phase is idempotent and keyed off the flash state,
|
staging loader. Every phase is idempotent and keyed off the flash state,
|
||||||
so a re-run resumes; the state file carries what the staging slot held."""
|
so a re-run resumes; the state file carries what the staging slot held.
|
||||||
|
|
||||||
|
`staged_link` is the new image's own `(baud, autobaud)` where it differs from
|
||||||
|
this session's — the copies the host enters *are* that image, so they answer
|
||||||
|
on its link and not the resident's. Note what this does to the idempotence
|
||||||
|
above: once the staging copy is installed, the resumable state is only
|
||||||
|
reachable on the new link, so a re-run has to name it too."""
|
||||||
info = loader.info
|
info = loader.info
|
||||||
image = loader_image(path)
|
image = loader_image(path)
|
||||||
for warning in update_preflight(image, info, fuse_bytes):
|
for warning in update_preflight(image, info, fuse_bytes):
|
||||||
@@ -1034,7 +1406,7 @@ def op_update_loader(loader, wait, path, state_path, fuse_bytes):
|
|||||||
state.load_or_save(loader)
|
state.load_or_save(loader)
|
||||||
|
|
||||||
# A loader already sitting whole in the staging slot IS the staging copy:
|
# A loader already sitting whole in the staging slot IS the staging copy:
|
||||||
# rewriting it would only meet its own running-slot guard. Any pureboot
|
# rewriting it in place would be a copy overwriting itself as it runs. Any pureboot
|
||||||
# with the device's info block serves, since a staged copy only streams
|
# with the device's info block serves, since a staged copy only streams
|
||||||
# pages. "Whole" needs both checks — the block where every image carries
|
# pages. "Whole" needs both checks — the block where every image carries
|
||||||
# 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
|
||||||
@@ -1060,7 +1432,7 @@ def op_update_loader(loader, wait, path, state_path, fuse_bytes):
|
|||||||
# routes through the resident, word 0 is re-aimed at the staging copy for
|
# routes through the resident, word 0 is re-aimed at the staging copy for
|
||||||
# the rewrite, so a power loss mid-rewrite still resets into a loader.
|
# the rewrite, so a power loss mid-rewrite still resets into a loader.
|
||||||
verbose(f"entering the staging copy at {info.stage:#06x}")
|
verbose(f"entering the staging copy at {info.stage:#06x}")
|
||||||
loader.enter_copy(info.stage, wait)
|
loader.enter_copy(info.stage, wait, link=staged_link)
|
||||||
redirect = info.patch_vector and info.stage != 0
|
redirect = info.patch_vector and info.stage != 0
|
||||||
if redirect:
|
if redirect:
|
||||||
verbose("word 0 re-aimed at the staging copy for the rewrite")
|
verbose("word 0 re-aimed at the staging copy for the rewrite")
|
||||||
@@ -1278,6 +1650,65 @@ def op_fuses(loader):
|
|||||||
return fuse_bytes
|
return fuse_bytes
|
||||||
|
|
||||||
|
|
||||||
|
def scan_ratios():
|
||||||
|
"""The probe walk, in percent of the built rate: the built rate itself
|
||||||
|
first, then ±10 % in 2 % steps nearest-first — a drifted oscillator near
|
||||||
|
its trim is the common case, and each probe costs a reset."""
|
||||||
|
return [0] + [sign * step for step in (2, 4, 6, 8, 10) for sign in (-1, 1)]
|
||||||
|
|
||||||
|
|
||||||
|
def scan_rate(baud, pct):
|
||||||
|
return round(baud * (100 + pct) / 100)
|
||||||
|
|
||||||
|
|
||||||
|
def scan_report(baud, pct, version, clock=None):
|
||||||
|
"""The findings, one per line: the found rate is the session workaround,
|
||||||
|
its ratio to the built rate is the oscillator's offset, and the fixes are
|
||||||
|
the OSCCAL bake (≈1 %/step, opposing the drift) or the autobaud build."""
|
||||||
|
rate = scan_rate(baud, pct)
|
||||||
|
lines = [f"scan: answered at {rate} Bd ({pct:+d} % of the built rate) — pureboot {version}",
|
||||||
|
f" session --baud {rate}"]
|
||||||
|
if clock:
|
||||||
|
lines.append(f" clock ~{clock * (100 + pct) // 100} Hz (built for {clock})")
|
||||||
|
if pct:
|
||||||
|
direction = "lower" if pct > 0 else "higher"
|
||||||
|
lines.append(f" fix rebuild with OSCCAL ~{abs(pct)} steps {direction} (~1 %/step), "
|
||||||
|
"or the autobaud build")
|
||||||
|
else:
|
||||||
|
lines.append(" fix none — the built rate answers; check the earlier wiring instead")
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
def op_scan(port_path, baud, wait, clock=None, one_wire=False):
|
||||||
|
"""A fixed-baud loader whose oscillator drifted still answers — at the
|
||||||
|
drifted ratio, since its rate scales with its clock. One probe per
|
||||||
|
activation window, and with an application resident the window opens
|
||||||
|
exactly once per reset, so each probe announces itself and expects a
|
||||||
|
fresh reset before knocking. On a shared line the probes echo back like
|
||||||
|
everything else; undiscarded they would answer every rate."""
|
||||||
|
for pct in scan_ratios():
|
||||||
|
rate = scan_rate(baud, pct)
|
||||||
|
print(f"scan: {rate} Bd ({pct:+d} %) — reset the target", flush=True)
|
||||||
|
try:
|
||||||
|
port = Port(port_path, rate)
|
||||||
|
except Error as unmakeable:
|
||||||
|
print(f"scan: {rate} Bd skipped — {unmakeable}")
|
||||||
|
continue
|
||||||
|
if one_wire:
|
||||||
|
port = OneWirePort(port)
|
||||||
|
try:
|
||||||
|
info = Loader(port).connect(wait)
|
||||||
|
except Error:
|
||||||
|
continue
|
||||||
|
finally:
|
||||||
|
port.close()
|
||||||
|
for line in scan_report(baud, pct, info.version, clock):
|
||||||
|
print(line)
|
||||||
|
return
|
||||||
|
raise Error("no answer within ±10 % of the built rate — check the wiring, or deploy the "
|
||||||
|
"autobaud build, which has no rate to miss (README.md)")
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------- cli ---
|
# -------------------------------------------------------------------- cli ---
|
||||||
|
|
||||||
|
|
||||||
@@ -1290,13 +1721,31 @@ 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("--one-wire", action="store_true",
|
||||||
|
help="the link is a shared line: read back and discard this tool's own "
|
||||||
|
"echoed bytes (any backend of a one-wire deployment)")
|
||||||
parser.add_argument("--autobaud", action="store_true",
|
parser.add_argument("--autobaud", action="store_true",
|
||||||
help="drive an autobaud loader: send the 0xC0 calibration pulse and a single "
|
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)")
|
"knock, and take geometry from the signature (no clock/baud baked in)")
|
||||||
|
parser.add_argument("--scan", action="store_true",
|
||||||
|
help="walk ±10%% around --baud for a fixed-baud loader gone silent — one "
|
||||||
|
"reset per probe, standalone (README.md: deployment)")
|
||||||
|
parser.add_argument("--clock", type=int, metavar="HZ",
|
||||||
|
help="the clock the loader was built for — lets --scan and an autobaud "
|
||||||
|
"--info state drift in absolute terms")
|
||||||
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")
|
||||||
parser.add_argument("--state", metavar="FILE", help="update state file (default: FILE.pbstate)")
|
parser.add_argument("--state", metavar="FILE", help="update state file (default: FILE.pbstate)")
|
||||||
|
# The update enters the staging copy, which is the new image and so speaks
|
||||||
|
# the link *it* was built for. Nothing in the image says which, so where it
|
||||||
|
# differs from this session's these name it and the host follows.
|
||||||
|
parser.add_argument("--staged-baud", metavar="BD", type=int,
|
||||||
|
help="the baud the --update-loader image was built for, where it "
|
||||||
|
"differs from --baud")
|
||||||
|
parser.add_argument("--staged-autobaud", action=argparse.BooleanOptionalAction, default=None,
|
||||||
|
help="whether that image is an autobaud build, where it differs "
|
||||||
|
"from --autobaud")
|
||||||
parser.add_argument("--assume-fuses", metavar="HEX8", help="fuse bytes low,lock,ext,high as 8 hex digits "
|
parser.add_argument("--assume-fuses", metavar="HEX8", help="fuse bytes low,lock,ext,high as 8 hex digits "
|
||||||
"(overrides reading them — e.g. under a simulator that cannot)")
|
"(overrides reading them — e.g. under a simulator that cannot)")
|
||||||
parser.add_argument("--erase-flash", action="store_true", help="0xff over the application flash")
|
parser.add_argument("--erase-flash", action="store_true", help="0xff over the application flash")
|
||||||
@@ -1330,8 +1779,17 @@ def main():
|
|||||||
except (ValueError, AssertionError):
|
except (ValueError, AssertionError):
|
||||||
parser.error("--assume-fuses takes 8 hex digits: low,lock,extended,high")
|
parser.error("--assume-fuses takes 8 hex digits: low,lock,extended,high")
|
||||||
|
|
||||||
|
if args.scan:
|
||||||
|
if args.autobaud:
|
||||||
|
parser.error("--scan probes fixed rates; an autobaud loader has none to miss")
|
||||||
|
op_scan(args.port, args.baud, args.wait, args.clock, args.one_wire)
|
||||||
|
return
|
||||||
|
|
||||||
port = Port(args.port, args.baud)
|
port = Port(args.port, args.baud)
|
||||||
verbose(f"{args.port}: {args.baud} Bd 8N1, DTR/RTS asserted")
|
if args.one_wire:
|
||||||
|
port = OneWirePort(port)
|
||||||
|
verbose(f"{args.port}: {args.baud} Bd 8N1, DTR/RTS asserted"
|
||||||
|
+ (", one-wire echo discarded" if args.one_wire else ""))
|
||||||
try:
|
try:
|
||||||
loader = Loader(port)
|
loader = Loader(port)
|
||||||
info = loader.connect_autobaud(args.wait) if args.autobaud else loader.connect(args.wait)
|
info = loader.connect_autobaud(args.wait) if args.autobaud else loader.connect(args.wait)
|
||||||
@@ -1339,6 +1797,18 @@ def main():
|
|||||||
print("device:")
|
print("device:")
|
||||||
for line in info.lines():
|
for line in info.lines():
|
||||||
print(f" {line}")
|
print(f" {line}")
|
||||||
|
if args.autobaud and info.unit_home is not None:
|
||||||
|
# The measured bit period, from wherever this version keeps it
|
||||||
|
# (unit_home); decoded and times the rate this session drives,
|
||||||
|
# that is the true clock — the number to hold an OSCCAL bake
|
||||||
|
# or a fixed-baud build against (README.md: deployment). The
|
||||||
|
# autobaud identity path refuses unknown signatures, so the
|
||||||
|
# home is always known here; the guard states that dependency.
|
||||||
|
unit = int.from_bytes(loader.read_ram(info.unit_home, 2), "little")
|
||||||
|
cycles = unit * UNIT_LOOP_CYCLES + UNIT_DISCOUNT
|
||||||
|
clock = cycles * args.baud
|
||||||
|
offset = f", {(clock / args.clock - 1) * 100:+.1f} % of {args.clock}" if args.clock else ""
|
||||||
|
print(f" measured {clock} Hz ({cycles} cycles/bit × {args.baud} Bd{offset})")
|
||||||
fuse_bytes = fuse_override
|
fuse_bytes = fuse_override
|
||||||
if args.fuses or (args.update_loader and not info.patch_vector and fuse_bytes is None):
|
if args.fuses or (args.update_loader and not info.patch_vector and fuse_bytes is None):
|
||||||
read = op_fuses(loader)
|
read = op_fuses(loader)
|
||||||
@@ -1346,7 +1816,14 @@ def main():
|
|||||||
fuse_bytes = read
|
fuse_bytes = read
|
||||||
if args.update_loader:
|
if args.update_loader:
|
||||||
state = args.state or args.update_loader + ".pbstate"
|
state = args.state or args.update_loader + ".pbstate"
|
||||||
op_update_loader(loader, args.wait, args.update_loader, state, fuse_bytes)
|
staged_link = None
|
||||||
|
if args.staged_baud is not None or args.staged_autobaud is not None:
|
||||||
|
staged_link = (
|
||||||
|
args.staged_baud if args.staged_baud is not None else args.baud,
|
||||||
|
args.staged_autobaud if args.staged_autobaud is not None else args.autobaud,
|
||||||
|
)
|
||||||
|
op_update_loader(loader, args.wait, args.update_loader, state, fuse_bytes,
|
||||||
|
staged_link)
|
||||||
if args.flash:
|
if args.flash:
|
||||||
op_flash(loader, args.flash, args.erase_flash, not args.no_verify, fuse_bytes, args.force)
|
op_flash(loader, args.flash, args.erase_flash, not args.no_verify, fuse_bytes, args.force)
|
||||||
elif args.erase_flash:
|
elif args.erase_flash:
|
||||||
@@ -1379,7 +1856,7 @@ def main():
|
|||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
try:
|
try:
|
||||||
main()
|
main()
|
||||||
except Error as error:
|
except (Error, OSError) as error:
|
||||||
print(f"error: {error}", file=sys.stderr)
|
print(f"error: {error}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
|
|||||||
36
test/check_unit.cmake
Normal file
36
test/check_unit.cmake
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Asserts the autobaud loader's measured unit sits where the host will read
|
||||||
|
# it (--info's measured clock — the address is wire contract). Two homes: on
|
||||||
|
# a chip with the GPIOR pair the unit lives there and the image must carry no
|
||||||
|
# RAM word for it at all; elsewhere it is the first RAM object at SRAM start.
|
||||||
|
# Run as
|
||||||
|
# cmake -DOBJDUMP=... -DELF=... -DRAM_START=<data address> [-DGPIOR=<data address>]
|
||||||
|
# -P check_unit.cmake
|
||||||
|
|
||||||
|
execute_process(COMMAND ${OBJDUMP} -t ${ELF} OUTPUT_VARIABLE _syms RESULT_VARIABLE _res)
|
||||||
|
if(NOT _res EQUAL 0)
|
||||||
|
message(FATAL_ERROR "${OBJDUMP} -t ${ELF} failed")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# The symbol line: "00800100 l O .noinit 00000002 <mangled>unit_E".
|
||||||
|
string(REGEX MATCH "\n0*([0-9a-f]+)[^\n]+[ \t][^ \t\n]*unit_E\n" _line "${_syms}")
|
||||||
|
|
||||||
|
if(GPIOR)
|
||||||
|
if(_line)
|
||||||
|
message(FATAL_ERROR "unit_ RAM symbol present although the unit's home is GPIOR ${GPIOR} — "
|
||||||
|
"the host peeks the pair, and a RAM copy would be dead weight")
|
||||||
|
endif()
|
||||||
|
message(STATUS "no unit_ RAM object — the unit lives in the GPIOR pair at ${GPIOR}")
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(NOT _line)
|
||||||
|
message(FATAL_ERROR "no unit_ symbol in ${ELF} — is this the autobaud loader?")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# AVR data-space symbols carry the 0x800000 VMA offset.
|
||||||
|
math(EXPR _want "0x800000 + ${RAM_START}" OUTPUT_FORMAT HEXADECIMAL)
|
||||||
|
math(EXPR _have "0x${CMAKE_MATCH_1}" OUTPUT_FORMAT HEXADECIMAL)
|
||||||
|
if(NOT _have STREQUAL _want)
|
||||||
|
message(FATAL_ERROR "unit_ sits at ${_have}, ram_start is ${_want} — the host peeks ram_start")
|
||||||
|
endif()
|
||||||
|
message(STATUS "unit_ at ${_have} == ram_start")
|
||||||
@@ -7,77 +7,95 @@
|
|||||||
// SPM genuinely writes avr->flash on the mega cores, so on exit (or SIGTERM)
|
// SPM genuinely writes avr->flash on the mega cores, so on exit (or SIGTERM)
|
||||||
// we dump the flash image to a file for a ground-truth cross-check against
|
// we dump the flash image to a file for a ground-truth cross-check against
|
||||||
// what the client read back through the bootloader.
|
// what the client read back through the bootloader.
|
||||||
#include <signal.h>
|
#include <csignal>
|
||||||
#include <stdint.h>
|
#include <cstdint>
|
||||||
#include <stdio.h>
|
#include <cstdio>
|
||||||
#include <stdlib.h>
|
#include <cstdlib>
|
||||||
#include <string.h>
|
#include <cstring>
|
||||||
|
#include <print>
|
||||||
|
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
|
// The parts headers (uart_pty.h) carry no C++ linkage guards of their own,
|
||||||
|
// unlike simavr's core headers — the block covers both harmlessly.
|
||||||
|
extern "C" {
|
||||||
#include "avr_uart.h"
|
#include "avr_uart.h"
|
||||||
#include "sim_avr.h"
|
#include "sim_avr.h"
|
||||||
#include "sim_elf.h"
|
#include "sim_elf.h"
|
||||||
#include "uart_pty.h"
|
#include "uart_pty.h"
|
||||||
|
}
|
||||||
|
|
||||||
static avr_t *avr;
|
namespace {
|
||||||
static uart_pty_t uart_pty;
|
|
||||||
static const char *dump_path;
|
|
||||||
|
|
||||||
static void finish(int sig)
|
avr_t *avr;
|
||||||
|
uart_pty_t uart_pty;
|
||||||
|
const char *dump_path;
|
||||||
|
|
||||||
|
[[noreturn]] void finish(int)
|
||||||
{
|
{
|
||||||
(void)sig;
|
|
||||||
if (dump_path) {
|
if (dump_path) {
|
||||||
FILE *f = fopen(dump_path, "wb");
|
std::FILE *f = std::fopen(dump_path, "wb");
|
||||||
if (f) {
|
if (f) {
|
||||||
fwrite(avr->flash, 1, avr->flashend + 1, f);
|
std::fwrite(avr->flash, 1, avr->flashend + 1, f);
|
||||||
fclose(f);
|
std::fclose(f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
uart_pty_stop(&uart_pty);
|
uart_pty_stop(&uart_pty);
|
||||||
_exit(0);
|
_exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
if (argc < 3) {
|
if (argc < 3) {
|
||||||
fprintf(stderr, "usage: %s <tsb.elf> <boot_base_hex> [flash_dump.bin]\n", argv[0]);
|
std::println(stderr, "usage: {} <tsb.elf> <boot_base_hex> [flash_dump.bin]", argv[0]);
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
uint32_t boot_base = (uint32_t)strtoul(argv[2], NULL, 0);
|
auto boot_base = static_cast<std::uint32_t>(std::strtoul(argv[2], nullptr, 0));
|
||||||
dump_path = argc >= 4 ? argv[3] : NULL;
|
dump_path = argc >= 4 ? argv[3] : nullptr;
|
||||||
|
|
||||||
avr = avr_make_mcu_by_name("atmega328p");
|
avr = avr_make_mcu_by_name("atmega328p");
|
||||||
if (!avr) {
|
if (!avr) {
|
||||||
fprintf(stderr, "device: no ATmega328P core\n");
|
std::println(stderr, "device: no ATmega328P core");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
avr_init(avr);
|
avr_init(avr);
|
||||||
avr->frequency = 16000000;
|
avr->frequency = 16000000;
|
||||||
// Real flash powers up erased (0xff); the app region must look erased
|
// Real flash powers up erased (0xff); the app region must look erased
|
||||||
// before the bootloader programs it.
|
// before the bootloader programs it.
|
||||||
memset(avr->flash, 0xff, avr->flashend + 1);
|
std::memset(avr->flash, 0xff, avr->flashend + 1);
|
||||||
|
|
||||||
// simavr's ELF loader flattens the flash base to 0 (it expects an app at
|
// simavr's ELF loader flattens the flash base to 0 (it expects an app at
|
||||||
// 0x0), but it hands back the boot code in fw.flash; place it at the boot
|
// 0x0), but it hands back the boot code in fw.flash; place it at the boot
|
||||||
// section base ourselves and enter there (BOOTRST is not modelled).
|
// section base ourselves and enter there (BOOTRST is not modelled).
|
||||||
elf_firmware_t fw = {0};
|
elf_firmware_t fw{};
|
||||||
if (elf_read_firmware(argv[1], &fw) != 0) {
|
if (elf_read_firmware(argv[1], &fw) != 0) {
|
||||||
fprintf(stderr, "device: cannot read %s\n", argv[1]);
|
std::println(stderr, "device: cannot read {}", argv[1]);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
memcpy(avr->flash + boot_base, fw.flash, fw.flashsize);
|
// An image that runs past flash end cannot execute on hardware, and a
|
||||||
|
// naive copy of it would smash the heap beyond avr->flash — after which
|
||||||
|
// the simulation misbehaves in ways that point everywhere but here.
|
||||||
|
// Refuse it loudly instead.
|
||||||
|
if (boot_base + fw.flashsize > avr->flashend + 1) {
|
||||||
|
std::println(stderr, "device: {} B at {:#x} runs past flash end {:#x} — image does not fit its slot",
|
||||||
|
fw.flashsize, boot_base, avr->flashend);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
std::memcpy(avr->flash + boot_base, fw.flash, fw.flashsize);
|
||||||
avr->pc = boot_base;
|
avr->pc = boot_base;
|
||||||
avr->codeend = avr->flashend;
|
avr->codeend = avr->flashend;
|
||||||
|
|
||||||
// Optional: seed the config page (one page below the boot section) with a
|
// Optional: seed the config page (one page below the boot section) with a
|
||||||
// hex byte string, so the password gate and emergency erase can be tested.
|
// hex byte string, so the password gate and emergency erase can be tested.
|
||||||
// Layout: [appjump lo][appjump hi][timeout][password...][0xff].
|
// Layout: [appjump lo][appjump hi][timeout][password...][0xff].
|
||||||
const char *cfg = getenv("TSB_CONFIG");
|
const char *cfg = std::getenv("TSB_CONFIG");
|
||||||
if (cfg) {
|
if (cfg) {
|
||||||
uint32_t app_end = boot_base - 128; // config page sits directly below the boot code
|
std::uint32_t app_end = boot_base - 128; // config page sits directly below the boot code
|
||||||
for (int i = 0; cfg[i] && cfg[i + 1]; i += 2) {
|
for (int i = 0; cfg[i] && cfg[i + 1]; i += 2) {
|
||||||
char b[3] = {cfg[i], cfg[i + 1], 0};
|
char b[3] = {cfg[i], cfg[i + 1], 0};
|
||||||
avr->flash[app_end + i / 2] = (uint8_t)strtoul(b, NULL, 16);
|
avr->flash[app_end + i / 2] = static_cast<std::uint8_t>(std::strtoul(b, nullptr, 16));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,18 +104,18 @@ int main(int argc, char *argv[])
|
|||||||
// tight-polling loader (one that releases TX between bytes, as one-wire does)
|
// tight-polling loader (one that releases TX between bytes, as one-wire does)
|
||||||
// in real time, distorting protocol timing. Clear it so the loader runs at
|
// in real time, distorting protocol timing. Clear it so the loader runs at
|
||||||
// true cycle speed.
|
// true cycle speed.
|
||||||
uint32_t uflags = 0;
|
std::uint32_t uflags = 0;
|
||||||
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS('0'), &uflags);
|
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS('0'), &uflags);
|
||||||
uflags &= ~AVR_UART_FLAG_POLL_SLEEP;
|
uflags &= ~AVR_UART_FLAG_POLL_SLEEP;
|
||||||
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS('0'), &uflags);
|
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS('0'), &uflags);
|
||||||
|
|
||||||
uart_pty_init(avr, &uart_pty);
|
uart_pty_init(avr, &uart_pty);
|
||||||
uart_pty_connect(&uart_pty, '0');
|
uart_pty_connect(&uart_pty, '0');
|
||||||
printf("TSB_PTY %s\n", uart_pty.pty.slavename);
|
std::println("TSB_PTY {}", uart_pty.pty.slavename);
|
||||||
fflush(stdout);
|
std::fflush(stdout);
|
||||||
|
|
||||||
signal(SIGTERM, finish);
|
std::signal(SIGTERM, finish);
|
||||||
signal(SIGINT, finish);
|
std::signal(SIGINT, finish);
|
||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
int state = avr_run(avr);
|
int state = avr_run(avr);
|
||||||
@@ -105,5 +123,4 @@ int main(int argc, char *argv[])
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
finish(0);
|
finish(0);
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,10 @@
|
|||||||
// reset reaches those loaders through the patched vector (or the runner
|
// reset reaches those loaders through the patched vector (or the runner
|
||||||
// models BOOTRST), so the application owes them nothing.
|
// models BOOTRST), so the application owes them nothing.
|
||||||
//
|
//
|
||||||
|
// PUREBOOT_HANDOVER drops the listening and jumps straight in, leaving the
|
||||||
|
// USART enabled behind it — the hand-over state a loader bit-banging on that
|
||||||
|
// USART's own pins has to survive.
|
||||||
|
//
|
||||||
// The fixture speaks the deployment its loader was built for: the same
|
// The fixture speaks the deployment its loader was built for: the same
|
||||||
// PUREBOOT_* defines configure it, and without them it assumes the stock
|
// PUREBOOT_* defines configure it, and without them it assumes the stock
|
||||||
// deployment (the crystal/RC clock table below, the chip's natural link).
|
// deployment (the crystal/RC clock table below, the chip's natural link).
|
||||||
@@ -37,6 +41,9 @@ consteval avr::hertz_t clock()
|
|||||||
#if !defined(PUREBOOT_TX)
|
#if !defined(PUREBOOT_TX)
|
||||||
#define PUREBOOT_TX pb1
|
#define PUREBOOT_TX pb1
|
||||||
#endif
|
#endif
|
||||||
|
#if !defined(PUREBOOT_RX)
|
||||||
|
#define PUREBOOT_RX pb0
|
||||||
|
#endif
|
||||||
#if !defined(PUREBOOT_USART)
|
#if !defined(PUREBOOT_USART)
|
||||||
#define PUREBOOT_USART 0
|
#define PUREBOOT_USART 0
|
||||||
#endif
|
#endif
|
||||||
@@ -46,7 +53,7 @@ consteval bool use_hardware()
|
|||||||
#if defined(PUREBOOT_SOFT_SERIAL)
|
#if defined(PUREBOOT_SOFT_SERIAL)
|
||||||
return false;
|
return false;
|
||||||
#else
|
#else
|
||||||
return avr::hw::db.has_instance("USART0") || avr::hw::db.has_instance("USART");
|
return avr::uart::has_usart<0>();
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,21 +66,38 @@ struct link {
|
|||||||
#else
|
#else
|
||||||
static constexpr avr::baud_t baud{115200};
|
static constexpr avr::baud_t baud{115200};
|
||||||
#endif
|
#endif
|
||||||
using tx_t = avr::uart::usart<'0' + PUREBOOT_USART, C, {.baud = baud, .max_baud_error = 2.5_pct}>;
|
using tx_t = avr::uart::usart<PUREBOOT_USART, C, {.baud = baud, .max_baud_error = 2.5_pct}>;
|
||||||
|
static void init()
|
||||||
|
{
|
||||||
|
avr::init<tx_t>();
|
||||||
|
}
|
||||||
static void tx(char c)
|
static void tx(char c)
|
||||||
{
|
{
|
||||||
tx_t::write(static_cast<std::uint8_t>(c));
|
tx_t::write(static_cast<std::uint8_t>(c));
|
||||||
}
|
}
|
||||||
|
// The loader sits in the top slot — 512 bytes on every chip. The jump
|
||||||
|
// takes a word address, which is what makes the >64 KiB chips' entry
|
||||||
|
// reachable through a 16-bit pointer at all.
|
||||||
|
static void enter_loader()
|
||||||
|
{
|
||||||
|
constexpr std::uint32_t slot = 512;
|
||||||
|
reinterpret_cast<void (*)()>(static_cast<std::uint16_t>((avr::hw::db.mem.flash_size - slot) / 2))();
|
||||||
|
}
|
||||||
|
|
||||||
[[noreturn]] static void idle()
|
[[noreturn]] static void idle()
|
||||||
{
|
{
|
||||||
// 'L' hands back to the loader in the top slot — 512 bytes on every
|
#if defined(PUREBOOT_HANDOVER)
|
||||||
// chip. The jump takes a word address, which is what makes the
|
// Hand back at once, with this USART still enabled — the state that
|
||||||
// >64 KiB chips' entry reachable through a 16-bit pointer at all.
|
// leaves a bit-banged loader on its pins mute unless the loader
|
||||||
constexpr std::uint32_t slot = 512;
|
// releases it. Unconditional because there is no command wire to
|
||||||
|
// wait on: that loader's link is the pins, not this peripheral.
|
||||||
|
enter_loader();
|
||||||
|
__builtin_unreachable();
|
||||||
|
#else
|
||||||
for (;;) {
|
for (;;) {
|
||||||
auto command = tx_t::read_blocking();
|
auto command = tx_t::read_blocking();
|
||||||
if (command == 'L')
|
if (command == 'L')
|
||||||
reinterpret_cast<void (*)()>(static_cast<std::uint16_t>((avr::hw::db.mem.flash_size - slot) / 2))();
|
enter_loader();
|
||||||
// 'D' leaves every word of the SPM page buffer dirty, so that a
|
// 'D' leaves every word of the SPM page buffer dirty, so that a
|
||||||
// following 'L' enters the loader with the buffer it never clears.
|
// following 'L' enters the loader with the buffer it never clears.
|
||||||
if (command == 'D') {
|
if (command == 'D') {
|
||||||
@@ -82,6 +106,7 @@ struct link {
|
|||||||
tx('D');
|
tx('D');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -92,15 +117,48 @@ struct link<C, false> {
|
|||||||
#else
|
#else
|
||||||
static constexpr avr::baud_t baud{57600};
|
static constexpr avr::baud_t baud{57600};
|
||||||
#endif
|
#endif
|
||||||
using tx_t = avr::uart::software_tx<C, avr::PUREBOOT_TX, baud>;
|
// A shared-pin deployment (RX == TX) banners as a guest on its own line:
|
||||||
|
// the pull-up input is the released line, the transmitter takes the pin
|
||||||
|
// for exactly one frame per byte — the shape a real one-wire application
|
||||||
|
// beside this loader uses.
|
||||||
|
static constexpr bool one_wire = avr::PUREBOOT_RX == avr::PUREBOOT_TX;
|
||||||
|
using tx_t = avr::uart::software_tx<C, avr::PUREBOOT_TX, baud, one_wire>;
|
||||||
|
static void init()
|
||||||
|
{
|
||||||
|
// The guest transmitter configures no pin; the released line — the
|
||||||
|
// pull-up input a receiver would own — is established here.
|
||||||
|
if constexpr (one_wire)
|
||||||
|
avr::init<avr::io::input<avr::PUREBOOT_TX, avr::io::pull::up>, tx_t>();
|
||||||
|
else
|
||||||
|
avr::init<tx_t>();
|
||||||
|
}
|
||||||
static void tx(char c)
|
static void tx(char c)
|
||||||
{
|
{
|
||||||
tx_t::write(static_cast<std::uint8_t>(c));
|
tx_t::write(static_cast<std::uint8_t>(c));
|
||||||
}
|
}
|
||||||
[[noreturn]] static void idle()
|
[[noreturn]] static void idle()
|
||||||
{
|
{
|
||||||
|
#if defined(PUREBOOT_HEARTBEAT)
|
||||||
|
// Repeat the banner forever, which turns the fixture into a fixed
|
||||||
|
// cycles-per-bit transmitter: `tools/pbrig.py rate` sweeps the host rate
|
||||||
|
// against it to find the part's true bit rate, and from that the clock
|
||||||
|
// its RC oscillator is really running at. Only the *bit* timing carries
|
||||||
|
// the measurement — the delay merely spaces the lines out, so its own
|
||||||
|
// error does not matter. Software link only: the hardware-link idle owes
|
||||||
|
// the self-update tests a command loop, and a crystal deployment has
|
||||||
|
// nothing to measure.
|
||||||
|
while (true) {
|
||||||
|
tx('A');
|
||||||
|
tx('P');
|
||||||
|
tx('P');
|
||||||
|
tx('\r');
|
||||||
|
tx('\n');
|
||||||
|
dev::delay<50_ms>();
|
||||||
|
}
|
||||||
|
#else
|
||||||
while (true) {
|
while (true) {
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -108,9 +166,14 @@ struct link<C, false> {
|
|||||||
|
|
||||||
int main()
|
int main()
|
||||||
{
|
{
|
||||||
avr::init<typename link<dev::clock>::tx_t>();
|
link<dev::clock>::init();
|
||||||
|
#if !defined(PUREBOOT_HANDOVER)
|
||||||
link<dev::clock>::tx('A');
|
link<dev::clock>::tx('A');
|
||||||
link<dev::clock>::tx('P');
|
link<dev::clock>::tx('P');
|
||||||
link<dev::clock>::tx('P');
|
link<dev::clock>::tx('P');
|
||||||
|
#endif
|
||||||
|
// The hand-over fixture stays silent: nothing is listening on the USART it
|
||||||
|
// brings up — the loader it hands to speaks those pins directly — so its
|
||||||
|
// banner would be a write into a peer that does not exist.
|
||||||
link<dev::clock>::idle();
|
link<dev::clock>::idle();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,16 +6,18 @@ the *same* loader binary, which is the property autobaud exists for: one
|
|||||||
clock-agnostic image that locks onto whatever rate the host sends.
|
clock-agnostic image that locks onto whatever rate the host sends.
|
||||||
|
|
||||||
Usage: pbautobaud.py <device_bin> <loader_elf> <mcu> <base_hex> <page>
|
Usage: pbautobaud.py <device_bin> <loader_elf> <mcu> <base_hex> <page>
|
||||||
<app_bin> <app_hz> <app_baud> <tool_py> <workdir>
|
<app_bin> <app_hz> <app_baud> <tool_py> <workdir> [link]
|
||||||
|
|
||||||
The loader is a software-serial build on PB0/PB1 (pureboot_add_autobaud's
|
The loader is a software-serial build, driven over the GPIO⇄pty bridge; the
|
||||||
default), so the runner drives it over the GPIO⇄pty bridge (-l sw:B0,B1). The
|
optional link overrides the default -l sw:B0,B1 — RX == TX in it is the
|
||||||
app fixture is built for (app_hz, app_baud); the hand-over is checked at that
|
one-wire deployment, and every session then runs with the host's echo
|
||||||
point, and a second point at half the clock proves the lock is measured, not
|
discard on. The app fixture is built for (app_hz, app_baud); the hand-over
|
||||||
baked in.
|
is checked at that point, and a second point at half the clock proves the
|
||||||
|
lock is measured, not baked in.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
@@ -26,8 +28,12 @@ def fail(message):
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
(device_bin, elf, mcu, base_hex, page, app_bin, app_hz, app_baud, tool, workdir) = sys.argv[1:]
|
args = sys.argv[1:]
|
||||||
|
link = args.pop() if len(args) == 11 else "sw:B0,B1"
|
||||||
|
(device_bin, elf, mcu, base_hex, page, app_bin, app_hz, app_baud, tool, workdir) = args
|
||||||
base, page, app_hz, app_baud = int(base_hex, 0), int(page), int(app_hz), int(app_baud)
|
base, page, app_hz, app_baud = int(base_hex, 0), int(page), int(app_hz), int(app_baud)
|
||||||
|
one_wire = re.fullmatch(r"sw:([A-H][0-7]),\1(@[01])?", link) is not None
|
||||||
|
extra = ("--one-wire",) if one_wire else ()
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
|
sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
import pbsim
|
import pbsim
|
||||||
@@ -54,19 +60,32 @@ def main():
|
|||||||
"""One clock point: reset, calibrate + knock, program, verify against the
|
"""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."""
|
simulator's own flash, and (at the app's point) hand over to the fixture."""
|
||||||
dump = os.path.join(workdir, f"flash_{label}.bin")
|
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")
|
device = pbsim.Device(device_bin, elf, mcu, str(hz), base_hex, page, baud, dump, link=link)
|
||||||
try:
|
try:
|
||||||
# The host tool, in autobaud mode, sends the 0xC0 calibration pulse
|
# The host tool, in autobaud mode, sends the 0xC0 calibration pulse
|
||||||
# and a single knock at `baud`; the loader locks to it.
|
# and a single knock at `baud`; the loader locks to it.
|
||||||
out = pbsim.run_tool(tool, device.pty, baud, "--autobaud", "--info", "--fuses",
|
out = pbsim.run_tool(tool, device.pty, baud, *extra, "--autobaud", "--info", "--clock", str(hz),
|
||||||
"--flash", app_bin, "--eeprom", ee_path, "--stay")
|
"--fuses", "--flash", app_bin, "--eeprom", ee_path, "--stay")
|
||||||
for needed in ("version", "signature", "fuses", "verify:", "stays"):
|
for needed in ("version", "signature", "fuses", "verify:", "stays"):
|
||||||
if needed not in out:
|
if needed not in out:
|
||||||
fail(f"{label}: session output lacks {needed!r}\n{out}")
|
fail(f"{label}: session output lacks {needed!r}\n{out}")
|
||||||
|
# The measured clock, decoded from the unit at whichever home this
|
||||||
|
# version keeps it in. The runner's clock is exact, so the figure
|
||||||
|
# must land inside the
|
||||||
|
# encoding's own envelope: the loader floors the bit period to
|
||||||
|
# 4-cycle spin granules after an 8-cycle discount, and the edge
|
||||||
|
# poll can shave a few cycles more — one granule of slack below
|
||||||
|
# the true clock, none above (in cycles per bit, times the rate).
|
||||||
|
measured = re.search(r"measured\s+(\d+) Hz", out)
|
||||||
|
if not measured:
|
||||||
|
fail(f"{label}: --info lacks the measured clock\n{out}")
|
||||||
|
measured = int(measured.group(1))
|
||||||
|
if not hz - 19 * baud <= measured <= hz + 4 * baud:
|
||||||
|
fail(f"{label}: measured clock {measured} Hz is {measured - hz:+d} off the true {hz}")
|
||||||
# Read both memories back over the locked link and check them.
|
# Read both memories back over the locked link and check them.
|
||||||
read_flash = os.path.join(workdir, f"rf_{label}.bin")
|
read_flash = os.path.join(workdir, f"rf_{label}.bin")
|
||||||
read_eeprom = os.path.join(workdir, f"re_{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,
|
out = pbsim.run_tool(tool, device.pty, baud, *extra, "--autobaud", "--verify-flash", app_bin,
|
||||||
"--verify-eeprom", ee_path, "--read-flash", read_flash,
|
"--verify-eeprom", ee_path, "--read-flash", read_flash,
|
||||||
"--read-eeprom", read_eeprom, "--stay")
|
"--read-eeprom", read_eeprom, "--stay")
|
||||||
if out.count("verify:") != 2:
|
if out.count("verify:") != 2:
|
||||||
@@ -85,6 +104,8 @@ def main():
|
|||||||
# pulse is genuinely seen and the test cannot pass vacuously.)
|
# pulse is genuinely seen and the test cannot pass vacuously.)
|
||||||
device.reset()
|
device.reset()
|
||||||
port = pb.Port(device.pty, baud)
|
port = pb.Port(device.pty, baud)
|
||||||
|
if one_wire:
|
||||||
|
port = pb.OneWirePort(port)
|
||||||
try:
|
try:
|
||||||
time.sleep(0.2)
|
time.sleep(0.2)
|
||||||
port.write(bytes((pb.CALIBRATE,)))
|
port.write(bytes((pb.CALIBRATE,)))
|
||||||
@@ -103,6 +124,8 @@ def main():
|
|||||||
|
|
||||||
device.reset()
|
device.reset()
|
||||||
port = pb.Port(device.pty, baud)
|
port = pb.Port(device.pty, baud)
|
||||||
|
if one_wire:
|
||||||
|
port = pb.OneWirePort(port)
|
||||||
try:
|
try:
|
||||||
loader = pb.Loader(port)
|
loader = pb.Loader(port)
|
||||||
live = loader.connect_autobaud(15)
|
live = loader.connect_autobaud(15)
|
||||||
@@ -141,13 +164,47 @@ def main():
|
|||||||
print(f" {label}: locked at {hz} Hz / {baud} Bd, flash+EEPROM verified"
|
print(f" {label}: locked at {hz} Hz / {baud} Bd, flash+EEPROM verified"
|
||||||
+ (", hand-over ok" if hand_over else ""))
|
+ (", hand-over ok" if hand_over else ""))
|
||||||
|
|
||||||
|
def must_lock(hz, baud, label):
|
||||||
|
"""The calibration alone, at a tight bit period. Nothing is programmed —
|
||||||
|
the question is only whether the loader can still measure the pulse."""
|
||||||
|
dump = os.path.join(workdir, f"flash_{label}.bin")
|
||||||
|
device = pbsim.Device(device_bin, elf, mcu, str(hz), base_hex, page, baud, dump,
|
||||||
|
link=link)
|
||||||
|
try:
|
||||||
|
port = pb.Port(device.pty, baud)
|
||||||
|
if one_wire:
|
||||||
|
port = pb.OneWirePort(port)
|
||||||
|
try:
|
||||||
|
live = pb.Loader(port).connect_autobaud(15)
|
||||||
|
if live.version != pb.NEWEST_LOADER:
|
||||||
|
fail(f"{label}: loader reports pureboot {live.version}")
|
||||||
|
finally:
|
||||||
|
port.close()
|
||||||
|
finally:
|
||||||
|
device.stop()
|
||||||
|
print(f" {label}: locked at {hz} Hz / {baud} Bd ({hz / baud:.0f} cycles a bit)")
|
||||||
|
|
||||||
# The app fixture is built for one clock; the hand-over banners there. A
|
# 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
|
# 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
|
# 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.)
|
# 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, app_baud, "clock-a", hand_over=True)
|
||||||
round_trip(app_hz * 2, app_baud, "clock-b", hand_over=False)
|
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")
|
|
||||||
|
# Both points above sit near 100 cycles a bit, which is comfortable. The
|
||||||
|
# calibration's real floor is far tighter, and it is worth a gate: measured
|
||||||
|
# here, the lock is solid down to ~36 cycles a bit and fails outright by ~31
|
||||||
|
# — a sharp edge, not a fraying one. This pins the tightest standard rate the
|
||||||
|
# fixture's clock reaches, so a change that raises the floor is caught.
|
||||||
|
#
|
||||||
|
# It does *not* bound what a real deployment can use. On silicon the
|
||||||
|
# oscillator's own jitter costs roughly a factor of two: an ATtiny13A on its
|
||||||
|
# factory RC trim was reliable at ~118 cycles a bit and already locking only
|
||||||
|
# 1 attempt in 5 by ~59, which no exact-clock simulation can show. The
|
||||||
|
# deployable envelope is a README matter; this is the logic's floor.
|
||||||
|
must_lock(app_hz, app_baud * 2, "tight-bit")
|
||||||
|
print("pbautobaud: calibration lock and flash/EEPROM/fuse round-trip pass at both clocks, "
|
||||||
|
"and the tight bit period still locks")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
124
test/pbglitch.py
Normal file
124
test/pbglitch.py
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""A link that damages bytes, deliberately and reproducibly, against the seal
|
||||||
|
that exists for it.
|
||||||
|
|
||||||
|
The board this was written for loses and mangles bytes on its own serial path,
|
||||||
|
and the failure that made it matter — a page-fill byte lost, the stream one
|
||||||
|
byte out, a page-address byte arriving where an SPMCSR value belongs — is not
|
||||||
|
reachable by asking a healthy link nicely. So the damage is injected here, at
|
||||||
|
a named byte index rather than a probability: a failing case is a case that
|
||||||
|
fails again.
|
||||||
|
|
||||||
|
Every check is a pair. The same bit flipped in the same field is applied on
|
||||||
|
one side of the seal and then the other: *after* the host seals the header,
|
||||||
|
which is a mangled command and must be refused, and *before*, which is a
|
||||||
|
well-formed command for something else and must be obeyed. Only the pair
|
||||||
|
proves anything — a test that showed the refusal alone would pass against a
|
||||||
|
loader that had simply stopped doing SPM, and one that showed the corruption
|
||||||
|
alone would not say what caught it.
|
||||||
|
|
||||||
|
Usage: pbglitch.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
|
||||||
|
<baud> <tool_py> <workdir>
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def fail(message):
|
||||||
|
print(f"FAIL: {message}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def frame(pb, op, space, address, count, damage=None, before_seal=False):
|
||||||
|
"""A sealed command header, optionally with one byte damaged.
|
||||||
|
|
||||||
|
`damage` is (index, mask). Applied before the seal is computed it produces
|
||||||
|
a valid command for whatever the damaged fields now say; applied after, a
|
||||||
|
command whose seal no longer matches its own body — which is the shape a
|
||||||
|
link fault actually has."""
|
||||||
|
head = bytearray((op, pb.selector(space, address), address & 0xFF,
|
||||||
|
(address >> 8) & 0xFF, count & 0xFF))
|
||||||
|
if damage and before_seal:
|
||||||
|
head[damage[0]] ^= damage[1]
|
||||||
|
seal = pb.SEAL
|
||||||
|
for byte in head:
|
||||||
|
seal ^= byte
|
||||||
|
out = bytearray(head + bytes((seal,)))
|
||||||
|
if damage and not before_seal:
|
||||||
|
out[damage[0]] ^= damage[1]
|
||||||
|
return bytes(out)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
device_bin, elf, mcu, hz, base_hex, page, baud, tool, workdir = sys.argv[1:]
|
||||||
|
base, page, baud = int(base_hex, 0), int(page), int(baud)
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
import pbsim
|
||||||
|
import pureboot as pb
|
||||||
|
|
||||||
|
os.makedirs(workdir, exist_ok=True)
|
||||||
|
dump = os.path.join(workdir, "dump.bin")
|
||||||
|
device = pbsim.Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump)
|
||||||
|
try:
|
||||||
|
port = pb.Port(device.pty, baud)
|
||||||
|
loader = pb.Loader(port)
|
||||||
|
info = loader.connect(25)
|
||||||
|
if info.version < pb.SEALED_LOADER:
|
||||||
|
fail(f"this test is for pureboot {pb.SEALED_LOADER} and later, not {info.version}")
|
||||||
|
|
||||||
|
# A page of known bytes to watch. Everything below aims at it, so
|
||||||
|
# "nothing happened" is a readable claim rather than an absence.
|
||||||
|
marker = bytes((0x40 + (i & 0x3F)) for i in range(page))
|
||||||
|
loader.write_page(0, marker)
|
||||||
|
if loader.read_flash(0, page) != marker:
|
||||||
|
fail("the marker page did not survive an undamaged write")
|
||||||
|
|
||||||
|
# Every field of the header, one bit each. A damaged seal must be
|
||||||
|
# refused, the loader must re-prompt, and the page must be untouched —
|
||||||
|
# and it is the erase being aimed at it, so a single escape is visible.
|
||||||
|
for index in range(6):
|
||||||
|
bad = frame(pb, pb.OP_WRITE, pb.SP_SPM, 0, pb.SPM_ERASE, damage=(index, 0x01))
|
||||||
|
port.write(bad)
|
||||||
|
answer = port.read_exact(1, 5.0)
|
||||||
|
if answer != pb.NAK:
|
||||||
|
fail(f"a header damaged in byte {index} was not refused (got {answer.hex()})")
|
||||||
|
if port.read_exact(1, 5.0) != pb.PROMPT:
|
||||||
|
fail(f"no prompt after refusing a header damaged in byte {index}")
|
||||||
|
if loader.read_flash(0, page) != marker:
|
||||||
|
fail(f"damage in byte {index} reached flash — the marker page changed")
|
||||||
|
|
||||||
|
# The fill, whose payload is the protocol's one unacked burst: a
|
||||||
|
# refused fill must be refused *before* the page is sent, or the host
|
||||||
|
# is left pushing 128 bytes into a loader reading commands. Nothing is
|
||||||
|
# sent after the verdict here, and the very next command must be
|
||||||
|
# understood — that is the whole claim.
|
||||||
|
bad = frame(pb, pb.OP_FILL, pb.SP_FLASH, 0, page, damage=(3, 0x80))
|
||||||
|
port.write(bad)
|
||||||
|
if port.read_exact(1, 5.0) != pb.NAK:
|
||||||
|
fail("a damaged fill header was not refused")
|
||||||
|
if port.read_exact(1, 5.0) != pb.PROMPT:
|
||||||
|
fail("no prompt after refusing a damaged fill header")
|
||||||
|
if loader.identity().raw != info.raw:
|
||||||
|
fail("the loader was out of step after refusing a fill")
|
||||||
|
|
||||||
|
# The pair's other half. The identical flip, applied before the seal:
|
||||||
|
# a well-formed erase of the page one bit away from the one intended.
|
||||||
|
# It must be obeyed — otherwise the refusals above prove nothing about
|
||||||
|
# the seal and only that this loader stopped erasing.
|
||||||
|
port.write(frame(pb, pb.OP_WRITE, pb.SP_SPM, 0, pb.SPM_ERASE,
|
||||||
|
damage=(2, 0x01), before_seal=True))
|
||||||
|
if port.read_exact(1, 5.0) != pb.PROMPT:
|
||||||
|
fail("a correctly sealed erase was refused")
|
||||||
|
if port.read_exact(1, 5.0) != pb.PROMPT:
|
||||||
|
fail("no prompt after a correctly sealed erase")
|
||||||
|
if loader.read_flash(0, page) != b"\xff" * page:
|
||||||
|
fail("the sealed erase did not reach flash — the marker page is intact")
|
||||||
|
port.close()
|
||||||
|
finally:
|
||||||
|
device.stop()
|
||||||
|
print("pbglitch: damaged headers are refused, the identical damage sealed is obeyed")
|
||||||
|
|
||||||
|
|
||||||
|
main()
|
||||||
79
test/pbmute.py
Normal file
79
test/pbmute.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Hand-over with a USART left enabled on the loader's own pins.
|
||||||
|
|
||||||
|
A software or autobaud link deployed on a USART's TxD is mute if an
|
||||||
|
application hands over with that USART still enabled: TXEN keeps the USART
|
||||||
|
owning the pin, so the bit-banged transmitter's port writes go nowhere and the
|
||||||
|
loader receives and obeys while answering nothing. The link's init releases it.
|
||||||
|
|
||||||
|
The state is reached the way silicon reaches it — an application that sets up
|
||||||
|
its USART and jumps in with no reset between, so nothing clears UCSRnB for it.
|
||||||
|
The pin ownership itself is modelled by the device runner: simavr wires a
|
||||||
|
USART through IRQs alone and never takes the pin from the port, so without
|
||||||
|
that the mute could not happen here at all (test/pureboot_device.cpp).
|
||||||
|
|
||||||
|
Usage: pbmute.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
|
||||||
|
<baud> <app_bin> <tool_py> <workdir> <link>
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def fail(message):
|
||||||
|
print(f"FAIL: {message}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
device_bin, elf, mcu, hz, base_hex, page, baud, app_bin, tool, workdir, link = sys.argv[1:]
|
||||||
|
page, baud = int(page), int(baud)
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
import pbsim
|
||||||
|
import pureboot as pb
|
||||||
|
|
||||||
|
if "@" not in link:
|
||||||
|
fail(f"the link {link} names no owning USART — nothing would be under test")
|
||||||
|
# A shared line (RX == TX) echoes the host's own bytes; discard them the
|
||||||
|
# way the shipped --one-wire mode does.
|
||||||
|
one_wire = re.fullmatch(r"sw:([A-H][0-7]),\1@[01]", link) is not None
|
||||||
|
|
||||||
|
os.makedirs(workdir, exist_ok=True)
|
||||||
|
dump = os.path.join(workdir, "dump.bin")
|
||||||
|
|
||||||
|
device = pbsim.Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump, link=link)
|
||||||
|
try:
|
||||||
|
port = pb.Port(device.pty, baud)
|
||||||
|
if one_wire:
|
||||||
|
port = pb.OneWirePort(port)
|
||||||
|
loader = pb.Loader(port)
|
||||||
|
loader.connect(25)
|
||||||
|
resident = loader.info.version
|
||||||
|
|
||||||
|
# Install the fixture and let it take over. It brings up the USART
|
||||||
|
# that owns these pins and jumps straight back in.
|
||||||
|
pb.op_flash(loader, app_bin, erase=False, verify=True)
|
||||||
|
loader.run_application()
|
||||||
|
|
||||||
|
# The loader is running again with that USART enabled behind it. Only
|
||||||
|
# the release makes it audible; without it the connect times out.
|
||||||
|
loader = pb.Loader(port)
|
||||||
|
try:
|
||||||
|
loader.connect(25)
|
||||||
|
except pb.Error as error:
|
||||||
|
fail(f"the loader never answered after the hand-over — the USART still owns its TX pin ({error})")
|
||||||
|
if loader.info.version != resident:
|
||||||
|
fail(f"identity changed across the hand-over: {resident} then {loader.info.version}")
|
||||||
|
|
||||||
|
# Answering is not enough: it has to still be a working loader.
|
||||||
|
pb.verify_pages(loader, pb.plan_flash(open(app_bin, "rb").read(), loader.info))
|
||||||
|
port.close()
|
||||||
|
finally:
|
||||||
|
device.stop()
|
||||||
|
print("pbmute: a loader on a USART's own pins answers after a hand-over that left it enabled")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
46
test/pbosccal.py
Normal file
46
test/pbosccal.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""The build-time OSCCAL trim, observed through the wire: a loader built with
|
||||||
|
the OSCCAL axis holds the trim register at the built byte from its first
|
||||||
|
prompt on — the write sits at the top of run(), ahead of the WDRF bail, so
|
||||||
|
every path out of reset runs on the corrected clock. simavr's clock does not
|
||||||
|
follow OSCCAL, which is what makes the value assertable at all: the register
|
||||||
|
is plain state there, and the peek must return exactly what the build
|
||||||
|
declared rather than whatever the oscillator needed.
|
||||||
|
|
||||||
|
Usage: pbosccal.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
|
||||||
|
<baud> <osccal_addr> <osccal_value> <tool_py> <workdir>
|
||||||
|
[link]
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def fail(message):
|
||||||
|
print(f"FAIL: {message}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = sys.argv[1:]
|
||||||
|
link = args.pop() if len(args) == 12 else None
|
||||||
|
(device_bin, elf, mcu, hz, base_hex, page, baud, addr, value, tool, workdir) = args
|
||||||
|
addr, value, baud = int(addr, 0), int(value, 0), int(baud)
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
import pbsim
|
||||||
|
|
||||||
|
os.makedirs(workdir, exist_ok=True)
|
||||||
|
dump = os.path.join(workdir, "flash_dump.bin")
|
||||||
|
device = pbsim.Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump, link=link)
|
||||||
|
try:
|
||||||
|
out = pbsim.run_tool(tool, device.pty, baud, "--peek", f"{addr:#x}:1")
|
||||||
|
want = f"{addr:#06x} {value:02x}"
|
||||||
|
if want not in out:
|
||||||
|
fail(f"OSCCAL at {addr:#x} did not read back {value:#04x}:\n{out}")
|
||||||
|
finally:
|
||||||
|
device.stop()
|
||||||
|
print("OK")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -3,11 +3,10 @@
|
|||||||
canonical slot must still be a working loader, and the ordinary
|
canonical slot must still be a working loader, and the ordinary
|
||||||
--update-loader flow must put a build into the top slot from there.
|
--update-loader flow must put a build into the top slot from there.
|
||||||
|
|
||||||
Two positions. Address 0, a raw .bin handed to a programmer: the staging
|
Two positions. Address 0, a raw .bin handed to a programmer. And the staging
|
||||||
install and the word-0 redirect run from copies outside page 0's slot, so the
|
slot itself, where a loader already sitting there IS the staging copy —
|
||||||
running-slot guard never blocks them. And the staging slot itself, where a
|
recognized by its embedded block and left in place, then streaming the new
|
||||||
loader already sitting there IS the staging copy — recognized by its embedded
|
resident like any staged copy.
|
||||||
block and left in place, then streaming the new resident like any staged copy.
|
|
||||||
|
|
||||||
Usage: pbrehome.py <device_bin> <pureboot_elf> <update_bin> <mcu> <hz>
|
Usage: pbrehome.py <device_bin> <pureboot_elf> <update_bin> <mcu> <hz>
|
||||||
<base_hex> <page> <baud> <app_bin> <tool_py> <workdir>
|
<base_hex> <page> <baud> <app_bin> <tool_py> <workdir>
|
||||||
@@ -22,7 +21,7 @@ def fail(message):
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def rehome_from(pbsim, pb, device_bin, elf, place_hex, guard_probe, update_bin, base, page, baud, app_bin, workdir,
|
def rehome_from(pbsim, pb, device_bin, elf, place_hex, update_bin, base, page, baud, app_bin, workdir,
|
||||||
mcu, hz):
|
mcu, hz):
|
||||||
"""Place the loader at `place_hex`, heal through --update-loader, flash
|
"""Place the loader at `place_hex`, heal through --update-loader, flash
|
||||||
the application, expect the banner."""
|
the application, expect the banner."""
|
||||||
@@ -38,14 +37,6 @@ def rehome_from(pbsim, pb, device_bin, elf, place_hex, guard_probe, update_bin,
|
|||||||
if info.base != base:
|
if info.base != base:
|
||||||
fail(f"the misplaced copy reports base {info.base:#06x} — the info block must stay canonical")
|
fail(f"the misplaced copy reports base {info.base:#06x} — the info block must stay canonical")
|
||||||
|
|
||||||
# The accidental slot still guards itself; re-homing rides on the
|
|
||||||
# canonical slots being writable from it.
|
|
||||||
probe = int(guard_probe, 0)
|
|
||||||
before = loader.read_flash(probe, info.page)
|
|
||||||
loader.write_page(probe, bytes(info.page))
|
|
||||||
if loader.read_flash(probe, info.page) != before:
|
|
||||||
fail("the misplaced copy's guard let its own slot change")
|
|
||||||
|
|
||||||
# The ordinary update flow puts the build into the top slot.
|
# The ordinary update flow puts the build into the top slot.
|
||||||
pb.op_update_loader(loader, 25, update_bin, state, None)
|
pb.op_update_loader(loader, 25, update_bin, state, None)
|
||||||
update = open(update_bin, "rb").read()
|
update = open(update_bin, "rb").read()
|
||||||
@@ -76,16 +67,15 @@ def main():
|
|||||||
|
|
||||||
os.makedirs(workdir, exist_ok=True)
|
os.makedirs(workdir, exist_ok=True)
|
||||||
|
|
||||||
# Address 0: the raw-.bin-to-a-programmer accident. The guard probe is
|
# Address 0: the raw-.bin-to-a-programmer accident.
|
||||||
# the copy's own page 0.
|
rehome_from(pbsim, pb, device_bin, elf, "0x0", update_bin, base, page, baud, app_bin, workdir, mcu, hz)
|
||||||
rehome_from(pbsim, pb, device_bin, elf, "0x0", "0x0", update_bin, base, page, baud, app_bin, workdir, mcu, hz)
|
|
||||||
print("re-home from address 0: converged")
|
print("re-home from address 0: converged")
|
||||||
|
|
||||||
# The staging slot: erased flash with the loader sitting exactly where
|
# The staging slot: erased flash with the loader sitting exactly where
|
||||||
# a staging copy would — the tool must leave it in place and let it
|
# a staging copy would — the tool must leave it in place and let it
|
||||||
# stream the (different) update build into the resident slot.
|
# stream the (different) update build into the resident slot.
|
||||||
stage = base - pb.SLOT
|
stage = base - pb.SLOT
|
||||||
rehome_from(pbsim, pb, device_bin, elf, hex(stage), hex(stage), update_bin, base, page, baud, app_bin, workdir,
|
rehome_from(pbsim, pb, device_bin, elf, hex(stage), update_bin, base, page, baud, app_bin, workdir,
|
||||||
mcu, hz)
|
mcu, hz)
|
||||||
print("re-home from the staging slot: converged")
|
print("re-home from the staging slot: converged")
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Position-independence acceptance test: the identical binary, flashed one
|
"""Position-independence acceptance test: the identical binary, flashed one
|
||||||
slot below the resident, must serve the complete command set from there. The
|
slot below the resident, must serve the complete command set from there. The
|
||||||
info block must come back byte-identical, the write guard must refuse the
|
info block must come back byte-identical, and the staged copy must be able to
|
||||||
staged copy's own slot and permit the resident's, and the staged copy must be
|
rewrite the resident verbatim — which is the whole of what relocation is for.
|
||||||
able to rewrite the resident verbatim.
|
|
||||||
|
|
||||||
Usage: pbreloc.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
|
Usage: pbreloc.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
|
||||||
<baud> <tool_py> <workdir>
|
<baud> <tool_py> <workdir>
|
||||||
@@ -64,14 +63,14 @@ def main():
|
|||||||
if loader.read_eeprom(0, len(pattern)) != pattern:
|
if loader.read_eeprom(0, len(pattern)) != pattern:
|
||||||
fail("EEPROM round-trip through the staged copy")
|
fail("EEPROM round-trip through the staged copy")
|
||||||
|
|
||||||
# The guard, both ways: its own slot refused (drained, unchanged), the
|
# The resident slot, written from the copy standing beside it — the
|
||||||
# resident slot writable. The refusal leaves its drained words in the
|
# whole point of relocating. pureboot 9 dropped the running-slot guard
|
||||||
# SPM buffer, so the write that follows may take them — and clears
|
# that used to sit behind this, so the probe that used to accompany it
|
||||||
# them by writing, so the retry must not.
|
# (aim a write at the copy's *own* slot and watch it be refused) is
|
||||||
before = loader.read_flash(stage, page)
|
# gone with it: there is nothing to refuse now, and a copy that erases
|
||||||
loader.write_page(stage, bytes(page))
|
# the page it is executing from does not come back to report it.
|
||||||
if loader.read_flash(stage, page) != before:
|
# pbselfwrite.py gates that direction on a device it is allowed to
|
||||||
fail("the staged copy's guard let its own slot change")
|
# destroy.
|
||||||
marker = bytes((i * 3) & 0xFF for i in range(page))
|
marker = bytes((i * 3) & 0xFF for i in range(page))
|
||||||
loader.write_page(base, marker)
|
loader.write_page(base, marker)
|
||||||
if loader.read_flash(base, page) != marker:
|
if loader.read_flash(base, page) != marker:
|
||||||
|
|||||||
102
test/pbselfwrite.py
Normal file
102
test/pbselfwrite.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""The seal, gated on the one command that proves it: erase the page the
|
||||||
|
loader is executing from.
|
||||||
|
|
||||||
|
pureboot 9 dropped the running-slot write guard, so this command is now
|
||||||
|
permitted — that is what lets a resident copy plant something in its own slot,
|
||||||
|
which on a chip whose boot section *is* the loader slot is the only route a
|
||||||
|
self-update has. Permitted means the loader must actually do it, and the only
|
||||||
|
honest proof is the flash afterwards.
|
||||||
|
|
||||||
|
What stands in the guard's place is the seal, and the two halves are tested
|
||||||
|
against each other here: the identical destructive command, refused when its
|
||||||
|
seal is wrong and honoured when it is right. A test that only showed the
|
||||||
|
refusal would pass just as well against a loader that ignores SPM entirely.
|
||||||
|
|
||||||
|
Usage: pbselfwrite.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
|
||||||
|
<baud> <tool_py> <workdir>
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def fail(message):
|
||||||
|
print(f"FAIL: {message}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def sealed_frame(pb, op, space, address, count):
|
||||||
|
"""A command header and its seal, built here rather than borrowed from the
|
||||||
|
tool: this test is about what the loader accepts, and a probe that shares
|
||||||
|
the host's frame builder cannot tell a wrong frame from a wrong loader."""
|
||||||
|
head = bytes((op, pb.selector(space, address), address & 0xFF,
|
||||||
|
(address >> 8) & 0xFF, count & 0xFF))
|
||||||
|
seal = pb.SEAL
|
||||||
|
for byte in head:
|
||||||
|
seal ^= byte
|
||||||
|
return head + bytes((seal,))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
device_bin, elf, mcu, hz, base_hex, page, baud, tool, workdir = sys.argv[1:]
|
||||||
|
base, page, baud = int(base_hex, 0), int(page), int(baud)
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
import pbsim
|
||||||
|
import pureboot as pb
|
||||||
|
|
||||||
|
os.makedirs(workdir, exist_ok=True)
|
||||||
|
dump = os.path.join(workdir, "dump.bin")
|
||||||
|
device = pbsim.Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump)
|
||||||
|
try:
|
||||||
|
port = pb.Port(device.pty, baud)
|
||||||
|
loader = pb.Loader(port)
|
||||||
|
info = loader.connect(25)
|
||||||
|
if info.version < pb.SEALED_LOADER:
|
||||||
|
fail(f"this test is for pureboot {pb.SEALED_LOADER} and later, not {info.version}")
|
||||||
|
|
||||||
|
# Erase the first page of the running slot: the entry stub and the
|
||||||
|
# command loop are both in it, so a loader that performs this does not
|
||||||
|
# answer again. Nothing else in the protocol is as sharp a probe.
|
||||||
|
frame = sealed_frame(pb, pb.OP_WRITE, pb.SP_SPM, base, pb.SPM_ERASE)
|
||||||
|
|
||||||
|
# Red: the same command with one bit wrong in its seal. Refused before
|
||||||
|
# anything happens, and the loader is still there to say so.
|
||||||
|
broken = bytearray(frame)
|
||||||
|
broken[-1] ^= 0x01
|
||||||
|
port.write(bytes(broken))
|
||||||
|
if port.read_exact(1, 5.0) != pb.NAK:
|
||||||
|
fail("an unsealed erase of the running page was not refused")
|
||||||
|
if port.read_exact(1, 5.0) != pb.PROMPT:
|
||||||
|
fail("the loader did not re-prompt after refusing the erase")
|
||||||
|
alive = loader.read_flash(base, 8)
|
||||||
|
if alive == b"\xff" * 8:
|
||||||
|
fail("the refused erase happened anyway — the running page reads erased")
|
||||||
|
|
||||||
|
# Green: the identical command, correctly sealed. Nothing is required
|
||||||
|
# of the link from here on. The verdict is *issued* before the SPM, but
|
||||||
|
# the erase takes the code that would have finished saying it, and how
|
||||||
|
# much of it survives is the chip's business — an erase removes one page
|
||||||
|
# and nothing else, so a loader whose command loop lives past the page
|
||||||
|
# erased will prompt as usual where one with 128-byte pages goes with
|
||||||
|
# the stub. None of that is the claim. The claim is that the erase
|
||||||
|
# reached flash, and the dump is both the only witness for it and a
|
||||||
|
# better one: it tells "accepted and performed" from "merely answered".
|
||||||
|
port.write(frame)
|
||||||
|
try:
|
||||||
|
port.read_exact(2, 2.0)
|
||||||
|
except pb.Error:
|
||||||
|
pass
|
||||||
|
port.close()
|
||||||
|
finally:
|
||||||
|
device.stop()
|
||||||
|
|
||||||
|
# Ground truth: the simulator's flash, not the loader's opinion of it.
|
||||||
|
flash = open(dump, "rb").read()
|
||||||
|
if flash[base : base + page] != b"\xff" * page:
|
||||||
|
fail("the sealed erase did not reach flash — the running page is intact")
|
||||||
|
print("pbselfwrite: the running slot is refused unsealed and erased sealed")
|
||||||
|
|
||||||
|
|
||||||
|
main()
|
||||||
@@ -8,10 +8,13 @@ import subprocess
|
|||||||
|
|
||||||
|
|
||||||
class Device:
|
class Device:
|
||||||
def __init__(self, binary, elf, mcu, hz, base_hex, page, baud, dump, reset_hex=None, resume=None, link=None):
|
def __init__(self, binary, elf, mcu, hz, base_hex, page, baud, dump, reset_hex=None, resume=None, link=None,
|
||||||
|
window=False):
|
||||||
cmd = [binary]
|
cmd = [binary]
|
||||||
if link:
|
if link:
|
||||||
cmd += ["-l", link]
|
cmd += ["-l", link]
|
||||||
|
if window:
|
||||||
|
cmd.append("-w") # report the first-transmit cycle, free-run idle
|
||||||
cmd += [elf, mcu, hz, base_hex, str(page), str(baud), dump]
|
cmd += [elf, mcu, hz, base_hex, str(page), str(baud), dump]
|
||||||
if reset_hex is not None or resume is not None:
|
if reset_hex is not None or resume is not None:
|
||||||
# Chips without a hardware boot section — the tinies and the
|
# Chips without a hardware boot section — the tinies and the
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ loader built off the chip's natural serial default.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
@@ -70,10 +71,15 @@ def main():
|
|||||||
+ bytes([flags])
|
+ bytes([flags])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# A shared-line link (RX == TX in the -l spec) makes the host read every
|
||||||
|
# byte it sends back off the line; all sessions then discard the echo.
|
||||||
|
one_wire = bool(link) and re.fullmatch(r"sw:([A-H][0-7]),\1(@[01])?", link) is not None
|
||||||
|
extra = ("--one-wire",) if one_wire else ()
|
||||||
|
|
||||||
device = pbsim.Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump, link=link)
|
device = pbsim.Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump, link=link)
|
||||||
try:
|
try:
|
||||||
# Session 1: knock from reset, identify, program everything, stay.
|
# Session 1: knock from reset, identify, program everything, stay.
|
||||||
out = pbsim.run_tool(tool, device.pty, baud, "--info", "--fuses", "--flash", app_bin,
|
out = pbsim.run_tool(tool, device.pty, baud, *extra, "--info", "--fuses", "--flash", app_bin,
|
||||||
"--eeprom", ee_path, "--stay")
|
"--eeprom", ee_path, "--stay")
|
||||||
for needed in ("version", "signature", "fuses", "verify:", "stays"):
|
for needed in ("version", "signature", "fuses", "verify:", "stays"):
|
||||||
if needed not in out:
|
if needed not in out:
|
||||||
@@ -83,7 +89,7 @@ def main():
|
|||||||
# the data space; hand over is deferred — the pty must be reopened for
|
# the data space; hand over is deferred — the pty must be reopened for
|
||||||
# the APP banner first.
|
# the APP banner first.
|
||||||
probe = "c0ffee"
|
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, *extra, "--verify-flash", app_bin, "--verify-eeprom", ee_path,
|
||||||
"--read-flash", read_flash, "--read-eeprom", read_eeprom,
|
"--read-flash", read_flash, "--read-eeprom", read_eeprom,
|
||||||
"--poke", f"{ram_base:#x}:{probe}", "--peek", f"{ram_base:#x}:3", "--stay")
|
"--poke", f"{ram_base:#x}:{probe}", "--peek", f"{ram_base:#x}:3", "--stay")
|
||||||
if out.count("verify:") != 2:
|
if out.count("verify:") != 2:
|
||||||
@@ -111,6 +117,8 @@ def main():
|
|||||||
# land in the application, which banners on the same link.
|
# land in the application, which banners on the same link.
|
||||||
device.reset()
|
device.reset()
|
||||||
port = pb.Port(device.pty, baud)
|
port = pb.Port(device.pty, baud)
|
||||||
|
if one_wire:
|
||||||
|
port = pb.OneWirePort(port)
|
||||||
try:
|
try:
|
||||||
loader = pb.Loader(port)
|
loader = pb.Loader(port)
|
||||||
live = loader.connect(15)
|
live = loader.connect(15)
|
||||||
@@ -123,19 +131,42 @@ def main():
|
|||||||
fail(f"loader reports pureboot {live.version}, the tool speaks "
|
fail(f"loader reports pureboot {live.version}, the tool speaks "
|
||||||
f"{pb.OLDEST_LOADER}..{pb.NEWEST_LOADER}")
|
f"{pb.OLDEST_LOADER}..{pb.NEWEST_LOADER}")
|
||||||
|
|
||||||
# A W addressed inside a page rather than at its base must still
|
# A fill addressed inside a page rather than at its base must still
|
||||||
# consume exactly one page and prompt. The loader's own slot is the
|
# consume exactly one page and prompt. The loader's own slot is the
|
||||||
# target — the guard refuses to commit it — and the payload is
|
# target and the payload is erased-state bytes, so the probe can
|
||||||
# erased-state bytes, so the probe can disturb neither the image nor
|
# disturb neither the image nor the page buffer it leaves behind: a
|
||||||
# the page buffer it leaves behind. Hand-built rather than through
|
# fill only loads the buffer, and nothing commits it. Hand-built
|
||||||
# write_page(), which would follow the fill with its erase and
|
# rather than through write_page(), which would follow the fill
|
||||||
# write; the point here is that the fill alone consumes exactly one
|
# with its erase and write; the point here is that the fill alone
|
||||||
# page whatever the address's low bits say.
|
# consumes exactly one page whatever the address's low bits say.
|
||||||
|
# Hand-sealed too — a protocol probe that borrowed the tool's own
|
||||||
|
# frame builder could not tell a wrong frame from a wrong loader.
|
||||||
wire = base + 1
|
wire = base + 1
|
||||||
port.write(bytes((ord("W"), pb.selector(pb.SP_FLASH, wire), wire & 0xFF, (wire >> 8) & 0xFF))
|
head = bytes((pb.OP_FILL, pb.selector(pb.SP_FLASH, wire), wire & 0xFF,
|
||||||
+ b"\xff" * page)
|
(wire >> 8) & 0xFF, page & 0xFF))
|
||||||
|
seal = pb.SEAL
|
||||||
|
for byte in head:
|
||||||
|
seal ^= byte
|
||||||
|
port.write(head + bytes((seal,)))
|
||||||
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("the loader refused a correctly sealed fill")
|
||||||
|
port.write(b"\xff" * page)
|
||||||
|
if port.read_exact(1, 5.0) != pb.PROMPT:
|
||||||
|
fail("unaligned fill did not return to the prompt")
|
||||||
|
|
||||||
|
# And the seal itself, red: one wrong bit in the address of that
|
||||||
|
# same frame must be refused outright. The verdict has to arrive
|
||||||
|
# *before* the page would have been sent — that ordering is what
|
||||||
|
# keeps a refusal from turning into a desync — so the probe sends
|
||||||
|
# no payload at all and expects the loader straight back at the
|
||||||
|
# command level.
|
||||||
|
broken = bytearray(head + bytes((seal,)))
|
||||||
|
broken[2] ^= 0x01
|
||||||
|
port.write(bytes(broken))
|
||||||
|
if port.read_exact(1, 5.0) != pb.NAK:
|
||||||
|
fail("a header with a broken seal was not refused")
|
||||||
|
if port.read_exact(1, 5.0) != pb.PROMPT:
|
||||||
|
fail("the loader did not re-prompt after refusing a broken seal")
|
||||||
|
|
||||||
loader.run_application()
|
loader.run_application()
|
||||||
banner = port.read_exact(3, 5.0)
|
banner = port.read_exact(3, 5.0)
|
||||||
|
|||||||
137
test/pbwindow.py
Normal file
137
test/pbwindow.py
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""The activation window as a behavioral duration gate.
|
||||||
|
|
||||||
|
The loader's window is a counted poll loop whose per-poll cost is hand-counted
|
||||||
|
in the source (`link::poll_cycles`) — but the loop compiles in consumer
|
||||||
|
context, so only the running image can prove the count. This test installs a
|
||||||
|
real application beside the loader (the host tool's own `plan_flash` supplies
|
||||||
|
the reset-vector surgery), starts the simulator with the line idle, and reads
|
||||||
|
the cycle of the first transmit activity: nothing talks until the window
|
||||||
|
closes and the application banners, so that cycle *is* the window, give or
|
||||||
|
take a banner lead measured in microseconds. Asserted at ±2 % — one
|
||||||
|
mis-counted cycle per poll shifts a window by 10 % and more.
|
||||||
|
|
||||||
|
Fixed-baud loaders declare their window in seconds (--seconds, the build's
|
||||||
|
TIMEOUT). The autobaud loader's window is its calibration poll budget
|
||||||
|
(--autobaud-polls); the seconds it amounts to are budget × 9 / f_cpu, the
|
||||||
|
measured cost of the calibrate() wait loop this gate pins.
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import importlib.util
|
||||||
|
import pathlib
|
||||||
|
import select
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
|
||||||
|
from pbsim import Device
|
||||||
|
|
||||||
|
# The calibrate() budget loop's cycles per poll in the built image — what the
|
||||||
|
# README's window arithmetic rests on, verified here. A measured fact, not a
|
||||||
|
# design constant: the wait's exit branches land where the compiler's block
|
||||||
|
# layout puts them, and the bounded-calibration rework moved the loop from
|
||||||
|
# ten cycles to nine.
|
||||||
|
AUTOBAUD_POLL_CYCLES = 9
|
||||||
|
|
||||||
|
|
||||||
|
def load_tool(path):
|
||||||
|
spec = importlib.util.spec_from_file_location("pureboot", path)
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
def compose_flash(pb, loader_bytes, app_bytes, mcu, base, page):
|
||||||
|
"""The flash image a completed programming session leaves: application
|
||||||
|
(with the tinies' vector surgery), loader at base — built through the
|
||||||
|
host tool's own planner so the surgery is the shipped one, not a copy."""
|
||||||
|
flash_size = base + pb.SLOT
|
||||||
|
patch = not mcu.startswith("atmega") or mcu.startswith("atmega48")
|
||||||
|
word_flash = flash_size > 0x10000
|
||||||
|
wire_base = base // 2 if word_flash else base
|
||||||
|
flags = (1 if patch else 0) | (2 if word_flash else 0)
|
||||||
|
raw = bytes((ord("P"), ord("B"), 5, 0, 0, 0, page & 0xFF,
|
||||||
|
wire_base & 0xFF, wire_base >> 8, 0, 0, flags))
|
||||||
|
info = pb.Info(raw)
|
||||||
|
|
||||||
|
flash = bytearray(b"\xff" * flash_size)
|
||||||
|
for address, content in pb.plan_flash(app_bytes, info).items():
|
||||||
|
flash[address:address + len(content)] = content
|
||||||
|
flash[base:base + len(loader_bytes)] = loader_bytes
|
||||||
|
return bytes(flash)
|
||||||
|
|
||||||
|
|
||||||
|
def first_tx_cycle(device, deadline):
|
||||||
|
"""The PB_WINDOW_TX report, or None. The runner prints it once."""
|
||||||
|
stream = device.proc.stdout
|
||||||
|
while True:
|
||||||
|
remaining = deadline - time.monotonic()
|
||||||
|
if remaining <= 0:
|
||||||
|
return None
|
||||||
|
ready, _, _ = select.select([stream], [], [], remaining)
|
||||||
|
if not ready:
|
||||||
|
return None
|
||||||
|
line = stream.readline()
|
||||||
|
if not line:
|
||||||
|
return None
|
||||||
|
if line.startswith("PB_WINDOW_TX"):
|
||||||
|
return int(line.split()[1])
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--device", required=True)
|
||||||
|
parser.add_argument("--loader", required=True)
|
||||||
|
parser.add_argument("--mcu", required=True)
|
||||||
|
parser.add_argument("--hz", type=int, required=True)
|
||||||
|
parser.add_argument("--base", required=True)
|
||||||
|
parser.add_argument("--page", type=int, required=True)
|
||||||
|
parser.add_argument("--baud", type=int, required=True)
|
||||||
|
parser.add_argument("--app", required=True)
|
||||||
|
parser.add_argument("--tool", required=True)
|
||||||
|
parser.add_argument("--workdir", required=True)
|
||||||
|
parser.add_argument("--link", default=None)
|
||||||
|
parser.add_argument("--seconds", type=float, default=None)
|
||||||
|
parser.add_argument("--autobaud-polls", type=int, default=None)
|
||||||
|
args = parser.parse_args()
|
||||||
|
if (args.seconds is None) == (args.autobaud_polls is None):
|
||||||
|
parser.error("exactly one of --seconds / --autobaud-polls")
|
||||||
|
|
||||||
|
pb = load_tool(args.tool)
|
||||||
|
base = int(args.base, 0)
|
||||||
|
expected = (args.seconds if args.seconds is not None
|
||||||
|
else args.autobaud_polls * AUTOBAUD_POLL_CYCLES / args.hz)
|
||||||
|
|
||||||
|
work = pathlib.Path(args.workdir)
|
||||||
|
work.mkdir(parents=True, exist_ok=True)
|
||||||
|
# Every loader target objcopies its slot content beside the ELF (.bin).
|
||||||
|
loader_bytes = pathlib.Path(args.loader + ".bin").read_bytes()
|
||||||
|
app_bytes = pathlib.Path(args.app).read_bytes()
|
||||||
|
flash_file = work / "window-flash.bin"
|
||||||
|
flash_file.write_bytes(compose_flash(pb, loader_bytes, app_bytes, args.mcu, base, args.page))
|
||||||
|
|
||||||
|
device = Device(args.device, args.loader, args.mcu, str(args.hz), args.base, args.page,
|
||||||
|
args.baud, str(work / "window-dump.bin"), resume=str(flash_file),
|
||||||
|
link=args.link, window=True)
|
||||||
|
try:
|
||||||
|
# Simulation speed is machine-dependent; a few hundred thousand
|
||||||
|
# cycles per wall second is the pessimistic floor.
|
||||||
|
budget = max(60.0, expected * args.hz / 300000)
|
||||||
|
cycle = first_tx_cycle(device, time.monotonic() + budget)
|
||||||
|
finally:
|
||||||
|
device.stop()
|
||||||
|
|
||||||
|
if cycle is None:
|
||||||
|
print(f" [FAIL] no transmit activity within {budget:.0f} s wall "
|
||||||
|
f"(expected a {expected:.2f} s window)")
|
||||||
|
return 1
|
||||||
|
measured = cycle / args.hz
|
||||||
|
error = (measured - expected) / expected
|
||||||
|
ok = abs(error) <= 0.02
|
||||||
|
print(f" [{'PASS' if ok else 'FAIL'}] window {measured:.3f} s vs declared "
|
||||||
|
f"{expected:.3f} s ({error:+.1%}, gate ±2%)")
|
||||||
|
return 0 if ok else 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -1,468 +0,0 @@
|
|||||||
// simavr "device" for the pureboot protocol tests, every chip. Loads the
|
|
||||||
// boot-linked ELF at the loader base, starts execution there (BOOTRST / the
|
|
||||||
// patched vector are not what is under test), and exposes the loader's
|
|
||||||
// serial link as a pty for the real host tool:
|
|
||||||
//
|
|
||||||
// - Hardware USART builds: simavr's uart_pty on the selected instance.
|
|
||||||
// - Software UART builds: an 8N1 bridge between a pty and the GPIO pins,
|
|
||||||
// timed against the simulated cycle counter (drives the loader's RX,
|
|
||||||
// decodes its TX).
|
|
||||||
//
|
|
||||||
// The link follows the chip's natural default (USART0 on the megas, the
|
|
||||||
// software UART on PB0/PB1 elsewhere) unless -l overrides it: `-l usart1`
|
|
||||||
// for the second instance, `-l sw:B5,B1` for a software build's RX,TX pins.
|
|
||||||
//
|
|
||||||
// simavr's tiny cores decode the SPM opcode but attach no NVM module — SPM
|
|
||||||
// is a silent no-op (the mega's boot section has one, avr_flash). The
|
|
||||||
// missing module is supplied here: the SPM ioctl reads SPMCSR/Z/r1:r0 and
|
|
||||||
// implements buffer fill, page erase, page write, and CTPB, completing
|
|
||||||
// instantly. RFLB's LPM diversion (fuse readout) stays unmodeled, so the
|
|
||||||
// 'F' command answers with flash bytes — the tests assert transport only.
|
|
||||||
//
|
|
||||||
// On exit (or SIGTERM) the flash and EEPROM are dumped to files for a
|
|
||||||
// ground-truth cross-check against what the host read back.
|
|
||||||
#include <fcntl.h>
|
|
||||||
#include <pty.h>
|
|
||||||
#include <signal.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include <termios.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
|
|
||||||
#include "avr_eeprom.h"
|
|
||||||
#include "avr_flash.h"
|
|
||||||
#include "avr_ioport.h"
|
|
||||||
#include "avr_uart.h"
|
|
||||||
#include "sim_avr.h"
|
|
||||||
#include "sim_elf.h"
|
|
||||||
#include "sim_io.h"
|
|
||||||
#include "uart_pty.h"
|
|
||||||
|
|
||||||
static avr_t *avr;
|
|
||||||
static uart_pty_t uart_pty;
|
|
||||||
static int link_software;
|
|
||||||
static char uart_digit = '0';
|
|
||||||
static char sw_rx_port = 'B', sw_tx_port = 'B';
|
|
||||||
static int sw_rx_bit = 0, sw_tx_bit = 1;
|
|
||||||
static const char *dump_path;
|
|
||||||
static uint32_t reset_pc;
|
|
||||||
static volatile sig_atomic_t reset_requested;
|
|
||||||
|
|
||||||
static int parse_link(const char *spec)
|
|
||||||
{
|
|
||||||
if (strcmp(spec, "usart0") == 0 || strcmp(spec, "usart1") == 0) {
|
|
||||||
link_software = 0;
|
|
||||||
uart_digit = spec[5];
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (strncmp(spec, "sw", 2) == 0) {
|
|
||||||
link_software = 1;
|
|
||||||
if (spec[2] == '\0')
|
|
||||||
return 0;
|
|
||||||
if (sscanf(spec + 2, ":%c%d,%c%d", &sw_rx_port, &sw_rx_bit, &sw_tx_port, &sw_tx_bit) == 4)
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// simavr 1.6's avr_flash PGERS handler erases spm_pagesize bytes starting at
|
|
||||||
// Z & ~1 instead of the page containing Z (its PGWRT path masks correctly) —
|
|
||||||
// hardware ignores the in-page bits (§26.8.1), so an erase issued with Z
|
|
||||||
// anywhere inside the page wipes half the neighbouring page in simulation
|
|
||||||
// only. Wrap the mega's registered flash ioctl and re-dispatch page erases
|
|
||||||
// with Z forced to the page boundary; everything else passes through.
|
|
||||||
//
|
|
||||||
// A second gap on the boot-section-less m48s: their RWWSRE bit is the
|
|
||||||
// temporary-buffer discard (Atmel-8271 §26.2/§26.3.1), but the stock model
|
|
||||||
// gates its RWWSRE branch on AVR_SELFPROG_HAVE_RWW — absent on the m48
|
|
||||||
// core — so the discard store falls through into the buffer-fill branch and
|
|
||||||
// plants whatever Z/R1:R0 happen to hold. Perform the silicon's discard
|
|
||||||
// here instead.
|
|
||||||
static avr_flash_t *mega_flash;
|
|
||||||
static int (*mega_flash_ioctl)(avr_io_t *io, uint32_t ctl, void *param);
|
|
||||||
|
|
||||||
static int fixed_flash_ioctl(avr_io_t *io, uint32_t ctl, void *param)
|
|
||||||
{
|
|
||||||
if (ctl == AVR_IOCTL_FLASH_SPM && avr_regbit_get(io->avr, mega_flash->pgers)) {
|
|
||||||
uint16_t z = (uint16_t)(io->avr->data[30] | (io->avr->data[31] << 8));
|
|
||||||
uint16_t masked = (uint16_t)(z & ~(mega_flash->spm_pagesize - 1));
|
|
||||||
io->avr->data[30] = (uint8_t)masked;
|
|
||||||
io->avr->data[31] = (uint8_t)(masked >> 8);
|
|
||||||
int result = mega_flash_ioctl(io, ctl, param);
|
|
||||||
io->avr->data[30] = (uint8_t)z;
|
|
||||||
io->avr->data[31] = (uint8_t)(z >> 8);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
if (ctl == AVR_IOCTL_FLASH_SPM && !(mega_flash->flags & AVR_SELFPROG_HAVE_RWW) &&
|
|
||||||
(io->avr->data[mega_flash->r_spm] & 0x11) == 0x11) { // RWWSRE|SELFPRGEN: the m48 buffer discard
|
|
||||||
for (int i = 0; i < mega_flash->spm_pagesize / 2; i++) {
|
|
||||||
mega_flash->tmppage[i] = 0xffff;
|
|
||||||
mega_flash->tmppage_used[i] = 0;
|
|
||||||
}
|
|
||||||
avr_regbit_clear(io->avr, mega_flash->selfprgen);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return mega_flash_ioctl(io, ctl, param);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void fix_mega_flash_erase(void)
|
|
||||||
{
|
|
||||||
for (avr_io_t *io = avr->io_port; io; io = io->next) {
|
|
||||||
if (io->kind && strcmp(io->kind, "flash") == 0) {
|
|
||||||
mega_flash = (avr_flash_t *)io;
|
|
||||||
mega_flash_ioctl = io->ioctl;
|
|
||||||
io->ioctl = fixed_flash_ioctl;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fprintf(stderr, "device: no flash module to fix — SPM page erases may misalign\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
static void request_reset(int sig)
|
|
||||||
{
|
|
||||||
(void)sig;
|
|
||||||
reset_requested = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------- tiny NVM ---
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
avr_io_t io;
|
|
||||||
uint8_t buffer[128];
|
|
||||||
uint8_t used[128]; // a buffer word loads once until erased — like silicon
|
|
||||||
unsigned page;
|
|
||||||
} tiny_nvm_t;
|
|
||||||
|
|
||||||
static tiny_nvm_t nvm;
|
|
||||||
|
|
||||||
static int nvm_ioctl(avr_io_t *io, uint32_t ctl, void *param)
|
|
||||||
{
|
|
||||||
(void)param;
|
|
||||||
if (ctl != AVR_IOCTL_FLASH_SPM)
|
|
||||||
return -1;
|
|
||||||
tiny_nvm_t *n = (tiny_nvm_t *)io;
|
|
||||||
avr_t *mcu = io->avr;
|
|
||||||
uint8_t command = mcu->data[0x57] & 0x1f; // SPMCSR, both tinies
|
|
||||||
uint16_t z = (uint16_t)(mcu->data[30] | (mcu->data[31] << 8));
|
|
||||||
uint32_t page_base = (uint32_t)(z & ~(n->page - 1)) % (mcu->flashend + 1);
|
|
||||||
if (command == 0x01) { // SPMEN alone: buffer fill from r1:r0
|
|
||||||
unsigned offset = z & (n->page - 1) & ~1u;
|
|
||||||
if (!n->used[offset]) { // first write wins until the buffer clears
|
|
||||||
n->buffer[offset] = mcu->data[0];
|
|
||||||
n->buffer[offset + 1] = mcu->data[1];
|
|
||||||
n->used[offset] = 1;
|
|
||||||
}
|
|
||||||
} else if (command == 0x03) { // PGERS
|
|
||||||
memset(mcu->flash + page_base, 0xff, n->page);
|
|
||||||
} else if (command == 0x05) { // PGWRT: programming only clears bits
|
|
||||||
for (unsigned i = 0; i < n->page; i++)
|
|
||||||
mcu->flash[page_base + i] &= n->buffer[i];
|
|
||||||
memset(n->buffer, 0xff, n->page);
|
|
||||||
memset(n->used, 0, n->page);
|
|
||||||
} else if (command == 0x11) { // CTPB
|
|
||||||
memset(n->buffer, 0xff, n->page);
|
|
||||||
memset(n->used, 0, n->page);
|
|
||||||
}
|
|
||||||
mcu->data[0x57] &= (uint8_t)~0x1f; // the operation completes instantly
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------- GPIO bridge ---
|
|
||||||
|
|
||||||
static int pty_master = -1;
|
|
||||||
static avr_irq_t *rx_pin; // the loader's RX (PB0), driven from the pty
|
|
||||||
static avr_cycle_count_t bit_cycles;
|
|
||||||
|
|
||||||
static int tx_level = 1, tx_active, tx_bit;
|
|
||||||
static uint8_t tx_shift;
|
|
||||||
|
|
||||||
static avr_cycle_count_t tx_sample(avr_t *mcu, avr_cycle_count_t when, void *param)
|
|
||||||
{
|
|
||||||
(void)mcu;
|
|
||||||
(void)param;
|
|
||||||
if (tx_bit < 8) {
|
|
||||||
tx_shift = (uint8_t)((tx_shift >> 1) | (tx_level ? 0x80 : 0));
|
|
||||||
if (++tx_bit < 8)
|
|
||||||
return when + bit_cycles;
|
|
||||||
/* The byte is not delivered until its stop bit has passed. A real
|
|
||||||
* receiver cannot answer sooner, and a host that did would put its
|
|
||||||
* start bit on the wire while the device is still driving the stop
|
|
||||||
* bit — which the device, transmitting, is not watching for. */
|
|
||||||
return when + bit_cycles;
|
|
||||||
}
|
|
||||||
if (write(pty_master, &tx_shift, 1) != 1)
|
|
||||||
fprintf(stderr, "device: pty write lost a byte\n");
|
|
||||||
tx_active = 0;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void tx_hook(avr_irq_t *irq, uint32_t value, void *param)
|
|
||||||
{
|
|
||||||
(void)irq;
|
|
||||||
(void)param;
|
|
||||||
int level = value & 1;
|
|
||||||
if (!tx_active && tx_level == 1 && level == 0) { // start edge
|
|
||||||
tx_active = 1;
|
|
||||||
tx_bit = 0;
|
|
||||||
avr_cycle_timer_register(avr, bit_cycles + bit_cycles / 2, tx_sample, NULL);
|
|
||||||
}
|
|
||||||
tx_level = level;
|
|
||||||
}
|
|
||||||
|
|
||||||
static uint8_t rx_queue[8192];
|
|
||||||
static unsigned rx_head, rx_tail; // ring: head = next to send
|
|
||||||
static int rx_active, rx_bit;
|
|
||||||
static uint8_t rx_byte;
|
|
||||||
|
|
||||||
static void rx_start_next(void);
|
|
||||||
|
|
||||||
static avr_cycle_count_t rx_step(avr_t *mcu, avr_cycle_count_t when, void *param)
|
|
||||||
{
|
|
||||||
(void)mcu;
|
|
||||||
(void)param;
|
|
||||||
if (rx_bit < 8) {
|
|
||||||
avr_raise_irq(rx_pin, (rx_byte >> rx_bit) & 1);
|
|
||||||
rx_bit++;
|
|
||||||
return when + bit_cycles;
|
|
||||||
}
|
|
||||||
if (rx_bit == 8) { // stop bit, plus one idle bit of margin
|
|
||||||
avr_raise_irq(rx_pin, 1);
|
|
||||||
rx_bit++;
|
|
||||||
return when + 2 * bit_cycles;
|
|
||||||
}
|
|
||||||
rx_active = 0;
|
|
||||||
rx_start_next();
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void rx_start_next(void)
|
|
||||||
{
|
|
||||||
if (rx_active || rx_head == rx_tail)
|
|
||||||
return;
|
|
||||||
rx_byte = rx_queue[rx_head];
|
|
||||||
rx_head = (rx_head + 1) % sizeof(rx_queue);
|
|
||||||
rx_active = 1;
|
|
||||||
rx_bit = 0;
|
|
||||||
avr_raise_irq(rx_pin, 0); // start bit
|
|
||||||
avr_cycle_timer_register(avr, bit_cycles, rx_step, NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
// A reset abandons whatever the bridge was mid-transfer: bytes still queued
|
|
||||||
// for a chip that no longer has the context to receive them meaningfully,
|
|
||||||
// and a decode in progress on a TX line the reset may have already changed.
|
|
||||||
// The pending cycle timers must go with the state: avr_reset drops the TX
|
|
||||||
// output latch, whose falling edge starts a spurious decode before this
|
|
||||||
// runs, and a stale tx_sample would then interleave with the loader's first
|
|
||||||
// real answer through the shared shift state, corrupting it.
|
|
||||||
static void bridge_reset(void)
|
|
||||||
{
|
|
||||||
avr_cycle_timer_cancel(avr, tx_sample, NULL);
|
|
||||||
avr_cycle_timer_cancel(avr, rx_step, NULL);
|
|
||||||
rx_head = rx_tail = 0;
|
|
||||||
rx_active = 0;
|
|
||||||
tx_active = 0;
|
|
||||||
tx_level = 1;
|
|
||||||
avr_raise_irq(rx_pin, 1); // idle line
|
|
||||||
}
|
|
||||||
|
|
||||||
static void poll_pty(void)
|
|
||||||
{
|
|
||||||
uint8_t chunk[256];
|
|
||||||
ssize_t got = read(pty_master, chunk, sizeof(chunk));
|
|
||||||
for (ssize_t i = 0; i < got; i++) {
|
|
||||||
unsigned next = (rx_tail + 1) % sizeof(rx_queue);
|
|
||||||
if (next == rx_head)
|
|
||||||
break; // full: the host will retry on timeout
|
|
||||||
rx_queue[rx_tail] = chunk[i];
|
|
||||||
rx_tail = next;
|
|
||||||
}
|
|
||||||
if (got > 0)
|
|
||||||
rx_start_next();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------ main ---
|
|
||||||
|
|
||||||
static void finish(int sig)
|
|
||||||
{
|
|
||||||
(void)sig;
|
|
||||||
if (dump_path) {
|
|
||||||
FILE *f = fopen(dump_path, "wb");
|
|
||||||
if (f) {
|
|
||||||
fwrite(avr->flash, 1, avr->flashend + 1, f);
|
|
||||||
fclose(f);
|
|
||||||
}
|
|
||||||
avr_eeprom_desc_t ee = {.ee = NULL, .offset = 0, .size = 0};
|
|
||||||
if (avr_ioctl(avr, AVR_IOCTL_EEPROM_GET, &ee) == 0 && ee.ee && ee.size) {
|
|
||||||
char path[512];
|
|
||||||
snprintf(path, sizeof(path), "%s.eeprom", dump_path);
|
|
||||||
f = fopen(path, "wb");
|
|
||||||
if (f) {
|
|
||||||
fwrite(ee.ee, 1, ee.size, f);
|
|
||||||
fclose(f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!link_software)
|
|
||||||
uart_pty_stop(&uart_pty);
|
|
||||||
_exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
|
||||||
{
|
|
||||||
int link_given = 0;
|
|
||||||
for (int opt; (opt = getopt(argc, argv, "l:")) != -1;) {
|
|
||||||
if (opt != 'l' || parse_link(optarg) != 0) {
|
|
||||||
fprintf(stderr, "device: bad link spec (usart0, usart1, sw, or sw:B0,B1 as RX,TX)\n");
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
link_given = 1;
|
|
||||||
}
|
|
||||||
int args = argc - optind;
|
|
||||||
if (args < 7 || args > 9) {
|
|
||||||
fprintf(stderr,
|
|
||||||
"usage: %s [-l link] <pureboot.elf> <mcu> <hz> <base_hex> <page> <baud> <flash_dump>"
|
|
||||||
" [reset_hex] [resume_flash]\n"
|
|
||||||
" -l link: usart0 | usart1 | sw[:B0,B1] (RX,TX); default: the chip's own\n"
|
|
||||||
" reset_hex: reset vector (default: base with a boot section, else 0)\n"
|
|
||||||
" resume_flash: raw full-flash image loaded instead of the ELF — a prior\n"
|
|
||||||
" run's dump, for power-fail resume tests\n",
|
|
||||||
argv[0]);
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
argv += optind - 1; // argv[1] is the ELF again, whatever was parsed
|
|
||||||
const char *mcu_name = argv[2];
|
|
||||||
uint32_t base = (uint32_t)strtoul(argv[4], NULL, 0);
|
|
||||||
unsigned page = (unsigned)atoi(argv[5]);
|
|
||||||
unsigned baud = (unsigned)atoi(argv[6]);
|
|
||||||
dump_path = argv[7];
|
|
||||||
int is_mega = strncmp(mcu_name, "atmega", 6) == 0;
|
|
||||||
if (!link_given)
|
|
||||||
link_software = !is_mega; // the chips' natural links: USART0, or PB0/PB1
|
|
||||||
|
|
||||||
avr = avr_make_mcu_by_name(mcu_name);
|
|
||||||
if (!avr) {
|
|
||||||
fprintf(stderr, "device: no %s core\n", mcu_name);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
avr_init(avr);
|
|
||||||
avr->frequency = (uint32_t)strtoul(argv[3], NULL, 0);
|
|
||||||
memset(avr->flash, 0xff, avr->flashend + 1); // real flash powers up erased
|
|
||||||
|
|
||||||
if (args > 8) {
|
|
||||||
// Resume: the full flash image of an interrupted prior run.
|
|
||||||
FILE *f = fopen(argv[9], "rb");
|
|
||||||
if (!f || fread(avr->flash, 1, avr->flashend + 1, f) == 0) {
|
|
||||||
fprintf(stderr, "device: cannot read %s\n", argv[9]);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
fclose(f);
|
|
||||||
} else {
|
|
||||||
elf_firmware_t fw = {0};
|
|
||||||
if (elf_read_firmware(argv[1], &fw) != 0) {
|
|
||||||
fprintf(stderr, "device: cannot read %s\n", argv[1]);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
memcpy(avr->flash + base, fw.flash, fw.flashsize);
|
|
||||||
}
|
|
||||||
// The boot-sectioned megas enter the loader in hardware (BOOTRST, not
|
|
||||||
// modeled — the argument picks the modeled fuse's target); the tinies
|
|
||||||
// and the boot-section-less m48s reset to word 0 like silicon — erased
|
|
||||||
// flash walks up into the loader, and after the host's surgery the
|
|
||||||
// patched vector routes there.
|
|
||||||
int boot_section = is_mega && strncmp(mcu_name, "atmega48", 8) != 0;
|
|
||||||
reset_pc = args > 7 ? (uint32_t)strtoul(argv[8], NULL, 0) : (boot_section ? base : 0);
|
|
||||||
avr->pc = reset_pc;
|
|
||||||
avr->codeend = avr->flashend;
|
|
||||||
|
|
||||||
// Erased EEPROM, as hardware powers up (simavr zeroes it).
|
|
||||||
uint8_t blank[1024];
|
|
||||||
memset(blank, 0xff, sizeof(blank));
|
|
||||||
avr_eeprom_desc_t seed = {.ee = blank, .offset = 0, .size = 0};
|
|
||||||
if (avr_ioctl(avr, AVR_IOCTL_EEPROM_GET, &seed) == 0 && seed.size <= sizeof(blank)) {
|
|
||||||
seed.ee = blank;
|
|
||||||
avr_ioctl(avr, AVR_IOCTL_EEPROM_SET, &seed);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The megas carry simavr's avr_flash module (and its two gaps the wrap
|
|
||||||
// above fixes); the tinies get the NVM module simavr lacks. Which serial
|
|
||||||
// bridge runs is the link's business, not the chip class's.
|
|
||||||
if (is_mega) {
|
|
||||||
fix_mega_flash_erase();
|
|
||||||
} else {
|
|
||||||
nvm.page = page;
|
|
||||||
memset(nvm.buffer, 0xff, sizeof(nvm.buffer));
|
|
||||||
nvm.io.kind = "tiny_nvm";
|
|
||||||
nvm.io.ioctl = nvm_ioctl;
|
|
||||||
avr_register_io(avr, &nvm.io);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!link_software) {
|
|
||||||
// POLL_SLEEP paces an idle-polling loader in host real time (a
|
|
||||||
// no-hardware CPU-saving hack); clear it so cycles run free.
|
|
||||||
uint32_t flags = 0;
|
|
||||||
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS(uart_digit), &flags);
|
|
||||||
flags &= ~AVR_UART_FLAG_POLL_SLEEP;
|
|
||||||
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS(uart_digit), &flags);
|
|
||||||
uart_pty_init(avr, &uart_pty);
|
|
||||||
uart_pty_connect(&uart_pty, uart_digit);
|
|
||||||
printf("PB_PTY %s\n", uart_pty.pty.slavename);
|
|
||||||
} else {
|
|
||||||
bit_cycles = (avr->frequency + baud / 2) / baud; // matches uart.hpp's own rounding exactly
|
|
||||||
rx_pin = avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ(sw_rx_port), (unsigned)sw_rx_bit);
|
|
||||||
avr_irq_register_notify(avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ(sw_tx_port), (unsigned)sw_tx_bit), tx_hook,
|
|
||||||
NULL);
|
|
||||||
avr_raise_irq(rx_pin, 1); // idle line
|
|
||||||
|
|
||||||
int slave;
|
|
||||||
struct termios raw;
|
|
||||||
cfmakeraw(&raw);
|
|
||||||
if (openpty(&pty_master, &slave, NULL, &raw, NULL) != 0) {
|
|
||||||
fprintf(stderr, "device: openpty failed\n");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
fcntl(pty_master, F_SETFL, O_NONBLOCK);
|
|
||||||
printf("PB_PTY %s\n", ttyname(slave));
|
|
||||||
}
|
|
||||||
fflush(stdout);
|
|
||||||
|
|
||||||
signal(SIGTERM, finish);
|
|
||||||
signal(SIGINT, finish);
|
|
||||||
signal(SIGUSR1, request_reset); // an external reset line, for the tests
|
|
||||||
|
|
||||||
long since_poll = 0;
|
|
||||||
for (;;) {
|
|
||||||
int state = avr_run(avr);
|
|
||||||
if (state == cpu_Done || state == cpu_Crashed)
|
|
||||||
break;
|
|
||||||
if (reset_requested) {
|
|
||||||
reset_requested = 0;
|
|
||||||
avr_reset(avr);
|
|
||||||
avr->pc = reset_pc;
|
|
||||||
if (!link_software) { // reset restores the pacing hack; re-clear it
|
|
||||||
uint32_t flags = 0;
|
|
||||||
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS(uart_digit), &flags);
|
|
||||||
flags &= ~AVR_UART_FLAG_POLL_SLEEP;
|
|
||||||
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS(uart_digit), &flags);
|
|
||||||
} else {
|
|
||||||
bridge_reset();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (link_software && ++since_poll >= 2000) {
|
|
||||||
since_poll = 0;
|
|
||||||
poll_pty();
|
|
||||||
// An unthrottled idle simulation runs the activation window out
|
|
||||||
// from under the host's real-time knock cadence: a 1 MHz build's
|
|
||||||
// 8 s window is 8 M cycles — tens of wall milliseconds — so a
|
|
||||||
// first knock lost to an in-flight reset misses the window
|
|
||||||
// entirely. Pace the simulation only while the bridge is fully
|
|
||||||
// quiet (nothing decoding, nothing queued); transfers keep full
|
|
||||||
// speed, and a quiet window stretches toward real time.
|
|
||||||
if (!rx_active && !tx_active && rx_head == rx_tail)
|
|
||||||
usleep(200);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finish(0);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
668
test/pureboot_device.cpp
Normal file
668
test/pureboot_device.cpp
Normal file
@@ -0,0 +1,668 @@
|
|||||||
|
// simavr "device" for the pureboot protocol tests, every chip. Loads the
|
||||||
|
// boot-linked ELF at the loader base, starts execution there (BOOTRST / the
|
||||||
|
// patched vector are not what is under test), and exposes the loader's
|
||||||
|
// serial link as a pty for the real host tool:
|
||||||
|
//
|
||||||
|
// - Hardware USART builds: simavr's uart_pty on the selected instance.
|
||||||
|
// - Software UART builds: an 8N1 bridge between a pty and the GPIO pins,
|
||||||
|
// timed against the simulated cycle counter (drives the loader's RX,
|
||||||
|
// decodes its TX).
|
||||||
|
//
|
||||||
|
// The link follows the chip's natural default (USART0 on the megas, the
|
||||||
|
// software UART on PB0/PB1 elsewhere) unless -l overrides it: `-l usart1`
|
||||||
|
// for the second instance, `-l sw:B5,B1` for a software build's RX,TX pins,
|
||||||
|
// and `-l sw:D0,D1@0` where those pins are a USART's own — see the pin
|
||||||
|
// ownership the bridge models below.
|
||||||
|
//
|
||||||
|
// simavr's tiny cores decode the SPM opcode but attach no NVM module — SPM
|
||||||
|
// is a silent no-op (the mega's boot section has one, avr_flash). The
|
||||||
|
// missing module is supplied here: the SPM ioctl reads SPMCSR/Z/r1:r0 and
|
||||||
|
// implements buffer fill, page erase, page write, and CTPB, completing
|
||||||
|
// instantly. RFLB's LPM diversion (fuse readout) stays unmodeled, so the
|
||||||
|
// 'F' command answers with flash bytes — the tests assert transport only.
|
||||||
|
//
|
||||||
|
// On exit (or SIGTERM) the flash and EEPROM are dumped to files for a
|
||||||
|
// ground-truth cross-check against what the host read back.
|
||||||
|
#include <csignal>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <print>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <pty.h>
|
||||||
|
#include <termios.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
// The parts headers (uart_pty.h) carry no C++ linkage guards of their own,
|
||||||
|
// unlike simavr's core headers — the block covers both harmlessly.
|
||||||
|
extern "C" {
|
||||||
|
#include "avr_eeprom.h"
|
||||||
|
#include "avr_flash.h"
|
||||||
|
#include "avr_ioport.h"
|
||||||
|
#include "avr_uart.h"
|
||||||
|
#include "sim_avr.h"
|
||||||
|
#include "sim_elf.h"
|
||||||
|
#include "sim_io.h"
|
||||||
|
#include "uart_pty.h"
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
avr_t *avr;
|
||||||
|
uart_pty_t uart_pty;
|
||||||
|
bool link_software;
|
||||||
|
avr_uart_t *hw_uart; // the pty-driven USART, for the datasheet-reset fix below
|
||||||
|
char uart_digit = '0';
|
||||||
|
char sw_rx_port = 'B', sw_tx_port = 'B';
|
||||||
|
int sw_rx_bit = 0, sw_tx_bit = 1;
|
||||||
|
char sw_tx_owner = 0; // the USART whose TXD the software link sits on
|
||||||
|
const char *dump_path;
|
||||||
|
std::uint32_t reset_pc;
|
||||||
|
volatile std::sig_atomic_t reset_requested;
|
||||||
|
|
||||||
|
// -w: report the cycle of the first transmit activity, once. What the
|
||||||
|
// activation-window gate reads — with an idle line and an application
|
||||||
|
// installed, the first thing that ever talks is the application's banner,
|
||||||
|
// so this cycle *is* the loader's window plus a banner lead measured in
|
||||||
|
// microseconds. Idle pacing is skipped in this mode: there is no real-time
|
||||||
|
// host in the loop, and a paced multi-second window would take hours.
|
||||||
|
bool window_report;
|
||||||
|
bool window_tx_seen;
|
||||||
|
|
||||||
|
void window_first_tx()
|
||||||
|
{
|
||||||
|
if (!window_report || window_tx_seen)
|
||||||
|
return;
|
||||||
|
window_tx_seen = true;
|
||||||
|
std::println("PB_WINDOW_TX {}", avr->cycle);
|
||||||
|
std::fflush(stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
void window_uart_hook(avr_irq_t *, std::uint32_t, void *)
|
||||||
|
{
|
||||||
|
window_first_tx();
|
||||||
|
}
|
||||||
|
|
||||||
|
// One-wire (RX == TX in the link spec): both directions on one GPIO line
|
||||||
|
// idling on the firmware's pull-up. The bridge then follows the pin's
|
||||||
|
// direction the way the real wiring does: it drives only while the
|
||||||
|
// firmware's DDR bit reads input, decodes transitions as the firmware's
|
||||||
|
// transmit only while the firmware owns the line, ignores its own raises
|
||||||
|
// coming back through the shared irq — and echoes every byte it drives back
|
||||||
|
// to the pty, which is what the host-side FTDI tie does and what the host
|
||||||
|
// tool's --one-wire mode reads back and discards.
|
||||||
|
bool link_one_wire;
|
||||||
|
bool mcu_owns_line;
|
||||||
|
bool self_drive;
|
||||||
|
|
||||||
|
int parse_link(std::string_view spec)
|
||||||
|
{
|
||||||
|
if (spec == "usart0" || spec == "usart1") {
|
||||||
|
link_software = false;
|
||||||
|
uart_digit = spec[5];
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (spec.starts_with("sw")) {
|
||||||
|
link_software = true;
|
||||||
|
if (spec.size() == 2)
|
||||||
|
return 0;
|
||||||
|
char owner = 0;
|
||||||
|
int fields =
|
||||||
|
std::sscanf(spec.data() + 2, ":%c%d,%c%d@%c", &sw_rx_port, &sw_rx_bit, &sw_tx_port, &sw_tx_bit, &owner);
|
||||||
|
if (fields == 4 || fields == 5) {
|
||||||
|
sw_tx_owner = owner;
|
||||||
|
link_one_wire = sw_rx_port == sw_tx_port && sw_rx_bit == sw_tx_bit;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// simavr 1.6's avr_flash PGERS handler erases spm_pagesize bytes starting at
|
||||||
|
// Z & ~1 instead of the page containing Z (its PGWRT path masks correctly) —
|
||||||
|
// hardware ignores the in-page bits (§26.8.1), so an erase issued with Z
|
||||||
|
// anywhere inside the page wipes half the neighbouring page in simulation
|
||||||
|
// only. Wrap the mega's registered flash ioctl and re-dispatch page erases
|
||||||
|
// with Z forced to the page boundary; everything else passes through.
|
||||||
|
//
|
||||||
|
// A second gap on the boot-section-less m48s: their RWWSRE bit is the
|
||||||
|
// temporary-buffer discard (Atmel-8271 §26.2/§26.3.1), but the stock model
|
||||||
|
// gates its RWWSRE branch on AVR_SELFPROG_HAVE_RWW — absent on the m48
|
||||||
|
// core — so the discard store falls through into the buffer-fill branch and
|
||||||
|
// plants whatever Z/R1:R0 happen to hold. Perform the silicon's discard
|
||||||
|
// here instead.
|
||||||
|
avr_flash_t *mega_flash;
|
||||||
|
int (*mega_flash_ioctl)(avr_io_t *io, std::uint32_t ctl, void *param);
|
||||||
|
|
||||||
|
int fixed_flash_ioctl(avr_io_t *io, std::uint32_t ctl, void *param)
|
||||||
|
{
|
||||||
|
if (ctl == AVR_IOCTL_FLASH_SPM && avr_regbit_get(io->avr, mega_flash->pgers)) {
|
||||||
|
auto z = static_cast<std::uint16_t>(io->avr->data[30] | (io->avr->data[31] << 8));
|
||||||
|
auto masked = static_cast<std::uint16_t>(z & ~(mega_flash->spm_pagesize - 1));
|
||||||
|
io->avr->data[30] = static_cast<std::uint8_t>(masked);
|
||||||
|
io->avr->data[31] = static_cast<std::uint8_t>(masked >> 8);
|
||||||
|
int result = mega_flash_ioctl(io, ctl, param);
|
||||||
|
io->avr->data[30] = static_cast<std::uint8_t>(z);
|
||||||
|
io->avr->data[31] = static_cast<std::uint8_t>(z >> 8);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (ctl == AVR_IOCTL_FLASH_SPM && !(mega_flash->flags & AVR_SELFPROG_HAVE_RWW) &&
|
||||||
|
(io->avr->data[mega_flash->r_spm] & 0x11) == 0x11) { // RWWSRE|SELFPRGEN: the m48 buffer discard
|
||||||
|
for (int i = 0; i < mega_flash->spm_pagesize / 2; i++) {
|
||||||
|
mega_flash->tmppage[i] = 0xffff;
|
||||||
|
mega_flash->tmppage_used[i] = 0;
|
||||||
|
}
|
||||||
|
avr_regbit_clear(io->avr, mega_flash->selfprgen);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return mega_flash_ioctl(io, ctl, param);
|
||||||
|
}
|
||||||
|
|
||||||
|
void fix_mega_flash_erase()
|
||||||
|
{
|
||||||
|
for (avr_io_t *io = avr->io_port; io; io = io->next) {
|
||||||
|
if (io->kind && std::string_view{io->kind} == "flash") {
|
||||||
|
mega_flash = reinterpret_cast<avr_flash_t *>(io);
|
||||||
|
mega_flash_ioctl = io->ioctl;
|
||||||
|
io->ioctl = fixed_flash_ioctl;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::println(stderr, "device: no flash module to fix — SPM page erases may misalign");
|
||||||
|
}
|
||||||
|
|
||||||
|
void request_reset(int)
|
||||||
|
{
|
||||||
|
reset_requested = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------- tiny NVM ---
|
||||||
|
|
||||||
|
struct tiny_nvm_t {
|
||||||
|
avr_io_t io;
|
||||||
|
std::uint8_t buffer[128];
|
||||||
|
std::uint8_t used[128]; // a buffer word loads once until erased — like silicon
|
||||||
|
unsigned page;
|
||||||
|
};
|
||||||
|
|
||||||
|
tiny_nvm_t nvm;
|
||||||
|
|
||||||
|
int nvm_ioctl(avr_io_t *io, std::uint32_t ctl, void *)
|
||||||
|
{
|
||||||
|
if (ctl != AVR_IOCTL_FLASH_SPM)
|
||||||
|
return -1;
|
||||||
|
auto *n = reinterpret_cast<tiny_nvm_t *>(io);
|
||||||
|
avr_t *mcu = io->avr;
|
||||||
|
std::uint8_t command = mcu->data[0x57] & 0x1f; // SPMCSR, both tinies
|
||||||
|
auto z = static_cast<std::uint16_t>(mcu->data[30] | (mcu->data[31] << 8));
|
||||||
|
std::uint32_t page_base = static_cast<std::uint32_t>(z & ~(n->page - 1)) % (mcu->flashend + 1);
|
||||||
|
if (command == 0x01) { // SPMEN alone: buffer fill from r1:r0
|
||||||
|
unsigned offset = z & (n->page - 1) & ~1u;
|
||||||
|
if (!n->used[offset]) { // first write wins until the buffer clears
|
||||||
|
n->buffer[offset] = mcu->data[0];
|
||||||
|
n->buffer[offset + 1] = mcu->data[1];
|
||||||
|
n->used[offset] = 1;
|
||||||
|
}
|
||||||
|
} else if (command == 0x03) { // PGERS
|
||||||
|
std::memset(mcu->flash + page_base, 0xff, n->page);
|
||||||
|
} else if (command == 0x05) { // PGWRT: programming only clears bits
|
||||||
|
for (unsigned i = 0; i < n->page; i++)
|
||||||
|
mcu->flash[page_base + i] &= n->buffer[i];
|
||||||
|
std::memset(n->buffer, 0xff, n->page);
|
||||||
|
std::memset(n->used, 0, n->page);
|
||||||
|
} else if (command == 0x11) { // CTPB
|
||||||
|
std::memset(n->buffer, 0xff, n->page);
|
||||||
|
std::memset(n->used, 0, n->page);
|
||||||
|
}
|
||||||
|
mcu->data[0x57] &= static_cast<std::uint8_t>(~0x1f); // the operation completes instantly
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------- GPIO bridge ---
|
||||||
|
|
||||||
|
int pty_master = -1;
|
||||||
|
avr_irq_t *rx_pin; // the loader's RX (PB0), driven from the pty
|
||||||
|
avr_cycle_count_t bit_cycles;
|
||||||
|
|
||||||
|
int tx_level = 1, tx_active, tx_bit;
|
||||||
|
std::uint8_t tx_shift;
|
||||||
|
|
||||||
|
avr_cycle_count_t tx_sample(avr_t *, avr_cycle_count_t when, void *)
|
||||||
|
{
|
||||||
|
if (tx_bit < 0) {
|
||||||
|
// Half a bit into the start bit: a real receiver re-samples here and
|
||||||
|
// abandons a false start. The device's own init produces one — DDR
|
||||||
|
// drives the pin low for the instructions until the idle level is
|
||||||
|
// written — and without this check that glitch decodes as a stray
|
||||||
|
// byte (and would read as first transmit activity under -w).
|
||||||
|
if (tx_level) {
|
||||||
|
tx_active = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
window_first_tx();
|
||||||
|
tx_bit = 0;
|
||||||
|
return when + bit_cycles;
|
||||||
|
}
|
||||||
|
if (tx_bit < 8) {
|
||||||
|
tx_shift = static_cast<std::uint8_t>((tx_shift >> 1) | (tx_level ? 0x80 : 0));
|
||||||
|
if (++tx_bit < 8)
|
||||||
|
return when + bit_cycles;
|
||||||
|
// The byte is delivered at the stop bit's sampling point (9.5 bit
|
||||||
|
// times), where a hardware receiver raises its RXC — not sooner: a
|
||||||
|
// host answering before the stop bit would put its start bit on the
|
||||||
|
// wire while the device is still driving, which the device,
|
||||||
|
// transmitting, is not watching for.
|
||||||
|
return when + bit_cycles;
|
||||||
|
}
|
||||||
|
if (write(pty_master, &tx_shift, 1) != 1)
|
||||||
|
std::println(stderr, "device: pty write lost a byte");
|
||||||
|
tx_active = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A USART owns its TxD pin whenever its transmitter is enabled, and the port
|
||||||
|
// register cannot drive it (§20.2 / Atmel-8271 §19.2) — which is why a
|
||||||
|
// bit-banged link deployed on those pins is mute until it clears UCSRnB.
|
||||||
|
// simavr wires a USART entirely through IRQs and never touches the port pin
|
||||||
|
// model, so the ownership does not exist there and the mute cannot happen:
|
||||||
|
// supply it, or the very state this models is untestable. The link spec's
|
||||||
|
// trailing @n names the USART; without one the pins are nobody's.
|
||||||
|
avr_uart_t *tx_owner;
|
||||||
|
|
||||||
|
bool tx_pin_taken()
|
||||||
|
{
|
||||||
|
if (!tx_owner)
|
||||||
|
return false;
|
||||||
|
if (avr_regbit_get(avr, tx_owner->txen))
|
||||||
|
return true;
|
||||||
|
// One-wire on the USART's RXD: RXEN forces the shared pin's direction to
|
||||||
|
// input (§20.7.3), so the firmware's drive goes nowhere until the
|
||||||
|
// release — the receive-side twin of the TXD hold.
|
||||||
|
return link_one_wire && avr_regbit_get(avr, tx_owner->rxen);
|
||||||
|
}
|
||||||
|
|
||||||
|
// simavr leaves TXEN set in UCSRnB out of reset, where silicon clears the
|
||||||
|
// whole register (§20.11.3) — which would hand the pin to a USART no code has
|
||||||
|
// enabled, making a freshly reset chip mute for reasons hardware does not
|
||||||
|
// have. Reset it the way the datasheet does, so the ownership starts from
|
||||||
|
// nobody's and only an application that really enables the USART takes it.
|
||||||
|
void reset_tx_owner()
|
||||||
|
{
|
||||||
|
if (tx_owner)
|
||||||
|
avr_regbit_clear(avr, tx_owner->txen);
|
||||||
|
}
|
||||||
|
|
||||||
|
void find_tx_owner()
|
||||||
|
{
|
||||||
|
for (avr_io_t *io = avr->io_port; io; io = io->next)
|
||||||
|
if (io->kind && std::string_view{io->kind} == "uart" &&
|
||||||
|
reinterpret_cast<avr_uart_t *>(io)->name == sw_tx_owner) {
|
||||||
|
tx_owner = reinterpret_cast<avr_uart_t *>(io);
|
||||||
|
reset_tx_owner();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::println(stderr, "device: no USART{} to own the software link's TX pin", sw_tx_owner);
|
||||||
|
}
|
||||||
|
|
||||||
|
void tx_hook(avr_irq_t *, std::uint32_t value, void *)
|
||||||
|
{
|
||||||
|
if (link_one_wire && (self_drive || !mcu_owns_line)) {
|
||||||
|
// The bridge's own drive coming back through the shared irq, or a
|
||||||
|
// transition while the line is the bridge's — either way not the
|
||||||
|
// firmware talking: the decoder sees an idle line.
|
||||||
|
tx_level = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (tx_pin_taken()) { // the USART holds the line; the port write goes nowhere
|
||||||
|
tx_level = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int level = value & 1;
|
||||||
|
if (!tx_active && tx_level == 1 && level == 0) { // start edge, confirmed mid-bit
|
||||||
|
tx_active = 1;
|
||||||
|
tx_bit = -1;
|
||||||
|
avr_cycle_timer_register(avr, bit_cycles / 2, tx_sample, nullptr);
|
||||||
|
}
|
||||||
|
tx_level = level;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint8_t rx_queue[8192];
|
||||||
|
unsigned rx_head, rx_tail; // ring: head = next to send
|
||||||
|
int rx_active, rx_bit;
|
||||||
|
std::uint8_t rx_byte;
|
||||||
|
|
||||||
|
void rx_start_next();
|
||||||
|
|
||||||
|
// Every level the bridge itself puts on the line goes through here, so the
|
||||||
|
// shared-pin decoder can tell its own drive from the firmware's.
|
||||||
|
void bridge_drive(int level)
|
||||||
|
{
|
||||||
|
self_drive = true;
|
||||||
|
avr_raise_irq(rx_pin, static_cast<std::uint32_t>(level));
|
||||||
|
self_drive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
avr_cycle_count_t rx_step(avr_t *, avr_cycle_count_t when, void *)
|
||||||
|
{
|
||||||
|
if (rx_bit < 8) {
|
||||||
|
bridge_drive((rx_byte >> rx_bit) & 1);
|
||||||
|
rx_bit++;
|
||||||
|
return when + bit_cycles;
|
||||||
|
}
|
||||||
|
if (rx_bit == 8) { // stop bit, plus one idle bit of margin
|
||||||
|
bridge_drive(1);
|
||||||
|
// The host-side tie: an FTDI adapter on a one-wire line reads every
|
||||||
|
// byte it transmits — supply that echo, which the host tool's
|
||||||
|
// --one-wire mode consumes as its wiring check.
|
||||||
|
if (link_one_wire && write(pty_master, &rx_byte, 1) != 1)
|
||||||
|
std::println(stderr, "device: pty echo lost a byte");
|
||||||
|
rx_bit++;
|
||||||
|
return when + 2 * bit_cycles;
|
||||||
|
}
|
||||||
|
rx_active = 0;
|
||||||
|
rx_start_next();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void rx_start_next()
|
||||||
|
{
|
||||||
|
if (rx_active || rx_head == rx_tail)
|
||||||
|
return;
|
||||||
|
// The firmware is answering on the shared line: hold the byte — a real
|
||||||
|
// host's transmission waits out the reply on the wire too. The next
|
||||||
|
// poll_pty tick retries once the line is handed back.
|
||||||
|
if (link_one_wire && mcu_owns_line)
|
||||||
|
return;
|
||||||
|
rx_byte = rx_queue[rx_head];
|
||||||
|
rx_head = (rx_head + 1) % sizeof(rx_queue);
|
||||||
|
rx_active = 1;
|
||||||
|
rx_bit = 0;
|
||||||
|
bridge_drive(0); // start bit
|
||||||
|
avr_cycle_timer_register(avr, bit_cycles, rx_step, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The shared pin's direction is the line's ownership: DDR-out is the
|
||||||
|
// firmware driving a frame, DDR-in hands the line back to the bridge.
|
||||||
|
void on_ddr(avr_irq_t *, std::uint32_t value, void *)
|
||||||
|
{
|
||||||
|
const bool owns = (value >> sw_rx_bit) & 1;
|
||||||
|
if (mcu_owns_line && !owns)
|
||||||
|
bridge_drive(1); // hand-back: a turn-based host idles here, and the cache stays truthful
|
||||||
|
mcu_owns_line = owns;
|
||||||
|
// A byte held back while the firmware answered starts from the next
|
||||||
|
// poll_pty tick, never from inside the DDR write itself — the port
|
||||||
|
// model's own pull-up re-derivation runs right after this notify and
|
||||||
|
// would erase a start edge raised here.
|
||||||
|
}
|
||||||
|
|
||||||
|
// A reset abandons whatever the bridge was mid-transfer: bytes still queued
|
||||||
|
// for a chip that no longer has the context to receive them meaningfully,
|
||||||
|
// and a decode in progress on a TX line the reset may have already changed.
|
||||||
|
// The pending cycle timers must go with the state: avr_reset drops the TX
|
||||||
|
// output latch, whose falling edge starts a spurious decode before this
|
||||||
|
// runs, and a stale tx_sample would then interleave with the loader's first
|
||||||
|
// real answer through the shared shift state, corrupting it.
|
||||||
|
void bridge_reset()
|
||||||
|
{
|
||||||
|
avr_cycle_timer_cancel(avr, tx_sample, nullptr);
|
||||||
|
avr_cycle_timer_cancel(avr, rx_step, nullptr);
|
||||||
|
rx_head = rx_tail = 0;
|
||||||
|
rx_active = 0;
|
||||||
|
tx_active = 0;
|
||||||
|
tx_level = 1;
|
||||||
|
mcu_owns_line = false; // avr_reset zeroed DDR: every pin reads input again
|
||||||
|
// Re-drive the idle line through a forced transition: ioport pin irqs are
|
||||||
|
// IRQ_FLAG_FILTERED, and avr_reset zeroes the port latch while the irq
|
||||||
|
// keeps its pre-reset cached value — so a plain raise(1) against a cached
|
||||||
|
// 1 is dropped and the device reads the line stuck low. A loader entering
|
||||||
|
// calibration on that line measures reset-to-first-edge as one giant
|
||||||
|
// pulse and mis-locks or boots the application on the first real knock.
|
||||||
|
// No cycles run between the two raises, so the device only ever sees the
|
||||||
|
// final idle-high.
|
||||||
|
bridge_drive(0);
|
||||||
|
bridge_drive(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void poll_pty()
|
||||||
|
{
|
||||||
|
std::uint8_t chunk[256];
|
||||||
|
ssize_t got = read(pty_master, chunk, sizeof(chunk));
|
||||||
|
for (ssize_t i = 0; i < got; i++) {
|
||||||
|
unsigned next = (rx_tail + 1) % sizeof(rx_queue);
|
||||||
|
if (next == rx_head)
|
||||||
|
break; // full: the host will retry on timeout
|
||||||
|
rx_queue[rx_tail] = chunk[i];
|
||||||
|
rx_tail = next;
|
||||||
|
}
|
||||||
|
// Unconditional: a byte held back while the firmware owned a shared
|
||||||
|
// line restarts from here once the hand-back has happened.
|
||||||
|
rx_start_next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ main ---
|
||||||
|
|
||||||
|
[[noreturn]] void finish(int)
|
||||||
|
{
|
||||||
|
if (dump_path) {
|
||||||
|
std::FILE *f = std::fopen(dump_path, "wb");
|
||||||
|
if (f) {
|
||||||
|
std::fwrite(avr->flash, 1, avr->flashend + 1, f);
|
||||||
|
std::fclose(f);
|
||||||
|
}
|
||||||
|
avr_eeprom_desc_t ee = {.ee = nullptr, .offset = 0, .size = 0};
|
||||||
|
if (avr_ioctl(avr, AVR_IOCTL_EEPROM_GET, &ee) == 0 && ee.ee && ee.size) {
|
||||||
|
char path[512];
|
||||||
|
std::snprintf(path, sizeof(path), "%s.eeprom", dump_path);
|
||||||
|
f = std::fopen(path, "wb");
|
||||||
|
if (f) {
|
||||||
|
std::fwrite(ee.ee, 1, ee.size, f);
|
||||||
|
std::fclose(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!link_software)
|
||||||
|
uart_pty_stop(&uart_pty);
|
||||||
|
_exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main(int argc, char *argv[])
|
||||||
|
{
|
||||||
|
bool link_given = false;
|
||||||
|
for (int opt; (opt = getopt(argc, argv, "l:w")) != -1;) {
|
||||||
|
if (opt == 'w') {
|
||||||
|
window_report = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (opt != 'l' || parse_link(optarg) != 0) {
|
||||||
|
std::println(stderr, "device: bad link spec (usart0, usart1, sw, or sw:B0,B1 as RX,TX)");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
link_given = true;
|
||||||
|
}
|
||||||
|
int args = argc - optind;
|
||||||
|
if (args < 7 || args > 9) {
|
||||||
|
std::print(stderr,
|
||||||
|
"usage: {} [-l link] [-w] <pureboot.elf> <mcu> <hz> <base_hex> <page> <baud> <flash_dump>"
|
||||||
|
" [reset_hex] [resume_flash]\n"
|
||||||
|
" -l link: usart0 | usart1 | sw[:B0,B1[@0]] (RX,TX, then the USART owning\n"
|
||||||
|
" them); default: the chip's own\n"
|
||||||
|
" -w: print PB_WINDOW_TX <cycle> at the first transmit activity and\n"
|
||||||
|
" free-run idle time (window measurement mode)\n"
|
||||||
|
" reset_hex: reset vector (default: base with a boot section, else 0)\n"
|
||||||
|
" resume_flash: raw full-flash image loaded instead of the ELF — a prior\n"
|
||||||
|
" run's dump, for power-fail resume tests\n",
|
||||||
|
argv[0]);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
argv += optind - 1; // argv[1] is the ELF again, whatever was parsed
|
||||||
|
const std::string_view mcu_name = argv[2];
|
||||||
|
auto base = static_cast<std::uint32_t>(std::strtoul(argv[4], nullptr, 0));
|
||||||
|
auto page = static_cast<unsigned>(std::atoi(argv[5]));
|
||||||
|
auto baud = static_cast<unsigned>(std::atoi(argv[6]));
|
||||||
|
dump_path = argv[7];
|
||||||
|
const bool is_mega = mcu_name.starts_with("atmega");
|
||||||
|
if (!link_given)
|
||||||
|
link_software = !is_mega; // the chips' natural links: USART0, or PB0/PB1
|
||||||
|
|
||||||
|
avr = avr_make_mcu_by_name(mcu_name.data());
|
||||||
|
if (!avr) {
|
||||||
|
std::println(stderr, "device: no {} core", mcu_name);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
avr_init(avr);
|
||||||
|
avr->frequency = static_cast<std::uint32_t>(std::strtoul(argv[3], nullptr, 0));
|
||||||
|
std::memset(avr->flash, 0xff, avr->flashend + 1); // real flash powers up erased
|
||||||
|
|
||||||
|
if (args > 8) {
|
||||||
|
// Resume: the full flash image of an interrupted prior run.
|
||||||
|
std::FILE *f = std::fopen(argv[9], "rb");
|
||||||
|
if (!f || std::fread(avr->flash, 1, avr->flashend + 1, f) == 0) {
|
||||||
|
std::println(stderr, "device: cannot read {}", argv[9]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
std::fclose(f);
|
||||||
|
} else {
|
||||||
|
elf_firmware_t fw{};
|
||||||
|
if (elf_read_firmware(argv[1], &fw) != 0) {
|
||||||
|
std::println(stderr, "device: cannot read {}", argv[1]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
// An image past flash end would smash the simulator's heap and turn
|
||||||
|
// into phantom peripheral behavior (lessons: believe the size gate
|
||||||
|
// first) — refuse it loudly instead.
|
||||||
|
if (base + fw.flashsize > avr->flashend + 1) {
|
||||||
|
std::println(stderr, "device: {} B at {:#x} runs past flash end {:#x} — image does not fit its slot",
|
||||||
|
fw.flashsize, base, avr->flashend);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
std::memcpy(avr->flash + base, fw.flash, fw.flashsize);
|
||||||
|
}
|
||||||
|
// The boot-sectioned megas enter the loader in hardware (BOOTRST, not
|
||||||
|
// modeled — the argument picks the modeled fuse's target); the tinies
|
||||||
|
// and the boot-section-less m48s reset to word 0 like silicon — erased
|
||||||
|
// flash walks up into the loader, and after the host's surgery the
|
||||||
|
// patched vector routes there.
|
||||||
|
const bool boot_section = is_mega && !mcu_name.starts_with("atmega48");
|
||||||
|
reset_pc = args > 7 ? static_cast<std::uint32_t>(std::strtoul(argv[8], nullptr, 0)) : (boot_section ? base : 0);
|
||||||
|
avr->pc = reset_pc;
|
||||||
|
avr->codeend = avr->flashend;
|
||||||
|
|
||||||
|
// Erased EEPROM, as hardware powers up (simavr zeroes it).
|
||||||
|
std::uint8_t blank[1024];
|
||||||
|
std::memset(blank, 0xff, sizeof(blank));
|
||||||
|
avr_eeprom_desc_t seed = {.ee = blank, .offset = 0, .size = 0};
|
||||||
|
if (avr_ioctl(avr, AVR_IOCTL_EEPROM_GET, &seed) == 0 && seed.size <= sizeof(blank)) {
|
||||||
|
seed.ee = blank;
|
||||||
|
avr_ioctl(avr, AVR_IOCTL_EEPROM_SET, &seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The megas carry simavr's avr_flash module (and its two gaps the wrap
|
||||||
|
// above fixes); the tinies get the NVM module simavr lacks. Which serial
|
||||||
|
// bridge runs is the link's business, not the chip class's.
|
||||||
|
if (is_mega) {
|
||||||
|
fix_mega_flash_erase();
|
||||||
|
} else {
|
||||||
|
nvm.page = page;
|
||||||
|
std::memset(nvm.buffer, 0xff, sizeof(nvm.buffer));
|
||||||
|
nvm.io.kind = "tiny_nvm";
|
||||||
|
nvm.io.ioctl = nvm_ioctl;
|
||||||
|
avr_register_io(avr, &nvm.io);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!link_software) {
|
||||||
|
// POLL_SLEEP paces an idle-polling loader in host real time (a
|
||||||
|
// no-hardware CPU-saving hack); clear it so cycles run free.
|
||||||
|
std::uint32_t flags = 0;
|
||||||
|
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS(uart_digit), &flags);
|
||||||
|
flags &= ~AVR_UART_FLAG_POLL_SLEEP;
|
||||||
|
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS(uart_digit), &flags);
|
||||||
|
// simavr leaves TXEN set out of reset where silicon clears the whole
|
||||||
|
// UCSR#B (§20.11.3). Harmless to a loader that enables TXEN itself —
|
||||||
|
// but a half-duplex build's receiver-only init then *drops* TXEN,
|
||||||
|
// and this uart model clears UDRE on that edge and never re-raises
|
||||||
|
// it on a later enable: the first transmitter after the hand-over
|
||||||
|
// waits UDRE forever, a wedge silicon does not have. Start from the
|
||||||
|
// datasheet's zero, as the software bridge's tx-owner model does.
|
||||||
|
for (avr_io_t *io = avr->io_port; io; io = io->next)
|
||||||
|
if (io->kind && std::string_view{io->kind} == "uart" &&
|
||||||
|
reinterpret_cast<avr_uart_t *>(io)->name == uart_digit)
|
||||||
|
hw_uart = reinterpret_cast<avr_uart_t *>(io);
|
||||||
|
if (hw_uart)
|
||||||
|
avr_regbit_clear(avr, hw_uart->txen);
|
||||||
|
uart_pty_init(avr, &uart_pty);
|
||||||
|
uart_pty_connect(&uart_pty, uart_digit);
|
||||||
|
if (window_report)
|
||||||
|
avr_irq_register_notify(avr_io_getirq(avr, AVR_IOCTL_UART_GETIRQ(uart_digit), UART_IRQ_OUTPUT),
|
||||||
|
window_uart_hook, nullptr);
|
||||||
|
std::println("PB_PTY {}", uart_pty.pty.slavename);
|
||||||
|
} else {
|
||||||
|
bit_cycles = (avr->frequency + baud / 2) / baud; // matches uart.hpp's own rounding exactly
|
||||||
|
if (sw_tx_owner)
|
||||||
|
find_tx_owner();
|
||||||
|
rx_pin = avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ(sw_rx_port), static_cast<unsigned>(sw_rx_bit));
|
||||||
|
avr_irq_register_notify(
|
||||||
|
avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ(sw_tx_port), static_cast<unsigned>(sw_tx_bit)), tx_hook,
|
||||||
|
nullptr);
|
||||||
|
if (link_one_wire)
|
||||||
|
avr_irq_register_notify(avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ(sw_rx_port), IOPORT_IRQ_DIRECTION_ALL),
|
||||||
|
on_ddr, nullptr);
|
||||||
|
bridge_drive(1); // idle line
|
||||||
|
|
||||||
|
int slave;
|
||||||
|
struct termios raw;
|
||||||
|
cfmakeraw(&raw);
|
||||||
|
if (openpty(&pty_master, &slave, nullptr, &raw, nullptr) != 0) {
|
||||||
|
std::println(stderr, "device: openpty failed");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
fcntl(pty_master, F_SETFL, O_NONBLOCK);
|
||||||
|
std::println("PB_PTY {}", ttyname(slave));
|
||||||
|
}
|
||||||
|
std::fflush(stdout);
|
||||||
|
|
||||||
|
std::signal(SIGTERM, finish);
|
||||||
|
std::signal(SIGINT, finish);
|
||||||
|
std::signal(SIGUSR1, request_reset); // an external reset line, for the tests
|
||||||
|
|
||||||
|
long since_poll = 0;
|
||||||
|
for (;;) {
|
||||||
|
int state = avr_run(avr);
|
||||||
|
if (state == cpu_Done || state == cpu_Crashed)
|
||||||
|
break;
|
||||||
|
if (reset_requested) {
|
||||||
|
reset_requested = 0;
|
||||||
|
avr_reset(avr);
|
||||||
|
avr->pc = reset_pc;
|
||||||
|
if (!link_software) { // reset restores the pacing hack; re-clear it
|
||||||
|
std::uint32_t flags = 0;
|
||||||
|
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS(uart_digit), &flags);
|
||||||
|
flags &= ~AVR_UART_FLAG_POLL_SLEEP;
|
||||||
|
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS(uart_digit), &flags);
|
||||||
|
if (hw_uart) // and simavr's bogus reset TXEN (§20.11.3: zero)
|
||||||
|
avr_regbit_clear(avr, hw_uart->txen);
|
||||||
|
} else {
|
||||||
|
bridge_reset();
|
||||||
|
reset_tx_owner();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (link_software && ++since_poll >= 2000) {
|
||||||
|
since_poll = 0;
|
||||||
|
poll_pty();
|
||||||
|
// An unthrottled idle simulation runs the activation window out
|
||||||
|
// from under the host's real-time knock cadence: a 1 MHz build's
|
||||||
|
// 8 s window is 8 M cycles — tens of wall milliseconds — so a
|
||||||
|
// first knock lost to an in-flight reset misses the window
|
||||||
|
// entirely. Pace the simulation only while the bridge is fully
|
||||||
|
// quiet (nothing decoding, nothing queued); transfers keep full
|
||||||
|
// speed, and a quiet window stretches toward real time.
|
||||||
|
if (!window_report && !rx_active && !tx_active && rx_head == rx_tail)
|
||||||
|
usleep(200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finish(0);
|
||||||
|
}
|
||||||
212
test/test_handshake.py
Normal file
212
test/test_handshake.py
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Host-tool activation handshake: bounded against a line that misbehaves.
|
||||||
|
|
||||||
|
`_handshake` drains the line after it sees a prompt, to absorb a real loader's
|
||||||
|
trailing bytes before it asks for the identity. That drain must be bounded: a
|
||||||
|
target that never falls quiet — a board stuck in a reset loop presents exactly
|
||||||
|
this, ~60 reboots/s of UART-reset garbage in which a stray 0x2b reads as a
|
||||||
|
prompt — otherwise spins the tool forever. Regression for that hang, plus a
|
||||||
|
control that a well-behaved loader still connects.
|
||||||
|
|
||||||
|
The handshake must also survive its own leftovers: after `--stay` the loader's
|
||||||
|
final prompt can still be in the USB pipeline when the next invocation opens
|
||||||
|
the port, and on a board wired to reset on open, that opening starts a fresh
|
||||||
|
activation window the stale prompt then betrays — the tool commits to an
|
||||||
|
identity read against a device that never heard its knock, and what it finally
|
||||||
|
collects is the application's banner. StaleDTRPort is that moment as a port.
|
||||||
|
|
||||||
|
Stdlib only, no device: host-tool logic, so it runs on every chip's preset
|
||||||
|
beside pureboot.planner.
|
||||||
|
"""
|
||||||
|
import importlib.util
|
||||||
|
import pathlib
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
PB = pathlib.Path(__file__).resolve().parents[1] / "pureboot" / "pureboot.py"
|
||||||
|
_spec = importlib.util.spec_from_file_location("pureboot", PB)
|
||||||
|
pb = importlib.util.module_from_spec(_spec)
|
||||||
|
_spec.loader.exec_module(pb)
|
||||||
|
|
||||||
|
P = F = 0
|
||||||
|
|
||||||
|
|
||||||
|
def check(name, ok):
|
||||||
|
global P, F
|
||||||
|
P, F = P + (1 if ok else 0), F + (0 if ok else 1)
|
||||||
|
print(f" [{'PASS' if ok else 'FAIL'}] {name}")
|
||||||
|
|
||||||
|
|
||||||
|
class FloodPort:
|
||||||
|
"""A line that never falls quiet: read_available always returns bytes, and
|
||||||
|
they contain a prompt. No identity ever completes."""
|
||||||
|
|
||||||
|
def flush_input(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def write(self, data):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def read_available(self, wait):
|
||||||
|
time.sleep(0.01) # a real read waits; keep the busy loop off a core
|
||||||
|
return b"+\x00\xff"
|
||||||
|
|
||||||
|
def read_exact(self, count, timeout):
|
||||||
|
raise pb.Error("no identity")
|
||||||
|
|
||||||
|
|
||||||
|
class LoaderPort:
|
||||||
|
"""A well-behaved pureboot 5: a prompt to the knock, then quiet, then the
|
||||||
|
slim identity (version 5 + m328p signature) and a closing prompt."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.pending = b""
|
||||||
|
self.exacts = 0
|
||||||
|
|
||||||
|
def flush_input(self):
|
||||||
|
self.pending = b""
|
||||||
|
|
||||||
|
def write(self, data):
|
||||||
|
if b"p" in data:
|
||||||
|
self.pending = b"+" # the prompt answers the knock, nothing else
|
||||||
|
|
||||||
|
def read_available(self, wait):
|
||||||
|
data, self.pending = self.pending, b""
|
||||||
|
return data
|
||||||
|
|
||||||
|
def read_exact(self, count, timeout):
|
||||||
|
self.exacts += 1
|
||||||
|
return b"\x05\x1e\x95\x0f" if self.exacts == 1 else b"+" # identity, then prompt
|
||||||
|
|
||||||
|
|
||||||
|
class StaleDTRPort:
|
||||||
|
"""`--stay`, then a fresh invocation on a board that resets when its port
|
||||||
|
opens. Three facts of that moment, all timed from the open: the previous
|
||||||
|
session's final prompt is still in transit and lands only after the
|
||||||
|
opening flush has already run; the reset holds the device off the line
|
||||||
|
at first, eating anything written before it completes; and the fresh
|
||||||
|
window is finite — once it expires the application boots and prints a
|
||||||
|
banner whose bytes are what a pending identity read collects. A
|
||||||
|
handshake that trusts the stale prompt spends the whole window waiting
|
||||||
|
on a device that never heard its knock; one that drains the line first
|
||||||
|
knocks into the real window and connects."""
|
||||||
|
|
||||||
|
STALE_AT = 0.02 # the leftover prompt becomes visible (post-flush)
|
||||||
|
READY_AT = 0.05 # reset complete, activation window opens
|
||||||
|
WINDOW = 1.0 # window length; expiry boots the application
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.t0 = time.monotonic()
|
||||||
|
# (visible-from, bytes): the line as a timed queue.
|
||||||
|
self.queue = [(self.t0 + self.STALE_AT, b"+")]
|
||||||
|
self.armed = False # a 'p' heard inside the window arms 'b'
|
||||||
|
self.booted = False
|
||||||
|
|
||||||
|
def _boot_check(self):
|
||||||
|
if not self.booted and time.monotonic() > self.t0 + self.READY_AT + self.WINDOW:
|
||||||
|
self.booted = True
|
||||||
|
self.queue.append((self.t0 + self.READY_AT + self.WINDOW,
|
||||||
|
b"W r libavr tempmon\r\n"))
|
||||||
|
|
||||||
|
def _visible(self):
|
||||||
|
self._boot_check()
|
||||||
|
now = time.monotonic()
|
||||||
|
return b"".join(d for t, d in self.queue if t <= now)
|
||||||
|
|
||||||
|
def _consume(self, n):
|
||||||
|
now = time.monotonic()
|
||||||
|
left = []
|
||||||
|
for t, d in self.queue:
|
||||||
|
if t <= now and n:
|
||||||
|
take = min(n, len(d))
|
||||||
|
d = d[take:]
|
||||||
|
n -= take
|
||||||
|
if d:
|
||||||
|
left.append((t, d))
|
||||||
|
self.queue = left
|
||||||
|
|
||||||
|
def flush_input(self):
|
||||||
|
self._consume(len(self._visible()))
|
||||||
|
|
||||||
|
def write(self, data):
|
||||||
|
self._boot_check()
|
||||||
|
now = time.monotonic()
|
||||||
|
if now < self.t0 + self.READY_AT or self.booted:
|
||||||
|
return # still in reset, or the application owns the line
|
||||||
|
if b"p" in data:
|
||||||
|
self.armed = True
|
||||||
|
self.queue.append((now + 0.01, b"+"))
|
||||||
|
if b"b" in data and self.armed:
|
||||||
|
# The slim identity (version 5 + m328p signature) and a prompt.
|
||||||
|
self.queue.append((now + 0.01, b"\x05\x1e\x95\x0f+"))
|
||||||
|
|
||||||
|
def read_available(self, wait):
|
||||||
|
deadline = time.monotonic() + wait
|
||||||
|
while True:
|
||||||
|
data = self._visible()
|
||||||
|
if data:
|
||||||
|
self._consume(len(data))
|
||||||
|
return data
|
||||||
|
if time.monotonic() >= deadline:
|
||||||
|
return b""
|
||||||
|
time.sleep(0.005)
|
||||||
|
|
||||||
|
def read_exact(self, count, timeout):
|
||||||
|
deadline = time.monotonic() + timeout
|
||||||
|
data = b""
|
||||||
|
while len(data) < count:
|
||||||
|
visible = self._visible()
|
||||||
|
if visible:
|
||||||
|
take = visible[:count - len(data)]
|
||||||
|
self._consume(len(take))
|
||||||
|
data += take
|
||||||
|
elif time.monotonic() >= deadline:
|
||||||
|
raise pb.Error(f"timeout: got {len(data)} of {count} bytes")
|
||||||
|
else:
|
||||||
|
time.sleep(0.005)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def terminates(port, wait, budget):
|
||||||
|
"""Run connect_autobaud in a thread; True if it returns/raises within
|
||||||
|
`budget` seconds rather than hanging."""
|
||||||
|
done = threading.Event()
|
||||||
|
|
||||||
|
def run():
|
||||||
|
try:
|
||||||
|
pb.Loader(port).connect_autobaud(wait)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
done.set()
|
||||||
|
|
||||||
|
threading.Thread(target=run, daemon=True).start()
|
||||||
|
return done.wait(budget)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# the hang: a flooding target must not spin the drain forever. With wait=0.5
|
||||||
|
# the whole handshake has to give up well inside a few seconds.
|
||||||
|
check("flooding target: handshake terminates, drain is bounded",
|
||||||
|
terminates(FloodPort(), wait=0.5, budget=4.0))
|
||||||
|
|
||||||
|
# the control: a real loader still connects and reads identity.
|
||||||
|
info = pb.Loader(LoaderPort()).connect_autobaud(2.0)
|
||||||
|
check("well-behaved loader still connects (version 5)", info.version == 5)
|
||||||
|
|
||||||
|
# the stale prompt: a --stay leftover plus reset-on-open must not burn the
|
||||||
|
# fresh window — the pre-knock drain absorbs it and the first real knock
|
||||||
|
# lands inside the window.
|
||||||
|
try:
|
||||||
|
stale_ok = pb.Loader(StaleDTRPort()).connect(2.5).version == 5
|
||||||
|
except pb.Error as failed:
|
||||||
|
print(f" ({failed})")
|
||||||
|
stale_ok = False
|
||||||
|
check("stale --stay prompt + reset-on-open: connects in the fresh window", stale_ok)
|
||||||
|
|
||||||
|
print(f"\n {P} passed, {F} failed")
|
||||||
|
return 1 if F else 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
71
test/test_scan.py
Normal file
71
test/test_scan.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""--scan's walk and report logic, no simulator: the probe order, the rate
|
||||||
|
arithmetic, and the advice's direction. The rate physics itself is not
|
||||||
|
sim-testable — a pty carries bytes at any termios rate — so what the wire
|
||||||
|
would arbitrate is pinned here as logic instead.
|
||||||
|
|
||||||
|
Usage: test_scan.py <tool_py>
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def fail(message):
|
||||||
|
print(f"FAIL: {message}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(sys.argv[1])))
|
||||||
|
import pureboot as pb
|
||||||
|
|
||||||
|
walk = pb.scan_ratios()
|
||||||
|
if walk != [0, -2, 2, -4, 4, -6, 6, -8, 8, -10, 10]:
|
||||||
|
fail(f"probe walk is not built-rate-first, nearest-out: {walk}")
|
||||||
|
|
||||||
|
if pb.scan_rate(9600, 4) != 9984 or pb.scan_rate(9600, -4) != 9216:
|
||||||
|
fail("probe rate arithmetic")
|
||||||
|
if pb.scan_rate(115200, 0) != 115200:
|
||||||
|
fail("the built rate must probe unchanged")
|
||||||
|
|
||||||
|
# A loader answering fast means a fast oscillator: the trim goes down.
|
||||||
|
report = "\n".join(pb.scan_report(9600, 4, 6))
|
||||||
|
for needle in ("9984", "+4 %", "--baud 9984", "4 steps lower", "pureboot 6"):
|
||||||
|
if needle not in report:
|
||||||
|
fail(f"+4 % report lacks {needle!r}:\n{report}")
|
||||||
|
report = "\n".join(pb.scan_report(9600, -6, 6))
|
||||||
|
if "6 steps higher" not in report:
|
||||||
|
fail(f"-6 % report advises the wrong direction:\n{report}")
|
||||||
|
|
||||||
|
report = "\n".join(pb.scan_report(9600, 0, 6))
|
||||||
|
if "none" not in report or "steps" in report:
|
||||||
|
fail(f"an on-rate answer must advise no trim:\n{report}")
|
||||||
|
|
||||||
|
report = "\n".join(pb.scan_report(9600, 4, 6, clock=9600000))
|
||||||
|
if "9984000" not in report:
|
||||||
|
fail(f"the absolute clock must scale with the found ratio:\n{report}")
|
||||||
|
|
||||||
|
# The walk's rates mostly have no termios B-constant, so the POSIX port
|
||||||
|
# must set them through termios2 — probed on a pty, which accepts the
|
||||||
|
# ioctl without caring about the speed. Without this every off-nominal
|
||||||
|
# probe would abort the walk on the platform --scan matters most on.
|
||||||
|
if os.name == "posix":
|
||||||
|
import pty
|
||||||
|
|
||||||
|
master, slave = pty.openpty()
|
||||||
|
try:
|
||||||
|
port = pb.Port(os.ttyname(slave), pb.scan_rate(9600, 4))
|
||||||
|
port.set_baud(pb.scan_rate(9600, -4))
|
||||||
|
port.close()
|
||||||
|
except pb.Error as error:
|
||||||
|
fail(f"PosixPort refused an off-nominal probe rate: {error}")
|
||||||
|
finally:
|
||||||
|
os.close(master)
|
||||||
|
os.close(slave)
|
||||||
|
|
||||||
|
print("OK")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
167
test/test_update_link.py
Executable file
167
test/test_update_link.py
Executable file
@@ -0,0 +1,167 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Self-update across a link change: the host must follow the staging copy.
|
||||||
|
|
||||||
|
`--update-loader` installs the new image in the staging slot and then *enters
|
||||||
|
it* to have it rewrite the resident. That copy is the new image, so it speaks the
|
||||||
|
new image's baud and backend — but the host was talking to the *resident*. Where
|
||||||
|
the two differ, the host kept knocking at the old rate in the old mode, the
|
||||||
|
staging copy never answered, and the update stranded: staging installed, resident
|
||||||
|
untouched, and on a 1 KiB tiny the application region (which *is* the staging
|
||||||
|
slot there) already gone.
|
||||||
|
|
||||||
|
The wire cannot be probed for this — 512 bytes of position-independent code carry
|
||||||
|
no header saying what rate they were built for — so the operator declares it, and
|
||||||
|
a mismatch with nothing declared has to say so instead of reporting a bare
|
||||||
|
timeout.
|
||||||
|
|
||||||
|
Stdlib only, no device: host-tool logic, so it runs on every chip's preset beside
|
||||||
|
pureboot.planner.
|
||||||
|
"""
|
||||||
|
import importlib.util
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
PB = pathlib.Path(__file__).resolve().parents[1] / "pureboot" / "pureboot.py"
|
||||||
|
_spec = importlib.util.spec_from_file_location("pureboot", PB)
|
||||||
|
pb = importlib.util.module_from_spec(_spec)
|
||||||
|
_spec.loader.exec_module(pb)
|
||||||
|
|
||||||
|
IDENTITY = b"\x05\x1e\x95\x0f" # pureboot 5 + m328p signature
|
||||||
|
P = F = 0
|
||||||
|
|
||||||
|
|
||||||
|
def check(name, ok, detail=""):
|
||||||
|
global P, F
|
||||||
|
P, F = P + (1 if ok else 0), F + (0 if ok else 1)
|
||||||
|
print(f" [{'PASS' if ok else 'FAIL'}] {name}" + (f" — {detail}" if detail else ""))
|
||||||
|
|
||||||
|
|
||||||
|
class TwoLinkPort:
|
||||||
|
"""A board whose resident and staging copy answer on different links.
|
||||||
|
|
||||||
|
Only the rate currently set decides who can be heard, which is the physical
|
||||||
|
truth: a loader's bit timing is a cycle count, so a copy built for another
|
||||||
|
rate is unreadable until the host retunes. The knock bytes carry the mode, so
|
||||||
|
a backend mismatch is caught the same way.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, resident=(57600, False), staged=(38400, False)):
|
||||||
|
self.resident, self.staged = resident, staged
|
||||||
|
self.baud = resident[0]
|
||||||
|
self.entered = False # a 'J' has handed control to the staging copy
|
||||||
|
self.switches = [] # every retune the host asked for
|
||||||
|
self.pending = bytearray() # what the device has queued to send
|
||||||
|
|
||||||
|
# --- the part under test needs this to exist at all
|
||||||
|
def set_baud(self, baud):
|
||||||
|
self.baud = baud
|
||||||
|
self.switches.append(baud)
|
||||||
|
|
||||||
|
def flush_input(self):
|
||||||
|
self.pending.clear()
|
||||||
|
|
||||||
|
def _audible(self, knock=None):
|
||||||
|
baud, autobaud = self.staged if self.entered else self.resident
|
||||||
|
if self.baud != baud:
|
||||||
|
return False
|
||||||
|
if knock is None:
|
||||||
|
return True
|
||||||
|
return knock == (bytes((pb.CALIBRATE, ord("p"))) if autobaud else b"pb")
|
||||||
|
|
||||||
|
def write(self, data):
|
||||||
|
data = bytes(data)
|
||||||
|
if data[:1] == b"J" and len(data) == 3:
|
||||||
|
# The resident acks the jump, then control moves to the copy.
|
||||||
|
if self._audible():
|
||||||
|
self.pending += pb.PROMPT
|
||||||
|
self.entered = True
|
||||||
|
elif data in (b"pb", bytes((pb.CALIBRATE, ord("p")))):
|
||||||
|
if self._audible(data):
|
||||||
|
self.pending += pb.PROMPT
|
||||||
|
elif data == b"b":
|
||||||
|
if self._audible():
|
||||||
|
self.pending += IDENTITY + pb.PROMPT
|
||||||
|
|
||||||
|
def read_available(self, wait):
|
||||||
|
out, self.pending = bytes(self.pending), bytearray()
|
||||||
|
return out
|
||||||
|
|
||||||
|
def read_exact(self, count, timeout):
|
||||||
|
if len(self.pending) < count:
|
||||||
|
raise pb.Error(f"timeout: got {len(self.pending)} of {count} bytes")
|
||||||
|
out, self.pending = bytes(self.pending[:count]), self.pending[count:]
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def connected(port):
|
||||||
|
"""A Loader already in session with the resident."""
|
||||||
|
loader = pb.Loader(port)
|
||||||
|
loader.connect(2.0)
|
||||||
|
return loader
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# The control first: where the staged image keeps the resident's link, the
|
||||||
|
# flow works and needs no retune. This is the case that always passed, and
|
||||||
|
# it is what made the bug look like "self-update is broken" rather than
|
||||||
|
# "self-update cannot change the link".
|
||||||
|
port = TwoLinkPort(resident=(57600, False), staged=(57600, False))
|
||||||
|
loader = connected(port)
|
||||||
|
try:
|
||||||
|
loader.enter_copy(0x7C00, 2.0)
|
||||||
|
check("same link: staging copy entered", True)
|
||||||
|
except pb.Error as error:
|
||||||
|
check("same link: staging copy entered", False, str(error))
|
||||||
|
|
||||||
|
# A baud change, declared. The host must retune before knocking.
|
||||||
|
port = TwoLinkPort(resident=(57600, False), staged=(38400, False))
|
||||||
|
loader = connected(port)
|
||||||
|
try:
|
||||||
|
loader.enter_copy(0x7C00, 2.0, link=(38400, False))
|
||||||
|
check("baud change declared: entered after retuning", 38400 in port.switches,
|
||||||
|
f"switches={port.switches}")
|
||||||
|
except (pb.Error, TypeError) as error:
|
||||||
|
check("baud change declared: entered after retuning", False, repr(error))
|
||||||
|
|
||||||
|
# A backend change, declared: the knock itself has to become the calibration
|
||||||
|
# pulse, or an autobaud staging copy never hears a thing.
|
||||||
|
port = TwoLinkPort(resident=(57600, False), staged=(57600, True))
|
||||||
|
loader = connected(port)
|
||||||
|
try:
|
||||||
|
loader.enter_copy(0x7C00, 2.0, link=(57600, True))
|
||||||
|
check("backend change declared: entered as autobaud", True)
|
||||||
|
except (pb.Error, TypeError) as error:
|
||||||
|
check("backend change declared: entered as autobaud", False, repr(error))
|
||||||
|
|
||||||
|
# Nothing declared against a changed link: it still cannot work, but the
|
||||||
|
# error has to name the cause. A bare "no answer" sent the operator looking
|
||||||
|
# at the wiring while the application region sat erased.
|
||||||
|
port = TwoLinkPort(resident=(57600, False), staged=(38400, False))
|
||||||
|
loader = connected(port)
|
||||||
|
try:
|
||||||
|
loader.enter_copy(0x7C00, 0.3)
|
||||||
|
check("undeclared mismatch: reported", False, "unexpectedly succeeded")
|
||||||
|
except pb.Error as error:
|
||||||
|
text = str(error).lower()
|
||||||
|
check("undeclared mismatch: error names the link, not just a timeout",
|
||||||
|
"link" in text or "baud" in text or "backend" in text, str(error))
|
||||||
|
except TypeError as error:
|
||||||
|
check("undeclared mismatch: error names the link, not just a timeout",
|
||||||
|
False, repr(error))
|
||||||
|
|
||||||
|
# The resident's own link must be restored for the caller: a declared
|
||||||
|
# staging link is for the copy, and the tool talks to the new resident after.
|
||||||
|
port = TwoLinkPort(resident=(57600, False), staged=(38400, False))
|
||||||
|
loader = connected(port)
|
||||||
|
try:
|
||||||
|
loader.enter_copy(0x7C00, 2.0, link=(38400, False))
|
||||||
|
check("session records the link it is now speaking", loader.baud == 38400,
|
||||||
|
f"loader.baud={getattr(loader, 'baud', None)}")
|
||||||
|
except (pb.Error, TypeError, AttributeError) as error:
|
||||||
|
check("session records the link it is now speaking", False, repr(error))
|
||||||
|
|
||||||
|
print(f"\n {P} passed, {F} failed")
|
||||||
|
return 1 if F else 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -3,8 +3,8 @@
|
|||||||
# the simulator-driven protocol suites. --full adds the reflect-spot builds
|
# the simulator-driven protocol suites. --full adds the reflect-spot builds
|
||||||
# (libavr's rule: reflect compiles are bounded to its spot set, never the
|
# (libavr's rule: reflect compiles are bounded to its spot set, never the
|
||||||
# full matrix) and swaps the compact size matrix for the exhaustive
|
# full matrix) and swaps the compact size matrix for the exhaustive
|
||||||
# clock × baud × backend cross product. LIBAVR_ROOT must point at the libavr
|
# clock × baud × backend cross product. libavr resolves from the `libavr/`
|
||||||
# checkout.
|
# submodule; LIBAVR_ROOT overrides it for a working tree.
|
||||||
set -e
|
set -e
|
||||||
cd "$(dirname "$0")/.."
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
@@ -36,4 +36,10 @@ if ((full)); then
|
|||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Every tree is freshly built now — the one moment the README's size table
|
||||||
|
# can be held to what the images measure (a per-preset ctest sees only its
|
||||||
|
# own chip; the table needs all of them, and ungated it drifts: a
|
||||||
|
# common-code shave moves every row at once with nothing over budget).
|
||||||
|
python3 tools/sizes.py check-readme
|
||||||
|
|
||||||
echo "check: every chip green"
|
echo "check: every chip green"
|
||||||
|
|||||||
@@ -7,11 +7,14 @@ port's TUs compile identically; the sims prove nothing new there) exist for
|
|||||||
libavr's reflect spot set only, mirroring its rule: the full reflect matrix
|
libavr's reflect spot set only, mirroring its rule: the full reflect matrix
|
||||||
is never built, one chip per hardware class and pack vintage is.
|
is never built, one chip per hardware class and pack vintage is.
|
||||||
|
|
||||||
Run from the repo root: tools/make_presets.py
|
Run from the repo root: tools/make_presets.py — or with --check, which
|
||||||
|
verifies the committed file matches this generator and edits nothing (the
|
||||||
|
ctest entry `presets.generated` runs that, so drift reds the gate).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
CHIPS = [
|
CHIPS = [
|
||||||
"attiny13", "attiny13a", "attiny25", "attiny45", "attiny85",
|
"attiny13", "attiny13a", "attiny25", "attiny45", "attiny85",
|
||||||
@@ -41,7 +44,7 @@ def main():
|
|||||||
"hidden": True,
|
"hidden": True,
|
||||||
"generator": "Ninja",
|
"generator": "Ninja",
|
||||||
"binaryDir": "${sourceDir}/build/${presetName}",
|
"binaryDir": "${sourceDir}/build/${presetName}",
|
||||||
"toolchainFile": "$env{LIBAVR_ROOT}/cmake/avr-toolchain.cmake",
|
"toolchainFile": "${sourceDir}/libavr/cmake/avr-toolchain.cmake",
|
||||||
"cacheVariables": {
|
"cacheVariables": {
|
||||||
"CMAKE_BUILD_TYPE": "Release",
|
"CMAKE_BUILD_TYPE": "Release",
|
||||||
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
|
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
|
||||||
@@ -72,6 +75,9 @@ def main():
|
|||||||
for chip in REFLECT_SPOT:
|
for chip in REFLECT_SPOT:
|
||||||
add(chip, "reflect")
|
add(chip, "reflect")
|
||||||
|
|
||||||
|
# CMake rejects unknown fields in the presets root, $comment included, so
|
||||||
|
# the file cannot carry a generated-file marker; the --check ctest is the
|
||||||
|
# whole of rule 10's guard here.
|
||||||
presets = {
|
presets = {
|
||||||
"version": 8,
|
"version": 8,
|
||||||
"configurePresets": configure,
|
"configurePresets": configure,
|
||||||
@@ -79,12 +85,19 @@ def main():
|
|||||||
"testPresets": test,
|
"testPresets": test,
|
||||||
"workflowPresets": workflows,
|
"workflowPresets": workflows,
|
||||||
}
|
}
|
||||||
|
rendered = json.dumps(presets, indent=1) + "\n"
|
||||||
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "CMakePresets.json")
|
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "CMakePresets.json")
|
||||||
|
if "--check" in sys.argv[1:]:
|
||||||
|
current = open(path).read() if os.path.exists(path) else ""
|
||||||
|
if current != rendered:
|
||||||
|
print("CMakePresets.json does not match its generator — run tools/make_presets.py")
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
with open(path, "w") as f:
|
with open(path, "w") as f:
|
||||||
json.dump(presets, f, indent=1)
|
f.write(rendered)
|
||||||
f.write("\n")
|
|
||||||
print(f"{len(CHIPS)} chips, {len(REFLECT_SPOT)} reflect: {os.path.normpath(path)}")
|
print(f"{len(CHIPS)} chips, {len(REFLECT_SPOT)} reflect: {os.path.normpath(path)}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
sys.exit(main())
|
||||||
|
|||||||
377
tools/pbhw.py
Executable file
377
tools/pbhw.py
Executable file
@@ -0,0 +1,377 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Hardware acceptance suite for a pureboot deployment.
|
||||||
|
|
||||||
|
`tools/check.sh` proves the protocol under simavr on every chip. This proves one
|
||||||
|
*board*: that the loader actually installed on it answers, that the memories
|
||||||
|
round-trip over the real link, that the application it flashes runs afterwards,
|
||||||
|
and that the refusals which keep a 512-byte slot alive still fire. Run it once
|
||||||
|
when a board is brought up, and again whenever the deployment moves — a new
|
||||||
|
clock, a new backend, new pins.
|
||||||
|
|
||||||
|
Every check derives its bounds from the info block the loader itself reports, so
|
||||||
|
nothing here is per-chip: the same run covers a 1 KiB tiny whose application
|
||||||
|
region is 510 usable bytes and a 128 KiB mega whose flash needs a bank in the
|
||||||
|
selector.
|
||||||
|
|
||||||
|
**This overwrites the board's application flash and EEPROM.** Capture them first
|
||||||
|
with `pbrig.py backup`, which verifies what it captured.
|
||||||
|
|
||||||
|
tools/pbhw.py --programmer atmelice_isp --part t13 --port COM6 \
|
||||||
|
--autobaud --loader build/ab.bin --app build/pbapp.hex \
|
||||||
|
--marker APP
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import pathlib
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
|
||||||
|
import pbrig # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
class Suite:
|
||||||
|
def __init__(self, rig: pbrig.Rig, work: pathlib.Path):
|
||||||
|
self.rig = rig
|
||||||
|
self.work = work
|
||||||
|
self.results: list[tuple[str, bool, str]] = []
|
||||||
|
|
||||||
|
def check(self, name: str, ok: bool, detail: str = "") -> bool:
|
||||||
|
self.results.append((name, ok, detail))
|
||||||
|
print(f" {'PASS' if ok else 'FAIL'} {name}" + (f" {detail}" if detail else ""))
|
||||||
|
return ok
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _brief(text: str, limit: int = 78) -> str:
|
||||||
|
return " | ".join(l.strip() for l in text.splitlines() if l.strip())[:limit]
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------- checks
|
||||||
|
|
||||||
|
def identity(self) -> object | None:
|
||||||
|
"""The info block, which every later check takes its bounds from."""
|
||||||
|
module = pbrig.load_pureboot(self.rig.d.pureboot)
|
||||||
|
self.rig.reset()
|
||||||
|
port = self.rig.open_port() # wrapped for the echo where the line is shared
|
||||||
|
try:
|
||||||
|
loader = module.Loader(port)
|
||||||
|
if self.rig.d.autobaud:
|
||||||
|
loader.connect_autobaud(self.rig.d.wait)
|
||||||
|
else:
|
||||||
|
loader.connect(self.rig.d.wait)
|
||||||
|
info = loader.info
|
||||||
|
self.check("identity read", True, info.describe())
|
||||||
|
return info
|
||||||
|
except Exception as error: # noqa: BLE001 — a dead link is a result
|
||||||
|
self.check("identity read", False, str(error)[:70])
|
||||||
|
return None
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
port.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def scan(self) -> None:
|
||||||
|
"""The --scan walk against real termios and a real oscillator: every
|
||||||
|
probe rate must open a port (the off-nominal rates exist only through
|
||||||
|
termios2), and one probe must answer — the nominal on a healthy board,
|
||||||
|
a neighbor on a drifted one. The rig injects the one reset per probe
|
||||||
|
the operator supplies in the field; this is the rate physics the
|
||||||
|
simulator cannot arbitrate (a pty carries bytes at any rate), pinned
|
||||||
|
on silicon."""
|
||||||
|
module = pbrig.load_pureboot(self.rig.d.pureboot)
|
||||||
|
found = None
|
||||||
|
try:
|
||||||
|
for pct in module.scan_ratios():
|
||||||
|
rate = module.scan_rate(self.rig.d.baud, pct)
|
||||||
|
self.rig.reset()
|
||||||
|
try:
|
||||||
|
# Same wrap as identity(): on a shared line an undiscarded
|
||||||
|
# echo answers every rate a scan probes, so the walk would
|
||||||
|
# report the first one it tried.
|
||||||
|
port = self.rig.open_port(rate)
|
||||||
|
except module.Error as error:
|
||||||
|
self.check("scan opens every probe rate", False, f"{rate} Bd: {error}")
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
module.Loader(port).connect(min(self.rig.d.wait, 6.0))
|
||||||
|
found = pct
|
||||||
|
break
|
||||||
|
except module.Error:
|
||||||
|
continue
|
||||||
|
finally:
|
||||||
|
port.close()
|
||||||
|
except Exception as error: # noqa: BLE001 — a rig hiccup is a result
|
||||||
|
self.check("scan walks the probe ladder", False, str(error)[:70])
|
||||||
|
return
|
||||||
|
self.check("scan finds the board's rate", found is not None,
|
||||||
|
"no probe answered" if found is None else f"{found:+d} % of {self.rig.d.baud} Bd")
|
||||||
|
|
||||||
|
def eeprom(self, info) -> None:
|
||||||
|
size = info.eeprom_size
|
||||||
|
if not size:
|
||||||
|
print(" skip EEPROM (this part has none)")
|
||||||
|
return
|
||||||
|
# A pattern no erase or partial write could produce by accident.
|
||||||
|
pattern = bytes((i * 7 + 3) & 0xFF for i in range(size))
|
||||||
|
image = self.work / "ee.bin"
|
||||||
|
image.write_bytes(pattern)
|
||||||
|
|
||||||
|
rc, out = self.rig.pureboot("--eeprom", str(image), "--verify-eeprom", str(image))
|
||||||
|
self.check(f"EEPROM write + verify ({size} B)", rc == 0, self._brief(out))
|
||||||
|
|
||||||
|
back = self.work / "ee-back.bin"
|
||||||
|
rc, out = self.rig.pureboot("--read-eeprom", str(back))
|
||||||
|
got = back.read_bytes() if back.exists() else b""
|
||||||
|
self.check("EEPROM reads back what was written", got == pattern, f"{len(got)} B")
|
||||||
|
|
||||||
|
self.rig.pureboot("--erase-eeprom")
|
||||||
|
erased = self.work / "ee-erased.bin"
|
||||||
|
self.rig.pureboot("--read-eeprom", str(erased))
|
||||||
|
got = erased.read_bytes() if erased.exists() else b""
|
||||||
|
self.check("EEPROM erase leaves 0xff", got == b"\xff" * size, f"{len(got)} B")
|
||||||
|
|
||||||
|
def application(self, info, app: pathlib.Path, marker: str,
|
||||||
|
marker_wait: float = 2.5) -> None:
|
||||||
|
rc, out = self.rig.pureboot("--flash", str(app), "--verify-flash", str(app))
|
||||||
|
self.check(f"application flash + verify ({app.name})", rc == 0, self._brief(out))
|
||||||
|
|
||||||
|
if marker:
|
||||||
|
# The tool hands over as it ends its session, so the application is
|
||||||
|
# already running — but only on a board whose DTR is unwired, where
|
||||||
|
# opening a port simply listens. Where DTR *is* wired to reset (an
|
||||||
|
# Arduino, most USB-serial dev boards), this open resets the part
|
||||||
|
# and the activation window comes first, so a marker emitted once at
|
||||||
|
# startup happens on the far side of a wait this cannot know the
|
||||||
|
# length of: the window is a compile-time constant and nothing on
|
||||||
|
# the wire reports it. Hence --marker-wait, and a fixture that
|
||||||
|
# repeats its banner (PUREBOOT_HEARTBEAT) rather than saying it once.
|
||||||
|
data = self.rig.capture(seconds=marker_wait)
|
||||||
|
seen = marker.encode() in data
|
||||||
|
sample = "".join(chr(b) if 32 <= b < 127 else "." for b in data[:40])
|
||||||
|
self.check(f"application runs (emits {marker!r})", seen,
|
||||||
|
f"|{sample}|" if seen or data else
|
||||||
|
f"nothing in {marker_wait:g} s — if this board resets when its port "
|
||||||
|
f"opens, that wait has to outlast the activation window")
|
||||||
|
|
||||||
|
back = self.work / "app-back.bin"
|
||||||
|
rc, out = self.rig.pureboot("--read-flash", str(back))
|
||||||
|
got = back.read_bytes() if back.exists() else b""
|
||||||
|
self.check("application flash reads back", rc == 0 and len(got) == info.base,
|
||||||
|
f"{len(got)} B of {info.base}")
|
||||||
|
|
||||||
|
def _witness(self, info, slot_length: int):
|
||||||
|
"""Read back the erased region and the loader slot: (erased, slot, how).
|
||||||
|
|
||||||
|
Prefers ISP, because an independent reader is the only one that can
|
||||||
|
testify about a loader just asked to erase around itself. Where no
|
||||||
|
programmer is attached the link answers instead — which is weaker for
|
||||||
|
exactly the reason it is worth having, a destroyed loader being unable
|
||||||
|
to report anything at all. The two are never printed under one word:
|
||||||
|
an absent probe is a fact about the bench, a wrong byte is a verdict on
|
||||||
|
the loader, and a check that conflates them stops being read.
|
||||||
|
"""
|
||||||
|
limit = info.base - 2 if info.patch_vector else info.base
|
||||||
|
whole = self.work / "whole.bin"
|
||||||
|
if self.rig.read_memory("flash", whole, "r"):
|
||||||
|
image = whole.read_bytes()
|
||||||
|
image += b"\xff" * (info.flash_size - len(image))
|
||||||
|
return image[0:limit], image[info.base:info.base + slot_length], "ISP"
|
||||||
|
|
||||||
|
module = pbrig.load_pureboot(self.rig.d.pureboot)
|
||||||
|
port = self.rig.open_port()
|
||||||
|
try:
|
||||||
|
loader = module.Loader(port)
|
||||||
|
if self.rig.d.autobaud:
|
||||||
|
loader.connect_autobaud(self.rig.d.wait)
|
||||||
|
else:
|
||||||
|
loader.connect(self.rig.d.wait)
|
||||||
|
return (loader.read_flash(0, limit),
|
||||||
|
loader.read_flash(info.base, slot_length),
|
||||||
|
"the link, no probe attached — the loader's own account")
|
||||||
|
except Exception as error: # noqa: BLE001 — a dead link is a result
|
||||||
|
print(f" skip slot checks: no programmer, and the link did not "
|
||||||
|
f"answer either ({str(error)[:60]})")
|
||||||
|
return None, None, ""
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
port.close()
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
pass
|
||||||
|
|
||||||
|
def erase_and_slot(self, info, loader_image: pathlib.Path | None) -> None:
|
||||||
|
rc, out = self.rig.pureboot("--erase-flash")
|
||||||
|
self.check("application region erases", rc == 0, self._brief(out))
|
||||||
|
|
||||||
|
want = loader_image.read_bytes() if loader_image and loader_image.exists() else b""
|
||||||
|
erased, slot, how = self._witness(info, len(want))
|
||||||
|
if erased is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Erased application flash, up to the trampoline word the host composes
|
||||||
|
# on a patched-vector part.
|
||||||
|
limit = info.base - 2 if info.patch_vector else info.base
|
||||||
|
self.check("erased application region is 0xff",
|
||||||
|
set(erased) <= {0xFF}, f"0x0000..{limit:#06x} via {how}")
|
||||||
|
|
||||||
|
if want:
|
||||||
|
self.check("loader slot survives the erase", slot == want,
|
||||||
|
f"{len(want)} B at {info.base:#06x} via {how}")
|
||||||
|
else:
|
||||||
|
print(" skip loader slot comparison (pass --loader <image.bin>)")
|
||||||
|
|
||||||
|
def seal(self, info, rounds: int = 1) -> None:
|
||||||
|
"""The seal, adversarially, over the real link.
|
||||||
|
|
||||||
|
pureboot 9 has no running-slot guard: what stops a mangled command from
|
||||||
|
erasing the loader is the seal and nothing else. So this aims the worst
|
||||||
|
command the protocol has — an SPM erase at the loader's own first page —
|
||||||
|
and damages one header byte at a time. Every one must come back NAK with
|
||||||
|
the slot untouched and the session still in step.
|
||||||
|
|
||||||
|
On a board whose link drops or mangles bytes of its own accord this is
|
||||||
|
also the stress test: `--seal-rounds` repeats it, and a link fault
|
||||||
|
during a round is indistinguishable to the loader from the damage being
|
||||||
|
injected, which is the point.
|
||||||
|
"""
|
||||||
|
module = pbrig.load_pureboot(self.rig.d.pureboot)
|
||||||
|
self.rig.reset()
|
||||||
|
port = self.rig.open_port()
|
||||||
|
try:
|
||||||
|
loader = module.Loader(port)
|
||||||
|
if self.rig.d.autobaud:
|
||||||
|
loader.connect_autobaud(self.rig.d.wait)
|
||||||
|
else:
|
||||||
|
loader.connect(self.rig.d.wait)
|
||||||
|
if loader.info.version < module.SEALED_LOADER:
|
||||||
|
print(f" skip seal checks (loader is pureboot {loader.info.version})")
|
||||||
|
return
|
||||||
|
|
||||||
|
head_of = lambda dmg: self._sealed(module, module.OP_WRITE, module.SP_SPM,
|
||||||
|
info.base, module.SPM_ERASE, dmg)
|
||||||
|
refused = 0
|
||||||
|
attempts = 0
|
||||||
|
for _ in range(rounds):
|
||||||
|
for index in range(6):
|
||||||
|
attempts += 1
|
||||||
|
port.write(head_of((index, 0x01)))
|
||||||
|
verdict = port.read_exact(1, 5.0)
|
||||||
|
if verdict != module.NAK:
|
||||||
|
self.check(f"damaged byte {index} refused", False,
|
||||||
|
f"verdict {verdict.hex()}")
|
||||||
|
return
|
||||||
|
if port.read_exact(1, 5.0) != module.PROMPT:
|
||||||
|
self.check(f"re-prompt after byte {index}", False, "no prompt")
|
||||||
|
return
|
||||||
|
refused += 1
|
||||||
|
self.check("damaged headers refused", refused == attempts,
|
||||||
|
f"{refused}/{attempts}, every header byte")
|
||||||
|
self.check("session still in step", loader.identity().raw == info.raw)
|
||||||
|
|
||||||
|
# And the slot itself, read back over the link: the loader is the
|
||||||
|
# thing that would have been erased, so its own account of its
|
||||||
|
# first bytes is a real witness — an erased page reads all 0xff.
|
||||||
|
head = loader.read_flash(info.base, 16)
|
||||||
|
self.check("loader slot intact", set(head) != {0xFF}, head[:8].hex())
|
||||||
|
except Exception as error: # noqa: BLE001 — a dead link is a result
|
||||||
|
self.check("seal checks", False, str(error)[:70])
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
port.close()
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
pass
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _sealed(module, op, space, address, count, damage=None):
|
||||||
|
"""A sealed header, damaged after sealing — the shape a link fault has."""
|
||||||
|
head = bytearray((op, module.selector(space, address), address & 0xFF,
|
||||||
|
(address >> 8) & 0xFF, count & 0xFF))
|
||||||
|
seal = module.SEAL
|
||||||
|
for byte in head:
|
||||||
|
seal ^= byte
|
||||||
|
out = bytearray(head + bytes((seal,)))
|
||||||
|
if damage:
|
||||||
|
out[damage[0]] ^= damage[1]
|
||||||
|
return bytes(out)
|
||||||
|
|
||||||
|
def refusals(self, info) -> None:
|
||||||
|
# One word too many: a patched-vector part spends the slot's last word
|
||||||
|
# on the trampoline, so its application stops two bytes short.
|
||||||
|
limit = info.base - 2 if info.patch_vector else info.base
|
||||||
|
oversized = self.work / "oversized.bin"
|
||||||
|
oversized.write_bytes(bytes(limit + 2))
|
||||||
|
rc, out = self.rig.pureboot("--flash", str(oversized))
|
||||||
|
self.check(f"image over {limit} B refused", rc != 0, self._brief(out))
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------- run
|
||||||
|
|
||||||
|
def run(self, app: pathlib.Path | None, loader_image: pathlib.Path | None,
|
||||||
|
marker: str, marker_wait: float = 2.5, seal_rounds: int = 1) -> int:
|
||||||
|
print("identity")
|
||||||
|
info = self.identity()
|
||||||
|
if info is None:
|
||||||
|
print("\nthe loader never answered; nothing below can be trusted")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if not self.rig.d.autobaud:
|
||||||
|
print("\nscan")
|
||||||
|
self.scan()
|
||||||
|
|
||||||
|
print("\nEEPROM")
|
||||||
|
self.eeprom(info)
|
||||||
|
|
||||||
|
if app:
|
||||||
|
print("\napplication")
|
||||||
|
self.application(info, app, marker, marker_wait)
|
||||||
|
else:
|
||||||
|
print("\nskip application checks (pass --app <image.hex>)")
|
||||||
|
|
||||||
|
print("\nerase and the slot boundary")
|
||||||
|
self.erase_and_slot(info, loader_image)
|
||||||
|
|
||||||
|
print("\nthe seal")
|
||||||
|
self.seal(info, seal_rounds)
|
||||||
|
|
||||||
|
print("\nrefusals")
|
||||||
|
self.refusals(info)
|
||||||
|
|
||||||
|
passed = sum(1 for _, ok, _ in self.results if ok)
|
||||||
|
print(f"\n{passed}/{len(self.results)} passed")
|
||||||
|
return 0 if passed == len(self.results) else 1
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="hardware acceptance suite for one pureboot deployment",
|
||||||
|
epilog="overwrites the board's application flash and EEPROM — back them up first")
|
||||||
|
pbrig.Deployment.add_arguments(parser)
|
||||||
|
parser.add_argument("--app", type=pathlib.Path,
|
||||||
|
help="application image to flash (test/pbapp.cpp built for this deployment)")
|
||||||
|
parser.add_argument("--loader", type=pathlib.Path,
|
||||||
|
help="the resident loader's .bin, to prove the slot survives an erase")
|
||||||
|
parser.add_argument("--seal-rounds", type=int, default=1,
|
||||||
|
help="repeat the adversarial seal sweep N times (a lossy board's stress test)")
|
||||||
|
parser.add_argument("--marker", default="",
|
||||||
|
help="text the application emits when it runs, e.g. APP")
|
||||||
|
parser.add_argument("--marker-wait", type=float, default=2.5,
|
||||||
|
help="seconds to listen for it. On a board whose DTR is wired to "
|
||||||
|
"reset, opening the port resets the part, so this must outlast "
|
||||||
|
"the activation window (default 2.5)")
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
rig = pbrig.Rig(pbrig.Deployment.from_args(args))
|
||||||
|
print(f"rig: {args.part} on {args.programmer}, link {args.port} at {args.baud} Bd"
|
||||||
|
f"{' (autobaud)' if args.autobaud else ''}")
|
||||||
|
print("this overwrites the application flash and EEPROM\n")
|
||||||
|
with tempfile.TemporaryDirectory(prefix="pbhw-") as temporary:
|
||||||
|
return Suite(rig, pathlib.Path(temporary)).run(args.app, args.loader, args.marker,
|
||||||
|
args.marker_wait, args.seal_rounds)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
sys.exit(main())
|
||||||
|
except pbrig.Error as error:
|
||||||
|
print(f"error: {error}", file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
450
tools/pbrig.py
Executable file
450
tools/pbrig.py
Executable file
@@ -0,0 +1,450 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Hardware rig driver for pureboot: an ISP programmer beside a serial link.
|
||||||
|
|
||||||
|
The simulated suites (`test/pb*.py`) prove the protocol; this drives the same
|
||||||
|
loader on real silicon, where the things a cycle-exact simulator cannot model
|
||||||
|
live — an RC oscillator off its nominal, a reset edge that has to come from
|
||||||
|
somewhere, a serial bridge with its own idea of what a baud is.
|
||||||
|
|
||||||
|
Nothing here knows a port name, a part or a programmer. Every deployment fact
|
||||||
|
arrives from the command line or the environment, so the same script serves any
|
||||||
|
board: see `Deployment`. As a module it is the reset/flash/talk primitives that
|
||||||
|
`pbhw.py` builds its acceptance suite from; as a command it is the handful of
|
||||||
|
one-shot operations worth having on a rig — most importantly `backup`, which is
|
||||||
|
the only thing standing between a fuse experiment and an unrecoverable part.
|
||||||
|
|
||||||
|
Two rig facts are encoded here because they are not guessable and cost a
|
||||||
|
session each to learn:
|
||||||
|
|
||||||
|
* **An ISP access resets the part**, and it runs again the moment the programmer
|
||||||
|
releases it. That is the only reset edge available when the serial adapter's
|
||||||
|
DTR is not wired to reset — so a loader session begins with an ISP touch and
|
||||||
|
knocks immediately after, which is what `Rig.pureboot()` does.
|
||||||
|
* **avrdude splits `-U memory:op:file:format` on colons**, so a Windows path's
|
||||||
|
drive letter breaks the spec. Every file argument is therefore passed as a
|
||||||
|
bare filename with avrdude run in that file's own directory.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import dataclasses
|
||||||
|
import importlib.util
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
HERE = pathlib.Path(__file__).resolve().parent
|
||||||
|
DEFAULT_PUREBOOT = HERE.parent / "pureboot" / "pureboot.py"
|
||||||
|
|
||||||
|
# Memories worth capturing before an experiment, and the format each is read in.
|
||||||
|
# Fuses and lock are per-part: a part without an extended fuse simply fails that
|
||||||
|
# one read, which `backup` reports and steps over rather than aborting on.
|
||||||
|
BACKUP_MEMORIES = (
|
||||||
|
("flash", "i", "hex"),
|
||||||
|
("flash", "r", "bin"),
|
||||||
|
("eeprom", "i", "hex"),
|
||||||
|
("eeprom", "r", "bin"),
|
||||||
|
("lfuse", "h", "hex"),
|
||||||
|
("hfuse", "h", "hex"),
|
||||||
|
("efuse", "h", "hex"),
|
||||||
|
("lock", "h", "hex"),
|
||||||
|
("calibration", "h", "hex"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Error(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def bitclock_for(hz: int) -> str:
|
||||||
|
"""A safe ISP bitclock for a part *currently running* at `hz`.
|
||||||
|
|
||||||
|
SCK must stay under a quarter of the target clock, so the bitclock follows
|
||||||
|
the clock in force — not the one about to be fused in. Halving that ceiling
|
||||||
|
again costs nothing on a link that moves a few hundred bytes and buys margin
|
||||||
|
against an oscillator that is already known to be off its nominal.
|
||||||
|
"""
|
||||||
|
ceiling = hz // 8
|
||||||
|
for candidate in (1000, 4000, 8000, 32000, 125000, 400000):
|
||||||
|
if candidate <= ceiling:
|
||||||
|
best = candidate
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
best = 400000
|
||||||
|
if ceiling < 1000:
|
||||||
|
raise Error(f"a part at {hz} Hz is too slow to reach over ISP safely")
|
||||||
|
return f"{best // 1000}kHz"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class Deployment:
|
||||||
|
"""Everything about one board. No default names a real device."""
|
||||||
|
|
||||||
|
port: str = "" # serial device the loader speaks on
|
||||||
|
baud: int = 57600 # host rate; for autobaud, the rate to drive
|
||||||
|
autobaud: bool = False # send the calibration pulse instead of p+b
|
||||||
|
one_wire: bool = False # shared line: the host discards its own echo
|
||||||
|
programmer: str = "" # avrdude -c
|
||||||
|
part: str = "" # avrdude -p
|
||||||
|
avrdude: str = "avrdude"
|
||||||
|
bitclock: str = "125kHz" # see bitclock_for()
|
||||||
|
pureboot: pathlib.Path = DEFAULT_PUREBOOT
|
||||||
|
wait: int = 12 # seconds the host keeps knocking
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_env(cls) -> "Deployment":
|
||||||
|
"""Environment defaults, so a rig's facts live in one place per machine."""
|
||||||
|
return cls(
|
||||||
|
port=os.environ.get("PUREBOOT_PORT", ""),
|
||||||
|
baud=int(os.environ.get("PUREBOOT_BAUD", "57600")),
|
||||||
|
autobaud=os.environ.get("PUREBOOT_AUTOBAUD", "") not in ("", "0"),
|
||||||
|
one_wire=os.environ.get("PUREBOOT_ONE_WIRE", "") not in ("", "0"),
|
||||||
|
programmer=os.environ.get("PUREBOOT_PROGRAMMER", ""),
|
||||||
|
part=os.environ.get("PUREBOOT_PART", ""),
|
||||||
|
avrdude=os.environ.get("AVRDUDE", "avrdude"),
|
||||||
|
bitclock=os.environ.get("PUREBOOT_BITCLOCK", "125kHz"),
|
||||||
|
pureboot=pathlib.Path(os.environ.get("PUREBOOT_TOOL", str(DEFAULT_PUREBOOT))),
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def add_arguments(parser: argparse.ArgumentParser) -> None:
|
||||||
|
"""Deployment flags, shared by this tool and pbhw.py."""
|
||||||
|
env = Deployment.from_env()
|
||||||
|
parser.add_argument("--port", default=env.port, help="serial device the loader speaks on")
|
||||||
|
parser.add_argument("--baud", type=int, default=env.baud,
|
||||||
|
help="host rate (for autobaud, the rate to drive)")
|
||||||
|
parser.add_argument("--autobaud", action="store_true", default=env.autobaud,
|
||||||
|
help="send the calibration pulse instead of the p+b knock")
|
||||||
|
parser.add_argument("--one-wire", action="store_true", default=env.one_wire,
|
||||||
|
help="shared line: pass the tool its echo discard")
|
||||||
|
parser.add_argument("--programmer", default=env.programmer, help="avrdude -c, e.g. atmelice_isp")
|
||||||
|
parser.add_argument("--part", default=env.part, help="avrdude -p, e.g. t13 or m328p")
|
||||||
|
parser.add_argument("--avrdude", default=env.avrdude, help="path to avrdude")
|
||||||
|
parser.add_argument("--bitclock", default=env.bitclock, help="ISP bitclock, e.g. 125kHz or 8kHz")
|
||||||
|
parser.add_argument("--pureboot", type=pathlib.Path, default=env.pureboot,
|
||||||
|
help="path to pureboot.py")
|
||||||
|
parser.add_argument("--wait", type=int, default=env.wait, help="seconds to keep knocking")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_args(cls, args: argparse.Namespace) -> "Deployment":
|
||||||
|
return cls(port=args.port, baud=args.baud, autobaud=args.autobaud,
|
||||||
|
one_wire=args.one_wire,
|
||||||
|
programmer=args.programmer, part=args.part, avrdude=args.avrdude,
|
||||||
|
bitclock=args.bitclock, pureboot=args.pureboot, wait=args.wait)
|
||||||
|
|
||||||
|
|
||||||
|
def load_pureboot(path: pathlib.Path = DEFAULT_PUREBOOT):
|
||||||
|
"""The host tool as a module — its Port and Loader, not a subprocess.
|
||||||
|
|
||||||
|
Used where a subprocess cannot express what is needed: a poke followed by a
|
||||||
|
peek in the *same* session, or a raw read at an arbitrary baud.
|
||||||
|
"""
|
||||||
|
spec = importlib.util.spec_from_file_location("pureboot", path)
|
||||||
|
if spec is None or spec.loader is None:
|
||||||
|
raise Error(f"cannot load the host tool from {path}")
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
class Rig:
|
||||||
|
"""One board: its programmer on one side, its serial link on the other."""
|
||||||
|
|
||||||
|
def __init__(self, deployment: Deployment):
|
||||||
|
self.d = deployment
|
||||||
|
if not deployment.programmer or not deployment.part:
|
||||||
|
raise Error("a rig needs --programmer and --part")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------- programmer
|
||||||
|
|
||||||
|
def avrdude(self, *args: str, cwd: pathlib.Path | None = None,
|
||||||
|
bitclock: str | None = None, timeout: int = 300) -> subprocess.CompletedProcess:
|
||||||
|
command = [self.d.avrdude, "-c", self.d.programmer, "-p", self.d.part,
|
||||||
|
"-B", bitclock or self.d.bitclock, *args]
|
||||||
|
return subprocess.run(command, capture_output=True, text=True,
|
||||||
|
cwd=None if cwd is None else str(cwd), timeout=timeout)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _ok(result: subprocess.CompletedProcess) -> bool:
|
||||||
|
return result.returncode == 0
|
||||||
|
|
||||||
|
def reset(self, bitclock: str | None = None) -> None:
|
||||||
|
"""An ISP access, which resets the part; it runs when avrdude exits."""
|
||||||
|
self.avrdude("-U", "signature:r:-:h", bitclock=bitclock)
|
||||||
|
|
||||||
|
def signature(self, bitclock: str | None = None) -> str:
|
||||||
|
result = self.avrdude("-U", "signature:r:-:h", bitclock=bitclock)
|
||||||
|
for line in reversed(result.stdout.splitlines()):
|
||||||
|
if line.strip().startswith("0x"):
|
||||||
|
return line.strip()
|
||||||
|
raise Error(f"no signature read: {(result.stderr or result.stdout).strip()[:200]}")
|
||||||
|
|
||||||
|
def read_memory(self, memory: str, destination: pathlib.Path, fmt: str = "r",
|
||||||
|
bitclock: str | None = None) -> bool:
|
||||||
|
"""Read `memory` into `destination`, whose directory avrdude runs in."""
|
||||||
|
destination = pathlib.Path(destination).resolve()
|
||||||
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
result = self.avrdude("-U", f"{memory}:r:{destination.name}:{fmt}",
|
||||||
|
cwd=destination.parent, bitclock=bitclock)
|
||||||
|
# A memory the part does not have (a tiny's extended fuse) leaves avrdude
|
||||||
|
# happy and the file empty. An empty capture is a miss, not a backup.
|
||||||
|
return self._ok(result) and destination.exists() and destination.stat().st_size > 0
|
||||||
|
|
||||||
|
def write_memory(self, memory: str, source: pathlib.Path, fmt: str = "i",
|
||||||
|
erase: bool = False, bitclock: str | None = None) -> bool:
|
||||||
|
source = pathlib.Path(source).resolve()
|
||||||
|
args = ["-U", f"{memory}:w:{source.name}:{fmt}"]
|
||||||
|
if erase:
|
||||||
|
args.insert(0, "-e")
|
||||||
|
result = self.avrdude(*args, cwd=source.parent, bitclock=bitclock)
|
||||||
|
return "verified" in (result.stdout + result.stderr)
|
||||||
|
|
||||||
|
def flash_hex(self, image: pathlib.Path, erase: bool = True,
|
||||||
|
bitclock: str | None = None) -> bool:
|
||||||
|
return self.write_memory("flash", image, "i", erase=erase, bitclock=bitclock)
|
||||||
|
|
||||||
|
def read_fuses(self, bitclock: str | None = None) -> dict[str, str]:
|
||||||
|
out: dict[str, str] = {}
|
||||||
|
for fuse in ("lfuse", "hfuse", "efuse", "lock"):
|
||||||
|
result = self.avrdude("-U", f"{fuse}:r:-:h", bitclock=bitclock)
|
||||||
|
values = [l.strip() for l in result.stdout.splitlines() if l.strip().startswith("0x")]
|
||||||
|
if values:
|
||||||
|
out[fuse] = values[-1]
|
||||||
|
return out
|
||||||
|
|
||||||
|
def write_fuses(self, bitclock: str | None = None, **fuses: str) -> bool:
|
||||||
|
"""Write named fuses. A fuse change moves the clock the *next* access is
|
||||||
|
timed against, so pass a bitclock safe for both sides of the change."""
|
||||||
|
args: list[str] = []
|
||||||
|
for name, value in fuses.items():
|
||||||
|
args += ["-U", f"{name}:w:{value}:m"]
|
||||||
|
if not args:
|
||||||
|
return True
|
||||||
|
result = self.avrdude(*args, bitclock=bitclock)
|
||||||
|
text = result.stdout + result.stderr
|
||||||
|
return "verified" in text or "written" in text
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ backup
|
||||||
|
|
||||||
|
def backup(self, directory: pathlib.Path, prefix: str = "") -> dict[str, bool]:
|
||||||
|
"""Capture every memory worth keeping, then prove it by a second read.
|
||||||
|
|
||||||
|
A backup nobody verified is a guess. Each memory is read twice and the
|
||||||
|
two reads compared; a mismatch is reported rather than quietly stored.
|
||||||
|
"""
|
||||||
|
directory = pathlib.Path(directory).resolve()
|
||||||
|
directory.mkdir(parents=True, exist_ok=True)
|
||||||
|
stem = prefix or self.d.part
|
||||||
|
status: dict[str, bool] = {}
|
||||||
|
for memory, fmt, extension in BACKUP_MEMORIES:
|
||||||
|
name = f"{stem}-{memory}.{extension}"
|
||||||
|
if not self.read_memory(memory, directory / name, fmt):
|
||||||
|
status[f"{memory}.{extension}"] = False
|
||||||
|
continue
|
||||||
|
if extension == "bin": # only the raw form is worth comparing byte-wise
|
||||||
|
again = directory / f".{name}.again"
|
||||||
|
self.read_memory(memory, again, fmt)
|
||||||
|
same = again.exists() and again.read_bytes() == (directory / name).read_bytes()
|
||||||
|
again.unlink(missing_ok=True)
|
||||||
|
status[f"{memory}.{extension}"] = same
|
||||||
|
else:
|
||||||
|
status[f"{memory}.{extension}"] = True
|
||||||
|
return status
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ serial link
|
||||||
|
|
||||||
|
def pureboot(self, *args: str, reset_first: bool = True, baud: int | None = None,
|
||||||
|
autobaud: bool | None = None, timeout: int = 300,
|
||||||
|
bitclock: str | None = None) -> tuple[int, str]:
|
||||||
|
"""Reset, then knock immediately — see the module docstring.
|
||||||
|
|
||||||
|
Returns the host tool's exit status and its combined output, so a caller
|
||||||
|
can assert on what it printed as well as on whether it succeeded.
|
||||||
|
"""
|
||||||
|
if reset_first:
|
||||||
|
self.reset(bitclock=bitclock)
|
||||||
|
command = [sys.executable, str(self.d.pureboot), "--port", self.d.port,
|
||||||
|
"--baud", str(self.d.baud if baud is None else baud),
|
||||||
|
"--wait", str(self.d.wait)]
|
||||||
|
if self.d.autobaud if autobaud is None else autobaud:
|
||||||
|
command.append("--autobaud")
|
||||||
|
if self.d.one_wire:
|
||||||
|
command.append("--one-wire")
|
||||||
|
command += [str(a) for a in args]
|
||||||
|
try:
|
||||||
|
result = subprocess.run(command, capture_output=True, text=True, timeout=timeout)
|
||||||
|
except subprocess.TimeoutExpired as expired:
|
||||||
|
return 99, f"TIMEOUT after {timeout}s\n{expired.stdout or ''}{expired.stderr or ''}"
|
||||||
|
return result.returncode, (result.stdout or "") + (result.stderr or "")
|
||||||
|
|
||||||
|
def open_port(self, baud: int | None = None):
|
||||||
|
"""A port opened the way this deployment says to speak to the board.
|
||||||
|
|
||||||
|
Everything the rig runs as a *subprocess* gets its flags from
|
||||||
|
`pureboot()` above; anything that drives the protocol in-process has
|
||||||
|
to reach the same facts, and until this existed only the subprocess
|
||||||
|
path could. A shared line is the one where that gap is fatal rather
|
||||||
|
than untidy: the host reads back every byte it writes, so an
|
||||||
|
undiscarded echo answers the knock before the device does. Open
|
||||||
|
through here and a one-wire deployment cannot be silently driven as
|
||||||
|
a two-wire one.
|
||||||
|
"""
|
||||||
|
module = load_pureboot(self.d.pureboot)
|
||||||
|
port = module.Port(self.d.port, self.d.baud if baud is None else baud)
|
||||||
|
return module.OneWirePort(port) if self.d.one_wire else port
|
||||||
|
|
||||||
|
def capture(self, seconds: float = 2.0, baud: int | None = None) -> bytes:
|
||||||
|
"""Listen to whatever the board is saying, at an arbitrary rate.
|
||||||
|
|
||||||
|
Opening the port does not reset a board whose DTR is unwired, so this can
|
||||||
|
sample a running application repeatedly without disturbing it — which is
|
||||||
|
what makes the rate sweep below possible.
|
||||||
|
"""
|
||||||
|
module = load_pureboot(self.d.pureboot)
|
||||||
|
port = module.Port(self.d.port, self.d.baud if baud is None else baud)
|
||||||
|
try:
|
||||||
|
data = b""
|
||||||
|
deadline = time.monotonic() + seconds
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
chunk = port.read_available(0.2)
|
||||||
|
if chunk:
|
||||||
|
data += chunk
|
||||||
|
return data
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
port.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def measure_rate(rig: Rig, marker: bytes, built_baud: int, nominal_hz: int | None = None,
|
||||||
|
span_percent: float = 12.0, step_percent: float = 0.5,
|
||||||
|
seconds: float = 0.75) -> dict:
|
||||||
|
"""Find a transmitting board's true bit rate, using only the serial port.
|
||||||
|
|
||||||
|
The board must be emitting something recognisable at a *fixed* cycles-per-bit
|
||||||
|
— `test/pbapp.cpp` built with PUREBOOT_HEARTBEAT does. Since its bit timing is
|
||||||
|
a cycle count, its wire rate scales with its actual clock, so the host rates
|
||||||
|
at which `marker` still decodes bracket that rate; the centre of the band is
|
||||||
|
the answer, and with the clock the image was built for it gives the real one.
|
||||||
|
|
||||||
|
This is the measurement that turns "the loader is silent, so the wiring must
|
||||||
|
be wrong" into a number, and it needs no instrument beyond the adapter
|
||||||
|
already attached.
|
||||||
|
"""
|
||||||
|
steps = int(span_percent / step_percent)
|
||||||
|
clean: list[int] = []
|
||||||
|
samples: list[tuple[int, int, bool]] = []
|
||||||
|
for index in range(-steps, steps + 1):
|
||||||
|
baud = int(round(built_baud * (1 + index * step_percent / 100.0)))
|
||||||
|
if baud <= 0:
|
||||||
|
continue
|
||||||
|
data = rig.capture(seconds=seconds, baud=baud)
|
||||||
|
hit = marker in data
|
||||||
|
samples.append((baud, len(data), hit))
|
||||||
|
if hit:
|
||||||
|
clean.append(baud)
|
||||||
|
result: dict = {"samples": samples, "clean": clean, "built_baud": built_baud}
|
||||||
|
if clean:
|
||||||
|
low, high = min(clean), max(clean)
|
||||||
|
centre = (low + high) / 2.0
|
||||||
|
result |= {"low": low, "high": high, "centre": centre,
|
||||||
|
"half_width_percent": (high - low) / 2.0 / centre * 100.0,
|
||||||
|
"error_percent": (centre / built_baud - 1.0) * 100.0}
|
||||||
|
if nominal_hz:
|
||||||
|
result["measured_hz"] = nominal_hz * centre / built_baud
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------- command
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="pureboot hardware rig: ISP reset/flash beside the serial link")
|
||||||
|
Deployment.add_arguments(parser)
|
||||||
|
sub = parser.add_subparsers(dest="command", required=True)
|
||||||
|
|
||||||
|
sub.add_parser("signature", help="read the part signature over ISP")
|
||||||
|
sub.add_parser("reset", help="reset the part (an ISP access) and let it run")
|
||||||
|
sub.add_parser("fuses", help="read the fuse and lock bytes")
|
||||||
|
|
||||||
|
p = sub.add_parser("flash", help="program a hex image over ISP")
|
||||||
|
p.add_argument("image", type=pathlib.Path)
|
||||||
|
p.add_argument("--no-erase", action="store_true", help="do not chip-erase first")
|
||||||
|
|
||||||
|
p = sub.add_parser("backup", help="capture and verify every memory")
|
||||||
|
p.add_argument("directory", type=pathlib.Path)
|
||||||
|
p.add_argument("--prefix", default="", help="filename stem (default: the part name)")
|
||||||
|
|
||||||
|
p = sub.add_parser("rate", help="measure the board's true bit rate and clock")
|
||||||
|
p.add_argument("--marker", default="APP", help="text the board emits (default: APP)")
|
||||||
|
p.add_argument("--built-baud", type=int, required=True,
|
||||||
|
help="the baud the running image was built for")
|
||||||
|
p.add_argument("--nominal-hz", type=int, default=0,
|
||||||
|
help="the clock the image was built for, to report the real one")
|
||||||
|
p.add_argument("--span", type=float, default=12.0, help="sweep +-this many percent")
|
||||||
|
p.add_argument("--step", type=float, default=0.5, help="sweep step in percent")
|
||||||
|
p.add_argument("--verbose", action="store_true", help="print every step")
|
||||||
|
|
||||||
|
p = sub.add_parser("bitclock", help="a safe ISP bitclock for a clock in force")
|
||||||
|
p.add_argument("hz", type=int)
|
||||||
|
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
if args.command == "bitclock":
|
||||||
|
print(bitclock_for(args.hz))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
rig = Rig(Deployment.from_args(args))
|
||||||
|
|
||||||
|
if args.command == "signature":
|
||||||
|
print(rig.signature())
|
||||||
|
elif args.command == "reset":
|
||||||
|
rig.reset()
|
||||||
|
print("reset")
|
||||||
|
elif args.command == "fuses":
|
||||||
|
for name, value in rig.read_fuses().items():
|
||||||
|
print(f"{name:<6} {value}")
|
||||||
|
elif args.command == "flash":
|
||||||
|
ok = rig.flash_hex(args.image, erase=not args.no_erase)
|
||||||
|
print(f"{args.image.name}: {'verified' if ok else 'FAILED'}")
|
||||||
|
return 0 if ok else 1
|
||||||
|
elif args.command == "backup":
|
||||||
|
status = rig.backup(args.directory, args.prefix)
|
||||||
|
for name, ok in status.items():
|
||||||
|
print(f" {'ok ' if ok else 'FAIL'} {name}")
|
||||||
|
missing = [n for n, ok in status.items() if not ok]
|
||||||
|
# Fuses a part does not have are expected misses, not failures.
|
||||||
|
fatal = [n for n in missing if not n.startswith(("efuse", "calibration"))]
|
||||||
|
print(f"\n{len(status) - len(missing)}/{len(status)} captured into {args.directory}")
|
||||||
|
return 1 if fatal else 0
|
||||||
|
elif args.command == "rate":
|
||||||
|
result = measure_rate(rig, args.marker.encode(), args.built_baud,
|
||||||
|
args.nominal_hz or None, args.span, args.step)
|
||||||
|
if args.verbose:
|
||||||
|
for baud, size, hit in result["samples"]:
|
||||||
|
print(f" {baud:7d} Bd {size:5d} B {'MARKER' if hit else ''}")
|
||||||
|
if not result["clean"]:
|
||||||
|
print(f"no capture contained {args.marker!r} at any rate — is the board "
|
||||||
|
f"transmitting, and on the pin this port is wired to?")
|
||||||
|
return 1
|
||||||
|
print(f"clean band {result['low']}..{result['high']} Bd")
|
||||||
|
print(f"centre {result['centre']:.0f} Bd "
|
||||||
|
f"(+-{result['half_width_percent']:.1f} %)")
|
||||||
|
print(f"vs built {result['built_baud']} Bd ({result['error_percent']:+.1f} %)")
|
||||||
|
if "measured_hz" in result:
|
||||||
|
print(f"true clock {result['measured_hz'] / 1e6:.3f} MHz")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
sys.exit(main())
|
||||||
|
except Error as error:
|
||||||
|
print(f"error: {error}", file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
187
tools/sizes.py
Executable file
187
tools/sizes.py
Executable file
@@ -0,0 +1,187 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""What the loader images actually measure, and whether the README still agrees.
|
||||||
|
|
||||||
|
The size matrix asserts every image fits its slot; it says nothing about the
|
||||||
|
numbers the README prints, and those drift. Every row of that table was eight
|
||||||
|
bytes stale once `startup::caller_page()` landed — common code, so every build
|
||||||
|
moved at once and no test noticed, because none of them was over budget.
|
||||||
|
|
||||||
|
Two questions, both answered from built trees:
|
||||||
|
|
||||||
|
sizes.py max the largest image per chip, and anything over budget
|
||||||
|
sizes.py check-readme the README's per-chip table against what is built
|
||||||
|
|
||||||
|
Nothing here knows a chip's geometry. The (image, budget) pairs come from each
|
||||||
|
build's own `CTestTestfile.cmake` — the same values the gate checks — so the
|
||||||
|
slot rules stay where they belong, in `pureboot/CMakeLists.txt`, and a chip
|
||||||
|
added or a budget changed needs no edit here. Only trees a configure preset
|
||||||
|
still owns are read: a stale directory keeps its last build, and a loader built
|
||||||
|
before a slot changed will happily report a size that was true once
|
||||||
|
(`tools/prune-build-trees.sh` in libavr removes them).
|
||||||
|
|
||||||
|
Sizes come from `avr-size`, and a target is only as current as its last build —
|
||||||
|
run the gate first if you want the table checked against today's source.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import pathlib
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||||
|
# add_test(<name>.size ... -DELF=<path> ... -DLIMIT=<n> ...) — the gate's own
|
||||||
|
# pairing of an image with the budget it must fit.
|
||||||
|
# ctest writes the name as a bracket argument ([=[name.size]=]) and quotes the
|
||||||
|
# rest, so the name starts after the bracket and the path ends at the quote.
|
||||||
|
SIZE_TEST = re.compile(r'add_test\(\s*\[=\[(?P<name>[^\]]+?)\.size\]=\][^\n]*?'
|
||||||
|
r'-DELF=(?P<elf>[^"\s]+)[^\n]*?-DLIMIT=(?P<limit>\d+)')
|
||||||
|
|
||||||
|
|
||||||
|
def avr_size() -> str:
|
||||||
|
for env in (ROOT / "../../toolchain").resolve().glob("avr-gcc-*/bin/avr-size"):
|
||||||
|
if env.is_file():
|
||||||
|
return str(env)
|
||||||
|
found = shutil.which("avr-size")
|
||||||
|
if not found:
|
||||||
|
sys.exit("no avr-size found (build the toolchain, or put it on PATH)")
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def preset_dirs() -> list[pathlib.Path]:
|
||||||
|
"""Build trees a configure preset still owns, newest-listed first."""
|
||||||
|
listing = subprocess.run(["cmake", "--list-presets"], cwd=ROOT, capture_output=True, text=True)
|
||||||
|
names = re.findall(r'^\s*"(.+)"$', listing.stdout, re.MULTILINE)
|
||||||
|
if not names:
|
||||||
|
sys.exit("cmake --list-presets returned nothing — run from a configured checkout")
|
||||||
|
return [d for d in (ROOT / "build" / n for n in names) if (d / "CTestTestfile.cmake").is_file()]
|
||||||
|
|
||||||
|
|
||||||
|
def measure(paths: list[str], tool: str) -> dict[str, int]:
|
||||||
|
""".text per ELF, in one avr-size call per batch."""
|
||||||
|
sizes: dict[str, int] = {}
|
||||||
|
for start in range(0, len(paths), 400):
|
||||||
|
batch = [p for p in paths[start:start + 400] if pathlib.Path(p).is_file()]
|
||||||
|
if not batch:
|
||||||
|
continue
|
||||||
|
out = subprocess.run([tool, *batch], capture_output=True, text=True).stdout
|
||||||
|
for line in out.splitlines()[1:]:
|
||||||
|
fields = line.split()
|
||||||
|
if len(fields) >= 6 and fields[0].isdigit():
|
||||||
|
sizes[fields[5]] = int(fields[0])
|
||||||
|
return sizes
|
||||||
|
|
||||||
|
|
||||||
|
def collect() -> dict[str, list[tuple[str, int, int]]]:
|
||||||
|
"""chip -> [(target, text, limit)], from every owned build tree."""
|
||||||
|
tool = avr_size()
|
||||||
|
found: dict[str, list[tuple[str, str, int]]] = {}
|
||||||
|
for tree in preset_dirs():
|
||||||
|
chip = tree.name.split("-")[0]
|
||||||
|
for match in SIZE_TEST.finditer((tree / "CTestTestfile.cmake").read_text()):
|
||||||
|
found.setdefault(chip, []).append((match["name"], match["elf"], int(match["limit"])))
|
||||||
|
sizes = measure([elf for rows in found.values() for _, elf, _ in rows], tool)
|
||||||
|
# A chip's generated and reflect trees must answer with the same bytes
|
||||||
|
# (the identity invariant), so the same target measuring two sizes means
|
||||||
|
# a stale tree — or an identity breach. Either is a finding; picking one
|
||||||
|
# silently is how a gate reports another build's numbers as today's.
|
||||||
|
for chip, rows in found.items():
|
||||||
|
seen: dict[str, tuple[int, str]] = {}
|
||||||
|
for name, elf, _ in rows:
|
||||||
|
if elf not in sizes:
|
||||||
|
continue
|
||||||
|
if name in seen and seen[name][0] != sizes[elf]:
|
||||||
|
sys.exit(f"{chip} {name}: {seen[name][0]} B in {seen[name][1]} but "
|
||||||
|
f"{sizes[elf]} B in {elf} — a stale tree (rebuild or remove it) "
|
||||||
|
f"or a cross-mode identity breach")
|
||||||
|
seen.setdefault(name, (sizes[elf], elf))
|
||||||
|
measured = {
|
||||||
|
chip: sorted(((name, sizes[elf], limit) for name, elf, limit in rows if elf in sizes),
|
||||||
|
key=lambda row: -row[1])
|
||||||
|
for chip, rows in sorted(found.items())
|
||||||
|
}
|
||||||
|
# A configured-but-unbuilt preset registers its tests with no images behind
|
||||||
|
# them; it is not a chip with nothing to say, it is a chip not built yet.
|
||||||
|
return {chip: rows for chip, rows in measured.items() if rows}
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_max(args) -> int:
|
||||||
|
measured = collect()
|
||||||
|
if not measured:
|
||||||
|
sys.exit("nothing built — configure and build a preset first")
|
||||||
|
over = []
|
||||||
|
print(f"{'chip':<13} {'largest image':<34} {'.text':>6} {'budget':>7} headroom")
|
||||||
|
for chip, rows in measured.items():
|
||||||
|
name, text, limit = rows[0]
|
||||||
|
flag = "OVER" if text > limit else f"{limit - text:>5} B"
|
||||||
|
print(f"{chip:<13} {name:<34} {text:>6} {limit:>7} {flag}")
|
||||||
|
over += [(chip, n, t, l) for n, t, l in rows if t > l]
|
||||||
|
total = sum(len(rows) for rows in measured.values())
|
||||||
|
print(f"\n{total} images across {len(measured)} chips")
|
||||||
|
if over:
|
||||||
|
print("\nOVER BUDGET:")
|
||||||
|
for chip, name, text, limit in over:
|
||||||
|
print(f" {chip} {name}: {text} > {limit}")
|
||||||
|
return 1
|
||||||
|
tightest = min(((chip, n, t, l) for chip, rows in measured.items() for n, t, l in rows),
|
||||||
|
key=lambda row: row[3] - row[2])
|
||||||
|
chip, name, text, limit = tightest
|
||||||
|
print(f"tightest fit: {chip} {name} — {text} of {limit}, {limit - text} B spare")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_check_readme(args) -> int:
|
||||||
|
"""The README's per-chip table, against the stock build and the worst
|
||||||
|
autobaud configuration (OSCCAL baked, plus the USART-pin release where
|
||||||
|
the chip has a USART; the one-wire fold of the same build is its twin
|
||||||
|
and competes for the same cell) — the config the Autobaud column
|
||||||
|
documents."""
|
||||||
|
readme = (ROOT / "pureboot" / "README.md").read_text()
|
||||||
|
measured = collect()
|
||||||
|
rows = re.findall(r"^\|\s*(AT\w+[^|]*?)\s*\|[^|]*\|[^|]*\|[^|]*\|\s*(\d+) B\s*\|\s*(\d+) B\s*\|$",
|
||||||
|
readme, re.MULTILINE)
|
||||||
|
if not rows:
|
||||||
|
sys.exit("no size table found in pureboot/README.md")
|
||||||
|
bad = skipped = 0
|
||||||
|
for chips, stock_doc, auto_doc in rows:
|
||||||
|
# "ATmega48, 48A, 48P, 48PA †" — the first name is the family's base.
|
||||||
|
chip = re.sub(r"[^a-z0-9]", "", chips.split(",")[0].strip().lower())
|
||||||
|
built = {name: text for name, text, _ in measured.get(chip, [])}
|
||||||
|
# The on-USART pair defines the column where the chip has a USART;
|
||||||
|
# the default-pin pair is the whole space elsewhere. Whichever twin
|
||||||
|
# measures larger is the number the cell must state.
|
||||||
|
candidates = [name for name in ("pureboot_autobaud_osccal_on_usart0",
|
||||||
|
"pureboot_1w_autobaud_osccal_on_usart0") if name in built]
|
||||||
|
if not candidates:
|
||||||
|
candidates = [name for name in ("pureboot_autobaud_osccal",
|
||||||
|
"pureboot_1w_autobaud_osccal") if name in built]
|
||||||
|
worst = max(candidates, key=lambda name: built[name], default="pureboot_autobaud_osccal")
|
||||||
|
for target, documented in (("pureboot", stock_doc), (worst, auto_doc)):
|
||||||
|
if target not in built:
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
if built[target] != int(documented):
|
||||||
|
print(f" {chip:<12} {target:<18} README says {documented} B, built is {built[target]} B")
|
||||||
|
bad += 1
|
||||||
|
if bad:
|
||||||
|
print(f"\n{bad} row(s) stale — update pureboot/README.md")
|
||||||
|
return 1
|
||||||
|
print(f"README size table matches every built image ({len(rows)} rows"
|
||||||
|
+ (f", {skipped} not built" if skipped else "") + ")")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||||
|
subs = parser.add_subparsers(dest="cmd", required=True)
|
||||||
|
subs.add_parser("max", help="largest image per chip, and anything over budget")
|
||||||
|
subs.add_parser("check-readme", help="the README's size table against what is built")
|
||||||
|
args = parser.parse_args()
|
||||||
|
return {"max": cmd_max, "check-readme": cmd_check_readme}[args.cmd](args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -65,7 +65,7 @@ constexpr std::uint8_t comm_window = 200;
|
|||||||
constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 20;
|
constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 20;
|
||||||
|
|
||||||
// Fixed 115200 8N1; the library solves UBRR + U2X from clock and baud.
|
// Fixed 115200 8N1; the library solves UBRR + U2X from clock and baud.
|
||||||
constexpr auto baud = avr::uart::detail::solve_baud(16_MHz, 115200_Bd);
|
constexpr auto baud = avr::uart::solve_baud(16_MHz, 115200_Bd);
|
||||||
|
|
||||||
// The 16-byte device-info block, streamed out on activation.
|
// The 16-byte device-info block, streamed out on activation.
|
||||||
// clang-format off
|
// clang-format off
|
||||||
@@ -263,7 +263,7 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
// 0, and rx()/tx() raise RXEN0/TXEN0 on first use — only the divisor low
|
// 0, and rx()/tx() raise RXEN0/TXEN0 on first use — only the divisor low
|
||||||
// byte and U2X0 need a store. The library still does the datasheet work.
|
// byte and U2X0 need a store. The library still does the datasheet work.
|
||||||
static_assert(baud.u2x && baud.ubrr < 256, "lean bring-up writes UBRR0L only, with U2X0");
|
static_assert(baud.u2x && baud.ubrr < 256, "lean bring-up writes UBRR0L only, with U2X0");
|
||||||
hw::reg<"UBRR0">::write(static_cast<std::uint8_t>(baud.ubrr));
|
hw::ubrr0::write(static_cast<std::uint8_t>(baud.ubrr));
|
||||||
hw::ucsr0a::write(hw::ucsr0a::u2x0(1));
|
hw::ucsr0a::write(hw::ucsr0a::u2x0(1));
|
||||||
// General-purpose registers are undefined at power-on (no crt zeroes them);
|
// General-purpose registers are undefined at power-on (no crt zeroes them);
|
||||||
// the direction latch must start "not receiving" so the first rx() enables
|
// the direction latch must start "not receiving" so the first rx() enables
|
||||||
|
|||||||
302
tsb/tsb_policy.cpp
Normal file
302
tsb/tsb_policy.cpp
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
// TinySafeBoot on libavr — the policy floor: pureboot's rules, measured.
|
||||||
|
//
|
||||||
|
// The full TinySafeBoot feature set — watchdog bail, one-wire half-duplex,
|
||||||
|
// config-page activation timeout, password gate, emergency erase, and
|
||||||
|
// config/flash/EEPROM read-write — under philosophy #5 exactly as pureboot
|
||||||
|
// obeys it: no assembly, no register variables; code, attributes, and flags
|
||||||
|
// only. Every lesson pureboot's development produced is applied — the
|
||||||
|
// library's half-duplex serial and startup entry, lean bring-up from reset
|
||||||
|
// state, one merged send loop over both memories, oracle-shaped loop bounds,
|
||||||
|
// locals threaded through noinline primitives, pureboot's codegen flags —
|
||||||
|
// and the result is 638 bytes: 198 below the idiomatic tier, and 126 above
|
||||||
|
// the 512 B boot section the tricks/asm tiers reach with the banned
|
||||||
|
// mechanisms (526/510). This tier exists to keep that number an artifact
|
||||||
|
// rather than a claim: the gap to 512 is the rent of policy-clean C++ —
|
||||||
|
// helpers that hold a cursor across rx()/tx() pay push/pop and argument
|
||||||
|
// threading where a global-register protocol pays nothing, and both
|
||||||
|
// control-flow merges tried (a parametrized paged session, a merged store
|
||||||
|
// loop) measured larger than the split cases they replaced. TSB's wire fixes
|
||||||
|
// the per-command loop shapes on the device, so pureboot 5's one-transfer-
|
||||||
|
// loop collapse has no purchase here.
|
||||||
|
//
|
||||||
|
// The wire protocol is strict request/response, which is what makes the
|
||||||
|
// shared line safe: the device drives it only between a received command and
|
||||||
|
// its reply, and releases it (the library's half-duplex choreography)
|
||||||
|
// whenever it waits.
|
||||||
|
|
||||||
|
#include <libavr/libavr.hpp>
|
||||||
|
|
||||||
|
using namespace avr::literals;
|
||||||
|
namespace spm = avr::spm;
|
||||||
|
namespace ee = avr::eeprom;
|
||||||
|
|
||||||
|
using dev = avr::device<{.clock = 16_MHz}>;
|
||||||
|
// One-wire: RX and TX share the line, exactly as the native-UART TSB expects.
|
||||||
|
using serial_t = dev::uart0<{.baud = 115200_Bd, .max_baud_error = 3_pct, .half_duplex = true}>;
|
||||||
|
inline constexpr serial_t serial{};
|
||||||
|
|
||||||
|
namespace tsb {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// The loader is purely polled — it never enables interrupts — so every SPM and
|
||||||
|
// EEPROM lock folds to nothing under this posture.
|
||||||
|
constexpr auto off = avr::irq::guard_policy::unused;
|
||||||
|
|
||||||
|
// The handshake bytes, identical across every TSB host.
|
||||||
|
constexpr std::uint8_t confirm = '!';
|
||||||
|
constexpr std::uint8_t request = '?';
|
||||||
|
constexpr std::uint8_t knock = '@';
|
||||||
|
|
||||||
|
// Boot geometry for the 1 KB boot section (BOOTSZ=10); the page size and the
|
||||||
|
// flash/EEPROM extents are the chip database's to know. app_end is the config
|
||||||
|
// page (TSB's LASTPAGE), one page below the boot section.
|
||||||
|
constexpr std::uint16_t page = spm::page_bytes;
|
||||||
|
constexpr std::uint16_t boot_bytes = 1024;
|
||||||
|
constexpr std::uint16_t app_end = spm::flash_bytes - boot_bytes - page;
|
||||||
|
constexpr std::uint16_t eeprom_end = avr::hw::db.mem.eeprom_size - 1;
|
||||||
|
|
||||||
|
// Lockout-proof floor for the activation window (the oracle's F_CPU/1MHz).
|
||||||
|
constexpr std::uint8_t act_min = 16;
|
||||||
|
// Post-activation window: the host gets seconds, not milliseconds, mid-session.
|
||||||
|
constexpr std::uint8_t comm_window = 200;
|
||||||
|
|
||||||
|
// Firmware version stamp: YY*512 + MM*32 + DD, the encoding the host decodes.
|
||||||
|
constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 27;
|
||||||
|
|
||||||
|
// The 16-byte device-info block, streamed out on activation.
|
||||||
|
// clang-format off
|
||||||
|
[[gnu::progmem]] constexpr std::uint8_t info[16] = {
|
||||||
|
'T', 'S', 'B',
|
||||||
|
build_date & 0xFF, build_date >> 8,
|
||||||
|
0xF3, // status: native-UART fixed-baud lineage
|
||||||
|
avr::hw::db.signature[0], avr::hw::db.signature[1], avr::hw::db.signature[2],
|
||||||
|
page / 2, // page size in words
|
||||||
|
(app_end / 2) & 0xFF, (app_end / 2) >> 8, // app-flash boundary, words
|
||||||
|
eeprom_end & 0xFF, eeprom_end >> 8,
|
||||||
|
0xAA, 0xAA, // ATmega processor-type marker (bytes 14 == 15)
|
||||||
|
};
|
||||||
|
// clang-format on
|
||||||
|
|
||||||
|
// The receive window, pre-floored where it is set. In .noinit: there is no
|
||||||
|
// crt to clear a .bss image, and run() stores it before the first receive.
|
||||||
|
[[gnu::section(".noinit")]] std::uint8_t window;
|
||||||
|
|
||||||
|
const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||||
|
{
|
||||||
|
return reinterpret_cast<const std::uint8_t *>(addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bounded byte receive: poll under nested countdowns, 0 on silence. The 0
|
||||||
|
// then falls through every compare — not a knock, not a confirm, not a
|
||||||
|
// command — so a silent host unwinds the loader to the application from
|
||||||
|
// anywhere, and a mid-session cable pull cannot wedge it. The line release on
|
||||||
|
// a direction change is the serial backend's.
|
||||||
|
[[gnu::noinline]] std::uint8_t rx()
|
||||||
|
{
|
||||||
|
std::uint16_t outer = static_cast<std::uint16_t>(window) << 8;
|
||||||
|
do {
|
||||||
|
std::uint8_t fine = 0;
|
||||||
|
do {
|
||||||
|
if (auto byte = serial.read())
|
||||||
|
return *byte;
|
||||||
|
} while (--fine);
|
||||||
|
} while (--outer);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// One-wire transmit: the backend takes the line with a turn-around guard and
|
||||||
|
// holds it until the whole frame is out.
|
||||||
|
[[gnu::noinline]] void tx(std::uint8_t byte)
|
||||||
|
{
|
||||||
|
serial.write(byte);
|
||||||
|
}
|
||||||
|
|
||||||
|
// '?', then hand back the host's reply for the callers' one-byte compare.
|
||||||
|
[[gnu::noinline]] std::uint8_t rcnf()
|
||||||
|
{
|
||||||
|
tx(request);
|
||||||
|
return rx();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The one send loop: the info block, the config page, application flash and
|
||||||
|
// EEPROM pages all stream through here.
|
||||||
|
[[gnu::noinline]] void send_block(bool eep, std::uint16_t at, std::uint8_t count)
|
||||||
|
{
|
||||||
|
do {
|
||||||
|
tx(eep ? ee::read(at) : avr::flash_load(flash_ptr(at)));
|
||||||
|
++at;
|
||||||
|
} while (--count);
|
||||||
|
}
|
||||||
|
|
||||||
|
// One EEPROM byte in — shared by the emergency wipe and the 'E' stream.
|
||||||
|
[[gnu::noinline]] void eeput(std::uint16_t at, std::uint8_t value)
|
||||||
|
{
|
||||||
|
ee::write<off>(at, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait out a running SPM op, then re-open the RWW section — after every page
|
||||||
|
// op and before handing over, as the oracle does.
|
||||||
|
[[gnu::noinline]] void settle()
|
||||||
|
{
|
||||||
|
spm::wait();
|
||||||
|
spm::rww_enable<off>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// One host page straight into the erased flash page at `at` — through the SPM
|
||||||
|
// word buffer (low byte then high), no SRAM staging — then committed. `at`
|
||||||
|
// names a page base, so the cursor's low byte reaching the boundary ends the
|
||||||
|
// walk.
|
||||||
|
[[gnu::noinline]] void store_flash_page(std::uint16_t at)
|
||||||
|
{
|
||||||
|
do {
|
||||||
|
std::uint8_t low = rx();
|
||||||
|
std::uint8_t high = rx();
|
||||||
|
spm::fill<off>(at, std::bit_cast<std::uint16_t>(std::array{low, high}));
|
||||||
|
at += 2;
|
||||||
|
} while (static_cast<std::uint8_t>(at) & (page - 1));
|
||||||
|
spm::write_page<off>(at - page);
|
||||||
|
settle();
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --defsym=tsb_app=0
|
||||||
|
|
||||||
|
[[noreturn]] void appjump()
|
||||||
|
{
|
||||||
|
settle();
|
||||||
|
tsb_app();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step one page down and erase it — the erase shared by the whole-app walk,
|
||||||
|
// the config rewrite and the emergency wipe; hands the stepped address back.
|
||||||
|
[[gnu::noinline]] std::uint16_t erase_below(std::uint16_t at)
|
||||||
|
{
|
||||||
|
at -= page;
|
||||||
|
spm::erase_page<off>(at);
|
||||||
|
settle();
|
||||||
|
return at;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Erase the whole application, top-down like the oracle: the loop bound is a
|
||||||
|
// compare with zero, and the returned 0 is the address every caller wants
|
||||||
|
// next.
|
||||||
|
[[gnu::noinline]] std::uint16_t erase_application()
|
||||||
|
{
|
||||||
|
std::uint16_t at = app_end;
|
||||||
|
do {
|
||||||
|
at = erase_below(at);
|
||||||
|
} while (at != 0);
|
||||||
|
return at;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[noreturn]] void run()
|
||||||
|
{
|
||||||
|
// A watchdog reset hands straight back to the application, as the
|
||||||
|
// reference loader does, rather than re-entering the bootloader.
|
||||||
|
if (avr::hw::mcusr::wdrf.test())
|
||||||
|
appjump();
|
||||||
|
|
||||||
|
// Lean bring-up from reset state: UCSR0C already reads 8N1, UBRR0H reads
|
||||||
|
// 0, and the half-duplex write()/read() raise TXEN0/RXEN0 on first use —
|
||||||
|
// only the divisor low byte and U2X0 need a store. The solver still does
|
||||||
|
// the datasheet work; the asserts pin the reset-state assumptions.
|
||||||
|
{
|
||||||
|
constexpr auto sol = avr::uart::solve_baud(dev::clock, 115200_Bd);
|
||||||
|
static_assert(sol.u2x && sol.ubrr < 256, "lean bring-up writes UBRR0L only, with U2X0");
|
||||||
|
avr::hw::ubrr0::write(static_cast<std::uint8_t>(sol.ubrr));
|
||||||
|
avr::hw::ucsr0a::write(avr::hw::ucsr0a::u2x0(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activation: 3×'@', each inside the config page's timeout window
|
||||||
|
// (floored so a corrupt page cannot lock the loader out); anything else —
|
||||||
|
// including silence — hands over.
|
||||||
|
window = avr::flash_load(flash_ptr(app_end + 2)) | act_min;
|
||||||
|
for (std::uint8_t k = 3; k; --k)
|
||||||
|
if (rx() != knock)
|
||||||
|
appjump();
|
||||||
|
window = comm_window;
|
||||||
|
|
||||||
|
// Password gate (config page from app_end+3, 0xff-terminated; a blank
|
||||||
|
// page is no password). A wrong byte blanks the comparison and drains the
|
||||||
|
// line forever, so a wrong password can never fall through; a 0 requests
|
||||||
|
// emergency erase behind two confirms. On pass the info block goes out;
|
||||||
|
// the emergency path skips it and drops into the command loop.
|
||||||
|
std::uint16_t at = app_end + 3;
|
||||||
|
std::uint8_t mask = 0xff;
|
||||||
|
for (;;) {
|
||||||
|
std::uint8_t expected = avr::flash_load(flash_ptr(at)) & mask;
|
||||||
|
++at;
|
||||||
|
if (expected == 0xff) {
|
||||||
|
send_block(false, reinterpret_cast<std::uint16_t>(&info[0]), sizeof info);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
std::uint8_t got = rx();
|
||||||
|
if (got == 0) {
|
||||||
|
if (mask == 0)
|
||||||
|
continue;
|
||||||
|
if (rcnf() != confirm || rcnf() != confirm)
|
||||||
|
appjump();
|
||||||
|
std::uint16_t a = erase_application();
|
||||||
|
do {
|
||||||
|
eeput(a, 0xff);
|
||||||
|
} while (++a <= eeprom_end);
|
||||||
|
erase_below(app_end + page);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (got != expected)
|
||||||
|
mask = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
tx(confirm); // Mainloop ready
|
||||||
|
const std::uint8_t command = rx();
|
||||||
|
switch (command) {
|
||||||
|
case 'f': // read application flash, one page per host '!'
|
||||||
|
for (std::uint16_t a = 0; a < app_end; a += page) {
|
||||||
|
if (rx() != confirm)
|
||||||
|
break;
|
||||||
|
send_block(false, a, page);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'e': // read EEPROM, one page per host '!', until the host stops
|
||||||
|
for (std::uint16_t a = 0;; a += page) {
|
||||||
|
if (rx() != confirm)
|
||||||
|
break;
|
||||||
|
send_block(true, a, page);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'F': { // erase the application, then take pages behind '?'
|
||||||
|
std::uint16_t a = erase_application();
|
||||||
|
for (; rcnf() == confirm; a += page)
|
||||||
|
store_flash_page(a);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'E': // take EEPROM pages behind '?', each write host-paced
|
||||||
|
for (std::uint16_t a = 0; rcnf() == confirm;) {
|
||||||
|
std::uint8_t count = page;
|
||||||
|
do {
|
||||||
|
eeput(a, rx());
|
||||||
|
++a;
|
||||||
|
} while (--count);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'c': // read the config page
|
||||||
|
read_config:
|
||||||
|
send_block(false, app_end, page);
|
||||||
|
break;
|
||||||
|
case 'C': // replace the config page, then echo it back to verify
|
||||||
|
if (rcnf() != confirm)
|
||||||
|
break;
|
||||||
|
store_flash_page(erase_below(app_end + page));
|
||||||
|
goto read_config;
|
||||||
|
default: // 'q' or any other byte runs the application
|
||||||
|
appjump();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace tsb
|
||||||
|
|
||||||
|
// Reset lands at the boot section base (BOOTRST): the entry stub in .vectors
|
||||||
|
// is laid first and does the one line of crt a crt-less image needs.
|
||||||
|
template struct avr::startup::entry<tsb::run>;
|
||||||
@@ -65,7 +65,7 @@ constexpr std::uint8_t comm_window = 200;
|
|||||||
constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 20;
|
constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 20;
|
||||||
|
|
||||||
// Fixed 115200 8N1; the library solves UBRR + U2X from clock and baud.
|
// Fixed 115200 8N1; the library solves UBRR + U2X from clock and baud.
|
||||||
constexpr auto baud = avr::uart::detail::solve_baud(16_MHz, 115200_Bd);
|
constexpr auto baud = avr::uart::solve_baud(16_MHz, 115200_Bd);
|
||||||
|
|
||||||
// The 16-byte device-info block, streamed out on activation.
|
// The 16-byte device-info block, streamed out on activation.
|
||||||
// clang-format off
|
// clang-format off
|
||||||
@@ -240,7 +240,7 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
// 0, and rx()/tx() raise RXEN0/TXEN0 on first use — only the divisor low
|
// 0, and rx()/tx() raise RXEN0/TXEN0 on first use — only the divisor low
|
||||||
// byte and U2X0 need a store. The library still does the datasheet work.
|
// byte and U2X0 need a store. The library still does the datasheet work.
|
||||||
static_assert(baud.u2x && baud.ubrr < 256, "lean bring-up writes UBRR0L only, with U2X0");
|
static_assert(baud.u2x && baud.ubrr < 256, "lean bring-up writes UBRR0L only, with U2X0");
|
||||||
hw::reg<"UBRR0">::write(static_cast<std::uint8_t>(baud.ubrr));
|
hw::ubrr0::write(static_cast<std::uint8_t>(baud.ubrr));
|
||||||
hw::ucsr0a::write(hw::ucsr0a::u2x0(1));
|
hw::ucsr0a::write(hw::ucsr0a::u2x0(1));
|
||||||
// General-purpose registers are undefined at power-on (no crt zeroes them);
|
// General-purpose registers are undefined at power-on (no crt zeroes them);
|
||||||
// the direction latch must start "not receiving" so the first rx() enables
|
// the direction latch must start "not receiving" so the first rx() enables
|
||||||
|
|||||||
Reference in New Issue
Block a user