Ethernet · Module 7
The Receive Path
A transmitter assembles from settled quantities; a receiver discovers, and every decision stays provisional until the check sequence at the very end — so the pipeline either waits for the verdict or acquires the ability to withdraw what it has already delivered.
Chapter 7.1 assembled a frame from pieces the transmitter already had. Every quantity was known before the preamble went out: the client's octet count, the padding target, the length field's value, the frame's eventual size.
A receiver has none of that, and the difference is not a detail — it inverts the whole design.
A receive path is discovering the frame as it arrives. It learns the destination address one octet at a time; it learns whether the length/type field is a length or a type when those two octets appear; it learns where the payload ends only when the frame does. And it learns whether any of it was real at the very last octet, when the check sequence arrives.
Chapter 5.1 made that last point structural rather than incidental. The check sequence is last by construction, so every decision a receiver takes before it is provisional — the address filter's accept, the parser's protocol identification, the pad stripper's boundary, all of it.
Which produces the question this chapter is about, and the transmit path never faces it: should a receiver begin delivering a frame it may have to withdraw?
1. Scope — What This Chapter Owns
This chapter owns the receive datapath's structure: the order in which a frame is discovered, why each decision is provisional, the two coherent delivery policies and what each obliges, the abort mechanism, and the accounting that attributes every discarded frame to the stage that discarded it.
It does not re-derive the stages. Chapter 5.2 owns delimiter detection; Chapter 5.3 and Chapter 5.4 own address matching; Chapter 5.5 owns length/type resolution; Chapter 5.6 owns pad stripping; Chapter 6.3 owns the check. Each appears here as a stage with an interface and a decision.
It does not own frame validity classification — runts, giants and the malformed cases — which is Chapter 7.3 and which takes the discard categories this chapter produces and defines them precisely.
And it does not own the client interface above it, though Section 7 has to state what the client owes a cut-through receiver, because the obligation is created here.
The question this chapter answers that its neighbours do not: given that nothing is certain until the last octet, when may a receiver start acting — and what must it be able to undo?
2. Discovery, Not Assembly
Two consequences follow from that asymmetry, and both shape the RTL.
The receive path cannot pre-compute anything. Chapter 7.1's controller made one decision at frame start — the client-data region's size — and read both the pad count and the length field off it. A receiver has no such moment. It cannot know the region's size until the frame ends, and it cannot know where the client's data stopped until it has read a length field that may itself be a type field.
And the receive path cannot refuse to start. Chapter 7.1 §5's entire underrun defence was safe_to_start — the decision not to begin a frame it could not finish. A receiver does not get to decide when frames arrive. Whatever arrives must be processed, at line rate, including frames that are malformed, truncated, or not frames at all.
So the receive path's freedom is entirely in what it does with what it has discovered so far — and that is the delivery policy of Section 5.
3. RTL 1 — The Discovery Datapath
// SYNTHESIZABLE.
//
// The receive datapath's control. The state graph looks like the mirror
// of Chapter 7.1's and is not: each state here is an INTERPRETATION of
// arriving octets rather than the emission of a decided region.
//
// Every output this module produces before R_DONE is provisional. The
// module says so explicitly, with a `provisional` qualifier that travels
// with the data -- because a downstream stage that cannot tell a settled
// result from a provisional one will act on both identically.
package mac_rx_pkg;
typedef enum logic [3:0] {
R_IDLE, // waiting for carrier
R_PREAMBLE, // synchronising -- Chapter 5.2
R_DA, // destination address: the filter's input
R_SA, // source address
R_LENTYPE, // the two ambiguous octets -- Chapter 5.5
R_PAYLOAD, // client data and, indistinguishably, pad
R_DONE, // frame ended; the check has a verdict
R_ABORTED // ended early: no verdict is possible
} rx_state_e;
// Why a frame was discarded. Every discard carries one of these, which
// is what makes Section 10's attribution possible.
typedef enum logic [3:0] {
D_NONE,
D_NO_DELIMITER, // never synchronised
D_FILTER, // not for this station -- Chapter 5.4
D_UNDERSIZE, // below the floor -- Chapter 5.6
D_OVERSIZE, // above the ceiling -- Chapter 5.7
D_LENTYPE_BAND, // the undefined band -- Chapter 5.5
D_SHORT_DECLARED, // declared more than arrived
D_FCS, // the check disagreed -- Chapter 6.3
D_FCS_STOMPED, // deliberately marked upstream -- Chapter 5.8
D_TRUNCATED, // carrier lost mid-frame
D_CLIENT_FULL // nowhere to put it
} discard_e;
endpackage
module mac_rx_controller
import mac_rx_pkg::*;
import frame_size_pkg::*;
import ltype_pkg::*;
import frame_size_pkg::*;
(
input logic clk,
input logic rst_n,
input logic rx_valid,
input logic [7:0] rx_data,
input logic carrier,
input logic sfd_seen,
// From the stages, as each reaches a provisional conclusion.
input logic filter_accept,
input logic filter_valid,
input logic lentype_valid,
input lt_class_e lentype_class,
input logic [10:0] declared_length,
input logic fcs_ok,
input logic fcs_stomped,
input logic fcs_valid,
// To the client. `provisional` is high for every octet handed over
// before the check has a verdict -- so a client can tell the two apart
// without knowing this module's policy.
output logic cli_valid,
output logic [7:0] cli_data,
output logic cli_last,
output logic cli_provisional,
output rx_state_e state,
output logic frame_settled,
output logic discard_valid,
output discard_e discard_reason,
output logic [13:0] frame_octets
);
rx_state_e state_q;
logic [13:0] len_q;
logic [3:0] fld_q;
logic accepted_q;
assign state = state_q;
assign frame_octets = len_q;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state_q <= R_IDLE;
len_q <= '0;
fld_q <= '0;
accepted_q <= 1'b0;
cli_valid <= 1'b0;
cli_data <= '0;
cli_last <= 1'b0;
cli_provisional<= 1'b0;
frame_settled <= 1'b0;
discard_valid <= 1'b0;
discard_reason <= D_NONE;
end else begin
cli_valid <= 1'b0;
cli_last <= 1'b0;
frame_settled <= 1'b0;
discard_valid <= 1'b0;
// Carrier loss is checked FIRST and outside the case, because it
// can happen in any state and the response is the same: there is
// no verdict to be had, so the frame is aborted rather than judged
// (Chapter 6.3 §8, and Chapter 5.8 §4's rule that a truncation
// must not be reported as a check-sequence mismatch).
if (!carrier && (state_q != R_IDLE) && (state_q != R_DONE)) begin
state_q <= R_ABORTED;
discard_valid <= 1'b1;
discard_reason <= D_TRUNCATED;
end else begin
case (state_q)
R_IDLE: if (carrier) begin
state_q <= R_PREAMBLE;
len_q <= '0;
end
R_PREAMBLE: if (sfd_seen) begin
state_q <= R_DA;
fld_q <= '0;
len_q <= '0;
end
R_DA: if (rx_valid) begin
len_q <= len_q + 1'b1;
if (fld_q == 4'd5) begin state_q <= R_SA; fld_q <= '0; end
else fld_q <= fld_q + 1'b1;
end
R_SA: if (rx_valid) begin
len_q <= len_q + 1'b1;
if (fld_q == 4'd5) begin state_q <= R_LENTYPE; fld_q <= '0; end
else fld_q <= fld_q + 1'b1;
end
R_LENTYPE: if (rx_valid) begin
len_q <= len_q + 1'b1;
if (fld_q == 4'd1) begin
state_q <= R_PAYLOAD;
fld_q <= '0;
// The filter's verdict is available by now -- one octet in,
// per Chapter 5.3 -- and it is PROVISIONAL like everything
// else. A corrupt address can produce an accept.
accepted_q <= filter_valid ? filter_accept : 1'b0;
end else begin
fld_q <= fld_q + 1'b1;
end
end
R_PAYLOAD: if (rx_valid) begin
len_q <= len_q + 1'b1;
if (accepted_q) begin
cli_valid <= 1'b1;
cli_data <= rx_data;
// Provisional until fcs_valid. The qualifier travels with
// the data rather than being a mode the client must know.
cli_provisional <= 1'b1;
end
if (fcs_valid) begin
state_q <= R_DONE;
frame_settled <= 1'b1;
cli_last <= accepted_q;
if (!fcs_ok) begin
discard_valid <= 1'b1;
// Stomped and mismatched are DIFFERENT discards, because
// a stomp says an upstream device already found the
// error and this link is proven good (Chapter 5.8 §9).
discard_reason <= fcs_stomped ? D_FCS_STOMPED : D_FCS;
end
end
end
R_DONE, R_ABORTED: state_q <= R_IDLE;
default: state_q <= R_IDLE;
endcase
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that cli_provisional travels with the data rather than being a mode. A receive path can be configured for cut-through or store-and-forward, and a client that has to know which in order to interpret its input has been given a coupling it cannot verify. Qualifying each octet makes the contract local: an octet marked provisional may be withdrawn, one not marked cannot, and the client's logic is the same under both policies.
Deliberately simplified: the filter's verdict is sampled once at the end of R_LENTYPE. A real receive path also handles promiscuous mode, multiple unicast addresses, and Chapter 5.4's hash filter with its software residue — all of which change what the verdict is and none of which change that it is provisional.
Production implication: carrier loss is handled outside the case statement, before any state's logic runs. Placing it inside means writing the same abort in seven states, and the seventh one gets forgotten — reliably, and in whichever state is least exercised. A condition that applies in every state belongs outside the state machine, and this one also carries Chapter 5.8 §4's rule: a truncation is reported as D_TRUNCATED and never as a check-sequence mismatch, because there is no completed computation to disagree with.
4. Cut-Through and Store-and-Forward, From the Receive Side
The two coherent policies are coherent because each one's obligation matches its behaviour.
Store-and-forward buffers the whole frame and delivers nothing until the check has a verdict. Every octet the client sees belongs to a frame that passed. There is no abort path because there is nothing to abort, and cli_provisional is never asserted. The cost is a frame time of latency and a frame of buffer per port — at 1518 octets and 1 Gb/s, about 12 microseconds and 1.5 kilobytes.
Cut-through delivers as soon as the filter accepts, which is a few octets after the delimiter. Latency falls to roughly the address fields plus the pipeline, and the buffer shrinks to a small elastic one. In exchange, the check's verdict arrives after octets have been handed over, so an abort path is not optional — and the client acquires an obligation Section 6 spells out.
The third design is the one that appears in practice and is not coherent: deliver early, provide no abort, and rely on the check almost always passing. It is correct on every good frame, which is nearly all of them, and on a bad frame it hands corrupt data to the client with no indication. The error counters still increment — the receiver knows the frame failed — and the client has already consumed it.
Note what makes that third design attractive, because it is not laziness. Aborting is genuinely awkward: the client may have already forwarded the octets, written them to memory, or acted on a header. The abort's cost is real and it is paid by the client, so a design that skips it is optimising something visible at the expense of something that shows up rarely.
And Section 11's rejected property is what makes it look verified, which is how it survives review.
5. RTL 2 — Managing a Delivery That May Be Withdrawn
// SYNTHESIZABLE.
//
// Governs when octets are released to the client and marks each release
// as provisional or committed.
//
// The three events that matter, and they are distinct:
//
// RELEASE -- an octet is handed to the client. May be provisional.
// COMMIT -- the check passed; every octet already released is now
// final and nothing will be withdrawn.
// ABORT -- the check failed (or the frame ended early); every octet
// released for this frame must be discarded by the client.
//
// A design that merges COMMIT and ABORT into a single "frame done, here
// is a status bit" has made the client responsible for noticing, which
// is the same failure as not having an abort at all.
module provisional_delivery_manager
import mac_rx_pkg::*;
#(
// CUT_THROUGH: release on filter accept. Otherwise release only after
// the check has a verdict.
parameter bit CUT_THROUGH = 1'b1,
parameter int unsigned CNT_W = 32
) (
input logic clk,
input logic rst_n,
input logic frame_start,
input logic filter_accept_valid,
input logic filter_accept,
input logic rx_valid,
input logic [7:0] rx_data,
input logic frame_end,
input logic fcs_valid,
input logic fcs_ok,
// Buffered frame data, for the store-and-forward path.
input logic buf_valid,
input logic [7:0] buf_data,
input logic buf_last,
output logic rel_valid,
output logic [7:0] rel_data,
output logic rel_last,
output logic rel_provisional,
// The two terminal events. Exactly one fires per accepted frame.
output logic commit,
output logic abort,
output logic [CNT_W-1:0] c_committed,
output logic [CNT_W-1:0] c_aborted,
// Octets the client had to throw away. The cost of cut-through,
// measured rather than assumed.
output logic [CNT_W-1:0] c_wasted_octets,
output logic [13:0] worst_wasted
);
logic releasing_q;
logic [13:0] released_q;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
releasing_q <= 1'b0;
released_q <= '0;
rel_valid <= 1'b0;
rel_data <= '0;
rel_last <= 1'b0;
rel_provisional <= 1'b0;
commit <= 1'b0;
abort <= 1'b0;
c_committed <= '0;
c_aborted <= '0;
c_wasted_octets <= '0;
worst_wasted <= '0;
end else begin
rel_valid <= 1'b0;
rel_last <= 1'b0;
commit <= 1'b0;
abort <= 1'b0;
if (frame_start) begin
releasing_q <= 1'b0;
released_q <= '0;
end
if (CUT_THROUGH) begin
// Release begins as soon as the filter accepts, and every octet
// carries the provisional qualifier until the verdict.
if (filter_accept_valid && filter_accept) releasing_q <= 1'b1;
if (releasing_q && rx_valid) begin
rel_valid <= 1'b1;
rel_data <= rx_data;
rel_provisional <= 1'b1;
released_q <= released_q + 1'b1;
end
if (fcs_valid && releasing_q) begin
if (fcs_ok) begin
commit <= 1'b1;
rel_last <= 1'b1;
c_committed <= c_committed + 1'b1;
end else begin
// The abort is an EVENT, not a status bit the client must
// poll. Everything released for this frame is now worthless.
abort <= 1'b1;
c_aborted <= c_aborted + 1'b1;
c_wasted_octets <= c_wasted_octets + CNT_W'(released_q);
if (released_q > worst_wasted) worst_wasted <= released_q;
end
releasing_q <= 1'b0;
end
end else begin
// Store-and-forward: nothing is released until the verdict, so
// `rel_provisional` is never asserted and `abort` never fires on
// released data. The frame is simply not released.
if (fcs_valid && fcs_ok) releasing_q <= 1'b1;
if (releasing_q && buf_valid) begin
rel_valid <= 1'b1;
rel_data <= buf_data;
rel_provisional <= 1'b0;
rel_last <= buf_last;
if (buf_last) begin
commit <= 1'b1;
c_committed <= c_committed + 1'b1;
releasing_q <= 1'b0;
end
end
if (fcs_valid && !fcs_ok) begin
// Discarded before release. Not an abort -- nothing was given
// to the client -- and counting it as one would overstate the
// cost of a policy that has none.
c_aborted <= c_aborted + 1'b1;
end
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that commit and abort must be events rather than a status bit accompanying the last octet. A status bit makes the client responsible for noticing — for holding the octets until it sees the flag, and for having somewhere to put them meanwhile. An explicit abort is a signal the client must handle, and the difference is whether the obligation is stated in the interface or left implicit in a bit somebody may not read.
Deliberately simplified: the store-and-forward path reads from an external buffer whose management is not shown. The buffer is the policy's whole cost and its sizing is a switching-architecture question.
Production implication: c_wasted_octets and worst_wasted measure what cut-through actually costs, and almost no design measures it. The policy's benefit is a latency figure that is easy to quote; its cost is octets the client processed and threw away, which is invisible unless counted. A link with a rising error rate makes cut-through progressively more expensive — and worst_wasted bounds the client's exposure: it is the largest number of octets the client has ever had to discard in one go, and it is what the client's own buffering has to survive.
6. What a Cut-Through Receiver Owes Its Client, and Vice Versa
The abort is not a signal a receiver emits into a void. It creates an obligation on the client, and the obligation has to be stated because a client that cannot meet it turns a coherent design back into the incoherent third one.
The client must be able to discard what it has already taken. Which means it must not have acted on it — not forwarded it, not written it somewhere unrecoverable, not signalled its own client. A client that streams received octets straight onward cannot support cut-through, and pairing one with a cut-through receiver produces exactly the silent corruption both were trying to avoid.
And the receiver must bound how much the client can be asked to discard. worst_wasted is that bound observed; a design should also have it by construction, because the client sizes its own holding buffer from it. An unbounded abort is not implementable by any finite client.
Which produces a clean statement of the contract:
| The receiver promises | The client promises |
|---|---|
| every provisional octet is marked | provisional octets are held, not acted on |
| exactly one of commit or abort per accepted frame | it handles both, as events |
| the abort arrives within a bounded time of the last octet | it can discard up to a maximum frame's worth |
| a committed frame is never withdrawn | it does not consume before commit |
Read the last row of each column together, because they are the same requirement stated from two sides — and a design that has one without the other is broken in a way neither side can detect alone.
7. RTL 3 — The Late Abort Path
// SYNTHESIZABLE.
//
// Delivers the abort to the client with a bounded latency, and proves
// the bound.
//
// The requirement that is easy to miss: an abort must reach the client
// BEFORE the client could have committed the octets on its own. A client
// holds provisional data until commit -- but it also has finite buffer,
// and a receiver that takes too long to abort forces the client to
// either overflow or commit early.
//
// So the abort's latency is not a performance figure. It is part of the
// contract, and this module reports violations of it rather than
// assuming they cannot happen.
module rx_late_abort
import mac_rx_pkg::*;
#(
// Cycles from the last octet within which the abort must be delivered.
// Derived from the client's holding capacity, not chosen freely.
parameter int unsigned ABORT_DEADLINE = 8,
parameter int unsigned CNT_W = 32
) (
input logic clk,
input logic rst_n,
input logic clear,
input logic last_octet, // last octet released to the client
input logic fcs_valid,
input logic fcs_ok,
input logic fcs_stomped,
input logic truncated,
output logic abort_out,
output discard_e abort_reason,
output logic commit_out,
// The contract check. High if an abort was delivered later than the
// deadline -- which means the client may already have committed.
output logic deadline_missed,
output logic [CNT_W-1:0] c_deadline_missed,
output logic [7:0] worst_abort_latency
);
logic [7:0] since_q;
logic pending_q;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
since_q <= '0;
pending_q <= 1'b0;
abort_out <= 1'b0;
abort_reason <= D_NONE;
commit_out <= 1'b0;
deadline_missed <= 1'b0;
c_deadline_missed <= '0;
worst_abort_latency <= '0;
end else begin
abort_out <= 1'b0;
commit_out <= 1'b0;
deadline_missed <= 1'b0;
if (clear) begin
c_deadline_missed <= '0;
// worst_abort_latency deliberately survives: it bounds what the
// client's buffer must have survived, and a clear must not erase
// the only record of the worst case.
end
if (last_octet) begin
pending_q <= 1'b1;
since_q <= '0;
end else if (pending_q) begin
since_q <= (since_q == 8'hFF) ? since_q : (since_q + 1'b1);
end
// A truncation resolves the frame with no verdict available, and it
// is a DIFFERENT reason from a failed check (Chapter 5.8 §4).
if (truncated && pending_q) begin
abort_out <= 1'b1;
abort_reason <= D_TRUNCATED;
pending_q <= 1'b0;
if (since_q > worst_abort_latency) worst_abort_latency <= since_q;
if (since_q > 8'(ABORT_DEADLINE)) begin
deadline_missed <= 1'b1;
c_deadline_missed <= c_deadline_missed + 1'b1;
end
end else if (fcs_valid && pending_q) begin
pending_q <= 1'b0;
if (since_q > worst_abort_latency) worst_abort_latency <= since_q;
if (fcs_ok) begin
commit_out <= 1'b1;
end else begin
abort_out <= 1'b1;
// Stomped is kept distinct all the way to the client, because
// it says the fault is upstream and this link is proven good.
abort_reason <= fcs_stomped ? D_FCS_STOMPED : D_FCS;
if (since_q > 8'(ABORT_DEADLINE)) begin
deadline_missed <= 1'b1;
c_deadline_missed <= c_deadline_missed + 1'b1;
end
end
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that the abort's latency is part of the contract and therefore has to be checked. A client holds provisional octets in a finite buffer; if the abort arrives after the client has been forced to commit or overflow, the abort is useless and the design has silently degraded to the incoherent policy of Section 4. deadline_missed is the signal that says the contract broke, and without it the degradation is invisible.
Deliberately simplified: a single deadline in cycles. A real bound depends on the client's buffer depth and its own drain rate, so it is derived at integration rather than parameterised in isolation.
Production implication: abort_reason carries D_FCS_STOMPED distinctly all the way to the client, rather than collapsing to "the frame was bad". A stomped frame means an upstream device already found the error, so the link this receiver sits on is proven good — and a client that logs "receive error" for both has discarded the strongest available evidence about where the fault is. It is Chapter 5.8 §9's argument surviving one more hop up the stack, which is the only place it can be acted on.
8. Every Discard Belongs to a Stage
The six stages point at six unrelated things, and a single "frames dropped" counter merges all of them.
A delimiter discard means the receiver never synchronised. That is a physical-layer story — Chapter 5.2's alignment failing — and no amount of frame-format investigation touches it.
A filter discard is not a fault. It is traffic for other stations, and on a shared or flooded segment it is the majority of arrivals. Counting it alongside errors makes the error count meaningless, and Chapter 5.4 §9 kept it as a separate denominator for exactly this reason.
A size discard points at a peer emitting undersized frames, or at this device's ceiling being lower than its neighbours' — Chapter 5.7's two different findings that share a word.
A length/type discard means the undefined band (Chapter 5.5) or a declared length that exceeded what arrived. Corruption that survived its check, or a transmitter doing something wrong.
A check-sequence discard points at this link — unless it was stomped, in which case it points at some earlier link and this one is proven good.
A client-full discard points at this device. The receive path worked perfectly and there was nowhere to put the frame, which is a buffer-sizing or drain-rate problem and has nothing to do with the network.
Six causes, six owners, six fixes — and one counter cannot express any of it. Section 9 is the accounting that keeps them apart.
9. RTL 4 — Attribution, With the Non-Faults Kept Separate
// SYNTHESIZABLE INSTRUMENTATION.
//
// Attributes every frame that did not reach the client to the stage that
// stopped it, and keeps the one non-fault separate from the five faults.
//
// The split that matters most is the first one:
//
// NOT A FAULT -- filter discards. Traffic for other stations. On a
// flooded segment this is most arrivals, and folding it into a
// drop count makes the drop count track traffic volume.
//
// THIS DEVICE -- client-full. The receive path worked; there was
// nowhere to put the frame.
//
// THIS LINK -- check-sequence mismatch, truncation.
//
// UPSTREAM -- a stomped frame: an earlier device already found the
// error, so this link is proven good (Chapter 5.8 §9).
//
// A PEER -- undersize with a good check, undefined length/type
// band, a declared length exceeding what arrived.
module rx_discard_accounting
import mac_rx_pkg::*;
#(
parameter int unsigned CNT_W = 32
) (
input logic clk,
input logic rst_n,
input logic clear,
input logic frame_done,
input logic delivered,
input logic discard_valid,
input discard_e discard_reason,
input logic [13:0] frame_octets,
output logic [CNT_W-1:0] c_delivered,
// Not a fault. Deliberately first, and deliberately not summed with
// anything below it.
output logic [CNT_W-1:0] c_filtered,
// This device.
output logic [CNT_W-1:0] c_client_full,
// This link.
output logic [CNT_W-1:0] c_fcs,
output logic [CNT_W-1:0] c_truncated,
output logic [CNT_W-1:0] c_no_delimiter,
// Upstream.
output logic [CNT_W-1:0] c_stomped,
// A peer.
output logic [CNT_W-1:0] c_undersize,
output logic [CNT_W-1:0] c_oversize,
output logic [CNT_W-1:0] c_lentype_band,
output logic [CNT_W-1:0] c_short_declared,
// The aggregate a management interface usually wants -- computed HERE,
// from the fault categories only, so it cannot accidentally include
// filter discards.
output logic [CNT_W-1:0] c_faults_total,
// First cause, kept across a clear.
output logic first_fault_seen,
output discard_e first_fault_reason,
output logic [13:0] first_fault_octets
);
function automatic logic [CNT_W-1:0] bump(input logic [CNT_W-1:0] v);
bump = (&v) ? v : (v + 1'b1);
endfunction
wire is_fault = discard_valid && (discard_reason != D_FILTER);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
c_delivered <= '0; c_filtered <= '0; c_client_full <= '0;
c_fcs <= '0; c_truncated <= '0; c_no_delimiter <= '0;
c_stomped <= '0; c_undersize <= '0; c_oversize <= '0;
c_lentype_band <= '0; c_short_declared <= '0; c_faults_total <= '0;
first_fault_seen <= 1'b0; first_fault_reason <= D_NONE;
first_fault_octets <= '0;
end else begin
if (clear) begin
c_delivered <= '0; c_filtered <= '0; c_client_full <= '0;
c_fcs <= '0; c_truncated <= '0; c_no_delimiter <= '0;
c_stomped <= '0; c_undersize <= '0; c_oversize <= '0;
c_lentype_band <= '0; c_short_declared <= '0; c_faults_total <= '0;
// The first fault's identity deliberately survives.
end else if (frame_done) begin
if (delivered) c_delivered <= bump(c_delivered);
if (discard_valid) begin
case (discard_reason)
D_FILTER: c_filtered <= bump(c_filtered);
D_CLIENT_FULL: c_client_full <= bump(c_client_full);
D_FCS: c_fcs <= bump(c_fcs);
D_FCS_STOMPED: c_stomped <= bump(c_stomped);
D_TRUNCATED: c_truncated <= bump(c_truncated);
D_NO_DELIMITER: c_no_delimiter <= bump(c_no_delimiter);
D_UNDERSIZE: c_undersize <= bump(c_undersize);
D_OVERSIZE: c_oversize <= bump(c_oversize);
D_LENTYPE_BAND: c_lentype_band <= bump(c_lentype_band);
D_SHORT_DECLARED: c_short_declared <= bump(c_short_declared);
default: ;
endcase
if (is_fault) begin
c_faults_total <= bump(c_faults_total);
if (!first_fault_seen) begin
first_fault_seen <= 1'b1;
first_fault_reason <= discard_reason;
first_fault_octets <= frame_octets;
end
end
end
end
end
end
endmoduleClassification: synthesizable instrumentation.
What it teaches: that the aggregate must be computed from the categories rather than counted alongside them. A design that increments c_faults_total at each discard site will eventually include the filter, because the filter site looks like every other discard site. Computing the total from is_fault puts the definition of "fault" in one place, where it can be read and reviewed.
Deliberately simplified: one counter per reason. A production design usually adds octet counts per category too, because a rate of frames and a rate of octets answer different questions — Chapter 5.6 §12 made the same distinction for padding.
Production implication: c_stomped sits under "upstream" rather than under "this link", and that placement is the single most valuable line in the module. A device with c_fcs rising is on a bad link; a device with c_stomped rising is on a good link downstream of a bad one. Merging them sends the investigation to the one link that has been proven innocent — which Chapter 5.8 §15 named as a debugging failure and this is where it is prevented structurally.
10. RTL 5 — Checking the Receive Path Against Itself
// SYNTHESIZABLE MONITOR.
//
// Verifies the receive path's own invariants: that every frame is
// accounted for exactly once, that provisional data is always resolved,
// and that no octet reaches the client without a subsequent verdict.
//
// The property it exists for is conservation. A receive path processes
// N frames; each is delivered or discarded, exactly once, with exactly
// one reason. A frame that is neither -- lost in the pipeline, or
// counted twice -- is invisible to every other check in this chapter,
// because every other check looks at frames it can see.
module rx_conformance_monitor
import mac_rx_pkg::*;
#(
parameter int unsigned CNT_W = 32,
// Cycles after the last released octet by which a verdict must exist.
parameter int unsigned VERDICT_DEADLINE = 16
) (
input logic clk,
input logic rst_n,
input logic clear,
input logic frame_start,
input logic frame_done,
input logic delivered,
input logic discard_valid,
input discard_e discard_reason,
input logic rel_valid,
input logic rel_provisional,
input logic commit,
input logic abort,
output logic [CNT_W-1:0] c_started,
output logic [CNT_W-1:0] c_accounted,
// The conservation violation: a frame started and never resolved, or
// resolved twice.
output logic conservation_error,
output logic [CNT_W-1:0] c_conservation_error,
// Provisional octets were released and no verdict followed within the
// deadline. The contract of Section 6, checked rather than assumed.
output logic unresolved_provisional,
output logic [CNT_W-1:0] c_unresolved,
output logic any_violation // sticky
);
logic [7:0] since_q;
logic prov_open_q;
logic frame_open_q;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
c_started <= '0; c_accounted <= '0;
conservation_error <= 1'b0; c_conservation_error <= '0;
unresolved_provisional <= 1'b0; c_unresolved <= '0;
since_q <= '0; prov_open_q <= 1'b0; frame_open_q <= 1'b0;
any_violation <= 1'b0;
end else begin
conservation_error <= 1'b0;
unresolved_provisional <= 1'b0;
if (clear) begin
c_started <= '0; c_accounted <= '0;
c_conservation_error <= '0; c_unresolved <= '0;
// any_violation deliberately survives.
end
if (frame_start) begin
// A frame starting while one is still open is a conservation
// violation on its own: the previous frame was never resolved.
if (frame_open_q) begin
conservation_error <= 1'b1;
c_conservation_error <= c_conservation_error + 1'b1;
any_violation <= 1'b1;
end
frame_open_q <= 1'b1;
c_started <= c_started + 1'b1;
end
if (frame_done) begin
// Exactly one of delivered / discarded, and never both.
if (delivered == discard_valid) begin
conservation_error <= 1'b1;
c_conservation_error <= c_conservation_error + 1'b1;
any_violation <= 1'b1;
end else begin
c_accounted <= c_accounted + 1'b1;
end
frame_open_q <= 1'b0;
end
// Provisional release must be resolved by commit or abort.
if (rel_valid && rel_provisional) begin
prov_open_q <= 1'b1;
since_q <= '0;
end else if (prov_open_q) begin
if (commit || abort) begin
prov_open_q <= 1'b0;
end else begin
since_q <= (since_q == 8'hFF) ? since_q : (since_q + 1'b1);
if (since_q == 8'(VERDICT_DEADLINE)) begin
unresolved_provisional <= 1'b1;
c_unresolved <= c_unresolved + 1'b1;
any_violation <= 1'b1;
end
end
end
end
end
endmoduleClassification: synthesizable monitor.
What it teaches: that conservation is the property no other check can see. Every other check in this chapter examines a frame the design knows about. A frame that started and was never resolved is invisible to all of them — it does not appear in the delivered count, it does not appear in any discard category, and the totals simply do not add up. Comparing c_started against c_accounted is the only thing that finds it.
Deliberately simplified: it assumes one frame in flight. A pipelined receive path has several, and the monitor becomes a small scoreboard — which is more code and exactly the same property.
Production implication: unresolved_provisional catches a failure mode that the incoherent design of Section 4 exhibits by construction. A receiver that releases provisionally and never commits or aborts is not merely missing a feature — it leaves the client holding octets with no instruction, and the client will eventually do something with them. The monitor turns "we did not implement the abort" from an omission into a firing counter, which is the difference between a gap somebody has to notice and one the design reports.
11. Assertions — Provisionality, Resolution and Attribution
// ---------------------------------------------------------------------
// P1 -- THE CENTRAL PROPERTY. Every provisionally released frame is
// resolved: exactly one of commit or abort follows, within a bound.
// ---------------------------------------------------------------------
property p_provisional_is_resolved;
@(posedge clk) disable iff (!rst_n)
(rel_valid && rel_provisional)
|-> ##[1:VERDICT_DEADLINE] (commit || abort);
endproperty
a_provisional_is_resolved: assert property (p_provisional_is_resolved)
else $error("provisional octets released and never resolved");
// ---------------------------------------------------------------------
// P2 -- Commit and abort are mutually exclusive on one frame.
// ---------------------------------------------------------------------
property p_commit_xor_abort;
@(posedge clk) disable iff (!rst_n)
(commit || abort) |-> !(commit && abort);
endproperty
a_commit_xor_abort: assert property (p_commit_xor_abort);
// ---------------------------------------------------------------------
// P3 -- A committed frame passed its check. Note the direction: this is
// about what commit MEANS, and it is the property the rejected one below
// tries to state at the wrong moment.
// ---------------------------------------------------------------------
property p_commit_implies_fcs_ok;
@(posedge clk) disable iff (!rst_n)
commit |-> $past(fcs_ok);
endproperty
a_commit_implies_fcs_ok: assert property (p_commit_implies_fcs_ok);
// ---------------------------------------------------------------------
// P4 -- A frame that failed its check is never committed.
// ---------------------------------------------------------------------
property p_bad_frame_never_committed;
@(posedge clk) disable iff (!rst_n)
(fcs_valid && !fcs_ok) |-> !commit;
endproperty
a_bad_frame_never_committed: assert property (p_bad_frame_never_committed);
// ---------------------------------------------------------------------
// P5 -- Every octet released before a verdict exists is MARKED
// provisional. The qualifier is what makes the contract local.
// ---------------------------------------------------------------------
property p_unverified_release_is_marked;
@(posedge clk) disable iff (!rst_n)
(rel_valid && !verdict_available) |-> rel_provisional;
endproperty
a_unverified_release_is_marked: assert property (p_unverified_release_is_marked);
// ---------------------------------------------------------------------
// P6 -- Under store-and-forward, nothing is ever provisional.
// ---------------------------------------------------------------------
property p_store_forward_never_provisional;
@(posedge clk) disable iff (!rst_n)
(!CUT_THROUGH && rel_valid) |-> !rel_provisional;
endproperty
a_store_forward_never_provisional: assert property (p_store_forward_never_provisional);
// ---------------------------------------------------------------------
// P7 -- The abort arrives within its deadline, because a late abort is
// an abort the client may already have been unable to honour.
// ---------------------------------------------------------------------
property p_abort_within_deadline;
@(posedge clk) disable iff (!rst_n)
abort |-> (since_q <= 8'(ABORT_DEADLINE));
endproperty
a_abort_within_deadline: assert property (p_abort_within_deadline);
// ---------------------------------------------------------------------
// P8 -- CONSERVATION. Every frame that starts is resolved exactly once.
// The property no other check in this chapter can see.
// ---------------------------------------------------------------------
property p_every_frame_accounted;
@(posedge clk) disable iff (!rst_n)
frame_start |-> ##[1:$] (frame_done && (delivered ^ discard_valid));
endproperty
a_every_frame_accounted: assert property (p_every_frame_accounted);
// ---------------------------------------------------------------------
// P9 -- A frame cannot start while another is open.
// ---------------------------------------------------------------------
property p_no_overlapping_frames;
@(posedge clk) disable iff (!rst_n)
frame_start |-> !frame_open_q;
endproperty
a_no_overlapping_frames: assert property (p_no_overlapping_frames);
// ---------------------------------------------------------------------
// P10 -- Exactly one discard reason per discarded frame.
// ---------------------------------------------------------------------
property p_one_reason_per_discard;
@(posedge clk) disable iff (!rst_n)
discard_valid |-> (discard_reason != D_NONE);
endproperty
a_one_reason_per_discard: assert property (p_one_reason_per_discard);
// ---------------------------------------------------------------------
// P11 -- A truncation is NEVER reported as a check-sequence mismatch.
// Chapter 5.8 §4's rule, enforced at the point where the two could be
// confused.
// ---------------------------------------------------------------------
property p_truncation_not_fcs;
@(posedge clk) disable iff (!rst_n)
(discard_valid && (discard_reason == D_TRUNCATED)) |-> !fcs_valid;
endproperty
a_truncation_not_fcs: assert property (p_truncation_not_fcs);
// ---------------------------------------------------------------------
// P12 -- A stomped frame is attributed UPSTREAM, never to this link.
// The single most valuable split in Section 9.
// ---------------------------------------------------------------------
property p_stomp_not_this_link;
@(posedge clk) disable iff (!rst_n)
(discard_valid && (discard_reason == D_FCS_STOMPED))
|=> ($stable(c_fcs) && (c_stomped == $past(c_stomped) + 1));
endproperty
a_stomp_not_this_link: assert property (p_stomp_not_this_link);
// ---------------------------------------------------------------------
// P13 -- Filter discards are NOT faults. The aggregate must not move.
// ---------------------------------------------------------------------
property p_filter_is_not_a_fault;
@(posedge clk) disable iff (!rst_n)
(discard_valid && (discard_reason == D_FILTER)) |=> $stable(c_faults_total);
endproperty
a_filter_is_not_a_fault: assert property (p_filter_is_not_a_fault);
// ---------------------------------------------------------------------
// P14 -- Carrier loss aborts from any state. Written over the state set
// rather than per state, because a per-state version omits one.
// ---------------------------------------------------------------------
property p_carrier_loss_aborts_anywhere;
@(posedge clk) disable iff (!rst_n)
(!carrier && (state inside {R_PREAMBLE, R_DA, R_SA, R_LENTYPE, R_PAYLOAD}))
|=> (state == R_ABORTED);
endproperty
a_carrier_loss_aborts_anywhere: assert property (p_carrier_loss_aborts_anywhere);
// ---------------------------------------------------------------------
// P15 -- Nothing is released for a frame the filter rejected.
// ---------------------------------------------------------------------
property p_no_release_when_filtered;
@(posedge clk) disable iff (!rst_n)
(filter_accept_valid && !filter_accept) |=> !rel_valid until frame_start;
endproperty
a_no_release_when_filtered: assert property (p_no_release_when_filtered);
// ---------------------------------------------------------------------
// P16 -- First-cause attribution survives a counter clear.
// ---------------------------------------------------------------------
property p_first_fault_survives_clear;
@(posedge clk) disable iff (!rst_n)
clear |=> ($stable(first_fault_reason) && $stable(first_fault_octets));
endproperty
a_first_fault_survives_clear: assert property (p_first_fault_survives_clear);
// ---------------------------------------------------------------------
// P17 -- COVERAGE. An abort that follows real provisional delivery. A
// run without one has not exercised the cut-through path at all.
// ---------------------------------------------------------------------
c_abort_after_release: cover property (
@(posedge clk) disable iff (!rst_n)
((rel_valid && rel_provisional) ##[1:$] abort)
);
// ---------------------------------------------------------------------
// P18 -- COVERAGE. Each discard reason reached at least once.
// ---------------------------------------------------------------------
c_discard_fcs: cover property (@(posedge clk) disable iff (!rst_n) (discard_reason == D_FCS));
c_discard_stomped: cover property (@(posedge clk) disable iff (!rst_n) (discard_reason == D_FCS_STOMPED));
c_discard_truncated: cover property (@(posedge clk) disable iff (!rst_n) (discard_reason == D_TRUNCATED));
c_discard_client: cover property (@(posedge clk) disable iff (!rst_n) (discard_reason == D_CLIENT_FULL));12. Verification — Twenty-Four Scenarios and a Frame That Fails at the Last Octet
| # | Scenario | Stimulus | What must be observed |
|---|---|---|---|
| 1 | Good frame, store-and-forward | valid 512-octet frame | delivered after the verdict; rel_provisional never asserts (P6) |
| 2 | Good frame, cut-through | the same frame | released early, all provisional; commit at the end (P1, P3) |
| 3 | Minimum-size frame | 64 octets | delivered; pad stripped by declared length (Chapter 5.6) |
| 4 | Maximum-size frame | 1518 octets | delivered; no oversize discard |
| 5 | Filtered frame | address for another station | D_FILTER; no release (P15); c_faults_total unchanged (P13) |
| 6 | Bad check, cut-through | valid until the last octet, then a wrong FCS | provisional octets released, then abort (P1, P4) |
| 7 | Bad check, store-and-forward | the same frame | nothing released at all; c_fcs increments |
| 8 | Stomped frame | FCS is the exact inverse | D_FCS_STOMPED; c_stomped up, c_fcs unchanged (P12) |
| 9 | Truncated frame | carrier lost mid-payload | D_TRUNCATED; not an FCS discard (P11) |
| 10 | Truncation in the header | carrier lost during R_DA | aborted from that state (P14) |
| 11 | Truncation during preamble | carrier lost before the delimiter | D_NO_DELIMITER or D_TRUNCATED; no release |
| 12 | Undersize with a good check | 40 octets, valid FCS | D_UNDERSIZE — a peer fault, not a link fault |
| 13 | Oversize | above the local ceiling | D_OVERSIZE (Chapter 5.7) |
| 14 | Undefined length/type band | field = 0x05F0 | D_LENTYPE_BAND (Chapter 5.5) |
| 15 | Declared more than delivered | length 400, 380 arrive | D_SHORT_DECLARED |
| 16 | Client cannot accept | back-pressure at the client | D_CLIENT_FULL — attributed to this device |
| 17 | Abort deadline met | bad check, normal pipeline | abort within ABORT_DEADLINE (P7) |
| 18 | Abort deadline missed | insert delay in the verdict path | deadline_missed; c_deadline_missed increments |
| 19 | Provisional never resolved | suppress commit and abort | P1 fires; c_unresolved increments |
| 20 | Conservation: frame lost | drop a frame inside the pipeline | c_started exceeds c_accounted; conservation_error |
| 21 | Conservation: counted twice | assert both delivered and discarded | conservation_error (P8) |
| 22 | Overlapping frames | start a frame while one is open | conservation_error (P9) |
| 23 | Wasted-octet accounting | 100 frames, 10 with bad checks | c_wasted_octets matches the released totals |
| 24 | Counter clear | assert clear after faults | counts zero; first-cause and worst_wasted survive (P16) |
13. Debugging — Which Stage, Which Owner
Symptom — the drop counter is high and the link looks healthy.
Read the attribution before anything else. If c_filtered dominates, nothing is wrong: that is traffic for other stations, and on a flooded segment it is most arrivals. A design that folds filter discards into a single drop count produces a number that rises with traffic volume and points at nothing — which is why Section 9 computes c_faults_total from the fault categories only.
Symptom — check-sequence discards on a link whose peer reports nothing.
Compare c_fcs against c_stomped. A stomped frame means an upstream device already found the error, so this link is proven good and the fault is one or more hops back — on a link neither of these two devices touches. Merging the two counters sends the investigation to the one link that has been eliminated.
Symptom — the application receives corrupt data and the receive counters show the frame was discarded.
The receiver knew, and the client got the octets anyway. This is the missing abort, and the confirming evidence is c_unresolved from Section 10 or rel_provisional asserting with no subsequent commit or abort. The receive path is not at fault in its adjudication — it detected the bad frame correctly — and the fault is entirely in the delivery contract.
Symptom — aborts are delivered but the client still forwards bad frames.
The client cannot honour the contract. Either it acts on provisional octets before commit, or the abort is arriving after its holding buffer forced it to commit. Check deadline_missed and worst_abort_latency: if the deadline is being met, the client is acting early and the fault is above; if it is not, the receiver's verdict path is too slow for the client it was paired with.
Symptom — c_started exceeds c_accounted and nothing else is wrong.
Frames are being lost inside the pipeline — a state machine that returns to idle without resolving, a discard site that forgets to signal, a stage that swallows a frame under back-pressure. No other check finds this, because every other check examines frames the design knows about. Conservation is the only property that sees a frame nobody counted.
Symptom — truncations and check-sequence mismatches counted together.
An attribution bug rather than a link problem. A truncated frame has no completed check to disagree with (Chapter 5.8 §4), so reporting it as a mismatch sends the investigation from flow control and buffering to signal integrity. The tell is that the two counters move together in a fixed ratio, which no physical fault produces.
Symptom — cut-through latency measured and no better than store-and-forward.
The client is holding everything until commit, which is correct behaviour and negates the benefit. The latency win requires the client to act provisionally too, and to be able to undo its own actions — which in a switch means the egress path may already be transmitting, and the abort becomes a stomp on the next link.
Symptom — everything works and one peer's frames are always discarded as D_LENTYPE_BAND.
Not corruption. A value in the undefined band arriving consistently from one peer is a transmitter emitting fields it should not (Chapter 5.5 §14) — the 100%-against-one-peer signature that distinguishes a structural fault from a physical one.
14. Common Misconceptions
"The receive path is the transmit path in reverse."
The wrong model: the same stages, run backwards.
What it costs: you expect the receiver to have the quantities the transmitter had, and you design as though the frame's shape were known. It is not — the payload's end is unknown until the frame ends, and the length/type field's meaning is unknown until it arrives.
The corrected model: a transmitter assembles from settled quantities; a receiver discovers, one octet at a time, and its last discovery invalidates or confirms all the earlier ones. And it cannot refuse to start, which was the transmit path's entire defence against its one unrecoverable event.
"A frame that passes the address filter is accepted."
The wrong model: the filter's verdict is the accept decision.
What it costs: a design that treats the filter as final has no reason to build an abort path, and a corrupted destination address that happens to match produces a delivered frame with no further scrutiny.
The corrected model: every decision before the check sequence is provisional, including the filter's — the address itself may be corrupt. The check is last by construction (Chapter 5.1), so it is the only stage whose verdict is final.
"Cut-through is a latency optimisation in the receive path."
The wrong model: the receiver chooses it and gains from it alone.
What it costs: a receiver implements provisional delivery, the client holds everything until commit — correctly — and the design has store-and-forward latency with cut-through complexity.
The corrected model: half the mechanism is the client's. The benefit is realised only if the client acts sooner, which requires the client to be able to undo its own actions. In a switch that pushes the problem to the egress path, where nothing can be withdrawn from the wire — and the resolution is Chapter 5.8 §9's stomp.
"The check almost always passes, so early delivery is safe."
The wrong model: an abort path is a rarely-used feature not worth the complexity.
What it costs: the design is correct on every good frame and hands corrupt data to the client on every bad one, with the receiver's own counters showing that it knew. The rarity is exactly what keeps it out of testing.
The corrected model: frequency is not the question — the contract is. Either delivery waits for the verdict, or it is marked provisional and resolved. There is no third coherent option, and the one that looks like a third is the one that corrupts silently.
"One drop counter is enough."
The wrong model: a discarded frame is a discarded frame.
What it costs: six unrelated faults — a physical-layer synchronisation failure, a peer's malformed frames, this device's configuration, this link's corruption, an upstream link's corruption, and this device's buffering — merge into one number that rises with normal traffic, because filter discards are in it too.
The corrected model: attribution by stage, with filter discards kept out of the fault total entirely and stomped frames attributed upstream rather than to this link. Six causes, six owners, six fixes.
15. Interview Reasoning
"How is a MAC receive path different from a transmit path?"
The weak answer is "it goes the other way". The answer that ends the topic is that a transmitter assembles from settled quantities and a receiver discovers, so every receive decision is provisional until the check sequence — which Chapter 5.1 put last by construction. The payoff is the question that follows: may a receiver begin delivering a frame it might have to withdraw? — which the transmit path never faces, because it never acts on information it does not have.
"What does cut-through oblige a receiver to do?"
Provide an abort. The complete answer states the contract in both directions: the receiver marks provisional octets, guarantees exactly one of commit or abort per accepted frame, and delivers the abort within a bounded time; the client holds provisional octets without acting on them and is able to discard up to a maximum frame's worth. A design with one side but not the other is broken in a way neither side can detect alone.
"A receiver delivers early and has no abort. What is wrong with that?"
It is correct on every good frame and hands corrupt data to the client on every bad one — while the receiver's own counters record that it detected the failure. The strong answer names why it survives review: the assertion people write, "delivered implies valid", passes, because a testbench evaluates validity with hindsight at end of frame. The property is true of the completed frame and false at the instant it samples.
"You have one 'frames dropped' counter. What would you split it into?"
Six categories with different owners, and the first split is the important one: filter discards are not faults — on a flooded segment they are most arrivals, and including them makes the drop count track traffic volume. Then this device (client-full), this link (check mismatch, truncation), upstream (a stomped frame, which proves this link good), and a peer (undersize with a good check, undefined length/type band, declared length exceeding what arrived). The finishing detail is that the fault total should be computed from the categories, so "fault" is defined in one place.
16. Understanding Check
Because the check sequence is last by construction, and it is the only stage whose verdict is final.
Chapter 5.1 established the field order and made the point structural: the check covers everything before it, so it cannot be anywhere but the end. Every earlier stage therefore acts on octets that have not been verified.
The address filter accepts on an address that may be corrupt. The length/type resolver classifies a field that may be corrupt. The pad stripper finds a boundary from a length field that may be corrupt. Each is a hypothesis, and one octet at the very end confirms or refutes all of them at once.
Which is why the receive path's only real design freedom is the delivery policy. It cannot pre-compute, and it cannot refuse to start — whatever arrives must be processed at line rate. What it can choose is whether to act on a hypothesis before the evidence arrives, and that choice determines whether an abort path has to exist.
17. What's Next
The claim this chapter defended: a receiver discovers rather than assembles, so every decision it takes is provisional until the last octet — and the delivery policy is the choice of what to do about that.
The check sequence is last by construction, so the filter's accept, the length/type resolution and the pad boundary are all hypotheses until it arrives. A receiver cannot pre-compute and cannot refuse to start. What it can decide is whether to act before the evidence: store-and-forward waits and delivers only settled frames; cut-through releases early, marks every octet provisional, and must be able to withdraw. The design that releases early with no abort is correct on every good frame and corrupts silently on every bad one — and the assertion that appears to verify it passes only because the testbench evaluates with hindsight.
And every frame that does not reach the client belongs to a stage. Six of them, with six owners — a physical-layer synchronisation failure, a peer's malformed frames, this device's configuration, this link's corruption, an upstream link's corruption, and this device's buffering — plus one category that is not a fault at all and must be kept out of the total.
Chapter 7.3 — Frame Validity: Runts, Giants and Malformed Frames takes the discard categories this chapter produced and defines them precisely. What exactly makes a frame invalid, which of the definitions are normative and which are this device's own, and what a MAC does with each case — including the ones where the correct action is to count and forward rather than to drop.
And it closes a thread Chapter 5.6 §8 opened: a short frame with a good check sequence and a short frame with a bad one are different faults with different owners, and the classification that separates them is the same one that has to decide, for every malformed frame, whether the problem is the frame, the link, or the device looking at it.
The full path is on the Ethernet curriculum index.
Continue learning
Related tutorials
- Related topic
The Shared-Medium Problem
Why several independent transmitters on one medium is a distributed timing problem, not a formatting problem. Propagation delay makes every station's view of the medium stale, so two locally correct decisions can still collide — and that is the constraint the Ethernet MAC was built around.
- Related topic
Ethernet System Architecture
Client, MAC, reconciliation sublayer, PCS, PMA, PMD, medium — six blocks whose port lists are the real content. Each contract has two halves: what a layer delivers, and what it is forbidden to know about its neighbours, which is why one MAC outlived every physical layer.
- 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
The MAC Layer
Framing, addressing, error detection, sizing, interframe gap and transmit access. Each exists because the medium is unreliable, shared, or both — and knowing which reason applies predicts exactly what full duplex deleted and what it left untouched.
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.
