Skip to content
VLSI Mentor

Ethernet · Module 4

Where the MAC Ends and the PHY Begins

The MAC/PHY boundary is generated by one rule: a responsibility belongs to the side that can detect its own failure. That rule decides every case — and it explains why each side is blind to the other's failures, which is what makes a contract violation invisible from both sides.

Module 3 crossed this boundary in every chapter and never defined it.

Chapter 2.5 listed the MAC's six responsibilities. Chapter 2.6 listed the PHY's. Chapter 3.4 approached the boundary from below — as the top of the PHY — and gave the reconciliation sublayer and the media-independent interface that sit at it.

None of those is a definition. Two lists side by side do not tell you where a new responsibility goes, and new ones arrive constantly: timestamping, energy-efficient idle, per-priority flow control, retimers, in-band management. An engineer holding two lists has to argue each case from analogy, and analogies disagree.

Worse, the lists are the wrong shape for the question people actually have. Nobody asks "which list is framing on". They ask "the link is up and frames are being lost — whose problem is it?" and a list cannot answer that at all.

What rule generates the split, and what does it tell you about who owns a failure?

1. Scope — And Exactly How This Differs From Its Neighbours

This chapter is deliberately narrow, and the narrowness is what makes it citable.

Chapter 3.4 approached this boundary from below. Its subject was the PHY's internal structure, and the media-independent interface appeared as the PHY's top edge — the thing above which everything is medium-independent. It built a reconciliation_tx model of that edge.

This chapter approaches it from above, as the contract a MAC is designed against, and asks a question 3.4 never needed to: given an arbitrary responsibility, which side owns it and why?

Chapter 4.2 owns the interface mechanics — the media-independent interface generations, their widths, clocking and control encodings, in detail. This chapter uses the interface as a boundary and does not enumerate its variants.

Chapter 4.3 owns the data flow across the boundary signal by signal in both directions.

So this chapter owns exactly three things, and nothing else: the rule that assigns responsibility, the assumption contract the rule implies, and failure attribution across the boundary. Those are what later modules will cite instead of re-deriving, which is the chapter's whole purpose.

What it deliberately does not do is repeat Chapter 2.5's or Chapter 2.6's responsibility lists. It uses them as worked examples of the rule and assumes you have them.

2. Why a List of Functions Is Not a Split

Take the conventional description: the MAC does framing, addressing, error detection, padding and the interframe gap; the PHY does coding, serialisation, clock recovery and line drive.

Three questions that description cannot answer, all of which arise in real projects:

Where does timestamping go? A precise timestamp must be taken as close to the wire as possible, which argues for the PHY. But it must be associated with a frame, and the PHY has no concept of a frame. Both lists have a claim and neither has an argument.

Where does rate adaptation go? Chapter 2.6 put elastic buffering in the PHY. But a MAC also throttles when its client outruns the link. Two mechanisms with the same name on opposite sides of the boundary, and the lists give no way to see why.

Where does energy-efficient idle go? It requires the MAC to know there is no traffic and the PHY to know it can safely quiesce and how long it takes to wake. It is genuinely joint, and a list has no representation for that.

3. The Criterion — Responsibility Follows Detectability

A responsibility belongs to the side that can detect its own failure.

The justification is that the three things you need from an owner all require observation:

  • Recovery. A side that cannot see a failure cannot respond to it.
  • Reporting. A side that cannot see a failure cannot tell anyone, and Module 3 established that unattributed failures have no owner.
  • Accountability. A side that cannot see a failure cannot be held to a requirement about it, because it can neither verify compliance nor demonstrate it.
For any responsibility, ask which side can detect its failure. If the failure is visible in the frame, the MAC owns it and can recover and report. If the failure is visible in the signal, the PHY owns it. If neither side can detect it alone, it is a boundary assumption and needs an explicit contract plus attribution instrumentation.A responsibilitywho owns it?Who detects itsfailure?the only questionVisible in the framethe MAC owns itVisible in the signalthe PHY owns itNeither, alonea contract, plus attribution12
Figure 1 — the question that decides every case, and the two things that follow from the answer.

The third branch is the one lists have no room for, and it is where the expensive failures live. Section 5 develops it.

Apply the rule to the conventional split and it reproduces it exactly, which is the first thing a proposed criterion has to do:

ResponsibilityHow its failure showsDetectable byOwner
framingthe frame's check value failsreading the frameMAC
addressinga frame arrives at the wrong stationcomparing addressesMAC
padding to the minimuman undersized framemeasuring the frameMAC
the interframe gapframes closer than the minimumtiming between framesMAC
line codinginvalid code groups appearparsing the coded streamPHY
clock recoverylock is lostwatching the recovery loopPHY
block alignmentblocks stop parsingChapter 3.5's invalid patternsPHY
line drivethe far end reports errors, or signal detect dropsmeasuring the signalPHY

Read the third column. Every MAC entry is detectable by looking at a frame; every PHY entry by looking at a signal or a coded stream. The MAC has no access to a signal and the PHY has no concept of a frame — so in each case exactly one side could possibly have owned it.

4. The Assumption Contract

The rule has a consequence that is easy to miss and expensive to ignore.

Because each side detects only its own failures, each side is blind to the other's. So each must assume things about the other that it cannot verify — and those assumptions are the real interface, more than any signal list.

A stack showing the assumption contract. The MAC client sits above. The MAC assumes the PHY delivers bits in order at a known rate and reports when it cannot. The interface carries data plus control plus status. The PHY assumes the MAC respects the interface handshake, gaps and sizes. Below the PHY is the medium.The contract in both directionsMAC clienthands over frames and expects them delivered or reported losthands over frames and expects them delivered or reported lostMACassumes: bits in order, at a known rate, with failures reportedassumes: bits in order, at a known rate, with failures reportedThe interfacedata, control and status — the contract made concretedata, control and status — the contract made concretePHYassumes: the handshake honoured, gaps respected, sizes legalassumes: the handshake honoured, gaps respected, sizes legalThe mediumeverything Module 3 describedeverything Module 3 described
Figure 2 — what each side may assume, and what it must therefore guarantee.

What the MAC may assume about the PHY:

  1. Bits arrive in the order they were sent. No reordering, ever. The PHY may lose or corrupt, never permute.
  2. The rate is known and constant while the link reports up. The MAC's gap enforcement and sizing arithmetic depend on it.
  3. Failures are reported, not hidden. A PHY that cannot deliver says so — Chapter 3.4's status vector, or a link-down indication — rather than delivering plausible garbage.
  4. The interface is transparent to payload. Whatever octets the MAC hands over come out the far end unchanged, or not at all. This is Chapter 2.4's opacity, seen from above.

What the PHY may assume about the MAC:

  1. The interface handshake is honoured. Data offered stays stable until accepted. Chapter 3.4 §15 asserted this and named it the rule people break.
  2. Frame boundaries are marked correctly. A start is followed by an end. The PHY's coding depends on knowing where frames begin and end, and it cannot check the claim.
  3. The interframe gap is respected. The PHY's rate compensation — inserting and deleting idles — needs somewhere to do it, and the gap is that somewhere.
  4. Sizes are within the negotiated limits. The PHY does not check frame length; that is the MAC's, by the rule.

5. RTL 1 — The MAC-Side Port List, Every Signal Justified

The clearest statement of a boundary is the port list, provided every signal has a reason. This one is annotated with which side owns the responsibility behind it and which assumption it discharges.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. The MAC's view of the boundary.
//
// EVERY SIGNAL BELOW CARRIES ITS JUSTIFICATION. That is the discipline this
// module exists to demonstrate: a boundary signal that cannot be traced to
// an owned responsibility or to a contract assumption is a leak of one
// side's internals into the other, and it will become a dependency that
// nobody intended and nobody can remove later.
//
// No xMII generation is encoded here. Chapter 4.2 owns the mechanics.
package macphy_pkg;
  // Which side owns a responsibility, by the rule of Section 3.
  typedef enum logic [1:0] {
    OWN_MAC      = 2'd0,   // failure detectable in the frame
    OWN_PHY      = 2'd1,   // failure detectable in the signal
    OWN_BOUNDARY = 2'd2    // detectable by neither alone -- needs a contract
  } owner_e;
 
  typedef enum logic [2:0] {
    XC_IDLE  = 3'd0,
    XC_START = 3'd1,
    XC_DATA  = 3'd2,
    XC_END   = 3'd3,
    XC_ERROR = 3'd4        // the MAC abandoned this frame
  } xctrl_e;
