test: gate autobaud's logic floor, and say what an RC oscillator costs
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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__":
|
||||
|
||||
Reference in New Issue
Block a user