Slow chorded presses still glitch lamps at 62500 after three rounds of
host-side hardening (NAK-race resend -> stop-and-wait -> typed
resolution): a chord maximizes board ISR latency exactly when lamp
replies arrive, and a 2-byte RX buffer loses bytes no protocol can
recover. Full post-mortem in ANALYSIS.md; all bench evidence archived.
Final ladder: 9600 production / 31250v2 FastRIO / 62500 shelved /
125000 not viable. Stop-and-wait stays on everywhere as cheap delivery
insurance.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bench round 3 (testlogs/riomash-patched-62500-sw): lamp glitches
persisted because reply-resolution was type-blind — an analog reply
still crossing USB from the previous poll could falsely confirm the
NEXT command (usually a lamp write) before the board judged it. Now a
reply resolves only its MATCHING pending request (analog/version/
check); ACK/NAK stay type-blind, which is safe because the board's TX
ISR prioritizes ACK/NAK ahead of reply data, so a command's ACK cannot
trail into its successor's window. Budget-exhausted drops settle 10ms
before releasing the gate so late stragglers land on an empty pending.
New test: a stray AnalogReply must not resolve a pending lamp command.
283 green; selftest regression unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bench falsified the v1 retransmit (testlogs/riomash-patched-62500-retx):
resends tracked NAKs 1:1 (321/321) yet lamps still stuck/missed — under
mash bursts the NAK arrives after a newer command is already "latest",
so the wrong packet was resent; and total shreds never NAK at all.
Now commands are stop-and-wait: ONE in flight (_commandGate), resolved
by ACK, NAK, or AckTimeout (50ms); NAK/timeout retransmits THE SAME
packet up to CommandRetransmitLimit (2), then drops (idempotent - the
next state update supersedes). Attribution is exact by construction and
timeouts catch silent shreds. Control-byte replies bypass the gate so
board traffic is never delayed; a request's own reply (analog/version/
check) also resolves the wait, so request/reply exchanges never burn
the timeout even if the board sends no explicit ACK.
7 tests (same-packet resend, timeout retry+drop, ACK completion,
serialization, reply-resolves-request, stray-NAK no-op, disable);
282 green. Mash summary now reports NAK/timeout resends.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
NAK=69/~1060 lamp commands explains the observed stuck-bright/missed
lamps (fire-and-forget writes turn each corrupt 4-byte burst into a
visible state error) while 2-byte traffic is pristine (analog 88.1%,
counters flat, zero wedges, worst gap 0.14s). The HC11 RX ISR's worst
path sits right at ~320 E-cycles. Ladder: 9600 production, 31250v2
CLEAN (FastRIO), 62500 needs host NAK-retransmit, 125000 CPU-walled.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Board->PC near-perfect at 125k (framing 5) while PC->board shredded by
packet length (2-byte polls 60% delivered, 4-byte lamp cmds ~2/3 NAKed
as checksum-corrupt): the 2MHz HC11 RX ISR can't drain back-to-back
bytes at 80us. MAX232 acquitted; zero wedges even at 40% inbound loss.
62500 image built as the sweet-spot candidate (RIOv4_2_patched_62500.bin).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
make_patch.py generalized: baud flags 31250/62500/125000, each holding
the byte-clocked ACK-wait at ~12.8ms wall time (40/80/160 ticks; the
widen is implied for 62500/125000 since stock 4 ticks would be 0.6/0.3ms
there). All historical hashes reproduce unchanged; the 125000 image
(sha256 d22c8d85..., 25 bytes) diffs from 31250v2 by exactly the BAUD
and CMPA operands.
Predictions on record for the bench run: FTDI side exact (3M/24); the
MAX232-class level shifter is AT its 120kbps rating so edge-rounding
may surface as noise flags; and the 2MHz HC11 has ~160 E-cycles/byte -
back-to-back inbound bytes likely outrun the RX ISR (the firmware sets
its overrun flag $3186 but never reads it), so expect dropped bytes ->
NAK/retry churn and a dirtier profile than v2's. Wedges should stay 0.
62500 is one flag away as the fallback sweet spot.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
192s mash at 31250 with the $D9E7 wait fix: framing 5655 -> 54
(-99.4%), AbandonCount 65 -> +0, duplicate replies gone (215% -> 88.6%
of poll slots — identical to the healthy 9600 profile, the structural
$D758 drop-gate ratio), zero wedges, counters perfectly flat. The
byte-clocked ACK-wait theory is confirmed end to end: disassembly
predicted the mechanism, one operand byte fixed it, the wire went
quiet. Evidence log archived; results table in ANALYSIS.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
127s mash at 31250: zero wedges — but 5655 framing resyncs, duplicate
replies (215% of poll slots) and AbandonCount=65 reveal the board's
reply ACK-wait scales with byte time: ~5+ms at 9600 (USB beats it,
zero framing) vs ~1.6ms at 31250 (USB cannot). All 65 retry
exhaustions self-recovered through the patched give-up path — the
strongest field proof of the latch fix yet; unpatched firmware would
have wedged on the first. Verdict recorded in ANALYSIS.md: cabinets
run the 9600 patched chip; 31250 stays a bench branch until the retry
window is widened in firmware.
Tool: before-snapshot now retries once (can lose the post-DTR boot
race, as this run showed); evidence log archived.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
testlogs/ carries the raw records behind the CONFIRMED verdict:
baseline wedged at 0.62s and stayed dead 300s; patched ran clean
through 152s of heavier mash, worst gap 0.58s self-recovered.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Baseline (original chip): reply path wedged 0.62s into the run — the
tool's own version/check exchange colliding with analog polling was
enough, no mash required — and stayed dead for all 300s despite 951
button presses (no button revival; power cycle only). Patched chip:
survived the same startup collision, 2419 analog replies at a steady
~16/s over 152s under heavier mash (peak 635/min), zero wedges, worst
gap 0.58s self-recovered — the $DFF0 latch-clear stub doing its job.
Results table + evidence logs committed (rio-firmware/testlogs/).
Remaining: cabinet soak + native-game session; 31250 variant A/B.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
make_patch.py gains --baud31250: one byte beyond the wedge patch — the SCI
init operand at $D62B ($30 -> $02). BAUD $30 = /13 prescale, /1 divider
(2MHz E / 208 = 9615); $02 = /1, /4 -> 31250 exactly, 3.3x faster. The
init write also confirms the 8MHz crystal, which is why 19200/38400 are
unreachable and why 62500/125k are left alone pending an ISR cycle count.
- 24 bytes changed vs original (sha256 9f866cf3...); re-disassembly diff
vs the classic patched image shows exactly the one operand line, and
the classic build still reproduces 3fc8170c... (script regression-safe).
- PC side: SerialPortTransport takes an optional baudRate (default 9600,
runtime untouched); RioSerialMonitor + --mash accept --baud 31250.
- Caveats documented in README/ANALYSIS: non-standard rate (FTDI-class
adapters only, no 16550s); native games still speak 9600 so this chip
is bench/RIOJoy-only; validate the wedge patch at 9600 first.
275 tests green; mash --selftest regression unchanged (FAIL/exit-1).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New --mash mode (tools/RioSerialMonitor/MashTest.cs) mechanizes the
wedge-patch validation plan from RIOv4_2-ANALYSIS.md:
- Runs the live link with the app's >5s reset-recovery DISABLED so a
board wedge stays observable, and echoes lamps on every press
(lamp/reply collisions are the wedge trigger).
- Gap timing uses ANY AnalogReply packet (0xFE sentinels included -
a sentinel still proves the reply path is alive); logs a gap
histogram + top-10 longest gaps with timestamps.
- WEDGE detector: analog silent past the threshold (default 2s) ->
beep + banner; on resume, classifies self-recovered (patched
expectation) vs button-revived (button event within 300ms of resume,
the unpatched signature) vs unresolved at run end.
- Board self-reported RestartCount/AbandonCount/FullBufferCount
snapshotted before/after via CheckRequest, delta printed
(7-bit wrap-aware).
- Fixed-layout summary teed to riomash-<label>-<stamp>.log so
baseline-vs-patched runs diff directly. Exit 0 = no wedge, 1 = wedge.
--mash --selftest drives the whole instrument against a scripted
in-memory board (SelftestTransport) that goes silent at t=4.0s and
revives 200ms after a button at t=6.5s: verified end-to-end - alarm at
6.0s, wedge classified button-revived (2.75s), counter delta +4/+0/+1,
verdict FAIL, exit 1. Use it to sanity-check the alarm at the cabinet.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- RIOv4_2.bin: 64K image dumped from the board's AM27C512 (code at
$C000-$FFFF, TMP68HC11).
- disasm_6811.py + RIOv4_2.disasm.asm: vector-rooted 68HC11 disassembly;
SCI ISR at $D630 traced to the $2521 reply-in-progress latch leak that
wedges the analog reply path under button-mash stress.
- make_patch.py + RIOv4_2_patched.bin: two in-place edits (abort-path stub
at $DFF0, unconditional latch clear at $DA21) statically verified by
re-disassembly diff. Dynamic proof awaits a burned W27C512.
- Analysis + burn/validation plan in RIOv4_2-ANALYSIS.md and README.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>