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>
This commit is contained in:
@@ -48,12 +48,14 @@ it carries the calibration machinery and no clock at all.
|
||||
is 510 bytes, since the slot's last word is the trampoline.
|
||||
|
||||
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.
|
||||
USART's own pins with the `OSCCAL` trim baked, 510 of its 512 — they alone
|
||||
carry the far-flash machinery (ELPM reads, RAMPZ page commands), autobaud
|
||||
alone carries the calibration loop, a bit-banged link on a USART's pins alone
|
||||
has to release it (below), and the trim adds its one register write. Without
|
||||
the trim that build is 504; on the default pins, 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.
|
||||
@@ -123,12 +125,13 @@ window per reset. Measure with an application in place.
|
||||
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
|
||||
software UART on hand-picked pins, say:
|
||||
software UART on hand-picked pins, say. A submodule pins the loader version
|
||||
(the tags name them; this repo pins its own libavr the same way), where
|
||||
FetchContent tracks whatever `main` is:
|
||||
|
||||
```cmake
|
||||
FetchContent_Declare(bootloader GIT_REPOSITORY git@git.blackmark.me:avr/bootloader.git GIT_TAG main)
|
||||
FetchContent_MakeAvailable(bootloader)
|
||||
add_subdirectory(${bootloader_SOURCE_DIR}/pureboot pureboot)
|
||||
# git submodule add <forge>/avr/bootloader.git bootloader — or FetchContent
|
||||
add_subdirectory(bootloader/pureboot pureboot)
|
||||
|
||||
pureboot_add_loader(myboot CLOCK 1000000 SERIAL software TX pb1 RX pb5)
|
||||
```
|
||||
@@ -512,7 +515,12 @@ Per chip preset, `ctest` runs:
|
||||
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;
|
||||
below sweeps. `pureboot*osccal*.size` add the `OSCCAL` trim on the stock
|
||||
shape and on the tightest image in the space (autobaud on a USART's own
|
||||
pins), holding both of the trim write's addressing encodings to the budget;
|
||||
- `pureboot_autobaud.unit` — the measured bit period is the loader's only RAM
|
||||
object and sits exactly at ram_start, where `--info` reads it: wire
|
||||
contract, not layout accident;
|
||||
- `pbm_*.size` — with `PUREBOOT_FULL_MATRIX=1`, the exhaustive cross product
|
||||
replacing that compact matrix, on **every** chip: every plausible oscillator
|
||||
(the internal ones, the CKDIV8 floor, the plain and the UART crystals) ×
|
||||
@@ -535,6 +543,13 @@ Per chip preset, `ctest` runs:
|
||||
recovery properties, the surgery, the staging composition, the boot-fuse
|
||||
decode, the update preflight over synthetic fuse bytes, and the repairing
|
||||
verify against a fake device;
|
||||
- `pureboot.scan` — `--scan`'s walk and report logic: the probe order, the
|
||||
rate arithmetic, and the trim advice's direction. A pty carries bytes at
|
||||
any termios rate, so the rate physics itself belongs to the hardware
|
||||
harness, and what the wire would arbitrate is pinned as logic;
|
||||
- `presets.generated` — CMakePresets.json matches its generator
|
||||
(`tools/make_presets.py --check`), so a hand edit or a generator change
|
||||
cannot drift the pair apart;
|
||||
- `pureboot.protocol` — end to end against a simavr device
|
||||
(`test/pureboot_device.c`: a hardware USART as a pty, or a cycle-timed
|
||||
GPIO⇄pty bridge for a software-UART build, plus the SPM/NVM module simavr's
|
||||
@@ -567,15 +582,21 @@ Per chip preset, `ctest` runs:
|
||||
- `pureboot.update` — the full `--update-loader` flow, then every power-fail
|
||||
phase: the device is killed mid-write, restarted from its flash dump, and a
|
||||
re-run must complete the update with the application intact;
|
||||
- `pureboot.osccal` (328P, t85) — a loader built with the `OSCCAL` axis holds
|
||||
the trim register at the built byte from its first prompt, observed through
|
||||
the wire on one chip per addressing encoding (`sts` and low-I/O `out`);
|
||||
- `pureboot.autobaud` (328P, 1284P) — the clock-free build over the GPIO⇄pty
|
||||
bridge: the calibration handshake, a flash + EEPROM + fuse round trip against
|
||||
the simulator's own memory, a data-space round trip, the hand-over — then the
|
||||
same binary again at double the clock, which is the property the backend
|
||||
exists for. A lone calibration pulse with no knock behind it must still let
|
||||
exists for. The measured clock `--info` prints is asserted against the
|
||||
simulator's exact clock, inside the unit encoding's own envelope, at both
|
||||
points. 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`, `planner` and `handshake` are host logic and run anywhere; the
|
||||
simulator-driven targets need simavr and a pty, so they are POSIX-only.
|
||||
`size`, `unit`, `pi`, `planner`, `scan` and `handshake` are host logic and run
|
||||
anywhere; the simulator-driven targets need simavr and a pty, so they are
|
||||
POSIX-only.
|
||||
|
||||
## Hardware
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ if os.name == "nt":
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
else:
|
||||
import array
|
||||
import fcntl
|
||||
import select
|
||||
import termios
|
||||
|
||||
@@ -158,36 +160,60 @@ class Progress:
|
||||
|
||||
|
||||
class PosixPort:
|
||||
"""A raw serial port with deadline-based reads, over termios."""
|
||||
"""A raw serial port with deadline-based reads, over termios. A rate with
|
||||
no B-constant — the off-nominal probes `--scan` walks — goes through
|
||||
Linux's termios2 BOTHER; a platform without that ioctl refuses the rate
|
||||
by name."""
|
||||
|
||||
# The termios2 ioctl pair and cflag bits, and the struct's ispeed/ospeed
|
||||
# word offsets: four flag words, then a line-discipline byte and 19
|
||||
# control chars padded to word 9 (include/uapi/asm-generic/termbits.h).
|
||||
_TCGETS2, _TCSETS2 = 0x802C542A, 0x402C542B
|
||||
_BOTHER, _CBAUD = 0o010000, 0o010017
|
||||
_ISPEED, _OSPEED = 9, 10
|
||||
|
||||
@staticmethod
|
||||
def _speed(baud):
|
||||
return getattr(termios, f"B{baud}", None)
|
||||
|
||||
def _set_arbitrary(self, baud):
|
||||
buf = array.array("i", [0] * (self._OSPEED + 1))
|
||||
try:
|
||||
return getattr(termios, f"B{baud}")
|
||||
except AttributeError:
|
||||
raise Error(f"unsupported baud rate {baud}") from None
|
||||
fcntl.ioctl(self.fd, self._TCGETS2, buf, True)
|
||||
buf[2] = (buf[2] & ~self._CBAUD) | self._BOTHER
|
||||
buf[self._ISPEED] = buf[self._OSPEED] = baud
|
||||
fcntl.ioctl(self.fd, self._TCSETS2, buf)
|
||||
except OSError:
|
||||
raise Error(f"this platform cannot set {baud} Bd (no termios2)") from None
|
||||
|
||||
def _apply_baud(self, attrs, baud):
|
||||
speed = self._speed(baud)
|
||||
attrs[4] = attrs[5] = speed if speed is not None else termios.B38400
|
||||
termios.tcsetattr(self.fd, termios.TCSANOW, attrs)
|
||||
if speed is None:
|
||||
self._set_arbitrary(baud)
|
||||
self.baud = baud
|
||||
|
||||
def __init__(self, path, baud):
|
||||
self.fd = os.open(path, os.O_RDWR | os.O_NOCTTY)
|
||||
attrs = termios.tcgetattr(self.fd)
|
||||
attrs[0] = 0 # iflag
|
||||
attrs[1] = 0 # oflag
|
||||
attrs[2] = termios.CREAD | termios.CLOCAL | termios.CS8 # cflag
|
||||
attrs[3] = 0 # lflag
|
||||
attrs[4] = attrs[5] = self._speed(baud)
|
||||
attrs[6][termios.VMIN] = 0
|
||||
attrs[6][termios.VTIME] = 0
|
||||
termios.tcsetattr(self.fd, termios.TCSANOW, attrs)
|
||||
self.baud = baud
|
||||
try:
|
||||
attrs = termios.tcgetattr(self.fd)
|
||||
attrs[0] = 0 # iflag
|
||||
attrs[1] = 0 # oflag
|
||||
attrs[2] = termios.CREAD | termios.CLOCAL | termios.CS8 # cflag
|
||||
attrs[3] = 0 # lflag
|
||||
attrs[6][termios.VMIN] = 0
|
||||
attrs[6][termios.VTIME] = 0
|
||||
self._apply_baud(attrs, baud)
|
||||
except BaseException:
|
||||
os.close(self.fd)
|
||||
raise
|
||||
|
||||
def set_baud(self, baud):
|
||||
"""Retune the port without closing it — the fd stays open, so no DTR
|
||||
pulse and no reset. That matters: the only caller is mid-session with a
|
||||
loader copy that a reset would throw away."""
|
||||
attrs = termios.tcgetattr(self.fd)
|
||||
attrs[4] = attrs[5] = self._speed(baud)
|
||||
termios.tcsetattr(self.fd, termios.TCSANOW, attrs)
|
||||
self.baud = baud
|
||||
self._apply_baud(termios.tcgetattr(self.fd), baud)
|
||||
|
||||
def close(self):
|
||||
os.close(self.fd)
|
||||
@@ -531,6 +557,7 @@ class Loader:
|
||||
drain absorbs whatever they produced."""
|
||||
deadline = time.monotonic() + wait
|
||||
knocks = 0
|
||||
refusal = None
|
||||
while True:
|
||||
self.port.flush_input()
|
||||
self.port.write(knock)
|
||||
@@ -552,12 +579,18 @@ class Loader:
|
||||
except Error as failed:
|
||||
if "pureboot" in str(failed):
|
||||
raise
|
||||
# A malformed or unknown identity is retried as noise, but
|
||||
# it was an answer: if nothing better ever arrives, naming
|
||||
# it beats reporting silence.
|
||||
refusal = failed
|
||||
self.info = None
|
||||
if self.info is not None:
|
||||
self._expect_prompt()
|
||||
verbose(f"loader answered {what} {knocks}; identity read")
|
||||
return self.info
|
||||
if time.monotonic() > deadline:
|
||||
if refusal is not None:
|
||||
raise Error(f"no usable answer — the last identity reply failed: {refusal}")
|
||||
raise Error("no answer — reset the device within its activation window")
|
||||
|
||||
def connect(self, wait):
|
||||
@@ -1408,7 +1441,11 @@ def op_scan(port_path, baud, wait, clock=None):
|
||||
for pct in scan_ratios():
|
||||
rate = scan_rate(baud, pct)
|
||||
print(f"scan: {rate} Bd ({pct:+d} %) — reset the target", flush=True)
|
||||
port = Port(port_path, rate)
|
||||
try:
|
||||
port = Port(port_path, rate)
|
||||
except Error as unmakeable:
|
||||
print(f"scan: {rate} Bd skipped — {unmakeable}")
|
||||
continue
|
||||
try:
|
||||
info = Loader(port).connect(wait)
|
||||
except Error:
|
||||
@@ -1504,11 +1541,13 @@ def main():
|
||||
print("device:")
|
||||
for line in info.lines():
|
||||
print(f" {line}")
|
||||
if args.autobaud:
|
||||
if args.autobaud and info.ram is not None:
|
||||
# The whole of the loader's RAM is the measured bit period at
|
||||
# ram_start; decoded and times the rate this session drives,
|
||||
# that is the true clock — the number to hold an OSCCAL bake
|
||||
# or a fixed-baud build against (README.md: deployment).
|
||||
# or a fixed-baud build against (README.md: deployment). The
|
||||
# autobaud identity path refuses unknown signatures, so ram is
|
||||
# always known here; the guard states that dependency.
|
||||
unit = int.from_bytes(loader.read_ram(info.ram, 2), "little")
|
||||
cycles = unit * UNIT_LOOP_CYCLES + UNIT_DISCOUNT
|
||||
clock = cycles * args.baud
|
||||
|
||||
Reference in New Issue
Block a user