Ethernet · Module 6
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.
Chapter 6.2 built an engine that consumes a message and produces four octets. The obvious way to use it at a receiver is the symmetric one: run the same engine over the arriving data, hold the result, wait for the four check octets to arrive, and compare.
That works, and essentially no receiver does it.
Instead a receiver keeps feeding the engine — through the check sequence as well — and tests the final register against a fixed constant. Nothing is held, nothing is compared at end of frame, and the test is identical for a 64-octet frame and a 9000-octet one.
Chapter 6.2 §6 already produced that constant twice without naming it. Its trailing-zero table showed a valid frame yielding 0xDEBB20E3, and a frame with one appended zero octet yielding something else. That first number is the residue, and this chapter is about why it exists, what it buys, and what it quietly encodes.
Because it encodes rather a lot. The residue is not a property of the polynomial. It is a property of the polynomial and all four conventions, which is why the same residue appears in the literature as three different constants — and why writing it into RTL as a magic number is the subject of this chapter's rejected property.
1. Scope — What This Chapter Owns
Chapter 6.2 owns generation — the division, the shift register, and the four conventions. Everything here uses that engine unchanged; the receive side is not a different computation.
This chapter owns checking: why feeding the check sequence through the engine produces a constant, what that constant is, what the residue approach buys and costs against a straight comparison, how to derive the constant instead of quoting it, and what happens at the boundaries — a frame that ends early, a frame with trailing octets.
Chapter 6.4 owns the transformation to eight or sixty-four bits per clock. Nothing in this chapter changes there: the residue is a property of the code, not of the datapath width.
Chapter 5.8 owns the check point — where in a receive datapath the verification happens, and what falls outside the protected interval. This chapter assumes that placement and works inside it.
Chapter 6.1 owns the guarantees, and it is worth restating that checking by residue detects exactly what checking by comparison detects. The two are algebraically equivalent. Choosing between them is an implementation decision with no effect on error detection whatsoever.
The question this chapter answers that its neighbours do not: why does a receiver test for a constant it never computed, where does that constant come from, and what does hard-coding it cost?
2. Why a Constant Exists
The property this rests on is stated here and derived in Chapter 6.2's terms rather than re-derived: the transmitter appends exactly the remainder that makes the whole codeword divisible by the generator.
That is what a remainder is. Divide the message by the generator, take what is left over, and append it — now the combined quantity has no remainder, because the leftover part has been subtracted off.
So a receiver that divides the whole codeword gets zero. Data and check sequence together, through the same division, and there is nothing left.
Except that Ethernet gets 0xDEBB20E3 rather than zero, and the difference is entirely the conventions.
The initial value is not zero, so the register starts somewhere the message has to move it away from. The final complement is applied to the transmitted value, so what arrives is not the bare remainder. Both of those shift the landing point, and neither of them shifts it anywhere that depends on the message. The result is still a constant — just not zero.
Read the two rows as doing the same algebra in two places. The comparing receiver stops the division early and does the final subtraction itself, in a 32-bit comparator. The residue receiver lets the division do it, and reads the answer off the register.
And the constant is genuinely constant. Computed over frames of 0, 1, 19 and 64 octets, the residue is 0xDEBB20E3 in every case — the message length does not enter, and neither does the content.
3. Three Constants, One Residue
The three are related by exactly the transformations Chapter 6.2 §4 identified as conventions, and the relationships are checkable arithmetic:
| Relationship | Holds |
|---|---|
reflect32(0xDEBB20E3) | 0xC704DD7B |
0x2144DF1C ^ 0xFFFFFFFF | 0xDEBB20E3 |
reflect32(0xC704DD7B) | 0xDEBB20E3 |
So which constant a design should test against depends entirely on where in its own datapath it samples the register, and there is no universally correct answer:
- an engine that reflects its output and does not complement it lands on
0xDEBB20E3; - the same value read out of a non-reflecting engine reads as
0xC704DD7B; - an engine that applies its full output pipeline — reflect and complement — lands on
0x2144DF1C.
All three are correct, for the design that produces them, and wrong for the other two. Which is why a specification, a reference implementation and a colleague's code can each quote a different number without any of them being mistaken.
4. RTL 1 — Checking by Residue
// SYNTHESIZABLE.
//
// Checks a frame by feeding EVERYTHING -- data and check sequence alike --
// through the division, and testing the final register against a constant.
//
// What makes this work: the transmitter appended exactly the remainder
// that makes the codeword divisible, so the result of dividing the whole
// codeword does not depend on the message. Every valid frame lands on the
// same value.
//
// Note the constant is a PARAMETER derived elsewhere (Section 7), not a
// literal. Section 12 explains at length why that distinction is the
// subject of this chapter's rejected property.
module crc32_residue_checker
import crc32_pkg::*;
#(
// The value a valid frame leaves in the register, given this design's
// conventions and this sample point. DERIVED, not quoted.
parameter logic [31:0] RESIDUE = 32'hDEBB_20E3
) (
input logic clk,
input logic rst_n,
input logic frame_start,
input logic oct_valid,
input logic [7:0] oct_data,
// Asserted on the last octet of the FRAME, not of the payload. The
// receiver never needs to know where the payload ended.
input logic frame_end,
output logic check_valid,
output logic fcs_ok,
output logic [31:0] residue_observed
);
logic [31:0] reg_q;
logic [7:0] oct_q;
logic [2:0] bit_q;
logic busy_q;
logic end_q;
wire [7:0] oct_applied = REFLECT_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_VALUE;
oct_q <= '0;
bit_q <= '0;
busy_q <= 1'b0;
end_q <= 1'b0;
check_valid <= 1'b0;
fcs_ok <= 1'b0;
residue_observed <= '0;
end else begin
check_valid <= 1'b0;
if (frame_start) begin
reg_q <= INIT_VALUE;
busy_q <= 1'b0;
end_q <= 1'b0;
end else if (oct_valid && !busy_q) begin
oct_q <= oct_applied;
bit_q <= '0;
busy_q <= 1'b1;
end_q <= frame_end;
end else if (busy_q) begin
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 (end_q) begin
automatic logic [31:0] final_reg =
{reg_q[30:0], 1'b0} ^ (feedback ? POLY_MSB : 32'h0);
// The whole check. One 32-bit equality against a constant --
// and crucially, against a constant known at elaboration
// rather than a value captured during the frame.
residue_observed <= final_reg;
fcs_ok <= (final_reg == RESIDUE);
check_valid <= 1'b1;
end
end else begin
bit_q <= bit_q + 1'b1;
end
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that frame_end marks the end of the frame, not the end of the payload, and that this is the whole structural saving. A comparing receiver has to identify the four check octets, which means knowing four octets in advance where the frame stops — on a stream whose end is signalled by carrier dropping, that requires a four-octet delay line and the logic to manage it. The residue receiver has no such requirement, because it treats the check sequence as more data.
Deliberately simplified: bit-serial, matching Chapter 6.2's engine so the two can be read side by side. Chapter 6.4 widens it, and the residue is unaffected: it is a property of the code, not of how many bits are consumed per clock.
Production implication: residue_observed is exposed even though fcs_ok is the answer, and it earns its ports twice over. A wrong residue constant rejects every frame, which looks like a dead link; reading the observed value immediately distinguishes "the frames are bad" from "the constant is wrong", because a valid frame under a mismatched constant lands on a value that is itself constant across every frame. A checker reporting only a boolean makes a configuration error indistinguishable from total link failure.
5. RTL 2 — Checking by Comparison, For Contrast
// SYNTHESIZABLE.
//
// The symmetric receiver: divide over the DATA only, hold the result,
// capture the arriving check sequence, compare at end of frame.
//
// Algebraically identical to Section 4 in what it detects. Structurally
// it needs three things Section 4 does not:
//
// 1. a four-octet delay line, because the engine must stop four octets
// before the frame ends and the frame's end is not known in advance
// 2. a capture register for the arriving check sequence
// 3. a holding register for the computed value
//
// The delay line is the expensive one, and it is the one that does not
// appear in a naive gate count.
module crc32_compare_checker
import crc32_pkg::*;
(
input logic clk,
input logic rst_n,
input logic frame_start,
input logic oct_valid,
input logic [7:0] oct_data,
input logic frame_end,
output logic check_valid,
output logic fcs_ok,
output logic [31:0] computed,
output logic [31:0] received
);
// (1) The delay line. Four octets, plus their valid bits, so that when
// frame_end arrives the four octets still in flight can be recognised
// as the check sequence rather than as payload.
logic [7:0] dly_q [4];
logic [2:0] fill_q;
logic [31:0] reg_q;
logic [7:0] oct_q;
logic [2:0] bit_q;
logic busy_q;
wire [7:0] oct_applied = REFLECT_IN ? reflect8(dly_q[3]) : dly_q[3];
wire feedback = oct_q[7] ^ reg_q[31];
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (int i = 0; i < 4; i++) dly_q[i] <= '0;
fill_q <= '0;
reg_q <= INIT_VALUE;
oct_q <= '0;
bit_q <= '0;
busy_q <= 1'b0;
check_valid <= 1'b0;
fcs_ok <= 1'b0;
computed <= '0;
received <= '0;
end else begin
check_valid <= 1'b0;
if (frame_start) begin
reg_q <= INIT_VALUE;
fill_q <= '0;
busy_q <= 1'b0;
end else if (oct_valid && !busy_q) begin
// Shift the delay line. Only once four octets are resident does
// the oldest one become payload rather than a possible FCS octet.
dly_q[3] <= dly_q[2];
dly_q[2] <= dly_q[1];
dly_q[1] <= dly_q[0];
dly_q[0] <= oct_data;
if (fill_q != 3'd4) begin
fill_q <= fill_q + 1'b1;
end else begin
oct_q <= oct_applied;
bit_q <= '0;
busy_q <= 1'b1;
end
if (frame_end) begin
// (2) The four octets still in the delay line ARE the check
// sequence -- and note the octet order, which is a fifth
// convention this receiver has to get right and Section 4's
// never encounters.
received <= {dly_q[0], dly_q[1], dly_q[2], dly_q[3]};
// (3) The held value, with the output conventions applied.
computed <= (REFLECT_OUT ? reflect32(reg_q) : reg_q) ^ XOR_OUT;
fcs_ok <= ((REFLECT_OUT ? reflect32(reg_q) : reg_q) ^ XOR_OUT)
== {dly_q[0], dly_q[1], dly_q[2], dly_q[3]};
check_valid <= 1'b1;
end
end else if (busy_q) begin
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) busy_q <= 1'b0;
else bit_q <= bit_q + 1'b1;
end
end
end
endmoduleClassification: synthesizable, shown for contrast.
What it teaches: that the comparing receiver acquires a fifth convention the residue receiver never meets. It has to assemble the four arriving octets into a 32-bit value, and the order in which it does that is a decision — the check sequence goes onto the wire in a specific octet order, and getting it backwards produces a receiver that rejects every frame while its engine is perfectly correct. Section 4 never assembles the value at all, so the question does not arise.
Deliberately simplified: the delay line is four flat registers. A wide datapath needs a shift structure with byte enables, and the management of a partial final transfer is the same problem Chapter 6.4 solves for the engine itself — solved twice, in two places, in a comparing design.
Production implication: the delay line is the cost that does not appear when people estimate this. A comparison "costs a 32-bit comparator and a register" in the usual account; it actually costs four octets of storage, their valid tracking, a fill counter, and a decision about which octets are payload — on the receive datapath's critical path. Section 6 puts numbers on it.
6. The Cost, Concretely
Illustrative figures for a bit-serial receive path at one octet per clock, counting only what differs between the two designs:
| Resource | Compare | Residue |
|---|---|---|
| four-octet delay line | 32 flops | — |
| delay-line valid/fill tracking | ~3 flops + control | — |
| computed-value holding register | 32 flops | — |
| received-value capture register | 32 flops | — |
| 32-bit equality against a variable | yes | — |
| 32-bit equality against a constant | — | yes |
| approximate flop delta | ~99 | 0 |
Read the last two rows together, because they are the part that matters for timing rather than area.
A comparison against a constant synthesises to a 32-input AND over inverted-or-not bits — a fixed reduction tree with no data dependency on one side. A comparison against a variable needs 32 XNORs feeding the same tree. The constant comparison is strictly cheaper and strictly faster, and at high line rates the end-of-frame path is often the receive datapath's critical path because it must resolve before the frame's disposition is decided.
The area figure is more interesting than it looks, too. Roughly a hundred flops is negligible in a modern MAC — but it is not negligible in a design with sixteen receive ports, and it is not negligible in the control it implies: the fill counter, the payload/FCS decision, and the octet-assembly order are all places a bug can live.
And there is a cost the table cannot show. The comparing receiver must decide, during the frame, which octets are payload — so a frame that ends unexpectedly leaves that decision half-made. The residue receiver has no such state, which is Section 9's subject.
7. RTL 3 — Deriving the Constant Instead of Quoting It
// SYNTHESIZABLE, and the work happens at elaboration.
//
// Derives the residue rather than quoting it, by doing exactly what a
// valid frame does: take an arbitrary message, compute its check
// sequence under this design's conventions, append it, and run the whole
// thing back through. Whatever comes out IS the residue -- by
// construction, for this configuration.
//
// The message is arbitrary because the residue does not depend on it.
// That is the property being relied on, and the module checks it rather
// than assuming it: two different messages must produce the same answer.
package residue_pkg;
import crc32_pkg::*;
// A constant-function model of the design's own engine. Not a second
// implementation -- it applies the SAME parameters, so it cannot drift
// from the datapath in the way an independently written reference can.
function automatic logic [31:0] crc_raw(input logic [7:0] msg [],
input int unsigned n);
logic [31:0] r;
logic [7:0] o;
r = INIT_VALUE;
for (int unsigned i = 0; i < n; i++) begin
o = REFLECT_IN ? reflect8(msg[i]) : msg[i];
for (int unsigned b = 0; b < 8; b++) begin
r = {r[30:0], 1'b0} ^ ((o[7] ^ r[31]) ? POLY_MSB : 32'h0);
o = {o[6:0], 1'b0};
end
end
crc_raw = r;
endfunction
function automatic logic [31:0] fcs_of(input logic [7:0] msg [],
input int unsigned n);
fcs_of = (REFLECT_OUT ? reflect32(crc_raw(msg, n)) : crc_raw(msg, n)) ^ XOR_OUT;
endfunction
// The derivation. Build message || FCS and run it all through.
function automatic logic [31:0] derive_residue(input logic [7:0] msg [],
input int unsigned n);
logic [7:0] full [];
logic [31:0] f;
f = fcs_of(msg, n);
full = new[n + 4];
for (int unsigned i = 0; i < n; i++) full[i] = msg[i];
// Octet order matters here and ONLY here -- it is the transmitter's
// convention for laying the value onto the wire, and getting it wrong
// yields a residue that is stable and wrong, which is the worst kind.
full[n+0] = f[7:0];
full[n+1] = f[15:8];
full[n+2] = f[23:16];
full[n+3] = f[31:24];
derive_residue = crc_raw(full, n + 4);
endfunction
endpackage
module residue_constant_verifier
import crc32_pkg::*;
import residue_pkg::*;
(
input logic clk,
input logic rst_n,
output logic [31:0] residue_derived,
output logic residue_consistent
);
// Two unrelated messages of different lengths. If the residue depends
// on the message, these disagree -- which would mean the derivation is
// wrong, or a convention is applied in the wrong place.
localparam logic [7:0] MSG_A [3] = '{8'h61, 8'h62, 8'h63};
localparam logic [7:0] MSG_B [9] = '{8'h31, 8'h32, 8'h33, 8'h34, 8'h35,
8'h36, 8'h37, 8'h38, 8'h39};
localparam logic [31:0] RES_A = derive_residue(MSG_A, 3);
localparam logic [31:0] RES_B = derive_residue(MSG_B, 9);
assign residue_derived = RES_A;
assign residue_consistent = (RES_A == RES_B);
// The two checks that make this a verifier rather than a calculator.
// synopsys translate_off
// (1) The residue really is message-independent. If this fails, the
// whole approach of Section 4 is invalid for this configuration.
a_residue_message_independent: assert final (RES_A == RES_B)
else $fatal(1, "residue depends on the message -- derivation or conventions are wrong");
// (2) And, for Ethernet's conventions specifically, it lands on the
// published value. This catches a derivation that is
// self-consistent and wrong -- two errors that cancel.
a_residue_matches_published: assert final
((INIT_VALUE != 32'hFFFF_FFFF) ||
(REFLECT_IN != 1'b1) ||
(REFLECT_OUT != 1'b1) ||
(XOR_OUT != 32'hFFFF_FFFF) ||
(RES_A == 32'hDEBB_20E3))
else $fatal(1, "Ethernet conventions but the derived residue is not 0xDEBB20E3");
// synopsys translate_on
endmoduleClassification: synthesizable; the derivation is elaboration-time.
What it teaches: that a derivation needs two checks, not one. a_residue_message_independent verifies the property the approach depends on — if the residue moved with the message, Section 4's checker would be nonsense. a_residue_matches_published verifies the derivation against an outside value, but only when the conventions are Ethernet's, which is what the guard expression does. That structure is the point: the module works for any configuration, and cross-checks against a published number in the one configuration where a published number exists.
Deliberately simplified: two test messages. Three or four cost nothing at elaboration and would catch a derivation that happens to agree on two.
Production implication: the octet-order comment marks the one place a mistake produces a stable wrong answer, which is the most dangerous kind. Reverse the four octets and the derivation still yields a message-independent constant — a_residue_message_independent passes — and the design tests every frame against a value no transmitter produces. Only the comparison against the published number catches it, which is why the second assertion exists and why the guard is written to keep it active for exactly the configuration that can be checked.
8. What the Residue Buys in a Stream
A frame that ends at the wrong moment is the case where the two designs stop being equivalent in effort.
The residue receiver needs no special case at all. Carrier drops, the register holds whatever it holds, and it is not the residue — because a truncated codeword is not divisible. The frame is rejected by the ordinary path, with no branch added and no state to unwind.
The comparing receiver has a partially resolved decision. It has four octets in a delay line that it was going to classify as payload or check sequence depending on where the frame ended, and the frame ended somewhere unexpected. Those four octets now have no defined role, and the design must decide explicitly what to do — which is a branch, in the end-of-frame path, on the least-tested condition in the receiver.
And Chapter 5.8 §4 already made the argument for what that branch must not do: it must not report a check-sequence mismatch, because a truncated frame has no completed computation to disagree with, and calling a truncation a corruption sends the investigation to the wrong layer.
So the residue's advantage here is not fewer gates — it is fewer decisions. The rejection falls out of the arithmetic rather than out of control logic, and arithmetic does not have an untested branch.
9. RTL 4 — Knowing a Frame Is Already Wrong
// SYNTHESIZABLE.
//
// Detects, DURING a frame, that it cannot end validly -- and does so for
// one specific, useful case.
//
// The general question is not decidable: any register value can still be
// steered to the residue by the octets that follow, because the FCS the
// transmitter appended is exactly what steers it. So there is no
// mid-frame test for "this frame is doomed" in general.
//
// What IS decidable is the case that matters in practice: a receiver that
// already knows how many octets remain -- from a length field
// (Chapter 5.5) or from a store-and-forward buffer -- can ask whether the
// remaining octets are enough. Zero octets left and a register that is
// not the residue is a definite answer, available before end of frame.
//
// The value is latency: a cut-through path can stop forwarding at the
// moment the answer is known rather than at the frame's end.
module early_terminate_detector
import crc32_pkg::*;
#(
parameter logic [31:0] RESIDUE = 32'hDEBB_20E3
) (
input logic clk,
input logic rst_n,
input logic frame_start,
input logic oct_consumed,
input logic [31:0] crc_reg, // the live register, Section 4's
// Remaining octets, where known. Held low when unknown, which is the
// common case on a pure streaming receiver and must be distinguishable
// from "zero remaining".
input logic remaining_valid,
input logic [13:0] remaining_octets,
output logic doomed, // cannot end validly
output logic doomed_valid,
output logic [13:0] octets_saved // how early the answer came
);
logic [13:0] consumed_q;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
consumed_q <= '0;
doomed <= 1'b0;
doomed_valid <= 1'b0;
octets_saved <= '0;
end else begin
doomed_valid <= 1'b0;
if (frame_start) begin
consumed_q <= '0;
doomed <= 1'b0;
end else begin
if (oct_consumed) consumed_q <= consumed_q + 1'b1;
// The one decidable case, and the guard on remaining_valid is
// what keeps it honest: without it, an unknown remaining count
// reads as zero and every frame is declared doomed at its start.
if (remaining_valid && (remaining_octets == '0) &&
(crc_reg != RESIDUE) && !doomed) begin
doomed <= 1'b1;
doomed_valid <= 1'b1;
octets_saved <= '0;
end
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that "is this frame already doomed" is undecidable in general and the module says so rather than approximating. Any register value can still be driven to the residue by the octets that follow — that is precisely what the transmitter's appended check sequence does. So there is no mid-frame test for future validity, and a design that invents one will discard good frames.
Deliberately simplified: it uses only the remaining-octet count. A store-and-forward receiver that already holds the whole frame can answer the question completely, and does not need this module at all.
Production implication: the remaining_valid guard is the difference between a useful detector and one that condemns every frame. An unknown remaining count must not read as zero, and the failure of a design that omits the guard is total and immediate — which is the least dangerous kind, and the reason this particular trap tends to be caught. The more insidious version is a remaining_octets that is correct for untagged frames and wrong for tagged ones, because Chapter 5.5 §8's displaced length field was read at a fixed offset.
10. RTL 5 — Running Both, and Requiring Them to Agree
// SYNTHESIZABLE MONITOR.
//
// Runs the residue check and the comparison check side by side and
// asserts they always agree.
//
// Why bother, when Section 2 argues they are algebraically equivalent:
// because the equivalence is a property of the MATHEMATICS and the two
// implementations are different CIRCUITS. The comparison path has a
// delay line, an octet-assembly order and a payload/FCS decision that the
// residue path does not; each is a place to be wrong.
//
// And when they disagree, the monitor says WHICH is likely wrong, from
// the shape of the disagreement:
//
// residue rejects, compare accepts -> the residue constant is wrong
// for this configuration, OR the compare path assembled the FCS
// octets in the wrong order and got lucky
// residue accepts, compare rejects -> almost always the compare path's
// octet order or delay-line alignment
module residue_vs_compare_monitor
#(
parameter int unsigned CNT_W = 32
) (
input logic clk,
input logic rst_n,
input logic clear,
input logic res_valid,
input logic res_ok,
input logic [31:0] res_observed,
input logic cmp_valid,
input logic cmp_ok,
input logic [31:0] cmp_computed,
input logic [31:0] cmp_received,
output logic disagree,
output logic suspect_residue_constant,
output logic suspect_compare_path,
output logic [CNT_W-1:0] c_disagree,
// The single most useful diagnostic in the module: when the residue
// path rejects everything, is the observed value CONSTANT? A constant
// wrong value is a configuration error; a varying one is real
// corruption.
output logic observed_is_constant,
output logic [31:0] first_observed,
output logic first_observed_valid
);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
disagree <= 1'b0;
suspect_residue_constant <= 1'b0;
suspect_compare_path <= 1'b0;
c_disagree <= '0;
observed_is_constant <= 1'b1;
first_observed <= '0;
first_observed_valid <= 1'b0;
end else begin
disagree <= 1'b0;
suspect_residue_constant <= 1'b0;
suspect_compare_path <= 1'b0;
if (clear) begin
c_disagree <= '0;
// The observed-value evidence deliberately survives: it is the
// thing that distinguishes a configuration error from a link
// fault, and it is destroyed by a routine counter clear.
end
if (res_valid && cmp_valid) begin
if (res_ok != cmp_ok) begin
disagree <= 1'b1;
if (!(&c_disagree)) c_disagree <= c_disagree + 1'b1;
if (!res_ok && cmp_ok) suspect_residue_constant <= 1'b1;
if (res_ok && !cmp_ok) suspect_compare_path <= 1'b1;
end
// Track whether the residue path's observed value ever varies.
if (!res_ok) begin
if (!first_observed_valid) begin
first_observed <= res_observed;
first_observed_valid <= 1'b1;
end else if (res_observed != first_observed) begin
observed_is_constant <= 1'b0;
end
end
end
end
end
endmoduleClassification: synthesizable monitor.
What it teaches: that observed_is_constant separates the two failures that both present as "no frames pass". A wrong residue constant makes every valid frame land on the same wrong value — so the observed residue is identical on every frame. Real corruption makes it land somewhere different each time. One bit distinguishes a configuration error from a dead link, and without it both look the same from every counter in the system.
Deliberately simplified: it assumes both checkers see the same frame. A design where the comparison runs on a slower diagnostic path needs frame tagging so the two results can be matched up.
Production implication: the two suspect_* outputs encode an asymmetry worth knowing. A disagreement in which the residue rejects and the comparison accepts points at the residue constant, because the comparison path never uses it. The reverse points at the comparison path, because it has three structures — delay line, octet assembly, payload decision — that the residue path does not have at all. The design with more moving parts is the more likely suspect, and encoding that prior into the monitor saves the first hour of every investigation.
11. The Constant Is Convention-Dependent, and That Is the Whole Risk
Collecting what Sections 3 and 7 establish, because it is the chapter's practical warning:
The residue is a function of four things, none of which appears anywhere near the checker in a typical design: the initial value, the input reflection, the output reflection, and the final complement — plus a fifth, the point in the output pipeline at which the register is sampled.
Change any one and the correct constant changes. And the change is invisible: the engine still works, the transmit path still produces correct check sequences, and only the receiver's constant is now wrong.
The failure is total and looks like something else. Every frame is rejected, including perfect ones. There are no errors on the wire, the link is up, the peer is fine, and the symptom is indistinguishable from a dead receive path.
Three defences, in increasing order of value:
A comment naming the conventions — better than nothing and load-bearing on nobody reading it.
An assertion comparing the constant against a published value — useful for Ethernet's exact configuration and silent for every other, because there is no published value to compare against.
Deriving the constant from the same parameters the datapath uses — Section 7's module, which makes the constant a function rather than a literal, so a convention change moves both together and cannot produce a mismatch at all.
12. Assertions — About the Property, Not About the Number
// ---------------------------------------------------------------------
// P1 -- THE CENTRAL PROPERTY, and note it does not mention a value. The
// residue is the same for every valid frame, whatever the frame
// contained. Written this way it survives every convention change.
// ---------------------------------------------------------------------
property p_residue_is_message_independent;
@(posedge clk) disable iff (!rst_n)
(check_valid && frame_was_valid) |-> (residue_observed == $past(last_valid_residue));
endproperty
a_residue_is_message_independent: assert property (p_residue_is_message_independent);
// ---------------------------------------------------------------------
// P2 -- The residue is length-independent too. A separate property,
// because a design can be message-independent and length-dependent if a
// convention is applied per-octet where it should be per-frame.
// ---------------------------------------------------------------------
property p_residue_is_length_independent;
@(posedge clk) disable iff (!rst_n)
(check_valid && frame_was_valid && (frame_octets != $past(frame_octets)))
|-> (residue_observed == $past(residue_observed));
endproperty
a_residue_is_length_independent: assert property (p_residue_is_length_independent);
// ---------------------------------------------------------------------
// P3 -- A valid frame is accepted. The residue constant matches what the
// derivation produced -- and RESIDUE is the DERIVED parameter, not a
// literal, which is what makes this property meaningful.
// ---------------------------------------------------------------------
property p_valid_frame_accepted;
@(posedge clk) disable iff (!rst_n)
(check_valid && frame_was_valid) |-> fcs_ok;
endproperty
a_valid_frame_accepted: assert property (p_valid_frame_accepted);
// ---------------------------------------------------------------------
// P4 -- The derived residue is message-independent at ELABORATION. If it
// were not, Section 4's whole approach would be invalid.
// ---------------------------------------------------------------------
// synopsys translate_off
a_derivation_consistent: assert final (RES_A == RES_B)
else $fatal(1, "derived residue depends on the message");
// synopsys translate_on
// ---------------------------------------------------------------------
// P5 -- And, for Ethernet's conventions specifically, the derivation
// lands on the published number. GUARDED, so the module stays usable for
// other configurations rather than asserting a value they do not have.
// ---------------------------------------------------------------------
// synopsys translate_off
a_derivation_matches_published: assert final
((INIT_VALUE != 32'hFFFF_FFFF) || (REFLECT_IN != 1'b1) ||
(REFLECT_OUT != 1'b1) || (XOR_OUT != 32'hFFFF_FFFF) ||
(RES_A == 32'hDEBB_20E3));
// synopsys translate_on
// ---------------------------------------------------------------------
// P6 -- The three published readings are one value. Constant
// relationships, asserted so an edit to any of them is caught here.
// ---------------------------------------------------------------------
// synopsys translate_off
a_readings_consistent: assert final
((reflect32(32'hDEBB_20E3) == 32'hC704_DD7B) &&
((32'h2144_DF1C ^ 32'hFFFF_FFFF) == 32'hDEBB_20E3));
// synopsys translate_on
// ---------------------------------------------------------------------
// P7 -- The residue checker holds no captured value. Written over the
// interface: there is no port carrying an assembled received FCS,
// because the design never assembles one.
// ---------------------------------------------------------------------
property p_no_captured_fcs;
@(posedge clk) disable iff (!rst_n)
check_valid |-> !fcs_capture_register_written;
endproperty
a_no_captured_fcs: assert property (p_no_captured_fcs);
// ---------------------------------------------------------------------
// P8 -- The register is preset every frame. Carry-over from the previous
// frame makes the residue history-dependent, which is the one way it can
// stop being constant.
// ---------------------------------------------------------------------
property p_preset_every_frame;
@(posedge clk) disable iff (!rst_n)
frame_start |=> (crc_reg == INIT_VALUE);
endproperty
a_preset_every_frame: assert property (p_preset_every_frame);
// ---------------------------------------------------------------------
// P9 -- A truncated frame is rejected by the ORDINARY path, with no
// special case. This is Section 8's structural claim, asserted.
// ---------------------------------------------------------------------
property p_truncated_frame_rejected;
@(posedge clk) disable iff (!rst_n)
(check_valid && frame_truncated) |-> !fcs_ok;
endproperty
a_truncated_frame_rejected: assert property (p_truncated_frame_rejected);
// ---------------------------------------------------------------------
// P10 -- A truncated frame is not reported as a mismatch by the layer
// above (Chapter 5.8 §4). The residue path must not manufacture one.
// ---------------------------------------------------------------------
property p_truncation_not_a_mismatch;
@(posedge clk) disable iff (!rst_n)
frame_truncated |-> !report_fcs_mismatch;
endproperty
a_truncation_not_a_mismatch: assert property (p_truncation_not_a_mismatch);
// ---------------------------------------------------------------------
// P11 -- The early-terminate detector never condemns a frame while the
// remaining count is unknown. Without this guard every frame is doomed
// at its start.
// ---------------------------------------------------------------------
property p_no_doom_without_remaining;
@(posedge clk) disable iff (!rst_n)
doomed_valid |-> $past(remaining_valid);
endproperty
a_no_doom_without_remaining: assert property (p_no_doom_without_remaining);
// ---------------------------------------------------------------------
// P12 -- Doom is sticky within a frame and cleared at the next start. A
// frame declared doomed cannot un-doom itself.
// ---------------------------------------------------------------------
property p_doom_sticky_within_frame;
@(posedge clk) disable iff (!rst_n)
(doomed && !frame_start) |=> doomed;
endproperty
a_doom_sticky_within_frame: assert property (p_doom_sticky_within_frame);
// ---------------------------------------------------------------------
// P13 -- THE EQUIVALENCE. Residue and comparison agree on every frame.
// The property that makes running both worthwhile.
// ---------------------------------------------------------------------
property p_residue_equals_compare;
@(posedge clk) disable iff (!rst_n)
(res_valid && cmp_valid) |-> (res_ok == cmp_ok);
endproperty
a_residue_equals_compare: assert property (p_residue_equals_compare)
else $error("residue and comparison disagree -- one path is misconfigured");
// ---------------------------------------------------------------------
// P14 -- The disagreement verdicts are exclusive and complete.
// ---------------------------------------------------------------------
property p_disagree_verdicts_exclusive;
@(posedge clk) disable iff (!rst_n)
disagree |-> $onehot({suspect_residue_constant, suspect_compare_path});
endproperty
a_disagree_verdicts_exclusive: assert property (p_disagree_verdicts_exclusive);
// ---------------------------------------------------------------------
// P15 -- A constant observed residue across rejected frames is evidence
// of misconfiguration, not corruption. Asserted as an implication so the
// monitor cannot report both.
// ---------------------------------------------------------------------
property p_constant_observed_implies_config;
@(posedge clk) disable iff (!rst_n)
(observed_is_constant && (c_disagree != '0)) |-> suspect_residue_constant;
endproperty
a_constant_observed_implies_config: assert property (p_constant_observed_implies_config);
// ---------------------------------------------------------------------
// P16 -- The evidence survives a counter clear.
// ---------------------------------------------------------------------
property p_evidence_survives_clear;
@(posedge clk) disable iff (!rst_n)
clear |=> ($stable(first_observed) && $stable(observed_is_constant));
endproperty
a_evidence_survives_clear: assert property (p_evidence_survives_clear);
// ---------------------------------------------------------------------
// P17 -- The residue checker never needs to know where the payload
// ended. Written over the interface: no signal marking the payload's
// last octet reaches this module at all.
// ---------------------------------------------------------------------
property p_no_payload_end_dependency;
@(posedge clk) disable iff (!rst_n)
check_valid |-> !payload_end_used_by_checker;
endproperty
a_no_payload_end_dependency: assert property (p_no_payload_end_dependency);
// ---------------------------------------------------------------------
// P18 -- A frame with an appended octet is rejected. The trailing-zero
// case of Chapter 6.2 §6, which the final complement exists to catch and
// which a residue checker inherits for free.
// ---------------------------------------------------------------------
property p_appended_octet_rejected;
@(posedge clk) disable iff (!rst_n)
(check_valid && frame_had_trailing_octet) |-> !fcs_ok;
endproperty
a_appended_octet_rejected: assert property (p_appended_octet_rejected);
// ---------------------------------------------------------------------
// P19 -- The observed residue is reported on every check, pass or fail.
// A checker that exposes it only on failure cannot establish that the
// value is CONSTANT across failures, which is Section 10's diagnostic.
// ---------------------------------------------------------------------
property p_observed_always_reported;
@(posedge clk) disable iff (!rst_n)
check_valid |-> !$isunknown(residue_observed);
endproperty
a_observed_always_reported: assert property (p_observed_always_reported);
// ---------------------------------------------------------------------
// P20 -- COVERAGE. Valid frames at several lengths, which is what makes
// P2 meaningful rather than vacuous.
// ---------------------------------------------------------------------
c_lengths_swept: cover property (
@(posedge clk) disable iff (!rst_n) three_distinct_valid_lengths_seen
);
// ---------------------------------------------------------------------
// P21 -- COVERAGE. A disagreement between the two paths. A run that
// never produced one never tested Section 10.
// ---------------------------------------------------------------------
c_paths_disagreed: cover property (
@(posedge clk) disable iff (!rst_n) disagree
);13. Verification — Twenty-Four Scenarios and a Constant That Must Be Derived
| # | Scenario | Stimulus | What must be observed |
|---|---|---|---|
| 1 | Valid minimum frame | 60 octets + correct FCS | fcs_ok; residue_observed = 0xDEBB20E3 |
| 2 | Valid maximum frame | 1514 octets + correct FCS | the same residue (P2) |
| 3 | Valid zero-payload codeword | FCS only | the same residue — length does not enter |
| 4 | Length sweep | valid frames at 4 distinct lengths | residue identical at all four (P2) |
| 5 | Content sweep | valid frames of equal length, different data | residue identical (P1) |
| 6 | Single bit flipped in payload | one bit inverted | residue differs; fcs_ok low |
| 7 | Single bit flipped in the FCS | one bit inverted in the check octets | residue differs — the FCS is checked though not covered |
| 8 | Truncated frame | carrier drops mid-frame | rejected by the ordinary path (P9); no mismatch report (P10) |
| 9 | Trailing octet appended | valid frame plus one octet | rejected — the residue moved (Chapter 6.2 §6) |
| 10 | Back-to-back frames | two valid frames, no idle | both accepted; register preset between (P8) |
| 11 | No preset between frames | force the preset off | the second frame's residue is history-dependent — P8 fires |
| 12 | Derivation consistency | elaboration, two messages | RES_A == RES_B (P4) |
| 13 | Derivation against published | Ethernet conventions | derived value is 0xDEBB20E3 (P5) |
| 14 | Derivation under a changed INIT | override INIT = 0 | derived residue moves; P5's guard disables the published check |
| 15 | Derivation under changed reflection | override REF_OUT = 0 | derived residue moves; still message-independent (P4) |
| 16 | Reversed FCS octet order in the derivation | swap the four octets | still message-independent — P4 passes; P5 fires |
| 17 | The three readings | elaboration | the reflection and complement relations hold (P6) |
| 18 | Early terminate, count known | remaining = 0, register wrong | doomed; doomed_valid (P11) |
| 19 | Early terminate, count unknown | remaining_valid low | never doomed (P11) |
| 20 | Doom is sticky | doomed, then more octets | stays doomed until frame_start (P12) |
| 21 | Both paths agree, valid frames | 1000 valid frames | res_ok == cmp_ok every time (P13) |
| 22 | Wrong residue constant | override RESIDUE by one bit | every frame rejected; observed_is_constant high (P15) |
| 23 | Real corruption | random bit errors | every frame rejected; observed_is_constant low |
| 24 | Compare path octet order reversed | swap the assembly order | suspect_compare_path (P14) |
14. Debugging — When Nothing Passes
Symptom — every received frame is rejected, and the link is otherwise healthy.
Read residue_observed before anything else. If it is the same value on every frame, this is a configuration error, not a link fault — a valid frame under a mismatched constant lands somewhere wrong and repeatable. Compare that constant value against the three readings of Section 3: if it is 0xC704DD7B where the design expects 0xDEBB20E3, the sample point or the output reflection is the disagreement. If it is 0x2144DF1C, the design is sampling after the final complement and testing against the value from before it.
Symptom — every frame is rejected and the observed residue varies.
Now it is corruption, or a receive datapath that is mangling octets. Fall back to Chapter 5.8's difference-pattern analysis, which needs the comparison path — and this is exactly where a residue-only receiver is blind, because it never computes the value that a difference pattern is computed from.
Symptom — frames pass in simulation and fail in hardware, with no RTL change.
Suspect a parameter that did not propagate. The residue constant is typically a parameter on a deeply instantiated module, and an integration wrapper that overrides the engine's conventions without overriding the checker's constant produces exactly this. Section 7's derivation makes the case impossible, which is the argument for it; in a design that quotes the constant, compare the elaborated value in hardware against the one in simulation.
Symptom — a design accepts frames that a reference implementation rejects.
Check for a residue constant that was derived with the FCS octets in the wrong order. Such a derivation is self-consistent — message-independent, stable, and produced by the same code the datapath uses — so every internal check passes. Only a comparison against an outside value catches it, which is scenario 16 and the reason Section 7 carries a second assertion.
Symptom — the early-terminate detector condemns every frame.
remaining_valid is being tied high with an unknown count, or the count is derived from a length field read at a fixed offset on tagged frames — Chapter 5.5 §8's displacement problem, arriving in a new place. The tell is whether untagged frames pass: if they do and tagged ones do not, the offset is the fault and the detector is behaving correctly on bad input.
Symptom — the residue and comparison paths disagree, intermittently.
Intermittent disagreement is not a configuration error — those are total. Look for a frame-boundary race: the two paths sample at slightly different points, and a frame that ends unusually (truncated, or back-to-back with no idle) resolves them differently. Check whether the disagreements correlate with frame spacing rather than with content.
15. Common Misconceptions
"A receiver recomputes the CRC and compares it with the received one."
The wrong model: checking is generation followed by an equality test.
What it costs: you cannot explain why a receiver needs no logic to find the four check octets, and you carry a mental model with a delay line, a capture register and a payload/FCS decision that a real receiver does not have.
The corrected model: a receiver feeds everything through the same division — data and check sequence alike — and tests the register against a fixed constant. Nothing is held, nothing is captured, and the receiver never needs to know where the payload ended.
"The residue should be zero."
The wrong model: a valid codeword divides exactly, so the remainder is nothing.
What it costs: a checker built for zero rejects every frame, and the engineer concludes the link is dead rather than that the constant is wrong.
The corrected model: it would be zero with a zero initial value and no final complement. Ethernet has neither, and both shift the landing point — without making it depend on the message. So the result is still a constant, and the constant is 0xDEBB20E3 at the raw-register sample point.
"0xC704DD7B is the Ethernet residue."
The wrong model: one published number is the answer.
What it costs: a checker that rejects every frame, because that number is the residue read most-significant-bit first and most engines sample it reflected.
The corrected model: three constants, one residue. 0xDEBB20E3 raw and reflected, 0xC704DD7B the same value MSB-first, 0x2144DF1C after the final complement — related by reflect32 and by exclusive-or with 0xFFFFFFFF. Which one is correct depends on where your design samples the register, and all three are right somewhere.
"Checking by residue is stronger than comparing."
The wrong model: the clever method detects more.
What it costs: you make an implementation decision on false grounds, and you may reach for the residue in a design where comparison is genuinely better — a store-and-forward device that has to strip the FCS anyway, or one whose diagnostics need the computed value.
The corrected model: the two are algebraically equivalent and detect exactly the same errors. The residue is cheaper and needs fewer decisions; the comparison yields the computed value that Chapter 5.8's difference-pattern diagnostics require. It is an implementation trade with no effect on detection at all.
"Hard-coding the residue is fine — it never changes."
The wrong model: a standard constant is safe to write as a literal.
What it costs: Section 12's rejected property. The constant is a function of four conventions plus a sample point; change any of them and the literal is silently wrong, the receiver rejects every frame, and the assertion that quoted the same literal fails to fire.
The corrected model: derive it from the same parameters the datapath uses, so a convention change moves both together — then compare the derivation against the published number for Ethernet's configuration only, which catches a derivation that is self-consistent and wrong.
16. Interview Reasoning
"How does a receiver check the frame check sequence?"
The weak answer is "it recomputes and compares". The answer that ends the topic is that it does not compare: it runs the same engine over the data and the check sequence and tests the register against a fixed constant, because a valid codeword is divisible by construction so the result stops depending on the message. The payoff is what falls out — no held value, no captured FCS, and no need to know where the payload ended, which is the real saving in a stream whose end is signalled by carrier dropping.
"Why isn't the residue zero?"
Because Ethernet does not start the register at zero and does not transmit the bare remainder. The initial value of 0xFFFFFFFF and the final complement both shift the landing point — and neither shifts it anywhere message-dependent, so the result is still a constant, just not zero. The strong close names the value at the sample point, 0xDEBB20E3, and notes that the same residue reads as 0xC704DD7B or 0x2144DF1C depending on reflection and complement.
"A receiver rejects every frame. Where do you start?"
Not at the physical layer. The first question is whether the observed residue is the same on every frame: a constant wrong value is a configuration error, a varying one is corruption. That single bit separates "the residue constant does not match this design's conventions" from "the link is broken", and the two have nothing in common. Adding that a wrong constant produces total, symptomless rejection — no errors on the wire, link up, peer fine — shows why it gets misdiagnosed as a dead receiver.
"Would you assert that the residue equals 0xDEBB20E3?"
Not as the primary property. The value is derivable from four conventions and a sample point, so a literal in the property is a second copy of the literal in the design — and two copies of an assumption can only agree, including when both are wrong. Change a convention and the checker breaks while the assertion stays silent. The right properties are that the residue is the same for every valid frame at every length, without naming a value; that the derivation is message-independent at elaboration; and a guarded comparison against the published number for Ethernet's exact configuration.
17. Understanding Check
Because the transmitter appended exactly the remainder that makes the codeword divisible.
That is what a remainder is: divide the message by the generator, take what is left, append it, and the leftover has been subtracted off. So dividing the whole codeword — data and check sequence together — leaves nothing that depends on the message.
Which is the property the whole approach rests on. Every valid frame lands on the same register value, whatever it contained and however long it was. Verified across frames of 0, 1, 19 and 64 octets: 0xDEBB20E3 every time.
It is not zero only because of the conventions. A zero initial value and no final complement would give zero; Ethernet has neither, and both shift the landing point without making it message-dependent.
And the receiver therefore needs no comparison at all — just a 32-bit equality against a constant known at elaboration, which is cheaper and faster than an equality against a value captured during the frame.
18. What's Next
The claim this chapter defended: a receiver tests for a constant it never computed, and the constant is a fingerprint of every convention in the design.
The mechanism is one property of division: a valid codeword is divisible by construction, so dividing data and check sequence together produces a result that no longer depends on the message. Every valid frame lands on the same value at every length. That removes the held register, the captured check sequence, the delay line, and — most usefully in a stream — the need to know where the payload ended at all, which is why a truncated frame is rejected by arithmetic rather than by a branch.
And the cost is a constant that is silently conditional on four conventions plus a sample point, presenting the same residue as 0xDEBB20E3, 0xC704DD7B or 0x2144DF1C. A design that writes one of those as a literal has a second copy of an assumption, and two copies can only agree — including when both are wrong. Derive it, and check the derivation twice: once for message independence, once against the published number in the one configuration that has one.
Chapter 6.4 — A Parallel CRC-32 Engine in RTL removes the constraint both this chapter and the last have quietly worked under: one bit per clock. At 10 Gb/s that is a 10 GHz register, which does not exist, so a real engine consumes eight, thirty-two or sixty-four bits per cycle.
The transformation is exact and mechanical — the next-state function for n bits is the serial function composed with itself n times, and it can be generated rather than written. What is not mechanical is the two places designs get it wrong: a final partial word, where a frame's length is not a multiple of the datapath width, and the byte-enable path at that last transfer.
And 6.4 closes Module 6 with the only correct way to sign such an engine off — equivalence against the bit-serial reference of Chapter 6.2 — together with the trap that makes that sign-off worthless: a reference model generated from the same matrix as the design.
The full path is on the Ethernet curriculum index.
Continue learning
Related tutorials
- Related topic
One Frame, End to End
A frame's journey down the stack and back up the other side, stage by stage. The transmit path decides and the receive path must discover — at four layers, not one — and that asymmetry is why the receive half of every Ethernet design is the larger, later and buggier one.
- 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-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.
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.
