Firmware: --e0thresh patch — gate the E0 error display (default N=5)

Stock $D5F2 repaints the cockpit display to the E0 counter readout on
the FIRST increment of $3187/$3184/$3185, so one benign give-up shows
E0000001 forever. New opt-in make_patch.py edit 5 hijacks the render's
LDX #$2038 into a 30-byte cave at $E000: all three counters below N ->
exit via the routine's own epilogue (registers restored, display
untouched); any >= N -> resume the render. Counters still accumulate.

Built + disassembly-verified (not yet burned): RIOv4_2_patched_e0t5.bin
(9600) and RIOv4_2_patched_31250v2_e0t5.bin (FastRIO). Docs updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-19 15:02:22 -05:00
co-authored by Claude Fable 5
parent 83fcb198c8
commit cb95c229da
6 changed files with 26369 additions and 0 deletions
+27
View File
@@ -459,3 +459,30 @@ detail in [`docs/hardware/display-board-1408.md`](../docs/hardware/display-board
- **RUN LED = buffered `AS*`** (address strobe): it indicates "clock
alive", not "software alive" — it stays lit even inside the `dEAd`
loop. TX/RX LEDs are buffered `SER_OUT`/`SER_IN`.
### E0-display threshold patch (edit 5, `--e0thresh[=N]`)
Stock firmware has **no threshold**: the first increment of any of the
three displayed counters repaints the cockpit display from `F0000000` to
the `E0` readout and it never reverts — so a single benign reply
give-up (e.g. one lost ACK healed by host-side stop-and-wait) leaves a
permanent `E0000001`. `make_patch.py --e0thresh=N` (default 5) gates the
renderer: `$D5F4`'s `LDX #$2038` becomes `JMP $E000`, and a 30-byte cave
at `$E000` (erased region, clear of the `$DFF0` wedge stub) compares
`$3187`/`$3184`/`$3185` against N — all below ⇒ exit through the
routine's own epilogue (`$D61D`, registers restored, display untouched);
any at/above ⇒ resume the render at `$D5F7`. Counters still accumulate
regardless, so the diagnostic history is intact — the display just stays
quiet until real trouble (N events since power-on).
Built variants (wedge fix + threshold 5):
| image | config | sha256 (first 12) |
|---|---|---|
| `RIOv4_2_patched_e0t5.bin` | 9600, native-game compatible | `9c21ac7199fb` |
| `RIOv4_2_patched_31250v2_e0t5.bin` | 31250 + widened ACK-wait (FastRIO) | `b43032b016d7` |
**Not yet burned or bench-verified.** To verify on hardware: burn, then
force reply give-ups (e.g. kill the host mid-poll repeatedly) — the
display must stay `F0000000` through the 4th event and flip to
`E00000 05` on the 5th; `RIO_TAP`/mash regression as usual.
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+34
View File
@@ -40,6 +40,13 @@ WIDEN_ACKWAIT = "--widen-ackwait" in sys.argv or BAUD_NAME in ("62500", "125000"
assert not ("--widen-ackwait" in sys.argv) or selected, \
"--widen-ackwait only makes sense with a baud retune (the wait is byte-scaled)"
# --e0thresh[=N]: gate the E0 error display (see edit 5). Default N=5.
E0T = None
for a in sys.argv:
if a.startswith("--e0thresh"):
E0T = int(a.split("=", 1)[1]) if "=" in a else 5
assert E0T is None or 1 <= E0T <= 255, f"--e0thresh out of range: {E0T}"
args = [a for a in sys.argv[1:] if not a.startswith("--")]
SRC = args[0] if len(args) > 0 else "RIOv4_2.bin"
if len(args) > 1:
@@ -50,6 +57,8 @@ elif BAUD_NAME:
DST = f"RIOv4_2_patched_{BAUD_NAME}.bin"
else:
DST = "RIOv4_2_patched.bin"
if E0T is not None and len(args) <= 1:
DST = DST[:-4] + f"_e0t{E0T}.bin"
d = bytearray(open(SRC, "rb").read())
assert len(d) == 0x10000, f"expected 64KB image, got {len(d)}"
@@ -107,6 +116,31 @@ if WIDEN_ACKWAIT:
"expected LDAA $317B ; CMPA #imm at $D9E3"
patch(0xD9E7, [0x04], [ACKWAIT_VAL])
# --- edit 5 (--e0thresh=N): threshold-gate the E0 error display -----------
# Stock behavior: every increment of $3187 (TX-ring overflow), $3184 (reply
# retransmit) or $3185 (reply teardown/give-up) immediately calls $D5F2,
# which paints 'E0'+counters over the F0 banner — so ONE benign give-up
# shows E0000001 forever. Gate the render: skip unless any counter >= N.
# $D5F2 prologue (PSHX/PSHA) has already run when we take over its
# LDX #$2038; the cave may clobber A/X because both exits restore them —
# below-threshold leaves via the routine's own epilogue at $D61D
# (PULA/PULX/RTS), at-or-above resumes the render at $D5F7.
if E0T is not None:
patch(0xD5F4, [0xCE, 0x20, 0x38], [0x7E, 0xE0, 0x00]) # LDX #$2038 -> JMP $E000
cave = [0xB6, 0x31, 0x87, # $E000 LDAA $3187 (ring overflows)
0x81, E0T, # $E003 CMPA #N
0x24, 0x11, # $E005 BCC $E018 (>=N -> render)
0xB6, 0x31, 0x84, # $E007 LDAA $3184 (reply retransmits)
0x81, E0T, # $E00A CMPA #N
0x24, 0x0A, # $E00C BCC $E018
0xB6, 0x31, 0x85, # $E00E LDAA $3185 (teardowns/give-ups)
0x81, E0T, # $E011 CMPA #N
0x24, 0x03, # $E013 BCC $E018
0x7E, 0xD6, 0x1D, # $E015 JMP $D61D (below threshold: epilogue)
0xCE, 0x20, 0x38, # $E018 LDX #$2038 (displaced instruction)
0x7E, 0xD5, 0xF7] # $E01B JMP $D5F7 (resume render)
patch(0xE000, [0xFF] * len(cave), cave)
open(DST, "wb").write(d)
new_sha = hashlib.sha256(d).hexdigest()
# byte-diff report