Frames and the FCS
You will learn
How an Ethernet frame ends in a CRC-32, and what the check computes.
An Ethernet frame ends in a frame check sequence: a CRC-32 over the bytes, computed by the sender and recomputed by the receiver. The spec of this lesson, written for this course, carries the polynomial and the arithmetic, and its header labels what the check does and does not claim. The recording runs t27c on it.
Try it
In the recording, watch the run; then in the spec frame find the polynomial the CRC-32 uses and what the header labels as an assumption.

t27c test-report on the Ethernet frame check sequence: 8 tests pass natively -- the table derived from one bit step, the check value 0xCBF43926, and the receiver residue 0xDEBB20E3.
specs/fpga/eth_crc.t27
// SPDX-License-Identifier: Apache-2.0
; eth_crc.t27 -- the Ethernet frame check sequence (FCS), as one spec.
; IEEE 802.3 clause 3.2.9: every MAC frame carries a 32-bit CRC over
; dst, src, length/type and payload, computed LSB-first with the
; reflected polynomial 0xEDB88320 (the reflection of 0x04C11DB7),
; register pre-set and post-inverted with 0xFFFFFFFF: the FCS field IS
; the post-inverted register, transmitted LSB first. A receiver that
; runs the same register over the frame INCLUDING its FCS is left at
; the residue 0xDEBB20E3 (zlib view 0x2144DF1C), not at the preset --
; that constant is the receiver's check, and a corrupted frame passes
; it with probability 1 - 2^-32 per the standard's error model.
; ----
; SOURCE (IEEE 802.3-2018):
; 3.2.9 FCS field definition, CRC-32 polynomial, transmission order
; 3.6 minimum frame 64 octets, FCS included
; ORACLE: every hex constant below was computed with a real tool, not
; transcribed by hand: Python 3 zlib.crc32 (the same reflected
; polynomial), run 2026-10-08:
; crc32(b"123456789") = 0xcbf43926 (the published check value)
; crc32(b"a") = 0xe8b7be43
; crc32(b"") = 0x00000000
; frame A crc32 = 0x68a6e702 -- the FCS field is that same value,
; sent LSB first: 02 e7 a6 68
; frame B crc32 = 0x755a0222
; crc32(frame A + fcs_le) = 0x2144df1c, and the running register
; over the same bytes is 0xdebb20e3: the receiver's residue. NOT
; 0xffffffff -- a register that returns to its preset belongs to a
; CRC convention without the final inversion, not to this one.
; The table below is NOT trusted either: test table_is_derived recomputes
; all 256 entries from crc_bit, the same self-check packets.t27 runs on
; the Xilinx CRC table.
; ----
; Course use: trinity buses-and-peripherals module 8 lesson frames-and-crc
; (gHashTag/trinity#1484). Numbers here are algorithm truth and tool
; output only -- no board measurement is claimed.
module fpga_eth_crc;
pub const ETH_CRC_POLY : u32 = 0xEDB88320;
pub const CRC_INIT : u32 = 0xFFFFFFFF;
; crc_buf carries up to 32 octets, frames here need 18
pub const BUF_WORDS : u32 = 32;
; One reflected step: if the low bits of register and data differ, shift and
; fold the polynomial in; else just shift.
pub fn crc_bit(bit : u32, crc : u32) u32 {
const differ = ((bit ^ crc) & 1) == 1;
if (differ) {
return (crc >> 1) ^ ETH_CRC_POLY;
}
return crc >> 1;
}
; Feed the low n bits of x, LSB first -- Ethernet order.
pub fn crc_bits(x : u32, n : u32, prev : u32) u32 {
var crc : u32 = prev;
var i : u32 = 0;
while i < n {
crc = crc_bit(x >> i, crc);
i = i + 1;
}
return crc;
}
; Table form: feeding 8 bits b into c equals (c >> 8) ^ T[(c ^ b) & 0xFF].
; All 256 entries recomputed from crc_bit by test table_is_derived.
pub const ETH_CRC_TABLE : [256]u32 = [
0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3,
0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91,
0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7,
0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5,
0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B,
0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59,
0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F,
0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D,
0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433,
0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01,
0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457,
0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65,
0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB,
0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9,
0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F,
0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD,
0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683,
0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1,
0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7,
0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5,
0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B,
0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79,
0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F,
0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D,
0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, 0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713,
0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21,
0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777,
0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45,
0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB,
0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9,
0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF,
0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D,
];
pub fn crc_octet(b : u32, crc : u32) u32 {
return (crc >> 8) ^ ETH_CRC_TABLE[(crc ^ b) & 0xFF];
}
; CRC over the first len octets of a 32-octet buffer, preset 0xFFFFFFFF.
; The caller pads the tail with zeros; only len octets are folded in.
pub fn crc_buf(data : [32]u8, len : u32) u32 {
var crc : u32 = CRC_INIT;
var i : u32 = 0;
while i < len {
crc = crc_octet(data[i], crc);
i = i + 1;
}
return crc;
}
; The FCS the sender appends: the post-inverted running register
; (802.3 3.2.9), transmitted LSB first. A receiver that runs the same
; register over the frame INCLUDING the FCS is left at the residue
; 0xDEBB20E3 -- that constant, not a return to the preset, is the
; receiver's check; zlib sees the same residue as 0x2144DF1C.
pub fn fcs_of(crc : u32) u32 {
return crc ^ CRC_INIT;
}
test table_is_derived {
var bad : u32 = 0;
var i : u32 = 0;
while i < 256 {
if (ETH_CRC_TABLE[i] != crc_bits(i, 8, 0)) {
bad = bad + 1;
}
i = i + 1;
}
assert bad == 0;
}
; zlib.crc32(b"123456789") -- the published CRC-32 check value. c8 below is
; the running register (0x340bc6d9); the published value is its post-inversion,
; the FCS view, so the assert goes through fcs_of.
test check_value_123456789 {
const c0 = crc_octet(0x31, CRC_INIT);
const c1 = crc_octet(0x32, c0);
const c2 = crc_octet(0x33, c1);
const c3 = crc_octet(0x34, c2);
const c4 = crc_octet(0x35, c3);
const c5 = crc_octet(0x36, c4);
const c6 = crc_octet(0x37, c5);
const c7 = crc_octet(0x38, c6);
const c8 = crc_octet(0x39, c7);
assert fcs_of(c8) == 0xcbf43926;
}
; zlib.crc32(b"a")
test single_octet_a {
assert fcs_of(crc_buf([0x61, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 1)) == 0xe8b7be43;
}
; No octets: preset, then the final inversion -- the CRC of nothing is 0.
; crc_buf of an empty run hands back the preset itself, and fcs_of of that
; is zero, so the assert goes through fcs_of.
test empty_is_zero {
assert fcs_of(crc_buf([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 0)) == 0x00000000;
}
; Frame A: broadcast dst, one src, IPv4 ethertype, first bytes of an IP header.
; zlib.crc32 = 0x68a6e702 (the FCS field, sent LSB first as 02 e7 a6 68);
; the running register = 0x975918fd. The asserts pin both views.
test frame_a_ipv4 {
const crc = crc_buf([
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
0x08, 0x00,
0x45, 0x00, 0x00, 0x1C,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
], 18);
assert crc == 0x975918fd;
assert fcs_of(crc) == 0x68a6e702;
}
; Frame B: the slow-protocols multicast dst an STP/LLDP frame uses, with a
; length field instead of a type. zlib.crc32 = 0x755a0222 (the FCS field
; value), running register = 0x8aa5fddd.
test frame_b_length_field {
const crc = crc_buf([
0x01, 0x80, 0xC2, 0x00, 0x00, 0x00, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x00, 0x26,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
], 14);
assert crc == 0x8aa5fddd;
assert fcs_of(crc) == 0x755a0222;
}
; The FCS is always the complement: every vector above obeys it in one sweep.
; Every vector above, both views, one sweep: the register and the FCS field
; are complements of each other.
test fcs_is_complement {
assert fcs_of(0x975918fd) == 0x68a6e702;
assert fcs_of(0x68a6e702) == 0x975918fd;
assert fcs_of(0x8aa5fddd) == 0x755a0222;
assert fcs_of(0x340bc6d9) == 0xcbf43926;
assert fcs_of(0x00000000) == 0xFFFFFFFF;
}
; The receiver's check: the register over frame A WITH its FCS appended
; (the FCS travels LSB first: 02 e7 a6 68) settles at the residue
; 0xDEBB20E3 -- never at the preset 0xFFFFFFFF, which this CRC's final
; inversion rules out. zlib.crc32(frame_a + fcs_le) = 0x2144df1c, the
; same residue in the post-inverted view; both computed, not recalled.
test receiver_residue_is_the_magic {
const c0 = crc_buf([
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0A, 0x0B,
0x0C, 0x0D, 0x0E, 0x0F, 0x08, 0x00, 0x45, 0x00,
0x00, 0x1C, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
], 18);
const r = crc_buf([
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0A, 0x0B,
0x0C, 0x0D, 0x0E, 0x0F, 0x08, 0x00, 0x45, 0x00,
0x00, 0x1C, 0x02, 0xE7, 0xA6, 0x68, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
], 22);
assert c0 == 0x975918fd;
assert r == 0xDEBB20E3;
}
invariant fcs_complement_matches_crc {
fcs_of(0x68a6e702) == 0x68a6e702 ^ CRC_INIT
}
All lessons
Module 1 · What a bus is
Why a bus exists at all: a conversation on wires, framed and addressed, and who is allowed to talk.
Module 2 · UART
The two-wire bus with no clock: the frame, the divisor that sets the rate, and the status a driver polls.
Module 3 · SPI
The clocked conversation: four modes, a prescaler ladder, and a chip select per servant.
Module 4 · APB
The register bus: PSEL and PENABLE, strobes and wait states, and how many address bits a peripheral count costs.
Module 5 · AXI4
The five channels: address, data and response in both directions, lite or full, bursts and IDs.
Module 6 · Memory
What sits on the far side of every bus: memory maps, port kinds, and latency that a wait state must cover.
Module 7 · Bridges
Why designs grow more than one bus, and the packet bridge that moves work between them.
Module 8 · Ethernet
Frames, the frame check sequence, RGMII timing, and the pre-registered steps of a real bring-up.
Module 9 · The bench
The discipline that guards real hardware: who holds the IO, taking and giving the lock, and what runs next.