Ethernet · Module 10
MII — The Original Media Independent Interface
Sixteen signals, four bits each way, and both clocks sourced by the PHY. Its defining failure is a transmit clock that stops — and every assertion sampled by that clock is green while it does.
Chapter 4.3 established that the MAC and the PHY are separate blocks with a defined boundary between them. Module 10 is about that boundary as an object in its own right — the pins, the clocks, the delay each variant adds, and the specific way each one fails on a bench at two in the morning.
MII is the original, and it set the shape every later variant reacted against.
Four bits each direction, two clocks both sourced by the PHY, and a set of control signals that includes two — carrier sense and collision — for a mode that Chapter 9.2 showed most links stopped using within a few years of MII being specified.
And it has a defining failure mode that follows directly from where its clocks come from: TX_CLK stops. Not glitches — stops, when the PHY loses link or changes speed. Which makes it the interface where the most natural assertion a verification engineer would write is the one that can never fire.
1. Scope — What This Chapter Owns
This chapter owns MII as an interface: its pin list, its clocking, its ordering, its error signalling, and the delay it adds.
It does not re-derive Module 9's rate arithmetic. Chapter 9.2 owns Fast Ethernet and why 100 Mbps happened; Chapter 9.3 §11 owns why a wider interface was needed at gigabit. This chapter starts from 4 bits × 25 MHz as a given and asks what it costs in pins, in clock domains, in latency and in bring-up time.
It also does not own management: Chapter 4.5 owns MDC and MDIO, which ride alongside every interface in this module and are a protocol of their own.
Chapter 10.2 owns RMII, which halves this interface's pin count by changing where the clock comes from. Chapter 10.3 owns GMII.
The claim this chapter defends: an interface's defining failure mode is determined by which side sources its clocks — and when a clock can stop, every property sampled by it is vacuously true at exactly the moment the failure occurs.
2. Sixteen Signals
Count them, because the count is the comparison every later variant is making.
| Signal | Width | Direction | Purpose |
|---|---|---|---|
TXD[3:0] | 4 | MAC → PHY | transmit nibble |
TX_EN | 1 | MAC → PHY | this nibble is frame data |
TX_ER | 1 | MAC → PHY | corrupt this frame deliberately |
TX_CLK | 1 | PHY → MAC | the MAC clocks transmit data out with it |
RXD[3:0] | 4 | PHY → MAC | receive nibble |
RX_DV | 1 | PHY → MAC | this nibble is frame data |
RX_ER | 1 | PHY → MAC | an error, or — without RX_DV — an indication |
RX_CLK | 1 | PHY → MAC | receive timing |
CRS | 1 | PHY → MAC | carrier sense — half duplex |
COL | 1 | PHY → MAC | collision — half duplex |
| 16 | 12 of them PHY → MAC |
Plus MDC and MDIO for management, giving 18 pins at each end of every port.
Two observations, and both drive later chapters.
Twelve of the sixteen originate at the PHY, including both clocks — even though data flows both ways. A MAC transmitting on MII does not supply the timing for its own transmission; it clocks its data out with a clock the PHY handed it.
And two of the sixteen — CRS and COL — exist entirely for half duplex. Chapter 9.2 established that a full-duplex switch port never enters carrier sense or collision handling, and Chapter 9.4 that the mode was eventually removed from the standard. They are 12.5% of this interface's pins, and on a modern link they are wires that never assert.
3. Two Clocks, Both From the PHY
MII has two independent clock domains, and neither belongs to the MAC.
RX_CLK from the PHY is unsurprising — the receive data is recovered from the wire, so its timing comes from the wire, and the PHY is what recovered it. Any interface must work this way.
TX_CLK from the PHY is the interesting one. The MAC has transmit data to send and no clock of its own to send it with; the PHY supplies TX_CLK, and the MAC clocks TXD and TX_EN out on it.
The reason is that the PHY's transmit timing is not the MAC's business. At 100 Mbps the PHY's line rate is derived from its own reference; at 10 Mbps it is a different rate entirely. Sourcing TX_CLK from the PHY means the MAC never has to know which — it clocks data at whatever rate arrives, and the speed change is invisible to it.
Which is elegant and produces the failure this chapter is about.
| Both clocks are | Consequence |
|---|---|
| sourced by the PHY | the MAC has no timing of its own on this interface |
| independent of each other | transmit and receive are separate domains — every crossing is a CDC |
| free to stop | and they do, on link loss and on speed change |
| free to change frequency | 25 MHz ↔ 2.5 MHz, on a speed change |
The second row costs area. Transmit and receive are genuinely asynchronous, so a MAC has a clock-domain crossing on every control signal that has to be visible in both — and Chapter 4.4's buffering exists partly because of it.
The fourth row costs sanity. A speed change is not a smooth frequency ramp: the PHY stops TX_CLK, re-locks, and starts it again at the other rate. A MAC whose transmit logic lives in that domain is frozen for the duration, and the freeze is normal.
4. RTL 1 — Getting a Byte Onto Four Wires
// SYNTHESIZABLE.
//
// Presents octets from the MAC onto MII's four transmit wires.
//
// THE INTERFACE, by the numbers:
// TXD[3:0] 4 bits per TX_CLK
// 100 Mbps: TX_CLK = 25 MHz -> 4 x 25 = 100 Mb/s, 40 ns per nibble
// 10 Mbps: TX_CLK = 2.5 MHz -> 4 x 2.5 = 10 Mb/s, 400 ns per nibble
// one octet = 2 nibbles = 80 ns at 100 Mbps, 800 ns at 10 Mbps
//
// THE ORDERING RULE: the LOW nibble of each octet is transmitted FIRST.
// This follows from Ethernet's bit ordering -- bits go out
// least-significant first within an octet, and TXD[0] is the first bit
// of each nibble.
//
// EVERYTHING here runs on TX_CLK, which the PHY sources and which STOPS
// on a link loss or a speed change. That is not an error condition to
// handle in this module; it is the reason Section 7's observer lives in
// a different clock domain.
package mii_pkg;
localparam int unsigned NIBBLE_BITS = 4;
localparam int unsigned CLK_MHZ_100 = 25;
localparam int unsigned CLK_MHZ_10 = 3; // 2.5, rounded for reporting
// Clause 22: RX_ER asserted with RX_DV DEASSERTED and RXD = 4'hE is a
// False Carrier indication rather than a data error.
localparam logic [3:0] RXD_FALSE_CARRIER = 4'hE;
typedef enum logic [1:0] {
SPD_10,
SPD_100,
SPD_UNKNOWN
} mii_speed_e;
endpackage
module mii_tx_interface
import mii_pkg::*;
#(
parameter int unsigned CNT_W = 24
) (
input logic tx_clk, // FROM THE PHY
input logic rst_n,
input logic [7:0] octet,
input logic octet_valid,
input logic frame_last,
input logic force_error, // deliberately corrupt this frame
output logic octet_ready,
output logic [NIBBLE_BITS-1:0] txd,
output logic tx_en,
output logic tx_er,
output logic [CNT_W-1:0] c_octets,
output logic [CNT_W-1:0] c_frames,
// An octet was offered while the low nibble was still on the wire.
// Reported rather than dropped, because a dropped octet becomes a
// short frame and a short frame is diagnosed at the far end.
output logic overrun
);
logic phase_q; // 0 = low nibble, 1 = high nibble
logic [7:0] held_q;
logic busy_q;
// Ready only on the low-nibble phase: an octet cannot be accepted
// while its predecessor's high nibble is still owed to the wire.
assign octet_ready = !busy_q || !phase_q;
always_ff @(posedge tx_clk or negedge rst_n) begin
if (!rst_n) begin
phase_q <= 1'b0; held_q <= 8'd0; busy_q <= 1'b0;
txd <= 4'd0; tx_en <= 1'b0; tx_er <= 1'b0;
c_octets <= '0; c_frames <= '0; overrun <= 1'b0;
end else begin
overrun <= 1'b0;
tx_er <= 1'b0;
if (!busy_q) begin
if (octet_valid) begin
// LOW NIBBLE FIRST. This single line is the ordering rule,
// and reversing it produces frames whose every octet is
// nibble-swapped -- correctly framed, correctly timed, and
// wrong in a way the FCS catches and nothing explains.
txd <= octet[3:0];
tx_en <= 1'b1;
tx_er <= force_error;
held_q <= octet;
busy_q <= 1'b1;
phase_q <= 1'b1;
end else begin
txd <= 4'd0;
tx_en <= 1'b0;
end
end else begin
// HIGH NIBBLE. Unconditional: once the low nibble is on the
// wire the octet is committed, and a source that stalls here
// cannot be obeyed.
txd <= held_q[7:4];
tx_en <= 1'b1;
tx_er <= force_error;
busy_q <= 1'b0;
phase_q <= 1'b0;
if (!(&c_octets)) c_octets <= c_octets + 1'b1;
if (frame_last && !(&c_frames)) c_frames <= c_frames + 1'b1;
if (octet_valid) overrun <= 1'b1;
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that an octet is committed once its low nibble is on the wire, and the interface has no way to take it back. octet_ready is therefore false during the high-nibble phase, and a source that ignores it does not stall the interface — it loses an octet, which becomes a short frame diagnosed at the far end with no indication of where it was shortened.
Deliberately simplified: no preamble generation and no interframe gap enforcement, both of which sit above this block.
Production implication: everything in this module runs on tx_clk, which the PHY sources and which stops. That is not an error this module can handle — a stopped clock means this module simply does not run — and it is precisely why Section 7's observer has to live somewhere else. A design whose only transmit-side monitoring is in the tx_clk domain has instrumented the domain that disappears.
5. The Nibble Order, and Why It Is the First Thing to Check
Ethernet transmits bits least-significant first within an octet. MII carries four bits per clock with TXD[0] first, so the low nibble of each octet goes first and the high nibble second.
Get it backwards and the result is unusually hard to diagnose, for three reasons.
The frame is structurally perfect. Correct length, correct timing, TX_EN asserted for exactly the right number of clocks. Nothing at the interface is wrong; every octet's two halves have simply been exchanged.
The failure is total and uniform. Every octet is affected, so the destination address is wrong, the EtherType is wrong, and the FCS is wrong. The far end reports 100% FCS failures — which is the same symptom as a dead cable, a broken PHY, or half a dozen other things.
And it survives a loopback. A MAC looped back to itself through the same swap un-swaps — the error cancels — so an internal loopback test passes, and the problem appears only against a conforming partner.
| Symptom | Nibble swap | Dead cable | Bad PHY |
|---|---|---|---|
| far-end FCS failures | 100% | 100% | 100% |
TX_EN timing correct | yes | yes | maybe |
| internal loopback | passes | passes | fails |
| far end sees any traffic at all | yes | no | maybe |
The fourth row is the one that isolates it. A nibble swap produces traffic that arrives and is wrong; a dead cable produces no traffic. A far-end counter that shows frames received and all of them bad is the signature.
6. RTL 2 — Taking a Byte Off Four Wires
// SYNTHESIZABLE.
//
// Reassembles octets from MII's four receive wires.
//
// Runs on RX_CLK, which the PHY recovers from the wire and which is
// INDEPENDENT of TX_CLK. Every signal that has to be visible in both
// directions crosses a clock domain here.
//
// THE ODD-NIBBLE CASE, and it is the interesting one:
// RX_DV can deassert on an ODD nibble count -- leaving half an octet
// assembled. This is not a corner case; it is what a frame damaged on
// the wire looks like when the damage lands mid-octet, and it is
// called a dribble nibble.
//
// The correct response is to DISCARD the partial octet and REPORT it.
// Padding it to a byte manufactures data; silently dropping it makes
// a mangled frame look like a short one.
module mii_rx_interface
import mii_pkg::*;
#(
parameter int unsigned CNT_W = 24
) (
input logic rx_clk, // FROM THE PHY
input logic rst_n,
input logic [NIBBLE_BITS-1:0] rxd,
input logic rx_dv,
input logic rx_er,
output logic [7:0] octet,
output logic octet_valid,
output logic frame_start,
output logic frame_end,
output logic frame_had_error,
// A frame that ended on an odd nibble. Reported separately from an
// error, because it says something specific: the damage landed
// mid-octet, which points at the wire rather than at the MAC.
output logic dribble_nibble,
output logic [CNT_W-1:0] c_frames,
output logic [CNT_W-1:0] c_dribbles,
output logic [CNT_W-1:0] c_false_carrier
);
logic phase_q;
logic [3:0] low_q;
logic dv_q;
logic err_q;
always_ff @(posedge rx_clk or negedge rst_n) begin
if (!rst_n) begin
phase_q <= 1'b0; low_q <= 4'd0; dv_q <= 1'b0; err_q <= 1'b0;
octet <= 8'd0; octet_valid <= 1'b0; frame_start <= 1'b0;
frame_end <= 1'b0; frame_had_error <= 1'b0; dribble_nibble <= 1'b0;
c_frames <= '0; c_dribbles <= '0; c_false_carrier <= '0;
end else begin
octet_valid <= 1'b0;
frame_start <= 1'b0;
frame_end <= 1'b0;
dribble_nibble <= 1'b0;
dv_q <= rx_dv;
if (rx_dv) begin
if (!dv_q) begin
frame_start <= 1'b1;
phase_q <= 1'b0;
err_q <= 1'b0;
end
// RX_ER WITH RX_DV is a data error: this frame is damaged, and
// the flag is sticky for the frame rather than per nibble,
// because one bad nibble condemns the whole frame.
if (rx_er) err_q <= 1'b1;
if (!phase_q) begin
low_q <= rxd; // low nibble arrives FIRST
phase_q <= 1'b1;
end else begin
octet <= {rxd, low_q};
octet_valid <= 1'b1;
phase_q <= 1'b0;
end
end else begin
// RX_ER WITHOUT RX_DV is not a data error at all -- it is an
// indication, encoded in RXD. 4'hE is False Carrier: the PHY
// saw something on the wire that never became a frame.
if (rx_er && (rxd == RXD_FALSE_CARRIER)) begin
if (!(&c_false_carrier)) c_false_carrier <= c_false_carrier + 1'b1;
end
if (dv_q) begin
frame_end <= 1'b1;
frame_had_error <= err_q;
if (!(&c_frames)) c_frames <= c_frames + 1'b1;
// ODD NIBBLE COUNT. Half an octet is held and there is no
// honest thing to do with it except say so.
if (phase_q) begin
dribble_nibble <= 1'b1;
if (!(&c_dribbles)) c_dribbles <= c_dribbles + 1'b1;
end
phase_q <= 1'b0;
end
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that RX_ER means two entirely different things depending on RX_DV, and a receiver that treats them alike loses a diagnostic. With RX_DV, it is a data error and the frame is damaged. Without RX_DV, it is an indication encoded in RXD — and 4'hE is False Carrier, meaning the PHY saw activity on the wire that never became a frame. A rising false-carrier count with no frame errors points at the medium, and it is invisible to any counter that only looks at frames.
Deliberately simplified: one indication code is decoded. Clause 22 defines others, and a production receiver decodes the full set.
Production implication: dribble_nibble is reported separately from frame_had_error because the two point at different things. A frame that ended on an odd nibble was damaged mid-octet — which is what wire damage looks like — while a frame with RX_ER was damaged in a way the PHY could name. Padding the partial octet manufactures data; dropping it silently turns a mangled frame into a short one, and short frames get attributed to the transmitter.
7. RTL 3 — Watching a Clock From Outside It
// SYNTHESIZABLE.
//
// Observes TX_CLK and RX_CLK from a domain that does not depend on
// them, and infers the speed from their frequency.
//
// WHY IT CANNOT LIVE IN THE CLOCK IT WATCHES:
// TX_CLK is sourced by the PHY. It STOPS on a link loss and it stops
// again during a speed change while the PHY re-locks. Any logic
// clocked by TX_CLK does not run while TX_CLK is stopped -- so a
// stopped clock is precisely the condition that logic cannot detect.
//
// Which is also why Section 13's rejected property is the one it is.
//
// WHAT IT MEASURES:
// edges of TX_CLK per fixed window of the system clock.
// 100 Mbps -> 25 MHz -> a known count
// 10 Mbps -> 2.5 MHz -> a tenth of it
// stopped -> zero
// Three outcomes, one measurement, and the third is the one nothing
// else in the design can see.
module mii_speed_clock_observer
import mii_pkg::*;
#(
parameter int unsigned SYS_MHZ = 100,
parameter int unsigned WINDOW_US = 100,
parameter int unsigned CNT_W = 20,
// Tolerance band, in per cent, around each expected count.
parameter int unsigned TOL_PERCENT = 10
) (
input logic sys_clk,
input logic rst_n,
// Synchronised toggle signals derived from the two MII clocks.
input logic tx_clk_toggle_sync,
input logic rx_clk_toggle_sync,
output mii_speed_e tx_speed,
output mii_speed_e rx_speed,
output logic tx_clk_stopped,
output logic rx_clk_stopped,
// The two directions disagree about speed. Legal only transiently,
// during a speed change; persistent disagreement is a PHY that has
// half-completed one.
output logic speed_mismatch,
output logic measurement_valid,
output logic [CNT_W-1:0] tx_edges_last_window,
output logic [CNT_W-1:0] rx_edges_last_window,
output logic ever_tx_clk_stopped
);
localparam int unsigned WINDOW_CYCLES = SYS_MHZ * WINDOW_US;
// A 25 MHz clock's toggles counted over the window, and a 2.5 MHz
// clock's. Both computed from the parameters rather than hardcoded,
// so a different system clock does not silently break the bands.
localparam int unsigned EXP_100 = (25 * WINDOW_US);
localparam int unsigned EXP_10 = (25 * WINDOW_US) / 10;
logic [31:0] win_q;
logic [CNT_W-1:0] tx_cnt_q, rx_cnt_q;
logic tx_prev, rx_prev;
function automatic mii_speed_e classify (input logic [CNT_W-1:0] n);
if (n == '0) classify = SPD_UNKNOWN;
else if ((n > CNT_W'((EXP_100 * (100 - TOL_PERCENT)) / 100)) &&
(n < CNT_W'((EXP_100 * (100 + TOL_PERCENT)) / 100)))
classify = SPD_100;
else if ((n > CNT_W'((EXP_10 * (100 - TOL_PERCENT)) / 100)) &&
(n < CNT_W'((EXP_10 * (100 + TOL_PERCENT)) / 100)))
classify = SPD_10;
else classify = SPD_UNKNOWN;
endfunction
always_ff @(posedge sys_clk or negedge rst_n) begin
if (!rst_n) begin
win_q <= '0; tx_cnt_q <= '0; rx_cnt_q <= '0;
tx_prev <= 1'b0; rx_prev <= 1'b0;
tx_speed <= SPD_UNKNOWN; rx_speed <= SPD_UNKNOWN;
tx_clk_stopped <= 1'b1; rx_clk_stopped <= 1'b1;
speed_mismatch <= 1'b0; measurement_valid <= 1'b0;
tx_edges_last_window <= '0; rx_edges_last_window <= '0;
ever_tx_clk_stopped <= 1'b0;
end else begin
tx_prev <= tx_clk_toggle_sync;
rx_prev <= rx_clk_toggle_sync;
if (tx_clk_toggle_sync != tx_prev)
if (!(&tx_cnt_q)) tx_cnt_q <= tx_cnt_q + 1'b1;
if (rx_clk_toggle_sync != rx_prev)
if (!(&rx_cnt_q)) rx_cnt_q <= rx_cnt_q + 1'b1;
if (win_q == 32'(WINDOW_CYCLES - 1)) begin
win_q <= '0;
tx_edges_last_window <= tx_cnt_q;
rx_edges_last_window <= rx_cnt_q;
tx_speed <= classify(tx_cnt_q);
rx_speed <= classify(rx_cnt_q);
// ZERO EDGES. The condition that logic inside the domain can
// never report, because it is not running to report it.
tx_clk_stopped <= (tx_cnt_q == '0);
rx_clk_stopped <= (rx_cnt_q == '0);
if (tx_cnt_q == '0) ever_tx_clk_stopped <= 1'b1;
speed_mismatch <= (tx_cnt_q != '0) && (rx_cnt_q != '0) &&
(classify(tx_cnt_q) != classify(rx_cnt_q));
measurement_valid <= 1'b1;
tx_cnt_q <= '0;
rx_cnt_q <= '0;
end else begin
win_q <= win_q + 1'b1;
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that a stopped clock is only observable from outside the clock, and this is not a subtlety — it is arithmetic. Logic clocked by TX_CLK advances on TX_CLK edges; zero edges means zero advances, so no counter increments, no timeout expires, and no assertion evaluates. The domain does not report its own absence.
Deliberately simplified: the two MII clocks arrive as pre-synchronised toggle signals. In practice each is divided down and passed through a two-flop synchroniser, and the divider is what keeps the toggle rate inside the system clock's Nyquist limit.
Production implication: speed_mismatch catches a real and confusing state. A PHY changing speed stops TX_CLK, re-locks and restarts it — and the two directions do not necessarily change together. A persistent disagreement between tx_speed and rx_speed is a PHY that half-completed a speed change, which presents as a link that passes traffic in one direction and not the other, and which no frame counter explains.
8. The Clock That Stops
Three ordinary events stop TX_CLK, and all three are normal.
Link loss. No link, no transmit timing. The PHY has nothing to derive a transmit clock from and no reason to supply one.
A speed change. 25 MHz and 2.5 MHz are not the same clock slowed down — the PHY stops, re-locks its synthesiser, and starts again, and the gap is however long its PLL takes.
PHY reset or power-down. Software writes a management register and the clock goes away in the middle of whatever was happening.
And in every case, the MAC's transmit logic is frozen — not stalled, frozen.
| a stalled interface | a stopped clock | |
|---|---|---|
| logic advances | yes, doing nothing | no, at all |
| counters increment | yes | no |
| timeouts expire | yes | no |
| assertions evaluate | yes | no |
| the failure is detectable in that domain | yes | never |
Read the last row and the whole of Section 13 follows. The most natural property a verification engineer writes about MII transmit — anything of the form @(posedge tx_clk) … — is vacuously satisfied for exactly as long as TX_CLK is stopped, which is exactly the failure the property was supposed to catch.
9. RTL 4 — Two Error Signals That Are Not Symmetric
// SYNTHESIZABLE.
//
// TX_ER and RX_ER look like a matched pair and are not.
//
// TX_ER -- the MAC telling the PHY "corrupt this frame deliberately".
// A REQUEST, and the PHY obeys by emitting an invalid symbol on
// the line so the far end's receiver rejects the frame.
// Its purpose is to abort a frame the MAC has already committed
// to, e.g. an underrun, WITHOUT leaving a plausible short frame
// on the wire.
//
// RX_ER -- the PHY telling the MAC something, and WHAT it is telling
// depends entirely on RX_DV:
// RX_ER & RX_DV -- this frame is damaged.
// RX_ER & !RX_DV -- an INDICATION, coded in RXD.
// 4'hE = False Carrier.
//
// One is a command, the other is a two-meaning status. Treating them as
// a matched pair loses the second meaning entirely.
module mii_error_signalling
import mii_pkg::*;
#(
parameter int unsigned CNT_W = 20
) (
input logic clk, // RX_CLK for the receive side
input logic rst_n,
input logic clear,
input logic rx_dv,
input logic rx_er,
input logic [NIBBLE_BITS-1:0] rxd,
output logic data_error, // damaged frame
output logic false_carrier, // activity that never became a frame
output logic unknown_indication, // RX_ER, no RX_DV, an RXD we do not decode
output logic [CNT_W-1:0] c_data_errors,
output logic [CNT_W-1:0] c_false_carrier,
output logic [CNT_W-1:0] c_unknown,
// Sticky: this link has produced an indication we could not decode,
// which usually means the PHY is a later revision than the MAC.
output logic ever_unknown
);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
data_error <= 1'b0; false_carrier <= 1'b0; unknown_indication <= 1'b0;
c_data_errors <= '0; c_false_carrier <= '0; c_unknown <= '0;
ever_unknown <= 1'b0;
end else if (clear) begin
c_data_errors <= '0; c_false_carrier <= '0; c_unknown <= '0;
data_error <= 1'b0; false_carrier <= 1'b0; unknown_indication <= 1'b0;
// ever_unknown survives -- it says the far side speaks a dialect
// this design does not fully decode, and that stays true.
end else begin
data_error <= 1'b0;
false_carrier <= 1'b0;
unknown_indication <= 1'b0;
if (rx_er) begin
if (rx_dv) begin
// WITH data valid: a damaged frame.
data_error <= 1'b1;
if (!(&c_data_errors)) c_data_errors <= c_data_errors + 1'b1;
end else begin
// WITHOUT data valid: an indication, and RXD says which.
unique case (rxd)
RXD_FALSE_CARRIER: begin
false_carrier <= 1'b1;
if (!(&c_false_carrier)) c_false_carrier <= c_false_carrier + 1'b1;
end
default: begin
// NOT silently ignored. An indication code this design
// does not decode is information about the partner, and
// discarding it is how a MAC stays permanently unaware
// that its PHY is telling it something.
unknown_indication <= 1'b1;
ever_unknown <= 1'b1;
if (!(&c_unknown)) c_unknown <= c_unknown + 1'b1;
end
endcase
end
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that false_carrier is a medium diagnostic that no frame counter can produce. It counts activity the PHY saw on the wire that never became a frame — noise, a partner starting and aborting, a marginal link — and it rises while every frame-level counter stays clean. A link with a climbing false-carrier count and zero frame errors has a physical problem that has not yet cost a frame.
Deliberately simplified: one indication code is decoded and the rest are counted as unknown. Clause 22 and its amendments define more.
Production implication: unknown_indication is not a fallthrough — it is a counter. A MAC paired with a PHY of a later revision will receive indication codes it does not decode, and the difference between counting them and discarding them is the difference between "this PHY is telling us something we do not understand" and permanent silence. ever_unknown surviving a clear says the mismatch is a property of the pairing, not of the moment.
10. Two Pins for a Mode Nobody Uses
CRS and COL carry Chapter 1.2's two inputs across the MAC/PHY boundary, and Chapter 9.2 established what happened to them.
On a full-duplex port the MAC never senses carrier and never collides. So both signals stay deasserted for the entire life of the link — two pins, two board traces, two package balls, permanently idle.
And that is not merely waste. It is a hazard, for a reason that follows from Chapter 9.2's dormant_mechanisms argument.
A MAC's collision handling is still implemented. It is unreachable on a full-duplex link because COL never asserts — not because the logic was removed. So a COL pin that asserts spuriously, from a floating input, a crosstalk event, or a PHY in a strange state, wakes logic that has not run since bring-up and drives it into a jam-and-backoff sequence on a link where that behaviour is meaningless.
| on a half-duplex link | on a full-duplex link | |
|---|---|---|
CRS asserting | normal — defer | should be impossible |
COL asserting | normal — jam and back off | should be impossible, and triggers dormant logic |
a floating COL input | indistinguishable from traffic | a fault with a plausible-looking symptom |
Which is why Section 11's monitor checks them. On a full-duplex link, any assertion of CRS or COL is a fault report, and a design that does not count them has no way to distinguish "this link is fine" from "this link is being told about collisions it cannot have."
11. RTL 5 — Checking the Partner Rather Than Yourself
// SYNTHESIZABLE.
//
// Checks the things a local self-test cannot: the partner's behaviour,
// and the conditions that live outside the transmit clock domain.
//
// Runs on SYS_CLK deliberately, taking synchronised versions of the MII
// control signals -- because two of the three faults it hunts make the
// MII clock domains unavailable or untrustworthy.
//
// WHAT IT CHECKS:
// 1. CRS/COL on a full-duplex link. Both should be silent forever.
// Any assertion is a fault report, not traffic.
// 2. TX_EN asserted while TX_CLK is stopped. Impossible from inside
// the domain; obvious from outside.
// 3. RX_DV without a preceding carrier. A PHY delivering data it
// never announced.
// 4. Frame length in nibbles. An ODD count means the interface
// delivered half an octet, which points at the wire.
module mii_conformance_monitor
import mii_pkg::*;
#(
parameter int unsigned CNT_W = 20
) (
input logic sys_clk,
input logic rst_n,
input logic clear,
input logic full_duplex,
input logic tx_clk_stopped, // from Section 7's observer
// Synchronised MII control signals.
input logic crs_sync,
input logic col_sync,
input logic tx_en_sync,
input logic rx_dv_sync,
output logic crs_on_full_duplex,
output logic col_on_full_duplex,
output logic tx_en_without_clock,
output logic rx_dv_without_carrier,
output logic [CNT_W-1:0] c_crs_violations,
output logic [CNT_W-1:0] c_col_violations,
output logic [CNT_W-1:0] c_tx_en_no_clock,
output logic [CNT_W-1:0] c_rx_dv_no_carrier,
// The FIRST violation seen, latched. A partner that has gone wrong
// produces many, and only the first one has a cause.
output logic first_violation_valid,
output logic [1:0] first_violation_kind,
output logic ever_violated
);
logic crs_q;
logic any_c;
logic [1:0] kind_c;
always_comb begin
any_c = 1'b0;
kind_c = 2'd0;
if (full_duplex && crs_sync) begin any_c = 1'b1; kind_c = 2'd0; end
else if (full_duplex && col_sync) begin any_c = 1'b1; kind_c = 2'd1; end
else if (tx_en_sync && tx_clk_stopped) begin any_c = 1'b1; kind_c = 2'd2; end
else if (rx_dv_sync && !crs_q) begin any_c = 1'b1; kind_c = 2'd3; end
end
always_ff @(posedge sys_clk or negedge rst_n) begin
if (!rst_n) begin
crs_q <= 1'b0;
crs_on_full_duplex <= 1'b0; col_on_full_duplex <= 1'b0;
tx_en_without_clock <= 1'b0; rx_dv_without_carrier <= 1'b0;
c_crs_violations <= '0; c_col_violations <= '0;
c_tx_en_no_clock <= '0; c_rx_dv_no_carrier <= '0;
first_violation_valid <= 1'b0; first_violation_kind <= 2'd0;
ever_violated <= 1'b0;
end else begin
crs_q <= crs_sync;
crs_on_full_duplex <= full_duplex && crs_sync;
col_on_full_duplex <= full_duplex && col_sync;
// TX_EN ASSERTED WITH NO CLOCK. The MAC believes it is
// transmitting and nothing is leaving, because the domain that
// would move the data is not running. Visible only from here.
tx_en_without_clock <= tx_en_sync && tx_clk_stopped;
rx_dv_without_carrier <= rx_dv_sync && !crs_q;
if (clear) begin
c_crs_violations <= '0; c_col_violations <= '0;
c_tx_en_no_clock <= '0; c_rx_dv_no_carrier <= '0;
first_violation_valid <= 1'b0;
// ever_violated survives.
end else begin
if (full_duplex && crs_sync && !(&c_crs_violations))
c_crs_violations <= c_crs_violations + 1'b1;
if (full_duplex && col_sync && !(&c_col_violations))
c_col_violations <= c_col_violations + 1'b1;
if (tx_en_sync && tx_clk_stopped && !(&c_tx_en_no_clock))
c_tx_en_no_clock <= c_tx_en_no_clock + 1'b1;
if (rx_dv_sync && !crs_q && !(&c_rx_dv_no_carrier))
c_rx_dv_no_carrier <= c_rx_dv_no_carrier + 1'b1;
if (any_c) begin
ever_violated <= 1'b1;
if (!first_violation_valid) begin
first_violation_valid <= 1'b1;
first_violation_kind <= kind_c;
end
end
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that tx_en_without_clock is the whole argument of this chapter in one signal. The MAC has TX_EN asserted — it believes it is transmitting — and TX_CLK is stopped, so nothing is moving. From inside the transmit domain this state is not merely hard to detect; it is not reachable, because the logic that would detect it is not running. From sys_clk it is one AND gate.
Deliberately simplified: crs_q stands in for a proper carrier-preceding-data check, which in a real monitor allows for the synchroniser latency between the two signals.
Production implication: first_violation_kind matters for the same reason Chapter 9.4 §8's does. A partner that has gone wrong produces violations continuously, and by the time anyone reads the counters they are all saturated. kind = 2 as a first violation says the PHY's clock went away while the MAC was mid-frame; kind = 1 says a collision was signalled on a link that cannot have one — the same "interface is broken" summary, two entirely different investigations.
12. The Latency MII Adds
Compute it, because the number is small and the comparison with Chapter 10.2 depends on it.
| Quantity | Working | 100 Mbps | 10 Mbps |
|---|---|---|---|
| clock | — | 25 MHz | 2.5 MHz |
| clock period | 1 ÷ f | 40 ns | 400 ns |
| nibbles per octet | — | 2 | 2 |
| an octet crosses in | 2 × period | 80 ns | 800 ns |
| a 64-octet frame | 64 × 8 ÷ 4 nibbles | 128 clocks = 5.12 µs | 51.2 µs |
| a 1518-octet frame | 1518 × 8 ÷ 4 nibbles | 3036 clocks = 121.44 µs | 1.2144 ms |
And the interface's own contribution to Chapter 8.4's decomposition is the assembly delay, not the frame time. A frame takes 121 µs to cross MII at 100 Mbps because it takes 121 µs to serialise at 100 Mbps — that is Chapter 8.1's term, already counted, and the interface is not adding it.
What MII adds is the nibble-assembly latency: 80 ns per octet of pipelining at 100 Mbps, plus whatever synchronisation the MAC needs between the two clock domains.
13. Properties Worth Asserting, and One Worth Refusing
The organising question for this chapter is not what to assert but in which clock. Properties about the data path belong in the domain that carries it. Properties about whether that domain exists cannot.
Transmit ordering and framing — in tx_clk, correctly
// P1. The low nibble goes first. The single ordering rule, and the one
// whose violation produces perfectly formed wrong frames.
property p_low_nibble_first;
@(posedge tx_clk) disable iff (!rst_n)
(octet_valid && octet_ready && !busy_q) |=> (txd == $past(octet[3:0]));
endproperty
a_low_nibble_first: assert property (p_low_nibble_first);
// P2. And the high nibble follows immediately. An octet is two
// consecutive transfers, never two separated ones.
property p_high_nibble_next;
@(posedge tx_clk) disable iff (!rst_n)
busy_q |=> (txd == $past(held_q[7:4]));
endproperty
a_high_nibble_next: assert property (p_high_nibble_next);
// P3. TX_EN is asserted for both nibbles of every octet. A gap between
// an octet's halves is a frame the far end will not reassemble.
property p_tx_en_spans_octet;
@(posedge tx_clk) disable iff (!rst_n)
(tx_en && !busy_q && octet_valid) |=> tx_en;
endproperty
a_tx_en_spans_octet: assert property (p_tx_en_spans_octet);
// P4. An octet is never accepted mid-octet. The commitment point.
property p_no_accept_mid_octet;
@(posedge tx_clk) disable iff (!rst_n)
(busy_q && phase_q) |-> !octet_ready;
endproperty
a_no_accept_mid_octet: assert property (p_no_accept_mid_octet);
// P5. An offered octet during the high-nibble phase is REPORTED rather
// than silently lost -- a dropped octet becomes a short frame, and a
// short frame is diagnosed at the far end.
property p_overrun_reported;
@(posedge tx_clk) disable iff (!rst_n)
(busy_q && octet_valid) |=> overrun;
endproperty
a_overrun_reported: assert property (p_overrun_reported);Receive assembly and error decoding — in rx_clk
// P6. An octet is assembled low nibble first, matching P1's ordering on
// the other side of the link.
property p_rx_assembly_order;
@(posedge rx_clk) disable iff (!rst_n)
octet_valid |-> (octet[3:0] == $past(low_q));
endproperty
a_rx_assembly_order: assert property (p_rx_assembly_order);
// P7. A frame ending on an odd nibble is REPORTED, never padded. A
// padded partial octet is manufactured data.
property p_dribble_reported;
@(posedge rx_clk) disable iff (!rst_n)
(frame_end && $past(phase_q)) |-> dribble_nibble;
endproperty
a_dribble_reported: assert property (p_dribble_reported);
// P8. RX_ER WITH RX_DV is a data error and nothing else.
property p_rx_er_with_dv_is_data_error;
@(posedge rx_clk) disable iff (!rst_n)
(rx_er && rx_dv) |=> (data_error && !false_carrier);
endproperty
a_rx_er_with_dv: assert property (p_rx_er_with_dv_is_data_error);
// P9. RX_ER WITHOUT RX_DV is an indication and never a data error --
// the asymmetry a receiver that treats them alike destroys.
property p_rx_er_without_dv_is_indication;
@(posedge rx_clk) disable iff (!rst_n)
(rx_er && !rx_dv) |=> !data_error;
endproperty
a_rx_er_without_dv: assert property (p_rx_er_without_dv_is_indication);
// P10. An indication code this design does not decode is COUNTED, not
// discarded. The property that keeps a MAC from being permanently
// unaware that its PHY is talking to it.
property p_unknown_indication_counted;
@(posedge rx_clk) disable iff (!rst_n)
(rx_er && !rx_dv && (rxd != RXD_FALSE_CARRIER)) |=> unknown_indication;
endproperty
a_unknown_counted: assert property (p_unknown_indication_counted);Existence of the clocks — in sys_clk, because it must be
// P11. THE PROPERTY THIS CHAPTER IS ABOUT. A stopped transmit clock is
// detected -- and the assertion is sampled by sys_clk, because a
// tx_clk-sampled property cannot evaluate while tx_clk is stopped.
property p_stopped_clock_detected;
@(posedge sys_clk) disable iff (!rst_n)
(measurement_valid && (tx_edges_last_window == '0)) |-> tx_clk_stopped;
endproperty
a_stopped_clock_detected: assert property (p_stopped_clock_detected);
// P12. TX_EN asserted with no transmit clock is a violation and is
// counted. The MAC believes it is transmitting; nothing is moving.
property p_tx_en_without_clock;
@(posedge sys_clk) disable iff (!rst_n)
(tx_en_sync && tx_clk_stopped) |=> tx_en_without_clock;
endproperty
a_tx_en_without_clock: assert property (p_tx_en_without_clock);
// P13. The speed classification is one of the three defined outcomes;
// there is no fourth state a link can sit in unreported.
property p_speed_defined;
@(posedge sys_clk) disable iff (!rst_n)
measurement_valid |-> (tx_speed inside {SPD_10, SPD_100, SPD_UNKNOWN});
endproperty
a_speed_defined: assert property (p_speed_defined);
// P14. A persistent disagreement between directions is reported, which
// is a PHY that half-completed a speed change.
property p_speed_mismatch_reported;
@(posedge sys_clk) disable iff (!rst_n)
(measurement_valid && (tx_speed != rx_speed) &&
(tx_speed != SPD_UNKNOWN) && (rx_speed != SPD_UNKNOWN))
|-> speed_mismatch;
endproperty
a_speed_mismatch_reported: assert property (p_speed_mismatch_reported);Full-duplex conformance
// P15. On a full-duplex link CRS never asserts, and if it does it is
// counted as a violation rather than obeyed.
property p_crs_silent_on_full_duplex;
@(posedge sys_clk) disable iff (!rst_n)
(full_duplex && crs_sync) |=> crs_on_full_duplex;
endproperty
a_crs_silent: assert property (p_crs_silent_on_full_duplex);
// P16. And COL likewise -- which matters more, because COL wakes logic
// that has not run since bring-up.
property p_col_silent_on_full_duplex;
@(posedge sys_clk) disable iff (!rst_n)
(full_duplex && col_sync) |=> col_on_full_duplex;
endproperty
a_col_silent: assert property (p_col_silent_on_full_duplex);
// P17. The first violation is latched and never overwritten.
property p_first_violation_stable;
@(posedge sys_clk) disable iff (!rst_n)
(first_violation_valid && !clear) |=> $stable(first_violation_kind);
endproperty
a_first_violation_stable: assert property (p_first_violation_stable);14. Verification Scenarios
Group by clock domain, because that is the axis this interface's faults sort on.
Transmit ordering — in tx_clk
- A single octet — low nibble then high nibble, two consecutive
TX_CLKcycles,TX_ENasserted for both. 0xA5—TXD = 0x5thenTXD = 0xA. The concrete ordering check, with a value whose halves differ.- Back-to-back octets —
octet_readylow on every high-nibble phase and high on every low-nibble phase. - An octet offered during the high-nibble phase —
overrunpulses, the offered octet is not accepted, and the in-flight octet completes. - A source that stalls between octets —
TX_ENdeasserts cleanly on an octet boundary, never mid-octet. force_errormid-frame —TX_ERasserts alongsideTX_EN, and the PHY's obligation is to corrupt the line symbol.- A frame of odd octet count — still an even nibble count; MII cannot express a half-octet on transmit.
Receive assembly — in rx_clk
- A clean frame —
frame_startonRX_DV's rise, octets assembled low nibble first,frame_endon its fall,dribble_nibblelow. RX_DVdeasserting after an odd number of nibbles —dribble_nibblehigh,c_dribblesincrements, and the partial octet is discarded rather than padded.RX_ERasserted for one nibble mid-frame —frame_had_errorsticky for the whole frame, not just that nibble.RX_ERwithRX_DVlow andRXD = 0xE—false_carrier, notdata_error. The asymmetry.RX_ERwithRX_DVlow andRXD = 0x5—unknown_indication, counted,ever_unknownset.RX_DVrising and falling within one nibble — no octet emitted, no crash.clearafter an unknown indication — counters clear,ever_unknownsurvives.
Clock observation — in sys_clk
TX_CLKat 25 MHz for a full window —tx_speed = SPD_100,tx_clk_stoppedlow.TX_CLKat 2.5 MHz —tx_speed = SPD_10.TX_CLKstopped for a full window —tx_edges_last_window = 0,tx_clk_stoppedhigh,ever_tx_clk_stoppedset. The condition notx_clk-sampled logic can produce.TX_CLKat 25 MHz,RX_CLKat 2.5 MHz —speed_mismatchhigh. A PHY that half-completed a speed change.TX_CLKat 12 MHz — neither band;SPD_UNKNOWN. A frequency nobody specified is reported as unknown rather than rounded to the nearest legal one.TX_CLKstopping and restarting within one window — the count falls into no band;SPD_UNKNOWNrather than a wrong classification.TX_ENasserted whiletx_clk_stopped—tx_en_without_clock,c_tx_en_no_clockincrements,first_violation_kind = 2.
Full-duplex conformance
CRSasserting on a full-duplex link —crs_on_full_duplex, counted.COLasserting on a full-duplex link —col_on_full_duplex, counted, and the MAC's collision handling must not be entered.CRSandCOLon a half-duplex link — no violations. The same signals, legal in the other mode.RX_DVwith no precedingCRS—rx_dv_without_carrier. A PHY delivering data it never announced.- A violation storm — counters saturate,
first_violation_kindunchanged from the first. clearduring a storm — counters clear,first_violation_validclears,ever_violatedsurvives.
15. Debugging: Which Domain Owns the Symptom
| Observation | Likely cause | The distinguishing check |
|---|---|---|
| far end sees 100% FCS failures, frames arrive | nibble order swapped | far-end frame count non-zero; internal loopback passes |
| far end sees no frames at all | cable, PHY, or TX_CLK stopped | tx_clk_stopped; a dead cable and a dead clock look identical from the MAC |
| MAC reports transmitting, nothing on the wire | TX_CLK stopped | tx_en_without_clock, first_violation_kind = 2 |
| link works one direction only | a half-completed speed change | speed_mismatch, tx_speed against rx_speed |
| everything freezes during a link bounce and recovers | normal — the PHY stopped and restarted TX_CLK | ever_tx_clk_stopped set with no violations; nothing to fix |
rising c_false_carrier, zero frame errors | a physical problem that has not yet cost a frame | the earliest warning MII gives; check the medium |
rising c_dribbles | damage landing mid-octet on the wire | points at the medium, not at the far MAC |
COL asserting on a full-duplex link | floating input, crosstalk, or a confused PHY | col_on_full_duplex; the MAC must not obey it |
rising c_unknown | the PHY is a later revision than the MAC's decoder | ever_unknown; a pairing issue, not a link issue |
| interface passes every local test, fails against a partner | nibble order, or a clocking assumption | loopback cannot see either — only a partner can |
Three habits.
First, check tx_clk_stopped before anything else on a transmit problem. It is the one condition that makes every other transmit-side measurement meaningless, and it is invisible to all of them. A MAC with TX_EN high and no clock is not slow, not stalled and not congested — it is not running.
Second, treat a passing loopback as evidence of nothing when the symptom is at the far end. A nibble swap cancels under internal loopback, so the test that feels most reassuring is the one guaranteed to pass. The signature is frames arriving at the far end and all of them bad, which distinguishes it from a dead path.
Third, watch c_false_carrier on a link that is working. It counts activity the PHY saw that never became a frame — noise, a marginal partner, a failing cable — and it rises before any frame counter does. It is the only leading indicator MII produces.
16. Common Misconceptions
"MII is a bus, so the MAC drives it."
The wrong model: the MAC is the master and supplies the timing.
What it costs: you cannot explain the stopped-clock failure, the two clock domains, or why RMII exists.
The corrected model: the PHY sources both clocks — TX_CLK as well as RX_CLK — so twelve of MII's sixteen signals originate at the PHY even though data flows both ways. The MAC clocks its own transmit data out with a clock it was handed, which means the MAC has no timing of its own on this interface and the PHY can take it away.
"A stopped clock will show up as a timeout."
The wrong model: logic notices when things stop happening.
What it costs: the interface's most famous failure is excluded from verification, and the more assertions are written the more completely it is excluded.
The corrected model: a timeout is a counter, and a counter needs a clock. With TX_CLK stopped, nothing in that domain advances — no counter, no timeout, no assertion. The domain does not report its own absence, and the only way to see it is to count edges from a domain that is still running.
"TX_ER and RX_ER are a matched pair."
The wrong model: symmetric error signals, one each way.
What it costs: the entire RX_ER-without-RX_DV indication channel, including the false-carrier count that is MII's only leading indicator.
The corrected model: TX_ER is a command — the MAC telling the PHY to corrupt this frame deliberately, so an aborted transmission does not leave a plausible short frame on the wire. RX_ER is a status with two meanings, selected by RX_DV: with it, a damaged frame; without it, an indication encoded in RXD, of which 0xE is False Carrier.
"A nibble-order bug would be obvious."
The wrong model: a data-ordering error produces visibly broken behaviour locally.
What it costs: days, because every local test passes.
The corrected model: the frames are structurally perfect — right length, right timing, TX_EN asserted for exactly the right clocks — and every octet's halves are exchanged. It cancels under internal loopback, so the most reassuring test passes. The signature is at the far end: frames arriving, 100% of them failing FCS — as opposed to a dead path, where no frames arrive at all.
"CRS and COL are harmless legacy pins."
The wrong model: unused signals cost nothing but board area.
What it costs: a full-duplex link that occasionally enters collision handling, which is behaviour with no defined meaning there.
The corrected model: the MAC's collision logic is still implemented — Chapter 9.2's dormant_mechanisms — and it is unreachable only because COL never asserts. A floating or crosstalk-driven COL wakes logic that has not run since bring-up and drives a jam-and-backoff sequence on a link where it is meaningless. On a full-duplex link, any CRS or COL assertion is a fault report.
17. Interview Reasoning
"Who sources MII's clocks, and why does it matter?"
The PHY sources both — RX_CLK, which is unavoidable because the receive timing was recovered from the wire, and TX_CLK, which is the interesting one. The MAC has transmit data and no clock of its own, so it clocks TXD and TX_EN out on a clock the PHY handed it. The reason is that the PHY's transmit rate is not the MAC's business — 25 MHz at 100 Mbps, 2.5 MHz at 10 — and sourcing the clock from the PHY makes the speed change invisible to the MAC. The consequence is that the PHY can also take it away, and it does: on link loss, on a speed change while the synthesiser re-locks, and on reset. The finishing point: twelve of MII's sixteen signals originate at the PHY, so the MAC owns almost nothing on this interface.
"What is the nibble ordering rule and why is a violation hard to find?"
The low nibble of each octet goes first, following Ethernet's least-significant-bit-first ordering with TXD[0] as the first bit. A violation is hard to find because the frames are structurally perfect — right length, right timing, TX_EN correct — with every octet's halves exchanged, so the far end reports 100% FCS failures, which is also what a dead cable and a broken PHY report. And it cancels under internal loopback, so the most obvious test passes. The strong answer names the discriminator: frames arriving at the far end and all of them bad distinguishes a swap from a dead path, where no frames arrive at all.
"What is the difference between RX_ER with and without RX_DV?"
With RX_DV, it is a data error: this frame is damaged and should be discarded. Without RX_DV, it is an indication encoded in RXD — a status channel that exists only in the interframe period — and RXD = 0xE is False Carrier, meaning the PHY saw activity on the wire that never became a frame. The strong answer says why that matters: false carrier is MII's only leading indicator. It rises on a marginal medium while every frame counter is still clean, so a receiver that folds both RX_ER cases into one error count has thrown away the earliest warning the interface produces. The finishing point: an indication code the design does not decode should be counted, not discarded — otherwise a MAC paired with a newer PHY stays permanently unaware that it is being told something.
"Would you assert that an MII transmission always completes within N clocks?"
Not sampled by TX_CLK, no — and the objection is about the clocking event, not the content. TX_CLK is sourced by the PHY and stops on link loss, speed change and reset; an assertion evaluates only on edges of its sampling clock, so with no edges there are no evaluations and no failures. The property does not wait while the clock is stopped — it does not exist, and the ##[1:N] window never advances because the window is measured in the clock that is gone. The frame that never completed is the failure and the property is green throughout. Worse, the effect scales with effort: the more tx_clk-sampled properties are written, the more completely the failure is excluded. Assert instead from a free-running domain: count TX_CLK edges over a window, call zero edges a stopped clock, and flag TX_EN asserted while it is stopped — a MAC that believes it is transmitting into a domain that is not running.
18. Understanding Check
Sixteen, and twelve of them originate at the PHY.
| Signal | Width | Direction |
|---|---|---|
TXD[3:0], TX_EN, TX_ER | 6 | MAC → PHY |
TX_CLK | 1 | PHY → MAC |
RXD[3:0], RX_DV, RX_ER | 6 | PHY → MAC |
RX_CLK | 1 | PHY → MAC |
CRS, COL | 2 | PHY → MAC |
Plus MDC and MDIO, giving 18 pins per port at each end.
Two things follow from the count. First, the PHY sources both clocks — the MAC has no timing of its own on this interface, and the PHY can stop either one. Second, CRS and COL are 12.5% of the pins and exist only for half duplex, so on a full-duplex link they are wires that never assert.
And the arithmetic at scale is why this module has three more chapters. Sixteen signals is fine for one port; a 24-port switch spends 24 × 16 = 384 pins on MII alone, before management and before anything else. That is a package decision, and it is the pressure RMII was created to relieve.
19. What's Next
The claim this chapter defended: an interface's defining failure mode is decided by which side sources its clocks.
MII is sixteen signals, four bits each way, and twelve of them originate at the PHY — including both clock domains. That choice keeps the line rate out of the MAC's business and makes a speed change invisible to it. It also means the PHY can stop the MAC's transmit clock, and three ordinary events do: link loss, a speed change while the synthesiser re-locks, and reset.
While TX_CLK is stopped the MAC's transmit logic is frozen — no counter advances, no timeout expires, and no assertion sampled by that clock evaluates. So the interface's most famous failure is invisible to every property written in the domain where the transmit path lives, which is the thirty-ninth rejected class and the only one in the track that is about an assertion's clocking event rather than its content.
The rest is ordering and asymmetry. The low nibble goes first, and reversing it produces perfectly framed wrong frames that cancel under loopback. TX_ER is a command and RX_ER is a two-meaning status, whose second meaning — false carrier — is the only leading indicator MII gives. And CRS and COL are 12.5% of the pins for a mode that stopped being used, sitting in front of collision logic that is dormant rather than absent.
Chapter 10.2 — RMII takes the pin count seriously.
Sixteen signals per port becomes 384 on a 24-port switch, which is a package decision rather than a protocol one. RMII cuts it to eight — two bits each way instead of four — and pays for the narrower bus by doubling the clock to 50 MHz, so the octet time is unchanged at 80 ns and the reduction is free in latency terms. But it does something more consequential than halving a bus: it takes the clock away from the PHY. A single 50 MHz reference now feeds both ends, which removes MII's two-domain structure and its stopped-clock failure — and introduces a new one, in which the two ends disagree about where that reference comes from. And it multiplexes carrier sense onto the data-valid signal, producing a wire with two meanings and a toggling convention to separate them.
The full path is on the Ethernet curriculum index.
Continue learning
Related tutorials
- Related topic
RMII — Half the Pins, One Shared Clock
Two bits at 50 MHz gives MII's exact octet time on half the signals, so the saving is free. What it spends is a clock nobody owns and a carrier signal multiplexed onto data valid.
- Related topic
GMII — Eight Bits, and a Clock That Changed Direction
The MAC sources GTX_CLK because MII's round-trip timing path consumed half an 8 ns period. What that bought was speed; what it cost was an abstraction, since setup and hold at the pins are invisible to RTL.
- Related topic
The Shared-Medium Problem
Why several independent transmitters on one medium is a distributed timing problem, not a formatting problem. Propagation delay makes every station's view of the medium stale, so two locally correct decisions can still collide — and that is the constraint the Ethernet MAC was built around.
- Related topic
CSMA/CD, Collision Domains and Slot Time
Slot time is the parameter the whole half-duplex MAC hangs on: it bounds medium acquisition, bounds a collision fragment, and is the retransmission quantum. Deriving it from round-trip propagation plus jam is what fixes Ethernet's minimum frame size — a timing constant wearing a frame-format costume.
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.
