RIO firmware: build + verify patched image (reply-wedge fix)
make_patch.py applies the two-site fix to RIOv4_2.bin (asserting original bytes first) -> RIOv4_2_patched.bin (23 bytes changed). Re-disassembling and diffing confirms the change is confined to $D9DD, $DA21-$DA2E, and the $DFF0 stub with no downstream desync. Clears the reply-in-progress latch $2521 on every teardown so a stress collision no longer leaves the board mute to analog. Flash directly to the DIP-28 W27C512; static verification only -- dynamic RIO_TAP mash test pending the burned chip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -133,6 +133,21 @@ safe. This is the board-side analogue of the game-side "make collisions
|
||||
harmless" patches (BTL4OPT v2-v4) — instead of widening a timing window it
|
||||
removes the latch leak entirely.
|
||||
|
||||
### Patched binary — built & statically verified (2026-07-04)
|
||||
|
||||
`make_patch.py` applies both edits to `RIOv4_2.bin` (asserting the exact
|
||||
original bytes at each site first) → **`RIOv4_2_patched.bin`**
|
||||
(sha256 `3fc8170caf60e2580641724ff995176c93c4f2e706f31487beded8233142493f`,
|
||||
23 bytes changed). Re-disassembling it (`RIOv4_2_patched.disasm.asm`) and
|
||||
diffing against the original confirms the change is confined to exactly
|
||||
three regions with no downstream desync:
|
||||
- `$D9DD` `JMP $DA2F` → `JMP $DFF0`
|
||||
- `$DFF0` new stub: `CLR $2521 ; CLR $2522 ; JMP $DA2F`
|
||||
- `$DA21` `CLR $2521 ; CLR $2522 ; NOP×7` (RTS at `$DA2E` intact)
|
||||
|
||||
Flash `RIOv4_2_patched.bin` directly to the W27C512 (DIP-28). This is
|
||||
static verification only; dynamic proof still needs the burned chip.
|
||||
|
||||
### Validation plan (when a chip is available)
|
||||
Burn the two edits to a W27C512, socket it (preserve the original AMD
|
||||
chip), then run the `RIO_TAP` two-handed 8-button mash test. Expect: no
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Apply the RIO v4.2 reply-wedge fix to RIOv4_2.bin -> RIOv4_2_patched.bin.
|
||||
|
||||
Fix (see RIOv4_2-ANALYSIS.md): clear the reply-in-progress latch $2521 on
|
||||
EVERY reply teardown, not just the $2522-gated success path.
|
||||
|
||||
1. Give-up path: redirect $D9DD `JMP $DA2F` to a stub at free ROM $DFF0
|
||||
that clears $2521/$2522 then continues to $DA2F.
|
||||
2. Success path: make $DA00's clear of $2521/$2522 unconditional.
|
||||
|
||||
Each edit asserts the exact original bytes first, so a wrong assumption
|
||||
aborts instead of corrupting the image. Address == file offset.
|
||||
"""
|
||||
import sys, hashlib
|
||||
|
||||
SRC = sys.argv[1] if len(sys.argv) > 1 else "RIOv4_2.bin"
|
||||
DST = sys.argv[2] if len(sys.argv) > 2 else "RIOv4_2_patched.bin"
|
||||
|
||||
d = bytearray(open(SRC, "rb").read())
|
||||
assert len(d) == 0x10000, f"expected 64KB image, got {len(d)}"
|
||||
orig_sha = hashlib.sha256(d).hexdigest()
|
||||
assert orig_sha == "60a88718835c654b6135dbec7721c40ef99dca07df2ad4b57eedeb24037a5f73", \
|
||||
f"unexpected source image {orig_sha}"
|
||||
|
||||
def patch(addr, expect, new):
|
||||
got = bytes(d[addr:addr+len(expect)])
|
||||
assert got == bytes(expect), (
|
||||
f"@${addr:04X}: expected {got.hex()} to be {bytes(expect).hex()}")
|
||||
assert len(new) == len(expect), "length mismatch"
|
||||
d[addr:addr+len(new)] = bytes(new)
|
||||
|
||||
# --- edit 1: give-up path redirect ---------------------------------------
|
||||
# $D9DD 7E DA 2F JMP $DA2F -> 7E DF F0 JMP $DFF0
|
||||
patch(0xD9DD, [0x7E, 0xDA, 0x2F], [0x7E, 0xDF, 0xF0])
|
||||
|
||||
# stub at $DFF0 (was erased $FF): CLR $2521; CLR $2522; JMP $DA2F
|
||||
patch(0xDFF0, [0xFF]*8,
|
||||
[0x7F, 0x25, 0x21, # CLR $2521
|
||||
0x7F, 0x25, 0x22, # CLR $2522
|
||||
0x7E, 0xDA]) # JMP $DA2F (hi + first target byte)
|
||||
patch(0xDFF8, [0xFF], [0x2F]) # JMP low byte
|
||||
|
||||
# --- edit 2: success teardown, unconditional clear -----------------------
|
||||
# $DA21 B6 25 22 LDAA $2522
|
||||
# $DA24 81 01 CMPA #$01
|
||||
# $DA26 26 06 BNE $DA2E
|
||||
# $DA28 7F 25 21 CLR $2521
|
||||
# $DA2B 7F 25 22 CLR $2522 (13 bytes $DA21-$DA2D; $DA2E RTS untouched)
|
||||
# -> CLR $2521 ; CLR $2522 ; NOP x7
|
||||
patch(0xDA21,
|
||||
[0xB6,0x25,0x22, 0x81,0x01, 0x26,0x06, 0x7F,0x25,0x21, 0x7F,0x25,0x22],
|
||||
[0x7F,0x25,0x21, 0x7F,0x25,0x22, 0x01,0x01,0x01,0x01,0x01,0x01,0x01])
|
||||
assert d[0xDA2E] == 0x39, "RTS at $DA2E must be intact"
|
||||
|
||||
open(DST, "wb").write(d)
|
||||
new_sha = hashlib.sha256(d).hexdigest()
|
||||
# byte-diff report
|
||||
diffs = [(a, orig, d[a]) for a, orig in
|
||||
enumerate(open(SRC,"rb").read()) if d[a] != orig]
|
||||
print(f"source : {SRC} sha256 {orig_sha}")
|
||||
print(f"patched: {DST} sha256 {new_sha}")
|
||||
print(f"{len(diffs)} bytes changed:")
|
||||
for a, o, n in diffs:
|
||||
print(f" ${a:04X}: {o:02X} -> {n:02X}")
|
||||
Reference in New Issue
Block a user