From a1e3ebfaff8520b9eff3716b8302adb1589a8f06 Mon Sep 17 00:00:00 2001 From: SDD Date: Fri, 16 Oct 2026 14:16:37 +0300 Subject: [PATCH] reference: the level-0 10-byte puzzle -- count-only, no loopback, no 10-byte device message --- reference/level0-10byte-puzzle.md | 274 ++++++++++++++++++++++++++++++ 1 file changed, 274 insertions(+) create mode 100644 reference/level0-10byte-puzzle.md diff --git a/reference/level0-10byte-puzzle.md b/reference/level0-10byte-puzzle.md new file mode 100644 index 0000000..c8ccc4a --- /dev/null +++ b/reference/level0-10byte-puzzle.md @@ -0,0 +1,274 @@ +# 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 |