Ethernet · Module 10
RGMII — Double Data Rate, and Half the Margin
Four bits on both edges of 125 MHz halves GMII's pins and halves the bit time. The clock must be delayed into the eye by about 2 ns, nothing says who does it, and both ways to get it wrong look identical.
Chapter 10.3 established that GMII's correctness is a physical-time relationship between a clock and eight data signals at the PHY's pins, and that RTL cannot see it.
RGMII takes that relationship and halves the margin.
Four data bits each direction instead of eight, clocked on both edges of a 125 MHz clock:
4 bits × 2 edges × 125 MHz = 1000 Mb/s
The same gigabit on half the wires — twelve signals against GMII's twenty-four, which is Chapter 10.2's trick applied to a source-synchronous interface instead of a shared-clock one.
And the bit time halves with it. GMII's data is valid for a whole 8 ns clock period. RGMII's is valid for 4 ns, because two bits share each period — so every picosecond of package delay, trace mismatch and clock-to-out now costs twice as much of the budget.
Which is why RGMII has a configuration item no other interface in this module has: a delay. Somebody must shift the clock relative to the data by roughly half a bit time, and there are three places to do it — inside the PHY, inside the MAC, or on the board. Nothing in the protocol says which, and both of the ways to get it wrong are symmetric.
1. Scope — What This Chapter Owns
This chapter owns RGMII: its pin list, its double-data-rate clocking, the control-signal encoding that folds two signals into one, the delay problem in all three of its forms, and the in-band status it carries in the interframe gap.
It does not re-derive what other chapters own. Chapter 9.3 §11 owns the 8 bits × 125 MHz arithmetic that produced GMII; Chapter 10.3 owns source-synchronous signalling and why the MAC sources the transmit clock; Chapter 10.2 owns the pin-reduction argument in its shared-clock form. Chapter 4.5 owns MDC and MDIO.
Chapter 10.5 owns SGMII, which reduces the pin count further by abandoning parallel data altogether.
The claim this chapter defends: a continuous configuration parameter with no protocol to establish it is worse than a discrete one — because its two failure modes are symmetric, both present as a working link that corrupts data, and a design can only distinguish them by measuring the thing the parameter is supposed to set.
2. Twelve Signals
| Signal | Width | Direction | Rising edge | Falling edge |
|---|---|---|---|---|
TXD[3:0] | 4 | MAC → PHY | data bits 3:0 | data bits 7:4 |
TX_CTL | 1 | MAC → PHY | TX_EN | TX_EN XOR TX_ER |
TXC | 1 | MAC → PHY | 125 / 25 / 2.5 MHz | |
RXD[3:0] | 4 | PHY → MAC | data bits 3:0 | data bits 7:4 |
RX_CTL | 1 | PHY → MAC | RX_DV | RX_DV XOR RX_ER |
RXC | 1 | PHY → MAC | 125 / 25 / 2.5 MHz | |
| 12 |
Plus MDC and MDIO: fourteen pins per port, against GMII's twenty-six.
Three things were folded, and each fold has a cost.
The data folded from 8 bits to 4, recovered by using both clock edges — which is the halved bit time and therefore the whole timing problem.
TX_EN and TX_ER folded onto one wire. Not multiplexed in time the way Chapter 10.2's CRS_DV is, but encoded: the rising edge carries TX_EN directly and the falling edge carries TX_EN XOR TX_ER, so both are recoverable from two samples. Elegant, and it means a control wire sampled on only one edge yields half a signal and a plausible-looking result.
CRS and COL are gone entirely. RGMII is a full-duplex interface; Chapter 10.1 §10's two half-duplex pins were dropped rather than kept idle, which is the first interface in this module to make that choice explicitly.
3. DDR Halves the Sampling Window
The clock frequency did not change. The bit time did.
| Quantity | GMII | RGMII |
|---|---|---|
| clock | 125 MHz | 125 MHz |
| clock period | 8 ns | 8 ns |
| edges used per period | 1 | 2 |
| bit time (UI) | 8 ns | 4 ns |
Now spend the budget. A receiver needs setup time before its sampling edge and hold time after it. Taking 1 ns each as a representative requirement:
| GMII, 8 ns UI | RGMII, 4 ns UI | |
|---|---|---|
| setup + hold consumed | 1 + 1 = 2 ns | 1 + 1 = 2 ns |
| left for everything else | 6 ns | 2 ns |
| as a fraction of the UI | 75% | 50% |
"Everything else" is the part that matters: the transmitter's clock-to-output spread, both packages' delays, the mismatch between the clock trace and four data traces, temperature, voltage, and process.
And halving the budget while keeping the same board is not a small change. A design with 6 ns of slack tolerates a couple of nanoseconds of accumulated error without anybody thinking about it. A design with 2 ns does not — which is why RGMII is the first interface in this module where the delay has to be engineered rather than merely not broken.
4. RTL 1 — Launching Two Nibbles per Period
// SYNTHESIZABLE (with a vendor DDR output primitive at the pins).
//
// RGMII transmit: one octet per TXC period, four bits on each edge.
//
// THE NUMBERS:
// 4 bits x 2 edges x 125 MHz = 1000 Mb/s
// TXC period = 8 ns ; UI = 4 ns <- HALF of GMII's
// 100 Mbps: TXC 25 MHz, UI 20 ns
// 10 Mbps: TXC 2.5 MHz, UI 200 ns
//
// THE CONTROL ENCODING, and it is not a multiplex:
// TX_CTL on the RISING edge = TX_EN
// TX_CTL on the FALLING edge = TX_EN XOR TX_ER
// Both original signals are recoverable from the two samples:
// TX_EN = rise
// TX_ER = rise XOR fall
// And because idle is (0,0) and a normal frame is (1,0), the wire is
// STATIC in both -- it only toggles within a period when something
// unusual is happening.
package rgmii_pkg;
localparam int unsigned NIBBLE_BITS = 4;
// Clock frequency per speed, in kHz so 2.5 MHz is expressible.
localparam int unsigned TXC_KHZ_1000 = 125_000;
localparam int unsigned TXC_KHZ_100 = 25_000;
localparam int unsigned TXC_KHZ_10 = 2_500;
// The nominal centre-of-eye delay is half a UI. At 1000 Mb/s the UI
// is 4 ns, so the geometric centre is 2.0 ns -- while RGMII v2.0's
// internal-delay option specifies a MINIMUM of 1.2 ns. Real PHYs
// expose an adjustable delay spanning that range, which is why the
// sweep in Section 12 exists.
localparam int unsigned UI_PS_1000 = 4000;
localparam int unsigned NOMINAL_DELAY_PS = 2000;
localparam int unsigned MIN_ID_DELAY_PS = 1200;
typedef enum logic [1:0] {
SPD_10,
SPD_100,
SPD_1000,
SPD_INVALID
} rgmii_speed_e;
// Where the clock-to-data delay is applied. Nothing in the protocol
// establishes this; it is a strap, a register or a trace length.
typedef enum logic [1:0] {
DLY_NONE, // nobody delays -- clock edge-aligned with data
DLY_INTERNAL, // this device applies it (RGMII-ID)
DLY_PARTNER, // the far device applies it
DLY_BOARD // extra clock trace length applies it
} delay_source_e;
// In-band status, carried on RXD[3:0] on the RISING edge while
// RX_CTL is deasserted (RGMII v2.0):
// RXD[0] link (1 = up)
// RXD[2:1] speed (00 = 10, 01 = 100, 10 = 1000, 11 = reserved)
// RXD[3] duplex (1 = full)
// So 4'hD = 1101 = full duplex, 1000 Mb/s, link up.
localparam logic [3:0] IBS_1000_FD_UP = 4'hD;
endpackage
module rgmii_tx_ddr_stage
import rgmii_pkg::*;
#(
parameter int unsigned CNT_W = 24
) (
input logic txc, // sourced by the MAC
input logic rst_n,
input logic [7:0] octet,
input logic octet_valid,
input logic tx_error, // corrupt this frame deliberately
output logic octet_ready,
// To the DDR output cells. rise_* is launched on the rising edge,
// fall_* on the falling edge, by a primitive outside this module.
output logic [NIBBLE_BITS-1:0] txd_rise,
output logic [NIBBLE_BITS-1:0] txd_fall,
output logic txctl_rise,
output logic txctl_fall,
output logic [CNT_W-1:0] c_octets,
output logic [CNT_W-1:0] c_errors,
// A transmit request arrived with no octet behind it. GMII could
// report this as an underrun and still hold TX_EN; RGMII must decide
// within one period, because a period IS an octet here.
output logic underrun
);
// One octet per period, so the interface accepts on every cycle and
// there is no multi-cycle commitment window of the kind MII and RMII
// have. What replaces the commitment problem is a timing problem.
assign octet_ready = 1'b1;
always_ff @(posedge txc or negedge rst_n) begin
if (!rst_n) begin
txd_rise <= '0; txd_fall <= '0;
txctl_rise <= 1'b0; txctl_fall <= 1'b0;
c_octets <= '0; c_errors <= '0; underrun <= 1'b0;
end else begin
underrun <= 1'b0;
if (octet_valid) begin
// LOW nibble on the rising edge, HIGH nibble on the falling --
// the same least-significant-first ordering as every other
// interface in this module, expressed as an edge rather than
// as a transfer.
txd_rise <= octet[3:0];
txd_fall <= octet[7:4];
// THE XOR ENCODING. Both source signals are recoverable, and
// the wire is static unless TX_ER is asserted.
txctl_rise <= 1'b1; // TX_EN
txctl_fall <= 1'b1 ^ tx_error; // TX_EN XOR TX_ER
if (!(&c_octets)) c_octets <= c_octets + 1'b1;
if (tx_error && !(&c_errors)) c_errors <= c_errors + 1'b1;
end else begin
txd_rise <= 4'h0;
txd_fall <= 4'h0;
txctl_rise <= 1'b0; // TX_EN = 0
txctl_fall <= 1'b0; // 0 XOR 0 -- STATIC LOW
// A frame that stops mid-transmission. On GMII the MAC could
// hold TX_EN and assert TX_ER for a cycle; here the period is
// the octet, so the decision is immediate.
if (txctl_rise) underrun <= 1'b1;
end
end
end
endmoduleClassification: synthesizable, with vendor DDR output primitives at the pins.
What it teaches: that txctl_fall is a computed value rather than a second signal, and computing it as TX_EN XOR TX_ER rather than as TX_ER makes the wire static in both of the states an interface spends almost all its time in. Idle is (0, 0); a normal frame is (1, 1). The control wire toggles within a period only when something unusual is happening, which is a free anomaly indicator and a real power and emissions saving on a signal that would otherwise switch at 125 MHz for no reason.
Deliberately simplified: the DDR launch itself is not shown. In hardware txd_rise/txd_fall feed a vendor DDR output cell, and the choice to instantiate rather than infer it is a timing decision — an inferred dual-edge structure becomes two flops and a mux, whose clock-to-out is a different number from the one the board was matched against.
Production implication: octet_ready is tied high because RGMII has no commitment window — one period carries one octet, so there is no half-transferred state to protect. The problem RGMII has instead is that the period is 8 ns and the data is only valid for 4 of it, so everything that made Chapter 10.1's and Chapter 10.2's interfaces awkward has been traded for a problem that does not appear in RTL at all.
5. Two Meanings on Two Edges
TX_CTL carries two signals and RX_CTL carries two, and the mechanism is different from Chapter 10.2's CRS_DV in a way worth being precise about.
RMII multiplexes in time. CRS_DV is one signal that becomes carrier sense or data valid depending on a toggling convention, and a receiver must decode a pattern over several cycles to know which meaning applies.
RGMII encodes in phase. Both meanings are present in every clock period — one on each edge — and recovering them is arithmetic on two samples with no history at all:
TX_EN = rise
TX_ER = rise XOR fall
rise | fall | TX_EN | TX_ER | Meaning |
|---|---|---|---|---|
| 0 | 0 | 0 | 0 | idle, or normal interframe |
| 1 | 1 | 1 | 0 | normal frame data |
| 1 | 0 | 1 | 1 | frame data, deliberately corrupted |
| 0 | 1 | 0 | 1 | an inter-frame code — not data at all |
The fourth row is the interesting one. TX_EN = 0 with TX_ER = 1 is not a frame and not an error in a frame — it is an escape, and on the receive side it is what carries carrier extend, false carrier, and the in-band status codes Section 11 covers.
6. RTL 2 — Recovering Two Nibbles, and Reporting the Phase
// SYNTHESIZABLE (with a vendor DDR input primitive at the pins).
//
// RGMII receive: capture both edges, reassemble the octet, decode the
// control encoding, AND report which sampling phase was used.
//
// The last of those is what makes this module more than a mirror of
// Section 4. A DDR receiver has a CHOICE about when to sample relative
// to the incoming clock -- an input delay line, a phase-shifted capture
// clock, or an IDELAY tap -- and the choice is a register value.
//
// The register value is in the RTL. WHETHER IT CORRESPONDS TO THE
// CENTRE OF THE DATA EYE IS NOT, because that depends on trace lengths
// and package delays the RTL has never seen. Section 16's rejected
// property is exactly this confusion, and the honest response is to
// EXPORT the phase rather than to assert anything about it.
module rgmii_rx_ddr_stage
import rgmii_pkg::*;
#(
parameter int unsigned PHASES = 8, // selectable sampling taps
parameter int unsigned CNT_W = 24
) (
input logic rxc, // sourced by the PHY
input logic rst_n,
// From the DDR input cells, already captured on each edge.
input logic [NIBBLE_BITS-1:0] rxd_rise,
input logic [NIBBLE_BITS-1:0] rxd_fall,
input logic rxctl_rise,
input logic rxctl_fall,
// The tap currently selected. An index, not a time.
input logic [2:0] sampling_phase,
output logic [7:0] octet,
output logic octet_valid,
output logic frame_start,
output logic frame_end,
output logic frame_had_error,
// The decoded originals.
output logic rx_dv,
output logic rx_er,
// RX_DV low with RX_ER high: not data, not an error -- an inter-frame
// code, which is where in-band status lives.
output logic interframe_code,
output logic [NIBBLE_BITS-1:0] interframe_value,
// Exported so a human can see WHICH tap produced this traffic. The
// design cannot know whether the tap is the eye centre; it can say
// which tap it used, and that is the honest output.
output logic [2:0] phase_in_use,
output logic [CNT_W-1:0] c_octets,
output logic [CNT_W-1:0] c_frames,
output logic [CNT_W-1:0] c_frame_errors,
output logic [CNT_W-1:0] c_interframe_codes
);
logic dv_q;
logic err_q;
// THE DECODE, and it is two lines with no history.
wire rx_dv_c = rxctl_rise;
wire rx_er_c = rxctl_rise ^ rxctl_fall;
assign phase_in_use = sampling_phase;
always_ff @(posedge rxc or negedge rst_n) begin
if (!rst_n) begin
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;
rx_dv <= 1'b0; rx_er <= 1'b0;
interframe_code <= 1'b0; interframe_value <= 4'd0;
c_octets <= '0; c_frames <= '0; c_frame_errors <= '0;
c_interframe_codes <= '0;
end else begin
octet_valid <= 1'b0;
frame_start <= 1'b0;
frame_end <= 1'b0;
interframe_code <= 1'b0;
rx_dv <= rx_dv_c;
rx_er <= rx_er_c;
dv_q <= rx_dv_c;
if (rx_dv_c) begin
if (!dv_q) begin
frame_start <= 1'b1;
err_q <= 1'b0;
end
if (rx_er_c) err_q <= 1'b1;
// Rising edge carried the LOW nibble.
octet <= {rxd_fall, rxd_rise};
octet_valid <= 1'b1;
if (!(&c_octets)) c_octets <= c_octets + 1'b1;
end else begin
// RX_DV low. If RX_ER is ALSO low this is ordinary idle and
// RXD carries the in-band status. If RX_ER is HIGH it is an
// inter-frame code -- carrier extend, false carrier, and so on.
if (rx_er_c) begin
interframe_code <= 1'b1;
interframe_value <= rxd_rise;
if (!(&c_interframe_codes))
c_interframe_codes <= c_interframe_codes + 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;
if (err_q && !(&c_frame_errors))
c_frame_errors <= c_frame_errors + 1'b1;
end
end
end
end
endmoduleClassification: synthesizable, with vendor DDR input primitives at the pins.
What it teaches: that phase_in_use is an index and not a time, and the distinction is this chapter's central epistemological point. The design knows which tap it selected — that is a register it wrote. It does not know whether that tap lands in the centre of the data eye, because the mapping from tap index to picoseconds depends on the input delay line's calibration, the package, and four data traces against one clock trace. Exporting the index is honest; asserting anything about its correctness is not.
Deliberately simplified: the tap selection arrives as an input. In hardware it is written into a delay-line control register or a PLL phase-shift register, and the value's meaning in picoseconds is a characterisation number rather than a design-time constant.
Production implication: interframe_code is separated from frame_had_error because they are read by different people. A frame error is a data-path event that belongs in a frame counter. An inter-frame code is a message from the PHY — carrier extend, false carrier, or, when RX_ER is low, the in-band status of Section 11 — and folding the two together means a MAC that cannot tell "a frame was damaged" from "the PHY is telling me the link just came up at 100 Mbps."
7. The Delay, and the Three Places to Put It
Section 3 established the requirement: the clock arrives edge-aligned with the data, and it must be moved to the middle of the eye — about 4 ÷ 2 = 2 ns at 1000 Mb/s.
RGMII v2.0's internal-delay option specifies a minimum of 1.2 ns, and real PHYs expose an adjustable delay spanning roughly 1.2 to 2.6 ns, because the correct value depends on the board it is fitted to.
There are exactly three places the delay can come from, and the original specification and its revision chose differently.
| Where | Named | How it is set | Cost |
|---|---|---|---|
| the board | RGMII v1.3's approach | extra clock trace length | see below |
| the PHY | RGMII-ID (v2.0) | a strap pin or an MDIO register | none, if configured once |
| the MAC | MAC-side delay | an SoC register or a device-tree property | none, if configured once |
Compute the board option, because the number is why it was abandoned.
FR-4 propagation is roughly 150 ps per inch on microstrip and 180 ps per inch on stripline. To buy 2 ns:
2000 ps ÷ 150 ps/inch = 13.3 inches ≈ 339 mm of extra clock trace
2000 ps ÷ 180 ps/inch = 11.1 inches ≈ 282 mm on stripline
Between a quarter and a third of a metre of serpentine, on one signal, next to four that must not have it. On a board where the MAC and PHY are 30 mm apart. That is the v1.3 approach, and it is why v2.0 added the internal delay.
8. RTL 3 — Refusing to Guess About the Delay
// SYNTHESIZABLE.
//
// Tracks and reports where the RGMII clock-to-data delay is supposed to
// come from, and refuses to enable an internal delay without positive
// configuration.
//
// THE PROBLEM (Section 7): the delay can come from the PHY, from the
// MAC, or from board trace length. Nothing in the protocol says which.
// So there are two symmetric failures:
//
// BOTH ends apply ~2 ns -> 4 ns total = exactly ONE UI at 1000 Mb/s
// -> the clock samples the NEXT bit. Data shifted by a nibble,
// uniformly, on every octet.
// NEITHER applies it -> the clock stays edge-aligned with the data
// transition -> setup AND hold violated together -> whatever the
// flop settles to.
//
// This module cannot fix either. What it can do is:
// 1. default its own delay OFF, so a reset value never becomes half
// of a double-delay,
// 2. record what it was TOLD about the far end, and flag when that
// claim plus its own setting sums to something impossible,
// 3. export the resulting expectation so two devices' assumptions can
// be compared without a scope.
module rgmii_delay_mode_arbiter
import rgmii_pkg::*;
#(
parameter int unsigned CNT_W = 16
) (
input logic clk,
input logic rst_n,
// Configuration, from straps, registers or a device tree.
input logic cfg_valid,
input delay_source_e cfg_rx_delay_source,
input delay_source_e cfg_tx_delay_source,
input logic [11:0] cfg_internal_delay_ps,
// What the far end is BELIEVED to be doing, read over MDIO or taken
// from board documentation. A claim, not a measurement.
input logic partner_claim_valid,
input logic partner_applies_rx_delay,
input logic partner_applies_tx_delay,
input logic [11:0] board_trace_delay_ps,
// The only outputs that change hardware behaviour.
output logic enable_internal_rx_delay,
output logic enable_internal_tx_delay,
output logic [11:0] internal_delay_ps,
// The arithmetic, exported. This is the module's real product.
output logic [12:0] expected_total_rx_delay_ps,
output logic [12:0] expected_total_tx_delay_ps,
// Two ends both delaying: the total lands near a whole UI.
output logic double_delay_suspected,
// Nobody delaying: the total lands near zero.
output logic no_delay_suspected,
// A configured delay outside the range the specification allows.
output logic delay_out_of_range,
output logic [CNT_W-1:0] c_config_writes,
output logic ever_double_delay,
output logic ever_no_delay
);
// A total within this band of one UI is a double delay; within this
// band of zero is no delay at all. Wide bands, because the numbers
// being added are nominal values rather than measurements.
localparam int unsigned BAND_PS = 800;
logic [12:0] rx_total_c, tx_total_c;
always_comb begin
rx_total_c = '0;
tx_total_c = '0;
// This device's own contribution, only if it is actually enabled.
if (enable_internal_rx_delay) rx_total_c = rx_total_c + 13'(internal_delay_ps);
if (enable_internal_tx_delay) tx_total_c = tx_total_c + 13'(internal_delay_ps);
// The board's contribution applies to both directions.
rx_total_c = rx_total_c + 13'(board_trace_delay_ps);
tx_total_c = tx_total_c + 13'(board_trace_delay_ps);
// The partner's CLAIMED contribution. Marked as a claim in the
// naming because nothing here measured it.
if (partner_claim_valid && partner_applies_rx_delay)
rx_total_c = rx_total_c + 13'(NOMINAL_DELAY_PS);
if (partner_claim_valid && partner_applies_tx_delay)
tx_total_c = tx_total_c + 13'(NOMINAL_DELAY_PS);
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
// OFF BY DEFAULT, always. A reset value that enables a delay is
// how a device becomes half of a double-delay without anybody
// configuring anything.
enable_internal_rx_delay <= 1'b0;
enable_internal_tx_delay <= 1'b0;
internal_delay_ps <= '0;
expected_total_rx_delay_ps <= '0;
expected_total_tx_delay_ps <= '0;
double_delay_suspected <= 1'b0; no_delay_suspected <= 1'b0;
delay_out_of_range <= 1'b0;
c_config_writes <= '0;
ever_double_delay <= 1'b0; ever_no_delay <= 1'b0;
end else begin
if (cfg_valid) begin
enable_internal_rx_delay <= (cfg_rx_delay_source == DLY_INTERNAL);
enable_internal_tx_delay <= (cfg_tx_delay_source == DLY_INTERNAL);
internal_delay_ps <= cfg_internal_delay_ps;
if (!(&c_config_writes)) c_config_writes <= c_config_writes + 1'b1;
// A configured internal delay below the specification's minimum
// is a value somebody typed rather than derived.
delay_out_of_range <=
(cfg_rx_delay_source == DLY_INTERNAL) &&
((cfg_internal_delay_ps < 12'(MIN_ID_DELAY_PS)) ||
(cfg_internal_delay_ps > 12'(UI_PS_1000)));
end
expected_total_rx_delay_ps <= rx_total_c;
expected_total_tx_delay_ps <= tx_total_c;
// NEAR ONE UI: the clock will land on the neighbouring bit.
double_delay_suspected <=
(rx_total_c > 13'(UI_PS_1000 - BAND_PS)) ||
(tx_total_c > 13'(UI_PS_1000 - BAND_PS));
if ((rx_total_c > 13'(UI_PS_1000 - BAND_PS)) ||
(tx_total_c > 13'(UI_PS_1000 - BAND_PS)))
ever_double_delay <= 1'b1;
// NEAR ZERO: the clock stays on the data transition.
no_delay_suspected <=
(rx_total_c < 13'(BAND_PS)) || (tx_total_c < 13'(BAND_PS));
if ((rx_total_c < 13'(BAND_PS)) || (tx_total_c < 13'(BAND_PS)))
ever_no_delay <= 1'b1;
end
end
endmoduleClassification: synthesizable.
What it teaches: that expected_total_rx_delay_ps is the module's real product, and it is a sum of claims rather than a measurement. The design knows its own setting exactly, knows the board's trace delay if somebody entered it, and knows what the far end says it does. None of those is observed — and the honest interface is one that publishes the arithmetic so a human can see that 2000 + 2000 = 4000 ps is one whole UI, rather than one that silently produces a number.
Deliberately simplified: the partner's contribution is taken as a nominal 2 ns when claimed. Real systems read the far end's actual delay register over MDIO where it is a standard PHY, and have nothing at all where it is not.
Production implication: both internal delays reset to disabled, and that single decision prevents the more common of the two failures. A device whose delay is enabled by a reset value is delaying before anybody configured anything — so it becomes half of a double-delay by default, and the board that works is the one whose other end happened to default to off. Chapter 10.2 §9 made the identical argument about a clock driver, and it is the same rule: a shared resource is never claimed by a reset value.
9. The Two Symmetric Failures
The two failures are mirror images arithmetically and completely different diagnostically, and the difference is the most useful thing in this chapter.
| Both ends delay | Neither end delays | |
|---|---|---|
| total delay | 2 + 2 = 4 ns | 0 ns |
| as a fraction of the 4 ns UI | exactly 1 UI | 0 UI |
| where the clock lands | on the next bit | on the data transition |
| what is captured | the neighbouring nibble | whatever the flop settles to |
| corruption pattern | consistent — the same shift on every octet | random, and temperature-dependent |
| does it change with temperature | no | yes |
| does it change between boards | no | yes, and between units of the same board |
| FCS failures | 100% | 100% |
Rows five to seven are the diagnosis.
A double delay is deterministic. Every octet is shifted by the same amount, every time, on every unit, at every temperature. Capture a frame and the payload is recognisable but nibble-shifted — a preamble that reads as a repeating pattern one nibble out of phase, a destination address whose bytes are all rotated.
No delay at all is not deterministic. The flop is being clocked at the moment its input changes, so the captured value depends on which of the four data bits happened to settle first, which varies with temperature, supply and the individual part. The same board fails differently on Tuesday.
And at 100 and 10 Mbps both failures disappear, which is the observation that makes people diagnose it wrongly.
100 Mbps: TXC = 25 MHz, UI = 20 ns
10 Mbps: TXC = 2.5 MHz, UI = 200 ns
A 2 ns error is 10% of a 100 Mbps UI and 1% of a 10 Mbps one. So a mis-delayed RGMII link works perfectly at 10 and 100 Mbps and fails only at 1000 — which reads as "a gigabit problem" and sends people to the PHY, the cable and the autonegotiation, none of which is involved.
10. RTL 4 — Reading the Status the Gap Carries
// SYNTHESIZABLE.
//
// Decodes RGMII's in-band status, which the PHY presents on RXD[3:0]
// during the interframe gap.
//
// WHY IT EXISTS: every earlier interface required MDIO to learn the
// link's speed and duplex, which meant a register read at some
// unspecified time after a change. RGMII v2.0 carries it CONTINUOUSLY,
// in a gap that was otherwise idle.
//
// THE ENCODING, on the RISING edge while RX_CTL is deasserted:
// RXD[0] link 1 = up
// RXD[2:1] speed 00 = 10 Mb/s, 01 = 100 Mb/s, 10 = 1000 Mb/s,
// 11 = reserved
// RXD[3] duplex 1 = full
// So 4'hD = 1101 = full duplex, 1000 Mb/s, link up.
//
// AND THE FALLING EDGE carries a separate inter-frame code, valid when
// RX_ER is asserted (RX_CTL rise 0, fall 1): carrier extend, false
// carrier, and the like. Two different messages, two different edges,
// in a gap that used to carry nothing.
module rgmii_inband_status_decoder
import rgmii_pkg::*;
#(
parameter int unsigned STABLE_SAMPLES = 8,
parameter int unsigned CNT_W = 16
) (
input logic rxc,
input logic rst_n,
input logic clear,
input logic rx_dv,
input logic rx_er,
input logic [NIBBLE_BITS-1:0] rxd_rise,
output logic link_up,
output rgmii_speed_e speed,
output logic full_duplex,
output logic status_valid,
// A status field that changed. Reported as an EVENT, because the
// whole value of in-band status is that a change is visible the
// moment it happens rather than at the next MDIO poll.
output logic status_changed,
output logic link_went_down,
output logic speed_changed,
// The reserved speed encoding. Not silently mapped to anything.
output logic reserved_speed_seen,
output logic [CNT_W-1:0] c_status_changes,
output logic [CNT_W-1:0] c_link_downs,
output logic ever_link_down,
output logic ever_reserved_speed
);
logic [3:0] raw_q;
logic [3:0] stable_q;
logic [3:0] count_q;
function automatic rgmii_speed_e decode_speed (input logic [1:0] s);
unique case (s)
2'b00: decode_speed = SPD_10;
2'b01: decode_speed = SPD_100;
2'b10: decode_speed = SPD_1000;
default: decode_speed = SPD_INVALID;
endcase
endfunction
always_ff @(posedge rxc or negedge rst_n) begin
if (!rst_n) begin
raw_q <= 4'd0; stable_q <= 4'd0; count_q <= 4'd0;
link_up <= 1'b0; speed <= SPD_INVALID; full_duplex <= 1'b0;
status_valid <= 1'b0; status_changed <= 1'b0;
link_went_down <= 1'b0; speed_changed <= 1'b0;
reserved_speed_seen <= 1'b0;
c_status_changes <= '0; c_link_downs <= '0;
ever_link_down <= 1'b0; ever_reserved_speed <= 1'b0;
end else if (clear) begin
c_status_changes <= '0; c_link_downs <= '0;
status_changed <= 1'b0; link_went_down <= 1'b0;
speed_changed <= 1'b0; reserved_speed_seen <= 1'b0;
// ever_link_down and ever_reserved_speed survive: a link that has
// bounced has bounced, and clearing a counter does not un-bounce it.
end else begin
status_changed <= 1'b0;
link_went_down <= 1'b0;
speed_changed <= 1'b0;
reserved_speed_seen <= 1'b0;
// In-band status is valid ONLY in the gap, and only when RX_ER is
// low -- with RX_ER high the same nibble is an inter-frame code
// instead. Sampling it during a frame reads frame data as status.
if (!rx_dv && !rx_er) begin
raw_q <= rxd_rise;
// DEBOUNCE. The nibble is present on every idle cycle, so a
// single-cycle glitch would otherwise be a link-down event.
if (rxd_rise == raw_q) begin
if (count_q != 4'(STABLE_SAMPLES)) begin
count_q <= count_q + 4'd1;
end else if (stable_q != raw_q) begin
stable_q <= raw_q;
status_valid <= 1'b1;
if (raw_q[0] != stable_q[0]) begin
if (!raw_q[0]) begin
link_went_down <= 1'b1;
ever_link_down <= 1'b1;
if (!(&c_link_downs)) c_link_downs <= c_link_downs + 1'b1;
end
end
if (raw_q[2:1] != stable_q[2:1]) speed_changed <= 1'b1;
status_changed <= 1'b1;
if (!(&c_status_changes)) c_status_changes <= c_status_changes + 1'b1;
link_up <= raw_q[0];
speed <= decode_speed(raw_q[2:1]);
full_duplex <= raw_q[3];
// The reserved encoding is REPORTED, not folded into the
// nearest legal speed. A PHY sending it is telling us
// something, even if the something is that it is broken.
if (raw_q[2:1] == 2'b11) begin
reserved_speed_seen <= 1'b1;
ever_reserved_speed <= 1'b1;
end
end
end else begin
count_q <= 4'd0;
end
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that in-band status must be debounced, and the reason is structural rather than defensive. The nibble is present on every idle clock — millions of times a second — so a single corrupted sample is not a rare event to be tolerated but a certainty over any useful interval. Without a stability count, link_went_down fires on noise, and a MAC that resets its data path on link-down events resets it constantly.
Deliberately simplified: one debounce depth for all four fields. Production decoders often debounce link separately and more aggressively than speed, because acting on a false link-down is more expensive than acting on a false speed change.
Production implication: the decoder is gated on !rx_dv && !rx_er, and getting that condition wrong reads frame data as status. During a frame, RXD[3:0] carries the payload's low nibble — so a decoder that samples unconditionally sees the destination address's nibbles as link, speed and duplex, and reports the link changing state several million times a second while traffic flows.
11. What the Interframe Gap Carries
RGMII put three different messages into a gap that every earlier interface left empty, and separating them is a two-bit decision.
RX_CTL rise | RX_CTL fall | RX_DV | RX_ER | RXD[3:0] means |
|---|---|---|---|---|
| 1 | 1 | 1 | 0 | frame data |
| 1 | 0 | 1 | 1 | frame data, flagged as errored |
| 0 | 0 | 0 | 0 | in-band status — link, speed, duplex |
| 0 | 1 | 0 | 1 | an inter-frame code — carrier extend, false carrier |
Read the third row against every other interface in this module.
Chapter 10.1's MII, Chapter 10.2's RMII and Chapter 10.3's GMII all learn the link's speed and duplex only over Chapter 4.5's MDIO — which means a poll, at some unspecified interval, discovering a change that happened at some earlier unspecified time.
RGMII carries the answer continuously, on wires that were idle anyway, at zero cost.
And it changes what a MAC can do. A speed change is visible within microseconds rather than at the next poll; a link-down is an event rather than a discovery; and a MAC can gate its transmit path on the PHY's own current statement rather than on a cached register value that may be a hundred milliseconds stale.
12. RTL 5 — Measuring the Eye Instead of Assuming It
// SYNTHESIZABLE.
//
// Bring-up self-test: sweep the receive sampling tap across its whole
// range, measure where the data is correct, and choose the middle.
//
// WHY THIS EXISTS. Section 8's arbiter can add up nominal delays and
// warn when the sum looks wrong, but every term in that sum is a CLAIM.
// This module does not add claims -- it MEASURES, by trying every tap
// and seeing which ones work.
//
// AND THE OUTPUT THAT MATTERS IS NOT THE CHOSEN TAP. It is the WIDTH of
// the passing run:
// 6 of 8 taps pass -> a wide eye, plenty of margin, nothing to do
// 1 of 8 taps pass -> the link works and is one temperature step from
// not working, and NOTHING ELSE IN THE DESIGN
// WOULD HAVE SAID SO
// Both report a link that comes up and passes traffic.
module rgmii_phase_sweep_selftest
import rgmii_pkg::*;
#(
parameter int unsigned PHASES = 8,
parameter int unsigned SAMPLES_PER_TAP = 32'd100_000,
parameter int unsigned CNT_W = 20,
// Errors permitted at a tap before it is called failing. Zero, on a
// known pattern -- a tap that produces ANY error is not in the eye.
parameter int unsigned ERROR_BUDGET = 0
) (
input logic clk,
input logic rst_n,
input logic start,
// From the receive path, running a known pattern.
input logic sample_valid,
input logic sample_error,
output logic [2:0] tap_under_test,
output logic sweeping,
output logic sweep_done,
// The measurement. Bit n set = tap n passed.
output logic [PHASES-1:0] tap_pass_mask,
output logic [3:0] eye_width_taps,
output logic [2:0] chosen_tap,
output logic chosen_tap_valid,
// No tap worked at all. The link cannot be brought up by choosing
// better, and that is a completely different report from "chose one".
output logic no_working_tap,
// The eye is narrow enough that this link is marginal rather than
// working. The output the whole module exists to produce.
output logic narrow_eye,
output logic [CNT_W-1:0] c_sweeps
);
// An eye of two taps or fewer out of eight is a link with no margin.
localparam int unsigned NARROW_AT = 2;
logic [31:0] samples_q;
logic [CNT_W-1:0] errors_q;
logic [2:0] tap_q;
assign tap_under_test = tap_q;
// Find the longest contiguous run of passing taps and its midpoint.
// Contiguous matters: two isolated passing taps are not an eye, they
// are two lucky samples, and averaging their indices would choose a
// tap that fails.
always_comb begin
logic [3:0] best_len, cur_len;
logic [2:0] best_start, cur_start;
best_len = 4'd0; cur_len = 4'd0;
best_start = 3'd0; cur_start = 3'd0;
for (int i = 0; i < PHASES; i = i + 1) begin
if (tap_pass_mask[i]) begin
if (cur_len == 4'd0) cur_start = 3'(i);
cur_len = cur_len + 4'd1;
if (cur_len > best_len) begin
best_len = cur_len;
best_start = cur_start;
end
end else begin
cur_len = 4'd0;
end
end
eye_width_taps = best_len;
chosen_tap = best_start + 3'(best_len >> 1);
chosen_tap_valid = sweep_done && (best_len != 4'd0);
no_working_tap = sweep_done && (best_len == 4'd0);
narrow_eye = sweep_done && (best_len != 4'd0) &&
(best_len <= 4'(NARROW_AT));
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
tap_q <= 3'd0; samples_q <= '0; errors_q <= '0;
sweeping <= 1'b0; sweep_done <= 1'b0;
tap_pass_mask <= '0; c_sweeps <= '0;
end else if (start) begin
tap_q <= 3'd0; samples_q <= '0; errors_q <= '0;
sweeping <= 1'b1; sweep_done <= 1'b0; tap_pass_mask <= '0;
if (!(&c_sweeps)) c_sweeps <= c_sweeps + 1'b1;
end else if (sweeping) begin
if (sample_valid) begin
samples_q <= samples_q + 1'b1;
if (sample_error && !(&errors_q)) errors_q <= errors_q + 1'b1;
end
if (samples_q == SAMPLES_PER_TAP) begin
// A tap passes only with errors at or below the budget, which
// defaults to ZERO. A tap that is nearly right is not in the
// eye; it is at its edge, and choosing it is choosing the worst
// usable position.
tap_pass_mask[tap_q] <= (errors_q <= CNT_W'(ERROR_BUDGET));
samples_q <= '0;
errors_q <= '0;
if (tap_q == 3'(PHASES - 1)) begin
sweeping <= 1'b0;
sweep_done <= 1'b1;
end else begin
tap_q <= tap_q + 3'd1;
end
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that eye_width_taps is worth more than chosen_tap, and that inverts what most designs report. A link with six of eight taps passing and a link with one of eight passing both come up, both pass traffic, and both report the same chosen_tap behaviour to everything downstream. The first has margin against temperature, ageing and the next board revision. The second is working by luck, and narrow_eye is the only signal in the entire design that distinguishes them.
Deliberately simplified: the run-finding is combinational over eight taps. With more taps it becomes a small sequential scan, and production implementations also weight the run by the error count at its edges rather than treating pass and fail as binary.
Production implication: the longest contiguous run is used rather than the count of passing taps, and the distinction is not pedantic. Two isolated passing taps separated by a failing one are not an eye — they are two positions that happened to sample correctly during a short test — and averaging their indices selects the failing tap between them. A design that counts rather than tracks contiguity will occasionally choose the one tap guaranteed not to work.
13. RTL 6 — Changing Speed Without Sampling Garbage
// SYNTHESIZABLE.
//
// Sequences an RGMII speed change, which is a clock frequency change on
// a source-synchronous DDR interface -- the most disruptive event this
// interface has.
//
// WHAT CHANGES AT A SPEED CHANGE:
// TXC and RXC frequency 125 MHz <-> 25 MHz <-> 2.5 MHz
// the UI 4 ns <-> 20 ns <-> 200 ns
// the CORRECT DELAY -- and this is the part designs miss
//
// A 2 ns delay that centres the eye at 1000 Mb/s is 10% of the UI at
// 100 Mb/s and 1% at 10 Mb/s. It does not BREAK those speeds, which is
// why the wrong delay is invisible below gigabit -- but it does mean
// the chosen sampling tap from Section 12's sweep is calibrated for one
// speed and merely tolerable at the others.
//
// And the clock STOPS while the PHY re-locks, exactly as in Chapter
// 10.1 -- so this sequencer runs on sys_clk and not on RXC.
module rgmii_speed_change_sequencer
import rgmii_pkg::*;
#(
parameter int unsigned QUIESCE_TIMEOUT = 32'd2_000_000,
parameter int unsigned RELOCK_CYCLES = 32'd200_000,
parameter int unsigned CNT_W = 16
) (
input logic sys_clk,
input logic rst_n,
// The requested speed, typically from Section 10's in-band decoder.
input logic req_valid,
input rgmii_speed_e req_speed,
input logic tx_active,
input logic rx_active,
input logic rxc_running, // from a clock observer
output rgmii_speed_e speed,
output logic tx_enabled,
output logic resample_request, // ask for a new phase sweep
output logic change_applied,
output logic change_pending,
output logic change_timed_out,
// The link changed speed while traffic was in flight. Legal, and
// worth counting: every such change discards a frame.
output logic changed_under_load,
output logic [CNT_W-1:0] c_changes,
output logic [CNT_W-1:0] c_changes_under_load,
output logic [CNT_W-1:0] c_timeouts
);
typedef enum logic [2:0] {
S_STEADY, S_QUIESCE, S_DISABLE, S_RELOCK, S_RESAMPLE, S_ENABLE
} st_e;
st_e st_q;
rgmii_speed_e target_q;
logic [31:0] timer_q;
assign change_pending = (st_q != S_STEADY);
always_ff @(posedge sys_clk or negedge rst_n) begin
if (!rst_n) begin
st_q <= S_STEADY; speed <= SPD_INVALID; target_q <= SPD_INVALID;
timer_q <= '0; tx_enabled <= 1'b0; resample_request <= 1'b0;
change_applied <= 1'b0; change_timed_out <= 1'b0;
changed_under_load <= 1'b0;
c_changes <= '0; c_changes_under_load <= '0; c_timeouts <= '0;
end else begin
change_applied <= 1'b0;
change_timed_out <= 1'b0;
changed_under_load <= 1'b0;
resample_request <= 1'b0;
unique case (st_q)
S_STEADY: if (req_valid && (req_speed != speed) &&
(req_speed != SPD_INVALID)) begin
target_q <= req_speed;
timer_q <= '0;
st_q <= S_QUIESCE;
if (tx_active || rx_active) begin
// The link is changing speed with traffic in flight. There
// is nothing to do about it -- the far end already changed
// -- but a frame WILL be lost and saying so is the
// difference between an unexplained drop and a known one.
changed_under_load <= 1'b1;
if (!(&c_changes_under_load))
c_changes_under_load <= c_changes_under_load + 1'b1;
end
end
S_QUIESCE: if (!tx_active && !rx_active) begin
st_q <= S_DISABLE;
end else if (timer_q == QUIESCE_TIMEOUT) begin
st_q <= S_STEADY;
change_timed_out <= 1'b1;
if (!(&c_timeouts)) c_timeouts <= c_timeouts + 1'b1;
end else begin
timer_q <= timer_q + 1'b1;
end
S_DISABLE: begin
// Stop driving before the clock changes underneath us.
tx_enabled <= 1'b0;
timer_q <= '0;
st_q <= S_RELOCK;
end
S_RELOCK: begin
// The PHY stops RXC and re-locks at the new frequency. Wait
// for it to come back rather than for a fixed time, with a
// fixed time as the fallback.
if (rxc_running && (timer_q > 32'd1000)) begin
st_q <= S_RESAMPLE;
timer_q <= '0;
end else if (timer_q == RELOCK_CYCLES) begin
st_q <= S_STEADY;
change_timed_out <= 1'b1;
if (!(&c_timeouts)) c_timeouts <= c_timeouts + 1'b1;
end else begin
timer_q <= timer_q + 1'b1;
end
end
S_RESAMPLE: begin
// THE STEP DESIGNS OMIT. The sampling tap chosen at the old
// speed was calibrated against the old UI. Re-running the
// sweep costs milliseconds once and removes a whole class of
// "works at 100, marginal at 1000" behaviour.
speed <= target_q;
resample_request <= 1'b1;
st_q <= S_ENABLE;
end
S_ENABLE: begin
tx_enabled <= 1'b1;
change_applied <= 1'b1;
st_q <= S_STEADY;
if (!(&c_changes)) c_changes <= c_changes + 1'b1;
end
default: st_q <= S_STEADY;
endcase
end
end
endmoduleClassification: synthesizable.
What it teaches: that a speed change invalidates the sampling calibration and almost nothing does anything about it. The tap chosen by Section 12's sweep was measured against a 4 ns UI; after a change to 100 Mb/s the UI is 20 ns and that tap is merely tolerable rather than centred. Going the other way is the dangerous direction — a tap calibrated at 100 Mb/s and carried into 1000 Mb/s is a tap chosen when the eye was five times wider, and it may sit outside the narrower one entirely.
Deliberately simplified: the re-lock wait combines a clock-present check with a timeout. Real sequencers also wait on the PHY's own status bits over Chapter 4.5's MDIO, which is slower and more definitive.
Production implication: changed_under_load counts something nobody can prevent and everybody needs to know. A speed change with traffic in flight loses a frame — the far end has already changed and this end has not — and without the counter that loss appears in the frame statistics as an unexplained drop. Attributing it to a known, timestamped event is the difference between a closed investigation and an open one.
14. RTL 7 — Conformance, and the Signature of a Shift
// SYNTHESIZABLE.
//
// Checks the partner's conformance AND -- uniquely in this module --
// tries to distinguish the two symmetric delay failures from each other
// using only what arrives at the pins.
//
// THE INSIGHT: the two failures have different STATISTICS.
// A DOUBLE DELAY samples the neighbouring bit, so the corruption is
// DETERMINISTIC. The preamble, which is a known repeating
// pattern, arrives as a DIFFERENT known repeating pattern --
// consistently, on every frame.
// NO DELAY samples on the transition, so the corruption is RANDOM.
// The preamble arrives as noise, differently every frame.
//
// So: watch the preamble. It is the one field whose correct value is
// known in advance, it is present on every frame, and its corruption
// pattern separates the two failures that every other counter reports
// identically.
module rgmii_conformance_monitor
import rgmii_pkg::*;
#(
parameter int unsigned CNT_W = 20,
parameter int unsigned WINDOW = 16'd256
) (
input logic rxc,
input logic rst_n,
input logic clear,
input logic rx_dv,
input logic rx_er,
input logic [7:0] octet,
input logic octet_valid,
input logic frame_start,
input logic full_duplex,
input logic tx_en,
// Preamble analysis -- the discriminator.
output logic preamble_correct,
output logic preamble_shifted, // consistent wrong value
output logic preamble_random, // inconsistent wrong value
output logic [7:0] preamble_observed,
// Ordinary conformance.
output logic rx_er_outside_frame,
output logic reserved_interframe_code,
output logic tx_while_link_down,
output logic [CNT_W-1:0] c_frames,
output logic [CNT_W-1:0] c_preamble_correct,
output logic [CNT_W-1:0] c_preamble_shifted,
output logic [CNT_W-1:0] c_preamble_random,
output logic [CNT_W-1:0] c_conformance_violations,
// The verdict, which is the module's product.
output logic verdict_valid,
output logic [1:0] verdict, // 0 ok, 1 double delay, 2 no delay
output logic ever_violated
);
localparam logic [7:0] PREAMBLE = 8'h55;
// 0x55 sampled one nibble late against a run of 0x55 reads back as
// 0x55 again -- so the discriminator uses the FIRST octet after
// frame_start, where the shift pulls in the preceding idle nibble.
localparam logic [7:0] SHIFTED_PREAMBLE = 8'h50;
logic [15:0] win_q;
logic [7:0] last_bad_q;
logic first_octet_q;
logic any_c;
always_comb begin
any_c = rx_er_outside_frame || reserved_interframe_code ||
tx_while_link_down;
end
always_ff @(posedge rxc or negedge rst_n) begin
if (!rst_n) begin
win_q <= '0; last_bad_q <= 8'd0; first_octet_q <= 1'b0;
preamble_correct <= 1'b0; preamble_shifted <= 1'b0;
preamble_random <= 1'b0; preamble_observed <= 8'd0;
rx_er_outside_frame <= 1'b0; reserved_interframe_code <= 1'b0;
tx_while_link_down <= 1'b0;
c_frames <= '0; c_preamble_correct <= '0;
c_preamble_shifted <= '0; c_preamble_random <= '0;
c_conformance_violations <= '0;
verdict_valid <= 1'b0; verdict <= 2'd0; ever_violated <= 1'b0;
end else if (clear) begin
c_frames <= '0; c_preamble_correct <= '0;
c_preamble_shifted <= '0; c_preamble_random <= '0;
c_conformance_violations <= '0; win_q <= '0; verdict_valid <= 1'b0;
// ever_violated survives.
end else begin
preamble_correct <= 1'b0;
preamble_shifted <= 1'b0;
preamble_random <= 1'b0;
if (frame_start) first_octet_q <= 1'b1;
if (octet_valid && first_octet_q) begin
first_octet_q <= 1'b0;
preamble_observed <= octet;
if (!(&c_frames)) c_frames <= c_frames + 1'b1;
if (octet == PREAMBLE) begin
preamble_correct <= 1'b1;
if (!(&c_preamble_correct))
c_preamble_correct <= c_preamble_correct + 1'b1;
end else if ((octet == SHIFTED_PREAMBLE) || (octet == last_bad_q)) begin
// THE SAME wrong value as last time. A deterministic
// corruption, which is what sampling the neighbouring bit
// produces.
preamble_shifted <= 1'b1;
if (!(&c_preamble_shifted))
c_preamble_shifted <= c_preamble_shifted + 1'b1;
end else begin
// A DIFFERENT wrong value. Random corruption, which is what
// sampling on the transition produces.
preamble_random <= 1'b1;
if (!(&c_preamble_random))
c_preamble_random <= c_preamble_random + 1'b1;
end
if (octet != PREAMBLE) last_bad_q <= octet;
end
// Ordinary conformance checks.
rx_er_outside_frame <= rx_er && !rx_dv && (octet[3:0] == 4'h0);
reserved_interframe_code <= rx_er && !rx_dv && (octet[3:0] == 4'h5);
tx_while_link_down <= tx_en && !full_duplex && rx_dv;
if (any_c) begin
ever_violated <= 1'b1;
if (!(&c_conformance_violations))
c_conformance_violations <= c_conformance_violations + 1'b1;
end
// THE VERDICT, over a window of frames.
win_q <= win_q + 1'b1;
if (win_q == WINDOW) begin
win_q <= '0;
verdict_valid <= 1'b1;
if (c_preamble_correct > (c_preamble_shifted + c_preamble_random))
verdict <= 2'd0; // healthy
else if (c_preamble_shifted > c_preamble_random)
verdict <= 2'd1; // DOUBLE DELAY
else
verdict <= 2'd2; // NO DELAY
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that the preamble is the only field on the wire whose correct value is known in advance, which makes it the natural probe for a corruption whose pattern matters. Every other field is data — a wrong destination address is indistinguishable from a different destination address. The preamble is a fixed constant on every frame, so a wrong preamble is unambiguously corruption, and the consistency of the wrongness separates the two failures.
Deliberately simplified: the shifted-preamble comparison uses one precomputed value plus a match against the previous bad value. A production monitor histograms the observed first octet, and the histogram's entropy is the discriminator — low entropy means deterministic, high means random.
Production implication: verdict turns two failures that every counter reports identically into a named work order. verdict = 1 says both ends are delaying — turn one off. verdict = 2 says neither is — turn one on. Both would otherwise present as "100% FCS failures at 1000 Mbps, works fine at 100", which is the point at which most people replace the cable, then the PHY, then the board.
15. Pins, Latency and What the Fold Cost
The pin saving is the reason RGMII exists and it is large.
| Ports | GMII signals | RGMII signals | Saved |
|---|---|---|---|
| 1 | 24 | 12 | 12 |
| 4 | 96 | 48 | 48 |
| 8 | 192 | 96 | 96 |
| 24 | 576 | 288 | 288 |
And the latency is unchanged, for exactly the reason Chapter 10.2's was.
| GMII | RGMII | |
|---|---|---|
| bits per transfer | 8 | 4 |
| transfers per clock period | 1 | 2 |
| clock period | 8 ns | 8 ns |
| octet time | 8 ns | 8 ns |
| a 1518-octet frame | 12.144 µs | 12.144 µs |
Halving the width and doubling the transfers per period cancel exactly, so RGMII carries the same traffic in the same time on half the wires and adds no pipelining at all.
What it cost is the 4 ns UI, and everything in this chapter follows from that. Section 3's setup-and-hold budget went from 75% of the UI to 50%; Section 7's delay became a required configuration item with three possible owners; Section 9's two failures are symmetric and both invisible below gigabit; and Section 12's sweep exists because the only honest way to set a parameter nobody can observe is to measure its effect.
16. Properties Worth Asserting, and One Worth Refusing
Chapter 10.3 established that a source-synchronous interface's timing is decided outside the RTL abstraction. RGMII adds a twist: the RTL now holds a register that names a position in that outside world — the sampling tap — and the temptation is to assert about the name.
The DDR encoding — what the RTL genuinely owns
// P1. The low nibble goes on the rising edge and the high nibble on the
// falling. The ordering rule, expressed as an edge.
property p_nibble_edge_order;
@(posedge txc) disable iff (!rst_n)
octet_valid |=> ((txd_rise == $past(octet[3:0])) &&
(txd_fall == $past(octet[7:4])));
endproperty
a_nibble_edge_order: assert property (p_nibble_edge_order);
// P2. TX_CTL's rising-edge value IS TX_EN. Half of the control
// encoding, and the half a single-edge sampler gets right.
property p_txctl_rise_is_tx_en;
@(posedge txc) disable iff (!rst_n)
octet_valid |=> txctl_rise;
endproperty
a_txctl_rise_is_tx_en: assert property (p_txctl_rise_is_tx_en);
// P3. TX_CTL's falling-edge value is TX_EN XOR TX_ER. The other half,
// and the half a single-edge sampler loses SILENTLY.
property p_txctl_fall_is_xor;
@(posedge txc) disable iff (!rst_n)
octet_valid |=> (txctl_fall == (1'b1 ^ $past(tx_error)));
endproperty
a_txctl_fall_is_xor: assert property (p_txctl_fall_is_xor);
// P4. Idle is STATIC on the control wire. Both samples zero, so the
// wire does not toggle when nothing is happening.
property p_idle_control_is_static;
@(posedge txc) disable iff (!rst_n)
!octet_valid |=> (!txctl_rise && !txctl_fall);
endproperty
a_idle_control_static: assert property (p_idle_control_is_static);
// P5. A normal frame is also static high. The wire toggles within a
// period ONLY when something unusual is happening -- which is the free
// anomaly indicator the XOR encoding buys.
property p_normal_frame_control_is_static;
@(posedge txc) disable iff (!rst_n)
(octet_valid && !tx_error) |=> (txctl_rise && txctl_fall);
endproperty
a_normal_frame_static: assert property (p_normal_frame_control_is_static);
// P6. The receive decode inverts the transmit encode exactly.
property p_rx_decode_inverts_encode;
@(posedge rxc) disable iff (!rst_n)
1'b1 |-> ((rx_dv == $past(rxctl_rise)) &&
(rx_er == $past(rxctl_rise ^ rxctl_fall)));
endproperty
a_rx_decode_inverts: assert property (p_rx_decode_inverts_encode);
// P7. An octet is reassembled with the rising-edge nibble low.
property p_rx_octet_assembly;
@(posedge rxc) disable iff (!rst_n)
octet_valid |-> (octet == {$past(rxd_fall), $past(rxd_rise)});
endproperty
a_rx_octet_assembly: assert property (p_rx_octet_assembly);In-band status
// P8. Status is decoded ONLY in the gap and only with RX_ER low.
// Sampling during a frame reads payload nibbles as link state.
property p_status_only_in_gap;
@(posedge rxc) disable iff (!rst_n)
status_changed |-> ($past(!rx_dv) && $past(!rx_er));
endproperty
a_status_only_in_gap: assert property (p_status_only_in_gap);
// P9. A status change requires a stable observation, not one sample.
// The nibble is present on every idle cycle, so a glitch is certain.
property p_status_requires_stability;
@(posedge rxc) disable iff (!rst_n)
status_changed |-> ($past(count_q) == 4'(STABLE_SAMPLES));
endproperty
a_status_requires_stability: assert property (p_status_requires_stability);
// P10. The reserved speed encoding is REPORTED, never mapped to a legal
// speed. A PHY sending it is saying something, even if the something is
// that it is broken.
property p_reserved_speed_reported;
@(posedge rxc) disable iff (!rst_n)
(status_changed && ($past(rxd_rise[2:1]) == 2'b11))
|-> reserved_speed_seen;
endproperty
a_reserved_speed_reported: assert property (p_reserved_speed_reported);
// P11. Link-down history survives a counter clear.
property p_link_down_history_sticky;
@(posedge rxc) disable iff (!rst_n)
ever_link_down |=> ever_link_down;
endproperty
a_link_down_sticky: assert property (p_link_down_history_sticky);The delay arbiter — refusal by default, and honest arithmetic
// P12. Neither internal delay is enabled out of reset. A reset value
// that delays is how a device becomes half of a double-delay.
property p_no_delay_after_reset;
@(posedge clk) disable iff (!rst_n)
$rose(rst_n) |=> (!enable_internal_rx_delay && !enable_internal_tx_delay);
endproperty
a_no_delay_after_reset: assert property (p_no_delay_after_reset);
// P13. An internal delay is enabled only by positive configuration.
property p_delay_needs_config;
@(posedge clk) disable iff (!rst_n)
$rose(enable_internal_rx_delay)
|-> ($past(cfg_valid) && ($past(cfg_rx_delay_source) == DLY_INTERNAL));
endproperty
a_delay_needs_config: assert property (p_delay_needs_config);
// P14. A total near one UI is flagged. The double-delay arithmetic,
// asserted -- not the timing, which the RTL cannot see, but the SUM OF
// CLAIMS, which it can.
property p_double_delay_flagged;
@(posedge clk) disable iff (!rst_n)
(expected_total_rx_delay_ps > 13'(UI_PS_1000 - BAND_PS))
|-> double_delay_suspected;
endproperty
a_double_delay_flagged: assert property (p_double_delay_flagged);
// P15. And a total near zero is flagged too, symmetrically.
property p_no_delay_flagged;
@(posedge clk) disable iff (!rst_n)
(expected_total_rx_delay_ps < 13'(BAND_PS)) |-> no_delay_suspected;
endproperty
a_no_delay_flagged: assert property (p_no_delay_flagged);
// P16. A configured delay below the specification's minimum is
// reported. A number somebody typed rather than derived.
property p_out_of_range_reported;
@(posedge clk) disable iff (!rst_n)
(cfg_valid && (cfg_rx_delay_source == DLY_INTERNAL) &&
(cfg_internal_delay_ps < 12'(MIN_ID_DELAY_PS))) |=> delay_out_of_range;
endproperty
a_out_of_range_reported: assert property (p_out_of_range_reported);The sweep — the measurement, not the meaning
// P17. Every tap is visited. A sweep that stops early reports an eye
// narrower than the real one and chooses badly.
property p_sweep_visits_all_taps;
@(posedge clk) disable iff (!rst_n)
sweep_done |-> ($past(tap_under_test) == 3'(PHASES - 1));
endproperty
a_sweep_visits_all: assert property (p_sweep_visits_all_taps);
// P18. THE CONTIGUITY PROPERTY. The chosen tap is inside the passing
// run. Averaging the indices of two isolated passing taps selects the
// failing tap between them.
property p_chosen_tap_passes;
@(posedge clk) disable iff (!rst_n)
chosen_tap_valid |-> tap_pass_mask[chosen_tap];
endproperty
a_chosen_tap_passes: assert property (p_chosen_tap_passes);
// P19. A narrow eye is reported even though the link works. The output
// the module exists to produce, and the one nothing else provides.
property p_narrow_eye_reported;
@(posedge clk) disable iff (!rst_n)
(sweep_done && (eye_width_taps != 4'd0) &&
(eye_width_taps <= 4'(NARROW_AT))) |-> narrow_eye;
endproperty
a_narrow_eye_reported: assert property (p_narrow_eye_reported);
// P20. No working tap is DISTINCT from having chosen one. Two
// different outcomes, two different reports.
property p_no_tap_is_distinct;
@(posedge clk) disable iff (!rst_n)
no_working_tap |-> !chosen_tap_valid;
endproperty
a_no_tap_distinct: assert property (p_no_tap_is_distinct);Speed changes
// P21. A speed change applies only when both directions are quiescent.
property p_speed_change_when_quiet;
@(posedge sys_clk) disable iff (!rst_n)
change_applied |-> ($past(!tx_active) && $past(!rx_active));
endproperty
a_speed_change_when_quiet: assert property (p_speed_change_when_quiet);
// P22. And every applied change requests a re-sweep, because the tap
// was calibrated against the old UI.
property p_change_requests_resample;
@(posedge sys_clk) disable iff (!rst_n)
change_applied |-> $past(resample_request);
endproperty
a_change_requests_resample: assert property (p_change_requests_resample);
// P23. Outputs are released before the clock changes underneath them.
property p_disable_before_relock;
@(posedge sys_clk) disable iff (!rst_n)
(st_q == S_RELOCK) |-> !tx_enabled;
endproperty
a_disable_before_relock: assert property (p_disable_before_relock);17. Verification Scenarios
The DDR encoding
- A single octet at 1000 Mb/s — low nibble on the rising edge, high nibble on the falling, one
TXCperiod. 0xA5—txd_rise = 0x5,txd_fall = 0xA. A value whose halves differ.0x00and0xFF— the two values where a nibble swap is invisible; included precisely because they cannot catch the bug.- Back-to-back octets — one per period,
octet_readyhigh throughout. - Idle —
txctl_riseandtxctl_fallboth low, and the wire does not toggle within any period. - A normal frame — both control samples high; again static.
tx_errorasserted for one octet —rise = 1,fall = 0; the only case where the wire toggles within a period.- A frame that stops with no octet behind it —
underrunpulses. - The full receive decode —
rx_dv = rise,rx_er = rise XOR fall, checked across all four combinations. rise = 0,fall = 1—interframe_codeasserted,octet_validlow. Not data, not an error.- A receiver that samples
RX_CTLonly on the rising edge (a deliberate mutation) —rx_dvis correct,rx_eris permanently zero, and errored frames are accepted as good. The silent half-loss.
In-band status
RXD[3:0] = 0xDsteadily in the gap — link up, 1000 Mb/s, full duplex, afterSTABLE_SAMPLESmatching observations.- A single-cycle glitch to
0xC— no status change; the debounce absorbs it. - A change to
0x0held stably —link_went_down,c_link_downsincrements,ever_link_downsticky. - A change from 1000 to 100 Mb/s —
speed_changed, and the value decodes toSPD_100. RXD[2:1] = 11—reserved_speed_seen, andspeedisSPD_INVALIDrather than mapped to anything legal.- Status nibbles present during a frame — ignored; the decoder is gated on
!rx_dv && !rx_er. - A mutation that removes the gating (deliberate) — the decoder reports several million link changes a second while traffic flows. The check that proves the gate matters.
clearafter a link-down — counters clear,ever_link_downsurvives.
The delay arbiter
- Out of reset with
cfg_rx_delay_source = DLY_INTERNALalready presented — both delays disabled untilcfg_valid. - Internal delay 2000 ps, partner claims none, board 0 — total 2000 ps; neither flag set.
- Internal 2000 ps, partner claims 2000 ps — total 4000 ps;
double_delay_suspected,ever_double_delaysticky. - No internal, no partner, no board — total 0;
no_delay_suspected. - Internal 1000 ps — below the 1200 ps minimum;
delay_out_of_range. - Board trace 2000 ps with no internal delay — total 2000 ps; no flags. The v1.3 arrangement, correctly recognised as valid.
- Board 2000 ps plus internal 2000 ps — total 4000 ps;
double_delay_suspected. The subtle case where nobody configured two devices to delay.
The phase sweep
- All eight taps pass —
eye_width_taps = 8,chosen_tap = 4,narrow_eyelow. - Taps 2–7 pass, 0–1 fail — width 6, chosen tap 5, no narrow-eye flag.
- Only tap 3 passes — width 1, chosen 3,
narrow_eyehigh while the link works perfectly. The scenario the module exists for. - Taps 1 and 5 pass, everything between fails — the longest contiguous run is 1, so the chosen tap is 1 or 5 and never 3. The averaging bug, tested directly.
- No tap passes —
no_working_taphigh,chosen_tap_validlow; a distinct outcome from having chosen. - A tap with one error in 100 000 samples — fails, because
ERROR_BUDGETis zero. A nearly-right tap is at the eye's edge, not in it. - A sweep interrupted by
start— restarts cleanly from tap 0 with the mask cleared.
Speed changes and conformance
- 1000 → 100 while idle — quiesce, disable, re-lock,
resample_request, enable. - 1000 → 100 with traffic in flight —
changed_under_loadcounts; the change still completes. RXCnot returning after the re-lock window —change_timed_out.- A speed change that omits the re-sweep (deliberate mutation) — P22 fires. The step designs skip.
- Preamble arriving as
0x55on every frame —verdict = 0. - Preamble arriving as the same wrong value on every frame —
verdict = 1, double delay. - Preamble arriving as a different wrong value on every frame —
verdict = 2, no delay.
18. Debugging: One Parameter, Two Mirror Failures
| Observation | Likely cause | The distinguishing check |
|---|---|---|
| works at 100, fails at 1000, same cable | the delay — this is the signature | verdict; the UI is 5× longer at 100 Mb/s |
| 100% FCS at 1000, corruption consistent frame to frame | both ends delaying | verdict = 1; preamble reads the same wrong value every time |
| 100% FCS at 1000, corruption different every frame | neither end delaying | verdict = 2; and it changes with temperature |
| the same, but different between units of one board | neither delaying, confirmed | a metastability signature; a double delay would be identical across units |
link works, narrow_eye high | one tap of margin | nothing else in the design reports this; act before it fails |
| errored frames accepted as good | RX_CTL sampled on one edge only | rx_er permanently zero while rx_dv is correct |
| link state flapping millions of times a second | status decoder not gated on the gap | payload nibbles being read as link/speed/duplex |
| a single spurious link-down per hour | status decoder not debounced | STABLE_SAMPLES; the nibble is sampled every idle cycle |
| worked, then failed after a PHY firmware update | a strap or register default changed the delay | expected_total_rx_delay_ps before and after |
| a frame lost at every speed change | normal — changed_under_load | attribute it and stop investigating |
Four habits.
First, read "works at 100, fails at 1000" as evidence for a delay problem, not against one. It is the reflex test everybody runs, it always passes, and the passing proves only that the cable is connected. The UI at 100 Mb/s is 20 ns against gigabit's 4 ns, so a 2 ns error is 10% of the budget instead of 50% — the same misconfiguration, five times more room.
Second, look at how the corruption is wrong, not that it is wrong. Both delay failures give 100% FCS errors and identical counters. A double delay is deterministic — the same wrong preamble on every frame; no delay is random — a different one each time, and temperature-dependent. That single observation halves the search.
Third, check the delay arithmetic before touching hardware. expected_total_rx_delay_ps is a sum of what this device is configured to do, what the board contributes, and what the far end claims. A total near 4000 ps or near 0 is a configuration answer available without a scope, and it is wrong far more often than the hardware is.
Fourth, treat narrow_eye on a working link as an open item. It is the only forward-looking signal RGMII produces: the link works today and has one tap of margin, which is not a fault and is not nothing.
19. Common Misconceptions
"RGMII is GMII with half the pins."
The wrong model: the same interface, more efficiently wired.
What it costs: you cannot explain the delay configuration, the two symmetric failures, or why the interface works at 100 Mb/s and fails at 1000.
The corrected model: it is GMII folded in half, and the fold halves the bit time as well as the pin count. 4 bits × 2 edges × 125 MHz is the same gigabit, but each bit is valid for 4 ns instead of 8 — so setup and hold consume 50% of the window rather than 25%, and the clock must be deliberately moved into the eye by roughly 2 ns. That delay is a configuration item with three possible owners and no protocol to choose between them, which is the entire subject of this chapter.
"The clock and data arrive together, so the timing is fine."
The wrong model: source-synchronous means aligned means correct.
What it costs: you do not understand why a delay is needed at all, and you cannot reason about either failure.
The corrected model: aligned is the one phase that cannot work. The clock and data leave the transmitter on the same edge through matched traces, so the clock edge arrives exactly where the data is changing — violating setup and hold simultaneously. Source-synchronous signalling delivers a clock with a known relationship to the data; making that relationship a usable one is a separate, deliberate act, and it costs about half a UI.
"Both ends adding delay is safer than neither."
The wrong model: extra margin, applied twice, is conservative.
What it costs: a link that is corrupted deterministically rather than randomly, which is not better.
The corrected model: the two are symmetric failures, not a safe side and an unsafe one. 2 + 2 = 4 ns is exactly one UI, so the clock samples the next bit — the data is uniformly shifted, every octet, on every unit, at every temperature. Neither delaying samples on the transition and gives random corruption. Both give 100% FCS errors; they differ only in whether the corruption is consistent, which is what makes them diagnosable.
"The sampling tap is calibrated, so the timing is verified."
The wrong model: a sweep that chose a tap has proved the interface meets timing.
What it costs: Section 16's rejected property, and a design that hardcodes the answer because an assertion rewarded it.
The corrected model: the sweep measures a consequence, not a cause. It knows which taps produced correct data during a test; it does not know where the eye is in picoseconds, because the mapping from tap index to time depends on the delay line's calibration, both packages, and four traces against one. What the sweep produces that is genuinely valuable is the eye WIDTH — six taps of margin against one — and a design that reports only the chosen tap has thrown away the number a human needed.
"In-band status is a convenience."
The wrong model: a shortcut that saves an MDIO read.
What it costs: you miss that it changes what a MAC can know and when.
The corrected model: every earlier interface learned speed and duplex only by polling MDIO, so a MAC's belief was always a cached copy of something read at an unspecified earlier time. RGMII carries the PHY's current view continuously, on pins that were idle anyway, at zero cost — a speed change is visible in microseconds rather than at the next poll, and a link-down is an event rather than a discovery. Chapter 10.5 takes the idea further and puts the same information inside the idle stream itself.
20. Interview Reasoning
"What does RGMII change relative to GMII, and what does that cost?"
It folds the data bus in half and recovers the rate with double-data-rate clocking — 4 bits × 2 edges × 125 MHz = 1000 Mb/s — halving the signal count from 24 to 12 while keeping the clock at 125 MHz and the octet time at 8 ns, so no throughput and no latency are given up. The cost is the bit time: 4 ns instead of 8, so a 1 ns setup and 1 ns hold consume 50% of the window rather than 25%. The strong answer names the consequence: the clock arrives edge-aligned with the data, which violates setup and hold simultaneously, so roughly 2 ns of delay must be added by somebody — the PHY, the MAC, or the board — and nothing in the protocol establishes which. The finishing point: TX_EN and TX_ER also fold onto one wire, encoded as TX_EN on the rising edge and TX_EN XOR TX_ER on the falling, which keeps the wire static during both idle and normal frames.
"An RGMII link works at 100 Mb/s and fails at 1000. Where do you look?"
At the delay configuration, and the working-at-100 result is evidence for that, not against it. The UI at 100 Mb/s is 20 ns against gigabit's 4 ns, so a 2 ns delay error is 10% of the budget instead of 50% — the same misconfiguration with five times the room. The strong answer separates the two failures by their pattern: both ends delaying gives 2 + 2 = 4 ns, exactly one UI, so the clock samples the neighbouring bit and the corruption is deterministic — the same wrong preamble on every frame, on every unit, at every temperature. Neither delaying leaves the clock on the transition, so the corruption is random and temperature-dependent. Both give 100% FCS errors and identical counters; only the consistency distinguishes them. The finishing observation: the link still comes up and reports the right speed and duplex, because in-band status is four static bits with enormous margin.
"How would you set the RGMII sampling delay in a design you were building?"
By measuring, not by configuring. A sweep steps the receive sampling tap across its whole range, runs a known pattern at each, and records which taps produce zero errors. The longest contiguous run of passing taps is the measured eye, and its midpoint is the tap with margin on both sides — and contiguity matters, because two isolated passing taps are not an eye and averaging their indices selects the failing tap between them. The answer that ends the topic names the output that matters: not the chosen tap but the eye width. A link with six passing taps and a link with one both work, both pass traffic, and both look identical to every counter — and the second is one temperature step from failing. narrow_eye asserting on a perfectly working link is the only forward-looking signal the interface produces.
"Would you assert that the chosen sampling tap is the eye centre?"
No — and it is a different objection from the GMII one. There, the quantity was absent from the model: setup time at a pin does not exist in RTL. Here the quantity is present — chosen_tap is a real register and TAP_CENTRE is a real constant, and comparing them is meaningful arithmetic. What is absent is the mapping: the tap is an index, the eye centre is a position in picoseconds, and the correspondence is set by the delay line's calibration, the packages, four traces against one, and whatever the far end did. So the property asserts that a name I chose matches a fact I cannot measure — and worse, a design graded on it has every incentive to skip the sweep and hardcode the answer, which works on the development board and fails on the next one. Assert the measurement instead: the sweep visits every tap, the chosen tap actually passed, a narrow eye is reported even though the link works, and "no tap worked" is a distinct outcome from "chose one".
21. Understanding Check
Because the recovered bandwidth comes from using both clock edges, and two bits per period means each bit lasts half as long.
| GMII | RGMII | |
|---|---|---|
| clock | 125 MHz | 125 MHz |
| period | 8 ns | 8 ns |
| edges used | 1 | 2 |
| bit time (UI) | 8 ns | 4 ns |
The clock frequency did not change. The interface still runs at 125 MHz, still moves an octet per period, and still takes 12.144 µs to carry a 1518-octet frame. Only the width of a valid data bit changed.
Now spend the budget with 1 ns of setup and 1 ns of hold:
| GMII | RGMII | |
|---|---|---|
| setup + hold | 2 ns of 8 | 2 ns of 4 |
| left over | 6 ns (75%) | 2 ns (50%) |
And "left over" is what absorbs the transmitter's clock-to-out spread, two packages, the mismatch between one clock trace and four data traces, temperature, voltage and process. A design with 6 ns of slack tolerates a couple of nanoseconds of accumulated error without anyone thinking about it. A design with 2 ns does not — which is why RGMII is the first interface in this module where the delay must be engineered rather than merely not broken.
22. What's Next
The claim this chapter defended: a continuous configuration parameter that no protocol establishes is worse than a discrete one.
RGMII folds GMII in half — 4 bits × 2 edges × 125 MHz on twelve signals instead of twenty-four, with the same octet time, the same throughput and no added latency. A 24-port design saves 288 pins for nothing.
What it spends is the bit time. 4 ns instead of 8, so setup and hold take 50% of the window rather than 25%, and the clock — which arrives edge-aligned with the data, the one phase that cannot work — must be moved into the eye by roughly 2 ns. The PHY can do it, the MAC can do it, or 339 mm of extra board trace can do it, and nothing says which. Both ends doing it sums to exactly one UI and samples the next bit deterministically; neither doing it samples on the transition and gives random, temperature-dependent corruption. Both come up, negotiate, report themselves healthy and corrupt every frame — and both work perfectly at 100 Mb/s, where the UI is five times longer.
The honest response is not to assert the timing, which no RTL can see, nor to assert that a chosen tap is the eye centre, which compares one of the designer's names against another. It is to measure the consequence — sweep every tap, find the longest contiguous passing run, and report its width — so a link with one tap of margin can be told from one with six while both are still working.
And RGMII gave something back: in-band status, three messages packed into a gap that every earlier interface left empty, turning a polled register read into a continuous statement.
Chapter 10.5 — SGMII stops folding and starts serialising.
Every interface in this module so far has been a parallel bus that got narrower — 4 bits, 2 bits, 8 bits, 4 bits on two edges. SGMII abandons parallel data entirely: the whole interface, both directions, on two differential pairs at 1.25 Gbaud, which is 1000 Mb/s × 10/8 for 8B/10B. Four wires instead of twelve, and — remarkably — the same four wires at 10, 100 and 1000 Mb/s, because the lower speeds are carried by repeating each code group rather than by changing the clock. There is no speed-dependent reconfiguration, no delay to set, and no per-speed timing to close. And the speed and duplex that RGMII squeezed into an idle nibble move inside the ordered sets that fill the idle stream itself, replacing autonegotiation's link code word with something the interface carries continuously.
The full path is on the Ethernet curriculum index.
Continue learning
Related tutorials
- Related topic
The Reconciliation Sublayer and the xMII Contract
The xMII generations are a record of what each had to give up — width, pins, timing margin, even parallelism — to keep carrying the same vocabulary as rates rose. That one vocabulary survived six unrelated physical forms is what media-independence actually means.
- Related topic
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.
- Related topic
Packet Switching
A circuit allocates capacity in advance and guarantees it; a packet network allocates on demand and guarantees nothing. The exchange is measurable in RTL — idle reserved slots against buffered, delayed and occasionally dropped packets — and it is why a packet must describe its own extent and destination.
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.
