New IO-ARCHITECTURE.md, derived from the v4.2 image, the U7 GAL decode and schematic sheets 1/3: - $A010 control-latch bit map (CCK / D_OUT / WR_SB / RD_SB / SEL0-2) - the 50-byte script format the firmware replays, and the ROM table map - full port/address population: 9 buttons boards + 2 keypads across all 8 ports; the 0x00-0x6F logical map and its 0x48-0x4F gap - the keypad engine ($CC53/$CC7E): 4-row matrix scan, row-patched RAM scripts, the $DC14 key-code table, message type $8B - lamp readback ($21C2) and the 72-byte lamp-fault mask at $DFA8 -- fault reports are type $03 with the lamp index in $2519 - scan cycle budget: 1.91 ms/pass, 17.3 ms/scan, ~58 Hz; demux is 60% - expansion: one spare buttons board, and nothing further without a protocol revision Corrections to existing docs: - $CC53 was cited as the encoder sweep; it is the keypad-1 scanner. The sweep is $C8CC-$C9A7, driven from $C0CB. - U31 is on sheet 3, not sheet 1, and is completely uncommitted: no address, data, strobe or pod-bus signal reaches it. Not an expansion hook -- populating it does nothing without new wiring. - No spare HCTL-2016 footprints exist. The decode has 3 free selects on U9, but the PCB carries exactly 5 positions and sheet 1 draws 5. More analog axes need hardware, not firmware. Also carries the previously-uncommitted standard-rate (16550) feasibility analysis and baudscan.py, plus a note that the FastRIO "FTDI-class adapter" rule really means arbitrary-rate generation: CP2102N qualifies, classic CP2102 snaps 31250 to 38400. Bench-measured 2026-07-26; on-cockpit latency check still pending. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
539 lines
22 KiB
Markdown
539 lines
22 KiB
Markdown
# RIO I/O architecture — the pod bus, decoded
|
||
|
||
How the RIO board actually moves buttons, keypads and lamps over its
|
||
eight-port "pod bus": the control-latch bit map, the 50-byte script
|
||
format the firmware replays, the complete port/address population of a
|
||
pod, and what it all costs in CPU cycles.
|
||
|
||
Derived 2026-07-26 from [`RIOv4_2.disasm.asm`](RIOv4_2.disasm.asm) plus
|
||
the recovered memory map in
|
||
[`../rio-hardware/gal/README.md`](../rio-hardware/gal/README.md) and
|
||
schematic sheet 3 (`RIO_1407`). Cross-checked against the physical
|
||
inventory (9 buttons boards + 2 keypads per pod, operator-confirmed).
|
||
|
||
Everything here is read out of the firmware image unless flagged
|
||
**INFERRED** or **UNVERIFIED**.
|
||
|
||
## The control latch at `$A010`
|
||
|
||
The pod bus is bit-banged by rewriting one 74HC574 latch (`BND_SEL`,
|
||
write-only, decoded by U7). Every bus edge is one `STAA $A010`. Bit
|
||
assignment recovered by diffing all 11 scripts in ROM:
|
||
|
||
| Bit | Signal | Behaviour |
|
||
|-----|--------|-----------|
|
||
| 0 | `CCK` | shift clock — toggles on every write |
|
||
| 1 | `D_OUT` / `DD_IN` | serial data into the pod's 74HCT164 |
|
||
| 2 | `WR_SB` | write strobe — **used only in write scripts** |
|
||
| 3 | `RD_SB` | read strobe — parallel-loads the pod's 74HCT165 |
|
||
| 4 | *(unidentified)* | **static 1** in every script |
|
||
| 5-7 | `SEL0-2` | port select → 74LS138; all 8 values used |
|
||
|
||
`SEL0` = bit 5, `SEL1` = bit 6, `SEL2` = bit 7, so a script's first byte
|
||
`$N0` gives its port directly as `N >> 1`.
|
||
|
||
## The 50-byte script format
|
||
|
||
The firmware does not bit-bang inline. It replays precomputed **50-byte
|
||
scripts**, one latch state per byte:
|
||
|
||
- **18 bytes — preamble.** Nine `CCK` pulses shifting a walking `1`
|
||
down the cable into the board's address 74HCT164; its position picks
|
||
the `ADR. SEL.` jumper (`A-0` = shift position 7, `A-1` = 6,
|
||
`A-2` = 5). The last pair asserts the strobe.
|
||
- **32 bytes — payload.** 16 `CCK` pulses, 2 bytes each. On reads,
|
||
`PORTE` is sampled between pulses; on writes, `WR_SB` (bit 2) latches.
|
||
|
||
18 + 32 = 50, matching the table stride exactly.
|
||
|
||
### The acquisition routine `$CFC0`
|
||
|
||
```
|
||
CFC0 PSHA/PSHB/PSHX/PSHY ; prologue
|
||
CFC5 LDY #$2152 ; sample destination
|
||
CFC9 LDX $2150 ; script pointer
|
||
CFCC LDAB #$12 ; 18 preamble bytes
|
||
CFCE LDAA $00,X / STAA $A010 / INX / DECB / BNE ; preamble loop
|
||
CFD8 LDAB #$10 / STAB $2036 ; 16 bits
|
||
CFDD LDAA $100A ; sample PORTE <-- bit loop top
|
||
CFE0 PSHX / STY $2031 / LDX $2031 / STAA $00,X / PULX / INY
|
||
CFED LDAB #$02
|
||
CFEF LDAA $00,X / STAA $A010 / INX / DECB / BNE ; one CCK pulse
|
||
CFF8 DEC $2036 / BNE $CFDD
|
||
CFFD NOP / PULY / PULX / PULB / PULA / RTS
|
||
```
|
||
|
||
> **Disassembler sync gap.** `RIOv4_2.disasm.asm` renders
|
||
> `$CFE0-$D003` as `.byte` — it lost sync on the `18 FF` (`STY`
|
||
> extended) prebyte. The listing above is the hand decode; both branch
|
||
> displacements (`$CFF6` → `$CFEF`, `$CFFB` → `$CFDD`) land exactly on
|
||
> its instruction boundaries, which confirms it. The same gap recurs
|
||
> at `$C380-$C38F`.
|
||
|
||
`PORTE` (`$100A`) is read at **exactly one site in the whole firmware**
|
||
— this one. Every bit of pod input arrives here.
|
||
|
||
### Script tables in ROM
|
||
|
||
| Region | Size | Contents |
|
||
|--------|------|----------|
|
||
| `$DBB0-$DC13` | 2 × 50 | **keypad scripts** — SEL 4, SEL 5 |
|
||
| `$DC14-$DC23` | 16 B | **keypad key-code table** (see below) |
|
||
| `$DC24-$DDE5` | 9 × 50 | **read scripts** — the 9 buttons boards |
|
||
| `$DDE6-$DFA7` | 9 × 50 | **write scripts** — lamps (`WR_SB` active) |
|
||
| `$DFA8-$DFEF` | **72 B** | **lamp-fault mask** — one byte per lamp (see below) |
|
||
| `$DFF0-$FFxx` | 8,144 B | erased — the largest free run in the EPROM |
|
||
|
||
The blocks butt against each other with **zero slack**: there is no
|
||
reserved tenth script anywhere in the image.
|
||
|
||
## Port and address population of a pod
|
||
|
||
Scan order is `$CF40`, nine `JSR $CFB6` calls. SEL values across the
|
||
scripts are `0,1,2,2,3,3,3,6,7`, matching the `$204E` increment
|
||
sequence exactly (`$204E` = SEL + 1; it starts at 1 per `$CF3E`).
|
||
|
||
| Port | Board | Addr | Logical range |
|
||
|-----:|-------|------|---------------|
|
||
| 0 | buttons | A-0 | `0x00-0x07` |
|
||
| 1 | buttons | A-0 | `0x08-0x0F` |
|
||
| 2 | buttons ×2 | A-0, A-1 | `0x10-0x1F` |
|
||
| 3 | buttons ×3 | A-0, A-1, A-2 | `0x20-0x37` |
|
||
| 4 | **keypad 1** | A-0 | `0x50-0x5F` |
|
||
| 5 | **keypad 2** | A-0 | `0x60-0x6F` |
|
||
| 6 | buttons | A-0 | `0x38-0x3F` |
|
||
| 7 | buttons | A-0 | `0x40-0x47` |
|
||
|
||
All eight ports are populated. The button scan's `INC $204E` ×3 at
|
||
`$CF8B` skips ports 4-5 because those are keypads, driven from the
|
||
separate script pair at `$DBB0`/`$DBE2` (referenced `LDX #$DBB0` at
|
||
`$C350`).
|
||
|
||
**Port→logical assignment within port 3 and the port-6/7 ordering is
|
||
INFERRED** from scan order (`$2037` increments monotonically, 8 per
|
||
script); the ROM does not label them.
|
||
|
||
## The keypads — a second, separate engine
|
||
|
||
The keypads do **not** ride the button scan. They have their own
|
||
scripts, their own bit-bang engine, their own RAM, and their own
|
||
message type. This is why they never appear in the 72-entry button
|
||
arrays.
|
||
|
||
### Templates are copied ROM → RAM so rows can be patched
|
||
|
||
`$C34C` copies **100 bytes** (`LDAB #$64`) from `$DBB0` to **`$2050`** —
|
||
both keypad scripts. `$C369` does the same for the 9 write scripts
|
||
(`$DDE6` → `$220A`, `#$09` × `#$32`). Scripts live in RAM because the
|
||
firmware **rewrites bits inside them between replays**.
|
||
|
||
| | Keypad 1 | Keypad 2 |
|
||
|---|---|---|
|
||
| Entry | `$CC53` | `$CC7E` |
|
||
| Port (SEL) | 4 | 5 |
|
||
| `$204E` | 5 | 6 |
|
||
| RAM script | `$2050` | `$2082` (= `$2050 + $32`) |
|
||
| Raw samples | `$20B6` (32 B) | `$20D6` (32 B) |
|
||
| Key states | `$20FC` (16 B) | `$210C` (16 B) |
|
||
| Key code out | `$204A` | `$204B` |
|
||
|
||
`$2082` never appears as a literal — `$CC7E` reaches it with
|
||
`LDAB #$32 / ABX`, and likewise offsets the two buffers by `$20`/`$10`.
|
||
|
||
`$204E` = 5 and 6 are **exactly the two indices the button scan skips**
|
||
with its `INC $204E` ×3 at `$CF8B`. The two engines share one index
|
||
space and partition it cleanly.
|
||
|
||
### The script shape differs from a buttons board
|
||
|
||
```
|
||
buttons: 7 pulses data=0, 1 pulse data=1, strobe, 16 read pulses
|
||
keypad : 15 pulses data=0, 1 pulse data=1, strobe, 8 read pulses
|
||
```
|
||
|
||
The keypad shifts **16 bits, not 8** — the 1402 carries *two* chained
|
||
74HCT164s (8 bits board address + 8 bits matrix row drive, through the
|
||
74LS05 open-collector inverters), so the walking `1` has to travel
|
||
twice as far. It then reads back only 8 bits, because the single
|
||
CD74HCT165 sees 4 columns plus `BRD_DT*` and spares.
|
||
|
||
### The scan engine `$CCB2` — four rows
|
||
|
||
```
|
||
LDY $20F6 ; result ptr LDAB #$04 / STAB $2034 ; 4 ROWS
|
||
CCBC: LDX $20B4 ; reload script base each row
|
||
dispatch on row: $CD1E (4) / $CD4F (3) / $CD80 (2) / $CDB1 (1)
|
||
; ^ patch the row bit in the RAM script
|
||
CCE3: LDAB #$22 ; 34 bytes = 17 CCK pulses + strobe
|
||
LDAA $00,X / STAA $A010 / INX / DECB / BNE
|
||
CCF2: LDAB #$08 / STAB $2035 ; 8 columns
|
||
CCF7: LDAA $100A ; sample PORTE (the 2nd of only 2 such sites)
|
||
... store, INY, 2 latch writes = one CCK pulse ...
|
||
CD12: DEC $2035 / BNE $CCF7 ; 8 bits
|
||
CD17: DEC $2034 / BNE $CCBC ; 4 rows
|
||
```
|
||
|
||
The four row patchers are near-identical; `$CD1E` is
|
||
`LDAA $08,X / ORAA #$02 / STAA $08,X` (etc.) — setting and clearing
|
||
**bit 1 (`D_OUT`)** at specific script offsets, which walks the row
|
||
`1` to a different shift position for each row.
|
||
|
||
### Demux `$CDE2` and the key-code table
|
||
|
||
Per row it does `INX` ×4 — **discarding the first 4 sampled bits** —
|
||
then takes 4. That is 4 columns × 4 rows = **16 keys**, indexed by
|
||
`$20FB` running 0-15. The discarded bits are the `165`'s status half;
|
||
`$CEFA` picks two of them (`$20B6+$03` bit 4, `$20D6+$03` bit 5) into
|
||
the fault array at `$24ED+9`/`+$0A` — **keypad board-detect**.
|
||
|
||
`$CE1F`/`$CE72` load **`LDY #$DC14`**, a 16-byte scan-code → legend
|
||
table occupying the gap between the keypad scripts and the button read
|
||
block. Contents are the stock hex-pad layout:
|
||
|
||
| | c0 | c1 | c2 | c3 |
|
||
|---|---|---|---|---|
|
||
| **r0** | 1 | 2 | 3 | C |
|
||
| **r1** | 4 | 5 | 6 | D |
|
||
| **r2** | 7 | 8 | 9 | E |
|
||
| **r3** | A | 0 | B | F |
|
||
|
||
`$CE72` is the press path (state → `$01`), `$CE1F` the release path
|
||
(state → `$00`); a pressed key reads **0**, being pulled low against
|
||
the 2.7K column pull-ups. Both enqueue message type **`$8B`** — *not*
|
||
the `$88`/`$89` button press/release pair.
|
||
|
||
**So `0x50`/`0x60` are host-side logical IDs, formed as a per-pad base
|
||
plus the 0-F key code; the wire carries a distinct message type and a
|
||
nibble.** That is why the pads consume none of the 112-entry button
|
||
map internally. (**INFERRED** — the base-plus-code composition is the
|
||
reading that fits both the firmware and the observed map; `PROTOCOL.md`
|
||
in the RIOJoy repo is the authority.)
|
||
|
||
### Cadence asymmetry
|
||
|
||
The main loop at `$C0B0` is:
|
||
|
||
```
|
||
C0B0 JSR $C579 ; protocol service
|
||
C0B3 JSR $CF31 ; button scan (9 boards)
|
||
C0B6 JSR $C579
|
||
C0B9 JSR $CC7E ; keypad 2 ONLY
|
||
C0BC JSR $C579
|
||
C0BF JSR $C1CB
|
||
C0C2 JSR $C579
|
||
C0C5 JSR $C0EB
|
||
C0C8 JMP $C0B0
|
||
```
|
||
|
||
**Both pads are polled every pass, but by different routes.** Keypad 2
|
||
is scanned directly at `$C0B9`. Keypad 1 goes through `$C33B`
|
||
(`JSR $CC53 / LDAA $204A / RTS` — scan and return the key), which is
|
||
called by the **A-9-E chord detector `$C1CB`** at `$C0BF`; `$CC53` is
|
||
also reached from the `CheckRequest` self-test at `$C5C9`. So keypad 1
|
||
feeds the test-mode entry sequence while keypad 2 feeds the normal
|
||
event stream — a functional split, not a polling gap.
|
||
|
||
## I/O inventory — one pod
|
||
|
||
| Inputs | Count | Address |
|
||
|--------|------:|---------|
|
||
| Buttons (9 × 1401 × 8) | **72** | `0x00-0x47` |
|
||
| Keypad 1 (1402 hex pad) | **16** | `0x50-0x5F` |
|
||
| Keypad 2 (1402 hex pad) | **16** | `0x60-0x6F` |
|
||
| **Total digital controls** | **104** | of a 112 map |
|
||
| Analog axes (HCTL-2016, 512 cpr) | 5 | `$A020-$A03F` |
|
||
| Panel switches (RESET, ABORT→`IRQ*`) | 2 | — |
|
||
|
||
| Outputs | Count |
|
||
|---------|------:|
|
||
| Button lamps (9 × 8, UCN5821A) | **72** |
|
||
| Global brightness PWM (`BRT_CTR`) | 1 |
|
||
| Display digits (ICM7228B) | 8 |
|
||
| Status LEDs | 6 |
|
||
|
||
### Logical address map
|
||
|
||
| Range | Count | Contents |
|
||
|-------|------:|----------|
|
||
| `0x00-0x47` | 72 | buttons — **fully populated** |
|
||
| `0x48-0x4F` | 8 | **gap** — one unused board slot |
|
||
| `0x50-0x5F` | 16 | keypad 1 |
|
||
| `0x60-0x6F` | 16 | keypad 2 |
|
||
|
||
`0x48` = 72 is the `#$48` constant that sizes five separate arrays —
|
||
the button space and the gap boundary are the same number.
|
||
|
||
## RAM layout (packed solid)
|
||
|
||
| Base | Size | Contents |
|
||
|------|-----:|----------|
|
||
| `$2050` | 2 × 50 | keypad scripts, copied from `$DBB0`, row-patched in place |
|
||
| `$20B6` / `$20D6` | 32 B ea | keypad raw samples (4 rows × 8 bits) |
|
||
| `$20FC` / `$210C` | 16 B ea | keypad key states |
|
||
| `$2152` | 16 B | raw `PORTE` samples, bit-major |
|
||
| `$2162` | `$60` = 96 | input state, logical `0x00-0x5F` |
|
||
| `$21C2` | `$48` = 72 | second demux path (`$D144`, index `$2442`) |
|
||
| `$220A` | 9 × 50 = 450 | lamp write-script buffer (board × 50) |
|
||
| `$23CC` | 2 B | script pointer used by `$D331` |
|
||
| `$23CE` | `$48` = 72 | lamp state |
|
||
| `$2454` | `$48` = 72 | lamp command |
|
||
| `$249C` | `$48` = 72 | (button-indexed) |
|
||
| `$24ED` | `$14` = 20 | fault/status flags (`$CEFA`) |
|
||
|
||
Arrays abut with no padding — `$21C2` + 72 ends at `$2209`, exactly
|
||
where the lamp buffer begins.
|
||
|
||
## Cycle budget (2 MHz E-clock, 0.5 µs/cycle)
|
||
|
||
Hand-counted from M68HC11 instruction timings; all register access is
|
||
extended mode (no `LDX #$1000` base pointer exists in the image), so
|
||
operand enumeration is exhaustive.
|
||
|
||
| Stage | Cycles | Time |
|
||
|-------|-------:|-----:|
|
||
| Preamble loop (18 × 16) | 288 | 144 µs |
|
||
| Bit loop (16 × 75) | 1,200 | 600 µs |
|
||
| Prologue/epilogue/JSR | 66 | 33 µs |
|
||
| **`$CFC0` acquisition** | **1,554** | **777 µs** |
|
||
| `$D004` demux (8 inputs) | ~1,132 | 566 µs |
|
||
| `$D144` demux (8 inputs) | ~1,132 | 566 µs |
|
||
| **`$CFB6` — one pass** | **~3,829** | **1.91 ms** |
|
||
| **Full scan (9 passes)** | **~34,500** | **17.3 ms** |
|
||
|
||
**Maximum scan rate ≈ 58 Hz; worst-case button latency ≈ 17 ms.**
|
||
Acquisition is only 40% of it (7.0 ms) — **demux is 60%** (10.2 ms).
|
||
|
||
Marginal costs: **one more pass (+16 bits) = 1.91 ms**; one more input
|
||
inside an existing pass = 135 cycles = 68 µs.
|
||
|
||
Keypads, same method (**estimates** — the row patchers and handlers are
|
||
state-dependent):
|
||
|
||
| Stage | Cycles | Time |
|
||
|-------|-------:|-----:|
|
||
| `$CCB2` per row (patch + 34 shift + 8 read + loop) | ~1,275 | 637 µs |
|
||
| `$CCB2` × 4 rows | ~5,150 | **2.6 ms** |
|
||
| `$CDE2` demux (16 keys) | ~2,800 | **1.4 ms** |
|
||
| **One keypad** | ~7,950 | **~4.0 ms** |
|
||
|
||
A main-loop pass therefore costs roughly **17.3 ms (buttons) + 4.0 ms
|
||
(keypad 2) ≈ 21 ms**, plus the encoder sweep and four `$C579` protocol
|
||
services — call it ~45 Hz of button/keypad latency. Keypad 1 adds its
|
||
4.0 ms only when the host asks for it.
|
||
|
||
The scan is **fully interruptible** — every `SEI`/`CLI` in the image is
|
||
at `$C0AF` and `$C6E9-$C804` (message-queue critical sections), none in
|
||
`$CF40-$D180`. Lengthening the scan costs latency, *not* SCI ISR margin.
|
||
|
||
### Why demux dominates
|
||
|
||
Two naive constructs, both fixable in the `$DFF0` cave:
|
||
|
||
1. Bit extraction shifts a byte right *k* times via
|
||
`DECB/BEQ/LSRA/JMP` — **10 cycles per position**, up to 75 to reach
|
||
bit 7. A mask table replaces it with ~4.
|
||
2. The press/release handler is a `JSR` with full register save
|
||
(58 cycles) **even when nothing changed**.
|
||
|
||
Estimated together: demux ~1,132 → ~400 cycles/pass, full scan
|
||
**17.3 → ~9.4 ms**. **UNVERIFIED** — not implemented or benched.
|
||
|
||
## Lamp readback and the fault mask
|
||
|
||
The second 8 bits of each board's 16-bit read — the `$D144` path into
|
||
`$21C2` — are **lamp readback**, not spare status. This closes the
|
||
loop the 1401's second 74HCT165 exists for.
|
||
|
||
- `$D144` compares each bit against the expected polarity in `$24EA`;
|
||
`$D180` sets the entry's bit 0 accordingly, `$D19E` clears it.
|
||
`$21C2[n]` therefore holds **lamp n's health**: 1 = OK, 0 = fault.
|
||
- `$D262` sweeps all `$48` = 72 entries and calls `$D28C` for every
|
||
**zero**.
|
||
- `$D28C` indexes **`$DFA8`** by the same lamp number. Mask byte 0
|
||
suppresses the report; nonzero emits report type **`$03`** with the
|
||
lamp index in `$2519` via `$C75E`.
|
||
|
||
So `$DFA8` is a **per-lamp populated map** — which of the 72 lamp
|
||
positions are physically fitted and therefore worth fault-checking. It
|
||
is **72 bytes** (`$DFA8-$DFEF`), exactly matching the array, with the
|
||
erased cave starting at `$DFF0`. Suppressed entries:
|
||
|
||
| Board | Lamps | Mask |
|
||
|------:|-------|------|
|
||
| 0-2 | `$00-$17` | all monitored |
|
||
| 3 | `$18-$1F` | `$1F` suppressed |
|
||
| 4-6 | `$20-$37` | all monitored |
|
||
| 7 | `$38-$3F` | `$3C`, `$3F` suppressed |
|
||
| **8** | `$40-$47` | **all 8 suppressed** |
|
||
|
||
61 of 72 positions are monitored. The ninth board carries no
|
||
fault-checked lamps at all.
|
||
|
||
This explains the persistent "board/lamp fault item" seen in both
|
||
`CheckRequest` snapshots (RIOv4_2-ANALYSIS.md) — it is a type-`$03`
|
||
report, and **`$2519` carries the exact lamp index**, so a captured
|
||
snapshot identifies precisely which lamp is dead.
|
||
|
||
`$D0BC` — the routine `$C0EB` defers by four main-loop passes, and
|
||
skips while the test-display flag `$2421` is set — walks the two other
|
||
72-entry lamp arrays (`$249C`, `$2454`) under a rotating bit mask
|
||
`$24E8` that advances one position per call and reloads at `$80` after
|
||
8 (`$24E9`). One bit-plane per tick across all lamps: most consistent
|
||
with **per-lamp blink/intensity sequencing** (**INFERRED** — the
|
||
bit-plane walk is plain, the visual effect is not proven).
|
||
|
||
## The encoder bank — decode has room, the board does not
|
||
|
||
Settled 2026-07-26 from the board photo and schematic sheet 1.
|
||
|
||
**Addressing.** Each HCTL-2016 consumes **two** 74LS138 outputs, not
|
||
one:
|
||
|
||
| Line | Role |
|
||
|------|------|
|
||
| `A[0]` | 0 = data read, 1 = strobe (the `OE`/`RST` pair) |
|
||
| `A[2:1]` | channel within a '138 |
|
||
| `A[3]` | which '138 — U8 or U9, via the GAL's `NOT_A3` |
|
||
| `A[4]` | `SEL` pin = high/low byte of the 16-bit count |
|
||
|
||
Both '138s take `A[0:2]` + `PH_SEL*` + `E`; U8's `G2B` is `A[3]`.
|
||
Firmware confirms the split — `$C8CC-$C9A7` reads the even (data)
|
||
addresses `$A020/$A030` … `$A028/$A038`, while `$C458-$C4C0` hits the
|
||
odd (strobe) addresses `$A021`-`$A029`.
|
||
|
||
So the ceiling is **8 channels**: U8 serves OPT1-4 (Y0/Y1 … Y6/Y7),
|
||
U9 serves OPT5 on Y0/Y1 and has **Y2-Y7 free = 3 spare channel
|
||
decodes**. (An earlier estimate reached the same "3 spare" by assuming
|
||
one decode line per chip; the count is right, the reasoning was not.)
|
||
|
||
**But there are no footprints.** The 1407 carries exactly five
|
||
HCTL-2016 positions — U12-U16 = OPT1-5, headers J5/J6/J7/J3/J4 — and
|
||
the schematic draws exactly five symbols, U16 being the last. Below
|
||
OPT5 the PCB is bare: no silkscreen outline, no plated holes. For
|
||
contrast, this board's genuinely unpopulated sites (**U31**, the 22V10,
|
||
and **R43/R44**) show an obvious white outline over two rows of empty
|
||
pads. The encoder bank has nothing of the kind.
|
||
|
||
The '138 outputs are pulled up by **R16**/**R15** (`PULLUP10`, 2.7K);
|
||
R16's spare pin is net `nc2`, carrying an explicit fab instruction on
|
||
the sheet: *"**Note: Do not wire net nc2 on pcb."*
|
||
|
||
**Verdict: +3 axes is NOT firmware-only.** The decode would accept
|
||
them; the board offers nowhere to put them. It needs a daughterboard
|
||
hung off U9's spare selects, or a respin.
|
||
|
||
## Expansion capacity
|
||
|
||
| Resource | Now | Add | Gated by |
|
||
|----------|----:|-----|----------|
|
||
| Buttons | 72 | **+8** | the `0x48-0x4F` gap = one 1401 board |
|
||
| Lamps | 72 | **+8** | same board |
|
||
| Analog axes | 5 | +3 **(hardware)** | decode has 3 spare selects but **no PCB footprints** |
|
||
| Keypad keys | 32 | 0 | map ends at `0x6F` |
|
||
| Display digits | 8 | 0 | ICM7228B is 8-digit |
|
||
| Pod-bus slots (hardware) | 64 | +53 | **no logical IDs to reach them** |
|
||
|
||
**The pod is built out.** 104 of 112 logical inputs are in use. Past
|
||
+8 buttons, expansion is *protocol*-bound, not hardware-bound: the bus
|
||
can address 64 slots and the CPU could service ~17 at 30 Hz, but the
|
||
map has no IDs left.
|
||
|
||
**The single tenth buttons board is the only expansion this hardware
|
||
takes without modification.** The analog axes looked like the free win
|
||
but are not: the decode has three spare channel selects on U9, and the
|
||
board has nowhere to solder them (see the encoder-bank section above).
|
||
Everything else — more buttons, more keys, more axes — needs either a
|
||
protocol revision or new hardware.
|
||
|
||
### Adding the tenth board
|
||
|
||
No free port — all 8 are populated, so it chains onto an existing
|
||
port's cable at a spare `ADR. SEL.` position (`A-3`+ are unused
|
||
everywhere; `DD_OUT` is open-collector precisely to allow sharing).
|
||
**INFERRED**: port 7 at `A-1` is the natural spot, since `0x48-0x4F`
|
||
follows port 7's `0x40-0x47` in scan order. The harness decides.
|
||
|
||
Cost: one scan pass (17.3 → 19.1 ms, 58 → 52 Hz), ~100 bytes of script
|
||
in the cave, a tenth `JSR $CFB6`, and five `#$48` → `#$50` constants
|
||
(`$C399`, `$C3A2`, `$C3B4`, `$C3BD`, `$D26D`). The catch is RAM: the
|
||
arrays abut, so growing them in place overruns the lamp buffer at
|
||
`$220A` — it needs a re-layout, though SRAM is 32K with only
|
||
~`$2000-$2500` in use.
|
||
|
||
## Corrections to earlier claims
|
||
|
||
Recorded so they don't get re-derived wrongly:
|
||
|
||
- **`PORTE` is the pod-bus return path, not spare ADC.** All 8 pins
|
||
carry `DI[0..7]`; the A/D is never powered up (`OPTION`/`ADCTL`/
|
||
`ADR1-4` untouched) because the pins are already busy. This is also
|
||
*why there are exactly 8 pod ports* — `PORTE` is 8 bits wide.
|
||
- **There is no 8× demux win.** An acquisition stores whole `PORTE`
|
||
bytes, which looks like all 8 ports captured in parallel — but `SEL`
|
||
varies per script and tracks `$204E`, so the 74LS138 gates the
|
||
strobes per port and only the selected port returns valid data.
|
||
- **Ports 4 and 5 are not empty** — they are the two keypads.
|
||
- **The keypads are not daisy-chained** into button shift chains; they
|
||
have their own ports and their own scripts at `$DBB0`/`$DBE2`.
|
||
|
||
- **`$CC53` is the keypad-1 scanner, not an encoder sweep.**
|
||
RIOv4_2-ANALYSIS.md's `CheckRequest` write-up cited `$CC53` for
|
||
"sweeps all five encoder channels". The behavioural claim is right
|
||
(the sweep does end on channel 4) but the address is wrong: the five
|
||
channels are read at **`$C8CC`-`$C9A7`** (pairs `$A028/$A038`,
|
||
`$A020/$A030`, `$A022/$A032`, `$A024/$A034`, `$A026/$A036`), driven
|
||
from `$C0CB`. `$C5C9`/`$C5CC` in the self-test are the two keypad
|
||
scans.
|
||
- **The disassembly under-reports hardware access.** Sync gaps hid a
|
||
second `PORTE` read (`$CCF7`) and two `$A010` writes
|
||
(`$CCEA`, `$CD0B`). Search the raw binary for `B6 10 0A` /
|
||
`B7 A0 10` rather than grepping the listing.
|
||
|
||
## The main loop, fully accounted for
|
||
|
||
```
|
||
C0B0 JSR $C579 ; protocol service (runs between every stage)
|
||
C0B3 JSR $CF31 ; button scan — 9 boards, 17.3 ms
|
||
C0B9 JSR $CC7E ; keypad 2 scan, 4.0 ms
|
||
C0BF JSR $C1CB ; keypad 1 via $C33B + A-9-E chord detector
|
||
C0C5 JSR $C0EB ; deferred lamp bit-plane tick (every 4th pass)
|
||
C0C8 JMP $C0B0
|
||
```
|
||
|
||
`$C1CB` is the **test-mode chord detector**. It calls `$C33B` (scan
|
||
keypad 1, return its key) every pass and matches the sequence
|
||
**A → 9 → E** in a 3-byte buffer at `$20F8`; on the third key it sets
|
||
the test-display flag `$2421`, sends `TestModeChange` (`$C70E`), and
|
||
runs `$CBA7`/`$C241`. So keypad 1 *is* polled continuously — the
|
||
earlier "on demand only" reading was wrong; the demand is this
|
||
detector. That is the keypad-A-9-E test mode described in
|
||
[`../rio-hardware/display-board-1408.md`](../rio-hardware/display-board-1408.md).
|
||
|
||
The encoder sweep is not in this loop — it runs from `$C0CB`
|
||
(`$C8CC`-`$C9A7`).
|
||
|
||
## Open questions
|
||
|
||
Nothing outstanding on the I/O path. Remaining loose ends are cosmetic:
|
||
the precise visual effect of `$D0BC`'s bit-plane walk, and the three
|
||
individually-suppressed lamp positions (`$1F`, `$3C`, `$3F`) — which
|
||
may record real cockpit wiring rather than anything in firmware.
|
||
|
||
### Reading the schematics without a PDF renderer
|
||
|
||
`Scans_018-014.pdf` holds seven **CCITT G4** scans (6800×4400, one per
|
||
sheet) and no text layer. With no `pdftoppm`/Ghostscript available, the
|
||
working route is to pull each image stream out of the PDF and rewrap it
|
||
in a minimal TIFF header (`Compression=4`, `PhotometricInterpretation=0`),
|
||
which Pillow decodes directly. Sheet order matches the table in
|
||
[`../rio-hardware/README.md`](../rio-hardware/README.md).
|
||
|
||
## Cross-references
|
||
|
||
- [`RIOv4_2-ANALYSIS.md`](RIOv4_2-ANALYSIS.md) — wedge fix, baud
|
||
ladder, certification history
|
||
- [`../rio-hardware/README.md`](../rio-hardware/README.md) — boards,
|
||
schematics, pod-bus signal set
|
||
- [`../rio-hardware/gal/README.md`](../rio-hardware/gal/README.md) —
|
||
U7 decode and the memory map
|