Files
TeslaRel410/emulator/RIO-NOTES.md
T
CydandClaude Fable 5 673cebf3b8 RIO: firmware v4.2 dump (Toshiba TMP68HC11) + first-look analysis
RIOv4_2.bin dumped from our own board EPROM (64KB, code $C000-$FFFF).
Vector table confirms 68HC11: RESET->$C000, SCI serial interrupt->$D630 =
the protocol state machine entry for the planned disassembly. Board patch
plan steps 1-2 done (RIO-NOTES.md updated); next: disassemble from the
SCI handler, find the reply-path wedge (button/event path survives it),
patch, burn to a fresh EPROM, preserve the original.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 20:21:42 -05:00

14 KiB
Raw Blame History

RIO cockpit controls — passthrough tuning (Phase 5+)

The RIO (Remote Input/Output) is the cockpit control board on COM1 (the plasma display is COM2, handled later). DOSBox-X talks to a real RIO through serial1=directserial realport:COM1 (game_rio.conf). On this host the RIO is a Prolific USB-to-Serial adapter enumerated as COM1.

The analog-poll latency problem (solved)

The initial RIO check request tolerates latency and passed easily. But the runtime control loop (L4CTRL.CPP:1145) sends an analog request every cycle and, if the reply doesn't return inside its window, logs LBE4ControlsManager::Execute, lost RIO analog request and re-requests. The board itself refuses/drops comms if the ACK is late by more than a few milliseconds — a hard real-time deadline.

Empirical result (2026-07-03): a slower CPU made the RIO fail sooner. So the dominant latency was how fast the game processes the RIO packet and emits the ACK, not the serial wiring. The fix:

[cpu]
core=dynamic     ; recompiler — many x faster than the 'normal' interpreter
cputype=pentium
cycles=max       ; full host speed

With core=dynamic + cycles=max the RIO stays in sync (the user confirmed "the rio behaved"). Notes:

  • cycles=fixed 20000 / 150000 and core=normal were all too slow — the RIO dropped comms, faster at lower speeds.
  • The DOSBox-X serial path is already low-latency on transmit (directserial.cpp transmitByte calls SERIAL_sendchar immediately).
  • Prolific PL2303 has no adjustable LatencyTimer registry value (unlike FTDI); an FTDI adapter set to 1 ms latency would be the lowest-latency host option if ever needed.

Receive-latency fork options (2026-07-03)

Even with the fast CPU, intermittent timeouts remained and the game would drop into its 15-second analog retry fallback — the source shows limit = 15.0; // 0.2 in L4CTRL.CPP, so the shipped binary re-requests very slowly once replies stop arriving ("the polling is really slow"). Two emulator receive latencies were fixed with new directserial options:

  • rxpollus:<us> (501000, stock 1000): host-port receive poll tick. Stock DOSBox-X discovers inbound bytes on a 1 ms tick; 100 µs discovers them ~10× sooner. First validated result: the sim advanced and the camera moved in the render window.
  • rxburst:<n> (164): stock DOSBox re-serializes each received byte at emulated wire speed (~1 ms/byte at 9600) even though the bytes already paid their wire time on the physical cable and sit in the host buffer — a 15-byte analog reply gained ~14 ms of artificial latency, single-handedly blowing the RIO's few-ms ACK window. rxburst:16 delivers buffered bytes 16× faster.

game_rio.conf uses realport:COM1 rxpollus:100 rxburst:16.

The crash-to-desktop: PCSPAK's DISABLE_AND_DIE (patched)

The recurring hard fault (Exception 0E, write to 0xFFFFFFFF-ish, e.g. at BTL4OPT CODE+0x7D1D1 with EAX=3) is deliberate: the RIO serial packet driver (CODE/RP/MUNGA_L4/PCSPAK.ASM) was shipped built with DIE_ON_ERROR equ 1, which compiles a DISABLE_AND_DIE <code> debugging macro at 12 error sites — it retracts the UART IRQ, EOIs the PIC, and then "crashes loudly" by writing the error code to address 0xFFFFFFFF.

