Files
vt1200/harness/mame/DUART_REPORT.md
T
arti 41454d5d4c harness: the stub level-1 run continues past both DUART failures; correct the "fatal/LED loop" claim
The stub run does not park at HOST PORT - 15. Both DUART failures are reported,
the board LED blinks once (finite L3F208660), TYPE 'GO' TO CONTINUE is printed
(no input is read), and the level-1 runner continues ('CONTINUING THE TESTS').
The runner never returns to 3F204440, so 3F208060/3F208650 are never executed.

The run then wedges at frame ~2839 in a DUART1 counter-ready interrupt storm:
the ISR polls DUART1 reg 5 (bit 0x06400050) and always reads 0x08; the counter
runs in timer mode (ACR=0x60) with CTUR/CTLR never programmed, so MAME re-arms
at 1 tick and the interrupt re-asserts every ~229 CPU cycles. Frames at 3500 /
6000 / 12000 are byte-identical; RAM differs by one byte.

Corrects README.md and annotates GATE_CLEANUP_REPORT.md, BRINGUP_LOG.md and
DUART_REPORT.md. Adds a console+PC/milestone instrument and the finding report.
2026-10-16 14:56:47 +03:00

57 KiB
Raw Blame History

DUART — wiring MAME's SCN2681 at the host-port window

CORRECTION (2026-10-16, later). The line 356 note — "an earlier note in this project records that INTENB never enables, because the level-0 restart enters at 3F208060 with DINT" — is wrong on both halves: 3F208060 is never entered on the stub level-1 path (measured), and the run does enable interrupts. The level-1 DUART failures are reported and the runner continues, then dies in a DUART1 counter-ready interrupt storm (ISR reg 5 = 0x08) from frame ~2839; see harness/mame/LEVEL1_CONTINUE_REPORT.md.

Bounded task, 2026-10-16. Goal: make the firmware's level-1 HOST PORT test pass (DUART1 channel A) by modelling the DUART with MAME's SCN2681, so that 'LEVEL1/2 ERROR -> HOST PORT - 15 ' stops appearing in the console capture.

Driver: harness/mame/vt1200.cpp. MAME: /home/arti/code/k-space/mame0289. ROM: harness/mame/roms/vt1200/vt1200_v22.bin. Acceptance instrument: harness/mame/cap.lua.


0. Answer in one paragraph

MAME's SCN2681 is the right part and is wireable: the two windows are byte-register DUARTs with duart_register = handler_off (verified below), and they are now mapped to an SCN2681 with TX wired back to RX. The acceptance line still appears — but not because of the DUART model. The level-1 host-port test reads DUART1 status/data (SRA at off 0x01, RHRA at off 0x03) and requires the receiver to hold a 5-byte external pattern (0xAA,0xFF,0xAA,0x55,…) with SRA overrun set ((SRA & 0xF0) == 0x10). The firmware never writes the DUART transmit register anywhere on that path (no write to off 0x03, driver-side logged), so no loopback — internal (MR2 loopback) or external (TX→RX) — can produce that data. The same is true of the level-0 "I/O data path test", which writes 0xE1 to DUART2's THRA and requires a 10-byte reply. These are host-protocol tests, not passive loopback tests: they need an external responder (the host, or a loopback connector that is more than a passive TX→RX strap). That is the specific blocker.


1. What I wired, and where

Device: MAME SCN2681 (Signetics 2681; same register model as the Motorola MC68681, which is the "same family"). Reasons for SCN2681 over MC68681:

  • The firmware uses only the RD/WR-strobe register interface; it never touches the 68681-specific Interrupt Vector Register behaviour or IACK. The 2681 is the strobe part, so it is the closer of the two; the 68681 would work identically here.
  • Both share duart_base_device; the 68681 adds only IVR/DTACK/IACK. Nothing is lost.

Windows and role (chosen from ROM strings and from which test the level-1 runner drives — see §3):

window (map units) device channels evidence
0x0640000 SCN2681 duart1 A = host, B = printer the level-1 host-port test writes this window's CRA/MR/CSRA (06400002, 06400000, 06400001)
0x0620000 SCN2681 duart2 A = keyboard, B = mouse ROM strings E - DUART1, CHANNEL A (HOST), H - DUART2, CHANNEL A (KEYBOARD), I - DUART2, CHANNEL B (MOUSE); the level-0 I/O test drives this window

Both are gated on VT1200_DUART so the no-env baseline is byte-identical. The entries cover only the DUART's 16 registers (32 bytes = 0x100 map units) and are registered after the board-window model, so the rest of a window still falls through to the model (a window can mix a DUART with other board registers, as the brief warned).

The monitor-selection switch is modelled the way the hardware does it: it is IP4-IP6 of the DUART at 0x0640000D, driven high (0x70 = "VR150") with ip4_w/ip5_w/ip6_w. Confirmed in the log: R 0640000D … val=00F0, and the firmware's andi 0x70 yields 0x70 (a legal monitor) rather than "Undefined monitor switch setting".

Loopback wiring: a_tx_cb → rx_a_w and b_tx_cb → rx_b_w, selectable per DUART (VT1200_DUART_LB1 for the host/printer DUART, VT1200_DUART_LB2 for keyboard/mouse) to model the service guide's loopback connectors. When MR2 bit 7:6 = 10 the chip does its own internal loopback and does not call the TX callback, so this wiring is harmless there and supplies external loopback for the normal-mode tests.

New env knobs (all off by default; documented in the driver header):

VT1200_DUART=1            map the SCN2681s (both windows unless SEL says otherwise)
VT1200_DUART_SEL=1|2|12   which DUART ("1" = 0x0640000, "2" = 0x0620000; default 12)
VT1200_DUART_LB1=1        external TX→RX on DUART1 A/B (host/printer connector)
VT1200_DUART_LB2=1        external TX→RX on DUART2 A/B (keyboard/mouse connector)

2. The remap, and the evidence for it

I did not assume it; I verified it, and it comes out as duart_byte = handler_off * 2, i.e. duart_register = handler_off.

Facts:

  • Map units are bits (tms34010.h: program space data_width = 1), and a handler on an entry at base receives (bitaddr − base) >> 4. So byte offset within the window = handler_off * 2.
  • The datasheet register map has register N at byte N (for the registers in use: 0x00 MR1A/MR2A, 0x01 SRA/CSRA, 0x02 CRA, 0x03 RHRA/THRA, 0x05 ISR/IMR, 0x08 MR1B/MR2B, 0x09 SRB/CSRB, 0x0A CRB, 0x0D IP/OPCR, 0x0F Stop/Reset OP).
  • Therefore register index = dup_byte / 2 = handler_off.

The observed traffic fits only under that rule:

  • The firmware's generic DUART driver (L3FA90960, bit 0x3FA90960) writes the command sequence 0xCA,0x2A,0x3A,0x4A,0x5A,0x1A to operand +0x20 (→ off 0x02) and the same sequence to operand +0xA0 (→ off 0x0A). 0x02 = CRA, 0x0A = CRB; the values are coherent 68681 command nibbles (0xC enable Rx, 0x2 reset Rx, 0x3 reset Tx, 0x4 reset errors, 0x5 reset break-change, 0x1 reset MR pointer). No other off assignment makes those coherent.
  • It then writes MR1A = 0x13 and MR2A = 0x07 to off 0x00 (MR pointer toggles), CSRA = 0x99 to off 0x01, MR1B/MR2B = 0x07 to off 0x08, CSRB = 0x99 to off 0x09, and the IMR value to off 0x05. All match the MAME map.
  • The host-port test writes MR1A = 0x12, MR2A = 0x80 to off 0x00 (i.e. local loopback: MR2 bit 7:6 = 10), CSRA = 0x88 to off 0x01, and CRA commands (0x05 = enable Tx+Rx, 0x0A = disable) to off 0x02 — then reads off 0x01 (SRA) and off 0x03 (RHRA). The monitor switch at 0x0640000D reads as IP (off 0x0D), bits 4-6, exactly as the service guide describes.
  • The register value travels in the low byte of the firmware's 16-bit move (the board log shows val=0088 for CSRA), which is why the driver passes data & 0xff.

