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)
|
||||
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,
|
||||
# installed one slot lower, must serve the full command set.
|
||||
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
|
||||
# link sits on. Unreachable rates drop out here rather than aborting the
|
||||
# configure.
|
||||
# The optional trailing argument is the one-wire shape of the same link:
|
||||
# ONE_WIRE folds a software point onto its RX pin (the default, or the
|
||||
# named USART's RXD), HALF_DUPLEX is the hardware USART's turn-around.
|
||||
function(pureboot_matrix_point hz baud link pins)
|
||||
set(_name pbm_${hz}_${baud}_${link})
|
||||
if(link STREQUAL "software")
|
||||
@@ -321,9 +341,21 @@ if(PROJECT_IS_TOP_LEVEL)
|
||||
list(APPEND _args RX ${PUREBOOT_USART${pins}_RX} TX ${PUREBOOT_USART${pins}_TX})
|
||||
set(_name ${_name}_on${pins})
|
||||
endif()
|
||||
if(ARGC GREATER 4 AND ARGV4 STREQUAL "ONE_WIRE")
|
||||
if(NOT pins STREQUAL "")
|
||||
set(_args SERIAL software RX ${PUREBOOT_USART${pins}_RX} TX ${PUREBOOT_USART${pins}_RX})
|
||||
else()
|
||||
list(APPEND _args RX pb0 TX pb0)
|
||||
endif()
|
||||
set(_name ${_name}_1w)
|
||||
endif()
|
||||
else()
|
||||
pureboot_baud_feasible(${hz} ${baud} 0 _ok)
|
||||
set(_args USART ${link})
|
||||
if(ARGC GREATER 4 AND ARGV4 STREQUAL "HALF_DUPLEX")
|
||||
list(APPEND _args HALF_DUPLEX)
|
||||
set(_name ${_name}_hd)
|
||||
endif()
|
||||
endif()
|
||||
if(_ok)
|
||||
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_baud IN LISTS _full_bauds)
|
||||
pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} software "")
|
||||
pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} software "" ONE_WIRE)
|
||||
if(PUREBOOT_HAS_USART)
|
||||
pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} software 0)
|
||||
pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} software 0 ONE_WIRE)
|
||||
pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} 0 "")
|
||||
pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} 0 "" HALF_DUPLEX)
|
||||
endif()
|
||||
if(PUREBOOT_HAS_USART1)
|
||||
pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} software 1)
|
||||
pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} software 1 ONE_WIRE)
|
||||
pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} 1 "")
|
||||
pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} 1 "" HALF_DUPLEX)
|
||||
endif()
|
||||
endforeach()
|
||||
endforeach()
|
||||
@@ -437,6 +474,30 @@ if(PROJECT_IS_TOP_LEVEL)
|
||||
RX ${PUREBOOT_USART0_RX} TX ${PUREBOOT_USART0_TX})
|
||||
endif()
|
||||
|
||||
# The one-wire axis at its fixed points, in both matrix modes (the
|
||||
# exhaustive sweep carries the same shapes across its cross product):
|
||||
# the software link folded onto one pin, the tightest autobaud image
|
||||
# likewise — on the default pin and on the USART's own RXD, whose
|
||||
# release the now-driven shared pin needs where a receive-only link
|
||||
# would not — and the hardware USART's half-duplex turn-around, stock
|
||||
# and at the widest fixed-baud shape.
|
||||
# The two spellings deliberately split across the two points: HALF_DUPLEX
|
||||
# folds TX onto RX, RX == TX states the same thing directly.
|
||||
pureboot_size_variant(pureboot_1w SERIAL software RX pb0 HALF_DUPLEX)
|
||||
pureboot_size_variant(pureboot_1w_autobaud_osccal SERIAL autobaud OSCCAL 0x9c RX pb0 TX pb0)
|
||||
if(PUREBOOT_HAS_USART)
|
||||
pureboot_size_variant(pureboot_1w_on_usart0 SERIAL software
|
||||
RX ${PUREBOOT_USART0_RX} TX ${PUREBOOT_USART0_RX})
|
||||
pureboot_size_variant(pureboot_1w_autobaud_osccal_on_usart0 SERIAL autobaud OSCCAL 0x9c
|
||||
RX ${PUREBOOT_USART0_RX} TX ${PUREBOOT_USART0_RX})
|
||||
pureboot_size_variant(pureboot_hd HALF_DUPLEX)
|
||||
list(GET _matrix_clocks -1 _hd_top_hz)
|
||||
pureboot_size_variant(pureboot_hd_wide CLOCK ${_hd_top_hz} BAUD 9600 HALF_DUPLEX)
|
||||
endif()
|
||||
if(PUREBOOT_HAS_USART1)
|
||||
pureboot_size_variant(pureboot_usart1_hd USART 1 HALF_DUPLEX)
|
||||
endif()
|
||||
|
||||
# The trim byte, observed through the wire from the first prompt — one
|
||||
# chip per OSCCAL addressing class: extended I/O on the 328P (data 0x66,
|
||||
# an sts — DS40002061B §36), plain I/O on the 85 (data 0x51, an out —
|
||||
@@ -511,6 +572,58 @@ if(PROJECT_IS_TOP_LEVEL)
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||
${CMAKE_BINARY_DIR}/pbmute-work ${_mute_link})
|
||||
set_tests_properties(pureboot.mute PROPERTIES TIMEOUT 180)
|
||||
|
||||
# The same hand-over against the one-wire deployment on that USART's
|
||||
# RXD: RXEN forces the shared pin's direction, so a loader that only
|
||||
# released the transmit-side hold would read the wire and answer into
|
||||
# a pin it cannot drive. The host runs with the --one-wire echo
|
||||
# discard, which the bridge's shared-line model feeds for real.
|
||||
get_target_property(_mute1w_link pureboot_1w_on_usart0 PUREBOOT_LINK)
|
||||
add_test(NAME pureboot.mute.onewire
|
||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbmute.py
|
||||
${PB_DEVICE} $<TARGET_FILE:pureboot_1w_on_usart0> ${PUREBOOT_SIM_MCU} ${_mute_hz}
|
||||
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_mute_baud}
|
||||
$<TARGET_FILE:pbapp_handover>.bin
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||
${CMAKE_BINARY_DIR}/pbmute-1w-work ${_mute1w_link})
|
||||
set_tests_properties(pureboot.mute.onewire PROPERTIES TIMEOUT 180)
|
||||
|
||||
# The full protocol suite over one shared pin: the loader folded onto
|
||||
# PB0, the bridge following the pin's direction, the fixture
|
||||
# bannering as a guest on the same line, and the host discarding its
|
||||
# own echo throughout.
|
||||
get_target_property(_1w_hz pureboot_1w PUREBOOT_HZ)
|
||||
get_target_property(_1w_baud pureboot_1w PUREBOOT_BAUD)
|
||||
get_target_property(_1w_link pureboot_1w PUREBOOT_LINK)
|
||||
add_executable(pbapp_1w test/pbapp.cpp)
|
||||
target_link_libraries(pbapp_1w PRIVATE libavr)
|
||||
target_compile_definitions(pbapp_1w PRIVATE PUREBOOT_CLOCK_HZ=${_1w_hz}
|
||||
PUREBOOT_BAUD=${_1w_baud} PUREBOOT_SOFT_SERIAL
|
||||
PUREBOOT_RX=pb0 PUREBOOT_TX=pb0)
|
||||
add_custom_command(TARGET pbapp_1w POST_BUILD
|
||||
COMMAND ${CMAKE_OBJCOPY} -O binary
|
||||
$<TARGET_FILE:pbapp_1w> $<TARGET_FILE:pbapp_1w>.bin)
|
||||
add_test(NAME pureboot.onewire
|
||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbtest.py
|
||||
${PB_DEVICE} $<TARGET_FILE:pureboot_1w> ${PUREBOOT_SIM_MCU} ${_1w_hz}
|
||||
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_1w_baud} ${PUREBOOT_EEPROM}
|
||||
$<TARGET_FILE:pbapp_1w>.bin ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||
${CMAKE_BINARY_DIR}/pb1w-work ${_1w_link})
|
||||
set_tests_properties(pureboot.onewire PROPERTIES TIMEOUT 180)
|
||||
|
||||
# The hardware USART's half-duplex turn-around, end to end: every
|
||||
# reply byte runs drive-line, TXC-hold, release — against simavr's
|
||||
# RXEN-gated receiver, which drops input to a disabled receiver the
|
||||
# way silicon does. The pty is a two-wire transport, so the host
|
||||
# needs no echo discard here; the off-chip tie itself is the
|
||||
# hardware bench's item.
|
||||
add_test(NAME pureboot.halfduplex
|
||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbtest.py
|
||||
${PB_DEVICE} $<TARGET_FILE:pureboot_hd> ${PUREBOOT_SIM_MCU} ${_pb_stock_hz}
|
||||
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_pb_stock_baud} ${PUREBOOT_EEPROM}
|
||||
$<TARGET_FILE:pbapp>.bin ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||
${CMAKE_BINARY_DIR}/pbhd-work)
|
||||
set_tests_properties(pureboot.halfduplex PROPERTIES TIMEOUT 180)
|
||||
endif()
|
||||
|
||||
# The second USART, driven for real on one chip: instance selection is
|
||||
@@ -559,6 +672,31 @@ if(PROJECT_IS_TOP_LEVEL)
|
||||
${CMAKE_BINARY_DIR}/pbautobaud-work)
|
||||
set_tests_properties(pureboot.autobaud PROPERTIES TIMEOUT 240)
|
||||
|
||||
# The tightest deployment in the space, end to end: the autobaud
|
||||
# loader folded onto the USART's own RXD with the OSCCAL trim baked
|
||||
# — one-wire calibration, the receive-side release, and the host's
|
||||
# echo discard, over the same two-clock sweep. One chip carries it;
|
||||
# the shape is chip-independent.
|
||||
if(LIBAVR_MCU STREQUAL "atmega328p")
|
||||
get_target_property(_ab1w_link pureboot_1w_autobaud_osccal_on_usart0 PUREBOOT_LINK)
|
||||
add_executable(pbapp_autobaud_1w test/pbapp.cpp)
|
||||
target_link_libraries(pbapp_autobaud_1w PRIVATE libavr)
|
||||
target_compile_definitions(pbapp_autobaud_1w PRIVATE PUREBOOT_CLOCK_HZ=1000000
|
||||
PUREBOOT_BAUD=9600 PUREBOOT_SOFT_SERIAL
|
||||
PUREBOOT_RX=pd0 PUREBOOT_TX=pd0)
|
||||
add_custom_command(TARGET pbapp_autobaud_1w POST_BUILD
|
||||
COMMAND ${CMAKE_OBJCOPY} -O binary
|
||||
$<TARGET_FILE:pbapp_autobaud_1w> $<TARGET_FILE:pbapp_autobaud_1w>.bin)
|
||||
add_test(NAME pureboot.autobaud.onewire
|
||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbautobaud.py
|
||||
${PB_DEVICE} $<TARGET_FILE:pureboot_1w_autobaud_osccal_on_usart0>
|
||||
${PUREBOOT_SIM_MCU} ${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE}
|
||||
$<TARGET_FILE:pbapp_autobaud_1w>.bin
|
||||
1000000 9600 ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||
${CMAKE_BINARY_DIR}/pbautobaud-1w-work ${_ab1w_link})
|
||||
set_tests_properties(pureboot.autobaud.onewire PROPERTIES TIMEOUT 240)
|
||||
endif()
|
||||
|
||||
# The autobaud window: the calibration poll budget, at the measured
|
||||
# 10 cycles a poll (pbwindow.py pins the constant the README's
|
||||
# seconds arithmetic uses; the budget itself is the clock-free knob).
|
||||
|
||||
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_HAS_USART ${_pb_has_usart})
|
||||
set_property(GLOBAL PROPERTY PUREBOOT_HAS_USART1 ${_pb_has_usart1})
|
||||
set_property(GLOBAL PROPERTY PUREBOOT_USART0_RX ${_pb_usart0_rx})
|
||||
set_property(GLOBAL PROPERTY PUREBOOT_USART0_TX ${_pb_usart0_tx})
|
||||
set_property(GLOBAL PROPERTY PUREBOOT_USART1_RX ${_pb_usart1_rx})
|
||||
set_property(GLOBAL PROPERTY PUREBOOT_USART1_TX ${_pb_usart1_tx})
|
||||
|
||||
# The port's own build (tests, the size matrix) reads the geometry from the
|
||||
@@ -236,7 +238,8 @@ endfunction()
|
||||
|
||||
# pureboot_add_loader(<name> [CLOCK <hz>] [BAUD <bd>]
|
||||
# [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,
|
||||
# <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
|
||||
# USART's own) — what a test harness speaks to it with.
|
||||
#
|
||||
# HALF_DUPLEX is the one-wire deployment, per backend: on the hardware USART
|
||||
# it enables the library's .half_duplex turn-around (RXD and TXD tied
|
||||
# together off-chip); on a software or autobaud link it puts both directions
|
||||
# on the RX pin — the same thing RX == TX spells directly.
|
||||
#
|
||||
# SERIAL autobaud measures the host's bit timing at run time, so the image
|
||||
# carries no clock and no baud: CLOCK and BAUD are not build parameters there,
|
||||
# and one binary per chip serves every F_CPU and every rate. The stamped
|
||||
@@ -257,7 +265,7 @@ endfunction()
|
||||
# purely for the application's benefit, its own link being clock-free. No
|
||||
# value, no code.
|
||||
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)
|
||||
message(FATAL_ERROR "pureboot_add_loader(${name}): unknown arguments ${PB_UNPARSED_ARGUMENTS}")
|
||||
endif()
|
||||
@@ -294,6 +302,9 @@ function(pureboot_add_loader name)
|
||||
message(FATAL_ERROR "pureboot_add_loader(${name}): ${LIBAVR_MCU} has no hardware USART")
|
||||
endif()
|
||||
set(_serial_defines PUREBOOT_USART=${PB_USART})
|
||||
if(PB_HALF_DUPLEX)
|
||||
list(APPEND _serial_defines PUREBOOT_HALF_DUPLEX)
|
||||
endif()
|
||||
set(_link usart${PB_USART})
|
||||
else()
|
||||
if(PB_SERIAL STREQUAL "auto")
|
||||
@@ -303,6 +314,9 @@ function(pureboot_add_loader name)
|
||||
endif()
|
||||
if(_usart)
|
||||
set(_link usart0)
|
||||
if(PB_HALF_DUPLEX)
|
||||
set(_serial_defines PUREBOOT_HALF_DUPLEX)
|
||||
endif()
|
||||
else()
|
||||
set(PB_SERIAL software)
|
||||
endif()
|
||||
@@ -311,6 +325,15 @@ function(pureboot_add_loader name)
|
||||
if(NOT PB_RX)
|
||||
set(PB_RX pb0)
|
||||
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)
|
||||
set(PB_TX pb1)
|
||||
endif()
|
||||
@@ -334,10 +357,19 @@ function(pureboot_add_loader name)
|
||||
string(REPLACE "SW" "sw" _link ${_link})
|
||||
get_property(_tx0 GLOBAL PROPERTY PUREBOOT_USART0_TX)
|
||||
get_property(_tx1 GLOBAL PROPERTY PUREBOOT_USART1_TX)
|
||||
get_property(_rx0 GLOBAL PROPERTY PUREBOOT_USART0_RX)
|
||||
get_property(_rx1 GLOBAL PROPERTY PUREBOOT_USART1_RX)
|
||||
if(_usart AND PB_TX STREQUAL _tx0)
|
||||
set(_link "${_link}@0")
|
||||
elseif(_usart1 AND PB_TX STREQUAL _tx1)
|
||||
set(_link "${_link}@1")
|
||||
elseif(PB_TX STREQUAL PB_RX AND _usart AND PB_RX STREQUAL _rx0)
|
||||
# One-wire on a USART's RXD: RXEN forces that pin's direction
|
||||
# (§20.7.3), so the driven shared pin is held exactly like a
|
||||
# TXD — the harness models the hold either way.
|
||||
set(_link "${_link}@0")
|
||||
elseif(PB_TX STREQUAL PB_RX AND _usart1 AND PB_RX STREQUAL _rx1)
|
||||
set(_link "${_link}@1")
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
@@ -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 —
|
||||
it alone carries the calibration machinery — with the `OSCCAL` trim baked
|
||||
and, where the chip has a USART, the link deployed on that USART's own pins,
|
||||
which the loader then has to release (*Pin ownership*). On default pins
|
||||
without the trim the same loaders run 4–10 B smaller.
|
||||
which the loader then has to release (*Pin ownership*). Folding the same
|
||||
build onto a single pin (*One-wire*) measures identically on every chip, so
|
||||
the column covers that twin too. On default pins without the trim the same
|
||||
loaders run 4–10 B smaller.
|
||||
|
||||
| Chip | Flash | Loader at | Link | Stock | Autobaud |
|
||||
|---|---|---|---|---|---|
|
||||
@@ -75,6 +77,7 @@ repo's build and by a downstream project alike:
|
||||
| `RX <pin>`, `TX <pin>` | software-UART pins | `pb0`, `pb1` |
|
||||
| `TIMEOUT <s>` | the activation window | 8 |
|
||||
| `OSCCAL <byte>` | a measured oscillator trim, applied before anything runs | none — no value, no code |
|
||||
| `HALF_DUPLEX` | one-wire: both directions on one line (*One-wire* below) | off |
|
||||
|
||||
The default baud is the fastest of 115200/57600/38400/19200/9600 the clock
|
||||
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
|
||||
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
|
||||
chip via the `LIBAVR_MCU` toolchain preset), consumes this directory, and
|
||||
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
|
||||
version that speaks it — and re-homes the autobaud unit into the GPIOR pair
|
||||
on the chips that have one (Session: what must not be written), which is
|
||||
where `--info`'s measured clock now reads it on those parts.
|
||||
where `--info`'s measured clock now reads it on those parts. **8** changes
|
||||
nothing on the wire either: it marks the builds whose deployment may be
|
||||
one-wire (*One-wire* above) — the hardware USART's half-duplex turn-around,
|
||||
or a software link folded onto a single pin. The host-side trace is
|
||||
`--one-wire`, the echo discard a shared line requires of any tool driving
|
||||
it.
|
||||
|
||||
Every closed generation is tagged in this repo at its era's last commit — the
|
||||
commit just before the next version bump, so a tag holds everything its
|
||||
@@ -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
|
||||
held against; `--clock <hz>` states the drift against a nominal.
|
||||
|
||||
`--one-wire` marks the link as a shared line (*One-wire* above): the tool
|
||||
reads back and verifies its own echoed bytes, whatever the backend.
|
||||
It combines with everything, `--scan` included — undiscarded echoes would
|
||||
answer every rate a scan probes.
|
||||
|
||||
`--scan` is the diagnosis once a fixed-baud loader has gone silent: it walks
|
||||
±10 % around `--baud` in 2 % steps, nearest first, one probe per activation
|
||||
window — reset the target as each probe announces itself (a board with DTR
|
||||
|
||||
@@ -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 —
|
||||
// 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
|
||||
// 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)
|
||||
#error "PUREBOOT_AUTOBAUD measures a software link; it cannot drive a hardware USART"
|
||||
#endif
|
||||
#if defined(PUREBOOT_HALF_DUPLEX) && (defined(PUREBOOT_SOFT_SERIAL) || defined(PUREBOOT_AUTOBAUD))
|
||||
#error "PUREBOOT_HALF_DUPLEX is the hardware USART's one-wire mode; a software link goes one-wire by RX == TX"
|
||||
#endif
|
||||
#if !defined(PUREBOOT_RX)
|
||||
#define PUREBOOT_RX pb0
|
||||
#endif
|
||||
@@ -169,22 +172,42 @@ constexpr int usart_unit = PUREBOOT_USART;
|
||||
constexpr int usart_unit = 0;
|
||||
#endif
|
||||
|
||||
// One-wire on the hardware USART (PUREBOOT_HALF_DUPLEX): RXD and TXD tied
|
||||
// together off-chip, exactly one direction enabled at a time — the library's
|
||||
// .half_duplex turn-around. The activation window is unchanged; only its
|
||||
// poll grows the release-line test rx_ready() carries in this mode.
|
||||
constexpr bool hw_half_duplex =
|
||||
#if defined(PUREBOOT_HALF_DUPLEX)
|
||||
true;
|
||||
#else
|
||||
false;
|
||||
#endif
|
||||
|
||||
template <avr::hertz_t C, avr::baud_t B>
|
||||
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 RXC test, then sbiw + sbci + brne (5). The test's cost follows the
|
||||
// status register's home — a 2-cycle bit-skip where UCSRnA sits in
|
||||
// bit-addressable I/O (the classic megas), lds + skip (4) in extended
|
||||
// I/O. A uint32_t countdown pays one more sbci — window_polls() adds it
|
||||
// where the count forces the wide type. Held by the pureboot.window gate.
|
||||
// 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.
|
||||
// I/O. Half-duplex polls through rx_ready()'s release-line test, which
|
||||
// -Os outlines: the rcall (3), the UCSR#B read and not-taken skip with
|
||||
// the jump over the write (I/O 3, extended 5), the ret (4) — and the
|
||||
// call in the loop body pushes the countdown into call-saved registers,
|
||||
// where the uint24_t step is ldi+sub+sbc+sbc (4) instead of sbiw+sbci
|
||||
// (3). Measured off the built loops: 18 a poll in bit-addressable I/O,
|
||||
// 22 in extended. A uint32_t countdown pays one more sbci —
|
||||
// window_polls() adds it where the count forces the wide type. Held per
|
||||
// chip by the pureboot.window gates. The lookup rides the baud parameter
|
||||
// so it stays dependent: the trait is an incomplete type on the
|
||||
// USART-less chips, which parse this template without ever instantiating
|
||||
// it.
|
||||
template <avr::baud_t Baud, typename U = avr::hw::usart_of<usart_unit>>
|
||||
static consteval std::uint8_t poll_cost()
|
||||
{
|
||||
if (hw_half_duplex)
|
||||
return U::ucsra::addr < 0x40 ? 18 : 22;
|
||||
return U::ucsra::addr < 0x40 ? 7 : 9;
|
||||
}
|
||||
static constexpr std::uint8_t poll_cycles = poll_cost<B>();
|
||||
@@ -220,8 +243,11 @@ struct hardware_link {
|
||||
|
||||
template <avr::hertz_t C, avr::baud_t B>
|
||||
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 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:
|
||||
// sbis skipping the exit (2), sbiw + sbci + brne (5). A uint32_t
|
||||
|
||||
@@ -26,15 +26,16 @@ else:
|
||||
import termios
|
||||
|
||||
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
|
||||
# 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
|
||||
# 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
|
||||
# for moves the floor.
|
||||
# builds and changes nothing on the wire; 8 the one-wire deployments, whose
|
||||
# only host-side trace is the --one-wire echo discard), and a version it has
|
||||
# no decoder for moves the floor.
|
||||
OLDEST_LOADER = 1
|
||||
NEWEST_LOADER = 7
|
||||
NEWEST_LOADER = 8
|
||||
SLOT = 512 # the loader slot, on every chip
|
||||
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;
|
||||
# 7 gives 'J' a selector byte (older loaders take the bare address — jump()
|
||||
# sends each form to the version that speaks it) and re-homes the autobaud
|
||||
# unit into the GPIOR pair where the chip has one.
|
||||
# unit into the GPIOR pair where the chip has one; 8 marks the builds whose
|
||||
# deployment may be one-wire (hardware half-duplex, or a software link folded
|
||||
# onto one pin) — nothing on the wire either, but a shared line makes the
|
||||
# host read its own bytes back, which is what --one-wire consumes.
|
||||
UNIFIED_LOADER = 5
|
||||
SP_FLASH, SP_EEPROM, SP_RAM, SP_FUSE, SP_SPM = 0, 1, 2, 3, 4
|
||||
|
||||
@@ -433,6 +437,99 @@ if os.name == "nt":
|
||||
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 ---
|
||||
|
||||
|
||||
@@ -599,9 +696,16 @@ class Loader:
|
||||
break
|
||||
knocks = 0
|
||||
refusal = None
|
||||
# The knock is the only write in the protocol with no ack between its
|
||||
# bytes, so on a shared line it is the only one whose echo may
|
||||
# legitimately not come back — the device's ack collides with it and
|
||||
# wins (OneWirePort.write). Losing a byte here is what the retry below
|
||||
# is for; raising instead aborted the loop before it ever ran, which on
|
||||
# real wiring made every reconnect into a live session fail.
|
||||
knock_out = getattr(self.port, "write_blind", self.port.write)
|
||||
while True:
|
||||
self.port.flush_input()
|
||||
self.port.write(knock)
|
||||
knock_out(knock)
|
||||
knocks += 1
|
||||
if PROMPT in self.port.read_available(0.4):
|
||||
# Settle: absorb a real loader's trailing bytes before asking
|
||||
@@ -1478,12 +1582,13 @@ def scan_report(baud, pct, version, clock=None):
|
||||
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
|
||||
drifted ratio, since its rate scales with its clock. One probe per
|
||||
activation window, and with an application resident the window opens
|
||||
exactly once per reset, so each probe announces itself and expects a
|
||||
fresh reset before knocking."""
|
||||
fresh reset before knocking. On a shared line the probes echo back like
|
||||
everything else; undiscarded they would answer every rate."""
|
||||
for pct in scan_ratios():
|
||||
rate = scan_rate(baud, pct)
|
||||
print(f"scan: {rate} Bd ({pct:+d} %) — reset the target", flush=True)
|
||||
@@ -1492,6 +1597,8 @@ def op_scan(port_path, baud, wait, clock=None):
|
||||
except Error as unmakeable:
|
||||
print(f"scan: {rate} Bd skipped — {unmakeable}")
|
||||
continue
|
||||
if one_wire:
|
||||
port = OneWirePort(port)
|
||||
try:
|
||||
info = Loader(port).connect(wait)
|
||||
except Error:
|
||||
@@ -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("--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("--one-wire", action="store_true",
|
||||
help="the link is a shared line: read back and discard this tool's own "
|
||||
"echoed bytes (any backend of a one-wire deployment)")
|
||||
parser.add_argument("--autobaud", action="store_true",
|
||||
help="drive an autobaud loader: send the 0xC0 calibration pulse and a single "
|
||||
"knock, and take geometry from the signature (no clock/baud baked in)")
|
||||
@@ -1575,11 +1685,14 @@ def main():
|
||||
if args.scan:
|
||||
if args.autobaud:
|
||||
parser.error("--scan probes fixed rates; an autobaud loader has none to miss")
|
||||
op_scan(args.port, args.baud, args.wait, args.clock)
|
||||
op_scan(args.port, args.baud, args.wait, args.clock, args.one_wire)
|
||||
return
|
||||
|
||||
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:
|
||||
loader = Loader(port)
|
||||
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)
|
||||
#define PUREBOOT_TX pb1
|
||||
#endif
|
||||
#if !defined(PUREBOOT_RX)
|
||||
#define PUREBOOT_RX pb0
|
||||
#endif
|
||||
#if !defined(PUREBOOT_USART)
|
||||
#define PUREBOOT_USART 0
|
||||
#endif
|
||||
@@ -64,6 +67,10 @@ struct link {
|
||||
static constexpr avr::baud_t baud{115200};
|
||||
#endif
|
||||
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)
|
||||
{
|
||||
tx_t::write(static_cast<std::uint8_t>(c));
|
||||
@@ -110,7 +117,21 @@ struct link<C, false> {
|
||||
#else
|
||||
static constexpr avr::baud_t baud{57600};
|
||||
#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)
|
||||
{
|
||||
tx_t::write(static_cast<std::uint8_t>(c));
|
||||
@@ -145,7 +166,7 @@ struct link<C, false> {
|
||||
|
||||
int main()
|
||||
{
|
||||
avr::init<typename link<dev::clock>::tx_t>();
|
||||
link<dev::clock>::init();
|
||||
#if !defined(PUREBOOT_HANDOVER)
|
||||
link<dev::clock>::tx('A');
|
||||
link<dev::clock>::tx('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.
|
||||
|
||||
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
|
||||
default), so the runner drives it over the GPIO⇄pty bridge (-l sw:B0,B1). The
|
||||
app fixture is built for (app_hz, app_baud); the hand-over is checked at that
|
||||
point, and a second point at half the clock proves the lock is measured, not
|
||||
baked in.
|
||||
The loader is a software-serial build, driven over the GPIO⇄pty bridge; the
|
||||
optional link overrides the default -l sw:B0,B1 — RX == TX in it is the
|
||||
one-wire deployment, and every session then runs with the host's echo
|
||||
discard on. The app fixture is built for (app_hz, app_baud); the hand-over
|
||||
is checked at that point, and a second point at half the clock proves the
|
||||
lock is measured, not baked in.
|
||||
"""
|
||||
|
||||
import os
|
||||
@@ -27,8 +28,12 @@ def fail(message):
|
||||
|
||||
|
||||
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)
|
||||
one_wire = re.fullmatch(r"sw:([A-H][0-7]),\1(@[01])?", link) is not None
|
||||
extra = ("--one-wire",) if one_wire else ()
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import pbsim
|
||||
@@ -55,11 +60,11 @@ def main():
|
||||
"""One clock point: reset, calibrate + knock, program, verify against the
|
||||
simulator's own flash, and (at the app's point) hand over to the fixture."""
|
||||
dump = os.path.join(workdir, f"flash_{label}.bin")
|
||||
device = pbsim.Device(device_bin, elf, mcu, str(hz), base_hex, page, baud, dump, link="sw:B0,B1")
|
||||
device = pbsim.Device(device_bin, elf, mcu, str(hz), base_hex, page, baud, dump, link=link)
|
||||
try:
|
||||
# The host tool, in autobaud mode, sends the 0xC0 calibration pulse
|
||||
# and a single knock at `baud`; the loader locks to it.
|
||||
out = pbsim.run_tool(tool, device.pty, baud, "--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")
|
||||
for needed in ("version", "signature", "fuses", "verify:", "stays"):
|
||||
if needed not in out:
|
||||
@@ -80,7 +85,7 @@ def main():
|
||||
# Read both memories back over the locked link and check them.
|
||||
read_flash = os.path.join(workdir, f"rf_{label}.bin")
|
||||
read_eeprom = os.path.join(workdir, f"re_{label}.bin")
|
||||
out = pbsim.run_tool(tool, device.pty, baud, "--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,
|
||||
"--read-eeprom", read_eeprom, "--stay")
|
||||
if out.count("verify:") != 2:
|
||||
@@ -99,6 +104,8 @@ def main():
|
||||
# pulse is genuinely seen and the test cannot pass vacuously.)
|
||||
device.reset()
|
||||
port = pb.Port(device.pty, baud)
|
||||
if one_wire:
|
||||
port = pb.OneWirePort(port)
|
||||
try:
|
||||
time.sleep(0.2)
|
||||
port.write(bytes((pb.CALIBRATE,)))
|
||||
@@ -117,6 +124,8 @@ def main():
|
||||
|
||||
device.reset()
|
||||
port = pb.Port(device.pty, baud)
|
||||
if one_wire:
|
||||
port = pb.OneWirePort(port)
|
||||
try:
|
||||
loader = pb.Loader(port)
|
||||
live = loader.connect_autobaud(15)
|
||||
@@ -160,9 +169,11 @@ def main():
|
||||
the question is only whether the loader can still measure the pulse."""
|
||||
dump = os.path.join(workdir, f"flash_{label}.bin")
|
||||
device = pbsim.Device(device_bin, elf, mcu, str(hz), base_hex, page, baud, dump,
|
||||
link="sw:B0,B1")
|
||||
link=link)
|
||||
try:
|
||||
port = pb.Port(device.pty, baud)
|
||||
if one_wire:
|
||||
port = pb.OneWirePort(port)
|
||||
try:
|
||||
live = pb.Loader(port).connect_autobaud(15)
|
||||
if live.version != pb.NEWEST_LOADER:
|
||||
|
||||
@@ -17,6 +17,7 @@ Usage: pbmute.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
@@ -35,6 +36,9 @@ def main():
|
||||
|
||||
if "@" not in link:
|
||||
fail(f"the link {link} names no owning USART — nothing would be under test")
|
||||
# A shared line (RX == TX) echoes the host's own bytes; discard them the
|
||||
# way the shipped --one-wire mode does.
|
||||
one_wire = re.fullmatch(r"sw:([A-H][0-7]),\1@[01]", link) is not None
|
||||
|
||||
os.makedirs(workdir, exist_ok=True)
|
||||
dump = os.path.join(workdir, "dump.bin")
|
||||
@@ -42,6 +46,8 @@ def main():
|
||||
device = pbsim.Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump, link=link)
|
||||
try:
|
||||
port = pb.Port(device.pty, baud)
|
||||
if one_wire:
|
||||
port = pb.OneWirePort(port)
|
||||
loader = pb.Loader(port)
|
||||
loader.connect(25)
|
||||
resident = loader.info.version
|
||||
|
||||
@@ -11,6 +11,7 @@ loader built off the chip's natural serial default.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
@@ -70,10 +71,15 @@ def main():
|
||||
+ 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)
|
||||
try:
|
||||
# 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")
|
||||
for needed in ("version", "signature", "fuses", "verify:", "stays"):
|
||||
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 APP banner first.
|
||||
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,
|
||||
"--poke", f"{ram_base:#x}:{probe}", "--peek", f"{ram_base:#x}:3", "--stay")
|
||||
if out.count("verify:") != 2:
|
||||
@@ -111,6 +117,8 @@ def main():
|
||||
# land in the application, which banners on the same link.
|
||||
device.reset()
|
||||
port = pb.Port(device.pty, baud)
|
||||
if one_wire:
|
||||
port = pb.OneWirePort(port)
|
||||
try:
|
||||
loader = pb.Loader(port)
|
||||
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
|
||||
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.
|
||||
"""
|
||||
import argparse
|
||||
|
||||
@@ -54,6 +54,7 @@ namespace {
|
||||
avr_t *avr;
|
||||
uart_pty_t uart_pty;
|
||||
bool link_software;
|
||||
avr_uart_t *hw_uart; // the pty-driven USART, for the datasheet-reset fix below
|
||||
char uart_digit = '0';
|
||||
char sw_rx_port = 'B', sw_tx_port = 'B';
|
||||
int sw_rx_bit = 0, sw_tx_bit = 1;
|
||||
@@ -85,6 +86,18 @@ void window_uart_hook(avr_irq_t *, std::uint32_t, void *)
|
||||
window_first_tx();
|
||||
}
|
||||
|
||||
// One-wire (RX == TX in the link spec): both directions on one GPIO line
|
||||
// idling on the firmware's pull-up. The bridge then follows the pin's
|
||||
// direction the way the real wiring does: it drives only while the
|
||||
// firmware's DDR bit reads input, decodes transitions as the firmware's
|
||||
// transmit only while the firmware owns the line, ignores its own raises
|
||||
// coming back through the shared irq — and echoes every byte it drives back
|
||||
// to the pty, which is what the host-side FTDI tie does and what the host
|
||||
// tool's --one-wire mode reads back and discards.
|
||||
bool link_one_wire;
|
||||
bool mcu_owns_line;
|
||||
bool self_drive;
|
||||
|
||||
int parse_link(std::string_view spec)
|
||||
{
|
||||
if (spec == "usart0" || spec == "usart1") {
|
||||
@@ -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);
|
||||
if (fields == 4 || fields == 5) {
|
||||
sw_tx_owner = owner;
|
||||
link_one_wire = sw_rx_port == sw_tx_port && sw_rx_bit == sw_tx_bit;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -260,7 +274,14 @@ avr_uart_t *tx_owner;
|
||||
|
||||
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
|
||||
@@ -288,6 +309,13 @@ void find_tx_owner()
|
||||
|
||||
void tx_hook(avr_irq_t *, std::uint32_t value, void *)
|
||||
{
|
||||
if (link_one_wire && (self_drive || !mcu_owns_line)) {
|
||||
// The bridge's own drive coming back through the shared irq, or a
|
||||
// transition while the line is the bridge's — either way not the
|
||||
// firmware talking: the decoder sees an idle line.
|
||||
tx_level = 1;
|
||||
return;
|
||||
}
|
||||
if (tx_pin_taken()) { // the USART holds the line; the port write goes nowhere
|
||||
tx_level = 1;
|
||||
return;
|
||||
@@ -308,15 +336,29 @@ std::uint8_t rx_byte;
|
||||
|
||||
void rx_start_next();
|
||||
|
||||
// Every level the bridge itself puts on the line goes through here, so the
|
||||
// shared-pin decoder can tell its own drive from the firmware's.
|
||||
void bridge_drive(int level)
|
||||
{
|
||||
self_drive = true;
|
||||
avr_raise_irq(rx_pin, static_cast<std::uint32_t>(level));
|
||||
self_drive = false;
|
||||
}
|
||||
|
||||
avr_cycle_count_t rx_step(avr_t *, avr_cycle_count_t when, void *)
|
||||
{
|
||||
if (rx_bit < 8) {
|
||||
avr_raise_irq(rx_pin, (rx_byte >> rx_bit) & 1);
|
||||
bridge_drive((rx_byte >> rx_bit) & 1);
|
||||
rx_bit++;
|
||||
return when + bit_cycles;
|
||||
}
|
||||
if (rx_bit == 8) { // stop bit, plus one idle bit of margin
|
||||
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++;
|
||||
return when + 2 * bit_cycles;
|
||||
}
|
||||
@@ -329,14 +371,33 @@ void rx_start_next()
|
||||
{
|
||||
if (rx_active || rx_head == rx_tail)
|
||||
return;
|
||||
// The firmware is answering on the shared line: hold the byte — a real
|
||||
// host's transmission waits out the reply on the wire too. The next
|
||||
// poll_pty tick retries once the line is handed back.
|
||||
if (link_one_wire && mcu_owns_line)
|
||||
return;
|
||||
rx_byte = rx_queue[rx_head];
|
||||
rx_head = (rx_head + 1) % sizeof(rx_queue);
|
||||
rx_active = 1;
|
||||
rx_bit = 0;
|
||||
avr_raise_irq(rx_pin, 0); // start bit
|
||||
bridge_drive(0); // start bit
|
||||
avr_cycle_timer_register(avr, bit_cycles, rx_step, nullptr);
|
||||
}
|
||||
|
||||
// The shared pin's direction is the line's ownership: DDR-out is the
|
||||
// firmware driving a frame, DDR-in hands the line back to the bridge.
|
||||
void on_ddr(avr_irq_t *, std::uint32_t value, void *)
|
||||
{
|
||||
const bool owns = (value >> sw_rx_bit) & 1;
|
||||
if (mcu_owns_line && !owns)
|
||||
bridge_drive(1); // hand-back: a turn-based host idles here, and the cache stays truthful
|
||||
mcu_owns_line = owns;
|
||||
// A byte held back while the firmware answered starts from the next
|
||||
// poll_pty tick, never from inside the DDR write itself — the port
|
||||
// model's own pull-up re-derivation runs right after this notify and
|
||||
// would erase a start edge raised here.
|
||||
}
|
||||
|
||||
// A reset abandons whatever the bridge was mid-transfer: bytes still queued
|
||||
// for a chip that no longer has the context to receive them meaningfully,
|
||||
// and a decode in progress on a TX line the reset may have already changed.
|
||||
@@ -352,6 +413,7 @@ void bridge_reset()
|
||||
rx_active = 0;
|
||||
tx_active = 0;
|
||||
tx_level = 1;
|
||||
mcu_owns_line = false; // avr_reset zeroed DDR: every pin reads input again
|
||||
// Re-drive the idle line through a forced transition: ioport pin irqs are
|
||||
// IRQ_FLAG_FILTERED, and avr_reset zeroes the port latch while the irq
|
||||
// keeps its pre-reset cached value — so a plain raise(1) against a cached
|
||||
@@ -360,8 +422,8 @@ void bridge_reset()
|
||||
// pulse and mis-locks or boots the application on the first real knock.
|
||||
// No cycles run between the two raises, so the device only ever sees the
|
||||
// final idle-high.
|
||||
avr_raise_irq(rx_pin, 0);
|
||||
avr_raise_irq(rx_pin, 1);
|
||||
bridge_drive(0);
|
||||
bridge_drive(1);
|
||||
}
|
||||
|
||||
void poll_pty()
|
||||
@@ -375,8 +437,9 @@ void poll_pty()
|
||||
rx_queue[rx_tail] = chunk[i];
|
||||
rx_tail = next;
|
||||
}
|
||||
if (got > 0)
|
||||
rx_start_next();
|
||||
// Unconditional: a byte held back while the firmware owned a shared
|
||||
// line restarts from here once the hand-back has happened.
|
||||
rx_start_next();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ main ---
|
||||
@@ -518,6 +581,19 @@ int main(int argc, char *argv[])
|
||||
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS(uart_digit), &flags);
|
||||
flags &= ~AVR_UART_FLAG_POLL_SLEEP;
|
||||
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS(uart_digit), &flags);
|
||||
// simavr leaves TXEN set out of reset where silicon clears the whole
|
||||
// UCSR#B (§20.11.3). Harmless to a loader that enables TXEN itself —
|
||||
// but a half-duplex build's receiver-only init then *drops* TXEN,
|
||||
// and this uart model clears UDRE on that edge and never re-raises
|
||||
// it on a later enable: the first transmitter after the hand-over
|
||||
// waits UDRE forever, a wedge silicon does not have. Start from the
|
||||
// datasheet's zero, as the software bridge's tx-owner model does.
|
||||
for (avr_io_t *io = avr->io_port; io; io = io->next)
|
||||
if (io->kind && std::string_view{io->kind} == "uart" &&
|
||||
reinterpret_cast<avr_uart_t *>(io)->name == uart_digit)
|
||||
hw_uart = reinterpret_cast<avr_uart_t *>(io);
|
||||
if (hw_uart)
|
||||
avr_regbit_clear(avr, hw_uart->txen);
|
||||
uart_pty_init(avr, &uart_pty);
|
||||
uart_pty_connect(&uart_pty, uart_digit);
|
||||
if (window_report)
|
||||
@@ -532,7 +608,10 @@ int main(int argc, char *argv[])
|
||||
avr_irq_register_notify(
|
||||
avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ(sw_tx_port), static_cast<unsigned>(sw_tx_bit)), tx_hook,
|
||||
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;
|
||||
struct termios raw;
|
||||
@@ -564,6 +643,8 @@ int main(int argc, char *argv[])
|
||||
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS(uart_digit), &flags);
|
||||
flags &= ~AVR_UART_FLAG_POLL_SLEEP;
|
||||
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS(uart_digit), &flags);
|
||||
if (hw_uart) // and simavr's bogus reset TXEN (§20.11.3: zero)
|
||||
avr_regbit_clear(avr, hw_uart->txen);
|
||||
} else {
|
||||
bridge_reset();
|
||||
reset_tx_owner();
|
||||
|
||||
@@ -53,7 +53,7 @@ class Suite:
|
||||
"""The info block, which every later check takes its bounds from."""
|
||||
module = pbrig.load_pureboot(self.rig.d.pureboot)
|
||||
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:
|
||||
loader = module.Loader(port)
|
||||
if self.rig.d.autobaud:
|
||||
@@ -87,7 +87,10 @@ class Suite:
|
||||
rate = module.scan_rate(self.rig.d.baud, pct)
|
||||
self.rig.reset()
|
||||
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:
|
||||
self.check("scan opens every probe rate", False, f"{rate} Bd: {error}")
|
||||
return
|
||||
@@ -129,18 +132,28 @@ class Suite:
|
||||
got = erased.read_bytes() if erased.exists() else b""
|
||||
self.check("EEPROM erase leaves 0xff", got == b"\xff" * size, f"{len(got)} B")
|
||||
|
||||
def application(self, info, app: pathlib.Path, marker: str) -> 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))
|
||||
self.check(f"application flash + verify ({app.name})", rc == 0, self._brief(out))
|
||||
|
||||
if marker:
|
||||
# The tool hands over as it ends its session, so the application is
|
||||
# already running; opening the port does not reset a board whose DTR
|
||||
# is unwired, so this simply listens.
|
||||
data = self.rig.capture(seconds=2.5)
|
||||
# already running — but only on a board whose DTR is unwired, where
|
||||
# opening a port simply listens. Where DTR *is* wired to reset (an
|
||||
# Arduino, most USB-serial dev boards), this open resets the part
|
||||
# and the activation window comes first, so a marker emitted once at
|
||||
# startup happens on the far side of a wait this cannot know the
|
||||
# length of: the window is a compile-time constant and nothing on
|
||||
# the wire reports it. Hence --marker-wait, and a fixture that
|
||||
# repeats its banner (PUREBOOT_HEARTBEAT) rather than saying it once.
|
||||
data = self.rig.capture(seconds=marker_wait)
|
||||
seen = marker.encode() in data
|
||||
sample = "".join(chr(b) if 32 <= b < 127 else "." for b in data[:40])
|
||||
self.check(f"application runs (emits {marker!r})", seen, f"|{sample}|")
|
||||
self.check(f"application runs (emits {marker!r})", seen,
|
||||
f"|{sample}|" if seen or data else
|
||||
f"nothing in {marker_wait:g} s — if this board resets when its port "
|
||||
f"opens, that wait has to outlast the activation window")
|
||||
|
||||
back = self.work / "app-back.bin"
|
||||
rc, out = self.rig.pureboot("--read-flash", str(back))
|
||||
@@ -187,7 +200,7 @@ class Suite:
|
||||
# ------------------------------------------------------------------- run
|
||||
|
||||
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")
|
||||
info = self.identity()
|
||||
if info is None:
|
||||
@@ -203,7 +216,7 @@ class Suite:
|
||||
|
||||
if app:
|
||||
print("\napplication")
|
||||
self.application(info, app, marker)
|
||||
self.application(info, app, marker, marker_wait)
|
||||
else:
|
||||
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")
|
||||
parser.add_argument("--marker", default="",
|
||||
help="text the application emits when it runs, e.g. APP")
|
||||
parser.add_argument("--marker-wait", type=float, default=2.5,
|
||||
help="seconds to listen for it. On a board whose DTR is wired to "
|
||||
"reset, opening the port resets the part, so this must outlast "
|
||||
"the activation window (default 2.5)")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
rig = pbrig.Rig(pbrig.Deployment.from_args(args))
|
||||
@@ -236,7 +253,8 @@ def main(argv: list[str] | None = None) -> int:
|
||||
f"{' (autobaud)' if args.autobaud else ''}")
|
||||
print("this overwrites the application flash and EEPROM\n")
|
||||
with tempfile.TemporaryDirectory(prefix="pbhw-") as temporary:
|
||||
return Suite(rig, pathlib.Path(temporary)).run(args.app, args.loader, args.marker)
|
||||
return Suite(rig, pathlib.Path(temporary)).run(args.app, args.loader, args.marker,
|
||||
args.marker_wait)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -87,6 +87,7 @@ class Deployment:
|
||||
port: str = "" # serial device the loader speaks on
|
||||
baud: int = 57600 # host rate; for autobaud, the rate to drive
|
||||
autobaud: bool = False # send the calibration pulse instead of p+b
|
||||
one_wire: bool = False # shared line: the host discards its own echo
|
||||
programmer: str = "" # avrdude -c
|
||||
part: str = "" # avrdude -p
|
||||
avrdude: str = "avrdude"
|
||||
@@ -101,6 +102,7 @@ class Deployment:
|
||||
port=os.environ.get("PUREBOOT_PORT", ""),
|
||||
baud=int(os.environ.get("PUREBOOT_BAUD", "57600")),
|
||||
autobaud=os.environ.get("PUREBOOT_AUTOBAUD", "") not in ("", "0"),
|
||||
one_wire=os.environ.get("PUREBOOT_ONE_WIRE", "") not in ("", "0"),
|
||||
programmer=os.environ.get("PUREBOOT_PROGRAMMER", ""),
|
||||
part=os.environ.get("PUREBOOT_PART", ""),
|
||||
avrdude=os.environ.get("AVRDUDE", "avrdude"),
|
||||
@@ -117,6 +119,8 @@ class Deployment:
|
||||
help="host rate (for autobaud, the rate to drive)")
|
||||
parser.add_argument("--autobaud", action="store_true", default=env.autobaud,
|
||||
help="send the calibration pulse instead of the p+b knock")
|
||||
parser.add_argument("--one-wire", action="store_true", default=env.one_wire,
|
||||
help="shared line: pass the tool its echo discard")
|
||||
parser.add_argument("--programmer", default=env.programmer, help="avrdude -c, e.g. atmelice_isp")
|
||||
parser.add_argument("--part", default=env.part, help="avrdude -p, e.g. t13 or m328p")
|
||||
parser.add_argument("--avrdude", default=env.avrdude, help="path to avrdude")
|
||||
@@ -128,6 +132,7 @@ class Deployment:
|
||||
@classmethod
|
||||
def from_args(cls, args: argparse.Namespace) -> "Deployment":
|
||||
return cls(port=args.port, baud=args.baud, autobaud=args.autobaud,
|
||||
one_wire=args.one_wire,
|
||||
programmer=args.programmer, part=args.part, avrdude=args.avrdude,
|
||||
bitclock=args.bitclock, pureboot=args.pureboot, wait=args.wait)
|
||||
|
||||
@@ -267,6 +272,8 @@ class Rig:
|
||||
"--wait", str(self.d.wait)]
|
||||
if self.d.autobaud if autobaud is None else autobaud:
|
||||
command.append("--autobaud")
|
||||
if self.d.one_wire:
|
||||
command.append("--one-wire")
|
||||
command += [str(a) for a in args]
|
||||
try:
|
||||
result = subprocess.run(command, capture_output=True, text=True, timeout=timeout)
|
||||
@@ -274,6 +281,22 @@ class Rig:
|
||||
return 99, f"TIMEOUT after {timeout}s\n{expired.stdout or ''}{expired.stderr or ''}"
|
||||
return result.returncode, (result.stdout or "") + (result.stderr or "")
|
||||
|
||||
def open_port(self, baud: int | None = None):
|
||||
"""A port opened the way this deployment says to speak to the board.
|
||||
|
||||
Everything the rig runs as a *subprocess* gets its flags from
|
||||
`pureboot()` above; anything that drives the protocol in-process has
|
||||
to reach the same facts, and until this existed only the subprocess
|
||||
path could. A shared line is the one where that gap is fatal rather
|
||||
than untidy: the host reads back every byte it writes, so an
|
||||
undiscarded echo answers the knock before the device does. Open
|
||||
through here and a one-wire deployment cannot be silently driven as
|
||||
a two-wire one.
|
||||
"""
|
||||
module = load_pureboot(self.d.pureboot)
|
||||
port = module.Port(self.d.port, self.d.baud if baud is None else baud)
|
||||
return module.OneWirePort(port) if self.d.one_wire else port
|
||||
|
||||
def capture(self, seconds: float = 2.0, baud: int | None = None) -> bytes:
|
||||
"""Listen to whatever the board is saying, at an arbitrary rate.
|
||||
|
||||
|
||||
@@ -136,7 +136,9 @@ def cmd_max(args) -> int:
|
||||
def cmd_check_readme(args) -> int:
|
||||
"""The README's per-chip table, against the stock build and the worst
|
||||
autobaud configuration (OSCCAL baked, plus the USART-pin release where
|
||||
the chip has a USART) — the 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()
|
||||
measured = collect()
|
||||
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.
|
||||
chip = re.sub(r"[^a-z0-9]", "", chips.split(",")[0].strip().lower())
|
||||
built = {name: text for name, text, _ in measured.get(chip, [])}
|
||||
worst = ("pureboot_autobaud_osccal_on_usart0"
|
||||
if "pureboot_autobaud_osccal_on_usart0" in built else "pureboot_autobaud_osccal")
|
||||
# The on-USART pair defines the column where the chip has a USART;
|
||||
# the default-pin pair is the whole space elsewhere. Whichever twin
|
||||
# measures larger is the number the cell must state.
|
||||
candidates = [name for name in ("pureboot_autobaud_osccal_on_usart0",
|
||||
"pureboot_1w_autobaud_osccal_on_usart0") if name in built]
|
||||
if not candidates:
|
||||
candidates = [name for name in ("pureboot_autobaud_osccal",
|
||||
"pureboot_1w_autobaud_osccal") if name in built]
|
||||
worst = max(candidates, key=lambda name: built[name], default="pureboot_autobaud_osccal")
|
||||
for target, documented in (("pureboot", stock_doc), (worst, auto_doc)):
|
||||
if target not in built:
|
||||
skipped += 1
|
||||
|
||||
Reference in New Issue
Block a user