Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eb213e1025 | |||
| 3e4bfbaf48 | |||
| 579ca81b27 | |||
| 5d520a1ff9 | |||
| f71d76a815 | |||
| 47419400f6 |
138
CMakeLists.txt
138
CMakeLists.txt
@@ -217,6 +217,23 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
--workdir ${CMAKE_BINARY_DIR}/pbwindow-work)
|
--workdir ${CMAKE_BINARY_DIR}/pbwindow-work)
|
||||||
set_tests_properties(pureboot.window PROPERTIES TIMEOUT 300)
|
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
|
||||||
@@ -312,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")
|
||||||
@@ -321,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})
|
||||||
@@ -365,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()
|
||||||
@@ -437,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 —
|
||||||
@@ -511,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
|
||||||
@@ -559,6 +672,31 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
${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
|
# The autobaud window: the calibration poll budget, at the measured
|
||||||
# 10 cycles a poll (pbwindow.py pins the constant the README's
|
# 10 cycles a poll (pbwindow.py pins the constant the README's
|
||||||
# seconds arithmetic uses; the budget itself is the clock-free knob).
|
# seconds arithmetic uses; the budget itself is the clock-free knob).
|
||||||
|
|||||||
2
libavr
2
libavr
Submodule libavr updated: a9fe6bed50...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()
|
||||||
|
|||||||
@@ -26,8 +26,10 @@ Every axis moves per build — see *Configuration*. The Autobaud column is the
|
|||||||
worst configuration the space produces for the chip: the clock-free build —
|
worst configuration the space produces for the chip: the clock-free build —
|
||||||
it alone carries the calibration machinery — with the `OSCCAL` trim baked
|
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,
|
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*). On default pins
|
which the loader then has to release (*Pin ownership*). Folding the same
|
||||||
without the trim the same loaders run 4–10 B smaller.
|
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 |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
@@ -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
|
||||||
@@ -126,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
|
||||||
@@ -302,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
|
||||||
@@ -469,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
|
||||||
|
|||||||
@@ -77,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
|
||||||
@@ -157,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
|
||||||
@@ -169,22 +172,42 @@ constexpr int usart_unit = PUREBOOT_USART;
|
|||||||
constexpr int usart_unit = 0;
|
constexpr int usart_unit = 0;
|
||||||
#endif
|
#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
|
||||||
|
|
||||||
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_unit, 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 around the window's narrow (uint24_t) countdown:
|
// The compiled idle poll around the window's narrow (uint24_t) countdown:
|
||||||
// the RXC test, then sbiw + sbci + brne (5). The test's cost follows the
|
// the RXC test, then sbiw + sbci + brne (5). The test's cost follows the
|
||||||
// status register's home — a 2-cycle bit-skip where UCSRnA sits in
|
// 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
|
// bit-addressable I/O (the classic megas), lds + skip (4) in extended
|
||||||
// I/O. A uint32_t countdown pays one more sbci — window_polls() adds it
|
// I/O. Half-duplex polls through rx_ready()'s release-line test, which
|
||||||
// where the count forces the wide type. Held by the pureboot.window gate.
|
// -Os outlines: the rcall (3), the UCSR#B read and not-taken skip with
|
||||||
// The lookup rides the baud parameter so it stays dependent: the trait is
|
// the jump over the write (I/O 3, extended 5), the ret (4) — and the
|
||||||
// an incomplete type on the USART-less chips, which parse this template
|
// call in the loop body pushes the countdown into call-saved registers,
|
||||||
// without ever instantiating it.
|
// 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>>
|
template <avr::baud_t Baud, typename U = avr::hw::usart_of<usart_unit>>
|
||||||
static consteval std::uint8_t poll_cost()
|
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;
|
return U::ucsra::addr < 0x40 ? 7 : 9;
|
||||||
}
|
}
|
||||||
static constexpr std::uint8_t poll_cycles = poll_cost<B>();
|
static constexpr std::uint8_t poll_cycles = poll_cost<B>();
|
||||||
@@ -220,8 +243,11 @@ struct hardware_link {
|
|||||||
|
|
||||||
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 around the window's narrow (uint24_t) countdown:
|
// The compiled idle poll around the window's narrow (uint24_t) countdown:
|
||||||
// sbis skipping the exit (2), sbiw + sbci + brne (5). A uint32_t
|
// sbis skipping the exit (2), sbiw + sbci + brne (5). A uint32_t
|
||||||
|
|||||||
@@ -26,15 +26,16 @@ else:
|
|||||||
import termios
|
import termios
|
||||||
|
|
||||||
PROMPT = b"+"
|
PROMPT = b"+"
|
||||||
VERSION = 8 # 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 ---
|
||||||
|
|
||||||
|
|
||||||
@@ -599,9 +696,16 @@ class Loader:
|
|||||||
break
|
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
|
||||||
@@ -1478,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)
|
||||||
@@ -1492,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:
|
||||||
@@ -1517,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)")
|
||||||
@@ -1575,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
|
||||||
@@ -64,6 +67,10 @@ struct link {
|
|||||||
static constexpr avr::baud_t baud{115200};
|
static constexpr avr::baud_t baud{115200};
|
||||||
#endif
|
#endif
|
||||||
using tx_t = avr::uart::usart<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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ mis-counted cycle per poll shifts a window by 10 % and more.
|
|||||||
|
|
||||||
Fixed-baud loaders declare their window in seconds (--seconds, the build's
|
Fixed-baud loaders declare their window in seconds (--seconds, the build's
|
||||||
TIMEOUT). The autobaud loader's window is its calibration poll budget
|
TIMEOUT). The autobaud loader's window is its calibration poll budget
|
||||||
(--autobaud-polls); the seconds it amounts to are budget × 10 / f_cpu, the
|
(--autobaud-polls); the seconds it amounts to are budget × 9 / f_cpu, the
|
||||||
measured cost of the calibrate() wait loop this gate pins.
|
measured cost of the calibrate() wait loop this gate pins.
|
||||||
"""
|
"""
|
||||||
import argparse
|
import argparse
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -85,6 +86,18 @@ void window_uart_hook(avr_irq_t *, std::uint32_t, void *)
|
|||||||
window_first_tx();
|
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") {
|
||||||
@@ -101,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -260,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
|
||||||
@@ -288,6 +309,13 @@ 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;
|
||||||
@@ -308,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;
|
||||||
}
|
}
|
||||||
@@ -329,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.
|
||||||
@@ -352,6 +413,7 @@ void bridge_reset()
|
|||||||
rx_active = 0;
|
rx_active = 0;
|
||||||
tx_active = 0;
|
tx_active = 0;
|
||||||
tx_level = 1;
|
tx_level = 1;
|
||||||
|
mcu_owns_line = false; // avr_reset zeroed DDR: every pin reads input again
|
||||||
// Re-drive the idle line through a forced transition: ioport pin irqs are
|
// 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
|
// 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
|
// keeps its pre-reset cached value — so a plain raise(1) against a cached
|
||||||
@@ -360,8 +422,8 @@ void bridge_reset()
|
|||||||
// pulse and mis-locks or boots the application on the first real knock.
|
// 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
|
// No cycles run between the two raises, so the device only ever sees the
|
||||||
// final idle-high.
|
// final idle-high.
|
||||||
avr_raise_irq(rx_pin, 0);
|
bridge_drive(0);
|
||||||
avr_raise_irq(rx_pin, 1);
|
bridge_drive(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void poll_pty()
|
void poll_pty()
|
||||||
@@ -375,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 ---
|
||||||
@@ -518,6 +581,19 @@ 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)
|
if (window_report)
|
||||||
@@ -532,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;
|
||||||
@@ -564,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();
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -136,7 +136,9 @@ 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 build and the worst
|
"""The README's per-chip table, against the stock build and the worst
|
||||||
autobaud configuration (OSCCAL baked, plus the USART-pin release where
|
autobaud configuration (OSCCAL baked, plus the USART-pin release where
|
||||||
the chip has a USART) — the config the Autobaud column documents."""
|
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*\|$",
|
||||||
@@ -148,8 +150,15 @@ 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, [])}
|
||||||
worst = ("pureboot_autobaud_osccal_on_usart0"
|
# The on-USART pair defines the column where the chip has a USART;
|
||||||
if "pureboot_autobaud_osccal_on_usart0" in built else "pureboot_autobaud_osccal")
|
# 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)):
|
for target, documented in (("pureboot", stock_doc), (worst, auto_doc)):
|
||||||
if target not in built:
|
if target not in built:
|
||||||
skipped += 1
|
skipped += 1
|
||||||
|
|||||||
Reference in New Issue
Block a user