Skip to content
VLSI Mentor

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

The reduced gigabit media independent interface carries four transmit data bits, a single transmit control signal, and the transmit clock from the media access control layer to the physical layer. In the receive direction the physical layer sends four receive data bits, a single receive control signal, and the receive clock. That is six signals in each direction, twelve in total, against twenty four for the full gigabit interface. Both directions clock data on both edges of their clock, so each data bit occupies half a clock period. The single control signal in each direction carries two meanings, one sampled on the rising edge and one on the falling edge.MACsources TXCTXD[3:0], TX_CTL,TXC6 signals →PHYsources RXCRXD[3:0], RX_CTL,RXC6 signals ←Both edgesUI = 4 ns, not 8One control wiretwo meanings, two edges12
Figure 1 — twelve signals instead of twenty-four, with two control meanings folded onto one wire per direction.
SignalWidthDirectionRising edgeFalling edge
TXD[3:0]4MAC → PHYdata bits 3:0data bits 7:4
TX_CTL1MAC → PHYTX_ENTX_EN XOR TX_ER
TXC1MAC → PHY125 / 25 / 2.5 MHz
RXD[3:0]4PHY → MACdata bits 3:0data bits 7:4
RX_CTL1PHY → MACRX_DVRX_DV XOR RX_ER
RXC1PHY → MAC125 / 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.

QuantityGMIIRGMII
clock125 MHz125 MHz
clock period8 ns8 ns
edges used per period12
bit time (UI)8 ns4 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 UIRGMII, 4 ns UI
setup + hold consumed1 + 1 = 2 ns1 + 1 = 2 ns
left for everything else6 ns2 ns
as a fraction of the UI75%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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
 
endmodule

Classification: 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

risefallTX_ENTX_ERMeaning
0000idle, or normal interframe
1110normal frame data
1011frame data, deliberately corrupted
0101an 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
 
endmodule

Classification: 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.

WhereNamedHow it is setCost
the boardRGMII v1.3's approachextra clock trace lengthsee below
the PHYRGMII-ID (v2.0)a strap pin or an MDIO registernone, if configured once
the MACMAC-side delayan SoC register or a device-tree propertynone, 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
 
endmodule

Classification: 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 clock and data leave the transmitter aligned, so the clock edge arrives at the data transition, which violates setup and hold at the same moment. About half a bit time of delay must be added to move the clock into the centre of the data eye. If both the media access control device and the physical layer device add that delay, the total is a full bit time and the clock samples the next bit, shifting all data by one nibble uniformly. If neither adds it, the clock stays on the transition and the captured value is whatever the flop settles to. Both cases produce a link that comes up, negotiates and reports itself healthy while corrupting every frame, and the two are distinguished by whether the corruption is a consistent shift or random.Launched alignedclock on the transitionNeeds ~2 nshalf a 4 ns UIExactly one appliesclock in the eyeBoth apply: 4 nsone whole UIData shifted anibbleconsistent corruptionNeither applies: 0nsrandom corruption12
Figure 2 — one parameter, two mirror-image failures, and both produce a link that comes up and corrupts data.

The two failures are mirror images arithmetically and completely different diagnostically, and the difference is the most useful thing in this chapter.

Both ends delayNeither end delays
total delay2 + 2 = 4 ns0 ns
as a fraction of the 4 ns UIexactly 1 UI0 UI
where the clock landson the next biton the data transition
what is capturedthe neighbouring nibblewhatever the flop settles to
corruption patternconsistent — the same shift on every octetrandom, and temperature-dependent
does it change with temperaturenoyes
does it change between boardsnoyes, and between units of the same board
FCS failures100%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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
 
endmodule

Classification: 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 riseRX_CTL fallRX_DVRX_ERRXD[3:0] means
1110frame data
1011frame data, flagged as errored
0000in-band status — link, speed, duplex
0101an 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

A bring-up self test steps the receive sampling tap through every available position and, at each one, runs a known pattern and counts errors. Taps that fail sit at the edges of the data eye where setup or hold is violated. The contiguous run of taps that pass is the measured eye width, and the middle of that run is the tap with the most margin on both sides. Choosing the tap by measurement rather than by a hardcoded constant means the design adapts to whatever the board and the far device actually did, and reporting the measured eye width tells a human whether the chosen tap had margin or was the only one that worked.Step every tap0 … PHASES−1Known patterncount errors per tapContiguous passingrun= the measured eyeChoose the middleargmax of marginReport the width6 of 8, or 1 of 8A human can actmargin, or luck12
Figure 3 — sweeping every sampling tap turns an unverifiable configuration into a measured eye width and a chosen centre.
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
 
endmodule

Classification: 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
 
endmodule

Classification: 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
 
endmodule

Classification: 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.

PortsGMII signalsRGMII signalsSaved
1241212
4964848
81929696
24576288288

And the latency is unchanged, for exactly the reason Chapter 10.2's was.

