Ethernet · Module 5
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.
Four octets close the frame. Chapter 5.1 placed them last and made an argument out of the position: the check sequence is at the end by construction, so every decision a receiver made before it arrived was provisional.
This chapter asks a different question, and it is the one that decides what the field is worth: what exactly does it cover?
The instinct is "the frame". That answer is close enough to be useful and wrong in three specific ways, each of which produces a distinct class of undetectable failure:
- it does not cover the preamble or start delimiter;
- it does not cover itself;
- and — the one that matters most and is least often stated — it does not cover anything that happens outside the interval between the point where it is appended and the point where it is checked.
That last exclusion is not a gap in the mechanism. It is the mechanism's definition. A check sequence protects a range, and the range's two ends are locations in a datapath, not locations in a frame.
1. Scope — What This Chapter Owns, and What Module 6 Owns
This boundary needs stating first, because the obvious content of this chapter belongs to a later one.
Module 6 owns the mathematics. How CRC-32 is generated as polynomial division in a shift register, the initial-value, reflection and complement conventions that make independent implementations disagree, the residue property that lets a receiver check without comparing, and how a serial definition becomes a byte- or word-wide parallel engine — those are Chapter 6.2, Chapter 6.3 and Chapter 6.4, and this chapter deliberately does not touch them.
Chapter 6.1 owns the error model and what a detector can promise — the difference between "detects errors" and a stated undetected-error probability.
This chapter owns the field as a structural element: what range of octets it covers, where in a transmit datapath it is appended, where in a receive datapath it is verified, what falls outside those two points, and what a design can say about a frame based on the result.
In other words: Module 6 asks how well the check works. This chapter asks what the check is pointed at. Both questions have to be answered, and they fail independently — a perfect CRC over the wrong range is worth nothing, and the wrong range is much easier to build by accident.
It does not own: the frame's field order (Chapter 5.1), padding (Chapter 5.6), or the switching architecture that determines whether a device is store-and-forward or cut-through, which is a switching-module subject that Section 9 touches only where it changes what the check means.
The question this chapter answers that its neighbours do not: between which two points is a frame actually protected, and what is a valid check sequence evidence of?
2. The Covered Range, Stated Exactly
The range is contiguous and it starts and ends at points that are easy to get wrong by one octet in either direction. Two of the three failures are silent:
Starting one octet early — including the last octet of the start delimiter — produces a transmitter whose frames every conforming receiver rejects. Loud, and found in the first minute of bring-up.
Starting one octet late — omitting the first address octet — produces frames that this design accepts and others do not, and it makes a corruption of the first address octet undetectable. That octet carries the individual/group flag (Chapter 5.3), so an error there redirects the frame between two entirely different receive pipelines. Quiet, and severe.
Including the check sequence in its own computation is the classic one and it does not fail the way people expect. It produces a self-consistent scheme in which the transmitter and receiver agree with each other and with nobody else — which passes every loopback test, every simulation with a matching reference model, and no interoperability test at all.
3. Why Each Exclusion Is Deliberate
The three exclusions look like an arbitrary list and each has a reason that says something about the layer.
The preamble is excluded because it does not survive. Chapter 5.2 showed the preamble consumed by the receiving physical layer and regenerated by the next transmitter, with a length that varies along a path. A check computed over it would be invalid on arrival at every hop by design, and the octets it protects are ones no receiver ever forwards.
The gap is excluded because it is not data. It is an absence — Chapter 5.9's subject — and there is nothing there to protect.
The field cannot cover itself, and the reason is worth stating precisely rather than waving at circularity. The value is the result of a computation over the covered octets. Including itself would require the result to be a fixed point of its own function, which for a general input is not solvable — there is no value that, when appended, produces itself.
And the pad is included, which is the one people find surprising. Chapter 5.6 established that the pad is not the client's data and is never delivered. It is still transmitted, and a corrupted pad octet means the frame was damaged — which is information about the link, whatever the pad's contents were meant to be. Excluding it would create a region of every short frame in which corruption is invisible, and the frames concerned are the most numerous ones on most links.
4. RTL 1 — Marking the Covered Range Explicitly
// SYNTHESIZABLE.
//
// Produces the enable that gates a CRC engine, and nothing else.
//
// The range is defined by two events, not by two counts:
//
// START -- the first octet of the destination address, which is the
// first octet AFTER the start delimiter.
// END -- the last octet of the client-data region, INCLUDING pad.
//
// Defining it by counts instead is the standard bug: a count assumes a
// known frame length, and the length is not known until the frame ends.
package fcs_pkg;
localparam int unsigned FCS_OCTETS = 4;
typedef enum logic [2:0] {
COV_IDLE, // between frames
COV_PREAMBLE, // preamble and delimiter -- NOT covered
COV_RANGE, // addresses through pad -- covered
COV_FCS, // the field itself -- NOT covered
COV_ERROR // the range never closed
} cov_state_e;
endpackage
module fcs_coverage_boundary
import fcs_pkg::*;
(
input logic clk,
input logic rst_n,
input logic sfd_seen, // start delimiter recognised (Chapter 5.2)
input logic octet_valid,
input logic last_data_octet, // last octet of client data or pad
input logic carrier_lost,
// THE output. High for exactly the octets a CRC engine must consume.
output logic crc_enable,
output cov_state_e state,
// Octets covered in this frame. Compared against the frame's own
// accounting by Section 10, which is how a one-octet range error is
// caught without needing a peer that disagrees.
output logic [13:0] covered_count,
output logic covered_valid,
// The range opened and the frame ended without it closing: a truncated
// frame, and the CRC over a partial range is meaningless rather than
// wrong. Reported so it is not silently checked anyway.
output logic range_unterminated
);
cov_state_e state_q;
logic [13:0] count_q;
logic [2:0] fcs_q;
assign state = state_q;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state_q <= COV_IDLE;
count_q <= '0;
fcs_q <= '0;
crc_enable <= 1'b0;
covered_count <= '0;
covered_valid <= 1'b0;
range_unterminated <= 1'b0;
end else begin
crc_enable <= 1'b0;
covered_valid <= 1'b0;
range_unterminated <= 1'b0;
if (carrier_lost && (state_q == COV_RANGE)) begin
// The range opened and never closed. NOT a CRC failure -- there is
// no complete computation to fail. Reporting it as one attributes
// a truncation to a corruption, which sends the investigation to
// the wrong layer entirely.
state_q <= COV_ERROR;
range_unterminated <= 1'b1;
end else begin
case (state_q)
COV_IDLE: if (sfd_seen) begin
// Coverage begins on the octet AFTER the delimiter. Starting
// on the delimiter itself is the loud failure; starting one
// octet later is the quiet one (Section 2).
state_q <= COV_RANGE;
count_q <= '0;
end
COV_RANGE: if (octet_valid) begin
crc_enable <= 1'b1;
count_q <= count_q + 1'b1;
if (last_data_octet) begin
state_q <= COV_FCS;
fcs_q <= '0;
covered_count <= count_q + 1'b1;
covered_valid <= 1'b1;
end
end
COV_FCS: if (octet_valid) begin
// crc_enable stays LOW here. The four octets of the field are
// on the bus and must not enter the computation that produced
// them -- Section 3's fixed-point argument.
if (fcs_q == 3'(FCS_OCTETS - 1)) state_q <= COV_IDLE;
else fcs_q <= fcs_q + 1'b1;
end
COV_ERROR: if (!carrier_lost) state_q <= COV_IDLE;
default: state_q <= COV_IDLE;
endcase
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that the range is delimited by events and not by counts. A count-based implementation needs the frame length in advance, and a receiver does not have it — the frame ends when carrier ends. Worse, a count that is one too large consumes the first octet of the check sequence into the computation, which produces the self-consistent scheme of Section 2 that passes every test involving only this design.
Deliberately simplified: last_data_octet arrives as an input. Deriving it is Chapter 5.6's problem on transmit, where the pad's end is known, and on receive it is simply the octet four before the end of carrier — which is why a receiver must delay by four octets to know where the payload stopped.
Production implication: range_unterminated is separated from any CRC result because a truncated frame has no meaningful check to fail. Reporting it as a CRC error attributes a truncation to a corruption; both indicate a problem on the link, but they point at different faults — truncation at a carrier or buffer event, corruption at a signal-integrity one — and merging them costs the distinction.
5. The Boundaries Are Points in a Datapath
Figure 1 is a diagram of a frame, and it encourages a false intuition: that the protection is a property of the octets.
It is not. It is a property of an interval in time, whose endpoints are two specific places in two different devices.
The append point is wherever, in a transmit path, the computation runs and the four octets are attached. Everything upstream of it — the client's buffer, the descriptor fetch, the DMA transfer, the transmit FIFO, every pipeline stage between them — is outside the protection. A bit that flips in any of those places is faithfully included in the computation, and the resulting frame is perfectly valid and carries wrong data.
The check point is wherever, in a receive path, the computation is verified. Everything downstream of it — the receive FIFO, the DMA to host memory, the driver's buffer — is likewise outside. A bit that flips there is delivered as data, after the frame was certified correct.
So a design's real protected interval is narrower than "the link", and how much narrower is a property of the implementation rather than of the standard. A transmit path with the CRC engine immediately before the interface has a short unprotected prefix; one that computes the CRC in software and passes the frame through several layers of memory has a long one.
6. RTL 2 — Measuring the Unprotected Prefix
// SYNTHESIZABLE INSTRUMENTATION.
//
// Reports how far a frame's octets travelled inside this device BEFORE
// the check sequence was computed over them -- the unprotected prefix of
// Section 5.
//
// The unit is deliberately STORAGE ELEMENTS, not cycles. A pipeline stage
// that a frame passes through is a place a bit can flip; a cycle spent
// waiting in the same register is not an additional exposure. Counting
// cycles overstates the exposure of a slow path and understates that of a
// deep one, which is exactly backwards.
module fcs_append_point
#(
// Storage elements between the client handoff and the CRC engine's
// input, on this design's transmit path. A structural property of the
// implementation, declared rather than measured, because the design
// knows it and the traffic cannot reveal it.
parameter int unsigned STAGES_BEFORE_APPEND = 4,
parameter int unsigned CNT_W = 40
) (
input logic clk,
input logic rst_n,
input logic clear,
input logic frame_done,
input logic [13:0] covered_octets,
// From whatever protects the internal path: parity or ECC on the
// buffers the frame crossed before the append point. This is the
// mechanism that covers what the CRC cannot.
input logic internal_error_detected,
input logic internal_error_corrected,
output logic [CNT_W-1:0] octets_appended,
output logic [CNT_W-1:0] frames_appended,
// Octet-stages: octets multiplied by the storage elements they crossed
// before being protected. The exposure figure, in the unit that lets two
// designs be compared.
output logic [CNT_W-1:0] unprotected_octet_stages,
// Errors caught by the INTERNAL mechanism, in the region the frame check
// sequence cannot see. Counted separately for exactly that reason.
output logic [CNT_W-1:0] c_internal_detected,
output logic [CNT_W-1:0] c_internal_corrected,
output logic internal_error_seen // sticky
);
function automatic logic [CNT_W-1:0] add(input logic [CNT_W-1:0] v,
input logic [CNT_W-1:0] d);
add = (v > ({CNT_W{1'b1}} - d)) ? {CNT_W{1'b1}} : (v + d);
endfunction
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
octets_appended <= '0;
frames_appended <= '0;
unprotected_octet_stages <= '0;
c_internal_detected <= '0;
c_internal_corrected <= '0;
internal_error_seen <= 1'b0;
end else begin
if (clear) begin
octets_appended <= '0;
frames_appended <= '0;
unprotected_octet_stages <= '0;
c_internal_detected <= '0;
c_internal_corrected <= '0;
// internal_error_seen deliberately survives: an error in the
// unprotected region is never routine, and a housekeeping clear
// must not erase the fact that one occurred.
end else begin
if (frame_done) begin
octets_appended <= add(octets_appended, CNT_W'(covered_octets));
frames_appended <= add(frames_appended, CNT_W'(1));
unprotected_octet_stages <=
add(unprotected_octet_stages,
CNT_W'(covered_octets) * CNT_W'(STAGES_BEFORE_APPEND));
end
if (internal_error_detected) begin
c_internal_detected <= add(c_internal_detected, CNT_W'(1));
internal_error_seen <= 1'b1;
end
if (internal_error_corrected)
c_internal_corrected <= add(c_internal_corrected, CNT_W'(1));
end
end
end
endmoduleClassification: synthesizable instrumentation.
What it teaches: that the exposure a design carries is a measurable quantity and almost never measured. unprotected_octet_stages is not a rate of failures — it is a rate of opportunity, and it is the correct denominator for interpreting c_internal_detected. Two designs with the same internal error count are not equally reliable if one exposes four times as many octet-stages.
Deliberately simplified: STAGES_BEFORE_APPEND is a parameter. It cannot be inferred from traffic, because the whole point is that the region is invisible from the wire — the number has to come from whoever built the datapath, and making it a parameter is what forces somebody to write it down.
Production implication: c_internal_detected and c_internal_corrected are separate. A corrected error is a healthy mechanism doing its job and a rising rate is an early warning; a detected but uncorrected error is a frame that had to be dropped inside the device, which is a loss the far end will attribute to the link. Merging them turns an early warning and an unattributable loss into one number, and the two have different urgencies.
7. RTL 3 — The Check Point, and What a Result Is Evidence Of
// SYNTHESIZABLE.
//
// Verifies the check sequence and -- more importantly -- classifies the
// RESULT into categories that mean different things.
//
// A binary pass/fail is not enough, because three situations produce a
// failure and one of them is not a corruption:
//
// MISMATCH -- the range completed and the value disagrees. A genuine
// detection. Something changed between append and check.
// NO_RANGE -- the frame ended before the range closed. There is no
// computation to compare; reporting a mismatch here is an
// attribution error (Section 4).
// STOMPED -- the value is the bitwise inverse of a valid one: an
// upstream device DELIBERATELY marked this frame bad
// (Section 9). Not damage on this link.
module fcs_check_point
import fcs_pkg::*;
#(
parameter int unsigned STAGES_AFTER_CHECK = 6,
parameter int unsigned CNT_W = 32
) (
input logic clk,
input logic rst_n,
input logic clear,
input logic frame_end,
input logic range_completed,
input logic [31:0] computed,
input logic [31:0] received,
output logic result_valid,
output logic fcs_ok,
output logic fcs_mismatch,
output logic fcs_no_range,
output logic fcs_stomped,
output logic [CNT_W-1:0] c_ok,
output logic [CNT_W-1:0] c_mismatch,
output logic [CNT_W-1:0] c_no_range,
output logic [CNT_W-1:0] c_stomped,
// First cause: the difference pattern of the first mismatch. A single
// set bit points at a signal-integrity event; a dense pattern points at
// a structural fault such as a mis-sized range.
output logic first_fail_seen,
output logic [31:0] first_fail_delta,
// Octets delivered downstream after this point, which the check does
// not cover. The receive-side twin of Section 6's prefix.
output logic [CNT_W-1:0] unprotected_suffix_stages
);
// A stomped frame carries the inverse of the value a correct frame would
// have carried. Recognising it turns "this link is corrupting frames"
// into "an upstream device already knew this frame was bad".
wire is_stomp = (received == ~computed);
function automatic logic [CNT_W-1:0] bump(input logic [CNT_W-1:0] v);
bump = (&v) ? v : (v + 1'b1);
endfunction
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
result_valid <= 1'b0;
fcs_ok <= 1'b0;
fcs_mismatch <= 1'b0;
fcs_no_range <= 1'b0;
fcs_stomped <= 1'b0;
c_ok <= '0;
c_mismatch <= '0;
c_no_range <= '0;
c_stomped <= '0;
first_fail_seen <= 1'b0;
first_fail_delta <= '0;
unprotected_suffix_stages <= '0;
end else begin
result_valid <= 1'b0;
fcs_ok <= 1'b0;
fcs_mismatch <= 1'b0;
fcs_no_range <= 1'b0;
fcs_stomped <= 1'b0;
if (clear) begin
c_ok <= '0; c_mismatch <= '0; c_no_range <= '0; c_stomped <= '0;
unprotected_suffix_stages <= '0;
// first_fail_* deliberately survives.
end
if (frame_end) begin
result_valid <= 1'b1;
if (!range_completed) begin
fcs_no_range <= 1'b1;
c_no_range <= bump(c_no_range);
end else if (computed == received) begin
fcs_ok <= 1'b1;
c_ok <= bump(c_ok);
unprotected_suffix_stages <=
unprotected_suffix_stages + CNT_W'(STAGES_AFTER_CHECK);
end else if (is_stomp) begin
fcs_stomped <= 1'b1;
c_stomped <= bump(c_stomped);
end else begin
fcs_mismatch <= 1'b1;
c_mismatch <= bump(c_mismatch);
if (!first_fail_seen) begin
first_fail_seen <= 1'b1;
first_fail_delta <= computed ^ received;
end
end
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that first_fail_delta is a diagnostic and a pass/fail bit is not. The exclusive-or of the computed and received values has a shape. A sparse pattern — a handful of set bits — is consistent with a small number of bit errors on the wire, which is a signal-integrity story. A dense pattern, with roughly half the bits set, is what a structurally wrong computation produces: a range off by an octet, an initial value that disagrees, the wrong bit ordering. The first says the link is marginal; the second says the two implementations were never computing the same thing, and no amount of cable replacement addresses it.
Deliberately simplified: the stomp test is exact inversion. Real deployments vary in how a frame is marked, and a design should treat the recognition as a heuristic hint rather than a certainty — it is a strong signal precisely because an inverted value is astronomically unlikely to arise from corruption.
Production implication: c_stomped must not be added to c_mismatch, and this is the single most valuable split in the module. A stomped frame means an upstream device found the error and told you — the fault is on some earlier link, and this link is behaving perfectly. Counting it as a local mismatch points the investigation at the one link that has been proven innocent.
8. Hop by Hop, Not End to End
A store-and-forward switch checks the sequence on ingress and computes a new one on egress. It must, whenever it modifies the frame — inserting or removing a tag changes the covered octets, so the old value no longer describes them. Many devices recompute unconditionally, because a single path is simpler than one with a conditional pass-through.
So the check sequence arriving at a destination was computed by the last device in the path. It certifies the last link. It says nothing whatsoever about the first.
And the region between a switch's check and its append is genuinely uncovered by anything in the frame. A frame sits in that switch's memory — possibly for a long time, on a congested port — and any bit that flips there is included in the new computation. The frame leaves with a valid check sequence and wrong contents, and every subsequent device confirms it.
9. Cut-Through, and Marking a Frame Bad on Purpose
A store-and-forward switch can check before it forwards. A cut-through switch cannot, and the consequence is a mechanism that looks bizarre until its purpose is clear.
Cut-through forwarding begins transmitting a frame on the egress port as soon as enough of the header has arrived to make a forwarding decision — long before the check sequence arrives. It reduces latency substantially and it means the switch is committed: by the time the check sequence arrives and proves the frame corrupt, most of the frame has already been sent onward.
So the frame cannot be discarded. It can only be marked. And the marking has to be something every downstream device already rejects, because no new field can be added and no downstream device can be asked to understand a new convention.
The mechanism is to deliberately emit an invalid check sequence — commonly the bitwise inverse of the correct one. Every conforming receiver rejects the frame, exactly as intended, and the corrupt data does not reach an application.
What the inverse buys over an arbitrary wrong value is attribution. A random wrong value is indistinguishable from corruption on the link that just carried it. An exact inversion is astronomically unlikely to arise by chance, so a receiver that tests for it — Section 7's is_stomp — can conclude that an upstream device already detected this error, and that the local link is not the problem.
Which reframes the whole practice. It is not a workaround for cut-through's inability to drop; it is a way of propagating a diagnosis through a protocol that has no field in which to carry one. The frame becomes its own error report, in the only encoding every device on the path already understands.
And the cost is worth naming: a design that does not test for the inversion counts these as local corruption, and the investigation converges on the one link that has been proven good.
10. RTL 4 — Proving the Range Is the Range
// SYNTHESIZABLE CONFORMANCE CHECKER.
//
// Compares the number of octets the CRC engine was actually fed against
// the number the frame's own structure says it should have been.
//
// This is the check that no interoperability test performs cheaply and no
// self-test performs at all: a design that covers the wrong range is
// SELF-CONSISTENT, so every loopback passes and every simulation with a
// matching reference model passes. The disagreement only appears against
// a peer -- which is to say, in the field.
module protected_range_checker
import fcs_pkg::*;
#(
parameter int unsigned CNT_W = 32
) (
input logic clk,
input logic rst_n,
// What the coverage boundary actually enabled.
input logic covered_valid,
input logic [13:0] covered_count,
// What the frame's structure implies, computed independently: the total
// octets between the delimiter and the end of carrier, minus the four
// octets of the field itself.
input logic structure_valid,
input logic [13:0] frame_octets_after_sfd,
output logic conformant,
output logic mismatch,
// Signed difference, because the two directions are different bugs:
// +1 -> the range consumed an octet of the check sequence itself
// -1 -> the range missed the first address octet
output logic signed [14:0] delta,
output logic [CNT_W-1:0] c_mismatch,
output logic first_mismatch_seen,
output logic signed [14:0] first_delta
);
wire [13:0] expected = frame_octets_after_sfd - 14'(FCS_OCTETS);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
conformant <= 1'b1;
mismatch <= 1'b0;
delta <= '0;
c_mismatch <= '0;
first_mismatch_seen <= 1'b0;
first_delta <= '0;
end else begin
mismatch <= 1'b0;
if (covered_valid && structure_valid) begin
delta <= $signed({1'b0, covered_count}) - $signed({1'b0, expected});
if (covered_count != expected) begin
mismatch <= 1'b1;
conformant <= 1'b0; // sticky: one non-conformant frame is enough
if (!(&c_mismatch)) c_mismatch <= c_mismatch + 1'b1;
if (!first_mismatch_seen) begin
first_mismatch_seen <= 1'b1;
first_delta <= $signed({1'b0, covered_count}) - $signed({1'b0, expected});
end
end
end
end
end
endmoduleClassification: synthesizable conformance checker.
What it teaches: that the sign of the error names the bug. A delta of +4 means the whole check sequence was fed into its own computation. +1 means one octet of it was. −1 means the first address octet was missed — the quiet, severe failure of Section 2, which leaves the individual/group flag unprotected. A boolean "range wrong" says none of this, and the three have different fixes in different places.
Deliberately simplified: it needs an independently derived frame length. On receive that is the octet count between the delimiter and the loss of carrier, which the physical interface supplies; on transmit it is known from the frame being built. The independence is the requirement — a length derived from the same counter that drove the coverage enable proves nothing at all.
Production implication: conformant is sticky and never clears. A design that has ever covered the wrong range is not intermittently wrong — it is structurally wrong, and it will be wrong on every frame. Clearing the flag would suggest the condition could pass, and a single assertion of it is a build-stopping finding rather than a counter to trend.
11. Where an Error Could Have Come From
Three of the four regions are invisible to this field, and the practical question is what a design can say about them anyway. The answer is: something, by elimination and by correlation.
A frame with a good check sequence and wrong contents cannot be attributed by the frame. But the application above will notice — its own checksum will fail, or its data will be wrong — and the correlation is diagnostic: an upper-layer checksum failing while every link-layer counter is clean is the signature of corruption in one of the three uncovered regions, and it eliminates the links.
A frame with a bad check sequence narrows to the wire since the last append. That is a strong localisation and it is what makes per-hop counters worth having.
A stomped frame narrows to a previous link — an upstream device saw the error, which means it is neither here nor in the region since.
And a device's own internal error counters — Section 6's parity and correction counts — cover the first and last regions from the inside. Where those counters exist, the elimination is complete; where they do not, there is a region of the system in which corruption occurs and nothing anywhere records it.
12. RTL 5 — Accounting by Origin, Not by Symptom
// SYNTHESIZABLE INSTRUMENTATION.
//
// Attributes every bad frame to a REGION of Figure 3 rather than to a
// symptom, and -- crucially -- records the frames it CANNOT attribute.
//
// The last output is the point of the module. A design that reports only
// what it detected implies that everything else was fine. A design that
// also reports what it could not have seen is honest about the size of
// its own blind spot, which is what makes the numbers usable.
module error_origin_accounting
#(
parameter int unsigned CNT_W = 32
) (
input logic clk,
input logic rst_n,
input logic clear,
input logic result_valid,
input logic fcs_ok,
input logic fcs_mismatch,
input logic fcs_stomped,
input logic fcs_no_range,
// From this device's internal protection (Section 6), covering the
// regions the check sequence cannot.
input logic internal_error_detected,
// From above: an upper-layer integrity check on a frame that PASSED
// the frame check sequence. The only evidence available for corruption
// in an uncovered region on some other device.
input logic upper_layer_mismatch,
output logic [CNT_W-1:0] c_this_link, // wire, since last append
output logic [CNT_W-1:0] c_upstream_link, // stomped: an earlier hop
output logic [CNT_W-1:0] c_this_device, // caught by internal ECC
output logic [CNT_W-1:0] c_uncovered_region, // good FCS, bad payload
output logic [CNT_W-1:0] c_unattributable, // truncation: no range
output logic [CNT_W-1:0] c_good,
// Frames that passed and were NOT independently verified above. The
// blind spot, stated as a number rather than left implicit.
output logic [CNT_W-1:0] c_unverified_pass
);
function automatic logic [CNT_W-1:0] bump(input logic [CNT_W-1:0] v);
bump = (&v) ? v : (v + 1'b1);
endfunction
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
c_this_link <= '0;
c_upstream_link <= '0;
c_this_device <= '0;
c_uncovered_region <= '0;
c_unattributable <= '0;
c_good <= '0;
c_unverified_pass <= '0;
end else if (clear) begin
c_this_link <= '0;
c_upstream_link <= '0;
c_this_device <= '0;
c_uncovered_region <= '0;
c_unattributable <= '0;
c_good <= '0;
c_unverified_pass <= '0;
end else begin
if (internal_error_detected) c_this_device <= bump(c_this_device);
if (result_valid) begin
if (fcs_mismatch) c_this_link <= bump(c_this_link);
else if (fcs_stomped) c_upstream_link <= bump(c_upstream_link);
else if (fcs_no_range) c_unattributable <= bump(c_unattributable);
else if (fcs_ok) begin
c_good <= bump(c_good);
if (upper_layer_mismatch) begin
// Passed the check and failed above it. By elimination the
// corruption happened in a region no check sequence covers.
c_uncovered_region <= bump(c_uncovered_region);
end else begin
// Passed, and nothing independent confirmed it. NOT proof of
// correctness -- proof only that nothing looked.
c_unverified_pass <= bump(c_unverified_pass);
end
end
end
end
end
endmoduleClassification: synthesizable instrumentation.
What it teaches: that a counter for what the design cannot see is as important as counters for what it can. c_unverified_pass looks useless — it counts frames that were fine. What it actually reports is the size of the population about which nothing is known, and that is the denominator for every claim of the form "we saw no corruption". A design reporting a million good frames and no errors is saying something quite different depending on whether those million were independently verified above.
Deliberately simplified: upper_layer_mismatch has to be routed back down from a layer that is architecturally above this one. That is unusual, and it is the only way this evidence reaches a place that can correlate it with link counters — a genuine exception to the usual downward flow, made deliberately and narrowly.
Production implication: c_upstream_link being non-zero completely changes what to do about c_this_link. A device seeing both is on a path with a fault that is not local, and its own mismatch count may be entirely incidental. Without the split — Section 7's stomp recognition feeding this module — both populations land in one counter, and the strongest available evidence about where the fault is gets averaged away.
13. Assertions — About the Range, Not About the Arithmetic
// ---------------------------------------------------------------------
// P1 -- THE CENTRAL PROPERTY. The check sequence's own octets never enter
// the computation that produced them.
// ---------------------------------------------------------------------
property p_fcs_not_self_covered;
@(posedge clk) disable iff (!rst_n)
(state == COV_FCS) |-> !crc_enable;
endproperty
a_fcs_not_self_covered: assert property (p_fcs_not_self_covered)
else $error("check sequence octets fed into their own computation");
// ---------------------------------------------------------------------
// P2 -- Coverage begins on the octet AFTER the delimiter, never on it.
// ---------------------------------------------------------------------
property p_coverage_starts_after_sfd;
@(posedge clk) disable iff (!rst_n)
sfd_seen |-> !crc_enable;
endproperty
a_coverage_starts_after_sfd: assert property (p_coverage_starts_after_sfd);
// ---------------------------------------------------------------------
// P3 -- The preamble is never covered.
// ---------------------------------------------------------------------
property p_preamble_not_covered;
@(posedge clk) disable iff (!rst_n)
(state == COV_PREAMBLE) |-> !crc_enable;
endproperty
a_preamble_not_covered: assert property (p_preamble_not_covered);
// ---------------------------------------------------------------------
// P4 -- The covered range is CONTIGUOUS. A gap in the enable means some
// octets between the addresses and the pad were skipped, which is not
// detectable from the CRC value at either end.
// ---------------------------------------------------------------------
property p_coverage_contiguous;
@(posedge clk) disable iff (!rst_n)
($fell(crc_enable) && (state == COV_RANGE)) |-> !octet_valid;
endproperty
a_coverage_contiguous: assert property (p_coverage_contiguous);
// ---------------------------------------------------------------------
// P5 -- Pad IS covered. Excluding it creates a blind region in the most
// numerous frames on most links (Section 3).
// ---------------------------------------------------------------------
property p_pad_is_covered;
@(posedge clk) disable iff (!rst_n)
(octet_valid && octet_is_pad && (state == COV_RANGE)) |-> crc_enable;
endproperty
a_pad_is_covered: assert property (p_pad_is_covered);
// ---------------------------------------------------------------------
// P6 -- The range checker's expectation is derived INDEPENDENTLY. If the
// two counts always agree by construction, the checker proves nothing.
// ---------------------------------------------------------------------
property p_range_conformance;
@(posedge clk) disable iff (!rst_n)
(covered_valid && structure_valid) |=> (mismatch == (delta != 0));
endproperty
a_range_conformance: assert property (p_range_conformance);
// ---------------------------------------------------------------------
// P7 -- Non-conformance is sticky. A wrong range is structural, not
// intermittent, so it cannot un-happen.
// ---------------------------------------------------------------------
property p_conformant_is_sticky_low;
@(posedge clk) disable iff (!rst_n)
!conformant |=> !conformant;
endproperty
a_conformant_is_sticky_low: assert property (p_conformant_is_sticky_low);
// ---------------------------------------------------------------------
// P8 -- The four result classes partition. A frame is exactly one of ok,
// mismatch, stomped, no-range.
// ---------------------------------------------------------------------
property p_result_classes_onehot;
@(posedge clk) disable iff (!rst_n)
result_valid |-> $onehot({fcs_ok, fcs_mismatch, fcs_stomped, fcs_no_range});
endproperty
a_result_classes_onehot: assert property (p_result_classes_onehot);
// ---------------------------------------------------------------------
// P9 -- A truncated frame is NEVER reported as a mismatch. There is no
// completed computation to disagree with (Section 4).
// ---------------------------------------------------------------------
property p_no_range_not_mismatch;
@(posedge clk) disable iff (!rst_n)
fcs_no_range |-> (!fcs_mismatch && !fcs_ok);
endproperty
a_no_range_not_mismatch: assert property (p_no_range_not_mismatch);
// ---------------------------------------------------------------------
// P10 -- A stomped frame is not counted as local corruption. Section 7's
// most valuable split, asserted so a refactor cannot merge it.
// ---------------------------------------------------------------------
property p_stomp_not_local;
@(posedge clk) disable iff (!rst_n)
fcs_stomped |=> ($stable(c_mismatch) && (c_stomped == $past(c_stomped) + 1));
endproperty
a_stomp_not_local: assert property (p_stomp_not_local);
// ---------------------------------------------------------------------
// P11 -- A stomp is exactly the inverse. Anything else is a mismatch, and
// the recognition must not be loosened into a range.
// ---------------------------------------------------------------------
property p_stomp_is_exact_inverse;
@(posedge clk) disable iff (!rst_n)
fcs_stomped |-> ($past(received) == ~$past(computed));
endproperty
a_stomp_is_exact_inverse: assert property (p_stomp_is_exact_inverse);
// ---------------------------------------------------------------------
// P12 -- First cause is captured once and is the DELTA, not the values.
// The pattern of set bits is what distinguishes a wire fault from a
// structural one (Section 7).
// ---------------------------------------------------------------------
property p_first_delta_stable;
@(posedge clk) disable iff (!rst_n)
first_fail_seen |=> $stable(first_fail_delta);
endproperty
a_first_delta_stable: assert property (p_first_delta_stable);
// ---------------------------------------------------------------------
// P13 -- Every result increments exactly one counter.
// ---------------------------------------------------------------------
property p_one_counter_per_result;
@(posedge clk) disable iff (!rst_n)
result_valid |=> (counter_sum == $past(counter_sum) + 1);
endproperty
a_one_counter_per_result: assert property (p_one_counter_per_result);
// ---------------------------------------------------------------------
// P14 -- A frame that failed above while passing here is attributed to an
// uncovered region, never to this link.
// ---------------------------------------------------------------------
property p_upper_failure_not_local;
@(posedge clk) disable iff (!rst_n)
(result_valid && fcs_ok && upper_layer_mismatch)
|=> ($stable(c_this_link) && (c_uncovered_region == $past(c_uncovered_region) + 1));
endproperty
a_upper_failure_not_local: assert property (p_upper_failure_not_local);
// ---------------------------------------------------------------------
// P15 -- Internal errors are counted separately from link errors. They
// occur in a region the check sequence provably cannot see.
// ---------------------------------------------------------------------
property p_internal_not_link;
@(posedge clk) disable iff (!rst_n)
internal_error_detected |=> $stable(c_this_link);
endproperty
a_internal_not_link: assert property (p_internal_not_link);
// ---------------------------------------------------------------------
// P16 -- COVERAGE. A frame at each result class, including the two that
// most environments never generate.
// ---------------------------------------------------------------------
c_stomped_seen: cover property (
@(posedge clk) disable iff (!rst_n) fcs_stomped
);
c_no_range_seen: cover property (
@(posedge clk) disable iff (!rst_n) fcs_no_range
);
// ---------------------------------------------------------------------
// P17 -- COVERAGE. Corruption in an uncovered region: a frame with a good
// check sequence whose payload is wrong. Section 14 builds it.
// ---------------------------------------------------------------------
c_uncovered_corruption_seen: cover property (
@(posedge clk) disable iff (!rst_n) (fcs_ok && upper_layer_mismatch)
);
// ---------------------------------------------------------------------
// P18 -- COVERAGE. A minimum-size frame, so the pad is inside the range.
// ---------------------------------------------------------------------
c_padded_frame_covered: cover property (
@(posedge clk) disable iff (!rst_n) (crc_enable && octet_is_pad)
);14. Verification — Twenty-Two Scenarios and an Error the Wire Cannot Carry
| # | Scenario | Stimulus | What must be observed |
|---|---|---|---|
| 1 | Clean frame | valid frame, no injection | fcs_ok; covered_count equals frame octets after the delimiter minus four |
| 2 | Minimum-size frame | 64 octets, padded | crc_enable high across the pad (P5) |
| 3 | Maximum-size frame | 1518 octets | coverage contiguous end to end (P4) |
| 4 | One bit flipped in the payload | wire injection | fcs_mismatch; first_fail_delta sparse |
| 5 | One bit flipped in the address | wire injection at octet 0 | fcs_mismatch — proves the first address octet is inside the range |
| 6 | One bit flipped in the pad | wire injection in a pad octet | fcs_mismatch — proves the pad is inside the range |
| 7 | One bit flipped in the preamble | wire injection before the delimiter | no mismatch, and the frame still delimits correctly |
| 8 | One bit flipped in the FCS field | wire injection in the last four octets | fcs_mismatch — the field is checked, though not covered |
| 9 | Truncated frame | carrier lost mid-frame | fcs_no_range; no mismatch (P9) |
| 10 | Stomped frame | received value is the exact inverse | fcs_stomped; c_mismatch unchanged (P10, P11) |
| 11 | Near-stomp | received value inverse in all but one bit | fcs_mismatch, not stomped — the recognition must stay exact |
| 12 | Range one octet short | force coverage to start at address octet 1 | mismatch with delta = −1 |
| 13 | Range one octet long | force coverage to include one FCS octet | mismatch with delta = +1 |
| 14 | Range four octets long | force coverage over the whole field | delta = +4; self-consistent loopback still passes |
| 15 | Non-conformance is sticky | one bad-range frame, then clean traffic | conformant stays low (P7) |
| 16 | Structural mismatch signature | a peer using a different range | first_fail_delta dense, roughly half the bits set |
| 17 | Internal error, corrected | assert internal_error_corrected | c_internal_corrected increments; c_this_link unchanged (P15) |
| 18 | Internal error, detected only | assert internal_error_detected | c_this_device increments; internal_error_seen sticky |
| 19 | Exposure accounting | 1000 frames of 1500 octets | unprotected_octet_stages equals octets × STAGES_BEFORE_APPEND |
| 20 | Unverified passes | clean traffic, no upper-layer signal | c_unverified_pass tracks c_good exactly |
| 21 | Good FCS, bad payload | corruption before the append point | fcs_ok high; c_uncovered_region increments (P14) |
| 22 | Counter clear | assert clear after faults | counters zero; conformant, internal_error_seen, first_fail_delta survive |
15. Debugging — Reading a Failure Back to a Region
Symptom — a link shows a steady rate of check-sequence mismatches.
Read first_fail_delta before touching anything physical. A sparse difference — a few set bits — is consistent with bit errors on the wire, and the physical layer is the right place to look. A dense difference, with roughly half the bits set, is what a structurally different computation produces: a range that differs by an octet, a different initial value, a different bit ordering. The second is not a link fault at all, and it appears at 100% on traffic from one particular peer and 0% from every other — which is the confirming test.
Symptom — mismatches on one link, and the neighbour's counters are clean.
Check c_stomped before c_mismatch. If the frames are stomped, an upstream device found the error and marked the frame, and this link is behaving perfectly — the fault is one or more hops back, on a link neither of these two devices touches. Without Section 7's inversion test the two populations are one counter, and the investigation converges on the link that has been proven good.
Symptom — an application reports corrupted data and every link-layer counter is clean.
This is the signature of corruption in one of the three uncovered regions of Figure 3, and the elimination is worth doing carefully. Clean link counters at every hop rule out the wire. What remains is: the sender's memory before its append point, some switch's memory between its check and re-append, or the receiver's memory after its check point. Read the internal error counters on each device — c_internal_detected from Section 6 — because where those exist the elimination completes, and where they do not, that device is the region nothing is watching.
Symptom — everything passes in loopback and the peer rejects every frame.
Suspect the covered range, not the arithmetic. A design whose range is wrong is self-consistent: its own transmitter and receiver compute over the same wrong octets and always agree, so loopback is exactly the test that cannot find it. Read delta from Section 10 — it is derived independently of the coverage logic — and its sign names the bug: +4 means the check sequence covered itself, +1 means one octet of it did, −1 means the first address octet was missed.
Symptom — fcs_no_range climbing with no mismatches.
Frames are being truncated rather than corrupted. Carrier is being lost mid-frame, or a buffer is running dry mid-transmission. This is not a signal-integrity problem and the usual signal-integrity remedies will not move it; look at flow control, buffer occupancy, and clock compensation (Chapter 4.4) instead.
Symptom — after a switch was reconfigured to insert tags, downstream devices report mismatches.
The switch is modifying the frame and not recomputing the check sequence. Inserting four octets changes the covered range, so the original value no longer describes it. Every frame that passes through the modification fails downstream, and frames that bypass it are fine — so the failure correlates perfectly with a configuration rather than with a physical path, which is the tell.
16. Common Misconceptions
"The FCS covers the frame."
The wrong model: everything on the wire between one gap and the next.
What it costs: you expect a corrupted preamble to be caught, and you cannot explain why a design needs internal parity when it already has a CRC. You also have no vocabulary for the failures that actually occur, because every one of them happens outside the region you believe is covered.
The corrected model: the covered octets are exactly the addresses, the length/type field, the client data and the pad. Preamble and delimiter are outside because they are regenerated at every hop; the field cannot cover itself; and the gap is not data.
"A frame with a good FCS is the frame the sender built."
The wrong model: the check is end-to-end.
What it costs: Section 13's rejected property, and with it the belief that corruption inside a device is impossible. The internal protection that would have caught it looks redundant and does not get built, and then the failures are both undetectable and unattributable.
The corrected model: the check spans one hop, from an append point in one device to a check point in the next — and every switch that modifies the frame recomputes it. The check sequence arriving at a destination was computed by the last device in the path, and says nothing about the first.
"A CRC error means a bad cable."
The wrong model: mismatches are always physical.
What it costs: you replace cables and optics on a link whose problem is that a peer computes over a different range, or that an upstream device already marked the frame bad. Neither responds to anything physical.
The corrected model: read the difference pattern and the stomp flag first. A sparse delta is a wire story; a dense one is two implementations disagreeing structurally; and an exact inversion is an upstream device telling you it already found the error.
"Cut-through switches corrupting the FCS is a bug."
The wrong model: deliberately emitting an invalid check sequence is bad behaviour.
What it costs: you count stomped frames as local corruption and investigate the one link that has been proven innocent — the strongest available evidence about where the fault is gets discarded.
The corrected model: a cut-through switch has already forwarded most of the frame by the time it knows the frame is bad, so it cannot drop it. Inverting the check sequence makes every conforming receiver reject it, and the exactness of the inversion is what lets a receiver tell "an upstream device found this" from "this link corrupted it". It is a diagnosis propagated through a protocol with no field for one.
"If the covered range were wrong, we would know."
The wrong model: a range error shows up in testing.
What it costs: the failure is missed by exactly the tests people run. A design whose range is wrong is self-consistent — its transmitter and receiver agree perfectly — so loopback passes, and a simulation whose reference model shares the assumption passes too.
The corrected model: it is found by comparing the covered count against the frame's independently derived structure, which is what Section 10 does — or in the field, against a peer, which is later and more expensive.
17. Interview Reasoning
"What does the Ethernet FCS cover?"
The weak answer is "the frame". The answer that ends the topic lists the range exactly — addresses, length/type, client data, pad — and then gives a reason for each exclusion: the preamble is regenerated at every hop so a check over it would fail everywhere by design; the field cannot cover itself, because no value is a fixed point of its own computation; and the gap is not data. The payoff is that the pad is included, and the reason: excluding it would blind the check across most of the shortest frames on the link.
"Is the FCS end-to-end?"
No, and the complete answer says what it is: a check from an append point in one device to a check point in the next, recomputed at every hop that modifies the frame. Then the consequence, which is the interesting half: the region inside a switch, between its check and its re-append, is covered by nothing the frame carries, so a bit that flips there leaves with a valid check sequence. And the constructive close — this is why devices carry ECC on internal buffers, and why end-to-end guarantees come from an end-to-end layer.
"You see CRC errors on a link. Walk me through it."
The strong answer refuses to start at the cable. First the difference pattern: sparse means bit errors on the wire, dense means two implementations computing over different ranges or with different conventions — and the latter correlates with a peer, not with a path. Then the stomp test: an exact inversion means an upstream device already found the error and marked the frame, so this link is proven good. Only after both is the physical layer the right place to look, and by then the search has been narrowed rather than guessed at.
"Your design passes loopback and the peer rejects everything. Where do you look?"
At the covered range, because a range error is self-consistent and loopback is precisely the test that cannot see it. The complete answer names the independent check — compare the octet count the engine consumed against the frame's own structure, derived separately — and reads the sign of the difference: four too many means the check sequence covered itself, one too few means the first address octet was missed, which additionally leaves the individual/group bit unprotected.
18. Understanding Check
Covered: destination address, source address, length/type, client data, and pad. Nothing before, nothing after.
The preamble and start delimiter are excluded because they do not survive a hop. Chapter 5.2 showed them consumed by the receiving physical layer and regenerated by the next transmitter, with a length that varies along the path. A check computed over them would be invalid on arrival everywhere, by design.
The field cannot cover itself, and the reason is stronger than circularity: the value is the result of a function of the covered octets, so including itself would require it to be a fixed point of that function — and for general input no such value exists.
The interframe gap is excluded because it is not data. It is an absence, and there is nothing there to protect.
And the pad is included, which surprises people because Chapter 5.6 established the pad is never delivered. It is still transmitted, so a corrupted pad octet is evidence the frame was damaged — information about the link, regardless of what the octets were supposed to contain. Excluding it would blind the check across most of the shortest frames on the link.
19. What's Next
The claim this chapter defended: the frame check sequence protects a range, and the range is shorter than the frame's journey.
The covered octets are exactly the addresses, the length/type field, the client data and the pad — the preamble excluded because it is regenerated at every hop, the field excluded because no value can be a fixed point of its own computation, the gap excluded because it is not data. And the covered interval runs from an append point in one device to a check point in the next, which makes the check hop-by-hop: the value arriving at a destination was computed by the last device in the path, and the memory inside every switch along the way is covered by nothing the frame carries.
Which is why the useful outputs of this chapter are not pass and fail. They are a range conformance check that can find a self-consistent design's structural error without a peer, a difference pattern that separates a marginal cable from two implementations that were never computing the same thing, a stomp test that recognises a diagnosis another device already made, and an honest count of the frames about which nothing is known.
Chapter 5.9 — Interframe Gap and Idle takes the last region of Figure 1, the one this chapter excluded in a single line. It is the other quantity inherited from a mechanism that is gone: a minimum gap of 96 bit times, which exists so a receiver can recover between frames.
And it turns out not to be an absence at all. Something occupies the gap — a signal the coding of Chapter 3.5 must keep alive — and on wide interfaces the gap cannot even be a fixed number of octets, because a frame must start on a lane boundary. 5.9 shows how a deficit is carried so the average gap is honoured while individual gaps are not, and why a gap measured in octets on one interface is a gap measured in time on another.
The full path is on the Ethernet curriculum index.
Continue learning
Related tutorials
- 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.
- 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.
- Related topic
A Parallel CRC-32 Engine in RTL
The wide next-state function is linear, so it is generated rather than derived — and the cost per bit falls as the datapath widens. What is designed is the final partial word, where eight sub-networks exist and real traffic reaches two of them.
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.
