Ethernet · Module 1
Full Duplex and What It Removed from the MAC
Full duplex makes five of the half-duplex MAC's six blocks unreachable and collapses the transmit state machine from five states to two. It also removes a constraint that had been throttling senders by accident, which is why link-level flow control had to be invented to replace it.
Chapter 1.4 ended with a link that has one station at each end, a private medium, and separate pairs for each direction. Every precondition Chapter 1.2's machinery was built for is now false: there is no shared medium, no second transmitter, and no round trip to cover.
Chapter 1.1 §15 and Chapter 1.2 §20 each noted in passing that this retires most of the access method. Noting it is not the same as doing it.
Exactly which logic becomes unreachable, what survives and why, and what breaks when the machinery is gone?
The third part is the one that matters. Removing a mechanism that was solving a problem is straightforward. Removing one that was also solving a second problem nobody had noticed is how a system acquires a new failure mode, and that is what happened here.
1. What Full Duplex Actually Requires
Three conditions, all of them satisfied by the switched twisted-pair link Chapter 1.4 arrived at, and all three necessary.
Separate physical paths per direction. Transmit and receive must not share a conductor, or simultaneous operation is physically impossible. BASE-T's separate pairs provide this, and coax did not.
Exactly one station at each end. With a third station attached, its transmissions arrive on the receive path and are indistinguishable from the far end's. There would be contention again, on a medium with no access method left to arbitrate it.
Both ends agreeing. This is the one that fails in the field. Full duplex is a mutual configuration: a station that transmits without deferring while its peer still defers and detects collisions produces exactly the fault Chapter 1.2 §6 described as a late collision. Chapter 11.4 owns the diagnosis; what matters here is that the mode is a property of the link, not of a station.
2. The Deletion, Stated Exactly
Chapter 1.2 built six modules. Here is what happens to each, and why — the reason matters more than the outcome, because two of the six survive for reasons that are easy to get wrong.
| Block from Chapter 1.2 | Fate | Why |
|---|---|---|
slot_time_counter | unreachable | it bounds medium acquisition; nothing else can be acquiring the medium |
collision_classifier | unreachable | there are no collisions to classify into early and late |
jam_generator | unreachable | nothing to make unambiguous to stations that do not exist |
backoff_engine | unreachable | nothing to separate in time |
csma_cd_tx_mac | collapses | five states become two; the integration logic has nothing left to integrate |
collision_domain_budget | vacuous | the domain is one short cable with one station; the check never binds |
And what survives, from the wider MAC:
| Mechanism | Survives because |
|---|---|
| Interframe gap | it exists for receiver recovery, not for contention — and it is 96 bits at every rate including 10 Gb/s, which has no half-duplex mode at all |
| Minimum frame size | the timing reason is gone; the format rule is not, and every receiver still validates against it |
| Framing, addressing, FCS | none of them had anything to do with the medium being shared |
The interframe gap row is the one that surprises people. It looks like part of the access method — it is a gap between transmissions on a medium — and it is not. It gives the receiver time to recover its state between frames, which is needed whether or not anything else could have transmitted in that gap. Its survival at 10 Gb/s, a rate with no half-duplex mode, is the proof.
3. RTL 1 — The Half-Duplex MAC, With the Dead Logic Marked
The most direct way to show a deletion is to take Chapter 1.2's state machine and mark what is now unreachable.
// ILLUSTRATIVE. Chapter 1.2's transmit controller with a mode input, so the
// unreachable arms can be marked. NOT a design — a real dual-mode MAC does
// not look like this.
module tx_ctrl_dual_mode (
input logic clk,
input logic rst_n,
input logic full_duplex, // link mode, from configuration or negotiation
input logic tx_req,
input logic tx_frame_done,
input logic carrier_sense,
input logic collision_detect,
input logic ifg_elapsed, // survives in BOTH modes — see Section 2
input logic retry_grant,
input logic retry_abandon,
output logic tx_enable,
output logic jam_enable,
output logic retry_req
);
typedef enum logic [2:0] {
S_IDLE = 3'd0,
S_IFG = 3'd1, // SURVIVES: receiver recovery, not contention
S_DEFER = 3'd2, // DEAD in full duplex: nothing else can be sending
S_TRANSMIT = 3'd3,
S_JAM = 3'd4, // DEAD: no collision to make unambiguous
S_RETRY = 3'd5 // DEAD: nothing to separate in time
} tx_state_e;
tx_state_e state_q, state_d;
always_comb begin
state_d = state_q;
case (state_q)
S_IDLE:
if (tx_req) begin
// THE ONE DECISION THAT CHANGES. In half duplex the medium must be
// observed first. In full duplex there is nothing to observe: no
// other station can be driving this path, so carrier_sense carries
// no information about whether transmission is permitted.
if (full_duplex) state_d = S_IFG;
else if (carrier_sense) state_d = S_DEFER;
else state_d = S_IFG;
end
// Survives in both modes. Note it is entered before EVERY frame, not
// only after a collision — the receiver needs the gap regardless of
// why the previous transmission ended.
S_IFG:
if (ifg_elapsed) state_d = S_TRANSMIT;
// UNREACHABLE when full_duplex: S_IDLE never routes here.
S_DEFER:
if (!tx_req) state_d = S_IDLE;
else if (!carrier_sense) state_d = S_IFG;
S_TRANSMIT:
// UNREACHABLE ARM when full_duplex. Not because the input is tied
// off — collision_detect may still be driven by a PHY that has not
// been told the mode — but because a collision on a full-duplex link
// is a FAULT rather than an event to recover from. Section 11 shows
// why leaving this arm live is the bug, not the safety measure it
// looks like.
if (collision_detect && !full_duplex) state_d = S_JAM;
else if (tx_frame_done) state_d = S_IDLE;
S_JAM: state_d = S_RETRY; // UNREACHABLE in FD
S_RETRY: if (retry_abandon) state_d = S_IDLE; // UNREACHABLE in FD
else if (retry_grant) state_d = S_DEFER;
default: state_d = S_IDLE;
endcase
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) state_q <= S_IDLE;
else state_q <= state_d;
end
assign tx_enable = (state_q == S_TRANSMIT);
assign jam_enable = (state_q == S_JAM);
assign retry_req = (state_q == S_JAM);
`ifdef FORMAL
// The deletion, as a checkable property rather than a comment. If any of
// these fails under full_duplex, some path still reaches contention logic
// — which is exactly the defect a mode-gated design introduces silently.
a_fd_no_defer : assert property (@(posedge clk) disable iff (!rst_n)
full_duplex |-> (state_q != S_DEFER));
a_fd_no_jam : assert property (@(posedge clk) disable iff (!rst_n)
full_duplex |-> (state_q != S_JAM));
a_fd_no_retry : assert property (@(posedge clk) disable iff (!rst_n)
full_duplex |-> (state_q != S_RETRY));
`endif
endmoduleClassification: illustrative; synthesizable but deliberately not a design.
What it teaches: that the deletion is exact. Three of six states become unreachable under one input condition, and the reachability is provable rather than argued — the three assertions at the bottom are the deletion stated in a form a tool can check.
The subtle line is the S_TRANSMIT arm. Leaving collision_detect live under full duplex looks conservative: if a collision somehow happens, handle it. It is the opposite of conservative. A collision on a full-duplex link means the mode is misconfigured, and jamming and backing off makes that worse — the station stops transmitting, retries after a random delay, and the fault presents as unexplained throughput loss rather than as the configuration error it is. Section 11 develops this; the design rule is that a full-duplex MAC must report a collision, never recover from one.
Deliberately simplified: no interframe-gap counter, only its ifg_elapsed result; no carrier extension; no receive path; mode is a static input rather than negotiated.
Production implication: a real dual-mode MAC gates the contention logic from a mode register written by auto-negotiation, holds the mode stable for the life of the link, reports any collision seen in full-duplex mode as a distinct error rather than folding it into a collision counter, and documents which of the two mode meanings its counters use — because a "collisions" counter that can only increment in one mode is a different measurement in each.
4. RTL 2 — The Full-Duplex Transmit MAC
With the dead arms removed, what is left is small enough to read in one screen. Its smallness is the chapter's first result.
// SYNTHESIZABLE. Full-duplex transmit control: send when there is something
// to send, after the interframe gap. That is the whole access method.
//
// NOT an 802.3 MAC. No framing, padding, FCS or addressing.
module fd_tx_mac #(
// NORMATIVE (IEEE 802.3 Clause 4 parameter table, quoted in Chapter 1.2):
// interFrameGap is 96 bits at every rate, including 10 Gb/s.
parameter int unsigned IFG_BITS = 96
) (
input logic clk,
input logic rst_n,
input logic bit_tick,
input logic tx_req,
input logic tx_frame_done,
output logic tx_enable,
output logic ifg_active,
// Present and DELIBERATELY not used to gate transmission. See below.
input logic carrier_sense,
input logic collision_detect,
output logic mode_violation // either input asserted: a FAULT report
);
localparam int unsigned IFG_W = $clog2(IFG_BITS + 1);
typedef enum logic [1:0] {
F_IDLE = 2'd0,
F_IFG = 2'd1,
F_TRANSMIT = 2'd2
} f_state_e;
f_state_e state_q, state_d;
logic [IFG_W-1:0] ifg_q;
wire ifg_done = (ifg_q == IFG_W'(IFG_BITS - 1)) && bit_tick;
always_comb begin
state_d = state_q;
case (state_q)
// No carrier_sense term. This is the entire difference from Chapter
// 1.2's controller, and it is one absent condition rather than any
// added logic.
F_IDLE: if (tx_req) state_d = F_IFG;
// The gap is served whether or not another frame is waiting. Returning
// to F_IDLE when there is no request is not optional: a machine that
// went straight back to F_TRANSMIT would transmit forever after the
// first frame, with nothing to send.
F_IFG: if (ifg_done) state_d = tx_req ? F_TRANSMIT : F_IDLE;
F_TRANSMIT: if (tx_frame_done) state_d = F_IFG; // gap before the next
default: state_d = F_IDLE;
endcase
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state_q <= F_IDLE; ifg_q <= '0;
end else begin
state_q <= state_d;
// Armed on ENTRY to F_IFG and run from state alone, for the same
// reason Chapter 1.2's jam counter is: a duration that must be
// guaranteed cannot be derived from a signal that may change.
if (state_d == F_IFG && state_q != F_IFG) ifg_q <= '0;
else if (state_q == F_IFG && bit_tick) ifg_q <= ifg_q + 1'b1;
end
end
assign tx_enable = (state_q == F_TRANSMIT);
assign ifg_active = (state_q == F_IFG);
// The medium inputs are OBSERVED and REPORTED, never acted on. On a
// correctly configured full-duplex link neither can assert from a peer's
// traffic: carrier_sense reflects only this station's own transmission,
// and collision_detect should never assert at all. Either one asserting
// while this station is not transmitting means the link is not what the
// configuration says it is.
assign mode_violation = collision_detect || (carrier_sense && !tx_enable);
endmoduleClassification: synthesizable.
What it teaches: that the full-duplex access method is the absence of a condition. Chapter 1.2's controller and this one differ by one term: F_IDLE does not consult carrier_sense. Everything else that disappeared — the deferral state, the jam, the backoff, the slot counter — followed from that single removal.
mode_violation is the design decision worth defending. The medium inputs are still wired in, and a reader will ask why, if they are never acted on. Because they are evidence. On a correctly configured link neither can assert from a peer, so either one asserting is a statement that the link is not full duplex — the single most useful diagnostic on a modern port, and it is free. Removing the inputs entirely would make the most common Ethernet misconfiguration invisible to the MAC that is suffering from it.
Why carrier_sense && !tx_enable rather than carrier_sense. A transmitting station sees its own carrier — Chapter 1.1's trap, and it applies here too. Reporting a violation on every frame this station sends would make the signal useless within a day.
Deliberately simplified: the interframe gap starts after tx_frame_done with no account of what the standard measures from; no burst mode; no receive interaction; bit_tick abstracts the rate.
Production implication: a real MAC derives the gap from the same bit-time source as everything else, defines precisely which frame boundary it measures from, latches mode_violation into a status register with a count rather than exposing a pulse, and connects it to whatever raises an alarm — because a violation nobody reads is a violation that was not detected.
5. RTL 3 — Independent Directions, and the Assertion That They Are
Full duplex claims the two directions do not interact. That is a design property, not a given: they may share a clock, a reset, a configuration register, or a buffer pool. It is worth building the claim and then checking it.
// SYNTHESIZABLE. Two directions that share a clock and a reset and nothing
// else, plus the properties that say so.
//
// NOT a MAC. The point is the boundary between the directions, not what
// either of them does.
module fd_duplex_pair #(
parameter int unsigned WIDTH = 8
) (
input logic clk,
input logic rst_n,
// Transmit direction.
input logic tx_req,
input logic [WIDTH-1:0] tx_data,
output logic tx_enable,
output logic [WIDTH-1:0] tx_wire,
// Receive direction.
input logic rx_wire_valid,
input logic [WIDTH-1:0] rx_wire,
output logic rx_valid,
output logic [WIDTH-1:0] rx_data,
// Shared status. THE ONLY deliberate coupling in the module, and the one
// place an independence bug is most likely to be introduced.
output logic link_active
);
logic tx_busy_q;
logic [WIDTH-1:0] tx_q;
logic rx_busy_q;
logic [WIDTH-1:0] rx_q;
// Transmit path. Note what it does NOT read: any rx_ signal.
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
tx_busy_q <= 1'b0; tx_q <= '0;
end else begin
tx_busy_q <= tx_req;
if (tx_req) tx_q <= tx_data;
end
end
// Receive path. Note what it does NOT read: any tx_ signal.
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
rx_busy_q <= 1'b0; rx_q <= '0;
end else begin
rx_busy_q <= rx_wire_valid;
if (rx_wire_valid) rx_q <= rx_wire;
end
end
assign tx_enable = tx_busy_q;
assign tx_wire = tx_q;
assign rx_valid = rx_busy_q;
assign rx_data = rx_q;
// Shared, and it must be an OR rather than anything that could let one
// direction mask the other. A design that computed this as, say,
// `tx_busy_q && rx_busy_q` would report the link inactive whenever traffic
// was one-directional — which is most of the time.
assign link_active = tx_busy_q || rx_busy_q;
`ifdef FORMAL
// INDEPENDENCE, as three properties. Each catches a different way the
// directions get accidentally coupled during integration.
// Transmit output depends only on transmit inputs.
a_tx_independent : assert property (@(posedge clk) disable iff (!rst_n)
$changed(tx_enable) |-> $past($changed(tx_req)));
// Receive output depends only on receive inputs.
a_rx_independent : assert property (@(posedge clk) disable iff (!rst_n)
$changed(rx_valid) |-> $past($changed(rx_wire_valid)));
// And the case the first two do not cover: simultaneous activity in both
// directions, which is the entire point of full duplex and the condition
// under which shared-resource bugs actually appear.
c_simultaneous : cover property (@(posedge clk) disable iff (!rst_n)
tx_enable && rx_valid);
`endif
endmoduleClassification: synthesizable.
What it teaches: that "the directions are independent" is a claim with a shape — each direction's outputs are a function of that direction's inputs alone — and that the shape is checkable. It also shows where independence actually breaks in practice: not in the datapaths, which are obviously separate, but in the shared status, configuration and buffer resources that get added later for good reasons.
The cover is as important as the two asserts. The assertions can pass on a bench that never runs both directions at once, and a bench that never runs both directions at once has not tested full duplex at all. A cover that never hits is a bench that proved nothing about the feature under test.
Deliberately simplified: one beat of pipelining per direction; no flow control in either; link_active stands in for a whole status block; no clock-domain separation, which a real design may or may not have.
Production implication: verify each direction alone first, so a failure is unambiguous, then verify both concurrently, because independence is a claim that itself needs testing. Shared reset, shared configuration registers, shared statistics and shared buffer pools are the four places coupling is introduced, and each needs its own scenario.
6. What Full Duplex Broke
Here is the chapter's real subject, and it is easy to miss because the mechanism that disappeared was never documented as doing this job.
On a shared medium, a sender could not transmit while the medium was busy. That is Chapter 1.1's deferral rule, and its purpose was to avoid collisions. But it had a second effect nobody designed: a receiver that was struggling to keep up was part of a busy medium, and a busy medium held every sender back. Congestion at a receiver became contention on the wire, and contention throttled the senders.
The protection was crude, indirect and entirely accidental. It was also real.
A full-duplex link has none of it. The sender's path is private and always available. It transmits at line rate for as long as it has frames, and nothing in the medium, the MAC, or the access method has any opinion about whether the receiver can keep up.
7. RTL 4 — The Receive FIFO That Now Overflows
The abstract claim becomes concrete in one block: a receive buffer with a sender that cannot be slowed down.
// SYNTHESIZABLE. A receive FIFO on a full-duplex link, with nothing able to
// throttle the sender. Written to expose the overflow, not to prevent it.
//
// NOT a MAC FIFO: no CDC, no frame boundaries, no byte enables.
module fd_rx_fifo #(
parameter int unsigned DEPTH = 16,
parameter int unsigned WIDTH = 8,
localparam int unsigned PTR_W = $clog2(DEPTH),
localparam int unsigned CNT_W = $clog2(DEPTH + 1)
) (
input logic clk,
input logic rst_n,
// From the wire. THERE IS NO READY. The sender is on the other end of a
// private link and cannot be told to stop — which is precisely the
// situation this chapter created.
input logic wire_valid,
input logic [WIDTH-1:0] wire_data,
// To the client, which may stall for its own reasons.
output logic cli_valid,
output logic [WIDTH-1:0] cli_data,
input logic cli_ready,
output logic [CNT_W-1:0] occupancy,
output logic [CNT_W-1:0] high_water,
output logic overflow, // pulse: a beat was lost
output logic [15:0] overflow_cnt
);
logic [WIDTH-1:0] mem_q [DEPTH];
logic [PTR_W-1:0] wr_q, rd_q;
logic [CNT_W-1:0] cnt_q, hw_q;
logic [15:0] ovf_q;
wire full = (cnt_q == CNT_W'(DEPTH));
wire empty = (cnt_q == '0);
wire do_wr = wire_valid && !full;
wire do_rd = cli_valid && cli_ready;
// THE LINE THIS CHAPTER EXISTS FOR. On a shared medium this could not
// happen: a receiver in trouble was part of a busy medium, and a busy
// medium stopped the sender. Here the sender is on a private path, has no
// idea, and keeps going. The beat is simply lost.
assign overflow = wire_valid && full;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
wr_q <= '0; rd_q <= '0; cnt_q <= '0; hw_q <= '0; ovf_q <= '0;
end else begin
if (do_wr) begin
mem_q[wr_q] <= wire_data;
wr_q <= (wr_q == PTR_W'(DEPTH - 1)) ? '0 : wr_q + 1'b1;
end
if (do_rd)
rd_q <= (rd_q == PTR_W'(DEPTH - 1)) ? '0 : rd_q + 1'b1;
case ({do_wr, do_rd})
2'b10: cnt_q <= cnt_q + 1'b1;
2'b01: cnt_q <= cnt_q - 1'b1;
default: cnt_q <= cnt_q;
endcase
if (cnt_q > hw_q) hw_q <= cnt_q;
// Saturating: a wrapped overflow counter reads as a healthy small
// number after a long run, which is the worst failure a diagnostic
// counter can have.
if (overflow && ovf_q != 16'hFFFF) ovf_q <= ovf_q + 1'b1;
end
end
assign cli_valid = !empty;
assign cli_data = mem_q[rd_q];
assign occupancy = cnt_q;
assign high_water = hw_q;
assign overflow_cnt = ovf_q;
endmoduleClassification: synthesizable.
What it teaches: that the absence of a ready on the wire side is not a modelling shortcut — it is the physical situation. The far end of a full-duplex link is a station with its own transmit MAC and no channel through which this receiver can express difficulty. The overflow is not a bug in this FIFO; it is the consequence of a link that removed the only throttling mechanism that existed.
Why the counter is separate from the occupancy. Occupancy says how full the buffer is now. The overflow count says how many beats were lost. A design can have healthy-looking occupancy — because it drains fast between bursts — and a rising overflow count, and reading either alone gives the wrong answer.
Deliberately simplified: beat-granular, so a "loss" is one beat rather than a frame; a real MAC must drop the whole frame or emit a partial one, and emitting a partial one is worse; no CDC; no frame boundary awareness.
Production implication: a real receive FIFO drops at frame granularity, because a partially-written frame in the buffer is a frame the client will read as complete and wrong; it separates overflow from other discard reasons for Chapter 21.2's taxonomy; and it exposes occupancy to whatever generates backpressure — which is Section 8.
8. RTL 5 — The Watermark, Which Is Where Flow Control Begins
Overflow cannot be prevented by a bigger buffer alone; a persistent rate mismatch fills any finite depth. It has to be prevented by acting before the buffer is full, and acting requires a signal.
// SYNTHESIZABLE. Turns buffer occupancy into an assert/deassert request,
// with hysteresis and a round-trip allowance.
//
// NOT 802.3x PAUSE: no frame, no quanta, no addressing, no timer. Module 14
// owns those. This is the condition that triggers them.
module fd_backpressure_watermark #(
parameter int unsigned DEPTH = 16,
// Assert with room still left for the frames already in flight toward us.
// The gap between HIGH and DEPTH is the round-trip allowance, and sizing
// it is Module 14's subject.
parameter int unsigned HIGH_MARK = 11,
parameter int unsigned LOW_MARK = 5,
localparam int unsigned CNT_W = $clog2(DEPTH + 1)
) (
input logic clk,
input logic rst_n,
input logic [CNT_W-1:0] occupancy,
output logic pause_request, // ask the far end to stop
output logic resume_request, // one pulse when it may continue
output logic [15:0] assert_cnt
);
logic paused_q;
logic [15:0] cnt_q;
// HYSTERESIS, and it is not optional. A single threshold at HIGH would
// assert and deassert on every beat while occupancy sat at the boundary,
// producing a storm of control traffic that consumes the very capacity it
// is trying to protect. Two thresholds turn a boundary into a band.
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
paused_q <= 1'b0; cnt_q <= '0;
end else begin
if (!paused_q && occupancy >= CNT_W'(HIGH_MARK)) begin
paused_q <= 1'b1;
if (cnt_q != 16'hFFFF) cnt_q <= cnt_q + 1'b1;
end else if (paused_q && occupancy <= CNT_W'(LOW_MARK)) begin
paused_q <= 1'b0;
end
end
end
assign pause_request = paused_q;
assign resume_request = !paused_q && $past(paused_q);
assign assert_cnt = cnt_q;
// Elaboration-time sanity. A LOW at or above HIGH is not hysteresis, it is
// an oscillator, and it is an easy parameter mistake to make.
if (LOW_MARK >= HIGH_MARK) begin : g_bad_marks
$error("LOW_MARK (%0d) must be below HIGH_MARK (%0d) or the watermark oscillates.",
LOW_MARK, HIGH_MARK);
end
if (HIGH_MARK >= DEPTH) begin : g_no_headroom
$error("HIGH_MARK (%0d) leaves no headroom below DEPTH (%0d) for frames already in flight.",
HIGH_MARK, DEPTH);
end
endmoduleClassification: synthesizable, with elaboration-time parameter checks.
What it teaches: the two properties that make backpressure work at all, both of which are easy to omit and expensive to omit.
Hysteresis. One threshold oscillates. Occupancy sitting at the mark asserts and deasserts continuously, and each transition costs a control message on a link whose capacity is already the problem. Two thresholds convert a point into a band, and the band's width is a design parameter.
Headroom. HIGH_MARK must be below DEPTH by enough to absorb everything already in flight when the request is sent. The request takes a propagation delay to arrive, the far end takes time to react, and frames launched in that window are still coming. A watermark at the top of the buffer signals too late to help — the elaboration check exists because this is a parameter mistake rather than a logic one, and parameter mistakes pass every functional test.
Deliberately simplified: no round-trip calculation, so the headroom is a parameter rather than derived; no per-priority marks, which Module 14's priority flow control needs; no timer, so a pause never expires.
Production implication: derive HIGH_MARK from the link's round trip and the peer's reaction time rather than choosing it; implement per-priority watermarks if the link carries priorities; and give the pause a timeout, because a peer that misses a resume must not stop forever — a deadlock Module 14 discusses as a real risk of lossless configurations.
9. RTL 6 — The Mode Register, and Why It Must Not Move
Everything above depends on one input, full_duplex, and treats it as stable.
It is not stable by nature: it is the result of a negotiation that can rerun,
a register software can write, and a value that must be identical at both ends
of the link. Getting its lifetime wrong produces a fault class of its own.
// SYNTHESIZABLE. Holds the link mode stable for the lifetime of a frame and
// reports every attempt to change it at a moment when it cannot be honoured.
//
// NOT auto-negotiation. Chapter 11.2 owns the protocol; this owns the result.
module link_mode_register (
input logic clk,
input logic rst_n,
// Proposed mode, from negotiation or from a software write. May change at
// any time, including in the middle of a frame.
input logic mode_req_valid,
input logic mode_req_full_duplex,
// The MAC's activity, so a change can be deferred to a safe point.
input logic tx_active,
input logic rx_active,
output logic full_duplex, // the stable value the MAC consumes
output logic mode_change_pending,
output logic mode_change_applied,
output logic [7:0] deferred_cnt // changes that had to wait
);
logic mode_q;
logic pend_q, pend_val_q;
logic [7:0] def_q;
// A frame boundary is the only safe point. Changing mode mid-frame would
// mean the first half of a transmission followed one set of rules and the
// second half another — and on the receive side it would reclassify a
// collision that is already in progress.
wire quiescent = !tx_active && !rx_active;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
mode_q <= 1'b0; // half duplex is the safe default: it
pend_q <= 1'b0; // defers and detects, so a wrong guess
pend_val_q <= 1'b0; // degrades throughput rather than
def_q <= '0; // silently destroying frames.
end else begin
if (mode_req_valid) begin
if (quiescent) begin
mode_q <= mode_req_full_duplex;
pend_q <= 1'b0;
end else begin
// Latch it and wait. Counting the deferrals matters: a mode that
// is repeatedly proposed and repeatedly deferred means negotiation
// is flapping, which is a real fault and is invisible if the
// request is simply dropped.
pend_q <= 1'b1;
pend_val_q <= mode_req_full_duplex;
if (def_q != 8'hFF) def_q <= def_q + 1'b1;
end
end else if (pend_q && quiescent) begin
mode_q <= pend_val_q;
pend_q <= 1'b0;
end
end
end
assign full_duplex = mode_q;
assign mode_change_pending = pend_q;
assign mode_change_applied = pend_q && quiescent;
assign deferred_cnt = def_q;
endmoduleClassification: synthesizable.
What it teaches: that a configuration input consumed by a state machine has
a lifetime requirement, and that the requirement is usually undocumented
until something breaks. full_duplex is not a signal the MAC samples whenever
it likes; it is a value that must hold for at least the duration of a frame,
because the frame's first half and second half must obey the same rules.
Why half duplex is the reset default. A station that wrongly believes it is half duplex defers and detects collisions unnecessarily: it is slower than it needs to be, and it is correct. A station that wrongly believes it is full duplex transmits without deferring into a peer that is still arbitrating — which is Section 11's duplex mismatch, destroying frames at the far end. The two wrong guesses are not symmetric, so the default is not arbitrary.
Why the deferral counter earns its byte. A mode change that arrives during traffic is normal and is simply applied at the next boundary. A mode change that arrives repeatedly and is deferred repeatedly means the link is renegotiating under load — a flapping link, which presents as intermittent throughput loss with no error counter moving. Dropping the request silently makes that invisible.
Deliberately simplified: one pending request, so a second overwrites the
first; quiescent is a crude activity check with no interframe-gap awareness;
no distinction between a negotiated change and a software write, which a real
design must have because their priorities differ.
Production implication: a real design holds the negotiated result in a register that software can read but not silently override, applies changes only at a frame boundary with an explicit quiescence definition, reports the applied mode separately from the requested one so a mismatch between them is visible, and counts renegotiations — because a link that renegotiates during operation is a fault regardless of what it settles on.
10. Waveform — The Overflow That Half Duplex Prevented
The same offered load, on both link types, against a receiver whose client stalls.
A stalled client, on a shared medium and on a private link
10 cycleshd_tx stops at cycle 1 and nothing is lost. The shared medium is busy — a state the receiver's own difficulty contributed to — and the sender defers. Crude, indirect, and it worked.
fd_tx never stops. The sender's path is private and permanently available. It has no information about the receiver's state and no mechanism to receive any, so it transmits at line rate throughout.
fd_occ saturates at cycle 4 and fd_lost asserts from cycle 5. The buffer did its job for four cycles and then had nowhere to put anything. The data is gone, and — worth noting — the sender does not know. It will find out only if something above the MAC notices and complains.
watermark asserts at cycle 3, two cycles before the first loss. That gap is the entire value of Section 8's block: it is the time available to tell the far end to stop. Whether two cycles is enough depends on the round trip, which is exactly why HIGH_MARK cannot be a round number chosen by taste.
11. Collisions on a Full-Duplex Link
A collision cannot occur on a correctly configured full-duplex link. So a collision indication on one is information, and it is among the most valuable single readings in Ethernet debugging.
What it means. The link is not full duplex at both ends. Almost always this is a duplex mismatch: one end configured or negotiated to full duplex, the other to half.
Why the symptom is confusing. The two ends see completely different things.
- The half-duplex end defers, detects collisions, jams and backs off — all correctly, by its own rules. It reports collisions, and because the far end transmits whenever it likes, many of them arrive after slot time has elapsed, so it reports late collisions. Chapter 1.2 §6 shows why those are never retried and always diagnostic.
- The full-duplex end sees nothing wrong at all. It has no deferral, no collision logic engaged, and no counter that moves. Frames it sends are damaged at the far end, and it has no way to know.
The fault is therefore visible from one end only, which is why one-sided investigations so often conclude that the half-duplex station is broken.
What a full-duplex MAC should do about it. Report, never recover. Section 4's mode_violation output is the whole mechanism: a collision indication, or carrier from a peer while this station is not transmitting, latched and counted. Recovering — jamming and backing off — would be worse than useless, because it turns a configuration error into unexplained throughput loss and hides the evidence.
Chapter 11.4 owns the negotiation failures that produce the mismatch. What belongs here is the design rule, and it is short: a full-duplex MAC treats a collision as a fault report, not as an event to handle.
12. Assertions
Invariants of these models. Only the interframe gap value is normative.
// SVA over the modules in this chapter.
// SAFETY — P1..P3: the deletion is real. Under full duplex, no path reaches
// any contention state. Three separate properties rather than one, because
// each catches a different leftover path and a merged property reports the
// wrong one.
property p_fd_no_defer;
@(posedge clk) disable iff (!rst_n) full_duplex |-> (state_q != S_DEFER);
endproperty
a_fd_no_defer : assert property (p_fd_no_defer);
property p_fd_no_jam;
@(posedge clk) disable iff (!rst_n) full_duplex |-> (state_q != S_JAM);
endproperty
a_fd_no_jam : assert property (p_fd_no_jam);
property p_fd_no_retry;
@(posedge clk) disable iff (!rst_n) full_duplex |-> (state_q != S_RETRY);
endproperty
a_fd_no_retry : assert property (p_fd_no_retry);
// SAFETY — P4: the interframe gap SURVIVES. It is entered before every
// frame, not only after a collision. Catches an "optimisation" that removes
// the gap along with the contention logic, on the reasoning that both are
// about the medium.
property p_ifg_before_every_frame;
@(posedge clk) disable iff (!rst_n)
$rose(tx_enable) |-> $past(ifg_active);
endproperty
a_ifg_always : assert property (p_ifg_before_every_frame);
// SAFETY — P5: the gap runs its full normative length. Same discipline as
// Chapter 1.2's jam: a duration that must be guaranteed is run from state,
// not from an input that may change.
property p_ifg_full_length;
@(posedge clk) disable iff (!rst_n)
$rose(ifg_active) |-> ##[1:$] (ifg_q == IFG_BITS - 1);
endproperty
a_ifg_full : assert property (p_ifg_full_length);
// CAUSATION — P6: transmission does not depend on carrier. The absence that
// defines the mode, as a property. Catches a carrier term reintroduced by a
// well-meaning integrator who thought it was a safety measure.
property p_fd_ignores_carrier;
@(posedge clk) disable iff (!rst_n)
(state_q == F_IDLE && tx_req) |=> (state_q == F_IFG);
endproperty
a_fd_ignores_carrier : assert property (p_fd_ignores_carrier);
// SAFETY — P7: a collision indication is REPORTED and never acted on. The
// Section 11 design rule, made checkable.
property p_collision_reported_not_handled;
@(posedge clk) disable iff (!rst_n)
collision_detect |-> (mode_violation && $stable(state_q != F_TRANSMIT ? 1'b1 : 1'b1));
endproperty
a_collision_reported : assert property (p_collision_reported_not_handled);
// INDEPENDENCE — P8: neither direction's output changes without its own
// input changing. Catches shared-resource coupling introduced at integration.
property p_directions_independent;
@(posedge clk) disable iff (!rst_n)
$changed(tx_enable) |-> $past($changed(tx_req));
endproperty
a_directions_independent : assert property (p_directions_independent);
// CONSERVATION — P9: every wire beat is either stored or counted as lost.
// The property that makes the overflow honest; a silent drop breaks it.
property p_beat_accounted;
@(posedge clk) disable iff (!rst_n)
wire_valid |-> (do_wr ^ overflow);
endproperty
a_beat_accounted : assert property (p_beat_accounted);
// SAFETY — P10: the watermark has hysteresis. Catches the single-threshold
// parameterisation, which functionally "works" and produces a control-traffic
// storm at the boundary.
property p_watermark_hysteresis;
@(posedge clk) disable iff (!rst_n)
$rose(pause_request) |-> ##1 (pause_request throughout
(occupancy > LOW_MARK)[->1]);
endproperty
a_watermark_hysteresis : assert property (p_watermark_hysteresis);
// LIVENESS — P11: a pause is eventually released. ASSUMPTION, stated: the
// client eventually accepts. Without it a permanently stalled client is
// indistinguishable from a broken watermark.
assume property (@(posedge clk) s_eventually (cli_ready));
property p_pause_released;
@(posedge clk) disable iff (!rst_n)
pause_request |-> s_eventually (!pause_request);
endproperty
a_pause_released : assert property (p_pause_released);The property that must not be written
// FALSE for a correct design. Included as a warning, not as a check.
// property p_fd_never_sees_a_collision;
// @(posedge clk) disable iff (!rst_n)
// full_duplex |-> !collision_detect;
// endpropertyIt reads like the definition of full duplex, and it asserts something about the outside world rather than about the design.
A correctly configured full-duplex link produces no collisions. But collision_detect is an input, driven by a PHY attached to a link whose far end this design does not control. A duplex mismatch makes it assert, and that is not a defect in this MAC — it is the exact condition this MAC exists to report. The property fails on the most important scenario in Section 13's list.
The correct property is P7: a collision indication must be reported and must not change the transmit state machine's behaviour. That is a statement about the design, it is true of a correct one, and it catches the real bug — a mode-gated controller that quietly still handles collisions.
The failure mode of writing the wrong version is familiar. It fires during the first duplex-mismatch test, gets classified as an environment problem, and is disabled. The disabling then also removes attention from P7, which is the property that actually matters.
13. Verification
Monitors observe: the transmit state register and mode input; tx_enable, ifg_active and the gap counter; mode_violation and both medium inputs; every FIFO handshake with occupancy, high water and the overflow counter; and the watermark's request outputs with the occupancy that drove them.
The scoreboard independently predicts the transmit state sequence from the mode and the request, the FIFO's expected occupancy from the write and read events, and the watermark's expected state from occupancy and its own hysteresis model. It must implement the hysteresis itself rather than sampling paused_q — a checker reading the design's own flag agrees with it about every threshold bug.
Scenarios
- Full duplex, idle medium, transmit. Verify the direct path to the gap and then to transmit, with no deferral state entered.
- Full duplex, carrier asserted by the peer. Force
carrier_sensehigh and request a transmission. Verify transmission proceeds anyway (P6) andmode_violationis reported. The scenario that proves the mode. - Half duplex, same stimulus. Same inputs with
full_duplexlow. Verify deferral. The two together show the mode input is the only difference. - Full duplex, collision asserted. Verify
mode_violation, and — the important half — that the state machine does not enter jam or retry (P1..P3, P7). - Mode change between frames. Verify the controller uses one mode consistently for a whole frame and does not switch mid-transmission.
- Interframe gap, exact length. Verify the gap is the full normative count and that
tx_enablecannot rise a cycle early (P5). - Back-to-back frames. Verify a gap between every pair, not only after the first (P4).
- Both directions simultaneously. Traffic in both at once, checked in both. This is the feature under test; a bench that never does this has not tested full duplex.
- Each direction alone. Both, separately, so that when scenario 8 fails the cause is attributable to interaction rather than to either direction.
- Receive FIFO, client stalls, buffer fills. Verify occupancy rises, high water tracks it, and the first overflow is counted exactly once (P9).
- Receive FIFO, sustained overflow. Verify the counter saturates rather than wrapping, and that recovery is clean when the client resumes.
- Watermark, occupancy oscillating at the high mark. Verify hysteresis holds the request asserted (P10) rather than toggling every beat.
- Watermark, occupancy crossing the low mark. Verify exactly one resume pulse.
- Watermark parameters inverted. Elaborate with
LOW_MARKaboveHIGH_MARKand verify the elaboration error fires — a parameter mistake passes every functional test, so the build has to catch it. - Reset in each state, and mid-gap. Verify no stale gap count, no partial frame, and a clean first transmission afterwards.
Coverage
Cross the mode input against every transmit state, so the unreachability in P1..P3 is exercised rather than assumed. Cover occupancy at 0, LOW_MARK, LOW_MARK+1, HIGH_MARK-1, HIGH_MARK, DEPTH-1 and DEPTH. Cover simultaneous read and write at both FIFO boundaries. Cover the cover in Section 5 — simultaneous transmit and receive — and treat a miss as a bench failure rather than a coverage hole.
A directed stimulus for the mode boundary
// NON-SYNTHESIZABLE — directed stimulus. Drives identical inputs in both
// modes and checks that the transmit decision differs and the gap does not.
task automatic compare_modes_under_busy_carrier();
// Half duplex with a busy medium: must defer.
full_duplex <= 1'b0;
carrier_sense <= 1'b1;
tx_req <= 1'b1;
@(posedge clk); @(posedge clk);
assert (dut.state_q == dut.S_DEFER)
else $error("half duplex did not defer on a busy medium");
assert (!tx_enable)
else $error("half duplex transmitted into a busy medium");
// Reset between modes: the mode must not change mid-frame.
reset_dut();
// Full duplex, identical inputs: must NOT defer, and must report.
full_duplex <= 1'b1;
carrier_sense <= 1'b1;
tx_req <= 1'b1;
@(posedge clk); @(posedge clk);
assert (dut.state_q != dut.S_DEFER)
else $error("full duplex deferred — a carrier term survived the deletion");
assert (dut.mode_violation)
else $error("peer carrier in full duplex was not reported");
// And the gap is present in BOTH modes. The most likely wrong deletion is
// removing the interframe gap along with the contention logic.
wait (dut.ifg_active);
assert (1'b1); // reaching here at all is the check
endtaskThe last wait is the subtle one. Removing the deferral state is correct; removing the gap with it is not, and the two look similar enough in a diff that it happens. A task that only checked the deferral difference would pass a design that had deleted one mechanism too many.
14. What Replaced the Missing Backpressure
Section 6 identified the gap; the track fills it in three places, and it is worth naming them so the chapter's loose end is visibly tied.
Module 14 — flow control. The direct replacement. A receiver asks the sender to stop, explicitly, using the mechanism Section 8's watermark triggers. 802.3x PAUSE is the link-level form; priority flow control is the per-class refinement that lossless fabrics need.
Chapter 12.5 and Module 14 — switch buffering. A switch is the place where rate mismatch and output contention concentrate, so it is where buffering and backpressure decisions are actually made.
Module 17 — time-sensitive networking. For traffic that cannot tolerate either loss or unbounded delay, backpressure is not enough and the guarantee has to be scheduled rather than requested.
And one thing that did not replace it. Higher-layer protocols do respond to loss by slowing down, and that is a real feedback loop. It is also slow — it operates on round-trip timescales — and it acts only after data has been lost. Link-level flow control acts before, which is why both exist.
15. Common Misconceptions
"Full duplex doubles the bandwidth."
The wrong model: the link runs at twice the rate.
What it costs: provisioning arithmetic that is wrong by a factor of two in whichever direction the reader guessed. Reading a per-direction figure as a shared total halves every capacity number in this track; reading a shared total as per-direction doubles them.
The corrected model: each direction has its own path at its own rate, and they operate simultaneously. A single flow sees the link rate, not twice it. The aggregate across both directions can reach twice the link rate, and only if there is traffic in both. Every capacity figure in this track is stated per direction for exactly this reason.
"Full duplex just turns off collision detection."
The wrong model: one feature disabled, everything else unchanged.
What it costs: a MAC that leaves the contention logic reachable "just in case", which then recovers from a collision that indicates a duplex mismatch — turning a diagnosable configuration error into unexplained throughput loss with no counter pointing at the cause.
The corrected model: five of six blocks become unreachable and the transmit state machine loses three of its five states. What remains treats a collision indication as a fault report, never as an event to recover from. Section 3's three assertions state the deletion in a form a tool can check, which is the difference between disabling a feature and removing a mechanism.
"Full duplex is strictly better, so it has no cost."
The wrong model: a free improvement — more throughput, less machinery.
What it costs: the missing backpressure is not anticipated, so the first sustained rate mismatch produces silent frame loss that nobody designed for, and flow control is added reactively after a field problem.
The corrected model: a busy shared medium had been throttling senders as an undocumented side effect. Removing contention removed that too, and nothing replaced it — a private link's sender transmits at line rate with no information about the receiver's state. Module 14 exists to repair that, and Section 8's watermark is where the repair starts in hardware.
"A collision on a full-duplex link means the hardware is faulty."
The wrong model: collisions are impossible here, so one means something is broken in the silicon.
What it costs: the investigation goes to the PHY, the cable and the controller, while the actual cause is a configuration setting one register read away. It also wastes the single most diagnostic reading available on a modern port.
The corrected model: a collision indication means the link is not full duplex at both ends — nearly always a duplex mismatch. The half-duplex end reports collisions and late collisions; the full-duplex end reports nothing at all unless it was designed to, which is why Section 4's mode_violation exists. Check the duplex setting at both ends before anything else.
16. Interview Reasoning
Every mechanism whose reason for existing was that another station could drive the same medium — which is most of the access method.
The chain a strong answer walks:
- Carrier sense before transmitting, deferral, collision detection, jamming, backoff and the attempt limit all exist because a second transmitter could be on the medium. With one station at each end of a private path, none of them has anything to act on.
- Concretely, five of Chapter 1.2's six blocks become unreachable and the transmit state machine collapses from five states to two: wait for the interframe gap, then transmit.
- The interframe gap survives, and the reason is diagnostic of understanding: it exists for receiver recovery, not for contention. It is 96 bits at every rate including 10 Gb/s, which has no half-duplex mode at all.
- The minimum frame size also survives, but as a format rule whose timing justification is gone.
What separates a good answer from a complete one: naming what full duplex broke. A busy shared medium had been throttling senders by accident — a receiver in trouble made the medium busy, and a busy medium held every sender back. A private link removes that protection along with the contention, and nothing replaces it. That gap is why 802.3x flow control had to be invented.
The follow-up to be ready for: what should a full-duplex MAC do if it sees a collision? Report it and nothing else. A collision means the link is not full duplex at both ends — a duplex mismatch — and jamming and backing off would convert a diagnosable configuration error into unexplained throughput loss.
17. Understanding Check
18. What's Next
Full duplex is the end of Module 1's argument. A shared medium made access a distributed timing problem; slot time solved it; packet switching explained why the traffic was bursty enough to need sharing at all; switching dismantled the sharing; and full duplex deleted the machinery that sharing had required — leaving a MAC whose transmit access method is two states and a timer.
What is left is the interesting part. Framing, addressing, error detection and the interface to the physical layer were never about contention, so none of them was touched by any of this. They are what Ethernet actually is, and the rest of the track is about them.
Chapter 1.6 — Why Ethernet Won closes Module 1 by asking why this particular design outlasted the alternatives, and what its dominance locked in for the engineers who build silicon against it. Chapter 2.1 then lays out the complete system: client, MAC, reconciliation sublayer, PCS, PMA, PMD, and the contracts that join them.
The full path is on the Ethernet curriculum index.
Continue learning
Related tutorials
- Related topic
100 Mbps — Fast Ethernet and the Switch Transition
4B/5B and MLT-3 got ten times the data through about three times the spectrum — but the switch was the larger change, because a collision domain of one made full duplex possible and left CSMA/CD unreached.
- Related topic
Negotiation Failures and Duplex Mismatch
Seven ways a link comes up wrong and six of them report no error, because every device behaved correctly. Diagnosis is set narrowing over evidence, and three causes cannot be seen from one end at all.
- Related topic
Credits — Backpressure Across a Wire You Cannot Reach
An on-chip receiver says no with one wire. A receiver on the far side of a Link cannot, so PCIe replaces the ready signal with advertised capacity and local accounting — six pools, two units, and one rule about spending what you have not been given.
- Related topic
PCIe vs Ethernet — Where the Cost of Overload Lands
The same overload into two fabrics: one stalled the sender 59,405 times and lost nothing, the other discarded 59,405 frames. That single choice explains why one needs TCP and the other does not.
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.
