Skip to content
VLSI Mentor

Ethernet · Module 7

The Transmit Path

Framing, padding, check append and interframe gap in one datapath — and their order is forced rather than chosen, because the pad is inside the covered range and the gap is outside the frame. Plus the one event the order cannot help with.

Modules 5 and 6 built the pieces one at a time. Chapter 5.6 built padding, Chapter 5.8 placed the check-sequence append point, Chapter 5.9 built the interframe gap, and Chapter 6.4 built an engine fast enough to run at line rate.

This chapter puts them in one datapath, and the interesting claim is that their order is not a choice.

It looks like a choice. A transmit path is a pipeline, pipelines have stages, and stage order is usually an engineering decision balanced against timing and area. Here it is forced — each ordering constraint follows from something already established, and every wrong order produces a specific, identifiable, and usually silent failure.

Two constraints do all the work:

  • Padding must precede the check sequence, because Chapter 5.8 established that the pad is inside the covered range.
  • The check sequence must precede the gap, because the gap is outside the frame and the check is the frame's last field.

And a third that is not about order at all but about which quantity is used: the length field carries the client's octet count, not the padded one — Chapter 5.6 §5's most damaging bug, which produces a frame that is internally consistent and delivers pad as data.

1. Scope — What This Chapter Owns

This chapter owns the assembly: the order of operations in a transmit datapath, why each ordering constraint is forced, how back-pressure interacts with a frame that has already begun, what happens on an underrun, and how to verify that the frames leaving the interface are well formed.

It does not re-derive the pieces. Chapter 5.6 owns the pad and the floor; Chapter 5.8 owns what the check covers and where it is appended; Chapter 5.9 owns the gap and its deficit; Chapter 6.4 owns the engine. Each appears here as a block with an interface.

It does not own the client interface above it — descriptor rings, DMA and buffer management are a host-interface subject — nor the transmit arbitration that decides which frame goes next, which is queueing.

And it does not own the receive path, which is Chapter 7.2 and which is not symmetric with this one: a transmitter knows what it is building, and a receiver is discovering it.

The question this chapter answers that its neighbours do not: given all the pieces, what constrains the order they are applied in — and what exactly breaks when the order is wrong?

2. The Order, and Why Each Step Is Where It Is

A transmit pipeline in stages. The preamble and start delimiter are generated first and are outside the covered range. The addresses and length or type field follow and begin the covered range. Client data follows, then padding, which is still inside the covered range. The check sequence is computed over everything from the addresses through the padding and is appended next. The interframe gap follows and is outside the frame entirely. Two constraints fix the order: padding precedes the check because the pad is covered, and the check precedes the gap because the gap is not part of the frame.Preamble + SFDoutside the covered rangeAddresses + L/Tcovered range beginsClient datacoveredPadcovered — so it comesfirstCheck sequencecomputed over all of theaboveInterframe gapoutside the frameTwo constraintspad before FCS, FCS beforegap12
Figure 1 — the two ordering constraints that fix the whole pipeline: the pad is inside the covered range, and the gap is outside the frame.

Work the constraints one at a time, because each one names its source.

The preamble is outside the covered range, so the check engine must not start until after the start delimiter. Chapter 5.8 §2 gave the reason: the preamble is consumed and regenerated at every hop, so a check computed over it would be invalid on arrival everywhere.

The pad is inside the covered range, so it must exist before the check is computed. Chapter 5.8 §3 gave that reason too: excluding the pad would create a blind region across most of the shortest frames on the link. This is the constraint people get wrong, because padding feels like a finishing touch applied to a completed frame.

The check sequence is the frame's last field, established in Chapter 5.1 — so nothing that belongs to the frame may follow it.

The gap is not part of the frame at all, so it follows the check. And Chapter 5.9 added a constraint the pipeline must respect rather than merely obey: the gap's duration is enforced by a state that has no early exit, so the transmit path cannot start the next frame's preamble on demand.

Which leaves exactly one genuine degree of freedom in the whole pipeline: whether the check is computed as the data flows or in a separate pass after the frame is assembled. Section 6 shows why the first is the only workable answer at line rate, and what it costs.

3. What Each Wrong Order Breaks

Four incorrect orderings and their consequences. Computing the check sequence before padding leaves the pad outside the covered range, so every padded frame is rejected by every peer. Computing the length after padding makes the length field report the padded size, so a receiver delivers padding to its client as data. Starting the interframe gap before the check sequence places the four check octets inside the gap, so the frame appears truncated. Starting the next preamble immediately after the check octets shortens the gap below its minimum.Check before padpad outside the coveredrangeEvery padded framerejectedloud, found at onceLength after paddingthe padded size isreportedPad delivered asdatasilent — no error anywhereGap before the checkcheck octets inside thegapFrame reads astruncatedplus a runt on the wirePreamble too earlygap below its minimumIntermittent peerdropsdepends on peer tolerance12
Figure 2 — four plausible reorderings, and the distinct failure each produces.

Take them in order of how hard they are to find, which is roughly the reverse of how bad they are.

Check sequence computed before padding. The pad is appended after the four check octets, so the frame's field order is wrong and the check covers only the client data. Every receiver computes over data-plus-pad and disagrees. Every padded frame is rejected by every peer — which is total, immediate, and therefore the easiest of the four to find. It also fails Chapter 5.1's structural requirement that the check sequence is last.

Interframe gap started before the check octets are emitted. The four octets land inside what should be idle. A receiver sees the frame end four octets early — a truncation, Chapter 6.3 §8's case — and then sees four stray octets that may or may not delimit as a runt. Two symptoms from one bug, and the runt count is the misleading one because it points at a peer.

Next preamble started as soon as the check octets are out. The gap is shortened, potentially below Chapter 5.9's permitted floor. The symptom depends on the peer, because receiver gap tolerance varies — so the design works against the peer it was developed with and fails intermittently against others, which is the worst debugging shape in this list.

Length computed after padding. No ordering is visibly wrong; the frame is well formed, the check is correct, and the field order is right. The length field simply reports the wrong quantity, and a conforming receiver delivers up to 43 octets of pad to its client as data. Chapter 5.6 §14 built the directed test for it, and it is worth repeating why nothing else finds it: the frame is internally consistent, so every structural check passes.

Read the last two rows of the figure together. The three ordering errors produce visible failures — rejected frames, truncations, intermittent drops. The quantity error produces a frame that is accepted and wrong. The pipeline's order is the easy part to get right and the easy part to verify; which value feeds which field is neither.

4. RTL 1 — The Ordered Datapath

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE.
//
// The transmit datapath's control, with the stage order of Section 2
// expressed as a state graph.
//
// Two things are computed at frame start and never recomputed:
//
//   the LENGTH to emit  -- the CLIENT's octet count
//   the PAD target      -- the client-data region's size
//
// They come from one decision (Chapter 5.6 §5) so they cannot disagree.
// A design that recomputes either later has two representations of one
// boundary and no mechanism keeping them equal.
package mac_tx_pkg;
  import frame_size_pkg::*;
 
  typedef enum logic [3:0] {
    S_IDLE,
    S_PREAMBLE,   // outside the covered range
    S_SFD,        // outside the covered range
    S_HEADER,     // covered range begins here
    S_DATA,       // covered
    S_PAD,        // covered -- BEFORE the check, per Section 2
    S_FCS,        // the frame's last field
    S_GAP,        // outside the frame -- AFTER the check
    S_ABORT       // underrun: finish the frame and mark it (Section 9)
  } tx_state_e;
 
