# LANCE loopback FCS: is MAME's `MODE_DTCR` term inverted? Date: 2026-10-16. Investigation only. ROM `vt1200_v22.bin` md5 `b46a3e442ccb86134d2356bd70d22fd2` (untouched). Nothing in the MAME tree or in `harness/mame/` was changed. Datasheets downloaded to `reference/lance-datasheet/` (originals + `pdftotext` extractions). ## Verdict (short) **MAME is right. The condition is NOT inverted.** `src/devices/machine/am79c90.cpp:263` ```c if (!(m_mode & MODE_LOOP) || (m_mode & MODE_DTCR)) ``` is a byte-for-byte implementation of what both the Am7990 and the Am79C90 datasheets say. The reason it "reads" as inverted is that the CRC generator and the CRC checker are **one shared circuit**, so the receiver can only check the CRC when the transmitter is **not** using the generator — i.e. exactly when `DTCR = 1`. The proposed "fix" (`(m_mode & MODE_DTCR)` → `!(m_mode & MODE_DTCR)`) would be wrong in **both** loopback cases. The `LANCE_REPORT.md` §5 diagnosis is a misreading of the hardware. See [Consequence for the VT1200](#consequence-for-the-vt1200) below. --- ## The two parts, and whether they differ | part | document used | loopback-CRC rule | |---|---|---| | **Am7990** LANCE | Am7990 Technical Manual, Aug 1986 | "no CRC check ... since the CRC logic is shared"; with DTCR=1 the receiver checks | | **Am79C90** C-LANCE | Am79C90 datasheet | identical wording | They do **not** differ on this question. They differ in one other place only: the C-LANCE adds the `ADD_FCS` bit (TMD1 bit 13) that can override `DTCR=1` on a per-packet basis ("The ADD_FCS bit (bit 13, TMD1) can be used to override a DTCR=1 setting on a per packet basis."). MAME already gates that on `type() == AM79C90` (`transmit()`). The unsettled Am7990-vs-Am79C90 question does **not** affect the loopback-FCS verdict. (Neither part is settled for the VT1200; that is out of scope here.) --- ## The decisive quotes ### Am7990 Technical Manual (1986), Mode register, printed p. 1-24 (PDF p. 31) > **03 DTCR** DISABLE TRANSMIT CRC. When DTCR = 0, the transmitter generates and > appends a CRC to the transmitted packet. When DTCR = 1, the CRC logic is > allocated to the receiver and no CRC is generated or sent with the transmitted > packet. > > During loopback, DTCR = O will cause a CRC to be generated on the transmitted > packet, but **no CRC check is done by the receiver since the CRC logic is shared > and cannot generate and check CRC at the same time.** The generated CRC is > written into memory with the data and can be checked by the host software. > > **If DTCR = 1 during loopback, the host software must append a CRC value to the > transmit data. The receiver checks the CRC on the received data and reports any > errors.** Since the CRC generator is used to generate the hash filter, the > multicast addressing cannot be used when DTCR = 1. > **02 LOOP** LOOPBACK allows the LANCE to operate in full duplex mode for test > purposes. The packet size is 8-32 bytes. The received packet can be up to 36 > bytes (32 + 4 bytes CRC) **when DTCR=O**. During loopback, the runt packet filter > is disabled because the packet is smaller than the minimum size Ethernet packet > (64 bytes). ### Am7990 Technical Manual (1986), §1.8.3 "CYCLIC REDUNDANCY CHECK (CRC)", printed p. 1-12 (PDF p. 19) > The LANCE requirements for the CRC logic are: > > 1. **TRANSMISSION - Mode bit 2 (LOOP) = 0, Mode bit 3 (DTCR) = 0.** The LANCE > calculates the CRC from the first bit following the Start bit to the last bit > of the data field. The CRC value inverted is appended to the transmission in > one unbroken bit stream. > 2. **RECEPTION - Mode bit 2 (LOOP) = 0.** The LANCE performs a check on the input > bit stream from the first bit following the Start bit to the last bit in the > frame. The LANCE continually samples the state of the CRC check on framed byte > boundaries and, when the incoming bit stream stops, the last sample determines > the state of the CRC error. Framing error (FRAM) is not reported if there is > no CRC error. > 3. **LOOPBACK - Mode bit 2 (LOOP) = 1, Mode bit 3 (DTRC) = 0.** The LANCE generates > and appends the CRC value to the outgoing bit stream as in transmission but > **does not perform the CRC check of the incoming bit stream.** > 4. **LOOPBACK - Mode bit 2 (LOOP) = 1, Mode bit 3 (DTRC) = 1.** The LANCE > **performs the CRC check on the incoming bit stream as in reception** but does > not generate or append the CRC value to the outgoing bit stream during > transmission. (Note the OCR/typo `DTRC` for `DTCR` in bullets 3–4; the bit is DTCR.) And, from the LOOPBACK constraints (§1.8.3, item 4): > CRC may be either generated and appended to the output serial bit stream or may > be checked on the input serial bit stream **but not both in the same > transaction.** ### Am7990 Technical Manual (1986), §4.3.3 "CRC GENERATOR/CHECKER LOGIC TEST", printed p. 4-4 (PDF p. 51) > During the loopback test (internal or external), the CRC logic may be tested. > Since the LANCE operates in half-duplex mode, the CRC generator or checker must > be tested separately as follows. The Transmit CRC Generator is tested by > programming DTCR=O in the Mode Register. This enables the transmit CRC > generation and **disables the Receive CRC Checker.** ... > > The Receive CRC Checker is tested by setting the DTCR bit in the Mode Register. > The user then calculates the CRC for the test packet **and appends it to the end > of the test packet for transmission** (test packet max size= 32 + 4 bytes CRC). > The Receive CRC Checker checks the last four bytes of the packet for CRC error > and updates the receive descriptors accordingly. ### Am79C90 C-LANCE datasheet, Mode register bit 03 DTCR, printed p. 24 (PDF p. 24) > **03 DTCR** DISABLE TRANSMIT CRC. When DTCR = 0, the transmitter will generate > and append a CRC to the transmitted packet. When DTCR = 1, the CRC logic is > allocated to the receiver and no CRC is generated and sent with the transmitted > packet. The ADD_FCS bit (bit 13, TMD1) can be used to override a DTCR=1 setting > on a per packet basis. > > During loopback, DTCR = 0 will cause a CRC to be generated on the transmitted > packet, but **no CRC check will be done by the receiver since the CRC logic is > shared and cannot generate and check CRC at the same time.** The generated CRC > will be written into memory with the data and can be checked by the host > software. > > **If DTCR = 1 during loopback, the host software must append a CRC value to the > transmit data. The receiver will check the CRC on the received data and report > any errors.** ### Am79C90 C-LANCE datasheet, "Cyclic Redundancy Check (CRC)", printed p. 35 > 2. **RECEPTION – MODE <02> LOOP = 0.** The C-LANCE performs a check on the input > bit stream from the first bit following the SFD to the last bit in the frame. ... > 3. **LOOPBACK – MODE <02> LOOP =1, MODE <03> DTRC = 0.** The C-LANCE generates and > appends the CRC value to the outgoing bit stream as in Transmission but **does > not perform the CRC check of the incoming bit stream.** > 4. **LOOPBACK – MODE <02> LOOP = 1 MODE <03> DTRC = 1.** C-LANCE **performs the CRC > check on the incoming bit stream as in Reception**, but does not generate or > append the CRC value to the outgoing bit stream during transmission. > CRC may not be used for both transmission and reception simultaneously. --- ## Question 2 — does the receiver's CRC check apply to a frame that carries no FCS? **The check is tied to the MODE bits, not to whether the frame's bytes happen to look like they end in an FCS.** The hardware has one shared CRC circuit, so the rule is purely `(LOOP, DTCR)`: | LOOP | DTCR | chip checks RX FCS? | which datasheet bullet | |---|---|---|---| | 0 | 0 | **yes** | 2 (RECEPTION, `LOOP=0`) | | 0 | 1 | **yes** | 2 (RECEPTION, `LOOP=0`; DTCR only affects the transmitter) | | 1 | 0 | **no** | 3 (LOOPBACK, generator owns the shared circuit) | | 1 | 1 | **yes** | 4 (LOOPBACK, checker owns the shared circuit) | In the one loopback case where the chip checks (`LOOP=1, DTCR=1`) the datasheet **requires the host to put the FCS there** ("the host software must append a CRC value to the transmit data"). So the chip is not "checking a frame with no FCS" by design — it is checking a frame the host was required to FCS-terminate. If the host breaks that contract, the chip reports a CRC error, correctly. MAME's condition reproduces the table exactly: | LOOP | DTCR | MAME `!(LOOP) \|\| DTCR` | chip (per datasheet) | |---|---|---|---| | 0 | 0 | check | check | | 0 | 1 | check | check | | 1 | 0 | skip | skip | | 1 | 1 | check | check | MAME's FCS arithmetic was also checked independently. `FCS_RESIDUE = 0xdebb20e3` (`am79c90.h:50`) and `crc32_creator::simple()` is zlib's `crc32()` (init `0xffffffff`, final complement). Over a frame `M || FCS` with a correct little-endian FCS, zlib returns `0x2144df1c`, and `~0x2144df1c == 0xdebb20e3`. So `~crc != FCS_RESIDUE` **passes a correctly-terminated frame** and fails one that is not. There is nothing wrong with the arithmetic either. --- ## Question 3 — upstream MAME history The shallow 0.289 checkout has no file history, so the history was read from the upstream GitHub repository (`mamedev/mame`). * Condition **introduced** in `3349e3e990ab` (2018-11-22, *"am79c90: fixes (nw)"*): `!(LOOP) || !(INTL) || DTCR`. * **Changed** in `67f9c842e9f2` (2018-11-25, *"am79c90: many fixes (nw)"*), whose message says **"corrected internal loopback behaviour"**, to the current `!(LOOP) || DTCR`. The diff drops the `!(INTL)` term: ```diff - if (!(m_mode & MODE_LOOP) || !(m_mode & MODE_INTL) || (m_mode & MODE_DTCR)) + if (!(m_mode & MODE_LOOP) || (m_mode & MODE_DTCR)) ``` Dropping `!(INTL)` makes external loopback follow the same rule as internal loopback — which is what the datasheet bullets 3/4 describe (they are written for `LOOP`, not `INTL`). So the change moved MAME **toward** the datasheet, not away from it. The commit message does not quote a datasheet, but the resulting term matches both datasheets exactly, so no further history is needed to accept it. --- ## Question 4 — verdict **MAME is right; the check is not inverted.** No change is warranted. The change proposed in `LANCE_REPORT.md` §5 — ```c // PROPOSED, DO NOT APPLY if (!(m_mode & MODE_LOOP) || !(m_mode & MODE_DTCR)) ``` — would be wrong in both loopback cases: it would **skip** the check for `LOOP=1, DTCR=1` (datasheet bullet 4: the chip checks) and **perform** it for `LOOP=1, DTCR=0` (datasheet bullet 3: the chip does not check). **Revert note (if anyone applies that change anyway):** it contradicts the four `MODE`-bit CRC requirements quoted above from the Am7990 Technical Manual p. 1-12 and the Am79C90 datasheet p. 35, and it would make MAME's receiver diverge from the part in both loopback modes. Revert to `!(m_mode & MODE_LOOP) || (m_mode & MODE_DTCR)` and keep the citation. --- ## Question 6 — when is the check skipped in normal (non-loopback) receive? **Never, on this chip.** Datasheet bullet 2 is `RECEPTION - Mode bit 2 (LOOP) = 0` with **no DTCR term**: outside loopback the receiver always runs the FCS check, and `DTCR` speaks only about the *transmitter*. The FCS's presence in a normal frame is the remote sender's business; the local `DTCR` bit says nothing about it. So `!(m_mode & MODE_LOOP)` (check whenever `LOOP=0`) is the correct half of the condition, and it must not be tightened to `!(LOOP) && !(DTCR)` or similar. --- ## Consequence for the VT1200 `harness/mame/LANCE_REPORT.md` §4.1 records the firmware's own `C - LANCE TEST` init block as: ``` mode = 0x004C = MODE_LOOP | MODE_INTL | MODE_DTCR ``` Per the datasheet, `LOOP=1, DTCR=1` is **bullet 4**: the receiver **does** check the FCS, and the host is **required** to append it. MAME therefore behaves like the part when it reports `RCV DESCR 1 = 0x4B00` (`ERR|CRC|STP|ENP`). The §5 inference — "the real chip must not fail the receive CRC ... i.e. the check should be skipped whenever the frame carries no FCS" — does **not** follow from the hardware; it assumes the frame carries no FCS, which is exactly the contract the datasheet forbids when `DTCR=1`. So the open question is really: **does the firmware's test frame carry a 4-byte FCS or not?** * If it does **not**, the real LANCE flags a CRC error too, the `C - LANCE TEST` fails on real hardware, and MAME is faithful (the failure is the test's, and a level-1/self-test failure is non-fatal, per `LEVEL1_CONTINUE_REPORT.md`). * If it **does**, then MAME's FCS arithmetic (verified above) should pass it, and any mismatch is a **different** MAME bug — not this condition — worth chasing separately. `LANCE_REPORT.md` §5 assumed the first bullet ("DTCR means no FCS was appended") without checking whether the *firmware* appends one; the datasheet says the firmware is obliged to. That is the gap, and it is the thing to observe on the real machine. ### What the real machine would have to show to settle it A boot does **not** settle it: a level-1/self-test failure is reported and the boot continues (`LEVEL1_CONTINUE_REPORT.md`, and `README.md` "How far the OS gets"), so "the terminal boots" proves nothing about the LANCE test. To demonstrate it, read the **LANCE test's own result**, not the boot outcome: 1. **Run the terminal's on-board diagnostics and read the specific LANCE result** (the `C - LANCE TEST` / "Ethernet port (LANCE) test", error 24). A displayed **pass** means the loopback frame the chip received was FCS-terminated (the host appended it, as the datasheet requires) — which is consistent with the datasheet and with MAME being right, and would mean MAME's failure is a different bug. A displayed **CRC error** means MAME is not only right about the rule, it is faithful about this very test. 2. **Stronger and independent of the firmware's verdict:** capture what the chip actually receives. Dump the transmit buffer the test builds (or watch the RX descriptor / bus on a real machine) and check for **4 trailing FCS bytes**. If present, the chip checks a terminated frame; if absent, the chip flags CRC. Note: the task's premise "if it passes ... then the real chip does not reject a loopback frame that carries no FCS" is backwards for this part. The datasheet says the chip **does** check when `DTCR=1`; a real-hardware *pass* would therefore prove the frame **carries** an FCS, not that the check is disabled. --- ## Assets Downloaded originals and extractions in `reference/lance-datasheet/` — see that directory's `README.md` for provenance and SHA-256 checksums.