pureboot v8: one-wire on every backend

HALF_DUPLEX deploys a shared line per backend. The hardware USART takes
the library's .half_duplex turn-around — RXD and TXD tied off-chip, each
reply byte held to transmit-complete before the line can be released
(m8 404 B, m328P 440, 1284P 460; the window poll runs through the
outlined release-line call at 18 or 22 cycles a poll, measured off the
built loops and held per chip by pureboot.window.halfduplex). The
software and autobaud links fold onto the RX pin — RX == TX spells the
same — and cost nothing: the frame's direction wrap is what the dropped
second-pin init paid, and the worst image in the space is unchanged at
the 1284s' 502 of 512, now with its one-wire twin proven equal across
the exhaustive matrix. The host gains --one-wire, the echo discard a
shared line requires: the adapter's echo is matched byte for byte and a
reply interleaving a blind write — a loader already in session
re-prompts inside the knock — is held for the reader. The device runner
models the shared line by direction (drives only while the firmware's
DDR reads input, decodes only while the firmware owns it, supplies the
host-side echo), extends the USART pin-ownership model to RXEN's hold
on RXD, and starts the pty USART from the datasheet's zeroed UCSR#B:
simavr's TXEN-set reset plus its clear-UDRE-on-TXEN-drop otherwise
wedges the first transmitter after a receiver-only program, which the
half-duplex window gate caught as a banner that never came. v7 is
tagged at its era's last commit; v8 changes nothing on the wire.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 02:32:11 +02:00
parent 47419400f6
commit f71d76a815
13 changed files with 507 additions and 50 deletions

View File

@@ -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).

View File

@@ -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()

View File

@@ -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 410 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 410 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

View File

@@ -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

View File

@@ -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 (14 speak # map lives: the tool keeps a decoder for every generation in it (14 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,61 @@ 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""
def __getattr__(self, name):
return getattr(self._port, name)
def write(self, data):
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:
budget = deadline - time.monotonic()
if budget <= 0:
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?")
byte = self._port.read_exact(1, budget)
if byte == remaining[:1]:
remaining = remaining[1:]
else:
self._pending += byte
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 ---
@@ -1478,12 +1537,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 +1552,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 +1579,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 +1640,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)

View File

@@ -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');

View File

@@ -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:

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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();

View File

@@ -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)

View File

@@ -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