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.
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:
| Responsibility | How its failure shows | Detectable by | Owner |
|---|---|---|---|
| framing | the frame's check value fails | reading the frame | MAC |
| addressing | a frame arrives at the wrong station | comparing addresses | MAC |
| padding to the minimum | an undersized frame | measuring the frame | MAC |
| the interframe gap | frames closer than the minimum | timing between frames | MAC |
| line coding | invalid code groups appear | parsing the coded stream | PHY |
| clock recovery | lock is lost | watching the recovery loop | PHY |
| block alignment | blocks stop parsing | Chapter 3.5's invalid patterns | PHY |
| line drive | the far end reports errors, or signal detect drops | measuring the signal | PHY |
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.
What the MAC may assume about the PHY:
- Bits arrive in the order they were sent. No reordering, ever. The PHY may lose or corrupt, never permute.
- The rate is known and constant while the link reports up. The MAC's gap enforcement and sizing arithmetic depend on it.
- 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.
- 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:
- The interface handshake is honoured. Data offered stays stable until accepted. Chapter 3.4 §15 asserted this and named it the rule people break.
- 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.
- 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.
- 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.
// 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;
endmoduleClassification: 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.
// 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
endmoduleClassification: 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.
// 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
endmoduleClassification: 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.
// 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
endmoduleClassification: 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.
// 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
endmoduleClassification: 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.
// 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];
endmoduleClassification: 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.
// ─── 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);
endproperty13. Verification
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
- A nominal frame across the boundary. Verify the control sequence is
XC_START, thenXC_DATAwords, thenXC_END, that no checker fires on either side, and that attribution reports nothing. - Back-pressure mid-frame with stable data. Stall
tx_readyand hold the offer unchanged. Verify no violation — this is the correct behaviour and a checker that fires here is unusable. - Back-pressure mid-frame with the data changed. The violation that ships. Verify
v_data_changed_while_stalledfires,c_data_changed_while_stalledadvances, and attribution reportsblame_mac_contract. - A
STARTwhile already inside a frame. Verifyv_start_inside_frameand correct attribution. - A
DATAword outside any frame. Verifyv_data_outside_frame. - 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.
- A frame one word over the maximum. Verify
v_frame_too_longfires and that the PHY does not silently truncate — noticing is the PHY's job even though enforcing is not. - An aborted frame. Verify
tx_errorproducesXC_ERRORand that the far end can distinguish it from a short valid frame. link_upasserted with an incomplete status vector. Verifyv_status_contradictionfires and that attribution reportsblame_phy_internal— notblame_phy_contract, because the PHY's self-report outranks contract evidence.- The link reporting up and delivering nothing for a long period. Verify
v_silent_while_up— Chapter 3.4 §9 established the PHY never goes silent, so this is a contract violation rather than idleness. - A rate change while the link stays up. Verify
v_rate_changed_while_upfires. Then repeat with the link dropping first and verify it does not — a rate change across a link-down is legal. rx_errorasserted outside a frame. Verifyv_error_outside_frameand that the MAC does not discard the following frame.- An
ENDwith no precedingSTART. Verifyv_end_without_start. - Attribution priority. Assert a PHY status failure and a MAC contract violation simultaneously. Verify only
blame_phy_internalfires. This is the ordering of Section 8 and getting it wrong sends people to the wrong team with apparent authority. - Frames lost with no violation on either side. Verify
blame_unattributedfires rather than defaulting to a side. An honest "no evidence" is the required output. - Capability shortfall. Configure the MAC for a capability the PHY lacks and verify
capability_shortfallwithmac_wants_unsupportednaming it — and that no other checker fires, because nothing at the interface is being violated. - Operation before capability agreement. Start traffic before
phy_caps_validand verifyoperated_before_agreementsticks. - First-violation ordering. Inject a gap violation, then a thousand handshake violations. Verify
first_violationstill names the gap. - Trace capture and freeze. Arm the trace, inject a violation, and verify recording continues for
POST_TRIGGERcycles and then stops, withtrigger_indexnaming the violating entry and earlier entries preserved. - 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_readylow 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 isblame_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:
| Attribution | What it means | Who owns it |
|---|---|---|
blame_phy_internal | the PHY says a sublayer is unsatisfied | PHY — descend Chapter 3.8's ladder |
blame_phy_contract | the PHY broke what the MAC assumed | PHY — and it is a contract bug, not a link problem |
blame_mac_contract | the MAC broke what the PHY assumed | MAC — even though the error surfaced at the PHY |
blame_unattributed | frames lost, no evidence either way | neither 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
Related tutorials
- Related topic
The Reconciliation Sublayer and the xMII Contract
The xMII generations are a record of what each had to give up — width, pins, timing margin, even parallelism — to keep carrying the same vocabulary as rates rose. That one vocabulary survived six unrelated physical forms is what media-independence actually means.
- Related topic
Data Flow Across the Boundary
Transmit is scheduled and receive is not. That one temporal fact is why the transmit path can be back-pressured and the receive path cannot — so one needs a handshake and the other a buffer, and being unready costs latency in one direction and a whole frame in the other.
- Related topic
Elastic Buffering and Clock Compensation
Two independent oscillators differ by a bounded amount forever, and a bounded rate difference still accumulates without limit unless something discharges it. The interframe gap is that opportunity — which is why it is not negotiable and why the buffer is far smaller than intuition suggests.
- Related topic
The MAC/PHY Boundary in RTL
What five chapters described as one boundary is three in silicon: a data boundary at the port list, a clock boundary inside the elastic buffer, and a reset boundary that is an order rather than a place. Confusing any two produces a specific, recognisable integration failure.
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.
