6 Commits

Author SHA1 Message Date
a3ea099105 pureboot: the detail reaches become the library's own API
The three things this repo took from under libavr's counter are now over
it, and the local copies fold away. The USART release on a software
link's pins is the library's init contract (its guard here becomes a
deletion, byte-identical images held by the gate); the WDRF routing test
is power::peek_reset_cause().watchdog instead of a hand lookup of the
flag's register; the tsb tiers' baud arithmetic is the public solver.
libavr pin advances over those three additions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 16:12:35 +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
321ff8a4ee test: the device runners speak the C++ the rest of the repo does
pureboot_device and the tsb device, C until now, rewritten in C++23 with
every modeled behavior intact — the PGERS Z-mask and m48-discard ioctl
wraps, the GPIO bridge's timing and pacing, the tiny NVM's write-once
buffer, pin ownership, and the PB_PTY/TSB_PTY lines the harnesses parse.
The one linkage fact worth a comment: simavr's parts headers (uart_pty.h)
carry no C++ guards where its core headers do, so those includes sit in an
extern "C" block. Warning-clean at -Wall -Wextra on the build line; the
full protocol suites on all four sim-driven chips prove the conversion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 13:59:03 +02:00
531ae6c8dc build: the libavr pin advances over the audit rounds
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 10:52:05 +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
16 changed files with 434 additions and 294 deletions

View File