GMIIRGMII
bits per transfer84
transfers per clock period12
clock period8 ns8 ns
octet time8 ns8 ns
a 1518-octet frame12.144 µs12.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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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

  1. A single octet at 1000 Mb/s — low nibble on the rising edge, high nibble on the falling, one TXC period.
  2. 0xA5txd_rise = 0x5, txd_fall = 0xA. A value whose halves differ.
  3. 0x00 and 0xFF — the two values where a nibble swap is invisible; included precisely because they cannot catch the bug.
  4. Back-to-back octets — one per period, octet_ready high throughout.
  5. Idletxctl_rise and txctl_fall both low, and the wire does not toggle within any period.
  6. A normal frame — both control samples high; again static.
  7. tx_error asserted for one octetrise = 1, fall = 0; the only case where the wire toggles within a period.
  8. A frame that stops with no octet behind itunderrun pulses.
  9. The full receive decoderx_dv = rise, rx_er = rise XOR fall, checked across all four combinations.
  10. rise = 0, fall = 1interframe_code asserted, octet_valid low. Not data, not an error.
  11. A receiver that samples RX_CTL only on the rising edge (a deliberate mutation) — rx_dv is correct, rx_er is permanently zero, and errored frames are accepted as good. The silent half-loss.

In-band status

  1. RXD[3:0] = 0xD steadily in the gap — link up, 1000 Mb/s, full duplex, after STABLE_SAMPLES matching observations.
  2. A single-cycle glitch to 0xCno status change; the debounce absorbs it.
  3. A change to 0x0 held stablylink_went_down, c_link_downs increments, ever_link_down sticky.
  4. A change from 1000 to 100 Mb/sspeed_changed, and the value decodes to SPD_100.
  5. RXD[2:1] = 11reserved_speed_seen, and speed is SPD_INVALID rather than mapped to anything legal.
  6. Status nibbles present during a frameignored; the decoder is gated on !rx_dv && !rx_er.
  7. 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.
  8. clear after a link-down — counters clear, ever_link_down survives.

The delay arbiter

  1. Out of reset with cfg_rx_delay_source = DLY_INTERNAL already presented — both delays disabled until cfg_valid.
  2. Internal delay 2000 ps, partner claims none, board 0 — total 2000 ps; neither flag set.
  3. Internal 2000 ps, partner claims 2000 ps — total 4000 ps; double_delay_suspected, ever_double_delay sticky.
  4. No internal, no partner, no board — total 0; no_delay_suspected.
  5. Internal 1000 ps — below the 1200 ps minimum; delay_out_of_range.
  6. Board trace 2000 ps with no internal delay — total 2000 ps; no flags. The v1.3 arrangement, correctly recognised as valid.
  7. 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

  1. All eight taps passeye_width_taps = 8, chosen_tap = 4, narrow_eye low.
  2. Taps 2–7 pass, 0–1 fail — width 6, chosen tap 5, no narrow-eye flag.
  3. Only tap 3 passes — width 1, chosen 3, narrow_eye high while the link works perfectly. The scenario the module exists for.
  4. 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.
  5. No tap passesno_working_tap high, chosen_tap_valid low; a distinct outcome from having chosen.
  6. A tap with one error in 100 000 samplesfails, because ERROR_BUDGET is zero. A nearly-right tap is at the eye's edge, not in it.
  7. A sweep interrupted by start — restarts cleanly from tap 0 with the mask cleared.

Speed changes and conformance

  1. 1000 → 100 while idle — quiesce, disable, re-lock, resample_request, enable.
  2. 1000 → 100 with traffic in flightchanged_under_load counts; the change still completes.
  3. RXC not returning after the re-lock windowchange_timed_out.
  4. A speed change that omits the re-sweep (deliberate mutation) — P22 fires. The step designs skip.
  5. Preamble arriving as 0x55 on every frameverdict = 0.
  6. Preamble arriving as the same wrong value on every frameverdict = 1, double delay.
  7. Preamble arriving as a different wrong value on every frameverdict = 2, no delay.

18. Debugging: One Parameter, Two Mirror Failures

ObservationLikely causeThe distinguishing check
works at 100, fails at 1000, same cablethe delay — this is the signatureverdict; the UI is 5× longer at 100 Mb/s
100% FCS at 1000, corruption consistent frame to frameboth ends delayingverdict = 1; preamble reads the same wrong value every time
100% FCS at 1000, corruption different every frameneither end delayingverdict = 2; and it changes with temperature
the same, but different between units of one boardneither delaying, confirmeda metastability signature; a double delay would be identical across units
link works, narrow_eye highone tap of marginnothing else in the design reports this; act before it fails
errored frames accepted as goodRX_CTL sampled on one edge onlyrx_er permanently zero while rx_dv is correct
link state flapping millions of times a secondstatus decoder not gated on the gappayload nibbles being read as link/speed/duplex
a single spurious link-down per hourstatus decoder not debouncedSTABLE_SAMPLES; the nibble is sampled every idle cycle
worked, then failed after a PHY firmware updatea strap or register default changed the delayexpected_total_rx_delay_ps before and after
a frame lost at every speed changenormalchanged_under_loadattribute 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 clocking4 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 presentchosen_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.

GMIIRGMII
clock125 MHz125 MHz
period8 ns8 ns
edges used12
bit time (UI)8 ns4 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:

GMIIRGMII
setup + hold2 ns of 82 ns of 4
left over6 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

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.