Commit Graph

13 Commits

Author SHA1 Message Date
3e4bfbaf48 pbhw: the marker check assumed a board whose port-open is not a reset
Its comment said "opening the port does not reset a board whose DTR is
unwired, so this simply listens" — true of the tiny it was written against,
false of an Arduino, and this is the generic harness. Where DTR is wired to
reset, that open resets the part and the activation window comes first, so a
fixture emitting its banner once says it on the far side of a wait the suite
cannot know the length of: the window is a compile-time constant and nothing
on the wire reports it. The suite read the silence as an application that
never ran, on a board where it demonstrably had.

So --marker-wait, defaulting to the 2.5 s that was hardcoded, and a failure
that names the window as the candidate rather than leaving the next person to
suspect the loader. The other half is the fixture: PUREBOOT_HEARTBEAT makes
the observation independent of when the listener arrives, which is what the
rig's own builds now pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 17:12:08 +02:00
5d520a1ff9 pbhw: --one-wire never reached the suite's own sessions
The flag was plumbed through pbrig.Deployment to the host-tool subprocess
calls and nowhere else, so identity() and scan() opened a raw port and drove
a shared line as though it were two wires. On real one-wire hardware the
adapter's echo answers the knock before the device does, so the suite would
have died at its very first check — "the loader never answered; nothing below
can be trusted" — for the one deployment the flag exists to test, and every
result after it is gated on that check passing.

