diff --git a/CMakeLists.txt b/CMakeLists.txt index be5d795..7ae2b12 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -157,6 +157,8 @@ if(PROJECT_IS_TOP_LEVEL) add_test(NAME pureboot.planner COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/test_planner.py ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py) + add_test(NAME pureboot.handshake + COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/test_handshake.py) endif() # The protocol test flashes this fixture through the loader with the real @@ -235,12 +237,12 @@ if(PROJECT_IS_TOP_LEVEL) # The size matrix: every configuration axis that could move the image # size — the serial backend (different code), the USART instance - # (different registers), the clock (different constants), and the baud - # through the shapes its bit timing takes — each combination must still - # fit the chip's slot budget. Pins are size-neutral (port and bit are - # immediate operands) and the timeout is a constant, so neither adds an - # axis. The stock build is one point of this matrix and already has its - # test. + # (different registers), the clock (different constants), the baud + # through the shapes its bit timing takes, and the pins through the one + # thing they decide (whether a bit-banged link has to release the USART + # that owns them) — each combination must still fit the chip's slot + # budget. The timeout is a constant and adds no axis. The stock build is + # one point of this matrix and already has its test. function(pureboot_size_variant name) pureboot_add_loader(${name} ${ARGN}) add_test(NAME ${name}.size @@ -258,18 +260,25 @@ if(PROJECT_IS_TOP_LEVEL) -DLIMIT=${PUREBOOT_LIMIT} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake) # One point of the exhaustive matrix, named from its resolved parameters - # so the enumeration cannot collide with itself. Unreachable rates drop - # out here rather than aborting the configure. - function(pureboot_matrix_point hz baud link) + # so the enumeration cannot collide with itself. `pins` is empty for the + # 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. + function(pureboot_matrix_point hz baud link pins) + set(_name pbm_${hz}_${baud}_${link}) if(link STREQUAL "software") pureboot_baud_feasible(${hz} ${baud} 1 _ok) set(_args SERIAL software) + if(NOT pins STREQUAL "") + list(APPEND _args RX ${PUREBOOT_USART${pins}_RX} TX ${PUREBOOT_USART${pins}_TX}) + set(_name ${_name}_on${pins}) + endif() else() pureboot_baud_feasible(${hz} ${baud} 0 _ok) set(_args USART ${link}) endif() if(_ok) - pureboot_size_variant(pbm_${hz}_${baud}_${link} CLOCK ${hz} BAUD ${baud} ${_args}) + pureboot_size_variant(${_name} CLOCK ${hz} BAUD ${baud} ${_args}) endif() endfunction() @@ -307,12 +316,14 @@ if(PROJECT_IS_TOP_LEVEL) if(DEFINED ENV{PUREBOOT_FULL_MATRIX}) 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 "") if(PUREBOOT_HAS_USART) - pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} 0) + pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} software 0) + pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} 0 "") endif() if(PUREBOOT_HAS_USART1) - pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} 1) + pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} software 1) + pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} 1 "") endif() endforeach() endforeach() @@ -331,11 +342,42 @@ if(PROJECT_IS_TOP_LEVEL) endforeach() list(GET _matrix_clocks -1 _matrix_top_hz) pureboot_size_variant(pureboot_sw_wide CLOCK ${_matrix_top_hz} BAUD 9600 SERIAL software) + # The pin axis at the widest software image — the slowest ladder rate + # against the fastest clock, whose bit spin needs the 16-bit delay + # loop — with the USART release on top of it. The exhaustive sweep + # above carries the same axis across its whole cross product. + if(PUREBOOT_HAS_USART) + pureboot_size_variant(pureboot_sw_wide_on_usart0 CLOCK ${_matrix_top_hz} BAUD 9600 + SERIAL software RX ${PUREBOOT_USART0_RX} TX ${PUREBOOT_USART0_TX}) + endif() + if(PUREBOOT_HAS_USART1) + pureboot_size_variant(pureboot_sw_wide_on_usart1 CLOCK ${_matrix_top_hz} BAUD 9600 + SERIAL software RX ${PUREBOOT_USART1_RX} TX ${PUREBOOT_USART1_TX}) + endif() endif() if(PUREBOOT_HAS_USART1) pureboot_size_variant(pureboot_usart1 USART 1) endif() + # The pin axis at its fixed points, in both matrix modes. The autobaud + # loader carries no clock and no baud, so the sweep has nothing to vary + # for it — yet it is the tightest image in the space, and on a USART's + # own pins it pays the release too: that combination is the one that + # overflowed the 1284's slot. The software build on those pins is the + # same deployment the mute test drives. + if(PUREBOOT_HAS_USART) + pureboot_size_variant(pureboot_sw_on_usart0 SERIAL software + RX ${PUREBOOT_USART0_RX} TX ${PUREBOOT_USART0_TX}) + pureboot_size_variant(pureboot_autobaud_on_usart0 SERIAL autobaud + RX ${PUREBOOT_USART0_RX} TX ${PUREBOOT_USART0_TX}) + endif() + if(PUREBOOT_HAS_USART1) + pureboot_size_variant(pureboot_sw_on_usart1 SERIAL software + RX ${PUREBOOT_USART1_RX} TX ${PUREBOOT_USART1_TX}) + pureboot_size_variant(pureboot_autobaud_on_usart1 SERIAL autobaud + RX ${PUREBOOT_USART1_RX} TX ${PUREBOOT_USART1_TX}) + endif() + # One configured deployment end to end — a real board's shape rather # than the stock assumption: the ATmega328P on its shipped 1 MHz fuses, # the software UART on hand-picked pins (TX = PB1, RX = PB5), the ladder @@ -364,6 +406,33 @@ if(PROJECT_IS_TOP_LEVEL) set_tests_properties(pureboot.custom PROPERTIES TIMEOUT 180) endif() + # Hand-over with the USART that owns the loader's pins left enabled — the + # state an application reaches by jumping in without a reset, and the one + # that made a bit-banged loader on PD0/PD1 (where the Uno's USB bridge + # lands) receive and obey while answering nothing. Run where it was found + # on silicon; the runner supplies the pin ownership simavr has no model + # for, which is what lets this fail when the release is gone. + if(LIBAVR_MCU STREQUAL "atmega328p" AND DEFINED PB_DEVICE) + get_target_property(_mute_hz pureboot_sw_on_usart0 PUREBOOT_HZ) + get_target_property(_mute_baud pureboot_sw_on_usart0 PUREBOOT_BAUD) + get_target_property(_mute_link pureboot_sw_on_usart0 PUREBOOT_LINK) + add_executable(pbapp_handover test/pbapp.cpp) + target_link_libraries(pbapp_handover PRIVATE libavr) + target_compile_definitions(pbapp_handover PRIVATE PUREBOOT_CLOCK_HZ=${_mute_hz} + PUREBOOT_BAUD=${_mute_baud} PUREBOOT_HANDOVER) + add_custom_command(TARGET pbapp_handover POST_BUILD + COMMAND ${CMAKE_OBJCOPY} -O binary + $ $.bin) + add_test(NAME pureboot.mute + COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbmute.py + ${PB_DEVICE} $ ${PUREBOOT_SIM_MCU} ${_mute_hz} + ${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_mute_baud} + $.bin + ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py + ${CMAKE_BINARY_DIR}/pbmute-work ${_mute_link}) + set_tests_properties(pureboot.mute PROPERTIES TIMEOUT 180) + endif() + # The second USART, driven for real on one chip: instance selection is # compile-checked everywhere, but only a live session proves the loader # initialized and polls the USART it claims to. The fixture application diff --git a/pureboot/CMakeLists.txt b/pureboot/CMakeLists.txt index 3eda44f..a15f814 100644 --- a/pureboot/CMakeLists.txt +++ b/pureboot/CMakeLists.txt @@ -116,6 +116,17 @@ else() math(EXPR _pb_limit "${_pb_slot} - 2") endif() +# The pins each USART owns. A bit-banged link deployed on them has to release +# that USART before it can drive the line, and those instructions are the one +# way the choice of pins moves the image — so a size matrix needs them as an +# axis even though pins are otherwise immediate operands. Uniform across every +# mega libavr covers: USART0 (the classics' un-numbered USART included) on +# PD0/PD1, USART1 on PD2/PD3. +set(_pb_usart0_rx pd0) +set(_pb_usart0_tx pd1) +set(_pb_usart1_rx pd2) +set(_pb_usart1_tx pd3) + # simavr names its cores after the base dies; the A revisions run on them # (the 644PA on the 644P core). set(_pb_sim_mcu ${LIBAVR_MCU}) @@ -133,6 +144,8 @@ 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_TX ${_pb_usart0_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 # parent scope; a downstream consumer gets the same variables for free. @@ -145,6 +158,10 @@ set(PUREBOOT_DEFAULT_HZ ${_pb_hz} PARENT_SCOPE) set(PUREBOOT_HAS_USART ${_pb_has_usart} PARENT_SCOPE) set(PUREBOOT_HAS_USART1 ${_pb_has_usart1} PARENT_SCOPE) set(PUREBOOT_SIM_MCU ${_pb_sim_mcu} PARENT_SCOPE) +set(PUREBOOT_USART0_RX ${_pb_usart0_rx} PARENT_SCOPE) +set(PUREBOOT_USART0_TX ${_pb_usart0_tx} PARENT_SCOPE) +set(PUREBOOT_USART1_RX ${_pb_usart1_rx} PARENT_SCOPE) +set(PUREBOOT_USART1_TX ${_pb_usart1_tx} PARENT_SCOPE) # The rates a default may pick, fastest first. set_property(GLOBAL PROPERTY PUREBOOT_BAUD_LADDER 115200 57600 38400 19200 9600) @@ -200,7 +217,8 @@ endfunction() # The loader target plus its flashable images (.hex for a programmer, # .bin for --update-loader). The resolved deployment is stamped on the # target as PUREBOOT_HZ / PUREBOOT_BAUD / PUREBOOT_LINK (the link spelled -# usart0, usart1 or sw:,) — what a test harness speaks to it with. +# usart0, usart1, or sw:, with a trailing @ where those pins are a +# USART's own) — what a test harness speaks to it with. # # 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, @@ -275,11 +293,21 @@ function(pureboot_add_loader name) else() set(_serial_defines PUREBOOT_SOFT_SERIAL PUREBOOT_RX=${PB_RX} PUREBOOT_TX=${PB_TX}) endif() - # sw:, as port letter and bit, upcased. + # sw:, as port letter and bit, upcased — with @ where + # the TX pin is a USART's own TXD, since a harness driving that + # link has to know the USART owns the pin until the loader + # releases it. string(SUBSTRING ${PB_RX} 1 2 _rx_pin) string(SUBSTRING ${PB_TX} 1 2 _tx_pin) string(TOUPPER "sw:${_rx_pin},${_tx_pin}" _link) string(REPLACE "SW" "sw" _link ${_link}) + get_property(_tx0 GLOBAL PROPERTY PUREBOOT_USART0_TX) + get_property(_tx1 GLOBAL PROPERTY PUREBOOT_USART1_TX) + if(_usart AND PB_TX STREQUAL _tx0) + set(_link "${_link}@0") + elseif(_usart1 AND PB_TX STREQUAL _tx1) + set(_link "${_link}@1") + endif() endif() endif() if(NOT PB_BAUD) diff --git a/pureboot/README.md b/pureboot/README.md index dad28ef..fe61f58 100644 --- a/pureboot/README.md +++ b/pureboot/README.md @@ -28,31 +28,32 @@ it carries the calibration machinery and no clock at all. | Chip | Flash | Loader at | Link | Stock | Autobaud | |---|---|---|---|---|---| -| ATtiny13, ATtiny13A † | 1 KiB | 0x0200 | software | 402 B | 472 B | -| ATtiny25 † | 2 KiB | 0x0600 | software | 406 B | 476 B | -| ATtiny45 † | 4 KiB | 0x0e00 | software | 410 B | 480 B | -| ATtiny85 † | 8 KiB | 0x1e00 | software | 410 B | 480 B | -| ATmega8, 8A | 8 KiB | 0x1e00 | USART0 | 372 B | 486 B | -| ATmega16, 16A | 16 KiB | 0x3e00 | USART0 | 374 B | 490 B | -| ATmega32, 32A | 32 KiB | 0x7e00 | USART0 | 374 B | 490 B | -| ATmega48, 48A, 48P, 48PA † | 4 KiB | 0x0e00 | USART0 | 400 B | 476 B | -| ATmega88, 88A, 88P, 88PA | 8 KiB | 0x1e00 | USART0 | 410 B | 486 B | -| ATmega168, 168A, 168P, 168PA | 16 KiB | 0x3e00 | USART0 | 412 B | 490 B | -| ATmega328, 328P | 32 KiB | 0x7e00 | USART0 | 412 B | 490 B | -| ATmega164A, 164P, 164PA | 16 KiB | 0x3e00 | USART0 | 412 B | 490 B | -| ATmega324A, 324P, 324PA | 32 KiB | 0x7e00 | USART0 | 412 B | 490 B | -| ATmega644, 644A, 644P, 644PA | 64 KiB | 0xfe00 | USART0 | 406 B | 484 B | -| ATmega1284, 1284P | 128 KiB | 0x1fe00 | USART0 | 432 B | 510 B | +| ATtiny13, ATtiny13A † | 1 KiB | 0x0200 | software | 394 B | 464 B | +| ATtiny25 † | 2 KiB | 0x0600 | software | 398 B | 468 B | +| ATtiny45 † | 4 KiB | 0x0e00 | software | 402 B | 472 B | +| ATtiny85 † | 8 KiB | 0x1e00 | software | 402 B | 472 B | +| ATmega8, 8A | 8 KiB | 0x1e00 | USART0 | 364 B | 478 B | +| ATmega16, 16A | 16 KiB | 0x3e00 | USART0 | 366 B | 482 B | +| ATmega32, 32A | 32 KiB | 0x7e00 | USART0 | 366 B | 482 B | +| ATmega48, 48A, 48P, 48PA † | 4 KiB | 0x0e00 | USART0 | 392 B | 468 B | +| ATmega88, 88A, 88P, 88PA | 8 KiB | 0x1e00 | USART0 | 402 B | 478 B | +| ATmega168, 168A, 168P, 168PA | 16 KiB | 0x3e00 | USART0 | 404 B | 482 B | +| ATmega328, 328P | 32 KiB | 0x7e00 | USART0 | 404 B | 482 B | +| ATmega164A, 164P, 164PA | 16 KiB | 0x3e00 | USART0 | 404 B | 482 B | +| ATmega324A, 324P, 324PA | 32 KiB | 0x7e00 | USART0 | 404 B | 482 B | +| ATmega644, 644A, 644P, 644PA | 64 KiB | 0xfe00 | USART0 | 398 B | 476 B | +| ATmega1284, 1284P | 128 KiB | 0x1fe00 | USART0 | 424 B | 502 B | † No hardware boot section: the host patches the reset vector, and the budget is 510 bytes, since the slot's last word is the trampoline. -The tightest fit in the whole space is the 1284s' autobaud build, 510 of its -512 — they alone carry the far-flash machinery (ELPM reads, RAMPZ page -commands) and autobaud alone carries the calibration loop. Everything else has -20 B of headroom or more. The flash bank riding in a transfer's selector byte -keeps even those chips' addressing the same 16-bit form every other chip uses, -which is why they are no longer the outlier they were. +The tightest fit in the whole space is the 1284s' autobaud build deployed on a +USART's own pins, 506 of its 512 — they alone carry the far-flash machinery +(ELPM reads, RAMPZ page commands), autobaud alone carries the calibration loop, +and a bit-banged link on a USART's pins alone has to release it (below). The +same build on the default pins is 502. The flash bank riding in a transfer's +selector byte keeps even those chips' addressing the same 16-bit form every +other chip uses, which is why they are no longer the outlier they were. The software UART enables the RX pull-up; TX idles high. All multi-byte wire quantities are little-endian. @@ -79,6 +80,13 @@ receiver's 100-cycles-a-bit floor. Whatever is picked or overridden is re-checked in the compile: an infeasible combination, or a USART the chip does not have, fails with a named static assert. +Putting a bit-banged link on a USART's own pins is a supported deployment, and +the usual one where a board's USB bridge is wired to RXD/TXD: the link's `init` +clears that USART's `UCSRnB` first, because while its `TXEN` is set the USART — +not the port register — owns the TX pin, and a loader entered from an +application that left it enabled would receive and obey while answering nothing +(§20.2). It costs four bytes, and only on those pins. + `SERIAL autobaud` takes neither: the loader **measures** the host's bit timing at run time, so `CLOCK` and `BAUD` are not build parameters there and one binary per chip serves every clock and every rate. It is for the deployments @@ -397,8 +405,10 @@ Per chip preset, `ctest` runs: the fastest clock — where a software UART's per-bit spin outgrows its one-register delay loop and takes the 16-bit one. That is the largest image the configuration space produces, and a shape the ladder default (always the - *fastest* rate a clock reaches) never picks. Pins are immediate operands and - the timeout is a constant: neither is an axis; + *fastest* rate a clock reaches) never picks. Pins are an axis for one reason + only, and it is enough: a bit-banged link on a USART's own pins has to + release that USART, so `pureboot_{sw,autobaud}_on_usart{0,1}` build there + too. The timeout is a constant and is no axis; - `pureboot_autobaud.size` — the clock-free build, which has no clock or baud axis of its own: one binary per chip has to serve every point the matrix below sweeps; @@ -412,6 +422,9 @@ Per chip preset, `ctest` runs: flash-resident section but `.text`, and the image byte-identical when linked at a different base — which is position independence itself rather than a proxy for it; +- `pureboot.handshake` — the host tool's activation must not hang on a target + that never falls quiet: the drain after a prompt is bounded by the handshake + deadline, and a well-behaved loader still connects; - `pureboot.planner` — the host tool's pure logic: programming orders and their recovery properties, the surgery, the staging composition, the boot-fuse decode, the update preflight over synthetic fuse bytes, and the repairing @@ -434,6 +447,12 @@ Per chip preset, `ctest` runs: - `pureboot.usart1` (644A) — the same suite over the second hardware USART: instance selection is compile-checked everywhere, but only a live session proves the loader polls the USART it claims; +- `pureboot.mute` (328P) — a software link on USART0's own pins, entered from an + application that handed over with that USART still enabled: the loader must + still answer, which it does only because it releases it. The pin ownership is + the runner's, not simavr's — simavr wires a USART through IRQs and never takes + the pin from the port, so without that model the state under test could not + arise at all; - `pureboot.dirty` (328P) — entering the loader from a running application over an SPM buffer it deliberately dirtied, the case the loader declines to guard: a bare verify must see the corruption and the repairing verify must fix it in @@ -449,5 +468,5 @@ Per chip preset, `ctest` runs: exists for. A lone calibration pulse with no knock behind it must still let the application boot, so no wait in activation can be unbounded. -`size`, `pi` and `planner` are host logic and run anywhere; the +`size`, `pi`, `planner` and `handshake` are host logic and run anywhere; the simulator-driven targets need simavr and a pty, so they are POSIX-only. diff --git a/test/pbapp.cpp b/test/pbapp.cpp index a501faa..e79c426 100644 --- a/test/pbapp.cpp +++ b/test/pbapp.cpp @@ -11,6 +11,10 @@ // reset reaches those loaders through the patched vector (or the runner // models BOOTRST), so the application owes them nothing. // +// PUREBOOT_HANDOVER drops the listening and jumps straight in, leaving the +// USART enabled behind it — the hand-over state a loader bit-banging on that +// USART's own pins has to survive. +// // The fixture speaks the deployment its loader was built for: the same // PUREBOOT_* defines configure it, and without them it assumes the stock // deployment (the crystal/RC clock table below, the chip's natural link). @@ -64,16 +68,29 @@ struct link { { tx_t::write(static_cast(c)); } + // The loader sits in the top slot — 512 bytes on every chip. The jump + // takes a word address, which is what makes the >64 KiB chips' entry + // reachable through a 16-bit pointer at all. + static void enter_loader() + { + constexpr std::uint32_t slot = 512; + reinterpret_cast(static_cast((avr::hw::db.mem.flash_size - slot) / 2))(); + } + [[noreturn]] static void idle() { - // 'L' hands back to the loader in the top slot — 512 bytes on every - // chip. The jump takes a word address, which is what makes the - // >64 KiB chips' entry reachable through a 16-bit pointer at all. - constexpr std::uint32_t slot = 512; +#if defined(PUREBOOT_HANDOVER) + // Hand back at once, with this USART still enabled — the state that + // leaves a bit-banged loader on its pins mute unless the loader + // releases it. Unconditional because there is no command wire to + // wait on: that loader's link is the pins, not this peripheral. + enter_loader(); + __builtin_unreachable(); +#else for (;;) { auto command = tx_t::read_blocking(); if (command == 'L') - reinterpret_cast(static_cast((avr::hw::db.mem.flash_size - slot) / 2))(); + enter_loader(); // 'D' leaves every word of the SPM page buffer dirty, so that a // following 'L' enters the loader with the buffer it never clears. if (command == 'D') { @@ -82,6 +99,7 @@ struct link { tx('D'); } } +#endif } }; @@ -109,8 +127,13 @@ struct link { int main() { avr::init::tx_t>(); +#if !defined(PUREBOOT_HANDOVER) link::tx('A'); link::tx('P'); link::tx('P'); +#endif + // The hand-over fixture stays silent: nothing is listening on the USART it + // brings up — the loader it hands to speaks those pins directly — so its + // banner would be a write into a peer that does not exist. link::idle(); } diff --git a/test/pbmute.py b/test/pbmute.py new file mode 100644 index 0000000..97121a4 --- /dev/null +++ b/test/pbmute.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""Hand-over with a USART left enabled on the loader's own pins. + +A software or autobaud link deployed on a USART's TxD is mute if an +application hands over with that USART still enabled: TXEN keeps the USART +owning the pin, so the bit-banged transmitter's port writes go nowhere and the +loader receives and obeys while answering nothing. The link's init releases it. + +The state is reached the way silicon reaches it — an application that sets up +its USART and jumps in with no reset between, so nothing clears UCSRnB for it. +The pin ownership itself is modelled by the device runner: simavr wires a +USART through IRQs alone and never takes the pin from the port, so without +that the mute could not happen here at all (test/pureboot_device.c). + +Usage: pbmute.py + +""" + +import os +import sys + + +def fail(message): + print(f"FAIL: {message}") + sys.exit(1) + + +def main(): + device_bin, elf, mcu, hz, base_hex, page, baud, app_bin, tool, workdir, link = sys.argv[1:] + page, baud = int(page), int(baud) + sys.path.insert(0, os.path.dirname(os.path.abspath(tool))) + sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + import pbsim + import pureboot as pb + + if "@" not in link: + fail(f"the link {link} names no owning USART — nothing would be under test") + + os.makedirs(workdir, exist_ok=True) + dump = os.path.join(workdir, "dump.bin") + + device = pbsim.Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump, link=link) + try: + port = pb.Port(device.pty, baud) + loader = pb.Loader(port) + loader.connect(25) + resident = loader.info.version + + # Install the fixture and let it take over. It brings up the USART + # that owns these pins and jumps straight back in. + pb.op_flash(loader, app_bin, erase=False, verify=True) + loader.run_application() + + # The loader is running again with that USART enabled behind it. Only + # the release makes it audible; without it the connect times out. + loader = pb.Loader(port) + try: + loader.connect(25) + except pb.Error as error: + fail(f"the loader never answered after the hand-over — the USART still owns its TX pin ({error})") + if loader.info.version != resident: + fail(f"identity changed across the hand-over: {resident} then {loader.info.version}") + + # Answering is not enough: it has to still be a working loader. + pb.verify_pages(loader, pb.plan_flash(open(app_bin, "rb").read(), loader.info)) + port.close() + finally: + device.stop() + print("pbmute: a loader on a USART's own pins answers after a hand-over that left it enabled") + + +if __name__ == "__main__": + main() diff --git a/test/pureboot_device.c b/test/pureboot_device.c index 143aa29..eb53862 100644 --- a/test/pureboot_device.c +++ b/test/pureboot_device.c @@ -10,7 +10,9 @@ // // The link follows the chip's natural default (USART0 on the megas, the // software UART on PB0/PB1 elsewhere) unless -l overrides it: `-l usart1` -// for the second instance, `-l sw:B5,B1` for a software build's RX,TX pins. +// for the second instance, `-l sw:B5,B1` for a software build's RX,TX pins, +// and `-l sw:D0,D1@0` where those pins are a USART's own — see the pin +// ownership the bridge models below. // // simavr's tiny cores decode the SPM opcode but attach no NVM module — SPM // is a silent no-op (the mega's boot section has one, avr_flash). The @@ -46,6 +48,7 @@ static int link_software; static char uart_digit = '0'; static char sw_rx_port = 'B', sw_tx_port = 'B'; static int sw_rx_bit = 0, sw_tx_bit = 1; +static char sw_tx_owner = 0; // the USART whose TXD the software link sits on static const char *dump_path; static uint32_t reset_pc; static volatile sig_atomic_t reset_requested; @@ -61,8 +64,12 @@ static int parse_link(const char *spec) link_software = 1; if (spec[2] == '\0') return 0; - if (sscanf(spec + 2, ":%c%d,%c%d", &sw_rx_port, &sw_rx_bit, &sw_tx_port, &sw_tx_bit) == 4) + char owner = 0; + int fields = sscanf(spec + 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; return 0; + } } return -1; } @@ -198,10 +205,50 @@ static avr_cycle_count_t tx_sample(avr_t *mcu, avr_cycle_count_t when, void *par return 0; } +// A USART owns its TxD pin whenever its transmitter is enabled, and the port +// register cannot drive it (§20.2 / Atmel-8271 §19.2) — which is why a +// bit-banged link deployed on those pins is mute until it clears UCSRnB. +// simavr wires a USART entirely through IRQs and never touches the port pin +// model, so the ownership does not exist there and the mute cannot happen: +// supply it, or the very state this models is untestable. The link spec's +// trailing @n names the USART; without one the pins are nobody's. +static avr_uart_t *tx_owner; + +static int tx_pin_taken(void) +{ + return tx_owner && avr_regbit_get(avr, tx_owner->txen); +} + +// simavr leaves TXEN set in UCSRnB out of reset, where silicon clears the +// whole register (§20.11.3) — which would hand the pin to a USART no code has +// enabled, making a freshly reset chip mute for reasons hardware does not +// have. Reset it the way the datasheet does, so the ownership starts from +// nobody's and only an application that really enables the USART takes it. +static void reset_tx_owner(void) +{ + if (tx_owner) + avr_regbit_clear(avr, tx_owner->txen); +} + +static void find_tx_owner(void) +{ + for (avr_io_t *io = avr->io_port; io; io = io->next) + if (io->kind && strcmp(io->kind, "uart") == 0 && ((avr_uart_t *)io)->name == sw_tx_owner) { + tx_owner = (avr_uart_t *)io; + reset_tx_owner(); + return; + } + fprintf(stderr, "device: no USART%c to own the software link's TX pin\n", sw_tx_owner); +} + static void tx_hook(avr_irq_t *irq, uint32_t value, void *param) { (void)irq; (void)param; + if (tx_pin_taken()) { // the USART holds the line; the port write goes nowhere + tx_level = 1; + return; + } int level = value & 1; if (!tx_active && tx_level == 1 && level == 0) { // start edge tx_active = 1; @@ -324,7 +371,8 @@ int main(int argc, char *argv[]) fprintf(stderr, "usage: %s [-l link] " " [reset_hex] [resume_flash]\n" - " -l link: usart0 | usart1 | sw[:B0,B1] (RX,TX); default: the chip's own\n" + " -l link: usart0 | usart1 | sw[:B0,B1[@0]] (RX,TX, then the USART owning\n" + " them); default: the chip's own\n" " reset_hex: reset vector (default: base with a boot section, else 0)\n" " resume_flash: raw full-flash image loaded instead of the ELF — a prior\n" " run's dump, for power-fail resume tests\n", @@ -410,6 +458,8 @@ int main(int argc, char *argv[]) printf("PB_PTY %s\n", uart_pty.pty.slavename); } else { bit_cycles = (avr->frequency + baud / 2) / baud; // matches uart.hpp's own rounding exactly + if (sw_tx_owner) + find_tx_owner(); rx_pin = avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ(sw_rx_port), (unsigned)sw_rx_bit); avr_irq_register_notify(avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ(sw_tx_port), (unsigned)sw_tx_bit), tx_hook, NULL); @@ -447,6 +497,7 @@ int main(int argc, char *argv[]) avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS(uart_digit), &flags); } else { bridge_reset(); + reset_tx_owner(); } } if (link_software && ++since_poll >= 2000) { diff --git a/test/test_handshake.py b/test/test_handshake.py index 68bdf9b..e045579 100644 --- a/test/test_handshake.py +++ b/test/test_handshake.py @@ -8,7 +8,8 @@ this, ~60 reboots/s of UART-reset garbage in which a stray 0x2b reads as a prompt — otherwise spins the tool forever. Regression for that hang, plus a control that a well-behaved loader still connects. -Stdlib only; run with `python test/test_handshake.py`. Not yet wired into ctest. +Stdlib only, no device: host-tool logic, so it runs on every chip's preset +beside pureboot.planner. """ import importlib.util import pathlib