Ethernet · Module 6
CRC-32 Generation
The arithmetic is a shift register performing polynomial division. The four conventions wrapped around it — initial value, input reflection, output reflection, final complement — change no guarantee and every value, which is where interoperability fails.
Chapter 6.1 specified what the check must achieve and deliberately touched none of the mathematics. It named the polynomial, counted its terms to disprove the odd-weight folklore, and stopped.
This chapter builds the mechanism, and the mechanism is smaller than what surrounds it.
The arithmetic is one shift register and a handful of exclusive-ors. It is polynomial division, it is thirty lines of RTL, and once seen it is not difficult. Two independent engineers implementing it from the polynomial alone will both be correct and will produce different values for the same frame.
That is not a paradox and it is not carelessness. There are four decisions the polynomial does not make, each of them arbitrary in the sense that either choice yields a working error detector, and each of them chosen differently by different standards. Get one wrong and every frame you send is rejected by every peer, while your own loopback passes perfectly.
Chapter 5.8 §7 already described the symptom without knowing the cause: a dense difference pattern, roughly half the bits set, appearing at 100% against one particular peer and 0% against every other. That is a convention mismatch, and this chapter is where it gets a name and a diagnosis.
1. Scope — Where This Sits in Module 6
Chapter 6.1 owns the guarantees — what the code detects with certainty, what it detects with probability 1 − 2⁻³², and why the error model has to be stated before either sentence means anything. Nothing in this chapter changes any of it.
This chapter owns generation: the division, the shift register that performs it, the four conventions layered on top, why each convention exists, and how to identify which one two implementations disagree about.
Chapter 6.3 owns checking, and specifically the residue property that lets a receiver test for a constant instead of comparing values. This chapter produces a value; the next chapter shows why a receiver need not compare it.
Chapter 6.4 owns the transformation from one bit per clock to eight or sixty-four, which is an engineering problem with an exact answer and no new mathematics.
Chapter 5.8 owns the covered range — which octets enter the computation, and where the append and check points sit in a datapath. This chapter assumes that range and computes over it.
The question this chapter answers that its neighbours do not: given the polynomial, what else must two implementations agree on before they interoperate — and when they do not, how do you tell which agreement is missing?
2. The Register Is a Remainder
The whole arithmetic is one sentence: treat the message as a polynomial over the field with two elements, divide it by the generator, and keep the remainder.
Two facts make that implementable in almost no hardware.
Addition and subtraction are the same operation, and both are exclusive-or. In a field with two elements there are no carries, so the borrow chain that makes ordinary long division awkward does not exist. Every step of the division is a conditional exclusive-or.
And division can be done incrementally, one bit at a time. The classical schoolbook procedure — align the divisor under the leading term, subtract, shift, repeat — maps exactly onto a shift register: the register holds the current partial remainder, the top bit says whether the divisor "fits", and the exclusive-or with the tapped positions performs the subtraction.
So the register is not an accumulator and not a hash state. It is a remainder, and it is a valid remainder at every clock, not only at the end. That is worth holding onto: it is what Chapter 6.3's residue property is built on, and it is why a partial computation over a partial frame is a meaningful quantity rather than garbage.
The taps are the polynomial, read off directly:
x^32 + x^26 + x^23 + x^22 + x^16 + x^12 + x^11 + x^10
+ x^8 + x^7 + x^5 + x^4 + x^2 + x + 1which is 0x04C11DB7 with the x^32 term implicit — implicit because it is the bit that shifts out and drives the feedback, so it never needs storing.
3. RTL 1 — The Division Itself, With No Conventions On It
// SYNTHESIZABLE.
//
// Polynomial division, one message bit per clock. This is the entire
// mathematics of CRC-32 and it is thirty lines.
//
// It is NOT an Ethernet FCS engine. Four conventions are missing, and a
// design that stops here interoperates with nothing -- while passing
// every test that compares it against itself.
package crc32_pkg;
// NORMATIVE. The generator, x^32 implicit: it is the bit that shifts
// out and drives the feedback, so it is never stored.
localparam logic [31:0] POLY_MSB = 32'h04C1_1DB7;
// The same polynomial with its bit order reversed. NOT a different
// polynomial -- Section 11 makes that precise. It is what an LSB-first
// shift register needs, because reversing the shift direction reverses
// which end the taps must be read from.
localparam logic [31:0] POLY_LSB = 32'hEDB8_8320;
// NORMATIVE, and all four are conventions rather than mathematics.
localparam logic [31:0] INIT_VALUE = 32'hFFFF_FFFF;
localparam bit REFLECT_IN = 1'b1;
localparam bit REFLECT_OUT = 1'b1;
localparam logic [31:0] XOR_OUT = 32'hFFFF_FFFF;
// The published check value for the nine ASCII characters "123456789"
// under all four Ethernet conventions. Section 7 uses it as a self-test.
localparam logic [31:0] CHECK_123456789 = 32'hCBF4_3926;
function automatic logic [31:0] reflect32(input logic [31:0] v);
for (int i = 0; i < 32; i++) reflect32[i] = v[31-i];
endfunction
function automatic logic [7:0] reflect8(input logic [7:0] v);
for (int i = 0; i < 8; i++) reflect8[i] = v[7-i];
endfunction
endpackage
module crc32_serial
import crc32_pkg::*;
(
input logic clk,
input logic rst_n,
// Load an arbitrary starting value. Exposed rather than hard-wired,
// because WHICH value is a convention and this module holds none.
input logic preset,
input logic [31:0] preset_value,
input logic bit_valid,
input logic bit_in,
// The raw register. Not the FCS: no reflection, no complement.
output logic [31:0] crc_reg
);
// The feedback bit is the message bit exclusive-ored with the bit about
// to leave the register. When it is set, the divisor "fits" and is
// subtracted -- and subtraction here is exclusive-or, because there are
// no carries in this field.
wire feedback = bit_in ^ crc_reg[31];
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
crc_reg <= '0;
end else if (preset) begin
crc_reg <= preset_value;
end else if (bit_valid) begin
// Shift left by one, and conditionally subtract the generator.
// Every tap position is a term of the polynomial; there is no
// arithmetic here beyond that.
crc_reg <= {crc_reg[30:0], 1'b0} ^ (feedback ? POLY_MSB : 32'h0);
end
end
endmoduleClassification: synthesizable.
What it teaches: that crc_reg is a remainder at every clock, not only at the end. It is the remainder of everything fed in so far, divided by the generator. That is why the module has a preset port rather than a hard-wired reset value — a remainder can be started from anywhere, and where it is started is a choice rather than a consequence of the mathematics.
Deliberately simplified: one bit per clock, and no conventions. Chapter 6.4 turns the first into eight or sixty-four; Section 5 supplies the second.
Production implication: the preset and preset_value ports are what make this module reusable and are exactly what a design tends to remove. Hard-wiring 32'hFFFFFFFF into the reset branch saves two ports and a mux, and it fuses a convention into the arithmetic — after which the module cannot be used for any other CRC-32 variant, cannot be tested against a zero-initialised reference, and cannot express Section 9's diagnostic, which works by recomputing under a different initial value.
4. Four Decisions the Polynomial Does Not Make
Read the bottom row as the chapter's central claim, because it is what makes these worth a chapter rather than a footnote.
The guarantees are unchanged by all four. Every statement Chapter 6.1 made — bursts to 32 bits, weight three at standard frame sizes, weight four below about 375 octets, 2⁻³² for everything else — depends only on the code, which is the set of valid message-plus-remainder pairs. Presetting the register, reversing bit order, or complementing the result are affine transformations: they relabel which value corresponds to which message without changing which pairs of messages are confusable. A wrong choice does not weaken detection at all.
The value is changed by all four, and that is the entire failure mode. Two implementations differing in any one of them compute different sixteen-thousand-bit-message remainders and reject each other's frames completely.
Which produces an unusually clean failure signature. A convention mismatch is not intermittent, not load-dependent, and not affected by cable quality. It is 100% against the peers that differ and 0% against the peers that agree — the pattern Chapter 5.8 §7 identified as "structural rather than physical" without being able to say what the structure was.
5. RTL 2 — The Conventions as a Wrapper
// SYNTHESIZABLE.
//
// The four conventions, applied around the division of Section 3.
//
// Order matters and is fixed:
//
// 1. preset the register to INIT
// 2. reflect each input octet, if REFLECT_IN
// 3. divide (the only mathematics)
// 4. reflect the 32-bit result, if REFLECT_OUT
// 5. exclusive-or with XOR_OUT
//
// Each is a parameter rather than a constant, so that ONE engine can be
// instantiated as the design's own and again as a differently-configured
// reference -- which is what Section 9 needs and what a hard-coded engine
// makes impossible.
module crc32_conventional
import crc32_pkg::*;
#(
parameter logic [31:0] INIT = INIT_VALUE,
parameter bit REF_IN = REFLECT_IN,
parameter bit REF_OUT = REFLECT_OUT,
parameter logic [31:0] FINAL_XOR = XOR_OUT
) (
input logic clk,
input logic rst_n,
input logic frame_start,
input logic oct_valid,
input logic [7:0] oct_data,
input logic oct_last,
output logic fcs_valid,
output logic [31:0] fcs,
// The raw register, exposed for Section 12's discussion of why it must
// not be asserted on.
output logic [31:0] raw_reg
);
logic [31:0] reg_q;
logic [2:0] bit_q;
logic busy_q;
logic [7:0] oct_q;
assign raw_reg = reg_q;
// Convention 2. Reflection is applied to the OCTET, not to the frame:
// each octet is fed least-significant-bit first, which is the order the
// bits go on the wire (Chapter 5.3's I/G bit is first for this reason).
wire [7:0] oct_applied = REF_IN ? reflect8(oct_data) : oct_data;
wire feedback = oct_q[7] ^ reg_q[31];
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
reg_q <= INIT;
bit_q <= '0;
busy_q <= 1'b0;
oct_q <= '0;
fcs_valid <= 1'b0;
fcs <= '0;
end else begin
fcs_valid <= 1'b0;
if (frame_start) begin
// Convention 1. The register starts at INIT, not at zero, and
// Section 6 shows what that buys.
reg_q <= INIT;
busy_q <= 1'b0;
end else if (oct_valid && !busy_q) begin
oct_q <= oct_applied;
bit_q <= '0;
busy_q <= 1'b1;
end else if (busy_q) begin
// The division, eight bits per octet.
reg_q <= {reg_q[30:0], 1'b0} ^ (feedback ? POLY_MSB : 32'h0);
oct_q <= {oct_q[6:0], 1'b0};
if (bit_q == 3'd7) begin
busy_q <= 1'b0;
if (oct_last) begin
// Conventions 3 and 4, in that order. Swapping them gives a
// different value: reflecting a complemented word is not the
// same as complementing a reflected one unless the mask is
// symmetric -- which 0xFFFFFFFF happens to be, so Ethernet
// survives the swap and other variants do not.
automatic logic [31:0] div_result =
{reg_q[30:0], 1'b0} ^ (feedback ? POLY_MSB : 32'h0);
automatic logic [31:0] reflected =
REF_OUT ? reflect32(div_result) : div_result;
fcs <= reflected ^ FINAL_XOR;
fcs_valid <= 1'b1;
end
end else begin
bit_q <= bit_q + 1'b1;
end
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that the reflection is per-octet on the way in and per-word on the way out, and the asymmetry is not arbitrary. Input reflection exists because Ethernet transmits each octet least-significant-bit first — the same fact that puts Chapter 5.3's individual/group flag first on the wire. Reflecting the input makes the engine consume bits in wire order. Output reflection is a separate decision about how the resulting 32-bit value is laid into the four FCS octets.
Deliberately simplified: eight clocks per octet. It is the shape that makes the conventions visible; Chapter 6.4 collapses it.
Production implication: the comment about ordering conventions 3 and 4 is a real trap that Ethernet happens to survive. Complementing then reflecting differs from reflecting then complementing unless the mask is bit-symmetric, and 0xFFFFFFFF is. So an implementation that swaps them is wrong in general, correct for Ethernet, and will fail the first time somebody parameterises it for a variant with a different final mask. A bug that a specific constant conceals is worse than one that shows, because it will be discovered by whoever reuses the block, not by whoever wrote it.
6. Why Each Convention Exists
Three of the four have concrete justifications and one is pure wire order. Working through them is what turns "arbitrary choices" into decisions you can reason about.
The initial value defends against leading zeros.
A register starting at zero stays at zero while zero bits are fed in — the remainder of 0 divided by anything is 0. So with a zero initial value, a message and the same message with any number of zero octets in front produce the same check value, and prefixing a frame with zeros is undetectable.
Illustrative, computed with the reflections and complement switched off so the effect is isolated:
| Message | Check value (init = 0) |
|---|---|
abc | 0x2C17398C |
00 00 00 abc | 0x2C17398C |
Identical. A non-zero initial value breaks the symmetry, because the register is now somewhere the zeros have to move it away from.
The final complement defends against trailing zeros, and the demonstration is the sharper of the two.
A valid codeword — message followed by its own check value — divides the generator exactly, so a receiver running the check over both gets a remainder that identifies a good frame. Appending zero octets to a codeword leaves it divisible. So without a final complement, a frame with trailing zeros added still passes.
Illustrative, again with the other conventions off:
| Frame | Residue, no complement | Residue, Ethernet conventions |
|---|---|---|
| message + FCS | 0x00000000 — passes | 0xDEBB20E3 — passes |
| message + FCS + one zero octet | 0x00000000 — still passes | 0x39DD08E2 — detected |
| message + FCS + two zero octets | 0x00000000 — still passes | 0x4E3D5E5C — detected |
Read the middle column. Without the complement, appended zeros are invisible at any length — the residue does not move at all. With Ethernet's conventions, one appended octet is caught.
That matters more than it looks, because a frame gaining trailing zeros is not an exotic corruption: it is what a receiver produces if it fails to stop at the right octet, and it is what a channel produces when a driver goes idle mid-frame. The complement converts a whole class of length errors from invisible to detected.
The two reflections are wire order, and have no protective role at all. Ethernet transmits each octet least-significant-bit first, so an engine that consumes bits in the order they arrive must reflect. The output reflection is the matching decision on the way out, so that the four FCS octets go onto the wire in the order a receiver expects to consume them.
7. RTL 3 — The Golden Vector, Built In
// SYNTHESIZABLE SELF-TEST.
//
// Runs a fixed nine-octet message through the design's OWN engine and
// compares against the published check value.
//
// Why a built-in self-test rather than a simulation-only check: the
// conventions are configuration, and configuration survives into
// silicon. A parameter overridden in an integration wrapper, a variant
// instantiated by mistake, a synthesis constant that did not propagate --
// all of these produce a working CRC engine computing the wrong value,
// and none of them is visible until a peer rejects every frame.
//
// This runs once at reset and answers the question in nine octets.
module crc32_golden_selftest
import crc32_pkg::*;
(
input logic clk,
input logic rst_n,
input logic start,
output logic drive_valid,
output logic [7:0] drive_data,
output logic drive_last,
output logic drive_start,
input logic fcs_valid,
input logic [31:0] fcs,
output logic selftest_done,
output logic selftest_pass,
// Kept on failure: the value the engine actually produced, which
// Section 9 turns into a diagnosis rather than a bare failure.
output logic [31:0] observed,
output logic [31:0] expected
);
// "123456789" -- the check message used by every published CRC
// catalogue, so the expected value can be looked up rather than
// derived, which is the point of a golden vector.
localparam int unsigned N = 9;
localparam logic [7:0] MSG [N] = '{
8'h31, 8'h32, 8'h33, 8'h34, 8'h35, 8'h36, 8'h37, 8'h38, 8'h39
};
typedef enum logic [1:0] { T_IDLE, T_START, T_DRIVE, T_WAIT } t_e;
t_e state_q;
logic [3:0] idx_q;
assign expected = CHECK_123456789;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state_q <= T_IDLE;
idx_q <= '0;
drive_valid <= 1'b0;
drive_start <= 1'b0;
drive_last <= 1'b0;
drive_data <= '0;
selftest_done <= 1'b0;
selftest_pass <= 1'b0;
observed <= '0;
end else begin
drive_valid <= 1'b0;
drive_start <= 1'b0;
drive_last <= 1'b0;
case (state_q)
T_IDLE: if (start) begin
state_q <= T_START;
idx_q <= '0;
selftest_done <= 1'b0;
end
T_START: begin
drive_start <= 1'b1;
state_q <= T_DRIVE;
end
T_DRIVE: begin
drive_valid <= 1'b1;
drive_data <= MSG[idx_q];
drive_last <= (idx_q == 4'(N-1));
if (idx_q == 4'(N-1)) state_q <= T_WAIT;
else idx_q <= idx_q + 1'b1;
end
T_WAIT: if (fcs_valid) begin
observed <= fcs;
selftest_pass <= (fcs == CHECK_123456789);
selftest_done <= 1'b1;
state_q <= T_IDLE;
end
default: state_q <= T_IDLE;
endcase
end
end
endmoduleClassification: synthesizable self-test.
What it teaches: that one published value is a stronger check than four separate configuration comparisons. A design could instead read back its four parameters and compare them against expected constants — and that verifies the parameters, not the datapath. The golden vector verifies what the hardware actually computes, so it also catches a reflection function with an off-by-one, a tap that got optimised away, and a reset that does not reach the register.
Deliberately simplified: it drives the engine through the same ports the datapath uses. A production version usually shares the engine with the receive path and needs an arbitration story, which is why the self-test is normally run once at reset rather than continuously.
Production implication: observed is retained on failure and that is the whole difference between a self-test and a diagnosis. A pass/fail bit says the conventions are wrong; the observed value says which one, because Section 9 can exclusive-or it against the expected value and read the answer off the difference. A self-test that reports only a boolean has thrown away the evidence it was uniquely positioned to collect.
8. Reading a Mismatch
The top row is the most useful result in this chapter, because it needs one frame and no recomputation.
If two engines differ only in the final complement, their values differ by exactly 0xFFFFFFFF — every time, for every message, at every length. The complement is an exclusive-or with a constant applied after everything else, so the difference between "applied" and "not applied" is that constant and nothing else.
Illustrative, computed over 123456789:
| Difference | Delta from Ethernet's value | Bits set | Varies with message? |
|---|---|---|---|
| no final complement | 0xFFFFFFFF | 32 | no |
| initial value = 0 | 0x19F6EB51 | 18 | yes |
| no output reflection | 0xAF6816F5 | 18 | yes |
| no input reflection | 0xD36CA819 | 15 | yes |
Read the last column. Only the complement produces a constant delta. Everything else produces a delta that changes with the message, which is why a single captured frame identifies the complement outright and the other three need a recomputation.
And the initial-value case has its own signature: the delta changes with message length. Feeding the same content at different lengths and watching the delta move is a positive identification, because a reflection difference depends on the content and not on how much of it there is.
Two of the four are recoverable by transforming a value; two are not. An output-reflection difference can be undone by bit-reversing one side, and a complement difference by inverting it. An input-reflection difference cannot be undone at all — it changed what was divided, not how the result was presented — so recovering it requires recomputing from the message.
9. RTL 4 — Identifying Which Convention Differs
// SYNTHESIZABLE DIAGNOSTIC.
//
// Turns "the check sequence disagrees" into "the peer's final complement
// is missing", which is a fix rather than a symptom.
//
// Method: hold the design's own computed value and the peer's received
// value, then apply the four candidate transformations and see which one
// reconciles them. Two of the four are pure transformations of the
// result; the other two need a recomputation, so a second engine
// instance is fed the same octets under an alternative configuration.
module convention_mismatch_identifier
import crc32_pkg::*;
#(
parameter int unsigned CNT_W = 16
) (
input logic clk,
input logic rst_n,
input logic clear,
input logic result_valid,
input logic [31:0] computed, // this design, Ethernet conventions
input logic [31:0] received, // from the peer's FCS field
input logic [13:0] frame_octets,
// A second engine over the same octets with INIT = 0. Everything else
// identical. This is the recomputation the initial-value test needs.
input logic alt_init_valid,
input logic [31:0] alt_init_result,
// A third engine with REF_IN = 0, everything else identical.
input logic alt_refin_valid,
input logic [31:0] alt_refin_result,
output logic verdict_valid,
output logic diff_complement,
output logic diff_out_reflect,
output logic diff_init,
output logic diff_in_reflect,
output logic unexplained,
output logic [CNT_W-1:0] c_mismatch,
// Sticky first verdict plus the evidence behind it.
output logic verdict_latched,
output logic [31:0] first_delta,
output logic [13:0] first_length
);
wire [31:0] delta = computed ^ received;
// TEST 1. A complement difference is an XOR with a constant applied
// after everything else, so the delta is that constant -- for every
// message, at every length. One frame settles it.
wire t_complement = (delta == 32'hFFFF_FFFF);
// TEST 2. An output-reflection difference means one value is the
// bit-reversal of the other. Also decidable from one frame.
wire t_out_reflect = (reflect32(computed) == received);
// TEST 3. An initial-value difference is only detectable by
// recomputing, because the delta depends on message content and length.
wire t_init = alt_init_valid && (alt_init_result == received);
// TEST 4. Likewise for input reflection, and note it cannot be
// recovered by transforming the result at all -- it changed what was
// divided, not how the answer was presented.
wire t_in_reflect = alt_refin_valid && (alt_refin_result == received);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
verdict_valid <= 1'b0;
diff_complement <= 1'b0;
diff_out_reflect <= 1'b0;
diff_init <= 1'b0;
diff_in_reflect <= 1'b0;
unexplained <= 1'b0;
c_mismatch <= '0;
verdict_latched <= 1'b0;
first_delta <= '0;
first_length <= '0;
end else begin
verdict_valid <= 1'b0;
diff_complement <= 1'b0;
diff_out_reflect <= 1'b0;
diff_init <= 1'b0;
diff_in_reflect <= 1'b0;
unexplained <= 1'b0;
if (clear) begin
c_mismatch <= '0;
// The latched verdict deliberately survives: a convention
// mismatch is a permanent property of a pairing, not a rate.
end
if (result_valid && (computed != received)) begin
if (!(&c_mismatch)) c_mismatch <= c_mismatch + 1'b1;
verdict_valid <= 1'b1;
// Priority order is deliberate. The two single-frame tests come
// first because they are decidable without a recomputation; the
// recomputation-based tests follow. `unexplained` is the honest
// default -- corruption on the wire also lands here, and calling
// it a convention fault would be a worse error than saying
// nothing.
if (t_complement) diff_complement <= 1'b1;
else if (t_out_reflect) diff_out_reflect <= 1'b1;
else if (t_init) diff_init <= 1'b1;
else if (t_in_reflect) diff_in_reflect <= 1'b1;
else unexplained <= 1'b1;
if (!verdict_latched) begin
verdict_latched <= 1'b1;
first_delta <= delta;
first_length <= frame_octets;
end
end
end
end
endmoduleClassification: synthesizable diagnostic.
What it teaches: that unexplained is the most important output and the easiest to leave out. Ordinary corruption on the wire produces a mismatch that matches none of the four transformations, and a design whose classifier has no fallback will force it into whichever branch is last. A diagnostic that always names a cause is worse than one that admits it does not know, because the wrong name is acted on.
Deliberately simplified: two alternative engines. A complete version needs four to cover every single-convention difference and does not attempt combinations, on the grounds that two simultaneous convention differences almost always mean somebody imported a whole different variant, which the golden vector of Section 7 catches first and more cheaply.
Production implication: the verdict is latched and survives clear because a convention mismatch is not a rate. It is a permanent property of a pairing between two implementations — it happens on 100% of frames or 0% — so a counter of occurrences carries almost no information while the first verdict carries all of it. This is the opposite of Chapter 6.1's residual, where the count is everything and no single event means anything.
10. RTL 5 — Checking That Your Own Two Ends Agree
There is a failure that precedes any peer, and it is embarrassing enough to be worth its own check: a design whose transmit engine and receive engine are configured differently.
// SYNTHESIZABLE CONFORMANCE CHECKER.
//
// Verifies that this design's transmit and receive CRC engines are
// configured identically -- by construction rather than by inspection.
//
// The failure it catches: two instantiations of a parameterised engine
// where one override was applied and the other was not. That design
// transmits frames its own receiver rejects, and does it silently until
// something loops back.
//
// Note what this does NOT do: it does not compare parameters. It drives
// the same message through both engines and compares results, because a
// parameter that matches while a datapath differs is exactly the case a
// parameter comparison cannot see.
module convention_conformance_checker
import crc32_pkg::*;
(
input logic clk,
input logic rst_n,
input logic start,
// To the transmit engine.
output logic tx_start,
output logic tx_valid,
output logic [7:0] tx_data,
output logic tx_last,
input logic tx_fcs_valid,
input logic [31:0] tx_fcs,
// To the receive engine, driven with the identical octets.
output logic rx_start,
output logic rx_valid,
output logic [7:0] rx_data,
output logic rx_last,
input logic rx_fcs_valid,
input logic [31:0] rx_fcs,
output logic check_done,
output logic engines_agree,
output logic matches_golden,
output logic [31:0] tx_observed,
output logic [31:0] rx_observed
);
localparam int unsigned N = 9;
localparam logic [7:0] MSG [N] = '{
8'h31, 8'h32, 8'h33, 8'h34, 8'h35, 8'h36, 8'h37, 8'h38, 8'h39
};
typedef enum logic [1:0] { C_IDLE, C_START, C_DRIVE, C_WAIT } c_e;
c_e state_q;
logic [3:0] idx_q;
logic tx_got_q, rx_got_q;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state_q <= C_IDLE; idx_q <= '0;
tx_start <= 1'b0; tx_valid <= 1'b0; tx_last <= 1'b0; tx_data <= '0;
rx_start <= 1'b0; rx_valid <= 1'b0; rx_last <= 1'b0; rx_data <= '0;
tx_got_q <= 1'b0; rx_got_q <= 1'b0;
check_done <= 1'b0; engines_agree <= 1'b0; matches_golden <= 1'b0;
tx_observed <= '0; rx_observed <= '0;
end else begin
tx_start <= 1'b0; tx_valid <= 1'b0; tx_last <= 1'b0;
rx_start <= 1'b0; rx_valid <= 1'b0; rx_last <= 1'b0;
case (state_q)
C_IDLE: if (start) begin
state_q <= C_START; idx_q <= '0;
tx_got_q <= 1'b0; rx_got_q <= 1'b0; check_done <= 1'b0;
end
C_START: begin
tx_start <= 1'b1; rx_start <= 1'b1;
state_q <= C_DRIVE;
end
C_DRIVE: begin
tx_valid <= 1'b1; tx_data <= MSG[idx_q]; tx_last <= (idx_q == 4'(N-1));
rx_valid <= 1'b1; rx_data <= MSG[idx_q]; rx_last <= (idx_q == 4'(N-1));
if (idx_q == 4'(N-1)) state_q <= C_WAIT;
else idx_q <= idx_q + 1'b1;
end
C_WAIT: begin
if (tx_fcs_valid) begin tx_observed <= tx_fcs; tx_got_q <= 1'b1; end
if (rx_fcs_valid) begin rx_observed <= rx_fcs; rx_got_q <= 1'b1; end
if (tx_got_q && rx_got_q) begin
// TWO independent results, and both matter. Agreement without
// matching the golden value means both engines are wrong the
// same way -- which interoperates with nothing and passes
// every loopback test in the suite.
engines_agree <= (tx_observed == rx_observed);
matches_golden <= (tx_observed == CHECK_123456789) &&
(rx_observed == CHECK_123456789);
check_done <= 1'b1;
state_q <= C_IDLE;
end
end
default: state_q <= C_IDLE;
endcase
end
end
endmoduleClassification: synthesizable conformance checker.
What it teaches: that engines_agree and matches_golden are different questions and reporting only the first is the classic mistake. Two engines that agree with each other and not with the golden value are the worst configuration, because every internal test passes: loopback works, the receive path accepts everything the transmit path sends, and nothing fails until a real peer appears. Internal consistency is not conformance, and a checker that measures only consistency certifies exactly the failure it was built to catch.
Deliberately simplified: it drives both engines from a shared sequencer. A real design usually cannot steal both datapaths simultaneously and runs the two checks in sequence at reset.
Production implication: the module compares results, not parameters, and the distinction is load-bearing. A parameter comparison passes whenever the two overrides match — including when both are wrong, and including when a parameter matches but a synthesis constant did not propagate into one instance. Driving the same octets through both and comparing outputs tests the thing that will actually be on the wire. It is the same argument Chapter 6.1 §6 made for measuring the residual against independent truth rather than deriving it from an assumption.
11. The Reflected Polynomial Is the Same Polynomial
A design reading two implementations side by side will find one using 0x04C11DB7 and another using 0xEDB88320, and the natural conclusion — that these are different polynomials, or different CRC-32 variants — is wrong and worth dismantling.
They are the same polynomial with its bit order written the other way round. 0xEDB88320 is the bit reversal of 0x04C11DB7, and it exists because the two are used by shift registers running in opposite directions.
A left-shifting register consumes the most significant bit first, so its taps are read from the polynomial as written. A right-shifting register consumes the least significant bit first, so its taps must be read from the reversed form. The two engines perform identical division and produce identical results under identical conventions.
Which is worth knowing for two practical reasons.
First, the right-shifting form is what you want when the input is reflected anyway. Ethernet reflects each input octet, and a right-shifting register consumes bits least-significant-first natively — so the reflection and the shift direction cancel, and a well-chosen implementation performs no reflection at all while producing exactly the reflected result. That is why most software CRC-32 routines look nothing like Section 3's module and are nonetheless computing the same thing.
Second, it is a trap in reverse. An engineer who has seen 0xEDB88320 in software and writes a left-shifting hardware engine with those taps has built a genuinely different code — a different polynomial, with different guarantees, that happens to be a valid CRC and is not Ethernet's. The golden vector of Section 7 catches it immediately, which is the argument for having one.
12. Assertions — About the Result, Not About the Register
// ---------------------------------------------------------------------
// P1 -- THE GOLDEN VECTOR. Nine known octets produce one published value.
// This single property pins the polynomial and all four conventions.
// ---------------------------------------------------------------------
property p_golden_check_value;
@(posedge clk) disable iff (!rst_n)
(selftest_done) |-> (observed == CHECK_123456789);
endproperty
a_golden_check_value: assert property (p_golden_check_value)
else $error("golden vector failed -- polynomial or a convention differs");
// ---------------------------------------------------------------------
// P2 -- The register is preset at frame start, every frame. A register
// carried over from the previous frame produces a value that depends on
// history, which is correct for no specification at all.
// ---------------------------------------------------------------------
property p_preset_every_frame;
@(posedge clk) disable iff (!rst_n)
frame_start |=> (raw_reg == INIT);
endproperty
a_preset_every_frame: assert property (p_preset_every_frame);
// ---------------------------------------------------------------------
// P3 -- The result is produced exactly once per frame, on the last octet.
// ---------------------------------------------------------------------
property p_one_result_per_frame;
@(posedge clk) disable iff (!rst_n)
(oct_valid && oct_last) |-> ##[1:9] fcs_valid;
endproperty
a_one_result_per_frame: assert property (p_one_result_per_frame);
// ---------------------------------------------------------------------
// P4 -- No result without a frame. A value emitted outside a frame is a
// stale register being read.
// ---------------------------------------------------------------------
property p_no_result_outside_frame;
@(posedge clk) disable iff (!rst_n)
fcs_valid |-> $past(frame_active);
endproperty
a_no_result_outside_frame: assert property (p_no_result_outside_frame);
// ---------------------------------------------------------------------
// P5 -- REFLECTION IS AN INVOLUTION. Applying it twice is the identity.
// A reflect function with an off-by-one satisfies almost everything else
// and fails here immediately.
// ---------------------------------------------------------------------
property p_reflect32_involution;
@(posedge clk) disable iff (!rst_n)
(reflect32(reflect32(probe_word)) == probe_word);
endproperty
a_reflect32_involution: assert property (p_reflect32_involution);
property p_reflect8_involution;
@(posedge clk) disable iff (!rst_n)
(reflect8(reflect8(probe_octet)) == probe_octet);
endproperty
a_reflect8_involution: assert property (p_reflect8_involution);
// ---------------------------------------------------------------------
// P6 -- The two polynomial forms are bit reversals of one another. A
// constant relationship, asserted so an edit to either is caught at
// elaboration rather than by a peer.
// ---------------------------------------------------------------------
// synopsys translate_off
a_poly_forms_consistent: assert final (POLY_LSB == reflect32(POLY_MSB))
else $fatal(1, "the two polynomial forms are not the same polynomial");
// synopsys translate_on
// ---------------------------------------------------------------------
// P7 -- The complement mask is bit-symmetric, which is the precondition
// that lets conventions 3 and 4 be applied in either order (Section 5).
// Asserted because a future variant with a different mask silently loses
// the property.
// ---------------------------------------------------------------------
// synopsys translate_off
a_final_mask_symmetric: assert final (XOR_OUT == reflect32(XOR_OUT))
else $warning("final mask is not bit-symmetric: reflect and complement no longer commute");
// synopsys translate_on
// ---------------------------------------------------------------------
// P8 -- A complement difference produces exactly the mask as its delta.
// The single-frame diagnosis of Section 8.
// ---------------------------------------------------------------------
property p_complement_delta_is_mask;
@(posedge clk) disable iff (!rst_n)
diff_complement |-> ($past(computed) ^ $past(received)) == XOR_OUT;
endproperty
a_complement_delta_is_mask: assert property (p_complement_delta_is_mask);
// ---------------------------------------------------------------------
// P9 -- An output-reflection difference means one value is the other
// reversed. Also decidable from one frame.
// ---------------------------------------------------------------------
property p_out_reflect_delta_is_reversal;
@(posedge clk) disable iff (!rst_n)
diff_out_reflect |-> (reflect32($past(computed)) == $past(received));
endproperty
a_out_reflect_delta_is_reversal: assert property (p_out_reflect_delta_is_reversal);
// ---------------------------------------------------------------------
// P10 -- The verdicts are mutually exclusive, and `unexplained` is one of
// them rather than an absence. A classifier with no fallback forces
// ordinary wire corruption into whichever branch is last.
// ---------------------------------------------------------------------
property p_verdicts_onehot;
@(posedge clk) disable iff (!rst_n)
verdict_valid |-> $onehot({diff_complement, diff_out_reflect, diff_init,
diff_in_reflect, unexplained});
endproperty
a_verdicts_onehot: assert property (p_verdicts_onehot);
// ---------------------------------------------------------------------
// P11 -- A recomputation-based verdict requires the recomputation to have
// happened. Without it the test is comparing against a stale value.
// ---------------------------------------------------------------------
property p_init_verdict_needs_recompute;
@(posedge clk) disable iff (!rst_n)
diff_init |-> $past(alt_init_valid);
endproperty
a_init_verdict_needs_recompute: assert property (p_init_verdict_needs_recompute);
// ---------------------------------------------------------------------
// P12 -- The verdict is latched once and survives a clear: a convention
// mismatch is a property of a pairing, not a rate.
// ---------------------------------------------------------------------
property p_verdict_latched_stable;
@(posedge clk) disable iff (!rst_n)
verdict_latched |=> ($stable(first_delta) && $stable(first_length));
endproperty
a_verdict_latched_stable: assert property (p_verdict_latched_stable);
// ---------------------------------------------------------------------
// P13 -- Internal agreement does NOT imply conformance. Stated as a
// property so the weaker check cannot quietly stand in for the stronger.
// ---------------------------------------------------------------------
property p_agreement_is_not_conformance;
@(posedge clk) disable iff (!rst_n)
(check_done && matches_golden) |-> engines_agree;
endproperty
a_agreement_is_not_conformance: assert property (p_agreement_is_not_conformance);
// ---------------------------------------------------------------------
// P14 -- Both engines are checked against the golden value, not only
// against each other.
// ---------------------------------------------------------------------
property p_both_engines_golden;
@(posedge clk) disable iff (!rst_n)
(check_done && matches_golden)
|-> ((tx_observed == CHECK_123456789) && (rx_observed == CHECK_123456789));
endproperty
a_both_engines_golden: assert property (p_both_engines_golden);
// ---------------------------------------------------------------------
// P15 -- The self-test drives exactly the golden message: nine octets,
// no more and no fewer. A self-test that drives eight produces a
// perfectly repeatable wrong value and reports a convention fault that
// does not exist.
// ---------------------------------------------------------------------
property p_selftest_drives_nine;
@(posedge clk) disable iff (!rst_n)
(drive_valid && drive_last) |-> (selftest_octets == 4'd9);
endproperty
a_selftest_drives_nine: assert property (p_selftest_drives_nine);
// ---------------------------------------------------------------------
// P16 -- Input reflection is applied if and only if configured. Written
// over the function rather than over a register, so it survives every
// restructuring the rejected property below does not.
// ---------------------------------------------------------------------
property p_input_reflection_applied_iff_configured;
@(posedge clk) disable iff (!rst_n)
oct_valid |-> (oct_applied == (REF_IN ? reflect8(oct_data) : oct_data));
endproperty
a_input_reflection_applied_iff_configured:
assert property (p_input_reflection_applied_iff_configured);
// ---------------------------------------------------------------------
// P17 -- The diagnostic's alternative engines must have consumed THE
// SAME octets. Comparing against an engine fed different data produces a
// verdict from a coincidence.
// ---------------------------------------------------------------------
property p_alt_engines_same_octets;
@(posedge clk) disable iff (!rst_n)
(alt_init_valid || alt_refin_valid) |-> (alt_octet_count == main_octet_count);
endproperty
a_alt_engines_same_octets: assert property (p_alt_engines_same_octets);
// ---------------------------------------------------------------------
// P18 -- COVERAGE. Each of the four verdicts reached. A run that never
// produced a convention mismatch never tested Section 9 at all.
// ---------------------------------------------------------------------
c_verdict_complement: cover property (@(posedge clk) disable iff (!rst_n) diff_complement);
c_verdict_out_reflect: cover property (@(posedge clk) disable iff (!rst_n) diff_out_reflect);
c_verdict_init: cover property (@(posedge clk) disable iff (!rst_n) diff_init);
c_verdict_in_reflect: cover property (@(posedge clk) disable iff (!rst_n) diff_in_reflect);
// ---------------------------------------------------------------------
// P19 -- COVERAGE. An UNEXPLAINED mismatch, which is what ordinary wire
// corruption looks like and what the fallback exists for.
// ---------------------------------------------------------------------
c_verdict_unexplained: cover property (@(posedge clk) disable iff (!rst_n) unexplained);13. Verification — Twenty-Four Scenarios and a Peer That Differs by Exactly One Convention
| # | Scenario | Stimulus | What must be observed |
|---|---|---|---|
| 1 | Golden vector | 123456789, all four conventions | 0xCBF43926 (P1) |
| 2 | Empty message | zero octets | the initial value, reflected and complemented — not zero |
| 3 | Single octet | one octet | a result; no dependence on the previous frame (P2) |
| 4 | Minimum frame | 60 octets of covered range | one result on the last octet (P3) |
| 5 | Maximum standard frame | 1514 octets of covered range | one result; no overflow in the octet counter |
| 6 | Back-to-back frames | two frames with no idle between | the second preset correctly (P2) — the classic carry-over bug |
| 7 | Leading zeros, INIT = 0 | abc and 00 00 00 abc | identical values — the failure the initial value prevents |
| 8 | Leading zeros, Ethernet INIT | the same two messages | different values |
| 9 | Trailing zeros, no complement | codeword, then codeword + 00 | identical residue — the failure the complement prevents |
| 10 | Trailing zeros, Ethernet | the same two frames | residue changes on the first appended octet |
| 11 | Reflection involution | any word through reflect32 twice | the original word (P5) |
| 12 | Polynomial forms | elaboration | POLY_LSB is the reversal of POLY_MSB (P6) |
| 13 | Result outside a frame | assert fcs_valid sampling between frames | never valid (P4) |
| 14 | Peer differs: complement | peer omits the final complement | delta is exactly 0xFFFFFFFF; diff_complement (P8) |
| 15 | Complement delta invariance | the same peer, ten different frame lengths | delta is 0xFFFFFFFF every time |
| 16 | Peer differs: output reflection | peer omits output reflection | reflect32(computed) == received; diff_out_reflect (P9) |
| 17 | Peer differs: initial value | peer uses INIT = 0 | diff_init — needs the recomputation (P11) |
| 18 | Initial-value delta varies | the same peer, several lengths | the delta changes with length — the positive identification |
| 19 | Peer differs: input reflection | peer omits input reflection | diff_in_reflect; no transform of the result reconciles them |
| 20 | Ordinary wire corruption | flip one bit in the payload | unexplained — not a convention verdict (P10, P16) |
| 21 | Verdict is latched | a mismatch, then a clear | verdict and first delta survive (P12) |
| 22 | Both engines correct | run the conformance checker | engines_agree and matches_golden (P14) |
| 23 | Both engines wrong the same way | override INIT on both instances | engines_agree high, matches_golden low (P13) |
| 24 | One engine overridden | override INIT on the transmit instance only | engines_agree low |
14. Debugging — Reading a Mismatch to a Cause
Symptom — every frame is rejected by one peer and accepted by every other.
Convention mismatch until proven otherwise, and the confirming characteristic is the ratio: 100% against one peer, 0% against the rest. A physical fault does not respect which peer sent the frame. Capture one rejected frame, exclusive-or the computed and received check values, and read Section 8's table: a delta of exactly 0xFFFFFFFF ends the investigation on the spot.
Symptom — a delta that is dense but not 0xFFFFFFFF, and changes with every frame.
Three candidates remain and they are separated by two cheap tests. Bit-reverse the computed value: if it equals the received one, the disagreement is output reflection. Feed the same content at several lengths: if the delta changes with length, it is the initial value. If neither test resolves it, the disagreement is input reflection — which cannot be recovered by transforming the result and needs a recomputation from the message.
Symptom — loopback passes, the golden vector fails.
The two engines in this design agree with each other and neither is Ethernet's. This is scenario 23, and it is the configuration that passes the most tests while interoperating with nothing. Check for a parameter override applied at an integration wrapper, which is the usual cause: one instantiation is overridden, the other inherits, and if both inherit from a wrong default they agree perfectly.
Symptom — a software reference and the hardware disagree, and both were written from the same specification.
Look for the shift direction first. Software CRC-32 routines almost universally use the right-shifting form with 0xEDB88320, which consumes bits least-significant-first and therefore performs no explicit reflection. A hardware engine written left-shifting with 0x04C11DB7 must reflect. Two implementations that differ in shift direction and both omit the reflection produce different values while each looking internally consistent.
Symptom — an engine using 0xEDB88320 in a left-shifting register.
Not a convention problem — a different code. The reversed constant in the wrong shift direction is a valid CRC-32 with different taps, different guarantees, and no relationship to Ethernet's. It will pass any test that compares it against itself and fail the golden vector on the first nine octets.
Symptom — the first frame after reset is rejected and every subsequent frame is accepted.
The register is not being preset at frame start, only at reset — so the first frame divides from the reset value and the rest divide from whatever the previous frame left. Confirm with P2, and note the symptom's shape: a fault that affects exactly one frame per reset is almost always an initialisation that happens in the wrong place rather than a datapath error.
Symptom — everything works and the check value drifts after a synthesis change.
Suspect an optimised-away tap or a reflection function that was constant-folded incorrectly. The golden vector is the instrument — it runs in nine octets, in silicon, and it is the only check in this chapter that survives the transition from simulation to hardware unchanged.
15. Common Misconceptions
"CRC-32 is CRC-32."
The wrong model: naming the algorithm determines the value.
What it costs: a library routine is imported, tested against that library's own vectors, passes everything, and produces frames no peer accepts. Nothing was implemented incorrectly — the polynomial was right, the division was right — and one of four unstated agreements was different.
The corrected model: the polynomial fixes the guarantees; four independent conventions fix the value. Both must match for two devices to interoperate, and only the first is what "CRC-32" names. Sixteen combinations are all valid CRC-32 and only one is Ethernet's.
"Getting a convention wrong weakens the error detection."
The wrong model: a wrong initial value or a missing complement means errors get through.
What it costs: the wrong mental model produces the wrong debugging instinct — looking at error rates, channels and margins for a fault that has nothing to do with any of them.
The corrected model: all four conventions are affine transformations that relabel values without changing which messages are confusable. Every guarantee from Chapter 6.1 is identical under all sixteen combinations. A convention mismatch does not weaken detection at all — it makes two devices compute different labels for the same frame.
"0xEDB88320 is a different polynomial."
The wrong model: two constants, two codes.
What it costs: either confusion about which to use, or — much worse — using the reversed constant in a left-shifting register, which really is a different code with different guarantees, passes every self-consistent test, and fails the golden vector.
The corrected model: it is the same polynomial with its bit order reversed, needed by a right-shifting register. The two engines perform identical division. The constant and the shift direction must be chosen together, and a mismatched pair is a genuinely different CRC.
"Our loopback test proves the CRC is right."
The wrong model: if the receiver accepts what the transmitter sends, the engine is correct.
What it costs: the worst configuration — two engines that agree with each other and not with the standard — is the one loopback certifies most confidently. Every internal test passes and nothing fails until a real peer appears.
The corrected model: internal consistency is not conformance. Only a comparison against an externally published value tests conformance, which is why the golden vector exists and why Section 10 reports engines_agree and matches_golden as two separate outputs.
"Assert the register value after each octet — it is the tightest check."
The wrong model: pinning internal state catches the most regressions.
What it costs: Section 12's rejected property. The assertion breaks on every legal restructuring — shift direction, retiming, a byte-wide engine — and each failure is false. It then gets updated to match the new implementation, at which point it is a transcript of the design rather than a constraint on it.
The corrected model: assert what the specification names: the final value under stated conventions, the involution of the reflection function, the relationship between the two polynomial constants. If a property needs a signal name from your own RTL, it describes your design rather than constraining it.
16. Interview Reasoning
"Two engineers implement CRC-32 from the same polynomial. Will they interoperate?"
The weak answer is yes. The answer that ends the topic is: not necessarily, and there are exactly four reasons — initial value, input reflection, output reflection, final complement. Then the part that shows the concept is held properly: none of the four changes any detection guarantee, because all four are affine transformations that relabel values without changing which messages are confusable. They change only the value, which is the only thing two devices compare.
"How would you diagnose a link where one peer rejects every frame and all others are fine?"
The trap is to start at the physical layer, and the ratio rules it out immediately — a physical fault does not respect which peer sent the frame. The strong answer captures one rejected frame and exclusive-ors the two check values: a delta of exactly 0xFFFFFFFF, constant across frames and lengths, is a missing final complement and needs no further work. Adding that the other three differences need either a bit-reversal test or a recomputation, and that the initial-value case is identified by the delta changing with length, shows the diagnosis is understood rather than memorised.
"Why does the Ethernet CRC start from all-ones instead of zero?"
Because a zero register stays at zero while zeros are fed in, so leading zero octets are invisible: abc and 00 00 00 abc produce the same value. A non-zero initial value breaks that symmetry. The strong follow-up is the matching one — the final complement does the same job at the other end, and it is the sharper demonstration: without it, appending zero octets to a valid codeword leaves the residue unchanged at any length, so a whole class of length errors is undetectable.
"What is the single most valuable test for a CRC implementation?"
The published check value over 123456789. One number pins the polynomial and all four conventions simultaneously, because it is their composition and any single difference changes it. The finishing point is where to run it: as a built-in self-test, not only in simulation — the conventions are configuration, configuration survives into silicon, and a parameter that failed to propagate produces a perfectly working engine computing the wrong value.
17. Understanding Check
A remainder — and a valid one at every clock, not only at the end.
Treat the message as a polynomial over the field with two elements and divide it by the generator. In that field addition and subtraction are both exclusive-or and there are no carries, so every step of long division is a conditional exclusive-or with the divisor.
That maps exactly onto a shift register. The register holds the current partial remainder; its top bit says whether the divisor "fits"; the exclusive-or with the tapped positions performs the subtraction; the shift moves to the next position.
So the taps are the polynomial read off directly, and x^32 is implicit because it is the bit that shifts out and drives the feedback — it never needs storing.
And the register being a remainder at every clock is not a curiosity. It is why a partial computation over a partial frame is a meaningful quantity, and it is the property Chapter 6.3's residue check is built on.
18. What's Next
The claim this chapter defended: the polynomial is the mathematics and the conventions are the interoperability, and they fail independently.
The arithmetic is one shift register performing polynomial division, and the register holds a valid remainder at every clock. Around it sit four decisions the polynomial does not make — initial value, input reflection, output reflection, final complement — of which two defend against real error classes (leading zeros, trailing zeros) and two are wire order. None of the four changes any guarantee; all four change the value.
Which makes a convention mismatch a failure with an unusually clean signature: 100% against the peers that differ and 0% against the peers that agree, unaffected by cable, load or temperature. And it makes one published number — 0xCBF43926 over nine ASCII characters — the highest-value test in the module, because it pins the polynomial and all four conventions at once, in nine octets, in silicon.
Chapter 6.3 — CRC Checking and the Residue turns the engine around. A receiver could recompute the check over the data, hold the result, and compare it against the four octets that arrive — and that is not what receivers do.
Instead they run the same engine over the data and the check sequence together and test the result against a fixed constant. No value to hold, no comparison at end of frame, and a check that works identically at any length. Section 6's trailing-zero table has already shown that constant twice without naming it: 0xDEBB20E3.
And 6.3's second half is this chapter's argument returning in a sharper form. That constant is a fingerprint of all four conventions at once — the same residue reads as 0xDEBB20E3, 0xC704DD7B or 0x2144DF1C depending on reflection and complement — so a hard-coded literal encodes four assumptions in a magic number that nothing derives and nothing checks.
The full path is on the Ethernet curriculum index.
Continue learning
Related tutorials
- Related topic
Why Ethernet Won
Ethernet offered weaker guarantees than token passing on every axis compared at the time. It won because failure was local rather than global, because two vendors had almost nothing to disagree about, and because a media-independent interface let one MAC outlive every physical layer it was attached to.
- Related topic
Frame Check Sequence
The check sequence protects a range, and the range is shorter than the frame's journey — appended at one point in a transmitter, verified at one point in the next receiver, and recomputed at every hop, so a device's own memory is covered by nothing the frame carries.
- Related topic
What Error Detection Must Guarantee
A detector's specification is a set of bounded guarantees plus a probability, and neither can be stated without an error model — including the guarantee everybody cites and this polynomial does not provide.
- Related topic
CRC Checking and the Residue
A receiver feeds the data and the check sequence through one division and tests for a fixed constant — no held value, no captured FCS, no need to know where the payload ended. And that constant is a fingerprint of all four conventions at once.
Standards & specifications
- Governing standard
- IEEE Std 802.3 (Ethernet)(opens IEEE in a new tab)
Defines the Ethernet MAC, the media-independent interfaces and the physical-layer sublayers, including framing, access control, auto-negotiation and per-rate PHY specifications. VLAN tagging, priority and time-sensitive shaping are defined by IEEE 802.1, not by 802.3.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the Ethernet curriculum.
