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>
101 lines
4.5 KiB
Python
101 lines
4.5 KiB
Python
#!/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.
|
|
|
|
With --baud31250, additionally retune the SCI from 9600 to 31250 baud
|
|
(-> RIOv4_2_patched_31250.bin): the init at $D62A loads BAUD ($102B) with
|
|
$30 (prescale /13, divider /1 -> 2MHz E / (16*13) = 9615). $02 selects
|
|
prescale /1, divider /4 -> 2MHz / (16*4) = 31250 exactly. One operand
|
|
byte at $D62B. NOTE: 31250 is a non-standard rate — FTDI-class USB
|
|
adapters produce it exactly; classic 16550 UARTs cannot. The native
|
|
games still expect 9600, so a 31250 chip is bench/RIOJoy-only until the
|
|
games are patched.
|
|
|
|
Each edit asserts the exact original bytes first, so a wrong assumption
|
|
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())
|
|
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"
|
|
|
|
# --- edit 3 (--baud31250 only): SCI 9600 -> 31250 --------------------------
|
|
# $D62A 86 30 LDAA #$30 (SCP=/13, SCR=/1) -> 86 02 (SCP=/1, SCR=/4)
|
|
# then STAA $102B (BAUD). 2MHz E-clock: 2e6/(16*13)=9615 -> 2e6/(16*4)=31250.
|
|
if BAUD31250:
|
|
assert d[0xD62A] == 0x86 and bytes(d[0xD62C:0xD62F]) == bytes([0xB7,0x10,0x2B]), \
|
|
"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
|
|
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}")
|