RIO working: serial wire tap, conf tuning, four-patch writeup
The RIO cockpit board now runs sustained sessions with button mashing; dropouts self-heal in ~1-3s. Documented in RIO-NOTES.md: - directserial RIO_TAP=<path> (host env): logs every TX/RX byte with host-us + emu-ms timestamps, plus config/RTS/DTR/break lines. This instrument found every root cause below. - Confs: rxburst:16 restored (no-burst reply pacing made the game ACK ~14ms late -> board dropped on the first long analog stream; the old 'rxburst corrupts boot' belief was the then-unpatched PCSPAK crash). priority=highest,highest (unfocused DOSBox was demoted and blew the ACK deadline). - BTL4OPT.EXE patch lineage (in ALPHA_1/, zip left pristine): v2 full DISABLE_AND_DIE NOPs (v1 left the IRQ/RTS-retract prologue live -> first protocol error deafened the driver), v3 TXMAXIDLE 4->32 (kills the button-press ACK-window livelock), v4 analog retry limit 15s->0.5s (dev value was 0.2s; recovery now near-instant). - Board firmware patch plan recorded for the EPROM-dump route. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -79,6 +79,137 @@ 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.5–15s); 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 ~71–86ms "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 `04`→`20`
|
||||
(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).
|
||||
|
||||
## Crash-on-advance fixed: arena terrain shadows
|
||||
|
||||
With the RIO in sync the sim advances and the game crashed dereferencing the
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
[sdl]
|
||||
output=opengl
|
||||
# keep scheduling priority when unfocused -- the default (higher,normal)
|
||||
# demotes DOSBox in the background, adding latency that blows the RIO's
|
||||
# few-ms ACK deadline (confirmed: hands-off run = zero dropouts).
|
||||
# highest = HIGH_PRIORITY_CLASS (higher,higher still showed a dropout).
|
||||
priority=highest,highest
|
||||
[dosbox]
|
||||
memsize=32
|
||||
machine=svga_s3
|
||||
@@ -10,7 +15,7 @@ cycles=max
|
||||
[serial]
|
||||
# RIO on COM1 with the low-latency options (rxpollus/rxburst) so the board's
|
||||
# few-ms ACK deadline is met; plasma COM2 later.
|
||||
serial1=directserial realport:COM1 rxpollus:100
|
||||
serial1=directserial realport:COM1 rxpollus:100 rxburst:16
|
||||
serial2=disabled
|
||||
[autoexec]
|
||||
mount c "C:\VWE\TeslaRel410\ALPHA_1"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
[sdl]
|
||||
output=opengl
|
||||
priority=highest,highest
|
||||
[dosbox]
|
||||
memsize=32
|
||||
machine=svga_s3
|
||||
@@ -8,7 +9,7 @@ core=dynamic
|
||||
cputype=pentium
|
||||
cycles=max
|
||||
[serial]
|
||||
serial1=directserial realport:COM1 rxpollus:100
|
||||
serial1=directserial realport:COM1 rxpollus:100 rxburst:16
|
||||
serial2=disabled
|
||||
[autoexec]
|
||||
mount c "C:\VWE\TeslaRel410\ALPHA_1"
|
||||
|
||||
@@ -29,9 +29,32 @@
|
||||
|
||||
#include "libserial.h"
|
||||
|
||||
#include <chrono>
|
||||
|
||||
/* This is a serial passthrough class. Its amazingly simple to */
|
||||
/* write now that the serial ports themselves were abstracted out */
|
||||
|
||||
static double tap_now_us() {
|
||||
using namespace std::chrono;
|
||||
return duration<double, std::micro>(
|
||||
steady_clock::now().time_since_epoch()).count();
|
||||
}
|
||||
|
||||
/* One log line per byte on the wire. Host-relative microseconds tell the
|
||||
* real-time story (the RIO's ACK deadline is wall clock); PIC_FullIndex()
|
||||
* (emulated ms) ties each byte to game-side timing. */
|
||||
void CDirectSerial::tapLine(char dir, unsigned val, unsigned err) {
|
||||
if (!tap_fp) return;
|
||||
double us = tap_now_us() - tap_t0_us;
|
||||
if (err)
|
||||
fprintf(tap_fp, "%12.1f %11.3f %c %02X err=%02X\n",
|
||||
us, PIC_FullIndex(), dir, val, err);
|
||||
else
|
||||
fprintf(tap_fp, "%12.1f %11.3f %c %02X\n",
|
||||
us, PIC_FullIndex(), dir, val);
|
||||
fflush(tap_fp);
|
||||
}
|
||||
|
||||
CDirectSerial::CDirectSerial (Bitu id, CommandLine* cmd)
|
||||
:CSerial (id, cmd) {
|
||||
InstallationSuccessful = false;
|
||||
@@ -86,6 +109,22 @@ CDirectSerial::CDirectSerial (Bitu id, CommandLine* cmd)
|
||||
(unsigned)rx_burst);
|
||||
}
|
||||
|
||||
// RIO_TAP=<path> (host env, like the VPX_* vars): append one line per
|
||||
// TX/RX byte. Appends across runs; each open writes a header line.
|
||||
const char *tp = getenv("RIO_TAP");
|
||||
if (tp && tp[0]) {
|
||||
tap_fp = fopen(tp, "a");
|
||||
if (tap_fp) {
|
||||
tap_t0_us = tap_now_us();
|
||||
fprintf(tap_fp, "# COM%d tap open (host-us rel | emu-ms | dir | byte)\n",
|
||||
(int)COMNUMBER);
|
||||
fflush(tap_fp);
|
||||
LOG_MSG("Serial%d: RIO_TAP logging to '%s'", (int)COMNUMBER, tp);
|
||||
} else {
|
||||
LOG_MSG("Serial%d: RIO_TAP cannot open '%s'", (int)COMNUMBER, tp);
|
||||
}
|
||||
}
|
||||
|
||||
CSerial::Init_Registers();
|
||||
InstallationSuccessful = true;
|
||||
rx_state = D_RX_IDLE;
|
||||
@@ -94,6 +133,7 @@ CDirectSerial::CDirectSerial (Bitu id, CommandLine* cmd)
|
||||
|
||||
CDirectSerial::~CDirectSerial () {
|
||||
if(comport) SERIAL_close(comport);
|
||||
if(tap_fp) fclose(tap_fp);
|
||||
// We do not use own events so we don't have to clear them.
|
||||
}
|
||||
|
||||
@@ -293,6 +333,7 @@ void CDirectSerial::handleUpperEvent(uint16_t type) {
|
||||
bool CDirectSerial::doReceive() {
|
||||
int value = SERIAL_getextchar(comport);
|
||||
if(value) {
|
||||
tapLine('R', (unsigned)(value & 0xff), (unsigned)((value >> 8) & 0xff));
|
||||
receiveByteEx((uint8_t)(value&0xff),(uint8_t)((value&0xff00)>>8));
|
||||
return true;
|
||||
}
|
||||
@@ -326,6 +367,12 @@ void CDirectSerial::updatePortConfig (uint16_t divider, uint8_t lcr) {
|
||||
else stopbits = SERIAL_2STOP;
|
||||
} else stopbits = SERIAL_1STOP;
|
||||
|
||||
if (tap_fp) {
|
||||
fprintf(tap_fp, "# %12.1f %11.3f config %u baud %d%c%d\n",
|
||||
tap_now_us() - tap_t0_us, PIC_FullIndex(),
|
||||
(unsigned)baudrate, (int)bytelength, (char)parity, (int)stopbits);
|
||||
fflush(tap_fp);
|
||||
}
|
||||
if(!SERIAL_setCommParameters(comport, (int)baudrate, (char)parity, (char)stopbits, (char)bytelength)) {
|
||||
#if SERIAL_DEBUG
|
||||
log_ser(dbg_aux,"Serial port settings not supported by host." );
|
||||
@@ -346,6 +393,7 @@ void CDirectSerial::updateMSR () {
|
||||
}
|
||||
|
||||
void CDirectSerial::transmitByte (uint8_t val, bool first) {
|
||||
tapLine('T', val, 0);
|
||||
if(!SERIAL_sendchar(comport, (char)val))
|
||||
LOG_MSG("Serial%d: COM port error: write failed!", (int)COMNUMBER);
|
||||
if(first) setEvent(SERIAL_THR_EVENT, bytetime/8);
|
||||
@@ -355,20 +403,41 @@ void CDirectSerial::transmitByte (uint8_t val, bool first) {
|
||||
|
||||
// setBreak(val) switches break on or off
|
||||
void CDirectSerial::setBreak (bool value) {
|
||||
if (tap_fp) {
|
||||
fprintf(tap_fp, "# %12.1f %11.3f break=%d\n",
|
||||
tap_now_us() - tap_t0_us, PIC_FullIndex(), value ? 1 : 0);
|
||||
fflush(tap_fp);
|
||||
}
|
||||
SERIAL_setBREAK(comport,value);
|
||||
}
|
||||
|
||||
// updateModemControlLines(mcr) sets DTR and RTS.
|
||||
// updateModemControlLines(mcr) sets DTR and RTS.
|
||||
void CDirectSerial::setRTSDTR(bool rts, bool dtr) {
|
||||
if (tap_fp) {
|
||||
fprintf(tap_fp, "# %12.1f %11.3f rts=%d dtr=%d\n",
|
||||
tap_now_us() - tap_t0_us, PIC_FullIndex(),
|
||||
rts ? 1 : 0, dtr ? 1 : 0);
|
||||
fflush(tap_fp);
|
||||
}
|
||||
SERIAL_setRTS(comport,rts);
|
||||
SERIAL_setDTR(comport,dtr);
|
||||
}
|
||||
|
||||
void CDirectSerial::setRTS(bool val) {
|
||||
if (tap_fp) {
|
||||
fprintf(tap_fp, "# %12.1f %11.3f rts=%d\n",
|
||||
tap_now_us() - tap_t0_us, PIC_FullIndex(), val ? 1 : 0);
|
||||
fflush(tap_fp);
|
||||
}
|
||||
SERIAL_setRTS(comport,val);
|
||||
}
|
||||
|
||||
void CDirectSerial::setDTR(bool val) {
|
||||
if (tap_fp) {
|
||||
fprintf(tap_fp, "# %12.1f %11.3f dtr=%d\n",
|
||||
tap_now_us() - tap_t0_us, PIC_FullIndex(), val ? 1 : 0);
|
||||
fflush(tap_fp);
|
||||
}
|
||||
SERIAL_setDTR(comport,val);
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,12 @@ private:
|
||||
// sitting in the host buffer. n>1 delivers buffered bytes n times
|
||||
// faster, cutting multi-byte reply latency (RIO analog packets).
|
||||
float rx_burst_div = 1.0f;
|
||||
// RIO_TAP=<path> (host env): wire-level byte log ("<host-us> <emu-ms>
|
||||
// T|R <hex>") to verify whether RIO requests reach the wire and when
|
||||
// replies come back. NULL when disabled.
|
||||
FILE *tap_fp = nullptr;
|
||||
double tap_t0_us = 0.0;
|
||||
void tapLine(char dir, unsigned val, unsigned err);
|
||||
bool doReceive();
|
||||
|
||||
#if SERIAL_DEBUG
|
||||
|
||||
Reference in New Issue
Block a user