14 KiB
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.cppduart2_r/duart2_w). So the firmware operand*a11(0x10)= handler off1= byte0x02= SRA;*a11(0x20)= off2= CRA;*a11(0x30)= off3= RHRA/THRA;*a11(0xa0)= off0x0A= 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:
L3FA911C0writesMR2A = 0x07(movk 7, a9; move a9, *a11, 0at3FA91A90/3FA91AB0).0x07 = 0b0000_0111→ bits 7:6 = 00 = normal mode.MR2B = 0x07likewise.L3FDCE470never 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(MAMEduart2_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:
- no loopback is programmed, and
- even if it were, only one byte is transmitted; loopback of
0xE1can 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
moviinL3FDCE470is 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 by0x1F,…; refs at3FA6E730,3FA72360,3FA736F0, … andDUART_REPORT.md§4).L3FDCE470has 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 /
FDself-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
Tself-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/3FDCEB00sendscount != 10toL3FDCED50. With this call'sarg0 = 2, the retry selector takes:dsj a11(2→1, non-zero) →L3FDCF160→subk 2→ 0 → wait forRXRDY(bounded delay loopL3FDCF440) →sub a8,a8; jauc L3FDD5980→ returnsa8 = 0. (Pass returnsa8 = 1viaL3FDD5960.) The retry/sync-on-0xD8…0xEBpath (L3FDCED90) is only reached whenarg0 = 1, which this call does not use. - First divergent read =
R 06200001 pc=3FDCE670. Confirmed. The load ismove *a11(0x10), a11, 0at3FDCE650(SRA, register 1); MAME records the PC after the load (+0x20), hencepc=3FDCE670, which is thebtst 0, a11that consumes it.- board read-back latch (old baseline):
val = 0x0099— the value last written to CSRA, bit 0 =RXRDYstuck 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.
- board read-back latch (old baseline):
- The old accidental pass.
SRA = 0x99(bit 0 set) +RHRAlatched at0x8Cmade the drain loop count ten identical phantom bytes and reachcount == 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 - 31at bit0x3FDC9068(reference/rom-strings.txt), printed by the level-0 error reporter at3FDE3340. 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 — inDUART_REPORT.md§3 andOS_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:
- Not loopback.
MR2A = 0x07;MR2B = 0x07. No model bug in our DUART's loopback path is implicated (and 1 byte ≠ 10 regardless). - Not a documented device message.
0xE1has no reply; the power-up/FDreply is 4 bytes; the mouse reply is 4 bytes; the tablet report is 5. - 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
0x0620000exists 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 that0xE1produces no reply — that would close the "unknown" row inPROTOCOL.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 |