Our crash is error 3 (PCSPAK.ASM:1630): a byte ≥ 0x80 found in the TX ring body (the protocol reserves high-bit bytes for commands). It gets hit via the ACK/NAK-interrupt → restart path — exactly what physical RIO resets and timeout storms exercise. On clean pod serial timing this never fired; on a USB-serial rig with resets it does.

The source's release configuration (DIE_ON_ERROR equ 0) makes the macro empty and the code recovers (the next instruction masks the byte with and al,7Fh and continues). We reproduce that intended behavior by patching all 12 die sequences (50 52 BA FF FF FF FF B8 xx 00 00 00 89 02 → NOPs) in the working image's BTL4OPT.EXE. Original preserved as BTL4OPT.EXE.orig. Error-code map (from PCSPAK.ASM): 0/1/2 rx framing states, 3 tx body >0x7F, 4/5 tx state.

Patch v2 (2026-07-03): the first patch was incomplete and wedged the driver. The macro's prologue also executes before the crash write: it retracts UART MCR bits IRQ+RTS (RETRACT_MCR) and EOIs the master PIC — 23 bytes (66 8B 15 <addr> 66 83 C2 04 EC 24 F5 EE 66 83 EA 04 B0 20 E6 20) that a release build would not compile at all. With only the crash write NOPed, the first PCSPAK protocol error silently killed the UART IRQ and dropped RTS: the game went deaf/mute while the RIO kept retransmitting into the void (captured on the wire tap at t≈112s — board streaming 89 00 09 FF FF FF FF FE forever, game emitting one byte per 15s retry). Fixed by NOPing the full 37-byte macro expansion at all 12 sites; the state before this extension is preserved as BTL4OPT.EXE.nop14. After patch v2 the driver recovers from every error, exactly like DIE_ON_ERROR equ 0.

Serial wire tap (RIO_TAP, 2026-07-03)

The fork's directserial.cpp logs every serial byte when the host env RIO_TAP=<path> is set: <host-us relative> <emu-ms> T|R <hex> plus # lines for port config, RTS/DTR, and break changes. This is the instrument that proved all of the above. Findings from tapped runs (9600 8N1):

  • Analog request = 82 02 FF FF FF FF FE; ACK = FC; packets are <cmd≥0x80> … FE-framed. The shipped 15s retry shows up as exactly 15.02s between request storms.
  • Without rxburst: reply bytes reach the game at ~1ms/byte (wire-speed re-serialization), so the first long analog stream makes the game's ACK ~14ms late → board drops comms permanently (captured at t≈45.6s). With rxburst:16 the same phase streams for minutes. The earlier "rxburst corrupts the boot handshake" belief was wrong — that corruption was the unpatched DISABLE_AND_DIE error-3 crash. Both confs now use rxpollus:100 rxburst:16.
  • Focus loss caused the self-recovering dropouts. A 4-minute run while the user multitasked showed ~25 self-recovering board-quiet windows (1.515s); an identical hands-off run showed zero gaps after boot (195s clean). DOSBox-X's default [sdl] priority = higher,normal demotes the process to NORMAL class when unfocused. Both gauge confs now set priority=highest,highest (HIGH_PRIORITY_CLASS; user saw one dropout at higher,higher). (The ~7186ms "turnaround tail" in the tap analysis was benign — bursts that need no ACK.)

Button-press livelock (2026-07-03, open)

