Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| f98ed406b8 | |||
| a4da885e36 | |||
| 335e494a31 | |||
| 799709efcf | |||
| 7ae80087b3 | |||
| b477f53ca5 | |||
| 84d3f679c2 | |||
| b5020a1e20 |
419
CMakeLists.txt
419
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,
|
||||||
@@ -153,10 +165,25 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
if(Python3_FOUND)
|
if(Python3_FOUND)
|
||||||
add_test(NAME pureboot.pi
|
add_test(NAME pureboot.pi
|
||||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/check_pi.py
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/check_pi.py
|
||||||
${CMAKE_OBJDUMP} ${CMAKE_NM} $<TARGET_FILE:pureboot> ${PUREBOOT_BASE_HEX})
|
${CMAKE_OBJDUMP} ${CMAKE_OBJCOPY} ${CMAKE_CXX_COMPILER} ${LIBAVR_MCU}
|
||||||
|
$<TARGET_FILE:pureboot>
|
||||||
|
${CMAKE_BINARY_DIR}/CMakeFiles/pureboot.dir/pureboot/pureboot.cpp.obj
|
||||||
|
${PUREBOOT_BASE_HEX})
|
||||||
add_test(NAME pureboot.planner
|
add_test(NAME pureboot.planner
|
||||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/test_planner.py
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/test_planner.py
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py)
|
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py)
|
||||||
|
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
|
||||||
@@ -175,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
|
||||||
@@ -235,12 +294,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
|
||||||
@@ -248,19 +307,58 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
-DLIMIT=${PUREBOOT_LIMIT} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
|
-DLIMIT=${PUREBOOT_LIMIT} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
|
# The autobaud loader: one clock-agnostic image per chip, so it has no
|
||||||
|
# clock x baud axis of its own — the matrix below sweeps those for the
|
||||||
|
# fixed-baud builds, and this one binary has to serve all of them at run
|
||||||
|
# time. Size-tested against the same per-chip budget as every other variant.
|
||||||
|
pureboot_add_loader(pureboot_autobaud SERIAL autobaud)
|
||||||
|
add_test(NAME pureboot_autobaud.size
|
||||||
|
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:pureboot_autobaud>
|
||||||
|
-DLIMIT=${PUREBOOT_LIMIT} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
|
||||||
|
# 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()
|
||||||
|
|
||||||
@@ -287,24 +385,30 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
# sites), the largest image the space produces and a shape the ladder
|
# sites), the largest image the space produces and a shape the ladder
|
||||||
# default — always the *fastest* rate a clock reaches — never picks.
|
# default — always the *fastest* rate a clock reaches — never picks.
|
||||||
#
|
#
|
||||||
# Bounded to one chip per size-bearing class: flash addressing (the
|
# Every chip runs the full cross product: the size-bearing classes (flash
|
||||||
# word-addressed 1284), hand-over shape (the patched vector on the tinies
|
# addressing, hand-over shape, page size, USART inventory) are what make
|
||||||
# and m48s), page size, and USART inventory. Everything else in the image
|
# the image differ, and a chip outside them is expected to match its class
|
||||||
# is chip-independent code, so a further chip buys builds and no
|
# — but "expected" is what a matrix is for, and the whole sweep is cheap
|
||||||
# coverage; every chip outside the set carries the compact matrix.
|
# enough to run rather than reason about. PUREBOOT_FULL_MATRIX is what
|
||||||
|
# selects it; the compact matrix below is the per-commit default.
|
||||||
get_property(_full_bauds GLOBAL PROPERTY PUREBOOT_BAUD_LADDER)
|
get_property(_full_bauds GLOBAL PROPERTY PUREBOOT_BAUD_LADDER)
|
||||||
list(APPEND _full_bauds 16000 4800 2400 1200)
|
list(APPEND _full_bauds 16000 4800 2400 1200)
|
||||||
set(_matrix_spot attiny13a attiny85 atmega48pa atmega8a atmega168pa
|
if(DEFINED ENV{PUREBOOT_FULL_MATRIX})
|
||||||
atmega328p atmega164a atmega644a atmega1284p)
|
|
||||||
if(DEFINED ENV{PUREBOOT_FULL_MATRIX} AND LIBAVR_MCU IN_LIST _matrix_spot)
|
|
||||||
foreach(_matrix_hz IN LISTS _full_clocks)
|
foreach(_matrix_hz IN LISTS _full_clocks)
|
||||||
foreach(_matrix_baud IN LISTS _full_bauds)
|
foreach(_matrix_baud IN LISTS _full_bauds)
|
||||||
pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} software)
|
pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} software "")
|
||||||
|
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()
|
||||||
@@ -323,11 +427,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
|
||||||
@@ -356,6 +547,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
|
||||||
@@ -378,4 +648,67 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
${CMAKE_BINARY_DIR}/pbusart1-work usart1)
|
${CMAKE_BINARY_DIR}/pbusart1-work usart1)
|
||||||
set_tests_properties(pureboot.usart1 PROPERTIES TIMEOUT 180)
|
set_tests_properties(pureboot.usart1 PROPERTIES TIMEOUT 180)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# The autobaud loader driven end to end over the software-UART bridge:
|
||||||
|
# the host sends the 0xC0 calibration pulse, the loader times it, locks,
|
||||||
|
# and programs. Run on the near-flash 328P
|
||||||
|
# and the word-addressed 1284P — the two flash-addressing classes — and each
|
||||||
|
# at two clocks with the one binary, which is the clock-agnostic property
|
||||||
|
# autobaud exists for (test/pbautobaud.py). The fixture application banners
|
||||||
|
# over the same software link at the first clock's rate.
|
||||||
|
if(LIBAVR_MCU MATCHES "^atmega(328p|1284p)$" AND DEFINED PB_DEVICE)
|
||||||
|
add_executable(pbapp_autobaud test/pbapp.cpp)
|
||||||
|
target_link_libraries(pbapp_autobaud PRIVATE libavr)
|
||||||
|
target_compile_definitions(pbapp_autobaud PRIVATE PUREBOOT_CLOCK_HZ=1000000
|
||||||
|
PUREBOOT_BAUD=9600 PUREBOOT_SOFT_SERIAL PUREBOOT_TX=pb1)
|
||||||
|
add_custom_command(TARGET pbapp_autobaud POST_BUILD
|
||||||
|
COMMAND ${CMAKE_OBJCOPY} -O binary
|
||||||
|
$<TARGET_FILE:pbapp_autobaud> $<TARGET_FILE:pbapp_autobaud>.bin)
|
||||||
|
add_test(NAME pureboot.autobaud
|
||||||
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbautobaud.py
|
||||||
|
${PB_DEVICE} $<TARGET_FILE:pureboot_autobaud> ${PUREBOOT_SIM_MCU}
|
||||||
|
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} $<TARGET_FILE:pbapp_autobaud>.bin
|
||||||
|
1000000 9600 ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
|
${CMAKE_BINARY_DIR}/pbautobaud-work)
|
||||||
|
set_tests_properties(pureboot.autobaud PROPERTIES TIMEOUT 240)
|
||||||
|
|
||||||
|
# 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()
|
||||||
|
|||||||
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: a8ed8c4851...c01b19b08f
@@ -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)
|
||||||
@@ -194,15 +237,35 @@ function(pureboot_default_baud clock software outvar)
|
|||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
# pureboot_add_loader(<name> [CLOCK <hz>] [BAUD <bd>]
|
# pureboot_add_loader(<name> [CLOCK <hz>] [BAUD <bd>]
|
||||||
# [SERIAL auto|hardware|software] [USART <n>]
|
# [SERIAL auto|hardware|software|autobaud] [USART <n>]
|
||||||
# [RX <pin>] [TX <pin>] [TIMEOUT <s>])
|
# [RX <pin>] [TX <pin>] [TIMEOUT <s>] [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
|
||||||
|
# carries no clock and no baud: CLOCK and BAUD are not build parameters there,
|
||||||
|
# and one binary per chip serves every F_CPU and every rate. The stamped
|
||||||
|
# PUREBOOT_HZ/PUREBOOT_BAUD then record what a harness should *drive* it at,
|
||||||
|
# not what it was built for.
|
||||||
|
#
|
||||||
|
# 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()
|
||||||
@@ -222,8 +285,8 @@ function(pureboot_add_loader name)
|
|||||||
if(NOT PB_SERIAL)
|
if(NOT PB_SERIAL)
|
||||||
set(PB_SERIAL auto)
|
set(PB_SERIAL auto)
|
||||||
endif()
|
endif()
|
||||||
if(DEFINED PB_USART AND PB_SERIAL STREQUAL "software")
|
if(DEFINED PB_USART AND NOT PB_SERIAL MATCHES "^(auto|hardware)$")
|
||||||
message(FATAL_ERROR "pureboot_add_loader(${name}): USART ${PB_USART} contradicts SERIAL software")
|
message(FATAL_ERROR "pureboot_add_loader(${name}): USART ${PB_USART} contradicts SERIAL ${PB_SERIAL}")
|
||||||
endif()
|
endif()
|
||||||
if(DEFINED PB_USART)
|
if(DEFINED PB_USART)
|
||||||
set(PB_SERIAL hardware)
|
set(PB_SERIAL hardware)
|
||||||
@@ -239,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")
|
||||||
@@ -248,14 +314,26 @@ 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()
|
||||||
endif()
|
endif()
|
||||||
if(PB_SERIAL STREQUAL "software")
|
if(PB_SERIAL MATCHES "^(software|autobaud)$")
|
||||||
if(NOT PB_RX)
|
if(NOT PB_RX)
|
||||||
set(PB_RX pb0)
|
set(PB_RX pb0)
|
||||||
endif()
|
endif()
|
||||||
|
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()
|
||||||
@@ -264,12 +342,35 @@ function(pureboot_add_loader name)
|
|||||||
message(FATAL_ERROR "pureboot_add_loader(${name}): pin '${_pin}' is not of the form pb1")
|
message(FATAL_ERROR "pureboot_add_loader(${name}): pin '${_pin}' is not of the form pb1")
|
||||||
endif()
|
endif()
|
||||||
endforeach()
|
endforeach()
|
||||||
set(_serial_defines PUREBOOT_SOFT_SERIAL PUREBOOT_RX=${PB_RX} PUREBOOT_TX=${PB_TX})
|
if(PB_SERIAL STREQUAL "autobaud")
|
||||||
# sw:<RX>,<TX> as port letter and bit, upcased.
|
set(_serial_defines PUREBOOT_AUTOBAUD PUREBOOT_RX=${PB_RX} PUREBOOT_TX=${PB_TX})
|
||||||
|
else()
|
||||||
|
set(_serial_defines PUREBOOT_SOFT_SERIAL PUREBOOT_RX=${PB_RX} PUREBOOT_TX=${PB_TX})
|
||||||
|
endif()
|
||||||
|
# sw:<RX>,<TX> as port letter and bit, upcased — 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)
|
||||||
@@ -280,23 +381,45 @@ function(pureboot_add_loader name)
|
|||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
set(_defines PUREBOOT_CLOCK_HZ=${PB_CLOCK} PUREBOOT_BAUD=${PB_BAUD} PUREBOOT_TIMEOUT=${PB_TIMEOUT}
|
if(PB_SERIAL STREQUAL "autobaud")
|
||||||
${_serial_defines})
|
# No clock and no baud reach the image; the window is a poll budget.
|
||||||
|
set(_defines ${_serial_defines})
|
||||||
|
else()
|
||||||
|
set(_defines PUREBOOT_CLOCK_HZ=${PB_CLOCK} PUREBOOT_BAUD=${PB_BAUD} PUREBOOT_TIMEOUT=${PB_TIMEOUT}
|
||||||
|
${_serial_defines})
|
||||||
|
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)
|
||||||
target_compile_definitions(${name} PRIVATE ${_defines})
|
target_compile_definitions(${name} PRIVATE ${_defines})
|
||||||
# Codegen shaping for the loader TU only, worth 14–36 B depending on the
|
# Codegen shaping for the loader TU only. At -Os GCC otherwise rewrites the
|
||||||
# chip. At -Os GCC otherwise rewrites the byte-stream loops' counters into
|
# byte-stream loops' counters into end-pointer forms that cost registers
|
||||||
# end-pointer forms that cost registers (-fno-ivopts,
|
# (-fno-ivopts, -fno-split-wide-types), leaves register pressure on the
|
||||||
# -fno-split-wide-types), leaves register pressure on the table with the
|
# table with the default allocator (-fira-algorithm=priority), and keeps
|
||||||
# default allocator (-fira-algorithm=priority), and keeps loop-invariant
|
# expression temporaries in registers (-fno-tree-ter) — but every loop body
|
||||||
# immediates and expression temporaries in registers
|
# here contains a call, so a register held across it costs more than the
|
||||||
# (-fno-move-loop-invariants, -fno-tree-ter) — but every loop body here
|
# load-immediate it saves. The set is fitted to the loader's body and has to
|
||||||
# contains a call, so a register held across it costs more than the
|
# be re-measured when that body changes: -fno-move-loop-invariants belonged
|
||||||
# load-immediate it saves.
|
# here while the command loop carried four transfer bodies and costs bytes
|
||||||
target_compile_options(${name} PRIVATE
|
# now that it carries one, and -fno-ivopts is fitted per backend — an
|
||||||
-fno-ivopts -fira-algorithm=priority -fno-move-loop-invariants -fno-tree-ter -fno-split-wide-types)
|
# 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
|
||||||
|
-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}>)
|
||||||
@@ -310,3 +433,4 @@ function(pureboot_add_loader name)
|
|||||||
set_target_properties(${name} PROPERTIES PUREBOOT_HZ ${PB_CLOCK} PUREBOOT_BAUD ${PB_BAUD}
|
set_target_properties(${name} PROPERTIES PUREBOOT_HZ ${PB_CLOCK} PUREBOOT_BAUD ${PB_BAUD}
|
||||||
PUREBOOT_LINK ${_link})
|
PUREBOOT_LINK ${_link})
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
|
|||||||
@@ -2,52 +2,62 @@
|
|||||||
|
|
||||||
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
|
||||||
read/write paths take wire addresses, the write guard protects the slot the
|
transfer paths take wire addresses, the write guard protects the slot the code
|
||||||
code is *running* in (from the runtime return address), the info block is
|
is *running* in (from the runtime return address), nothing else is
|
||||||
addressed from that same anchor, and the application jump is an indirect call
|
flash-resident to address at all, and the application jump is an indirect call
|
||||||
to an absolute entry. The identical binary therefore runs from any slot with
|
to an absolute entry. The identical binary therefore runs from any slot with
|
||||||
every command intact, which makes pureboot **its own staging loader**: the
|
every command intact, which makes pureboot **its own staging loader**: the host
|
||||||
host installs the same binary one slot below the resident, jumps into it, and
|
installs the same binary one slot below the resident, jumps into it, and lets
|
||||||
lets it rewrite the resident.
|
it rewrite the resident. The lint holds it to that literally — the image must
|
||||||
|
come out byte-identical linked at a different base.
|
||||||
|
|
||||||
## Chips
|
## Chips
|
||||||
|
|
||||||
Sizes are the default configuration: the hardware USART0 at 115200 8N1 on a
|
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 largest image any of them produces is a
|
Every axis moves per build — see *Configuration*. The Autobaud column is the
|
||||||
software UART at a slow baud, which on the 1284s is 494 B, the tightest fit in
|
worst configuration the space produces for the chip: the clock-free build —
|
||||||
the whole matrix at 18 B spare.
|
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 | Size |
|
| Chip | Flash | Loader at | Link | Stock | Autobaud |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| ATtiny13, ATtiny13A † | 1 KiB | 0x0200 | software | 416 B |
|
| ATtiny13, ATtiny13A † | 1 KiB | 0x0200 | software | 384 B | 474 B |
|
||||||
| ATtiny25 † | 2 KiB | 0x0600 | software | 420 B |
|
| ATtiny25 † | 2 KiB | 0x0600 | software | 388 B | 466 B |
|
||||||
| ATtiny45 † | 4 KiB | 0x0e00 | software | 424 B |
|
| ATtiny45 † | 4 KiB | 0x0e00 | software | 388 B | 466 B |
|
||||||
| ATtiny85 † | 8 KiB | 0x1e00 | software | 424 B |
|
| ATtiny85 † | 8 KiB | 0x1e00 | software | 388 B | 466 B |
|
||||||
| ATmega8, 8A | 8 KiB | 0x1e00 | USART0 | 396 B |
|
| ATmega8, 8A | 8 KiB | 0x1e00 | USART0 | 362 B | 494 B |
|
||||||
| ATmega16, 16A | 16 KiB | 0x3e00 | USART0 | 400 B |
|
| ATmega16, 16A | 16 KiB | 0x3e00 | USART0 | 364 B | 496 B |
|
||||||
| ATmega32, 32A | 32 KiB | 0x7e00 | USART0 | 400 B |
|
| ATmega32, 32A | 32 KiB | 0x7e00 | USART0 | 364 B | 496 B |
|
||||||
| ATmega48, 48A, 48P, 48PA † | 4 KiB | 0x0e00 | USART0 | 414 B |
|
| ATmega48, 48A, 48P, 48PA † | 4 KiB | 0x0e00 | USART0 | 378 B | 468 B |
|
||||||
| ATmega88, 88A, 88P, 88PA | 8 KiB | 0x1e00 | USART0 | 434 B |
|
| ATmega88, 88A, 88P, 88PA | 8 KiB | 0x1e00 | USART0 | 388 B | 478 B |
|
||||||
| ATmega168, 168A, 168P, 168PA | 16 KiB | 0x3e00 | USART0 | 438 B |
|
| ATmega168, 168A, 168P, 168PA | 16 KiB | 0x3e00 | USART0 | 390 B | 480 B |
|
||||||
| ATmega328, 328P | 32 KiB | 0x7e00 | USART0 | 438 B |
|
| ATmega328, 328P | 32 KiB | 0x7e00 | USART0 | 390 B | 480 B |
|
||||||
| ATmega164A, 164P, 164PA | 16 KiB | 0x3e00 | USART0 | 438 B |
|
| ATmega164A, 164P, 164PA | 16 KiB | 0x3e00 | USART0 | 390 B | 480 B |
|
||||||
| ATmega324A, 324P, 324PA | 32 KiB | 0x7e00 | USART0 | 438 B |
|
| ATmega324A, 324P, 324PA | 32 KiB | 0x7e00 | USART0 | 390 B | 480 B |
|
||||||
| ATmega644, 644A, 644P, 644PA | 64 KiB | 0xfe00 | USART0 | 432 B |
|
| ATmega644, 644A, 644P, 644PA | 64 KiB | 0xfe00 | USART0 | 384 B | 474 B |
|
||||||
| ATmega1284, 1284P | 128 KiB | 0x1fe00 | USART0 | 478 B |
|
| ATmega1284, 1284P | 128 KiB | 0x1fe00 | USART0 | 410 B | 502 B |
|
||||||
|
|
||||||
† No hardware boot section: the host patches the reset vector, and the budget
|
† No hardware boot section: the host patches the reset vector, and the budget
|
||||||
is 510 bytes, since the slot's last word is the trampoline.
|
is 510 bytes, since the slot's last word is the trampoline.
|
||||||
|
|
||||||
The 1284s are the heaviest because they alone carry the far-flash machinery —
|
The tightest fit in the whole space is therefore the 1284s' 502 of their
|
||||||
ELPM reads, RAMPZ page commands, a word-addressed wire.
|
512: they alone carry the far-flash machinery (ELPM reads, RAMPZ page
|
||||||
|
commands) on top of everything the column already stacks. The flash bank
|
||||||
|
riding in a transfer's selector byte keeps even those chips' addressing the
|
||||||
|
same 16-bit form every other chip uses, which is why they are no longer the
|
||||||
|
outlier they were.
|
||||||
|
|
||||||
The software UART enables the RX pull-up; TX idles high. All multi-byte wire
|
The software UART enables the RX pull-up; TX idles high. All multi-byte wire
|
||||||
quantities are little-endian.
|
quantities are little-endian.
|
||||||
@@ -62,10 +72,12 @@ repo's build and by a downstream project alike:
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `CLOCK <hz>` | the clock the board runs | 16 MHz megas, 8 MHz t25/45/85, 9.6 MHz t13s |
|
| `CLOCK <hz>` | the clock the board runs | 16 MHz megas, 8 MHz t25/45/85, 9.6 MHz t13s |
|
||||||
| `BAUD <bd>` | the wire rate | the ladder below |
|
| `BAUD <bd>` | the wire rate | the ladder below |
|
||||||
| `SERIAL auto\|hardware\|software` | the link backend | `auto`: the hardware USART where the chip has one |
|
| `SERIAL auto\|hardware\|software\|autobaud` | the link backend | `auto`: the hardware USART where the chip has one |
|
||||||
| `USART <n>` | the USART instance (x4 megas carry two) | 0 |
|
| `USART <n>` | the USART instance (x4 megas carry two) | 0 |
|
||||||
| `RX <pin>`, `TX <pin>` | software-UART pins | `pb0`, `pb1` |
|
| `RX <pin>`, `TX <pin>` | software-UART pins | `pb0`, `pb1` |
|
||||||
| `TIMEOUT <s>` | the activation window | 8 |
|
| `TIMEOUT <s>` | the activation window | 8 |
|
||||||
|
| `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
|
||||||
@@ -74,15 +86,96 @@ 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
|
||||||
|
at run time, so `CLOCK` and `BAUD` are not build parameters there and one
|
||||||
|
binary per chip serves every clock and every rate. It is for the deployments
|
||||||
|
whose clock is not known at build time and does not hold still — the internal
|
||||||
|
RC oscillator, ±10 % from the factory and moving with supply and temperature —
|
||||||
|
where a fixed-baud software build has to be rebuilt per clock and still drifts
|
||||||
|
out of tolerance. The cost is that it is software-serial only (a hardware USART
|
||||||
|
needs its divisor programmed) and that activation counts poll iterations rather
|
||||||
|
than seconds, since there is no clock to convert them against
|
||||||
|
(`PUREBOOT_AUTOBAUD_POLLS`, default 4,000,000). 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)
|
||||||
```
|
```
|
||||||
@@ -98,17 +191,35 @@ speak to the build. This exact deployment runs the full protocol suite in CI
|
|||||||
|
|
||||||
Reset enters the loader (BOOTRST on the boot-sectioned megas, the patched
|
Reset enters the loader (BOOTRST on the boot-sectioned megas, the patched
|
||||||
reset vector elsewhere) — except a watchdog reset, which hands straight to the
|
reset vector elsewhere) — except a watchdog reset, which hands straight to the
|
||||||
application, since the application owns its watchdog and must clear WDRF
|
application with no activation window, since the application owns its watchdog.
|
||||||
itself.
|
This is deliberate: it lets an application reboot itself instantly rather than
|
||||||
|
sit through the window. The application must clear WDRF itself (libavr's
|
||||||
|
`watchdog::disable()` does). **Gotcha:** WDRF is sticky (cleared only by
|
||||||
|
software, not by a later reset), so an application that watchdog-resets and
|
||||||
|
never clears it diverts *every* subsequent reset — external ones included —
|
||||||
|
past the window too, and the loader becomes reachable only through an external
|
||||||
|
programmer until the flag is cleared. A serial recovery path therefore assumes
|
||||||
|
the application clears WDRF on its own reset path.
|
||||||
|
|
||||||
The host then knocks `p` then `b`, each awaited byte under a fresh activation
|
The host then knocks `p` then `b`, each awaited byte under a fresh activation
|
||||||
window; any other byte is discarded and awaited again, so line noise can delay
|
window; any other byte is discarded and awaited again, so line noise can delay
|
||||||
the loader but never lock it. A window expiring on an idle line boots the
|
the loader but never lock it. A window expiring on an idle line boots the
|
||||||
application.
|
application.
|
||||||
|
|
||||||
|
An autobaud build opens differently, because it has to learn the rate before it
|
||||||
|
can read a byte at all: the host sends the **calibration byte 0xC0** — a start
|
||||||
|
bit plus six zero data bits form one low pulse of seven bit-times — and the
|
||||||
|
loader times that pulse into its bit period. A single `p` then activates; the
|
||||||
|
pulse has already proven a host is present, which the two-byte knock exists to
|
||||||
|
establish elsewhere. Both waits are bounded, so a stray low pulse with no host
|
||||||
|
behind it costs one window and then boots the application rather than holding
|
||||||
|
the loader.
|
||||||
|
|
||||||
The window is a compile-time constant (`TIMEOUT`, 8 s by default), so the whole
|
The window is a compile-time constant (`TIMEOUT`, 8 s by default), so the whole
|
||||||
EEPROM belongs to the application — pureboot keeps no state of its own.
|
EEPROM belongs to the application — pureboot keeps no state of its own.
|
||||||
Re-timing a deployed loader is a self-update with a re-timed build.
|
Re-timing a deployed loader is a self-update with a re-timed build. An autobaud
|
||||||
|
build counts poll iterations instead (`PUREBOOT_AUTOBAUD_POLLS`), there being
|
||||||
|
no clock to turn into seconds.
|
||||||
|
|
||||||
## Session
|
## Session
|
||||||
|
|
||||||
@@ -118,68 +229,138 @@ write and sends the prompt `+` (0x2b), which is therefore also the previous
|
|||||||
command's completion ack. A session is: await `+`, send a command, read its
|
command's completion ack. A session is: await `+`, send a command, read its
|
||||||
reply, repeat.
|
reply, repeat.
|
||||||
|
|
||||||
On chips whose flash exceeds 64 KiB (the 1284s — info-block flag bit 1) the
|
Addresses are **byte addresses within a 64 KiB bank**, and the bank rides in
|
||||||
`R`/`W` flash addresses are **word** addresses; everywhere else they are byte
|
the command's selector byte, so no command has to speak word addresses. `J` is
|
||||||
addresses (the 644s' 64 KiB is exactly the 16-bit byte space). EEPROM
|
the exception: its address is a word address, because that is what the
|
||||||
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
|
||||||
|
against the chip. **Gotcha:** a write (or read) that runs past `E2END` wraps —
|
||||||
|
EEAR is only as wide as the array, so an address past the end truncates onto
|
||||||
|
low EEPROM and the write silently overwrites it. Keeping transfers within the
|
||||||
|
real sizes is the host's job (the shipped tool does); the flash budget is
|
||||||
|
better spent on features than on re-checking a bound the host already holds.
|
||||||
|
|
||||||
| Cmd | Arguments | Reply |
|
| Cmd | Arguments | Reply |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `b` | — | the 12-byte info block |
|
| `b` | — | 4 bytes: the pureboot version, then the three signature bytes |
|
||||||
| `R` | addr16, n8 | n flash bytes (n = 0 means 256) |
|
| `G` | sel8, addr16, n8 | n bytes from the selected space (n = 0 means 256) |
|
||||||
| `W` | addr16 (any address in the page), then one page of data | — (completion = next prompt) |
|
| `g` | sel8, addr16, n8, then n data bytes | `+` per byte, sent once its write has begun |
|
||||||
| `r` | addr16, n8 | n EEPROM bytes (n = 0 means 256) |
|
| `W` | sel8, addr16, then one page of data | — (completion = next prompt) |
|
||||||
| `w` | addr16, n8, then n data bytes | `+` per byte, sent once its write has begun |
|
| `J` | sel8 (reserved), word address (16-bit) | `+`, then execution continues there |
|
||||||
| `F` | — | 4 bytes: low fuse, lock, extended fuse, high fuse |
|
|
||||||
| `J` | word address (16-bit) | `+`, then execution continues there |
|
|
||||||
| other | — | ignored; the loop re-prompts (send a junk byte, await `+`, to resync) |
|
| other | — | ignored; the loop re-prompts (send a junk byte, await `+`, to resync) |
|
||||||
|
|
||||||
`W` streams exactly one SPM page (size from the info block) into the buffer,
|
`G` and `g` are one letter in two cases, which is the whole command set for
|
||||||
then erases and programs — except pages inside the 512-byte slot
|
every memory: the **selector** byte's low nibble names the space and its high
|
||||||
the loader is *running* in, which are drained and left alone, so a broken host
|
nibble carries the flash bank.
|
||||||
cannot brick the running copy and a staged copy may rewrite the resident.
|
|
||||||
|
| Space | | |
|
||||||
|
|---|---|---|
|
||||||
|
| 0 | flash | read-only here; it is written through `W` and the SPM space |
|
||||||
|
| 1 | EEPROM | |
|
||||||
|
| 2 | data | SRAM — and with it the register file and every I/O register, which share the data address space on AVR |
|
||||||
|
| 3 | fuse and lock | index 0..3 in the hardware's own Z order: low, lock, extended, high |
|
||||||
|
| 4 | SPM | write-only: the byte goes to SPMCSR and fires the instruction at the address |
|
||||||
|
|
||||||
|
The data space is worth more than it looks. pureboot keeps **zero static RAM**
|
||||||
|
and pushes no register, so at loader entry an application's SRAM is still
|
||||||
|
whatever the application left there, bar the handful of bytes of return-address
|
||||||
|
stack — which makes `G` over space 2 a post-mortem of a running application,
|
||||||
|
not just a poke hole. The same address space carries the register file and the
|
||||||
|
I/O registers, so peripheral state is readable too; reading some of those has
|
||||||
|
side effects (reading UDR clears its flags), which is the host's business to
|
||||||
|
know.
|
||||||
|
|
||||||
|
Programming a page is therefore `W` to fill the buffer, then a `g` to the SPM
|
||||||
|
space for the erase, another for the write, and on a boot-sectioned chip a
|
||||||
|
third to re-enable the RWW section — `0x03`, `0x05` and `0x11`, the SPMCSR
|
||||||
|
encodings every part pureboot targets shares. The loader carries no page-commit
|
||||||
|
logic of its own, and the same primitive reaches every other SPM operation,
|
||||||
|
lock bits included.
|
||||||
|
|
||||||
|
The SPM store and the SPM instruction must issue within four cycles of each
|
||||||
|
other (§26.2), which no host can hit across a serial link — so this one
|
||||||
|
primitive is *fused* rather than being a poke of SPMCSR followed by a poke of
|
||||||
|
something else. That four-cycle window is the floor on how low-level a
|
||||||
|
bootloader's primitives can go; it is not a byte-count decision.
|
||||||
|
|
||||||
|
An SPM command aimed at the 512-byte slot the loader is **running in** is
|
||||||
|
dropped, so a broken host cannot brick the running copy, while a staged copy
|
||||||
|
one slot lower may rewrite the resident — which is what a self-update is.
|
||||||
|
|
||||||
The loader never clears the SPM buffer before a fill, so **one `W` may program
|
The loader never clears the SPM buffer before a fill, so **one `W` may program
|
||||||
the wrong bytes, and the host is what fixes it**. The buffer is write-once per
|
the wrong bytes, and the host is what fixes it**. The buffer is write-once per
|
||||||
word until cleared, and two things leave words in it: a refused page, and —
|
word until cleared, and two things leave words in it: a refused page, and —
|
||||||
where SPM runs from anywhere, the tinies and the m48s — an application that
|
where SPM runs from anywhere, the tinies and the m48s — an application that
|
||||||
self-programmed before entering. The next `W` takes those stale words and
|
self-programmed before entering. The next page write takes those stale words
|
||||||
clears them, since a page write auto-erases the buffer (§26.2.1; §19.2 on the
|
and clears them, since a page write auto-erases the buffer (§26.2.1; §19.2 on
|
||||||
tinies), so repeating it programs correctly. The host therefore verifies every
|
the tinies), so repeating it programs correctly. The host therefore verifies
|
||||||
page it writes and rewrites what comes back wrong (three retries, then it
|
every page it writes and rewrites what comes back wrong (three retries, then it
|
||||||
stops).
|
stops).
|
||||||
|
|
||||||
`w` is host-paced: send the next byte only after the previous byte's `+`. `F`
|
`g` is host-paced: send the next byte only after the previous byte's `+`. Fuse
|
||||||
returns the bytes in the hardware's Z order; on a chip without an extended
|
*writing* does not exist — SPM reaches flash and boot lock bits only.
|
||||||
fuse byte that slot carries no meaning. Fuse *writing* does not exist — SPM
|
|
||||||
reaches flash and boot lock bits only.
|
|
||||||
|
|
||||||
`J` is the one control-transfer primitive: it runs the application (word 0 or
|
`J` is the one control-transfer primitive: it runs the application (word 0 or
|
||||||
the trampoline word, both known from the info block) and moves between loader
|
the trampoline word, both derived from the chip) and moves between loader
|
||||||
copies during a self-update. A jump to a slot's base re-enters that copy's own
|
copies during a self-update. A jump to a slot's base re-enters that copy's own
|
||||||
startup, which must then be knocked afresh.
|
startup, which must then be knocked afresh.
|
||||||
|
|
||||||
The info block (`b`):
|
`b` answers with the loader's identity — its version and the chip's signature —
|
||||||
|
and nothing else. Everything else the host needs (page size, loader base,
|
||||||
|
EEPROM size, whether the reset vector must be patched, how many flash banks)
|
||||||
|
follows from the signature, and the host holds that table; the loader derived
|
||||||
|
the same facts from its own chip database at build time, so nothing is guessed,
|
||||||
|
it is simply not sent twice.
|
||||||
|
|
||||||
| Offset | Content |
|
An update image, though, is a bare 512-byte slot with no device to ask, and
|
||||||
|---|---|
|
installing one built for another chip bricks the target. Every loader image
|
||||||
| 0–2 | `'P'`, `'B'`, pureboot version (3) |
|
therefore carries a six-byte **stamp** — `'P'`, `'B'`, the version, the three
|
||||||
| 3–5 | device signature |
|
signature bytes — which the loader itself never reads and the host tool refuses
|
||||||
| 6 | SPM page size in bytes (0 means 256) |
|
to install a mismatch against.
|
||||||
| 7–8 | loader base — application flash ends here (a word address when bit 1 is set) |
|
|
||||||
| 9–10 | EEPROM size |
|
|
||||||
| 11 | bit 0: host must patch the reset vector (no hardware boot section); bit 1: flash wire addresses are word addresses |
|
|
||||||
|
|
||||||
## Version
|
## Version
|
||||||
|
|
||||||
The info block's third byte is the **pureboot version** — the loader's one
|
`b`'s first byte is the **pureboot version** — the loader's one identity
|
||||||
identity number, and the only way to tell what a deployed loader is. Nothing
|
number, and the only way to tell what a deployed loader is. Nothing else is
|
||||||
else is numbered: the wire protocol has no version, a pureboot version implies
|
numbered: the wire protocol has no version, a pureboot version implies it, and
|
||||||
it, and the host tool holds that map. The tool states the window of loader
|
the host tool holds that map. The tool states the window of loader versions it
|
||||||
versions it speaks (`OLDEST_LOADER`/`NEWEST_LOADER` in `pureboot.py`), and a
|
speaks (`OLDEST_LOADER`/`NEWEST_LOADER` in `pureboot.py`), and a version that
|
||||||
version that changes the protocol becomes the new floor there. None has so
|
changes the protocol becomes the new floor there. A loader newer than the tool
|
||||||
far: 1 through 3 speak the identical session. A loader newer than the tool is
|
is refused by name rather than decoded on the assumption that nothing moved.
|
||||||
refused by name rather than decoded on the assumption that nothing moved.
|
|
||||||
|
Two generations exist. **1 through 4** speak one session — a 12-byte info block
|
||||||
|
from `b`, and a command per memory (`R`/`W` flash, `r`/`w` EEPROM, `F` fuses).
|
||||||
|
**5** replaced those with the single `G`/`g` pair over selector-named spaces
|
||||||
|
above; the shipped tool speaks both, choosing on the version it reads, so a
|
||||||
|
deployed pureboot 4 stays drivable and self-updatable to 5. **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.
|
||||||
|
|
||||||
|
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
|
||||||
|
version: the data space, the host-issued SPM operations and the fuses now share
|
||||||
|
the loop, the cursor and the argument decode that `R`/`r`/`w` each carried a
|
||||||
|
copy of. The loader shrank while gaining all three.
|
||||||
|
|
||||||
The tool carries its own version, free to drift; `--version` prints it and the
|
The tool carries its own version, free to drift; `--version` prints it and the
|
||||||
window.
|
window.
|
||||||
@@ -231,6 +412,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
|
||||||
@@ -238,11 +431,32 @@ with any pureboot build — a re-timed window, a newer version — using the
|
|||||||
loader itself as its own staging loader. The image is the loader's own 512
|
loader itself as its own staging loader. The image is the loader's own 512
|
||||||
bytes as a raw binary, or the Intel HEX the build emits beside it.
|
bytes as a raw binary, or the Intel HEX the build emits beside it.
|
||||||
|
|
||||||
The preflight refuses an image built for another chip: the info block embedded
|
One thing the image cannot tell the host: **which link it speaks.** The update
|
||||||
in every pureboot binary (signature, page size, loader base, EEPROM size,
|
works by entering copies of the *new* image (steps 3 and 4 below), so a build
|
||||||
flags) must match the device's own, and the error names both. Die revisions
|
made for another baud or another backend answers on that one and not on the
|
||||||
share their base signature and geometry, so their images are interchangeable —
|
session's — and 512 bytes of position-independent code carry no header to read
|
||||||
as the silicon is.
|
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
|
||||||
|
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
|
||||||
|
are interchangeable — as the silicon is.
|
||||||
|
|
||||||
1. The staging slot `[base−512, base)` is saved to a host-side state file (on
|
1. The staging slot `[base−512, base)` is saved to a host-side state file (on
|
||||||
the 1 KB tiny13s that is the whole application, vectors included).
|
the 1 KB tiny13s that is the whole application, vectors included).
|
||||||
@@ -259,10 +473,15 @@ 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).
|
||||||
|
|
||||||
@@ -279,8 +498,8 @@ to reset gets its reset pulse and opens the activation window by itself.
|
|||||||
--info --fuses --flash app.hex
|
--info --fuses --flash app.hex
|
||||||
|
|
||||||
Operations run in a fixed order within one session: info, fuses, loader
|
Operations run in a fixed order within one session: info, fuses, loader
|
||||||
update, flash (erase / program / read / verify), EEPROM (the same) — then the
|
update, flash (erase / program / read / verify), EEPROM (the same), then
|
||||||
loader hands over to the application. `--stay` keeps the session alive
|
`--peek`/`--poke` — then the loader hands over to the application. `--stay` keeps the session alive
|
||||||
instead, and a later invocation reconnects into it. `--flash` and `--eeprom`
|
instead, and a later invocation reconnects into it. `--flash` and `--eeprom`
|
||||||
verify by read-back unless `--no-verify`, and a flash page that reads back
|
verify by read-back unless `--no-verify`, and a flash page that reads back
|
||||||
wrong is rewritten up to three times before the run stops (see `W` above).
|
wrong is rewritten up to three times before the run stops (see `W` above).
|
||||||
@@ -288,16 +507,63 @@ wrong is rewritten up to three times before the run stops (see `W` above).
|
|||||||
extension. `--force` overrides the refusable safety checks — today, flashing
|
extension. `--force` overrides the refusable safety checks — today, flashing
|
||||||
application data into a mega's reset walk region.
|
application data into a mega's reset walk region.
|
||||||
|
|
||||||
Readouts come one fact per line: `--info` decodes the info block field by
|
`--autobaud` opens with the calibration pulse instead of the plain knock, for a
|
||||||
field, `--fuses` each fuse byte plus, on a boot-sectioned mega, its decoded
|
loader built `SERIAL autobaud`; the rest of the session is identical, at
|
||||||
meaning. Transfers that take wire time draw a transient progress bar on stderr
|
whatever `--baud` the host chose. 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) —
|
||||||
|
SRAM, and through the same address space the register file and every I/O
|
||||||
|
register. Reading an I/O register can have side effects (reading UDR clears its
|
||||||
|
flags), which is the caller's business to know.
|
||||||
|
|
||||||
|
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
|
||||||
|
signature and the geometry that follows from them, `--fuses` each fuse byte
|
||||||
|
plus, on a boot-sectioned mega, its decoded meaning. Transfers that take wire time draw a transient progress bar on stderr
|
||||||
when it is a tty. `-v`/`--verbose` adds the decisions as they happen: knock
|
when it is a tty. `-v`/`--verbose` adds the decisions as they happen: knock
|
||||||
counts, the programming plan, update state handling and per-phase page counts.
|
counts, the programming plan, update state handling and per-phase page counts.
|
||||||
|
|
||||||
## 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;
|
||||||
@@ -307,23 +573,50 @@ 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
|
||||||
- `pbm_*.size` — under `--full`, the exhaustive cross product replacing that
|
release that USART, so `pureboot_{sw,autobaud}_on_usart{0,1}` build there
|
||||||
compact matrix: every plausible oscillator (the internal ones, the CKDIV8
|
too. The timeout is a constant and is no axis;
|
||||||
floor, the plain and the UART crystals) × every rate reachable from it ×
|
- `pureboot_autobaud.size` — the clock-free build, which has no clock or baud
|
||||||
every backend, unreachable combinations dropping out rather than aborting
|
axis of its own: one binary per chip has to serve every point the matrix
|
||||||
the configure. Bounded to one chip per size-bearing class — flash
|
below sweeps. `pureboot*osccal*.size` add the `OSCCAL` trim on the stock
|
||||||
addressing, hand-over shape, page size, USART inventory — since everything
|
shape and on the tightest image in the space (autobaud on a USART's own
|
||||||
else in the image is chip-independent code;
|
pins), holding both of the trim write's addressing encodings to the budget;
|
||||||
- `pureboot.pi` — the position-independence lint: no absolute `jmp`/`call`, the
|
- `pureboot_autobaud.unit` — the measured bit period sits where `--info`
|
||||||
info block within the image's first 256 bytes;
|
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
|
||||||
|
replacing that compact matrix, on **every** chip: every plausible oscillator
|
||||||
|
(the internal ones, the CKDIV8 floor, the plain and the UART crystals) ×
|
||||||
|
every rate reachable from it × every backend, unreachable combinations
|
||||||
|
dropping out rather than aborting the configure. Thousands of points per
|
||||||
|
chip, and cheap enough to run rather than reason about;
|
||||||
|
- `pureboot.pi` — the position-independence lint: no absolute `jmp`/`call`, no
|
||||||
|
flash-resident section but `.text`, and the image byte-identical when linked
|
||||||
|
at a different base — which is position independence itself rather than a
|
||||||
|
proxy for it;
|
||||||
|
- `pureboot.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
|
||||||
@@ -340,6 +633,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
|
||||||
@@ -347,7 +646,59 @@ Per chip preset, `ctest` runs:
|
|||||||
anywhere, which is what makes the path constructible;
|
anywhere, which is what makes the path constructible;
|
||||||
- `pureboot.update` — the full `--update-loader` flow, then every power-fail
|
- `pureboot.update` — the full `--update-loader` flow, then every power-fail
|
||||||
phase: the device is killed mid-write, restarted from its flash dump, and a
|
phase: the device is killed mid-write, restarted from its flash dump, and a
|
||||||
re-run must complete the update with the application intact.
|
re-run must complete the update with the application intact;
|
||||||
|
- `pureboot.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
|
||||||
|
bridge: the calibration handshake, a flash + EEPROM + fuse round trip against
|
||||||
|
the simulator's own memory, a data-space round trip, the hand-over — then the
|
||||||
|
same binary again at double the clock, which is the property the backend
|
||||||
|
exists for. 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.
|
||||||
|
|
||||||
`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,6 +1,6 @@
|
|||||||
// 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.
|
||||||
//
|
//
|
||||||
@@ -10,6 +10,8 @@
|
|||||||
// is what makes a copy one slot below able to rewrite the resident one, and
|
// is what makes a copy one slot below able to rewrite 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).
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
#include <libavr/libavr.hpp>
|
#include <libavr/libavr.hpp>
|
||||||
|
|
||||||
using namespace avr::literals;
|
using namespace avr::literals;
|
||||||
@@ -26,70 +28,138 @@ constexpr std::uint8_t ack = '+';
|
|||||||
|
|
||||||
// Deployment parameters come from the build (pureboot_add_loader()). The
|
// Deployment parameters come from the build (pureboot_add_loader()). The
|
||||||
// signature is not one of them: the chip database is the only universal
|
// signature is not one of them: the chip database is the only universal
|
||||||
// source — a tiny13A cannot read its own signature row from code.
|
// source — a tiny13A cannot read its own signature row from code. An autobaud
|
||||||
#if !defined(PUREBOOT_CLOCK_HZ) || !defined(PUREBOOT_BAUD)
|
// build carries no clock and no baud at all; it measures both.
|
||||||
|
#if !defined(PUREBOOT_AUTOBAUD) && (!defined(PUREBOOT_CLOCK_HZ) || !defined(PUREBOOT_BAUD))
|
||||||
#error \
|
#error \
|
||||||
"PUREBOOT_CLOCK_HZ and PUREBOOT_BAUD select this build's clock and baud — create loader targets with pureboot_add_loader() (README.md)"
|
"PUREBOOT_CLOCK_HZ and PUREBOOT_BAUD select this build's clock and baud — create loader targets with pureboot_add_loader(), or PUREBOOT_AUTOBAUD for a clock-free one (README.md)"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if !defined(PUREBOOT_AUTOBAUD)
|
||||||
using dev = avr::device<{.clock = avr::hertz_t{PUREBOOT_CLOCK_HZ}}>;
|
using dev = avr::device<{.clock = avr::hertz_t{PUREBOOT_CLOCK_HZ}}>;
|
||||||
constexpr avr::baud_t wire_baud{PUREBOOT_BAUD};
|
constexpr avr::baud_t wire_baud{PUREBOOT_BAUD};
|
||||||
|
#endif
|
||||||
// The watchdog reset flag's home: MCUSR, or the classic megas' MCUCSR.
|
|
||||||
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.
|
||||||
constexpr std::uint16_t slot_bytes = 512;
|
constexpr std::uint16_t slot_bytes = 512;
|
||||||
constexpr std::uint32_t base = spm::flash_bytes - slot_bytes;
|
|
||||||
constexpr std::uint16_t page = spm::page_bytes;
|
constexpr std::uint16_t page = spm::page_bytes;
|
||||||
constexpr bool boot_section = avr::hw::curated::has_boot_section();
|
constexpr bool boot_section = avr::hw::curated::has_boot_section();
|
||||||
|
|
||||||
// Past 64 KiB a byte address no longer fits the wire's 16 bits, so flash
|
// Past 64 KiB one bank of flash does not cover the chip, so a transfer's
|
||||||
// addresses there are word addresses ('J' always was one). A slot is 256 of
|
// selector byte carries the bank and the wire address stays a byte address
|
||||||
// those — one value of a wire address's high byte, where 512 bytes span two.
|
// within it. 'J' is the exception: it is a word address everywhere, because
|
||||||
constexpr bool word_flash = spm::flash_bytes > 65536;
|
// that is what the hardware's own jump takes.
|
||||||
constexpr std::uint16_t wire_base =
|
constexpr bool banked_flash = spm::flash_bytes > 65536;
|
||||||
word_flash ? static_cast<std::uint16_t>(base / 2) : static_cast<std::uint16_t>(base);
|
|
||||||
|
|
||||||
// A compile-time window, so the whole EEPROM belongs to the application;
|
// A compile-time window, so the whole EEPROM belongs to the application;
|
||||||
// re-timing a deployed loader is a self-update with a re-timed build.
|
// re-timing a deployed loader is a self-update with a re-timed build. An
|
||||||
|
// autobaud build has no clock to convert seconds against and counts polls.
|
||||||
#if !defined(PUREBOOT_TIMEOUT)
|
#if !defined(PUREBOOT_TIMEOUT)
|
||||||
#define PUREBOOT_TIMEOUT 8
|
#define PUREBOOT_TIMEOUT 8
|
||||||
#endif
|
#endif
|
||||||
constexpr std::uint8_t timeout_seconds = PUREBOOT_TIMEOUT;
|
constexpr std::uint8_t timeout_seconds = PUREBOOT_TIMEOUT;
|
||||||
|
|
||||||
|
#if !defined(PUREBOOT_AUTOBAUD_POLLS)
|
||||||
|
#define PUREBOOT_AUTOBAUD_POLLS 4000000
|
||||||
|
#endif
|
||||||
|
constexpr avr::uint24_t autobaud_budget = PUREBOOT_AUTOBAUD_POLLS;
|
||||||
|
|
||||||
|
// 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 = 3;
|
constexpr std::uint8_t version = 8;
|
||||||
|
|
||||||
// The 'b' reply, byte for byte (layout: README.md). Flash-resident because
|
// The image's identity stamp, for the host tool rather than for the wire: an
|
||||||
// no crt copies a .data image — and flash_table's storage carries the word
|
// update image is a bare 512-byte slot, and without this nothing in it says
|
||||||
// alignment 'b' needs to halve the address on the large chips.
|
// which chip it was built for. The tool refuses to install an image whose
|
||||||
inline constexpr avr::flash_table<std::array<std::uint8_t, 12>{
|
// stamp does not match the device — flashing a foreign loader bricks the
|
||||||
'P', 'B', version, avr::hw::db.signature[0], avr::hw::db.signature[1], avr::hw::db.signature[2],
|
// target, and the loader itself cannot check what has already replaced it.
|
||||||
static_cast<std::uint8_t>(page), // 0 means 256
|
//
|
||||||
wire_base & 0xff, wire_base >> 8, avr::hw::db.mem.eeprom_size & 0xff, avr::hw::db.mem.eeprom_size >> 8,
|
// Never read from flash by the loader — 'b' answers out of this array, but at
|
||||||
static_cast<std::uint8_t>((boot_section ? 0 : 1) | (word_flash ? 2 : 0)), // patch-vector, word-addressed
|
// constant indices, so those fold to immediates and no runtime address of it
|
||||||
}>
|
// is ever formed. `used` keeps the compiler from dropping the copy the host
|
||||||
info_data;
|
// needs and `retain` keeps --gc-sections from collecting it.
|
||||||
|
// clang-format off
|
||||||
|
[[gnu::used, gnu::retain, gnu::section(".text.stamp")]]
|
||||||
|
inline constexpr std::uint8_t identity_stamp[]{
|
||||||
|
'P', 'B', // the magic the host scans an image for
|
||||||
|
version, // and from here on, exactly what 'b' answers
|
||||||
|
avr::hw::db.signature[0],
|
||||||
|
avr::hw::db.signature[1],
|
||||||
|
avr::hw::db.signature[2],
|
||||||
|
};
|
||||||
|
// clang-format on
|
||||||
|
// Where the identity proper starts: past the magic the host scans for.
|
||||||
|
constexpr std::uint8_t stamp_identity = 2;
|
||||||
|
|
||||||
// The serial link, per the build's PUREBOOT_USART / PUREBOOT_SOFT_SERIAL,
|
// The address spaces a transfer can name, in a selector byte's low nibble.
|
||||||
// defaulting to the chip's USART0 where it has one. The software receiver is
|
// Flash is 0 so it is the cheapest to select.
|
||||||
// the polled one: the vector table belongs to the application. Templates on
|
//
|
||||||
// the clock, so only the selected backend instantiates. pending() is the
|
// spm_ops is the one that is not memory: a write there hands its byte to
|
||||||
// cheap line test the activation window polls; drain() holds until the last
|
// SPMCSR and fires the instruction at the transfer's address, which is how
|
||||||
// frame is off the wire, so a hand-over cannot let the target's re-init clip
|
// page erase, page write and RWW re-enable reach the wire without the loader
|
||||||
// the ack.
|
// carrying a command for each. The hardware's four-cycle store-to-SPM window
|
||||||
|
// is why this is one fused primitive and not a poke of SPMCSR — no host can
|
||||||
|
// hit that window across a serial link.
|
||||||
|
enum : std::uint8_t { sp_flash = 0, sp_eeprom = 1, sp_data = 2, sp_fuse = 3, sp_spm = 4 };
|
||||||
|
|
||||||
|
// A selector's high nibble is the flash bank — the address bits above the
|
||||||
|
// 16-bit wire address, RAMPZ on the chips that have one. Keeping it here
|
||||||
|
// rather than widening the wire address is what lets one 16-bit cursor serve
|
||||||
|
// every space: a 24-bit cursor would pay its extra byte on EEPROM and data
|
||||||
|
// reads that can never need it.
|
||||||
|
[[gnu::always_inline]] inline std::uint8_t space_of(std::uint8_t selector)
|
||||||
|
{
|
||||||
|
return selector & 0x0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[gnu::always_inline]] inline std::uint8_t bank_of(std::uint8_t selector)
|
||||||
|
{
|
||||||
|
return static_cast<std::uint8_t>(selector >> 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The slot a flash address falls in, as one byte. A slot is half as many words
|
||||||
|
// as bytes, so the word address's high byte is exactly this index — which is
|
||||||
|
// what lets the write guard compare a single byte, and what the running copy's
|
||||||
|
// own return address yields for free.
|
||||||
|
constexpr std::uint8_t slot_shift = std::countr_zero(slot_bytes);
|
||||||
|
constexpr std::uint8_t bank_shift = 16 - slot_shift;
|
||||||
|
|
||||||
|
[[gnu::always_inline]] inline std::uint8_t slot_of([[maybe_unused]] std::uint8_t bank, std::uint16_t at)
|
||||||
|
{
|
||||||
|
const auto within = static_cast<std::uint8_t>(at >> slot_shift);
|
||||||
|
if constexpr (banked_flash)
|
||||||
|
return static_cast<std::uint8_t>((bank << bank_shift) | within);
|
||||||
|
else
|
||||||
|
return within;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The serial link, per the build's PUREBOOT_USART / PUREBOOT_SOFT_SERIAL /
|
||||||
|
// PUREBOOT_AUTOBAUD, defaulting to the chip's USART0 where it has one. The
|
||||||
|
// software receiver is the polled one: the vector table belongs to the
|
||||||
|
// application. Templates on the clock, so only the selected backend
|
||||||
|
// instantiates. pending() is the cheap line test the activation window polls;
|
||||||
|
// drain() holds until the last frame is off the wire, so a hand-over cannot
|
||||||
|
// let the target's re-init clip the ack.
|
||||||
#if defined(PUREBOOT_SOFT_SERIAL) && defined(PUREBOOT_USART)
|
#if defined(PUREBOOT_SOFT_SERIAL) && defined(PUREBOOT_USART)
|
||||||
#error "PUREBOOT_SOFT_SERIAL and PUREBOOT_USART select opposing serial backends"
|
#error "PUREBOOT_SOFT_SERIAL and PUREBOOT_USART select opposing serial backends"
|
||||||
#endif
|
#endif
|
||||||
|
#if defined(PUREBOOT_AUTOBAUD) && defined(PUREBOOT_USART)
|
||||||
|
#error "PUREBOOT_AUTOBAUD measures a software link; it cannot drive a hardware USART"
|
||||||
|
#endif
|
||||||
|
#if defined(PUREBOOT_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
|
||||||
@@ -97,18 +167,50 @@ inline constexpr avr::flash_table<std::array<std::uint8_t, 12>{
|
|||||||
#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
|
#endif
|
||||||
|
|
||||||
template <avr::hertz_t C>
|
// One-wire on the hardware USART (PUREBOOT_HALF_DUPLEX): RXD and TXD tied
|
||||||
struct hardware_link {
|
// together off-chip, exactly one direction enabled at a time — the library's
|
||||||
using uart = avr::uart::usart<usart_digit, C, {.baud = wire_baud, .max_baud_error = 2.5_pct}>;
|
// .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
|
||||||
|
|
||||||
// The compiled idle poll: lds UCSR0A (2), sbrc skipping the exit (2),
|
template <avr::hertz_t C, avr::baud_t B>
|
||||||
// sbiw + sbci + sbci + brne (6).
|
struct hardware_link {
|
||||||
static constexpr std::uint8_t poll_cycles = 10;
|
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 around the window's narrow (uint24_t) countdown:
|
||||||
|
// the RXC test, then sbiw + sbci + brne (5). The test's cost follows the
|
||||||
|
// 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()
|
||||||
{
|
{
|
||||||
@@ -132,18 +234,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>
|
template <avr::hertz_t C, avr::baud_t B>
|
||||||
struct software_link {
|
struct software_link {
|
||||||
using rx_t = avr::uart::software_rx_polled<C, avr::PUREBOOT_RX, wire_baud>;
|
// RX == TX is the one-wire deployment: the transmitter becomes a guest
|
||||||
using tx_t = avr::uart::software_tx<C, avr::PUREBOOT_TX, wire_baud>;
|
// 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 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()
|
||||||
{
|
{
|
||||||
@@ -171,14 +281,50 @@ struct software_link {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
#if defined(PUREBOOT_USART)
|
// The clock-free link: the bit period is measured from the host's calibration
|
||||||
static_assert(avr::uart::has_usart<usart_digit>(), "PUREBOOT_USART selects a hardware USART this chip does not have");
|
// pulse instead of derived from a clock, so one image serves every F_CPU and
|
||||||
using link = hardware_link<dev::clock>;
|
// every rate. Activation differs in kind from the other two — there is no
|
||||||
|
// clock to time a window against — so this backend brings its own, below.
|
||||||
|
struct autobaud_link {
|
||||||
|
// 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()
|
||||||
|
{
|
||||||
|
avr::init<uart>();
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::uint8_t rx()
|
||||||
|
{
|
||||||
|
return uart::template read<off>();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void tx(std::uint8_t byte)
|
||||||
|
{
|
||||||
|
uart::template write<off>(byte);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void drain()
|
||||||
|
{
|
||||||
|
// 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)
|
||||||
|
using link = autobaud_link;
|
||||||
|
#elif defined(PUREBOOT_USART)
|
||||||
|
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>;
|
||||||
#elif defined(PUREBOOT_SOFT_SERIAL)
|
#elif defined(PUREBOOT_SOFT_SERIAL)
|
||||||
using link = software_link<dev::clock>;
|
using link = software_link<dev::clock, wire_baud>;
|
||||||
#else
|
#else
|
||||||
using link =
|
using link = std::conditional_t<avr::uart::has_usart<usart_unit>(), hardware_link<dev::clock, wire_baud>,
|
||||||
std::conditional_t<avr::uart::has_usart<usart_digit>(), hardware_link<dev::clock>, software_link<dev::clock>>;
|
software_link<dev::clock, wire_baud>>;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// The application's entry, pinned by the linker (--defsym): word 0 on a
|
// The application's entry, pinned by the linker (--defsym): word 0 on a
|
||||||
@@ -193,21 +339,65 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The window as one 32-bit countdown, divided by the backend's counted
|
// Activation: a bounded wait for the host, then the knock. Both forms boot the
|
||||||
// poll-loop cycles. Whole seconds is all it promises.
|
// application when the window closes on an idle line, and both bound *every*
|
||||||
|
// wait — a knock awaited without a deadline would let one stray edge hold an
|
||||||
|
// unattended device in the loader forever.
|
||||||
|
#if defined(PUREBOOT_AUTOBAUD)
|
||||||
|
// The window is a fixed poll budget: with no clock, whole seconds cannot be
|
||||||
|
// timed. A uint24_t holds it — a fourth byte would cost two words at every
|
||||||
|
// countdown step for range never used.
|
||||||
|
void await_host()
|
||||||
|
{
|
||||||
|
for (;;) {
|
||||||
|
if (!link::uart::calibrate(autobaud_budget))
|
||||||
|
run_app();
|
||||||
|
// The calibration pulse has already proven a host is there, so one
|
||||||
|
// byte activates. A knock that never arrives falls back to calibrate(),
|
||||||
|
// whose own budget then boots the application.
|
||||||
|
if (link::uart::template read<off>(autobaud_budget) == 'p')
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
// The window as one countdown, divided by the backend's counted poll-loop
|
||||||
|
// 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;
|
||||||
@@ -224,6 +414,14 @@ std::uint8_t rx_deadline()
|
|||||||
return link::rx();
|
return link::rx();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void await_host()
|
||||||
|
{
|
||||||
|
// 'p' then 'b', each under a fresh window; anything else is line noise.
|
||||||
|
while (rx_deadline() != 'p' || rx_deadline() != 'b') {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// Inlined: read across a call, the first byte strands in a call-saved
|
// Inlined: read across a call, the first byte strands in a call-saved
|
||||||
// register the caller has to push and pop.
|
// register the caller has to push and pop.
|
||||||
[[gnu::always_inline]] inline std::uint16_t rx16()
|
[[gnu::always_inline]] inline std::uint16_t rx16()
|
||||||
@@ -241,157 +439,116 @@ std::uint8_t rx_deadline()
|
|||||||
return std::bit_cast<std::uint16_t>(pair);
|
return std::bit_cast<std::uint16_t>(pair);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Counts arrive in the wire's 8-bit form: 0 means 256. Both streamers fold
|
// Out of line: several sites send it, and a call is shorter than a
|
||||||
// into the one command that reads flash, which is what lets the far one's
|
// load-immediate at each.
|
||||||
// 24-bit cursor sit in the command loop's own call-saved registers.
|
|
||||||
[[maybe_unused, gnu::always_inline]] inline void send_flash_near(std::uint16_t address, std::uint8_t count)
|
|
||||||
{
|
|
||||||
do
|
|
||||||
link::tx(avr::flash_load(reinterpret_cast<const std::uint8_t *>(address++)));
|
|
||||||
while (--count);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The 24-bit cursor as the machine holds it — the RAMPZ byte and a 16-bit Z,
|
|
||||||
// carried apart; the reassembled address folds away inside the far load.
|
|
||||||
[[maybe_unused, gnu::always_inline]] inline void send_flash_far(std::uint16_t address, std::uint8_t count)
|
|
||||||
{
|
|
||||||
std::uint8_t rampz = static_cast<std::uint8_t>(address >> 15);
|
|
||||||
std::uint16_t z = static_cast<std::uint16_t>(address << 1);
|
|
||||||
do {
|
|
||||||
link::tx(avr::flash_load_far<std::uint8_t>((static_cast<std::uint32_t>(rampz) << 16) | z));
|
|
||||||
// Carrying the wrap is smaller than the flat 32-bit cursor GCC
|
|
||||||
// builds without it.
|
|
||||||
if (++z == 0)
|
|
||||||
++rampz;
|
|
||||||
} while (--count);
|
|
||||||
}
|
|
||||||
|
|
||||||
[[gnu::always_inline]] inline void send_flash(std::uint16_t address, std::uint8_t count)
|
|
||||||
{
|
|
||||||
if constexpr (word_flash)
|
|
||||||
send_flash_far(address, count);
|
|
||||||
else
|
|
||||||
send_flash_near(address, count);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Out of line: three sites send it, and a call is shorter than three
|
|
||||||
// load-immediates.
|
|
||||||
[[gnu::noinline]] void tx_ack()
|
[[gnu::noinline]] void tx_ack()
|
||||||
{
|
{
|
||||||
link::tx(ack);
|
link::tx(ack);
|
||||||
}
|
}
|
||||||
|
|
||||||
void send_eeprom(std::uint16_t address, std::uint8_t count)
|
// A wire address and its selector's bank as the flash address they name.
|
||||||
|
[[gnu::always_inline]] inline spm::flash_address_t flash_address([[maybe_unused]] std::uint8_t bank, std::uint16_t at)
|
||||||
{
|
{
|
||||||
do
|
if constexpr (banked_flash)
|
||||||
link::tx(ee::read(address++));
|
return (static_cast<spm::flash_address_t>(bank) << 16) | at;
|
||||||
while (--count);
|
else
|
||||||
|
return at;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Host-paced: the ack goes out once the write has begun, so the next byte
|
// One byte out of any space. Every accessor shares the transfer's cursor, its
|
||||||
// arrives while it completes and nothing is missed without a buffer.
|
// loop and its call site, so a space costs only its own instruction rather
|
||||||
void store_eeprom(std::uint16_t address, std::uint8_t count)
|
// than a body, a loop and a dispatch arm of its own.
|
||||||
|
[[gnu::always_inline]] inline std::uint8_t load(std::uint8_t space, [[maybe_unused]] std::uint8_t bank,
|
||||||
|
std::uint16_t at)
|
||||||
{
|
{
|
||||||
do {
|
if (space == sp_eeprom)
|
||||||
ee::write<off>(address++, link::rx());
|
return ee::read(at);
|
||||||
tx_ack();
|
if (space == sp_data)
|
||||||
} while (--count);
|
return *reinterpret_cast<volatile std::uint8_t *>(at);
|
||||||
|
if (space == sp_fuse)
|
||||||
|
return spm::read_fuse<off>(static_cast<spm::fuse>(at));
|
||||||
|
if constexpr (banked_flash)
|
||||||
|
return avr::flash_load_far<std::uint8_t>(flash_address(bank, at));
|
||||||
|
else
|
||||||
|
return avr::flash_load(reinterpret_cast<const std::uint8_t *>(at));
|
||||||
}
|
}
|
||||||
|
|
||||||
// One page into the SPM buffer, then erase and program — except the slot
|
// One byte into a writable space. Flash is not one of them — it arrives a
|
||||||
// this code is running in (`slot_high`, from run()), which is drained and
|
// page at a time through 'W' and is committed through sp_spm — and the fuses
|
||||||
// left alone. A broken host therefore cannot brick the running loader, and a
|
// are not writable at all: SPM reaches flash and boot lock bits only.
|
||||||
// copy one slot lower may rewrite the resident one.
|
[[gnu::always_inline]] inline void store(std::uint8_t space, std::uint8_t bank, std::uint16_t at, std::uint8_t value,
|
||||||
|
std::uint8_t slot_high)
|
||||||
|
{
|
||||||
|
if (space == sp_data) {
|
||||||
|
*reinterpret_cast<volatile std::uint8_t *>(at) = value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (space == sp_spm) {
|
||||||
|
// The running-slot write guard. An SPM command aimed at the slot this
|
||||||
|
// code executes from is dropped, so a broken host cannot brick the
|
||||||
|
// running loader — while a copy one slot lower may still rewrite the
|
||||||
|
// resident one, which is what a self-update is. Guarding the commit
|
||||||
|
// rather than the page fill covers erase and write both, and leaves a
|
||||||
|
// refused page's words in the buffer: harmless, since the next page
|
||||||
|
// write auto-erases it (§26.2.1).
|
||||||
|
if (slot_of(bank, at) != slot_high)
|
||||||
|
spm::command<off>(value, flash_address(bank, at));
|
||||||
|
// Only a boot-sectioned mega runs on while its RWW section programs;
|
||||||
|
// everywhere else the CPU halts through erase and write, so the wait
|
||||||
|
// is already over by the time it returns.
|
||||||
|
if constexpr (boot_section)
|
||||||
|
spm::wait();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Host-paced: the ack goes out once the write has begun, so the next byte
|
||||||
|
// arrives while it completes and nothing is missed without a buffer.
|
||||||
|
ee::write<off>(at, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// One page into the SPM buffer, and only that: the erase and the write that
|
||||||
|
// commit it are host-issued sp_spm stores, which reach the same fused
|
||||||
|
// store-and-SPM pair through the transfer path's own address and data.
|
||||||
//
|
//
|
||||||
// Nothing discards the buffer first: it is write-once per word (§26.2.1), so
|
// Nothing discards the buffer first: it is write-once per word (§26.2.1), so
|
||||||
// filling over a refused page or an application's leavings programs stale
|
// filling over a refused page or an application's leavings programs stale
|
||||||
// words — but a page write auto-erases it (§26.2.1; §19.2 on the tinies), so
|
// words — but a page write auto-erases it (§26.2.1; §19.2 on the tinies), so
|
||||||
// that write clears the condition and the host's read-back rewrites the page.
|
// that write clears the condition and the host's read-back rewrites the page.
|
||||||
void program_flash(std::uint16_t wire_address, std::uint8_t slot_high)
|
void fill_page(std::uint8_t bank, std::uint16_t at)
|
||||||
{
|
{
|
||||||
// The address names a page, so its in-page bits are dropped and the walk
|
// The address names a page, so its in-page bits are dropped and the walk
|
||||||
// starts at the page base — one induction either way: a byte-addressed
|
// starts at the page base; the low byte of the cursor is the whole in-page
|
||||||
// wire address walks the page itself (the offset bits wrap back to zero),
|
// offset, since a page is aligned and never crosses a bank.
|
||||||
// while a word one becomes a byte cursor once. The slot index is the wire
|
std::uint16_t z = at & ~static_cast<std::uint16_t>(page - 1);
|
||||||
// address's high byte — on byte-addressed chips the byte address's, with
|
do {
|
||||||
// the low bit dropped, since a slot is two of those.
|
std::uint8_t low = link::rx();
|
||||||
spm::flash_address_t address;
|
std::uint8_t high = link::rx();
|
||||||
std::uint8_t page_high;
|
spm::fill<off>(flash_address(bank, z), word_of({low, high}));
|
||||||
if constexpr (word_flash) {
|
z += 2;
|
||||||
// A page is aligned, so it never crosses 64 KiB: RAMPZ is a per-page
|
} while (static_cast<std::uint8_t>(z) & (page - 1));
|
||||||
// constant and the 16-bit Z's low byte is the whole in-page offset.
|
|
||||||
const std::uint8_t rampz = static_cast<std::uint8_t>(wire_address >> 15);
|
|
||||||
const std::uint16_t z0 =
|
|
||||||
static_cast<std::uint16_t>(wire_address << 1) & ~static_cast<std::uint16_t>(page - 1);
|
|
||||||
std::uint16_t z = z0;
|
|
||||||
do {
|
|
||||||
std::uint8_t low = link::rx();
|
|
||||||
std::uint8_t high = link::rx();
|
|
||||||
spm::fill<off>((static_cast<spm::flash_address_t>(rampz) << 16) | z, word_of({low, high}));
|
|
||||||
z += 2;
|
|
||||||
} while (static_cast<std::uint8_t>(z));
|
|
||||||
address = (static_cast<spm::flash_address_t>(rampz) << 16) | z0;
|
|
||||||
page_high = static_cast<std::uint8_t>(wire_address >> 8);
|
|
||||||
} else {
|
|
||||||
address = static_cast<spm::flash_address_t>(wire_address & ~static_cast<std::uint16_t>(page - 1));
|
|
||||||
do {
|
|
||||||
std::uint8_t low = link::rx();
|
|
||||||
std::uint8_t high = link::rx();
|
|
||||||
spm::fill<off>(address, word_of({low, high}));
|
|
||||||
address += 2;
|
|
||||||
} while (static_cast<std::uint8_t>(address) & (page - 1));
|
|
||||||
address -= 2; // back inside the page — erase and write ignore the word bits
|
|
||||||
page_high = static_cast<std::uint8_t>(address >> 8) & 0xfe;
|
|
||||||
}
|
|
||||||
if (page_high != slot_high) {
|
|
||||||
// Only a boot-sectioned mega runs on while its RWW section programs;
|
|
||||||
// everywhere else the CPU halts through erase and write.
|
|
||||||
spm::erase_page<off>(address);
|
|
||||||
if constexpr (boot_section)
|
|
||||||
spm::wait();
|
|
||||||
spm::write_page<off>(address);
|
|
||||||
if constexpr (boot_section)
|
|
||||||
spm::wait();
|
|
||||||
}
|
|
||||||
// Programming leaves the RWW section disabled; reads need it back on. The
|
|
||||||
// same store discards the buffer (§26.2.2), so a boot-sectioned mega never
|
|
||||||
// meets the stale-word case above.
|
|
||||||
if constexpr (boot_section)
|
|
||||||
spm::rww_enable<off>();
|
|
||||||
}
|
|
||||||
|
|
||||||
// The four fuse and lock bytes in the hardware's own Z order: low, lock,
|
|
||||||
// extended, high.
|
|
||||||
void send_fuses()
|
|
||||||
{
|
|
||||||
std::uint8_t which = 0;
|
|
||||||
do
|
|
||||||
link::tx(spm::read_fuse<off>(static_cast<spm::fuse>(which)));
|
|
||||||
while (++which != 4);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[[noreturn]] void run()
|
[[noreturn]] void run()
|
||||||
{
|
{
|
||||||
|
#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 high byte of the slot this copy runs at, which the write guard and
|
// The slot this copy runs in, which the write guard follows: the return
|
||||||
// the info block both follow: the return address is a word address, so its
|
// address is a word address and a slot is half as many words as bytes, so
|
||||||
// high byte is the 256-word slot index, doubled back into byte terms where
|
// its high byte is the slot index outright. No absolute address is ever
|
||||||
// the wire counts bytes. Taken as byteswap's low byte — the builtin already
|
// formed, so the image stays position-independent.
|
||||||
// swaps the two stacked bytes, and the double swap folds away, where `>> 8`
|
const auto slot_high = avr::startup::caller_page();
|
||||||
// would leave the swap materialized.
|
|
||||||
const std::uint16_t ra_words = reinterpret_cast<std::uint16_t>(__builtin_return_address(0));
|
|
||||||
const std::uint8_t ra_high = static_cast<std::uint8_t>(std::byteswap(ra_words));
|
|
||||||
const std::uint8_t slot_high = word_flash ? ra_high : static_cast<std::uint8_t>(ra_high << 1);
|
|
||||||
|
|
||||||
// 'p' then 'b', each under a fresh window; anything else is line noise.
|
await_host();
|
||||||
while (rx_deadline() != 'p' || rx_deadline() != 'b') {
|
|
||||||
}
|
|
||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
// No prompt while an EEPROM write runs: it blocks SPM and fuse reads
|
// No prompt while an EEPROM write runs: it blocks SPM and fuse reads
|
||||||
@@ -400,53 +557,51 @@ void send_fuses()
|
|||||||
tx_ack();
|
tx_ack();
|
||||||
const std::uint8_t command = link::rx();
|
const std::uint8_t command = link::rx();
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case 'J': { // jump to a wire word address: hand-over and staging transfer
|
case 'b': // identity: the version, then the three signature bytes
|
||||||
auto target = reinterpret_cast<void (*)()>(rx16());
|
// Straight out of the stamp, so the wire and the image can never
|
||||||
tx_ack();
|
// disagree about what this loader is. The indices are constant and
|
||||||
link::drain();
|
// the array is constexpr, so these are immediates, not flash reads:
|
||||||
jump(target);
|
// nothing here needs the stamp's runtime address.
|
||||||
}
|
for (std::uint8_t at = stamp_identity; at != sizeof identity_stamp; ++at)
|
||||||
case 'b': // info block, read relative to the running slot
|
link::tx(identity_stamp[at]);
|
||||||
case 'R': // read flash: addr16, n8 (0 = 256)
|
break;
|
||||||
case 'r': // read EEPROM: addr16, n8
|
case 'J': // jump: sel8 (reserved), addr16 as a wire word address
|
||||||
case 'w': { // write EEPROM: addr16, n8, then n bytes each acked
|
case 'W': // fill one flash page buffer: sel8, addr16, then page bytes
|
||||||
// One address-and-count path for all four: 'b' is a flash read
|
case 'G': // read: sel8, addr16, n8 (0 = 256)
|
||||||
// whose arguments the loader already knows, so it joins the
|
case 'g': { // write: sel8, addr16, n8, then n bytes, each acked
|
||||||
// wire-argument three rather than streaming from a call site of its
|
// One decode, one cursor and one loop for every space, both
|
||||||
// own. That leaves one flash streamer in the image, and lets its
|
// directions and the jump: a command per memory would carry a copy
|
||||||
// cursor live in this never-returning loop's own call-saved
|
// of all three each. 'J' — the hand-over and staging transfer —
|
||||||
// registers instead of being saved and restored around a call.
|
// carries a selector it ignores so its address rides the same two
|
||||||
std::uint16_t address;
|
// reads as everything else; 'W' joins the same decode rather than
|
||||||
std::uint8_t count;
|
// keeping an address form of its own, so flash addressing is
|
||||||
if (command == 'b') {
|
// uniform across every command that names it.
|
||||||
// The block sits in the image's first 256 bytes (check_pi.py
|
const std::uint8_t selector = link::rx();
|
||||||
// asserts it) and slots are 512-aligned, so the low byte of its
|
const std::uint8_t space = space_of(selector);
|
||||||
// link address is its offset in any slot — halved where wire
|
const std::uint8_t bank = bank_of(selector);
|
||||||
// units are words. The high byte is runtime data, so no
|
std::uint16_t at = rx16();
|
||||||
// absolute address is ever materialized.
|
if (command == 'J') {
|
||||||
const auto link_byte =
|
tx_ack();
|
||||||
static_cast<std::uint8_t>(reinterpret_cast<std::uint16_t>(info_data.storage.data()));
|
link::drain();
|
||||||
const std::uint8_t low = word_flash ? static_cast<std::uint8_t>(link_byte >> 1) : link_byte;
|
jump(reinterpret_cast<void (*)()>(at));
|
||||||
address = static_cast<std::uint16_t>(low | (slot_high << 8));
|
|
||||||
count = static_cast<std::uint8_t>(info_data.size());
|
|
||||||
} else {
|
|
||||||
address = rx16();
|
|
||||||
count = link::rx();
|
|
||||||
}
|
}
|
||||||
if (command == 'r')
|
if (command == 'W') {
|
||||||
send_eeprom(address, count);
|
fill_page(bank, at);
|
||||||
else if (command == 'w')
|
break;
|
||||||
store_eeprom(address, count);
|
}
|
||||||
else
|
std::uint8_t count = link::rx();
|
||||||
send_flash(address, count);
|
do {
|
||||||
|
// Read and write are one letter apart in case, so the direction
|
||||||
|
// is a single bit and the loop picks it with a one-word skip.
|
||||||
|
if (command & 0x20) {
|
||||||
|
store(space, bank, at, link::rx(), slot_high);
|
||||||
|
tx_ack();
|
||||||
|
} else
|
||||||
|
link::tx(load(space, bank, at));
|
||||||
|
++at;
|
||||||
|
} while (--count);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'W': // program one flash page: addr16, page bytes
|
|
||||||
program_flash(rx16(), slot_high);
|
|
||||||
break;
|
|
||||||
case 'F': // fuse and lock bytes
|
|
||||||
send_fuses();
|
|
||||||
break;
|
|
||||||
default: // unknown bytes are ignored; the loop re-acks
|
default: // unknown bytes are ignored; the loop re-acks
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -456,4 +611,7 @@ void send_fuses()
|
|||||||
} // 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,20 +20,109 @@ 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 = 2 # this tool's own version — free to drift from a loader's
|
VERSION = 9 # this tool's own version — free to drift from a loader's
|
||||||
# The loader versions this tool speaks. A pureboot version implies its wire
|
# The loader versions this tool 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), and a version it has
|
||||||
|
# no decoder for moves the floor.
|
||||||
OLDEST_LOADER = 1
|
OLDEST_LOADER = 1
|
||||||
NEWEST_LOADER = 3
|
NEWEST_LOADER = 8
|
||||||
SLOT = 512 # the loader slot, on every chip
|
SLOT = 512 # the loader slot, on every chip
|
||||||
RETRIES = 3 # rewrites of a page that reads back wrong, before the run stops
|
RETRIES = 3 # rewrites of a page that reads back wrong, before the run stops
|
||||||
|
|
||||||
|
# pureboot 5 replaced the four per-memory commands with one pair: 'G' reads and
|
||||||
|
# 'g' writes, each taking a selector byte, a 16-bit address and a count, over
|
||||||
|
# the spaces below. The loader carries one transfer loop instead of four bodies
|
||||||
|
# — which is what buys the data space and the host-issued SPM operations.
|
||||||
|
# 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
|
||||||
|
SP_FLASH, SP_EEPROM, SP_RAM, SP_FUSE, SP_SPM = 0, 1, 2, 3, 4
|
||||||
|
|
||||||
|
# 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
|
||||||
|
# wire address — so a transfer names a byte address within one 64 KiB bank and
|
||||||
|
# no command has to speak word addresses. No single transfer may cross a bank
|
||||||
|
# boundary; the host chunks to keep that true.
|
||||||
|
def selector(space, address):
|
||||||
|
return space | ((address >> 16) << 4)
|
||||||
|
|
||||||
|
|
||||||
|
# The SPM operations pureboot 5 leaves to the host: a write to SP_SPM hands its
|
||||||
|
# byte to SPMCSR and fires the instruction at the selected flash address. Every
|
||||||
|
# part pureboot targets agrees on these encodings.
|
||||||
|
SPM_ERASE, SPM_WRITE, SPM_RWWSRE = 0x03, 0x05, 0x11
|
||||||
|
|
||||||
|
# Calibration byte for an autobaud loader: 0xC0 is a start bit plus six zero
|
||||||
|
# data bits — one low pulse of seven bit-times, which the loader times into its
|
||||||
|
# per-bit unit. Sent at whatever baud the host chose; the loader locks to it.
|
||||||
|
CALIBRATE = 0xC0
|
||||||
|
|
||||||
|
# pureboot 5 answers 'b' with its version and the chip signature; the host
|
||||||
|
# derives the rest of the geometry from the signature rather than reading a
|
||||||
|
# table off the device. flash, page, eeprom, patch-vector per distinct
|
||||||
|
# signature, over every chip pureboot targets (the loader computes the same
|
||||||
|
# from its chip database at build time). Die revisions that share a signature
|
||||||
|
# share this row, as they share the silicon.
|
||||||
|
CHIP_GEOMETRY = {
|
||||||
|
# signature : (flash, page, eeprom, patch_vector, ram_start, gpior1)
|
||||||
|
# ram_start is where SRAM begins in data space: the classic megas and the
|
||||||
|
# tinies keep it right after the plain I/O registers (0x60), the x8/x4
|
||||||
|
# generations past their extended I/O file (0x100). gpior1 is GPIOR1's
|
||||||
|
# data address — 0x32 on the t25/45/85, 0x4A from the x8 generation on,
|
||||||
|
# None where the chip has no pair (t13, classic megas). A v7 autobaud
|
||||||
|
# loader's measured bit period lives in GPIOR2:GPIOR1 where they exist
|
||||||
|
# and at exactly ram_start elsewhere (its only RAM object; the loader's
|
||||||
|
# own build pins the layout); v5/v6 always used ram_start. --info reads
|
||||||
|
# whichever home the answering version implies.
|
||||||
|
(0x1E, 0x90, 0x07): (1024, 32, 64, True, 0x60, None), # ATtiny13/13A
|
||||||
|
(0x1E, 0x91, 0x08): (2048, 32, 128, True, 0x60, 0x32), # ATtiny25
|
||||||
|
(0x1E, 0x92, 0x06): (4096, 64, 256, True, 0x60, 0x32), # ATtiny45
|
||||||
|
(0x1E, 0x93, 0x0B): (8192, 64, 512, True, 0x60, 0x32), # ATtiny85
|
||||||
|
(0x1E, 0x92, 0x05): (4096, 64, 256, True, 0x100, 0x4A), # ATmega48/48A
|
||||||
|
(0x1E, 0x92, 0x0A): (4096, 64, 256, True, 0x100, 0x4A), # ATmega48P/48PA
|
||||||
|
(0x1E, 0x93, 0x07): (8192, 64, 512, False, 0x60, None), # ATmega8/8A
|
||||||
|
(0x1E, 0x93, 0x0A): (8192, 64, 512, False, 0x100, 0x4A), # ATmega88/88A
|
||||||
|
(0x1E, 0x93, 0x0F): (8192, 64, 512, False, 0x100, 0x4A), # ATmega88P/88PA
|
||||||
|
(0x1E, 0x94, 0x03): (16384, 128, 512, False, 0x60, None), # ATmega16/16A
|
||||||
|
(0x1E, 0x94, 0x06): (16384, 128, 512, False, 0x100, 0x4A), # ATmega168/168A
|
||||||
|
(0x1E, 0x94, 0x0B): (16384, 128, 512, False, 0x100, 0x4A), # ATmega168P/168PA
|
||||||
|
(0x1E, 0x94, 0x0A): (16384, 128, 512, False, 0x100, 0x4A), # ATmega164P/164PA
|
||||||
|
(0x1E, 0x94, 0x0F): (16384, 128, 512, False, 0x100, 0x4A), # ATmega164A
|
||||||
|
(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
|
||||||
|
|
||||||
|
|
||||||
@@ -85,23 +174,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)
|
||||||
attrs = termios.tcgetattr(self.fd)
|
|
||||||
attrs[0] = 0 # iflag
|
|
||||||
attrs[1] = 0 # oflag
|
|
||||||
attrs[2] = termios.CREAD | termios.CLOCAL | termios.CS8 # cflag
|
|
||||||
attrs[3] = 0 # lflag
|
|
||||||
try:
|
try:
|
||||||
speed = getattr(termios, f"B{baud}")
|
attrs = termios.tcgetattr(self.fd)
|
||||||
except AttributeError:
|
attrs[0] = 0 # iflag
|
||||||
raise Error(f"unsupported baud rate {baud}") from None
|
attrs[1] = 0 # oflag
|
||||||
attrs[4] = attrs[5] = speed
|
attrs[2] = termios.CREAD | termios.CLOCAL | termios.CS8 # cflag
|
||||||
attrs[6][termios.VMIN] = 0
|
attrs[3] = 0 # lflag
|
||||||
attrs[6][termios.VTIME] = 0
|
attrs[6][termios.VMIN] = 0
|
||||||
termios.tcsetattr(self.fd, termios.TCSANOW, attrs)
|
attrs[6][termios.VTIME] = 0
|
||||||
|
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)
|
||||||
@@ -230,6 +356,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
|
||||||
@@ -237,6 +364,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)
|
||||||
|
|
||||||
@@ -295,12 +437,135 @@ 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 ---
|
||||||
|
|
||||||
|
|
||||||
class Info:
|
class Info:
|
||||||
"""The 12-byte info block."""
|
"""The 12-byte info block."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_identity(cls, raw):
|
||||||
|
"""pureboot 5's reply: the version and the chip signature. The rest of
|
||||||
|
the geometry is looked up from the signature — the loader derived the
|
||||||
|
same facts from its chip database at build time, so nothing is guessed,
|
||||||
|
it is simply not sent. Reconstructs a block in the older layout, so
|
||||||
|
every derived attribute below is shared with the loaders that do send
|
||||||
|
one.
|
||||||
|
|
||||||
|
The base is where application flash ends, which is a property of the
|
||||||
|
chip and not of the copy answering: a loader staged one slot lower
|
||||||
|
reports the same geometry the resident one does, exactly as the loaders
|
||||||
|
that send a block do. Which slot a copy runs in matters only to its own
|
||||||
|
write guard, which is the loader's business."""
|
||||||
|
if len(raw) != 4:
|
||||||
|
raise Error(f"bad identity reply: {raw.hex()}")
|
||||||
|
version, signature = raw[0], tuple(raw[1:4])
|
||||||
|
geometry = CHIP_GEOMETRY.get(signature)
|
||||||
|
if geometry is None:
|
||||||
|
sig = " ".join(f"{b:02x}" for b in signature)
|
||||||
|
raise Error(f"unknown signature {sig} — this tool has no geometry for it")
|
||||||
|
flash, page, eeprom, patch, _, _ = geometry
|
||||||
|
base = flash - SLOT
|
||||||
|
word_flash = flash > 0x10000
|
||||||
|
wire_base = base // 2 if word_flash else base
|
||||||
|
flags = (1 if patch else 0) | (2 if word_flash else 0)
|
||||||
|
raw12 = bytes((ord("P"), ord("B"), version, *signature, page & 0xFF,
|
||||||
|
wire_base & 0xFF, wire_base >> 8, eeprom & 0xFF, eeprom >> 8, flags))
|
||||||
|
return cls(raw12)
|
||||||
|
|
||||||
def __init__(self, raw):
|
def __init__(self, raw):
|
||||||
if len(raw) != 12 or raw[0:2] != b"PB":
|
if len(raw) != 12 or raw[0:2] != b"PB":
|
||||||
raise Error(f"bad info block: {raw.hex()}")
|
raise Error(f"bad info block: {raw.hex()}")
|
||||||
@@ -314,8 +579,11 @@ class Info:
|
|||||||
self.signature = raw[3:6]
|
self.signature = raw[3:6]
|
||||||
self.page = raw[6] or 256 # the wire count convention: 0 means 256
|
self.page = raw[6] or 256 # the wire count convention: 0 means 256
|
||||||
self.patch_vector = bool(raw[11] & 1)
|
self.patch_vector = bool(raw[11] & 1)
|
||||||
# Bit 1: flash addresses are words on the wire. Every address here
|
# Bit 1: the flash runs past what one 16-bit address covers. Through
|
||||||
# stays a byte address and converts at the wire.
|
# pureboot 4 that made flash addresses words on the wire; pureboot 5
|
||||||
|
# keeps them bytes and carries the bank in the selector instead. Every
|
||||||
|
# address in this tool stays a byte address either way and converts at
|
||||||
|
# the wire.
|
||||||
self.word_flash = bool(raw[11] & 2)
|
self.word_flash = bool(raw[11] & 2)
|
||||||
scale = 2 if self.word_flash else 1
|
scale = 2 if self.word_flash else 1
|
||||||
self.base = (raw[7] | (raw[8] << 8)) * scale
|
self.base = (raw[7] | (raw[8] << 8)) * scale
|
||||||
@@ -325,6 +593,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)
|
||||||
@@ -345,7 +623,7 @@ class Info:
|
|||||||
f"version pureboot {self.version}",
|
f"version pureboot {self.version}",
|
||||||
f"signature {' '.join(f'{b:02x}' for b in self.signature)}",
|
f"signature {' '.join(f'{b:02x}' for b in self.signature)}",
|
||||||
f"flash {self.flash_size} B, {self.page} B pages"
|
f"flash {self.flash_size} B, {self.page} B pages"
|
||||||
+ (", word-addressed wire" if self.word_flash else ""),
|
+ (", past one 16-bit bank" if self.word_flash else ""),
|
||||||
f"application 0x0000..{self.base - 1:#06x} ({self.base} B)",
|
f"application 0x0000..{self.base - 1:#06x} ({self.base} B)",
|
||||||
f"loader {self.base:#06x} ({SLOT} B slot)",
|
f"loader {self.base:#06x} ({SLOT} B slot)",
|
||||||
f"staging {self.stage:#06x}",
|
f"staging {self.stage:#06x}",
|
||||||
@@ -362,40 +640,119 @@ class Loader:
|
|||||||
def __init__(self, port):
|
def __init__(self, port):
|
||||||
self.port = port
|
self.port = port
|
||||||
self.info = None
|
self.info = None
|
||||||
|
# Set once a session is established over an autobaud link, so a
|
||||||
|
# re-entry after 'J' repeats the handshake that worked.
|
||||||
|
self.autobaud = False
|
||||||
|
# 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 connect(self, wait):
|
def _read_identity(self):
|
||||||
"""Knock until the info block comes back. The block is what proves the
|
"""The 'b' reply, in either of the two layouts a loader may send.
|
||||||
loader is listening — a prompt byte alone does not, since one left over
|
pureboot 5 answers with its version and the signature; older loaders
|
||||||
from a previous session can still be in the pipeline while the port
|
answer with a 12-byte block. The version byte cannot be mistaken for
|
||||||
opening resets the device into a fresh activation window, where a
|
the older block's 'P', so four bytes are enough to tell them apart.
|
||||||
command without its knock is discarded. Each attempt is therefore the
|
|
||||||
whole handshake, retried until it produces the block or the window
|
The timeout is short on purpose: a real answer follows the prompt
|
||||||
closes. Also converges into a live session: the knock bytes are ignored
|
within a frame time or two, so half a second is dozens of times the
|
||||||
there and the drain absorbs whatever they produced."""
|
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":
|
||||||
|
return Info(head + self.port.read_exact(8, 0.5))
|
||||||
|
return Info.from_identity(head)
|
||||||
|
|
||||||
|
def _handshake(self, wait, knock, what):
|
||||||
|
"""One activation, retried until the loader answers or the window
|
||||||
|
closes. The identity reply is what proves the loader is listening — a
|
||||||
|
prompt byte alone does not, since one left over from a previous session
|
||||||
|
can still be in the pipeline while the port opening resets the device
|
||||||
|
into a fresh window, where a command without its knock is discarded.
|
||||||
|
Each attempt is therefore the whole handshake. This also converges into
|
||||||
|
an already-live session: the knock bytes are ignored there and the
|
||||||
|
drain absorbs whatever they produced.
|
||||||
|
|
||||||
|
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(b"pb")
|
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:
|
||||||
block = self.port.read_exact(12, 2.0)
|
# A version the tool cannot speak is the loader's own
|
||||||
except Error:
|
# answer, not a failed knock: Info reports it rather than
|
||||||
block = b""
|
# sending the tool round the loop again.
|
||||||
# A version the tool cannot speak is the loader's own answer,
|
self.info = self._read_identity()
|
||||||
# not a failed knock: Info reports it rather than retrying.
|
except Error as failed:
|
||||||
if block[0:2] == b"PB":
|
if "pureboot" in str(failed):
|
||||||
self.info = Info(block)
|
raise
|
||||||
|
# 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
|
||||||
|
if self.info is not None:
|
||||||
self._expect_prompt()
|
self._expect_prompt()
|
||||||
verbose(f"loader answered knock {knocks}; info block read")
|
verbose(f"loader answered {what} {knocks}; identity read")
|
||||||
return self.info
|
return self.info
|
||||||
if time.monotonic() > deadline:
|
if time.monotonic() > deadline:
|
||||||
|
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):
|
||||||
|
"""Knock 'p' then 'b' and read the identity."""
|
||||||
|
return self._handshake(wait, b"pb", "knock")
|
||||||
|
|
||||||
|
def connect_autobaud(self, wait):
|
||||||
|
"""The autobaud handshake. In place of the p+b knock the host sends the
|
||||||
|
calibration pulse — one seven-bit-time low pulse at the host's chosen
|
||||||
|
baud, which the loader times into its per-bit unit — then a single 'p'
|
||||||
|
the loader decodes at the rate it just measured. A lost pulse, or a
|
||||||
|
knock landing while the loader is mid-frame, simply fails to answer and
|
||||||
|
leaves the measurement loop waiting for the next pulse, so the retry in
|
||||||
|
_handshake covers it."""
|
||||||
|
self.autobaud = True
|
||||||
|
return self._handshake(wait, bytes((CALIBRATE, ord("p"))), "calibration")
|
||||||
|
|
||||||
def _expect_prompt(self, timeout=2.0):
|
def _expect_prompt(self, timeout=2.0):
|
||||||
byte = self.port.read_exact(1, timeout)
|
byte = self.port.read_exact(1, timeout)
|
||||||
if byte != PROMPT:
|
if byte != PROMPT:
|
||||||
@@ -418,7 +775,58 @@ class Loader:
|
|||||||
count -= chunk
|
count -= chunk
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unified(self):
|
||||||
|
"""pureboot 5 and later: one 'G'/'g' pair over selector-named spaces."""
|
||||||
|
return self.info is not None and self.info.version >= UNIFIED_LOADER
|
||||||
|
|
||||||
|
def _read_space(self, space, address, count):
|
||||||
|
"""A run out of any space, chunked to 256 bytes and to bank bounds."""
|
||||||
|
data = b""
|
||||||
|
while count:
|
||||||
|
chunk = min(count, 256, 0x10000 - (address & 0xFFFF))
|
||||||
|
head = bytes((ord("G"), selector(space, address), address & 0xFF,
|
||||||
|
(address >> 8) & 0xFF, chunk & 0xFF))
|
||||||
|
data += self._command(head, chunk, 5.0)
|
||||||
|
address += chunk
|
||||||
|
count -= chunk
|
||||||
|
return data
|
||||||
|
|
||||||
|
def _write_space(self, space, address, data, progress=None):
|
||||||
|
"""A run into any space. Each byte is acked as its write begins — an
|
||||||
|
EEPROM cell and an SPM operation both need that pacing, and the ack is
|
||||||
|
what the loader sends in place of a completion status."""
|
||||||
|
offset = 0
|
||||||
|
while offset < len(data):
|
||||||
|
chunk = data[offset : offset + min(256, 0x10000 - (address & 0xFFFF))]
|
||||||
|
head = bytes((ord("g"), selector(space, address), address & 0xFF,
|
||||||
|
(address >> 8) & 0xFF, len(chunk) & 0xFF))
|
||||||
|
self.port.write(head)
|
||||||
|
for byte in chunk:
|
||||||
|
self.port.write(bytes((byte,)))
|
||||||
|
self._expect_prompt()
|
||||||
|
if progress:
|
||||||
|
progress.step()
|
||||||
|
self._expect_prompt() # the next command prompt
|
||||||
|
address += len(chunk)
|
||||||
|
offset += len(chunk)
|
||||||
|
|
||||||
|
def spm(self, operation, address):
|
||||||
|
"""One SPM operation at a flash address — the erase, write and RWW
|
||||||
|
re-enable that pureboot 4 ran inside 'W' and pureboot 5 leaves here."""
|
||||||
|
self._write_space(SP_SPM, address, bytes((operation,)))
|
||||||
|
|
||||||
|
def read_ram(self, address, count):
|
||||||
|
"""Data space: SRAM, and with it the register file and every I/O
|
||||||
|
register, which share the address space on AVR. New in pureboot 5."""
|
||||||
|
return self._read_space(SP_RAM, address, count)
|
||||||
|
|
||||||
|
def write_ram(self, address, data):
|
||||||
|
self._write_space(SP_RAM, address, data)
|
||||||
|
|
||||||
def read_flash(self, address, count):
|
def read_flash(self, address, count):
|
||||||
|
if self.unified:
|
||||||
|
return self._read_space(SP_FLASH, address, count)
|
||||||
if not self.info.word_flash:
|
if not self.info.word_flash:
|
||||||
return self._stream_read("R", address, count)
|
return self._stream_read("R", address, count)
|
||||||
# Word-addressed wire: widen to even bounds and never let one read
|
# Word-addressed wire: widen to even bounds and never let one read
|
||||||
@@ -436,15 +844,32 @@ class Loader:
|
|||||||
return data[address - start : address - start + count]
|
return data[address - start : address - start + count]
|
||||||
|
|
||||||
def read_eeprom(self, address, count):
|
def read_eeprom(self, address, count):
|
||||||
|
if self.unified:
|
||||||
|
return self._read_space(SP_EEPROM, address, count)
|
||||||
return self._stream_read("r", address, count)
|
return self._stream_read("r", address, count)
|
||||||
|
|
||||||
def write_page(self, address, data):
|
def write_page(self, address, data):
|
||||||
assert len(data) == self.info.page and address % self.info.page == 0
|
assert len(data) == self.info.page and address % self.info.page == 0
|
||||||
|
if self.unified:
|
||||||
|
# 'W' fills the page buffer and stops there; the erase and the write
|
||||||
|
# are host-issued SPM operations. Only a chip with a boot section
|
||||||
|
# has RWW to re-enable — on the others bit 4 of SPMCSR means
|
||||||
|
# something else entirely, so it must not be sent.
|
||||||
|
head = bytes((ord("W"), selector(SP_FLASH, address), address & 0xFF, (address >> 8) & 0xFF))
|
||||||
|
self._command(head + data, 0, 2.0)
|
||||||
|
self.spm(SPM_ERASE, address)
|
||||||
|
self.spm(SPM_WRITE, address)
|
||||||
|
if not self.info.patch_vector:
|
||||||
|
self.spm(SPM_RWWSRE, address)
|
||||||
|
return
|
||||||
wire = address // (2 if self.info.word_flash else 1)
|
wire = address // (2 if self.info.word_flash else 1)
|
||||||
head = bytes((ord("W"), wire & 0xFF, wire >> 8))
|
head = bytes((ord("W"), wire & 0xFF, wire >> 8))
|
||||||
self._command(head + data, 0, 2.0)
|
self._command(head + data, 0, 2.0)
|
||||||
|
|
||||||
def write_eeprom(self, address, data, progress=None):
|
def write_eeprom(self, address, data, progress=None):
|
||||||
|
if self.unified:
|
||||||
|
self._write_space(SP_EEPROM, address, data, progress)
|
||||||
|
return
|
||||||
offset = 0
|
offset = 0
|
||||||
while offset < len(data):
|
while offset < len(data):
|
||||||
chunk = data[offset : offset + 256]
|
chunk = data[offset : offset + 256]
|
||||||
@@ -460,18 +885,55 @@ class Loader:
|
|||||||
offset += len(chunk)
|
offset += len(chunk)
|
||||||
|
|
||||||
def read_fuses(self):
|
def read_fuses(self):
|
||||||
|
if self.unified:
|
||||||
|
return self._read_space(SP_FUSE, 0, 4)
|
||||||
return self._command(b"F", 4, 2.0)
|
return self._command(b"F", 4, 2.0)
|
||||||
|
|
||||||
def jump(self, word_address):
|
def jump(self, word_address):
|
||||||
"""The device acks, then execution continues at the word address."""
|
"""The device acks, then execution continues at the word address.
|
||||||
self.port.write(bytes((ord("J"), word_address & 0xFF, word_address >> 8)))
|
From v7 'J' rides the unified decode, so it carries a selector byte
|
||||||
|
the loader ignores; older loaders take the bare address."""
|
||||||
|
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._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.
|
||||||
|
|
||||||
|
`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)
|
||||||
return self.connect(wait)
|
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)
|
||||||
|
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)
|
||||||
@@ -638,12 +1100,26 @@ def mega_boot(info, fuse_bytes):
|
|||||||
|
|
||||||
|
|
||||||
def image_info(image):
|
def image_info(image):
|
||||||
"""The info block embedded in a pureboot binary, or None. Searched once
|
"""What a pureboot binary says about itself, or None.
|
||||||
per known version, so the magic stays three selective bytes rather than
|
|
||||||
two that code could carry by chance."""
|
An update image is a bare slot: nothing about it names the chip it was
|
||||||
|
built for, and installing a foreign one bricks the target — so every
|
||||||
|
loader carries a stamp for this. Through pureboot 4 the stamp is the
|
||||||
|
12-byte info block the device also serves; pureboot 5 serves its identity
|
||||||
|
from immediates and carries a 6-byte stamp (magic, version, signature)
|
||||||
|
that only this exists for, from which the geometry is looked up exactly as
|
||||||
|
it is for a live device.
|
||||||
|
|
||||||
|
Searched once per known version, so the magic stays three selective bytes
|
||||||
|
rather than two that code could carry by chance."""
|
||||||
for version in range(OLDEST_LOADER, NEWEST_LOADER + 1):
|
for version in range(OLDEST_LOADER, NEWEST_LOADER + 1):
|
||||||
at = image.find(b"PB" + bytes((version,)))
|
at = image.find(b"PB" + bytes((version,)))
|
||||||
if 0 <= at <= len(image) - 12:
|
if at < 0:
|
||||||
|
continue
|
||||||
|
if version >= UNIFIED_LOADER:
|
||||||
|
if at <= len(image) - 6:
|
||||||
|
return Info.from_identity(image[at + 2 : at + 6])
|
||||||
|
elif at <= len(image) - 12:
|
||||||
return Info(image[at : at + 12])
|
return Info(image[at : at + 12])
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -805,10 +1281,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):
|
||||||
@@ -833,7 +1315,10 @@ def op_update_loader(loader, wait, path, state_path, fuse_bytes):
|
|||||||
# it and matching byte for byte, and the slot unchanged since this update
|
# it and matching byte for byte, and the slot unchanged since this update
|
||||||
# began, so a half-written install takes the path below instead.
|
# began, so a half-written install takes the path below instead.
|
||||||
current = loader.read_flash(info.stage, SLOT)
|
current = loader.read_flash(info.stage, SLOT)
|
||||||
staged_loader = image_info(current[:268])
|
# The whole slot is searched: a loader's stamp sits wherever its image put
|
||||||
|
# it, which is the end of the code on pureboot 5 and the front of it
|
||||||
|
# before that.
|
||||||
|
staged_loader = image_info(current)
|
||||||
if staged_loader is not None and staged_loader.raw == info.raw and current == state.staging:
|
if staged_loader is not None and staged_loader.raw == info.raw and current == state.staging:
|
||||||
print("staging slot already holds a loader — left in place")
|
print("staging slot already holds a loader — left in place")
|
||||||
else:
|
else:
|
||||||
@@ -850,7 +1335,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")
|
||||||
@@ -1016,6 +1501,37 @@ def op_read_eeprom(loader, path):
|
|||||||
print(f"read EEPROM: {len(data)} B -> {path}")
|
print(f"read EEPROM: {len(data)} B -> {path}")
|
||||||
|
|
||||||
|
|
||||||
|
def _require_unified(loader, what):
|
||||||
|
if not loader.unified:
|
||||||
|
raise Error(f"{what} needs pureboot {UNIFIED_LOADER} or later; this loader is {loader.info.version}")
|
||||||
|
|
||||||
|
|
||||||
|
def _peek_spec(spec):
|
||||||
|
"""ADDR[:N] — addresses and counts in any Python integer base."""
|
||||||
|
address, _, count = spec.partition(":")
|
||||||
|
return int(address, 0), int(count, 0) if count else 1
|
||||||
|
|
||||||
|
|
||||||
|
def op_peek(loader, spec):
|
||||||
|
_require_unified(loader, "--peek")
|
||||||
|
address, count = _peek_spec(spec)
|
||||||
|
data = loader.read_ram(address, count)
|
||||||
|
for offset in range(0, len(data), 16):
|
||||||
|
row = data[offset : offset + 16]
|
||||||
|
text = "".join(chr(b) if 0x20 <= b < 0x7F else "." for b in row)
|
||||||
|
print(f"{address + offset:#06x} {row.hex(' '):<47} {text}")
|
||||||
|
|
||||||
|
|
||||||
|
def op_poke(loader, spec):
|
||||||
|
_require_unified(loader, "--poke")
|
||||||
|
address, _, payload = spec.partition(":")
|
||||||
|
if not payload:
|
||||||
|
raise Error("--poke needs ADDR:HEX, for example 0x200:deadbeef")
|
||||||
|
data = bytes.fromhex(payload.replace(" ", ""))
|
||||||
|
loader.write_ram(int(address, 0), data)
|
||||||
|
print(f"poke: {len(data)} B at {int(address, 0):#06x}")
|
||||||
|
|
||||||
|
|
||||||
def op_fuses(loader):
|
def op_fuses(loader):
|
||||||
low, lock, extended, high = loader.read_fuses()
|
low, lock, extended, high = loader.read_fuses()
|
||||||
print("fuses:")
|
print("fuses:")
|
||||||
@@ -1037,6 +1553,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 ---
|
||||||
|
|
||||||
|
|
||||||
@@ -1049,10 +1624,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",
|
||||||
|
help="drive an autobaud loader: send the 0xC0 calibration pulse and a single "
|
||||||
|
"knock, and take geometry from the signature (no clock/baud baked in)")
|
||||||
|
parser.add_argument("--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")
|
||||||
@@ -1064,6 +1660,10 @@ def main():
|
|||||||
parser.add_argument("--eeprom", metavar="FILE", help="program the EEPROM (bin or ihex)")
|
parser.add_argument("--eeprom", metavar="FILE", help="program the EEPROM (bin or ihex)")
|
||||||
parser.add_argument("--read-eeprom", metavar="FILE", help="dump the EEPROM")
|
parser.add_argument("--read-eeprom", metavar="FILE", help="dump the EEPROM")
|
||||||
parser.add_argument("--verify-eeprom", metavar="FILE", help="compare EEPROM against an image")
|
parser.add_argument("--verify-eeprom", metavar="FILE", help="compare EEPROM against an image")
|
||||||
|
parser.add_argument("--peek", metavar="ADDR[:N]", help="read N bytes of data space (SRAM, registers, "
|
||||||
|
"I/O) — pureboot 5 and later")
|
||||||
|
parser.add_argument("--poke", metavar="ADDR:HEX", help="write hex bytes into data space — "
|
||||||
|
"pureboot 5 and later")
|
||||||
parser.add_argument("--force", action="store_true", help="override refusable safety checks")
|
parser.add_argument("--force", action="store_true", help="override refusable safety checks")
|
||||||
parser.add_argument("--stay", action="store_true", help="leave the loader in its session")
|
parser.add_argument("--stay", action="store_true", help="leave the loader in its session")
|
||||||
parser.add_argument("-v", "--verbose", action="store_true",
|
parser.add_argument("-v", "--verbose", action="store_true",
|
||||||
@@ -1082,15 +1682,36 @@ 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(args.wait)
|
info = loader.connect_autobaud(args.wait) if args.autobaud else loader.connect(args.wait)
|
||||||
if args.info:
|
if args.info:
|
||||||
print("device:")
|
print("device:")
|
||||||
for line in info.lines():
|
for line in info.lines():
|
||||||
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)
|
||||||
@@ -1098,7 +1719,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:
|
||||||
@@ -1115,6 +1743,10 @@ def main():
|
|||||||
op_read_eeprom(loader, args.read_eeprom)
|
op_read_eeprom(loader, args.read_eeprom)
|
||||||
if args.verify_eeprom:
|
if args.verify_eeprom:
|
||||||
op_verify_eeprom(loader, args.verify_eeprom)
|
op_verify_eeprom(loader, args.verify_eeprom)
|
||||||
|
if args.poke:
|
||||||
|
op_poke(loader, args.poke)
|
||||||
|
if args.peek:
|
||||||
|
op_peek(loader, args.peek)
|
||||||
if args.stay:
|
if args.stay:
|
||||||
print("loader stays in its session (reset to leave)")
|
print("loader stays in its session (reset to leave)")
|
||||||
else:
|
else:
|
||||||
@@ -1127,7 +1759,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:
|
||||||
|
|||||||
@@ -1,47 +1,75 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Position-independence lint: the two link-time facts that let the identical
|
"""Position-independence lint: the property that lets the identical image run
|
||||||
image run from any slot, asserted from the built ELF.
|
from any slot, asserted from the built ELF and its object.
|
||||||
|
|
||||||
1. No absolute jmp/call — -mrelax normally guarantees it, but a branch that
|
1. No absolute jmp/call — -mrelax normally guarantees it, but a branch that
|
||||||
grows out of relaxation range would break it silently.
|
grows out of relaxation range would break it silently.
|
||||||
2. The info block within the image's first 256 bytes: 'b' rebuilds its
|
2. Nothing flash-resident to address: the image is .text alone, so there is
|
||||||
address as (running slot high byte : link address low byte).
|
no table whose runtime address has to be reconstructed.
|
||||||
|
3. The image is byte-identical when linked at a different base. This is
|
||||||
|
position independence itself rather than a proxy for it — an absolute
|
||||||
|
address anywhere in the image would move with the link and show up as a
|
||||||
|
differing byte.
|
||||||
|
|
||||||
Usage: check_pi.py <objdump> <nm> <elf> <text_start_hex>
|
Usage: check_pi.py <objdump> <objcopy> <cxx> <mcu> <elf> <object> <text_start_hex>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
|
def fail(message):
|
||||||
|
print(f"FAIL: {message}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
objdump, nm, elf, text_start = sys.argv[1:]
|
objdump, objcopy, cxx, mcu, elf, obj, text_start = sys.argv[1:]
|
||||||
text_start = int(text_start, 0)
|
text_start = int(text_start, 0)
|
||||||
|
|
||||||
listing = subprocess.run([objdump, "-d", elf], capture_output=True, text=True, check=True).stdout
|
listing = subprocess.run([objdump, "-d", elf], capture_output=True, text=True, check=True).stdout
|
||||||
absolute = [
|
absolute = [line for line in listing.splitlines() if re.search(r"\t(jmp|call)\t", line)]
|
||||||
line
|
|
||||||
for line in listing.splitlines()
|
|
||||||
if re.search(r"\t(jmp|call)\t", line)
|
|
||||||
]
|
|
||||||
if absolute:
|
if absolute:
|
||||||
print("FAIL: absolute control flow in the image:")
|
fail("absolute control flow in the image:\n" + "\n".join(absolute))
|
||||||
print("\n".join(absolute))
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
symbols = subprocess.run([nm, "-C", elf], capture_output=True, text=True, check=True).stdout
|
# Allocated flash beyond .text would be data the running copy has to find.
|
||||||
info = [line for line in symbols.splitlines() if "flash_table" in line and "::storage" in line]
|
# Only ALLOC sections reach the device at all; .comment and the debug
|
||||||
if len(info) != 1:
|
# sections ride along in the ELF container and are never flashed. objdump
|
||||||
print(f"FAIL: expected one info-block storage symbol, found {len(info)}")
|
# prints each section's flags on the line following its header.
|
||||||
sys.exit(1)
|
headers = subprocess.run([objdump, "-h", elf], capture_output=True, text=True, check=True).stdout.splitlines()
|
||||||
address = int(info[0].split()[0], 16)
|
for index, line in enumerate(headers):
|
||||||
offset = address - text_start
|
fields = line.split()
|
||||||
if not 0 <= offset < 256:
|
if len(fields) < 6 or not fields[0].isdigit():
|
||||||
print(f"FAIL: info block at image offset {offset:#x}, must sit in the first 256 bytes")
|
continue
|
||||||
sys.exit(1)
|
name, size = fields[1], int(fields[2], 16)
|
||||||
|
flags = headers[index + 1] if index + 1 < len(headers) else ""
|
||||||
|
if "ALLOC" not in flags or not size:
|
||||||
|
continue
|
||||||
|
if name not in (".text", ".noinit", ".bss"):
|
||||||
|
fail(f"flash-resident section {name} ({size} bytes): the image must be .text alone")
|
||||||
|
|
||||||
print(f"PI lint: control flow PC-relative, info block at offset {offset:#x}")
|
# Relink at a different base and compare the bytes.
|
||||||
|
with tempfile.TemporaryDirectory() as work:
|
||||||
|
elsewhere = text_start - 0x200 if text_start >= 0x200 else text_start + 0x200
|
||||||
|
images = []
|
||||||
|
for base, tag in ((text_start, "here"), (elsewhere, "there")):
|
||||||
|
relinked = os.path.join(work, f"{tag}.elf")
|
||||||
|
binary = os.path.join(work, f"{tag}.bin")
|
||||||
|
subprocess.run(
|
||||||
|
[cxx, f"-mmcu={mcu}", "-nostartfiles", f"-Wl,--section-start=.text={base:#x}",
|
||||||
|
"-Wl,--defsym=pureboot_app=0", "-mrelax", obj, "-o", relinked],
|
||||||
|
check=True, capture_output=True)
|
||||||
|
subprocess.run([objcopy, "-O", "binary", relinked, binary], check=True)
|
||||||
|
images.append(open(binary, "rb").read())
|
||||||
|
if images[0] != images[1]:
|
||||||
|
differing = [i for i, (a, b) in enumerate(zip(*images)) if a != b]
|
||||||
|
fail(f"the image changes when linked at {elsewhere:#x} instead of {text_start:#x}: "
|
||||||
|
f"{len(differing)} byte(s) differ, first at offset {differing[0]:#x}")
|
||||||
|
|
||||||
|
print(f"PI lint: control flow PC-relative, .text only, identical linked at {text_start:#x} and {elsewhere:#x}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
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();
|
||||||
}
|
}
|
||||||
|
|||||||
211
test/pbautobaud.py
Normal file
211
test/pbautobaud.py
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""End-to-end autobaud test: drive an autobaud loader in simavr through the
|
||||||
|
calibration handshake and a flash + EEPROM + fuse round-trip, cross-checked
|
||||||
|
against the simulator's ground-truth memory — then repeat at a second F_CPU with
|
||||||
|
the *same* loader binary, which is the property autobaud exists for: one
|
||||||
|
clock-agnostic image that locks onto whatever rate the host sends.
|
||||||
|
|
||||||
|
Usage: pbautobaud.py <device_bin> <loader_elf> <mcu> <base_hex> <page>
|
||||||
|
<app_bin> <app_hz> <app_baud> <tool_py> <workdir> [link]
|
||||||
|
|
||||||
|
The loader is a software-serial build, driven over the GPIO⇄pty bridge; the
|
||||||
|
optional link overrides the default -l sw:B0,B1 — RX == TX in it is the
|
||||||
|
one-wire deployment, and every session then runs with the host's echo
|
||||||
|
discard on. The app fixture is built for (app_hz, app_baud); the hand-over
|
||||||
|
is checked at that point, and a second point at half the clock proves the
|
||||||
|
lock is measured, not baked in.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
def fail(message):
|
||||||
|
print(f"FAIL: {message}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
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)
|
||||||
|
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(__file__)))
|
||||||
|
import pbsim
|
||||||
|
import pureboot as pb
|
||||||
|
|
||||||
|
os.makedirs(workdir, exist_ok=True)
|
||||||
|
ee_image = bytes(range(0xA0, 0xB0))
|
||||||
|
ee_path = os.path.join(workdir, "ee.bin")
|
||||||
|
open(ee_path, "wb").write(ee_image)
|
||||||
|
|
||||||
|
# The geometry the surgery planner needs, from the chip class the runner is
|
||||||
|
# told — the same derivation pbtest.py makes: the boot-sectioned megas need
|
||||||
|
# no vector surgery, the tinies and the boot-section-less m48s do, and the
|
||||||
|
# large chips speak word addresses.
|
||||||
|
mega = mcu.startswith("atmega")
|
||||||
|
patch = not mega or mcu.startswith("atmega48")
|
||||||
|
word_flash = base + pb.SLOT > 0x10000
|
||||||
|
wire_base = base // 2 if word_flash else base
|
||||||
|
flags = (1 if patch else 0) | (2 if word_flash else 0)
|
||||||
|
ground_truth = pb.Info(bytes([ord("P"), ord("B"), pb.NEWEST_LOADER, 0, 0, 0, page & 0xFF,
|
||||||
|
wire_base & 0xFF, wire_base >> 8, 0, 0, flags]))
|
||||||
|
|
||||||
|
def round_trip(hz, baud, label, hand_over):
|
||||||
|
"""One clock point: reset, calibrate + knock, program, verify against the
|
||||||
|
simulator's own flash, and (at the app's point) hand over to the fixture."""
|
||||||
|
dump = os.path.join(workdir, f"flash_{label}.bin")
|
||||||
|
device = pbsim.Device(device_bin, elf, mcu, str(hz), base_hex, page, baud, dump, link=link)
|
||||||
|
try:
|
||||||
|
# The host tool, in autobaud mode, sends the 0xC0 calibration pulse
|
||||||
|
# and a single knock at `baud`; the loader locks to it.
|
||||||
|
out = pbsim.run_tool(tool, device.pty, baud, *extra, "--autobaud", "--info", "--clock", str(hz),
|
||||||
|
"--fuses", "--flash", app_bin, "--eeprom", ee_path, "--stay")
|
||||||
|
for needed in ("version", "signature", "fuses", "verify:", "stays"):
|
||||||
|
if needed not in out:
|
||||||
|
fail(f"{label}: session output lacks {needed!r}\n{out}")
|
||||||
|
# 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_flash = os.path.join(workdir, f"rf_{label}.bin")
|
||||||
|
read_eeprom = os.path.join(workdir, f"re_{label}.bin")
|
||||||
|
out = pbsim.run_tool(tool, device.pty, baud, *extra, "--autobaud", "--verify-flash", app_bin,
|
||||||
|
"--verify-eeprom", ee_path, "--read-flash", read_flash,
|
||||||
|
"--read-eeprom", read_eeprom, "--stay")
|
||||||
|
if out.count("verify:") != 2:
|
||||||
|
fail(f"{label}: did not verify both memories\n{out}")
|
||||||
|
if open(read_eeprom, "rb").read()[: len(ee_image)] != ee_image:
|
||||||
|
fail(f"{label}: EEPROM read-back mismatch")
|
||||||
|
|
||||||
|
if hand_over:
|
||||||
|
# Regression: a calibration pulse with no knock behind it must
|
||||||
|
# not wedge the loader. The knock's edge wait used to be
|
||||||
|
# unbudgeted, so one stray low pulse — EMI, or a host that opens
|
||||||
|
# the port and never knocks — held the loader forever and the
|
||||||
|
# application never ran. The whole activation is bounded now, so
|
||||||
|
# the window closes and the app boots; the banner is the proof.
|
||||||
|
# (The pause lets the loader reach its measurement loop, so the
|
||||||
|
# pulse is genuinely seen and the test cannot pass vacuously.)
|
||||||
|
device.reset()
|
||||||
|
port = pb.Port(device.pty, baud)
|
||||||
|
if one_wire:
|
||||||
|
port = pb.OneWirePort(port)
|
||||||
|
try:
|
||||||
|
time.sleep(0.2)
|
||||||
|
port.write(bytes((pb.CALIBRATE,)))
|
||||||
|
# Accumulate rather than match exactly: the reset leaves the
|
||||||
|
# idle line a framing artefact ahead of the banner, which is
|
||||||
|
# noise here — the question is only whether the app ran.
|
||||||
|
seen = b""
|
||||||
|
deadline = time.monotonic() + 180.0
|
||||||
|
while b"APP" not in seen and time.monotonic() < deadline:
|
||||||
|
seen += port.read_available(1.0)
|
||||||
|
if b"APP" not in seen:
|
||||||
|
fail(f"{label}: lone calibration pulse wedged the loader — app never bannered, saw {seen!r}")
|
||||||
|
print(f" {label}: lone calibration pulse does not wedge the loader")
|
||||||
|
finally:
|
||||||
|
port.close()
|
||||||
|
|
||||||
|
device.reset()
|
||||||
|
port = pb.Port(device.pty, baud)
|
||||||
|
if one_wire:
|
||||||
|
port = pb.OneWirePort(port)
|
||||||
|
try:
|
||||||
|
loader = pb.Loader(port)
|
||||||
|
live = loader.connect_autobaud(15)
|
||||||
|
if not pb.OLDEST_LOADER <= live.version <= pb.NEWEST_LOADER:
|
||||||
|
fail(f"{label}: loader reports pureboot {live.version}")
|
||||||
|
if loader.unified:
|
||||||
|
# pureboot 5's data space. 0x0200 is clear of the
|
||||||
|
# loader's own .noinit unit at the bottom of SRAM and of
|
||||||
|
# the stack at the top. Reading it back over the same
|
||||||
|
# locked link proves both directions of the new space.
|
||||||
|
probe = bytes(range(0x30, 0x40))
|
||||||
|
loader.write_ram(0x0200, probe)
|
||||||
|
if loader.read_ram(0x0200, len(probe)) != probe:
|
||||||
|
fail(f"{label}: RAM round-trip mismatch")
|
||||||
|
# The register file and the I/O space share the data
|
||||||
|
# address space on AVR, so the same command reaches a
|
||||||
|
# peripheral register. SPMCSR reads back as idle here.
|
||||||
|
verbose_ram = loader.read_ram(0x0200, 4)
|
||||||
|
print(f" {label}: RAM read/write ok ({verbose_ram.hex()})")
|
||||||
|
loader.run_application()
|
||||||
|
banner = port.read_exact(3, 5.0)
|
||||||
|
if banner != b"APP":
|
||||||
|
fail(f"{label}: application banner was {banner!r}")
|
||||||
|
finally:
|
||||||
|
port.close()
|
||||||
|
finally:
|
||||||
|
device.stop()
|
||||||
|
|
||||||
|
# Ground truth (read after the runner exits and writes its dump): what
|
||||||
|
# the tool programmed must be what the simulator actually holds.
|
||||||
|
pages = pb.plan_flash(open(app_bin, "rb").read(), ground_truth)
|
||||||
|
flash_true = open(dump, "rb").read()
|
||||||
|
for address, data in pages.items():
|
||||||
|
if flash_true[address : address + page] != data:
|
||||||
|
fail(f"{label}: simulator flash differs from the programmed image at {address:#06x}")
|
||||||
|
print(f" {label}: locked at {hz} Hz / {baud} Bd, flash+EEPROM verified"
|
||||||
|
+ (", hand-over ok" if hand_over else ""))
|
||||||
|
|
||||||
|
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
|
||||||
|
# second point at double that clock, same loader binary, proves the lock is
|
||||||
|
# measured, not baked in — the whole point of autobaud. (Doubling keeps the
|
||||||
|
# bit period healthy; halving would drop it below the software UART's floor.)
|
||||||
|
round_trip(app_hz, app_baud, "clock-a", hand_over=True)
|
||||||
|
round_trip(app_hz * 2, app_baud, "clock-b", hand_over=False)
|
||||||
|
|
||||||
|
# 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__":
|
||||||
|
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()
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
@@ -55,6 +56,12 @@ def main():
|
|||||||
# the page byte is the wire's 0-means-256.
|
# the page byte is the wire's 0-means-256.
|
||||||
mega = mcu.startswith("atmega")
|
mega = mcu.startswith("atmega")
|
||||||
patch = not mega or mcu.startswith("atmega48")
|
patch = not mega or mcu.startswith("atmega48")
|
||||||
|
# Where SRAM begins: the x8 and x4 megas push it past their extended I/O
|
||||||
|
# space, everything else starts right after the plain I/O registers. The
|
||||||
|
# loader keeps no statics and its stack sits at RAMEND, so the first SRAM
|
||||||
|
# byte is free for the data-space probe below.
|
||||||
|
classic = mcu in ("atmega8", "atmega8a", "atmega16", "atmega16a", "atmega32", "atmega32a")
|
||||||
|
ram_base = 0x0100 if mega and not classic else 0x0060
|
||||||
word_flash = base + pb.SLOT > 0x10000
|
word_flash = base + pb.SLOT > 0x10000
|
||||||
wire_base = base // 2 if word_flash else base
|
wire_base = base // 2 if word_flash else base
|
||||||
flags = (1 if patch else 0) | (2 if word_flash else 0)
|
flags = (1 if patch else 0) | (2 if word_flash else 0)
|
||||||
@@ -64,21 +71,34 @@ 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:
|
||||||
fail(f"session 1 output lacks {needed!r}")
|
fail(f"session 1 output lacks {needed!r}")
|
||||||
|
|
||||||
# Session 2: reconnect into the live session, verify, dump, hand over
|
# Session 2: reconnect into the live session, verify, dump, exercise
|
||||||
# is deferred — the pty must be reopened for the APP banner first.
|
# the data space; hand over is deferred — the pty must be reopened for
|
||||||
out = pbsim.run_tool(tool, device.pty, baud, "--verify-flash", app_bin, "--verify-eeprom", ee_path,
|
# the APP banner first.
|
||||||
"--read-flash", read_flash, "--read-eeprom", read_eeprom, "--stay")
|
probe = "c0ffee"
|
||||||
|
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,
|
||||||
|
"--poke", f"{ram_base:#x}:{probe}", "--peek", f"{ram_base:#x}:3", "--stay")
|
||||||
if out.count("verify:") != 2:
|
if out.count("verify:") != 2:
|
||||||
fail("session 2 did not verify both memories")
|
fail("session 2 did not verify both memories")
|
||||||
|
# What went into SRAM must come back out of it: the data space is one
|
||||||
|
# more selector on the same transfer as flash and EEPROM, so a wrong
|
||||||
|
# selector decode would show up here and nowhere else.
|
||||||
|
if probe not in out.replace(" ", ""):
|
||||||
|
fail(f"data-space round trip at {ram_base:#x} did not read back {probe}\n{out}")
|
||||||
|
|
||||||
eeprom_back = open(read_eeprom, "rb").read()
|
eeprom_back = open(read_eeprom, "rb").read()
|
||||||
if eeprom_back[: len(ee_image)] != ee_image:
|
if eeprom_back[: len(ee_image)] != ee_image:
|
||||||
@@ -97,22 +117,31 @@ 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)
|
||||||
# The loader built from this tree and the tool beside it must
|
# The loader built from this tree must report a version the tool
|
||||||
# agree on where the version numbering stands: a bump the tool
|
# beside it speaks — a bump the tool was never told about is a
|
||||||
# was never told about is a loader it would refuse to speak to.
|
# loader it would refuse to talk to. Not equality with the newest:
|
||||||
if live.version != pb.NEWEST_LOADER:
|
# the tool now spans two loader generations, the fixed-baud one
|
||||||
fail(f"loader reports pureboot {live.version}, the tool's newest is {pb.NEWEST_LOADER}")
|
# here and the unified autobaud loader that follows it.
|
||||||
|
if not pb.OLDEST_LOADER <= live.version <= pb.NEWEST_LOADER:
|
||||||
|
fail(f"loader reports pureboot {live.version}, the tool speaks "
|
||||||
|
f"{pb.OLDEST_LOADER}..{pb.NEWEST_LOADER}")
|
||||||
|
|
||||||
# A W addressed inside a page rather than at its base must still
|
# A W addressed inside a page rather than at its base must still
|
||||||
# consume exactly one page and prompt. The loader's own slot is
|
# consume exactly one page and prompt. The loader's own slot is the
|
||||||
# the target — it is drained and never programmed — and the
|
# target — the guard refuses to commit it — and the payload is
|
||||||
# payload is erased-state bytes, so the probe can disturb neither
|
# erased-state bytes, so the probe can disturb neither the image nor
|
||||||
# the image nor the page buffer it leaves behind.
|
# the page buffer it leaves behind. Hand-built rather than through
|
||||||
wire = wire_base + 1
|
# write_page(), which would follow the fill with its erase and
|
||||||
port.write(bytes((ord("W"), wire & 0xFF, wire >> 8)) + b"\xff" * page)
|
# write; the point here is that the fill alone consumes exactly one
|
||||||
|
# page whatever the address's low bits say.
|
||||||
|
wire = base + 1
|
||||||
|
port.write(bytes((ord("W"), pb.selector(pb.SP_FLASH, wire), wire & 0xFF, (wire >> 8) & 0xFF))
|
||||||
|
+ b"\xff" * page)
|
||||||
if port.read_exact(1, 5.0) != pb.PROMPT:
|
if port.read_exact(1, 5.0) != pb.PROMPT:
|
||||||
fail("unaligned W did not return to the prompt")
|
fail("unaligned W did not return to the prompt")
|
||||||
|
|
||||||
|
|||||||
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,461 +0,0 @@
|
|||||||
// simavr "device" for the pureboot protocol tests, every chip. Loads the
|
|
||||||
// boot-linked ELF at the loader base, starts execution there (BOOTRST / the
|
|
||||||
// patched vector are not what is under test), and exposes the loader's
|
|
||||||
// serial link as a pty for the real host tool:
|
|
||||||
//
|
|
||||||
// - Hardware USART builds: simavr's uart_pty on the selected instance.
|
|
||||||
// - Software UART builds: an 8N1 bridge between a pty and the GPIO pins,
|
|
||||||
// timed against the simulated cycle counter (drives the loader's RX,
|
|
||||||
// decodes its TX).
|
|
||||||
//
|
|
||||||
// The link follows the chip's natural default (USART0 on the megas, the
|
|
||||||
// software UART on PB0/PB1 elsewhere) unless -l overrides it: `-l usart1`
|
|
||||||
// for the second instance, `-l sw:B5,B1` for a software build's RX,TX pins.
|
|
||||||
//
|
|
||||||
// simavr's tiny cores decode the SPM opcode but attach no NVM module — SPM
|
|
||||||
// is a silent no-op (the mega's boot section has one, avr_flash). The
|
|
||||||
// missing module is supplied here: the SPM ioctl reads SPMCSR/Z/r1:r0 and
|
|
||||||
// implements buffer fill, page erase, page write, and CTPB, completing
|
|
||||||
// instantly. RFLB's LPM diversion (fuse readout) stays unmodeled, so the
|
|
||||||
// 'F' command answers with flash bytes — the tests assert transport only.
|
|
||||||
//
|
|
||||||
// On exit (or SIGTERM) the flash and EEPROM are dumped to files for a
|
|
||||||
// ground-truth cross-check against what the host read back.
|
|
||||||
#include <fcntl.h>
|
|
||||||
#include <pty.h>
|
|
||||||
#include <signal.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include <termios.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
|
|
||||||
#include "avr_eeprom.h"
|
|
||||||
#include "avr_flash.h"
|
|
||||||
#include "avr_ioport.h"
|
|
||||||
#include "avr_uart.h"
|
|
||||||
#include "sim_avr.h"
|
|
||||||
#include "sim_elf.h"
|
|
||||||
#include "sim_io.h"
|
|
||||||
#include "uart_pty.h"
|
|
||||||
|
|
||||||
static avr_t *avr;
|
|
||||||
static uart_pty_t uart_pty;
|
|
||||||
static int link_software;
|
|
||||||
static char uart_digit = '0';
|
|
||||||
static char sw_rx_port = 'B', sw_tx_port = 'B';
|
|
||||||
static int sw_rx_bit = 0, sw_tx_bit = 1;
|
|
||||||
static const char *dump_path;
|
|
||||||
static uint32_t reset_pc;
|
|
||||||
static volatile sig_atomic_t reset_requested;
|
|
||||||
|
|
||||||
static int parse_link(const char *spec)
|
|
||||||
{
|
|
||||||
if (strcmp(spec, "usart0") == 0 || strcmp(spec, "usart1") == 0) {
|
|
||||||
link_software = 0;
|
|
||||||
uart_digit = spec[5];
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (strncmp(spec, "sw", 2) == 0) {
|
|
||||||
link_software = 1;
|
|
||||||
if (spec[2] == '\0')
|
|
||||||
return 0;
|
|
||||||
if (sscanf(spec + 2, ":%c%d,%c%d", &sw_rx_port, &sw_rx_bit, &sw_tx_port, &sw_tx_bit) == 4)
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// simavr 1.6's avr_flash PGERS handler erases spm_pagesize bytes starting at
|
|
||||||
// Z & ~1 instead of the page containing Z (its PGWRT path masks correctly) —
|
|
||||||
// hardware ignores the in-page bits (§26.8.1), so an erase issued with Z
|
|
||||||
// anywhere inside the page wipes half the neighbouring page in simulation
|
|
||||||
// only. Wrap the mega's registered flash ioctl and re-dispatch page erases
|
|
||||||
// with Z forced to the page boundary; everything else passes through.
|
|
||||||
//
|
|
||||||
// A second gap on the boot-section-less m48s: their RWWSRE bit is the
|
|
||||||
// temporary-buffer discard (Atmel-8271 §26.2/§26.3.1), but the stock model
|
|
||||||
// gates its RWWSRE branch on AVR_SELFPROG_HAVE_RWW — absent on the m48
|
|
||||||
// core — so the discard store falls through into the buffer-fill branch and
|
|
||||||
// plants whatever Z/R1:R0 happen to hold. Perform the silicon's discard
|
|
||||||
// here instead.
|
|
||||||
static avr_flash_t *mega_flash;
|
|
||||||
static int (*mega_flash_ioctl)(avr_io_t *io, uint32_t ctl, void *param);
|
|
||||||
|
|
||||||
static int fixed_flash_ioctl(avr_io_t *io, uint32_t ctl, void *param)
|
|
||||||
{
|
|
||||||
if (ctl == AVR_IOCTL_FLASH_SPM && avr_regbit_get(io->avr, mega_flash->pgers)) {
|
|
||||||
uint16_t z = (uint16_t)(io->avr->data[30] | (io->avr->data[31] << 8));
|
|
||||||
uint16_t masked = (uint16_t)(z & ~(mega_flash->spm_pagesize - 1));
|
|
||||||
io->avr->data[30] = (uint8_t)masked;
|
|
||||||
io->avr->data[31] = (uint8_t)(masked >> 8);
|
|
||||||
int result = mega_flash_ioctl(io, ctl, param);
|
|
||||||
io->avr->data[30] = (uint8_t)z;
|
|
||||||
io->avr->data[31] = (uint8_t)(z >> 8);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
if (ctl == AVR_IOCTL_FLASH_SPM && !(mega_flash->flags & AVR_SELFPROG_HAVE_RWW) &&
|
|
||||||
(io->avr->data[mega_flash->r_spm] & 0x11) == 0x11) { // RWWSRE|SELFPRGEN: the m48 buffer discard
|
|
||||||
for (int i = 0; i < mega_flash->spm_pagesize / 2; i++) {
|
|
||||||
mega_flash->tmppage[i] = 0xffff;
|
|
||||||
mega_flash->tmppage_used[i] = 0;
|
|
||||||
}
|
|
||||||
avr_regbit_clear(io->avr, mega_flash->selfprgen);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return mega_flash_ioctl(io, ctl, param);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void fix_mega_flash_erase(void)
|
|
||||||
{
|
|
||||||
for (avr_io_t *io = avr->io_port; io; io = io->next) {
|
|
||||||
if (io->kind && strcmp(io->kind, "flash") == 0) {
|
|
||||||
mega_flash = (avr_flash_t *)io;
|
|
||||||
mega_flash_ioctl = io->ioctl;
|
|
||||||
io->ioctl = fixed_flash_ioctl;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fprintf(stderr, "device: no flash module to fix — SPM page erases may misalign\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
static void request_reset(int sig)
|
|
||||||
{
|
|
||||||
(void)sig;
|
|
||||||
reset_requested = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------- tiny NVM ---
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
avr_io_t io;
|
|
||||||
uint8_t buffer[128];
|
|
||||||
uint8_t used[128]; // a buffer word loads once until erased — like silicon
|
|
||||||
unsigned page;
|
|
||||||
} tiny_nvm_t;
|
|
||||||
|
|
||||||
static tiny_nvm_t nvm;
|
|
||||||
|
|
||||||
static int nvm_ioctl(avr_io_t *io, uint32_t ctl, void *param)
|
|
||||||
{
|
|
||||||
(void)param;
|
|
||||||
if (ctl != AVR_IOCTL_FLASH_SPM)
|
|
||||||
return -1;
|
|
||||||
tiny_nvm_t *n = (tiny_nvm_t *)io;
|
|
||||||
avr_t *mcu = io->avr;
|
|
||||||
uint8_t command = mcu->data[0x57] & 0x1f; // SPMCSR, both tinies
|
|
||||||
uint16_t z = (uint16_t)(mcu->data[30] | (mcu->data[31] << 8));
|
|
||||||
uint32_t page_base = (uint32_t)(z & ~(n->page - 1)) % (mcu->flashend + 1);
|
|
||||||
if (command == 0x01) { // SPMEN alone: buffer fill from r1:r0
|
|
||||||
unsigned offset = z & (n->page - 1) & ~1u;
|
|
||||||
if (!n->used[offset]) { // first write wins until the buffer clears
|
|
||||||
n->buffer[offset] = mcu->data[0];
|
|
||||||
n->buffer[offset + 1] = mcu->data[1];
|
|
||||||
n->used[offset] = 1;
|
|
||||||
}
|
|
||||||
} else if (command == 0x03) { // PGERS
|
|
||||||
memset(mcu->flash + page_base, 0xff, n->page);
|
|
||||||
} else if (command == 0x05) { // PGWRT: programming only clears bits
|
|
||||||
for (unsigned i = 0; i < n->page; i++)
|
|
||||||
mcu->flash[page_base + i] &= n->buffer[i];
|
|
||||||
memset(n->buffer, 0xff, n->page);
|
|
||||||
memset(n->used, 0, n->page);
|
|
||||||
} else if (command == 0x11) { // CTPB
|
|
||||||
memset(n->buffer, 0xff, n->page);
|
|
||||||
memset(n->used, 0, n->page);
|
|
||||||
}
|
|
||||||
mcu->data[0x57] &= (uint8_t)~0x1f; // the operation completes instantly
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------- GPIO bridge ---
|
|
||||||
|
|
||||||
static int pty_master = -1;
|
|
||||||
static avr_irq_t *rx_pin; // the loader's RX (PB0), driven from the pty
|
|
||||||
static avr_cycle_count_t bit_cycles;
|
|
||||||
|
|
||||||
static int tx_level = 1, tx_active, tx_bit;
|
|
||||||
static uint8_t tx_shift;
|
|
||||||
|
|
||||||
static avr_cycle_count_t tx_sample(avr_t *mcu, avr_cycle_count_t when, void *param)
|
|
||||||
{
|
|
||||||
(void)mcu;
|
|
||||||
(void)param;
|
|
||||||
tx_shift = (uint8_t)((tx_shift >> 1) | (tx_level ? 0x80 : 0));
|
|
||||||
if (++tx_bit < 8)
|
|
||||||
return when + bit_cycles;
|
|
||||||
if (write(pty_master, &tx_shift, 1) != 1)
|
|
||||||
fprintf(stderr, "device: pty write lost a byte\n");
|
|
||||||
tx_active = 0;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void tx_hook(avr_irq_t *irq, uint32_t value, void *param)
|
|
||||||
{
|
|
||||||
(void)irq;
|
|
||||||
(void)param;
|
|
||||||
int level = value & 1;
|
|
||||||
if (!tx_active && tx_level == 1 && level == 0) { // start edge
|
|
||||||
tx_active = 1;
|
|
||||||
tx_bit = 0;
|
|
||||||
avr_cycle_timer_register(avr, bit_cycles + bit_cycles / 2, tx_sample, NULL);
|
|
||||||
}
|
|
||||||
tx_level = level;
|
|
||||||
}
|
|
||||||
|
|
||||||
static uint8_t rx_queue[8192];
|
|
||||||
static unsigned rx_head, rx_tail; // ring: head = next to send
|
|
||||||
static int rx_active, rx_bit;
|
|
||||||
static uint8_t rx_byte;
|
|
||||||
|
|
||||||
static void rx_start_next(void);
|
|
||||||
|
|
||||||
static avr_cycle_count_t rx_step(avr_t *mcu, avr_cycle_count_t when, void *param)
|
|
||||||
{
|
|
||||||
(void)mcu;
|
|
||||||
(void)param;
|
|
||||||
if (rx_bit < 8) {
|
|
||||||
avr_raise_irq(rx_pin, (rx_byte >> rx_bit) & 1);
|
|
||||||
rx_bit++;
|
|
||||||
return when + bit_cycles;
|
|
||||||
}
|
|
||||||
if (rx_bit == 8) { // stop bit, plus one idle bit of margin
|
|
||||||
avr_raise_irq(rx_pin, 1);
|
|
||||||
rx_bit++;
|
|
||||||
return when + 2 * bit_cycles;
|
|
||||||
}
|
|
||||||
rx_active = 0;
|
|
||||||
rx_start_next();
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void rx_start_next(void)
|
|
||||||
{
|
|
||||||
if (rx_active || rx_head == rx_tail)
|
|
||||||
return;
|
|
||||||
rx_byte = rx_queue[rx_head];
|
|
||||||
rx_head = (rx_head + 1) % sizeof(rx_queue);
|
|
||||||
rx_active = 1;
|
|
||||||
rx_bit = 0;
|
|
||||||
avr_raise_irq(rx_pin, 0); // start bit
|
|
||||||
avr_cycle_timer_register(avr, bit_cycles, rx_step, NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
// A reset abandons whatever the bridge was mid-transfer: bytes still queued
|
|
||||||
// for a chip that no longer has the context to receive them meaningfully,
|
|
||||||
// and a decode in progress on a TX line the reset may have already changed.
|
|
||||||
// The pending cycle timers must go with the state: avr_reset drops the TX
|
|
||||||
// output latch, whose falling edge starts a spurious decode before this
|
|
||||||
// runs, and a stale tx_sample would then interleave with the loader's first
|
|
||||||
// real answer through the shared shift state, corrupting it.
|
|
||||||
static void bridge_reset(void)
|
|
||||||
{
|
|
||||||
avr_cycle_timer_cancel(avr, tx_sample, NULL);
|
|
||||||
avr_cycle_timer_cancel(avr, rx_step, NULL);
|
|
||||||
rx_head = rx_tail = 0;
|
|
||||||
rx_active = 0;
|
|
||||||
tx_active = 0;
|
|
||||||
tx_level = 1;
|
|
||||||
avr_raise_irq(rx_pin, 1); // idle line
|
|
||||||
}
|
|
||||||
|
|
||||||
static void poll_pty(void)
|
|
||||||
{
|
|
||||||
uint8_t chunk[256];
|
|
||||||
ssize_t got = read(pty_master, chunk, sizeof(chunk));
|
|
||||||
for (ssize_t i = 0; i < got; i++) {
|
|
||||||
unsigned next = (rx_tail + 1) % sizeof(rx_queue);
|
|
||||||
if (next == rx_head)
|
|
||||||
break; // full: the host will retry on timeout
|
|
||||||
rx_queue[rx_tail] = chunk[i];
|
|
||||||
rx_tail = next;
|
|
||||||
}
|
|
||||||
if (got > 0)
|
|
||||||
rx_start_next();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------ main ---
|
|
||||||
|
|
||||||
static void finish(int sig)
|
|
||||||
{
|
|
||||||
(void)sig;
|
|
||||||
if (dump_path) {
|
|
||||||
FILE *f = fopen(dump_path, "wb");
|
|
||||||
if (f) {
|
|
||||||
fwrite(avr->flash, 1, avr->flashend + 1, f);
|
|
||||||
fclose(f);
|
|
||||||
}
|
|
||||||
avr_eeprom_desc_t ee = {.ee = NULL, .offset = 0, .size = 0};
|
|
||||||
if (avr_ioctl(avr, AVR_IOCTL_EEPROM_GET, &ee) == 0 && ee.ee && ee.size) {
|
|
||||||
char path[512];
|
|
||||||
snprintf(path, sizeof(path), "%s.eeprom", dump_path);
|
|
||||||
f = fopen(path, "wb");
|
|
||||||
if (f) {
|
|
||||||
fwrite(ee.ee, 1, ee.size, f);
|
|
||||||
fclose(f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!link_software)
|
|
||||||
uart_pty_stop(&uart_pty);
|
|
||||||
_exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
|
||||||
{
|
|
||||||
int link_given = 0;
|
|
||||||
for (int opt; (opt = getopt(argc, argv, "l:")) != -1;) {
|
|
||||||
if (opt != 'l' || parse_link(optarg) != 0) {
|
|
||||||
fprintf(stderr, "device: bad link spec (usart0, usart1, sw, or sw:B0,B1 as RX,TX)\n");
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
link_given = 1;
|
|
||||||
}
|
|
||||||
int args = argc - optind;
|
|
||||||
if (args < 7 || args > 9) {
|
|
||||||
fprintf(stderr,
|
|
||||||
"usage: %s [-l link] <pureboot.elf> <mcu> <hz> <base_hex> <page> <baud> <flash_dump>"
|
|
||||||
" [reset_hex] [resume_flash]\n"
|
|
||||||
" -l link: usart0 | usart1 | sw[:B0,B1] (RX,TX); default: the chip's own\n"
|
|
||||||
" reset_hex: reset vector (default: base with a boot section, else 0)\n"
|
|
||||||
" resume_flash: raw full-flash image loaded instead of the ELF — a prior\n"
|
|
||||||
" run's dump, for power-fail resume tests\n",
|
|
||||||
argv[0]);
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
argv += optind - 1; // argv[1] is the ELF again, whatever was parsed
|
|
||||||
const char *mcu_name = argv[2];
|
|
||||||
uint32_t base = (uint32_t)strtoul(argv[4], NULL, 0);
|
|
||||||
unsigned page = (unsigned)atoi(argv[5]);
|
|
||||||
unsigned baud = (unsigned)atoi(argv[6]);
|
|
||||||
dump_path = argv[7];
|
|
||||||
int is_mega = strncmp(mcu_name, "atmega", 6) == 0;
|
|
||||||
if (!link_given)
|
|
||||||
link_software = !is_mega; // the chips' natural links: USART0, or PB0/PB1
|
|
||||||
|
|
||||||
avr = avr_make_mcu_by_name(mcu_name);
|
|
||||||
if (!avr) {
|
|
||||||
fprintf(stderr, "device: no %s core\n", mcu_name);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
avr_init(avr);
|
|
||||||
avr->frequency = (uint32_t)strtoul(argv[3], NULL, 0);
|
|
||||||
memset(avr->flash, 0xff, avr->flashend + 1); // real flash powers up erased
|
|
||||||
|
|
||||||
if (args > 8) {
|
|
||||||
// Resume: the full flash image of an interrupted prior run.
|
|
||||||
FILE *f = fopen(argv[9], "rb");
|
|
||||||
if (!f || fread(avr->flash, 1, avr->flashend + 1, f) == 0) {
|
|
||||||
fprintf(stderr, "device: cannot read %s\n", argv[9]);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
fclose(f);
|
|
||||||
} else {
|
|
||||||
elf_firmware_t fw = {0};
|
|
||||||
if (elf_read_firmware(argv[1], &fw) != 0) {
|
|
||||||
fprintf(stderr, "device: cannot read %s\n", argv[1]);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
memcpy(avr->flash + base, fw.flash, fw.flashsize);
|
|
||||||
}
|
|
||||||
// The boot-sectioned megas enter the loader in hardware (BOOTRST, not
|
|
||||||
// modeled — the argument picks the modeled fuse's target); the tinies
|
|
||||||
// and the boot-section-less m48s reset to word 0 like silicon — erased
|
|
||||||
// flash walks up into the loader, and after the host's surgery the
|
|
||||||
// patched vector routes there.
|
|
||||||
int boot_section = is_mega && strncmp(mcu_name, "atmega48", 8) != 0;
|
|
||||||
reset_pc = args > 7 ? (uint32_t)strtoul(argv[8], NULL, 0) : (boot_section ? base : 0);
|
|
||||||
avr->pc = reset_pc;
|
|
||||||
avr->codeend = avr->flashend;
|
|
||||||
|
|
||||||
// Erased EEPROM, as hardware powers up (simavr zeroes it).
|
|
||||||
uint8_t blank[1024];
|
|
||||||
memset(blank, 0xff, sizeof(blank));
|
|
||||||
avr_eeprom_desc_t seed = {.ee = blank, .offset = 0, .size = 0};
|
|
||||||
if (avr_ioctl(avr, AVR_IOCTL_EEPROM_GET, &seed) == 0 && seed.size <= sizeof(blank)) {
|
|
||||||
seed.ee = blank;
|
|
||||||
avr_ioctl(avr, AVR_IOCTL_EEPROM_SET, &seed);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The megas carry simavr's avr_flash module (and its two gaps the wrap
|
|
||||||
// above fixes); the tinies get the NVM module simavr lacks. Which serial
|
|
||||||
// bridge runs is the link's business, not the chip class's.
|
|
||||||
if (is_mega) {
|
|
||||||
fix_mega_flash_erase();
|
|
||||||
} else {
|
|
||||||
nvm.page = page;
|
|
||||||
memset(nvm.buffer, 0xff, sizeof(nvm.buffer));
|
|
||||||
nvm.io.kind = "tiny_nvm";
|
|
||||||
nvm.io.ioctl = nvm_ioctl;
|
|
||||||
avr_register_io(avr, &nvm.io);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!link_software) {
|
|
||||||
// POLL_SLEEP paces an idle-polling loader in host real time (a
|
|
||||||
// no-hardware CPU-saving hack); clear it so cycles run free.
|
|
||||||
uint32_t flags = 0;
|
|
||||||
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS(uart_digit), &flags);
|
|
||||||
flags &= ~AVR_UART_FLAG_POLL_SLEEP;
|
|
||||||
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS(uart_digit), &flags);
|
|
||||||
uart_pty_init(avr, &uart_pty);
|
|
||||||
uart_pty_connect(&uart_pty, uart_digit);
|
|
||||||
printf("PB_PTY %s\n", uart_pty.pty.slavename);
|
|
||||||
} else {
|
|
||||||
bit_cycles = (avr->frequency + baud / 2) / baud; // matches uart.hpp's own rounding exactly
|
|
||||||
rx_pin = avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ(sw_rx_port), (unsigned)sw_rx_bit);
|
|
||||||
avr_irq_register_notify(avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ(sw_tx_port), (unsigned)sw_tx_bit), tx_hook,
|
|
||||||
NULL);
|
|
||||||
avr_raise_irq(rx_pin, 1); // idle line
|
|
||||||
|
|
||||||
int slave;
|
|
||||||
struct termios raw;
|
|
||||||
cfmakeraw(&raw);
|
|
||||||
if (openpty(&pty_master, &slave, NULL, &raw, NULL) != 0) {
|
|
||||||
fprintf(stderr, "device: openpty failed\n");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
fcntl(pty_master, F_SETFL, O_NONBLOCK);
|
|
||||||
printf("PB_PTY %s\n", ttyname(slave));
|
|
||||||
}
|
|
||||||
fflush(stdout);
|
|
||||||
|
|
||||||
signal(SIGTERM, finish);
|
|
||||||
signal(SIGINT, finish);
|
|
||||||
signal(SIGUSR1, request_reset); // an external reset line, for the tests
|
|
||||||
|
|
||||||
long since_poll = 0;
|
|
||||||
for (;;) {
|
|
||||||
int state = avr_run(avr);
|
|
||||||
if (state == cpu_Done || state == cpu_Crashed)
|
|
||||||
break;
|
|
||||||
if (reset_requested) {
|
|
||||||
reset_requested = 0;
|
|
||||||
avr_reset(avr);
|
|
||||||
avr->pc = reset_pc;
|
|
||||||
if (!link_software) { // reset restores the pacing hack; re-clear it
|
|
||||||
uint32_t flags = 0;
|
|
||||||
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS(uart_digit), &flags);
|
|
||||||
flags &= ~AVR_UART_FLAG_POLL_SLEEP;
|
|
||||||
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS(uart_digit), &flags);
|
|
||||||
} else {
|
|
||||||
bridge_reset();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (link_software && ++since_poll >= 2000) {
|
|
||||||
since_poll = 0;
|
|
||||||
poll_pty();
|
|
||||||
// An unthrottled idle simulation runs the activation window out
|
|
||||||
// from under the host's real-time knock cadence: a 1 MHz build's
|
|
||||||
// 8 s window is 8 M cycles — tens of wall milliseconds — so a
|
|
||||||
// first knock lost to an in-flight reset misses the window
|
|
||||||
// entirely. Pace the simulation only while the bridge is fully
|
|
||||||
// quiet (nothing decoding, nothing queued); transfers keep full
|
|
||||||
// speed, and a quiet window stretches toward real time.
|
|
||||||
if (!rx_active && !tx_active && rx_head == rx_tail)
|
|
||||||
usleep(200);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finish(0);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
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())
|
||||||
@@ -30,8 +30,14 @@ def info_of(pb, base, page, patch, flash, signature=(0x1E, 0x93, 0x0B), word_fla
|
|||||||
scale = 2 if word_flash else 1
|
scale = 2 if word_flash else 1
|
||||||
wire_base = base // scale
|
wire_base = base // scale
|
||||||
flags = (1 if patch else 0) | (2 if word_flash else 0)
|
flags = (1 if patch else 0) | (2 if word_flash else 0)
|
||||||
|
# The EEPROM size comes from the signature, as it must: pureboot 5 derives
|
||||||
|
# the whole geometry from the signature rather than sending it, so a
|
||||||
|
# synthetic block that disagreed with its own signature would describe a
|
||||||
|
# chip that cannot exist.
|
||||||
|
eeprom = pb.CHIP_GEOMETRY[signature][2]
|
||||||
raw = bytes((0x50, 0x42, pb.NEWEST_LOADER if version is None else version,
|
raw = bytes((0x50, 0x42, pb.NEWEST_LOADER if version is None else version,
|
||||||
*signature, page & 0xFF, wire_base & 0xFF, wire_base >> 8, 0, 2, flags))
|
*signature, page & 0xFF, wire_base & 0xFF, wire_base >> 8,
|
||||||
|
eeprom & 0xFF, eeprom >> 8, flags))
|
||||||
info = pb.Info(raw)
|
info = pb.Info(raw)
|
||||||
if info.flash_size != flash:
|
if info.flash_size != flash:
|
||||||
fail(f"info_of({base:#x}) decodes to {info.flash_size:#x} of flash, not {flash:#x}")
|
fail(f"info_of({base:#x}) decodes to {info.flash_size:#x} of flash, not {flash:#x}")
|
||||||
@@ -162,11 +168,16 @@ def main():
|
|||||||
fail("mega staging content should be the bare image")
|
fail("mega staging content should be the bare image")
|
||||||
expect_error("mega staging size", lambda: pb.staging_content(image + b"!", mega), "512")
|
expect_error("mega staging size", lambda: pb.staging_content(image + b"!", mega), "512")
|
||||||
|
|
||||||
# The embedded info block: found in a synthetic binary, absent in noise.
|
# The image stamp: found in a synthetic binary, absent in noise. pureboot
|
||||||
binary = bytes((0xAA,)) * 10 + tiny.raw + bytes((0xBB,)) * 10
|
# 5 stamps the magic, its version and the signature, and the geometry is
|
||||||
|
# looked up from there — so what comes back must equal what a live device
|
||||||
|
# of the same chip reports.
|
||||||
|
stamp = bytes((0x50, 0x42, pb.NEWEST_LOADER)) + bytes(tiny.signature)
|
||||||
|
binary = bytes((0xAA,)) * 10 + stamp + bytes((0xBB,)) * 10
|
||||||
found = pb.image_info(binary)
|
found = pb.image_info(binary)
|
||||||
if found is None or found.raw != tiny.raw:
|
if found is None or found.raw != tiny.raw:
|
||||||
fail("image_info misses the embedded block")
|
fail(f"image_info misreads the v{pb.NEWEST_LOADER} stamp: "
|
||||||
|
f"{found.raw.hex() if found else None} != {tiny.raw.hex()}")
|
||||||
if pb.image_info(bytes((0xAA,)) * 40) is not None:
|
if pb.image_info(bytes((0xAA,)) * 40) is not None:
|
||||||
fail("image_info invents a block")
|
fail("image_info invents a block")
|
||||||
# An older loader's image stays readable, so a deployed build can be
|
# An older loader's image stays readable, so a deployed build can be
|
||||||
|
|||||||
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())
|
||||||
|
|||||||
265
tools/pbhw.py
Executable file
265
tools/pbhw.py
Executable file
@@ -0,0 +1,265 @@
|
|||||||
|
#!/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 erase_and_guard(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))
|
||||||
|
|
||||||
|
# The slot must be untouched by an application erase, which only an
|
||||||
|
# independent read can show — so this one goes over ISP, not the link.
|
||||||
|
whole = self.work / "whole.bin"
|
||||||
|
if not self.rig.read_memory("flash", whole, "r"):
|
||||||
|
self.check("loader slot survives the erase", False, "ISP read failed")
|
||||||
|
return
|
||||||
|
image = whole.read_bytes()
|
||||||
|
image += b"\xff" * (info.flash_size - len(image))
|
||||||
|
|
||||||
|
# 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(image[0:limit]) <= {0xFF}, f"0x0000..{limit:#06x}")
|
||||||
|
|
||||||
|
if loader_image and loader_image.exists():
|
||||||
|
want = loader_image.read_bytes()
|
||||||
|
got = image[info.base:info.base + len(want)]
|
||||||
|
self.check("loader slot survives the erase", got == want,
|
||||||
|
f"{len(want)} B at {info.base:#06x}")
|
||||||
|
else:
|
||||||
|
print(" skip loader slot comparison (pass --loader <image.bin>)")
|
||||||
|
|
||||||
|
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) -> 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 write guard")
|
||||||
|
self.erase_and_guard(info, loader_image)
|
||||||
|
|
||||||
|
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("--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)
|
||||||
|
|
||||||
|
|
||||||
|
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