Both now open through pbrig.Rig.open_port(), which applies the deployment's
link mode. The gap underneath was that only the subprocess path could reach
those facts at all; anything driving the protocol in-process had to restate
them, and did not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 13:25:46 +02:00
f71d76a815 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>
2026-07-31 02:32:11 +02:00
a743ea64a3 tool: a size collision across build trees is an error, not a coin toss
sizes.py merged every owned tree's rows and let the last one win, so a
stale reflect tree — last built before the window constants moved —
reported the atmega8's old stock size over the fresh build and failed
the README check with yesterday's number. Generated and reflect must
answer with the same bytes (the identity invariant), so the same target
measuring two sizes is a stale tree or an identity breach; collect()
refuses now, naming both trees. The stale reflect trees are removed —
the reflect sweep rebuilds them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 16:32:35 +02:00
8e7cc86fb3 pureboot: the activation window gets a behavioral gate, and honest per-poll constants under it
The window's per-poll cycle counts were hand-counted for a uint32_t
countdown, but every default window fits uint24_t, whose decrement chain
is one sbci shorter — so deployed loaders ran 9/10ths of their stated
seconds (a 328P's 8 s was 7.2 s on the wire). No golden-asm pin can hold
this: the loops compile in consumer context. pbwindow.py measures the
behavior instead: it installs a real application beside the loader
through the host tool's own plan_flash (surgery included), starts the
simulator with the line idle, and reads the cycle of the first transmit
— the application's banner, so that cycle is the window. Held at plus or
minus 2 percent per chip (pureboot.window), red at -10.0 percent against
the old constants, green with poll_cycles now counted for the narrow
countdown (hardware 9, software 7; window_polls() solves narrow-first
and adds the wide loop's cycle where the count forces uint32_t — a count
narrow only at the wide cost stays wide, so the choice cannot
oscillate). The autobaud window is its poll budget at the measured ten
cycles a poll, gated the same way (pureboot.window.autobaud), and the
README carries that arithmetic now. No version bump: timing-window
precision is not meaningful behavior, v7 stays.

The gate flushed out two runner gaps. The software bridge accepted any
falling edge as a start bit, so the device's own TX-init glitch decoded
as a stray byte; it re-samples mid-bit now and abandons a false start,
as silicon does. And after avr_reset, the idle-line re-raise was
silently dropped: ioport pin irqs are IRQ_FLAG_FILTERED and the irq's
cached value survives the reset the port latch does not, so the device
read the line stuck low, calibrate() measured reset-to-first-edge as one
wrapping pulse, and the first knock after a reset could boot the
application instead of locking — the intermittent autobaud failure.
bridge_reset forces a real transition (0 then 1, no cycles between).

The README's Autobaud column now carries each chip's worst
configuration — autobaud with OSCCAL baked, on a USART's own pins where
the chip has one (tinies: autobaud + OSCCAL) — the numbers the existing
pureboot_autobaud_osccal[_on_usart0] matrix points already gate;
sizes.py checks the column against exactly those targets. Tool sizes
and window prose updated with it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 16:05:42 +02:00
c535c4756c pureboot: the activation countdown in the narrowest type that holds it
The fixed-baud window counted down a uint32 where almost every window fits
24 bits; the countdown now takes avr::uint24_t when the poll budget allows
(the autobaud budget's own choice), uint32 past 16.7M polls — four bytes
off every fixed-baud image on every chip, the full suites green on the
changed window. The README size table is refreshed — its autobaud column
had also gone stale by the no-assembly pass's measurement-loop win, which
nothing gated: sizes.py check-readme now runs as the gate's final stage,
where every tree is freshly built and the table can actually be held.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 14:47:29 +02:00
b3f41caf6e audit: round three on the port — the hardware scan check fails soft
A rig hiccup mid-walk records the scan check as failed and lets the suite
continue, matching its siblings' envelope; a nonexistent --port path
reports as an error instead of a traceback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 10:31:32 +02:00
7716e1e291 audit: the port's pass — the scan that could not walk, and the drift a generator ends
--scan's walk was unwalkable on POSIX: probe rates have no termios
B-constant, so the first off-nominal probe raised out of the loop. The port
speaks termios2 BOTHER now (red-proven on a pty at 9984 Bd), the probe's
open lives inside the walk's error handling, an fd no longer leaks on an
unmakeable rate, and the swallowed unknown-signature reply is named at
timeout instead of reported as silence. CMakePresets.json's generator emits
the submodule toolchain path it had drifted from — a hand edit on a
generated file, exactly the class rule 10 exists for — and presets.generated
gates the pair from here on (the  marker CMake rejects at the
presets root stayed out; the check is the guard). The over-slot image guard
the tsb runner gained reaches the pureboot runner too; the GPIO bridge's
delivery comment states the hardware truth (RXC at the stop bit's sampling
point); the hardware suite gains the scan check — the one place the rate
physics is real; and the libavr pin advances over both audit rounds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 10:12:17 +02:00
fe0d9f8790 build: the submodule is how libavr arrives; the era already carries it
The pin the whole history now encodes becomes the primary route: the
submodule default replaces FetchContent and the unpinned forge fallback,
LIBAVR_ROOT stays as the tandem-development override, the presets already
take the toolchain file from the submodule, and the Studio projects anchor
their include path there — correct by construction. The version tags and
the one-command historical build are documented beside the version map.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 00:28:49 +02:00
9a8a5b0082 tools: measure the images, and hold the README to what is built
The size matrix proves every image fits its slot and says nothing about the
numbers the README prints. Those drift silently: caller_page() took eight bytes
off every build at once, so all fifteen rows went stale together and no test
noticed, because nothing was over budget. sizes.py check-readme compares the
table against the built images; sizes.py max reports the largest image per chip
and anything over its slot.

It is a check rather than a generator, so the table stays prose someone can
write. No chip geometry lives here either: the image/budget pairs come out of
each build's own CTestTestfile.cmake, which is what the gate checks, so a chip
added or a budget changed needs no edit. Only trees a preset still owns are
read — a stale directory answers with a size that was true once.

16243 images across 37 chips today, none over budget, tightest tsb_asm at 510
of 512.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 16:45:20 +02:00
433bec3e58 tools: a hardware harness, so a board can be proven and not just a protocol
check.sh proves the protocol under simavr on every chip; it cannot prove a
board. Two things live only on silicon — an RC oscillator that is not on its
nominal, and a reset edge that has to come from somewhere — and until now the
scripts that reached them were per-session scratch on the machine holding the
programmer, which is where the ATtiny13A run's findings nearly stayed.

pbrig.py is the primitives, knowing nothing per-board: every deployment fact is
a flag or a PUREBOOT_* variable. Two rig facts are encoded in it because neither
is guessable and each cost a session to learn: an ISP access *is* the reset edge
where the adapter's DTR is unwired, so a session begins with an ISP touch and
knocks immediately after; and avrdude splits -U on colons, so a Windows drive
letter breaks the spec and every file goes as a bare name with avrdude run in
its own directory. Its `rate` subcommand is the one that turns "the loader is
silent, so the wiring must be wrong" into a number, by sweeping the host rate
against a fixed cycles-per-bit transmitter — PUREBOOT_HEARTBEAT makes the
existing fixture into one, software link only, since the hardware-link idle owes
the self-update tests its command loop.

pbhw.py takes every bound from the info block the loader reports, so one run
covers a 1 KiB tiny and a 128 KiB mega alike. Both are exercised on an ATtiny13A:
backup verified against a known-good capture, the clock measured at 9.048 MHz
against a 9.6 MHz nominal, and the suite 11/11.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 16:06:15 +02:00
8470ce3da0 test: the exhaustive clock x baud x backend size matrix
Every plausible oscillator against every rate it reaches against every
backend, on one chip per size-bearing class, under --full only. The baud
ladder becomes a reachability predicate the enumeration filters on, so an
unreachable point drops out instead of aborting the configure.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 23:32:28 +02:00
dd7490a3df build: generated presets and a check entry point
tools/make_presets.py emits the uniform pipeline the hand-grown file had
drifted from — generated configure/build/test presets and workflows for
all 37 chips, reflect configure/build for libavr's 12-chip spot set —
and tools/check.sh runs every chip's workflow (--full adds the reflect
spot) as the port's gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 23:42:57 +02:00