@@ -22,15 +22,17 @@ if(PROJECT_IS_TOP_LEVEL)
# The behavioral tests drive the real wire protocols over a simavr pty # The behavioral tests drive the real wire protocols over a simavr pty
# (as the host tools do) and actually flash the device. The runners are # (as the host tools do) and actually flash the device. The runners are
# host programs built at configure time against libsimavr; if they or # host programs built at configure time against libsimavr (C++23 — what
# Python are missing, only the size tests run. # the distribution's compiler speaks in full); if they or Python are
find_program(_host_cc NAMES cc gcc) # missing, only the size tests run.
find_program(_host_cxx NAMES c++ g++)
find_package(Python3 COMPONENTS Interpreter) find_package(Python3 COMPONENTS Interpreter)
if(_host_cc AND Python3_FOUND) if(_host_cxx AND Python3_FOUND)
set(PB_DEVICE ${CMAKE_BINARY_DIR}/pureboot_device) set(PB_DEVICE ${CMAKE_BINARY_DIR}/pureboot_device)
execute_process( execute_process(
COMMAND ${_host_cc} -O2 -I/usr/include/simavr -I/usr/include/simavr/parts COMMAND ${_host_cxx} -std=c++23 -Wall -Wextra -O2
-o ${PB_DEVICE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pureboot_device.c -I/usr/include/simavr -I/usr/include/simavr/parts
-o ${PB_DEVICE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pureboot_device.cpp
-lsimavr -lsimavrparts -lelf -lutil -lsimavr -lsimavrparts -lelf -lutil
RESULT_VARIABLE _pbdev_res ERROR_VARIABLE _pbdev_err) RESULT_VARIABLE _pbdev_res ERROR_VARIABLE _pbdev_err)
if(NOT _pbdev_res EQUAL 0) if(NOT _pbdev_res EQUAL 0)
@@ -40,8 +42,9 @@ if(PROJECT_IS_TOP_LEVEL)
if(LIBAVR_MCU STREQUAL "atmega328p") if(LIBAVR_MCU STREQUAL "atmega328p")
set(TSB_DEVICE ${CMAKE_BINARY_DIR}/tsb_device) set(TSB_DEVICE ${CMAKE_BINARY_DIR}/tsb_device)
execute_process( execute_process(
COMMAND ${_host_cc} -O2 -I/usr/include/simavr -I/usr/include/simavr/parts COMMAND ${_host_cxx} -std=c++23 -Wall -Wextra -O2
-o ${TSB_DEVICE} ${CMAKE_CURRENT_SOURCE_DIR}/test/device.c -I/usr/include/simavr -I/usr/include/simavr/parts
-o ${TSB_DEVICE} ${CMAKE_CURRENT_SOURCE_DIR}/test/device.cpp
-lsimavr -lsimavrparts -lelf -lsimavr -lsimavrparts -lelf
RESULT_VARIABLE _dev_res ERROR_VARIABLE _dev_err) RESULT_VARIABLE _dev_res ERROR_VARIABLE _dev_err)
if(NOT _dev_res EQUAL 0) if(NOT _dev_res EQUAL 0)
@@ -66,16 +69,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 +175,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 +519,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

View File

@@ -2,7 +2,7 @@
`master` carries `bootloader.atsln`, so this branch does too: `ide/bootloader.atsln` `master` carries `bootloader.atsln`, so this branch does too: `ide/bootloader.atsln`
builds the loaders from the same sources Ninja does, to a **byte-identical builds the loaders from the same sources Ninja does, to a **byte-identical
`.text`** — 404 B for the 328P pureboot loader, 510 B for the `tsb_asm` tier in `.text`** — 400 B for the 328P pureboot loader, 510 B for the `tsb_asm` tier in
its 512-byte section. CMake remains the build system; the solution is here so the its 512-byte section. CMake remains the build system; the solution is here so the
port opens in Studio as its predecessor did. port opens in Studio as its predecessor did.

2
libavr

Submodule libavr updated: 43b1f34ed1...911a87538f

View File

@@ -28,32 +28,34 @@ it carries the calibration machinery and no clock at all.
| Chip | Flash | Loader at | Link | Stock | Autobaud | | Chip | Flash | Loader at | Link | Stock | Autobaud |
|---|---|---|---|---|---| |---|---|---|---|---|---|
| ATtiny13, ATtiny13A † | 1 KiB | 0x0200 | software | 394 B | 464 B | | ATtiny13, ATtiny13A † | 1 KiB | 0x0200 | software | 390 B | 460 B |
| ATtiny25 † | 2 KiB | 0x0600 | software | 398 B | 468 B | | ATtiny25 † | 2 KiB | 0x0600 | software | 394 B | 464 B |
| ATtiny45 † | 4 KiB | 0x0e00 | software | 402 B | 472 B | | ATtiny45 † | 4 KiB | 0x0e00 | software | 398 B | 468 B |
| ATtiny85 † | 8 KiB | 0x1e00 | software | 402 B | 472 B | | ATtiny85 † | 8 KiB | 0x1e00 | software | 398 B | 468 B |
| ATmega8, 8A | 8 KiB | 0x1e00 | USART0 | 364 B | 478 B | | ATmega8, 8A | 8 KiB | 0x1e00 | USART0 | 360 B | 474 B |
| ATmega16, 16A | 16 KiB | 0x3e00 | USART0 | 366 B | 482 B | | ATmega16, 16A | 16 KiB | 0x3e00 | USART0 | 362 B | 480 B |
| ATmega32, 32A | 32 KiB | 0x7e00 | USART0 | 366 B | 482 B | | ATmega32, 32A | 32 KiB | 0x7e00 | USART0 | 362 B | 480 B |
| ATmega48, 48A, 48P, 48PA † | 4 KiB | 0x0e00 | USART0 | 392 B | 468 B | | ATmega48, 48A, 48P, 48PA † | 4 KiB | 0x0e00 | USART0 | 388 B | 464 B |
| ATmega88, 88A, 88P, 88PA | 8 KiB | 0x1e00 | USART0 | 402 B | 478 B | | ATmega88, 88A, 88P, 88PA | 8 KiB | 0x1e00 | USART0 | 398 B | 474 B |
| ATmega168, 168A, 168P, 168PA | 16 KiB | 0x3e00 | USART0 | 404 B | 482 B | | ATmega168, 168A, 168P, 168PA | 16 KiB | 0x3e00 | USART0 | 400 B | 480 B |
| ATmega328, 328P | 32 KiB | 0x7e00 | USART0 | 404 B | 482 B | | ATmega328, 328P | 32 KiB | 0x7e00 | USART0 | 400 B | 480 B |
| ATmega164A, 164P, 164PA | 16 KiB | 0x3e00 | USART0 | 404 B | 482 B | | ATmega164A, 164P, 164PA | 16 KiB | 0x3e00 | USART0 | 400 B | 480 B |
| ATmega324A, 324P, 324PA | 32 KiB | 0x7e00 | USART0 | 404 B | 482 B | | ATmega324A, 324P, 324PA | 32 KiB | 0x7e00 | USART0 | 400 B | 480 B |
| ATmega644, 644A, 644P, 644PA | 64 KiB | 0xfe00 | USART0 | 398 B | 476 B | | ATmega644, 644A, 644P, 644PA | 64 KiB | 0xfe00 | USART0 | 394 B | 474 B |
| ATmega1284, 1284P | 128 KiB | 0x1fe00 | USART0 | 424 B | 502 B | | ATmega1284, 1284P | 128 KiB | 0x1fe00 | USART0 | 420 B | 500 B |
† No hardware boot section: the host patches the reset vector, and the budget † No hardware boot section: the host patches the reset vector, and the budget
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, 500. 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,8 +543,15 @@ 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.cpp`: 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
tiny cores lack) driven by the real host tool through knock-from-reset, tiny cores lack) driven by the real host tool through knock-from-reset,
program + verify of both memories, session reconnect, an external reset program + verify of both memories, session reconnect, an external reset
@@ -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

View File

@@ -38,13 +38,6 @@ using dev = avr::device<{.clock = avr::hertz_t{PUREBOOT_CLOCK_HZ}}>;
constexpr avr::baud_t wire_baud{PUREBOOT_BAUD}; constexpr avr::baud_t wire_baud{PUREBOOT_BAUD};
#endif #endif
// The watchdog reset flag's home: MCUSR, or the classic megas' MCUCSR.
consteval std::int16_t wdrf_field()
{
auto reg = std::string_view{avr::hw::db.regs[static_cast<std::size_t>(avr::power::detail::reset_reg())].name};
return avr::hw::db.field_index(reg, "WDRF");
}
// The loader owns the top 512 bytes; a staging copy goes in the slot below. // The loader owns the top 512 bytes; a staging copy goes in the slot below.
// Chips without a hardware boot section — the tinies and the m48s, whose SPM // Chips without a hardware boot section — the tinies and the m48s, whose SPM
// runs from anywhere (Atmel-8271 §26) — keep the application's relocated // runs from anywhere (Atmel-8271 §26) — keep the application's relocated
@@ -174,27 +167,6 @@ constexpr char usart_digit = '0' + PUREBOOT_USART;
constexpr char usart_digit = '0'; constexpr char usart_digit = '0';
#endif #endif
// Release a hardware USART the application may have left enabled onto a
// bit-banged link's pins. A software transmitter drives its TX pin through the
// port register, but while that USART's TXEN is set the USART owns the pin and
// the port write does nothing — the loader would receive and obey yet never
// answer. Writing UCSRnB zero hands the pin back to the port. Guarded on the
// pin actually being a USART's TXD, so a link on non-USART pins emits nothing.
template <char Inst, avr::io::pin Tx>
[[gnu::always_inline]] inline void release_usart_on()
{
if constexpr (avr::uart::has_usart<Inst>())
if constexpr (avr::uart::detail::usart_pin<Inst>("TXD") == Tx)
avr::hw::reg_impl<avr::uart::detail::ureg<Inst, "UCSR#B">()>::write(0);
}
template <avr::io::pin Tx>
[[gnu::always_inline]] inline void release_usarts_on()
{
release_usart_on<'0', Tx>();
release_usart_on<'1', Tx>();
}
template <avr::hertz_t C, avr::baud_t B> template <avr::hertz_t C, avr::baud_t B>
struct hardware_link { struct hardware_link {
using uart = avr::uart::usart<usart_digit, C, {.baud = B, .max_baud_error = 2.5_pct}>; using uart = avr::uart::usart<usart_digit, C, {.baud = B, .max_baud_error = 2.5_pct}>;
@@ -241,7 +213,6 @@ struct software_link {
static void init() static void init()
{ {
avr::init<rx_t, tx_t>(); avr::init<rx_t, tx_t>();
release_usarts_on<avr::PUREBOOT_TX>();
} }
static bool pending() static bool pending()
@@ -275,7 +246,6 @@ struct autobaud_link {
static void init() static void init()
{ {
avr::init<uart>(); avr::init<uart>();
release_usarts_on<avr::PUREBOOT_TX>();
} }
static std::uint8_t rx() static std::uint8_t rx()
@@ -351,9 +321,14 @@ consteval std::uint32_t window_polls()
return timeout_seconds * static_cast<std::uint32_t>(dev::clock.hz / link::poll_cycles); return timeout_seconds * static_cast<std::uint32_t>(dev::clock.hz / link::poll_cycles);
} }
// The countdown in the narrowest type that holds it: a fourth byte would
// cost a wider decrement chain at every poll for range most windows never
// use (the autobaud budget makes the same choice).
using window_t = std::conditional_t<window_polls() <= 0xffffff, avr::uint24_t, std::uint32_t>;
bool pending_before_deadline() bool pending_before_deadline()
{ {
std::uint32_t polls = window_polls(); window_t polls = window_polls();
do { do {
if (link::pending()) if (link::pending())
return true; return true;
@@ -493,7 +468,7 @@ void fill_page(std::uint8_t bank, std::uint16_t at)
#endif #endif
// A watchdog reset belongs to the application, whose watchdog stays forced // A watchdog reset belongs to the application, whose watchdog stays forced
// on until it clears WDRF — no activation window in its way. // on until it clears WDRF — no activation window in its way.
if (avr::hw::field_impl<wdrf_field()>::test()) if (avr::power::peek_reset_cause().watchdog)
run_app(); run_app();
link::init(); link::init();

View File

@@ -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
@@ -1561,7 +1600,7 @@ def main():
if __name__ == "__main__": if __name__ == "__main__":
try: try:
main() main()
except Error as error: except (Error, OSError) as error:
print(f"error: {error}", file=sys.stderr) print(f"error: {error}", file=sys.stderr)
sys.exit(1) sys.exit(1)
except KeyboardInterrupt: except KeyboardInterrupt:

View File

@@ -7,62 +7,71 @@
// SPM genuinely writes avr->flash on the mega cores, so on exit (or SIGTERM) // SPM genuinely writes avr->flash on the mega cores, so on exit (or SIGTERM)
// we dump the flash image to a file for a ground-truth cross-check against // we dump the flash image to a file for a ground-truth cross-check against
// what the client read back through the bootloader. // what the client read back through the bootloader.
#include <signal.h> #include <csignal>
#include <stdint.h> #include <cstdint>
#include <stdio.h> #include <cstdio>
#include <stdlib.h> #include <cstdlib>
#include <string.h> #include <cstring>
#include <print>
#include <unistd.h> #include <unistd.h>
// The parts headers (uart_pty.h) carry no C++ linkage guards of their own,
// unlike simavr's core headers — the block covers both harmlessly.
extern "C" {
#include "avr_uart.h" #include "avr_uart.h"
#include "sim_avr.h" #include "sim_avr.h"
#include "sim_elf.h" #include "sim_elf.h"
#include "uart_pty.h" #include "uart_pty.h"
}
static avr_t *avr; namespace {
static uart_pty_t uart_pty;
static const char *dump_path;
static void finish(int sig) avr_t *avr;
uart_pty_t uart_pty;
const char *dump_path;
[[noreturn]] void finish(int)
{ {
(void)sig;
if (dump_path) { if (dump_path) {
FILE *f = fopen(dump_path, "wb"); std::FILE *f = std::fopen(dump_path, "wb");
if (f) { if (f) {
fwrite(avr->flash, 1, avr->flashend + 1, f); std::fwrite(avr->flash, 1, avr->flashend + 1, f);
fclose(f); std::fclose(f);
} }
} }
uart_pty_stop(&uart_pty); uart_pty_stop(&uart_pty);
_exit(0); _exit(0);
} }
} // namespace
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
if (argc < 3) { if (argc < 3) {
fprintf(stderr, "usage: %s <tsb.elf> <boot_base_hex> [flash_dump.bin]\n", argv[0]); std::println(stderr, "usage: {} <tsb.elf> <boot_base_hex> [flash_dump.bin]", argv[0]);
return 2; return 2;
} }
uint32_t boot_base = (uint32_t)strtoul(argv[2], NULL, 0); auto boot_base = static_cast<std::uint32_t>(std::strtoul(argv[2], nullptr, 0));
dump_path = argc >= 4 ? argv[3] : NULL; dump_path = argc >= 4 ? argv[3] : nullptr;
avr = avr_make_mcu_by_name("atmega328p"); avr = avr_make_mcu_by_name("atmega328p");
if (!avr) { if (!avr) {
fprintf(stderr, "device: no ATmega328P core\n"); std::println(stderr, "device: no ATmega328P core");
return 1; return 1;
} }
avr_init(avr); avr_init(avr);
avr->frequency = 16000000; avr->frequency = 16000000;
// Real flash powers up erased (0xff); the app region must look erased // Real flash powers up erased (0xff); the app region must look erased
// before the bootloader programs it. // before the bootloader programs it.
memset(avr->flash, 0xff, avr->flashend + 1); std::memset(avr->flash, 0xff, avr->flashend + 1);
// simavr's ELF loader flattens the flash base to 0 (it expects an app at // simavr's ELF loader flattens the flash base to 0 (it expects an app at
// 0x0), but it hands back the boot code in fw.flash; place it at the boot // 0x0), but it hands back the boot code in fw.flash; place it at the boot
// section base ourselves and enter there (BOOTRST is not modelled). // section base ourselves and enter there (BOOTRST is not modelled).
elf_firmware_t fw = {0}; elf_firmware_t fw{};
if (elf_read_firmware(argv[1], &fw) != 0) { if (elf_read_firmware(argv[1], &fw) != 0) {
fprintf(stderr, "device: cannot read %s\n", argv[1]); std::println(stderr, "device: cannot read {}", argv[1]);
return 1; return 1;
} }
// An image that runs past flash end cannot execute on hardware, and a // An image that runs past flash end cannot execute on hardware, and a
@@ -70,23 +79,23 @@ int main(int argc, char *argv[])
// the simulation misbehaves in ways that point everywhere but here. // the simulation misbehaves in ways that point everywhere but here.
// Refuse it loudly instead. // Refuse it loudly instead.
if (boot_base + fw.flashsize > avr->flashend + 1) { if (boot_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", std::println(stderr, "device: {} B at {:#x} runs past flash end {:#x} — image does not fit its slot",
(unsigned)fw.flashsize, boot_base, avr->flashend); fw.flashsize, boot_base, avr->flashend);
return 1; return 1;
} }
memcpy(avr->flash + boot_base, fw.flash, fw.flashsize); std::memcpy(avr->flash + boot_base, fw.flash, fw.flashsize);
avr->pc = boot_base; avr->pc = boot_base;
avr->codeend = avr->flashend; avr->codeend = avr->flashend;
// Optional: seed the config page (one page below the boot section) with a // Optional: seed the config page (one page below the boot section) with a
// hex byte string, so the password gate and emergency erase can be tested. // hex byte string, so the password gate and emergency erase can be tested.
// Layout: [appjump lo][appjump hi][timeout][password...][0xff]. // Layout: [appjump lo][appjump hi][timeout][password...][0xff].
const char *cfg = getenv("TSB_CONFIG"); const char *cfg = std::getenv("TSB_CONFIG");
if (cfg) { if (cfg) {
uint32_t app_end = boot_base - 128; // config page sits directly below the boot code std::uint32_t app_end = boot_base - 128; // config page sits directly below the boot code
for (int i = 0; cfg[i] && cfg[i + 1]; i += 2) { for (int i = 0; cfg[i] && cfg[i + 1]; i += 2) {
char b[3] = {cfg[i], cfg[i + 1], 0}; char b[3] = {cfg[i], cfg[i + 1], 0};
avr->flash[app_end + i / 2] = (uint8_t)strtoul(b, NULL, 16); avr->flash[app_end + i / 2] = static_cast<std::uint8_t>(std::strtoul(b, nullptr, 16));
} }
} }
@@ -95,18 +104,18 @@ int main(int argc, char *argv[])
// tight-polling loader (one that releases TX between bytes, as one-wire does) // tight-polling loader (one that releases TX between bytes, as one-wire does)
// in real time, distorting protocol timing. Clear it so the loader runs at // in real time, distorting protocol timing. Clear it so the loader runs at
// true cycle speed. // true cycle speed.
uint32_t uflags = 0; std::uint32_t uflags = 0;
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS('0'), &uflags); avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS('0'), &uflags);
uflags &= ~AVR_UART_FLAG_POLL_SLEEP; uflags &= ~AVR_UART_FLAG_POLL_SLEEP;
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS('0'), &uflags); avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS('0'), &uflags);
uart_pty_init(avr, &uart_pty); uart_pty_init(avr, &uart_pty);
uart_pty_connect(&uart_pty, '0'); uart_pty_connect(&uart_pty, '0');
printf("TSB_PTY %s\n", uart_pty.pty.slavename); std::println("TSB_PTY {}", uart_pty.pty.slavename);
fflush(stdout); std::fflush(stdout);
signal(SIGTERM, finish); std::signal(SIGTERM, finish);
signal(SIGINT, finish); std::signal(SIGINT, finish);
for (;;) { for (;;) {
int state = avr_run(avr); int state = avr_run(avr);
@@ -114,5 +123,4 @@ int main(int argc, char *argv[])
break; break;
} }
finish(0); finish(0);
return 0;
} }

View File

@@ -10,7 +10,7 @@ The state is reached the way silicon reaches it — an application that sets up
its USART and jumps in with no reset between, so nothing clears UCSRnB for it. its USART and jumps in with no reset between, so nothing clears UCSRnB for it.
The pin ownership itself is modelled by the device runner: simavr wires a The pin ownership itself is modelled by the device runner: simavr wires a
USART through IRQs alone and never takes the pin from the port, so without USART through IRQs alone and never takes the pin from the port, so without
that the mute could not happen here at all (test/pureboot_device.c). that the mute could not happen here at all (test/pureboot_device.cpp).
Usage: pbmute.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page> Usage: pbmute.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
<baud> <app_bin> <tool_py> <workdir> <link> <baud> <app_bin> <tool_py> <workdir> <link>

View File

@@ -23,16 +23,22 @@
// //
// On exit (or SIGTERM) the flash and EEPROM are dumped to files for a // On exit (or SIGTERM) the flash and EEPROM are dumped to files for a
// ground-truth cross-check against what the host read back. // ground-truth cross-check against what the host read back.
#include <csignal>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <print>
#include <string_view>
#include <fcntl.h> #include <fcntl.h>
#include <pty.h> #include <pty.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h> #include <termios.h>
#include <unistd.h> #include <unistd.h>
// The parts headers (uart_pty.h) carry no C++ linkage guards of their own,
// unlike simavr's core headers — the block covers both harmlessly.
extern "C" {
#include "avr_eeprom.h" #include "avr_eeprom.h"
#include "avr_flash.h" #include "avr_flash.h"
#include "avr_ioport.h" #include "avr_ioport.h"
@@ -41,31 +47,35 @@
#include "sim_elf.h" #include "sim_elf.h"
#include "sim_io.h" #include "sim_io.h"
#include "uart_pty.h" #include "uart_pty.h"
}
static avr_t *avr; namespace {
static uart_pty_t uart_pty;
static int link_software;
static char uart_digit = '0';
static char sw_rx_port = 'B', sw_tx_port = 'B';
static int sw_rx_bit = 0, sw_tx_bit = 1;
static char sw_tx_owner = 0; // the USART whose TXD the software link sits on
static const char *dump_path;
static uint32_t reset_pc;
static volatile sig_atomic_t reset_requested;
static int parse_link(const char *spec) avr_t *avr;
uart_pty_t uart_pty;
bool link_software;
char uart_digit = '0';
char sw_rx_port = 'B', sw_tx_port = 'B';
int sw_rx_bit = 0, sw_tx_bit = 1;
char sw_tx_owner = 0; // the USART whose TXD the software link sits on
const char *dump_path;
std::uint32_t reset_pc;
volatile std::sig_atomic_t reset_requested;
int parse_link(std::string_view spec)
{ {
if (strcmp(spec, "usart0") == 0 || strcmp(spec, "usart1") == 0) { if (spec == "usart0" || spec == "usart1") {
link_software = 0; link_software = false;
uart_digit = spec[5]; uart_digit = spec[5];
return 0; return 0;
} }
if (strncmp(spec, "sw", 2) == 0) { if (spec.starts_with("sw")) {
link_software = 1; link_software = true;
if (spec[2] == '\0') if (spec.size() == 2)
return 0; return 0;
char owner = 0; char owner = 0;
int fields = sscanf(spec + 2, ":%c%d,%c%d@%c", &sw_rx_port, &sw_rx_bit, &sw_tx_port, &sw_tx_bit, &owner); int fields =
std::sscanf(spec.data() + 2, ":%c%d,%c%d@%c", &sw_rx_port, &sw_rx_bit, &sw_tx_port, &sw_tx_bit, &owner);
if (fields == 4 || fields == 5) { if (fields == 4 || fields == 5) {
sw_tx_owner = owner; sw_tx_owner = owner;
return 0; return 0;
@@ -87,19 +97,19 @@ static int parse_link(const char *spec)
// core — so the discard store falls through into the buffer-fill branch and // core — so the discard store falls through into the buffer-fill branch and
// plants whatever Z/R1:R0 happen to hold. Perform the silicon's discard // plants whatever Z/R1:R0 happen to hold. Perform the silicon's discard
// here instead. // here instead.
static avr_flash_t *mega_flash; avr_flash_t *mega_flash;
static int (*mega_flash_ioctl)(avr_io_t *io, uint32_t ctl, void *param); int (*mega_flash_ioctl)(avr_io_t *io, std::uint32_t ctl, void *param);
static int fixed_flash_ioctl(avr_io_t *io, uint32_t ctl, void *param) int fixed_flash_ioctl(avr_io_t *io, std::uint32_t ctl, void *param)
{ {
if (ctl == AVR_IOCTL_FLASH_SPM && avr_regbit_get(io->avr, mega_flash->pgers)) { if (ctl == AVR_IOCTL_FLASH_SPM && avr_regbit_get(io->avr, mega_flash->pgers)) {
uint16_t z = (uint16_t)(io->avr->data[30] | (io->avr->data[31] << 8)); auto z = static_cast<std::uint16_t>(io->avr->data[30] | (io->avr->data[31] << 8));
uint16_t masked = (uint16_t)(z & ~(mega_flash->spm_pagesize - 1)); auto masked = static_cast<std::uint16_t>(z & ~(mega_flash->spm_pagesize - 1));
io->avr->data[30] = (uint8_t)masked; io->avr->data[30] = static_cast<std::uint8_t>(masked);
io->avr->data[31] = (uint8_t)(masked >> 8); io->avr->data[31] = static_cast<std::uint8_t>(masked >> 8);
int result = mega_flash_ioctl(io, ctl, param); int result = mega_flash_ioctl(io, ctl, param);
io->avr->data[30] = (uint8_t)z; io->avr->data[30] = static_cast<std::uint8_t>(z);
io->avr->data[31] = (uint8_t)(z >> 8); io->avr->data[31] = static_cast<std::uint8_t>(z >> 8);
return result; return result;
} }
if (ctl == AVR_IOCTL_FLASH_SPM && !(mega_flash->flags & AVR_SELFPROG_HAVE_RWW) && if (ctl == AVR_IOCTL_FLASH_SPM && !(mega_flash->flags & AVR_SELFPROG_HAVE_RWW) &&
@@ -114,46 +124,44 @@ static int fixed_flash_ioctl(avr_io_t *io, uint32_t ctl, void *param)
return mega_flash_ioctl(io, ctl, param); return mega_flash_ioctl(io, ctl, param);
} }
static void fix_mega_flash_erase(void) void fix_mega_flash_erase()
{ {
for (avr_io_t *io = avr->io_port; io; io = io->next) { for (avr_io_t *io = avr->io_port; io; io = io->next) {
if (io->kind && strcmp(io->kind, "flash") == 0) { if (io->kind && std::string_view{io->kind} == "flash") {
mega_flash = (avr_flash_t *)io; mega_flash = reinterpret_cast<avr_flash_t *>(io);
mega_flash_ioctl = io->ioctl; mega_flash_ioctl = io->ioctl;
io->ioctl = fixed_flash_ioctl; io->ioctl = fixed_flash_ioctl;
return; return;
} }
} }
fprintf(stderr, "device: no flash module to fix — SPM page erases may misalign\n"); std::println(stderr, "device: no flash module to fix — SPM page erases may misalign");
} }
static void request_reset(int sig) void request_reset(int)
{ {
(void)sig;
reset_requested = 1; reset_requested = 1;
} }
// ------------------------------------------------------------- tiny NVM --- // ------------------------------------------------------------- tiny NVM ---
typedef struct { struct tiny_nvm_t {
avr_io_t io; avr_io_t io;
uint8_t buffer[128]; std::uint8_t buffer[128];
uint8_t used[128]; // a buffer word loads once until erased — like silicon std::uint8_t used[128]; // a buffer word loads once until erased — like silicon
unsigned page; unsigned page;
} tiny_nvm_t; };
static tiny_nvm_t nvm; tiny_nvm_t nvm;
static int nvm_ioctl(avr_io_t *io, uint32_t ctl, void *param) int nvm_ioctl(avr_io_t *io, std::uint32_t ctl, void *)
{ {
(void)param;
if (ctl != AVR_IOCTL_FLASH_SPM) if (ctl != AVR_IOCTL_FLASH_SPM)
return -1; return -1;
tiny_nvm_t *n = (tiny_nvm_t *)io; auto *n = reinterpret_cast<tiny_nvm_t *>(io);
avr_t *mcu = io->avr; avr_t *mcu = io->avr;
uint8_t command = mcu->data[0x57] & 0x1f; // SPMCSR, both tinies std::uint8_t command = mcu->data[0x57] & 0x1f; // SPMCSR, both tinies
uint16_t z = (uint16_t)(mcu->data[30] | (mcu->data[31] << 8)); auto z = static_cast<std::uint16_t>(mcu->data[30] | (mcu->data[31] << 8));
uint32_t page_base = (uint32_t)(z & ~(n->page - 1)) % (mcu->flashend + 1); std::uint32_t page_base = static_cast<std::uint32_t>(z & ~(n->page - 1)) % (mcu->flashend + 1);
if (command == 0x01) { // SPMEN alone: buffer fill from r1:r0 if (command == 0x01) { // SPMEN alone: buffer fill from r1:r0
unsigned offset = z & (n->page - 1) & ~1u; unsigned offset = z & (n->page - 1) & ~1u;
if (!n->used[offset]) { // first write wins until the buffer clears if (!n->used[offset]) { // first write wins until the buffer clears
@@ -162,45 +170,44 @@ static int nvm_ioctl(avr_io_t *io, uint32_t ctl, void *param)
n->used[offset] = 1; n->used[offset] = 1;
} }
} else if (command == 0x03) { // PGERS } else if (command == 0x03) { // PGERS
memset(mcu->flash + page_base, 0xff, n->page); std::memset(mcu->flash + page_base, 0xff, n->page);
} else if (command == 0x05) { // PGWRT: programming only clears bits } else if (command == 0x05) { // PGWRT: programming only clears bits
for (unsigned i = 0; i < n->page; i++) for (unsigned i = 0; i < n->page; i++)
mcu->flash[page_base + i] &= n->buffer[i]; mcu->flash[page_base + i] &= n->buffer[i];
memset(n->buffer, 0xff, n->page); std::memset(n->buffer, 0xff, n->page);
memset(n->used, 0, n->page); std::memset(n->used, 0, n->page);
} else if (command == 0x11) { // CTPB } else if (command == 0x11) { // CTPB
memset(n->buffer, 0xff, n->page); std::memset(n->buffer, 0xff, n->page);
memset(n->used, 0, n->page); std::memset(n->used, 0, n->page);
} }
mcu->data[0x57] &= (uint8_t)~0x1f; // the operation completes instantly mcu->data[0x57] &= static_cast<std::uint8_t>(~0x1f); // the operation completes instantly
return 0; return 0;
} }
// ----------------------------------------------------------- GPIO bridge --- // ----------------------------------------------------------- GPIO bridge ---
static int pty_master = -1; int pty_master = -1;
static avr_irq_t *rx_pin; // the loader's RX (PB0), driven from the pty avr_irq_t *rx_pin; // the loader's RX (PB0), driven from the pty
static avr_cycle_count_t bit_cycles; avr_cycle_count_t bit_cycles;
static int tx_level = 1, tx_active, tx_bit; int tx_level = 1, tx_active, tx_bit;
static uint8_t tx_shift; std::uint8_t tx_shift;
static avr_cycle_count_t tx_sample(avr_t *mcu, avr_cycle_count_t when, void *param) avr_cycle_count_t tx_sample(avr_t *, avr_cycle_count_t when, void *)
{ {
(void)mcu;
(void)param;
if (tx_bit < 8) { if (tx_bit < 8) {
tx_shift = (uint8_t)((tx_shift >> 1) | (tx_level ? 0x80 : 0)); tx_shift = static_cast<std::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)
fprintf(stderr, "device: pty write lost a byte\n"); std::println(stderr, "device: pty write lost a byte");
tx_active = 0; tx_active = 0;
return 0; return 0;
} }
@@ -212,9 +219,9 @@ static avr_cycle_count_t tx_sample(avr_t *mcu, avr_cycle_count_t when, void *par
// model, so the ownership does not exist there and the mute cannot happen: // model, so the ownership does not exist there and the mute cannot happen:
// supply it, or the very state this models is untestable. The link spec's // supply it, or the very state this models is untestable. The link spec's
// trailing @n names the USART; without one the pins are nobody's. // trailing @n names the USART; without one the pins are nobody's.
static avr_uart_t *tx_owner; avr_uart_t *tx_owner;
static int tx_pin_taken(void) bool tx_pin_taken()
{ {
return tx_owner && avr_regbit_get(avr, tx_owner->txen); return tx_owner && avr_regbit_get(avr, tx_owner->txen);
} }
@@ -224,27 +231,26 @@ static int tx_pin_taken(void)
// enabled, making a freshly reset chip mute for reasons hardware does not // enabled, making a freshly reset chip mute for reasons hardware does not
// have. Reset it the way the datasheet does, so the ownership starts from // have. Reset it the way the datasheet does, so the ownership starts from
// nobody's and only an application that really enables the USART takes it. // nobody's and only an application that really enables the USART takes it.
static void reset_tx_owner(void) void reset_tx_owner()
{ {
if (tx_owner) if (tx_owner)
avr_regbit_clear(avr, tx_owner->txen); avr_regbit_clear(avr, tx_owner->txen);
} }
static void find_tx_owner(void) void find_tx_owner()
{ {
for (avr_io_t *io = avr->io_port; io; io = io->next) for (avr_io_t *io = avr->io_port; io; io = io->next)
if (io->kind && strcmp(io->kind, "uart") == 0 && ((avr_uart_t *)io)->name == sw_tx_owner) { if (io->kind && std::string_view{io->kind} == "uart" &&
tx_owner = (avr_uart_t *)io; reinterpret_cast<avr_uart_t *>(io)->name == sw_tx_owner) {
tx_owner = reinterpret_cast<avr_uart_t *>(io);
reset_tx_owner(); reset_tx_owner();
return; return;
} }
fprintf(stderr, "device: no USART%c to own the software link's TX pin\n", sw_tx_owner); std::println(stderr, "device: no USART{} to own the software link's TX pin", sw_tx_owner);
} }
static void tx_hook(avr_irq_t *irq, uint32_t value, void *param) void tx_hook(avr_irq_t *, std::uint32_t value, void *)
{ {
(void)irq;
(void)param;
if (tx_pin_taken()) { // the USART holds the line; the port write goes nowhere if (tx_pin_taken()) { // the USART holds the line; the port write goes nowhere
tx_level = 1; tx_level = 1;
return; return;
@@ -253,22 +259,20 @@ static void tx_hook(avr_irq_t *irq, uint32_t value, void *param)
if (!tx_active && tx_level == 1 && level == 0) { // start edge if (!tx_active && tx_level == 1 && level == 0) { // start edge
tx_active = 1; tx_active = 1;
tx_bit = 0; tx_bit = 0;
avr_cycle_timer_register(avr, bit_cycles + bit_cycles / 2, tx_sample, NULL); avr_cycle_timer_register(avr, bit_cycles + bit_cycles / 2, tx_sample, nullptr);
} }
tx_level = level; tx_level = level;
} }
static uint8_t rx_queue[8192]; std::uint8_t rx_queue[8192];
static unsigned rx_head, rx_tail; // ring: head = next to send unsigned rx_head, rx_tail; // ring: head = next to send
static int rx_active, rx_bit; int rx_active, rx_bit;
static uint8_t rx_byte; std::uint8_t rx_byte;
static void rx_start_next(void); void rx_start_next();
static avr_cycle_count_t rx_step(avr_t *mcu, avr_cycle_count_t when, void *param) avr_cycle_count_t rx_step(avr_t *, avr_cycle_count_t when, void *)
{ {
(void)mcu;
(void)param;
if (rx_bit < 8) { if (rx_bit < 8) {
avr_raise_irq(rx_pin, (rx_byte >> rx_bit) & 1); avr_raise_irq(rx_pin, (rx_byte >> rx_bit) & 1);
rx_bit++; rx_bit++;
@@ -284,7 +288,7 @@ static avr_cycle_count_t rx_step(avr_t *mcu, avr_cycle_count_t when, void *param
return 0; return 0;
} }
static void rx_start_next(void) void rx_start_next()
{ {
if (rx_active || rx_head == rx_tail) if (rx_active || rx_head == rx_tail)
return; return;
@@ -293,7 +297,7 @@ static void rx_start_next(void)
rx_active = 1; rx_active = 1;
rx_bit = 0; rx_bit = 0;
avr_raise_irq(rx_pin, 0); // start bit avr_raise_irq(rx_pin, 0); // start bit
avr_cycle_timer_register(avr, bit_cycles, rx_step, NULL); avr_cycle_timer_register(avr, bit_cycles, rx_step, nullptr);
} }
// A reset abandons whatever the bridge was mid-transfer: bytes still queued // A reset abandons whatever the bridge was mid-transfer: bytes still queued
@@ -303,10 +307,10 @@ static void rx_start_next(void)
// output latch, whose falling edge starts a spurious decode before this // output latch, whose falling edge starts a spurious decode before this
// runs, and a stale tx_sample would then interleave with the loader's first // runs, and a stale tx_sample would then interleave with the loader's first
// real answer through the shared shift state, corrupting it. // real answer through the shared shift state, corrupting it.
static void bridge_reset(void) void bridge_reset()
{ {
avr_cycle_timer_cancel(avr, tx_sample, NULL); avr_cycle_timer_cancel(avr, tx_sample, nullptr);
avr_cycle_timer_cancel(avr, rx_step, NULL); avr_cycle_timer_cancel(avr, rx_step, nullptr);
rx_head = rx_tail = 0; rx_head = rx_tail = 0;
rx_active = 0; rx_active = 0;
tx_active = 0; tx_active = 0;
@@ -314,9 +318,9 @@ static void bridge_reset(void)
avr_raise_irq(rx_pin, 1); // idle line avr_raise_irq(rx_pin, 1); // idle line
} }
static void poll_pty(void) void poll_pty()
{ {
uint8_t chunk[256]; std::uint8_t chunk[256];
ssize_t got = read(pty_master, chunk, sizeof(chunk)); ssize_t got = read(pty_master, chunk, sizeof(chunk));
for (ssize_t i = 0; i < got; i++) { for (ssize_t i = 0; i < got; i++) {
unsigned next = (rx_tail + 1) % sizeof(rx_queue); unsigned next = (rx_tail + 1) % sizeof(rx_queue);
@@ -331,23 +335,22 @@ static void poll_pty(void)
// ------------------------------------------------------------------ main --- // ------------------------------------------------------------------ main ---
static void finish(int sig) [[noreturn]] void finish(int)
{ {
(void)sig;
if (dump_path) { if (dump_path) {
FILE *f = fopen(dump_path, "wb"); std::FILE *f = std::fopen(dump_path, "wb");
if (f) { if (f) {
fwrite(avr->flash, 1, avr->flashend + 1, f); std::fwrite(avr->flash, 1, avr->flashend + 1, f);
fclose(f); std::fclose(f);
} }
avr_eeprom_desc_t ee = {.ee = NULL, .offset = 0, .size = 0}; avr_eeprom_desc_t ee = {.ee = nullptr, .offset = 0, .size = 0};
if (avr_ioctl(avr, AVR_IOCTL_EEPROM_GET, &ee) == 0 && ee.ee && ee.size) { if (avr_ioctl(avr, AVR_IOCTL_EEPROM_GET, &ee) == 0 && ee.ee && ee.size) {
char path[512]; char path[512];
snprintf(path, sizeof(path), "%s.eeprom", dump_path); std::snprintf(path, sizeof(path), "%s.eeprom", dump_path);
f = fopen(path, "wb"); f = std::fopen(path, "wb");
if (f) { if (f) {
fwrite(ee.ee, 1, ee.size, f); std::fwrite(ee.ee, 1, ee.size, f);
fclose(f); std::fclose(f);
} }
} }
} }
@@ -356,77 +359,87 @@ static void finish(int sig)
_exit(0); _exit(0);
} }
} // namespace
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
int link_given = 0; bool link_given = false;
for (int opt; (opt = getopt(argc, argv, "l:")) != -1;) { for (int opt; (opt = getopt(argc, argv, "l:")) != -1;) {
if (opt != 'l' || parse_link(optarg) != 0) { if (opt != 'l' || parse_link(optarg) != 0) {
fprintf(stderr, "device: bad link spec (usart0, usart1, sw, or sw:B0,B1 as RX,TX)\n"); std::println(stderr, "device: bad link spec (usart0, usart1, sw, or sw:B0,B1 as RX,TX)");
return 2; return 2;
} }
link_given = 1; link_given = true;
} }
int args = argc - optind; int args = argc - optind;
if (args < 7 || args > 9) { if (args < 7 || args > 9) {
fprintf(stderr, std::print(stderr,
"usage: %s [-l link] <pureboot.elf> <mcu> <hz> <base_hex> <page> <baud> <flash_dump>" "usage: {} [-l link] <pureboot.elf> <mcu> <hz> <base_hex> <page> <baud> <flash_dump>"
" [reset_hex] [resume_flash]\n" " [reset_hex] [resume_flash]\n"
" -l link: usart0 | usart1 | sw[:B0,B1[@0]] (RX,TX, then the USART owning\n" " -l link: usart0 | usart1 | sw[:B0,B1[@0]] (RX,TX, then the USART owning\n"
" them); default: the chip's own\n" " them); default: the chip's own\n"
" reset_hex: reset vector (default: base with a boot section, else 0)\n" " reset_hex: reset vector (default: base with a boot section, else 0)\n"
" resume_flash: raw full-flash image loaded instead of the ELF — a prior\n" " resume_flash: raw full-flash image loaded instead of the ELF — a prior\n"
" run's dump, for power-fail resume tests\n", " run's dump, for power-fail resume tests\n",
argv[0]); argv[0]);
return 2; return 2;
} }
argv += optind - 1; // argv[1] is the ELF again, whatever was parsed argv += optind - 1; // argv[1] is the ELF again, whatever was parsed
const char *mcu_name = argv[2]; const std::string_view mcu_name = argv[2];
uint32_t base = (uint32_t)strtoul(argv[4], NULL, 0); auto base = static_cast<std::uint32_t>(std::strtoul(argv[4], nullptr, 0));
unsigned page = (unsigned)atoi(argv[5]); auto page = static_cast<unsigned>(std::atoi(argv[5]));
unsigned baud = (unsigned)atoi(argv[6]); auto baud = static_cast<unsigned>(std::atoi(argv[6]));
dump_path = argv[7]; dump_path = argv[7];
int is_mega = strncmp(mcu_name, "atmega", 6) == 0; const bool is_mega = mcu_name.starts_with("atmega");
if (!link_given) if (!link_given)
link_software = !is_mega; // the chips' natural links: USART0, or PB0/PB1 link_software = !is_mega; // the chips' natural links: USART0, or PB0/PB1
avr = avr_make_mcu_by_name(mcu_name); avr = avr_make_mcu_by_name(mcu_name.data());
if (!avr) { if (!avr) {
fprintf(stderr, "device: no %s core\n", mcu_name); std::println(stderr, "device: no {} core", mcu_name);
return 1; return 1;
} }
avr_init(avr); avr_init(avr);
avr->frequency = (uint32_t)strtoul(argv[3], NULL, 0); avr->frequency = static_cast<std::uint32_t>(std::strtoul(argv[3], nullptr, 0));
memset(avr->flash, 0xff, avr->flashend + 1); // real flash powers up erased std::memset(avr->flash, 0xff, avr->flashend + 1); // real flash powers up erased
if (args > 8) { if (args > 8) {
// Resume: the full flash image of an interrupted prior run. // Resume: the full flash image of an interrupted prior run.
FILE *f = fopen(argv[9], "rb"); std::FILE *f = std::fopen(argv[9], "rb");
if (!f || fread(avr->flash, 1, avr->flashend + 1, f) == 0) { if (!f || std::fread(avr->flash, 1, avr->flashend + 1, f) == 0) {
fprintf(stderr, "device: cannot read %s\n", argv[9]); std::println(stderr, "device: cannot read {}", argv[9]);
return 1; return 1;
} }
fclose(f); std::fclose(f);
} else { } else {
elf_firmware_t fw = {0}; elf_firmware_t fw{};
if (elf_read_firmware(argv[1], &fw) != 0) { if (elf_read_firmware(argv[1], &fw) != 0) {
fprintf(stderr, "device: cannot read %s\n", argv[1]); std::println(stderr, "device: cannot read {}", argv[1]);
return 1; return 1;
} }
memcpy(avr->flash + base, fw.flash, fw.flashsize); // 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) {
std::println(stderr, "device: {} B at {:#x} runs past flash end {:#x} — image does not fit its slot",
fw.flashsize, base, avr->flashend);
return 1;
}
std::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
// modeled — the argument picks the modeled fuse's target); the tinies // modeled — the argument picks the modeled fuse's target); the tinies
// and the boot-section-less m48s reset to word 0 like silicon — erased // and the boot-section-less m48s reset to word 0 like silicon — erased
// flash walks up into the loader, and after the host's surgery the // flash walks up into the loader, and after the host's surgery the
// patched vector routes there. // patched vector routes there.
int boot_section = is_mega && strncmp(mcu_name, "atmega48", 8) != 0; const bool boot_section = is_mega && !mcu_name.starts_with("atmega48");
reset_pc = args > 7 ? (uint32_t)strtoul(argv[8], NULL, 0) : (boot_section ? base : 0); reset_pc = args > 7 ? static_cast<std::uint32_t>(std::strtoul(argv[8], nullptr, 0)) : (boot_section ? base : 0);
avr->pc = reset_pc; avr->pc = reset_pc;
avr->codeend = avr->flashend; avr->codeend = avr->flashend;
// Erased EEPROM, as hardware powers up (simavr zeroes it). // Erased EEPROM, as hardware powers up (simavr zeroes it).
uint8_t blank[1024]; std::uint8_t blank[1024];
memset(blank, 0xff, sizeof(blank)); std::memset(blank, 0xff, sizeof(blank));
avr_eeprom_desc_t seed = {.ee = blank, .offset = 0, .size = 0}; avr_eeprom_desc_t seed = {.ee = blank, .offset = 0, .size = 0};
if (avr_ioctl(avr, AVR_IOCTL_EEPROM_GET, &seed) == 0 && seed.size <= sizeof(blank)) { if (avr_ioctl(avr, AVR_IOCTL_EEPROM_GET, &seed) == 0 && seed.size <= sizeof(blank)) {
seed.ee = blank; seed.ee = blank;
@@ -440,7 +453,7 @@ int main(int argc, char *argv[])
fix_mega_flash_erase(); fix_mega_flash_erase();
} else { } else {
nvm.page = page; nvm.page = page;
memset(nvm.buffer, 0xff, sizeof(nvm.buffer)); std::memset(nvm.buffer, 0xff, sizeof(nvm.buffer));
nvm.io.kind = "tiny_nvm"; nvm.io.kind = "tiny_nvm";
nvm.io.ioctl = nvm_ioctl; nvm.io.ioctl = nvm_ioctl;
avr_register_io(avr, &nvm.io); avr_register_io(avr, &nvm.io);
@@ -449,37 +462,38 @@ int main(int argc, char *argv[])
if (!link_software) { if (!link_software) {
// POLL_SLEEP paces an idle-polling loader in host real time (a // POLL_SLEEP paces an idle-polling loader in host real time (a
// no-hardware CPU-saving hack); clear it so cycles run free. // no-hardware CPU-saving hack); clear it so cycles run free.
uint32_t flags = 0; std::uint32_t flags = 0;
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS(uart_digit), &flags); avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS(uart_digit), &flags);
flags &= ~AVR_UART_FLAG_POLL_SLEEP; flags &= ~AVR_UART_FLAG_POLL_SLEEP;
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS(uart_digit), &flags); avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS(uart_digit), &flags);
uart_pty_init(avr, &uart_pty); uart_pty_init(avr, &uart_pty);
uart_pty_connect(&uart_pty, uart_digit); uart_pty_connect(&uart_pty, uart_digit);
printf("PB_PTY %s\n", uart_pty.pty.slavename); std::println("PB_PTY {}", uart_pty.pty.slavename);
} else { } else {
bit_cycles = (avr->frequency + baud / 2) / baud; // matches uart.hpp's own rounding exactly bit_cycles = (avr->frequency + baud / 2) / baud; // matches uart.hpp's own rounding exactly
if (sw_tx_owner) if (sw_tx_owner)
find_tx_owner(); find_tx_owner();
rx_pin = avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ(sw_rx_port), (unsigned)sw_rx_bit); rx_pin = avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ(sw_rx_port), static_cast<unsigned>(sw_rx_bit));
avr_irq_register_notify(avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ(sw_tx_port), (unsigned)sw_tx_bit), tx_hook, avr_irq_register_notify(
NULL); avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ(sw_tx_port), static_cast<unsigned>(sw_tx_bit)), tx_hook,
nullptr);
avr_raise_irq(rx_pin, 1); // idle line avr_raise_irq(rx_pin, 1); // idle line
int slave; int slave;
struct termios raw; struct termios raw;
cfmakeraw(&raw); cfmakeraw(&raw);
if (openpty(&pty_master, &slave, NULL, &raw, NULL) != 0) { if (openpty(&pty_master, &slave, nullptr, &raw, nullptr) != 0) {
fprintf(stderr, "device: openpty failed\n"); std::println(stderr, "device: openpty failed");
return 1; return 1;
} }
fcntl(pty_master, F_SETFL, O_NONBLOCK); fcntl(pty_master, F_SETFL, O_NONBLOCK);
printf("PB_PTY %s\n", ttyname(slave)); std::println("PB_PTY {}", ttyname(slave));
} }
fflush(stdout); std::fflush(stdout);
signal(SIGTERM, finish); std::signal(SIGTERM, finish);
signal(SIGINT, finish); std::signal(SIGINT, finish);
signal(SIGUSR1, request_reset); // an external reset line, for the tests std::signal(SIGUSR1, request_reset); // an external reset line, for the tests
long since_poll = 0; long since_poll = 0;
for (;;) { for (;;) {
@@ -491,7 +505,7 @@ int main(int argc, char *argv[])
avr_reset(avr); avr_reset(avr);
avr->pc = reset_pc; avr->pc = reset_pc;
if (!link_software) { // reset restores the pacing hack; re-clear it if (!link_software) { // reset restores the pacing hack; re-clear it
uint32_t flags = 0; std::uint32_t flags = 0;
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS(uart_digit), &flags); avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS(uart_digit), &flags);
flags &= ~AVR_UART_FLAG_POLL_SLEEP; flags &= ~AVR_UART_FLAG_POLL_SLEEP;
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS(uart_digit), &flags); avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS(uart_digit), &flags);
@@ -515,5 +529,4 @@ int main(int argc, char *argv[])
} }
} }
finish(0); finish(0);
return 0;
} }

View File

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

View File

@@ -36,4 +36,10 @@ if ((full)); then
done done
fi fi
# Every tree is freshly built now — the one moment the README's size table
# can be held to what the images measure (a per-preset ctest sees only its
# own chip; the table needs all of them, and ungated it drifts: a
# common-code shave moves every row at once with nothing over budget).
python3 tools/sizes.py check-readme
echo "check: every chip green" echo "check: every chip green"

View File

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

View File

@@ -72,6 +72,39 @@ 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
try:
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()
except Exception as error: # noqa: BLE001 — a rig hiccup is a result
self.check("scan walks the probe ladder", False, str(error)[:70])
return
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 +194,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)

View File

@@ -65,7 +65,7 @@ constexpr std::uint8_t comm_window = 200;
constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 20; constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 20;
// Fixed 115200 8N1; the library solves UBRR + U2X from clock and baud. // Fixed 115200 8N1; the library solves UBRR + U2X from clock and baud.
constexpr auto baud = avr::uart::detail::solve_baud(16_MHz, 115200_Bd); constexpr auto baud = avr::uart::solve_baud(16_MHz, 115200_Bd);
// The 16-byte device-info block, streamed out on activation. // The 16-byte device-info block, streamed out on activation.
// clang-format off // clang-format off

View File

@@ -200,7 +200,7 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
// only the divisor low byte and U2X0 need a store. The solver still does // only the divisor low byte and U2X0 need a store. The solver still does
// the datasheet work; the asserts pin the reset-state assumptions. // the datasheet work; the asserts pin the reset-state assumptions.
{ {
constexpr auto sol = avr::uart::detail::solve_baud(dev::clock, 115200_Bd); constexpr auto sol = avr::uart::solve_baud(dev::clock, 115200_Bd);
static_assert(sol.u2x && sol.ubrr < 256, "lean bring-up writes UBRR0L only, with U2X0"); static_assert(sol.u2x && sol.ubrr < 256, "lean bring-up writes UBRR0L only, with U2X0");
avr::hw::reg<"UBRR0">::write(static_cast<std::uint8_t>(sol.ubrr)); avr::hw::reg<"UBRR0">::write(static_cast<std::uint8_t>(sol.ubrr));
avr::hw::ucsr0a::write(avr::hw::ucsr0a::u2x0(1)); avr::hw::ucsr0a::write(avr::hw::ucsr0a::u2x0(1));

View File

@@ -65,7 +65,7 @@ constexpr std::uint8_t comm_window = 200;
constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 20; constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 20;
// Fixed 115200 8N1; the library solves UBRR + U2X from clock and baud. // Fixed 115200 8N1; the library solves UBRR + U2X from clock and baud.
constexpr auto baud = avr::uart::detail::solve_baud(16_MHz, 115200_Bd); constexpr auto baud = avr::uart::solve_baud(16_MHz, 115200_Bd);
// The 16-byte device-info block, streamed out on activation. // The 16-byte device-info block, streamed out on activation.
// clang-format off // clang-format off