Commit Graph

8 Commits

Author SHA1 Message Date
7d046c3b89 pureboot: PI lint, tightened gates, and the self-update test suite
check_pi.py asserts the two link-time facts position independence rests on
(no absolute jmp/call; the info block within the image's first 256 bytes);
the size gates drop to 510 on the tinies for the trampoline word.

New per-chip tests beside the reworked protocol test: the planner units
(programming orders and their recovery properties, the surgery, staging
composition, boot-fuse decode, and the update preflight's error/warning
matrix over synthetic fuse bytes), the relocated-copy sweep (the identical
image installed one slot lower serves the full command set — the PI
acceptance test, and the one that caught the temporary-buffer trap), and
the self-update end-to-end: --update-loader to a re-timed build
(pureboot9, byte-different by PUREBOOT_TIMEOUT alone), then every
power-fail phase killed mid-write, restarted from the runner's flash dump,
and completed by a re-run with the application intact throughout. The mega
rounds run the BOOTRST-unprogrammed profile: the fixture application's 'L'
jump is the application-owned loader entry that profile relies on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 19:37:57 +02:00
f964b875b9 pureboot: one position-independent binary — its own staging loader
The image now runs from any 512-byte slot with every command intact:
control flow stays PC-relative, the write guard keys on the running slot
(the return-address anchor, computed once), the info block is addressed
from that same anchor as a byte pair (no absolute 16-bit address in the
image), and the application jump is an indirect call through a noipa-
laundered pointer to the absolute entry. 'J' — jump to a wire word
address, the one transfer primitive — replaces 'G': the host knows the
application entry from the info block, and moving between loader copies
needs arbitrary targets. The activation window is a compile-time 8 s
(PUREBOOT_TIMEOUT overrides), counted as a single calibrated poll loop.

A refused page no longer poisons the write-once temporary buffer (a real
silicon trap: the next write would program the drained data): every page
write discards the buffer first — CTPB on the tinies, on the mega the same
RWWSRE store that re-enables RWW after programming. The tinies' post-op
busy-waits go with it: their CPU halts through page erase and write.

488 / 502 / 504 B on t13a / t85 / mega — under the tinies' 510-byte budget,
whose last slot word is the host-managed trampoline: the resident's holds
the application entry, a staging copy's the jump through which an abandoned
update still times out into a loader.

The host tool updates the loader with itself: --update-loader installs the
identical image one slot below the resident, jumps into it, lets it rewrite
the resident, and restores the staging region from a state file — each
phase idempotent off the flash state, resumable after any interruption
(t13a: the staging slot carries the reset vector, written last in and
first out; t85: word 0 redirected around the resident rewrite; mega:
fuse-matrix preflight with a hard BOOTSZ gate and --assume-fuses for
simulators). Application flashing recovers by reset from any interruption:
patched page 0 and trampoline first, erase descending, and a walk-region
refusal behind --force on BOOTRST-below-loader megas.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 19:37:43 +02:00
6594c1b044 pureboot: harden the sim device runner
Cancel the GPIO bridge's cycle timers with the state they drive: avr_reset
drops the TX latch, whose falling edge starts a spurious decode before
bridge_reset runs, and the stale sampler then interleaves with the loader's
first real answer through the shared shift state — the first post-reset
replies came back corrupted and the knock retries burned the activation
window into the application.

Wrap the mega's registered flash ioctl to re-dispatch page erases with Z
masked to the page boundary: simavr's PGERS handler erases spm_pagesize
bytes from Z & ~1 (its PGWRT path masks correctly), wiping the neighbouring
page when Z sits past the page start, which hardware permits (§26.8.1).
Model the write-once temporary buffer in the tiny NVM module — silicon
refuses a second load per word until the buffer clears, and a last-write-
wins model masks real firmware bugs.

Optional arguments select the reset vector (the mega's fuse profiles) and a
raw flash image to resume from (power-fail tests re-enter a dumped state).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 19:37:10 +02:00
cbe6acff13 pureboot: review-pass fixes to the host tool and device runner
pureboot.py: reject an empty image file with a clear error instead of
an IndexError deep in the vector-surgery planner; tighten the erase
docstring (order is irrelevant there — every target byte is the same
value, unlike a real flash where page 0 must go last).

pureboot_device.c: the GPIO bridge's bit_cycles used plain truncating
division where the firmware computes its own bit period with
round-to-nearest (uart.hpp: (Clock.hz + Baud.bd/2)/Baud.bd) — one
cycle off per bit on both tinies, harmless in practice but needless
drift against a firmware built to a different constant. Matched
exactly. Also clear the queued-bytes/decode-in-progress bridge state
on the test-only reset signal, so a future reset-mid-transfer scenario
can't feed a freshly reset chip bytes queued for its previous life.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 10:45:19 +02:00
2133627057 pureboot: host tool and end-to-end protocol tests, all three chips
pureboot.py (Python stdlib only): images as raw binary or Intel HEX,
flash and EEPROM programming with read-back verify, erase composites,
fuse and info readout, activation-timeout configuration, and the
tinies' reset-vector surgery — the trampoline word below the loader,
page 0 written last.

The test spawns a simavr device (pureboot_device.c) — the mega's USART
as a pty; on the tinies a cycle-timed GPIO<->pty bridge for the polled
software UART plus the NVM module simavr's tiny cores lack (their SPM
opcode ioctls into a void and silently does nothing) — and drives it
with the real tool: knock from reset (erased-flash walk on the tinies),
program and verify both memories, timeout write, session reconnect, an
external reset through the patched vector, hand-over, and the fixture
application's banner. Results are cross-checked against ground-truth
memory dumps and an independent decode of the surgery's rjmp words,
red-verified against a sabotaged encoder.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 05:54:15 +02:00
beff7bb3ee tsb: protocol test covers the password gate and emergency erase
Each scenario group now runs on its own freshly-reset device: the round-trip
on a blank config page, plus a password-config device that must be sent the
password after the knock to activate, and an emergency-erase device where a
0-byte + two confirms wipes flash, EEPROM and the config page (verified by
reading all three back as 0xff). All three tiers pass every group.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 16:49:20 +02:00
3ea8957e69 tsb: asm tier reaches full oracle feature parity at 502 B
Rewrite the inline-asm tier so it matches the hand-written fixed-baud oracle's
feature set inside the 512 B boot section: watchdog-reset bail, one-wire
half-duplex (RXEN/TXEN toggled per direction, TX turnaround guard),
config-page activation timeout, the password gate (wrong byte hangs draining
the UART), emergency erase (password \0 + double-confirm wipes flash, EEPROM
and the config page), and config/flash/EEPROM read-write. Every geometry,
baud and info-block constant comes from libavr consteval; only the dense
control flow is hand-written. 502 B, byte-identical across generated and
reflect modes.

Test harness: seed the config page from TSB_CONFIG so the password and
emergency-erase paths are exercisable, and clear simavr's AVR_UART_FLAG_POLL_
SLEEP — a host-CPU-saving usleep(1)-per-idle-poll hack that models no hardware
and paces a one-wire loader (which releases TX between bytes) in real time,
distorting protocol timing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 16:23:16 +02:00
64c1e484b5 tsb: reimplement TinySafeBoot on libavr in three size tiers
The native-UART fixed-baud TinySafeBoot protocol, ported onto libavr as a
crt-free boot-section loader, in three variants that trade clarity for size:

  tsb_pure   740 B  idiomatic C++: SRAM page buffer, separate flash/EEPROM
                    leaves, shared framing; the polled `unused` guard posture.
  tsb_tricks 658 B  unified runtime-flag paths (noinline/noclone), call-saved
                    global-register page walk — attributes only, no asm.
  tsb_asm    508 B  streaming store + hand-rolled UART/SPM/EEPROM/erase loops;
                    fits the 512 B boot section (BOOTSZ=11). Trims the optional
                    password gate and WDT-reset bail — unreachable in C++ with
                    both (hand-asm is ~15 % denser). Tiers 1-2 keep them and
                    live in the 1 KB section they fit.

All three are .text byte-identical across libavr's generated and reflect modes.
The CMake build strips the leaked -O3 (a Release build is silently -O3, not the
-Os this loader is measured against) and gates each variant's size against its
section. A simavr harness (test/device.c + test/tsbtest.py) drives the real wire
protocol over a pty and flashes the device; the size and protocol tests run in
ctest. Verified byte-for-byte against the reference tsbloader_adv (C#/mono):
activate, read info, flash write + verify.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 05:00:51 +02:00