endpackage
 
module mac_tx_controller
  import mac_tx_pkg::*;
  import frame_size_pkg::*;
  import crc32_pkg::*;
(
  input  logic clk,
  input  logic rst_n,
 
  // From the client.
  input  logic        req_valid,
  input  logic [10:0] req_octets,        // client data only
  input  logic        req_is_length_form,
  input  logic        cli_valid,
  input  logic [7:0]  cli_data,
  input  logic        cli_last,
 
  // From the gap enforcer of Chapter 5.9. The transmit path may not
  // start a frame until this permits it, and it is an INPUT rather than
  // something this module counts, so the gap's floor stays structural.
  input  logic        gap_permits_start,
 
  // To the interface.
  output logic        tx_valid,
  output logic [7:0]  tx_data,
  output logic        tx_last,
 
  // To the CRC engine. Note crc_enable is high for exactly the covered
  // range -- Chapter 5.8's boundary, expressed as one signal.
  output logic        crc_start,
  output logic        crc_enable,
  output logic [7:0]  crc_data,
  input  logic [31:0] crc_fcs,
 
  output tx_state_e   state,
  output logic        frame_aborted,
  output logic [10:0] emitted_length
);
 
  tx_state_e   state_q;
  logic [3:0]  cnt_q;          // preamble octets, FCS octets
  logic [10:0] sent_q;         // octets of the client-data region sent
  logic [10:0] target_q;       // that region's size
  logic [10:0] length_q;       // the value the length field will carry
  logic [31:0] fcs_q;
 
  assign state          = state_q;
  assign emitted_length = length_q;
 
  // ONE decision, taken at frame start. The pad count and the length are
  // both read off it, so they cannot drift (Chapter 5.6 §5).
  wire [10:0] region_target = (req_octets < 11'(MIN_CLIENT))
                                ? 11'(MIN_CLIENT) : req_octets;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      state_q       <= S_IDLE;
      cnt_q         <= '0;
      sent_q        <= '0;
      target_q      <= '0;
      length_q      <= '0;
      fcs_q         <= '0;
      tx_valid      <= 1'b0;
      tx_data       <= '0;
      tx_last       <= 1'b0;
      crc_start     <= 1'b0;
      crc_enable    <= 1'b0;
      crc_data      <= '0;
      frame_aborted <= 1'b0;
    end else begin
      tx_valid   <= 1'b0;
      tx_last    <= 1'b0;
      crc_start  <= 1'b0;
      crc_enable <= 1'b0;
 
      case (state_q)
        S_IDLE: if (req_valid && gap_permits_start) begin
          state_q       <= S_PREAMBLE;
          cnt_q         <= '0;
          sent_q        <= '0;
          target_q      <= region_target;
          // The CLIENT's count, never the target. Section 3's fourth row.
          length_q      <= req_octets;
          frame_aborted <= 1'b0;
        end
 
        // Seven preamble octets. crc_enable stays LOW: outside the
        // covered range (Chapter 5.8 §2).
        S_PREAMBLE: begin
          tx_valid <= 1'b1;
          tx_data  <= 8'h55;
          if (cnt_q == 4'd6) begin state_q <= S_SFD; cnt_q <= '0; end
          else                     cnt_q   <= cnt_q + 1'b1;
        end
 
        S_SFD: begin
          tx_valid  <= 1'b1;
          tx_data   <= 8'hD5;
          // The engine is started HERE, so the first covered octet is the
          // first address octet and not the delimiter.
          crc_start <= 1'b1;
          state_q   <= S_HEADER;
          cnt_q     <= '0;
        end
 
        // Addresses and length/type. Covered.
        S_HEADER: if (cli_valid) begin
          tx_valid   <= 1'b1;
          tx_data    <= cli_data;
          crc_enable <= 1'b1;
          crc_data   <= cli_data;
          if (cnt_q == 4'd13) begin state_q <= S_DATA; cnt_q <= '0; end
          else                      cnt_q   <= cnt_q + 1'b1;
        end
 
        S_DATA: begin
          if (cli_valid) begin
            tx_valid   <= 1'b1;
            tx_data    <= cli_data;
            crc_enable <= 1'b1;
            crc_data   <= cli_data;
            sent_q     <= sent_q + 1'b1;
            if (cli_last) begin
              // Pad if short, otherwise straight to the check. The two
              // exits are the only ones, and neither can reach S_GAP.
              state_q <= ((sent_q + 1'b1) < target_q) ? S_PAD : S_FCS;
              cnt_q   <= '0;
            end
          end else if (tx_needs_octet_now) begin
            // UNDERRUN. The frame cannot be un-started, so it is finished
            // and marked. Section 9 owns what "marked" means.
            state_q <= S_ABORT;
          end
        end
 
        // Pad is COVERED, so crc_enable is high here too. This is the
        // constraint of Section 2 expressed in one line.
        S_PAD: begin
          tx_valid   <= 1'b1;
          tx_data    <= 8'h00;
          crc_enable <= 1'b1;
          crc_data   <= 8'h00;
          sent_q     <= sent_q + 1'b1;
          if ((sent_q + 1'b1) >= target_q) begin
            state_q <= S_FCS;
            cnt_q   <= '0;
            fcs_q   <= crc_fcs;
          end
        end
 
        // The check sequence. crc_enable is LOW: the field does not cover
        // itself (Chapter 5.8 §3).
        S_FCS: begin
          tx_valid <= 1'b1;
          tx_data  <= fcs_q[8*cnt_q +: 8];
          if (cnt_q == 4'd3) begin
            tx_last <= 1'b1;
            state_q <= S_GAP;
            cnt_q   <= '0;
          end else begin
            cnt_q <= cnt_q + 1'b1;
          end
        end
 
        // The gap is entered unconditionally after the check and left
        // only when the enforcer permits -- so the floor is not this
        // module's to get wrong (Chapter 5.9 §9).
        S_GAP: if (gap_permits_start) state_q <= S_IDLE;
 
        S_ABORT: begin
          frame_aborted <= 1'b1;
          state_q       <= S_GAP;
        end
 
        default: state_q <= S_IDLE;
      endcase
    end
  end
 
endmodule

Classification: synthesizable.

What it teaches: that crc_enable is the covered range, expressed as one signal across the whole state graph. It is low in S_PREAMBLE and S_SFD, high in S_HEADER, S_DATA and S_PAD, and low again in S_FCS. Reading those five values in order is Chapter 5.8's Figure 1, and a design in which they do not match that figure has an incorrect covered range regardless of how good its engine is.

Deliberately simplified: one octet per clock, a fixed 14-octet header, and no tag insertion. A tagged frame moves the length/type field (Chapter 5.5 §8) and changes the client-data floor (Chapter 5.6 §9), which adds states rather than changing any ordering constraint.

Production implication: gap_permits_start is an input rather than a counter inside this module, and that is deliberate. Chapter 5.9 §9 made the gap floor structural by putting it in a state with no early exit; importing it as a permission signal keeps that guarantee intact. A transmit controller that counts the gap itself has taken a structural guarantee and turned it into a computed one, which is the specific downgrade that chapter argued against.

5. RTL 2 — Back-Pressure Without Losing the Frame

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE.
//
// The assembler between a bursty client and an interface that cannot be
// stalled.
//
// The two directions are not symmetric and conflating them is the bug:
//
//   client -> assembler : may stall. The client is a memory system and
//        stalls whenever it feels like it. The assembler absorbs this.
//
//   assembler -> wire   : may NOT stall. Once the preamble is out, the
//        interface consumes an octet per cycle until the frame ends.
//        There is no back-pressure signal in that direction because
//        there is no such thing as pausing a frame mid-transmission.
//
// So the assembler's job is to guarantee that, from the moment it starts
// a frame, it can supply an octet every cycle until the end -- and its
// only tool is refusing to start until it can.
module tx_assembler
#(
  parameter int unsigned DEPTH = 64,
  // Octets that must be buffered before a frame may start. The whole
  // underrun defence lives in this number.
  parameter int unsigned START_THRESHOLD = 32
) (
  input  logic clk,
  input  logic rst_n,
 
  // Client side: may stall freely.
  input  logic        cli_valid,
  input  logic [7:0]  cli_data,
  input  logic        cli_last,
  output logic        cli_ready,
 
  // Wire side: consumes on demand, cannot be stalled.
  input  logic        wire_take,
  output logic        wire_valid,
  output logic [7:0]  wire_data,
  output logic        wire_last,
 
  // To the controller: it is safe to begin a frame.
  output logic        safe_to_start,
  // The frame is in flight and the buffer has run dry.
  output logic        underrun
);
 
  logic [7:0]  mem_q [DEPTH];
  logic [7:0]  wr_q, rd_q;
  logic [8:0]  occ_q;
  logic        in_frame_q;
  logic        last_seen_q;
 
  assign cli_ready = (occ_q != 9'(DEPTH));
 
  // A frame may start when either the whole frame is buffered -- the
  // client's last octet has arrived -- or enough is buffered to cover
  // the client's worst-case stall. The first condition is what a
  // store-and-forward transmitter uses and it removes underrun entirely.
  assign safe_to_start = last_seen_q || (occ_q >= 9'(START_THRESHOLD));
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      wr_q <= '0; rd_q <= '0; occ_q <= '0;
      in_frame_q <= 1'b0; last_seen_q <= 1'b0;
      wire_valid <= 1'b0; wire_data <= '0; wire_last <= 1'b0;
      underrun <= 1'b0;
    end else begin
      wire_valid <= 1'b0;
      wire_last  <= 1'b0;
      underrun   <= 1'b0;
 
      if (cli_valid && cli_ready) begin
        mem_q[wr_q] <= cli_data;
        wr_q        <= wr_q + 1'b1;
        occ_q       <= occ_q + 1'b1;
        if (cli_last) last_seen_q <= 1'b1;
      end
 
      if (wire_take) begin
        if (occ_q != '0) begin
          wire_valid <= 1'b1;
          wire_data  <= mem_q[rd_q];
          rd_q       <= rd_q + 1'b1;
          occ_q      <= occ_q - 1'b1;
          if (last_seen_q && (occ_q == 9'd1)) begin
            wire_last   <= 1'b1;
            last_seen_q <= 1'b0;
            in_frame_q  <= 1'b0;
          end else begin
            in_frame_q <= 1'b1;
          end
        end else if (in_frame_q) begin
          // The wire wants an octet, the buffer is empty, and a frame is
          // in flight. There is no stall available in this direction, so
          // the frame is already unfixable -- report it and let the
          // controller mark it (Section 9).
          underrun <= 1'b1;
        end
      end
    end
  end
 
endmodule

Classification: synthesizable.

What it teaches: that the only defence against an underrun is the decision not to start. Once the preamble is out there is no back-pressure toward the wire, no way to pause, and no way to withdraw the frame. safe_to_start is therefore the entire mechanism, and every other underrun measure is damage control.

Deliberately simplified: a single threshold. Real designs derive it from the client's worst-case stall and the frame's remaining length, and a store-and-forward transmitter sets it to "the whole frame", which removes underrun entirely at the cost of a frame's worth of latency and buffer.

Production implication: underrun is reported by the assembler and acted on by the controller, rather than handled locally. The assembler cannot fix it — it has no access to the check engine and cannot mark the frame — and a design that lets the assembler paper over the condition by inserting filler octets has produced a frame that is well formed and carries wrong data. The component that detects a fault is often not the one that can respond to it, and wiring the report out is the difference between a marked frame and silent corruption.

6. Computing the Check As the Data Flows

Section 2 identified one genuine degree of freedom in the pipeline: whether the check sequence is computed as the data flows past or in a separate pass over an assembled frame. It is worth working through, because it is the only place in this chapter where a design chooses rather than obeys.

The two-pass approach is simpler and it is not available. Assemble the frame in a buffer, run the engine over it, append the result, transmit. The control logic is trivial — no state machine has to coordinate the engine with the datapath — and the check is computed over a frame that is already complete, so padding, the length field and the covered range are all settled before the engine starts.

What it costs is a frame time of latency and a frame of buffer, per transmission. At 1518 octets and 1 Gb/s that is about 12 microseconds and 1.5 kilobytes; at ten ports it is fifteen kilobytes of buffer whose only purpose is to let the check be computed conveniently.

And it does not scale in the direction that matters. The buffer is per-port and the latency is per-frame, so both grow with port count while the saving stays constant.

So the check is computed on the fly, and the cost of that decision is a coordination problem: the engine's enable must track the covered range exactly, cycle by cycle, through a state machine that is also generating padding and counting octets. Every ordering constraint in Section 2 becomes a constraint on crc_enable's timing rather than on the arrangement of buffers.

Which is why Section 4's controller looks the way it does. crc_enable is not a separate mechanism bolted onto the datapath; it is a per-state output of the same machine that drives tx_valid, so the two cannot drift apart.

7. Underrun — Finish It and Mark It

A frame that has started cannot be withdrawn. Its preamble and header are on the wire, a receiver is already synchronised to it, and there is no abort symbol at this layer.

So there are exactly four things a transmitter can do, and three of them are wrong.

ResponseWhat the receiver seesVerdict
stop transmittinga truncated framewrong — indistinguishable from a link fault
pad the shortfall and finish normallya valid frame with wrong dataworst — accepted and corrupt
finish with a deliberately wrong checka frame that fails its checkcorrect
assert that underrun never happenswhatever the design does anywaythe rejected property of Section 11

Row two is the one that must be argued against explicitly, because it is superficially attractive: the frame is the right length, the check is correct, nothing is malformed. And it delivers filler to the peer's client as data, with no error anywhere in either device — the same silent-corruption shape as Chapter 5.6's length-field bug, arriving by a different route.

Row one is wrong for a subtler reason. A truncated frame is what a link fault produces, so a transmitter that truncates on underrun is generating link-fault symptoms from a host-interface problem. Chapter 5.6 §8's classifier would call it a fragment — bad check sequence, below the floor — and send the investigation to the cable.

Row three is correct and it has a specific implementation: finish the frame to the right length and emit a check sequence that is deliberately wrong. Chapter 5.8 §9 already established the convention — the bitwise inverse of the correct value — and established why the exact inverse matters: it is astronomically unlikely to arise from corruption, so a receiver testing for it learns that an upstream device already knew this frame was bad.

Which makes the underrun response and the cut-through stomp the same mechanism used for the same reason: propagating a diagnosis through a protocol that has no field to carry one.

8. RTL 3 — The Underrun Handler

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE.
//
// Finishes a starved frame to the correct length and marks it, and
// records enough locally that the fault is attributable HERE rather than
// only at the far end.
//
// The three outputs are three different audiences:
//
//   force_bad_fcs   -> the receiver, which must reject the frame
//   c_underrun      -> whoever operates this device, who must fix it
//   first_at_octet  -> whoever debugs it, because WHERE it starved names
//                      the subsystem that starved
module tx_underrun_handler
  import frame_size_pkg::*;
#(
  parameter int unsigned CNT_W = 32
) (
  input  logic clk,
  input  logic rst_n,
  input  logic clear,
 
  input  logic        frame_start,
  input  logic        underrun,
  input  logic [13:0] octets_sent,
  input  logic [13:0] octets_expected,
 
  // To the datapath: keep transmitting to the correct length, with
  // filler, so the frame is the right SIZE -- and then invert the check
  // sequence so it is rejected. Length correct, check deliberately wrong.
  output logic        fill_active,
  output logic [7:0]  fill_data,
  output logic        force_bad_fcs,
 
  output logic [CNT_W-1:0] c_underrun,
  output logic             underrun_seen,     // sticky
  output logic [13:0]      first_at_octet,
  output logic [13:0]      first_frame_length,
  // Deepest into a frame that a starvation has ever occurred. Survives
  // clear: it bounds how much buffering would have been enough.
  output logic [13:0]      worst_at_octet
);
 
  logic active_q;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      active_q           <= 1'b0;
      fill_active        <= 1'b0;
      fill_data          <= '0;
      force_bad_fcs      <= 1'b0;
      c_underrun         <= '0;
      underrun_seen      <= 1'b0;
      first_at_octet     <= '0;
      first_frame_length <= '0;
      worst_at_octet     <= '0;
    end else begin
      if (clear) begin
        c_underrun <= '0;
        // The context deliberately survives. A count says how often; the
        // octet position says which subsystem, and only one of those can
        // be reconstructed later.
      end
 
      if (frame_start) begin
        active_q      <= 1'b0;
        fill_active   <= 1'b0;
        force_bad_fcs <= 1'b0;
      end else if (underrun && !active_q) begin
        active_q      <= 1'b1;
        // Fill to the CORRECT length. A frame that is the right size and
        // fails its check is unambiguous; a short frame is a fragment and
        // points at the link (Section 7).
        fill_active   <= 1'b1;
        fill_data     <= 8'h00;
        // And mark it. The inversion is Chapter 5.8 §9's convention, so a
        // receiver that tests for it learns this frame was known-bad
        // rather than corrupted in transit.
        force_bad_fcs <= 1'b1;
 
        if (!(&c_underrun)) c_underrun <= c_underrun + 1'b1;
        underrun_seen <= 1'b1;
 
        if (octets_sent > worst_at_octet) worst_at_octet <= octets_sent;
 
        if (c_underrun == '0) begin
          first_at_octet     <= octets_sent;
          first_frame_length <= octets_expected;
        end
      end
    end
  end
 
endmodule

Classification: synthesizable.

What it teaches: that the frame is filled to the correct length and marked, and both halves matter. Filling without marking produces a valid frame with filler in it — Section 7's worst row. Marking without filling produces a short frame, which is a fragment and points at the link. Only both together produce a frame that is unambiguously "this transmitter knew this was bad".

Deliberately simplified: the filler is zeros. Its value is irrelevant because the frame will be rejected — and Chapter 5.6 §13's rejected property applies here too, so nothing downstream should assume anything about it.

Production implication: worst_at_octet bounds the buffering that would have prevented every observed underrun, and it is the one number in this module that leads to a fix. A starvation at octet 12 of a frame means the start threshold was too low; one at octet 1400 means the client's sustained bandwidth is short, and those have nothing in common. A count alone distinguishes neither, which is why the position survives a counter clear and the count does not.

9. RTL 4 — Deliberate Error Insertion, and Keeping It Out of Production

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE TEST FEATURE.
//
// Emits deliberately malformed frames so that a receive path's error
// handling can be exercised against real traffic.
//
// Every error class Chapters 5 and 6 defined is reachable from here, and
// each is generated by ONE mechanism rather than by a special path, so
// the normal datapath is exercised even while producing bad frames.
//
// The guard is the design content. A test feature that can be enabled in
// production is a way to emit corrupt traffic onto a live network, and
// "it defaults to off" is not a guard -- a register defaults to whatever
// software last wrote.
module tx_error_insertion
  import frame_size_pkg::*;
#(
  // Tied off at integration. A production build ties this low and the
  // whole feature synthesises away; a test build ties it high. It is NOT
  // a software-writable bit, which is the point.
  parameter bit ERROR_INSERTION_PRESENT = 1'b0
) (
  input  logic clk,
  input  logic rst_n,
 
  input  logic       enable,           // ignored unless PRESENT
  input  logic [2:0] error_class,
 
  input  logic       frame_start,
  input  logic [13:0] octet_index,
  input  logic [13:0] frame_length,
 
  output logic       corrupt_octet,    // flip a bit in this octet
  output logic       force_bad_fcs,
  output logic       force_short,      // end the frame early: a fragment
  output logic       force_long,       // overrun the ceiling: a giant
  output logic       force_short_gap,  // Chapter 5.9's floor, violated
  output logic       insertion_active
);
 
  localparam logic [2:0] E_NONE      = 3'd0;
  localparam logic [2:0] E_BIT_FLIP  = 3'd1;  // one payload bit
  localparam logic [2:0] E_BAD_FCS   = 3'd2;  // Chapter 5.8's mismatch
  localparam logic [2:0] E_STOMP     = 3'd3;  // the exact inverse
  localparam logic [2:0] E_FRAGMENT  = 3'd4;  // below the floor
  localparam logic [2:0] E_GIANT     = 3'd5;  // above the ceiling
  localparam logic [2:0] E_SHORT_GAP = 3'd6;  // below the gap floor
 
  wire armed = ERROR_INSERTION_PRESENT && enable;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      corrupt_octet    <= 1'b0;
      force_bad_fcs    <= 1'b0;
      force_short      <= 1'b0;
      force_long       <= 1'b0;
      force_short_gap  <= 1'b0;
      insertion_active <= 1'b0;
    end else begin
      corrupt_octet   <= 1'b0;
      force_short     <= 1'b0;
      force_short_gap <= 1'b0;
 
      if (frame_start) begin
        force_bad_fcs    <= 1'b0;
        force_long       <= 1'b0;
        insertion_active <= armed && (error_class != E_NONE);
      end else if (armed) begin
        case (error_class)
          // A bit flipped mid-payload. Inside the covered range, so the
          // check catches it -- which is what makes it a useful stimulus
          // rather than an interesting one.
          E_BIT_FLIP: if (octet_index == (frame_length >> 1)) corrupt_octet <= 1'b1;
          E_BAD_FCS:  force_bad_fcs <= 1'b1;
          E_STOMP:    force_bad_fcs <= 1'b1;   // datapath inverts instead
          E_FRAGMENT: if (octet_index == 14'd40) force_short <= 1'b1;
          E_GIANT:    force_long <= 1'b1;
          E_SHORT_GAP: force_short_gap <= 1'b1;
          default: ;
        endcase
      end
    end
  end
 
  // The guard, restated where a reviewer will see it. A feature that can
  // emit malformed frames onto a live network must be ABSENT from the
  // netlist, not merely disabled in it -- so the check is on the
  // parameter, and a production build proves it by elaboration.
`ifndef TEST_BUILD
  // synopsys translate_off
  a_insertion_absent_in_production: assert final (ERROR_INSERTION_PRESENT == 1'b0)
    else $fatal(1, "error insertion is present in a non-test build");
  // synopsys translate_on
`endif
 
endmodule

Classification: synthesizable test feature.

What it teaches: that a test feature's guard belongs in the parameter, not in the register. enable is a runtime signal and a runtime signal is whatever software last wrote — a driver bug, a stale register image after a warm reset, or a debug script left running. ERROR_INSERTION_PRESENT is tied at integration, so a production build has no logic that can emit a corrupt frame, and the question stops being one of policy.

Deliberately simplified: the error positions are fixed. A verification-oriented version parameterises them so a sweep can place a bit flip at every octet, which is how the covered-range boundaries of Chapter 5.8 get exercised at their edges.

Production implication: E_STOMP and E_BAD_FCS are separate classes even though both produce a failing check, and the distinction is exactly Chapter 5.8 §9's. A stomp is the exact inverse and tells a receiver the frame was already known-bad; an ordinary bad check is indistinguishable from corruption on the link. A receive path must be tested against both, because a receiver that treats them identically discards the strongest evidence available about where a fault is.

10. RTL 5 — Checking What Actually Left the Interface

A transmit conformance monitor observes the octet stream at the interface rather than the controller's internal state. Watching the controller confirms that the state machine sequenced correctly. Watching the interface confirms what actually left the device, so it also catches faults introduced downstream of the controller, in a serialiser, a clock domain crossing, or a physical layer wrapper. The monitor checks preamble length, frame size against the floor and ceiling, the check sequence, and the interframe gap.Controller stateconfirms the FSM sequencedSerialiser, CDC, PHYwrapperdownstream of thecontrollerThe interfacewhat actually leftConformance monitorwatches hereBlind to thisa controller-side monitorSize, FCS, gap,preamblestructural, notbehavioural12
Figure 3 — the conformance monitor watches the interface, not the controller, so it catches faults introduced after the control logic is correct.
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE MONITOR.
//
// Watches the octet stream at the INTERFACE and checks that every frame
// leaving this device is well formed.
//
// Placement is the design decision. A monitor on the controller's state
// confirms the state machine sequenced correctly -- which it did, because
// it was written to. A monitor at the interface confirms what actually
// LEFT, so it also catches everything between: a serialiser dropping an
// octet, a clock-domain crossing losing a transfer, a physical-layer
// wrapper inserting or removing idle.
module tx_conformance_monitor
  import frame_size_pkg::*;
  import ifg_pkg::*;
#(
  parameter int unsigned CNT_W = 32,
  parameter int unsigned MAX_FRAME = 1518
) (
  input  logic clk,
  input  logic rst_n,
  input  logic clear,
 
  input  logic       tx_valid,
  input  logic [7:0] tx_data,
  input  logic       tx_last,
  input  logic       in_preamble,
 
  output logic [CNT_W-1:0] c_frames,
  output logic [CNT_W-1:0] c_short_preamble,
  output logic [CNT_W-1:0] c_undersize,     // below the floor
  output logic [CNT_W-1:0] c_oversize,      // above the ceiling
  output logic [CNT_W-1:0] c_short_gap,     // below Chapter 5.9's floor
 
  output logic             any_violation,   // sticky
  output logic [13:0]      first_bad_length
);
 
  logic [13:0] len_q;
  logic [7:0]  pre_q;
  logic [7:0]  gap_q;
  logic        in_frame_q;
 
  function automatic logic [CNT_W-1:0] bump(input logic [CNT_W-1:0] v);
    bump = (&v) ? v : (v + 1'b1);
  endfunction
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      len_q <= '0; pre_q <= '0; gap_q <= '0; in_frame_q <= 1'b0;
      c_frames <= '0; c_short_preamble <= '0; c_undersize <= '0;
      c_oversize <= '0; c_short_gap <= '0;
      any_violation <= 1'b0; first_bad_length <= '0;
    end else begin
      if (clear) begin
        c_frames <= '0; c_short_preamble <= '0; c_undersize <= '0;
        c_oversize <= '0; c_short_gap <= '0;
        // any_violation and first_bad_length deliberately survive: a
        // malformed frame leaving this device is never routine.
      end
 
      if (tx_valid) begin
        gap_q <= '0;
        if (in_preamble) begin
          pre_q      <= pre_q + 1'b1;
          in_frame_q <= 1'b0;
        end else begin
          len_q      <= in_frame_q ? (len_q + 1'b1) : 14'd1;
          in_frame_q <= 1'b1;
 
          if (!in_frame_q) begin
            // First octet after the delimiter: the preamble just ended,
            // so its length can be judged now.
            if (pre_q != 8'd8) begin
              c_short_preamble <= bump(c_short_preamble);
              any_violation    <= 1'b1;
            end
            pre_q <= '0;
          end
        end
 
        if (tx_last) begin
          automatic logic [13:0] final_len = in_frame_q ? (len_q + 1'b1) : 14'd1;
          c_frames <= bump(c_frames);
          in_frame_q <= 1'b0;
 
          // The two size bounds, checked on what actually left rather
          // than on what the controller intended.
          if (final_len < 14'(MIN_FRAME_OCTETS)) begin
            c_undersize <= bump(c_undersize);
            any_violation <= 1'b1;
            if (first_bad_length == '0) first_bad_length <= final_len;
          end
          if (final_len > 14'(MAX_FRAME)) begin
            c_oversize <= bump(c_oversize);
            any_violation <= 1'b1;
            if (first_bad_length == '0) first_bad_length <= final_len;
          end
        end
      end else begin
        // Idle. Counting it here means the gap is measured on the wire,
        // which is the only place it is real.
        gap_q <= (gap_q == 8'hFF) ? gap_q : (gap_q + 1'b1);
        if (tx_valid_next_cycle && (gap_q < 8'(MIN_SINGLE_GAP))) begin
          c_short_gap   <= bump(c_short_gap);
          any_violation <= 1'b1;
        end
      end
    end
  end
 
endmodule

Classification: synthesizable monitor.

What it teaches: that a conformance monitor's placement determines what it can find, and the obvious placement is the useless one. A monitor watching the controller's state confirms that the state machine sequenced correctly — which it did, because the same engineer wrote both. A monitor at the interface confirms what left the device, so it also covers the serialiser, the clock-domain crossing, and the physical-layer wrapper, none of which the controller can see.

Deliberately simplified: it checks structure and not content — sizes, preamble length, gap — and does not verify the check sequence, which would require a second engine. A production monitor usually adds one, because Chapter 6.2 §10's argument applies: a transmit engine and a receive engine that agree with each other and not with the standard pass every internal test.

Production implication: the gap is measured in the idle branch, from the interface's own valid signal, rather than taken from the controller's gap state. Chapter 5.9 §10 showed that the gap on the wire is not the gap the controller emitted — clock compensation and physical-layer processing change it downstream — and a monitor reading the controller's intent would report a conforming gap while a short one went out.

11. Assertions — Ordering, Quantities and the Response to Starvation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// P1 -- THE COVERED RANGE, as a property of crc_enable across the state
// graph. This one property is Chapter 5.8's Figure 1 (Section 4).
// ---------------------------------------------------------------------
property p_crc_enable_matches_covered_range;
  @(posedge clk) disable iff (!rst_n)
    crc_enable |-> (state inside {S_HEADER, S_DATA, S_PAD});
endproperty
a_crc_enable_matches_covered_range: assert property (p_crc_enable_matches_covered_range);
 
// ---------------------------------------------------------------------
// P2 -- The preamble and delimiter are never covered.
// ---------------------------------------------------------------------
property p_preamble_never_covered;
  @(posedge clk) disable iff (!rst_n)
    (state inside {S_PREAMBLE, S_SFD}) |-> !crc_enable;
endproperty
a_preamble_never_covered: assert property (p_preamble_never_covered);
 
// ---------------------------------------------------------------------
// P3 -- The check sequence never covers itself.
// ---------------------------------------------------------------------
property p_fcs_never_covered;
  @(posedge clk) disable iff (!rst_n)
    (state == S_FCS) |-> !crc_enable;
endproperty
a_fcs_never_covered: assert property (p_fcs_never_covered);
 
// ---------------------------------------------------------------------
// P4 -- THE FIRST ORDERING CONSTRAINT. Padding, when it happens, happens
// before the check sequence -- there is no path from S_FCS to S_PAD.
// ---------------------------------------------------------------------
property p_pad_precedes_fcs;
  @(posedge clk) disable iff (!rst_n)
    (state == S_FCS) |=> (state != S_PAD);
endproperty
a_pad_precedes_fcs: assert property (p_pad_precedes_fcs);
 
// ---------------------------------------------------------------------
// P5 -- THE SECOND ORDERING CONSTRAINT. The gap is only ever entered
// from the check sequence or an abort, never from data or pad.
// ---------------------------------------------------------------------
property p_gap_only_after_fcs;
  @(posedge clk) disable iff (!rst_n)
    (state == S_GAP) |-> ($past(state) inside {S_FCS, S_ABORT, S_GAP});
endproperty
a_gap_only_after_fcs: assert property (p_gap_only_after_fcs);
 
// ---------------------------------------------------------------------
// P6 -- THE QUANTITY, not an ordering. The length field carries the
// CLIENT's octet count. Section 3's fourth row, and the one failure that
// produces a well-formed frame.
// ---------------------------------------------------------------------
property p_length_is_client_count;
  @(posedge clk) disable iff (!rst_n)
    (state == S_HEADER) |-> (emitted_length == $past(req_octets, header_offset));
endproperty
a_length_is_client_count: assert property (p_length_is_client_count)
  else $error("length field carries the padded size, not the client's");
 
// ---------------------------------------------------------------------
// P7 -- The client-data region always reaches the floor.
// ---------------------------------------------------------------------
property p_region_reaches_floor;
  @(posedge clk) disable iff (!rst_n)
    (state == S_FCS) |-> (sent_q >= 11'(MIN_CLIENT));
endproperty
a_region_reaches_floor: assert property (p_region_reaches_floor);
 
// ---------------------------------------------------------------------
// P8 -- A frame is only started when the gap enforcer permits it. The
// floor stays structural because this module asks rather than counts.
// ---------------------------------------------------------------------
property p_start_requires_gap_permission;
  @(posedge clk) disable iff (!rst_n)
    ($past(state) == S_IDLE && state == S_PREAMBLE) |-> $past(gap_permits_start);
endproperty
a_start_requires_gap_permission: assert property (p_start_requires_gap_permission);
 
// ---------------------------------------------------------------------
// P9 -- A frame is only started when the assembler says it can be
// finished. The entire underrun defence (Section 5).
// ---------------------------------------------------------------------
property p_start_requires_safe_to_start;
  @(posedge clk) disable iff (!rst_n)
    ($past(state) == S_IDLE && state == S_PREAMBLE) |-> $past(safe_to_start);
endproperty
a_start_requires_safe_to_start: assert property (p_start_requires_safe_to_start);
 
// ---------------------------------------------------------------------
// P10 -- THE RESPONSE TO STARVATION. An underrun always produces a
// marked frame. Note the shape: it constrains what the design DOES when
// the event happens, and does not forbid the event (Section 11).
// ---------------------------------------------------------------------
property p_underrun_marks_the_frame;
  @(posedge clk) disable iff (!rst_n)
    underrun |-> ##[1:$] (force_bad_fcs until_with tx_last);
endproperty
a_underrun_marks_the_frame: assert property (p_underrun_marks_the_frame)
  else $error("a starved frame was transmitted without being marked");
 
// ---------------------------------------------------------------------
// P11 -- A starved frame is filled to the correct LENGTH as well as
// marked. Marking without filling produces a fragment, which points at
// the link (Section 7).
// ---------------------------------------------------------------------
property p_underrun_frame_is_full_length;
  @(posedge clk) disable iff (!rst_n)
    (frame_aborted && tx_last) |-> (frame_octets >= 14'(MIN_FRAME_OCTETS));
endproperty
a_underrun_frame_is_full_length: assert property (p_underrun_frame_is_full_length);
 
// ---------------------------------------------------------------------
// P12 -- A starved frame is NEVER emitted with a valid check sequence.
// The prohibition on Section 7's second row -- fill and finish normally.
// ---------------------------------------------------------------------
property p_underrun_never_valid;
  @(posedge clk) disable iff (!rst_n)
    (frame_aborted && tx_last) |-> force_bad_fcs;
endproperty
a_underrun_never_valid: assert property (p_underrun_never_valid);
 
// ---------------------------------------------------------------------
// P13 -- An underrun is counted locally. The receiver's rejection tells
// the far end; only this counter tells whoever can fix it.
// ---------------------------------------------------------------------
property p_underrun_counted_locally;
  @(posedge clk) disable iff (!rst_n)
    $rose(underrun) |=> (c_underrun == $past(c_underrun) + 1);
endproperty
a_underrun_counted_locally: assert property (p_underrun_counted_locally);
 
// ---------------------------------------------------------------------
// P14 -- First-cause context survives a counter clear.
// ---------------------------------------------------------------------
property p_underrun_context_survives_clear;
  @(posedge clk) disable iff (!rst_n)
    clear |=> ($stable(first_at_octet) && $stable(worst_at_octet) &&
               $stable(underrun_seen));
endproperty
a_underrun_context_survives_clear: assert property (p_underrun_context_survives_clear);
 
// ---------------------------------------------------------------------
// P15 -- Error insertion is absent, not merely disabled, unless the
// build is a test build.
// ---------------------------------------------------------------------
property p_no_insertion_when_absent;
  @(posedge clk) disable iff (!rst_n)
    (!ERROR_INSERTION_PRESENT) |-> !insertion_active;
endproperty
a_no_insertion_when_absent: assert property (p_no_insertion_when_absent);
 
// ---------------------------------------------------------------------
// P16 -- Every frame leaving the interface is within the size bounds,
// checked on the WIRE rather than on the controller's intent.
// ---------------------------------------------------------------------
property p_wire_frames_in_bounds;
  @(posedge clk) disable iff (!rst_n)
    (tx_valid && tx_last && !frame_aborted)
      |-> ((frame_octets >= 14'(MIN_FRAME_OCTETS)) && (frame_octets <= 14'(MAX_FRAME)));
endproperty
a_wire_frames_in_bounds: assert property (p_wire_frames_in_bounds);
 
// ---------------------------------------------------------------------
// P17 -- The preamble is exactly eight octets including the delimiter.
// ---------------------------------------------------------------------
property p_preamble_length;
  @(posedge clk) disable iff (!rst_n)
    $rose(in_frame_q) |-> ($past(pre_q) == 8'd8);
endproperty
a_preamble_length: assert property (p_preamble_length);
 
// ---------------------------------------------------------------------
// P18 -- COVERAGE. A padded frame, an unpadded frame, and an underrun.
// A run without the third has not exercised Section 8 at all.
// ---------------------------------------------------------------------
c_padded_frame:   cover property (@(posedge clk) disable iff (!rst_n) (state == S_PAD));
c_unpadded_frame: cover property (@(posedge clk) disable iff (!rst_n) (state == S_DATA ##1 state == S_FCS));
c_underrun_seen:  cover property (@(posedge clk) disable iff (!rst_n) underrun);

12. Verification — Twenty-Four Scenarios and a Starvation Nothing Else Produces

#ScenarioStimulusWhat must be observed
1Minimum-size frame46 octets of client datano padding; frame is 64 octets
2Short frame3 octets of client dataS_PAD entered; 43 pad octets; emitted_length = 3 (P6)
3One below the floor45 octetsexactly one pad octet
4One above the floor47 octetsno padding; S_DATA goes straight to S_FCS
5Maximum-size frame1500 octetsno padding; frame is 1518 octets
6Covered rangeany framecrc_enable matches header, data and pad only (P1, P2, P3)
7Pad is covereda padded framecrc_enable high throughout S_PAD
8Check sequence not coveredany framecrc_enable low throughout S_FCS (P3)
9Ordering: pad before FCSany padded frameno transition from S_FCS to S_PAD (P4)
10Ordering: FCS before gapany frameS_GAP entered only from S_FCS or S_ABORT (P5)
11Length after paddinginject the mutationP6 fires; the frame is otherwise well formed
12Check before paddinginject the mutationevery padded frame rejected by a reference receiver
13Gap started earlyinject the mutationreference receiver reports truncation plus a runt
14Preamble too earlyforce the gap shortc_short_gap on the interface monitor
15Client stalls before startno data bufferedframe does not start; safe_to_start low (P9)
16Client stalls mid-frame, recoversbrief stall, buffer non-emptyframe completes normally; no underrun
17Client starves mid-framebuffer empties in flightunderrun; frame filled and marked (P10, P11, P12)
18Starved frame lengththe same stimulusframe reaches the floor — a fragment would point at the link
19Starved frame is countedthe same stimulusc_underrun increments; first_at_octet captured (P13)
20Context survives clearassert clear after an underruncount zeroes; context and worst_at_octet survive (P14)
21Error insertion absentproduction buildinsertion_active never asserts (P15)
22Error insertion, bad FCStest build, E_BAD_FCSa receiver reports a mismatch
23Error insertion, stomptest build, E_STOMPa receiver reports stomped, not a mismatch
24Interface conformance10 000 framesno size, preamble or gap violations on the wire (P16, P17)

13. Debugging — Which Ordering, Which Quantity

Symptom — every padded frame is rejected by every peer, and full-size frames are fine.

The pad is outside the covered range: either the check is computed before padding, or crc_enable is low during S_PAD. The correlation with frame size is the diagnosis — only frames short enough to need padding are affected, so a link carrying mostly maximum-size frames looks healthy until a burst of small ones arrives.

Symptom — a peer's application receives extra bytes on small transfers.

The length field is carrying the padded size. Not an ordering fault — the frame is well formed, the check is correct, and no counter anywhere increments. Check emitted_length against the client's octet count directly at the transmitter, and note the signature: the excess is exactly the difference between the client's count and 46, and it appears only on transfers below 46 octets.

Symptom — a peer reports truncated frames and runts at roughly equal rates.

One bug, two symptoms: the gap is being started before the check octets are emitted, so the frame ends four octets early and the four octets land in the idle period where they may delimit as a runt. The equal rates are the tell — a genuine link fault does not produce truncations and runts in a one-to-one ratio.

Symptom — frames are dropped by some peers and not others, intermittently.

Gap tolerance, which varies between receivers (Chapter 5.9 §10). Read c_short_gap from the interface monitor rather than the controller's gap state, because clock compensation and physical-layer processing change the gap downstream of the controller and the controller's intent is not what went out.

Symptom — the far end reports check-sequence failures and this device reports nothing.

Check c_underrun first. An underrun marks the frame, so the far end sees a failing check — and if this device has no local counter, the fault is a host-interface problem being reported as a network problem on somebody else's equipment. first_at_octet then names the subsystem: an underrun at octet 12 is a start-threshold problem, and one at octet 1400 is a sustained-bandwidth problem.

Symptom — underruns rise under load and the link is not saturated.

The client side is not keeping up, and the position tells you where to look. Compare worst_at_octet against the buffer depth: if the deepest starvation is within the buffer's capacity, the fill rate is the problem; if it is far beyond, the client's sustained bandwidth is short and no threshold will fix it.

Symptom — malformed frames appear on a production link and nobody enabled anything.

Check whether ERROR_INSERTION_PRESENT was tied high in the build. A runtime enable is not a guard — it is whatever software last wrote, including after a warm reset that left a stale register image — which is why the parameter exists and why P15 checks the absence rather than the disable.

14. Common Misconceptions

"The transmit pipeline's stage order is an engineering choice."

The wrong model: stages can be reordered for timing or area like any other pipeline.

What it costs: plausible optimisations that produce broken frames. Padding after the check is attractive because the pad is cheap; starting the next preamble as soon as the check octets are out saves cycles where frame rate matters most. Both produce frames peers reject.

The corrected model: two constraints fix the whole order and both are consequences established elsewhere — the pad is inside the covered range (Chapter 5.8), and the gap is outside the frame. A proposed reordering is a contradiction of a stated fact, not a trade-off.

"Padding is a finishing touch applied to a completed frame."

The wrong model: pad last, because it is filler.

What it costs: the pad ends up outside the covered range, and every padded frame is rejected by every peer. It also puts octets after the check sequence, which contradicts Chapter 5.1's field order.

The corrected model: the pad is covered, so it is part of what the check is computed over and must exist before the computation finishes. In the state graph, crc_enable is high throughout S_PAD — and reading crc_enable across the whole graph is the covered range.

"An underrun means we should truncate the frame."

The wrong model: stop transmitting what you cannot finish.

What it costs: a truncated frame is what a link fault produces, so a host-interface problem generates link-fault symptoms — a fragment, below the floor, with a bad check — and the investigation goes to the cable.

The corrected model: finish the frame to the correct length and mark it with a deliberately wrong check sequence, using Chapter 5.8 §9's inversion so the receiver learns the frame was known-bad rather than corrupted in transit. And count it locally, because the receiver's rejection tells the far end and nothing tells the end that can fix it.

"Filling the shortfall and finishing normally is the graceful option."

The wrong model: the frame is the right size with a correct check, so nothing is malformed.

What it costs: the worst outcome available. The frame is accepted and delivers filler to the peer's client as data, with no error anywhere in either device.

The corrected model: a frame whose contents are wrong must not carry a valid check sequence. Gracefulness is exactly the wrong goal here — the design's job on an unrecoverable event is to make the failure loud, not to make it look normal.

"A monitor on the controller's state verifies the transmit path."

The wrong model: checking that the state machine sequenced correctly checks the output.

What it costs: everything between the controller and the interface is unverified — the serialiser, the clock-domain crossing, the physical-layer wrapper — and those are exactly where an octet gets dropped or the gap gets shortened.

The corrected model: the monitor watches what actually left. And the gap in particular must be measured on the wire, because Chapter 5.9 §10 showed the gap the controller emitted is not the gap that goes out.

15. Interview Reasoning

"What order does a MAC transmit path do things in, and why?"

The weak answer lists stages. The answer that ends the topic gives the two constraints that force the list: the pad is inside the covered range so it must precede the check, and the gap is outside the frame so it must follow it. Everything else is a consequence. The payoff is that a proposed reordering is not a trade-off but a contradiction — and naming which fact it contradicts is what a design review is for.

"What happens if you pad after computing the CRC?"

Every padded frame is rejected by every peer, because the pad is outside the covered range and the receiver computes over data-plus-pad. The strong answer adds the diagnostic shape: the failure correlates with frame size, so a link carrying mostly large frames looks healthy until small ones arrive — and it also contradicts the frame's field order, since octets now follow the check sequence.

"Your transmitter runs out of data mid-frame. What do you do?"

The frame cannot be withdrawn — its preamble is already gone. There are four options and three are wrong: truncating produces link-fault symptoms from a host-interface problem; filling and finishing normally produces a valid frame carrying filler, which is the worst outcome; and asserting it never happens forbids the response rather than the event. The correct answer is to fill to the right length and mark the frame with a deliberately wrong check, then count it locally with the octet position — because the far end's rejection is not visible to whoever can fix it.

"Would you assert that the transmit FIFO never underruns?"

No, and the reason is that the design cannot prevent it — the client side is a memory system with its own timing, and safe_to_start reduces the probability rather than eliminating it. The complete answer names the danger: the cheapest way to make that assertion pass is the fill-and-finish behaviour that corrupts data silently. Assert instead that an underrun always produces a marked, full-length frame, that it is never emitted with a valid check, and that it is counted locally with first-cause context.

16. Understanding Check

Because the pad is inside the covered range.

Chapter 5.8 §3 established that the check covers the addresses, the length/type field, the client data and the pad — and gave the reason: excluding the pad would create a region in which corruption is invisible across most of the shortest frames on the link.

So the pad must exist before the computation finishes, which fixes its position in the pipeline. In the state graph, crc_enable is high throughout S_PAD, exactly as it is in S_HEADER and S_DATA.

Getting it wrong is loud. Compute the check first and the pad lands after the four check octets — the field order is wrong and the check covers only the client data, so every receiver disagrees and every padded frame is rejected.

The useful diagnostic property is that it correlates with size. Only frames short enough to need padding are affected, so a link carrying mostly maximum-size frames looks healthy until a burst of small ones arrives — which is a very different signature from a physical fault.

17. What's Next

The claim this chapter defended: the transmit path's order is derived, not chosen, and every wrong order contradicts something already established.

Two constraints fix the whole pipeline — the pad is inside the covered range, so it precedes the check; the gap is outside the frame, so it follows it. Everything else is a consequence, which means a proposed reordering is a contradiction rather than a trade-off, and a design review's job is to name which fact it contradicts. And separately from any ordering, the length field carries the client's octet count, which is the one failure that produces a frame every structural check accepts and that delivers pad as data.

Then the event the order cannot help with. A started frame cannot be withdrawn, there is no back-pressure toward the wire, and the only defence is the decision not to start. When starvation happens anyway, the frame is finished to the correct length and marked — because truncating produces link-fault symptoms from a host-interface fault, and filling-and-finishing produces a valid frame carrying filler. And it is counted locally, with the octet position, because the far end's rejection is invisible to the end that can fix it.

Chapter 7.2 — The Receive Path is the mirror, and it is not symmetric. A transmitter knows what it is building. A receiver is discovering the frame as it arrives, and every discovery is provisional until the check sequence at the very end — which Chapter 5.1 established is last by construction.

So the receive path faces a question the transmit path never does: whether to begin delivering a frame it may have to withdraw. Start early and latency falls, but a frame that fails its check has already been partly handed over. Wait for the check and the frame is certainly good, at the cost of holding it entirely. 7.2 works through both, and through the accounting that attributes every discarded frame to the stage that discarded 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.