At highest,highest the link ran clean for 120s, then the user pressed a RIO button and the link died permanently (until board reset). Tap decode of the packet protocol (PCSPAK.ASM equates: FC=ACK, FD=NAK, FE=RESTART, FF=IDLE, cmd bytes 0x80-0xFB, packet = cmd + body + 7-bit checksum):

  • Steady state = game 82 02 analog request → board 87 <12 analog> 07 reply → game FC — at ~10Hz, clean for minutes.
  • At t=123.65s the board truncated its in-flight analog reply exactly when the button was pressed, then went ~3s silent, then began retransmitting the button event 88 03 0B every ~10ms forever. The game ACKs (FC) every copy AND storms its own 82 02 retransmits (RESTART-paced: packet + FF×4 idle + FE restart, back-to-back). Neither side ever accepts: livelock.
  • Mechanism (PCSPAK.ASM txBodyState/txWaitState, and the board firmware is the same protocol): an ACK is honored only during the ~4ms post-checksum idle window (TXMAXIDLE=4 idle chars); an ACK arriving during the peer's transmit phase = TX_EARLY_ERR → restart+retransmit. With both sides' timing driven by each other's bytes, the phases lock and the ACKs land in the wrong window forever. Retry budget TXMAXRESET=3/ TXMAXERROR=3 per packet, but fresh packets keep the storm alive.
  • Why the pod never saw this: ISA-UART ACK latency is sub-ms, so the ACK always lands at the START of the 4ms window. Our Prolific USB-serial adds 1-10ms each way (invisible to the tap, which stamps host-side I/O), so a collision can push the exchange into the locked phase.

Tried, in order:

  • Prolific FIFO disabled in Device Manager: no effect — button mash still livelocked the link at t≈110s.
  • TXMAXIDLE binary patch (v3, 2026-07-03): FIXED the livelock. The two mov txIdleCount[esi],TXMAXIDLE reloads (C6 46 0B 04, txIdleCount = struct offset 0x0B) at exe offsets 0x7dff0/0x7e093 changed 0420 (4 → 32 idle chars ≈ 33ms). This widens the game's ACK-accept window 8x and calms its retransmit storm so its own ACKs to the board transmit promptly. Backup: BTL4OPT.EXE.pre_idle. Result of a 5-minute button-mash run: three board-quiet gaps (1.9s/9.3s/14.6s), all self-recovered; clean steady state otherwise. Previously one button press = permanent livelock until manual board reset.

Recovery-time patch (v4, 2026-07-04). Collision recovery rode L4CTRL's analog re-request fallback: limit = 15.0; // 0.2 (L4CTRL.CPP:1132 — the dev value was 0.2s; 15s looks like a forgotten debug slowdown). In the binary this is two mov dword [ebp-24h], 41700000h (15.0f) immediates — one per branch of the RunningMission if — at file offsets 0x763fa/0x76403, anchored by the fld/fcomp of delta_t and the "lost RIO analog request" string push at 0x76441 directly after. Both immediates changed to 3F000000h (0.5f). Backup: BTL4OPT.EXE.pre_limit. Validated 2026-07-04: 5-minute two-handed button-mash run — forced dropouts now last 1.2s/2.9s (vs 9.3s/14.6s under the 15s limit), max turnaround 199ms (vs 11.8s), and most collisions no longer register as

1s gaps at all. User: "after one more button is pressed and the .5 seconds elapses it picks right back up."

BTL4OPT.EXE patch lineage: .orig (pristine) → .nop14 (v1: crash writes NOPed) → .pre_idle (v2: full 37-byte DISABLE_AND_DIE NOPs) → .pre_limit (v3: TXMAXIDLE 4→32) → current (v4: retry limit 15s→0.5s).

Board firmware patch plan (user wants to pursue)

