Files
vt1200/reference/level0-10byte-puzzle.md

275 lines
14 KiB
Markdown
Raw Permalink 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.
# Level 0's "10-byte" puzzle — where are the 10 bytes supposed to come from?
Static analysis of the level-0 **I/O data path test**, routine `L3FDCE470`
(called from `3F3DFA80`), window `0x06200000` = DUART2, channel A = keyboard.
Read-only task. ROM under test: `vt1200_v22.bin`,
`md5 = b46a3e442ccb86134d2356bd70d22fd2`.
---
## 0. Address conventions used in this note
* **Bit address** — the firmware's own address space, as printed by
`harness/mame/hostport_dis34.txt` (e.g. `3FDCE470`). `file_offset = (bitaddr - 0x3F200000) / 8`.
* **Ghidra address** = `0x07E40000 + file_offset` (e.g. `3FDCE470` → `0x07FB9C8E` =
`FUN_rom_07fb9c8e`).
* **Driver handler offset** = `(bitaddr - window_base) >> 4`; byte offset in the window
= `off * 2`; DUART register index = `off` (`vt1200.cpp` `duart2_r/duart2_w`).
So the firmware operand `*a11(0x10)` = handler off `1` = byte `0x02` = **SRA**;
`*a11(0x20)` = off `2` = **CRA**; `*a11(0x30)` = off `3` = **RHRA/THRA**;
`*a11(0xa0)` = off `0x0A` = **CRB**.
* Values below show the store *instruction* bit address; MAME's access log for the
same access shows `pc = instruction + 0x20` (the PC at the cycle of the access).
Both are quoted where it helps.
---
## TL;DR
The check is **count-only**, the channel is **not** put in loopback, and the firmware
transmits **one** byte (`0xE1`). No documented LK401/LK201 keyboard message and no
documented mouse/tablet message is 10 bytes long. There is **no expected-data table**
anywhere in the routine. So the routine cannot be satisfied by the firmware's own
loopback, and cannot be satisfied by any documented device message either — the only
way it has ever "passed" is the board read-back latch sticking `RXRDY`.
**Verdict: "only the count is checked and the source is unconstrained"** — with the
qualifier that the bytes must arrive *on DUART2 channel A* (nothing else in the ROM
feeds this routine), but their *content/length semantics* match no documented device
behaviour. Details and implications in §7.
---
## 1. What exactly does `L3FDCE470` write to DUART2, and how much?
Two things, and exactly **one data byte**.
**(a) The channel initialiser** `L3FA911C0`, called once from `L3FDCE470` at
`3FDCE5D0`, does the whole 68681 setup. It is a generic "reset + program both
channels" routine (the level-1 host-port test uses the same one). Every store, in
order (`L3FA911C0` body `3FA911C0`–`3FA92090`):
| reg (handler off) | register | value | store PC | MAME log pc |
|---|---|---|---|---|
| 2 | CRA | `0xCA` | `3FA91240` | `3FA91260` |
| 2 | CRA | `0x2A` | `3FA912E0` | `3FA91300` |
| 2 | CRA | `0x3A` | `3FA91390` | `3FA913B0` |
| 2 | CRA | `0x4A` | `3FA91440` | `3FA91460` |
| 2 | CRA | `0x5A` | `3FA914E0` | `3FA91500` |
| 2 | CRA | `0x1A` | `3FA91580` | `3FA915A0` |
| 0x0A | CRB | `0xCA` | `3FA91630` | `3FA91650` |
| 0x0A | CRB | `0x2A` | `3FA916D0` | `3FA916F0` |
| 0x0A | CRB | `0x3A` | `3FA91780` | `3FA917A0` |
| 0x0A | CRB | `0x4A` | `3FA91830` | `3FA91850` |
| 0x0A | CRB | `0x5A` | `3FA918E0` | `3FA91900` |
| 0x0A | CRB | `0x1A` | `3FA91980` | `3FA919A0` |
| 0 | MR1A | `0x13` | `3FA91A20` | `3FA91A30` |
| 0 | **MR2A** | **`0x07`** | **`3FA91AB0`** | **`3FA91AC0`** |
| 1 | CSRA | `0x99` | `3FA91B50` | `3FA91B70` |
| 2 | CRA | `0x05` (Rx+Tx enable) | `3FA91C50` | `3FA91C70` |
| 8 | MR1B | `0x07` | `3FA91CF0` | `3FA91D10` |
| 8 | MR2B | `0x07` | `3FA91D90` | `3FA91DB0` |
| 9 | CSRB | `0x99` | `3FA91E40` | `3FA91E60` |
| 0x0A | CRB | `0x05` | `3FA91F20` | `3FA91F40` |
| 5 | IMR | `0x00` (all interrupts masked) | `3FA91FB0` | `3FA91FE0` |
The CR nibbles decode as a standard 68681 reset sequence: `C`=reset error status,
`2`=reset Rx, `3`=reset Tx, `4`=reset error status, `5`=reset break-change, `1`=reset
MR pointer. The trailing `CRA = (arg2<<2)|arg1`, `CRB = (arg2<<2)|arg1` are `0x05`
here (args `1,1`) = enable Rx + enable Tx. All 21 stores are confirmed live in
`harness/mame/board-access.log` lines 26–46.
**(b) The only data write.** `L3FDCE470` writes **exactly one byte**: `0xE1` to THRA
(reg 3), at `3FDCE8B0` (MAME log pc `3FDCE8D0`):
```
3FDCE870: a11 = *(a13-0x80) ; = arg1 = 0x06200000 (DUART2 base)
3FDCE890: a9 = 0xe1
3FDCE8B0: move a9, *a11(0x30), 0 ; THRA <- 0xE1
```
A scan of the whole routine (bit `3FDCE470`–`3FDD59B8`) finds **one** store to
`*a11(0x30)` (`3FDCE8B0`) and **eleven** loads from it (all `RHRA`). There are no writes to
reg 0/1/2 (mode/clock/command) inside `L3FDCE470` itself. So:
* data bytes transmitted by this test = **1** (`0xE1`);
* no burst, no pattern, no repeat.
For context, the firmware *had* already sent the keyboard `0x11 0x8C`
("LEDs off") through a different routine, `L3FDF3860`, at `3FDF3AA0`/`3FDF3BB0`
(MAME log `3FDF3AD0`/`3FDF3BE0`), just before this test — total keys the LK401 sees
before the test is 3 bytes (`11 8C E1`), none of which has a documented reply.
## 2. Does it put the channel in local loopback (MR2 bits 7:6 = `10`)?
**No. Definitively no.**
* Statically: `L3FA911C0` writes `MR2A = 0x07` (`movk 7, a9; move a9, *a11, 0` at
`3FA91A90`/`3FA91AB0`). `0x07 = 0b0000_0111` → bits 7:6 = **00 = normal mode**.
`MR2B = 0x07` likewise. `L3FDCE470` never writes reg 0 or reg 1 again, so nothing
re-enables loopback later.
* Empirically: `harness/mame/board-access.log:39` — `W 06200000 pc=3FA91AC0 val=0007`
(MAME `duart2_w(0)` → register 0 = MR2A).
So the "cleanest outcome" the brief hoped for — *the 10 bytes are the firmware's own
transmission coming back, and our DUART has a loopback bug* — is **ruled out**:
1. no loopback is programmed, and
2. even if it were, only **one** byte is transmitted; loopback of `0xE1` can produce at
most **1** received byte, never 10.
The external TX→RX loopback our harness can wire (`VT1200_DUART_LB2`, `a_tx_cb`→
`rx_a_w`) was tried in `OS_BRINGUP_REPORT.md` §8.2 and does not change the outcome,
for the same reason (`transmit count is 3`, still < 10).
## 3. Does it check the byte *values*, or only the *count*?
**Only the count.** The received byte is stored each iteration to the local
`*(a13+0x80)` (`3FDCE730`, `3FDCE9A0`) and is **never read back or compared**. The
pass branch at `3FDCEAE0` is:
```
3FDCEAE0: a11 = *a13 ; *a13 = byte counter
3FDCEAF0: a11 -= 10
3FDCEB00: jrne L3FDCED50 ; count != 10 -> fail
... pass: set flag *(a13+0x60)=1, rewrite *(arg2) ... jauc L3FDD5960 (return a8=1)
```
The **only** value comparisons in the whole routine are `cmpi 0xd8` (`3FDCEF40`) and
`cmpi 0xeb` (`3FDCF100`), and both live in the *failure* path that is reached only when
the retry selector (arg0) is 1 — not for this call, which passes `arg0 = 2`. So for the
level-0 call there is not a single content check: **any** stream of 10 bytes passes.
This is exactly the hole the latch fell through: with `SRA` stuck at `0x0099` (bit 0 =
`RXRDY` set) and `RHRA` stuck at the latched `0x008C`, the loop read 10 phantom bytes
and hit `count == 10`.
## 4. What are the 10 bytes meant to be? Is there an expected table?
**There is no pattern table and no expected data — on either side.**
* The routine contains **no ROM/RAM address constants at all**. Every `movi` in
`L3FDCE470` is either a byte to transmit (`0xE1`), a delay (`0x64`), or a status
mask (`0xffff`, `0x1ffff`, `0x20000000`, `0x40100`, `0x1000000`, `0x4000000`,
`0x60000000`, `0x80000000`). Nothing is dereferenced as a table.
* Contrast with the sister test that *does* have a table: the level-1 host-port test
builds its expected stream from the RAM tables at bit addresses `0x270220` /
`0x270470` (`0xAA,0xFF,0xAA,0x55,…` masked by `0x1F,…`; refs at `3FA6E730`,
`3FA72360`, `3FA736F0`, … and `DUART_REPORT.md` §4). `L3FDCE470` has no analogue.
So the "10" carries no value semantics: it is purely the loop/pass bound, hard-coded
as `subk 10` (`3FDCE6A0`, `3FDCEAC0`, `3FDCEAF0`).
## 5. Cross-check against the keyboard/mouse protocol (`PROTOCOL.md`)
The byte it sends is `0xE1`. Per `reference/keyboard-mouse/PROTOCOL.md`:
* `0xE1` = "**disable auto-repeat across keyboard**" (VCB02 Table B-7). It is a
*parameter* command (bit 7 set, bit 0 = peripheral type). **No reply is documented.**
* The keyboard's power-up / `FD` self-test report is **4 bytes** (`01 00 00 00`;
`PROTOCOL.md` §3.5, LK401 spec §3.3.4.6, VCB02 §B.5.4.3).
* The mouse `T` self-test report is **4 bytes**; the tablet absolute report is **5
bytes** (`PROTOCOL.md` §4.4/§4.5).
Nothing the platform documents produces 10 bytes. Say it plainly: **10 ≠ 4**, so this
is not "the keyboard's reply" in any documented sense, and it is not the power-up
sequence either. It is not a keystroke burst: a keystroke is one keycode byte, and a
down+up pair is two — an operator cannot produce *exactly* 10.
Note also that the test *flushes* the receiver first (loop `L3FDCE6C0`, up to 10
bytes, `3FDCE650`–`3FDCE6B0`) **before** sending `0xE1`. So even the real keyboard's
4-byte power-up message, if it were pending, would be consumed by the flush, not
counted. Whatever the 10 bytes are, they are expected to arrive *after* `0xE1`.
## 6. What happens on failure, and the first divergent read
* **Failure branch.** `3FDCEAF0`/`3FDCEB00` sends `count != 10` to `L3FDCED50`. With
this call's `arg0 = 2`, the retry selector takes: `dsj a11` (2→1, non-zero) →
`L3FDCF160` → `subk 2` → 0 → wait for `RXRDY` (bounded delay loop `L3FDCF440`) →
`sub a8,a8; jauc L3FDD5980` → **returns `a8 = 0`**. (Pass returns `a8 = 1` via
`L3FDD5960`.) The retry/sync-on-`0xD8…0xEB` path (`L3FDCED90`) is only reached when
`arg0 = 1`, which this call does not use.
* **First divergent read = `R 06200001 pc=3FDCE670`. Confirmed.** The load is
`move *a11(0x10), a11, 0` at `3FDCE650` (SRA, register 1); MAME records the PC after
the load (`+0x20`), hence `pc=3FDCE670`, which is the `btst 0, a11` that consumes it.
* board read-back latch (old baseline): `val = 0x0099` — the value last written to
CSRA, bit 0 = `RXRDY` **stuck set**;
* real SCN2681 (`harness/mame/board-access.log:47` / `OS_BRINGUP_REPORT.md` §8.2):
`val = 0x000C` = `TxRDY|TxEMT`, bit 0 clear → nothing to drain → `count = 0` →
fail.
* **The old accidental pass.** `SRA = 0x99` (bit 0 set) + `RHRA` latched at `0x8C`
made the drain loop count ten identical phantom bytes and reach `count == 10`. That
is the "write-back latch used to pass this test by accident".
* **Reaching the diagnostics menu.** The level-0 I/O-path error has its own firmware
string: `LEVEL0 ERROR -> IO path error - 31` at bit `0x3FDC9068` (`reference/rom-strings.txt`),
printed by the level-0 error reporter at `3FDE3340`. Empirically (measured, not
inferred) the no-responder faithful boot ends in the interactive diagnostics menu —
`VERSION 2.08 - SELECTED TEST TYPE:` + the A–Z list — in `DUART_REPORT.md` §3 and
`OS_BRINGUP_REPORT.md` §3/§8.2.
---
## 7. Verdict and implications
> **Verdict: only the count is checked, and the content of the 10 bytes is
> unconstrained** — but the bytes must arrive on **DUART2 channel A** (the keyboard
> port). The firmware's own loopback is **not** enabled, and it transmits only **one**
> byte, so the bytes cannot be the firmware's own transmission; and no documented
> keyboard/mouse/tablet message is 10 bytes, so they are not an identifiable device
> message either.
Precisely:
1. **Not loopback.** `MR2A = 0x07`; `MR2B = 0x07`. No model bug in our DUART's
loopback path is implicated (and 1 byte ≠ 10 regardless).
2. **Not a documented device message.** `0xE1` has no reply; the power-up/`FD` reply
is 4 bytes; the mouse reply is 4 bytes; the tablet report is 5.
3. **Count-only.** Any 10 bytes on DUART2-A pass. The routine stores the received byte
and never compares it; there is no expected-data table.
**What that means for satisfying the test honestly.** To satisfy `count == 10` a
faithful model would have to deliver 10 bytes on DUART2-A that no document says exist.
So this test is *not* satisfiable by faithfully modelling the documented LK401: the
keyboard powers up with 4 bytes, and the flush loop would consume those before `0xE1`.
The only thing that ever made it pass was the read-back artefact (stuck `RXRDY`), which
this project deliberately removed. We should therefore **not** add a responder that
feeds 10 bytes; we should record the test as an *unsatisfied, unexplained firmware
requirement* (a firmware expectation that matches no documented device and no enabled
loopback).
**One observable shape worth flagging (hypothesis, not conclusion).** `10` is used
three times in the routine — twice as the *flush/read buffer bound*
(`L3FDCE6A0`, `L3FDCEAC0`) and once as the *pass threshold* (`L3FDCEAF0`). A
transaction helper that sends one byte and then demands the receive buffer be filled
to its maximum is unusual; a bound written for "flush/drain up to N" being reused as
"pass iff count == N" would explain the whole anomaly without any device at all. This
is consistent with all the evidence here but is not proven (the pass branch also
writes a status word via `arg2`, so the author clearly intended a real receive); treat
it as a lead for the firmware-side question, not as fact.
### What is still missing / next steps
* A **live-hardware capture** of DUART2-A at power-up (what the real VT1200 actually
receives in the first ~2 s) is the one datum that would settle whether the firmware
sees 10 bytes on a real machine. Everything here is ROM + emulator evidence; no
VT1200 hardware trace of `0x0620000` exists in the repo.
* If the LK401 firmware ROM (`bitsavers-23-004M2-00_LK201AA_Firmware.zip`) is ever
disassembled, confirm its power-up transmit count is 4 and that `0xE1` produces no
reply — that would close the "unknown" row in `PROTOCOL.md` §7.
---
## Evidence index
| Claim | Evidence |
|---|---|
| one data byte, `0xE1` → THRA | `hostport_dis34.txt` `3FDCE8B0`; `board-access.log:48` |
| MR2A = `0x07`, no loopback | `hostport_dis34.txt` `3FA91A90`–`3FA91AB0`; `board-access.log:39` |
| full channel init (21 writes) | `hostport_dis34.txt` `3FA911C0`–`3FA92090`; `board-access.log:26–46` |
| pass iff count == 10 (count only) | `hostport_dis34.txt` `3FDCEAE0`–`3FDCEB00`; received byte stored, never compared (`3FDCE730`, `3FDCE9A0`) |
| no expected-data table | all `movi` in `L3FDCE470` are `0xE1`/`0x64`/masks — no table address |
| `0xE1` has no reply; power-up = 4 bytes | `reference/keyboard-mouse/PROTOCOL.md` §3.3, §3.5 |
| failure returns 0; first divergence | `hostport_dis34.txt` `3FDCED50`–`3FDD5980`; `board-access.log:47`; `OS_BRINGUP_REPORT.md` §8.2 |
| lands in diagnostics menu | `DUART_REPORT.md` §3; `OS_BRINGUP_REPORT.md` §3 |