pureboot: the activation window gets a behavioral gate, and honest per-poll constants under it
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 <noreply@anthropic.com>
This commit is contained in:
@@ -202,6 +202,21 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
${CMAKE_BINARY_DIR}/pbtest-work)
|
${CMAKE_BINARY_DIR}/pbtest-work)
|
||||||
set_tests_properties(pureboot.protocol PROPERTIES TIMEOUT 180)
|
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 $<TARGET_FILE:pureboot>
|
||||||
|
--mcu ${PUREBOOT_SIM_MCU} --hz ${_pb_stock_hz}
|
||||||
|
--base ${PUREBOOT_BASE_HEX} --page ${PUREBOOT_PAGE}
|
||||||
|
--baud ${_pb_stock_baud} --app $<TARGET_FILE:pbapp>.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,
|
# The position-independence acceptance test: the identical image,
|
||||||
# installed one slot lower, must serve the full command set.
|
# installed one slot lower, must serve the full command set.
|
||||||
add_test(NAME pureboot.reloc
|
add_test(NAME pureboot.reloc
|
||||||
@@ -543,5 +558,19 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
1000000 9600 ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
1000000 9600 ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
${CMAKE_BINARY_DIR}/pbautobaud-work)
|
${CMAKE_BINARY_DIR}/pbautobaud-work)
|
||||||
set_tests_properties(pureboot.autobaud PROPERTIES TIMEOUT 240)
|
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 $<TARGET_FILE:pureboot_autobaud>
|
||||||
|
--mcu ${PUREBOOT_SIM_MCU} --hz 1000000
|
||||||
|
--base ${PUREBOOT_BASE_HEX} --page ${PUREBOOT_PAGE}
|
||||||
|
--baud 9600 --app $<TARGET_FILE:pbapp_autobaud>.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()
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -19,42 +19,42 @@ come out byte-identical linked at a different base.
|
|||||||
|
|
||||||
## Chips
|
## Chips
|
||||||
|
|
||||||
Sizes are the default configuration: the hardware USART0 at 115200 8N1 on a
|
The Stock column is the default configuration: the hardware USART0 at 115200
|
||||||
16 MHz crystal, or the software UART on RX = PB0 / TX = PB1 at 57600 8N1 on
|
8N1 on a 16 MHz crystal, or the software UART on RX = PB0 / TX = PB1 at
|
||||||
the tinies' RC oscillator (9.6 MHz on the t13s, 8 MHz above). Every axis moves
|
57600 8N1 on the tinies' RC oscillator (9.6 MHz on the t13s, 8 MHz above).
|
||||||
per build — see *Configuration*. The autobaud column is the clock-free build,
|
Every axis moves per build — see *Configuration*. The Autobaud column is the
|
||||||
which is the largest the space produces and the tightest fit in the matrix;
|
worst configuration the space produces for the chip: the clock-free build —
|
||||||
it carries the calibration machinery and no clock at all.
|
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 |
|
| Chip | Flash | Loader at | Link | Stock | Autobaud |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| ATtiny13, ATtiny13A † | 1 KiB | 0x0200 | software | 384 B | 452 B |
|
| ATtiny13, ATtiny13A † | 1 KiB | 0x0200 | software | 384 B | 456 B |
|
||||||
| ATtiny25 † | 2 KiB | 0x0600 | software | 388 B | 442 B |
|
| ATtiny25 † | 2 KiB | 0x0600 | software | 388 B | 446 B |
|
||||||
| ATtiny45 † | 4 KiB | 0x0e00 | software | 388 B | 442 B |
|
| ATtiny45 † | 4 KiB | 0x0e00 | software | 388 B | 446 B |
|
||||||
| ATtiny85 † | 8 KiB | 0x1e00 | software | 388 B | 442 B |
|
| ATtiny85 † | 8 KiB | 0x1e00 | software | 388 B | 446 B |
|
||||||
| ATmega8, 8A | 8 KiB | 0x1e00 | USART0 | 358 B | 470 B |
|
| ATmega8, 8A | 8 KiB | 0x1e00 | USART0 | 358 B | 476 B |
|
||||||
| ATmega16, 16A | 16 KiB | 0x3e00 | USART0 | 360 B | 474 B |
|
| ATmega16, 16A | 16 KiB | 0x3e00 | USART0 | 360 B | 480 B |
|
||||||
| ATmega32, 32A | 32 KiB | 0x7e00 | USART0 | 360 B | 474 B |
|
| ATmega32, 32A | 32 KiB | 0x7e00 | USART0 | 360 B | 480 B |
|
||||||
| ATmega48, 48A, 48P, 48PA † | 4 KiB | 0x0e00 | USART0 | 378 B | 438 B |
|
| ATmega48, 48A, 48P, 48PA † | 4 KiB | 0x0e00 | USART0 | 378 B | 450 B |
|
||||||
| ATmega88, 88A, 88P, 88PA | 8 KiB | 0x1e00 | USART0 | 388 B | 448 B |
|
| ATmega88, 88A, 88P, 88PA | 8 KiB | 0x1e00 | USART0 | 388 B | 460 B |
|
||||||
| ATmega168, 168A, 168P, 168PA | 16 KiB | 0x3e00 | USART0 | 390 B | 454 B |
|
| ATmega168, 168A, 168P, 168PA | 16 KiB | 0x3e00 | USART0 | 390 B | 464 B |
|
||||||
| ATmega328, 328P | 32 KiB | 0x7e00 | USART0 | 390 B | 454 B |
|
| ATmega328, 328P | 32 KiB | 0x7e00 | USART0 | 390 B | 464 B |
|
||||||
| ATmega164A, 164P, 164PA | 16 KiB | 0x3e00 | USART0 | 390 B | 454 B |
|
| ATmega164A, 164P, 164PA | 16 KiB | 0x3e00 | USART0 | 390 B | 464 B |
|
||||||
| ATmega324A, 324P, 324PA | 32 KiB | 0x7e00 | USART0 | 390 B | 454 B |
|
| ATmega324A, 324P, 324PA | 32 KiB | 0x7e00 | USART0 | 390 B | 464 B |
|
||||||
| ATmega644, 644A, 644P, 644PA | 64 KiB | 0xfe00 | USART0 | 384 B | 448 B |
|
| ATmega644, 644A, 644P, 644PA | 64 KiB | 0xfe00 | USART0 | 384 B | 458 B |
|
||||||
| ATmega1284, 1284P | 128 KiB | 0x1fe00 | USART0 | 410 B | 474 B |
|
| ATmega1284, 1284P | 128 KiB | 0x1fe00 | USART0 | 410 B | 484 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 therefore the 1284s' 484 of their
|
||||||
USART's own pins with the `OSCCAL` trim baked, 484 of its 512 — they alone
|
512: they alone carry the far-flash machinery (ELPM reads, RAMPZ page
|
||||||
carry the far-flash machinery (ELPM reads, RAMPZ page commands), autobaud
|
commands) on top of everything the column already stacks. The flash bank
|
||||||
alone carries the calibration loop, a bit-banged link on a USART's pins alone
|
riding in a transfer's selector byte keeps even those chips' addressing the
|
||||||
has to release it (below), and the trim adds its one register write. Without
|
same 16-bit form every other chip uses, which is why they are no longer the
|
||||||
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
|
|
||||||
outlier they were.
|
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
|
||||||
@@ -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
|
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
|
needs its divisor programmed) and that activation counts poll iterations rather
|
||||||
than seconds, since there is no clock to convert them against
|
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
|
**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
|
calibration can measure is bounded by how many clock cycles one bit lasts, so a
|
||||||
|
|||||||
@@ -171,9 +171,11 @@ template <avr::hertz_t C, avr::baud_t B>
|
|||||||
struct hardware_link {
|
struct hardware_link {
|
||||||
using uart = avr::uart::usart<usart_unit, C, {.baud = B, .max_baud_error = 2.5_pct}>;
|
using uart = avr::uart::usart<usart_unit, C, {.baud = B, .max_baud_error = 2.5_pct}>;
|
||||||
|
|
||||||
// The compiled idle poll: lds UCSR0A (2), sbrc skipping the exit (2),
|
// The compiled idle poll around the window's narrow (uint24_t) countdown:
|
||||||
// sbiw + sbci + sbci + brne (6).
|
// lds UCSR0A (2), sbrc skipping the exit (2), sbiw + sbci + brne (5).
|
||||||
static constexpr std::uint8_t poll_cycles = 10;
|
// 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()
|
static void init()
|
||||||
{
|
{
|
||||||
@@ -206,9 +208,11 @@ struct software_link {
|
|||||||
using rx_t = avr::uart::software_rx_polled<C, avr::PUREBOOT_RX, B>;
|
using rx_t = avr::uart::software_rx_polled<C, avr::PUREBOOT_RX, B>;
|
||||||
using tx_t = avr::uart::software_tx<C, avr::PUREBOOT_TX, B>;
|
using tx_t = avr::uart::software_tx<C, avr::PUREBOOT_TX, B>;
|
||||||
|
|
||||||
// The compiled idle poll: sbis skipping the exit (2), sbiw + sbci +
|
// The compiled idle poll around the window's narrow (uint24_t) countdown:
|
||||||
// sbci + brne (6).
|
// sbis skipping the exit (2), sbiw + sbci + brne (5). A uint32_t
|
||||||
static constexpr std::uint8_t poll_cycles = 8;
|
// 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()
|
static void init()
|
||||||
{
|
{
|
||||||
@@ -317,17 +321,32 @@ void await_host()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
// The window as one 32-bit countdown, divided by the backend's counted
|
// The window as one countdown, divided by the backend's counted poll-loop
|
||||||
// poll-loop cycles. Whole seconds is all it promises.
|
// 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()
|
consteval std::uint32_t window_polls()
|
||||||
{
|
{
|
||||||
return timeout_seconds * static_cast<std::uint32_t>(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
|
// 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
|
// cost a wider decrement chain at every poll for range most windows never
|
||||||
// use (the autobaud budget makes the same choice).
|
// use (the autobaud budget makes the same choice).
|
||||||
using window_t = std::conditional_t<window_polls() <= 0xffffff, avr::uint24_t, std::uint32_t>;
|
using window_t = std::conditional_t<narrow_window(), avr::uint24_t, std::uint32_t>;
|
||||||
|
|
||||||
bool pending_before_deadline()
|
bool pending_before_deadline()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,10 +8,13 @@ import subprocess
|
|||||||
|
|
||||||
|
|
||||||
class Device:
|
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]
|
cmd = [binary]
|
||||||
if link:
|
if link:
|
||||||
cmd += ["-l", 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]
|
cmd += [elf, mcu, hz, base_hex, str(page), str(baud), dump]
|
||||||
if reset_hex is not None or resume is not None:
|
if reset_hex is not None or resume is not None:
|
||||||
# Chips without a hardware boot section — the tinies and the
|
# Chips without a hardware boot section — the tinies and the
|
||||||
|
|||||||
134
test/pbwindow.py
Normal file
134
test/pbwindow.py
Normal file
@@ -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())
|
||||||
@@ -62,6 +62,29 @@ const char *dump_path;
|
|||||||
std::uint32_t reset_pc;
|
std::uint32_t reset_pc;
|
||||||
volatile std::sig_atomic_t reset_requested;
|
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)
|
int parse_link(std::string_view spec)
|
||||||
{
|
{
|
||||||
if (spec == "usart0" || spec == "usart1") {
|
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 *)
|
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) {
|
if (tx_bit < 8) {
|
||||||
tx_shift = static_cast<std::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)
|
||||||
@@ -256,10 +293,10 @@ void tx_hook(avr_irq_t *, std::uint32_t value, void *)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int level = value & 1;
|
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_active = 1;
|
||||||
tx_bit = 0;
|
tx_bit = -1;
|
||||||
avr_cycle_timer_register(avr, bit_cycles + bit_cycles / 2, tx_sample, nullptr);
|
avr_cycle_timer_register(avr, bit_cycles / 2, tx_sample, nullptr);
|
||||||
}
|
}
|
||||||
tx_level = level;
|
tx_level = level;
|
||||||
}
|
}
|
||||||
@@ -315,7 +352,16 @@ void bridge_reset()
|
|||||||
rx_active = 0;
|
rx_active = 0;
|
||||||
tx_active = 0;
|
tx_active = 0;
|
||||||
tx_level = 1;
|
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()
|
void poll_pty()
|
||||||
@@ -364,7 +410,11 @@ void poll_pty()
|
|||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
bool link_given = false;
|
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) {
|
if (opt != 'l' || parse_link(optarg) != 0) {
|
||||||
std::println(stderr, "device: bad link spec (usart0, usart1, sw, or sw:B0,B1 as RX,TX)");
|
std::println(stderr, "device: bad link spec (usart0, usart1, sw, or sw:B0,B1 as RX,TX)");
|
||||||
return 2;
|
return 2;
|
||||||
@@ -374,10 +424,12 @@ int main(int argc, char *argv[])
|
|||||||
int args = argc - optind;
|
int args = argc - optind;
|
||||||
if (args < 7 || args > 9) {
|
if (args < 7 || args > 9) {
|
||||||
std::print(stderr,
|
std::print(stderr,
|
||||||
"usage: {} [-l link] <pureboot.elf> <mcu> <hz> <base_hex> <page> <baud> <flash_dump>"
|
"usage: {} [-l link] [-w] <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"
|
||||||
|
" -w: print PB_WINDOW_TX <cycle> 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"
|
" 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",
|
||||||
@@ -468,6 +520,9 @@ int main(int argc, char *argv[])
|
|||||||
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);
|
||||||
|
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);
|
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
|
||||||
@@ -524,7 +579,7 @@ int main(int argc, char *argv[])
|
|||||||
// entirely. Pace the simulation only while the bridge is fully
|
// entirely. Pace the simulation only while the bridge is fully
|
||||||
// quiet (nothing decoding, nothing queued); transfers keep full
|
// quiet (nothing decoding, nothing queued); transfers keep full
|
||||||
// speed, and a quiet window stretches toward real time.
|
// 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);
|
usleep(200);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,7 +120,9 @@ def cmd_max(args) -> int:
|
|||||||
|
|
||||||
|
|
||||||
def cmd_check_readme(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()
|
readme = (ROOT / "pureboot" / "README.md").read_text()
|
||||||
measured = collect()
|
measured = collect()
|
||||||
rows = re.findall(r"^\|\s*(AT\w+[^|]*?)\s*\|[^|]*\|[^|]*\|[^|]*\|\s*(\d+) B\s*\|\s*(\d+) B\s*\|$",
|
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.
|
# "ATmega48, 48A, 48P, 48PA †" — the first name is the family's base.
|
||||||
chip = re.sub(r"[^a-z0-9]", "", chips.split(",")[0].strip().lower())
|
chip = re.sub(r"[^a-z0-9]", "", chips.split(",")[0].strip().lower())
|
||||||
built = {name: text for name, text, _ in measured.get(chip, [])}
|
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:
|
if target not in built:
|
||||||
skipped += 1
|
skipped += 1
|
||||||
continue
|
continue
|
||||||
|
|||||||
Reference in New Issue
Block a user