Move RIO hardware + firmware archive to the TeslaRel410 repo

The physical RIO board docs (photos, schematics, GAL decode) and the
board firmware (dumps, disassembly, make_patch.py, RIO 4.3 + FastRIO
images, analysis, testlogs) describe hardware shared across all Tesla
cockpits with a lifecycle independent of this Windows app — a
native-game-only cabinet runs the firmware and never touches RIOjoy.
They now live in TeslaRel410/restoration/{rio-hardware,rio-firmware}
with full history preserved (git subtree).

Kept here: docs/PROTOCOL.md (this app's interface contract) and the
RioSerialMonitor bench harness (--mash/--e0test, C# on RioJoy.Core).
README + code comments now point at the TeslaRel410 archive.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-19 20:50:46 -05:00
co-authored by Claude Fable 5
parent adfcee4bac
commit 30c1a85445
64 changed files with 10 additions and 133660 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ Red Planet — talk to the RIO directly and do not use this app.)
| [`tools/XcfRegionExtract`](tools/XcfRegionExtract/) | Extracts cockpit label regions from `riojoy.xcf``regions.json` |
| [`docs/PLAN.md`](docs/PLAN.md) | Full modernization plan (7 phases) |
| [`docs/PROTOCOL.md`](docs/PROTOCOL.md) | RIO wire format + `iRIO` input-map reference |
| [`docs/hardware/`](docs/hardware/) | Board photos, scanned schematics, GAL fuse dump — the physical RIO board set |
| _RIO board hardware & firmware_ | Moved to the [TeslaRel410 `restoration/`](https://gitea.mysticmachines.com/VWE/TeslaRel410/src/branch/main/restoration) archive — board photos, schematics, GAL decode (`restoration/rio-hardware`) and the RIO 4.3 board firmware (`restoration/rio-firmware`) |
| [`docs/reference/`](docs/reference/) | Cockpit overlay art & the legacy labeling pipeline |
| [`legacy/`](legacy/) | Original C++/vJoy implementation, kept as reference |
-170
View File
@@ -1,170 +0,0 @@
# RIO cockpit hardware — boards, schematics, GAL dump
Physical documentation of the VWE Tesla-cockpit **RIO** (Remote Input/Output)
board set, gathered during the 2026 restoration: board photos (Signal,
2026-07-15), a scan of the original paper wire schematics, and the fuse map
read out of the RIO board's GAL address decoder.
All boards are **Copyright 1994 V.W.E. Inc.** — the schematic title blocks
spell it out: **Virtual World Entertainment, 1100 W Cermak Suite B404,
Chicago, Illinois**, every sheet and silkscreen credited **"Design by M.C."**
Schematic plots are dated 1/11/1996 (peripheral boards) and 6/26/1996 (RIO
board), so these drawings post-date the Rev. 0/1 boards they describe.
## Board family
| P/N | Title | Rev photographed | Key silicon |
|-----|-------|------------------|-------------|
| **1407** | RIO board ("CPU board") | Rev. 3 | Toshiba **TMP68HC11** @ 8 MHz, GAL20V8A decoder, 27C512 EPROM, LH52B256 32K SRAM, 5× HCTL-2016 |
| **1401** | Buttons board | Rev. 0 | 2× UCN5821A lamp drivers, 2× 74HCT165, 74HCT164, 74LS00/05 |
| **1402** | Keypad board | Rev. 1 | 2× 74HCT164, CD74HCT165, 74LS00/05 |
| **1408** | Display board ("RIO external display") | Rev. 1 | Intersil **ICM7228B** 8-digit driver, 8× LTS313A 7-segment |
| **1413** | Quad Amplifier Board | Rev. 1 | **Pyramid PB-150P "Pro Plus"** car amp (bolt-on) + passive 3-way crossovers |
Plus the cockpit position sensors: **HP HEDS-5700** two-channel quadrature
optical encoders (option code **I** = 512 counts/rev; the photographed unit is
date-coded 9515, Singapore), one per analog axis, read by the five HCTL-2016
counters on the RIO board (`OPT1``OPT5` = stick X/Y, throttle, left/right
pedals).
## RIO board 1407 (schematic sheets 13, `RIO_1407`)
**Sheet 1 — CPU : Memory Decoder.** The TMP68HC11 runs in expanded
multiplexed mode: a 74HCT573 latches A[0:7] off the AD bus, and **U7 — drawn
as a 20L8, fitted as a GAL20V8A-15LP** — decodes A[8:15] into `ROM_SEL*`
(27C512 EPROM, U10), `RAM_SEL*` (LH52B256-70LL 32K SRAM, U11), `PH_SEL*`
(HCTL-2016 bank via two 74LS138s), `DSP_SEL*` (external display port) and
`BND_SEL` (I/O-interface latch, sheet 3). Each HCTL-2016 feeds a 5-pin
header (`J4``J8`) carrying CHA/CHB from a HEDS-5700. The DB9 COM port goes
through a MAX231/MAX232-class level shifter; the schematic's `IN_RESET*` from
the serial side is the hardware behind the **DTR reset pulse** described in
[PROTOCOL.md](../PROTOCOL.md) §1. The "display interface" block (ALS541
buffers, HEADER14) exports latched data lines `HD[00:04]` + digit address
`AS0-2` to the display board — this is a memory-mapped port, *not* the pod
bus. A 22V10 site (U31) is drawn mostly N/C and is **unpopulated** on the
photographed board.
**Sheet 2 — Power & Reset Interface.** A MAX690 supervisor provides
power-fail reset and a **watchdog (WDI)**; reset can also come from the
front-panel `RESET` toggle (SW1) or remotely through an **H11L1 optocoupler**
fed from an isolated 3-pin `+5 VDC` input (J8) — the game PC can hard-reset
the board. The `ABORT` toggle (SW2) pulls the HC11 `IRQ*`. A 74LS241 drives
the status LED bank: `POWER ON`, `FAULT`, `RESET`, `RUN`, `TX`, `RX`. Power
enters on J9 through 5 A/10 A fuses, LC filters and 1N5341B clamp zeners,
producing separate logic `VCC` and peripheral `VCC_EXT` rails.
**Sheet 3 — I/O Interface ("pod bus").** Eight identical 16-pin IDC ports
(`J11``J18`) fan out through ALS541 buffers and SIP resistor packs. All
ports share the outbound signals; each port has its own return line
(`DI[0..7]`, selected by a 74LS138 from `SEL0-2`) and its own buffered
brightness line (`BRT_CTR0-7`, all copies of one PWM signal). A 74HC574
latch written via `BND_SEL` holds the bus control bits (`SEL0-2`, `RD_SB`,
`WR_SB`, `D_OUT`, `CCK`) — so the firmware bit-bangs the bus by rewriting
this latch.
### Pod bus signal set (per port)
| Signal | Direction | Purpose |
|--------|-----------|---------|
| `CCK` | RIO → pod | shift clock |
| `DD_IN` | RIO → pod | serial data into the pod's 74HCT164s (address select + payload) |
| `WR_SB` | RIO → pod | write strobe (latch lamp/display data) |
| `RD_SB` | RIO → pod | read strobe (parallel-load the pod's 74HCT165) |
| `DD_OUT` | pod → RIO | serial readback, **open-collector 74LS05** so pods can share a cable |
| `BRT_CTR` | RIO → pod | global lamp-brightness PWM, gates the UCN5821A output enable |
| `VCC_EXT`, GND | — | peripheral power rail |
Every pod board carries a 74HCT164 whose parallel outputs go to an
**`ADR. SEL.` jumper block (`A-0``A-7`)**: the RIO shifts an address
pattern down the cable, the jumper picks which bit this board answers to,
and 74LS00 gates qualify `RD_SB`/`WR_SB` with that selection. This
hardware addressing is what the 0x000x6F logical input map in
[PROTOCOL.md](../PROTOCOL.md) ultimately resolves to.
## Peripheral boards
- **1401 Buttons board** (`PBE_1401`) — eight 4-pin connectors (`J3``J10`),
pinout `1 = buttons GND, 2 = button in, 3 = lamps +5V, 4 = lamps logic GND`.
Two UCN5821A serial-in latched sink drivers light the button lamps
(brightness = `BRT_CTR` PWM on their OE), two 74HCT165s read the buttons
back, one of them also echoing board status/address.
- **1402 Keypad board** (`KEY_1402`) — hosts the COTS 16-key hex pad (0F)
on an 8-line header (4×4 matrix). A second 74HCT164 drives the matrix
rows through open-collector 74LS05 inverters; the 74HCT165 reads the four
columns (`IN0-3`, 2.7K pull-ups) plus a board-detect line `BRD_DT*`.
- **1408 Display board** (`DSP_1408`) — ICM7228B driving eight LTS313A
digits (hex decode) from the RIO's dedicated 14-pin *external display*
port; write-only, plus an isolated 26-pin **"IBM printer port"** LED
monitor block. Silkscreen reads "P/N 140B" but the schematic title block
confirms **1408**. Full pin-level output definition — and what the
firmware actually displays (boot banner + the keypad-A-9-E test mode) —
in [display-board-1408.md](display-board-1408.md).
- **1413 Quad Amplifier Board** (`AMP_PLT`, silkscreen "Quad Amplifier
Board … P/N 1413 REV. 1") — the actual amplification is a stock
**Pyramid PB-150P "Pro Plus"** 4-channel 12 V car-audio amplifier bolted
to the board as a module; the schematic only drew the VWE-designed
passive circuitry around it. The board feeds the Pyramid's `B+12V / GND
/ REMOTE` screw terminals and speaker-level `HIGH INPUT` connectors, and
its four outputs (`1CH/L``4CH/R`) return through terminal strips into
per-corner 3-way passive crossovers: Dale IHB-3 inductors (820 µH woofer
low-pass, 180 µH midrange, date-coded 9531), MMP 100 VDC film capacitors
(2.2 µF/10 µF), and Dale CP-5 2 Ω 5 W pad resistors. Corner speaker
connectors J1J4 (front/back × left/right) are 6-pin: `1 woofer+,
2 midrange+, 3 tweeter+, 4 woofer, 5 midrange, 6 tweeter` (silkscreen,
matching sheet 5). Power enters on J5 (4-pin: `+12VDC ×2, GND ×2`) with a
green POWER ON LED; the per-channel red LED pairs across the outputs are
the schematic's "speaker outputs checking circuit". PCB etch NT 1-0A 3695
— the same fab batch series (week 36 '95) as the buttons board.
## GAL dump — [`gal/GAL20v8a_5764.JED`](gal/GAL20v8a_5764.JED)
Fuse map read 2023-03-09 from **U7 on the 1407 board** (GAL20V8A-15LP,
hand-labeled **"5764"** — which matches the JEDEC fuse checksum `*C5764`, the
source of the file name). Per sheet 1 it is the CPU **memory decoder**
(drawn as a 20L8). **Decoded** — equations, the recovered memory map
(`$2000-$9FFF` SRAM, `$A000/$A010/$A020` I/O windows, `$C000-$FFFF` EPROM)
and the firmware cross-check are in [`gal/README.md`](gal/README.md); the
chip's programmed signature even reads `U7`.
## Schematic scan — [`schematics/Scans_018-014.pdf`](schematics/Scans_018-014.pdf)
| PDF page | Sheet | Title block |
|----------|-------|-------------|
| 1 | RIO_1407 sheet 1/3 | CPU : Memory Decoder |
| 2 | RIO_1407 sheet 2/3 | Power & Reset Interface |
| 3 | RIO_1407 sheet 3/3 | I/O Interface |
| 4 | PBE_1401 1/1 | Buttons Board |
| 5 | AMP_PLT (P/N 1413) 1/1 | Amplifier Board |
| 6 | KEY_1402 1/1 | Keypad Board |
| 7 | DSP_1408 1/1 | Display Board |
## Photo index ([`photos/`](photos/))
Original Signal filenames preserved here for provenance (taken 2026-07-15;
amplifier board 2026-07-19).
| File | Original | Shows |
|------|----------|-------|
| `encoder-heds5700-control-assembly.jpeg` | `signal-2026-07-15-093011.jpeg` | HP HEDS-5700 I02 encoder (9515 A, Singapore) on a control-mechanism shaft |
| `display-board-1408-front-digits.jpeg` | `signal-2026-07-15-093017.jpeg` | Display board digit side, 8× LTS313A (U2U9), PCB etch NT 1-0 5094 |
| `display-board-1408-back-driver.jpeg` | `signal-2026-07-15-093023.jpeg` | Display board driver side: ICM7228BIPI (date 9438), 16-pin IDC, "RIO EXTERNAL DISPLAY / P/N 140B REV. 1" |
| `keypad-1402-front-keys.jpeg` | `signal-2026-07-15-093032.jpeg` | 16-key hex keypad module (keys 0F), front bezel |
| `keypad-board-1402-back-logic.jpeg` | `signal-2026-07-15-093038.jpeg` | Keypad board logic: DM74LS00N, 2× MM74HCT164N, SN74LS05N, CD74HCT165E, ADR. SEL. jumpers, "P/N 1402 REV. 1" |
| `buttons-board-1401-solder-side.jpeg` | `signal-2026-07-15-093045.jpeg` | Buttons board solder side, PCB etch NT 1-0 3695 |
| `buttons-board-1401-component-side.jpeg` | `signal-2026-07-15-093051.jpeg` | Buttons board: 2× CD74HCT165E, 74HCT00AP, 2× UCN5821A-type drivers, 74HCT164N, 8× lamp/button connectors, "P/N 1401 REV. 0" |
| `rio-board-1407-solder-side.jpeg` | `signal-2026-07-15-093058.jpeg` | RIO board solder side, PCB etch NT 3-0 4996 |
| `rio-board-1407-component-side.jpeg` | `signal-2026-07-15-093105.jpeg` | RIO board: TMP68HC11 QFP, GAL20V8A ("5764" sticker), AM27C512 EPROM, LH52B256 SRAM, 5× HCTL-2016 + OPT15, J11J18 pod ports, status LEDs, "P/N 1407 REV. 3" |
| `amp-board-1413-component-side.jpeg` | `signal-2026-07-19-134932.jpeg` | Quad Amplifier Board full view: Pyramid PB-150P module, corner crossovers, "P/N 1413 REV. 1", J5 pinout silkscreen |
| `amp-board-1413-solder-side.jpeg` | `signal-2026-07-19-134926.jpeg` | Amplifier board solder side, PCB etch NT 1-0A 3695 |
| `amp-board-1413-pyramid-inputs.jpeg` | `signal-2026-07-19-134921.jpeg` | Pyramid input face: HIGH INPUT connectors, LEVEL pots, LOW INPUT RCAs; front-corner crossovers |
| `amp-board-1413-pyramid-outputs.jpeg` | `signal-2026-07-19-134915.jpeg` | Pyramid output face: SPEAKERS terminals 1CH/L4CH/R, blade FUSE, B+12V/GND/REMOTE; check LEDs, J1J4 pinout silkscreen |
## Cross-references
- [`../PROTOCOL.md`](../PROTOCOL.md) — the 9600 8N1 wire protocol this
hardware speaks; its DTR reset pulse and 0x000x6F input map both have
their physical explanation above.
- [`../../rio-firmware/`](../../rio-firmware/) — v4.2 EPROM dump (from the
AM27C512 at U10) and 68HC11 disassembly/patches.
- [`../Win32RIO/`](../Win32RIO/) — FASA's original Windows driver for the
same hardware.
-148
View File
@@ -1,148 +0,0 @@
# Display Board 1408 — outputs, and what the RIO shows on it
Complete definition of the RIO External Display board (P/N 1408 Rev. 1,
`DSP_1408` sheet 7), from the schematic, the board photos, the GAL decode,
and the firmware disassembly (`RIOv4_2.disasm.asm`).
## Outputs: light only — the board is electrically write-only
No signal leaves the board toward the RIO or anything else. J1 has no
readback line, matching the GAL's write-only `DSP_SEL*` decode, and the
PC↔RIO protocol has no display command — only the firmware writes it.
### 1. Eight multiplexed 7-segment digits (U2U9, LTS313A, red, common-cathode)
The ICM7228B (U1) does all driving. `HEX/B/SHD` is strapped high via 2.7 K
(R1): **hexadecimal decode mode, always enabled** — each digit renders 0F
plus a decimal point; no raw-segment access exists as built.
- **Segment bus** — U1 pins 16, 17, 20, 18, 21, 22, 23, 15 drive
`SEGA``SEGG`, `SEGDP`, shared by all digits (digit pins AG, DP =
10, 9, 8, 5, 4, 2, 3, 7). Segments source the multiplexed current
(~20 mA peak).
- **Digit commons** — U1 outputs `DSP1``DSP8` (pins 4, 25, 3, 1, 26, 2,
27, 24) each sink one digit's tied-together commons (CC1+CC2, pins 6+1).
`DSP1`→U2 (leftmost) … `DSP8`→U9 (rightmost).
- Multiplex scanning is internal to the ICM7228; the host only writes
digit registers.
From the 68HC11 the digits are bytes at **`$A000``$A007`** (`AS0-2` =
A0A2, strobed by `DSP_SEL*` = `E & /R/W`-qualified, arriving as the
7228's `/WR` on J1 pin 7). Data format per write: bits 03 = hex value
(`MD[00:03]``ID0-3`), bit 4 = decimal point (`MD[04]``ID7`; DP lights
when the bit is **low** — the firmware always sets it, so the DP is never
used). **`$A000` = leftmost digit (U2), `$A007` = rightmost (U9)**; the
firmware's render buffer (`$2038`, routine `$CB49`) fills right-to-left so
multi-digit hex numbers read naturally.
### 2. Twelve passive monitor LEDs on the "IBM printer port" header (J2) — an input, not an output
J2 (26-pin IDC, standard LPT-ribbon mapping to a DB25) is electrically
isolated from the display circuit; its `DD` nets touch nothing but LEDs
and 1 K resistors referenced to the PC's own ground (`IBM_GND`). With a PC
parallel port plugged in, the LEDs show: **D0D7** = data bits (J2 pins
3, 5, 7, 9, 11, 13, 15, 17), **D8** = `/STROBE` (pin 1), **D9D11** =
`/AUTOFD`, `/INIT`, `/SELECTIN` (pins 2, 6, 8). The LPT status returns are
strapped to ground so a PC streams without waiting. It is a built-in
parallel-port activity monitor / bench tool, powered entirely by the PC.
### 3. Nothing else
J1 (14-pin, from the RIO's EXTERNAL DISPLAY port) is all input:
`MD[00:04]` on pins 2, 4, 6, 8, 10; `AS0-2` on 1, 3, 5; `BF_DSP_SEL*` on
7; GND on 9, 11, 13; VCC on 12, 14.
## What the RIO firmware displays (v4.2)
All display writes go through `$CB49` (buffer render) or five canned
patterns at `$CB7C$CC52`, driven from four contexts: initialization,
the built-in **test mode**, the **serial error/crash displays**, and
the **CheckRequest self-test** (all below). During normal, healthy
gameplay the display is static.
**Normal operation:** `F0000000` — written at cold boot (`$C068`) and warm
re-init (`$C64B`), then left alone.
**Test mode** — entered by typing the keypad sequence **A, 9, E** on a
cockpit keypad (`$C1CB` tracks the sequence at `$20F8`); entry sets flag
`$2421`, shows `F1000000`, and notifies the PC (`TestModeChange`, cmd
`0x8C` — see [PROTOCOL.md](../PROTOCOL.md)). Within test mode the next
keypad digit selects a sub-test; **F** ends a sub-test (back to
`F1000000`), **D** exits test mode (back to `F0000000`).
Display format convention: **left pair = test number, right = value**,
zeros as filler; the decimal points are never used.
| Key | Display | Test |
|-----|---------|------|
| *(entry)* | `F1000000` | test-mode banner |
| `0` | `00000000` | encoder-counter reset: presets the five 16-bit accumulators to `$FAFA` and re-reads all HCTL-2016s (`$C446`) |
| `1` | `0100hhll` | axis test, channel 01 — counter at `$A028/$A038` |
| `2` | `0200hhll` | axis test, channel 02 — `$A020/$A030` |
| `3` | `0300hhll` | axis test, channel 03 — `$A022/$A032` |
| `C` | `0C00hhll` | axis test, channel 0C — `$A024/$A034` |
| `4` | `0400hhll` | axis test, channel 04 — `$A026/$A036` |
| `5` | `050000nn` | button test — `nn` = index of the pressed input (scan at `$D004` over the pod input state at `$2152`), with lamp feedback |
| `6` | `06000003` | lamp test, pattern 3 (`$D34A`) — right digit is the pattern code |
| `7` | `07000001` | lamp test, pattern 1 (`$D370`) |
| `8` | `080000kk` | keypad test — `kk` = keypad status byte `$204B`, refreshed while scanning (`$CC7E`) |
In the axis tests `hhll` is the **live 16-bit quadrature count in hex**
(high:low = `$201D:$201E`, alternating with the raw latched bytes
`$2026:$2025` as the loop runs) — spin the stick/throttle/pedal encoder
and watch the number move. The channel IDs are literally the keypad keys
that start them, which is why the fourth axis reads `0C`.
## The CheckRequest self-test leftover: `04000000`
The host `CheckRequest` (`0x80`) triggers a full self-test in the
handler at `$C5A6`: it sets the same display flag test mode uses
(`$2421`), brackets itself with `TestModeChange 0x8C` notifications,
flashes a lamp pattern, scans the pods, and sweeps the five encoder
channels — rendering each channel's `cc00hhll` frame as it goes. It
clears the flag on exit but **never repaints the display**, so after
every status check the digits are left showing the last frame:
channel 4, usually `04000000` with idle encoders. It is a stale
snapshot (axis movement does not update it — bench-confirmed
2026-07-19), purely cosmetic, and the board keeps operating normally.
Any reset repaints `F0000000`.
## Error displays (the third use)
Two more display writers live in the serial machinery — the display is
also the RIO's **error annunciator**:
- **`E0` serial-diagnostics readout** (`$D5F2`) — `E0xxyyzz` where
`xx` = TX-ring overflow count (`$3187`, outbound byte dropped because
the ring was full, `$D66A`), `yy` = give-up count (`$3184`, `$D96A`
reply cycles whose 5 timeout retransmits all went unACKed), `zz` =
reply-teardown count (`$3185`, `$DA2F` — any reply cycle that needed
at least one retransmission; the counter behind a lone `E0000001`).
Semantics bench-confirmed 2026-07-19: display read `E0000105` after
one give-up + five teardowns. Each increment site immediately re-renders,
so the display live-updates as serial trouble accumulates. If a
cockpit's display reads `E0…` instead of `F0000000`, the serial link
has been dropping or retrying. Note there is **no threshold in stock
firmware** — the first event paints `E0000001` permanently; the
**RIO 4.3** firmware (`make_patch.py --e0thresh`, see
[RIOv4_2-ANALYSIS.md](../../rio-firmware/RIOv4_2-ANALYSIS.md)) keeps
the display quiet until a counter reaches 5, and repaints honestly
after every check self-test.
- **`dEAd` crash screen** (`$DAB8`, loop at `$DAB2`) — the TX-ISR ring
drain (`$D8C2`) looks each outgoing command byte up in an 8-entry
dispatch table at `$3158`; an unmatched byte means internal state
corruption, and the firmware jumps to an infinite loop that renders
`#$DE #$AD`**`dEAd`** followed by RAM bytes `$0000/$0001` (nothing
in this build writes them — vestigial debug data). The board is dead
until reset; there is no watchdog to rescue it (the MAX690's WDI is
not connected to the CPU and no strobe code exists in the firmware).
## Cross-references
- [README.md](README.md) — board family, photos, schematic page map
- [gal/README.md](gal/README.md) — `DSP_SEL*` decode (`$A000-$A00F`,
write-only) recovered from the GAL fuse map
- [`rio-firmware/RIOv4_2.disasm.asm`](../../rio-firmware/RIOv4_2.disasm.asm)
— display routines `$CB20/$CB36/$CB49` (buffer render), `$CB7C` (`F0`),
`$CBA7` (`F1`), `$CBD2`/`$CBFD` (lamp-test banners), `$CC28` (zeros),
test dispatcher `$C241`, entry-sequence tracker `$C1CB`
-38
View File
@@ -1,38 +0,0 @@

Device : GAL20V8A
Created By: http://www.autoelectric.cn
Date : 2023-03-09 19:02
*QP24
*QF2706
*G0
*F0
*L00000 11111111111111111111111111111111
*L00032 11111111111111011111111111111111
*L00064 11111111110111110000000000000000
*L00320 11111111111111111111111111111111
*L00352 11111111111111111111111111111111
*L00384 11111110111111110000000000000000
*L00640 11111111111111111111111111111111
*L00672 11111111100101111011101110111011
*L00704 10111011101101110000000000000000
*L00960 11111111111111111111111111111111
*L00992 11111111111001111111111111111111
-71
View File
@@ -1,71 +0,0 @@
# U7 GAL20V8A decode — the RIO memory map, recovered
[`GAL20v8a_5764.JED`](GAL20v8a_5764.JED) is the fuse map read out of **U7**
on the RIO board (P/N 1407 Rev. 3) — the chip the schematic draws as a
"20L8" memory decoder. Decoding the fuses (see below for how) yields the
complete address decode, which cross-checks perfectly against both the
schematic net names and the firmware disassembly.
Three provenance details line up: the chip's hand-written sticker reads
**"5764"** = the JED fuse checksum (`*C5764`); the fuse map is in **complex
(20L8-emulation) mode** exactly as the schematic symbol suggests; and the
programmed electronic signature (UES) literally reads **`U7`** — VWE
signed the chip with its own reference designator.
## Recovered equations
Inputs: `A[15:3]` (A3 arrives via the pin-16 feedback path), `E`, `R/W`.
All outputs are combinatorial, always enabled.
```
/ROM_SEL = A15 & A14 ; pin 15
/RAM_SEL = /A15 & A14 & E | /A15 & A13 & E | A15 & /A14 & /A13 & E ; pin 19
/PH_SEL = A15 & /A14 & A13 & /A12../A6 & A5 ; pin 20
/BRD_SEL = A15 & /A14 & A13 & /A12../A6 & /A5 & A4 & E & /RW ; pin 18
/DSP_SEL = A15 & /A14 & A13 & /A12../A6 & /A5 & /A4 & E & /RW ; pin 17
/OE = E & RW ; pin 22
NOT_A3 = /A3 ; pin 21
```
(`/A12../A6` abbreviates the run `/A12 & /A11 & /A10 & /A9 & /A8 & /A7 & /A6`;
full machine output comes from `galdecode.py`, below.)
## The memory map
| Address | Select | Device | Notes |
|---------|--------|--------|-------|
| `$0000-$1FFF` | *(none)* | 68HC11 internal | RAM `$0000-$00FF`, registers `$1000-$103F` |
| `$2000-$9FFF` | `RAM_SEL*` | LH52B256 32K SRAM | E-qualified, read/write; exactly 32K |
| `$A000-$A00F` | `DSP_SEL*` | External-display write port | write-only (`E & /RW`) |
| `$A010-$A01F` | `BRD_SEL` | Pod-bus control latch (74HC574, sheet 3) | write-only (`E & /RW`) |
| `$A020-$A03F` | `PH_SEL*` | HCTL-2016 encoder counters ×5 | `A3` picks high/low count byte via `NOT_A3`, `A[2:0]` picks chip via the 74LS138s |
| `$C000-$FFFF` | `ROM_SEL*` | 27C512 EPROM | top 16K of the 64K device |
`OE*` (`E & R/W`) is the shared read-side output enable for
EPROM/SRAM/counters; the write-only decodes double as write strobes.
**Firmware cross-check** — [`RIOv4_2.disasm.asm`](../../../rio-firmware/RIOv4_2.disasm.asm)
touches precisely these windows and nothing else in `$A0xx`: digit writes to
`$A000-$A007`, pod-bus latch writes to `$A010`, and encoder reads at
`$A021/$A023/$A025/$A027/$A029` + `$A028/$A030-$A038` (five chips × two count
bytes). It also explains the EPROM dump being `FF` below `$C000`: with
`ROM_SEL* = A15·A14` and all 16 address lines wired, only the top quarter of
the 27C512 is ever addressed.
## How the decode was done
[`galdecode.py`](galdecode.py) parses the JED and applies the GAL20V8
complex-mode fuse geometry — row/column-to-pin tables, product-term-disable
fuses, XOR polarity, SYN/AC0 mode bits — taken from MAME's `jedutil.cpp`
(the reference implementation for PLD fuse maps). Pin-to-net names come
from schematic sheet 1 (`RIO_1407`) and live in
[`u7-pin-names.txt`](u7-pin-names.txt). Reproduce with:
```sh
py docs/hardware/gal/galdecode.py docs/hardware/gal/GAL20v8a_5764.JED docs/hardware/gal/u7-pin-names.txt
```
The JED itself was read from the chip on 2023-03-09 (header says an
autoelectric.cn / XGecu-style programmer). Note `*F0` in the header: unlisted
fuse rows default to 0, i.e. unused product terms are all-connected
(always-false) rather than blown — the decoder handles this.
-91
View File
@@ -1,91 +0,0 @@
"""Decode a GAL20V8 JED (complex mode) into boolean equations.
Fuse-map semantics taken from MAME jedutil.cpp (config_gal20v8_pins,
get_pin_fuse_state, is_gal20v8_product_term_enabled,
does_output_enable_fuse_row_allow_output).
"""
import re
import sys
JEDPATH = sys.argv[1] if len(sys.argv) > 1 else r"C:\VWE\riojoy\docs\hardware\gal\GAL20v8a_5764.JED"
QF = 2706
fuses = [0] * QF # *F0 default
text = open(JEDPATH).read()
for m in re.finditer(r"\*L(\d+)\s+([01]+)", text):
start = int(m.group(1))
for i, ch in enumerate(m.group(2)):
fuses[start + i] = int(ch)
SYN, AC0 = fuses[2704], fuses[2705]
mode = {(1, 1): "complex", (1, 0): "simple", (0, 1): "registered", (0, 0): "registered"}[(SYN, AC0)]
print(f"SYN={SYN} AC0={AC0} -> {mode} mode")
assert mode == "complex", "decoder only handles complex mode"
# UES signature 2568-2631, 8 bytes MSB-first
ues = bytes(
int("".join(map(str, fuses[2568 + 8 * i : 2576 + 8 * i])), 2) for i in range(8)
)
print("UES signature:", ues, "->", ues.decode("ascii", "replace"))
# complex-mode column map: pin -> (lowfusecolumn, highfusecolumn)
COLS = {1: (3, 2), 2: (1, 0), 3: (5, 4), 4: (9, 8), 5: (13, 12), 6: (17, 16),
7: (21, 20), 8: (25, 24), 9: (29, 28), 10: (33, 32), 11: (37, 36),
13: (39, 38), 14: (35, 34), 16: (31, 30), 17: (27, 26), 18: (23, 22),
19: (19, 18), 20: (15, 14), 21: (11, 10), 23: (7, 6)}
# complex-mode rows: pin -> (OE row fuse, first PT fuse, last PT fuse)
ROWS = {22: (0, 40, 280), 21: (320, 360, 600), 20: (640, 680, 920),
19: (960, 1000, 1240), 18: (1280, 1320, 1560), 17: (1600, 1640, 1880),
16: (1920, 1960, 2200), 15: (2240, 2280, 2520)}
XOR = {15: 2567, 16: 2566, 17: 2565, 18: 2564, 19: 2563, 20: 2562, 21: 2561, 22: 2560}
# Optional net names for pins, filled after reading the schematic:
NAMES = {}
if len(sys.argv) > 2:
for kv in open(sys.argv[2]):
kv = kv.strip()
if kv and not kv.startswith("#"):
p, n = kv.split(None, 1)
NAMES[int(p)] = n
def name(pin):
return NAMES.get(pin, f"pin{pin}")
def pt_enabled(fuserow):
return fuses[2640 + fuserow // 40] == 1
def term(fuserow):
"""Return list of literals, or None if term is always-false, '1' if always-true."""
lits = []
for pin in sorted(COLS):
lo, hi = COLS[pin]
l, h = fuses[fuserow + lo], fuses[fuserow + hi]
if l == 0 and h == 1:
lits.append("/" + name(pin))
elif l == 1 and h == 0:
lits.append(name(pin))
elif l == 0 and h == 0:
return None # both intact -> always false
return lits if lits else ["1"]
for pin in sorted(ROWS, reverse=True):
oerow, first, last = ROWS[pin]
oe = term(oerow) if pt_enabled(oerow) else None
if oe is None:
print(f"\npin {pin} ({name(pin)}): OE never true -> used as INPUT")
continue
pol = "active-HIGH" if fuses[XOR[pin]] else "active-LOW"
pts = []
for f in range(first, last + 40, 40):
if not pt_enabled(f):
continue
t = term(f)
if t is not None:
pts.append(" & ".join(t))
lhs = name(pin) if fuses[XOR[pin]] else "/" + name(pin)
oestr = " & ".join(oe)
print(f"\npin {pin} ({name(pin)}): {pol}, OE = {oestr}")
if pts:
print(f" {lhs} = " + ("\n" + " " * (len(lhs) + 5) + "| ").join(pts))
else:
print(f" {lhs} = 0 (no active product terms)")
-23
View File
@@ -1,23 +0,0 @@
# U7 (GAL20V8A, "20L8" on schematic RIO_1407 sheet 1) pin -> net name
1 A15
2 A14
3 A13
4 A12
5 A11
6 A10
7 A9
8 A8
9 A7
10 A6
11 A5
13 A4
14 E
23 RW
16 A3
15 ROM_SEL
22 OE
21 NOT_A3
20 PH_SEL
19 RAM_SEL
18 BRD_SEL
17 DSP_SEL
Binary file not shown.

Before

Width:  |  Height:  |  Size: 738 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 637 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 647 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 587 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 858 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 844 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 712 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 614 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 828 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 987 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 916 KiB

Binary file not shown.
-100
View File
@@ -1,100 +0,0 @@
# RIO board firmware
> **Pod owner, not an engineer?** Read
> [wedge-explained.md](wedge-explained.md) — a plain-language account of
> the "stick died mid-battle" bug that RIO 4.3 fixes.
- **`RIOv4_2.bin`** — RIO cockpit I/O board firmware **v4.2**, dumped
2026-07-04 from one of our own boards' EPROM: an **AMD AM27C512-150**
(64K x 8 UV EPROM, 150ns — the image fills it exactly).
sha256 `60a88718835c654b6135dbec7721c40ef99dca07df2ad4b57eedeb24037a5f73`.
For the eventual patched burn: a pin-compatible Winbond W27C512
(electrically erasable, TL866-friendly) drops straight into the socket;
the original AMD chip gets labeled and preserved unmodified.
## First-look analysis (from the image alone, confirmed on hardware)
- MCU: **Toshiba TMP68HC11** (read off the chip; the code fingerprint
agrees — 6800-family opcodes with writes into the 68HC11 internal
register block at `$10xx`).
- Memory map: image is FF up to **0xC000**; 16KB of code occupies
`$C000-$FFFF` (EPROM mapped at the top of the HC11 address space).
- Startup at `$C000`: `SEI; LDS #$8000; STAA $1024 (TMSK2);
STAA $1022 (TMSK1); ...` then a long `JSR` init chain — textbook HC11
bring-up.
- Vector table (`$FFD6-$FFFF`, big-endian):
- `$FFFE` RESET → `$C000`
- **`$FFD6` SCI (serial) → `$D630`** — the entry point of the board's
receive/protocol interrupt handler. The suspected board-side
DISABLE_AND_DIE-style wedge (see RIO-NOTES.md: the board mirrors the
game's PCSPAK state machine, and mash-stress leaves the reply path
dead while the button/event path stays alive) is reachable from here.
- `$FFE4` → `$C1B2`, `$FFE6` → `$C18E` (timer output-compares); most
other vectors → `$DB07..$DB3D` stubs.
## Why this exists
The remaining RIO reliability issue is board-side: under button-mash
stress the board's reply/analog state machine wedges (RX dead, TX alive;
a button press or power cycle revives it), reproduced identically on two
different USB serial adapters. The game-side half of the protocol was
binary-patched for tolerance (BTL4OPT patches v2-v4); the board firmware
is the other half. Plan (RIO-NOTES.md "Board firmware patch plan"):
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.
## RIO 4.3 — `RIOv4_3.bin` (current production firmware)
**Christened 2026-07-19** after full on-hardware certification. Built
by `make_patch.py --e0thresh=5 --checkrepaint --reportversion=4.3
RIOv4_2.bin RIOv4_3.bin`; sha256
`6d67a2fc77130b601fdb0ac02042dd4d0a98ac7e29a8077d588987a81073939c`
(69 bytes changed vs stock). 9600 baud, native-game compatible. On the
stock v4.2 base it carries:
1. **Reply-latch wedge fix** (edits 1-2) — bench-certified.
2. **E0-display threshold, N=5** (edit 5) — certified: display holds
`F0000000` through sub-threshold error events, flips to the live
`E0` readout at the 5th teardown (`E0000105` on the bench).
3. **Check self-test display repaint** (edit 6) — certified both ways:
version+check leaves `F0000000` when healthy, and re-renders the E0
readout when counters are over threshold (`E0000305` on the bench)
instead of stock's stale `04000000`.
4. **Version bump** (edit 7, `--reportversion=4.3`) — the VersionReply
now announces **4.3** (one operand byte at `$C6FF`). Nothing
host-side validates the value (native games verified not to check
it). The certification runs below were made on the pre-edit-7 bytes;
the only delta is the version literal. Chips burned before this edit
report 4.2 — re-burn to announce 4.3.
Certification runs (all in `testlogs/` + RIOv4_2-ANALYSIS.md): two
`--e0test` acceptance passes (display verified by eye at each step) and
a 120 s mash at **549 presses/min sustained** — zero wedges, board
counters flat, zero NAK, 247 resends all healed
(`riomash-rio43rc1-9600-20260719-164901.log`).
## FastRIO 4.3 — `RIOv4_3_fastrio.bin`
Same generation for the RIOJoy-only cockpits: `make_patch.py
--baud31250 --widen-ackwait --e0thresh=5 --checkrepaint
--reportversion=4.3 RIOv4_2.bin RIOv4_3_fastrio.bin`; sha256
`ee807831fe0df9dc147eb5cdc3302672ae21f6ff349da30babea27fc863234b9`
(71 bytes vs stock — differs from `RIOv4_3.bin` in exactly two bytes:
`$D62B` baud `30→02` and `$D9E7` ACK-wait `04→28`). 31250 baud,
FTDI-class adapter required, **not** native-game compatible.
Disassembly-verified; run the acceptance ladder on the FastRIO cockpit
before deploying (`--e0test COM1` auto-probes 31250, then a mash
spot-check at `--baud 31250`).
## Archive
All prior generations live in [`archive/`](archive/) with their
disassemblies: `RIOv4_2_patched` (wedge fix only), `_31250`/`_31250v2`
(FastRIO speed builds), `_62500`/`_125000` (speed-ladder science runs),
and the `_e0t5` pair (threshold without check-repaint). Bench verdicts
and per-edit history: RIOv4_2-ANALYSIS.md. The pristine dump
`RIOv4_2.bin` stays at top level — it is the source every patch builds
from. RIO 4.3 is in the cockpit socket as of 2026-07-19; the original
AMD chip and the retired e0t5 burn are labeled and preserved.
-559
View File
@@ -1,559 +0,0 @@
# RIO v4.2 firmware — protocol wedge analysis
Reverse-engineering of `RIOv4_2.bin` (Toshiba TMP68HC11, AM27C512) to find
the board-side cause of the "reply path wedges under stress, button-press
revives it" fault. Disassembly by `disasm_6811.py`
`RIOv4_2.disasm.asm`. **Research only** — the fix below is proposed, not
yet burned or tested (no spare EPROM on hand). Validate on hardware with
the `RIO_TAP` mash test before trusting.
Addresses are CPU = file offset (EPROM at `$C000-$FFFF`; reset `$FFFE`
`$C000`). RAM lives at `$20xx-$31xx`.
## How the serial protocol is structured
- **SCI interrupt** (`$FFD6``$D630`): `JSR $D634; RTI`. `$D634` runs BOTH
workers every interrupt: `JSR $D6EA` (RX) then `JSR $D887` (TX). So the
transmitter is poked after every received byte, not only on TX-empty
interrupts.
- **RX ISR** `$D6EA`: reads SCSR/SCDR, stores the byte at `$3172`, then
`LDX $292F; JMP $00,X` — dispatches through a **state-handler pointer**
at `$292F`. Handlers classify bytes (`$D717`: `FE`=RESTART, `FF`=IDLE,
`FC`/`FD`=game ACK/NAK, `$82`=analog request, table lookup at `$3144`),
accumulate the body + checksum (`AND $7F`), and on a complete packet run
the ACK/NAK decision at `$D81F`.
- **TX ISR** `$D887`: if TDRE, send a pending ACK (`$316F``$FC`) or NAK
(`$3170``$FD`), else dispatch through the TX state pointer `$2D3B`
(`$D8C2` ring-drain → `$D90E` reply/retry machine). When idle it disarms
the TX interrupt (SCCR2 `#$2C`, TIE off) at `$D918`; the enqueue routine
`$D63B` re-arms it (SCCR2 `#$AC`, TIE on) at `$D664`.
## The wedge: an orphaned "reply-in-progress" latch (`$2521`)
`$2521` = "an analog reply is in progress." The analog-request handler
gates on it:
```
D74F CMPB #$82 ; analog request from the game
D753 LDAA #$01
D755 STAA $2520 ; arm reply generation
D758 TST $2521 ; already replying?
D75B BNE $D77A ; YES -> D77A: CLR $2520, drop this request
```
So while `$2521` is set, **every analog request is silently dropped**.
The latch is set when a reply is generated:
```
D847 JSR $C5EC ; build the analog reply
D84C STAA $2521 ; reply-in-progress = 1
```
and is cleared in only three places: power-on init (`$C0A3`), a host
reset/init command handler (`$C686`), and the reply **success** teardown
(`$DA00`). The success teardown is reached at `$D9C1` when the game ACKs
the reply, and clears the latch — but only conditionally:
```
DA21 LDAA $2522 ; did the $87 reply byte actually start sending?
DA24 CMPA #$01
DA26 BNE $DA2E ; if not, skip the clears <-- fragile
DA28 CLR $2521
DA2B CLR $2522
```
`$2521` is set the instant the reply is *generated* (`$D84C`), but `$2522`
is set only once the `$87` command byte *starts transmitting* (`$D8FD`).
**The leak** is the retry-exhausted give-up path, which is *separate* from
the success teardown. When the game fails to ACK a reply, `$D90E`/`$D9BE`
retries up to 4 times, then gives up:
```
D9D5 LDAB #$FE ; give up: send RESTART
D9D7 STAB $102F ; SCDR
D9DA INC $317A
D9DD JMP $DA2F ; teardown -- but DA2F never touches $2521
```
`$DA2F` resets the TX pointers and calls `$D5F2` (a debug-counter
formatter that does *not* clear the latch), then returns. **`$2521` is
left set forever.** From then on every `$82` analog request is dropped at
`$D758` → the board is mute to analog while its RX/event path stays fully
alive.
### Why a button press / new game revives it
The only mid-run code that clears `$2521` is the host command handler at
`$C669-$C689` (it clears `$2520`/`$2521`/`$2522` plus a raft of state).
That runs for a host-level reset/init command — exactly what the game
sends at game-start / on the player's opening button actions. Mid-mission
button-mashing sends no such command, so the leaked latch stays stuck
until the next game-start reset. This matches the field observation
precisely: the board goes mute under stress and only a new-game/button
resync brings analog back.
### Why mash stress triggers it
Button-event traffic floods the link while the board is mid-analog-reply;
the reply's ACKs collide/drop, the 4-retry budget exhausts, and the
give-up path (`$DA2F`) fires — leaking the latch. Light traffic rarely
exhausts the retries, so it's a stress-only fault. Two different USB
adapters showed the identical stall because the defect is in the board,
not the transport — consistent with this being firmware, not timing.
## Proposed fix (minimal, in-place; UNTESTED)
Clear `$2521` on *every* reply teardown, not just the `$2522`-gated
success path. Two edits, no code-size change, 8 KB of free ROM exists at
`$DFF0-$FFBF` for the stub:
1. **Give-up path** — redirect its teardown through a stub that clears the
latch first. At `$D9DD` change `JMP $DA2F` (`7E DA 2F`) →
`JMP $DFF0` (`7E DF F0`), and place at `$DFF0`:
```
DFF0 7F 25 21 CLR $2521
DFF3 7F 25 22 CLR $2522
DFF6 7E DA 2F JMP $DA2F
```
2. **Success path** — make the clear unconditional (belt-and-suspenders,
covers an abort before `$87` is sent). Replace `$DA21-$DA2D` (13 bytes)
in place:
```
DA21 7F 25 21 CLR $2521
DA24 7F 25 22 CLR $2522
DA27 01 01 01 01 01 01 01 (NOP x7)
DA2E 39 RTS (unchanged)
```
Rationale: `$2521` means "a reply is in progress"; any path that tears
down reply state must release it. There is no case where you reset the
reply machine yet want the latch to stay set, so unconditional clearing is
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.
### 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
permanent analog mute; any collision self-recovers without a game-start
reset. Compare dropout counts to the 2026-07-03/04 baseline taps.
**Instrumented harness** (2026-07-17): the mash protocol is mechanized in
`tools/RioSerialMonitor` —
dotnet run --project tools/RioSerialMonitor -- --mash COM1 300 --label patched
It disables the app's own >5 s reset-recovery (a wedge must stay observable),
echoes lamps on every press (lamp/reply collisions are the trigger), and logs
to `riomash-<label>-<stamp>.log`: analog-gap histogram + longest gaps, WEDGE
events with beep/banner and a self-recovered vs button-revived classification
(button within 300 ms of resume = the unpatched revival signature), and the
board's own RestartCount/AbandonCount/FullBufferCount before/after with the
delta (7-bit wrap-aware). Exit 0 = no wedge, 1 = wedge seen. Run once with
`--label baseline` on the original chip (control: prove the test still bites),
then `--label patched`; the fixed-layout summary blocks diff directly.
`--mash --selftest` runs a scripted in-memory board that fakes a
button-revived wedge — use it to sanity-check the alarm before a session.
## Firmware memory map (as decoded so far)
| addr | meaning |
|---|---|
| `$292F` | RX state-handler pointer (`JMP $00,X` dispatch) |
| `$2D3B` | TX state-handler pointer |
| `$2D34/$36/$38` | TX ring read/write/aux pointers (ring `$2932-$2D31`) |
| `$2520` | reply gate (analog request pending) |
| `$2521` | **reply-in-progress latch — the wedge** |
| `$2522` | `$87` analog-reply-byte-sent flag |
| `$316C/$6D` | game ACK / NAK received |
| `$316E` | unknown-command seen |
| `$316F/$70` | ACK / NAK pending to send (→ TX ISR) |
| `$3172` | last received byte |
| `$3173/$74/$75` | ACK/NAK/wait retry counters (limit 4) |
| `$317A/$7B` | RESTART / IDLE keep-alive counters |
| `$3184/$85` | give-up / error diagnostic counters |
| `$3186` | RX overrun flag (set at `$D701`, **never read** — not the cause) |
| `$102D/$2E/$2F` | SCCR2 / SCSR / SCDR (HC11 SCI) |
## Bench validation results (2026-07-18) — PATCH CONFIRMED
Rig: real RIO board, FTDI FT232R on COM1, `RioSerialMonitor --mash`
(app auto-recovery disabled, lamp echo on). Logs in `testlogs/`.
| | baseline (original AMD chip) | patched (W27C512) |
|---|---|---|
| startup version/check exchange | **wedged at 0.62s** (reply/reply collision) | survived |
| analog replies | 6 total, then dead for 300s | 2419 @ 152s, steady ~16/s |
| mash | 951 presses (peak 460/min) | 1315 presses (peak 635/min) |
| worst analog gap | infinite — no recovery in 300s despite 951 presses | 0.58s, self-recovered |
| wedge events | 1, unresolved (power cycle required) | 0 |
Notes: the baseline wedge didn't even need the mash — the tool's own
version+check request colliding with the analog poll killed the reply
path instantly, and (contrary to the earlier RIO-NOTES observation)
button presses did NOT revive it. The patched chip's single 0.58s gap
is the intended failure mode: collision → teardown → latch cleared by
the $DFF0 stub → self-recovery. Patched run cut at ~155s by operator
(finger fatigue); given the baseline's 0.62s time-to-wedge, 2.5 min of
heavier mash is decisive. Remaining (cabinet): overnight idle soak +
a native-game session. Both check snapshots also reported a board/lamp
fault item — persistent across chips, likely a real tired lamp; inspect
separately.
## 31250-baud bench run (2026-07-18) — patch holds; retry window is byte-scaled
`testlogs/riomash-patched-31250-20260718-003114.log` (127s mash, FTDI
COM1 @ 31250, latency timer already 1ms): **zero wedges**, but
framing=5655, analog replies at 215% of poll slots (duplicates), and
AbandonCount=65 — a reply-retry storm. Reading: the board's reply
ACK-wait window scales with BYTE TIME, not wall clock. At 9600 the
window (~5+ms) hides USB turnaround entirely (zero framing in both
9600 runs); at 31250 it shrinks to ~1.6ms, which USB cannot reliably
beat, so most replies retransmit and 65 exhausted all 4 retries.
Two conclusions:
1. **Brutal confirmation of the wedge patch**: 65 trips through the
give-up path in 127s, every one self-recovered ($DFF0 stub). The
unpatched firmware would have latched dead on the first.
2. **31250 needs one more firmware tweak to be clean**: widen the
reply-retry wait (the pacing/limit around the $D90E/$D9BE retry
machine) so the window stays >=10ms of wall clock at 31250. Until
then: cabinets run the 9600 patched chip (validated clean); 31250
stays a bench branch. Note the duplicate replies also mean the raw
reply rate overstates throughput — unique samples are still capped
by the host's 55ms poll; harvesting the speed needs a faster poll
AFTER the retry window is fixed.
### 31250 v2 (2026-07-18) — `RIOv4_2_patched_31250v2.bin` — widened ACK-wait
Root cause of the v1 retry storm CONFIRMED in the disassembly: the reply
ACK-wait loop at `$D9E0` self-clocks in **byte times** — each tick sends an
IDLE (`$FF`) keep-alive and re-enters on that byte's TX-complete interrupt,
so the `CMPA #$04` limit means ~5 byte times of grace: ~5.2ms at 9600
(USB ACK wins; zero framing in both 9600 runs) vs ~1.6ms at 31250 (USB
loses; framing 5655 / Abandon 65 on the v1 bench run). Those keep-alive/
RESTART bytes landing mid-packet are exactly the host-side framing resyncs.
`make_patch.py --baud31250 --widen-ackwait` → one more byte,
`$D9E7: $04 → $28` (40 ticks ≈ 12.8ms at 31250, clears FTDI worst case
with margin). The NAK-retry limit ($3175) is event-counted, untouched.
sha256 `420d4cfc6b513651687982a70db2daeecda7bd00a5324321e272f35b95dca753`,
25 bytes vs original; re-disassembly diff vs the v1 31250 image is exactly
the one CMPA operand line. Expected on the bench: framing 5655 → ~0,
Abandon 65 → ~0, wedges 0 → 0.
### 31250 v2 bench results (2026-07-18) — CLEAN. Byte-clock theory confirmed.
`testlogs/riomash-patched-31250v2-20260718-110853.log` (192s mash):
| metric | v1 @31250 | v2 @31250 (ACK-wait $28) |
|-------------------|----------------|--------------------------|
| framing resyncs | 5655 (~45/s) | **54 (~0.3/s, -99.4%)** |
| Abandon delta | 65 | **+0** |
| Restart delta | 2 | **+0** |
| analog vs polls | 215% (dupes) | **88.6% (no dupes)** |
| wedge events | 0 | **0** |
| worst mid-run gap | 0.09s (steady) | 0.14s (tail <100ms) |
The 88.6% delivery ratio equals the healthy 9600 patched profile (87.5%)
— the deficit is the $D758 in-flight drop gate, identical at both rates,
i.e. structural, not a v2 artifact. Counters perfectly flat: the board
never entered retry escalation in 192s. FastRIO is now *clean*, not
just survivable. To actually harvest the bandwidth, the next dial is
host-side: drop the 55ms analog poll interval (the board now answers in
~4.6ms of wire time). Cabinet adoption still gated on game-side baud
patches; 9600 patched chip remains the production standard meanwhile.
### 125000 bench results (2026-07-18) — CPU wall found; RX overrun confirmed
`testlogs/riomash-patched-125000-20260718-125310.log` (103s): zero wedges
(all gaps self-recovered <=0.52s even here), but the board's RX drowns:
NAK=1082 (~2/3 of the ~1650 lamp commands arrived checksum-corrupt —
overrun-dropped bytes; lamps visibly dead), analog delivery 60.6% (2-byte
requests survive better than 4-byte packets), both status exchanges
failed. Direction asymmetry nails the cause: board->PC was near-perfect
(framing 5), so the MAX232 leg is fine at 125k — the 2MHz HC11 RX ISR
(~160 E-cycles/byte, 1-byte buffer) is the wall, exactly as the cycle
estimate predicted. Verdict: 125000 NOT viable. Sweet-spot candidate:
`RIOv4_2_patched_62500.bin` (320 cycles/byte, MAX232 in-spec, ACK-wait
80 ticks = 12.8ms), sha256 below, awaiting bench.
### 62500 bench results (2026-07-18) — the ladder is complete
`testlogs/riomash-patched-62500-20260718-130333.log` (132s): analog
pristine (88.1% delivery = the structural drop-gate ratio; worst mid-run
gap 0.14s; counters flat; zero wedges; status exchanges fine) but
**NAK=69 of ~1060 lamp commands (~6.5%)** — 4-byte bursts still outrun
the RX ISR occasionally, and fire-and-forget lamp writes turn each loss
into a visible stuck-bright / missed-bright lamp. The ISR's worst-case
path therefore sits right at ~320 E-cycles.
**The RIO speed ladder (2 MHz E-clock, all with wedge fix):**
| rate | bytes OK inbound | verdict |
|---|---|---|
| 9600 | all | production (native-game compatible) |
| 31250 v2 | all (NAK 0.3%) | CLEAN — FastRIO recommended |
| 62500 | 2-byte yes, 4-byte ~6.5% loss | usable ONLY with host NAK-retransmit |
| 125000 | ~40-65% loss | not viable — CPU wall |
Host-side NAK-driven retransmit (the protocol's intended use of ACK/NAK;
RioSerialLink currently fire-and-forgets outbound) would make 62500
lamp-solid (1 retry -> ~0.4% residual, 2 -> ~0.03%) and is good
robustness at any rate. Until then, 31250 v2 is the honest FastRIO.
### 62500 final verdict (2026-07-19) — SHELVED. Chord-driven RX overrun.
Three rounds of host-side hardening at 62500 (evidence in `testlogs/`,
implementations in RioJoy.Core RioSerialLink):
1. **NAK-race resend** — resends tracked NAKs 1:1 (321/321) yet lamps
still glitched: under bursts the NAK arrives after a newer command is
already "latest", so the wrong packet was resent; total shreds never
NAK at all.
2. **Stop-and-wait** (one command in flight; ACK/NAK/50ms-timeout;
retransmit the SAME packet, limit 2) — still glitched: reply
resolution was type-blind, so an analog reply in USB transit from the
previous poll falsely confirmed the next lamp command.
3. **Typed resolution** (a reply only resolves its matching request;
ACK/NAK safe type-blind because the board's TX ISR prioritizes them
ahead of reply data; 10ms settle after budget-exhausted drops) —
improved, but **chorded presses still glitch even at slow press
rates**.
The chord observation closes the case: a chord makes the board scan,
queue and transmit N button events at once — worst-case ISR latency —
exactly when our 4-byte lamp replies arrive at 160us/byte into a 2-byte
RX buffer. Corruption at 62500 is chord-shaped, not rate-shaped; no
host-side protocol can prevent the board losing bytes it never latched,
and retries can land in the same busy window. Fixing it for real means
firmware ISR restructuring (or sequence numbers) — out of scope.
**Final ladder verdict: 31250 v2 is the FastRIO standard** (3.3x stock,
clean without any host-side compensation — NAK 5 in 192s of mash).
The stop-and-wait + typed-resolution machinery stays enabled at every
rate as delivery insurance (it hardens 9600 production cabinets against
ordinary electrical noise, and its retries are near-zero on clean links).
62500 images remain in the repo for future firmware work.
## FINAL CERTIFICATION (2026-07-19) — 31250 v2 ships
Operator-verified at 31250 v2 with stop-and-wait active: **zero lamp
misses/hangs** through the slow-chord test (the exact scenario that
condemned 62500) and a 392-presses/min mash.
`testlogs/riomash-31250v2-final`: analog 89.0% (structural), **NAK = 0**,
25 resends — all timeout-triggered, i.e. the retry machinery silently
healing occasional INBOUND response loss (29 host-side framing events),
which is exactly the insurance it exists to provide. Counters flat,
zero wedges, gaps <100ms except run-start/stop artifacts.
`testlogs/riomash-31250v2-poll20` (--poll 20): delivered ~31 Hz analog
(2036 replies/65s) vs the legacy 18 Hz — 1.7×. The 62.6%-of-slots figure
is a host artifact, not board saturation: the poll loop delays AFTER each
awaited exchange (~6-9 ms), so the real cadence is interval+exchange.
Absolute-schedule pacing would approach 40+ Hz; ~94% of actually-issued
polls were answered. Future host tweak, not firmware.
### Production configuration (updated 2026-07-19: RIO 4.3 christened)
| deployment | chip | config.json |
|---|---|---|
| native-game cabinets | **`RIOv4_3.bin`** (9600; supersedes `archive/RIOv4_2_patched.bin`) | defaults (RioBaudRate 9600, AnalogPollMs 55) |
| RIOJoy-only cockpits (FastRIO) | **`RIOv4_3_fastrio.bin`** (built 2026-07-19, disassembly-verified; run the acceptance ladder on-cockpit before deploying — supersedes `archive/RIOv4_2_patched_31250v2.bin`) | RioBaudRate 31250, AnalogPollMs 20-25, FTDI-class adapter |
AppConfig.RioBaudRate + AnalogPollMs are plumbed through RioCoordinator;
stop-and-wait delivery is always on (near-zero cost on clean links).
Firmware images, hashes, disassemblies, the mechanized mash harness, and
all eleven bench logs live in this directory. The original AMD chip is
retired, preserved unmodified.
## Error-handling inventory (v4.2, unpatched baseline)
Complete map of what the board does — and pointedly does not do — when
things go wrong. Complements the wedge analysis above; display-side
detail in [`docs/hardware/display-board-1408.md`](../docs/hardware/display-board-1408.md).
### Host-facing (protocol)
- **Inbound packet checksum** (7-bit sum, verified at the `$D81F`
decision): good packet → queue **ACK `$FC`** (`$316F`); bad → **NAK
`$FD`** (`$3170`). The TX ISR sends pending ACK/NAK ahead of reply
data (`$D8A6`). Control bytes `$FE` RESTART / `$FF` IDLE.
- **Outbound reply delivery**: replies must be ACKed by the game; the
retry machine (`$D90E`/`$D9BE`) retransmits — **5 times** (bench
2026-07-19; the earlier code reading of "4" was wrong) — then gives
up: sends `$FE` RESTART, `INC $317A`, `INC $3184` (give-up counter),
teardown at `$DA2F` (`INC $3185` — teardowns: ANY reply cycle that
needed at least one retransmission ends here, including NAK-recovered
ones, so `$3185` is the counter behind a lone cockpit `E0000001`).
The give-up teardown is where the `$2521` wedge leak lives (see
above; fixed in the patched images). Bench-observed await behavior:
the ACK wait arms only after the COMPLETE reply frame is sent
(mid-frame ACK/NAK/RESTART is ignored); a game NAK of the completed
frame triggers exactly one immediate counted retransmit; once the
first retry timeout lapses the cycle runs blind to give-up — a late
ACK cannot rescue it.
- **RX overrun: unhandled.** The RX ISR reads SCSR/SCDR but never acts
on the overrun flag — dropped bytes just corrupt the frame and
surface as checksum NAKs. This is why the 125000-baud bench run
produced NAK=1082 rather than any explicit overrun signal.
### Internal
- **TX-ring overflow** (`$D63B` enqueue): if the ring would overtake
the drain pointer, the byte is silently dropped, `INC $3187`, and the
E0 diagnostic display refreshes. The host is not told.
- **TX dispatcher sanity trap** (`$D8C2`): each outgoing command byte
is looked up in an 8-entry table at `$3158`; no match ⇒ internal
corruption ⇒ `JMP $DAB2` — the **`dEAd` crash screen**, an infinite
loop rendering `dEAd` + RAM `$0000/$0001` on the 8-digit display.
Permanent until reset.
- **Unused interrupt vectors** → `NOP; BRA self` stubs at
`$DB07-$DB3D`: silent hang, no display, no recovery.
- **Error counters** `$3173-$317B` (ACK/NAK/retry/give-up bookkeeping)
and the three displayed ones `$3187/$3184/$3185` — rendered as
`E0 <ring-ovf> <give-ups> <teardowns>` by `$D5F2` at every increment.
### Hardware (RIO_1407 sheet 2)
- **MAX690 supervisor**: power-on/brownout reset via the PFI
comparator. The front-panel RESET toggle (SW1) and the optocoupled
remote-reset input both work by pulling PFI low — a "commanded
power-fail". **FAULT LED = PFO**, so it lights during any such event.
- **No working watchdog.** WDI has no off-sheet tag (never reaches the
CPU) and the firmware contains no strobe code — so software hangs
(`dEAd`, vector stubs, the reply wedge) persist until a human or the
game PC resets the board. This is corroborating evidence for the
bench-observed behavior: a wedged board stays wedged indefinitely.
- **RUN LED = buffered `AS*`** (address strobe): it indicates "clock
alive", not "software alive" — it stays lit even inside the `dEAd`
loop. TX/RX LEDs are buffered `SER_OUT`/`SER_IN`.
### E0-display threshold patch (edit 5, `--e0thresh[=N]`)
Stock firmware has **no threshold**: the first increment of any of the
three displayed counters repaints the cockpit display from `F0000000` to
the `E0` readout and it never reverts — so a single benign reply
give-up (e.g. one lost ACK healed by host-side stop-and-wait) leaves a
permanent `E0000001`. `make_patch.py --e0thresh=N` (default 5) gates the
renderer: `$D5F4`'s `LDX #$2038` becomes `JMP $E000`, and a 30-byte cave
at `$E000` (erased region, clear of the `$DFF0` wedge stub) compares
`$3187`/`$3184`/`$3185` against N — all below ⇒ exit through the
routine's own epilogue (`$D61D`, registers restored, display untouched);
any at/above ⇒ resume the render at `$D5F7`. Counters still accumulate
regardless, so the diagnostic history is intact — the display just stays
quiet until real trouble (N events since power-on).
Built variants (wedge fix + threshold 5):
| image | config | sha256 (first 12) |
|---|---|---|
| `RIOv4_2_patched_e0t5.bin` | 9600, native-game compatible | `9c21ac7199fb` |
| `RIOv4_2_patched_31250v2_e0t5.bin` | 31250 + widened ACK-wait (FastRIO) | `b43032b016d7` |
**Bench-verified 2026-07-19 (9600 image, burned): PASS.** Driven by
`RioSerialMonitor --e0test` (DTR reset → clean handshake → four
NAK-then-ACK sub-threshold teardown cycles → one unACKed give-up
cycle): the display held `F0000000` through the handshake and all four
sub-threshold teardowns — stock paints `E0…` at the first — and flipped
exactly at the 5th teardown to **`E0000105`** (`$3187`=00 overflows,
`$3184`=01 give-up, `$3185`=05 teardowns), confirmed by eye on the
cockpit display. Mash regression (120s, lamps on,
`testlogs/riomash-patched-e0t5-9600-20260719-154259.log`): **PASS**
0 wedges, 236/236 presses, 88.0% analog fill, NAK 0, board counters
flat. The 31250v2_e0t5 image is the same patch on the certified
FastRIO base and is expected to behave identically.
**Post-run display `04000000` — the CheckRequest self-test (corrected
finding).** The first read of this (keypad A-9-E chord entering test
mode) was wrong — disproved on the bench: the display is static (axis
movement does not change it) and version-only exchanges leave it alone.
The real mechanism is the **CheckRequest (`0x80`) handler at `$C5A6`**:
a full built-in self-test that sets the test-display flag `$2421`
(`$C5B1`), sends `TestModeChange 0x8C` to the host (`$C70E`), runs a
lamp pattern (`$D34A`), scans the pods, sweeps all five encoder
channels (`$CC53`, ending on channel 4 — whose `cc00hhll` frame is the
last thing rendered), enqueues the fault reports, clears the flag
(`$C5E1`), notifies `0x8C` again — and **returns without repainting the
display**. Every status check therefore leaves a stale
`04 00 <ch4 count>` snapshot (idle: `04000000`) on the digits. The
board is fully healthy afterwards; no reset is needed. Proper fix:
**edit 6** (`make_patch.py --checkrepaint`) — a 10-byte cave at `$E020`
hooked from the handler's final notify (`$C5E4`): `JSR $C70E` (original
notify), `JSR $CB7C` (repaint `F0000000`), `JSR $D5F2` (re-render the
E0 readout, which the edit-5 gate suppresses below threshold), `RTS`.
Built into **`RIOv4_3rc1.bin`** (with edits 1-2 and 5; sha256
`dc59bd51cae3…`); prior generations moved to `archive/`.
Why `04000000` was never seen in period (user-verified by re-slotting
the ORIGINAL stock chip, 2026-07-19): stock leaves the same stale frame
after every check, but the ungated E0 readout papers over it at the
first counter event — `04000000` flashes momentarily, then `E000000x`
as errors accrue and the reply-latch wedge trips under load. The wedge
fix plus the E0 threshold gate are what exposed the stale frame.
**RC1 acceptance (burned 2026-07-19): PASS on both edit-6 paths.**
Below threshold: a version+check exchange leaves the display at
`F0000000`. Over threshold: the full `--e0test` sequence reproduced the
certified behavior (held `F0000000` through four sub-threshold events,
flipped at the 5th teardown), and the closing CheckRequest — 65 status
frames — left the display at **`E0000305`**: repaint + live E0
re-render, not the stale `04000000`. The `$3184`=03 (vs predicted 01)
pins down the counter semantics one notch further: two of the 65 check
frames hit a timeout-retry that our late ACK then rescued — so
**`$3184` counts started timeout-retry sequences** (give-ups are the
subset that exhaust all 5 retransmits), and a timeout-recovered cycle
exits via the success teardown (no `$3185`), while NAK-recovered cycles
tear down through `$DA2F` (`$3185`). Every observation from both bench
sessions fits this model.
**CHRISTENED RIO 4.3 (2026-07-19).** Final gate: 120 s mash at 549
presses/min sustained (1129 presses — the heaviest stress run of the
campaign), zero wedges, counters flat, NAK 0, 247 resends all healed
(`testlogs/riomash-rio43rc1-9600-20260719-164901.log`). `RIOv4_3rc1`
renamed to **`RIOv4_3.bin`** — the production firmware for native-game
cabinets. Post-christening, **edit 7** (`--reportversion=4.3`, one
operand byte at `$C6FF` in the VersionReply builder `$C6EA`) makes the
board announce 4.3; final sha256 `6d67a2fc7713…` (69 bytes vs stock).
No host software validates the version value (native games verified
not to check it — Cyd, 2026-07-19); tools will print `firmware: 4.3`
after the next burn.
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
-236
View File
@@ -1,236 +0,0 @@
#!/usr/bin/env python3
"""Recursive-descent 68HC11 disassembler for the RIO board firmware.
The RIO board (Toshiba TMP68HC11 + AM27C512) speaks the PCSPAK serial
protocol to the game. This tool disassembles RIOv4_2.bin to help locate the
receive/reply state machine and the wedge where the reply path dies under
stress. Address == file offset for this image (EPROM occupies $C000-$FFFF,
reset vector $FFFE -> $C000 confirms).
Usage: python disasm_6811.py [RIOv4_2.bin] > RIOv4_2.disasm.asm
"""
import sys
# ---- HC11 internal register block ($1000-$103F) annotations --------------
REG = {
0x00:"PORTA",0x02:"PIOC",0x03:"PORTC",0x04:"PORTB",0x05:"PORTCL",
0x07:"DDRC",0x08:"PORTD",0x09:"DDRD",0x0A:"PORTE",0x0B:"CFORC",
0x0C:"OC1M",0x0D:"OC1D",0x0E:"TCNT",0x10:"TIC1",0x12:"TIC2",
0x14:"TIC3",0x16:"TOC1",0x18:"TOC2",0x1A:"TOC3",0x1C:"TOC4",
0x1E:"TI4O5",0x20:"TCTL1",0x21:"TCTL2",0x22:"TMSK1",0x23:"TFLG1",
0x24:"TMSK2",0x25:"TFLG2",0x26:"PACTL",0x27:"PACNT",0x28:"SPCR",
0x29:"SPSR",0x2A:"SPDR",0x2B:"BAUD",0x2C:"SCCR1",0x2D:"SCCR2",
0x2E:"SCSR",0x2F:"SCDR",0x30:"ADCTL",0x31:"ADR1",0x32:"ADR2",
0x33:"ADR3",0x34:"ADR4",0x39:"OPTION",0x3A:"COPRST",0x3B:"PPROG",
0x3C:"HPRIO",0x3D:"INIT",0x3E:"TEST1",0x3F:"CONFIG",
}
def reg_ann(addr):
if 0x1000 <= addr <= 0x103F and (addr-0x1000) in REG:
return " ; "+REG[addr-0x1000]
return ""
# addressing modes and their extra operand byte counts
INH,IMM8,IMM16,DIR,EXT,IDX,IDY,REL = range(8)
BSET_DIR,BCLR_DIR,BRSET_DIR,BRCLR_DIR = range(8,12)
BSET_IDX,BCLR_IDX,BRSET_IDX,BRCLR_IDX = range(12,16)
MODELEN = {INH:0,IMM8:1,IMM16:2,DIR:1,EXT:2,IDX:1,IDY:1,REL:1,
BSET_DIR:2,BCLR_DIR:2,BRSET_DIR:3,BRCLR_DIR:3,
BSET_IDX:2,BCLR_IDX:2,BRSET_IDX:3,BRCLR_IDX:3}
# base page opcode table: opcode -> (mnemonic, mode)
OP = {
0x00:("TEST",INH),0x01:("NOP",INH),0x02:("IDIV",INH),0x03:("FDIV",INH),
0x04:("LSRD",INH),0x05:("LSLD",INH),0x06:("TAP",INH),0x07:("TPA",INH),
0x08:("INX",INH),0x09:("DEX",INH),0x0A:("CLV",INH),0x0B:("SEV",INH),
0x0C:("CLC",INH),0x0D:("SEC",INH),0x0E:("CLI",INH),0x0F:("SEI",INH),
0x10:("SBA",INH),0x11:("CBA",INH),0x12:("BRSET",BRSET_DIR),0x13:("BRCLR",BRCLR_DIR),
0x14:("BSET",BSET_DIR),0x15:("BCLR",BCLR_DIR),0x16:("TAB",INH),0x17:("TBA",INH),
0x19:("DAA",INH),0x1B:("ABA",INH),
0x1C:("BSET",BSET_IDX),0x1D:("BCLR",BCLR_IDX),0x1E:("BRSET",BRSET_IDX),0x1F:("BRCLR",BRCLR_IDX),
0x20:("BRA",REL),0x21:("BRN",REL),0x22:("BHI",REL),0x23:("BLS",REL),
0x24:("BCC",REL),0x25:("BCS",REL),0x26:("BNE",REL),0x27:("BEQ",REL),
0x28:("BVC",REL),0x29:("BVS",REL),0x2A:("BPL",REL),0x2B:("BMI",REL),
0x2C:("BGE",REL),0x2D:("BLT",REL),0x2E:("BGT",REL),0x2F:("BLE",REL),
0x30:("TSX",INH),0x31:("INS",INH),0x32:("PULA",INH),0x33:("PULB",INH),
0x34:("DES",INH),0x35:("TXS",INH),0x36:("PSHA",INH),0x37:("PSHB",INH),
0x38:("PULX",INH),0x39:("RTS",INH),0x3A:("ABX",INH),0x3B:("RTI",INH),
0x3C:("PSHX",INH),0x3D:("MUL",INH),0x3E:("WAI",INH),0x3F:("SWI",INH),
0x40:("NEGA",INH),0x43:("COMA",INH),0x44:("LSRA",INH),0x46:("RORA",INH),
0x47:("ASRA",INH),0x48:("LSLA",INH),0x49:("ROLA",INH),0x4A:("DECA",INH),
0x4C:("INCA",INH),0x4D:("TSTA",INH),0x4F:("CLRA",INH),
0x50:("NEGB",INH),0x53:("COMB",INH),0x54:("LSRB",INH),0x56:("RORB",INH),
0x57:("ASRB",INH),0x58:("LSLB",INH),0x59:("ROLB",INH),0x5A:("DECB",INH),
0x5C:("INCB",INH),0x5D:("TSTB",INH),0x5F:("CLRB",INH),
0x60:("NEG",IDX),0x63:("COM",IDX),0x64:("LSR",IDX),0x66:("ROR",IDX),
0x67:("ASR",IDX),0x68:("LSL",IDX),0x69:("ROL",IDX),0x6A:("DEC",IDX),
0x6C:("INC",IDX),0x6D:("TST",IDX),0x6E:("JMP",IDX),0x6F:("CLR",IDX),
0x70:("NEG",EXT),0x73:("COM",EXT),0x74:("LSR",EXT),0x76:("ROR",EXT),
0x77:("ASR",EXT),0x78:("LSL",EXT),0x79:("ROL",EXT),0x7A:("DEC",EXT),
0x7C:("INC",EXT),0x7D:("TST",EXT),0x7E:("JMP",EXT),0x7F:("CLR",EXT),
0x80:("SUBA",IMM8),0x81:("CMPA",IMM8),0x82:("SBCA",IMM8),0x83:("SUBD",IMM16),
0x84:("ANDA",IMM8),0x85:("BITA",IMM8),0x86:("LDAA",IMM8),0x88:("EORA",IMM8),
0x89:("ADCA",IMM8),0x8A:("ORAA",IMM8),0x8B:("ADDA",IMM8),0x8C:("CPX",IMM16),
0x8D:("BSR",REL),0x8E:("LDS",IMM16),0x8F:("XGDX",INH),
0x90:("SUBA",DIR),0x91:("CMPA",DIR),0x92:("SBCA",DIR),0x93:("SUBD",DIR),
0x94:("ANDA",DIR),0x95:("BITA",DIR),0x96:("LDAA",DIR),0x97:("STAA",DIR),
0x98:("EORA",DIR),0x99:("ADCA",DIR),0x9A:("ORAA",DIR),0x9B:("ADDA",DIR),
0x9C:("CPX",DIR),0x9D:("JSR",DIR),0x9E:("LDS",DIR),0x9F:("STS",DIR),
0xA0:("SUBA",IDX),0xA1:("CMPA",IDX),0xA2:("SBCA",IDX),0xA3:("SUBD",IDX),
0xA4:("ANDA",IDX),0xA5:("BITA",IDX),0xA6:("LDAA",IDX),0xA7:("STAA",IDX),
0xA8:("EORA",IDX),0xA9:("ADCA",IDX),0xAA:("ORAA",IDX),0xAB:("ADDA",IDX),
0xAC:("CPX",IDX),0xAD:("JSR",IDX),0xAE:("LDS",IDX),0xAF:("STS",IDX),
0xB0:("SUBA",EXT),0xB1:("CMPA",EXT),0xB2:("SBCA",EXT),0xB3:("SUBD",EXT),
0xB4:("ANDA",EXT),0xB5:("BITA",EXT),0xB6:("LDAA",EXT),0xB7:("STAA",EXT),
0xB8:("EORA",EXT),0xB9:("ADCA",EXT),0xBA:("ORAA",EXT),0xBB:("ADDA",EXT),
0xBC:("CPX",EXT),0xBD:("JSR",EXT),0xBE:("LDS",EXT),0xBF:("STS",EXT),
0xC0:("SUBB",IMM8),0xC1:("CMPB",IMM8),0xC2:("SBCB",IMM8),0xC3:("ADDD",IMM16),
0xC4:("ANDB",IMM8),0xC5:("BITB",IMM8),0xC6:("LDAB",IMM8),0xC8:("EORB",IMM8),
0xC9:("ADCB",IMM8),0xCA:("ORAB",IMM8),0xCB:("ADDB",IMM8),0xCC:("LDD",IMM16),
0xCE:("LDX",IMM16),0xCF:("STOP",INH),
0xD0:("SUBB",DIR),0xD1:("CMPB",DIR),0xD2:("SBCB",DIR),0xD3:("ADDD",DIR),
0xD4:("ANDB",DIR),0xD5:("BITB",DIR),0xD6:("LDAB",DIR),0xD7:("STAB",DIR),
0xD8:("EORB",DIR),0xD9:("ADCB",DIR),0xDA:("ORAB",DIR),0xDB:("ADDB",DIR),
0xDC:("LDD",DIR),0xDD:("STD",DIR),0xDE:("LDX",DIR),0xDF:("STX",DIR),
0xE0:("SUBB",IDX),0xE1:("CMPB",IDX),0xE2:("SBCB",IDX),0xE3:("ADDD",IDX),
0xE4:("ANDB",IDX),0xE5:("BITB",IDX),0xE6:("LDAB",IDX),0xE7:("STAB",IDX),
0xE8:("EORB",IDX),0xE9:("ADCB",IDX),0xEA:("ORAB",IDX),0xEB:("ADDB",IDX),
0xEC:("LDD",IDX),0xED:("STD",IDX),0xEE:("LDX",IDX),0xEF:("STX",IDX),
0xF0:("SUBB",EXT),0xF1:("CMPB",EXT),0xF2:("SBCB",EXT),0xF3:("ADDD",EXT),
0xF4:("ANDB",EXT),0xF5:("BITB",EXT),0xF6:("LDAB",EXT),0xF7:("STAB",EXT),
0xF8:("EORB",EXT),0xF9:("ADCB",EXT),0xFA:("ORAB",EXT),0xFB:("ADDB",EXT),
0xFC:("LDD",EXT),0xFD:("STD",EXT),0xFE:("LDX",EXT),0xFF:("STX",EXT),
}
# page 2 ($18): Y-register/Y-indexed forms
OP18 = {
0x08:("INY",INH),0x09:("DEY",INH),0x1C:("BSET",BSET_IDX),0x1D:("BCLR",BCLR_IDX),
0x1E:("BRSET",BRSET_IDX),0x1F:("BRCLR",BRCLR_IDX),0x30:("TSY",INH),0x35:("TYS",INH),
0x38:("PULY",INH),0x3A:("ABY",INH),0x3C:("PSHY",INH),0x60:("NEG",IDY),
0x63:("COM",IDY),0x64:("LSR",IDY),0x66:("ROR",IDY),0x67:("ASR",IDY),
0x68:("LSL",IDY),0x69:("ROL",IDY),0x6A:("DEC",IDY),0x6C:("INC",IDY),
0x6D:("TST",IDY),0x6E:("JMP",IDY),0x6F:("CLR",IDY),0x8C:("CPY",IMM16),
0x8F:("XGDY",INH),0x9C:("CPY",DIR),0xA0:("SUBA",IDY),0xA1:("CMPA",IDY),
0xA2:("SBCA",IDY),0xA3:("SUBD",IDY),0xA4:("ANDA",IDY),0xA5:("BITA",IDY),
0xA6:("LDAA",IDY),0xA7:("STAA",IDY),0xA8:("EORA",IDY),0xA9:("ADCA",IDY),
0xAA:("ORAA",IDY),0xAB:("ADDA",IDY),0xAC:("CPY",IDY),0xAD:("JSR",IDY),
0xAE:("LDS",IDY),0xAF:("STS",IDY),0xBC:("CPY",EXT),0xCE:("LDY",IMM16),
0xDE:("LDY",DIR),0xDF:("STY",DIR),0xE0:("SUBB",IDY),0xE1:("CMPB",IDY),
0xE2:("SBCB",IDY),0xE3:("ADDD",IDY),0xE4:("ANDB",IDY),0xE5:("BITB",IDY),
0xE6:("LDAB",IDY),0xE7:("STAB",IDY),0xE8:("EORB",IDY),0xE9:("ADCB",IDY),
0xEA:("ORAB",IDY),0xEB:("ADDB",IDY),0xEC:("LDD",IDY),0xED:("STD",IDY),
0xEE:("LDY",IDY),0xEF:("STY",IDY),0xBE:("LDY",EXT),0xBF:("STY",EXT),
}
OP1A = {0x83:("CPD",IMM16),0x93:("CPD",DIR),0xA3:("CPD",IDX),0xB3:("CPD",EXT),
0xAC:("CPY",IDX),0xEE:("LDY",IDX),0xEF:("STY",IDX)}
OPCD = {0xA3:("CPD",IDY),0xAC:("CPX",IDY),0xEE:("LDX",IDY),0xEF:("STX",IDY)}
def u16(d,a): return (d[a]<<8)|d[a+1]
class Insn:
__slots__=("addr","end","mnem","mode","opbytes","txt","target","flow")
def __init__(s,**k):
for n,v in k.items(): setattr(s,n,v)
def decode(d,a):
"""Decode one instruction at address a. Returns Insn or None (illegal)."""
start=a; op=d[a]; a+=1; pfx=None; table=OP
if op==0x18: pfx=0x18; table=OP18; op=d[a]; a+=1
elif op==0x1A: pfx=0x1A; table=OP1A; op=d[a]; a+=1
elif op==0xCD: pfx=0xCD; table=OPCD; op=d[a]; a+=1
ent=table.get(op)
if ent is None: return None
mnem,mode=ent; n=MODELEN[mode]; ops=d[a:a+n]; a+=n
idxreg="Y" if (mode in (IDY,) or pfx in (0x18,) and mode in (IDX,)) else "X"
if pfx==0xCD: idxreg="Y"
if pfx==0x1A and mode==IDY: idxreg="Y"
tgt=None; flow="seq"; ann=""
if mode==INH: txt=mnem
elif mode==IMM8: txt=f"{mnem} #${ops[0]:02X}"
elif mode==IMM16:
v=(ops[0]<<8)|ops[1]; txt=f"{mnem} #${v:04X}"
elif mode==DIR:
txt=f"{mnem} ${ops[0]:02X}"; ann=reg_ann(ops[0])
elif mode==EXT:
v=(ops[0]<<8)|ops[1]; txt=f"{mnem} ${v:04X}"; ann=reg_ann(v)
if mnem=="JSR": tgt=v; flow="call"
elif mnem=="JMP": tgt=v; flow="jump"
elif mode==IDX or mode==IDY:
txt=f"{mnem} ${ops[0]:02X},{idxreg}"
elif mode==REL:
rel=ops[0]-256 if ops[0]>127 else ops[0]; tgt=a+rel
txt=f"{mnem} ${tgt:04X}"
if mnem=="BRA": flow="jump"
elif mnem=="BSR": flow="call"
else: flow="branch"
elif mode in (BSET_DIR,BCLR_DIR):
txt=f"{mnem} ${ops[0]:02X},#${ops[1]:02X}"; ann=reg_ann(ops[0])
elif mode in (BSET_IDX,BCLR_IDX):
txt=f"{mnem} ${ops[0]:02X},{idxreg},#${ops[1]:02X}"
elif mode in (BRSET_DIR,BRCLR_DIR):
rel=ops[2]-256 if ops[2]>127 else ops[2]; tgt=a+rel
txt=f"{mnem} ${ops[0]:02X},#${ops[1]:02X},${tgt:04X}"; ann=reg_ann(ops[0]); flow="branch"
elif mode in (BRSET_IDX,BRCLR_IDX):
rel=ops[2]-256 if ops[2]>127 else ops[2]; tgt=a+rel
txt=f"{mnem} ${ops[0]:02X},{idxreg},#${ops[1]:02X},${tgt:04X}"; flow="branch"
else: txt=mnem
if mnem in ("RTS","RTI","JMP","BRA","STOP","WAI") and flow not in ("call","branch"):
if mnem in ("RTS","RTI","STOP"): flow="end"
ob=bytes([d[i] for i in range(start,a)])
return Insn(addr=start,end=a,mnem=mnem,mode=mode,opbytes=ob,
txt=txt+ann,target=tgt,flow=flow)
def main():
path=sys.argv[1] if len(sys.argv)>1 else "RIOv4_2.bin"
d=open(path,"rb").read()
LO,HI=0xC000,0x10000
# entry points: reset + all IRQ vectors
entries=set()
for va in range(0xFFD6,0x10000,2):
entries.add(u16(d,va))
# The RX/TX protocol runs as a pointer state machine: handlers are stored
# into dispatch-pointer RAM vars and reached via `JMP $00,X`, which a
# recursive tracer can't follow. Seed every handler by scanning for
# `LDX #imm16 ; STX <dispatchvar>` (CE iw FF pp) and taking imm16.
DISPATCH={0x292F,0x2927,0x2929,0x292B,0x2D3B,0x2D34,0x2D36,0x2D38}
for a in range(LO,HI-5):
if d[a]==0xCE and d[a+3]==0xFF:
iw=(d[a+1]<<8)|d[a+2]; pp=(d[a+4]<<8)|d[a+5]
if pp in DISPATCH and LO<=iw<HI:
entries.add(iw)
insns={}; labels=set(); calls=set()
work=[e for e in entries if LO<=e<HI]
for e in work: labels.add(e)
seen=set()
while work:
a=work.pop()
while LO<=a<HI and a not in insns:
ins=decode(d,a)
if ins is None: break
insns[a]=ins
if ins.target is not None and LO<=ins.target<HI:
labels.add(ins.target)
if ins.flow in ("call",): calls.add(ins.target)
if ins.flow in ("call","branch","jump"):
if ins.target not in insns: work.append(ins.target)
if ins.flow in ("end","jump"): break
a=ins.end
# emit listing
out=[]
a=LO
while a<HI:
if a in insns:
ins=insns[a]
lbl=f"L{a:04X}:" if a in labels else ""
mark=" <<<CALLED" if a in calls else ""
hexb=" ".join(f"{b:02X}" for b in ins.opbytes)
out.append(f"{a:04X} {hexb:<20} {lbl:<8}{ins.txt}{mark}")
a=ins.end
else:
# data byte
out.append(f"{a:04X} {d[a]:02X} .byte ${d[a]:02X}")
a+=1
sys.stdout.write("\n".join(out)+"\n")
sys.stderr.write(f"decoded {len(insns)} insns, {len(labels)} labels, "
f"{len(calls)} call targets\n")
if __name__=="__main__":
main()
-195
View File
@@ -1,195 +0,0 @@
#!/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
# Baud retunes: flag -> (BAUD register value, ACK-wait ticks for ~12.8ms, name).
# The ACK-wait loop self-clocks in byte times (see edit 4), so each rate needs
# its own tick count to hold the same ~12.8ms wall-clock ACK grace window.
BAUD_OPTS = {
"--baud31250": (0x02, 0x28, "31250"), # /1 /4 -> 31250 (40 ticks)
"--baud62500": (0x01, 0x50, "62500"), # /1 /2 -> 62500 (80 ticks)
"--baud125000": (0x00, 0xA0, "125000"), # /1 /1 -> 125000 (160 ticks)
}
selected = [f for f in BAUD_OPTS if f in sys.argv]
assert len(selected) <= 1, f"pick one baud flag, got {selected}"
BAUD_VAL, ACKWAIT_VAL, BAUD_NAME = BAUD_OPTS[selected[0]] if selected else (None, None, None)
# --widen-ackwait: required semantics for 62500/125000 (implied there); optional
# for 31250 to keep the historical v1 image reproducible.
WIDEN_ACKWAIT = "--widen-ackwait" in sys.argv or BAUD_NAME in ("62500", "125000")
assert not ("--widen-ackwait" in sys.argv) or selected, \
"--widen-ackwait only makes sense with a baud retune (the wait is byte-scaled)"
# --e0thresh[=N]: gate the E0 error display (see edit 5). Default N=5.
E0T = None
for a in sys.argv:
if a.startswith("--e0thresh"):
E0T = int(a.split("=", 1)[1]) if "=" in a else 5
assert E0T is None or 1 <= E0T <= 255, f"--e0thresh out of range: {E0T}"
# --checkrepaint: repaint the display after the CheckRequest self-test (edit 6).
CHECKREPAINT = "--checkrepaint" in sys.argv
assert not CHECKREPAINT or E0T is not None, \
"--checkrepaint requires --e0thresh (it re-renders E0 via the gated $D5F2)"
# --reportversion=M.N (edit 7): the major.minor bytes the VersionReply returns.
REPORTVER = None
for a in sys.argv:
if a.startswith("--reportversion="):
REPORTVER = tuple(int(x) for x in a.split("=", 1)[1].split("."))
assert len(REPORTVER) == 2 and all(0 <= v <= 127 for v in REPORTVER), \
f"--reportversion must be M.N with 7-bit values: {REPORTVER}"
args = [a for a in sys.argv[1:] if not a.startswith("--")]
SRC = args[0] if len(args) > 0 else "RIOv4_2.bin"
if len(args) > 1:
DST = args[1]
elif BAUD_NAME == "31250":
DST = "RIOv4_2_patched_31250v2.bin" if WIDEN_ACKWAIT else "RIOv4_2_patched_31250.bin"
elif BAUD_NAME:
DST = f"RIOv4_2_patched_{BAUD_NAME}.bin"
else:
DST = "RIOv4_2_patched.bin"
if E0T is not None and len(args) <= 1:
DST = DST[:-4] + f"_e0t{E0T}.bin"
if CHECKREPAINT and len(args) <= 1:
DST = DST[:-4] + "_ckr.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 BAUD_VAL is not None:
assert d[0xD62A] == 0x86 and bytes(d[0xD62C:0xD62F]) == bytes([0xB7,0x10,0x2B]), \
"expected LDAA #imm ; STAA $102B at $D62A"
patch(0xD62B, [0x30], [BAUD_VAL])
# --- edit 4 (--widen-ackwait; implied for 62500/125000): scale the ACK-wait --
# 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.
# The stock limit (4 ticks) = ~5.2ms at 9600 (USB ACK wins) but shrinks with
# baud (retry storm at 31250; bench 2026-07-18: framing 5655, Abandon 65).
# Each retune holds the window at ~12.8ms wall clock: 40/80/160 ticks.
# $D9E6 81 04 CMPA #$04 -> 81 <ticks>
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], [ACKWAIT_VAL])
# --- edit 5 (--e0thresh=N): threshold-gate the E0 error display -----------
# Stock behavior: every increment of $3187 (TX-ring overflow), $3184 (reply
# retransmit) or $3185 (reply teardown/give-up) immediately calls $D5F2,
# which paints 'E0'+counters over the F0 banner — so ONE benign give-up
# shows E0000001 forever. Gate the render: skip unless any counter >= N.
# $D5F2 prologue (PSHX/PSHA) has already run when we take over its
# LDX #$2038; the cave may clobber A/X because both exits restore them —
# below-threshold leaves via the routine's own epilogue at $D61D
# (PULA/PULX/RTS), at-or-above resumes the render at $D5F7.
if E0T is not None:
patch(0xD5F4, [0xCE, 0x20, 0x38], [0x7E, 0xE0, 0x00]) # LDX #$2038 -> JMP $E000
cave = [0xB6, 0x31, 0x87, # $E000 LDAA $3187 (ring overflows)
0x81, E0T, # $E003 CMPA #N
0x24, 0x11, # $E005 BCC $E018 (>=N -> render)
0xB6, 0x31, 0x84, # $E007 LDAA $3184 (reply retransmits)
0x81, E0T, # $E00A CMPA #N
0x24, 0x0A, # $E00C BCC $E018
0xB6, 0x31, 0x85, # $E00E LDAA $3185 (teardowns/give-ups)
0x81, E0T, # $E011 CMPA #N
0x24, 0x03, # $E013 BCC $E018
0x7E, 0xD6, 0x1D, # $E015 JMP $D61D (below threshold: epilogue)
0xCE, 0x20, 0x38, # $E018 LDX #$2038 (displaced instruction)
0x7E, 0xD5, 0xF7] # $E01B JMP $D5F7 (resume render)
patch(0xE000, [0xFF] * len(cave), cave)
# --- edit 6 (--checkrepaint): repaint the display after the check self-test --
# The CheckRequest handler ($C5A6) renders its five-channel encoder sweep to
# the cockpit display (it sets the test-display flag $2421 for the duration)
# and returns without repainting, leaving a stale '04 00 <ch4 count>' frame
# (idle: 04000000) after every status check. Hook its final TestModeChange
# notify ($C5E4 JSR $C70E) through a cave that also repaints F0000000
# ($CB7C) and then re-renders the E0 error readout via $D5F2 — which, with
# the edit-5 gate, paints only when a counter is at/over threshold. Display
# after any check: F0000000 when healthy, E0... when not.
if CHECKREPAINT:
patch(0xC5E4, [0xBD, 0xC7, 0x0E], [0xBD, 0xE0, 0x20]) # JSR $C70E -> JSR $E020
cave6 = [0xBD, 0xC7, 0x0E, # $E020 JSR $C70E (original notify, kept)
0xBD, 0xCB, 0x7C, # $E023 JSR $CB7C (repaint F0000000)
0xBD, 0xD5, 0xF2, # $E026 JSR $D5F2 (E0 readout iff >= threshold)
0x39] # $E029 RTS
patch(0xE020, [0xFF] * len(cave6), cave6)
# --- edit 7 (--reportversion=M.N): version bytes in the VersionReply --------
# The reply builder at $C6EA loads the payload with LDAA #$04 ($C6F7) and
# LDAA #$02 ($C6FE) — hardcoded "4.2". Nothing host-side validates the value
# (legacy app prints it, RIOJoy parses it, native games ignore it — verified
# by Cyd 2026-07-19), so the christened firmware can announce itself.
if REPORTVER is not None:
assert d[0xC6F7] == 0x86 and d[0xC6FE] == 0x86, "expected LDAA #imm pair at $C6F7/$C6FE"
patch(0xC6F8, [0x04], [REPORTVER[0]])
patch(0xC6FF, [0x02], [REPORTVER[1]])
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}")
@@ -1,28 +0,0 @@
== RIO mash test :: COM1 @ 31250 8N1, 300s, chip label '31250v2-final' ==
lamp echo ON (drives reply/lamp collisions), wedge threshold 2.0s, poll 55ms, app auto-recovery DISABLED
log: C:\VWE\riojoy\riomash-31250v2-final-20260719-141437.log
firmware: 4.2
counters before: RestartCount=0 AbandonCount=0 FullBufferCount=0 [board/lamp faults reported!]
>>> MASH NOW: two hands, 8 lamp buttons, as fast as you can. <<<
>>> Test runs 300s. A wedge alarm will beep + banner. <<<
>>> Ctrl+C ends the run early and still prints the summary. <<<
[ 31.83s] progress: presses=157 (157/min) analog=500 maxGap=0.28s wedges=0
[ 61.84s] progress: presses=432 (419/min) analog=976 maxGap=0.28s wedges=0
[ 65.45s] operator stop (Ctrl+C) — ending run, snapshotting counters...
== MASH SUMMARY [31250v2-final] ==
run : 65s on COM1, lamps on, wedge threshold 2.0s
firmware : 4.2
presses/releases : 436 / 436 (392 presses/min)
analog replies : 1051 (~1181 poll slots; 89.0%), sentinels 0
framing / NAK : 29 / 0 (command resends NAK/timeout: 25)
gap histogram : <100ms:1048 100-250ms:0 250-500ms:2 0.5-1s:0 1-2s:0 2-5s:0 >5s:0
longest gaps : 0.33s@65.4s, 0.28s@0.6s, 0.09s@45.0s, 0.08s@19.8s, 0.08s@22.4s, 0.08s@41.7s, 0.08s@24.5s, 0.08s@8.5s, 0.08s@37.5s, 0.08s@14.6s
wedge events : 0
counters before : RestartCount=0 AbandonCount=0 FullBufferCount=0 [board/lamp faults reported!]
counters after : RestartCount=0 AbandonCount=0 FullBufferCount=0 [board/lamp faults reported!]
counter delta : RestartCount=+0 AbandonCount=+0 FullBufferCount=+0
verdict : PASS — no wedge; compare gap histogram + counter delta to baseline
log file : C:\VWE\riojoy\riomash-31250v2-final-20260719-141437.log
@@ -1,28 +0,0 @@
== RIO mash test :: COM1 @ 31250 8N1, 120s, chip label '31250v2-poll20' ==
lamp echo ON (drives reply/lamp collisions), wedge threshold 2.0s, poll 20ms, app auto-recovery DISABLED
log: C:\VWE\riojoy\riomash-31250v2-poll20-20260719-141620.log
firmware: 4.2
counters before: RestartCount=0 AbandonCount=0 FullBufferCount=0 [board/lamp faults reported!]
>>> MASH NOW: two hands, 8 lamp buttons, as fast as you can. <<<
>>> Test runs 120s. A wedge alarm will beep + banner. <<<
>>> Ctrl+C ends the run early and still prints the summary. <<<
[ 31.97s] progress: presses=63 (63/min) analog=986 maxGap=0.51s wedges=0
[ 62.02s] progress: presses=314 (304/min) analog=1890 maxGap=0.51s wedges=0
[ 65.66s] operator stop (Ctrl+C) — ending run, snapshotting counters...
== MASH SUMMARY [31250v2-poll20] ==
run : 65s on COM1, lamps on, wedge threshold 2.0s
firmware : 4.2
presses/releases : 318 / 318 (285 presses/min)
analog replies : 2036 (~3250 poll slots; 62.6%), sentinels 0
framing / NAK : 73 / 0 (command resends NAK/timeout: 57)
gap histogram : <100ms:2032 100-250ms:1 250-500ms:1 0.5-1s:1 1-2s:0 2-5s:0 >5s:0
longest gaps : 0.51s@0.7s, 0.29s@65.7s, 0.13s@50.6s, 0.08s@55.1s, 0.07s@50.9s, 0.07s@50.7s, 0.07s@50.8s, 0.07s@54.3s, 0.06s@48.5s, 0.06s@57.9s
wedge events : 0
counters before : RestartCount=0 AbandonCount=0 FullBufferCount=0 [board/lamp faults reported!]
counters after : RestartCount=0 AbandonCount=0 FullBufferCount=0 [board/lamp faults reported!]
counter delta : RestartCount=+0 AbandonCount=+0 FullBufferCount=+0
verdict : PASS — no wedge; compare gap histogram + counter delta to baseline
log file : C:\VWE\riojoy\riomash-31250v2-poll20-20260719-141620.log
@@ -1,20 +0,0 @@
== RIO mash test :: COM1 @ 9600 8N1, 300s, chip label 'baseline' ==
lamp echo ON (drives reply/lamp collisions), wedge threshold 2.0s, app auto-recovery DISABLED
log: C:\VWE\riojoy\riomash-baseline-20260718-000311.log
firmware: 4.2
counters before: RestartCount=0 AbandonCount=0 FullBufferCount=0 [board/lamp faults reported!]
>>> MASH NOW: two hands, 8 lamp buttons, as fast as you can. <<<
>>> Test runs 300s. A wedge alarm will beep + banner. <<<
[ 2.70s] *** WEDGE: no analog reply for >2.0s (last at 0.62s). Keep pressing buttons — do NOT power cycle. ***
[ 31.89s] progress: presses=196 (196/min) analog=6 maxGap=0.08s wedges=0 [WEDGED NOW]
[ 62.00s] progress: presses=475 (460/min) analog=6 maxGap=0.08s wedges=0 [WEDGED NOW]
[ 92.03s] progress: presses=663 (432/min) analog=6 maxGap=0.08s wedges=0 [WEDGED NOW]
[ 122.14s] progress: presses=781 (384/min) analog=6 maxGap=0.08s wedges=0 [WEDGED NOW]
[ 152.22s] progress: presses=894 (352/min) analog=6 maxGap=0.08s wedges=0 [WEDGED NOW]
[ 182.28s] progress: presses=894 (294/min) analog=6 maxGap=0.08s wedges=0 [WEDGED NOW]
[ 212.28s] progress: presses=951 (269/min) analog=6 maxGap=0.08s wedges=0 [WEDGED NOW]
[ 242.36s] progress: presses=951 (235/min) analog=6 maxGap=0.08s wedges=0 [WEDGED NOW]
[ 272.40s] progress: presses=951 (209/min) analog=6 maxGap=0.08s wedges=0 [WEDGED NOW]
[ 301.88s] *** RUN ENDED WHILE WEDGED (301.3s and counting) ***
@@ -1,29 +0,0 @@
== RIO mash test :: COM1 @ 125000 8N1, 300s, chip label 'patched-125000' ==
lamp echo ON (drives reply/lamp collisions), wedge threshold 2.0s, app auto-recovery DISABLED
log: C:\VWE\riojoy\riomash-patched-125000-20260718-125310.log
firmware: 4.2
counters before: (no status reply — reply path dead?)
>>> MASH NOW: two hands, 8 lamp buttons, as fast as you can. <<<
>>> Test runs 300s. A wedge alarm will beep + banner. <<<
>>> Ctrl+C ends the run early and still prints the summary. <<<
[ 33.05s] progress: presses=147 (147/min) analog=344 maxGap=0.31s wedges=0
[ 63.12s] progress: presses=472 (449/min) analog=668 maxGap=0.37s wedges=0
[ 93.14s] progress: presses=741 (477/min) analog=999 maxGap=0.44s wedges=0
[ 103.09s] operator stop (Ctrl+C) — ending run, snapshotting counters...
== MASH SUMMARY [patched-125000] ==
run : 103s on COM1, lamps on, wedge threshold 2.0s
firmware : 4.2
presses/releases : 827 / 824 (470 presses/min)
analog replies : 1134 (~1872 poll slots; 60.6%), sentinels 0
framing / NAK : 5 / 1082
gap histogram : <100ms:804 100-250ms:298 250-500ms:30 0.5-1s:1 1-2s:0 2-5s:0 >5s:0
longest gaps : 0.52s@95.7s, 0.44s@63.3s, 0.44s@86.0s, 0.38s@79.8s, 0.37s@56.4s, 0.37s@73.4s, 0.37s@81.5s, 0.34s@95.3s, 0.31s@40.8s, 0.31s@58.4s
wedge events : 0
counters before : (no status reply — reply path dead?)
counters after : (no status reply — reply path dead?)
counter delta : (incomplete — a snapshot got no reply)
verdict : PASS — no wedge; compare gap histogram + counter delta to baseline
log file : C:\VWE\riojoy\riomash-patched-125000-20260718-125310.log
@@ -1,16 +0,0 @@
== RIO mash test :: COM1 @ 9600 8N1, 300s, chip label 'patched' ==
lamp echo ON (drives reply/lamp collisions), wedge threshold 2.0s, app auto-recovery DISABLED
log: C:\VWE\riojoy\riomash-patched-20260718-001800.log
firmware: 4.2
counters before: RestartCount=0 AbandonCount=0 FullBufferCount=0 [board/lamp faults reported!]
>>> MASH NOW: two hands, 8 lamp buttons, as fast as you can. <<<
>>> Test runs 300s. A wedge alarm will beep + banner. <<<
[ 31.82s] progress: presses=227 (227/min) analog=501 maxGap=0.58s wedges=0
[ 61.91s] progress: presses=556 (539/min) analog=984 maxGap=0.58s wedges=0
[ 91.99s] progress: presses=862 (562/min) analog=1462 maxGap=0.58s wedges=0
[ 122.05s] progress: presses=1292 (635/min) analog=1941 maxGap=0.58s wedges=0
[ 152.13s] progress: presses=1315 (519/min) analog=2419 maxGap=0.58s wedges=0
[ 182.17s] progress: presses=1315 (433/min) analog=2899 maxGap=0.58s wedges=0
[ 212.18s] progress: presses=1315 (372/min) analog=3378 maxGap=0.58s wedges=0
@@ -1,30 +0,0 @@
== RIO mash test :: COM1 @ 31250 8N1, 300s, chip label 'patched-31250' ==
lamp echo ON (drives reply/lamp collisions), wedge threshold 2.0s, app auto-recovery DISABLED
log: C:\VWE\riojoy\riomash-patched-31250-20260718-003114.log
firmware: (no version reply!)
counters before: (no status reply — reply path dead?)
>>> MASH NOW: two hands, 8 lamp buttons, as fast as you can. <<<
>>> Test runs 300s. A wedge alarm will beep + banner. <<<
>>> Ctrl+C ends the run early and still prints the summary. <<<
[ 31.90s] progress: presses=188 (188/min) analog=1185 maxGap=0.09s wedges=0
[ 61.90s] progress: presses=395 (383/min) analog=2370 maxGap=0.09s wedges=0
[ 91.93s] progress: presses=798 (521/min) analog=3559 maxGap=0.09s wedges=0
[ 121.96s] progress: presses=932 (459/min) analog=4713 maxGap=0.09s wedges=0
[ 126.86s] operator stop (Ctrl+C) — ending run, snapshotting counters...
== MASH SUMMARY [patched-31250] ==
run : 126s on COM1, lamps on, wedge threshold 2.0s
firmware : 4.2
presses/releases : 946 / 977 (443 presses/min)
analog replies : 4927 (~2290 poll slots; 215.2%), sentinels 0
framing / NAK : 5655 / 5
gap histogram : <100ms:4925 100-250ms:0 250-500ms:0 0.5-1s:1 1-2s:0 2-5s:0 >5s:0
longest gaps : 0.54s@126.9s, 0.09s@28.6s, 0.08s@29.5s, 0.08s@121.9s, 0.08s@24.3s, 0.08s@108.9s, 0.08s@21.8s, 0.08s@85.9s, 0.08s@88.9s, 0.08s@83.5s
wedge events : 0
counters before : (no status reply — reply path dead?)
counters after : RestartCount=2 AbandonCount=65 FullBufferCount=0 [board/lamp faults reported!]
counter delta : (incomplete — a snapshot got no reply)
verdict : PASS — no wedge; compare gap histogram + counter delta to baseline
log file : C:\VWE\riojoy\riomash-patched-31250-20260718-003114.log
@@ -1,32 +0,0 @@
== RIO mash test :: COM1 @ 31250 8N1, 300s, chip label 'patched-31250v2' ==
lamp echo ON (drives reply/lamp collisions), wedge threshold 2.0s, app auto-recovery DISABLED
log: C:\VWE\riojoy\riomash-patched-31250v2-20260718-110853.log
firmware: 4.2
counters before: RestartCount=0 AbandonCount=0 FullBufferCount=0 [board/lamp faults reported!]
>>> MASH NOW: two hands, 8 lamp buttons, as fast as you can. <<<
>>> Test runs 300s. A wedge alarm will beep + banner. <<<
>>> Ctrl+C ends the run early and still prints the summary. <<<
[ 33.03s] progress: presses=63 (63/min) analog=523 maxGap=0.28s wedges=0
[ 63.05s] progress: presses=165 (157/min) analog=1002 maxGap=0.28s wedges=0
[ 93.08s] progress: presses=185 (119/min) analog=1481 maxGap=0.28s wedges=0
[ 123.11s] progress: presses=347 (169/min) analog=1958 maxGap=0.28s wedges=0
[ 153.22s] progress: presses=504 (197/min) analog=2440 maxGap=0.28s wedges=0
[ 183.26s] progress: presses=763 (250/min) analog=2919 maxGap=0.28s wedges=0
[ 191.98s] operator stop (Ctrl+C) — ending run, snapshotting counters...
== MASH SUMMARY [patched-31250v2] ==
run : 191s on COM1, lamps on, wedge threshold 2.0s
firmware : 4.2
presses/releases : 775 / 775 (241 presses/min)
analog replies : 3076 (~3472 poll slots; 88.6%), sentinels 0
framing / NAK : 54 / 5
gap histogram : <100ms:3068 100-250ms:5 250-500ms:2 0.5-1s:0 1-2s:0 2-5s:0 >5s:0
longest gaps : 0.30s@192.0s, 0.28s@1.8s, 0.14s@57.2s, 0.13s@107.8s, 0.12s@98.5s, 0.11s@130.9s, 0.10s@165.4s, 0.10s@24.5s, 0.10s@151.9s, 0.10s@167.4s
wedge events : 0
counters before : RestartCount=0 AbandonCount=0 FullBufferCount=0 [board/lamp faults reported!]
counters after : RestartCount=0 AbandonCount=0 FullBufferCount=0 [board/lamp faults reported!]
counter delta : RestartCount=+0 AbandonCount=+0 FullBufferCount=+0
verdict : PASS — no wedge; compare gap histogram + counter delta to baseline
log file : C:\VWE\riojoy\riomash-patched-31250v2-20260718-110853.log
@@ -1,30 +0,0 @@
== RIO mash test :: COM1 @ 62500 8N1, 300s, chip label 'patched-62500' ==
lamp echo ON (drives reply/lamp collisions), wedge threshold 2.0s, app auto-recovery DISABLED
log: C:\VWE\riojoy\riomash-patched-62500-20260718-130333.log
firmware: 4.2
counters before: RestartCount=0 AbandonCount=0 FullBufferCount=0 [board/lamp faults reported!]
>>> MASH NOW: two hands, 8 lamp buttons, as fast as you can. <<<
>>> Test runs 300s. A wedge alarm will beep + banner. <<<
>>> Ctrl+C ends the run early and still prints the summary. <<<
[ 33.04s] progress: presses=156 (156/min) analog=521 maxGap=0.22s wedges=0
[ 63.12s] progress: presses=299 (284/min) analog=998 maxGap=0.22s wedges=0
[ 93.15s] progress: presses=396 (255/min) analog=1478 maxGap=0.22s wedges=0
[ 123.17s] progress: presses=505 (246/min) analog=1954 maxGap=0.22s wedges=0
[ 132.35s] operator stop (Ctrl+C) — ending run, snapshotting counters...
== MASH SUMMARY [patched-62500] ==
run : 132s on COM1, lamps on, wedge threshold 2.0s
firmware : 4.2
presses/releases : 530 / 530 (238 presses/min)
analog replies : 2115 (~2400 poll slots; 88.1%), sentinels 0
framing / NAK : 24 / 69
gap histogram : <100ms:2103 100-250ms:11 250-500ms:0 0.5-1s:0 1-2s:0 2-5s:0 >5s:0
longest gaps : 0.25s@132.3s, 0.22s@1.8s, 0.14s@54.9s, 0.14s@117.2s, 0.13s@43.6s, 0.13s@14.8s, 0.13s@43.1s, 0.13s@115.0s, 0.12s@124.0s, 0.12s@94.9s
wedge events : 0
counters before : RestartCount=0 AbandonCount=0 FullBufferCount=0 [board/lamp faults reported!]
counters after : RestartCount=0 AbandonCount=0 FullBufferCount=0 [board/lamp faults reported!]
counter delta : RestartCount=+0 AbandonCount=+0 FullBufferCount=+0
verdict : PASS — no wedge; compare gap histogram + counter delta to baseline
log file : C:\VWE\riojoy\riomash-patched-62500-20260718-130333.log
@@ -1,29 +0,0 @@
== RIO mash test :: COM1 @ 62500 8N1, 300s, chip label 'patched-62500-retx' ==
lamp echo ON (drives reply/lamp collisions), wedge threshold 2.0s, app auto-recovery DISABLED
log: C:\VWE\riojoy\riomash-patched-62500-retx-20260719-131037.log
firmware: 4.2
counters before: RestartCount=0 AbandonCount=0 FullBufferCount=0 [board/lamp faults reported!]
>>> MASH NOW: two hands, 8 lamp buttons, as fast as you can. <<<
>>> Test runs 300s. A wedge alarm will beep + banner. <<<
>>> Ctrl+C ends the run early and still prints the summary. <<<
[ 33.03s] progress: presses=245 (245/min) analog=515 maxGap=0.27s wedges=0
[ 63.05s] progress: presses=568 (541/min) analog=992 maxGap=0.27s wedges=0
[ 93.11s] progress: presses=709 (457/min) analog=1470 maxGap=0.27s wedges=0
[ 95.30s] operator stop (Ctrl+C) — ending run, snapshotting counters...
== MASH SUMMARY [patched-62500-retx] ==
run : 95s on COM1, lamps on, wedge threshold 2.0s
firmware : 4.2
presses/releases : 709 / 710 (440 presses/min)
analog replies : 1522 (~1727 poll slots; 88.1%), sentinels 0
framing / NAK : 61 / 321 (NAK-triggered resends: 321)
gap histogram : <100ms:1510 100-250ms:9 250-500ms:2 0.5-1s:0 1-2s:0 2-5s:0 >5s:0
longest gaps : 0.27s@1.8s, 0.25s@95.3s, 0.13s@77.1s, 0.13s@26.6s, 0.12s@26.3s, 0.12s@57.4s, 0.12s@12.7s, 0.12s@11.5s, 0.12s@18.8s, 0.12s@31.3s
wedge events : 0
counters before : RestartCount=0 AbandonCount=0 FullBufferCount=0 [board/lamp faults reported!]
counters after : RestartCount=0 AbandonCount=0 FullBufferCount=0 [board/lamp faults reported!]
counter delta : RestartCount=+0 AbandonCount=+0 FullBufferCount=+0
verdict : PASS — no wedge; compare gap histogram + counter delta to baseline
log file : C:\VWE\riojoy\riomash-patched-62500-retx-20260719-131037.log
@@ -1,28 +0,0 @@
== RIO mash test :: COM1 @ 62500 8N1, 300s, chip label 'patched-62500-sw' ==
lamp echo ON (drives reply/lamp collisions), wedge threshold 2.0s, app auto-recovery DISABLED
log: C:\VWE\riojoy\riomash-patched-62500-sw-20260719-132841.log
firmware: 4.2
counters before: RestartCount=0 AbandonCount=0 FullBufferCount=0 [board/lamp faults reported!]
>>> MASH NOW: two hands, 8 lamp buttons, as fast as you can. <<<
>>> Test runs 300s. A wedge alarm will beep + banner. <<<
>>> Ctrl+C ends the run early and still prints the summary. <<<
[ 31.90s] progress: presses=115 (115/min) analog=492 maxGap=0.22s wedges=0
[ 61.94s] progress: presses=224 (217/min) analog=968 maxGap=0.22s wedges=0
[ 80.54s] operator stop (Ctrl+C) — ending run, snapshotting counters...
== MASH SUMMARY [patched-62500-sw] ==
run : 80s on COM1, lamps on, wedge threshold 2.0s
firmware : 4.2
presses/releases : 285 / 285 (209 presses/min)
analog replies : 1279 (~1454 poll slots; 88.0%), sentinels 0
framing / NAK : 22 / 48 (command resends NAK/timeout: 67)
gap histogram : <100ms:1261 100-250ms:17 250-500ms:0 0.5-1s:0 1-2s:0 2-5s:0 >5s:0
longest gaps : 0.24s@80.5s, 0.22s@0.6s, 0.13s@47.9s, 0.13s@77.5s, 0.12s@21.2s, 0.12s@20.4s, 0.12s@22.9s, 0.11s@21.4s, 0.11s@49.3s, 0.11s@28.9s
wedge events : 0
counters before : RestartCount=0 AbandonCount=0 FullBufferCount=0 [board/lamp faults reported!]
counters after : RestartCount=0 AbandonCount=0 FullBufferCount=0 [board/lamp faults reported!]
counter delta : RestartCount=+0 AbandonCount=+0 FullBufferCount=+0
verdict : PASS — no wedge; compare gap histogram + counter delta to baseline
log file : C:\VWE\riojoy\riomash-patched-62500-sw-20260719-132841.log
@@ -1,27 +0,0 @@
== RIO mash test :: COM1 @ 62500 8N1, 300s, chip label 'patched-62500-sw' ==
lamp echo ON (drives reply/lamp collisions), wedge threshold 2.0s, app auto-recovery DISABLED
log: C:\VWE\riojoy\riomash-patched-62500-sw-20260719-133628.log
firmware: 4.2
counters before: RestartCount=0 AbandonCount=0 FullBufferCount=0 [board/lamp faults reported!]
>>> MASH NOW: two hands, 8 lamp buttons, as fast as you can. <<<
>>> Test runs 300s. A wedge alarm will beep + banner. <<<
>>> Ctrl+C ends the run early and still prints the summary. <<<
[ 31.90s] progress: presses=72 (72/min) analog=497 maxGap=0.21s wedges=0
[ 58.27s] operator stop (Ctrl+C) — ending run, snapshotting counters...
== MASH SUMMARY [patched-62500-sw] ==
run : 58s on COM1, lamps on, wedge threshold 2.0s
firmware : 4.2
presses/releases : 123 / 123 (124 presses/min)
analog replies : 931 (~1054 poll slots; 88.3%), sentinels 0
framing / NAK : 7 / 20 (command resends NAK/timeout: 34)
gap histogram : <100ms:921 100-250ms:9 250-500ms:0 0.5-1s:0 1-2s:0 2-5s:0 >5s:0
longest gaps : 0.22s@58.3s, 0.21s@0.6s, 0.14s@41.9s, 0.14s@26.8s, 0.13s@23.6s, 0.11s@12.4s, 0.11s@24.4s, 0.10s@54.1s, 0.10s@53.4s, 0.10s@16.5s
wedge events : 0
counters before : RestartCount=0 AbandonCount=0 FullBufferCount=0 [board/lamp faults reported!]
counters after : RestartCount=0 AbandonCount=0 FullBufferCount=0 [board/lamp faults reported!]
counter delta : RestartCount=+0 AbandonCount=+0 FullBufferCount=+0
verdict : PASS — no wedge; compare gap histogram + counter delta to baseline
log file : C:\VWE\riojoy\riomash-patched-62500-sw-20260719-133628.log
@@ -1,28 +0,0 @@
== RIO mash test :: COM1 @ 9600 8N1, 120s, chip label 'patched-e0t5-9600' ==
lamp echo ON (drives reply/lamp collisions), wedge threshold 2.0s, poll 55ms, app auto-recovery DISABLED
log: c:\VWE\riojoy\riomash-patched-e0t5-9600-20260719-154259.log
firmware: 4.2
counters before: RestartCount=0 AbandonCount=0 FullBufferCount=0 [board/lamp faults reported!]
>>> MASH NOW: two hands, 8 lamp buttons, as fast as you can. <<<
>>> Test runs 120s. A wedge alarm will beep + banner. <<<
>>> Ctrl+C ends the run early and still prints the summary. <<<
[ 31.88s] progress: presses=44 (44/min) analog=496 maxGap=0.63s wedges=0
[ 61.96s] progress: presses=226 (219/min) analog=952 maxGap=0.63s wedges=0
[ 91.97s] progress: presses=236 (154/min) analog=1428 maxGap=0.63s wedges=0
== MASH SUMMARY [patched-e0t5-9600] ==
run : 120s on COM1, lamps on, wedge threshold 2.0s
firmware : 4.2
presses/releases : 236 / 236 (115 presses/min)
analog replies : 1919 (~2181 poll slots; 88.0%), sentinels 0
framing / NAK : 75 / 0 (command resends NAK/timeout: 69)
gap histogram : <100ms:1911 100-250ms:5 250-500ms:0 0.5-1s:2 1-2s:0 2-5s:0 >5s:0
longest gaps : 0.66s@121.8s, 0.63s@0.6s, 0.14s@40.3s, 0.13s@42.0s, 0.13s@37.2s, 0.11s@76.9s, 0.11s@58.3s, 0.10s@53.4s, 0.10s@112.9s, 0.10s@35.4s
wedge events : 0
counters before : RestartCount=0 AbandonCount=0 FullBufferCount=0 [board/lamp faults reported!]
counters after : RestartCount=0 AbandonCount=0 FullBufferCount=0 [board/lamp faults reported!]
counter delta : RestartCount=+0 AbandonCount=+0 FullBufferCount=+0
verdict : PASS — no wedge; compare gap histogram + counter delta to baseline
log file : c:\VWE\riojoy\riomash-patched-e0t5-9600-20260719-154259.log
@@ -1,28 +0,0 @@
== RIO mash test :: COM1 @ 9600 8N1, 120s, chip label 'rio43rc1-9600' ==
lamp echo ON (drives reply/lamp collisions), wedge threshold 2.0s, poll 55ms, app auto-recovery DISABLED
log: c:\VWE\riojoy\riomash-rio43rc1-9600-20260719-164901.log
firmware: 4.2
counters before: RestartCount=0 AbandonCount=0 FullBufferCount=0 [board/lamp faults reported!]
>>> MASH NOW: two hands, 8 lamp buttons, as fast as you can. <<<
>>> Test runs 120s. A wedge alarm will beep + banner. <<<
>>> Ctrl+C ends the run early and still prints the summary. <<<
[ 31.92s] progress: presses=258 (258/min) analog=474 maxGap=0.64s wedges=0
[ 61.98s] progress: presses=588 (569/min) analog=918 maxGap=0.64s wedges=0
[ 91.98s] progress: presses=922 (601/min) analog=1356 maxGap=0.64s wedges=0
== MASH SUMMARY [rio43rc1-9600] ==
run : 120s on COM1, lamps on, wedge threshold 2.0s
firmware : 4.2
presses/releases : 1129 / 1118 (549 presses/min)
analog replies : 1811 (~2181 poll slots; 83.0%), sentinels 0
framing / NAK : 298 / 0 (command resends NAK/timeout: 247)
gap histogram : <100ms:1778 100-250ms:29 250-500ms:0 0.5-1s:3 1-2s:0 2-5s:0 >5s:0
longest gaps : 0.68s@121.9s, 0.64s@0.6s, 0.57s@122.6s, 0.15s@32.2s, 0.14s@84.3s, 0.13s@52.3s, 0.13s@51.5s, 0.13s@93.5s, 0.13s@60.1s, 0.13s@15.2s
wedge events : 0
counters before : RestartCount=0 AbandonCount=0 FullBufferCount=0 [board/lamp faults reported!]
counters after : RestartCount=0 AbandonCount=0 FullBufferCount=0 [board/lamp faults reported!]
counter delta : RestartCount=+0 AbandonCount=+0 FullBufferCount=+0
verdict : PASS — no wedge; compare gap histogram + counter delta to baseline
log file : c:\VWE\riojoy\riomash-rio43rc1-9600-20260719-164901.log
-34
View File
@@ -1,34 +0,0 @@
# Why your pod's stick sometimes "died" mid-battle — and why it won't anymore
*A plain-language explanation for pod owners. The technical write-up is in
[RIOv4_2-ANALYSIS.md](RIOv4_2-ANALYSIS.md).*
Inside every pod is a small computer board from 1994 (the **RIO**) whose job
is to tell the game what you're doing: which buttons you pressed, and where
you've moved the stick, throttle, and pedals.
The board and the game talk like two people on walkie-talkies. Every time the
board reports your stick position, it waits for the game to answer *"got it."*
During calm flying that works fine. But in a frantic firefight — lamps
flashing, buttons hammering — the channel gets crowded, and once in a while
that little *"got it"* gets drowned out.
When that happens, the board repeats itself a few times and then, sensibly,
gives up and moves on. Here's the bug: while it's sending a position report,
the board puts up a mental sticky note that says *"busy — already sending
one."* When a report goes through normally, it takes the note down. But on the
give-up path, the 1994 programmers forgot one line — **the note never comes
down.** From that moment the board looks at the sticky note, decides it's still
busy, and quietly throws away every stick and throttle update forever after.
That's the "wedge" you may have felt: mid-mission, your stick and throttle
just stop doing anything, while the buttons and lamps still work fine —
because those travel a different path that the sticky note doesn't block.
Starting a new mission always cured it, since a new game tells the board to
clean house, sticky note included.
The **RIO 4.3** firmware adds the one missing instruction: whenever the board
gives up on a report, it now takes the sticky note down too. We stress-tested
it by mashing buttons at over nine presses *per second* for two minutes
straight — the old board wedged under that abuse; the new one never misses a
beat.
+2 -2
View File
@@ -12,8 +12,8 @@ public sealed class AppConfig
/// <summary>
/// RIO link baud rate. 9600 = stock/wedge-patched firmware (native-game
/// compatible). 31250 = the validated FastRIO chip
/// (rio-firmware/RIOv4_2_patched_31250v2.bin) — needs an FTDI-class
/// compatible). 31250 = the FastRIO chip (TeslaRel410
/// restoration/rio-firmware/RIOv4_3_fastrio.bin) — needs an FTDI-class
/// adapter; classic 16550 UARTs cannot make this rate.
/// </summary>
public int RioBaudRate { get; set; } = 9600;
@@ -25,8 +25,8 @@ public sealed class SerialPortTransport : IRioTransport
/// <param name="portName">COM port name, e.g. "COM1".</param>
/// <param name="baudRate">
/// Link rate; default 9600 (stock firmware). The 31250-baud firmware
/// variant (rio-firmware/, --baud31250) needs an adapter that can make
/// Link rate; default 9600 (stock firmware). The 31250-baud FastRIO
/// firmware (TeslaRel410 restoration/rio-firmware) needs an adapter that can make
/// non-standard rates exactly — FTDI-class USB serial can, classic
/// 16550 UARTs cannot.
/// </param>
+1 -1
View File
@@ -4,7 +4,7 @@ using RioJoy.Core.Serial;
namespace RioSerialMonitor;
/// <summary>
/// E0-threshold firmware verification (rio-firmware `--e0thresh` images).
/// E0-threshold firmware verification (TeslaRel410 restoration/rio-firmware `--e0thresh` images).
///
/// Two phases against a DTR-reset board (counters zeroed, display F0000000):
/// - Phase A (gate hold): analog requests answered NAK-then-ACK — NAKing
+2 -2
View File
@@ -7,8 +7,8 @@ namespace RioSerialMonitor;
/// <summary>
/// Instrumented firmware mash test (the RIO_TAP protocol, mechanized) for
/// validating the RIOv4.2 reply-latch wedge patch — see
/// rio-firmware/RIOv4_2-ANALYSIS.md "Validation plan".
/// validating the RIOv4.2 reply-latch wedge patch — see the firmware
/// analysis in the TeslaRel410 repo (restoration/rio-firmware/RIOv4_2-ANALYSIS.md).
///
/// Runs the live link with the app's own &gt;5s reset-recovery DISABLED (so a
/// board wedge stays observable instead of being revived by our watchdog),
+2 -2
View File
@@ -12,10 +12,10 @@ using RioJoy.Core.Serial;
// 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] [--baud rate]
// --baud: 31250 for the retuned firmware variant (rio-firmware --baud31250).
// --baud: 31250 for the retuned FastRIO firmware (TeslaRel410 restoration/rio-firmware, make_patch.py --baud31250).
// Exit: 0 = ran, 2 = could not open the port (mash: 1 = wedge detected).
// E0-threshold firmware verification (rio-firmware --e0thresh images, see E0Test.cs):
// E0-threshold firmware verification (TeslaRel410 restoration/rio-firmware --e0thresh images, see E0Test.cs):
// dotnet run --project tools/RioSerialMonitor -- --e0test [port] [--baud rate] [--events n]
if (args.Contains("--e0test"))
return await RioSerialMonitor.E0Test.RunAsync(args);