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

14 KiB
Raw Blame History

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