Files
TeslaRel410/restoration/rio-firmware/IO-ARCHITECTURE.md
T
CydandClaude Opus 5 b615f319db RIO: decode the pod-bus I/O architecture end to end
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>
2026-07-26 22:05:29 -05:00

539 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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