Add the Am7990 Technical Manual and Am79C90 datasheet to reference/lance-datasheet/ and a report, reference/lance-loopback-fcs.md, that answers whether MAME's am79c90 receive() FCS condition (!(MODE_LOOP) || MODE_DTCR) is inverted. Verdict: MAME is right. The CRC generator and checker share one circuit, so with LOOP=1 the receiver can only check the FCS when DTCR=1 -- exactly what MAME does. The proposed inversion would be wrong in both loopback cases. No MAME change made.
14 KiB
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
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 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:
- 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.
- 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.
- 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.
- 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
- 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. ...
- 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.
- 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:- 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 forLOOP, notINTL). 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 —
// 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 TESTfails on real hardware, and MAME is faithful (the failure is the test's, and a level-1/self-test failure is non-fatal, perLEVEL1_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:
- 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. - 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.