Skip to content
VLSI Mentor

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

A comparison of the transmit and receive paths. The transmit path knows the client octet count, the padding target, the length field value and the eventual frame size before the preamble is sent, so every stage acts on settled information. The receive path learns the destination address one octet at a time, learns whether the length or type field is a length or a type when it arrives, learns where the payload ends only when the frame ends, and learns whether any of it was valid at the very last octet when the check sequence arrives.Transmit: knowsevery quantity before thepreambleCount, target,length, sizeall settled up frontReceive: discoversone octet at a timeAddress, then type,then endeach learned on arrivalThe check, lastsettles whether any of itwas realDeliver early, orwait?the question 7.1 neverfaces12
Figure 1 — the transmit path knows every quantity before it starts; the receive path learns each one as it arrives, and the last one settles the rest.

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
 
endmodule

Classification: 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

Two receive policies compared over the life of a frame. A store and forward receiver buffers the entire frame, waits for the check sequence verdict, and then delivers a frame that is certainly good, so no abort path is needed but latency is one frame time. A cut through receiver begins delivering as soon as the address filter accepts, so latency is a few octet times, but the check verdict arrives after octets have already been handed over and an abort path is therefore mandatory.Store-and-forwardbuffer the whole frameVerdict, thendeliverdelivery is always finalNo abort pathlatency: one frame timeCut-throughdeliver on filter acceptVerdict arriveslateroctets already handed overAbort path mandatorylatency: a few octet timesEarly delivery, noabortthe incoherent thirddesign12
Figure 2 — the two policies differ in when delivery starts, and therefore in whether an abort path has to exist.

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
 
endmodule

Classification: 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 promisesThe client promises
every provisional octet is markedprovisional octets are held, not acted on
exactly one of commit or abort per accepted frameit handles both, as events
the abort arrives within a bounded time of the last octetit can discard up to a maximum frame's worth
a committed frame is never withdrawnit 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
 
endmodule

Classification: 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

Discarded frames are attributed to the stage that rejected them. A discard at the delimiter stage means synchronisation never happened and points at the physical layer. A discard at the address filter is normal traffic for other stations and is not a fault at all. A discard at the size check points at a peer or at a configuration. A discard at the length or type stage points at a corrupt or non conforming field. A discard at the check sequence points at the link, or upstream if the frame was stomped. And a discard because the client had nowhere to put it points at this device.Delimiternever synchronised —physical layerAddress filternot a fault at allSizea peer, or configurationLength/typea corrupt or odd fieldCheck sequencethis link — or upstream ifstompedClient fullthis deviceOne counter mergesall sixand points nowhere12
Figure 3 — a discarded frame is attributed to the stage that discarded it, because the stages point at unrelated faults.

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
 
endmodule

Classification: 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
 
endmodule

Classification: 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// 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

#ScenarioStimulusWhat must be observed
1Good frame, store-and-forwardvalid 512-octet framedelivered after the verdict; rel_provisional never asserts (P6)
2Good frame, cut-throughthe same framereleased early, all provisional; commit at the end (P1, P3)
3Minimum-size frame64 octetsdelivered; pad stripped by declared length (Chapter 5.6)
4Maximum-size frame1518 octetsdelivered; no oversize discard
5Filtered frameaddress for another stationD_FILTER; no release (P15); c_faults_total unchanged (P13)
6Bad check, cut-throughvalid until the last octet, then a wrong FCSprovisional octets released, then abort (P1, P4)
7Bad check, store-and-forwardthe same framenothing released at all; c_fcs increments
8Stomped frameFCS is the exact inverseD_FCS_STOMPED; c_stomped up, c_fcs unchanged (P12)
9Truncated framecarrier lost mid-payloadD_TRUNCATED; not an FCS discard (P11)
10Truncation in the headercarrier lost during R_DAaborted from that state (P14)
11Truncation during preamblecarrier lost before the delimiterD_NO_DELIMITER or D_TRUNCATED; no release
12Undersize with a good check40 octets, valid FCSD_UNDERSIZE — a peer fault, not a link fault
13Oversizeabove the local ceilingD_OVERSIZE (Chapter 5.7)
14Undefined length/type bandfield = 0x05F0D_LENTYPE_BAND (Chapter 5.5)
15Declared more than deliveredlength 400, 380 arriveD_SHORT_DECLARED
16Client cannot acceptback-pressure at the clientD_CLIENT_FULL — attributed to this device
17Abort deadline metbad check, normal pipelineabort within ABORT_DEADLINE (P7)
18Abort deadline missedinsert delay in the verdict pathdeadline_missed; c_deadline_missed increments
19Provisional never resolvedsuppress commit and abortP1 fires; c_unresolved increments
20Conservation: frame lostdrop a frame inside the pipelinec_started exceeds c_accounted; conservation_error
21Conservation: counted twiceassert both delivered and discardedconservation_error (P8)
22Overlapping framesstart a frame while one is openconservation_error (P9)
23Wasted-octet accounting100 frames, 10 with bad checksc_wasted_octets matches the released totals
24Counter clearassert clear after faultscounts 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

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.