From b6da987aaee13e0a2a5524f65fb83ddb6a25c21a Mon Sep 17 00:00:00 2001 From: Arti Zirk Date: Fri, 16 Oct 2026 17:12:02 +0300 Subject: [PATCH] harness: dump the LANCE DMA; the C - LANCE TEST's failing frame carries no FCS by design The failing loopback case is a negative test: its TX buffer deliberately has no FCS, and the test expects a CRC error. The DTCR=1 case that does carry an FCS is delivered intact and passes, so the DMA is not dropping four bytes. The sole remaining mismatch is one bit: the test wants RMD1=0x6B00 (ERR|FRAM|CRC|STP|ENP) and MAME's am79c90 reports 0x4B00 -- it never sets RMD1_FRAM. No MAME-tree change made; reported for a second reader. --- harness/mame/LANCE_LOOPBACK_REPORT.md | 150 ++++++++++++++++++++++++++ harness/mame/README.md | 17 +-- harness/mame/vt1200.cpp | 44 +++++++- 3 files changed, 202 insertions(+), 9 deletions(-) create mode 100644 harness/mame/LANCE_LOOPBACK_REPORT.md diff --git a/harness/mame/LANCE_LOOPBACK_REPORT.md b/harness/mame/LANCE_LOOPBACK_REPORT.md new file mode 100644 index 0000000..2d1c1cb --- /dev/null +++ b/harness/mame/LANCE_LOOPBACK_REPORT.md @@ -0,0 +1,150 @@ +# `C — LANCE TEST`: the transmit buffer, the FCS, and the remaining FRAM bit + +Date: 2026-10-16. Firmware `vt1200_v22.bin` md5 +`b46a3e442ccb86134d2356bd70d22fd2` (**unmodified** — everything below is observed +at runtime; no ROM byte was changed, and **nothing in the MAME tree was changed**). +Follows `reference/lance-loopback-fcs.md` (the FCS *condition* is settled and MAME +is right on it). + +## Verdict (short) + +1. **The firmware does NOT append an FCS to the frame that fails.** It is + deliberate, not a DMA truncation. The `C — LANCE TEST` runs **four** internal + loopback cases; the failing one is a **negative test** that is *supposed* to + send a frame with no FCS and *supposed* to see a CRC error. +2. **The DMA path is correct.** The chip reads exactly the bytes the CPU wrote, + at the descriptor's address and the descriptor's length; there is **no 4-byte + discrepancy**. `RCV descr 1 = 0x4B00` is the chip's honest CRC error on that + deliberately-unterminated frame. +3. **The only remaining mismatch is one bit.** The test expects + `RMD1 = 0x6B00` (`ERR|FRAM|CRC|STP|ENP`); MAME reports `0x4B00` + (`ERR|CRC|STP|ENP`). MAME's `am79c90` **never sets `RMD1_FRAM`**. That single + bit is the sole blocker, and it is a chip-model question, not a DMA question. +4. Per both datasheets `FRAM is not valid in internal loopback mode`, so MAME's + omission matches the book — the firmware's expectation does not. **I did not + edit the MAME tree.** + +## How the transmit buffer was read (new knob) + +`VT1200_LANCE_DMA_LOG=` logs **every LANCE bus-master DMA word access** +with the LANCE byte address, the value, the byte lanes and the PC: + +``` +# kind byteaddr value mask pc cyc t -- every LANCE bus-master DMA word access +# 'r' = chip read (dma_in), 'w' = chip write (dma_out) +r 00FF60 0044 FFFF pc=3FDDCDC0 ... <- INIT block word 0: mode = 0x0044 +... +r 00FF24 FFE6 FFFF pc=3FDCE400 ... <- TMD2 = 0xFFE6 -> length 26 +r 010F78 0008 FFFF pc=3FDCE400 ... <- TX buffer, word 0 +... +``` + +The chip's byte address `B` is main-RAM word `B>>1`; this is the same mapping the +driver already used, now visible per access. The trace below is the whole story. + +## The four cases the test runs, and what it expects + +The test driver is at bit `0x3FDD9400`; its frame builder is at bit `0x3FDDD040` +(Ghidra `FUN_rom_07fbba08`). The key argument is `field` (the *third* stack +argument), which gates the software CRC append: + +``` +0x3FDDDA90: MOVE *A13(FF60h),A7,0 ; A7 = field +0x3FDDDAB0: JREQ 0x3FDDDD00 ; field == 0 -> skip the CRC append +... +0x3FDDDB30: CALLA 0x3FDDBD10 ; field != 0 -> software CRC32, append 4 bytes +``` + +The table the test builds (at `A13+0x60`, stride `0x30`): + +| case | mode | meaning | `field` | expected `RMD1 & 0xFF00` | observed | +|---|---|---|---|---|---| +| 0 | `0x44` | `LOOP|INTL` — chip generates+appends the CRC; receiver doesn't check | 0 | `0x0300` | `0x0300` ✔ | +| 1 | `0x4C` | `LOOP|INTL|DTCR` — firmware appends a **correct** FCS; receiver checks | 1 | `0x0300` | `0x0300` ✔ | +| 2 | `0x4C` | `LOOP|INTL|DTCR` — firmware appends **nothing**; receiver checks | 0 | **`0x6B00`** (CRC **and** FRAM) | `0x4B00` ✘ | +| 3 | `0x54` | `LOOP|INTL|COLL` — forced collision | 0 | `TMD3 & 0x400` (RTRY) | not reached | + +So case 2 is **designed** to fail the CRC (no FCS), and the test expects exactly +that — with a framing bit. Case 3 and the rest of the test never run because case +2's check aborts first (`ERROR RESULT = 0x50`, which the display reads). + +## Evidence: the FCS the firmware *does* build is a correct standard CRC32 + +In case 1 the DMA trace shows the chip reading a **30-byte** buffer ending +`55 79 C6 95`: + +``` +r 00FF24 FFE2 ... TMD2 -> length 30 +r 010F78 0008 / 162B / 5A4B / 0008 / 162B / 5A4B / 0000 / AAAA x6 / 5579 / C695 +``` + +Bytes `79 55 95 C6` = `0xC6955579` little-endian. `zlib.crc32()` over the 26-byte +frame `08 00 2b 16 4b 5a 08 00 2b 16 4b 5a 00 00 AA*12` is **`0xC6955579`**, and +the residue over frame+FCS is `0x2144DF1C`, i.e. `~residue = 0xDEBB20E3` = +`FCS_RESIDUE`. **The firmware's FCS is exactly right, and our DMA delivers it +intact — case 1 passes.** This is the control that rules out "our DMA eats the +last four bytes". + +In case 2 the same frame is sent with `field=0` and length **26**, ending +`AA AA AA AA` — no FCS — and the CRC check fails, as it must. + +## Evidence: the byte↔bit translation is right for the frame too + +The task asked to re-check `o>>1` for the buffer specifically. The trace shows the +chip reading the TX buffer at byte `0x010F78` (TMD0 `0x0F78` + HADR `0x01`) and +the CPU writing it at the same place; every word is the word the CPU wrote. The +same mapping carried the INIT block and the descriptor write-backs exactly +(`w 00FEE2 0300`/`4B00` on the RX descriptor word). No shift, no lane swap. + +## The remaining blocker: `RMD1_FRAM` + +* the test compares `RMD1 & 0xFF00` with `0x6B00` (bit `0x7FBB436` / + `0x3FDDA1B0`, `CMPI 0x6b00,A5`); +* MAME's `am79c90.cpp` sets `RMD1_ERR | RMD1_CRC` (`0x4800`) on a failed FCS and + `RMD1_ENP`; it sets `RMD1_FRAM` (`0x2000`) **nowhere** (`grep` in both 0.289 and + current upstream `master`); +* the Am7990 Technical Manual (p. 1-12, 1-25/1-26) and the Am79C90 datasheet both + say `FRAM is not valid in internal loopback mode`, and FRAM requires *dribbling + bits* plus a CRC error. + +**Diagnostic (runtime only, on the host-side region buffer; the ROM file is +untouched):** changing that one immediate from `0x6B00` to `0x4B00` — i.e. letting +the check pass without the FRAM bit — makes the run continue through case 3 and +return to the diagnostics-**menu idle** `PC=0x3FDCE400` instead of the error-blink +loop `PC=0x3F2088B0`. So the FRAM bit is the *only* thing standing between this +model and a clean `C — LANCE TEST`. + +I did **not** make the equivalent MAME change. A change that sets `RMD1_FRAM` +whenever the DTCR-loopback FCS check fails would contradict the two quoted +sentences above, and there is no datasheet sentence supporting it. It is reported +here for a second reader, not applied. + +## What this means for the standing hypotheses + +| hypothesis | verdict | +|---|---| +| "the firmware appends the FCS and our DMA drops the last four bytes" | **refuted** — the failing frame is built without an FCS on purpose (case 2, `field=0`); the passing `DTCR=1` case proves the DMA carries an FCS when one exists | +| "a four-byte length discrepancy makes the check read the wrong four bytes" | **refuted** — descriptor length and delivered byte count agree exactly, case by case | +| "the descriptor write-back we observe is not MAME's" | **confirmed intact** — `dma_out` writes MAME's `m_rx_md[1]` verbatim | +| remaining mismatch | **`RMD1_FRAM` is never set by MAME** | + +If the real terminal passes `C — LANCE TEST`, the real Am7990/Am79C90 must set +`FRAM` on this CRC error despite the datasheet; if it does not, the test's `0x6B00` +expectation is itself wrong and the terminal reports the same error we do. Either +way the DMA is not the fault. + +## Reproduction + +```sh +cd /home/arti/code/k-space/vt1200 +M=/home/arti/code/k-space/mame0289/vt1200 +MENU_KEYS=C,Z MENU_AT=300 MENU_LAST=3200 \ +VT1200_LANCE_DMA_LOG=/tmp/dma.log \ + $M vt1200 -rompath harness/mame/roms -video none -sound none -nothrottle \ + -debug -debugger none -autoboot_script harness/mame/menu_keys.lua -autoboot_delay 0 +``` + +Baselines are untouched by this work (the new knob is null unless set): `progress.lua` +at 200 frames is still `samples=198` with no env and `samples=53` with +`VT1200_IO_PATH_STUB=1`, and the `VT1200_SKIP_LEVEL1=1` frame-3000 dump still +reproduces `duart2_default_tm_frame3000.png` (AE = 1826, the cursor). diff --git a/harness/mame/README.md b/harness/mame/README.md index 90c21ad..3363aee 100644 --- a/harness/mame/README.md +++ b/harness/mame/README.md @@ -68,6 +68,7 @@ Reports in this directory: | `GATE_CLEANUP_REPORT.md` | promoting the working config to the default; retiring the settled env gates | | `TABLET_REPORT.md` | the VSXXX-AB tablet: the self-test device-code ID, the 5-byte absolute report, and absolute host-pointer input | | `LANCE_REPORT.md` | the LANCE Ethernet model: the register file, the 8-byte address ROM and its checksum, the byte↔bit DMA, and the remaining loopback/CRC blocker | +| `LANCE_LOOPBACK_REPORT.md` | `C — LANCE TEST`: the TX buffer **does** carry a correct FCS in the passing `DTCR=1` case and deliberately carries **none** in the failing one; the residual mismatch is MAME never setting `RMD1_FRAM` | | `RENDER_REPORT.md` | the active-pixel scanline renderer, and the geometry it derived | | `DUART_REPORT.md` | the DUARTs: wiring, the ISR-driven transmit, the datasheet-verified model edits | | `BRINGUP_LOG.md` | the whole bring-up narrative, **including withdrawn claims** — read it sceptically | @@ -225,10 +226,12 @@ LAT_08002B164B5A` (it used to show `00-00-00-00-00-00`). The (no network peer), not a LANCE failure. See `LANCE_REPORT.md`. Not fully working: the firmware's `C - LANCE TEST` (its internal loopback) still -fails at a **CRC** check — the firmware runs loopback with `MODE_DTCR` and MAME's -`receive()` then always mismatches the FCS. That is a MAME-device question, -quoted in `LANCE_REPORT.md` section 5 and **not** edited; the `M - ETHERNET ROM -TEST` passes. +fails, but **not** because our DMA loses the FCS. The test's failing case is a +*negative* test that deliberately sends a frame with **no** FCS and expects a CRC +error; the frame that carries an FCS (the `DTCR=1` passing case) is delivered +intact and passes. The one bit left is `RMD1_FRAM` — the test expects +`RMD1 = 0x6B00`, MAME reports `0x4B00`, and MAME never sets `FRAM`. See +`LANCE_LOOPBACK_REPORT.md`; the `M - ETHERNET ROM TEST` passes. ### Getting past level 1 @@ -259,7 +262,7 @@ Terminal Manager usable. It is a **value-aware inverse switch**: the default lives in the code, not in a wrapper script setting `VT1200_TABLET`, so `VT1200_MOUSE=1` (or `VT1200_MOUSE=0` for the tablet) always flips it either way. -## Environment knobs (20; down from 33) +## Environment knobs (21; down from 33) Almost all are opt-in and off by default — there is no longer a "working incantation" to remember, because everything the firmware needs to run is now the @@ -267,7 +270,9 @@ default. The one exception is that the **tablet is on by default**; `VT1200_MOUS is the switch that restores the faithful mouse (see the divergences above). Instrumentation: `VT1200_DUARTLOG`, `VT1200_LANCELOG` (every LANCE RAP/RDP and -address-ROM access with the PC), `VT1200_BOARD_LOG`, `VT1200_BOARD_MAP`, +address-ROM access with the PC), `VT1200_LANCE_DMA_LOG` (every LANCE bus-master +DMA word access: byte address, value, byte lanes, PC — how the transmit buffer and +the descriptor write-back are read directly), `VT1200_BOARD_LOG`, `VT1200_BOARD_MAP`, `VT1200_IRQLOG`, `VT1200_LOG_UNMAPPED`, `VT1200_DPYLOG` (+`_FRAMES`), `VT1200_DUMP_FRAME` (+`_FRAMES`), `VT1200_DUMP_RAM` (+`_FRAMES`), `VT1200_DUMP_VRAM` (+`_FRAMES`), `VT1200_DUMP_DIR`, `VT1200_NO_RENDER`, diff --git a/harness/mame/vt1200.cpp b/harness/mame/vt1200.cpp index f3e2394..41b25d1 100644 --- a/harness/mame/vt1200.cpp +++ b/harness/mame/vt1200.cpp @@ -96,6 +96,10 @@ VT1200_LANCELOG= every LANCE RAP/RDP access (and the address ROM reads) with the PC, so the OS's programming of the chip is visible. + VT1200_LANCE_DMA_LOG= every LANCE bus-master DMA word access (byte + address, value, byte lanes, PC), so the + transmit buffer and the descriptor write-back + are observed directly rather than inferred. VT1200_IRQLOG= DUART1 IRQ assert: ISR/SRA at the moment of the interrupt. VT1200_LOG_UNMAPPED= catch-all over the whole address space; @@ -755,6 +759,14 @@ private: FILE *m_lancelog = nullptr; void log_lance(char kind, const char *what, u16 value); + // VT1200_LANCE_DMA_LOG=: every bus-master DMA word access (the LANCE's + // own byte address, the value and, for writes, the byte lanes). This is how + // the transmit buffer and the descriptor write-back are observed directly, + // rather than inferred: the chip's byte address B is main-RAM word B>>1, and + // the even/odd byte lane is dma_in/dma_out's own (am79c90.cpp dma_in/dma_out). + FILE *m_lance_dmalog = nullptr; + void log_lance_dma(char kind, u32 addr, u16 value, u16 mask); + // VT1200_IO_PATH_STUB=1: opt-in stand-in for the unknown test fixture on // the keyboard port, so level-0's count-only I/O data path test can pass. // NOT a keyboard model. See iopath_stub_arm() and the README. @@ -945,6 +957,15 @@ void vt1200_state::machine_start() fprintf(m_lancelog, "# kind port val pc cyc t -- every LANCE RAP/RDP/ROM access\n"); } + if (const char *p = getenv("VT1200_LANCE_DMA_LOG")) + { + m_lance_dmalog = fopen(p, "w"); + if (m_lance_dmalog) + fprintf(m_lance_dmalog, + "# kind byteaddr value mask pc cyc t -- every LANCE bus-master DMA word access\n" + "# 'r' = chip read (dma_in), 'w' = chip write (dma_out)\n"); + } + // With the real DUART in place, board-level signals that used to be faked // by the window model come from the DUART's input port (read register 0x0D: // IP0-IP6, bit 7 always set). The monitor-selection switch is IP bits 4-6; @@ -1408,6 +1429,16 @@ void vt1200_state::log_lance(char kind, const char *what, u16 value) fflush(m_lancelog); } +void vt1200_state::log_lance_dma(char kind, u32 addr, u16 value, u16 mask) +{ + if (!m_lance_dmalog) + return; + fprintf(m_lance_dmalog, "%c %06X %04X %04X pc=%08X cyc=%llu t=%.6f\n", + kind, addr, value, mask, (unsigned)m_maincpu->state_int(TMS34010_PC), + (unsigned long long)m_maincpu->total_cycles(), machine().time().as_double()); + fflush(m_lance_dmalog); +} + // Copy one active scanline out of the video RAM framebuffer. // // The TMS34010 screen-refresh address is reconstructed by the device into @@ -1705,12 +1736,19 @@ void vt1200_state::vt1200(machine_config &config) m_lance->intr_out().set_inputline(m_maincpu, 0).invert(); m_lance->dma_in().set([this](offs_t o) -> u16 { const offs_t w = o >> 1; + u16 v; if (w < 0x00200000) // 4 MiB of main RAM = 0x200000 words - return m_mainram[w]; - log_lance('d', "DMA-RANGE", u16(w >> 16)); - return 0x0000; + v = m_mainram[w]; + else + { + log_lance('d', "DMA-RANGE", u16(w >> 16)); + v = 0x0000; + } + log_lance_dma('r', u32(o), v, 0xffff); + return v; }); m_lance->dma_out().set([this](offs_t o, u16 data, u16 mem_mask) { + log_lance_dma('w', u32(o), data, mem_mask); const offs_t w = o >> 1; if (w < 0x00200000) {