Compare commits
16 Commits
b60f182105
...
v8
| Author | SHA1 | Date | |
|---|---|---|---|
| eb213e1025 | |||
| 3e4bfbaf48 | |||
| 579ca81b27 | |||
| 5d520a1ff9 | |||
| f71d76a815 | |||
| 47419400f6 | |||
| bad8b6b43e | |||
| 5d1b4497d4 | |||
| a743ea64a3 | |||
| aa66cfccff | |||
| 459d463283 | |||
| 8e7cc86fb3 | |||
| c8ac61779e | |||
| 4362886c39 | |||
| 0513d07e87 | |||
| d4ab28aa17 |
167
CMakeLists.txt
167
CMakeLists.txt
@@ -202,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
|
||||||
@@ -297,6 +329,9 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
# default pair, or the index of the USART whose own pins a bit-banged
|
# default pair, or the index of the USART whose own pins a bit-banged
|
||||||
# link sits on. Unreachable rates drop out here rather than aborting the
|
# link sits on. Unreachable rates drop out here rather than aborting the
|
||||||
# configure.
|
# 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)
|
function(pureboot_matrix_point hz baud link pins)
|
||||||
set(_name pbm_${hz}_${baud}_${link})
|
set(_name pbm_${hz}_${baud}_${link})
|
||||||
if(link STREQUAL "software")
|
if(link STREQUAL "software")
|
||||||
@@ -306,9 +341,21 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
list(APPEND _args RX ${PUREBOOT_USART${pins}_RX} TX ${PUREBOOT_USART${pins}_TX})
|
list(APPEND _args RX ${PUREBOOT_USART${pins}_RX} TX ${PUREBOOT_USART${pins}_TX})
|
||||||
set(_name ${_name}_on${pins})
|
set(_name ${_name}_on${pins})
|
||||||
endif()
|
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(${_name} CLOCK ${hz} BAUD ${baud} ${_args})
|
pureboot_size_variant(${_name} CLOCK ${hz} BAUD ${baud} ${_args})
|
||||||
@@ -350,13 +397,18 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
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} software 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 "")
|
||||||
|
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} software 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 "")
|
||||||
|
pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} 1 "" HALF_DUPLEX)
|
||||||
endif()
|
endif()
|
||||||
endforeach()
|
endforeach()
|
||||||
endforeach()
|
endforeach()
|
||||||
@@ -422,6 +474,30 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
RX ${PUREBOOT_USART0_RX} TX ${PUREBOOT_USART0_TX})
|
RX ${PUREBOOT_USART0_RX} TX ${PUREBOOT_USART0_TX})
|
||||||
endif()
|
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
|
# 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,
|
# 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 —
|
# an sts — DS40002061B §36), plain I/O on the 85 (data 0x51, an out —
|
||||||
@@ -496,6 +572,58 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
${CMAKE_BINARY_DIR}/pbmute-work ${_mute_link})
|
${CMAKE_BINARY_DIR}/pbmute-work ${_mute_link})
|
||||||
set_tests_properties(pureboot.mute PROPERTIES TIMEOUT 180)
|
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()
|
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
|
||||||
@@ -543,5 +671,44 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
1000000 9600 ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
1000000 9600 ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
${CMAKE_BINARY_DIR}/pbautobaud-work)
|
${CMAKE_BINARY_DIR}/pbautobaud-work)
|
||||||
set_tests_properties(pureboot.autobaud PROPERTIES TIMEOUT 240)
|
set_tests_properties(pureboot.autobaud PROPERTIES TIMEOUT 240)
|
||||||
|
|
||||||
|
# The tightest deployment in the space, end to end: the autobaud
|
||||||
|
# loader folded onto the USART's own RXD with the OSCCAL trim baked
|
||||||
|
# — one-wire calibration, the receive-side release, and the host's
|
||||||
|
# echo discard, over the same two-clock sweep. One chip carries it;
|
||||||
|
# the shape is chip-independent.
|
||||||
|
if(LIBAVR_MCU STREQUAL "atmega328p")
|
||||||
|
get_target_property(_ab1w_link pureboot_1w_autobaud_osccal_on_usart0 PUREBOOT_LINK)
|
||||||
|
add_executable(pbapp_autobaud_1w test/pbapp.cpp)
|
||||||
|
target_link_libraries(pbapp_autobaud_1w PRIVATE libavr)
|
||||||
|
target_compile_definitions(pbapp_autobaud_1w PRIVATE PUREBOOT_CLOCK_HZ=1000000
|
||||||
|
PUREBOOT_BAUD=9600 PUREBOOT_SOFT_SERIAL
|
||||||
|
PUREBOOT_RX=pd0 PUREBOOT_TX=pd0)
|
||||||
|
add_custom_command(TARGET pbapp_autobaud_1w POST_BUILD
|
||||||
|
COMMAND ${CMAKE_OBJCOPY} -O binary
|
||||||
|
$<TARGET_FILE:pbapp_autobaud_1w> $<TARGET_FILE:pbapp_autobaud_1w>.bin)
|
||||||
|
add_test(NAME pureboot.autobaud.onewire
|
||||||
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbautobaud.py
|
||||||
|
${PB_DEVICE} $<TARGET_FILE:pureboot_1w_autobaud_osccal_on_usart0>
|
||||||
|
${PUREBOOT_SIM_MCU} ${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE}
|
||||||
|
$<TARGET_FILE:pbapp_autobaud_1w>.bin
|
||||||
|
1000000 9600 ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
|
${CMAKE_BINARY_DIR}/pbautobaud-1w-work ${_ab1w_link})
|
||||||
|
set_tests_properties(pureboot.autobaud.onewire PROPERTIES TIMEOUT 240)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# The autobaud window: the calibration poll budget, at the measured
|
||||||
|
# 10 cycles a poll (pbwindow.py pins the constant the README's
|
||||||
|
# seconds arithmetic uses; the budget itself is the clock-free knob).
|
||||||
|
add_test(NAME pureboot.window.autobaud
|
||||||
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbwindow.py
|
||||||
|
--device ${PB_DEVICE} --loader $<TARGET_FILE:pureboot_autobaud>
|
||||||
|
--mcu ${PUREBOOT_SIM_MCU} --hz 1000000
|
||||||
|
--base ${PUREBOOT_BASE_HEX} --page ${PUREBOOT_PAGE}
|
||||||
|
--baud 9600 --app $<TARGET_FILE:pbapp_autobaud>.bin
|
||||||
|
--autobaud-polls 4000000 --link sw
|
||||||
|
--tool ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
|
--workdir ${CMAKE_BINARY_DIR}/pbwindow-autobaud-work)
|
||||||
|
set_tests_properties(pureboot.window.autobaud PROPERTIES TIMEOUT 300)
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
2
libavr
2
libavr
Submodule libavr updated: 674e63e8f9...c01b19b08f
@@ -166,7 +166,9 @@ 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_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})
|
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
|
||||||
@@ -236,7 +238,8 @@ endfunction()
|
|||||||
|
|
||||||
# pureboot_add_loader(<name> [CLOCK <hz>] [BAUD <bd>]
|
# pureboot_add_loader(<name> [CLOCK <hz>] [BAUD <bd>]
|
||||||
# [SERIAL auto|hardware|software|autobaud] [USART <n>]
|
# [SERIAL auto|hardware|software|autobaud] [USART <n>]
|
||||||
# [RX <pin>] [TX <pin>] [TIMEOUT <s>] [OSCCAL <byte>])
|
# [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
|
||||||
@@ -244,6 +247,11 @@ endfunction()
|
|||||||
# usart0, usart1, or sw:<RX>,<TX> with a trailing @<n> where those pins are a
|
# 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.
|
# USART's own) — what a test harness speaks to it with.
|
||||||
#
|
#
|
||||||
|
# HALF_DUPLEX is the one-wire deployment, per backend: on the hardware USART
|
||||||
|
# it enables the library's .half_duplex turn-around (RXD and TXD tied
|
||||||
|
# together off-chip); on a software or autobaud link it puts both directions
|
||||||
|
# on the RX pin — the same thing RX == TX spells directly.
|
||||||
|
#
|
||||||
# SERIAL autobaud measures the host's bit timing at run time, so the image
|
# SERIAL autobaud measures the host's bit timing at run time, so the image
|
||||||
# carries no clock and no baud: CLOCK and BAUD are not build parameters there,
|
# carries no clock and no baud: CLOCK and BAUD are not build parameters there,
|
||||||
# and one binary per chip serves every F_CPU and every rate. The stamped
|
# and one binary per chip serves every F_CPU and every rate. The stamped
|
||||||
@@ -257,7 +265,7 @@ endfunction()
|
|||||||
# purely for the application's benefit, its own link being clock-free. No
|
# purely for the application's benefit, its own link being clock-free. No
|
||||||
# value, no code.
|
# value, no code.
|
||||||
function(pureboot_add_loader name)
|
function(pureboot_add_loader name)
|
||||||
cmake_parse_arguments(PB "" "CLOCK;BAUD;SERIAL;USART;RX;TX;TIMEOUT;OSCCAL" "" ${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()
|
||||||
@@ -294,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")
|
||||||
@@ -303,6 +314,9 @@ function(pureboot_add_loader name)
|
|||||||
endif()
|
endif()
|
||||||
if(_usart)
|
if(_usart)
|
||||||
set(_link usart0)
|
set(_link usart0)
|
||||||
|
if(PB_HALF_DUPLEX)
|
||||||
|
set(_serial_defines PUREBOOT_HALF_DUPLEX)
|
||||||
|
endif()
|
||||||
else()
|
else()
|
||||||
set(PB_SERIAL software)
|
set(PB_SERIAL software)
|
||||||
endif()
|
endif()
|
||||||
@@ -311,6 +325,15 @@ function(pureboot_add_loader name)
|
|||||||
if(NOT PB_RX)
|
if(NOT PB_RX)
|
||||||
set(PB_RX pb0)
|
set(PB_RX pb0)
|
||||||
endif()
|
endif()
|
||||||
|
if(PB_HALF_DUPLEX)
|
||||||
|
# One-wire: both directions on the RX pin. RX == TX spells
|
||||||
|
# the same deployment directly.
|
||||||
|
if(PB_TX AND NOT PB_TX STREQUAL PB_RX)
|
||||||
|
message(FATAL_ERROR "pureboot_add_loader(${name}): HALF_DUPLEX puts both "
|
||||||
|
"directions on RX (${PB_RX}); TX ${PB_TX} contradicts it")
|
||||||
|
endif()
|
||||||
|
set(PB_TX ${PB_RX})
|
||||||
|
endif()
|
||||||
if(NOT PB_TX)
|
if(NOT PB_TX)
|
||||||
set(PB_TX pb1)
|
set(PB_TX pb1)
|
||||||
endif()
|
endif()
|
||||||
@@ -334,10 +357,19 @@ function(pureboot_add_loader name)
|
|||||||
string(REPLACE "SW" "sw" _link ${_link})
|
string(REPLACE "SW" "sw" _link ${_link})
|
||||||
get_property(_tx0 GLOBAL PROPERTY PUREBOOT_USART0_TX)
|
get_property(_tx0 GLOBAL PROPERTY PUREBOOT_USART0_TX)
|
||||||
get_property(_tx1 GLOBAL PROPERTY PUREBOOT_USART1_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)
|
if(_usart AND PB_TX STREQUAL _tx0)
|
||||||
set(_link "${_link}@0")
|
set(_link "${_link}@0")
|
||||||
elseif(_usart1 AND PB_TX STREQUAL _tx1)
|
elseif(_usart1 AND PB_TX STREQUAL _tx1)
|
||||||
set(_link "${_link}@1")
|
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()
|
endif()
|
||||||
@@ -376,9 +408,18 @@ function(pureboot_add_loader name)
|
|||||||
# load-immediate it saves. The set is fitted to the loader's body and has to
|
# load-immediate it saves. The set is fitted to the loader's body and has to
|
||||||
# be re-measured when that body changes: -fno-move-loop-invariants belonged
|
# be re-measured when that body changes: -fno-move-loop-invariants belonged
|
||||||
# here while the command loop carried four transfer bodies and costs bytes
|
# here while the command loop carried four transfer bodies and costs bytes
|
||||||
# now that it carries one.
|
# now that it carries one, and -fno-ivopts is fitted per backend — an
|
||||||
target_compile_options(${name} PRIVATE
|
# autobaud body needs ivopts to keep the calibration countdown a single
|
||||||
-fno-ivopts -fira-algorithm=priority -fno-tree-ter -fno-split-wide-types)
|
# 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}>)
|
||||||
|
|||||||
@@ -19,42 +19,44 @@ come out byte-identical linked at a different base.
|
|||||||
|
|
||||||
## Chips
|
## Chips
|
||||||
|
|
||||||
Sizes are the default configuration: the hardware USART0 at 115200 8N1 on a
|
The Stock column is the default configuration: the hardware USART0 at 115200
|
||||||
16 MHz crystal, or the software UART on RX = PB0 / TX = PB1 at 57600 8N1 on
|
8N1 on a 16 MHz crystal, or the software UART on RX = PB0 / TX = PB1 at
|
||||||
the tinies' RC oscillator (9.6 MHz on the t13s, 8 MHz above). Every axis moves
|
57600 8N1 on the tinies' RC oscillator (9.6 MHz on the t13s, 8 MHz above).
|
||||||
per build — see *Configuration*. The autobaud column is the clock-free build,
|
Every axis moves per build — see *Configuration*. The Autobaud column is the
|
||||||
which is the largest the space produces and the tightest fit in the matrix;
|
worst configuration the space produces for the chip: the clock-free build —
|
||||||
it carries the calibration machinery and no clock at all.
|
it alone carries the calibration machinery — with the `OSCCAL` trim baked
|
||||||
|
and, where the chip has a USART, the link deployed on that USART's own pins,
|
||||||
|
which the loader then has to release (*Pin ownership*). Folding the same
|
||||||
|
build onto a single pin (*One-wire*) measures identically on every chip, so
|
||||||
|
the column covers that twin too. On default pins without the trim the same
|
||||||
|
loaders run 4–10 B smaller.
|
||||||
|
|
||||||
| Chip | Flash | Loader at | Link | Stock | Autobaud |
|
| Chip | Flash | Loader at | Link | Stock | Autobaud |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| ATtiny13, ATtiny13A † | 1 KiB | 0x0200 | software | 384 B | 452 B |
|
| ATtiny13, ATtiny13A † | 1 KiB | 0x0200 | software | 384 B | 474 B |
|
||||||
| ATtiny25 † | 2 KiB | 0x0600 | software | 388 B | 442 B |
|
| ATtiny25 † | 2 KiB | 0x0600 | software | 388 B | 466 B |
|
||||||
| ATtiny45 † | 4 KiB | 0x0e00 | software | 388 B | 442 B |
|
| ATtiny45 † | 4 KiB | 0x0e00 | software | 388 B | 466 B |
|
||||||
| ATtiny85 † | 8 KiB | 0x1e00 | software | 388 B | 442 B |
|
| ATtiny85 † | 8 KiB | 0x1e00 | software | 388 B | 466 B |
|
||||||
| ATmega8, 8A | 8 KiB | 0x1e00 | USART0 | 358 B | 470 B |
|
| ATmega8, 8A | 8 KiB | 0x1e00 | USART0 | 362 B | 494 B |
|
||||||
| ATmega16, 16A | 16 KiB | 0x3e00 | USART0 | 360 B | 474 B |
|
| ATmega16, 16A | 16 KiB | 0x3e00 | USART0 | 364 B | 496 B |
|
||||||
| ATmega32, 32A | 32 KiB | 0x7e00 | USART0 | 360 B | 474 B |
|
| ATmega32, 32A | 32 KiB | 0x7e00 | USART0 | 364 B | 496 B |
|
||||||
| ATmega48, 48A, 48P, 48PA † | 4 KiB | 0x0e00 | USART0 | 378 B | 438 B |
|
| ATmega48, 48A, 48P, 48PA † | 4 KiB | 0x0e00 | USART0 | 378 B | 468 B |
|
||||||
| ATmega88, 88A, 88P, 88PA | 8 KiB | 0x1e00 | USART0 | 388 B | 448 B |
|
| ATmega88, 88A, 88P, 88PA | 8 KiB | 0x1e00 | USART0 | 388 B | 478 B |
|
||||||
| ATmega168, 168A, 168P, 168PA | 16 KiB | 0x3e00 | USART0 | 390 B | 454 B |
|
| ATmega168, 168A, 168P, 168PA | 16 KiB | 0x3e00 | USART0 | 390 B | 480 B |
|
||||||
| ATmega328, 328P | 32 KiB | 0x7e00 | USART0 | 390 B | 454 B |
|
| ATmega328, 328P | 32 KiB | 0x7e00 | USART0 | 390 B | 480 B |
|
||||||
| ATmega164A, 164P, 164PA | 16 KiB | 0x3e00 | USART0 | 390 B | 454 B |
|
| ATmega164A, 164P, 164PA | 16 KiB | 0x3e00 | USART0 | 390 B | 480 B |
|
||||||
| ATmega324A, 324P, 324PA | 32 KiB | 0x7e00 | USART0 | 390 B | 454 B |
|
| ATmega324A, 324P, 324PA | 32 KiB | 0x7e00 | USART0 | 390 B | 480 B |
|
||||||
| ATmega644, 644A, 644P, 644PA | 64 KiB | 0xfe00 | USART0 | 384 B | 448 B |
|
| ATmega644, 644A, 644P, 644PA | 64 KiB | 0xfe00 | USART0 | 384 B | 474 B |
|
||||||
| ATmega1284, 1284P | 128 KiB | 0x1fe00 | USART0 | 410 B | 474 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 tightest fit in the whole space is the 1284s' autobaud build deployed on a
|
The tightest fit in the whole space is therefore the 1284s' 502 of their
|
||||||
USART's own pins with the `OSCCAL` trim baked, 484 of its 512 — they alone
|
512: they alone carry the far-flash machinery (ELPM reads, RAMPZ page
|
||||||
carry the far-flash machinery (ELPM reads, RAMPZ page commands), autobaud
|
commands) on top of everything the column already stacks. The flash bank
|
||||||
alone carries the calibration loop, a bit-banged link on a USART's pins alone
|
riding in a transfer's selector byte keeps even those chips' addressing the
|
||||||
has to release it (below), and the trim adds its one register write. Without
|
same 16-bit form every other chip uses, which is why they are no longer the
|
||||||
the trim that build is 478; on the default pins, 474. 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.
|
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
|
||||||
@@ -75,6 +77,7 @@ repo's build and by a downstream project alike:
|
|||||||
| `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 |
|
| `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
|
||||||
@@ -100,7 +103,10 @@ where a fixed-baud software build has to be rebuilt per clock and still drifts
|
|||||||
out of tolerance. The cost is that it is software-serial only (a hardware USART
|
out of tolerance. The cost is that it is software-serial only (a hardware USART
|
||||||
needs its divisor programmed) and that activation counts poll iterations rather
|
needs its divisor programmed) and that activation counts poll iterations rather
|
||||||
than seconds, since there is no clock to convert them against
|
than seconds, since there is no clock to convert them against
|
||||||
(`PUREBOOT_AUTOBAUD_POLLS`, default 4,000,000).
|
(`PUREBOOT_AUTOBAUD_POLLS`, default 4,000,000). The wait spends nine cycles a
|
||||||
|
poll (measured, and held by the `pureboot.window.autobaud` gate), so the
|
||||||
|
default window is 36 M cycles: 4.5 s at 8 MHz, 3.75 s at 9.6 MHz, 36 s at
|
||||||
|
1 MHz.
|
||||||
|
|
||||||
**Pick the rate by cycles a bit, and leave the oscillator room.** What the
|
**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
|
calibration can measure is bounded by how many clock cycles one bit lasts, so a
|
||||||
@@ -123,6 +129,43 @@ 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
|
more reliable than the same part with an application resident, which gets one
|
||||||
window per reset. Measure with an application in place.
|
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
|
||||||
@@ -299,7 +342,12 @@ 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
|
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
|
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
|
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.
|
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
|
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
|
commit just before the next version bump, so a tag holds everything its
|
||||||
@@ -466,6 +514,11 @@ 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
|
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.
|
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
|
`--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
|
±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
|
window — reset the target as each probe announces itself (a board with DTR
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -75,7 +77,7 @@ static_assert(PUREBOOT_OSCCAL >= 0 && PUREBOOT_OSCCAL <= 0xff, "PUREBOOT_OSCCAL
|
|||||||
|
|
||||||
// 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 = 7;
|
constexpr std::uint8_t version = 8;
|
||||||
|
|
||||||
// The image's identity stamp, for the host tool rather than for the wire: an
|
// The image's identity stamp, for the host tool rather than for the wire: an
|
||||||
// update image is a bare 512-byte slot, and without this nothing in it says
|
// update image is a bare 512-byte slot, and without this nothing in it says
|
||||||
@@ -155,6 +157,9 @@ constexpr std::uint8_t bank_shift = 16 - slot_shift;
|
|||||||
#if defined(PUREBOOT_AUTOBAUD) && defined(PUREBOOT_USART)
|
#if defined(PUREBOOT_AUTOBAUD) && defined(PUREBOOT_USART)
|
||||||
#error "PUREBOOT_AUTOBAUD measures a software link; it cannot drive a hardware USART"
|
#error "PUREBOOT_AUTOBAUD measures a software link; it cannot drive a hardware USART"
|
||||||
#endif
|
#endif
|
||||||
|
#if defined(PUREBOOT_HALF_DUPLEX) && (defined(PUREBOOT_SOFT_SERIAL) || defined(PUREBOOT_AUTOBAUD))
|
||||||
|
#error "PUREBOOT_HALF_DUPLEX is the hardware USART's one-wire mode; a software link goes one-wire by RX == TX"
|
||||||
|
#endif
|
||||||
#if !defined(PUREBOOT_RX)
|
#if !defined(PUREBOOT_RX)
|
||||||
#define PUREBOOT_RX pb0
|
#define PUREBOOT_RX pb0
|
||||||
#endif
|
#endif
|
||||||
@@ -162,18 +167,50 @@ constexpr std::uint8_t bank_shift = 16 - slot_shift;
|
|||||||
#define PUREBOOT_TX pb1
|
#define PUREBOOT_TX pb1
|
||||||
#endif
|
#endif
|
||||||
#if defined(PUREBOOT_USART)
|
#if defined(PUREBOOT_USART)
|
||||||
constexpr char usart_digit = '0' + PUREBOOT_USART;
|
constexpr int usart_unit = PUREBOOT_USART;
|
||||||
#else
|
#else
|
||||||
constexpr char usart_digit = '0';
|
constexpr int usart_unit = 0;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// One-wire on the hardware USART (PUREBOOT_HALF_DUPLEX): RXD and TXD tied
|
||||||
|
// together off-chip, exactly one direction enabled at a time — the library's
|
||||||
|
// .half_duplex turn-around. The activation window is unchanged; only its
|
||||||
|
// poll grows the release-line test rx_ready() carries in this mode.
|
||||||
|
constexpr bool hw_half_duplex =
|
||||||
|
#if defined(PUREBOOT_HALF_DUPLEX)
|
||||||
|
true;
|
||||||
|
#else
|
||||||
|
false;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
template <avr::hertz_t C, avr::baud_t B>
|
template <avr::hertz_t C, avr::baud_t B>
|
||||||
struct hardware_link {
|
struct hardware_link {
|
||||||
using uart = avr::uart::usart<usart_digit, C, {.baud = B, .max_baud_error = 2.5_pct}>;
|
using uart = avr::uart::usart<usart_unit, C, {.baud = B, .max_baud_error = 2.5_pct, .half_duplex = hw_half_duplex}>;
|
||||||
|
|
||||||
// The compiled idle poll: lds UCSR0A (2), sbrc skipping the exit (2),
|
// The compiled idle poll around the window's narrow (uint24_t) countdown:
|
||||||
// sbiw + sbci + sbci + brne (6).
|
// the RXC test, then sbiw + sbci + brne (5). The test's cost follows the
|
||||||
static constexpr std::uint8_t poll_cycles = 10;
|
// status register's home — a 2-cycle bit-skip where UCSRnA sits in
|
||||||
|
// bit-addressable I/O (the classic megas), lds + skip (4) in extended
|
||||||
|
// I/O. Half-duplex polls through rx_ready()'s release-line test, which
|
||||||
|
// -Os outlines: the rcall (3), the UCSR#B read and not-taken skip with
|
||||||
|
// the jump over the write (I/O 3, extended 5), the ret (4) — and the
|
||||||
|
// call in the loop body pushes the countdown into call-saved registers,
|
||||||
|
// where the uint24_t step is ldi+sub+sbc+sbc (4) instead of sbiw+sbci
|
||||||
|
// (3). Measured off the built loops: 18 a poll in bit-addressable I/O,
|
||||||
|
// 22 in extended. A uint32_t countdown pays one more sbci —
|
||||||
|
// window_polls() adds it where the count forces the wide type. Held per
|
||||||
|
// chip by the pureboot.window gates. The lookup rides the baud parameter
|
||||||
|
// so it stays dependent: the trait is an incomplete type on the
|
||||||
|
// USART-less chips, which parse this template without ever instantiating
|
||||||
|
// it.
|
||||||
|
template <avr::baud_t Baud, typename U = avr::hw::usart_of<usart_unit>>
|
||||||
|
static consteval std::uint8_t poll_cost()
|
||||||
|
{
|
||||||
|
if (hw_half_duplex)
|
||||||
|
return U::ucsra::addr < 0x40 ? 18 : 22;
|
||||||
|
return U::ucsra::addr < 0x40 ? 7 : 9;
|
||||||
|
}
|
||||||
|
static constexpr std::uint8_t poll_cycles = poll_cost<B>();
|
||||||
|
|
||||||
static void init()
|
static void init()
|
||||||
{
|
{
|
||||||
@@ -197,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, avr::baud_t B>
|
template <avr::hertz_t C, avr::baud_t B>
|
||||||
struct software_link {
|
struct software_link {
|
||||||
|
// RX == TX is the one-wire deployment: the transmitter becomes a guest
|
||||||
|
// on the receiver's pull-up line, taking the pin's direction for exactly
|
||||||
|
// one frame per byte.
|
||||||
using rx_t = avr::uart::software_rx_polled<C, avr::PUREBOOT_RX, B>;
|
using rx_t = avr::uart::software_rx_polled<C, avr::PUREBOOT_RX, B>;
|
||||||
using tx_t = avr::uart::software_tx<C, avr::PUREBOOT_TX, B>;
|
using tx_t = avr::uart::software_tx<C, avr::PUREBOOT_TX, B, avr::PUREBOOT_RX == avr::PUREBOOT_TX>;
|
||||||
|
|
||||||
// The compiled idle poll: sbis skipping the exit (2), sbiw + sbci +
|
// The compiled idle poll around the window's narrow (uint24_t) countdown:
|
||||||
// sbci + brne (6).
|
// sbis skipping the exit (2), sbiw + sbci + brne (5). A uint32_t
|
||||||
static constexpr std::uint8_t poll_cycles = 8;
|
// countdown pays one more sbci — window_polls() adds it where the count
|
||||||
|
// forces the wide type. Held by the pureboot.window gate.
|
||||||
|
static constexpr std::uint8_t poll_cycles = 7;
|
||||||
|
|
||||||
static void init()
|
static void init()
|
||||||
{
|
{
|
||||||
@@ -263,19 +308,22 @@ struct autobaud_link {
|
|||||||
|
|
||||||
static void drain()
|
static void drain()
|
||||||
{
|
{
|
||||||
uart::drain();
|
// A drain here always follows this link's own write — the frame is
|
||||||
|
// in flight by construction, so the completion the wait needs is
|
||||||
|
// guaranteed and the bounded default's countdown would be dead bytes.
|
||||||
|
uart::drain_unbounded();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
#if defined(PUREBOOT_AUTOBAUD)
|
#if defined(PUREBOOT_AUTOBAUD)
|
||||||
using link = autobaud_link;
|
using link = autobaud_link;
|
||||||
#elif defined(PUREBOOT_USART)
|
#elif defined(PUREBOOT_USART)
|
||||||
static_assert(avr::uart::has_usart<usart_digit>(), "PUREBOOT_USART selects a hardware USART this chip does not have");
|
static_assert(avr::uart::has_usart<usart_unit>(), "PUREBOOT_USART selects a hardware USART this chip does not have");
|
||||||
using link = hardware_link<dev::clock, wire_baud>;
|
using link = hardware_link<dev::clock, wire_baud>;
|
||||||
#elif defined(PUREBOOT_SOFT_SERIAL)
|
#elif defined(PUREBOOT_SOFT_SERIAL)
|
||||||
using link = software_link<dev::clock, wire_baud>;
|
using link = software_link<dev::clock, wire_baud>;
|
||||||
#else
|
#else
|
||||||
using link = std::conditional_t<avr::uart::has_usart<usart_digit>(), hardware_link<dev::clock, wire_baud>,
|
using link = std::conditional_t<avr::uart::has_usart<usart_unit>(), hardware_link<dev::clock, wire_baud>,
|
||||||
software_link<dev::clock, wire_baud>>;
|
software_link<dev::clock, wire_baud>>;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -317,17 +365,35 @@ void await_host()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
// The window as one 32-bit countdown, divided by the backend's counted
|
// The window as one countdown, divided by the backend's counted poll-loop
|
||||||
// poll-loop cycles. Whole seconds is all it promises.
|
// cycles. Whole seconds is all it promises. The per-poll cost depends on the
|
||||||
|
// countdown's own width (a uint32_t decrement chain is one sbci longer), and
|
||||||
|
// the width depends on the poll count — solved narrow-first: a count that
|
||||||
|
// fits 24 bits at the narrow cost keeps the narrow loop, anything else takes
|
||||||
|
// the wide loop at its own cost. A count fitting 24 bits only at the wide
|
||||||
|
// cost stays wide, so the choice cannot oscillate on the boundary.
|
||||||
|
consteval std::uint32_t polls_at(std::uint32_t per_poll)
|
||||||
|
{
|
||||||
|
// Whole-window cycles first, then the per-poll division: one truncation
|
||||||
|
// instead of one per second. Same instructions either way — only the
|
||||||
|
// countdown's immediate moves.
|
||||||
|
return static_cast<std::uint32_t>(dev::cycles_for<std::chrono::seconds{timeout_seconds}>() / per_poll);
|
||||||
|
}
|
||||||
|
|
||||||
|
consteval bool narrow_window()
|
||||||
|
{
|
||||||
|
return polls_at(link::poll_cycles) <= 0xffffff;
|
||||||
|
}
|
||||||
|
|
||||||
consteval std::uint32_t window_polls()
|
consteval std::uint32_t window_polls()
|
||||||
{
|
{
|
||||||
return timeout_seconds * static_cast<std::uint32_t>(dev::clock.hz / link::poll_cycles);
|
return polls_at(narrow_window() ? link::poll_cycles : link::poll_cycles + 1u);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The countdown in the narrowest type that holds it: a fourth byte would
|
// 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
|
// cost a wider decrement chain at every poll for range most windows never
|
||||||
// use (the autobaud budget makes the same choice).
|
// use (the autobaud budget makes the same choice).
|
||||||
using window_t = std::conditional_t<window_polls() <= 0xffffff, avr::uint24_t, std::uint32_t>;
|
using window_t = std::conditional_t<narrow_window(), avr::uint24_t, std::uint32_t>;
|
||||||
|
|
||||||
bool pending_before_deadline()
|
bool pending_before_deadline()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -26,15 +26,16 @@ else:
|
|||||||
import termios
|
import termios
|
||||||
|
|
||||||
PROMPT = b"+"
|
PROMPT = b"+"
|
||||||
VERSION = 7 # 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 can drive. 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: the tool keeps a decoder for every generation in it (1–4 speak
|
# map lives: the tool keeps a decoder for every generation in it (1–4 speak
|
||||||
# the per-memory commands, 5 the unified pair; 6 marks the OSCCAL-carrying
|
# the per-memory commands, 5 the unified pair; 6 marks the OSCCAL-carrying
|
||||||
# builds and changes nothing on the wire), and a version it has no decoder
|
# builds and changes nothing on the wire; 8 the one-wire deployments, whose
|
||||||
# for moves the floor.
|
# 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 = 7
|
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
|
||||||
|
|
||||||
@@ -45,7 +46,10 @@ RETRIES = 3 # rewrites of a page that reads back wrong, before the run stops
|
|||||||
# 6 marks the builds that may carry a baked OSCCAL trim, nothing on the wire;
|
# 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()
|
# 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
|
# sends each form to the version that speaks it) and re-homes the autobaud
|
||||||
# unit into the GPIOR pair where the chip has one.
|
# unit into the GPIOR pair where the chip has one; 8 marks the builds whose
|
||||||
|
# deployment may be one-wire (hardware half-duplex, or a software link folded
|
||||||
|
# onto one pin) — nothing on the wire either, but a shared line makes the
|
||||||
|
# host read its own bytes back, which is what --one-wire consumes.
|
||||||
UNIFIED_LOADER = 5
|
UNIFIED_LOADER = 5
|
||||||
SP_FLASH, SP_EEPROM, SP_RAM, SP_FUSE, SP_SPM = 0, 1, 2, 3, 4
|
SP_FLASH, SP_EEPROM, SP_RAM, SP_FUSE, SP_SPM = 0, 1, 2, 3, 4
|
||||||
|
|
||||||
@@ -433,6 +437,99 @@ if os.name == "nt":
|
|||||||
Port = WindowsPort if os.name == "nt" else PosixPort
|
Port = WindowsPort if os.name == "nt" else PosixPort
|
||||||
|
|
||||||
|
|
||||||
|
class OneWirePort:
|
||||||
|
"""The host side of a shared line (--one-wire): an FTDI-style adapter on
|
||||||
|
a one-wire link reads back every byte it transmits — its RX is tied to
|
||||||
|
its own TX through the line. Consume that echo at each write and verify
|
||||||
|
it, which doubles as a wiring check: an echo that never comes is an RX
|
||||||
|
not on the line, and is reported as itself instead of decoding as a
|
||||||
|
device reply.
|
||||||
|
|
||||||
|
The device's reply may interleave with the echo of a multi-byte write —
|
||||||
|
a loader already in session re-prompts after the knock's first byte
|
||||||
|
while the second is still queued behind that reply — so the echo is
|
||||||
|
matched byte for byte and anything else arriving in between is device
|
||||||
|
traffic, held for the next read."""
|
||||||
|
|
||||||
|
def __init__(self, port):
|
||||||
|
self._port = port
|
||||||
|
self._pending = b""
|
||||||
|
self.lost_echoes = 0
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
return getattr(self._port, name)
|
||||||
|
|
||||||
|
def write(self, data, blind=False):
|
||||||
|
"""Put `data` on the line and consume its echo.
|
||||||
|
|
||||||
|
`blind` marks the protocol's one multi-byte write with no ack between
|
||||||
|
its bytes — the knock. Aimed at a loader already in session, its first
|
||||||
|
byte draws a prompt while the second is still going out, and on real
|
||||||
|
wiring the device's push-pull ack **wins the line** against the host's
|
||||||
|
1 k series resistor: that second byte is *destroyed, not delayed*, and
|
||||||
|
its echo never comes. Measured on an ATtiny13A at 57600 — the loader
|
||||||
|
answers a single byte perfectly and loses the knock's second every
|
||||||
|
time. So on a blind write a missing echo is a property of the wiring
|
||||||
|
rather than a fault in it, and the caller's retry is what deals with
|
||||||
|
it. Every other write is ack-paced and cannot collide, so a missing
|
||||||
|
echo there really is an RX that is not on the line.
|
||||||
|
"""
|
||||||
|
data = bytes(data)
|
||||||
|
self._port.write(data)
|
||||||
|
# The echo arrives at line rate — 10 bits a byte — plus adapter
|
||||||
|
# latency; a generous floor keeps slow rates and USB scheduling out
|
||||||
|
# of the error path.
|
||||||
|
deadline = time.monotonic() + 10 * len(data) / self._port.baud + 0.5
|
||||||
|
remaining = data
|
||||||
|
while remaining and time.monotonic() < deadline:
|
||||||
|
# Speculative, so it cannot be read_exact, whose contract is to
|
||||||
|
# raise: doing that made the diagnosis below unreachable on every
|
||||||
|
# quiet line and surfaced a bare "timeout: got 0 of 1 bytes" in
|
||||||
|
# its place — the one message this class exists to replace.
|
||||||
|
for byte in self._port.read_available(0.02):
|
||||||
|
if remaining and byte == remaining[0]:
|
||||||
|
remaining = remaining[1:]
|
||||||
|
else:
|
||||||
|
self._pending += bytes((byte,))
|
||||||
|
if not remaining:
|
||||||
|
return
|
||||||
|
if not blind:
|
||||||
|
raise Error(f"one-wire echo missing after {len(data) - len(remaining)} of "
|
||||||
|
f"{len(data)} byte(s) — is the adapter's RX tied to the line?")
|
||||||
|
self.lost_echoes += len(remaining)
|
||||||
|
# Which loss this is matters, and the count says it. *Some* bytes lost is
|
||||||
|
# the device's ack winning the line against the host's series resistor —
|
||||||
|
# ordinary, and what the retry absorbs. *Every* byte lost is nothing
|
||||||
|
# coming back at all, which is a line that is not free: an application
|
||||||
|
# holding the shared pin low (this rig's LED demo ends that way), a
|
||||||
|
# wedge, or an RX that is not on the line. Same retry either way, but
|
||||||
|
# blaming an ack that never happened sends the reader to the wrong place.
|
||||||
|
if len(remaining) == len(data):
|
||||||
|
verbose(f"one-wire: none of {len(data)} byte(s) echoed — the line is not "
|
||||||
|
f"coming back. Held low by something? (a pin driven low, a wedge, "
|
||||||
|
f"or an RX not on the line)")
|
||||||
|
else:
|
||||||
|
verbose(f"one-wire: {len(remaining)} of {len(data)} knock byte(s) lost to the "
|
||||||
|
f"device's ack; retrying")
|
||||||
|
|
||||||
|
def write_blind(self, data):
|
||||||
|
self.write(data, blind=True)
|
||||||
|
|
||||||
|
def read_exact(self, count, timeout):
|
||||||
|
taken, self._pending = self._pending[:count], self._pending[count:]
|
||||||
|
if len(taken) == count:
|
||||||
|
return taken
|
||||||
|
return taken + self._port.read_exact(count - len(taken), timeout)
|
||||||
|
|
||||||
|
def read_available(self, wait):
|
||||||
|
taken, self._pending = self._pending, b""
|
||||||
|
return taken + self._port.read_available(0 if taken else wait)
|
||||||
|
|
||||||
|
def flush_input(self):
|
||||||
|
self._pending = b""
|
||||||
|
self._port.flush_input()
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------------------- protocol ---
|
# -------------------------------------------------------------- protocol ---
|
||||||
|
|
||||||
|
|
||||||
@@ -546,6 +643,11 @@ class Loader:
|
|||||||
# Set once a session is established over an autobaud link, so a
|
# Set once a session is established over an autobaud link, so a
|
||||||
# re-entry after 'J' repeats the handshake that worked.
|
# re-entry after 'J' repeats the handshake that worked.
|
||||||
self.autobaud = False
|
self.autobaud = False
|
||||||
|
# The pre-knock drain runs once per port: the bytes it exists for are
|
||||||
|
# leftovers from before this process opened the port. Re-knocks later
|
||||||
|
# in the same session must not pay it — a fresh activation window is
|
||||||
|
# already burning while they wait.
|
||||||
|
self._line_drained = False
|
||||||
# The link this session is speaking. It moves when the host follows a
|
# The link this session is speaking. It moves when the host follows a
|
||||||
# staging copy built for another one (enter_copy).
|
# staging copy built for another one (enter_copy).
|
||||||
self.baud = getattr(port, "baud", None)
|
self.baud = getattr(port, "baud", None)
|
||||||
@@ -555,10 +657,16 @@ class Loader:
|
|||||||
"""The 'b' reply, in either of the two layouts a loader may send.
|
"""The 'b' reply, in either of the two layouts a loader may send.
|
||||||
pureboot 5 answers with its version and the signature; older loaders
|
pureboot 5 answers with its version and the signature; older loaders
|
||||||
answer with a 12-byte block. The version byte cannot be mistaken for
|
answer with a 12-byte block. The version byte cannot be mistaken for
|
||||||
the older block's 'P', so four bytes are enough to tell them apart."""
|
the older block's 'P', so four bytes are enough to tell them apart.
|
||||||
head = self.port.read_exact(4, 2.0)
|
|
||||||
|
The timeout is short on purpose: a real answer follows the prompt
|
||||||
|
within a frame time or two, so half a second is dozens of times the
|
||||||
|
worst case — while a *false* prompt match (a stale byte, reset
|
||||||
|
garbage) makes this read collect noise, and every second spent on it
|
||||||
|
comes out of the activation window the retry needs."""
|
||||||
|
head = self.port.read_exact(4, 0.5)
|
||||||
if head[0:2] == b"PB":
|
if head[0:2] == b"PB":
|
||||||
return Info(head + self.port.read_exact(8, 2.0))
|
return Info(head + self.port.read_exact(8, 0.5))
|
||||||
return Info.from_identity(head)
|
return Info.from_identity(head)
|
||||||
|
|
||||||
def _handshake(self, wait, knock, what):
|
def _handshake(self, wait, knock, what):
|
||||||
@@ -569,13 +677,35 @@ class Loader:
|
|||||||
into a fresh window, where a command without its knock is discarded.
|
into a fresh window, where a command without its knock is discarded.
|
||||||
Each attempt is therefore the whole handshake. This also converges into
|
Each attempt is therefore the whole handshake. This also converges into
|
||||||
an already-live session: the knock bytes are ignored there and the
|
an already-live session: the knock bytes are ignored there and the
|
||||||
drain absorbs whatever they produced."""
|
drain absorbs whatever they produced.
|
||||||
|
|
||||||
|
Before the port's first knock ever, the line is drained until quiet: a
|
||||||
|
prompt from a previous session (`--stay`) can still be in the USB
|
||||||
|
pipeline when the port opens, where a flush cannot clear what has not
|
||||||
|
arrived yet — and on a board that resets when its port opens, trusting
|
||||||
|
that stale byte would spend the fresh activation window reading noise
|
||||||
|
from a device that never heard the knock. Once only, and bounded:
|
||||||
|
later re-knocks in this session face no foreign leftovers, and their
|
||||||
|
own window is already burning."""
|
||||||
deadline = time.monotonic() + wait
|
deadline = time.monotonic() + wait
|
||||||
|
if not self._line_drained:
|
||||||
|
self._line_drained = True
|
||||||
|
drain = time.monotonic() + 0.25
|
||||||
|
while self.port.read_available(0.05):
|
||||||
|
if time.monotonic() > drain:
|
||||||
|
break
|
||||||
knocks = 0
|
knocks = 0
|
||||||
refusal = None
|
refusal = None
|
||||||
|
# The knock is the only write in the protocol with no ack between its
|
||||||
|
# bytes, so on a shared line it is the only one whose echo may
|
||||||
|
# legitimately not come back — the device's ack collides with it and
|
||||||
|
# wins (OneWirePort.write). Losing a byte here is what the retry below
|
||||||
|
# is for; raising instead aborted the loop before it ever ran, which on
|
||||||
|
# real wiring made every reconnect into a live session fail.
|
||||||
|
knock_out = getattr(self.port, "write_blind", self.port.write)
|
||||||
while True:
|
while True:
|
||||||
self.port.flush_input()
|
self.port.flush_input()
|
||||||
self.port.write(knock)
|
knock_out(knock)
|
||||||
knocks += 1
|
knocks += 1
|
||||||
if PROMPT in self.port.read_available(0.4):
|
if PROMPT in self.port.read_available(0.4):
|
||||||
# Settle: absorb a real loader's trailing bytes before asking
|
# Settle: absorb a real loader's trailing bytes before asking
|
||||||
@@ -1452,12 +1582,13 @@ def scan_report(baud, pct, version, clock=None):
|
|||||||
return lines
|
return lines
|
||||||
|
|
||||||
|
|
||||||
def op_scan(port_path, baud, wait, clock=None):
|
def op_scan(port_path, baud, wait, clock=None, one_wire=False):
|
||||||
"""A fixed-baud loader whose oscillator drifted still answers — at the
|
"""A fixed-baud loader whose oscillator drifted still answers — at the
|
||||||
drifted ratio, since its rate scales with its clock. One probe per
|
drifted ratio, since its rate scales with its clock. One probe per
|
||||||
activation window, and with an application resident the window opens
|
activation window, and with an application resident the window opens
|
||||||
exactly once per reset, so each probe announces itself and expects a
|
exactly once per reset, so each probe announces itself and expects a
|
||||||
fresh reset before knocking."""
|
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():
|
for pct in scan_ratios():
|
||||||
rate = scan_rate(baud, pct)
|
rate = scan_rate(baud, pct)
|
||||||
print(f"scan: {rate} Bd ({pct:+d} %) — reset the target", flush=True)
|
print(f"scan: {rate} Bd ({pct:+d} %) — reset the target", flush=True)
|
||||||
@@ -1466,6 +1597,8 @@ def op_scan(port_path, baud, wait, clock=None):
|
|||||||
except Error as unmakeable:
|
except Error as unmakeable:
|
||||||
print(f"scan: {rate} Bd skipped — {unmakeable}")
|
print(f"scan: {rate} Bd skipped — {unmakeable}")
|
||||||
continue
|
continue
|
||||||
|
if one_wire:
|
||||||
|
port = OneWirePort(port)
|
||||||
try:
|
try:
|
||||||
info = Loader(port).connect(wait)
|
info = Loader(port).connect(wait)
|
||||||
except Error:
|
except Error:
|
||||||
@@ -1491,6 +1624,9 @@ def main():
|
|||||||
parser.add_argument("--port", required=True, help="serial device: COM6, /dev/ttyUSB0, or a simavr pty")
|
parser.add_argument("--port", required=True, help="serial device: COM6, /dev/ttyUSB0, or a simavr pty")
|
||||||
parser.add_argument("--baud", type=int, default=115200, help="115200 mega, 57600 tinies")
|
parser.add_argument("--baud", type=int, default=115200, help="115200 mega, 57600 tinies")
|
||||||
parser.add_argument("--wait", type=float, default=30.0, help="seconds to keep knocking")
|
parser.add_argument("--wait", type=float, default=30.0, help="seconds to keep knocking")
|
||||||
|
parser.add_argument("--one-wire", action="store_true",
|
||||||
|
help="the link is a shared line: read back and discard this tool's own "
|
||||||
|
"echoed bytes (any backend of a one-wire deployment)")
|
||||||
parser.add_argument("--autobaud", action="store_true",
|
parser.add_argument("--autobaud", action="store_true",
|
||||||
help="drive an autobaud loader: send the 0xC0 calibration pulse and a single "
|
help="drive an autobaud loader: send the 0xC0 calibration pulse and a single "
|
||||||
"knock, and take geometry from the signature (no clock/baud baked in)")
|
"knock, and take geometry from the signature (no clock/baud baked in)")
|
||||||
@@ -1549,11 +1685,14 @@ def main():
|
|||||||
if args.scan:
|
if args.scan:
|
||||||
if args.autobaud:
|
if args.autobaud:
|
||||||
parser.error("--scan probes fixed rates; an autobaud loader has none to miss")
|
parser.error("--scan probes fixed rates; an autobaud loader has none to miss")
|
||||||
op_scan(args.port, args.baud, args.wait, args.clock)
|
op_scan(args.port, args.baud, args.wait, args.clock, args.one_wire)
|
||||||
return
|
return
|
||||||
|
|
||||||
port = Port(args.port, args.baud)
|
port = Port(args.port, args.baud)
|
||||||
verbose(f"{args.port}: {args.baud} Bd 8N1, DTR/RTS asserted")
|
if args.one_wire:
|
||||||
|
port = OneWirePort(port)
|
||||||
|
verbose(f"{args.port}: {args.baud} Bd 8N1, DTR/RTS asserted"
|
||||||
|
+ (", one-wire echo discarded" if args.one_wire else ""))
|
||||||
try:
|
try:
|
||||||
loader = Loader(port)
|
loader = Loader(port)
|
||||||
info = loader.connect_autobaud(args.wait) if args.autobaud else loader.connect(args.wait)
|
info = loader.connect_autobaud(args.wait) if args.autobaud else loader.connect(args.wait)
|
||||||
|
|||||||
@@ -41,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
|
||||||
@@ -50,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +66,11 @@ 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));
|
||||||
@@ -110,7 +117,21 @@ 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));
|
||||||
@@ -145,7 +166,7 @@ struct link<C, false> {
|
|||||||
|
|
||||||
int main()
|
int main()
|
||||||
{
|
{
|
||||||
avr::init<typename link<dev::clock>::tx_t>();
|
link<dev::clock>::init();
|
||||||
#if !defined(PUREBOOT_HANDOVER)
|
#if !defined(PUREBOOT_HANDOVER)
|
||||||
link<dev::clock>::tx('A');
|
link<dev::clock>::tx('A');
|
||||||
link<dev::clock>::tx('P');
|
link<dev::clock>::tx('P');
|
||||||
|
|||||||
@@ -6,13 +6,14 @@ the *same* loader binary, which is the property autobaud exists for: one
|
|||||||
clock-agnostic image that locks onto whatever rate the host sends.
|
clock-agnostic image that locks onto whatever rate the host sends.
|
||||||
|
|
||||||
Usage: pbautobaud.py <device_bin> <loader_elf> <mcu> <base_hex> <page>
|
Usage: pbautobaud.py <device_bin> <loader_elf> <mcu> <base_hex> <page>
|
||||||
<app_bin> <app_hz> <app_baud> <tool_py> <workdir>
|
<app_bin> <app_hz> <app_baud> <tool_py> <workdir> [link]
|
||||||
|
|
||||||
The loader is a software-serial build on PB0/PB1 (pureboot_add_autobaud's
|
The loader is a software-serial build, driven over the GPIO⇄pty bridge; the
|
||||||
default), so the runner drives it over the GPIO⇄pty bridge (-l sw:B0,B1). The
|
optional link overrides the default -l sw:B0,B1 — RX == TX in it is the
|
||||||
app fixture is built for (app_hz, app_baud); the hand-over is checked at that
|
one-wire deployment, and every session then runs with the host's echo
|
||||||
point, and a second point at half the clock proves the lock is measured, not
|
discard on. The app fixture is built for (app_hz, app_baud); the hand-over
|
||||||
baked in.
|
is checked at that point, and a second point at half the clock proves the
|
||||||
|
lock is measured, not baked in.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -27,8 +28,12 @@ def fail(message):
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
(device_bin, elf, mcu, base_hex, page, app_bin, app_hz, app_baud, tool, workdir) = sys.argv[1:]
|
args = sys.argv[1:]
|
||||||
|
link = args.pop() if len(args) == 11 else "sw:B0,B1"
|
||||||
|
(device_bin, elf, mcu, base_hex, page, app_bin, app_hz, app_baud, tool, workdir) = args
|
||||||
base, page, app_hz, app_baud = int(base_hex, 0), int(page), int(app_hz), int(app_baud)
|
base, page, app_hz, app_baud = int(base_hex, 0), int(page), int(app_hz), int(app_baud)
|
||||||
|
one_wire = re.fullmatch(r"sw:([A-H][0-7]),\1(@[01])?", link) is not None
|
||||||
|
extra = ("--one-wire",) if one_wire else ()
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
|
sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
import pbsim
|
import pbsim
|
||||||
@@ -55,11 +60,11 @@ def main():
|
|||||||
"""One clock point: reset, calibrate + knock, program, verify against the
|
"""One clock point: reset, calibrate + knock, program, verify against the
|
||||||
simulator's own flash, and (at the app's point) hand over to the fixture."""
|
simulator's own flash, and (at the app's point) hand over to the fixture."""
|
||||||
dump = os.path.join(workdir, f"flash_{label}.bin")
|
dump = os.path.join(workdir, f"flash_{label}.bin")
|
||||||
device = pbsim.Device(device_bin, elf, mcu, str(hz), base_hex, page, baud, dump, link="sw:B0,B1")
|
device = pbsim.Device(device_bin, elf, mcu, str(hz), base_hex, page, baud, dump, link=link)
|
||||||
try:
|
try:
|
||||||
# The host tool, in autobaud mode, sends the 0xC0 calibration pulse
|
# The host tool, in autobaud mode, sends the 0xC0 calibration pulse
|
||||||
# and a single knock at `baud`; the loader locks to it.
|
# and a single knock at `baud`; the loader locks to it.
|
||||||
out = pbsim.run_tool(tool, device.pty, baud, "--autobaud", "--info", "--clock", str(hz),
|
out = pbsim.run_tool(tool, device.pty, baud, *extra, "--autobaud", "--info", "--clock", str(hz),
|
||||||
"--fuses", "--flash", app_bin, "--eeprom", ee_path, "--stay")
|
"--fuses", "--flash", app_bin, "--eeprom", ee_path, "--stay")
|
||||||
for needed in ("version", "signature", "fuses", "verify:", "stays"):
|
for needed in ("version", "signature", "fuses", "verify:", "stays"):
|
||||||
if needed not in out:
|
if needed not in out:
|
||||||
@@ -80,7 +85,7 @@ def main():
|
|||||||
# Read both memories back over the locked link and check them.
|
# Read both memories back over the locked link and check them.
|
||||||
read_flash = os.path.join(workdir, f"rf_{label}.bin")
|
read_flash = os.path.join(workdir, f"rf_{label}.bin")
|
||||||
read_eeprom = os.path.join(workdir, f"re_{label}.bin")
|
read_eeprom = os.path.join(workdir, f"re_{label}.bin")
|
||||||
out = pbsim.run_tool(tool, device.pty, baud, "--autobaud", "--verify-flash", app_bin,
|
out = pbsim.run_tool(tool, device.pty, baud, *extra, "--autobaud", "--verify-flash", app_bin,
|
||||||
"--verify-eeprom", ee_path, "--read-flash", read_flash,
|
"--verify-eeprom", ee_path, "--read-flash", read_flash,
|
||||||
"--read-eeprom", read_eeprom, "--stay")
|
"--read-eeprom", read_eeprom, "--stay")
|
||||||
if out.count("verify:") != 2:
|
if out.count("verify:") != 2:
|
||||||
@@ -99,6 +104,8 @@ def main():
|
|||||||
# pulse is genuinely seen and the test cannot pass vacuously.)
|
# pulse is genuinely seen and the test cannot pass vacuously.)
|
||||||
device.reset()
|
device.reset()
|
||||||
port = pb.Port(device.pty, baud)
|
port = pb.Port(device.pty, baud)
|
||||||
|
if one_wire:
|
||||||
|
port = pb.OneWirePort(port)
|
||||||
try:
|
try:
|
||||||
time.sleep(0.2)
|
time.sleep(0.2)
|
||||||
port.write(bytes((pb.CALIBRATE,)))
|
port.write(bytes((pb.CALIBRATE,)))
|
||||||
@@ -117,6 +124,8 @@ def main():
|
|||||||
|
|
||||||
device.reset()
|
device.reset()
|
||||||
port = pb.Port(device.pty, baud)
|
port = pb.Port(device.pty, baud)
|
||||||
|
if one_wire:
|
||||||
|
port = pb.OneWirePort(port)
|
||||||
try:
|
try:
|
||||||
loader = pb.Loader(port)
|
loader = pb.Loader(port)
|
||||||
live = loader.connect_autobaud(15)
|
live = loader.connect_autobaud(15)
|
||||||
@@ -160,9 +169,11 @@ def main():
|
|||||||
the question is only whether the loader can still measure the pulse."""
|
the question is only whether the loader can still measure the pulse."""
|
||||||
dump = os.path.join(workdir, f"flash_{label}.bin")
|
dump = os.path.join(workdir, f"flash_{label}.bin")
|
||||||
device = pbsim.Device(device_bin, elf, mcu, str(hz), base_hex, page, baud, dump,
|
device = pbsim.Device(device_bin, elf, mcu, str(hz), base_hex, page, baud, dump,
|
||||||
link="sw:B0,B1")
|
link=link)
|
||||||
try:
|
try:
|
||||||
port = pb.Port(device.pty, baud)
|
port = pb.Port(device.pty, baud)
|
||||||
|
if one_wire:
|
||||||
|
port = pb.OneWirePort(port)
|
||||||
try:
|
try:
|
||||||
live = pb.Loader(port).connect_autobaud(15)
|
live = pb.Loader(port).connect_autobaud(15)
|
||||||
if live.version != pb.NEWEST_LOADER:
|
if live.version != pb.NEWEST_LOADER:
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ Usage: pbmute.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
@@ -35,6 +36,9 @@ def main():
|
|||||||
|
|
||||||
if "@" not in link:
|
if "@" not in link:
|
||||||
fail(f"the link {link} names no owning USART — nothing would be under test")
|
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)
|
os.makedirs(workdir, exist_ok=True)
|
||||||
dump = os.path.join(workdir, "dump.bin")
|
dump = os.path.join(workdir, "dump.bin")
|
||||||
@@ -42,6 +46,8 @@ def main():
|
|||||||
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:
|
||||||
port = pb.Port(device.pty, baud)
|
port = pb.Port(device.pty, baud)
|
||||||
|
if one_wire:
|
||||||
|
port = pb.OneWirePort(port)
|
||||||
loader = pb.Loader(port)
|
loader = pb.Loader(port)
|
||||||
loader.connect(25)
|
loader.connect(25)
|
||||||
resident = loader.info.version
|
resident = loader.info.version
|
||||||
|
|||||||
@@ -8,10 +8,13 @@ import subprocess
|
|||||||
|
|
||||||
|
|
||||||
class Device:
|
class Device:
|
||||||
def __init__(self, binary, elf, mcu, hz, base_hex, page, baud, dump, reset_hex=None, resume=None, link=None):
|
def __init__(self, binary, elf, mcu, hz, base_hex, page, baud, dump, reset_hex=None, resume=None, link=None,
|
||||||
|
window=False):
|
||||||
cmd = [binary]
|
cmd = [binary]
|
||||||
if link:
|
if link:
|
||||||
cmd += ["-l", link]
|
cmd += ["-l", link]
|
||||||
|
if window:
|
||||||
|
cmd.append("-w") # report the first-transmit cycle, free-run idle
|
||||||
cmd += [elf, mcu, hz, base_hex, str(page), str(baud), dump]
|
cmd += [elf, mcu, hz, base_hex, str(page), str(baud), dump]
|
||||||
if reset_hex is not None or resume is not None:
|
if reset_hex is not None or resume is not None:
|
||||||
# Chips without a hardware boot section — the tinies and the
|
# Chips without a hardware boot section — the tinies and the
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ loader built off the chip's natural serial default.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
@@ -70,10 +71,15 @@ def main():
|
|||||||
+ bytes([flags])
|
+ bytes([flags])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# A shared-line link (RX == TX in the -l spec) makes the host read every
|
||||||
|
# byte it sends back off the line; all sessions then discard the echo.
|
||||||
|
one_wire = bool(link) and re.fullmatch(r"sw:([A-H][0-7]),\1(@[01])?", link) is not None
|
||||||
|
extra = ("--one-wire",) if one_wire else ()
|
||||||
|
|
||||||
device = pbsim.Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump, link=link)
|
device = pbsim.Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump, link=link)
|
||||||
try:
|
try:
|
||||||
# Session 1: knock from reset, identify, program everything, stay.
|
# Session 1: knock from reset, identify, program everything, stay.
|
||||||
out = pbsim.run_tool(tool, device.pty, baud, "--info", "--fuses", "--flash", app_bin,
|
out = pbsim.run_tool(tool, device.pty, baud, *extra, "--info", "--fuses", "--flash", app_bin,
|
||||||
"--eeprom", ee_path, "--stay")
|
"--eeprom", ee_path, "--stay")
|
||||||
for needed in ("version", "signature", "fuses", "verify:", "stays"):
|
for needed in ("version", "signature", "fuses", "verify:", "stays"):
|
||||||
if needed not in out:
|
if needed not in out:
|
||||||
@@ -83,7 +89,7 @@ def main():
|
|||||||
# the data space; hand over is deferred — the pty must be reopened for
|
# the data space; hand over is deferred — the pty must be reopened for
|
||||||
# the APP banner first.
|
# the APP banner first.
|
||||||
probe = "c0ffee"
|
probe = "c0ffee"
|
||||||
out = pbsim.run_tool(tool, device.pty, baud, "--verify-flash", app_bin, "--verify-eeprom", ee_path,
|
out = pbsim.run_tool(tool, device.pty, baud, *extra, "--verify-flash", app_bin, "--verify-eeprom", ee_path,
|
||||||
"--read-flash", read_flash, "--read-eeprom", read_eeprom,
|
"--read-flash", read_flash, "--read-eeprom", read_eeprom,
|
||||||
"--poke", f"{ram_base:#x}:{probe}", "--peek", f"{ram_base:#x}:3", "--stay")
|
"--poke", f"{ram_base:#x}:{probe}", "--peek", f"{ram_base:#x}:3", "--stay")
|
||||||
if out.count("verify:") != 2:
|
if out.count("verify:") != 2:
|
||||||
@@ -111,6 +117,8 @@ def main():
|
|||||||
# land in the application, which banners on the same link.
|
# land in the application, which banners on the same link.
|
||||||
device.reset()
|
device.reset()
|
||||||
port = pb.Port(device.pty, baud)
|
port = pb.Port(device.pty, baud)
|
||||||
|
if one_wire:
|
||||||
|
port = pb.OneWirePort(port)
|
||||||
try:
|
try:
|
||||||
loader = pb.Loader(port)
|
loader = pb.Loader(port)
|
||||||
live = loader.connect(15)
|
live = loader.connect(15)
|
||||||
|
|||||||
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())
|
||||||
@@ -54,6 +54,7 @@ namespace {
|
|||||||
avr_t *avr;
|
avr_t *avr;
|
||||||
uart_pty_t uart_pty;
|
uart_pty_t uart_pty;
|
||||||
bool link_software;
|
bool link_software;
|
||||||
|
avr_uart_t *hw_uart; // the pty-driven USART, for the datasheet-reset fix below
|
||||||
char uart_digit = '0';
|
char uart_digit = '0';
|
||||||
char sw_rx_port = 'B', sw_tx_port = 'B';
|
char sw_rx_port = 'B', sw_tx_port = 'B';
|
||||||
int sw_rx_bit = 0, sw_tx_bit = 1;
|
int sw_rx_bit = 0, sw_tx_bit = 1;
|
||||||
@@ -62,6 +63,41 @@ const char *dump_path;
|
|||||||
std::uint32_t reset_pc;
|
std::uint32_t reset_pc;
|
||||||
volatile std::sig_atomic_t reset_requested;
|
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)
|
int parse_link(std::string_view spec)
|
||||||
{
|
{
|
||||||
if (spec == "usart0" || spec == "usart1") {
|
if (spec == "usart0" || spec == "usart1") {
|
||||||
@@ -78,6 +114,7 @@ int parse_link(std::string_view spec)
|
|||||||
std::sscanf(spec.data() + 2, ":%c%d,%c%d@%c", &sw_rx_port, &sw_rx_bit, &sw_tx_port, &sw_tx_bit, &owner);
|
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) {
|
if (fields == 4 || fields == 5) {
|
||||||
sw_tx_owner = owner;
|
sw_tx_owner = owner;
|
||||||
|
link_one_wire = sw_rx_port == sw_tx_port && sw_rx_bit == sw_tx_bit;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -195,6 +232,20 @@ std::uint8_t tx_shift;
|
|||||||
|
|
||||||
avr_cycle_count_t tx_sample(avr_t *, avr_cycle_count_t when, void *)
|
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) {
|
if (tx_bit < 8) {
|
||||||
tx_shift = static_cast<std::uint8_t>((tx_shift >> 1) | (tx_level ? 0x80 : 0));
|
tx_shift = static_cast<std::uint8_t>((tx_shift >> 1) | (tx_level ? 0x80 : 0));
|
||||||
if (++tx_bit < 8)
|
if (++tx_bit < 8)
|
||||||
@@ -223,7 +274,14 @@ avr_uart_t *tx_owner;
|
|||||||
|
|
||||||
bool tx_pin_taken()
|
bool tx_pin_taken()
|
||||||
{
|
{
|
||||||
return tx_owner && avr_regbit_get(avr, tx_owner->txen);
|
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
|
// simavr leaves TXEN set in UCSRnB out of reset, where silicon clears the
|
||||||
@@ -251,15 +309,22 @@ void find_tx_owner()
|
|||||||
|
|
||||||
void tx_hook(avr_irq_t *, std::uint32_t value, void *)
|
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
|
if (tx_pin_taken()) { // the USART holds the line; the port write goes nowhere
|
||||||
tx_level = 1;
|
tx_level = 1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int level = value & 1;
|
int level = value & 1;
|
||||||
if (!tx_active && tx_level == 1 && level == 0) { // start edge
|
if (!tx_active && tx_level == 1 && level == 0) { // start edge, confirmed mid-bit
|
||||||
tx_active = 1;
|
tx_active = 1;
|
||||||
tx_bit = 0;
|
tx_bit = -1;
|
||||||
avr_cycle_timer_register(avr, bit_cycles + bit_cycles / 2, tx_sample, nullptr);
|
avr_cycle_timer_register(avr, bit_cycles / 2, tx_sample, nullptr);
|
||||||
}
|
}
|
||||||
tx_level = level;
|
tx_level = level;
|
||||||
}
|
}
|
||||||
@@ -271,15 +336,29 @@ std::uint8_t rx_byte;
|
|||||||
|
|
||||||
void rx_start_next();
|
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 *)
|
avr_cycle_count_t rx_step(avr_t *, avr_cycle_count_t when, void *)
|
||||||
{
|
{
|
||||||
if (rx_bit < 8) {
|
if (rx_bit < 8) {
|
||||||
avr_raise_irq(rx_pin, (rx_byte >> rx_bit) & 1);
|
bridge_drive((rx_byte >> rx_bit) & 1);
|
||||||
rx_bit++;
|
rx_bit++;
|
||||||
return when + bit_cycles;
|
return when + bit_cycles;
|
||||||
}
|
}
|
||||||
if (rx_bit == 8) { // stop bit, plus one idle bit of margin
|
if (rx_bit == 8) { // stop bit, plus one idle bit of margin
|
||||||
avr_raise_irq(rx_pin, 1);
|
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++;
|
rx_bit++;
|
||||||
return when + 2 * bit_cycles;
|
return when + 2 * bit_cycles;
|
||||||
}
|
}
|
||||||
@@ -292,14 +371,33 @@ void rx_start_next()
|
|||||||
{
|
{
|
||||||
if (rx_active || rx_head == rx_tail)
|
if (rx_active || rx_head == rx_tail)
|
||||||
return;
|
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_byte = rx_queue[rx_head];
|
||||||
rx_head = (rx_head + 1) % sizeof(rx_queue);
|
rx_head = (rx_head + 1) % sizeof(rx_queue);
|
||||||
rx_active = 1;
|
rx_active = 1;
|
||||||
rx_bit = 0;
|
rx_bit = 0;
|
||||||
avr_raise_irq(rx_pin, 0); // start bit
|
bridge_drive(0); // start bit
|
||||||
avr_cycle_timer_register(avr, bit_cycles, rx_step, nullptr);
|
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
|
// 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,
|
// 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.
|
// and a decode in progress on a TX line the reset may have already changed.
|
||||||
@@ -315,7 +413,17 @@ void bridge_reset()
|
|||||||
rx_active = 0;
|
rx_active = 0;
|
||||||
tx_active = 0;
|
tx_active = 0;
|
||||||
tx_level = 1;
|
tx_level = 1;
|
||||||
avr_raise_irq(rx_pin, 1); // idle line
|
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()
|
void poll_pty()
|
||||||
@@ -329,8 +437,9 @@ void poll_pty()
|
|||||||
rx_queue[rx_tail] = chunk[i];
|
rx_queue[rx_tail] = chunk[i];
|
||||||
rx_tail = next;
|
rx_tail = next;
|
||||||
}
|
}
|
||||||
if (got > 0)
|
// Unconditional: a byte held back while the firmware owned a shared
|
||||||
rx_start_next();
|
// line restarts from here once the hand-back has happened.
|
||||||
|
rx_start_next();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------ main ---
|
// ------------------------------------------------------------------ main ---
|
||||||
@@ -364,7 +473,11 @@ void poll_pty()
|
|||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
bool link_given = false;
|
bool link_given = false;
|
||||||
for (int opt; (opt = getopt(argc, argv, "l:")) != -1;) {
|
for (int opt; (opt = getopt(argc, argv, "l:w")) != -1;) {
|
||||||
|
if (opt == 'w') {
|
||||||
|
window_report = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (opt != 'l' || parse_link(optarg) != 0) {
|
if (opt != 'l' || parse_link(optarg) != 0) {
|
||||||
std::println(stderr, "device: bad link spec (usart0, usart1, sw, or sw:B0,B1 as RX,TX)");
|
std::println(stderr, "device: bad link spec (usart0, usart1, sw, or sw:B0,B1 as RX,TX)");
|
||||||
return 2;
|
return 2;
|
||||||
@@ -374,10 +487,12 @@ int main(int argc, char *argv[])
|
|||||||
int args = argc - optind;
|
int args = argc - optind;
|
||||||
if (args < 7 || args > 9) {
|
if (args < 7 || args > 9) {
|
||||||
std::print(stderr,
|
std::print(stderr,
|
||||||
"usage: {} [-l link] <pureboot.elf> <mcu> <hz> <base_hex> <page> <baud> <flash_dump>"
|
"usage: {} [-l link] [-w] <pureboot.elf> <mcu> <hz> <base_hex> <page> <baud> <flash_dump>"
|
||||||
" [reset_hex] [resume_flash]\n"
|
" [reset_hex] [resume_flash]\n"
|
||||||
" -l link: usart0 | usart1 | sw[:B0,B1[@0]] (RX,TX, then the USART owning\n"
|
" -l link: usart0 | usart1 | sw[:B0,B1[@0]] (RX,TX, then the USART owning\n"
|
||||||
" them); default: the chip's own\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"
|
" 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"
|
" resume_flash: raw full-flash image loaded instead of the ELF — a prior\n"
|
||||||
" run's dump, for power-fail resume tests\n",
|
" run's dump, for power-fail resume tests\n",
|
||||||
@@ -466,8 +581,24 @@ int main(int argc, char *argv[])
|
|||||||
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS(uart_digit), &flags);
|
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS(uart_digit), &flags);
|
||||||
flags &= ~AVR_UART_FLAG_POLL_SLEEP;
|
flags &= ~AVR_UART_FLAG_POLL_SLEEP;
|
||||||
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS(uart_digit), &flags);
|
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_init(avr, &uart_pty);
|
||||||
uart_pty_connect(&uart_pty, uart_digit);
|
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);
|
std::println("PB_PTY {}", uart_pty.pty.slavename);
|
||||||
} else {
|
} else {
|
||||||
bit_cycles = (avr->frequency + baud / 2) / baud; // matches uart.hpp's own rounding exactly
|
bit_cycles = (avr->frequency + baud / 2) / baud; // matches uart.hpp's own rounding exactly
|
||||||
@@ -477,7 +608,10 @@ int main(int argc, char *argv[])
|
|||||||
avr_irq_register_notify(
|
avr_irq_register_notify(
|
||||||
avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ(sw_tx_port), static_cast<unsigned>(sw_tx_bit)), tx_hook,
|
avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ(sw_tx_port), static_cast<unsigned>(sw_tx_bit)), tx_hook,
|
||||||
nullptr);
|
nullptr);
|
||||||
avr_raise_irq(rx_pin, 1); // idle line
|
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;
|
int slave;
|
||||||
struct termios raw;
|
struct termios raw;
|
||||||
@@ -509,6 +643,8 @@ int main(int argc, char *argv[])
|
|||||||
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS(uart_digit), &flags);
|
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS(uart_digit), &flags);
|
||||||
flags &= ~AVR_UART_FLAG_POLL_SLEEP;
|
flags &= ~AVR_UART_FLAG_POLL_SLEEP;
|
||||||
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS(uart_digit), &flags);
|
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 {
|
} else {
|
||||||
bridge_reset();
|
bridge_reset();
|
||||||
reset_tx_owner();
|
reset_tx_owner();
|
||||||
@@ -524,7 +660,7 @@ int main(int argc, char *argv[])
|
|||||||
// entirely. Pace the simulation only while the bridge is fully
|
// entirely. Pace the simulation only while the bridge is fully
|
||||||
// quiet (nothing decoding, nothing queued); transfers keep full
|
// quiet (nothing decoding, nothing queued); transfers keep full
|
||||||
// speed, and a quiet window stretches toward real time.
|
// speed, and a quiet window stretches toward real time.
|
||||||
if (!rx_active && !tx_active && rx_head == rx_tail)
|
if (!window_report && !rx_active && !tx_active && rx_head == rx_tail)
|
||||||
usleep(200);
|
usleep(200);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Host-tool activation handshake: it must not hang on a flooding target.
|
"""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
|
`_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
|
trailing bytes before it asks for the identity. That drain must be bounded: a
|
||||||
@@ -8,6 +8,13 @@ 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
|
prompt — otherwise spins the tool forever. Regression for that hang, plus a
|
||||||
control that a well-behaved loader still connects.
|
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
|
Stdlib only, no device: host-tool logic, so it runs on every chip's preset
|
||||||
beside pureboot.planner.
|
beside pureboot.planner.
|
||||||
"""
|
"""
|
||||||
@@ -49,27 +56,117 @@ class FloodPort:
|
|||||||
|
|
||||||
|
|
||||||
class LoaderPort:
|
class LoaderPort:
|
||||||
"""A well-behaved pureboot 5: one prompt to the knock, then quiet, then the
|
"""A well-behaved pureboot 5: a prompt to the knock, then quiet, then the
|
||||||
slim identity (version 5 + m328p signature) and a closing prompt."""
|
slim identity (version 5 + m328p signature) and a closing prompt."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.reads = self.exacts = 0
|
self.pending = b""
|
||||||
|
self.exacts = 0
|
||||||
|
|
||||||
def flush_input(self):
|
def flush_input(self):
|
||||||
pass
|
self.pending = b""
|
||||||
|
|
||||||
def write(self, data):
|
def write(self, data):
|
||||||
pass
|
if b"p" in data:
|
||||||
|
self.pending = b"+" # the prompt answers the knock, nothing else
|
||||||
|
|
||||||
def read_available(self, wait):
|
def read_available(self, wait):
|
||||||
self.reads += 1
|
data, self.pending = self.pending, b""
|
||||||
return b"+" if self.reads == 1 else b"" # prompt once, then settle quiet
|
return data
|
||||||
|
|
||||||
def read_exact(self, count, timeout):
|
def read_exact(self, count, timeout):
|
||||||
self.exacts += 1
|
self.exacts += 1
|
||||||
return b"\x05\x1e\x95\x0f" if self.exacts == 1 else b"+" # identity, then prompt
|
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):
|
def terminates(port, wait, budget):
|
||||||
"""Run connect_autobaud in a thread; True if it returns/raises within
|
"""Run connect_autobaud in a thread; True if it returns/raises within
|
||||||
`budget` seconds rather than hanging."""
|
`budget` seconds rather than hanging."""
|
||||||
@@ -97,6 +194,16 @@ def main():
|
|||||||
info = pb.Loader(LoaderPort()).connect_autobaud(2.0)
|
info = pb.Loader(LoaderPort()).connect_autobaud(2.0)
|
||||||
check("well-behaved loader still connects (version 5)", info.version == 5)
|
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")
|
print(f"\n {P} passed, {F} failed")
|
||||||
return 1 if F else 0
|
return 1 if F else 0
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class Suite:
|
|||||||
"""The info block, which every later check takes its bounds from."""
|
"""The info block, which every later check takes its bounds from."""
|
||||||
module = pbrig.load_pureboot(self.rig.d.pureboot)
|
module = pbrig.load_pureboot(self.rig.d.pureboot)
|
||||||
self.rig.reset()
|
self.rig.reset()
|
||||||
port = module.Port(self.rig.d.port, self.rig.d.baud)
|
port = self.rig.open_port() # wrapped for the echo where the line is shared
|
||||||
try:
|
try:
|
||||||
loader = module.Loader(port)
|
loader = module.Loader(port)
|
||||||
if self.rig.d.autobaud:
|
if self.rig.d.autobaud:
|
||||||
@@ -87,7 +87,10 @@ class Suite:
|
|||||||
rate = module.scan_rate(self.rig.d.baud, pct)
|
rate = module.scan_rate(self.rig.d.baud, pct)
|
||||||
self.rig.reset()
|
self.rig.reset()
|
||||||
try:
|
try:
|
||||||
port = module.Port(self.rig.d.port, rate)
|
# 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:
|
except module.Error as error:
|
||||||
self.check("scan opens every probe rate", False, f"{rate} Bd: {error}")
|
self.check("scan opens every probe rate", False, f"{rate} Bd: {error}")
|
||||||
return
|
return
|
||||||
@@ -129,18 +132,28 @@ class Suite:
|
|||||||
got = erased.read_bytes() if erased.exists() else b""
|
got = erased.read_bytes() if erased.exists() else b""
|
||||||
self.check("EEPROM erase leaves 0xff", got == b"\xff" * size, f"{len(got)} B")
|
self.check("EEPROM erase leaves 0xff", got == b"\xff" * size, f"{len(got)} B")
|
||||||
|
|
||||||
def application(self, info, app: pathlib.Path, marker: str) -> None:
|
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))
|
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))
|
self.check(f"application flash + verify ({app.name})", rc == 0, self._brief(out))
|
||||||
|
|
||||||
if marker:
|
if marker:
|
||||||
# The tool hands over as it ends its session, so the application is
|
# The tool hands over as it ends its session, so the application is
|
||||||
# already running; opening the port does not reset a board whose DTR
|
# already running — but only on a board whose DTR is unwired, where
|
||||||
# is unwired, so this simply listens.
|
# opening a port simply listens. Where DTR *is* wired to reset (an
|
||||||
data = self.rig.capture(seconds=2.5)
|
# 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
|
seen = marker.encode() in data
|
||||||
sample = "".join(chr(b) if 32 <= b < 127 else "." for b in data[:40])
|
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}|")
|
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"
|
back = self.work / "app-back.bin"
|
||||||
rc, out = self.rig.pureboot("--read-flash", str(back))
|
rc, out = self.rig.pureboot("--read-flash", str(back))
|
||||||
@@ -187,7 +200,7 @@ class Suite:
|
|||||||
# ------------------------------------------------------------------- run
|
# ------------------------------------------------------------------- run
|
||||||
|
|
||||||
def run(self, app: pathlib.Path | None, loader_image: pathlib.Path | None,
|
def run(self, app: pathlib.Path | None, loader_image: pathlib.Path | None,
|
||||||
marker: str) -> int:
|
marker: str, marker_wait: float = 2.5) -> int:
|
||||||
print("identity")
|
print("identity")
|
||||||
info = self.identity()
|
info = self.identity()
|
||||||
if info is None:
|
if info is None:
|
||||||
@@ -203,7 +216,7 @@ class Suite:
|
|||||||
|
|
||||||
if app:
|
if app:
|
||||||
print("\napplication")
|
print("\napplication")
|
||||||
self.application(info, app, marker)
|
self.application(info, app, marker, marker_wait)
|
||||||
else:
|
else:
|
||||||
print("\nskip application checks (pass --app <image.hex>)")
|
print("\nskip application checks (pass --app <image.hex>)")
|
||||||
|
|
||||||
@@ -229,6 +242,10 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
help="the resident loader's .bin, to prove the slot survives an erase")
|
help="the resident loader's .bin, to prove the slot survives an erase")
|
||||||
parser.add_argument("--marker", default="",
|
parser.add_argument("--marker", default="",
|
||||||
help="text the application emits when it runs, e.g. APP")
|
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)
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
rig = pbrig.Rig(pbrig.Deployment.from_args(args))
|
rig = pbrig.Rig(pbrig.Deployment.from_args(args))
|
||||||
@@ -236,7 +253,8 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
f"{' (autobaud)' if args.autobaud else ''}")
|
f"{' (autobaud)' if args.autobaud else ''}")
|
||||||
print("this overwrites the application flash and EEPROM\n")
|
print("this overwrites the application flash and EEPROM\n")
|
||||||
with tempfile.TemporaryDirectory(prefix="pbhw-") as temporary:
|
with tempfile.TemporaryDirectory(prefix="pbhw-") as temporary:
|
||||||
return Suite(rig, pathlib.Path(temporary)).run(args.app, args.loader, args.marker)
|
return Suite(rig, pathlib.Path(temporary)).run(args.app, args.loader, args.marker,
|
||||||
|
args.marker_wait)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ class Deployment:
|
|||||||
port: str = "" # serial device the loader speaks on
|
port: str = "" # serial device the loader speaks on
|
||||||
baud: int = 57600 # host rate; for autobaud, the rate to drive
|
baud: int = 57600 # host rate; for autobaud, the rate to drive
|
||||||
autobaud: bool = False # send the calibration pulse instead of p+b
|
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
|
programmer: str = "" # avrdude -c
|
||||||
part: str = "" # avrdude -p
|
part: str = "" # avrdude -p
|
||||||
avrdude: str = "avrdude"
|
avrdude: str = "avrdude"
|
||||||
@@ -101,6 +102,7 @@ class Deployment:
|
|||||||
port=os.environ.get("PUREBOOT_PORT", ""),
|
port=os.environ.get("PUREBOOT_PORT", ""),
|
||||||
baud=int(os.environ.get("PUREBOOT_BAUD", "57600")),
|
baud=int(os.environ.get("PUREBOOT_BAUD", "57600")),
|
||||||
autobaud=os.environ.get("PUREBOOT_AUTOBAUD", "") not in ("", "0"),
|
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", ""),
|
programmer=os.environ.get("PUREBOOT_PROGRAMMER", ""),
|
||||||
part=os.environ.get("PUREBOOT_PART", ""),
|
part=os.environ.get("PUREBOOT_PART", ""),
|
||||||
avrdude=os.environ.get("AVRDUDE", "avrdude"),
|
avrdude=os.environ.get("AVRDUDE", "avrdude"),
|
||||||
@@ -117,6 +119,8 @@ class Deployment:
|
|||||||
help="host rate (for autobaud, the rate to drive)")
|
help="host rate (for autobaud, the rate to drive)")
|
||||||
parser.add_argument("--autobaud", action="store_true", default=env.autobaud,
|
parser.add_argument("--autobaud", action="store_true", default=env.autobaud,
|
||||||
help="send the calibration pulse instead of the p+b knock")
|
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("--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("--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("--avrdude", default=env.avrdude, help="path to avrdude")
|
||||||
@@ -128,6 +132,7 @@ class Deployment:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def from_args(cls, args: argparse.Namespace) -> "Deployment":
|
def from_args(cls, args: argparse.Namespace) -> "Deployment":
|
||||||
return cls(port=args.port, baud=args.baud, autobaud=args.autobaud,
|
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,
|
programmer=args.programmer, part=args.part, avrdude=args.avrdude,
|
||||||
bitclock=args.bitclock, pureboot=args.pureboot, wait=args.wait)
|
bitclock=args.bitclock, pureboot=args.pureboot, wait=args.wait)
|
||||||
|
|
||||||
@@ -267,6 +272,8 @@ class Rig:
|
|||||||
"--wait", str(self.d.wait)]
|
"--wait", str(self.d.wait)]
|
||||||
if self.d.autobaud if autobaud is None else autobaud:
|
if self.d.autobaud if autobaud is None else autobaud:
|
||||||
command.append("--autobaud")
|
command.append("--autobaud")
|
||||||
|
if self.d.one_wire:
|
||||||
|
command.append("--one-wire")
|
||||||
command += [str(a) for a in args]
|
command += [str(a) for a in args]
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(command, capture_output=True, text=True, timeout=timeout)
|
result = subprocess.run(command, capture_output=True, text=True, timeout=timeout)
|
||||||
@@ -274,6 +281,22 @@ class Rig:
|
|||||||
return 99, f"TIMEOUT after {timeout}s\n{expired.stdout or ''}{expired.stderr or ''}"
|
return 99, f"TIMEOUT after {timeout}s\n{expired.stdout or ''}{expired.stderr or ''}"
|
||||||
return result.returncode, (result.stdout or "") + (result.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:
|
def capture(self, seconds: float = 2.0, baud: int | None = None) -> bytes:
|
||||||
"""Listen to whatever the board is saying, at an arbitrary rate.
|
"""Listen to whatever the board is saying, at an arbitrary rate.
|
||||||
|
|
||||||
|
|||||||
@@ -84,6 +84,20 @@ def collect() -> dict[str, list[tuple[str, int, int]]]:
|
|||||||
for match in SIZE_TEST.finditer((tree / "CTestTestfile.cmake").read_text()):
|
for match in SIZE_TEST.finditer((tree / "CTestTestfile.cmake").read_text()):
|
||||||
found.setdefault(chip, []).append((match["name"], match["elf"], int(match["limit"])))
|
found.setdefault(chip, []).append((match["name"], match["elf"], int(match["limit"])))
|
||||||
sizes = measure([elf for rows in found.values() for _, elf, _ in rows], tool)
|
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 = {
|
measured = {
|
||||||
chip: sorted(((name, sizes[elf], limit) for name, elf, limit in rows if elf in sizes),
|
chip: sorted(((name, sizes[elf], limit) for name, elf, limit in rows if elf in sizes),
|
||||||
key=lambda row: -row[1])
|
key=lambda row: -row[1])
|
||||||
@@ -120,7 +134,11 @@ def cmd_max(args) -> int:
|
|||||||
|
|
||||||
|
|
||||||
def cmd_check_readme(args) -> int:
|
def cmd_check_readme(args) -> int:
|
||||||
"""The README's per-chip table, against the stock and autobaud builds."""
|
"""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()
|
readme = (ROOT / "pureboot" / "README.md").read_text()
|
||||||
measured = collect()
|
measured = collect()
|
||||||
rows = re.findall(r"^\|\s*(AT\w+[^|]*?)\s*\|[^|]*\|[^|]*\|[^|]*\|\s*(\d+) B\s*\|\s*(\d+) B\s*\|$",
|
rows = re.findall(r"^\|\s*(AT\w+[^|]*?)\s*\|[^|]*\|[^|]*\|[^|]*\|\s*(\d+) B\s*\|\s*(\d+) B\s*\|$",
|
||||||
@@ -132,7 +150,16 @@ def cmd_check_readme(args) -> int:
|
|||||||
# "ATmega48, 48A, 48P, 48PA †" — the first name is the family's base.
|
# "ATmega48, 48A, 48P, 48PA †" — the first name is the family's base.
|
||||||
chip = re.sub(r"[^a-z0-9]", "", chips.split(",")[0].strip().lower())
|
chip = re.sub(r"[^a-z0-9]", "", chips.split(",")[0].strip().lower())
|
||||||
built = {name: text for name, text, _ in measured.get(chip, [])}
|
built = {name: text for name, text, _ in measured.get(chip, [])}
|
||||||
for target, documented in (("pureboot", stock_doc), ("pureboot_autobaud", auto_doc)):
|
# 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:
|
if target not in built:
|
||||||
skipped += 1
|
skipped += 1
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
{
|
{
|
||||||
constexpr auto sol = avr::uart::solve_baud(dev::clock, 115200_Bd);
|
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");
|
static_assert(sol.u2x && sol.ubrr < 256, "lean bring-up writes UBRR0L only, with U2X0");
|
||||||
avr::hw::reg<"UBRR0">::write(static_cast<std::uint8_t>(sol.ubrr));
|
avr::hw::ubrr0::write(static_cast<std::uint8_t>(sol.ubrr));
|
||||||
avr::hw::ucsr0a::write(avr::hw::ucsr0a::u2x0(1));
|
avr::hw::ucsr0a::write(avr::hw::ucsr0a::u2x0(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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