Goal: fix the livelock's other half at the root — the board rejects ACKs arriving outside its ~4ms post-checksum window (its firmware mirrors the PCSPAK state machine, including the early-ACK=error behavior). The game-side patches make failures self-healing; a board patch would make collisions harmless entirely.

  1. Identify: open the RIO board, photograph it, note the MCU part number and the ROM chip. Era suggests an 8051-family MCU (80C31/32 with external 27C256/27C512 EPROM) or 68HC11/Z80-class part. The observed wire behavior (9600 8N1, ~10ms retransmit cadence = 3-byte packet + ~4 idle chars + restart) confirms a TXMAXIDLE≈4-equivalent constant in firmware.
  2. Dump: any TL866-class programmer reads the EPROM to a .bin. If the code is in MCU-internal ROM instead, dumping gets harder (part-specific tricks) — check the board first.
  3. Disassemble (Claude's job): locate the protocol state machine by searching for 0xFC/0xFD/0xFE/0xFF handling, the idle-counter reload value 4, and checksum AND 7Fh operations — we know the protocol byte-for-byte from the game side, so this is pattern matching.
  4. Patch (preference order): (a) accept ACK in any TX state — delete the early-ACK=error path; (b) widen the idle window 4→32 like the game-side v3 patch; (c) raise the retry budget.
  5. Burn to a NEW EPROM, socket it, label and store the original chip untouched (preservation first).
  6. Validate with the RIO_TAP button-mash protocol; compare dropout counts against the 2026-07-03/04 baseline captures in the session scratchpad (riotap_*.txt).

No firmware source or image exists in the archive (searched sda4 + CODE for .HEX/.A51/.S19/.ROM and for the protocol constant names — only PCSPAK.ASM, the game side, matches).

UPDATE 2026-07-04: steps 1-2 DONE. The board's MCU is a Toshiba TMP68HC11 (read off the chip) and the user dumped the EPROM: emulator/rio-firmware/RIOv4_2.bin (v4.2, 64KB image, code in $C000-$FFFF). First-look analysis in emulator/rio-firmware/README.md — reset vector $C000, SCI serial interrupt vector → $D630 (the protocol state machine's entry point for step 3's disassembly). Also new evidence for the wedge shape: a button press revives a mute board (event path alive, reply path dead), so step 4 should look for a reply-path-only dead state reachable from the SCI handler.

Crash-on-advance fixed: arena terrain shadows

With the RIO in sync the sim advances and the game crashed dereferencing the mech terrain shadow renderable: *_TSHD.BGF live only in VIDEO/GEO/ARENA/ + …/POLAR/, which the object search path misses, so thr_tshd.bgf failed to load and left a null renderable that the moving sim eventually touched. Fix: the 11 *_TSHD.BGF files are copied from ARENA/ into VIDEO/GEO/ in the working image (reversible, one file each). After the fix the game runs sustained (500+ frames, no crash).

Next wall: mission content (a game-data issue, not protocol/RIO)

With the RIO feeding real input, the simulation advances — and then the game faults:

32loader runtime error: Unhandled exception
Exception 0E at 00FF:0040223B
Module 'BTL4OPT.EXE' section 'CODE' offset 0000123B
[..] 8B 4D 0C (mov ecx,[ebp+0C])  8B 11 (mov edx,[ecx])   ECX=2
The instruction referenced illegal address 00000002

A near-null pointer (2) dereference: a function got 2 where an object pointer was expected. This is downstream of the failed content load logged just before it:

Entity -1:63 class:42 couldn't figure out how to MakeEntityRenderables
L4VIDEO.cpp couldn't load object thr_tshd.bgf

thr_tshd.bgf exists in VIDEO/GEO/ARENA/ and VIDEO/GEO/POLAR/, but not the top VIDEO/GEO/. The game builds its object search path from objectpath entries in the mission notation file (L4VIDEO.CPP:1852), i.e. from the .egg. The shipped test.egg is only mission parameters and does not set up the arena object path, so arena-specific objects aren't found; the entity gets a broken renderable and the sim eventually dereferences it.

To progress past this needs a real mission/arena .egg (or an object path manually pointed at the selected arena's VIDEO/GEO/<ARENA> subdir). That is content/mission configuration, separate from the VPX protocol, the renderer, and the RIO — all of which now work.