endpackage
 
module mac_phy_boundary
  import macphy_pkg::*;
#(
  parameter int unsigned W = 32
) (
  // ── Clocking ────────────────────────────────────────────────────────────
  // OWNER: PHY. The link rate is a property of the medium and the coding,
  // both of which the PHY owns. The MAC runs against what it is given --
  // which is the concrete form of the MAC's assumption 2, that the rate is
  // known and constant while the link is up.
  input  logic clk,
  input  logic rst_n,
 
  // ── Transmit: MAC to PHY ────────────────────────────────────────────────
  // OWNER: MAC. Frame construction is MAC-owned because a malformed frame
  // is detectable in the frame -- the check value fails.
  output logic [W-1:0] tx_data,
  output xctrl_e       tx_ctrl,
  output logic         tx_valid,
 
  // DISCHARGES: PHY assumption 1 -- the handshake is honoured. Data offered
  // must stay stable until tx_ready is seen. The PHY cannot verify this and
  // must simply rely on it, which is why Section 8 checks it explicitly.
  input  logic         tx_ready,
 
  // OWNER: MAC. An abandoned frame must be MARKED, not silently truncated,
  // or the far end receives a short but structurally valid frame -- which
  // fails its check value and gets attributed to the link.
  output logic         tx_error,
 
  // ── Receive: PHY to MAC ─────────────────────────────────────────────────
  // OWNER: PHY for delivery, MAC for interpretation. The PHY guarantees
  // order and reports what it could not deliver; it does not interpret.
  input  logic [W-1:0] rx_data,
  input  xctrl_e       rx_ctrl,
  input  logic         rx_valid,
 
  // OWNER: PHY. DISCHARGES: MAC assumption 3 -- failures are reported, not
  // hidden. This is the PHY saying "these octets are suspect" rather than
  // delivering plausible garbage the MAC would then trust.
  input  logic         rx_error,
 
  // ── Status: PHY to MAC ──────────────────────────────────────────────────
  // OWNER: PHY. Chapter 3.4 §13 argued this must be a VECTOR rather than a
  // bit, because the lowest deasserted element names the sublayer that owns
  // the problem. Reducing it here would discard that at the boundary --
  // after which the MAC can report only "the link is down".
  input  logic         link_up,
  input  logic [3:0]   link_status_vector,
 
  // OWNER: BOUNDARY. Neither side can detect a rate mismatch alone: the MAC
  // has no view of the signal, the PHY no view of what the MAC expects. So
  // the negotiated rate is CARRIED, not assumed -- Section 11's subject.
  input  logic [3:0]   negotiated_rate,
  input  logic         rate_valid,
 
  // ── Boundary responsibilities ───────────────────────────────────────────
  // OWNER: BOUNDARY, by Section 3's third branch. The PHY captures the
  // instant because only it is near the wire; the MAC associates it with a
  // frame because only it knows what a frame is. Neither can do both, so
  // the interface carries the association.
  input  logic         ts_capture_valid,
  input  logic [63:0]  ts_capture_value,
  output logic         ts_request,
 
  // ── What is deliberately ABSENT ─────────────────────────────────────────
  // There is no signal here for: the PHY's block lock state, its equaliser
  // taps, its FEC correction counts, its eye margin, or its lane skew.
  //
  // Every one of those is a PHY-owned failure the PHY detects itself, so
  // exposing it at this boundary would give the MAC a dependency on PHY
  // internals -- and a MAC that reads FEC counters is a MAC that cannot be
  // paired with a PHY that has no FEC.
  //
  // Those quantities belong on a MANAGEMENT interface, which is a different
  // boundary with a different contract. The distinction is the single most
  // common architectural mistake at this interface.
  output logic         mgmt_request   // the separate path, named not modelled
);
 
  // The module is a declaration. Its content is the justification above,
  // and its value is that a reviewer can check every signal against a rule.
  assign ts_request   = 1'b0;
  assign mgmt_request = 1'b0;
 
endmodule

Classification: synthesizable boundary declaration.

What it teaches: that every boundary signal must trace to an owned responsibility or a contract assumption, and that a signal doing neither is a leak. The absent-signals comment is the most important part of the module: exposing PHY internals at this boundary creates a MAC that only works with one kind of PHY, which destroys the composability Chapter 3.4 §4 showed the whole architecture depends on.

Deliberately simplified: no specific interface generation. Chapter 4.2 owns widths, clocking and control encodings, and inventing one here would be a second, competing account.

Production implication: the distinction between this boundary and the management boundary is the most commonly muddled thing at this interface. Data-path status — is the link usable, is this frame trustworthy — belongs here. Diagnostic detail — FEC counts, eye margin, skew, equaliser state — belongs on a management path. They have different consumers, different rates, and different failure semantics, and merging them produces a MAC that cannot be paired with a different PHY.

Later ownership: the interface mechanics are Chapter 4.2; the signal-by-signal data flow is Chapter 4.3.

6. RTL 2 — The MAC-Side Conformance Checker

Section 4 established that neither side can verify the other's assumptions, so the checking must be added deliberately. This is the MAC's half: instrumentation that detects the PHY violating what the MAC was allowed to assume.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE INSTRUMENTATION. MAC side.
//
// Checks the PHY against the four things the MAC is allowed to assume
// (Section 4). None of these is checkable by the MAC's ordinary logic --
// that is precisely why they are ASSUMPTIONS -- so each needs deliberate
// instrumentation or the violation is invisible.
//
// Every finding here is one the MAC would otherwise blame on the medium.
module mac_side_conformance
  import macphy_pkg::*;
