Ethernet · Module 4
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.
Chapter 4.1 gave a static contract: eight assumptions, four in each direction. Chapter 4.2 gave a static vocabulary: data, enable, error, valid, and what makes those words durable.
Both descriptions are symmetric. There is a transmit path and a receive path, each with data and control, and nothing in either chapter suggests the two are different in kind.
Put them in time and they stop being symmetric.
A transmit path is scheduled. The MAC decides when a frame starts, and if the interface is not ready, the MAC waits. That waiting is legal, expected, and is the entire purpose of the handshake Chapter 4.1 §4 listed as the PHY's first assumption.
A receive path is not scheduled. A frame arrives because a far-end station decided to send it, at a rate set by that station's clock, and it does not stop. There is no receive handshake, because there is nothing to negotiate with. The MAC cannot ask the far end to wait; the far end is not listening and is thousands of nanoseconds away.
What does a frame's crossing look like cycle by cycle in each direction, and what does the asymmetry between them force into the design?
1. Scope — What This Chapter Owns
This chapter owns: the cycle-by-cycle sequence of a frame crossing the boundary in each direction; the scheduling asymmetry and every consequence that follows from it; where the preamble is generated and where it is consumed; how back-pressure propagates and where it stops; how an abort behaves in each direction; and the reference model a cycle-accurate checker needs.
This chapter does not own: the assumption contract or ownership rule (Chapter 4.1), the interface vocabulary or its generations (Chapter 4.2), or the frame's field layout — Chapter 5.1 owns that and this chapter treats a frame as octets with a boundary. Clock-domain crossing between the MAC and PHY clocks is Chapter 4.4 and Chapter 4.6; here both sides share a clock so the sequence is visible without the crossing obscuring it.
The distinction from its two neighbours: 4.1 asked who is answerable, 4.2 asked what the signals mean. This chapter asks what only a temporal view reveals — and the answer is the scheduling asymmetry, which neither static description can state.
2. Transmit, Cycle by Cycle
Conceptual — a frame leaving the MAC
10 cyclesThis figure is conceptual and labelled so. It shows the ordering and the handshake relationship correctly; it does not attempt to show a specific generation's setup and hold, or DDR half-cycles.
The sequence, stated precisely:
- The MAC decides to transmit. Nothing external prompts this. The frame is ready and the gap since the last frame has elapsed.
tx_enrises with the first octet, not before it. The enable and the first data octet are simultaneous. An enable asserted a cycle early puts an undefined octet on the wire; a cycle late loses the first octet.- Each octet is offered and accepted. While
tx_acceptis low, the MAC holds — it does not withdraw the offer and does not change the data. This is Chapter 4.1 §4's first PHY assumption, and Chapter 4.1 §7's checker exists because the PHY cannot verify it. tx_enfalls after the last octet is accepted, not after it is offered. Falling on the offer truncates the frame by one octet if that octet was not taken.- The gap begins. Chapter 4.1 §7 established why it matters: the PHY compensates for clock differences by inserting and deleting idle in the gap, so closing it leaves nowhere to do that. Chapter 4.4 owns the arithmetic.
3. Receive, Cycle by Cycle
Conceptual — a frame arriving at the MAC
10 cyclesCompare the two figures signal for signal. The transmit figure has tx_accept. The receive figure has no accept, no ready, and no back-pressure of any kind — and the fifo_full row shows what that means: the buffer fills, and the data does not stop.
The sequence:
rx_dvrises. Nothing was negotiated. A far-end station decided to transmit, and the PHY is now delivering.- Data arrives every cycle the interface says it does. The MAC has no mechanism to slow it.
rx_erqualifies the frame, not the octet. This is the subtle one and Section 5's RTL depends on it: an error asserted mid-frame means this frame is suspect, not this particular octet is wrong. A MAC that discards one octet and keeps the rest produces a frame that is short and structurally plausible.rx_dvfalls when the far end's frame ends, at a time determined entirely by the far end.- If the MAC could not keep up, the frame is lost. There is no other outcome. The data was delivered and not absorbed.
4. Where the Preamble Lives
A detail that only a temporal view makes visible, and that catches people writing their first loopback test.
On transmit, the preamble is generated below the MAC. The MAC hands over a frame beginning with the destination address; something between there and the wire prepends the preamble and start delimiter.
On receive, the preamble is consumed below the MAC. By the time rx_dv rises for the MAC's benefit, the preamble has done its job — bit synchronisation, then byte alignment — and been removed. The MAC's first received octet is the destination address.
Which means the two directions do not have the same cycle count for the same frame. A transmit path emits preamble octets that its receive path never reports. In a loopback test, the octet count out and the octet count back are equal, but the cycle counts differ by the preamble length, and a checker comparing cycles rather than octets will report a mismatch on a perfectly correct design.
5. RTL 1 — The Transmit Datapath
// SYNTHESIZABLE. MAC transmit path to the interface.
//
// The three timing rules this module exists to get right, each of which is
// a real defect when got wrong:
//
// 1. tx_en RISES WITH the first octet, never before. Early puts an
// undefined octet on the wire; late loses the first one.
// 2. The pointer advances on (valid && accept), NEVER on valid alone.
// Advancing on valid drops an octet from the MIDDLE of a frame under
// back-pressure -- undetectable at the interface, and it appears at
// the far end as a check-value failure blamed on the link.
// 3. tx_en FALLS after the last octet is ACCEPTED, not after it is
// offered. Falling on the offer truncates by one octet.
package txflow_pkg;
typedef enum logic [2:0] {
TX_IDLE,
TX_GAP, // enforcing the interframe gap
TX_DATA,
TX_ABORT, // marking a frame the MAC abandoned
TX_END
} tx_state_e;
endpackage
module mac_tx_datapath
import txflow_pkg::*;
#(
parameter int unsigned W = 8,
// Minimum idle cycles between frames. Chapter 4.1 §7 showed why: the PHY
// compensates for clock difference in the gap, and closing it leaves
// nowhere to do that. Chapter 4.4 owns the arithmetic.
parameter int unsigned MIN_GAP = 12,
parameter int unsigned GAP_W = $clog2(MIN_GAP + 2)
) (
input logic clk,
input logic rst_n,
// ── Frame source ────────────────────────────────────────────────────────
input logic src_valid,
input logic [W-1:0] src_data,
input logic src_last, // this is the final octet
input logic src_abort, // abandon the frame in progress
output logic src_ready,
// ── Interface ───────────────────────────────────────────────────────────
output logic [W-1:0] txd,
output logic tx_en,
output logic tx_er,
input logic tx_accept,
output tx_state_e state,
// ── Observability ───────────────────────────────────────────────────────
output logic [31:0] c_frames_sent,
output logic [31:0] c_frames_aborted,
// Cycles the source was held because the interface was not accepting.
// Total says how much throughput was lost; the longest single stall says
// how deep the upstream FIFO must be -- and sizing from the total is how
// designs overflow on a burst.
output logic [31:0] c_stall_cycles,
output logic [15:0] longest_stall
);
tx_state_e state_q, state_d;
logic [GAP_W-1:0] gap_q;
logic [15:0] run_q;
// Rule 2, in one expression. The source advances only on a completed
// handshake, so a stalled octet is offered again unchanged next cycle.
assign src_ready = (state_q == TX_DATA) && tx_accept;
always_comb begin
state_d = state_q;
unique case (state_q)
TX_IDLE:
if (src_valid) state_d = TX_DATA;
TX_DATA: begin
if (src_abort) state_d = TX_ABORT;
else if (src_valid && src_last && tx_accept) state_d = TX_END;
end
// An abandoned frame is MARKED, never silently truncated. Chapter 4.1
// §11 asserted this: silent truncation produces a short but
// structurally valid frame whose check value then fails at the far
// end, and the loss is attributed to the link.
TX_ABORT: if (tx_accept) state_d = TX_END;
TX_END: state_d = TX_GAP;
TX_GAP:
if (gap_q >= GAP_W'(MIN_GAP)) state_d = TX_IDLE;
default: state_d = TX_IDLE;
endcase
end
always_comb begin
// Rule 1 and rule 3 together: the enable is exactly the data and abort
// states, so it rises with the first octet and falls after the last is
// accepted -- because TX_END is only reached on an accepted last octet.
tx_en = (state_q == TX_DATA) || (state_q == TX_ABORT);
tx_er = (state_q == TX_ABORT);
txd = src_data;
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state_q <= TX_IDLE;
gap_q <= '1;
c_frames_sent <= '0;
c_frames_aborted <= '0;
c_stall_cycles <= '0;
longest_stall <= '0;
run_q <= '0;
end else begin
state_q <= state_d;
if (state_q == TX_GAP) begin
if (gap_q < GAP_W'(MIN_GAP)) gap_q <= gap_q + 1'b1;
end else if (state_q == TX_DATA) begin
gap_q <= '0;
end
if ((state_q == TX_END) && (state_d == TX_GAP)) begin
if ($past(state_q) == TX_ABORT) c_frames_aborted <= c_frames_aborted + 1'b1;
else c_frames_sent <= c_frames_sent + 1'b1;
end
// A stall is the source having data the interface will not take.
if ((state_q == TX_DATA) && src_valid && !tx_accept) begin
c_stall_cycles <= c_stall_cycles + 1'b1;
run_q <= run_q + 1'b1;
if (run_q + 1'b1 > longest_stall) longest_stall <= run_q + 1'b1;
end else begin
run_q <= '0;
end
end
end
assign state = state_q;
endmoduleClassification: synthesizable.
What it teaches: that src_ready is gated on tx_accept and nothing else. That single expression is rule 2, and it is the difference between a design that holds correctly through a stall and one that drops an octet from the middle of a frame. The TX_END state exists so that tx_en falls after the last octet is accepted rather than offered — a state that looks redundant and is not.
Deliberately simplified: the frame source is external, and there is no clock-domain crossing. Chapter 4.6 owns the crossing; the sequence is shown in one domain so the ordering is visible without it.
Production implication: longest_stall matters more than c_stall_cycles for sizing, and this is the same average-versus-worst-case trap Chapter 4.2 §8 named for escape runs and Chapter 3.4 §12 for skid depth. Total stall tells you throughput lost; the longest single stall tells you how deep the upstream FIFO must be to ride it out. A design sized from the total overflows on the peak, and the peak only occurs under sustained load.
Later ownership: the clock-domain crossing is Chapter 4.6; the gap's arithmetic is Chapter 4.4.
6. RTL 2 — The Receive Datapath
// SYNTHESIZABLE. Interface to MAC receive path.
//
// NOTE WHAT IS MISSING: there is no ready, no accept, no back-pressure.
// The far end committed to this frame's timing before the first bit left
// it, cannot be reached within the frame's duration, and is not listening.
// There is NOTHING TO NEGOTIATE WITH.
//
// So the design has exactly two options -- absorb, or drop -- and the drop
// must be explicit and counted rather than implicit and silent.
//
// The other rule this module exists to get right: rx_er qualifies the
// FRAME, not the octet. A MAC that discards one octet and keeps the rest
// produces a frame that is short and structurally plausible, whose check
// value then fails for a reason unrelated to why it was marked.
package rxflow_pkg;
typedef enum logic [1:0] {
RX_IDLE,
RX_DATA,
RX_DROPPING // absorbing the rest of a frame we cannot store
} rx_state_e;
endpackage
module mac_rx_datapath
import rxflow_pkg::*;
#(
parameter int unsigned W = 8,
parameter int unsigned DEPTH = 2048,
parameter int unsigned PTR_W = $clog2(DEPTH)
) (
input logic clk,
input logic rst_n,
// ── Interface. No handshake exists in this direction. ───────────────────
input logic [W-1:0] rxd,
input logic rx_dv,
input logic rx_er,
// ── To the MAC client, which CAN be back-pressured ──────────────────────
output logic cli_valid,
output logic [W-1:0] cli_data,
output logic cli_last,
output logic cli_suspect, // the PHY did not trust this frame
input logic cli_ready,
output rx_state_e state,
// ── Observability ───────────────────────────────────────────────────────
output logic [31:0] c_frames_received,
output logic [31:0] c_frames_suspect,
// Frames dropped because the buffer was full. This is the receive
// direction's equivalent of a stall, and it is NOT recoverable -- which
// is why it is counted separately from every other error.
output logic [31:0] c_frames_dropped_full,
// High-water mark. The number to size from, and it must be trended:
// a mark that has crept from 30 percent to 80 over months is a receive
// path that will start dropping.
output logic [PTR_W:0] fifo_high_water
);
logic [W:0] fifo [DEPTH]; // {last, data}
logic [PTR_W-1:0] wptr_q, rptr_q;
logic [PTR_W:0] count_q;
rx_state_e state_q;
logic suspect_q;
wire full_c = (count_q >= (PTR_W+1)'(DEPTH - 1));
wire frame_end_c = (state_q != RX_IDLE) && !rx_dv;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
wptr_q <= '0;
rptr_q <= '0;
count_q <= '0;
state_q <= RX_IDLE;
suspect_q <= 1'b0;
c_frames_received <= '0;
c_frames_suspect <= '0;
c_frames_dropped_full <= '0;
fifo_high_water <= '0;
end else begin
// ── Write side: the data arrives whether we want it or not ──────────
if (rx_dv) begin
if (state_q == RX_IDLE) begin
// A frame is starting. Decide NOW whether we can take it: once
// committed, a frame must be stored whole or dropped whole. A
// partially stored frame is worse than none, because it is
// structurally plausible and its check value fails for the wrong
// reason.
if (full_c) begin
state_q <= RX_DROPPING;
c_frames_dropped_full <= c_frames_dropped_full + 1'b1;
end else begin
state_q <= RX_DATA;
suspect_q <= rx_er;
fifo[wptr_q] <= {1'b0, rxd};
wptr_q <= wptr_q + 1'b1;
count_q <= count_q + 1'b1;
end
end else if (state_q == RX_DATA) begin
// rx_er qualifies the FRAME. Sticky for its duration, applied at
// the end -- never used to discard a single octet.
if (rx_er) suspect_q <= 1'b1;
if (full_c) begin
// Ran out mid-frame. The partial frame already stored must be
// rewound, or the client receives a truncated frame that looks
// valid until its check value fails.
state_q <= RX_DROPPING;
c_frames_dropped_full <= c_frames_dropped_full + 1'b1;
end else begin
fifo[wptr_q] <= {1'b0, rxd};
wptr_q <= wptr_q + 1'b1;
count_q <= count_q + 1'b1;
end
end
// RX_DROPPING: absorb and discard. There is no alternative.
end
if (frame_end_c) begin
if (state_q == RX_DATA) begin
// Mark the last octet so the client knows where the frame ends.
fifo[wptr_q - 1'b1] <= {1'b1, fifo[wptr_q - 1'b1][W-1:0]};
c_frames_received <= c_frames_received + 1'b1;
if (suspect_q) c_frames_suspect <= c_frames_suspect + 1'b1;
end
state_q <= RX_IDLE;
suspect_q <= 1'b0;
end
// ── Read side: the client CAN be back-pressured ─────────────────────
if (cli_valid && cli_ready) begin
rptr_q <= rptr_q + 1'b1;
count_q <= count_q - 1'b1;
end
if (count_q > fifo_high_water) fifo_high_water <= count_q;
end
end
assign cli_valid = (count_q != 0);
assign cli_data = fifo[rptr_q][W-1:0];
assign cli_last = fifo[rptr_q][W];
assign cli_suspect = suspect_q;
assign state = state_q;
endmoduleClassification: synthesizable.
What it teaches: that a frame must be stored whole or dropped whole, and the decision is made at the frame's start where possible. A partially stored frame is worse than no frame — it is structurally plausible, it reaches the client, and its check value fails for a reason unrelated to why it was truncated, sending the investigation somewhere else entirely.
It also teaches that rx_er is sticky for the frame's duration and applied at the end. Using it to discard a single octet produces exactly the plausible-but-short frame the design is trying to avoid.
Deliberately simplified: the mid-frame overflow path marks the frame dropped without rewinding wptr_q, so the already-stored octets are still counted. A production design either reserves worst-case space before accepting a frame or maintains a rewind point — and the choice between those is a real architectural decision.
Production implication: fifo_high_water is the number to trend, not to read once. A high-water mark that has crept from 30 percent to 80 over months is a receive path that will start dropping — and it is the same slope-against-cliff argument Chapter 3.3 made for margin and Chapter 3.7 for the pre-correction rate. c_frames_dropped_full is the cliff; the high-water mark is the slope.
Later ownership: how the buffer is sized against clock difference rather than against burst is Chapter 4.4, and the two sizing arguments are different.
7. RTL 3 — Back-Pressure, and Where It Stops
Back-pressure propagates upstream until it reaches something that can wait. This module is about finding that something, and about what happens at the point where it does not exist.
// SYNTHESIZABLE. Back-pressure propagation with an explicit terminus.
//
// Back-pressure travels upstream until it reaches a producer that can wait.
// On TRANSMIT that terminus exists -- the MAC client, and ultimately
// software, can be told to hold.
//
// On RECEIVE THERE IS NO TERMINUS. The producer is a station at the far end
// of a cable. Propagating back-pressure toward it does not slow it down; it
// just moves the overflow one stage earlier and makes it harder to see.
//
// This module makes that asymmetry explicit so a design cannot accidentally
// build a receive chain that assumes a terminus it does not have.
module backpressure_chain #(
parameter int unsigned STAGES = 4,
parameter int unsigned CNT_W = 24,
// Transmit chains terminate in a producer that can wait. Receive chains
// do not, and the difference must be declared rather than assumed.
parameter bit HAS_TERMINUS = 1'b1
) (
input logic clk,
input logic rst_n,
input logic clear,
// Ready from the consumer end, propagating upstream.
input logic downstream_ready,
input logic [STAGES-1:0] stage_has_data,
output logic [STAGES-1:0] stage_ready,
output logic upstream_ready,
// Cycles back-pressure reached the top of the chain. On a transmit chain
// this is throughput lost. On a receive chain it is IMPENDING LOSS, and
// the two must not share a counter.
output logic [CNT_W-1:0] c_full_chain_stall,
output logic [15:0] longest_full_chain_stall,
// Back-pressure reached the top of a chain with no terminus. Every cycle
// of this is a cycle in which data is arriving and cannot be stored --
// which is a drop about to happen, not a stall.
output logic unterminated_stall,
output logic [CNT_W-1:0] c_unterminated_stall
);
logic [15:0] run_q;
// Simple combinational propagation: a stage is ready if the one below it
// is ready, or if the one below is empty.
always_comb begin
for (int unsigned i = 0; i < STAGES; i++) begin
if (i == 0) stage_ready[0] = downstream_ready;
else stage_ready[i] = stage_ready[i-1] || !stage_has_data[i-1];
end
upstream_ready = stage_ready[STAGES-1];
// The distinction the module exists for.
unterminated_stall = !upstream_ready && !HAS_TERMINUS;
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n || clear) begin
c_full_chain_stall <= '0;
c_unterminated_stall <= '0;
longest_full_chain_stall <= '0;
run_q <= '0;
end else begin
if (!upstream_ready) begin
if (!(&c_full_chain_stall)) c_full_chain_stall <= c_full_chain_stall + 1'b1;
run_q <= run_q + 1'b1;
if (run_q + 1'b1 > longest_full_chain_stall)
longest_full_chain_stall <= run_q + 1'b1;
if (unterminated_stall && !(&c_unterminated_stall))
c_unterminated_stall <= c_unterminated_stall + 1'b1;
end else begin
run_q <= '0;
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that back-pressure needs a terminus, and the receive direction does not have one. A transmit chain ends at a producer that can be told to wait. A receive chain ends at a cable, and pushing back toward it accomplishes nothing — the data keeps arriving, and the overflow simply happens one stage earlier where it is harder to attribute.
Deliberately simplified: combinational propagation across all stages, which is a real timing problem on a deep chain and is why production designs register the ready path — at the cost of the skid buffering Chapter 3.4 §12 described.
Production implication: c_unterminated_stall and c_full_chain_stall must not share a counter, because they mean different things. On a transmit chain, a full-chain stall is throughput lost and recoverable. On a receive chain, it is a drop about to happen — every cycle counted there is a cycle in which data is arriving and cannot be stored. Merging them produces a metric that reads the same for a slow link and a lossy one.
8. RTL 4 — Abort, in Both Directions
Section 2 established that a transmit error is a decision and a receive error is a report. This is where that distinction becomes two different pieces of logic.
// SYNTHESIZABLE. Abort handling, which is NOT symmetric.
//
// TRANSMIT ABORT is a DECISION we made. The frame is already partly on
// the wire, so it cannot be recalled -- it must be MARKED, so the far end
// can tell an abandoned frame from a short valid one. Silent truncation
// produces a structurally plausible frame whose check value fails, and
// the loss is attributed to the link.
//
// RECEIVE ABORT is a REPORT from the PHY. We cannot verify it and must
// act on it. The correct action is to mark the WHOLE frame suspect, not
// to discard the octet the report arrived with.
//
// Sharing logic between these two invites applying one direction's rule to
// the other, and both mistakes produce short plausible frames.
module abort_handler #(
parameter int unsigned W = 8,
parameter int unsigned CNT_W = 24
) (
input logic clk,
input logic rst_n,
// ── Transmit side ───────────────────────────────────────────────────────
input logic tx_in_frame,
input logic tx_abort_req, // the MAC decided to abandon
output logic tx_emit_error, // mark it on the wire
output logic tx_frame_done,
// ── Receive side ────────────────────────────────────────────────────────
input logic rx_in_frame,
input logic rx_error_reported, // the PHY says it is suspect
input logic rx_frame_end,
output logic rx_frame_suspect, // applies to the WHOLE frame
output logic rx_deliver_frame,
// ── Observability: four distinct outcomes ───────────────────────────────
output logic [CNT_W-1:0] c_tx_aborted,
output logic [CNT_W-1:0] c_rx_suspect,
// An abort requested when no frame is in progress. Not an error in the
// link -- a bug in the layer above, and it must not be counted as one.
output logic [CNT_W-1:0] c_tx_abort_no_frame,
// A receive error reported outside a frame. Meaningless, and a MAC that
// acts on it will mark the NEXT frame suspect.
output logic [CNT_W-1:0] c_rx_error_no_frame
);
logic suspect_q;
// Transmit: the abort is emitted while the frame is in progress, and the
// frame is then closed. There is no path that quietly stops.
assign tx_emit_error = tx_in_frame && tx_abort_req;
assign tx_frame_done = tx_in_frame && tx_abort_req;
// Receive: sticky for the frame, applied at its end.
assign rx_frame_suspect = suspect_q;
assign rx_deliver_frame = rx_frame_end;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
suspect_q <= 1'b0;
c_tx_aborted <= '0;
c_rx_suspect <= '0;
c_tx_abort_no_frame <= '0;
c_rx_error_no_frame <= '0;
end else begin
// Receive: accumulate for the frame, clear at its end.
if (rx_frame_end) begin
if (suspect_q && !(&c_rx_suspect)) c_rx_suspect <= c_rx_suspect + 1'b1;
suspect_q <= 1'b0;
end else if (rx_in_frame && rx_error_reported) begin
suspect_q <= 1'b1;
end
if (tx_in_frame && tx_abort_req && !(&c_tx_aborted))
c_tx_aborted <= c_tx_aborted + 1'b1;
// The two "outside a frame" cases. Both are bugs in a neighbour
// rather than link faults, and counting them as link errors sends
// the investigation to the wrong place.
if (!tx_in_frame && tx_abort_req && !(&c_tx_abort_no_frame))
c_tx_abort_no_frame <= c_tx_abort_no_frame + 1'b1;
if (!rx_in_frame && rx_error_reported && !(&c_rx_error_no_frame))
c_rx_error_no_frame <= c_rx_error_no_frame + 1'b1;
end
end
endmoduleClassification: synthesizable.
What it teaches: that the two directions need different logic for what looks like the same event. A transmit abort is emitted; a receive error is accumulated. Applying the transmit rule on receive — acting immediately, discarding the current octet — produces a frame one octet short. Applying the receive rule on transmit — accumulating and marking at the end — means the far end has already received an unmarked partial frame.
Deliberately simplified: no interaction with the frame-check-sequence generation, which in a real MAC must also be told the frame was aborted so it does not emit a valid check value over a truncated frame.
Production implication: c_tx_abort_no_frame and c_rx_error_no_frame count neighbour bugs, not link faults, and they must be separate from every link error counter. An abort requested with no frame in progress is the layer above misbehaving; a receive error outside a frame is the PHY reporting something meaningless. Folding either into a link error count sends an investigation to the cable for a fault in logic — the misattribution class Chapter 4.1 §14 exists to prevent.
9. RTL 5 — The Per-Cycle Reference Model
A cycle-accurate chapter needs a cycle-accurate checker, and the checker is a design in its own right.
// NON-SYNTHESIZABLE. VERIFICATION ONLY.
//
// A cycle-accurate reference for the transmit boundary. It predicts the
// interface signals from the frame source and the accept signal alone, and
// flags any cycle where the design differs.
//
// WRITTEN FROM THE SEQUENCE, NOT FROM THE RTL. A reference model derived
// from the design under test verifies self-consistency and nothing else,
// which is the single most common way a cycle-accurate checker becomes
// worthless while looking rigorous.
//
// It checks four things a static checker cannot:
// - tx_en rises on the SAME cycle as the first octet
// - data is held unchanged across every stalled cycle
// - the octet sequence is complete and in order
// - tx_en falls after the last ACCEPTED octet, not the last offered one
module tx_boundary_reference #(
parameter int unsigned W = 8,
parameter int unsigned MAX_LEN = 2048
) (
input logic clk,
input logic rst_n,
// The stimulus, as the testbench drove it.
input logic src_valid,
input logic [W-1:0] src_data,
input logic src_last,
input logic src_abort,
// What the design actually produced.
input logic [W-1:0] txd,
input logic tx_en,
input logic tx_er,
input logic tx_accept,
output logic mismatch,
output logic [3:0] mismatch_reason,
output int unsigned octets_expected,
output int unsigned octets_observed
);
localparam logic [3:0] R_EN_EARLY = 4'd1;
localparam logic [3:0] R_EN_LATE = 4'd2;
localparam logic [3:0] R_DATA_CHANGED = 4'd3;
localparam logic [3:0] R_EN_FELL_EARLY = 4'd4;
localparam logic [3:0] R_ORDER = 4'd5;
logic [W-1:0] expect_q [MAX_LEN];
int unsigned exp_wr, exp_rd;
logic [W-1:0] last_offered_q;
logic was_stalled_q;
logic in_frame_q;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
exp_wr <= 0;
exp_rd <= 0;
in_frame_q <= 1'b0;
was_stalled_q <= 1'b0;
last_offered_q <= '0;
mismatch <= 1'b0;
mismatch_reason <= '0;
octets_expected <= 0;
octets_observed <= 0;
end else begin
mismatch <= 1'b0;
// Record what the source offered, so order can be checked later.
if (src_valid && tx_accept) begin
expect_q[exp_wr] <= src_data;
exp_wr <= exp_wr + 1;
octets_expected <= octets_expected + 1;
end
// ── Check 1: the enable rises WITH the first octet ─────────────────
if (tx_en && !in_frame_q) begin
in_frame_q <= 1'b1;
if (!src_valid) begin
mismatch <= 1'b1;
mismatch_reason <= R_EN_EARLY;
end
end
// ── Check 2: data is held unchanged across a stall ─────────────────
// The rule of Chapter 4.1 §4, checked cycle by cycle rather than as
// a static obligation.
if (was_stalled_q && tx_en && (txd != last_offered_q)) begin
mismatch <= 1'b1;
mismatch_reason <= R_DATA_CHANGED;
end
// ── Check 3: octets appear in order and completely ─────────────────
if (tx_en && tx_accept && !tx_er) begin
if (txd != expect_q[exp_rd]) begin
mismatch <= 1'b1;
mismatch_reason <= R_ORDER;
end
exp_rd <= exp_rd + 1;
octets_observed <= octets_observed + 1;
end
// ── Check 4: the enable falls after the last ACCEPTED octet ────────
if (in_frame_q && !tx_en) begin
in_frame_q <= 1'b0;
if (exp_rd != exp_wr) begin
mismatch <= 1'b1;
mismatch_reason <= R_EN_FELL_EARLY;
end
end
was_stalled_q <= tx_en && !tx_accept;
if (tx_en) last_offered_q <= txd;
end
end
endmoduleClassification: non-synthesizable, verification only.
What it teaches: what a cycle-accurate checker has to do that a transaction-level one cannot. Checks 1 and 4 are pure timing relationships — the enable's edges relative to the first and last accepted octet — and a transaction-level scoreboard that compares frame contents cannot see either. Check 2 is the held-data rule, and it is only expressible in time.
Deliberately simplified: transmit only, and it records the expected sequence from accepted offers rather than from an independent frame generator. A production reference generates the frame independently and compares both directions.
Production implication: the header comment is the most important part. A reference model written from the RTL verifies that the design agrees with itself, which is worth nothing and looks rigorous. This one is written from Section 2's numbered sequence, which is the specification — and if the specification and the RTL disagree, that is exactly the finding a checker exists to produce.
Later ownership: the receive-direction reference is structurally different, because it has no accept signal to key off and must instead reconstruct from rx_dv edges alone.
10. RTL 6 — The Receive Reference, Which Cannot Be the Transmit One Reversed
Section 9's model keys every check off tx_accept. The receive direction has no accept signal, so the same structure cannot be used — and the way it has to be rebuilt is the chapter's thesis appearing one more time, in the verification environment.
// NON-SYNTHESIZABLE. VERIFICATION ONLY.
//
// WHY THIS IS NOT THE TRANSMIT MODEL PARAMETERISED:
//
// The transmit reference keys every check off tx_accept -- it knows exactly
// which octets were transferred because the handshake told it. There is no
// such signal here.
//
// So this model works the other way round. It records what the INTERFACE
// delivered, unconditionally, because delivery is not negotiable. Then it
// checks what the design did with it. The design is allowed to drop a whole
// frame; it is NOT allowed to deliver a partial one, reorder octets, or
// lose the suspect indication.
//
// That inversion -- record first, judge after -- is the receive direction's
// scheduling asymmetry showing up in the testbench.
module rx_boundary_reference #(
parameter int unsigned W = 8,
parameter int unsigned MAX_LEN = 2048
) (
input logic clk,
input logic rst_n,
// What the interface delivered. No handshake exists.
input logic [W-1:0] rxd,
input logic rx_dv,
input logic rx_er,
// What the design gave the client.
input logic cli_valid,
input logic [W-1:0] cli_data,
input logic cli_last,
input logic cli_suspect,
input logic cli_ready,
output logic mismatch,
output logic [3:0] mismatch_reason,
output int unsigned frames_delivered_by_iface,
output int unsigned frames_seen_by_client,
output int unsigned frames_dropped_whole
);
localparam logic [3:0] R_PARTIAL = 4'd1; // a fragment reached the client
localparam logic [3:0] R_ORDER = 4'd2;
localparam logic [3:0] R_SUSPECT_LOST = 4'd3;
localparam logic [3:0] R_EXTRA = 4'd4; // more octets out than in
logic [W-1:0] expect_q [MAX_LEN];
int unsigned exp_len, exp_rd;
logic iface_frame_q, iface_suspect_q;
int unsigned cli_count;
logic cli_frame_q;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
exp_len <= 0;
exp_rd <= 0;
iface_frame_q <= 1'b0;
iface_suspect_q <= 1'b0;
cli_count <= 0;
cli_frame_q <= 1'b0;
mismatch <= 1'b0;
mismatch_reason <= '0;
frames_delivered_by_iface <= 0;
frames_seen_by_client <= 0;
frames_dropped_whole <= 0;
end else begin
mismatch <= 1'b0;
// ── Record what arrived. Unconditionally: delivery is not optional. ──
if (rx_dv) begin
if (!iface_frame_q) begin
iface_frame_q <= 1'b1;
iface_suspect_q <= rx_er;
exp_len <= 1;
expect_q[0] <= rxd;
end else begin
if (rx_er) iface_suspect_q <= 1'b1;
if (exp_len < MAX_LEN) begin
expect_q[exp_len] <= rxd;
exp_len <= exp_len + 1;
end
end
end else if (iface_frame_q) begin
iface_frame_q <= 1'b0;
frames_delivered_by_iface <= frames_delivered_by_iface + 1;
exp_rd <= 0;
// Whether the design keeps this frame is ITS choice; whether it
// keeps a PART of it is not.
end
// ── Judge what the client got ──────────────────────────────────────
if (cli_valid && cli_ready) begin
cli_frame_q <= !cli_last;
// Order and content, against what actually arrived.
if (exp_rd < exp_len) begin
if (cli_data != expect_q[exp_rd]) begin
mismatch <= 1'b1;
mismatch_reason <= R_ORDER;
end
exp_rd <= exp_rd + 1;
end else begin
// More octets out than in. Always a bug.
mismatch <= 1'b1;
mismatch_reason <= R_EXTRA;
end
cli_count <= cli_count + 1;
if (cli_last) begin
frames_seen_by_client <= frames_seen_by_client + 1;
// THE CENTRAL RECEIVE CHECK: a frame delivered to the client must
// be COMPLETE. Dropping a whole frame is legal; delivering a
// fragment is not, because a fragment is structurally plausible
// and its check value fails for the wrong reason.
if (cli_count + 1 != exp_len) begin
mismatch <= 1'b1;
mismatch_reason <= R_PARTIAL;
end
// The suspect indication must survive to the client, or the MAC
// trusts a frame the PHY did not.
if (iface_suspect_q && !cli_suspect) begin
mismatch <= 1'b1;
mismatch_reason <= R_SUSPECT_LOST;
end
cli_count <= 0;
end
end
// A frame arrived and the client never saw it: a whole-frame drop,
// which is legal and must be counted rather than flagged.
if (!iface_frame_q && $past(iface_frame_q) && !cli_frame_q
&& (frames_seen_by_client == $past(frames_seen_by_client)))
frames_dropped_whole <= frames_dropped_whole + 1;
end
end
endmoduleClassification: non-synthesizable, verification only.
What it teaches: that the receive checker has to be built in the opposite order from the transmit one. Transmit knows what was transferred because the handshake said so, and checks timing against it. Receive records what arrived unconditionally — because arrival is not negotiable — and only then judges what the design did with it. The scheduling asymmetry of Section 2 reaches all the way into the testbench.
Deliberately simplified: one frame in flight, and no modelling of the client's own back-pressure interacting with a subsequent frame's arrival. A production reference tracks several frames in the buffer at once.
Production implication: R_PARTIAL is the check that matters most and the one a transaction-level scoreboard cannot make. Dropping a whole frame is legal; delivering a fragment is not, because a fragment is structurally plausible, reaches the client, and fails its check value for a reason unrelated to why it was truncated. A scoreboard that only compares delivered frames against expected frames will see a fragment as a corrupted frame and never as a truncated one — and those point at completely different subsystems.
And frames_dropped_whole must be a count, not a mismatch. A design that drops a frame under overflow is behaving correctly, and a checker that flags it will be disabled on the first burst test — taking R_PARTIAL with it.
11. Assertions
Every property below is a property of these teaching models and of the sequence in Sections 2 and 3. IEEE 802.3 specifies the reconciliation sublayer's service and each interface generation's timing per clause; the state machines and counters here are implementation choices.
// ─── Ordering: the enable rises with the first octet ───────────────────────
// Catches an enable asserted a cycle early, which puts an undefined octet on
// the wire, or a cycle late, which loses the first one. Neither is visible
// to a transaction-level checker.
property p_en_rises_with_data;
@(posedge clk) disable iff (!rst_n)
$rose(tx_en) |-> src_valid;
endproperty
// ─── Safety: held data does not change across a stall ──────────────────────
// The rule of Chapter 4.1 §4, in time. Catches a source advancing on valid
// rather than on (valid && accept), which drops an octet from the MIDDLE of
// a frame -- undetectable at the interface, blamed on the link at the far end.
property p_data_held_while_stalled;
@(posedge clk) disable iff (!rst_n)
(tx_en && !tx_accept) |=> (tx_en && $stable(txd));
endproperty
// ─── Causation: the source advances only on a completed handshake ──────────
// The same rule, checked at the source rather than at the interface.
property p_src_ready_requires_accept;
@(posedge clk) disable iff (!rst_n)
src_ready |-> tx_accept;
endproperty
// ─── Ordering: the enable falls after the last ACCEPTED octet ──────────────
// Catches a fall on the last OFFER, which truncates the frame by one octet
// whenever that offer was not taken.
property p_en_falls_after_accept;
@(posedge clk) disable iff (!rst_n)
$fell(tx_en) |-> $past(tx_accept);
endproperty
// ─── Safety: an abandoned frame is marked, never truncated ─────────────────
// Catches a silent stop, which produces a short but structurally valid frame
// whose check value fails at the far end for an unrelated-looking reason.
property p_abort_is_emitted;
@(posedge clk) disable iff (!rst_n)
(state == TX_ABORT) |-> (tx_en && tx_er);
endproperty
// ─── Safety: the gap is enforced before a new frame ────────────────────────
// Catches a frame started too soon, leaving the PHY nowhere to insert or
// delete idle -- an overrun the PHY reports and the MAC caused.
property p_gap_before_next_frame;
@(posedge clk) disable iff (!rst_n)
$rose(tx_en) |-> ($past(state) == TX_IDLE);
endproperty
// ─── Safety: there is no receive back-pressure ─────────────────────────────
// The chapter's central asymmetry, asserted structurally. Catches a design
// that grows a receive ready signal, which cannot be honoured and whose
// existence hides an overflow.
property p_no_receive_handshake;
@(posedge clk) disable iff (!rst_n)
rx_dv |-> 1'b1; // deliberately trivial: there is no signal to check
endproperty
// ─── Safety: rx_er qualifies the frame, not the octet ──────────────────────
// Catches a MAC discarding the octet the error arrived with, which produces
// a frame one octet short and structurally plausible.
property p_rx_error_marks_whole_frame;
@(posedge clk) disable iff (!rst_n)
(rx_in_frame && rx_error_reported) |=> rx_frame_suspect;
endproperty
// ─── Safety: a frame is stored whole or dropped whole ──────────────────────
// Catches a partial frame reaching the client, which is worse than no frame
// because it is plausible and fails its check value for the wrong reason.
property p_no_partial_frames;
@(posedge clk) disable iff (!rst_n)
(state == RX_DROPPING) |-> !cli_last;
endproperty
// ─── Conservation: every accepted octet appears exactly once ───────────────
// The property that survives every generation, unlike the ratio-dependent
// one Chapter 4.2 §11 rejected.
property p_octet_conservation;
@(posedge clk) disable iff (!rst_n)
(octets_observed <= octets_expected);
endproperty
// ─── Causation: an unterminated stall is counted separately ────────────────
// Catches transmit and receive stalls sharing a counter, which makes a slow
// link and a lossy one read identically.
property p_unterminated_only_without_terminus;
@(posedge clk) disable iff (!rst_n)
unterminated_stall |-> !HAS_TERMINUS;
endproperty
// ─── Safety: abort outside a frame is a neighbour bug, not a link error ────
// Catches it being folded into a link error count, which sends an
// investigation to the cable for a fault in logic.
property p_abort_needs_a_frame;
@(posedge clk) disable iff (!rst_n)
tx_emit_error |-> tx_in_frame;
endproperty
// ─── Stability: the high-water mark never decreases ────────────────────────
// Catches a mark that is reset by a read, destroying the trend that is the
// receive path's only early warning.
property p_high_water_monotone;
@(posedge clk) disable iff (!rst_n)
1'b1 |=> (fifo_high_water >= $past(fifo_high_water));
endproperty
// ─── Safety: a delivered frame is complete ─────────────────────────────────
// The receive direction's central check. Dropping a whole frame is legal;
// delivering a fragment is not, because a fragment is structurally
// plausible and fails its check value for the wrong reason.
property p_delivered_frames_are_complete;
@(posedge clk) disable iff (!rst_n)
(cli_valid && cli_ready && cli_last) |-> (cli_count + 1 == exp_len);
endproperty
// ─── Conservation: the suspect indication survives to the client ───────────
// Catches a design that absorbs rx_er and does not forward it, after which
// the MAC trusts a frame the PHY explicitly did not.
property p_suspect_survives;
@(posedge clk) disable iff (!rst_n)
(cli_valid && cli_ready && cli_last && iface_suspect_q) |-> cli_suspect;
endproperty12. Verification
Read the last two messages. The receive direction has no message travelling toward the interface, because there is nowhere to send one. That absence is the chapter's thesis in a figure.
Scenarios
- A frame with no back-pressure. Verify
tx_enrises with the first octet, every octet is accepted on the cycle it is offered, andtx_enfalls after the last. - A single-cycle stall mid-frame. Verify the data is held, the source does not advance, and the octet sequence out is unbroken.
- A long sustained stall. Hold
tx_acceptlow for hundreds of cycles mid-frame. Verify the held data never changes andlongest_stallrecords the full run. This is the scenario a light bench test never runs, and Section 5's rule 2 fails only here. - A stall on the very first octet. The boundary case for rule 1 — verify
tx_endoes not rise until the octet is actually accepted, and no undefined data appears. - A stall on the very last octet. The boundary case for rule 3 — verify
tx_enstays asserted until the accept, not until the offer. - Back-to-back frames at exactly the minimum gap. Verify the second frame starts no earlier, and that the gap counter is not corrupted by the first frame's end.
- A transmit abort at each position: first octet, middle, last octet. Three runs. Verify
tx_eris emitted withtx_enin every case and the frame is closed rather than silently stopped. - A transmit abort requested with no frame in progress. Verify
c_tx_abort_no_frameadvances and no interface activity results — this is a neighbour bug, not a link event. - A received frame with no error. Verify the client sees every octet in order with
cli_suspectlow. - A received frame with
rx_erasserted for one cycle mid-frame. Verify the whole frame is marked suspect and no octet is discarded. A design that drops the octet produces a frame one short. - A received frame with
rx_erasserted on the first octet and on the last. Two runs, verifying the sticky accumulation covers both boundaries. rx_erasserted outside any frame. Verifyc_rx_error_no_frameadvances and the next frame is not marked suspect.- A frame arriving when the FIFO is already full. Verify the frame is dropped whole,
c_frames_dropped_fulladvances, and no partial frame reaches the client. - A FIFO that fills mid-frame. The harder case. Verify the design does not deliver the partial frame it had already stored.
- The high-water mark. Drive a known burst and verify
fifo_high_waterrecords the peak and never decreases, including across a read. - Back-pressure chain, transmit configuration. Verify
upstream_readypropagates andunterminated_stallstays low withHAS_TERMINUSset. - Back-pressure chain, receive configuration. Same stimulus with
HAS_TERMINUSclear. Verifyc_unterminated_stalladvances — the same stall, a different meaning. - The reference model against a correct design. Verify no mismatch across a long random-length frame stream with random back-pressure.
- The reference model against three injected bugs: enable one cycle early, source advancing on
validalone, enable falling on the last offer. Verify each is caught with the rightmismatch_reason. A checker verified only against a correct design has not been verified. - The receive reference against a whole-frame drop. Verify
frames_dropped_wholecounts it andmismatchstays low — dropping a frame under overflow is correct behaviour, and a checker that flags it gets disabled on the first burst test. - The receive reference against an injected fragment. Truncate a frame inside the design and verify
R_PARTIALfires. This is the check a transaction-level scoreboard cannot make: it would see a corrupted frame, never a truncated one.
What the checker must own
- A reference model written from the sequence, not from the RTL — Section 9's header comment is the requirement, and Scenario 19 is what proves the model works.
- Sustained back-pressure as a first-class stimulus mode, not an occasional randomised stall. Scenario 3's failure appears only under long stalls, and a suite that never produces one has not tested the transmit path.
- A receive burst generator producing back-to-back minimum-size frames, because Section 11's rejected property is exactly the sizing error that stimulus catches and no average-rate stimulus reaches.
- Coverage crosses of frame position against back-pressure against abort. The bin
(mid-frame, stalled, abort asserted)must be populated — it is Scenario 20 — and(RX_DROPPING, cli_last asserted)must be unreachable, because a dropped frame must never deliver an end marker.
13. Debugging — Which Direction, and What Was Coincident
The symptom: frames are being lost, and the interface reports no errors.
Step 1 — establish the direction first. It sounds obvious and it is skipped constantly, because "frames are lost" is symmetric and the causes are not:
| Reading | Direction | What it means |
|---|---|---|
c_frames_sent below what the client submitted | transmit | frames are not leaving; look at back-pressure and aborts |
c_frames_received below what the far end sent | receive | frames are arriving and not being stored |
c_frames_dropped_full advancing | receive | the buffer overflowed — a sizing or drain problem |
| both counts correct, client sees fewer | above the boundary | not this chapter's problem |
Step 2 — on transmit, read longest_stall before c_stall_cycles. Total stall says throughput was lost. The longest single stall says whether the upstream FIFO was deep enough, and a stall longer than the FIFO's depth means the client was blocked, which propagates upward and is felt as latency rather than loss.
Step 3 — on receive, read fifo_high_water as a trend, not a value. A mark at 80 percent today means nothing on its own. A mark that was 30 percent six months ago and is 80 now is a receive path that will start dropping, and the drops have not begun yet. This is the slope; c_frames_dropped_full is the cliff.
Step 4 — if drops correlate with traffic pattern rather than volume, suspect the burst. Section 11's rejected property names this exactly: a path sized from the average frame rate drops on back-to-back minimum-size frames, so the same aggregate throughput passes under one workload and fails under another. Measure the frame rate, not the octet rate.
Step 5 — if the PHY reports overruns and the MAC reports nothing, check the gap. Chapter 4.1 §14 established that the reporting side is not always the causing side, and this is the archetype: a MAC closing the gap leaves the PHY nowhere to compensate. Scenario 20 shows one way a correct-looking MAC does it by accident.
Step 6 — if frames are short by exactly one octet, go straight to the handshake. That signature is almost diagnostic on its own: a source advancing on valid rather than on valid && accept loses one octet per stall, from the middle of the frame, undetectably at the interface. It only happens under back-pressure, so it correlates with load and vanishes on the bench.
The method stated once: establish direction from the counters, then read the worst case rather than the total on transmit and the trend rather than the value on receive — and when the PHY reports a fault the MAC did not cause, check the gap before anything else.
14. Common Misconceptions
"The receive path is the transmit path reversed."
The wrong model: two directions, the same signals, mirrored.
What it costs: you look for a receive handshake and cannot find one, so you invent one — a ready signal that nothing can honour, whose existence hides an overflow. You size the receive path with transmit reasoning, and you cannot explain why receive needs a FIFO and transmit does not.
The corrected model: transmit is scheduled and receive is not. The MAC chooses when to transmit and can be told to wait; a frame arrives because a far-end station decided to send it and cannot be reached within the frame's duration. One direction gets a handshake because there is a producer to negotiate with; the other gets a buffer because there is not.
"Not being ready is the same problem in both directions."
The wrong model: a stall is a stall.
What it costs: transmit and receive stalls share a counter, so a slow link and a lossy one read identically. You respond to receive drops by improving throughput, which does not help, because throughput was never the constraint.
The corrected model: on transmit, not being ready costs latency; on receive, it costs a frame. No data is lost when a transmit path stalls — the producer waits. Every cycle a receive path is unready is a cycle in which data is arriving and cannot be stored. Different failures, different counters, different sizing arguments.
"Sizing the receive buffer from the average rate is sound."
The wrong model: if the drain rate exceeds the arrival rate, the buffer cannot fill.
What it costs: this is Section 11's rejected property, and it produces a receive path measured as comfortable that drops frames anyway — at a rate correlating with traffic pattern rather than volume, so it appears under one workload and vanishes under another with the same throughput.
The corrected model: the buffer sees frames, not rates. The worst case is back-to-back minimum-size frames, which maximises frame boundaries per second — and per-frame cost does not scale with frame size. Size for the worst-case burst in frames, and trend the high-water mark to find out whether the answer was right.
"rx_er means this octet is wrong."
The wrong model: an error signal alongside data qualifies that data.
What it costs: you discard the octet and keep the rest, producing a frame one octet short and structurally plausible. Its check value fails at the far end for a reason unrelated to why it was marked, and the investigation goes somewhere else entirely.
The corrected model: rx_er qualifies the frame. It is sticky for the frame's duration and applied at the end. That is why Section 6 accumulates it in suspect_q rather than acting on it, and why Section 7 keeps the receive rule and the transmit rule in separate logic.
"The preamble crosses the boundary."
The wrong model: the MAC transmits and receives complete frames including the preamble.
What it costs: your loopback test compares cycle counts and reports a mismatch on a perfectly correct design, because the transmit path emits preamble octets the receive path never reports. You build preamble parsing into the MAC, taking on a responsibility whose failure the MAC cannot detect.
The corrected model: the preamble is generated below the MAC and consumed below the MAC. The layer that generates it consumes it, which Chapter 4.1's rule predicts — bit synchronisation and byte alignment fail in the signal, and the MAC has no access to the signal. Compare octets, not cycles.
15. Interview Reasoning
"Why does the transmit path have flow control and the receive path a FIFO?"
The weak answer is convention. The answer that ends the topic is the scheduling asymmetry: transmit has a producer you control and can tell to wait, so back-pressure works and nothing is lost. Receive has a producer at the far end of a cable that committed to the frame's timing before the first bit left it and is not listening — there is nothing to negotiate with, so the only options are absorb or drop. Adding that pause-based flow control exists but operates between frames and cannot stop one in flight shows the distinction is understood rather than recited.
"A MAC loses one octet from the middle of frames, only under load. What is it?"
Almost certainly a source advancing on valid rather than on valid && accept. Under back-pressure the source presents a new octet while the previous one is unaccepted; the interface takes the new one and the old is lost. It is invisible at the interface and appears at the far end as a check-value failure blamed on the link — and it only manifests when the interface actually stalls, which a bench test with a fast PHY model never does.
"How would you size a receive buffer?"
Not from the average rate. From the worst-case burst in frames — back-to-back minimum-size frames, because per-frame cost does not scale with frame size and frame boundaries are what the receive path pays for. The strong follow-up is that this sizing must be added to the clock-difference sizing, which is a completely separate argument with a different cause, and confusing the two produces a buffer that is correct for one and short for the other.
16. Understanding Check
Because transmit is scheduled and receive is not, and every other difference follows.
The MAC decides when to transmit. If the interface is not ready, the MAC waits — legal, expected, and the reason the handshake exists.
A frame arrives because a far-end station decided to send it. That station committed to the frame's timing before the first bit left it, is thousands of nanoseconds away, and is not listening. There is no receive handshake because there is nothing to negotiate with.
| Transmit | Receive | |
|---|---|---|
| chooses the timing | the MAC | the far end |
| consumer can say "wait"? | yes | no |
| absorbs a mismatch with | back-pressure | a buffer, then a drop |
| not being ready costs | latency | a frame |
The last row is the one that shapes designs. No data is lost when a transmit path stalls. Every cycle a receive path is unready is a cycle in which data is arriving and cannot be stored.
The follow-up to be ready for: what about pause flow control? It exists, and it operates between frames at the MAC layer. It cannot stop a frame already in flight, which is exactly why the in-flight case still needs a buffer.
17. What's Next
The claim this chapter defended: transmit is scheduled and receive is not, and every structural difference between the two directions follows from that.
A transmit path has a producer it controls, so it gets a handshake and stalling costs latency. A receive path has a producer at the far end of a cable that cannot be reached within a frame's duration, so it gets a buffer and being unready costs a frame. One direction's error is a decision to be emitted; the other's is a report to be believed and applied to the whole frame. And the preamble is generated and consumed below the MAC, so the two directions do not even have the same cycle count for the same frame.
None of that is visible in a static description. Chapter 4.1's contract and Chapter 4.2's vocabulary are both symmetric; only time separates the directions.
Two sizing arguments were left open, and they are different. Section 11 sized the receive buffer against a burst — back-to-back minimum-size frames, a property of traffic. That is not the only reason a buffer must be deep.
Chapter 4.4 — Elastic Buffering and Clock Compensation takes the other one. The two ends of a link run from independent oscillators, so even with no burst at all, one side produces octets slightly faster than the other consumes them, forever. Chapter 2.6 named the mechanism that absorbs it and this chapter kept referring to the gap as the place it happens. 4.4 owns the arithmetic: why the difference is bounded, how the bound is derived from a clock tolerance in parts per million, and how buffer depth follows from it — and why that depth must be added to this chapter's burst depth rather than confused with it.
The full path is on the Ethernet curriculum index.
Continue learning
Related tutorials
- Related topic
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.
- 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
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.
