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:
@@ -66,16 +66,18 @@ function(add_image_outputs name)
|
|||||||
$<TARGET_FILE:${name}> $<TARGET_FILE:${name}>.bin)
|
$<TARGET_FILE:${name}> $<TARGET_FILE:${name}>.bin)
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
# The TinySafeBoot protocol reimplemented on libavr in three variants that trade
|
# The TinySafeBoot protocol reimplemented on libavr in variants that trade
|
||||||
# clarity for size. Each links into the ATmega328P boot section (BOOTSZ selects
|
# clarity for size. Each links into the ATmega328P boot section (BOOTSZ selects
|
||||||
# its size; BOOTRST vectors a reset to its base) with -nostartfiles — a polled
|
# its size; BOOTRST vectors a reset to its base) with -nostartfiles — a polled
|
||||||
# loader has no use for the crt or the vector table. The naked entry sits in
|
# loader has no use for the crt or the vector table. The entry sits in
|
||||||
# .vectors, laid first, and runs. The boot base is FLASHEND+1 minus the section
|
# .vectors, laid first, and runs — avr::startup::entry on the policy tier,
|
||||||
# size; the linker section-start and the source's boot_bytes agree. tsb_app is
|
# the experiment tiers' own naked stubs elsewhere, each documented in its
|
||||||
|
# source. The boot base is FLASHEND+1 minus the section size; the linker
|
||||||
|
# section-start and the source's boot_bytes agree. tsb_app is
|
||||||
# the application's reset vector, pinned to 0 here so the loaders jump to a
|
# the application's reset vector, pinned to 0 here so the loaders jump to a
|
||||||
# named function; --pmem-wrap-around lets relaxation turn that absolute jump
|
# named function; --pmem-wrap-around lets relaxation turn that absolute jump
|
||||||
# into the wrapped rjmp AVR's modulo-flash PC actually executes.
|
# into the wrapped rjmp AVR's modulo-flash PC actually executes.
|
||||||
# All three implement the full oracle feature set (see oracle/README.md):
|
# All four implement the full oracle feature set (see oracle/README.md):
|
||||||
# watchdog bail, one-wire half-duplex, config-page activation timeout, password
|
# watchdog bail, one-wire half-duplex, config-page activation timeout, password
|
||||||
# gate, emergency erase, config/flash/EEPROM read-write. They differ only in how,
|
# gate, emergency erase, config/flash/EEPROM read-write. They differ only in how,
|
||||||
# and the size gradient is the cost of that "how" — see dev/lessons.md.
|
# and the size gradient is the cost of that "how" — see dev/lessons.md.
|
||||||
@@ -170,6 +172,11 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
add_test(NAME pureboot.scan
|
add_test(NAME pureboot.scan
|
||||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/test_scan.py
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/test_scan.py
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py)
|
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py)
|
||||||
|
# CMakePresets.json is generated; hand edits drift the moment the
|
||||||
|
# generator reruns, so the gate holds the pair together.
|
||||||
|
add_test(NAME presets.generated
|
||||||
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/make_presets.py
|
||||||
|
--check)
|
||||||
add_test(NAME pureboot.handshake
|
add_test(NAME pureboot.handshake
|
||||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/test_handshake.py)
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/test_handshake.py)
|
||||||
add_test(NAME pureboot.updatelink
|
add_test(NAME pureboot.updatelink
|
||||||
@@ -509,9 +516,9 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
set_tests_properties(pureboot.usart1 PROPERTIES TIMEOUT 180)
|
set_tests_properties(pureboot.usart1 PROPERTIES TIMEOUT 180)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# The autobaud variants driven end to end over the software-UART bridge (both
|
# The autobaud loader driven end to end over the software-UART bridge:
|
||||||
# under review — pureboot/autobaud.md): the host sends the 0xC0 calibration
|
# the host sends the 0xC0 calibration pulse, the loader times it, locks,
|
||||||
# pulse, the loader times it, locks, and programs. Run on the near-flash 328P
|
# and programs. Run on the near-flash 328P
|
||||||
# and the word-addressed 1284P — the two flash-addressing classes — and each
|
# and the word-addressed 1284P — the two flash-addressing classes — and each
|
||||||
# at two clocks with the one binary, which is the clock-agnostic property
|
# at two clocks with the one binary, which is the clock-agnostic property
|
||||||
# autobaud exists for (test/pbautobaud.py). The fixture application banners
|
# autobaud exists for (test/pbautobaud.py). The fixture application banners
|
||||||
|
|||||||
2
libavr
2
libavr
Submodule libavr updated: 43b1f34ed1...d952c7e317
@@ -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.
|
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
|
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
|
USART's own pins with the `OSCCAL` trim baked, 510 of its 512 — they alone
|
||||||
(ELPM reads, RAMPZ page commands), autobaud alone carries the calibration loop,
|
carry the far-flash machinery (ELPM reads, RAMPZ page commands), autobaud
|
||||||
and a bit-banged link on a USART's pins alone has to release it (below). The
|
alone carries the calibration loop, a bit-banged link on a USART's pins alone
|
||||||
same build on the default pins is 502. The flash bank riding in a transfer's
|
has to release it (below), and the trim adds its one register write. Without
|
||||||
selector byte keeps even those chips' addressing the same 16-bit form every
|
the trim that build is 504; on the default pins, 502. The flash bank riding
|
||||||
other chip uses, which is why they are no longer the outlier they were.
|
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
|
The software UART enables the RX pull-up; TX idles high. All multi-byte wire
|
||||||
quantities are little-endian.
|
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
|
A downstream project brings its usual libavr setup (the `libavr` target, the
|
||||||
chip via the `LIBAVR_MCU` toolchain preset), consumes this directory, and
|
chip via the `LIBAVR_MCU` toolchain preset), consumes this directory, and
|
||||||
states its deployment — an ATmega328P on its shipped 1 MHz fuses with the
|
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
|
```cmake
|
||||||
FetchContent_Declare(bootloader GIT_REPOSITORY git@git.blackmark.me:avr/bootloader.git GIT_TAG main)
|
# git submodule add <forge>/avr/bootloader.git bootloader — or FetchContent
|
||||||
FetchContent_MakeAvailable(bootloader)
|
add_subdirectory(bootloader/pureboot pureboot)
|
||||||
add_subdirectory(${bootloader_SOURCE_DIR}/pureboot pureboot)
|
|
||||||
|
|
||||||
pureboot_add_loader(myboot CLOCK 1000000 SERIAL software TX pb1 RX pb5)
|
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;
|
too. The timeout is a constant and is no axis;
|
||||||
- `pureboot_autobaud.size` — the clock-free build, which has no clock or baud
|
- `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
|
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
|
- `pbm_*.size` — with `PUREBOOT_FULL_MATRIX=1`, the exhaustive cross product
|
||||||
replacing that compact matrix, on **every** chip: every plausible oscillator
|
replacing that compact matrix, on **every** chip: every plausible oscillator
|
||||||
(the internal ones, the CKDIV8 floor, the plain and the UART crystals) ×
|
(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
|
recovery properties, the surgery, the staging composition, the boot-fuse
|
||||||
decode, the update preflight over synthetic fuse bytes, and the repairing
|
decode, the update preflight over synthetic fuse bytes, and the repairing
|
||||||
verify against a fake device;
|
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
|
- `pureboot.protocol` — end to end against a simavr device
|
||||||
(`test/pureboot_device.c`: a hardware USART as a pty, or a cycle-timed
|
(`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
|
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
|
- `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
|
phase: the device is killed mid-write, restarted from its flash dump, and a
|
||||||
re-run must complete the update with the application intact;
|
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
|
- `pureboot.autobaud` (328P, 1284P) — the clock-free build over the GPIO⇄pty
|
||||||
bridge: the calibration handshake, a flash + EEPROM + fuse round trip against
|
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
|
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
|
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.
|
the application boot, so no wait in activation can be unbounded.
|
||||||
|
|
||||||
`size`, `pi`, `planner` and `handshake` are host logic and run anywhere; the
|
`size`, `unit`, `pi`, `planner`, `scan` and `handshake` are host logic and run
|
||||||
simulator-driven targets need simavr and a pty, so they are POSIX-only.
|
anywhere; the simulator-driven targets need simavr and a pty, so they are
|
||||||
|
POSIX-only.
|
||||||
|
|
||||||
## Hardware
|
## Hardware
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ if os.name == "nt":
|
|||||||
import ctypes
|
import ctypes
|
||||||
from ctypes import wintypes
|
from ctypes import wintypes
|
||||||
else:
|
else:
|
||||||
|
import array
|
||||||
|
import fcntl
|
||||||
import select
|
import select
|
||||||
import termios
|
import termios
|
||||||
|
|
||||||
@@ -158,36 +160,60 @@ class Progress:
|
|||||||
|
|
||||||
|
|
||||||
class PosixPort:
|
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
|
@staticmethod
|
||||||
def _speed(baud):
|
def _speed(baud):
|
||||||
|
return getattr(termios, f"B{baud}", None)
|
||||||
|
|
||||||
|
def _set_arbitrary(self, baud):
|
||||||
|
buf = array.array("i", [0] * (self._OSPEED + 1))
|
||||||
try:
|
try:
|
||||||
return getattr(termios, f"B{baud}")
|
fcntl.ioctl(self.fd, self._TCGETS2, buf, True)
|
||||||
except AttributeError:
|
buf[2] = (buf[2] & ~self._CBAUD) | self._BOTHER
|
||||||
raise Error(f"unsupported baud rate {baud}") from None
|
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):
|
def __init__(self, path, baud):
|
||||||
self.fd = os.open(path, os.O_RDWR | os.O_NOCTTY)
|
self.fd = os.open(path, os.O_RDWR | os.O_NOCTTY)
|
||||||
attrs = termios.tcgetattr(self.fd)
|
try:
|
||||||
attrs[0] = 0 # iflag
|
attrs = termios.tcgetattr(self.fd)
|
||||||
attrs[1] = 0 # oflag
|
attrs[0] = 0 # iflag
|
||||||
attrs[2] = termios.CREAD | termios.CLOCAL | termios.CS8 # cflag
|
attrs[1] = 0 # oflag
|
||||||
attrs[3] = 0 # lflag
|
attrs[2] = termios.CREAD | termios.CLOCAL | termios.CS8 # cflag
|
||||||
attrs[4] = attrs[5] = self._speed(baud)
|
attrs[3] = 0 # lflag
|
||||||
attrs[6][termios.VMIN] = 0
|
attrs[6][termios.VMIN] = 0
|
||||||
attrs[6][termios.VTIME] = 0
|
attrs[6][termios.VTIME] = 0
|
||||||
termios.tcsetattr(self.fd, termios.TCSANOW, attrs)
|
self._apply_baud(attrs, baud)
|
||||||
self.baud = baud
|
except BaseException:
|
||||||
|
os.close(self.fd)
|
||||||
|
raise
|
||||||
|
|
||||||
def set_baud(self, baud):
|
def set_baud(self, baud):
|
||||||
"""Retune the port without closing it — the fd stays open, so no DTR
|
"""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
|
pulse and no reset. That matters: the only caller is mid-session with a
|
||||||
loader copy that a reset would throw away."""
|
loader copy that a reset would throw away."""
|
||||||
attrs = termios.tcgetattr(self.fd)
|
self._apply_baud(termios.tcgetattr(self.fd), baud)
|
||||||
attrs[4] = attrs[5] = self._speed(baud)
|
|
||||||
termios.tcsetattr(self.fd, termios.TCSANOW, attrs)
|
|
||||||
self.baud = baud
|
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
os.close(self.fd)
|
os.close(self.fd)
|
||||||
@@ -531,6 +557,7 @@ class Loader:
|
|||||||
drain absorbs whatever they produced."""
|
drain absorbs whatever they produced."""
|
||||||
deadline = time.monotonic() + wait
|
deadline = time.monotonic() + wait
|
||||||
knocks = 0
|
knocks = 0
|
||||||
|
refusal = None
|
||||||
while True:
|
while True:
|
||||||
self.port.flush_input()
|
self.port.flush_input()
|
||||||
self.port.write(knock)
|
self.port.write(knock)
|
||||||
@@ -552,12 +579,18 @@ class Loader:
|
|||||||
except Error as failed:
|
except Error as failed:
|
||||||
if "pureboot" in str(failed):
|
if "pureboot" in str(failed):
|
||||||
raise
|
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
|
self.info = None
|
||||||
if self.info is not None:
|
if self.info is not None:
|
||||||
self._expect_prompt()
|
self._expect_prompt()
|
||||||
verbose(f"loader answered {what} {knocks}; identity read")
|
verbose(f"loader answered {what} {knocks}; identity read")
|
||||||
return self.info
|
return self.info
|
||||||
if time.monotonic() > deadline:
|
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")
|
raise Error("no answer — reset the device within its activation window")
|
||||||
|
|
||||||
def connect(self, wait):
|
def connect(self, wait):
|
||||||
@@ -1408,7 +1441,11 @@ def op_scan(port_path, baud, wait, clock=None):
|
|||||||
for pct in scan_ratios():
|
for pct in scan_ratios():
|
||||||
rate = scan_rate(baud, pct)
|
rate = scan_rate(baud, pct)
|
||||||
print(f"scan: {rate} Bd ({pct:+d} %) — reset the target", flush=True)
|
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:
|
try:
|
||||||
info = Loader(port).connect(wait)
|
info = Loader(port).connect(wait)
|
||||||
except Error:
|
except Error:
|
||||||
@@ -1504,11 +1541,13 @@ def main():
|
|||||||
print("device:")
|
print("device:")
|
||||||
for line in info.lines():
|
for line in info.lines():
|
||||||
print(f" {line}")
|
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
|
# The whole of the loader's RAM is the measured bit period at
|
||||||
# ram_start; decoded and times the rate this session drives,
|
# ram_start; decoded and times the rate this session drives,
|
||||||
# that is the true clock — the number to hold an OSCCAL bake
|
# 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")
|
unit = int.from_bytes(loader.read_ram(info.ram, 2), "little")
|
||||||
cycles = unit * UNIT_LOOP_CYCLES + UNIT_DISCOUNT
|
cycles = unit * UNIT_LOOP_CYCLES + UNIT_DISCOUNT
|
||||||
clock = cycles * args.baud
|
clock = cycles * args.baud
|
||||||
|
|||||||
@@ -193,10 +193,11 @@ static avr_cycle_count_t tx_sample(avr_t *mcu, avr_cycle_count_t when, void *par
|
|||||||
tx_shift = (uint8_t)((tx_shift >> 1) | (tx_level ? 0x80 : 0));
|
tx_shift = (uint8_t)((tx_shift >> 1) | (tx_level ? 0x80 : 0));
|
||||||
if (++tx_bit < 8)
|
if (++tx_bit < 8)
|
||||||
return when + bit_cycles;
|
return when + bit_cycles;
|
||||||
/* The byte is not delivered until its stop bit has passed. A real
|
/* The byte is delivered at the stop bit's sampling point (9.5 bit
|
||||||
* receiver cannot answer sooner, and a host that did would put its
|
* times), where a hardware receiver raises its RXC — not sooner: a
|
||||||
* start bit on the wire while the device is still driving the stop
|
* host answering before the stop bit would put its start bit on the
|
||||||
* bit — which the device, transmitting, is not watching for. */
|
* wire while the device is still driving, which the device,
|
||||||
|
* transmitting, is not watching for. */
|
||||||
return when + bit_cycles;
|
return when + bit_cycles;
|
||||||
}
|
}
|
||||||
if (write(pty_master, &tx_shift, 1) != 1)
|
if (write(pty_master, &tx_shift, 1) != 1)
|
||||||
@@ -412,6 +413,14 @@ int main(int argc, char *argv[])
|
|||||||
fprintf(stderr, "device: cannot read %s\n", argv[1]);
|
fprintf(stderr, "device: cannot read %s\n", argv[1]);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
// An image past flash end would smash the simulator's heap and turn
|
||||||
|
// into phantom peripheral behavior (lessons: believe the size gate
|
||||||
|
// first) — refuse it loudly instead.
|
||||||
|
if (base + fw.flashsize > avr->flashend + 1) {
|
||||||
|
fprintf(stderr, "device: %u B at 0x%x runs past flash end 0x%x — image does not fit its slot\n",
|
||||||
|
(unsigned)fw.flashsize, base, avr->flashend);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
memcpy(avr->flash + base, fw.flash, fw.flashsize);
|
memcpy(avr->flash + base, fw.flash, fw.flashsize);
|
||||||
}
|
}
|
||||||
// The boot-sectioned megas enter the loader in hardware (BOOTRST, not
|
// The boot-sectioned megas enter the loader in hardware (BOOTRST, not
|
||||||
|
|||||||
@@ -46,6 +46,24 @@ def main():
|
|||||||
if "9984000" not in report:
|
if "9984000" not in report:
|
||||||
fail(f"the absolute clock must scale with the found ratio:\n{report}")
|
fail(f"the absolute clock must scale with the found ratio:\n{report}")
|
||||||
|
|
||||||
|
# The walk's rates mostly have no termios B-constant, so the POSIX port
|
||||||
|
# must set them through termios2 — probed on a pty, which accepts the
|
||||||
|
# ioctl without caring about the speed. Without this every off-nominal
|
||||||
|
# probe would abort the walk on the platform --scan matters most on.
|
||||||
|
if os.name == "posix":
|
||||||
|
import pty
|
||||||
|
|
||||||
|
master, slave = pty.openpty()
|
||||||
|
try:
|
||||||
|
port = pb.Port(os.ttyname(slave), pb.scan_rate(9600, 4))
|
||||||
|
port.set_baud(pb.scan_rate(9600, -4))
|
||||||
|
port.close()
|
||||||
|
except pb.Error as error:
|
||||||
|
fail(f"PosixPort refused an off-nominal probe rate: {error}")
|
||||||
|
finally:
|
||||||
|
os.close(master)
|
||||||
|
os.close(slave)
|
||||||
|
|
||||||
print("OK")
|
print("OK")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,14 @@ port's TUs compile identically; the sims prove nothing new there) exist for
|
|||||||
libavr's reflect spot set only, mirroring its rule: the full reflect matrix
|
libavr's reflect spot set only, mirroring its rule: the full reflect matrix
|
||||||
is never built, one chip per hardware class and pack vintage is.
|
is never built, one chip per hardware class and pack vintage is.
|
||||||
|
|
||||||
Run from the repo root: tools/make_presets.py
|
Run from the repo root: tools/make_presets.py — or with --check, which
|
||||||
|
verifies the committed file matches this generator and edits nothing (the
|
||||||
|
ctest entry `presets.generated` runs that, so drift reds the gate).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
CHIPS = [
|
CHIPS = [
|
||||||
"attiny13", "attiny13a", "attiny25", "attiny45", "attiny85",
|
"attiny13", "attiny13a", "attiny25", "attiny45", "attiny85",
|
||||||
@@ -41,7 +44,7 @@ def main():
|
|||||||
"hidden": True,
|
"hidden": True,
|
||||||
"generator": "Ninja",
|
"generator": "Ninja",
|
||||||
"binaryDir": "${sourceDir}/build/${presetName}",
|
"binaryDir": "${sourceDir}/build/${presetName}",
|
||||||
"toolchainFile": "$env{LIBAVR_ROOT}/cmake/avr-toolchain.cmake",
|
"toolchainFile": "${sourceDir}/libavr/cmake/avr-toolchain.cmake",
|
||||||
"cacheVariables": {
|
"cacheVariables": {
|
||||||
"CMAKE_BUILD_TYPE": "Release",
|
"CMAKE_BUILD_TYPE": "Release",
|
||||||
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
|
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
|
||||||
@@ -72,6 +75,9 @@ def main():
|
|||||||
for chip in REFLECT_SPOT:
|
for chip in REFLECT_SPOT:
|
||||||
add(chip, "reflect")
|
add(chip, "reflect")
|
||||||
|
|
||||||
|
# CMake rejects unknown fields in the presets root, $comment included, so
|
||||||
|
# the file cannot carry a generated-file marker; the --check ctest is the
|
||||||
|
# whole of rule 10's guard here.
|
||||||
presets = {
|
presets = {
|
||||||
"version": 8,
|
"version": 8,
|
||||||
"configurePresets": configure,
|
"configurePresets": configure,
|
||||||
@@ -79,12 +85,19 @@ def main():
|
|||||||
"testPresets": test,
|
"testPresets": test,
|
||||||
"workflowPresets": workflows,
|
"workflowPresets": workflows,
|
||||||
}
|
}
|
||||||
|
rendered = json.dumps(presets, indent=1) + "\n"
|
||||||
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "CMakePresets.json")
|
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "CMakePresets.json")
|
||||||
|
if "--check" in sys.argv[1:]:
|
||||||
|
current = open(path).read() if os.path.exists(path) else ""
|
||||||
|
if current != rendered:
|
||||||
|
print("CMakePresets.json does not match its generator — run tools/make_presets.py")
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
with open(path, "w") as f:
|
with open(path, "w") as f:
|
||||||
json.dump(presets, f, indent=1)
|
f.write(rendered)
|
||||||
f.write("\n")
|
|
||||||
print(f"{len(CHIPS)} chips, {len(REFLECT_SPOT)} reflect: {os.path.normpath(path)}")
|
print(f"{len(CHIPS)} chips, {len(REFLECT_SPOT)} reflect: {os.path.normpath(path)}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
sys.exit(main())
|
||||||
|
|||||||
@@ -72,6 +72,35 @@ class Suite:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def scan(self) -> None:
|
||||||
|
"""The --scan walk against real termios and a real oscillator: every
|
||||||
|
probe rate must open a port (the off-nominal rates exist only through
|
||||||
|
termios2), and one probe must answer — the nominal on a healthy board,
|
||||||
|
a neighbor on a drifted one. The rig injects the one reset per probe
|
||||||
|
the operator supplies in the field; this is the rate physics the
|
||||||
|
simulator cannot arbitrate (a pty carries bytes at any rate), pinned
|
||||||
|
on silicon."""
|
||||||
|
module = pbrig.load_pureboot(self.rig.d.pureboot)
|
||||||
|
found = None
|
||||||
|
for pct in module.scan_ratios():
|
||||||
|
rate = module.scan_rate(self.rig.d.baud, pct)
|
||||||
|
self.rig.reset()
|
||||||
|
try:
|
||||||
|
port = module.Port(self.rig.d.port, rate)
|
||||||
|
except module.Error as error:
|
||||||
|
self.check("scan opens every probe rate", False, f"{rate} Bd: {error}")
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
module.Loader(port).connect(min(self.rig.d.wait, 6.0))
|
||||||
|
found = pct
|
||||||
|
break
|
||||||
|
except module.Error:
|
||||||
|
continue
|
||||||
|
finally:
|
||||||
|
port.close()
|
||||||
|
self.check("scan finds the board's rate", found is not None,
|
||||||
|
"no probe answered" if found is None else f"{found:+d} % of {self.rig.d.baud} Bd")
|
||||||
|
|
||||||
def eeprom(self, info) -> None:
|
def eeprom(self, info) -> None:
|
||||||
size = info.eeprom_size
|
size = info.eeprom_size
|
||||||
if not size:
|
if not size:
|
||||||
@@ -161,6 +190,10 @@ class Suite:
|
|||||||
print("\nthe loader never answered; nothing below can be trusted")
|
print("\nthe loader never answered; nothing below can be trusted")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
if not self.rig.d.autobaud:
|
||||||
|
print("\nscan")
|
||||||
|
self.scan()
|
||||||
|
|
||||||
print("\nEEPROM")
|
print("\nEEPROM")
|
||||||
self.eeprom(info)
|
self.eeprom(info)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user