Cross-check against the brief's own numbers: byte = off*2 maps the brief's hot 06400002 → byte 0x04 and 0640000A → byte 0x14, which the brief labels CRA and CRB. 0x04 = register 0x02 = CRA, 0x14 = register 0x0A = CRB. Consistent.

(VT240, DEC's own terminal, wires the same part as m_duart->read(offset >> 1) on a 16-bit bus with .umask16(0x00ff) at 0172000 — the same "register = byte/2, value in the low byte" convention. Independent corroboration.)


3. Acceptance test — before / after

Baseline (board model only), 900 frames (cap.lua), confirmed this session:

'LEVEL1/2 TESTING ** '
'LEVEL1/2 ERROR -> HOST PORT - 15 '

With the DUART wired at the host window — the config that actually reaches the host-port test (DUART2 left to the board model so level 0 still passes):

VT1200_VRAM=1 VT1200_BOARD_MODEL=handshake VT1200_BOARD_BUSY_TICKS=2 \
VT1200_BOARD_SWITCH="0640000D:70:70" \
VT1200_DUART=1 VT1200_DUART_SEL=1 \
  $MAME vt1200 -rompath harness/mame/roms -video none -sound none -nothrottle \
    -debug -debugger none -debuglog -autoboot_script harness/mame/cap.lua -autoboot_delay 0
'LEVEL1/2 TESTING ** '
'LEVEL1/2 ERROR -> HOST PORT - 15 '      <-- STILL PRESENT

So the acceptance criterion is not met, and I can say exactly why (§4).

For completeness, mapping both DUARTs (VT1200_DUART_SEL unset) changes the transcript to the interactive diagnostics menu (VERSION 2.08 - SELECTED TEST TYPE: + the A–Z test list). As the owner noted, that menu is what the firmware falls into on a failure, and I confirm the specific failure: the level-0 I/O data-path test (L3FDCE470) writes 0xE1 to DUART2's THRA and then requires 10 received bytes; a real DUART with no responder has none, so it diverts to the menu. That is the same underlying requirement as §4, one level earlier.


4. Why HOST PORT still fails — the specific, measured condition

The host-port test is reached and executed against the real DUART. Driver-side (VT1200_BOARD_LOG) shows the test's whole transaction against 0x0640000, at PCs 3FA71F00…3FA76390:

W 06400002 val=003A / 002A / 004A / 001A   ; CRA commands (reset Tx/Rx/err/MR ptr)
W 06400001 val=0088                        ; CSRA
W 06400000 val=0012                        ; MR1A
W 06400000 val=0080                        ; MR2A = 0x80 -> local loopback
W 06400002 val=0005                        ; CRA: enable Tx + Rx
W 06400002 val=000A                        ; CRA: disable Tx + Rx
R 06400001 val=0000                        ; SRA
R 06400003 val=0000                        ; RHRA

There is no write to off 0x03 (THRA/RHRA) anywhere on this path — the only 06400003 line in the whole log is the read above. So the transmitter is never fed.

I made that claim airtight with a full, non-deduplicated DUART access log (VT1200_DUARTLOG), 1400 frames, both DUARTs:

total DUART1 accesses: 134
writes to reg 3 (THRA): 0
writes to reg B (THRB): 0
reg-write histogram (DUART1): reg2=45  regA=24  regF=10  regE=7
                              reg5=6   regD=4   reg0=4   reg1=2  reg4=1

Every write is a mode/command/status register (MR, CSRA/CRA, ACR, IMR, CRB, OPCR, OP bits). No transmit register is ever written, on either channel of either DUART.

Breakpointing the test's own comparisons (harness/mame/duart_probe.lua) gives the expected vs actual:

3FA761E0  SRA & 0xF0  -> 0x00      ; test requires 0x10
3FA76390  RHRA        -> 0x00      ; test requires 0x0A, then 0x1F, then 0x0A

The expected values come from a RAM table at bit 0x270220 (dumped from main RAM): 0xAA, 0xFF, 0xAA, 0x55, …, masked by the table at 0x270470 (0x1F,…), i.e. 0xAA & 0x1F = 0x0A, 0xFF & 0x1F = 0x1F. Two details then fit together exactly:

  • the test first waits for a receive count to reach 5 (*struct+0x320 ≥ 5), and
  • it requires the overrun bit (SRA & 0xF0 == 0x10) — which is what a 5-byte burst into the 4-deep 68681 Rx FIFO produces.

In other words the test expects a 5-byte external pattern to have arrived on the receiver, overflowing the FIFO, and then reads the status and the first bytes back. Nothing in the firmware transmits that pattern, so the data can only come from off-chip.

This is why MAME's SCN2681 is necessary but not sufficient: the model is faithful, but the test's data source is external. The owner's fallback of "wire TX back to RX" cannot help, because there is no TX activity to loop. (Both were tried: internal loopback is already set by the firmware via MR2=0x80; external TX→RX was wired with VT1200_DUART_LB1 — the transcript is identical.)

What would be needed (an honest statement, not done)

To make the line disappear, something must play the host / active loopback: e.g. a driver-side callback that drives duart1->rx_a_w with the expected serial pattern (0xAA,0xFF,0xAA,0x55,…) in response to the receiver being enabled. That is a model of the host protocol, not of the DUART, and it is exactly the kind of hand-rolled stand-in the owner asked me to avoid unless the real device is impractical. I did not add it. I am flagging it as the concrete remaining piece.


5. Regression

With no VT1200_* env vars set, progress.lua at 200 frames still reports:

PC=3FDCE400  samples=199

All new behaviour is behind VT1200_DUART / VT1200_DUART_SEL / VT1200_DUART_LB1 / VT1200_DUART_LB2; the device entries are only registered when VT1200_DUART is set, and the SCN2681s are instantiated but idle (no timer running, no interrupt wired) otherwise.

6. MAME-tree changes

None this task. The only local MAME change remains the earlier, already-recorded REFCNT edit in src/devices/cpu/tms34010/tms34010.cpp (with its revert note inline). MAME's mc68681.cpp already implements the 68681's local loopback (MR2 bit 7:6 = 10) and the break-in-loopback behaviour; that is stock upstream code, not a local modification (verified: git status in the MAME tree shows only tms34010.cpp and mame.lst modified).

7. What I could not determine

  • The source of the host-port test's expected pattern. Every transmit path I could find in the ROM (*aX(0x30) writes at bits 3FA79C00, 3FA7B8E0, 3FA7BAA0) is in a different routine that is not reached by the level-1 host-port test. I could not find a THRA write on the host-port path, and the driver-side log agrees. I do not know whether on real hardware the pattern comes from the connected host or from a loopback connector with active logic.
  • Whether the VT1200's level-1 host-port test is expected to pass with nothing but a passive loopback plug. The service guide says level-1 "completes within 25 seconds" and mentions loopback connectors, but the firmware's own comparison (5-byte pattern + overrun) implies an active responder.
  • The DUART interrupt path. There is a DUART ISR in the ROM (L3FA78D90), and the second host-port sub-test waits on a flag that an ISR would set; I did not wire irq_cb() to the CPU, and did not establish whether the host-port test needs it. The failing sub-test (L3FA753D0) does not wait on that flag — it reads the chip directly — so the IRQ is not what fails it.

8. Files added / changed

  • harness/mame/vt1200.cpp — SCN2681 at 0x0640000 (host/printer) and 0x0620000 (keyboard/mouse); duart1_r/w, duart2_r/w with register = off, data & 0xff; optional external TX→RX loopback; monitor switch on IP4-IP6; all gated on VT1200_DUART*. Doc block updated. Also VT1200_DUARTLOG (full, non-deduplicated DUART access log — the proof in §4).
  • harness/mame/duart_probe.lua — breakpoints on the host-port test's comparisons and register writes (the evidence in §4).
  • harness/mame/DUART_REPORT.md — this report.

ORCHESTRATOR CORRECTION — §0 and §4 conclusions are superseded

Reviewed after delivery. The wiring and the remap in §1–§2 are correct and stand (I re-verified the remap against MAME's source, below). The conclusion in §0/§4 — that the host-port test "needs an external responder" and cannot pass without one — does not stand. Three grounds.

1. The service guide says this test is local loopback

docs/EK-V1000-SV-002_service-guide.txt:

Host Port Test — Verifies the internal data paths of the host port's universal asynchronous receiver transmitter (UART) by using its local loopback mode. If you also choose the external loopback test, this test checks the driver and receiver hardware as well.

External loopback is a separate opt-in selection ("External loopback: host port", bit 0). It is off by default. So the failing test must pass with no external equipment — which is what we independently know from the real terminal booting normally, and from level 0 already passing in this harness.

2. §4's own evidence is the loopback signature — with the transmit missing

§4 already establishes that the firmware sets MR2 = 0x80 (local loopback) and then expects 5 bytes to have arrived and set overrun (SRA & 0xF0 == 0x10, and STATUS_OVERRUN_ERROR = 0x10) in a 4-deep FIFO. A 5-byte burst overflowing a 4-deep FIFO is exactly what transmitting 5 bytes in local loopback produces. And the ROM table 0xAA,0xFF,0xAA,0x55 is far more plausibly the transmit pattern (a BIST pattern) than an inbound host stream — no host sends that.

So the open question is not "who supplies the pattern". It is why the firmware never writes THRA.

3. MAME's semantics, verified (this changes what §2/§4 mean)

Read from src/devices/machine/mc68681.cpp:

  • duart_base_device::read/write(offs_t offset) do offset &= 0xf — they take a register index 0..0xF directly. Channel A = regs 0x0–0x3, channel B = 0x8–0xB.
    • reg 0x3 write = THRA, read = RHRA; reg 0xB likewise for channel B.
    • reg 0xD read = IP (the monitor switch). So §2's remap is right.
  • write_CR() takes the command from (data >> 4) & 0x07, and enables the channel from BIT(data, 0). Consequences that contradict §2/§4's reading:
    • the init sequence 0xCA,0x2A,0x3A,0x4A,0x5A,0x1A has bit 0 = 0 in every value, i.e. every one of them disables; they are not "the command nibbles" in the low nibble sense;
    • the host-port test's CRA=0x05 is the enable (bit 0 set); the following CRA=0x0A disables again.
  • recalc_framing() sets set_data_frame(1, (MR1 & 3) + 5, ...). So 5-bit characters require MR1 & 3 == 0. §4 says the test writes MR1A = 0x12; 0x12 & 3 == 2 → 7 bits. But §4 also says the expected received bytes are the pattern masked by 0x1F (5 bits). Those two cannot both be true — one is misread, and resolving it tells us what the test is really doing.

4. Ruled out, so not the cause

  • The DUARTs are registered after the board-window model (vt1200.cpp 630–651), so they win offs 0–0xF and fall through to the board model above that. Not shadowed.
  • The DUART handlers log through the same log_pc_access() as the board model (lines 731/744), and duart1_w covers the whole window (< 0x10 → DUART, else board). So a THRA write would have appeared in the VT1200_BOARD_LOG transcript too. "No write to 06400003" is a real observation, not a logging gap.

5. Leading hypothesis for the missing transmit

The task did not wire m_duart1->irq_cb() to the CPU, and §7 notes both that the ROM has a DUART ISR (L3FA78D90) and that transmit routines exist at 3FA79C00/3FA7B8E0/3FA7BAA0 but are not reached. MAME's TMS34010 receives external interrupts through execute_set_input(): line 0 → INTPEND bit TMS34010_INT1, line 1 → TMS34010_INT2 (tms34010.cpp 817–838), gated by REG_INTENB. "Transmitter never fed" + "ISR not wired" + "transmit routines unreached" have one obvious common cause: an ISR-driven transmit whose interrupt is never delivered.

Caveat to check first: an earlier note in this project records that INTENB never enables, because the level-0 restart enters at 3F208060 with DINT. If the CPU really runs with interrupts disabled during the tests, an IRQ-driven transmit cannot work either — establish whether the test enables interrupts before relying on this.


FINAL FINDINGS — root cause found, acceptance NOT met (2026-10-16)

0. Status in one paragraph

Root cause found and fixed: the level-1 HOST PORT test drives its loopback with an ISR, and the DUART interrupt was never wired to the CPU, so the transmit register (THRA, reg 3) was never written. Wiring duart1->irq_cb() to the TMS34010's external interrupt 1 makes THRA get written (0 -> 20 writes per run) and immediately fixes two of the test's five checks: SRA & 0xF0 == 0x10 (overrun) and the three RHRA pattern reads now pass. The acceptance line LEVEL1/2 ERROR -> HOST PORT - 15 still appears. The remaining failure is a timing condition (below), not a wiring or a modelling-of-the-device error.

Address conventions: Ghidra = 0x07E40000 + file; bit = 0x3F200000 + file*8; a handler at base receives (bitaddr-base) >> 4. All numbers below are stated in the convention named.

1. The root cause (verified)

  • The firmware's fixed CPU vector table points INT1 (vector 0xffffffc0) and INT2 (0xffffffa0) at RAM trampolines: RLONG(0xffffffc0) = 0x00000F60, and bit 0x0F60 holds JA 0x3FA92F40 (a 3-word stub the firmware installs at boot). INT2 -> bit 0x0F30 -> JA 0x3FA93070. This resolves the earlier "vector reads 0xF60, which is bogus" confusion: the vectors are fine, they point at RAM stubs.
  • Those stubs call the DUART ISR L3FA78D90 with (mask, struct, window) = (2, 0x270860, 0x6400000) for INT1. So DUART1's interrupt is expected on INT1.
  • The ISR is the only code anywhere that writes THRA: the TxRDYA path at 3FA79ED0 does window[reg3] = struct[i].0x360, and struct[i].0x360 (ISR base) is the test's pattern array struct[i].0x330.
  • INTENB is enabled at the test: VT1200_* run shows INTENB going 0x0400 (display) at f154 and 0x0006 (INT1|INT2) at f474, i.e. the orchestrator's caveat is cleared — interrupts are on when the host-port test runs.

2. What I changed (kept)

  • harness/mame/vt1200.cpp: m_duart1->irq_cb() / m_duart2->irq_cb() -> new duart1_irq_w / duart2_irq_w, which call m_maincpu->set_input_line(0/1, ...). Gated on VT1200_DUART (no-env baseline untouched). This is the fix for the missing transmit.
  • /home/arti/code/k-space/mame0289/src/devices/machine/mc68681.cpp: one datasheet- backed timing correction, with a revert note inline. MAME asserted TxRDY mid-character ("not until the end of start bit time"); the datasheet says TxRDY is re-asserted at the THR->TSR transfer ("Data is transferred from the holding register to transmit shift register when it is idle or has completed transmission of the previous character. The TxRDY conditions are then asserted again"). Corrected to assert at tra_complete(). This took the failure from two failing checks to one. (Caveat: for the idle case the datasheet's transfer is immediate, so this is exact only for the buffered case, which is the test's steady state.)

3. Evidence that the wiring is the fix

VT1200_DUARTLOG (non-deduplicated), 900 frames, before vs after:

before: writes to reg 3 (THRA): 0
after : writes to reg 3 (THRA): 20   (0xAA,0xFF,0xAA,0x55,0x00 ..., one per TxRDYA)

The test's own comparisons, breakpointed:

before  SRA&0xF0 = 0x00  (needs 0x10)      RHRA = 0x00 (needs 0x0A,0x1F,0x0A)
after   SRA&0xF0 = 0x10  PASS              RHRA = 0x0A,0x1F,0x0A  PASS

So the "external responder" conclusion in §0/§4 is dead: the source of the pattern is the firmware's own DUART ISR, reached only when the DUART IRQ is wired.

4. The remaining failure — measured, with numbers

The host-port test's L3FA753D0 pass additionally snapshots SRA at every TxRDY interrupt and stores it into the descriptor as struct[index].0x3b0 (ISR base), where index = number of THRA writes so far. The test then requires:

  • struct[4].0x3b0 (read as the test's struct+0x3c0) to have bit 1 = FFULL, and
  • struct[5].0x3b0 (test struct+0x3d0) to have bit 4 = OVERRUN.

i.e. the loopback receiver must be in lockstep with the transmitter: after k bytes are transmitted, k bytes must have been received.

Measured (cycles of the TMS34010, total_cycles()), final pass:

TxRDY IRQ         T_k = 44592754, 44611168, 44629580, 44647992, 44666403, 44684815   (spacing 18414)
THRA write        W_k = T_k + 185
RX start-bit sync     = W_k + 2605   (= 1 bit time; bit = 2604)
RX rcv_complete       = sync + 16926 (= 6.5 bit times, the bit-centre samples)
                      = W_k + 19531

So the received character is delivered 19531 cycles after its THRA write, while the test's next snapshot is 18414 cycles later. The receiver is 1117 cycles (~0.43 bit) behind per character, so at index5 the FIFO holds 3, not the required 4, and OVERRUN is not yet set. Concretely: flags = 0x00000001 (only the struct[5] OVR check fails), SRA snapshots = 0x0C,0x0C,0x0D,0x0D,0x0F,0x0F.

To pass, the delivery must be < 18229 cycles after the write.

5. MAME-vs-datasheet audit of the remaining margin

I checked each candidate against the SCC68681 datasheet. Do not tune these — one is a genuine bug, the others are faithful.

  1. FIFO depth / overrun threshold — MAME is FAITHFUL. The owner's candidate (rx_fifo_push overruns at SIZE+1 == 4) is correct: the datasheet says the FIFO is three characters, a 4th character is held in the receive shift register, and overrun occurs only on the 5th ("If the FIFO is full when a new character is received, that character is held in the receive shift register ... If an additional character is received while this state exists ... the overrun error status bit (SR[4]) will be set"). MAME's SIZE+1 guard models exactly that. Not the bug.
  2. TxRDY timing — MAME was WRONG (fixed, §2). Removed one failing check.
  3. Overrun timing — MAME is WRONG, and this is the remaining margin. The datasheet says SR[4] is set "upon receipt of the start bit of the new (overrunning) character" — i.e. at the start of the 5th character (~W+1.5 bits), not at its completion. MAME sets it in rx_fifo_push(), i.e. at the 5th character's completion (W+7.5 bits) — about a whole character late. Setting it at the overrunning character's start bit would move SRA's OVR bit into struct[5]'s snapshot with ~15k cycles of margin and should make the test pass. This is the concrete next step; it was not implemented here because it needs a "new character started" hook in the receive path (generic diserial.cpp, or a DUART override), which I could not land and validate within budget without risking a green-on-a-guess.
  4. Loopback receive clock — MAME deviates, but is not the immediate blocker. The datasheet's local-loopback mode says "the transmit clock is used for the receiver"; MAME clocks the loopback receiver from the independent receive clock (set_rcv_rate + the generic RX sampling, with a deliberate half-cycle start guard). This is the source of the 0.5-bit phase; I tried removing the guard and the received bytes became wrong (0x14,0x1E,0x14 instead of 0x0A,0x1F,0x0A), confirming the guard is what puts the samples on the bit centres. Do not remove it.

6. Acceptance / regression

  • Acceptance: NOT MET. With VT1200_VRAM=1 VT1200_BOARD_MODEL=handshake VT1200_BOARD_BUSY_TICKS=2 VT1200_BOARD_SWITCH="0640000D:70:70" VT1200_DUART=1 VT1200_DUART_SEL=1, cap.lua still prints 'LEVEL1/2 ERROR -> HOST PORT - 15 '. No next gate is reached.
  • Regression: HELD. With no VT1200_* env vars, progress.lua at 200 frames: PC=3FDCE400 samples=199.

7. Artifacts

  • harness/mame/vt1200.cpp — IRQ wiring + VT1200_STRUCTLOG / VT1200_IRQLOG (transparent logging proxies) and a cycle stamp on VT1200_DUARTLOG.
  • harness/mame/hp_trace.lua, hp_probe.lua, hp_probe2.lua, hp_snap.lua, hp_int.lua, hp_ret.lua, hp_dump.lua, hp_dump2.lua — the measurement scripts.
  • harness/mame/hp_*.log, hp_*.out — the raw evidence quoted above.
  • MAME tree: mc68681.cpp (TxRDY timing, revert note inline). diserial.cpp left unmodified.

8. UPDATE — overrun-latch timing fixed; acceptance still not met

Item 3 of §5 was implemented and is the second datasheet-backed MAME correction:

  • mc68681.h / mc68681.cpp: added duart_channel::rx_w() (and routed rx_a_w/rx_b_w through it). When the RxD line falls (a start bit) while rx_fifo_num == MC68681_RX_FIFO_SIZE + 1 (FIFO full and the previous character still held), it latches STATUS_OVERRUN_ERROR. This is the datasheet's timing ("the overrun error status bit (SR[4]) will be set upon receipt of the start bit of the new (overrunning) character") instead of rx_fifo_push()'s latch-at-completion. Revert note inline.

Effect on the test's own comparisons (the struct[4]/struct[5] SRA snapshots):

before  RX-3c0 = 0x70005 (FFULL clear)   RX-3d0 = 0x0007 (OVR clear)   flags = 0x00000001
after   RX-3c0 = 0x1F000F (FFULL set)    RX-3d0 = 0x001F (OVR set)     flags = 0x00000000

i.e. every check in L3FA753D0 (RXCHECK) now passes, and it proceeds to test the next parameter combination — which it never did before. This confirms §5.3 was a real MAME inaccuracy, not a guess.

The acceptance is still not met, and I did not resolve the next failure. The host-port test's first sub-test L3FA71BC0 now returns 6, and the caller (3FA84FE0) turns any non-zero return into the reported error. L3FA71BC0's own check sites (3FA73500/3FA73580/3FA73610, which set its flag bits) were breakpointed and never fired in 700 frames, so its return value does not come from the path I read statically; that is the loose end. L3FA71BC0 is the DUART baud/timer sub-test that runs before RXCHECK, so it is a separate condition from the loopback data path.

Final state of the acceptance instrument (900 frames):

'LEVEL1/2 TESTING ** '
'LEVEL1/2 ERROR -> HOST PORT - 15 '      <-- unchanged
regression (no VT1200_* env): PC=3FDCE400 samples=199   HOLD

CAUSALITY AUDIT (four-config A/B) — the failure is PRE-EXISTING

Reviewed after the owner flagged that "the failure moved backwards" and that my two MAME edits are both timing changes while the newly-blamed sub-test is a timer test. Method: four configurations of the MAME tree (IRQ wiring kept in all four — without it the test fails even earlier), built with harness/mame/hp_ab.sh <cfg>:

cfg IRQ TxRDY edit overrun edit console L3FA6E4B0 return
1 on on on HOST PORT - 15 6
2 on reverted on HOST PORT - 15 6
3 on on reverted HOST PORT - 15 6
4 on reverted reverted HOST PORT - 15 6

Verdict: PRE-EXISTING. The console line and the caller's return are identical in all four configurations, including cfg 4 where both MAME edits are reverted to stock. Neither mc68681 edit causes or relocates the failure; they only change RXCHECK's own flag word (Section UPDATE). Variant files are kept in harness/mame/mame_edits/ (*.cfg1, *.cfg2, *.cfg3, *.upstream).

Methodology correction (important)

My earlier hp_ret.lua attached labels to the wrong rets addresses, so the return values I quoted were mis-attributed. Verified against the disassembly, the correct rets sites and their owning functions (prologue register mask) are:

rets function return
3FA71BB0 L3FA6E4B0 (the generic port-test routine) 6
3FA753C0 L3FA71BC0 (baud/timer sub-test) 0x00900040
3FA767D0 L3FA753D0 (RXCHECK sub-test) 0x00000408
3FA77800 L3FA767E0 (XMITPASS sub-test) 0

harness/mame/hp_ret2.lua uses the corrected addresses. So the owner's premise "6 comes from L3FA71BC0" is off: L3FA71BC0 returns 0x00900040; the 6 is L3FA6E4B0's own return.

What 6 actually is — not a failure code

L3FA6E4B0 (the routine the level-1 host-port caller invokes) ends its body by reducing five descriptor fields to a 5-bit value (3FA718D0–3FA71B50) and returning it:

bit0 <- (struct+0x60 != 0)      bit3 <- (struct+0xc0 != 0)
bit1 <- (struct+0x80 != 0)      bit4 <- (struct+0xe0 != 0)
bit2 <- (struct+0xa0 != 0)

with struct+0x80/struct+0xa0/struct+0xc0/struct+0xe0 set from the four sub-test calls (L3FA71BC0, L3FA753D0 x2 each). So:

  • 6 = 0b00110 — a bitmap of which sub-tests returned nonzero, not a device status code: L3FA71BC0 non-zero and L3FA753D0 non-zero; L3FA767E0 returned 0, and the second-trio fields were never written (the caller aborts after the first trio).
  • The host-port caller (3FA85000) is move a8,a8 ; jreq <continue>: any nonzero value takes the error path, which returns 0x400 (3FA853A0). 0x400 is what the status dispatcher turns into LEVEL1/2 ERROR -> HOST PORT - 15.

Unresolved, and flagged: the polarity is suspicious. L3FA753D0 (RXCHECK) returns 0x408 while all four of its own checks pass (flag word 0 — see UPDATE), so "sub-test returned nonzero" is not the same as "sub-test failed its checks". Either its success path leaves a stale a8, or the five fields are pass flags and the caller's test is inverted relative to my reading. I could not settle this, so 6 should not be treated as a plain failure code until it is.

The two leads

  1. Counter spuriously active — checked, not the cause. ISR bit 0x08 (COUNTER_READY) is set from frame 1, but MAME sets it in the Start Counter Command read path (mc68681.cpp case 0x0e: start_ct(); set_ISR_bits(INT_COUNTER_READY)), and the firmware itself performs that read at boot (R 0640000E pc=3F200150). It is not masked into the IRQ (IMR = 7 = TxRDYA|RxRDYA|DeltaBreakA, no bit 3), and the DUART ISR L3FA78D90 does not handle ISR bit 3, so it cannot disturb the test. ACR reads 0x0000 except when the sub-test writes 0x60.
  2. DUART clock — unverified. The service guide has no crystal/baud figure for the DUART (grep -i '3.6864|crystal|oscillator' docs/EK-V1000-SV-002_service-guide.txt → nothing; its only "baud" hit is test 16 "Test all baud rates", a level-2+ test). 3.6864 MHz is the 68681 datasheet's nominal and remains the only evidence; I could not confirm it against the guide.

Acceptance / regression (cfg 1, committed state)

  • Console: LEVEL1/2 TESTING ** then LEVEL1/2 ERROR -> HOST PORT - 15 — unchanged.
  • Regression (no VT1200_*): PC=3FDCE400 samples=199 — holds.

CORRECTION — edit 1 (TxRDY) was REFUTED and has been REVERTED

An independent worker read the fitted part's datasheet and checked both citations (reference/mc68681-datasheet-verification.md, commit 031d30da67dd).

  • Edit 1 (assert TxRDY at tra_complete() / the character boundary) — REFUTED. Signetics SCN2681 p.22, "Transmitter Disable Note": "TxRDY sets at the end of the 'start bit' time. It is during the start bit that the data in the transmit holding register is transferred to the transmit shift register." That is exactly what upstream tra_callback() implemented. The p.7 sentence the edit cited is real but says nothing about when TxRDY is set. So upstream was faithful and the edit was a datasheet-contradicting change that happened to line up one snapshot. It has been reverted (the note in mc68681.cpp now records the refutation and says not to re-apply it). The claim in commit 9902722165fc — "fix two DUART timing deviations" — is therefore half false; only edit 2 stands.
  • Edit 2 (latch overrun at the start bit) — CONFIRMED, quote and application. Kept.
  • FIFO depth faithful — CONFIRMED. (My refutation of the owner's FIFO candidate also stands.)

Did reverting edit 1 relocate the failure to L3FA71BC0? No.

Config 2 = edit 1 reverted, edit 2 kept:

L3FA71BC0  (rets 3FA753C0) = 0x00900040
L3FA753D0  (rets 3FA767D0) = 0x00000003     (was 0x00000408 with edit 1)
L3FA767E0  (rets 3FA77800) = 0
L3FA6E4B0  (rets 3FA71BB0) = 6              (unchanged)
console                    = 'LEVEL1/2 ERROR -> HOST PORT - 15'   (unchanged)

Neither the console nor the caller's value changes; the failure did not relocate. It was never "in L3FA71BC0" — that came from the wrong-rets attribution corrected above.

The "ISR one byte ahead of the receiver" symptom — explained

With faithful TxRDY (config 2), the last pass measures (cycles, total_cycles()):

THRA writes            44592939, 44598330, 44611350, 44629580, 44647808
                       => inter-write gap 5391, 13020, 18230, 18228  (char = 18226)
SRA at each TxRDY      0C, 04, 04, 05, 05, 07
                       => index4 is NOT FFULL, index5 is NOT OVR

Cause: TxRDY re-asserts at the end of the start bit (faithful), so the ISR writes the next byte ~1 bit into the current character — i.e. the transmitter is ~1 byte ahead of the character boundary — while the loopback receiver only completes a character a full character after it was written. So the received count lags the transmit count, and the test's per-index snapshots (which expect lockstep) miss.

The reason the receiver lags is a real, independently-confirmed MAME deviation: in local loopback the datasheet says (SCN2681 printed p.12, MR2A[7:6] = 10, item 2) "The transmit clock is used for the receiver." MAME instead clocks the loopback receiver from the independent receive clock (set_rcv_rate + the generic m_rcv_clock sampling with its half-cycle start guard). The confirmation is already recorded in reference/mc68681-datasheet-verification.md ("Incidental findings"): "MAME clocks the loopback receiver from the independent receive clock — directionally correct against the datasheet."

So the story closes: edit 1 masked the loopback-clock deviation by dragging the transmitter back to the receiver; with faithful timing the deviation is exposed. Edit 1 must not be restored. The next fix is to clock the loopback receiver from the transmit clock (SCN2681 p.12), which per the project's process needs the quoted sentence read independently before it is written.

The other step-5 leads

  • DUART crystal. reference/duart-datasheet/SCN2681-transputer.txt gives fCLK nominal 3.6864 MHz ("the crystal oscillator operates directly from a 3.6864MHz clock"; Table 3 is specified "Crystal or Clock = 3.6864MHz"), and reference/keyboard-mouse/... also shows a 3.6864 MHz part. That is the datasheet's nominal; the VT1200 board crystal itself is still unconfirmed (no schematic/guide figure). The driver's XTAL(3'686'400) therefore remains an assumption, but a well-founded one.
  • CSRA-derived rates. CSRA = 0x88 -> calc_baud(ch, false, 8) and calc_baud(ch, true, 8). In duart_base_device::calc_baud the rx argument is unused and both nibbles index the same baud_rate_ACR_* table, so TX and RX are both 2400 baud — equal. Not a mismatch. (Measured bit time ~415 us at 2400 baud confirms it.)
  • Character frame. MR1A = 0x10 -> recalc_framing uses (MR1 & 3) + 5 = 5 bits, no parity, 1 stop; the test's pattern arithmetic masks with 0x1F. Consistent.
  • Counter lead — cleared. ISR bit 0x08 is set by MAME's Start Counter Command read path (case 0x0e), and the firmware performs that read itself at boot. It is not in IMR (7) and L3FA78D90 does not handle ISR bit 3, so it cannot disturb the test.
  • INT1 vs INT2 — immaterial. The two vector stubs differ only in their 3rd argument (2 / 4), and L3FA78D90 never reads that argument (only *a13(0xffc0)/0xffe0). OPCR and OPR are written 0 on every transmit, so the discrete interrupt outputs (OP4/OP6) are not in use — the chip's single INTRN is the path, and the INT1 wiring is correct.

Acceptance / regression (current tree: edit 1 reverted, edit 2 kept)

  • Console: 'LEVEL1/2 TESTING ** ' then 'LEVEL1/2 ERROR -> HOST PORT - 15 ' — still present; no next gate.
  • Regression (no VT1200_*): PC=3FDCE400 samples=199 — holds.

WHAT MAKES THE CALLER RAISE 0x400 — resolved empirically

The owner's gating worry (if the caller does not actually turn a nonzero L3FA6E4B0 return into 0x400, then fixing the sub-tests' checks cannot clear HOST PORT - 15) is resolved. Breakpoints at the call site and at the handler, one run (harness/mame/hp_disp.lua):

DISPATCH-READ         code=0000FFFF          <- 'LEVEL1/2 TESTING **'
CALL-L3FA6E4B0                               <- call site 3FA84FB0
AFTER-CALL            a8=00000006            <- call site 3FA84FE0 stores a8
CALLER-ERR-RET        a8=00000006 a11=00000400   <- error path sets a11=0x400, returns it
DISPATCH-READ         code=00000400
HOSTPORT-HANDLER      code=00000400          <- handler 3FDE9D70 prints HOST PORT - 15

So the chain is confirmed at the call site, not from a register snapshot:

  • 3FA84FB0 calla L3FA6E4B0; 3FA84FE0 move a8, *a13(0x160) (the return); 3FA85000 move a8,a8 ; 3FA85010 jreq L3FA853D0 — any nonzero takes the error path; that path ends at 3FA85360 movi 0x400,a11 ; 3FA85380 *a7(0x40)=a11 ; 3FA853A0 a8=a11.
  • 0x400 is the HOST PORT dispatch code (measured live at 3FDE9D70). My earlier static reading of the dispatcher's subi chain suggested 0x3F8; dis34's subi rendering is misleading here and the live value is authoritative.

The quantity to zero is L3FA6E4B0's return value: it must be 0, i.e. all five descriptor fields must be 0.

Polarity — resolved, and my earlier caveat was wrong

L3FA71BC0 returns *a13(0xc0) (its 32-bit flag word) and L3FA753D0 returns the same word (3FA76550) or a9, which is set to that word at 3FA76440 and is untouched by the success loop — so on success both return 0. Therefore L3FA6E4B0's bits are failure bits, and "nonzero" does mean "this sub-test failed its checks".

The earlier "L3FA753D0 returns 0x408 while all its checks pass" was a misread: the 32-bit flag word is not the four loop flags alone — 3FA76480–3FA76520 also write a 4-bit field at a13(0xc4) and 2-bit fields at a13(0xc8/ca/cc), which are inside the same 32-bit read. So 0x408 is a genuine failure, not a false positive. This also removes the concern that the checks are the wrong quantity: they are the right one.

Is the local-loopback clock deviation the cause? No.

Mechanism, stated explicitly (the owner's requirement): CSRA = 0x88 selects the same calc_baud value for TX and RX (the rx argument is unused in duart_base_device), so the two clocks are the same rate (2400 baud). Clocking the loopback receiver from the transmit clock therefore changes only the sampling phase/edges, not the rate — and it does not change how many bit-times the receiver takes (6.5) nor when it finishes.

Measured, it is not even observable: the received bytes are correct (0x0A,0x1F,0x0A = the pattern masked by 0x1F), so MAME already samples the TX bit centres; and the completion is sync + 6.5 bits either way. Verdict: a real deviation (SCN2681 p.12 item 2) but NOT the cause of the residual, and it cannot move the symptom — so I did not change it rather than bank a green.

The measured residual is a ~1-bit write->receive offset: RX completion = W + 1.07 char while the write interval is 1.00 char, so the received byte misses the next snapshot by 0.5 bit. Its source is the ~1 bit between a THRA write and the first transmitted bit (sync = W + 2605, bit = 2604) — i.e. the transmit clock's first edge is at 2*m_tra_rate, not m_tra_rate. I tried forcing it to m_tra_rate and to zero; the first did nothing and the second corrupted the received bytes (sampling moved to the bit boundaries), so the half-bit TX offset is load-bearing for the sampling phase. Not banked; recorded as the open item.

The datasheet items checked against our model

  • item 3 (TxDA held High) and item 4 (RxDA ignored): modelled — tra_callback() does not drive the TX pin in loopback and feeds the bit to rx_w instead; the external RxD input is unused in this mode.
  • item 5 (transmitter must be enabled, receiver need not be): matches — rcv_complete() pushes when rx_enabled || loopback, so the FIFO keeps filling with the receiver disabled, which is what the firmware's CRA=0x05 then CRA=0x0A (bit1 = disable Rx, bit3 = disable Tx) sequence relies on. write_CR(0x0A) also disables the transmitter, and MAME lets the in-flight character finish (the tra timer is already running), so the 5-byte burst completes.
  • One minor deviation, not biting here: write_CR's bit1 handler clears RxRDY, whereas the datasheet says a receiver disable preserves the FIFO and its status. read_rx_fifo re-derives RxRDY, and the test's SRA check looks only at the high nibble (OVR), so it does not affect this test.

Acceptance / regression (current tree: edit 1 reverted, edit 2 kept)

  • Console: HOST PORT - 15 — still present; no next gate.
  • Regression: PC=3FDCE400 samples=199 — holds.

ENUMERATION — the exact remaining failing checks (measurement only)

Broken on every flag-set site in both sub-tests (hp_flags.lua) and on the three comparisons (hp_vals.lua), current tree (edit 1 reverted, edit 2 kept). Of the 18 flag-set sites in L3FA71BC0 and 4 in L3FA753D0, exactly three fire; every other check in both sub-tests passes:

sub-test flag comparison PC quantity / expected actual
L3FA71BC0 c6 3FA73970 btst 5,a9 bit5 of struct[index].0x60 = 1 0x0001, 0x0009 -> bit5 = 0 (3 passes; 0x0021 passes)
L3FA753D0 c1 3FA760E0 btst 1,a11 bit1 (FFULL) of struct+0x3c0 (index4 SRA snapshot) = 1 0x05 -> FFULL = 0
L3FA753D0 c0 3FA76050 btst 4,a11 bit4 (OVR) of struct+0x3d0 (index5 SRA snapshot) = 1 0x07 -> OVR = 0

Note: cE (3FA75060) also fires once but it is a summary (it copies *a13(0x40) when any earlier flag is set), not an independent check. So L3FA71BC0 has one failing condition and L3FA753D0 has two — the two status-snapshot checks. The direct SRA/RHRA reads in L3FA753D0 pass (flags c2/c3 do not fire).

Delta at the two snapshot checks: at index4 the FIFO holds 1-2 (needs 3 for FFULL) and at index5 it holds 3 (needs 4 for OVR) — i.e. the receive count is one to two bytes behind the transmit count at the comparison instants.

TASK 2 — the transmit-start citation (for independent reading)

Question: when does the fitted part begin transmitting after a character is loaded into the THR of an idle transmitter? The two sentences that bear on it (reference/duart-datasheet/SCN2681-transputer.txt):

  • p.7, "Transmitter Operation":

    "Data is transferred from the holding register to transmit shift register when it is idle or has completed transmission of the previous character. The TxRDY conditions are then asserted again which means one full character time of buffering is provided."

  • p.22, "Transmitter Disable Note":

    "TxRDY sets at the end of the 'start bit' time. It is during the start bit that the data in the transmit holding register is transferred to the transmit shift register."

Reading: "when it is idle" implies no waiting for a character boundary for an idle transmitter (the transfer is immediate), and p.22 places the transfer inside the start bit — so the start bit must already be running when the transfer happens, i.e. the first bit begins at (or within a fraction of a bit of) the write. Neither sentence states a bit-count latency. MAME measurably inserts ~1 bit: measured RX sync = THRA write + 2605 cycles with a 2604-cycle bit (hp_duart_cfg2.log / hp_irq_cfg2.log). So the candidate deviation is "MAME delays the first transmitted bit by ~1 bit", justified by p.7's "when it is idle" — but that sentence does not by itself pin the number, so this needs the independent read before it is written, exactly like the last two.

FULL PICTURE (bound reached — stopping here)

Established.

  • The quantity to zero is L3FA6E4B0's return (all five descriptor fields = 0); verified at the call site and at the handler (0x400).
  • The IRQ wiring is required and correct (THRA 0 -> 20 writes/run); the direct SRA/RHRA checks pass; the overrun-latch edit (edit 2) is confirmed and clears the RXCHECK flags it targets.
  • The exact three failing checks and their comparison PCs (table above).

Ruled out. external responder; FIFO depth (faithful); loopback-clock deviation as the cause (phase-only, received bytes correct); the counter/COUNTER_READY (not in IMR, not handled); INT1-vs-INT2 and the OP interrupt outputs (L3FA78D90 ignores its mask arg; OPCR/OPR cleared); character frame width (MR1A=0x10 -> 5 bits matches the 0x1F mask); CSRA rate mismatch (TX == RX == 2400).

Remaining discrepancy. A ~0.5-1 bit transmit-start latency (above), which at the snapshot boundaries shows up as a 1-2 byte receive lag. Measured numbers: THRA writes at t, t+0.3, t+1.0, t+2.0, t+3.0 char; RX completion at write + 1.07 char; snapshots at the write instants.

Open MAME deviations. (1) transmit-start latency ~1 bit — candidate, citation not crisp, not written; (2) loopback receive clock, SCN2681 p.12 item 2 — real, not the cause, not written; (3) write_CR bit1 clears RxRDY where the datasheet preserves FIFO status — minor, not biting; (4) COUNTER_READY set at the Start-Counter read — not biting; (5) the pre-existing tms34010 REFCNT local edit.

Acceptance: HOST PORT - 15 still present. Regression: holds (PC=3FDCE400 samples=199).


WHO WRITES +0x3c0 / +0x3d0, AND WHAT L3FA71BC0 ACTUALLY CHECKS

Direct writer evidence from the transparent descriptor-area proxy (VT1200_STRUCTLOG, hp_struct.log), not inference:

W 270C50 pc=3FA79E50 val=0004 / 0005      <- test +0x3c0 = ISR struct[idx4].0x3b0 (SRA snapshot)
W 270C60 pc=3FA7A3C0 val=0004 / 0007      <- test +0x3d0 = ISR index5 SRA snapshot
W 2708F0 pc=3FA796D0 val=0001 / 0021      <- test struct[i].0x60 = ISR struct[i].0x90 (SRA snapshot)

All three are written by the DUART ISR L3FA78D90, and only there:

  • 3FA79E30/3FA7A3A0 are its TxRDYA path (*a7(0x3b0) = window reg1) — the per-index SRA snapshot. So test +0x3c0 = the snapshot at index 4 and +0x3d0 = the snapshot at index 5 (the test's descriptor is the ISR's descriptor +0x30, so test +0x3cN == ISR struct[idxN].0x3b0). Bit 1 = FFULL, bit 4 = OVR.
  • 3FA79690 is its RxRDYA path (*a11(0x90) = window reg1) — the received status for each character. Test struct[i].0x60 is exactly that field, which is why the c6 comparison sees 0x0001 / 0x0021.

So both sub-tests' failures are the ISR's SRA snapshots, and nothing else.

What L3FA71BC0 checks (my own evidence)

Broken on all 18 of its flag-set sites (hp_flags.lua): exactly one fires — c6. The comparison is 3FA73970 btst 5, a9 with a9 = struct[i].0x60 (the ISR's received status for character i), and jreq skips the set — so c6 is set when bit 5 is SET. SRA bit 5 is PARITY ERROR. Observed: a9 = 0x0001 (no error) in most combinations, and a9 = 0x0021 (= RxRDY | parity error) in 3 combinations, which are the 3 times c6 fires.

So:

  • L3FA71BC0's remaining failure = a PARITY ERROR in the received status, in 3 of its combinations (the parity-enabled MR1 settings) — a status mismatch, not a timing one. Its c5 check (bit 6 = framing error) does not fire.
  • L3FA753D0's remaining failure = the ISR's index4/index5 snapshots lack FFULL / OVR — a receive-count lag.

These are two different mismatches (one status, one count), both read from SRA by the ISR. The single mechanism that could produce both is a receive-side mis-sampling/offset: a sub-bit offset shifts character completion (-> FFULL/OVR missing) and can flip the parity bit (-> parity error) while leaving the data bits correct (-> the RHRA compares still pass). That keeps the measured ~1-bit write->receive offset (§ earlier) as the leading candidate, but the parity error is a new, testable consequence of it and should be checked directly before attributing.

Owner's static reading of L3FA753D0 — verified, with corrections

Correct on all substantive points (the four checks, the 144 = 4x3x3x4 nested sweep, the CRA sequence, the per-check flags, and the index record at +0xc4/c8/ca/cc on failure). Two corrections:

  1. The PCs quoted are the loads (3FA76030, 3FA760C0, 3FA76190, 3FA76340); the actual comparisons are 3FA76050 (btst 4), 3FA760E0 (btst 1), 3FA761E0 (andi 0xf0 / subk 16) and 3FA76390 (cmp).
  2. Flag bit encoding confirmed: each flag is a 1-bit field at bit address a13 + 0xc0 + N, so the 32-bit return read gives bit N = flag +0xcN. Proof: the +0xc6 store shows up in the proxy as W 001E30 pc=3FA739D0 val=0040 — bit 6 of the word at a13(0xc0). Caveat: the 32-bit read also spans the word at a13+0xd0 (bits 16-31). For L3FA71BC0 the return 0x00900040 has bits 20/23 set from that word, i.e. the return carries adjacent frame data as well as flags — so "return == 0" is stricter than "all checks pass" unless that word is zero. Worth confirming before treating L3FA71BC0's return as a pure flag word.

LAST AUTHORISED TIMING CHANGE — tested, did not move the symptom, reverted

Mechanism quantified first (as required)

Per-byte, same 5-byte burst, current tree (hp_lat.log / hp_lat.out). Character start = first transmitted bit (VTCHSTART); THRA write = VT1200_DUARTLOG:

byte THRA write char start start latency
0 44592939 44595544 2605
1 44598330 44613772 15442
2 44611350 44632002 20652
3 44629580 44650230 20650
4 44647808 44668460 20652

So the transmit-start latency does not accumulate: it is 1 bit for the idle byte 0 only, then constant (~1.13 char) because every later character is pinned by the previous character's end (the buffered path). The owner's magnitude argument therefore predicted the deviation could not be the cause, and this measurement confirms it.

The change, tested

Implemented as authorised, contained to the idle path: write_TX's idle branch calls transmit_register_setup(data, true), which schedules the first bit immediately instead of at the next 1x tick (new start_now parameter in emu/diserial.{h,cpp}); the buffered path and the whole receive side untouched. Citations recorded in the edit: SCN2681 "The clock outputs from the BRG are at 16X the actual baud rate", the 1x/16x external clock note, CSRA = receiver/transmitter clock select, and the receiver's "each 16X clock for 7-1/2 clocks" sampling note.

Result (hp_lat2.log, hp_lat2.out): byte 0's start latency went 2605 -> 5 cycles, and nothing else changed — bytes 1-4 stayed 15442/20652/20650/20652, the receive completions moved with the writes, and the outcome was identical:

flag-set sites that fire:  BC0/c6 x3, BC0/cE (summary) x1, RX/c0 x1, RX/c1 x1   (unchanged)
returns:                   L3FA71BC0 = 0x00900040, L3FA753D0 = 3, L3FA767E0 = 0, L3FA6E4B0 = 6
console:                   'LEVEL1/2 ERROR -> HOST PORT - 15'                    (unchanged)

Decision: REVERTED (not kept)

The deviation is real and datasheet-backed, but the change does not move the symptom, and it is only a partial fidelity fix — a fully faithful model would clock the transmitter at 16x (the real part), which is a much larger change, and the buffered path would still quantise to a bit. Keeping a partial change that buys nothing adds noise to the tree, so it is reverted; the deviation and its citations are recorded here instead. The MAME tree is back to: edit 2 (overrun latch) only, plus the inline refutation note for edit 1. emu/diserial.{h,cpp} are unmodified.

HANDOVER — full picture

Established.

  • Root cause of the original missing transmit: the DUART IRQ was unwired; wiring DUART1 -> INT1 makes THRA get written (0 -> 20/run) and fixes the direct SRA/RHRA checks.
  • The quantity to zero is L3FA6E4B0's return, verified at the call site and at the handler (0x400 -> HOST PORT).
  • Exactly three checks still fail, all reading the DUART ISR's own SRA snapshots (writers confirmed by proxy: ISR TxRDYA path 3FA79E30/3FA7A3A0 for +0x3c0/+0x3d0, ISR RxRDYA path 3FA79690 for struct[i].0x60):
    • L3FA71BC0 c6 (3FA73970): the ISR's received status has SRA bit 5 = PARITY ERROR in 3 combinations (0x0021), clean (0x0001) otherwise.
    • L3FA753D0 c1/c0 (3FA760E0/3FA76050): the index4/index5 snapshots lack FFULL/OVR — a receive-count lag at the snapshot boundary.
  • The direct loopback data path is correct (RHRA compares pass; received bytes 0x0A,0x1F,0x0A as expected).

Ruled out. external responder; FIFO depth (faithful); loopback-clock deviation as a cause; the counter/COUNTER_READY lead; INT1-vs-INT2 and the OP interrupt outputs; frame width; CSRA rate mismatch; and now the transmit-start latency (real, but non-accumulating and provably not the cause).

Remaining discrepancy. The ISR's SRA snapshots differ from the firmware's expectation in two ways: a ~1-byte receive-count lag, and a parity error in 3 parity-enabled combinations. Both are sub-bit phase/sampling artifacts, not data-path failures.

Open MAME deviations. (1) transmit-start 16x quantisation — real, tested, not the cause; (2) loopback receive clock (SCN2681 p.12 item 2) — real, phase-only, not the cause; (3) write_CR bit1 clears RxRDY where the datasheet preserves FIFO status — minor; (4) COUNTER_READY set at the Start-Counter read — not biting; (5) pre-existing tms34010 REFCNT local edit.

Acceptance: LEVEL1/2 ERROR -> HOST PORT - 15 — still present; no next gate. Regression: holds (PC=3FDCE400 samples=199).

Suggested next step (not this gate). The renderer is independent of this gate; the remaining host-port discrepancy is a sub-bit phase artifact in the DUART ISR's SRA snapshots and would need either a 16x-clock transmit model or a receive-phase rework to chase further.