pureboot v8: one-wire on every backend

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

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

View File

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