Ethernet · Module 4
PHY Management — MDC/MDIO, Clause 22 and Clause 45
The management path is the datapath inverted — one wire, slow, asynchronous, handshaked every transaction. It carries values that were true when they were read rather than values that are true now, which makes every read a sample with an age and every read-back a check of storage rather than effect.
Chapter 4.1 §5 drew a line and then walked away from it. Its port list carried link_status_vector and deliberately excluded FEC counts, eye margin and lane skew, with the reasoning that data-path status and diagnostic detail belong on separate paths with different contracts. It named the management path and did not describe it.
That separation looks like tidiness. It is not.
The two paths are opposites in every dimension, and comparing them side by side is the fastest way to see why one cannot do the other's job:
| Datapath | Management path | |
|---|---|---|
| width | parallel, 4 to 32 bits | one wire |
| clock | synchronous to the link, tens to hundreds of MHz | up to 2.5 MHz, and asynchronous to everything |
| a transaction | one cycle | 64 bit times or more |
| handshake | on transmit only | every transaction |
| back-pressure on receive | impossible | irrelevant — nothing arrives unasked |
Those are not arbitrary engineering choices that happened to differ. They follow from a difference in what the two paths carry, and the difference has one property that decides everything.
The datapath carries values that are true now. The management path carries values that were true when they were read.
What does that difference force into the interface, and why did a second, incompatible addressing scheme have to be invented?
1. Scope — What This Chapter Owns
This chapter owns: the management path as a second boundary with its own contract; the Clause 22 frame in full; why Clause 45's indirect addressing exists and what it costs; bus turnaround and why it is where designs actually break; sample age and staleness as a first-class property; and the telemetry that distinguishes a bus fault from an absent device.
This chapter does not own: the datapath contract (Chapter 4.1), the interface vocabulary (Chapter 4.2), or the cycle-by-cycle data flow (Chapter 4.3). It does not enumerate any register map — those are vendor-specific and clause-specific, and reproducing part of one would look authoritative while being wrong. Chapter 4.6 owns how the management interface is clocked and reset relative to the datapath.
The debt it repays: Chapter 4.1 §5 excluded diagnostic detail from the datapath boundary and named a management path without describing it. Chapter 3.2 §11 introduced staleness for optical module status and treated it as a module-specific concern. It is not — it is the defining property of every management path, and this chapter generalises it.
2. The Clause 22 Frame
Small enough to show completely, and worth showing completely for the same reason Chapter 3.5 showed the 4B/5B table in full: an abbreviated version of a small exact thing is worse than none.
The published frame structure:
| Field | Bits | Meaning |
|---|---|---|
| preamble | 32 | all ones — lets a device synchronise before anything meaningful |
| ST | 2 | start of frame. 01 selects Clause 22 |
| OP | 2 | operation. 10 read, 01 write |
| PHYAD | 5 | which physical device, of 32 |
| REGAD | 5 | which register, of 32 |
| TA | 2 | turnaround — who drives the wire next |
| DATA | 16 | the value written, or the value read |
Total after the preamble: 32 bits. With the preamble, 64 bit times per transaction — and at the specified maximum MDC frequency of 2.5 MHz, that is roughly 26 microseconds per register access.
Conceptual — a Clause 22 read, field by field
10 cyclesThis figure is conceptual and labelled so. It shows the field order and the change of ownership correctly; it compresses the 32-bit preamble and the 16 data bits into single columns so the structure is visible.
The address space this gives: 5 bits of device address and 5 of register address, so 32 devices with 32 registers each. That was ample when a PHY had a control register, a status register, a pair of identifiers and some link-partner information.
3. Why Clause 45 Exists
By the time a PHY contained several sublayers — a PCS with block-lock state, a PMA with lane status, a PMD with optical readings, plus FEC counters — 32 registers was nowhere near enough.
The obvious fix is a wider address field, and it was not available. The frame is fixed; widening REGAD would break every existing master and device. So Clause 45 does something else: it keeps the 32-bit frame and spends two frames per access.
The Clause 45 structure, as published:
| Field | Bits | Clause 45 meaning |
|---|---|---|
| ST | 2 | 00 — distinguishes it from Clause 22's 01 |
| OP | 2 | 00 address · 01 write · 11 read · 10 read-and-increment |
| PRTAD | 5 | which port, of 32 |
| DEVAD | 5 | which MMD within that port, of 32 |
| TA | 2 | turnaround, as before |
| DATA | 16 | an address, or a data value, depending on OP |
The mechanism, in two frames:
- An address frame —
OP = 00— carries a 16-bit register address in the data field. The device stores it. - A data frame —
OP = 01or11— reads or writes the register the stored address points at.
The arithmetic of what that buys:
Clause 22: 32 devices × 32 registers = 1,024 registers
Clause 45: 32 ports × 32 MMDs × 65,536 registers = about 67 millionFive orders of magnitude, paid for by doubling the transaction cost — roughly 52 microseconds for an addressed read instead of 26.
4. RTL 1 — The MDIO Master
// SYNTHESIZABLE. Clause 22 MDIO master.
//
// The frame is the published one: 32 preamble bits, then ST(2) OP(2)
// PHYAD(5) REGAD(5) TA(2) DATA(16) -- 32 bits after the preamble.
//
// THE PART THAT BREAKS is the turnaround. MDIO is one bidirectional wire.
// On a read the master must STOP driving before the device starts, and if
// the two disagree by one bit time the frame still completes -- with one
// data bit sampled from contention. The result is a read that is usually
// correct and occasionally wrong in the same bit position.
//
// So output enable is derived from an explicit bit counter, never from a
// state that could be reached one cycle early or late.
package mdio_pkg;
localparam logic [1:0] ST_C22 = 2'b01;
localparam logic [1:0] ST_C45 = 2'b00;
localparam logic [1:0] OP_C22_WRITE = 2'b01;
localparam logic [1:0] OP_C22_READ = 2'b10;
// Clause 45 opcodes, as published.
localparam logic [1:0] OP_C45_ADDR = 2'b00;
localparam logic [1:0] OP_C45_WRITE = 2'b01;
localparam logic [1:0] OP_C45_READ = 2'b11;
localparam logic [1:0] OP_C45_READ_INC = 2'b10;
// Bit positions within the post-preamble frame, counting from 0.
localparam int unsigned BIT_ST = 0; // 2 bits
localparam int unsigned BIT_OP = 2; // 2 bits
localparam int unsigned BIT_PHY = 4; // 5 bits
localparam int unsigned BIT_REG = 9; // 5 bits
localparam int unsigned BIT_TA = 14; // 2 bits
localparam int unsigned BIT_DATA = 16; // 16 bits
localparam int unsigned FRAME_BITS = 32;
endpackage
module mdio_master
import mdio_pkg::*;
#(
parameter int unsigned PREAMBLE_BITS = 32,
// MDC is specified up to 2.5 MHz. The divider is from the system clock,
// and it is a parameter because getting it wrong is a common integration
// fault that produces a bus no device answers.
parameter int unsigned MDC_DIVIDER = 40,
parameter int unsigned DIV_W = $clog2(MDC_DIVIDER + 1)
) (
input logic clk,
input logic rst_n,
// ── Request ─────────────────────────────────────────────────────────────
input logic req_valid,
input logic req_is_read,
input logic [4:0] req_phyad,
input logic [4:0] req_regad,
input logic [15:0] req_wdata,
output logic req_ready,
// ── Response ────────────────────────────────────────────────────────────
output logic rsp_valid,
output logic [15:0] rsp_rdata,
// The device did not take the wire during turnaround. Either no device
// responded at that address, or the bus is faulty -- Section 10 separates
// those two, and they have different owners.
output logic rsp_no_response,
// ── Pins ────────────────────────────────────────────────────────────────
output logic mdc,
output logic mdio_out,
output logic mdio_oe, // drive enable; low releases the wire
input logic mdio_in
);
typedef enum logic [2:0] {
M_IDLE, M_PREAMBLE, M_FRAME, M_DONE
} m_state_e;
m_state_e state_q;
logic [DIV_W-1:0] div_q;
logic [5:0] bit_q;
logic [31:0] shift_q;
logic [15:0] rdata_q;
logic is_read_q;
logic saw_device_q;
wire mdc_rise = (div_q == DIV_W'(MDC_DIVIDER - 1));
wire mdc_fall = (div_q == DIV_W'(MDC_DIVIDER / 2 - 1));
// THE TURNAROUND RULE, expressed against the bit counter alone.
//
// On a READ the master drives bits 0..14 -- through the first TA bit --
// and releases from bit 15 onward. On a WRITE it drives the whole frame.
//
// Deriving this from a bit index rather than from a state means it cannot
// be off by a cycle, which is exactly the failure this module exists to
// avoid.
wire drive_c = (state_q == M_PREAMBLE)
|| ((state_q == M_FRAME)
&& (!is_read_q || (bit_q < 6'd15)));
assign req_ready = (state_q == M_IDLE);
assign mdio_oe = drive_c;
assign mdio_out = (state_q == M_PREAMBLE) ? 1'b1 : shift_q[31];
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state_q <= M_IDLE;
div_q <= '0;
bit_q <= '0;
shift_q <= '0;
rdata_q <= '0;
is_read_q <= 1'b0;
saw_device_q <= 1'b0;
mdc <= 1'b0;
rsp_valid <= 1'b0;
rsp_rdata <= '0;
rsp_no_response <= 1'b0;
end else begin
rsp_valid <= 1'b0;
div_q <= (div_q == DIV_W'(MDC_DIVIDER - 1)) ? '0 : div_q + 1'b1;
if (mdc_rise) mdc <= 1'b1;
if (mdc_fall) mdc <= 1'b0;
unique case (state_q)
M_IDLE: begin
if (req_valid) begin
is_read_q <= req_is_read;
// Assemble the frame once, most-significant bit first.
shift_q <= {ST_C22,
req_is_read ? OP_C22_READ : OP_C22_WRITE,
req_phyad, req_regad,
// On a write the master drives TA as 10. On a
// read these two bits are not driven at all.
2'b10,
req_wdata};
bit_q <= '0;
saw_device_q <= 1'b0;
state_q <= M_PREAMBLE;
end
end
M_PREAMBLE: if (mdc_rise) begin
if (bit_q == 6'(PREAMBLE_BITS - 1)) begin
bit_q <= '0;
state_q <= M_FRAME;
end else begin
bit_q <= bit_q + 1'b1;
end
end
M_FRAME: if (mdc_rise) begin
// Sample on the rising edge; the device drives on the falling one.
if (is_read_q && (bit_q >= 6'd16)) begin
rdata_q <= {rdata_q[14:0], mdio_in};
end
// During the second TA bit a responding device pulls the line
// low. Nothing pulling it low means nothing answered.
if (is_read_q && (bit_q == 6'd15) && !mdio_in) saw_device_q <= 1'b1;
shift_q <= {shift_q[30:0], 1'b0};
if (bit_q == 6'(FRAME_BITS - 1)) state_q <= M_DONE;
else bit_q <= bit_q + 1'b1;
end
M_DONE: begin
rsp_valid <= 1'b1;
rsp_rdata <= rdata_q;
rsp_no_response <= is_read_q && !saw_device_q;
state_q <= M_IDLE;
end
default: state_q <= M_IDLE;
endcase
end
end
endmoduleClassification: synthesizable.
What it teaches: that the drive enable must be derived from an explicit bit index, not from a state. A state-derived enable can be entered or left one cycle early under any timing change, and the resulting contention corrupts exactly one data bit — producing reads that are usually correct and occasionally wrong in the same position, which is one of the hardest failure signatures to interpret.
Deliberately simplified: no tri-state pad, and no modelling of the pull-up that holds the wire during turnaround. Both are real and both belong to the physical implementation.
Production implication: rsp_no_response is the difference between a diagnosable bus and an opaque one. A read of an address where no device exists returns whatever the pull-up gives — all ones — and all ones is a plausible register value. Without detecting that nothing pulled the line low during turnaround, an absent device is indistinguishable from a device reporting 0xFFFF. Section 10 makes that distinction the basis of its telemetry.
5. RTL 2 — The MDIO Slave
// SYNTHESIZABLE. Clause 22 MDIO slave.
//
// NO REGISTER MAP IS ENCODED. Register meanings are vendor-specific and
// clause-specific; the module presents a generic read/write port and the
// map lives where it belongs.
//
// The slave's turnaround rule is the EXACT MIRROR of the master's, and the
// two must agree bit for bit. The slave starts driving on the SECOND TA
// bit -- bit 15 -- and drives through the data field. One bit either way
// and the frame still completes, with one bit sampled from contention.
module mdio_slave
import mdio_pkg::*;
#(
parameter logic [4:0] MY_PHYAD = 5'd1
) (
input logic rst_n,
input logic mdc,
input logic mdio_in,
output logic mdio_out,
output logic mdio_oe,
// Register file interface. Deliberately generic.
output logic reg_wr,
output logic reg_rd,
output logic [4:0] reg_addr,
output logic [15:0] reg_wdata,
input logic [15:0] reg_rdata,
// A frame addressed to a different device. Counted, because a bus with
// many devices should see mostly these -- and a device seeing NONE is
// probably not connected to the bus it thinks it is.
output logic frame_not_for_me
);
typedef enum logic [1:0] { S_HUNT, S_FRAME, S_TURN, S_DATA } s_state_e;
s_state_e state_q;
logic [5:0] bit_q;
logic [31:0] sh_q;
logic [15:0] rdata_q;
logic is_read_q, for_me_q;
logic [5:0] ones_q; // consecutive ones seen, for preamble detection
// Drive only on the second TA bit onward, and only for a read addressed
// to us. Same bit-index derivation as the master, for the same reason.
wire drive_c = for_me_q && is_read_q
&& ((state_q == S_TURN) || (state_q == S_DATA));
assign mdio_oe = drive_c;
assign mdio_out = (state_q == S_TURN) ? 1'b0 // pull low: "I am here"
: rdata_q[15];
assign reg_addr = sh_q[9 +: 5];
assign reg_wdata = sh_q[15:0];
always_ff @(posedge mdc or negedge rst_n) begin
if (!rst_n) begin
state_q <= S_HUNT;
bit_q <= '0;
sh_q <= '0;
rdata_q <= '0;
is_read_q <= 1'b0;
for_me_q <= 1'b0;
ones_q <= '0;
reg_wr <= 1'b0;
reg_rd <= 1'b0;
frame_not_for_me <= 1'b0;
end else begin
reg_wr <= 1'b0;
reg_rd <= 1'b0;
frame_not_for_me <= 1'b0;
unique case (state_q)
S_HUNT: begin
// A preamble is 32 consecutive ones. Counting them is how a
// device finds a frame boundary on a bus with no framing signal
// -- the same evidence-accumulation argument Chapter 3.5 §9 made
// for block alignment, at a much lower rate.
if (mdio_in) begin
if (ones_q >= 6'd31) begin
ones_q <= '0;
bit_q <= '0;
state_q <= S_FRAME;
end else begin
ones_q <= ones_q + 1'b1;
end
end else begin
ones_q <= '0;
end
end
S_FRAME: begin
sh_q <= {sh_q[30:0], mdio_in};
// Once the address fields have arrived, decide whether this frame
// is ours and what kind it is.
if (bit_q == 6'd13) begin
is_read_q <= (sh_q[11:10] == OP_C22_READ);
for_me_q <= (sh_q[9:5] == MY_PHYAD);
if (sh_q[9:5] != MY_PHYAD) frame_not_for_me <= 1'b1;
end
if (bit_q == 6'd14) begin
state_q <= S_TURN;
if (for_me_q && is_read_q) begin
reg_rd <= 1'b1;
rdata_q <= reg_rdata;
end
end
bit_q <= bit_q + 1'b1;
end
S_TURN: begin
state_q <= S_DATA;
bit_q <= '0;
end
S_DATA: begin
if (is_read_q) rdata_q <= {rdata_q[14:0], 1'b0};
else sh_q <= {sh_q[30:0], mdio_in};
if (bit_q == 6'd15) begin
if (for_me_q && !is_read_q) reg_wr <= 1'b1;
state_q <= S_HUNT;
for_me_q <= 1'b0;
end else begin
bit_q <= bit_q + 1'b1;
end
end
endcase
end
end
endmoduleClassification: synthesizable.
What it teaches: that the slave finds frames by counting 32 consecutive ones, because the bus has no framing signal. That is evidence accumulation on a shared wire — structurally the same argument Chapter 3.5 §9 made for block alignment, at five orders of magnitude lower rate, and it works for the same reason: a long run of ones is a pattern ordinary traffic does not produce.
Deliberately simplified: no register map, no Clause 45 support, and clocking directly from MDC rather than synchronising it into a system clock domain — which a real design must do, and which Chapter 4.6 owns.
Production implication: the slave pulls the line low on the first driven turnaround bit, and that is not decoration. It is how the master distinguishes "this device answered" from "nothing is at this address". Without it, an absent device and a device reporting all-ones are identical, and rsp_no_response cannot exist.
And frame_not_for_me is worth counting even though it is not an error. On a bus with several devices, most frames are for someone else — so a device seeing zero such frames is probably not connected to the bus it believes it is, which is a wiring fault that no other signal reveals.
6. RTL 3 — Clause 45 Indirect Addressing
// SYNTHESIZABLE. Clause 45 indirect addressing.
//
// Two frames per access: an ADDRESS frame stores a 16-bit register address,
// then a DATA frame reads or writes at it. That buys about 67 million
// addressable registers -- 32 ports x 32 MMDs x 65536 -- from a frame whose
// register field is only 5 bits wide.
//
// THE HAZARD THIS MODULE MAKES VISIBLE: the stored address is PERSISTENT
// STATE, shared by everyone who touches this port. Two software threads
// issuing Clause 45 accesses without coordination corrupt each other --
// one sets an address, the other overwrites it, the first reads the wrong
// register and gets a plausible value.
//
// Nothing in the protocol detects this. Only the sequence counter below
// can, and only if someone looks.
module mdio_c45_addressing
import mdio_pkg::*;
#(
parameter int unsigned CNT_W = 20
) (
input logic clk,
input logic rst_n,
input logic frame_valid,
input logic [1:0] frame_op,
input logic [4:0] frame_prtad,
input logic [4:0] frame_devad,
input logic [15:0] frame_data,
// The stored address, per MMD. Persistent between transactions.
output logic [15:0] current_addr [32],
output logic addr_valid [32],
output logic do_read,
output logic do_write,
output logic [15:0] access_addr,
// ── Observability ───────────────────────────────────────────────────────
// A data frame arrived for an MMD whose address was never set. The access
// targets whatever the address register happened to hold -- a plausible
// value from a wrong register, which is worse than an error.
output logic unaddressed_access,
output logic [CNT_W-1:0] c_unaddressed_access,
// An address frame overwrote an address that had been set and not yet
// used. Almost always two uncoordinated accessors, and the signature of
// the hazard above.
output logic addr_overwritten,
output logic [CNT_W-1:0] c_addr_overwritten,
// Address frames against data frames. A ratio near one means every access
// is paying the two-frame cost; well below one means read-and-increment
// is being used, which is a 45 percent saving on bulk reads.
output logic [CNT_W-1:0] c_addr_frames,
output logic [CNT_W-1:0] c_data_frames
);
logic [15:0] addr_q [32];
logic valid_q [32];
logic used_q [32];
always_comb begin
unaddressed_access = frame_valid
&& ((frame_op == OP_C45_READ) || (frame_op == OP_C45_WRITE)
|| (frame_op == OP_C45_READ_INC))
&& !valid_q[frame_devad];
addr_overwritten = frame_valid && (frame_op == OP_C45_ADDR)
&& valid_q[frame_devad] && !used_q[frame_devad];
do_read = frame_valid && ((frame_op == OP_C45_READ)
|| (frame_op == OP_C45_READ_INC));
do_write = frame_valid && (frame_op == OP_C45_WRITE);
access_addr = addr_q[frame_devad];
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (int unsigned d = 0; d < 32; d++) begin
addr_q[d] <= '0;
valid_q[d] <= 1'b0;
used_q[d] <= 1'b0;
end
c_unaddressed_access <= '0;
c_addr_overwritten <= '0;
c_addr_frames <= '0;
c_data_frames <= '0;
end else if (frame_valid) begin
unique case (frame_op)
OP_C45_ADDR: begin
addr_q[frame_devad] <= frame_data;
valid_q[frame_devad] <= 1'b1;
used_q[frame_devad] <= 1'b0;
if (!(&c_addr_frames)) c_addr_frames <= c_addr_frames + 1'b1;
if (addr_overwritten && !(&c_addr_overwritten))
c_addr_overwritten <= c_addr_overwritten + 1'b1;
end
OP_C45_READ_INC: begin
// The optimisation that makes bulk reads affordable: advance the
// stored address so the next data frame needs no address frame.
addr_q[frame_devad] <= addr_q[frame_devad] + 16'd1;
used_q[frame_devad] <= 1'b1;
if (!(&c_data_frames)) c_data_frames <= c_data_frames + 1'b1;
end
default: begin // READ or WRITE
used_q[frame_devad] <= 1'b1;
if (!(&c_data_frames)) c_data_frames <= c_data_frames + 1'b1;
end
endcase
if (unaddressed_access && !(&c_unaddressed_access))
c_unaddressed_access <= c_unaddressed_access + 1'b1;
end
end
assign current_addr = addr_q;
assign addr_valid = valid_q;
endmoduleClassification: synthesizable.
What it teaches: that indirect addressing introduces shared persistent state, and that shared state on a bus with several accessors is a hazard the protocol itself cannot detect. A read that targets the wrong register because someone else moved the address pointer returns a plausible value, not an error — and plausible wrong values are the worst kind.
Deliberately simplified: the address store is per-MMD in one device. A real system has this state in every device on the bus, and coordination must span all of them.
Production implication: c_addr_overwritten is the only detector of the uncoordinated-accessor hazard, and it is cheap. An address set and overwritten before use is almost always two software threads sharing a bus without a lock — a defect that produces intermittently wrong register readings and is otherwise diagnosed only by inspection of the software.
And the ratio of c_addr_frames to c_data_frames is a performance finding. Near one means every access pays the two-frame cost; well below one means read-and-increment is being used properly. On a system that sweeps counter blocks regularly, that ratio is worth roughly half the management bandwidth.
7. RTL 4 — Sample Age, and Why It Is Not Optional
The chapter's central claim made into hardware: a management read is a sample, and a sample without an age is a value you cannot reason about.
// SYNTHESIZABLE. Sample age tracking for management reads.
//
// A Clause 22 transaction takes about 26 microseconds at 2.5 MHz; a Clause
// 45 addressed read takes about 52. During that time the link keeps running.
// The value that reaches software describes a moment that has PASSED.
//
// That is not a defect to fix -- it is what a management path is. What IS a
// defect is presenting the value without its age, because software then
// reasons about the link's present state from a stale sample.
//
// Chapter 3.2 §11 built this for optical modules and called it module
// status. It is not module-specific. It is what every management path does.
module mgmt_sample_age #(
parameter int unsigned FIELDS = 8,
parameter int unsigned AGE_W = 24,
// Cycles after which a sample is too old to reason from. A link event
// takes microseconds; a management sweep takes milliseconds.
parameter int unsigned STALE_AFTER = 250_000
) (
input logic clk,
input logic rst_n,
input logic sample_valid,
input logic [$clog2(FIELDS)-1:0] sample_index,
input logic [15:0] sample_data,
input logic sample_failed, // the read did not complete
output logic [15:0] value [FIELDS],
output logic has_value [FIELDS],
output logic [AGE_W-1:0] age [FIELDS],
output logic is_stale [FIELDS],
// The oldest field in the set. Software reasoning across several
// registers is only as current as its oldest sample, and a design that
// reports per-field ages without this makes that easy to forget.
output logic [AGE_W-1:0] worst_age,
output logic [$clog2(FIELDS)-1:0] worst_field,
// A read that never completed. Distinct from a stale value: stale means
// old, failed means absent, and they need different responses.
output logic [15:0] c_read_failed
);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (int unsigned f = 0; f < FIELDS; f++) begin
value[f] <= '0;
has_value[f] <= 1'b0;
age[f] <= '0;
end
c_read_failed <= '0;
end else begin
// Every field ages every cycle. Ageing is the default; being fresh
// is the exception that a completed read produces.
for (int unsigned f = 0; f < FIELDS; f++) begin
if (has_value[f] && (age[f] != '1)) age[f] <= age[f] + 1'b1;
end
if (sample_valid && !sample_failed) begin
value[sample_index] <= sample_data;
has_value[sample_index] <= 1'b1;
age[sample_index] <= '0;
end
if (sample_valid && sample_failed && !(&c_read_failed))
c_read_failed <= c_read_failed + 1'b1;
end
end
always_comb begin
for (int unsigned f = 0; f < FIELDS; f++)
is_stale[f] = has_value[f] && (age[f] >= AGE_W'(STALE_AFTER));
worst_age = '0;
worst_field = '0;
for (int unsigned f = 0; f < FIELDS; f++) begin
if (has_value[f] && (age[f] > worst_age)) begin
worst_age = age[f];
worst_field = ($clog2(FIELDS))'(f);
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that ageing is the default and freshness is the exception. Every field ages every cycle; only a completed read resets it. That inversion is the right way round, because a design where staleness must be actively detected will forget to detect it somewhere.
Deliberately simplified: a single staleness threshold for all fields. In practice a link-status bit goes stale in microseconds while a device identifier never does, and production designs carry per-field thresholds.
Production implication: worst_age exists because software reasoning across several registers is only as current as its oldest sample. A sweep that reads eight registers over half a millisecond and then correlates them is comparing values from eight different moments — and a design reporting only per-field ages makes that easy to overlook. The worst age is the age of the conclusion.
And c_read_failed is distinct from staleness deliberately. Stale means old; failed means absent. A stale value is still evidence about the past; a failed read is no evidence at all, and software that treats a failed read as a zero will make a confident wrong decision.
8. RTL 5 — Bus Fault Against Absent Device
// SYNTHESIZABLE INSTRUMENTATION.
//
// Reading an address where NO DEVICE EXISTS returns whatever the pull-up
// gives: all ones. And 0xFFFF is a plausible register value.
//
// So three situations produce the same 16 bits:
// - no device at that address -> a configuration or wiring fault
// - a device that genuinely reads all ones -> normal
// - a bus stuck high (broken driver, missing pull-down path) -> hardware
//
// The ONLY thing that separates them is whether something pulled the line
// low during turnaround, and whether it did so consistently. That is what
// this module records.
module mdio_bus_telemetry #(
parameter int unsigned CNT_W = 20
) (
input logic clk,
input logic rst_n,
input logic clear,
input logic rsp_valid,
input logic rsp_is_read,
input logic [4:0] rsp_phyad,
input logic [15:0] rsp_rdata,
input logic rsp_no_response, // nothing drove during turnaround
// Per-address evidence. A device that has EVER responded exists; one that
// never has is absent or misaddressed -- and the two need different
// investigations, exactly as Chapter 3.1 §9's ever_ready distinguished a
// broken pair from a marginal one.
output logic [31:0] device_ever_responded,
output logic [31:0] device_responding_now,
output logic [CNT_W-1:0] c_reads,
output logic [CNT_W-1:0] c_no_response,
// Reads that returned all ones AND had a responder. Normal, and worth
// separating from the no-responder case so the two are not conflated.
output logic [CNT_W-1:0] c_all_ones_with_responder,
// Every address on the bus reads all ones with nothing responding. That
// is not 32 absent devices -- it is one stuck bus, and the distinction
// is the difference between a wiring check and a driver replacement.
output logic bus_stuck_high_suspected,
// A device that responded before and does not now. Distinct from one that
// never has: this one was working.
output logic device_lost,
output logic [4:0] lost_address
);
logic [31:0] ever_q, now_q;
logic [31:0] seen_q; // addresses actually probed since reset
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
ever_q <= '0;
now_q <= '0;
seen_q <= '0;
c_reads <= '0;
c_no_response <= '0;
c_all_ones_with_responder <= '0;
device_lost <= 1'b0;
lost_address <= '0;
end else begin
device_lost <= 1'b0;
if (clear) begin
c_reads <= '0;
c_no_response <= '0;
c_all_ones_with_responder <= '0;
// ever_q deliberately survives: it is a property of the
// installation, not of a measurement window.
end
if (rsp_valid && rsp_is_read) begin
seen_q[rsp_phyad] <= 1'b1;
if (!(&c_reads)) c_reads <= c_reads + 1'b1;
if (rsp_no_response) begin
now_q[rsp_phyad] <= 1'b0;
if (!(&c_no_response)) c_no_response <= c_no_response + 1'b1;
// It answered before and does not now. That is a loss, not an
// absence, and it points at the device rather than the map.
if (ever_q[rsp_phyad]) begin
device_lost <= 1'b1;
lost_address <= rsp_phyad;
end
end else begin
ever_q[rsp_phyad] <= 1'b1;
now_q[rsp_phyad] <= 1'b1;
if ((rsp_rdata == 16'hFFFF) && !(&c_all_ones_with_responder))
c_all_ones_with_responder <= c_all_ones_with_responder + 1'b1;
end
end
end
end
assign device_ever_responded = ever_q;
assign device_responding_now = now_q;
// Every probed address silent. One stuck bus, not many absent devices.
assign bus_stuck_high_suspected = (seen_q != '0) && (now_q == '0)
&& (ever_q == '0);
endmoduleClassification: synthesizable instrumentation.
What it teaches: that a value alone cannot distinguish three different situations, and that the turnaround response is the only thing that can. All ones is what an absent device, a stuck bus, and a genuinely-all-ones register all return — and only whether something pulled the line low separates them.
Deliberately simplified: no timing analysis of when during turnaround the response occurred, which a real design uses to detect the one-bit-early and one-bit-late failures of Section 2's callout.
Production implication: bus_stuck_high_suspected is the check worth having and it is nearly free. Thirty-two absent devices is not a plausible configuration; one stuck bus is — and reporting the first when the second is true sends someone to check thirty-two addresses in a device map instead of one pull-up or one driver.
And device_lost against a never-responding address is the same distinction Chapter 3.1 §9 drew with ever_ready. A device that answered before and does not now has failed; one that never answered is misaddressed or absent. Same reading, opposite investigations, and only the sticky history separates them.
9. RTL 6 — Turnaround Contention Detection
Section 2's callout named the turnaround as where implementations break, and Section 11's debugging makes it the second check. Nothing so far detects it — and the detection has to live in the master, because the master is the only participant that knows when it stopped driving.
// SYNTHESIZABLE INSTRUMENTATION. Lives in the master.
//
// A testbench can see both output enables and check contention directly.
// SILICON CANNOT -- no device exposes its enable. So this infers from the
// wire's behaviour during the two turnaround bits, which is weaker and is
// what is actually available.
//
// The three things the wire can do during turnaround, and what each means:
//
// stays high, then data -> nothing responded. Absent device.
// goes low on bit 2, data -> normal. The device announced itself.
// goes low on bit 1 -> the device drove EARLY, overlapping the
// master's last driven bit. Contention.
//
// The third is the failure that produces reads wrong in one bit position,
// and it is invisible in the returned data because the contended bit is
// usually resolved to a plausible value.
module mdio_turnaround_monitor #(
parameter int unsigned CNT_W = 16
) (
input logic clk,
input logic rst_n,
input logic clear,
input logic frame_active,
input logic is_read,
input logic [5:0] bit_index, // position within the 32-bit frame
input logic mdio_in,
input logic master_oe,
// The device drove during the FIRST turnaround bit, while the master was
// still driving. Direct contention.
output logic contention_detected,
output logic [CNT_W-1:0] c_contention,
// The device never drove during either turnaround bit. Either absent, or
// it drove late and the first data bit was sampled from the pull-up.
output logic late_or_absent,
output logic [CNT_W-1:0] c_late_or_absent,
// Turnarounds that behaved exactly as specified. The denominator: a
// contention rate is meaningless without knowing how many were clean.
output logic [CNT_W-1:0] c_clean_turnaround,
// Sticky. Contention is intermittent by nature -- it depends on
// temperature, supply and the exact clock relationship -- so by the time
// anyone looks it has usually stopped. This bit is what remains.
output logic contention_ever
);
// Bit 14 is the first turnaround bit, 15 the second. On a read the master
// drives through 14 and releases from 15, per Section 4's rule.
wire ta_first_c = frame_active && is_read && (bit_index == 6'd14);
wire ta_second_c = frame_active && is_read && (bit_index == 6'd15);
logic saw_low_first_q, saw_low_second_q;
always_comb begin
// The device pulling low while the master is still driving.
contention_detected = ta_first_c && master_oe && !mdio_in;
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
saw_low_first_q <= 1'b0;
saw_low_second_q <= 1'b0;
c_contention <= '0;
c_late_or_absent <= '0;
c_clean_turnaround <= '0;
late_or_absent <= 1'b0;
contention_ever <= 1'b0;
end else begin
late_or_absent <= 1'b0;
if (clear) begin
c_contention <= '0;
c_late_or_absent <= '0;
c_clean_turnaround <= '0;
// contention_ever deliberately survives -- see below.
end
if (ta_first_c) begin
saw_low_first_q <= !mdio_in;
if (contention_detected) begin
if (!(&c_contention)) c_contention <= c_contention + 1'b1;
contention_ever <= 1'b1;
end
end
if (ta_second_c) begin
saw_low_second_q <= !mdio_in;
// Classify the turnaround now that both bits are known.
if (!saw_low_first_q && !mdio_in) begin
// Clean: high through the first, low on the second.
if (!(&c_clean_turnaround)) c_clean_turnaround <= c_clean_turnaround + 1'b1;
end else if (!saw_low_first_q && mdio_in) begin
// Nothing ever drove.
late_or_absent <= 1'b1;
if (!(&c_late_or_absent)) c_late_or_absent <= c_late_or_absent + 1'b1;
end
end
end
end
endmoduleClassification: synthesizable instrumentation.
What it teaches: that a fault which is trivially checkable in a testbench may be only inferable in silicon, and that the inference is still worth building. A testbench sees both output enables and can assert they are never simultaneously high. A device does not expose its enable, so the master must infer contention from the wire's behaviour — weaker evidence, and the only evidence there is.
Deliberately simplified: it classifies at bit granularity, and real contention is a sub-bit-time overlap that may or may not be visible at the sampling instant. A production monitor with a faster sampling clock catches more of it.
Production implication: contention_ever is sticky and survives clear because contention is intermittent by its nature. It depends on temperature, on supply, and on the exact phase relationship between the master's release and the device's drive — so it appears, disappears, and is usually not happening when anyone investigates. The sticky bit is what remains, and it converts "reads are occasionally wrong" into "this bus has had contention", which names the turnaround immediately.
And c_clean_turnaround is the denominator. A contention count of forty means nothing without knowing whether that is out of fifty transactions or fifty million — and a rate is what distinguishes a marginal timing relationship from a broken one.
10. Assertions
Some properties below rest on the published Clause 22 and Clause 45 frame structures — field widths, the ST and OP encodings, and the two-frame indirect sequence are defined by IEEE 802.3. The state machines, counters and staleness thresholds are implementation choices, and each property says which it is.
// ─── FRAME PROPERTY: Clause 22 field widths and order ──────────────────────
// Published structure. Catches a frame assembled with fields transposed,
// which addresses the wrong register in the wrong device and returns a
// plausible value.
property p_c22_frame_structure;
@(posedge clk) disable iff (!rst_n)
(state_q == M_FRAME) && (bit_q == 0) |-> (shift_q[31:30] == ST_C22);
endproperty
// ─── FRAME PROPERTY: the read opcode is 10, write is 01 ────────────────────
// Catches the two transposed, which turns every read into a write to the
// same address -- corrupting configuration while appearing to read it.
property p_c22_opcode;
@(posedge clk) disable iff (!rst_n)
(state_q == M_FRAME) && (bit_q == 0)
|-> (shift_q[29:28] == (is_read_q ? OP_C22_READ : OP_C22_WRITE));
endproperty
// ─── Safety: the master releases the wire before the device drives ─────────
// THE property of this chapter. Catches an overlap of even one bit time,
// which produces contention and a read that is usually correct and
// occasionally wrong in the same bit position.
property p_master_releases_for_read;
@(posedge clk) disable iff (!rst_n)
(is_read_q && (state_q == M_FRAME) && (bit_q >= 15)) |-> !mdio_oe;
endproperty
// ─── Safety: the slave drives only its own read data ───────────────────────
// The mirror. Catches a slave driving on a frame addressed elsewhere, which
// corrupts another device's response.
property p_slave_drives_only_when_addressed;
@(posedge mdc) disable iff (!rst_n)
mdio_oe |-> (for_me_q && is_read_q);
endproperty
// ─── Mutual exclusion: never two drivers ───────────────────────────────────
// The contention check itself, expressible only where both enables are
// visible -- so it is a testbench property, and Section 10's note explains
// why that is acceptable here and was not in Chapter 4.1.
property p_never_two_drivers;
@(posedge mdc) disable iff (!rst_n)
!(master_oe && slave_oe);
endproperty
// ─── Ordering: a data frame follows an address frame ───────────────────────
// Clause 45's sequence. Catches a data frame issued with no address set,
// which accesses whatever the pointer held -- a plausible value from a
// wrong register.
property p_c45_data_needs_address;
@(posedge clk) disable iff (!rst_n)
(do_read || do_write) |-> addr_valid[frame_devad];
endproperty
// ─── Causation: read-and-increment advances the address ────────────────────
// Catches an increment that does not happen, turning a bulk sweep into
// repeated reads of one register -- which returns plausible data and looks
// like a device whose counters are stuck.
property p_read_inc_advances;
@(posedge clk) disable iff (!rst_n)
(frame_valid && (frame_op == OP_C45_READ_INC))
|=> (current_addr[$past(frame_devad)] == $past(current_addr[$past(frame_devad)]) + 1);
endproperty
// ─── Safety: ageing is the default ─────────────────────────────────────────
// Catches an implementation where freshness must be actively detected,
// which will forget to detect it somewhere and present a stale value as
// current.
property p_fields_age_by_default;
@(posedge clk) disable iff (!rst_n)
(has_value[0] && !(sample_valid && (sample_index == 0)))
|=> (age[0] >= $past(age[0]));
endproperty
// ─── Causation: only a completed read resets an age ────────────────────────
// Catches a failed read resetting the age, which presents an absent value
// as a fresh one.
property p_failed_read_does_not_refresh;
@(posedge clk) disable iff (!rst_n)
(sample_valid && sample_failed) |=> (age[$past(sample_index)] != 0);
endproperty
// ─── Conservation: worst_age is the maximum over valid fields ──────────────
// Catches a worst-age that ignores a field, after which software reasoning
// across registers believes its conclusion is fresher than it is.
property p_worst_age_is_max;
@(posedge clk) disable iff (!rst_n)
has_value[0] |-> (worst_age >= age[0]);
endproperty
// ─── Causation: no-response requires no turnaround drive ───────────────────
// Catches a design inferring absence from the DATA rather than from the
// turnaround, which cannot distinguish an absent device from one reporting
// all ones.
property p_no_response_from_turnaround;
@(posedge clk) disable iff (!rst_n)
rsp_no_response |-> !$past(saw_device_q);
endproperty
// ─── Stability: the ever-responded history survives a clear ────────────────
// Catches it folded into the clear branch, destroying the distinction
// between a device that failed and one that was never there.
property p_ever_responded_sticky;
@(posedge clk) disable iff (!rst_n)
clear |=> (device_ever_responded == $past(device_ever_responded));
endproperty
// ─── Safety: a stuck bus is not reported as many absent devices ────────────
// Catches the conclusion that 32 devices are missing, which sends someone
// to check a device map instead of one pull-up.
property p_stuck_bus_not_absent_devices;
@(posedge clk) disable iff (!rst_n)
bus_stuck_high_suspected |-> (device_ever_responded == '0);
endproperty
// ─── Causation: contention is inferred only during turnaround ──────────────
// Catches a monitor that flags contention on data bits, where the device is
// legitimately driving and the master is not.
property p_contention_only_in_turnaround;
@(posedge clk) disable iff (!rst_n)
contention_detected |-> (bit_index == 6'd14) && master_oe;
endproperty
// ─── Stability: the contention history survives a clear ────────────────────
// Contention is intermittent and usually not happening when anyone looks.
// Catches the sticky bit folded into the clear branch, removing the only
// evidence that remains.
property p_contention_ever_sticky;
@(posedge clk) disable iff (!rst_n)
contention_ever |=> contention_ever;
endproperty11. Verification
Read the last message. The value software receives is not the device's present state; it is what the device held when the data frame's turnaround happened. Section 7's ageing exists because that gap is real and unavoidable.
Scenarios
- A Clause 22 write, then a read of a plain read/write register. Verify the value round-trips. This is valid only because the register is documented side-effect-free — Section 10's rejected property explains why it is not a general check.
- A Clause 22 read of an address with no device. Verify
rsp_no_responseasserts and the returned data is not trusted, even though it reads all ones. - A read of a device that genuinely reads all ones. Verify
rsp_no_responsestays low. Together with scenario 2 this proves the turnaround detection works and the data alone cannot. - Frame field order. Drive a known device and register address and verify the bits appear in the published order on the wire. A transposed field addresses the wrong register and returns a plausible value.
- Turnaround, exact. Verify the master's output enable falls before the slave's rises, with no cycle of overlap. Sample both enables and assert they are never simultaneously high.
- Turnaround, one bit early and one bit late. Two fault-injection runs. Verify contention is detected — this is the failure that produces reads wrong in one bit position, and a suite that never injects it will not find it.
- A frame addressed to another device. Verify the slave does not drive and
frame_not_for_meadvances. - Preamble detection. Feed fewer than 32 ones followed by a frame and verify the slave does not lock onto it.
- A Clause 45 address frame followed by a data read. Verify the access targets the addressed register.
- A Clause 45 data frame with no preceding address frame. Verify
unaddressed_accessfires rather than the access proceeding against a stale pointer. - Two address frames before a data frame. Verify
addr_overwrittenfires — the signature of two uncoordinated accessors. - Read-and-increment across a block. Issue one address frame and twenty read-increment data frames. Verify the addresses advance by one each time and the ratio of address to data frames reflects the saving.
- A latching-high status bit. Set it, read it, read it again. Verify the second read returns zero and that no assertion fires — this is correct behaviour, and scenario 1's check must not be applied here.
- A self-clearing command bit. Write a one, verify the action occurs and a subsequent read returns zero.
- Sample ageing. Read a field, hold, and verify its age increments every cycle and
is_staleasserts at the threshold. - A failed read. Verify the age is not reset — a failed read is absence of evidence, not fresh evidence.
- Worst-age across fields. Read several fields at different times and verify
worst_agereports the oldest, not an average. - Device lost against never present. Two runs. Verify a device that responded and then stopped sets
device_lost, and one that never responded does not — same reading, different investigations. - A stuck bus. Make every address silent and verify
bus_stuck_high_suspectedrather than thirty-two absent-device reports. - Turnaround contention, injected. Make the device drive on the first turnaround bit while the master is still driving. Verify
contention_detectedfires andcontention_eversticks across aclear. - Turnaround late or absent. Make the device never drive during either turnaround bit and verify
late_or_absentrather than contention — the same wrong reading, an opposite cause.
What the checker must own
- Both output enables visible simultaneously. Contention is the failure mode of this interface and it is only observable where both drivers are. This is the rare case where a cross-boundary testbench property is correct — the bus is genuinely shared, unlike Chapter 4.1 §9's rejected cross-domain comparison.
- A register model with all four behaviours — plain read/write, latching-high, self-clearing and read-only-within-writable. A model with only the first will pass a design that breaks the other three.
- Coordinated datapath and management stimulus, so Scenario 20 can be constructed. Independent sequences cannot produce it.
- Coverage crosses of opcode against device presence against turnaround timing. The bin
(read, no device, no responder detected)must be populated, and(master driving, slave driving)must be unreachable.
12. Debugging — Turnaround, Presence, Then Age
The symptom: management reads returning wrong or implausible values.
Step 1 — check whether anything responded at all. rsp_no_response separates the two situations that produce identical data:
| Reading | What it means | Where to go |
|---|---|---|
rsp_no_response set, device_ever_responded clear | nothing was ever at that address | the device map or the wiring |
rsp_no_response set, device_ever_responded set | it worked before and does not now | the device — it has failed or reset |
bus_stuck_high_suspected | every address silent | one bus fault, not many absent devices |
| responded, value implausible | the transaction worked | go to step 2 |
Row three is the one that saves the most time. Thirty-two absent devices is not a plausible configuration; one stuck pull-up or one broken driver is.
Step 2 — if values are wrong in a consistent bit position, it is the turnaround. This signature is nearly diagnostic on its own: reads that are usually correct and occasionally wrong, always the same bit, means the master and device disagree about the handover by one bit time. Section 4's bit-index derivation exists to make that impossible, and a design deriving drive enable from a state rather than an index is where to look.
Step 3 — if values are plausible but inconsistent, suspect the address pointer. On Clause 45, c_addr_overwritten names it directly: two accessors sharing a bus without coordination, one moving the pointer under the other. The reads return valid data from the wrong register, which is the worst kind of wrong.
Step 4 — if the values are right but the conclusions are wrong, read the ages. worst_age is the age of a multi-register conclusion, and a sweep that takes milliseconds is comparing values from different moments. A conclusion drawn from eight registers is only as current as the oldest of them.
Step 5 — if a status bit reads clear when an event definitely occurred, it was consumed. Latching-high bits clear on read, so a previous read — possibly by other software — already took it. That is not a fault; it is the mechanism, and it is why two independent pollers of the same status register will each see roughly half the events.
Step 6 — if a configuration read-back is correct and the hardware behaves as though it were not, stop reading registers. A read-back verifies storage, not effect. Some devices latch a configuration into use on a separate event, so the register can be right while the hardware runs on the previous value. Verify behaviourally.
The method stated once: presence before value, turnaround before content, pointer before plausibility, and age before conclusion — because a management read is a sample, and every one of those steps is a way of asking what the sample is actually a sample of.
13. Common Misconceptions
"MDIO is a debug interface."
The wrong model: a side channel for engineers, not part of the design proper.
What it costs: you give it no verification budget, no telemetry and no error handling. The interface is then the least reliable thing in the system and it is the only way to see anything, so every field investigation begins by debugging the debugging path.
The corrected model: it is the second boundary between the MAC and the PHY, and Chapter 4.1 §5 kept diagnostic detail off the datapath specifically so it could live here. It carries configuration that the link's behaviour depends on. It is a datapath for a different kind of information, and it needs the same rigour.
"A management read tells you the current state."
The wrong model: read the register, get the state.
What it costs: software correlates registers read at different moments and draws a conclusion about a state that never simultaneously existed. A link event inside a read window is invisible, or worse, consumed.
The corrected model: every read is a sample with an age. A Clause 22 transaction takes about 26 microseconds and a Clause 45 addressed read about 52; the link keeps running throughout. Section 7 makes ageing the default and freshness the exception, and worst_age is the age of a multi-register conclusion.
"Read-back verifies a write."
The wrong model: write, read, compare — the universal register test.
What it costs: Section 10's rejected property in full. It fires on latching-high status bits and self-clearing command bits, which behave that way on purpose, and the only way to satisfy it is to remove the behaviour — losing every event shorter than a polling interval.
The corrected model: a read-back verifies storage, and only for registers documented as side-effect-free. It does not verify effect: a device may latch a configuration into use on a separate event, so the register can read correctly while the hardware still runs on the previous setting. Verify configuration behaviourally.
"All ones means the register reads all ones."
The wrong model: the returned value is the register's value.
What it costs: an absent device, a stuck bus and a genuinely-all-ones register are indistinguishable, so a wiring fault is investigated as a device fault, or thirty-two addresses are checked for one broken pull-up.
The corrected model: the wire has a pull-up, so nothing driving reads as all ones. Only whether something pulled the line low during turnaround separates the three — which is why Section 4's master detects it and Section 8's telemetry is built on it.
"Clause 45 is just Clause 22 with more registers."
The wrong model: a bigger address field, same mechanism.
What it costs: you miss that the address is persistent shared state, so two uncoordinated accessors corrupt each other's addressing and read valid data from wrong registers. You also issue an address frame per access and take nearly twice as long as necessary.
The corrected model: the frame width did not change; Clause 45 spends two frames — an address frame that stores a 16-bit address, then a data frame that uses it. That buys about 67 million registers from a 5-bit field, at double the transaction cost, and read-and-increment exists to recover most of that cost on bulk reads.
14. Interview Reasoning
"Why does a PHY need a separate management interface at all?"
The weak answer is "for configuration". The answer that ends the topic contrasts the two boundaries: the datapath is parallel, synchronous to the link, and carries values that are true now; the management path is one wire at 2.5 MHz carrying values that were true when read. Diagnostic detail cannot go on the datapath boundary without making the MAC depend on PHY internals — Chapter 4.1 §5's argument — and fast-changing state cannot be reported coherently over a slow serial link. Two kinds of information, two contracts.
"A management read returns wrong values intermittently, always in the same bit. What is it?"
Almost certainly the turnaround. MDIO is one bidirectional wire, and if the master releases a bit late or the device drives a bit early, the frame still completes with one data bit sampled from contention. The signature — usually correct, occasionally wrong, always the same bit position — is nearly diagnostic. The fix is to derive drive enable from an explicit bit index rather than a state, so it cannot be off by a cycle.
"Why can you not verify a configuration by reading it back?"
Two reasons, and the second is the deeper one. First, whole classes of register do not read back what was written — latching-high status bits clear on read, self-clearing command bits clear themselves, and both behave that way on purpose. Second, and true even for ordinary registers: a read-back verifies storage, not effect. A device may latch a configuration into use on a separate event, so the register reads correctly while the hardware runs on the old value. Configuration is verified behaviourally.
15. Understanding Check
Because they carry different kinds of information, and the difference has one decisive property.
The datapath carries values that are true now. The management path carries values that were true when they were read.
| Datapath | Management path | |
|---|---|---|
| width | parallel, 4 to 32 bits | one wire |
| clock | link rate, tens to hundreds of MHz | up to 2.5 MHz, asynchronous |
| a transaction | one cycle | 64 bit times, about 26 µs |
| handshake | transmit only | every transaction |
A datapath signal is consumed in the cycle it is produced. A management read takes tens of microseconds, during which the link keeps running — the value describes a moment that has passed.
Which is why Chapter 4.1 §5 kept diagnostic detail off the datapath boundary. Not tidiness: putting FEC counts or eye margin there makes the MAC depend on PHY internals, and a slow serial link cannot report fast-changing state coherently anyway.
The follow-up to be ready for: is slow a problem? No — one wire at 2.5 MHz is cheap in pins and adequate for something read occasionally. The mistake is expecting it to report fast-changing state, not the speed itself.
16. What's Next
The claim this chapter defended: the management path is the datapath inverted, and it carries values that were true when they were read rather than values that are true now.
Everything follows from that. One wire at 2.5 MHz is cheap and adequate for something read occasionally, and its 26-microsecond transaction time is why a read is a sample with an age — which makes ageing the default, worst_age the age of a multi-register conclusion, and a failed read categorically different from a stale one. The turnaround exists because one wire needs an owner and is where designs break, with a signature — wrong in the same bit, intermittently — that is nearly diagnostic. Clause 45 buys five orders of magnitude of address space with a second frame, and introduces shared persistent state that the protocol cannot police.
And the read-back that everyone writes first verifies storage, not effect — and for latching and self-clearing registers, not even that.
Module 4 now has all five of its parts, and one thing has been deferred from every one of them. Chapter 4.2 showed generation adapters and left the crossing out. Chapter 4.3 put both directions in one clock domain so the sequence would be visible. Chapter 4.4 maintained a fill count in a single domain, said so, and made the reason that is dangerous its rejected property. This chapter clocked a slave directly from MDC.
Chapter 4.6 — The MAC/PHY Boundary in RTL assembles the module and finally owns what all four deferred: the real port list, the clock-domain crossing between MAC and PHY clocks, the reset boundary and its ordered release, and the elaboration-time checks that catch a configuration mismatch before simulation rather than after silicon.
The full path is on the Ethernet curriculum index.
Continue learning
Related tutorials
- Related topic
The PHY Layer
Coding, serialisation, clock recovery and line drive all exist because a real channel attenuates, disperses and carries no clock. The PCS, PMA and PMD split follows the same logic — each owns one consequence of physics, and each changes on its own schedule.
- Related topic
Copper Ethernet
From 1000BASE-T onward every twisted pair carries both directions at once, so each receiver hears its own transmitter louder than the far end. Cancelling a known local signal is why BASE-T PHYs are adaptive signal-processing engines, and why pair count and signalling changed at every generation.
- Related topic
Fibre Ethernet
Separate strands per direction delete the echo problem that shapes copper PHYs, and introduce two others: a conversion boundary inside a pluggable module you do not own, and a link that can break in one direction while the far end still reports perfect health.
- Related topic
Differential Signalling and the Analog Channel
A PHY does not read bits off a wire — it infers symbols from a waveform the channel has attenuated, reflected and smeared into its neighbours. Differential signalling, impedance, jitter and the eye are one subject: what margin is left after the channel takes its share.
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.
