From 7702c6b7006886edac8daa11d892363a4134ec8f Mon Sep 17 00:00:00 2001 From: BlackMark Date: Mon, 27 Jul 2026 16:44:21 +0200 Subject: [PATCH] test: gate autobaud's logic floor, and say what an RC oscillator costs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ATtiny13A run left autobaud's low-clock lock looking unreliable: 1/5 at 19200 on a 1.2 MHz RC part. The simulator does not reproduce it. At an exact clock the calibration is solid down to ~36 cycles a bit and fails outright by ~31 — a sharp edge, not a fraying one — where the real part was already 1 in 5 by ~59. So the effect is the oscillator's own jitter and not backend logic, and the two floors are different quantities about a factor of two apart. Both are worth having. pureboot.autobaud gates a tight-bit point, since its two existing clock points both sat near 100 cycles a bit and would not notice the floor moving. The README carries the other half: both floors side by side, the per-clock envelope measured on silicon, and the reason budgeting the logic's ~36 on an RC part is wrong. It also carries the trap that produced the confusion. On a patched-vector chip an erased application region walks back up into the loader, so every expired window opens another and the host's retries eventually catch the pulse — 5/5 where the same part with an application resident gives 1/5. Measure with an application in place, or the fixture flatters the backend. Co-Authored-By: Claude Opus 5 --- pureboot/README.md | 21 +++++++++++++++++++++ test/pbautobaud.py | 34 +++++++++++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/pureboot/README.md b/pureboot/README.md index 9388ac8..8732c7a 100644 --- a/pureboot/README.md +++ b/pureboot/README.md @@ -98,6 +98,27 @@ 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). +**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 +rate is only ever sensible relative to the clock. Two different floors matter: + +| | cycles a bit | +|---|---| +| the logic's floor — exact clock, simulated | solid to ~36, fails outright by ~31 (`pureboot.autobaud` gates a point here) | +| a factory-trimmed internal RC, measured on an ATtiny13A | reliable at ~118; already locking 1 attempt in 5 by ~59 | + +The gap is the oscillator's own jitter, and no exact-clock simulation shows it. +So on an RC part, **budget about 100 cycles a bit** — the same order as the +fixed-baud software receiver's floor — rather than the logic's ~36. Measured +envelope on that ATtiny13A, with an application resident: 9.6 and 4.8 MHz reach +115200, 1.2 MHz reaches 9600, 600 kHz reaches 4800, 128 kHz reaches 2400. + +One trap in testing this: on a patched-vector chip an **erased** application +region walks straight back up into the loader, so every expired window opens +another one and the host's retries eventually catch the pulse. That reads as far +more reliable than the same part with an application resident, which gets one +window per reset. Measure with an application in place. + A downstream project brings its usual libavr setup (the `libavr` target, the chip via the `LIBAVR_MCU` toolchain preset), consumes this directory, and states its deployment — an ATmega328P on its shipped 1 MHz fuses with the diff --git a/test/pbautobaud.py b/test/pbautobaud.py index 7ab532c..8875241 100644 --- a/test/pbautobaud.py +++ b/test/pbautobaud.py @@ -141,13 +141,45 @@ def main(): print(f" {label}: locked at {hz} Hz / {baud} Bd, flash+EEPROM verified" + (", hand-over ok" if hand_over else "")) + def must_lock(hz, baud, label): + """The calibration alone, at a tight bit period. Nothing is programmed — + the question is only whether the loader can still measure the pulse.""" + dump = os.path.join(workdir, f"flash_{label}.bin") + device = pbsim.Device(device_bin, elf, mcu, str(hz), base_hex, page, baud, dump, + link="sw:B0,B1") + try: + port = pb.Port(device.pty, baud) + try: + live = pb.Loader(port).connect_autobaud(15) + if live.version != pb.NEWEST_LOADER: + fail(f"{label}: loader reports pureboot {live.version}") + finally: + port.close() + finally: + device.stop() + print(f" {label}: locked at {hz} Hz / {baud} Bd ({hz / baud:.0f} cycles a bit)") + # The app fixture is built for one clock; the hand-over banners there. A # second point at double that clock, same loader binary, proves the lock is # measured, not baked in — the whole point of autobaud. (Doubling keeps the # bit period healthy; halving would drop it below the software UART's floor.) round_trip(app_hz, app_baud, "clock-a", hand_over=True) round_trip(app_hz * 2, app_baud, "clock-b", hand_over=False) - print("pbautobaud: calibration lock and flash/EEPROM/fuse round-trip pass at both clocks") + + # Both points above sit near 100 cycles a bit, which is comfortable. The + # calibration's real floor is far tighter, and it is worth a gate: measured + # here, the lock is solid down to ~36 cycles a bit and fails outright by ~31 + # — a sharp edge, not a fraying one. This pins the tightest standard rate the + # fixture's clock reaches, so a change that raises the floor is caught. + # + # It does *not* bound what a real deployment can use. On silicon the + # oscillator's own jitter costs roughly a factor of two: an ATtiny13A on its + # factory RC trim was reliable at ~118 cycles a bit and already locking only + # 1 attempt in 5 by ~59, which no exact-clock simulation can show. The + # deployable envelope is a README matter; this is the logic's floor. + must_lock(app_hz, app_baud * 2, "tight-bit") + print("pbautobaud: calibration lock and flash/EEPROM/fuse round-trip pass at both clocks, " + "and the tight bit period still locks") if __name__ == "__main__":