From 8e7cc86fb3522e3daac1fc036f8a9337b404ea3e Mon Sep 17 00:00:00 2001 From: BlackMark Date: Thu, 30 Jul 2026 16:05:42 +0200 Subject: [PATCH] pureboot: the activation window gets a behavioral gate, and honest per-poll constants under it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The window's per-poll cycle counts were hand-counted for a uint32_t countdown, but every default window fits uint24_t, whose decrement chain is one sbci shorter — so deployed loaders ran 9/10ths of their stated seconds (a 328P's 8 s was 7.2 s on the wire). No golden-asm pin can hold this: the loops compile in consumer context. pbwindow.py measures the behavior instead: it installs a real application beside the loader through the host tool's own plan_flash (surgery included), starts the simulator with the line idle, and reads the cycle of the first transmit — the application's banner, so that cycle is the window. Held at plus or minus 2 percent per chip (pureboot.window), red at -10.0 percent against the old constants, green with poll_cycles now counted for the narrow countdown (hardware 9, software 7; window_polls() solves narrow-first and adds the wide loop's cycle where the count forces uint32_t — a count narrow only at the wide cost stays wide, so the choice cannot oscillate). The autobaud window is its poll budget at the measured ten cycles a poll, gated the same way (pureboot.window.autobaud), and the README carries that arithmetic now. No version bump: timing-window precision is not meaningful behavior, v7 stays. The gate flushed out two runner gaps. The software bridge accepted any falling edge as a start bit, so the device's own TX-init glitch decoded as a stray byte; it re-samples mid-bit now and abandons a false start, as silicon does. And after avr_reset, the idle-line re-raise was silently dropped: ioport pin irqs are IRQ_FLAG_FILTERED and the irq's cached value survives the reset the port latch does not, so the device read the line stuck low, calibrate() measured reset-to-first-edge as one wrapping pulse, and the first knock after a reset could boot the application instead of locking — the intermittent autobaud failure. bridge_reset forces a real transition (0 then 1, no cycles between). The README's Autobaud column now carries each chip's worst configuration — autobaud with OSCCAL baked, on a USART's own pins where the chip has one (tinies: autobaud + OSCCAL) — the numbers the existing pureboot_autobaud_osccal[_on_usart0] matrix points already gate; sizes.py checks the column against exactly those targets. Tool sizes and window prose updated with it. Co-Authored-By: Claude Fable 5 --- CMakeLists.txt | 29 +++++++++ pureboot/README.md | 63 +++++++++--------- pureboot/pureboot.cpp | 39 +++++++++--- test/pbsim.py | 5 +- test/pbwindow.py | 134 +++++++++++++++++++++++++++++++++++++++ test/pureboot_device.cpp | 69 ++++++++++++++++++-- tools/sizes.py | 8 ++- 7 files changed, 297 insertions(+), 50 deletions(-) create mode 100644 test/pbwindow.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 126ed7e..29499a8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -202,6 +202,21 @@ if(PROJECT_IS_TOP_LEVEL) ${CMAKE_BINARY_DIR}/pbtest-work) set_tests_properties(pureboot.protocol PROPERTIES TIMEOUT 180) + # The activation window as a measured duration: application installed, + # line idle, the first transmit is the application's banner — its + # cycle is the window the source declares, held to ±2 % (one + # mis-counted cycle per poll is a 10 % shift). + add_test(NAME pureboot.window + COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbwindow.py + --device ${PB_DEVICE} --loader $ + --mcu ${PUREBOOT_SIM_MCU} --hz ${_pb_stock_hz} + --base ${PUREBOOT_BASE_HEX} --page ${PUREBOOT_PAGE} + --baud ${_pb_stock_baud} --app $.bin + --seconds ${PUREBOOT_TIMEOUT} + --tool ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py + --workdir ${CMAKE_BINARY_DIR}/pbwindow-work) + set_tests_properties(pureboot.window PROPERTIES TIMEOUT 300) + # The position-independence acceptance test: the identical image, # installed one slot lower, must serve the full command set. add_test(NAME pureboot.reloc @@ -543,5 +558,19 @@ if(PROJECT_IS_TOP_LEVEL) 1000000 9600 ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py ${CMAKE_BINARY_DIR}/pbautobaud-work) set_tests_properties(pureboot.autobaud PROPERTIES TIMEOUT 240) + + # The autobaud window: the calibration poll budget, at the measured + # 10 cycles a poll (pbwindow.py pins the constant the README's + # seconds arithmetic uses; the budget itself is the clock-free knob). + add_test(NAME pureboot.window.autobaud + COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbwindow.py + --device ${PB_DEVICE} --loader $ + --mcu ${PUREBOOT_SIM_MCU} --hz 1000000 + --base ${PUREBOOT_BASE_HEX} --page ${PUREBOOT_PAGE} + --baud 9600 --app $.bin + --autobaud-polls 4000000 --link sw + --tool ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py + --workdir ${CMAKE_BINARY_DIR}/pbwindow-autobaud-work) + set_tests_properties(pureboot.window.autobaud PROPERTIES TIMEOUT 300) endif() endif() diff --git a/pureboot/README.md b/pureboot/README.md index 7bbc70e..fa2f497 100644 --- a/pureboot/README.md +++ b/pureboot/README.md @@ -19,42 +19,42 @@ come out byte-identical linked at a different base. ## Chips -Sizes are the default configuration: the hardware USART0 at 115200 8N1 on a -16 MHz crystal, or the software UART on RX = PB0 / TX = PB1 at 57600 8N1 on -the tinies' RC oscillator (9.6 MHz on the t13s, 8 MHz above). Every axis moves -per build — see *Configuration*. The autobaud column is the clock-free build, -which is the largest the space produces and the tightest fit in the matrix; -it carries the calibration machinery and no clock at all. +The Stock column is the default configuration: the hardware USART0 at 115200 +8N1 on a 16 MHz crystal, or the software UART on RX = PB0 / TX = PB1 at +57600 8N1 on the tinies' RC oscillator (9.6 MHz on the t13s, 8 MHz above). +Every axis moves per build — see *Configuration*. The Autobaud column is the +worst configuration the space produces for the chip: the clock-free build — +it alone carries the calibration machinery — with the `OSCCAL` trim baked +and, where the chip has a USART, the link deployed on that USART's own pins, +which the loader then has to release (*Pin ownership*). On default pins +without the trim the same loaders run 10–30 B smaller. | Chip | Flash | Loader at | Link | Stock | Autobaud | |---|---|---|---|---|---| -| ATtiny13, ATtiny13A † | 1 KiB | 0x0200 | software | 384 B | 452 B | -| ATtiny25 † | 2 KiB | 0x0600 | software | 388 B | 442 B | -| ATtiny45 † | 4 KiB | 0x0e00 | software | 388 B | 442 B | -| ATtiny85 † | 8 KiB | 0x1e00 | software | 388 B | 442 B | -| ATmega8, 8A | 8 KiB | 0x1e00 | USART0 | 358 B | 470 B | -| ATmega16, 16A | 16 KiB | 0x3e00 | USART0 | 360 B | 474 B | -| ATmega32, 32A | 32 KiB | 0x7e00 | USART0 | 360 B | 474 B | -| ATmega48, 48A, 48P, 48PA † | 4 KiB | 0x0e00 | USART0 | 378 B | 438 B | -| ATmega88, 88A, 88P, 88PA | 8 KiB | 0x1e00 | USART0 | 388 B | 448 B | -| ATmega168, 168A, 168P, 168PA | 16 KiB | 0x3e00 | USART0 | 390 B | 454 B | -| ATmega328, 328P | 32 KiB | 0x7e00 | USART0 | 390 B | 454 B | -| ATmega164A, 164P, 164PA | 16 KiB | 0x3e00 | USART0 | 390 B | 454 B | -| ATmega324A, 324P, 324PA | 32 KiB | 0x7e00 | USART0 | 390 B | 454 B | -| ATmega644, 644A, 644P, 644PA | 64 KiB | 0xfe00 | USART0 | 384 B | 448 B | -| ATmega1284, 1284P | 128 KiB | 0x1fe00 | USART0 | 410 B | 474 B | +| ATtiny13, ATtiny13A † | 1 KiB | 0x0200 | software | 384 B | 456 B | +| ATtiny25 † | 2 KiB | 0x0600 | software | 388 B | 446 B | +| ATtiny45 † | 4 KiB | 0x0e00 | software | 388 B | 446 B | +| ATtiny85 † | 8 KiB | 0x1e00 | software | 388 B | 446 B | +| ATmega8, 8A | 8 KiB | 0x1e00 | USART0 | 358 B | 476 B | +| ATmega16, 16A | 16 KiB | 0x3e00 | USART0 | 360 B | 480 B | +| ATmega32, 32A | 32 KiB | 0x7e00 | USART0 | 360 B | 480 B | +| ATmega48, 48A, 48P, 48PA † | 4 KiB | 0x0e00 | USART0 | 378 B | 450 B | +| ATmega88, 88A, 88P, 88PA | 8 KiB | 0x1e00 | USART0 | 388 B | 460 B | +| ATmega168, 168A, 168P, 168PA | 16 KiB | 0x3e00 | USART0 | 390 B | 464 B | +| ATmega328, 328P | 32 KiB | 0x7e00 | USART0 | 390 B | 464 B | +| ATmega164A, 164P, 164PA | 16 KiB | 0x3e00 | USART0 | 390 B | 464 B | +| ATmega324A, 324P, 324PA | 32 KiB | 0x7e00 | USART0 | 390 B | 464 B | +| ATmega644, 644A, 644P, 644PA | 64 KiB | 0xfe00 | USART0 | 384 B | 458 B | +| ATmega1284, 1284P | 128 KiB | 0x1fe00 | USART0 | 410 B | 484 B | † 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. -The tightest fit in the whole space is the 1284s' autobaud build deployed on a -USART's own pins with the `OSCCAL` trim baked, 484 of its 512 — they alone -carry the far-flash machinery (ELPM reads, RAMPZ page commands), autobaud -alone carries the calibration loop, a bit-banged link on a USART's pins alone -has to release it (below), and the trim adds its one register write. Without -the trim that build is 478; on the default pins, 474. The flash bank riding -in a transfer's selector byte keeps even those chips' addressing the same -16-bit form every other chip uses, which is why they are no longer the +The tightest fit in the whole space is therefore the 1284s' 484 of their +512: they alone carry the far-flash machinery (ELPM reads, RAMPZ page +commands) on top of everything the column already stacks. The flash bank +riding in a transfer's selector byte keeps even those chips' addressing the +same 16-bit form every other chip uses, which is why they are no longer the outlier they were. The software UART enables the RX pull-up; TX idles high. All multi-byte wire @@ -100,7 +100,10 @@ where a fixed-baud software build has to be rebuilt per clock and still drifts out of tolerance. The cost is that it is software-serial only (a hardware USART needs its divisor programmed) and that activation counts poll iterations rather than seconds, since there is no clock to convert them against -(`PUREBOOT_AUTOBAUD_POLLS`, default 4,000,000). +(`PUREBOOT_AUTOBAUD_POLLS`, default 4,000,000). The wait spends ten cycles a +poll (measured, and held by the `pureboot.window.autobaud` gate), so the +default window is 40 M cycles: 5 s at 8 MHz, about 4.2 s at 9.6 MHz, 40 s at +1 MHz. **Pick the rate by cycles a bit, and leave the oscillator room.** What the calibration can measure is bounded by how many clock cycles one bit lasts, so a diff --git a/pureboot/pureboot.cpp b/pureboot/pureboot.cpp index 95414ba..11f1726 100644 --- a/pureboot/pureboot.cpp +++ b/pureboot/pureboot.cpp @@ -171,9 +171,11 @@ template struct hardware_link { using uart = avr::uart::usart; - // The compiled idle poll: lds UCSR0A (2), sbrc skipping the exit (2), - // sbiw + sbci + sbci + brne (6). - static constexpr std::uint8_t poll_cycles = 10; + // The compiled idle poll around the window's narrow (uint24_t) countdown: + // lds UCSR0A (2), sbrc skipping the exit (2), sbiw + sbci + brne (5). + // A uint32_t countdown pays one more sbci — window_polls() adds it where + // the count forces the wide type. Held by the pureboot.window gate. + static constexpr std::uint8_t poll_cycles = 9; static void init() { @@ -206,9 +208,11 @@ struct software_link { using rx_t = avr::uart::software_rx_polled; using tx_t = avr::uart::software_tx; - // The compiled idle poll: sbis skipping the exit (2), sbiw + sbci + - // sbci + brne (6). - static constexpr std::uint8_t poll_cycles = 8; + // The compiled idle poll around the window's narrow (uint24_t) countdown: + // sbis skipping the exit (2), sbiw + sbci + brne (5). A uint32_t + // countdown pays one more sbci — window_polls() adds it where the count + // forces the wide type. Held by the pureboot.window gate. + static constexpr std::uint8_t poll_cycles = 7; static void init() { @@ -317,17 +321,32 @@ void await_host() } } #else -// The window as one 32-bit countdown, divided by the backend's counted -// poll-loop cycles. Whole seconds is all it promises. +// The window as one countdown, divided by the backend's counted poll-loop +// cycles. Whole seconds is all it promises. The per-poll cost depends on the +// countdown's own width (a uint32_t decrement chain is one sbci longer), and +// the width depends on the poll count — solved narrow-first: a count that +// fits 24 bits at the narrow cost keeps the narrow loop, anything else takes +// the wide loop at its own cost. A count fitting 24 bits only at the wide +// cost stays wide, so the choice cannot oscillate on the boundary. +consteval std::uint32_t polls_at(std::uint32_t per_poll) +{ + return timeout_seconds * (dev::clock.hz / per_poll); +} + +consteval bool narrow_window() +{ + return polls_at(link::poll_cycles) <= 0xffffff; +} + consteval std::uint32_t window_polls() { - return timeout_seconds * static_cast(dev::clock.hz / link::poll_cycles); + return polls_at(narrow_window() ? link::poll_cycles : link::poll_cycles + 1u); } // 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; +using window_t = std::conditional_t; bool pending_before_deadline() { diff --git a/test/pbsim.py b/test/pbsim.py index 1a3faae..1ab1b23 100644 --- a/test/pbsim.py +++ b/test/pbsim.py @@ -8,10 +8,13 @@ import subprocess class Device: - def __init__(self, binary, elf, mcu, hz, base_hex, page, baud, dump, reset_hex=None, resume=None, link=None): + def __init__(self, binary, elf, mcu, hz, base_hex, page, baud, dump, reset_hex=None, resume=None, link=None, + window=False): cmd = [binary] if link: cmd += ["-l", link] + if window: + cmd.append("-w") # report the first-transmit cycle, free-run idle cmd += [elf, mcu, hz, base_hex, str(page), str(baud), dump] if reset_hex is not None or resume is not None: # Chips without a hardware boot section — the tinies and the diff --git a/test/pbwindow.py b/test/pbwindow.py new file mode 100644 index 0000000..519a7e2 --- /dev/null +++ b/test/pbwindow.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +"""The activation window as a behavioral duration gate. + +The loader's window is a counted poll loop whose per-poll cost is hand-counted +in the source (`link::poll_cycles`) — but the loop compiles in consumer +context, so only the running image can prove the count. This test installs a +real application beside the loader (the host tool's own `plan_flash` supplies +the reset-vector surgery), starts the simulator with the line idle, and reads +the cycle of the first transmit activity: nothing talks until the window +closes and the application banners, so that cycle *is* the window, give or +take a banner lead measured in microseconds. Asserted at ±2 % — one +mis-counted cycle per poll shifts a window by 10 % and more. + +Fixed-baud loaders declare their window in seconds (--seconds, the build's +TIMEOUT). The autobaud loader's window is its calibration poll budget +(--autobaud-polls); the seconds it amounts to are budget × 10 / f_cpu, the +measured cost of the calibrate() wait loop this gate pins. +""" +import argparse +import importlib.util +import pathlib +import select +import sys +import time + +sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent)) +from pbsim import Device + +# The calibrate() budget loop's cycles per poll in the built image — what the +# README's window arithmetic rests on, verified here. +AUTOBAUD_POLL_CYCLES = 10 + + +def load_tool(path): + spec = importlib.util.spec_from_file_location("pureboot", path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def compose_flash(pb, loader_bytes, app_bytes, mcu, base, page): + """The flash image a completed programming session leaves: application + (with the tinies' vector surgery), loader at base — built through the + host tool's own planner so the surgery is the shipped one, not a copy.""" + flash_size = base + pb.SLOT + patch = not mcu.startswith("atmega") or mcu.startswith("atmega48") + word_flash = flash_size > 0x10000 + wire_base = base // 2 if word_flash else base + flags = (1 if patch else 0) | (2 if word_flash else 0) + raw = bytes((ord("P"), ord("B"), 5, 0, 0, 0, page & 0xFF, + wire_base & 0xFF, wire_base >> 8, 0, 0, flags)) + info = pb.Info(raw) + + flash = bytearray(b"\xff" * flash_size) + for address, content in pb.plan_flash(app_bytes, info).items(): + flash[address:address + len(content)] = content + flash[base:base + len(loader_bytes)] = loader_bytes + return bytes(flash) + + +def first_tx_cycle(device, deadline): + """The PB_WINDOW_TX report, or None. The runner prints it once.""" + stream = device.proc.stdout + while True: + remaining = deadline - time.monotonic() + if remaining <= 0: + return None + ready, _, _ = select.select([stream], [], [], remaining) + if not ready: + return None + line = stream.readline() + if not line: + return None + if line.startswith("PB_WINDOW_TX"): + return int(line.split()[1]) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--device", required=True) + parser.add_argument("--loader", required=True) + parser.add_argument("--mcu", required=True) + parser.add_argument("--hz", type=int, required=True) + parser.add_argument("--base", required=True) + parser.add_argument("--page", type=int, required=True) + parser.add_argument("--baud", type=int, required=True) + parser.add_argument("--app", required=True) + parser.add_argument("--tool", required=True) + parser.add_argument("--workdir", required=True) + parser.add_argument("--link", default=None) + parser.add_argument("--seconds", type=float, default=None) + parser.add_argument("--autobaud-polls", type=int, default=None) + args = parser.parse_args() + if (args.seconds is None) == (args.autobaud_polls is None): + parser.error("exactly one of --seconds / --autobaud-polls") + + pb = load_tool(args.tool) + base = int(args.base, 0) + expected = (args.seconds if args.seconds is not None + else args.autobaud_polls * AUTOBAUD_POLL_CYCLES / args.hz) + + work = pathlib.Path(args.workdir) + work.mkdir(parents=True, exist_ok=True) + # Every loader target objcopies its slot content beside the ELF (.bin). + loader_bytes = pathlib.Path(args.loader + ".bin").read_bytes() + app_bytes = pathlib.Path(args.app).read_bytes() + flash_file = work / "window-flash.bin" + flash_file.write_bytes(compose_flash(pb, loader_bytes, app_bytes, args.mcu, base, args.page)) + + device = Device(args.device, args.loader, args.mcu, str(args.hz), args.base, args.page, + args.baud, str(work / "window-dump.bin"), resume=str(flash_file), + link=args.link, window=True) + try: + # Simulation speed is machine-dependent; a few hundred thousand + # cycles per wall second is the pessimistic floor. + budget = max(60.0, expected * args.hz / 300000) + cycle = first_tx_cycle(device, time.monotonic() + budget) + finally: + device.stop() + + if cycle is None: + print(f" [FAIL] no transmit activity within {budget:.0f} s wall " + f"(expected a {expected:.2f} s window)") + return 1 + measured = cycle / args.hz + error = (measured - expected) / expected + ok = abs(error) <= 0.02 + print(f" [{'PASS' if ok else 'FAIL'}] window {measured:.3f} s vs declared " + f"{expected:.3f} s ({error:+.1%}, gate ±2%)") + return 0 if ok else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test/pureboot_device.cpp b/test/pureboot_device.cpp index 5b73b95..9e71316 100644 --- a/test/pureboot_device.cpp +++ b/test/pureboot_device.cpp @@ -62,6 +62,29 @@ const char *dump_path; std::uint32_t reset_pc; volatile std::sig_atomic_t reset_requested; +// -w: report the cycle of the first transmit activity, once. What the +// activation-window gate reads — with an idle line and an application +// installed, the first thing that ever talks is the application's banner, +// so this cycle *is* the loader's window plus a banner lead measured in +// microseconds. Idle pacing is skipped in this mode: there is no real-time +// host in the loop, and a paced multi-second window would take hours. +bool window_report; +bool window_tx_seen; + +void window_first_tx() +{ + if (!window_report || window_tx_seen) + return; + window_tx_seen = true; + std::println("PB_WINDOW_TX {}", avr->cycle); + std::fflush(stdout); +} + +void window_uart_hook(avr_irq_t *, std::uint32_t, void *) +{ + window_first_tx(); +} + int parse_link(std::string_view spec) { if (spec == "usart0" || spec == "usart1") { @@ -195,6 +218,20 @@ std::uint8_t tx_shift; avr_cycle_count_t tx_sample(avr_t *, avr_cycle_count_t when, void *) { + if (tx_bit < 0) { + // Half a bit into the start bit: a real receiver re-samples here and + // abandons a false start. The device's own init produces one — DDR + // drives the pin low for the instructions until the idle level is + // written — and without this check that glitch decodes as a stray + // byte (and would read as first transmit activity under -w). + if (tx_level) { + tx_active = 0; + return 0; + } + window_first_tx(); + tx_bit = 0; + return when + bit_cycles; + } if (tx_bit < 8) { tx_shift = static_cast((tx_shift >> 1) | (tx_level ? 0x80 : 0)); if (++tx_bit < 8) @@ -256,10 +293,10 @@ void tx_hook(avr_irq_t *, std::uint32_t value, void *) return; } int level = value & 1; - if (!tx_active && tx_level == 1 && level == 0) { // start edge + if (!tx_active && tx_level == 1 && level == 0) { // start edge, confirmed mid-bit tx_active = 1; - tx_bit = 0; - avr_cycle_timer_register(avr, bit_cycles + bit_cycles / 2, tx_sample, nullptr); + tx_bit = -1; + avr_cycle_timer_register(avr, bit_cycles / 2, tx_sample, nullptr); } tx_level = level; } @@ -315,7 +352,16 @@ void bridge_reset() rx_active = 0; tx_active = 0; tx_level = 1; - avr_raise_irq(rx_pin, 1); // idle line + // Re-drive the idle line through a forced transition: ioport pin irqs are + // IRQ_FLAG_FILTERED, and avr_reset zeroes the port latch while the irq + // keeps its pre-reset cached value — so a plain raise(1) against a cached + // 1 is dropped and the device reads the line stuck low. A loader entering + // calibration on that line measures reset-to-first-edge as one giant + // pulse and mis-locks or boots the application on the first real knock. + // No cycles run between the two raises, so the device only ever sees the + // final idle-high. + avr_raise_irq(rx_pin, 0); + avr_raise_irq(rx_pin, 1); } void poll_pty() @@ -364,7 +410,11 @@ void poll_pty() int main(int argc, char *argv[]) { bool link_given = false; - for (int opt; (opt = getopt(argc, argv, "l:")) != -1;) { + for (int opt; (opt = getopt(argc, argv, "l:w")) != -1;) { + if (opt == 'w') { + window_report = true; + continue; + } if (opt != 'l' || parse_link(optarg) != 0) { std::println(stderr, "device: bad link spec (usart0, usart1, sw, or sw:B0,B1 as RX,TX)"); return 2; @@ -374,10 +424,12 @@ int main(int argc, char *argv[]) int args = argc - optind; if (args < 7 || args > 9) { std::print(stderr, - "usage: {} [-l link] " + "usage: {} [-l link] [-w] " " [reset_hex] [resume_flash]\n" " -l link: usart0 | usart1 | sw[:B0,B1[@0]] (RX,TX, then the USART owning\n" " them); default: the chip's own\n" + " -w: print PB_WINDOW_TX at the first transmit activity and\n" + " free-run idle time (window measurement mode)\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" " run's dump, for power-fail resume tests\n", @@ -468,6 +520,9 @@ int main(int argc, char *argv[]) avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS(uart_digit), &flags); uart_pty_init(avr, &uart_pty); uart_pty_connect(&uart_pty, uart_digit); + if (window_report) + avr_irq_register_notify(avr_io_getirq(avr, AVR_IOCTL_UART_GETIRQ(uart_digit), UART_IRQ_OUTPUT), + window_uart_hook, nullptr); std::println("PB_PTY {}", uart_pty.pty.slavename); } else { bit_cycles = (avr->frequency + baud / 2) / baud; // matches uart.hpp's own rounding exactly @@ -524,7 +579,7 @@ int main(int argc, char *argv[]) // entirely. Pace the simulation only while the bridge is fully // quiet (nothing decoding, nothing queued); transfers keep full // speed, and a quiet window stretches toward real time. - if (!rx_active && !tx_active && rx_head == rx_tail) + if (!window_report && !rx_active && !tx_active && rx_head == rx_tail) usleep(200); } } diff --git a/tools/sizes.py b/tools/sizes.py index 58c0bc4..184e460 100755 --- a/tools/sizes.py +++ b/tools/sizes.py @@ -120,7 +120,9 @@ def cmd_max(args) -> int: def cmd_check_readme(args) -> int: - """The README's per-chip table, against the stock and autobaud builds.""" + """The README's per-chip table, against the stock build and the worst + autobaud configuration (OSCCAL baked, plus the USART-pin release where + the chip has a USART) — the config the Autobaud column documents.""" readme = (ROOT / "pureboot" / "README.md").read_text() measured = collect() rows = re.findall(r"^\|\s*(AT\w+[^|]*?)\s*\|[^|]*\|[^|]*\|[^|]*\|\s*(\d+) B\s*\|\s*(\d+) B\s*\|$", @@ -132,7 +134,9 @@ def cmd_check_readme(args) -> int: # "ATmega48, 48A, 48P, 48PA †" — the first name is the family's base. chip = re.sub(r"[^a-z0-9]", "", chips.split(",")[0].strip().lower()) built = {name: text for name, text, _ in measured.get(chip, [])} - for target, documented in (("pureboot", stock_doc), ("pureboot_autobaud", auto_doc)): + worst = ("pureboot_autobaud_osccal_on_usart0" + if "pureboot_autobaud_osccal_on_usart0" in built else "pureboot_autobaud_osccal") + for target, documented in (("pureboot", stock_doc), (worst, auto_doc)): if target not in built: skipped += 1 continue