Firmware 31250 v2: widen the byte-scaled reply ACK-wait ($D9E7: 04->28)

Disassembly confirmed the bench theory: the no-ACK wait loop at $D9E0
self-clocks in byte times (each tick transmits an IDLE keep-alive and
re-enters on its TX-complete interrupt), so raising the baud silently
shrank the ACK grace from ~5.2ms to ~1.6ms - under USB turnaround,
hence the v1 retry storm. --widen-ackwait (requires --baud31250) sets
the limit to 40 ticks (~12.8ms at 31250). v2 image: 25 bytes changed,
sha256 420d4cfc...; re-disasm diff vs v1 is exactly the CMPA operand.
v1/classic hashes reproduce unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-18 00:45:40 -05:00
co-authored by Claude Fable 5
parent 56d951cee9
commit 9a12b779bb
4 changed files with 13205 additions and 0 deletions
+18
View File
@@ -264,3 +264,21 @@ Two conclusions:
reply rate overstates throughput — unique samples are still capped
by the host's 55ms poll; harvesting the speed needs a faster poll
AFTER the retry window is fixed.
### 31250 v2 (2026-07-18) — `RIOv4_2_patched_31250v2.bin` — widened ACK-wait
Root cause of the v1 retry storm CONFIRMED in the disassembly: the reply
ACK-wait loop at `$D9E0` self-clocks in **byte times** — each tick sends an
IDLE (`$FF`) keep-alive and re-enters on that byte's TX-complete interrupt,
so the `CMPA #$04` limit means ~5 byte times of grace: ~5.2ms at 9600
(USB ACK wins; zero framing in both 9600 runs) vs ~1.6ms at 31250 (USB
loses; framing 5655 / Abandon 65 on the v1 bench run). Those keep-alive/
RESTART bytes landing mid-packet are exactly the host-side framing resyncs.
`make_patch.py --baud31250 --widen-ackwait` → one more byte,
`$D9E7: $04 → $28` (40 ticks ≈ 12.8ms at 31250, clears FTDI worst case
with margin). The NAK-retry limit ($3175) is event-counted, untouched.
sha256 `420d4cfc6b513651687982a70db2daeecda7bd00a5324321e272f35b95dca753`,
25 bytes vs original; re-disassembly diff vs the v1 31250 image is exactly
the one CMPA operand line. Expected on the bench: framing 5655 → ~0,
Abandon 65 → ~0, wedges 0 → 0.
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+16
View File
@@ -23,9 +23,13 @@ aborts instead of corrupting the image. Address == file offset.
import sys, hashlib
BAUD31250 = "--baud31250" in sys.argv
WIDEN_ACKWAIT = "--widen-ackwait" in sys.argv
assert not WIDEN_ACKWAIT or BAUD31250, \
"--widen-ackwait only makes sense with --baud31250 (the wait is byte-scaled)"
args = [a for a in sys.argv[1:] if not a.startswith("--")]
SRC = args[0] if len(args) > 0 else "RIOv4_2.bin"
DST = args[1] if len(args) > 1 else (
"RIOv4_2_patched_31250v2.bin" if WIDEN_ACKWAIT else
"RIOv4_2_patched_31250.bin" if BAUD31250 else "RIOv4_2_patched.bin")
d = bytearray(open(SRC, "rb").read())
@@ -72,6 +76,18 @@ if BAUD31250:
"expected LDAA #imm ; STAA $102B at $D62A"
patch(0xD62B, [0x30], [0x02])
# --- edit 4 (--widen-ackwait, 31250 only): reply ACK-wait 4 -> 40 ticks -----
# The no-ACK wait loop at $D9E0 self-clocks in BYTE TIMES: each tick transmits
# an IDLE ($FF) keep-alive and re-enters on that byte's TX-complete interrupt.
# 4 ticks = ~5.2ms at 9600 (USB ACK wins) but ~1.6ms at 31250 (USB loses ->
# retry storm; bench 2026-07-18: framing 5655, Abandon 65). $28 = 40 ticks
# = ~12.8ms at 31250, clearing FTDI worst-case turnaround with margin.
# $D9E6 81 04 CMPA #$04 -> 81 28 CMPA #$28
if WIDEN_ACKWAIT:
assert d[0xD9E6] == 0x81 and bytes(d[0xD9E3:0xD9E6]) == bytes([0xB6,0x31,0x7B]), \
"expected LDAA $317B ; CMPA #imm at $D9E3"
patch(0xD9E7, [0x04], [0x28])
open(DST, "wb").write(d)
new_sha = hashlib.sha256(d).hexdigest()
# byte-diff report