#(
  parameter int unsigned CNT_W = 20,
  // Cycles the link may report up while delivering nothing before that
  // combination is treated as a contract violation rather than idleness.
  parameter int unsigned SILENCE_LIMIT = 1_000_000
) (
  input  logic clk,
  input  logic rst_n,
  input  logic clear,
 
  input  logic       link_up,
  input  logic [3:0] link_status_vector,
  input  logic       rate_valid,
  input  logic [3:0] negotiated_rate,
 
  input  logic   rx_valid,
  input  xctrl_e rx_ctrl,
  input  logic   rx_error,
 
  // ── Assumption 3: failures are reported, not hidden ─────────────────────
  // The link claims to be up while its own status vector says a sublayer is
  // unsatisfied. The PHY is contradicting itself, and the MAC would
  // otherwise trust the summary bit and deliver corrupt frames upward.
  output logic v_status_contradiction,
  output logic [CNT_W-1:0] c_status_contradiction,
 
  // The link reports up and delivers nothing at all for a long time.
  // Chapter 3.4 §9 established the PHY never goes silent -- idle is
  // transmitted. Prolonged silence with link_up asserted is a violation.
  output logic v_silent_while_up,
  output logic [CNT_W-1:0] c_silent_while_up,
 
  // ── Assumption 2: the rate is known and constant while up ───────────────
  // A rate change without the link dropping invalidates every gap and
  // sizing calculation the MAC has in flight.
  output logic v_rate_changed_while_up,
  output logic [CNT_W-1:0] c_rate_changed_while_up,
 
  // ── Frame structure: an END without a START ─────────────────────────────
  // The PHY delivering a malformed control sequence. Not a corrupted frame
  // -- a corrupted CONTRACT, and the two need different responses.
  output logic v_end_without_start,
  output logic [CNT_W-1:0] c_end_without_start,
 
  // ── Assumption 3 again, on a per-frame basis ────────────────────────────
  // rx_error asserted outside a frame is meaningless: there is nothing for
  // it to qualify, and a MAC acting on it will discard the NEXT frame.
  output logic v_error_outside_frame,
  output logic [CNT_W-1:0] c_error_outside_frame,
 
  // First violation observed, held. During an incident several will be set;
  // which came first identifies the initiating fault.
  output logic [2:0] first_violation,
  output logic       first_violation_valid
);
 
  logic       in_frame_q;
  logic [3:0] rate_q;
  logic [$clog2(SILENCE_LIMIT+1)-1:0] silence_q;
 
  function automatic logic [CNT_W-1:0] bump(input logic [CNT_W-1:0] v,
                                            input logic             en);
    bump = (en && !(&v)) ? (v + 1'b1) : v;
  endfunction
 
  always_comb begin
    // link_up asserted while the status vector is not fully satisfied.
    v_status_contradiction  = link_up && !(&link_status_vector);
    v_silent_while_up       = link_up
                            && (silence_q >= ($clog2(SILENCE_LIMIT+1))'(SILENCE_LIMIT));
    v_rate_changed_while_up = link_up && rate_valid && (negotiated_rate != rate_q);
    v_end_without_start     = rx_valid && (rx_ctrl == XC_END) && !in_frame_q;
    v_error_outside_frame   = rx_valid && rx_error && !in_frame_q
                            && (rx_ctrl != XC_START);
  end
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      in_frame_q              <= 1'b0;
      rate_q                  <= '0;
      silence_q               <= '0;
      c_status_contradiction  <= '0;
      c_silent_while_up       <= '0;
      c_rate_changed_while_up <= '0;
      c_end_without_start     <= '0;
      c_error_outside_frame   <= '0;
      first_violation         <= '0;
      first_violation_valid   <= 1'b0;
    end else begin
      if (clear) begin
        c_status_contradiction  <= '0;
        c_silent_while_up       <= '0;
        c_rate_changed_while_up <= '0;
        c_end_without_start     <= '0;
        c_error_outside_frame   <= '0;
      end else begin
        c_status_contradiction  <= bump(c_status_contradiction,  v_status_contradiction);
        c_silent_while_up       <= bump(c_silent_while_up,       v_silent_while_up);
        c_rate_changed_while_up <= bump(c_rate_changed_while_up, v_rate_changed_while_up);
        c_end_without_start     <= bump(c_end_without_start,     v_end_without_start);
        c_error_outside_frame   <= bump(c_error_outside_frame,   v_error_outside_frame);
      end
 
      // Frame tracking, from the control encoding alone.
      if (rx_valid) begin
        if      (rx_ctrl == XC_START) in_frame_q <= 1'b1;
        else if (rx_ctrl == XC_END)   in_frame_q <= 1'b0;
      end
 
      if (rate_valid) rate_q <= negotiated_rate;
 
      // Silence tracking: any delivery resets it.
      if (rx_valid)        silence_q <= '0;
      else if (!(&silence_q)) silence_q <= silence_q + 1'b1;
 
      // First cause, held.
      if (!first_violation_valid) begin
        if      (v_status_contradiction)  begin first_violation <= 3'd0; first_violation_valid <= 1'b1; end
        else if (v_rate_changed_while_up) begin first_violation <= 3'd1; first_violation_valid <= 1'b1; end
        else if (v_end_without_start)     begin first_violation <= 3'd2; first_violation_valid <= 1'b1; end
        else if (v_error_outside_frame)   begin first_violation <= 3'd3; first_violation_valid <= 1'b1; end
        else if (v_silent_while_up)       begin first_violation <= 3'd4; first_violation_valid <= 1'b1; end
      end
    end
  end
 
endmodule

Classification: synthesizable instrumentation.

What it teaches: that an assumption is only safe if something checks it, and that the check belongs on the side that relies on the assumption rather than the side that must satisfy it. The PHY has no incentive and no ability to check that it is reporting its own failures honestly — if it could detect the dishonesty it would not be dishonest. So the MAC checks, and the check is cheap.

Deliberately simplified: the checks are structural rather than semantic. A production checker adds timing conformance and, on a multi-lane link, per-lane consistency.

Production implication: v_status_contradiction is the highest-value check in the module and catches a real and common integration fault — a PHY whose summary link_up is derived independently of its status vector, so the two can disagree. Chapter 3.4 §13 built the vector so its lowest deasserted element names an owner; a summary bit that does not track it makes the MAC deliver frames from a link the PHY itself considers unusable, and the resulting corruption is attributed to the medium.

And first_violation is sticky for the reason every first-cause register in this track is: during an incident several violations will be set, ordering is the only remaining information, and two flip-flops preserve it.

7. RTL 3 — The PHY-Side Conformance Checker

The mirror image, and it is not symmetric — the PHY's assumptions about the MAC are different in kind, and so are the failures.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE INSTRUMENTATION. PHY side.
//
// Checks the MAC against the four things the PHY is allowed to assume.
//
// Note the asymmetry with Section 6, because it is the point of having
// both: the MAC's violations are PROTOCOL violations -- it broke the
// handshake, mismarked a frame, closed the gap. The PHY's violations are
// HONESTY violations -- it claimed a state it was not in. Different kinds
// of assumption produce different kinds of check, and a design that
// implements one checker and calls the boundary covered has covered half.
module phy_side_conformance
  import macphy_pkg::*;
#(
  parameter int unsigned CNT_W = 20,
  parameter int unsigned W     = 32,
  // Minimum idle cycles the PHY needs between frames for rate compensation.
  parameter int unsigned MIN_GAP = 12,
  parameter int unsigned MAX_FRAME_WORDS = 380
) (
  input  logic clk,
  input  logic rst_n,
  input  logic clear,
 
  input  logic         tx_valid,
  input  logic [W-1:0] tx_data,
  input  xctrl_e       tx_ctrl,
  input  logic         tx_error,
  input  logic         tx_ready,
 
  // ── Assumption 1: the handshake is honoured ─────────────────────────────
  // Data offered must stay stable until accepted. THE violation that only
  // appears under back-pressure -- which light testing never creates, so it
  // ships. Chapter 3.4 §15 named it and this is where it is caught.
  output logic v_data_changed_while_stalled,
  output logic [CNT_W-1:0] c_data_changed_while_stalled,
 
  // ── Assumption 2: frame boundaries are marked correctly ─────────────────
  output logic v_start_inside_frame,
  output logic v_data_outside_frame,
  output logic [CNT_W-1:0] c_start_inside_frame,
  output logic [CNT_W-1:0] c_data_outside_frame,
 
  // ── Assumption 3: the interframe gap is respected ───────────────────────
  // The PHY inserts and deletes idles in the gap to absorb the frequency
  // difference between two independent oscillators (Chapter 2.6). A MAC
  // that closes the gap leaves nowhere to do it, and the elastic buffer
  // eventually overruns -- an error attributed to the PHY, caused above it.
  output logic v_gap_too_short,
  output logic [CNT_W-1:0] c_gap_too_short,
 
  // ── Assumption 4: sizes are within negotiated limits ────────────────────
  // The PHY does not enforce length -- that is MAC-owned by Section 3's
  // rule. But it must NOTICE, or an oversized frame silently overruns
  // whatever the PHY sized for it.
  output logic v_frame_too_long,
  output logic [CNT_W-1:0] c_frame_too_long,
 
  output logic [2:0] first_violation,
  output logic       first_violation_valid
);
 
  logic         in_frame_q;
  logic [W-1:0] held_data_q;
  xctrl_e       held_ctrl_q;
  logic         was_stalled_q;
  logic [$clog2(MIN_GAP+2)-1:0]         gap_q;
  logic [$clog2(MAX_FRAME_WORDS+2)-1:0] len_q;
 
  function automatic logic [CNT_W-1:0] bump(input logic [CNT_W-1:0] v,
                                            input logic             en);
    bump = (en && !(&v)) ? (v + 1'b1) : v;
  endfunction
 
  always_comb begin
    // Offered last cycle, not accepted, and the offer has changed.
    v_data_changed_while_stalled = was_stalled_q && tx_valid
                                && ((tx_data != held_data_q) || (tx_ctrl != held_ctrl_q));
 
    v_start_inside_frame = tx_valid && tx_ready && (tx_ctrl == XC_START) && in_frame_q;
    v_data_outside_frame = tx_valid && tx_ready && (tx_ctrl == XC_DATA)  && !in_frame_q;
 
    v_gap_too_short = tx_valid && tx_ready && (tx_ctrl == XC_START)
                    && (gap_q < ($clog2(MIN_GAP+2))'(MIN_GAP));
 
    v_frame_too_long = tx_valid && tx_ready && in_frame_q
                     && (len_q >= ($clog2(MAX_FRAME_WORDS+2))'(MAX_FRAME_WORDS));
  end
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      in_frame_q                   <= 1'b0;
      held_data_q                  <= '0;
      held_ctrl_q                  <= XC_IDLE;
      was_stalled_q                <= 1'b0;
      gap_q                        <= '1;
      len_q                        <= '0;
      c_data_changed_while_stalled <= '0;
      c_start_inside_frame         <= '0;
      c_data_outside_frame         <= '0;
      c_gap_too_short              <= '0;
      c_frame_too_long             <= '0;
      first_violation              <= '0;
      first_violation_valid        <= 1'b0;
    end else begin
      if (clear) begin
        c_data_changed_while_stalled <= '0;
        c_start_inside_frame         <= '0;
        c_data_outside_frame         <= '0;
        c_gap_too_short              <= '0;
        c_frame_too_long             <= '0;
      end else begin
        c_data_changed_while_stalled <= bump(c_data_changed_while_stalled, v_data_changed_while_stalled);
        c_start_inside_frame         <= bump(c_start_inside_frame,         v_start_inside_frame);
        c_data_outside_frame         <= bump(c_data_outside_frame,         v_data_outside_frame);
        c_gap_too_short              <= bump(c_gap_too_short,              v_gap_too_short);
        c_frame_too_long             <= bump(c_frame_too_long,             v_frame_too_long);
      end
 
      // Hold the offer so the next cycle can compare against it.
      was_stalled_q <= tx_valid && !tx_ready;
      if (tx_valid && !tx_ready) begin
        held_data_q <= tx_data;
        held_ctrl_q <= tx_ctrl;
      end
 
      if (tx_valid && tx_ready) begin
        unique case (tx_ctrl)
          XC_START: begin in_frame_q <= 1'b1; gap_q <= '0; len_q <= '0; end
          XC_END:   begin in_frame_q <= 1'b0; gap_q <= '0;              end
          XC_DATA:  if (!(&len_q)) len_q <= len_q + 1'b1;
          default:  if (!in_frame_q && !(&gap_q)) gap_q <= gap_q + 1'b1;
        endcase
      end else if (!in_frame_q && !(&gap_q)) begin
        gap_q <= gap_q + 1'b1;
      end
 
      if (!first_violation_valid) begin
        if      (v_data_changed_while_stalled) begin first_violation <= 3'd0; first_violation_valid <= 1'b1; end
        else if (v_start_inside_frame)         begin first_violation <= 3'd1; first_violation_valid <= 1'b1; end
        else if (v_data_outside_frame)         begin first_violation <= 3'd2; first_violation_valid <= 1'b1; end
        else if (v_gap_too_short)              begin first_violation <= 3'd3; first_violation_valid <= 1'b1; end
        else if (v_frame_too_long)             begin first_violation <= 3'd4; first_violation_valid <= 1'b1; end
      end
    end
  end
 
endmodule

Classification: synthesizable instrumentation.

What it teaches: that the two sides' assumptions are different in kind, so two checkers are needed rather than one generic one. The MAC's obligations are protocol obligations at the interface — honour the handshake, mark boundaries, respect the gap. The PHY's are honesty obligations about its own state — report what you cannot deliver, do not claim a link you do not have. A single "interface checker" covers one of these well and the other not at all.

Deliberately simplified: MIN_GAP and MAX_FRAME_WORDS are parameters rather than derived from a negotiated configuration, and the gap counter does not model idle deletion.

Production implication: v_gap_too_short catches a failure that is always misattributed. Chapter 2.6 established that the PHY absorbs the frequency difference between two independent oscillators by inserting and deleting idles in the gap. A MAC that closes the gap leaves nowhere to do it, the elastic buffer drifts, and eventually it overruns. The error is reported by the PHY, so it is attributed to the PHY — and the cause is a MAC transmitting legally-formatted frames too close together. Without this check, that investigation goes to the wrong team and stays there.

And v_data_changed_while_stalled is the one that ships. It only manifests under back-pressure, and light integration testing rarely creates sustained back-pressure — so a MAC that mutates a held word passes every early test and corrupts data in production under load.

8. RTL 4 — Failure Attribution

Two checkers produce two sets of findings. This block turns them into an answer to the question that actually gets asked.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE INSTRUMENTATION.
//
// The question is always "the link is up and frames are being lost -- whose
// problem is it?" Neither side can answer it alone, by Section 4. This block
// answers it from both checkers plus the PHY's own status.
//
// The PRIORITY ORDERING is the design. It encodes which evidence is more
// conclusive, and getting it wrong sends people to the wrong team with
// apparent authority -- which is worse than sending them nowhere.
module boundary_attribution
  import macphy_pkg::*;
#(
  parameter int unsigned CNT_W = 20
) (
  input  logic clk,
  input  logic rst_n,
  input  logic clear,
 
  // From Section 6 -- the PHY violated what the MAC assumed.
  input  logic mac_side_violation,
  input  logic [2:0] mac_side_first,
 
  // From Section 7 -- the MAC violated what the PHY assumed.
  input  logic phy_side_violation,
  input  logic [2:0] phy_side_first,
 
  // The PHY's own view of itself (Chapter 3.4 §13).
  input  logic       link_up,
  input  logic [3:0] link_status_vector,
 
  // Frames lost, from the MAC's own accounting.
  input  logic frame_lost,
 
  // At most one is asserted. All low means no attribution is possible,
  // which is itself a reportable state and must not be silently defaulted.
  output logic blame_phy_internal,   // the PHY says it is not working
  output logic blame_phy_contract,   // the PHY broke the contract
  output logic blame_mac_contract,   // the MAC broke the contract
  output logic blame_unattributed,   // frames lost, no evidence either way
 
  output logic [CNT_W-1:0] c_phy_internal,
  output logic [CNT_W-1:0] c_phy_contract,
  output logic [CNT_W-1:0] c_mac_contract,
  output logic [CNT_W-1:0] c_unattributed
);
 
  function automatic logic [CNT_W-1:0] bump(input logic [CNT_W-1:0] v,
                                            input logic             en);
    bump = (en && !(&v)) ? (v + 1'b1) : v;
  endfunction
 
  // THE PRIORITY ORDERING, and the reasoning for each step.
  //
  // 1. The PHY's own status FIRST. If the PHY says a sublayer is
  //    unsatisfied, that is a self-report and it is the most conclusive
  //    evidence available -- nothing else needs explaining. Checking a
  //    contract violation before this would blame an interface for a link
  //    that is simply down.
  //
  // 2. Then contract violations, MAC-side checker before PHY-side. A PHY
  //    that contradicts its own status vector is broken in a way that makes
  //    every other observation unreliable, so it must be ruled out before
  //    the MAC is blamed for anything.
  //
  // 3. Then MAC violations.
  //
  // 4. Only then "unattributed" -- and it must be REPORTED, not defaulted
  //    to one side. A design whose fallback blames one party will blame it
  //    for every fault it cannot explain.
  always_comb begin
    blame_phy_internal = 1'b0;
    blame_phy_contract = 1'b0;
    blame_mac_contract = 1'b0;
    blame_unattributed = 1'b0;
 
    if (!link_up || !(&link_status_vector)) blame_phy_internal = 1'b1;
    else if (mac_side_violation)            blame_phy_contract = 1'b1;
    else if (phy_side_violation)            blame_mac_contract = 1'b1;
    else if (frame_lost)                    blame_unattributed = 1'b1;
  end
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n || clear) begin
      c_phy_internal <= '0;
      c_phy_contract <= '0;
      c_mac_contract <= '0;
      c_unattributed <= '0;
    end else begin
      c_phy_internal <= bump(c_phy_internal, blame_phy_internal);
      c_phy_contract <= bump(c_phy_contract, blame_phy_contract);
      c_mac_contract <= bump(c_mac_contract, blame_mac_contract);
      c_unattributed <= bump(c_unattributed, blame_unattributed);
    end
  end
 
endmodule

Classification: synthesizable instrumentation.

What it teaches: that attribution is a design output, not something an engineer derives afterwards from logs. Both sides' evidence exists in hardware at the moment of failure; combining it costs a few gates, and not combining it costs an investigation.

Deliberately simplified: one attribution for the whole boundary rather than per-frame or per-lane.

Production implication: blame_unattributed must exist as its own output. The tempting simplification is to default the fallback to one side — usually the PHY, because failures near the wire feel more likely. A design that does this will blame the PHY for every fault it cannot explain, with the apparent authority of a hardware register, and PHY teams will spend weeks disproving faults that were never theirs. An honest "no evidence" is far more useful than a confident guess.

And step 1 of the ordering is the one most often got wrong. Checking contract violations before the PHY's own status blames an interface for a link that is simply down — the contract checkers will fire, because a down link produces all sorts of anomalous interface behaviour, and the report will name the wrong thing with conviction.

9. RTL 5 — Negotiated Capability at the Boundary

Section 3's third branch — responsibilities neither side can detect alone — needs a mechanism, and this is it. Some parameters must be agreed rather than assumed, because a mismatch is invisible to both sides.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. MAC/PHY capability agreement, inside one device.
//
// NOT Chapter 3.8's auto-negotiation. That is between LINK PARTNERS, across
// the wire. This is between a MAC and the PHY it is bonded to, and the
// failure it prevents is different: a MAC configured for a frame size or a
// feature its PHY does not implement, which produces frames the PHY mangles
// while both sides report healthy.
//
// The rule: a parameter both sides must agree on and NEITHER can verify
// unilaterally must be NEGOTIATED, not configured twice. Configuring twice
// is how the two ends come to disagree while each believes it is correct.
module macphy_capability
  import macphy_pkg::*;
#(
  parameter int unsigned CAP_W = 8
) (
  input  logic clk,
  input  logic rst_n,
 
  // What each side can do. From straps, fuses, firmware or discovery.
  input  logic [CAP_W-1:0] mac_caps,
  input  logic [CAP_W-1:0] phy_caps,
  input  logic             phy_caps_valid,
 
  // The agreed set, and a per-capability enable both sides use.
  output logic [CAP_W-1:0] agreed_caps,
  output logic             agreement_valid,
 
  // A capability the MAC needs and the PHY does not have. This is the fault
  // that otherwise appears as inexplicable frame corruption -- the MAC
  // emitting something structurally legal that this PHY cannot carry.
  output logic [CAP_W-1:0] mac_wants_unsupported,
  output logic             capability_shortfall,
 
  // A capability the PHY has that the MAC will not use. Not a fault, but
  // worth reporting: it usually means a configuration was not applied, and
  // a system quietly running below its hardware is a real cost.
  output logic [CAP_W-1:0] phy_offers_unused,
 
  // Sticky: the MAC operated before agreement completed. Every parameter it
  // used until then was assumed rather than agreed.
  output logic             operated_before_agreement
);
 
  logic agreed_q;
 
  always_comb begin
    agreed_caps           = mac_caps & phy_caps;
    mac_wants_unsupported = mac_caps & ~phy_caps;
    phy_offers_unused     = phy_caps & ~mac_caps;
    capability_shortfall  = phy_caps_valid && (mac_wants_unsupported != '0);
  end
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      agreed_q                  <= 1'b0;
      agreement_valid           <= 1'b0;
      operated_before_agreement <= 1'b0;
    end else begin
      // Agreement requires the PHY to have actually reported. A design that
      // treats "no report yet" as "no capabilities" silently configures the
      // intersection with zero and disables everything.
      agreed_q        <= phy_caps_valid;
      agreement_valid <= phy_caps_valid;
 
      if (!phy_caps_valid && agreed_q) operated_before_agreement <= 1'b1;
    end
  end
 
endmodule

Classification: synthesizable.

What it teaches: that a parameter both sides must agree on, and that neither can verify unilaterally, must be negotiated rather than configured twice. Configuring twice is precisely how two internally-correct implementations come to disagree — each is configured correctly according to its own source of truth, and the sources differ.

Deliberately simplified: capabilities as a flat bit vector. Real capability sets have structure — sizes, rates, optional feature groups with dependencies.

Production implication: capability_shortfall names a fault that otherwise presents as inexplicable frame corruption. A MAC configured for a larger frame size than its PHY supports emits frames that are structurally perfect and that this PHY cannot carry. The MAC's own checks pass, the PHY reports no violation because size is not its responsibility, and the far end sees corruption. Both sides report healthy and the link does not work — Section 4's characteristic failure, in its most common concrete form.

And phy_offers_unused is worth reporting even though it is not a fault. A system running below its hardware's capability is usually a configuration that was never applied, and it is invisible unless something looks for it.

10. RTL 6 — The Boundary Trace Buffer

Section 13's debugging method ends by saying an unspecified interaction must be reproduced "with both sides instrumented simultaneously". Nothing built so far does that. Section 9's checkers report that a violation occurred; none of them preserves what led to it.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE INSTRUMENTATION. Outside the datapath.
//
// The checkers of Sections 6 and 7 answer WHETHER and WHICH. Neither
// answers WHAT LED TO IT, and for the interaction faults of Section 13 that
// is the only useful question -- the violation is a consequence, and the
// coincidence that caused it happened several cycles earlier.
//
// A circular buffer that FREEZES on the first violation is the whole idea.
// It costs a small memory and it is the difference between "the gap check
// fired" and "the gap check fired two cycles after an abort coincided with
// an idle deletion".
module boundary_trace
  import macphy_pkg::*;
#(
  parameter int unsigned DEPTH  = 64,
  parameter int unsigned PTR_W  = $clog2(DEPTH),
  parameter int unsigned W      = 32,
  // Cycles to keep recording AFTER the trigger, so the consequence is
  // captured alongside its cause. Without this the trace stops exactly at
  // the interesting moment.
  parameter int unsigned POST_TRIGGER = 8
) (
  input  logic clk,
  input  logic rst_n,
  input  logic arm,               // begin recording; clears a previous freeze
 
  // Both directions, sampled together. Sampling only one side is what makes
  // an interaction fault unreconstructable.
  input  logic         tx_valid,
  input  logic         tx_ready,
  input  xctrl_e       tx_ctrl,
  input  logic         tx_error,
  input  logic         rx_valid,
  input  xctrl_e       rx_ctrl,
  input  logic         rx_error,
  input  logic         link_up,
  input  logic [3:0]   link_status_vector,
 
  // Any checker firing, from either side.
  input  logic         trigger,
 
  output logic         frozen,
  output logic [PTR_W-1:0] entries_valid,
  // Read port for software or a debug interface.
  input  logic [PTR_W-1:0] rd_addr,
  output logic [15:0]      rd_entry,
  // Where the trigger landed in the buffer, so software knows which entry
  // is the violation and which are its history.
  output logic [PTR_W-1:0] trigger_index,
  output logic             trigger_valid
);
 
  // A compact per-cycle record. Deliberately narrow: depth matters more
  // than width here, because an interaction fault needs history and the
  // signals that matter at this boundary are nearly all single bits.
  logic [15:0] mem [DEPTH];
  logic [PTR_W-1:0] wptr_q;
  logic [PTR_W-1:0] count_q;
  logic [$clog2(POST_TRIGGER+1)-1:0] post_q;
  logic             armed_q;
 
  wire [15:0] sample_c = {
    tx_valid, tx_ready, tx_error,
    rx_valid, rx_error,
    link_up,
    link_status_vector,          // 4 bits
    tx_ctrl[2:0], rx_ctrl[1:0]   // 5 bits
  };
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      wptr_q        <= '0;
      count_q       <= '0;
      post_q        <= '0;
      armed_q       <= 1'b0;
      frozen        <= 1'b0;
      trigger_index <= '0;
      trigger_valid <= 1'b0;
    end else if (arm) begin
      wptr_q        <= '0;
      count_q       <= '0;
      post_q        <= '0;
      armed_q       <= 1'b1;
      frozen        <= 1'b0;
      trigger_valid <= 1'b0;
    end else if (armed_q && !frozen) begin
      mem[wptr_q] <= sample_c;
      wptr_q      <= wptr_q + 1'b1;
      if (count_q != PTR_W'(DEPTH - 1)) count_q <= count_q + 1'b1;
 
      // First trigger only. A later violation is almost always a consequence
      // of the first, and overwriting would replace the cause with an effect.
      if (trigger && !trigger_valid) begin
        trigger_index <= wptr_q;
        trigger_valid <= 1'b1;
      end
 
      // Keep recording briefly past the trigger so the consequence is in
      // the same trace as its cause.
      if (trigger_valid) begin
        if (post_q == ($clog2(POST_TRIGGER+1))'(POST_TRIGGER)) frozen <= 1'b1;
        else post_q <= post_q + 1'b1;
      end
    end
  end
 
  assign entries_valid = count_q;
  assign rd_entry      = mem[rd_addr];
 
endmodule

Classification: synthesizable instrumentation.

What it teaches: that for interaction faults the violation is a consequence, so a checker that reports only the violation cannot help. The cause is a coincidence some cycles earlier between two individually-legal events, and only a trace with history reaches it.

Deliberately simplified: a 16-bit record. A production trace is wider and often includes a cycle counter so entries can be correlated with events elsewhere in the system.

Production implication: sampling both directions together is the entire point. A trace that records only the transmit side, or only the receive side, cannot reconstruct an interaction — the two events that coincided are on opposite sides of the boundary, and separate traces cannot be aligned after the fact to single-cycle accuracy. This is the one module in the chapter that must span the boundary, and it is instrumentation rather than datapath precisely so that spanning it costs nothing architecturally.

And POST_TRIGGER matters more than it looks. Freezing exactly on the trigger stops the trace at the interesting moment and loses what happened next — which is often what identifies the failure mode. A handful of cycles of post-trigger recording is nearly free and repeatedly decisive.

11. Where the Rule Is Hard to Apply

An honest criterion has to say where it strains. Three cases, and the way each resolves is instructive.

Retimers and repeaters in the path. A retimer sits between a MAC and a PHY, or between two PHYs, and re-clocks the signal. It has no frame concept, so by the rule it is PHY-side. But it introduces latency the MAC's timestamping must account for, and skew the deskew of Chapter 3.8 must absorb. The rule assigns it correctly and does not tell you the whole story — its effects propagate to boundary responsibilities. The resolution is that retimers are PHY-side components whose characteristics must be exposed to boundary mechanisms, which is why they appear in latency budgets rather than in interface signal lists.

Cut-through forwarding. Chapter 2.7 §5 showed a cut-through switch begins forwarding before the check value has been verified. That means the MAC is emitting a frame whose validity is not yet known — which sits oddly with "the MAC owns framing because a malformed frame is detectable in the frame". The rule still holds; the detection has simply moved later. The MAC still owns it and still detects it — after transmission has begun — which is exactly why cut-through requires a mechanism to mark a frame bad after it has started, and why that mechanism is MAC-owned.

In-band management. Some systems carry management traffic in the data path rather than on a separate interface. It looks like a boundary violation of Section 5's careful separation. It is not, because the traffic is still frames — the MAC frames them, the PHY carries them opaquely, and the fact that their contents happen to concern the PHY is invisible to the boundary. The separation Section 5 insisted on is between status paths, not between subject matters.

12. Assertions

Every property below is a property of these teaching models and of the assumption contract of Section 4. IEEE 802.3 specifies the reconciliation sublayer's service interface per clause; how a design instruments and attributes contract violations is an implementation choice.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─── Safety: the handshake holds data stable ───────────────────────────────
// The PHY's assumption 1, asserted at the boundary. Catches the violation
// that only appears under back-pressure and therefore ships.
property p_held_data_stable;
  @(posedge clk) disable iff (!rst_n)
  (tx_valid && !tx_ready) |=> (tx_valid && $stable(tx_data) && $stable(tx_ctrl));
endproperty
 
// ─── Ordering: frame structure is well formed ──────────────────────────────
// The PHY's assumption 2. Catches a MAC emitting a START inside a frame,
// which the PHY cannot detect through any ordinary path.
property p_no_start_inside_frame;
  @(posedge clk) disable iff (!rst_n)
  (tx_valid && tx_ready && (tx_ctrl == XC_START)) |-> !in_frame_q;
endproperty
 
property p_no_data_outside_frame;
  @(posedge clk) disable iff (!rst_n)
  (tx_valid && tx_ready && (tx_ctrl == XC_DATA)) |-> in_frame_q;
endproperty
 
// ─── Safety: an abandoned frame is marked ──────────────────────────────────
// Carried from Chapter 3.4 §15 because it is a boundary obligation. Catches
// silent truncation, which produces a short but structurally valid frame at
// the far end -- attributed to the link, caused above it.
property p_abort_is_marked;
  @(posedge clk) disable iff (!rst_n)
  (tx_valid && tx_error) |-> (tx_ctrl == XC_ERROR) || in_frame_q;
endproperty
 
// ─── Safety: the gap is respected ──────────────────────────────────────────
// The PHY's assumption 3. Catches a MAC that closes the gap, leaving the
// elastic buffer nowhere to insert or delete idles -- an overrun the PHY
// reports and the MAC caused.
property p_gap_respected;
  @(posedge clk) disable iff (!rst_n)
  (tx_valid && tx_ready && (tx_ctrl == XC_START)) |-> (gap_q >= MIN_GAP);
endproperty
 
// ─── Causation: link_up agrees with the status vector ──────────────────────
// The MAC's assumption 3. Catches a PHY whose summary bit is derived
// independently of its own status, so the two can disagree -- after which
// the MAC delivers frames from a link the PHY considers unusable.
property p_link_up_agrees_with_vector;
  @(posedge clk) disable iff (!rst_n)
  link_up |-> (&link_status_vector);
endproperty
 
// ─── Safety: the rate is stable while the link is up ───────────────────────
// The MAC's assumption 2. Catches a rate change without a link drop, which
// invalidates every gap and sizing calculation the MAC has in flight.
property p_rate_stable_while_up;
  @(posedge clk) disable iff (!rst_n)
  (link_up && rate_valid) |=> (!link_up || (negotiated_rate == $past(negotiated_rate)));
endproperty
 
// ─── Safety: receive errors qualify something ──────────────────────────────
// Catches rx_error asserted outside a frame, which is meaningless and makes
// a MAC acting on it discard the NEXT frame.
property p_rx_error_inside_frame;
  @(posedge clk) disable iff (!rst_n)
  (rx_valid && rx_error) |-> (in_frame_q || (rx_ctrl == XC_START));
endproperty
 
// ─── Mutual exclusion: at most one party is blamed ─────────────────────────
// Catches an attribution that fires two outputs, which sends two teams to
// two places for one fault.
property p_attribution_onehot;
  @(posedge clk) disable iff (!rst_n)
  $onehot0({blame_phy_internal, blame_phy_contract,
            blame_mac_contract, blame_unattributed});
endproperty
 
// ─── Causation: PHY self-report outranks contract evidence ─────────────────
// The priority ordering of Section 8, asserted. Catches an ordering that
// blames an interface for a link that is simply down.
property p_status_outranks_contract;
  @(posedge clk) disable iff (!rst_n)
  (!link_up || !(&link_status_vector)) |-> blame_phy_internal;
endproperty
 
// ─── Conservation: a lost frame is always attributed ───────────────────────
// Catches a frame loss producing no attribution at all -- which is the same
// as no instrumentation, since the question was "whose problem is it".
property p_loss_is_attributed;
  @(posedge clk) disable iff (!rst_n)
  frame_lost |-> $onehot({blame_phy_internal, blame_phy_contract,
                          blame_mac_contract, blame_unattributed});
endproperty
 
// ─── Safety: capabilities are the intersection, and only after report ──────
// Catches a design treating "no PHY report yet" as "no capabilities", which
// silently configures the intersection with zero and disables everything.
property p_agreement_needs_phy_report;
  @(posedge clk) disable iff (!rst_n)
  agreement_valid |-> $past(phy_caps_valid);
endproperty
 
// ─── Stability: first-violation records the first ──────────────────────────
// Catches a first-cause register that is overwritten, reporting the loudest
// violation rather than the initiating one.
property p_first_violation_is_stable;
  @(posedge clk) disable iff (!rst_n)
  first_violation_valid |=> $stable(first_violation);
endproperty
 
// ─── Stability: the trace freezes after the post-trigger window ────────────
// Catches a trace that keeps recording after freezing, overwriting the
// history that was the reason for capturing it.
property p_trace_freezes;
  @(posedge clk) disable iff (!rst_n)
  frozen |=> ($stable(trigger_index) && frozen);
endproperty
 
// ─── Causation: the trigger index is captured once ─────────────────────────
// Catches a trigger index overwritten by a later violation, which replaces
// the cause with one of its consequences.
property p_trigger_index_captured_once;
  @(posedge clk) disable iff (!rst_n)
  trigger_valid |=> $stable(trigger_index);
endproperty

13. Verification

A MAC transmits frames closer together than the minimum gap. The PHY accepts them because gap enforcement is not its responsibility, but its elastic buffer has nowhere to insert or delete idles. The buffer eventually overruns and the PHY reports an error. The MAC sees a PHY error and blames the link. Only the PHY-side conformance checker attributes the fault to the MAC.A gap violation, and who gets blamedMACPHYCheckerframes closer thanthe minimum gapaccepts — gap is notits responsibilityelastic buffer hasnowhere tocompensatebuffer overrunreported as a PHYerrorblames the link —wrong team, weekslostgap_too_short:attributed to theMAC
Figure 3 — a violation neither side can see, and the only path that resolves it.

Read the fifth message. Without the checker, the observable fact is a PHY error, so the PHY team investigates, finds nothing wrong with the PHY, and the loop repeats. The cause is a MAC emitting perfectly legal frames slightly too close together.

Scenarios

  1. A nominal frame across the boundary. Verify the control sequence is XC_START, then XC_DATA words, then XC_END, that no checker fires on either side, and that attribution reports nothing.
  2. Back-pressure mid-frame with stable data. Stall tx_ready and hold the offer unchanged. Verify no violation — this is the correct behaviour and a checker that fires here is unusable.
  3. Back-pressure mid-frame with the data changed. The violation that ships. Verify v_data_changed_while_stalled fires, c_data_changed_while_stalled advances, and attribution reports blame_mac_contract.
  4. A START while already inside a frame. Verify v_start_inside_frame and correct attribution.
  5. A DATA word outside any frame. Verify v_data_outside_frame.
  6. Frames at exactly the minimum gap, and one word short of it. Two runs. Verify the boundary is where the parameter says and that the legal case does not fire.
  7. A frame one word over the maximum. Verify v_frame_too_long fires and that the PHY does not silently truncate — noticing is the PHY's job even though enforcing is not.
  8. An aborted frame. Verify tx_error produces XC_ERROR and that the far end can distinguish it from a short valid frame.
  9. link_up asserted with an incomplete status vector. Verify v_status_contradiction fires and that attribution reports blame_phy_internalnot blame_phy_contract, because the PHY's self-report outranks contract evidence.
  10. The link reporting up and delivering nothing for a long period. Verify v_silent_while_upChapter 3.4 §9 established the PHY never goes silent, so this is a contract violation rather than idleness.
  11. A rate change while the link stays up. Verify v_rate_changed_while_up fires. Then repeat with the link dropping first and verify it does not — a rate change across a link-down is legal.
  12. rx_error asserted outside a frame. Verify v_error_outside_frame and that the MAC does not discard the following frame.
  13. An END with no preceding START. Verify v_end_without_start.
  14. Attribution priority. Assert a PHY status failure and a MAC contract violation simultaneously. Verify only blame_phy_internal fires. This is the ordering of Section 8 and getting it wrong sends people to the wrong team with apparent authority.
  15. Frames lost with no violation on either side. Verify blame_unattributed fires rather than defaulting to a side. An honest "no evidence" is the required output.
  16. Capability shortfall. Configure the MAC for a capability the PHY lacks and verify capability_shortfall with mac_wants_unsupported naming it — and that no other checker fires, because nothing at the interface is being violated.
  17. Operation before capability agreement. Start traffic before phy_caps_valid and verify operated_before_agreement sticks.
  18. First-violation ordering. Inject a gap violation, then a thousand handshake violations. Verify first_violation still names the gap.
  19. Trace capture and freeze. Arm the trace, inject a violation, and verify recording continues for POST_TRIGGER cycles and then stops, with trigger_index naming the violating entry and earlier entries preserved.
  20. Trace across an interaction. Run Scenario 21's abort-plus-idle-deletion coincidence and verify both sides' activity for that cycle appear in the same trace entry. Separate per-side traces cannot be aligned to single-cycle accuracy after the fact, and this scenario is what proves it.

What the checker must own

  • Two independently written models, one for each side's obligations. A single model derived from one specification reproduces that specification's ambiguities on both sides, and this boundary's failures are exactly ambiguities.
  • Sustained back-pressure, not occasional stalls. Scenario 3's violation only manifests under it, and a suite that never holds tx_ready low for long has not tested the handshake at all.
  • A "no violation" oracle. Scenarios 2 and 21 assert that checkers do not fire. A checker suite verified only against violations will pass with a checker that fires constantly.
  • Independent configuration of the two sides, so Scenario 16 can be constructed. A testbench configuring both from one parameter cannot produce a capability mismatch — which is the most common real fault at this boundary.
  • Coverage crosses of attribution against each violation source. The bin (frames lost, no violation either side) must be populated — that is blame_unattributed, and a run that never reaches it has not verified the honest-fallback path.

14. Debugging — Whose Problem Is It

The symptom: the link reports up and frames are being lost. Two teams, and each is confident the fault is the other's.

Step 1 — read the attribution output. One register, and it is the whole point of Sections 6 through 9 existing:

AttributionWhat it meansWho owns it
blame_phy_internalthe PHY says a sublayer is unsatisfiedPHY — descend Chapter 3.8's ladder
blame_phy_contractthe PHY broke what the MAC assumedPHY — and it is a contract bug, not a link problem
blame_mac_contractthe MAC broke what the PHY assumedMAC — even though the error surfaced at the PHY
blame_unattributedframes lost, no evidence either wayneither yet — go to step 4

Step 2 — if blame_phy_internal, stop arguing and descend. The PHY has self-reported. Chapter 3.8 §14's stall_state names the gate, and that investigation is entirely below this boundary.

Step 3 — if a contract violation, read first_violation rather than the counts. During an incident several will be set. The first identifies the initiating fault, and the rest are usually consequences of it — a broken handshake produces spurious gap and length findings on everything after it.

Step 4 — if unattributed, check capability agreement before anything else. capability_shortfall is the fault that produces exactly this signature: both sides report healthy, nothing at the interface is violated, and frames are corrupted. The MAC is emitting something structurally perfect that this PHY cannot carry. It is invisible to every interface checker because nothing at the interface is wrong.

Step 5 — still unattributed. Now suspect an unspecified interaction — Scenario 21's class. Two legal behaviours whose coincidence the contract does not cover. The signature is a fault that correlates with load or with a specific traffic pattern rather than with time, and that neither side can reproduce in isolation. The fix is to the contract, and the first step is to reproduce it with both sides instrumented simultaneously.

Step 6 — before escalating to either team, check operated_before_agreement. If the MAC ran before capabilities were agreed, every parameter it used until then was assumed. Faults from that window are configuration faults, not implementation faults, and they will not reproduce after a clean bring-up.

The method stated once: attribution names the owner in one read, first_violation separates the initiating fault from its consequences, and an unattributed loss points at capability mismatch first and an unspecified interaction second — because those are the two faults that leave both sides internally correct and the link not working.

15. Common Misconceptions

"The MAC/PHY split is arbitrary — it is just where the standard drew a line."

The wrong model: two lists of functions, divided by convention.

What it costs: you cannot place a new responsibility, so timestamping, energy-efficient idle and per-priority flow control each become an argument from analogy. Two teams reach different conclusions, both defensible, and the disagreement surfaces at integration.

The corrected model: the split is generated by one rule — a responsibility belongs to the side that can detect its own failure — and the conventional assignment falls out of it. Every MAC responsibility fails visibly in a frame; every PHY responsibility fails visibly in a signal. The rule extends to new cases and the lists do not.

"A well-defined interface means the two sides are independent."

The wrong model: honour the signal list and the blocks compose.

What it costs: you build no cross-boundary instrumentation, and the two most expensive failures at this boundary become undiagnosable. Both sides pass their own verification, both are conformant to their own reading, and the link does not work.

The corrected model: the signal list is the smaller half of the interface. The larger half is the assumption contract — eight things each side must believe about the other and none of which it can check. Ownership is not isolation; the sides are coupled through latency, timing and capability.

"If the PHY reports an error, the fault is in the PHY."

The wrong model: the reporting side is the failing side.

What it costs: the gap violation of Figure 3, in its full form. A MAC transmits legal frames slightly too close together, the PHY's elastic buffer has nowhere to compensate, it overruns, and the PHY reports the error. The PHY team investigates for weeks and finds nothing wrong, because nothing is.

The corrected model: the reporting side is the side that can detect the failure, which by Section 3's rule is often not the causing side. That is a direct consequence of responsibility following detectability, and it is why attribution must be a design output rather than an inference from which counter moved.

"Cross-boundary assertions verify the boundary."

The wrong model: a testbench with access to both sides can check their agreement, so the boundary is covered.

What it costs: Section 12's rejected property in full. The assertion is green for the project's life, creates confidence the boundary is verified, and — worst — displaces the synthesizable checkers that would have caught the fault in production.

The corrected model: apply the test could this be implemented in silicon, using only signals that cross the boundary? If not, it is testbench-only, must be labelled as such, and something implementable must exist alongside it. Section 7's v_frame_too_long catches less and catches it where it matters.

"Status and management are the same interface."

The wrong model: the PHY's state is the PHY's state, so expose it all in one place.

What it costs: a MAC that reads FEC correction counts cannot be paired with a PHY that has no FEC. Exposing PHY internals at the data-path boundary creates a dependency on one kind of PHY and destroys the composability Chapter 3.4 §4 showed the architecture depends on.

The corrected model: data-path status — is the link usable, is this frame trustworthy — belongs at this boundary because the MAC's per-frame behaviour depends on it. Diagnostic detail — FEC counts, eye margin, skew, equaliser state — belongs on a management path with a different consumer, a different rate and different failure semantics.

16. Interview Reasoning

"Where exactly does the MAC end and the PHY begin?"

The weak answer recites two lists. The answer that ends the topic gives a rule — a responsibility belongs to the side that can detect its own failure — shows it reproducing the conventional split, and then applies it to a case the lists do not cover. Timestamping is the best one: the rule predicts a split implementation, because detecting a bad timestamp needs both the instant and the frame identity, and real designs are split exactly that way.

"Frames are being lost on a link that reports up. How do you decide whose problem it is?"

Not "check the PHY first". The strong answer observes that neither side can verify the other's assumptions, so attribution must be instrumented deliberately on both sides — and then gives the priority: the PHY's own self-report outranks contract evidence, because a PHY that says it is not working makes every other observation unreliable. The candidate who adds that an honest "unattributed" must be a distinct outcome has thought about the failure mode where a default blames one party for everything.

"Give me a failure that is neither a MAC bug nor a PHY bug."

The gap violation is the cleanest: a MAC transmitting legal frames slightly too close together leaves the PHY's elastic buffer nowhere to insert or delete idles, it overruns, and the PHY reports the error. Both implementations are correct. Naming the abort-coinciding-with-idle-deletion case as well shows understanding of the deeper class: two independently-legal behaviours whose interaction the contract never specified, where each side can prove its own correctness and the bug is in the agreement.

17. Understanding Check

A responsibility belongs to the side that can detect its own failure.

The justification is that everything you need from an owner requires observation: a side that cannot see a failure cannot recover from it, cannot report it, and cannot be held to a requirement about it — it can neither verify compliance nor demonstrate it.

Applied to the conventional split, it reproduces it exactly. Framing, addressing, padding and the gap all fail visibly in a frame, and the MAC is the only side that has frames. Coding, clock recovery, alignment and line drive all fail visibly in a signal or a coded stream, and the MAC has no access to either. In every case exactly one side could possibly have owned it.

Its value is that it extends. Timestamping: detecting a bad timestamp needs both when the signal left and which frame it was, and no single side knows both — so the rule assigns it to the boundary, and real designs implement it as a split with the PHY capturing the instant and the MAC associating it. That the rule predicts a split implementation, and that reality is split, is evidence it is doing work rather than describing.

The follow-up to be ready for: what about rate adaptation? Two responsibilities sharing a name. Clock-rate difference shows up as a buffer drifting — PHY-visible, so PHY-owned. Client-outruns-link shows up as a queue growing — MAC-visible, so MAC-owned. The rule separates them; two lists cannot.

18. What's Next

The claim this chapter defended: the MAC/PHY split is not a list to memorise. It is generated by one rule — a responsibility belongs to the side that can detect its own failure — and that rule decides every case, including the ones the lists never mention.

Two things follow, and later modules can cite them rather than re-derive them.

First, the assignment. Every MAC responsibility fails visibly in a frame; every PHY responsibility fails visibly in a signal. Responsibilities that neither side can detect alone — timestamping, energy-efficient idle, capability agreement — are boundary responsibilities and are implemented as protocols between the two rather than as functions of either.

Second, and more useful operationally, the blindness. Because each side detects only its own failures, each is blind to the other's, and every assumption in the contract is unverifiable by the side that relies on it. That is why the two most expensive failures here are neither a MAC bug nor a PHY bug: a capability mismatch, where both sides are correctly configured from sources that disagree, and an unspecified interaction, where two legal behaviours meet in a case the contract never covered. In both, each side can prove its own correctness and the link does not work.

Hence the three things this boundary needs rather than one: an assignment, an explicit contract, and attribution instrumentation — because the first two guarantee no single side can attribute a failure alone.

Module 4 now builds on this. Chapter 4.2 takes the media-independent interface itself — the generations, their widths and clocking, and the control encodings Chapter 3.4 approached from below and this chapter used as a boundary without enumerating. Chapter 4.3 traces a frame across it signal by signal in both directions, where the assumption contract stated here becomes a cycle-by-cycle sequence.

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.