From a8ec285b8ee6a87ad430d44e996d8099ae2d4467 Mon Sep 17 00:00:00 2001 From: Cyd Date: Fri, 17 Jul 2026 20:25:00 -0500 Subject: [PATCH] =?UTF-8?q?Firmware:=20RIOv4=5F2=5Fpatched=5F31250.bin=20?= =?UTF-8?q?=E2=80=94=20wedge=20fix=20+=20SCI=20retuned=20to=2031250=20baud?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- rio-firmware/README.md | 10 + rio-firmware/RIOv4_2-ANALYSIS.md | 25 + rio-firmware/RIOv4_2_patched_31250.bin | 38 + rio-firmware/RIOv4_2_patched_31250.disasm.asm | 13133 ++++++++++++++++ rio-firmware/make_patch.py | 24 +- src/RioJoy.Core/Serial/SerialPortTransport.cs | 15 +- tools/RioSerialMonitor/MashTest.cs | 7 +- tools/RioSerialMonitor/Program.cs | 20 +- 8 files changed, 13258 insertions(+), 14 deletions(-) create mode 100644 rio-firmware/RIOv4_2_patched_31250.bin create mode 100644 rio-firmware/RIOv4_2_patched_31250.disasm.asm diff --git a/rio-firmware/README.md b/rio-firmware/README.md index 4d5bf5b..71a8def 100644 --- a/rio-firmware/README.md +++ b/rio-firmware/README.md @@ -40,3 +40,13 @@ disassemble as 68HC11 from `$C000` with the vector entries as roots, find the SCI state machine (protocol constants FC=ACK FD=NAK FE=RESTART FF=IDLE, idle-reload-4 patterns), patch the early-ACK/error wedge path or widen its window, burn a new EPROM, keep this original safe. + +## Patched images (built by make_patch.py; see RIOv4_2-ANALYSIS.md) + +- **`RIOv4_2_patched.bin`** — the reply-latch wedge fix, 23 bytes changed. + sha256 `3fc8170caf60e2580641724ff995176c93c4f2e706f31487beded8233142493f`. +- **`RIOv4_2_patched_31250.bin`** (`make_patch.py --baud31250`) — wedge fix + + SCI retuned 9600 → 31250 baud (one more byte, `$D62B: 30→02`). + sha256 `9f866cf353d04906e1d3e3847b6375eaae251c987b656f68f093e61ba1bd545b`. + Bench/RIOJoy-only until the native games get baud patches; needs an + FTDI-class adapter (16550s can't make 31250). Test tools take `--baud 31250`. diff --git a/rio-firmware/RIOv4_2-ANALYSIS.md b/rio-firmware/RIOv4_2-ANALYSIS.md index d95d3f5..59443dd 100644 --- a/rio-firmware/RIOv4_2-ANALYSIS.md +++ b/rio-firmware/RIOv4_2-ANALYSIS.md @@ -148,6 +148,31 @@ three regions with no downstream desync: Flash `RIOv4_2_patched.bin` directly to the W27C512 (DIP-28). This is static verification only; dynamic proof still needs the burned chip. +### 31250-baud variant (2026-07-17) — `RIOv4_2_patched_31250.bin` + +`make_patch.py --baud31250` adds **one byte** to the wedge patch: the SCI +init at `$D62A` (`LDAA #$30 ; STAA $102B BAUD`) becomes `LDAA #$02`. +BAUD `$30` = prescale ÷13, divider ÷1 → 2 MHz E-clock / (16·13) = 9615 +("9600"); `$02` = prescale ÷1, divider ÷4 → 2 MHz / (16·4) = **31250 +exactly** (3.3× faster; analog exchange ~15 ms → ~4.6 ms; 96-lamp burst +~0.4 s → ~0.12 s; every collision window shrinks 3×). The BAUD write +confirms the 8 MHz crystal / 2 MHz E-clock — 19200/38400 are unreachable. + +sha256 `9f866cf353d04906e1d3e3847b6375eaae251c987b656f68f093e61ba1bd545b`, +24 bytes changed (the 23 wedge bytes + `$D62B: 30→02`). Re-disassembly +diff vs the classic patched image shows exactly the one operand line. + +**Caveats:** 31250 is non-standard — FTDI-class USB adapters make it +exactly, classic 16550 UARTs cannot; the **native games still speak +9600**, so a 31250 chip is bench/RIOJoy-only until Firestorm/Red Planet +get BTL4OPT-style baud patches; and the 2 MHz HC11's RX ISR has ~640 +cycles/byte at this rate (fine) — do NOT be tempted to `$01`/62500 or +`$00`/125 k without cycle-counting the `$D630` ISR worst path first. +PC side: `SerialPortTransport` takes a baud parameter and the +monitor/mash tools accept `--baud 31250`. Validate the wedge patch at +9600 FIRST (one variable at a time), then A/B this chip with +`--mash COM1 300 --label patched-31250 --baud 31250`. + ### 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 diff --git a/rio-firmware/RIOv4_2_patched_31250.bin b/rio-firmware/RIOv4_2_patched_31250.bin new file mode 100644 index 0000000..f3a374b --- /dev/null +++ b/rio-firmware/RIOv4_2_patched_31250.bin @@ -0,0 +1,38 @@ +$"f qFLiÐÙƽôýâë,ʽ5ӽ$$ J K M 4 7  N$ɽ|$!$B$C$G$E$I$F$J$$K$$H$1111% %!%"Ty1y~y˽y~<<67̽4hɜ$F$E32889}$I'|$J$J&$!'м$J$I9̭#""9    =  =   @#""9 #""9^ q9oZ&J&9í$E#; @&    @ @#; $I#;%;<<76 %;% +&~8 & +&G ~8& +&6 &0~$') +&  &&~8$!˧A%523889~;&F(;'~O& ̽;'~^& ;'u~o& 4;'d~€ & h;'S~‘& ɜ;'B~¢&%ұB;'\~¹&ҽJ;'H~Ё&*p;'4~˧; '~A|,$!9&ν~;'~~%~ÐÙi M9 K~S J91Ԣ%9 P۰d< 1 18Z&9" + 32< 1 18Z&z 3&9`!b>9H#ν>9H!½>9$>9H$T>9H$>9$Ļ3U9%08 (9 >9 >9oZ&9   Π)Π!Π#Π%Π' 9 Π) 9 Π! 9 Π# 9 Π% 9 Π' 9 + '>9$"91D1X 9i&$"1''X' +''9~~ƿâë$$!J1Ѱ$ɽ1S~bNJǵ$!99˽ 9~ƾi&1'~ƏLiÐÙƽôýâë,$$ J K M 4 7  N$ɽ|$!$B$C$G$E$I$F$J$$K$$H$1111% %!%"~ƾ&z~ƾ&Ċ~ƾ&Ě~ƾ&Ī~ƾ&ĺ9i&1$Gi&1$K$GH$?967<<-A-B;8832967<<-A-B$!;8832967<<-A-B%%;8832967<<-A-B%%;8832967<<-A-B1;8832967<<-A-B1;8832967<<-A-B1;8832967<<$!'0 '% +%-B -A<%%8z%&;88329$!'-A$ -B 7;9$!'#%-A$ -B% J;9$!'#%-A$ -B% K;99 LΠ( Π8 !ʗ #ʰ &   + 9 LΠ Π0 !ʗ #ʰ &    9 LΠ" Π2 !ʗ  #ʰ &     9 LΠ$ Π4 !ʗ  #ʰ &     9 LΠ& Π6 !ʗ  #ʰ &     9 ' L&#  H$ H~ʖ H~ʖ&#  H$ H~ʖ H~ʖ&#  H$ H~ʖ H~ʖ &#  H$ H~ʖ H~ʖ&  H$ H ~ʖ H 9  & ! %9< 89O_ % & #'|   ҽ9<6$!& 8  H  H 6I289<6$!& 8 % H & H 6I289 HDDDD9O L H 9 8962962962962962967<<  N P !̲8832967<<  N : P2: :!̲88329  4 4&~&O~&̀~ͱ "Z& 5 +< 1 18Z&z 5&z 4&9  + + 9 + +  9 + +   9 + + 9  6 5 NZ'D~&r~| z 5&z 6&967<< !:&7< 1 1 : N& J8~d K8$ Ȝ~l$ o8832967<< !:'@< 1 1 : N& J8~κ K8$ Ȝ~Ȇ$ J%o88329<6$!& 8 K H O L6I289 M& +$ ~$ # M& +$ +~0$ +967<< 7$B N$!P϶| NV!P϶| N܈!P϶ܺ!P϶| N!P϶!P϶P!P϶| N| N| N݂!P϶| Nݴ!P϶883299D967<<!R!PZ& 6 +< 1 18Z&z 6&8832967< 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 -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" +BAUD31250 = "--baud31250" in sys.argv +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_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)}" @@ -52,6 +64,14 @@ patch(0xDA21, [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]) + open(DST, "wb").write(d) new_sha = hashlib.sha256(d).hexdigest() # byte-diff report diff --git a/src/RioJoy.Core/Serial/SerialPortTransport.cs b/src/RioJoy.Core/Serial/SerialPortTransport.cs index 49c1617..72dec33 100644 --- a/src/RioJoy.Core/Serial/SerialPortTransport.cs +++ b/src/RioJoy.Core/Serial/SerialPortTransport.cs @@ -11,7 +11,7 @@ namespace RioJoy.Core.Serial; /// public sealed class SerialPortTransport : IRioTransport { - /// RIO link bit rate. + /// Default RIO link bit rate (stock v4.2 firmware). public const int BaudRate = 9600; /// DTR reset-pulse hold time on open. @@ -23,11 +23,18 @@ public sealed class SerialPortTransport : IRioTransport private readonly SerialPort _port; private readonly Stream _stream; - public SerialPortTransport(string portName) + /// COM port name, e.g. "COM1". + /// + /// Link rate; default 9600 (stock firmware). The 31250-baud firmware + /// variant (rio-firmware/, --baud31250) needs an adapter that can make + /// non-standard rates exactly — FTDI-class USB serial can, classic + /// 16550 UARTs cannot. + /// + public SerialPortTransport(string portName, int baudRate = BaudRate) { if (string.IsNullOrWhiteSpace(portName)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(portName)); - _port = new SerialPort(portName, BaudRate, Parity.None, 8, StopBits.One) + _port = new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One) { Handshake = Handshake.None, // The framing state machine handles timing; keep reads non-throwing. @@ -45,7 +52,7 @@ public sealed class SerialPortTransport : IRioTransport _stream = _port.BaseStream; } - public string Description => $"{_port.PortName} @ {BaudRate} 8N1"; + public string Description => $"{_port.PortName} @ {_port.BaudRate} 8N1"; public Task ReadAsync(byte[] buffer, CancellationToken cancellationToken) => _stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken); diff --git a/tools/RioSerialMonitor/MashTest.cs b/tools/RioSerialMonitor/MashTest.cs index 23a4549..1f10739 100644 --- a/tools/RioSerialMonitor/MashTest.cs +++ b/tools/RioSerialMonitor/MashTest.cs @@ -51,6 +51,7 @@ internal static class MashTest bool lampEcho = true; double wedgeSec = 2.0; bool selftest = false; + int baud = 9600; var positional = new List(); for (int i = 0; i < args.Length; i++) @@ -63,6 +64,8 @@ internal static class MashTest case "--label" when i + 1 < args.Length: label = args[++i]; break; case "--wedge" when i + 1 < args.Length && double.TryParse(args[i + 1], out double w): wedgeSec = w; i++; break; + case "--baud" when i + 1 < args.Length && int.TryParse(args[i + 1], out int b): + baud = b; i++; break; default: positional.Add(args[i]); break; } } @@ -96,7 +99,7 @@ internal static class MashTest lock (gate) { Console.WriteLine(msg); logFile.WriteLine(msg); } } - Raw($"== RIO mash test :: {port} @ 9600 8N1, {seconds}s, chip label '{label}' =="); + Raw($"== RIO mash test :: {port} @ {baud} 8N1, {seconds}s, chip label '{label}' =="); Raw($" lamp echo {(lampEcho ? "ON (drives reply/lamp collisions)" : "OFF")}, " + $"wedge threshold {wedgeSec:F1}s, app auto-recovery DISABLED"); Raw($" log: {Path.GetFullPath(logPath)}"); @@ -104,7 +107,7 @@ internal static class MashTest IRioTransport transport; try { - transport = selftest ? new SelftestTransport() : new SerialPortTransport(port); + transport = selftest ? new SelftestTransport() : new SerialPortTransport(port, baud); } catch (Exception ex) { diff --git a/tools/RioSerialMonitor/Program.cs b/tools/RioSerialMonitor/Program.cs index aea8373..c3f8298 100644 --- a/tools/RioSerialMonitor/Program.cs +++ b/tools/RioSerialMonitor/Program.cs @@ -8,17 +8,25 @@ using RioJoy.Core.Serial; // log every event (buttons, keypad, axis, version/check, control bytes, framing). // It flashes all lamps once to prove the PC -> RIO output path, then echoes a lamp // on each button press so a physical press lights up. -// dotnet run --project tools/RioSerialMonitor -- [port] [seconds] +// dotnet run --project tools/RioSerialMonitor -- [port] [seconds] [--baud rate] // Firmware wedge-patch validation (RIO_TAP mash test, see MashTest.cs): // dotnet run --project tools/RioSerialMonitor -- --mash [port] [seconds] -// [--label baseline|patched] [--no-lamps] [--wedge seconds] +// [--label baseline|patched] [--no-lamps] [--wedge seconds] [--baud rate] +// --baud: 31250 for the retuned firmware variant (rio-firmware --baud31250). // Exit: 0 = ran, 2 = could not open the port (mash: 1 = wedge detected). if (args.Contains("--mash")) return await RioSerialMonitor.MashTest.RunAsync(args); -string port = args.Length > 0 ? args[0] : "COM1"; -int seconds = args.Length > 1 && int.TryParse(args[1], out int s) ? s : 30; +int baud = 9600; +var positional = new List(); +for (int i = 0; i < args.Length; i++) +{ + if (args[i] == "--baud" && i + 1 < args.Length && int.TryParse(args[i + 1], out int b)) { baud = b; i++; } + else positional.Add(args[i]); +} +string port = positional.Count > 0 ? positional[0] : "COM1"; +int seconds = positional.Count > 1 && int.TryParse(positional[1], out int s) ? s : 30; Dictionary groupOf = CockpitPanel.Buttons().ToDictionary(b => b.Address, b => b.Group.Title); string Name(int addr) => groupOf.TryGetValue(addr, out string? g) ? $"0x{addr:X2} ({g})" : $"0x{addr:X2}"; @@ -30,12 +38,12 @@ void Log(string msg) lock (gate) Console.WriteLine($"[{sw.Elapsed.TotalSeconds,6:F2}s] {msg}"); } -Console.WriteLine($"== RIO serial monitor :: {port} @ 9600 8N1 for {seconds}s =="); +Console.WriteLine($"== RIO serial monitor :: {port} @ {baud} 8N1 for {seconds}s =="); SerialPortTransport transport; try { - transport = new SerialPortTransport(port); + transport = new SerialPortTransport(port, baud); } catch (Exception ex) {