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
|
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).
|
||||||
|
|
||||||
|
**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
|
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
|
||||||
|
|||||||
@@ -141,13 +141,45 @@ def main():
|
|||||||
print(f" {label}: locked at {hz} Hz / {baud} Bd, flash+EEPROM verified"
|
print(f" {label}: locked at {hz} Hz / {baud} Bd, flash+EEPROM verified"
|
||||||
+ (", hand-over ok" if hand_over else ""))
|
+ (", 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
|
# 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
|
# 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
|
# 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.)
|
# 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, app_baud, "clock-a", hand_over=True)
|
||||||
round_trip(app_hz * 2, app_baud, "clock-b", hand_over=False)
|
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__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user