reference: the level-0 10-byte puzzle -- count-only, no loopback, no 10-byte device message

This commit is contained in:
SDD committed 2026-10-16 14:16:37 +03:00
1 parent d017953d52
commit a1e3ebfaff
1 file changed
+274
+274
View File
@@ -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 |