Ethernet · Module 10
XGMII — Seventy-Four Signals, and the Interface Built to Avoid Them
32 data and 4 control bits each way at 156.25 MHz DDR is 74 signals nobody routes. XAUI carries the same 10 Gb/s on sixteen wires and pays in per-lane deskew — plus a transparency that holds for frames and not for the stream.
Chapter 9.4 established what XGMII carries: 64B/66B below it, four byte lanes, a start character confined to lane 0, and a deficit idle mechanism that keeps the interframe gap honest four bytes at a time.
What it did not confront is the pin count that width implies.
32 data + 4 control + 1 clock = 37 signals per direction
37 × 2 = 74 signals
Seventy-four, clocked at 156.25 MHz on both edges, across a board.
That is not an interface anybody wants to route, and 10 Gigabit's answer was to define a second interface whose only job is to make the first one unnecessary between chips: XAUI, four lanes at 3.125 Gbaud with 8B/10B, sixteen wires instead of seventy-four.
And XAUI brings back everything Chapter 10.5 just built, multiplied by four — comma alignment per lane, running disparity per lane, ordered sets per lane — plus one problem SGMII does not have. Four independently serialised lanes traverse four paths and arrive at genuinely different times, so a receiver must deskew them before the four streams can be reassembled into one.
1. Scope — What This Chapter Owns
This chapter owns XGMII and XAUI as interfaces: the pin count, the clocking, the serialisation that exists because of the pin count, the per-lane machinery it requires, and the deskew problem it introduces.
It does not re-derive what other chapters own. Chapter 9.4 owns 64B/66B, the lane-0 start rule and deficit idle; Chapter 3.5 owns 8B/10B's construction; Chapter 10.5 owns comma alignment, running disparity and their convergence behaviour, which this chapter uses four times over; Chapter 4.4 owns clock compensation as a mechanism, which the /R/ column implements here; Chapter 9.5 owns lane striping and deskew at 100 Gigabit, which is the same problem at a different scale with a different marker.
Chapter 11.1 opens the module that owns link discovery and autonegotiation in their own right.
The claim this chapter defends: a layer described as transparent is transparent at exactly one granularity, and asserting its guarantee at a finer one produces a property that is false because of the mechanism the layer needs in order to be transparent at all.
2. Seventy-Four Signals
| Signal | Width | Direction | Notes |
|---|---|---|---|
TXD[31:0] | 32 | MAC → PHY | four byte lanes |
TXC[3:0] | 4 | MAC → PHY | one control bit per lane |
TX_CLK | 1 | MAC → PHY | 156.25 MHz, both edges |
RXD[31:0] | 32 | PHY → MAC | |
RXC[3:0] | 4 | PHY → MAC | |
RX_CLK | 1 | PHY → MAC | 156.25 MHz, both edges |
| 74 |
The clocking, computed:
10 Gb/s ÷ 32 bits = 312.5 M transfers/s
312.5 ÷ 2 = 156.25 MHz DDR
And 74 signals at 156.25 MHz DDR is not a routing problem so much as a package problem. Every signal is single-ended, every group must be length-matched, and Chapter 10.4's entire delay argument applies to thirty-six data and control lines against one clock rather than to four.
Compare the whole module's pin counts in one place, because XGMII is the outlier in both directions.
| Interface | Rate | Signals |
|---|---|---|
| SGMII | 1 Gb/s | 4 |
| RMII | 100 Mb/s | 8 |
| RGMII | 1 Gb/s | 12 |
| MII | 100 Mb/s | 16 |
| GMII | 1 Gb/s | 24 |
| XGMII | 10 Gb/s | 74 |
| XAUI | 10 Gb/s | 16 |
XGMII carries ten times GMII's rate on three times its pins, which is efficient per bit and unusable in absolute terms. XAUI carries the same 10 Gb/s on sixteen wires — a 4.6× reduction — and is what actually appears on a board.
3. The Rate, Computed
XAUI's numbers all fall out of one requirement: carry an XGMII column, and do it on four serial lanes.
| Step | Working | Result |
|---|---|---|
| XGMII column | 4 bytes | 32 bits |
| columns per second | 10 Gb/s ÷ 32 | 312.5 M |
| one lane carries one byte per column | — | 312.5 M bytes/s per lane |
| as code groups | one per byte | 312.5 M code groups/s |
| 8B/10B | × 10 bits each | 3125 Mbaud per lane |
| four lanes | 4 × 3.125 × 8/10 | 10 Gb/s ✓ |
| unit interval | 1 ÷ 3.125 GHz | 320 ps |
The third row is the design. An XGMII column is four bytes on four byte lanes; XAUI gives each byte lane its own serial lane. So the mapping is not a striping decision at all — lane n of XAUI carries byte lane n of XGMII, always — which makes the adapter almost trivial and the reassembly exact.
Compare that with Chapter 9.5's 100 Gigabit striping, where 66-bit blocks are dealt round-robin across twenty PCS lanes and the receiver must recover the deal from alignment markers.
| XAUI | 100GBASE-R | |
|---|---|---|
| how data maps to lanes | byte lane n → serial lane n | round-robin across 20 |
| lanes are interchangeable | no — each carries its own byte position | no — each carries its own deal position |
| how a receiver knows which lane is which | /A/ columns, sent simultaneously | alignment markers naming the lane |
| what identifies a lane | its wire | its marker's contents |
| a swapped lane | breaks the link | is absorbed |
The last row is the difference that matters. XAUI lanes are identified by which wire they arrive on, so swapping two is a board fault that produces perfectly legal traffic with the bytes rearranged — exactly Chapter 9.4 §9's XGMII lane swap, one layer down. 100 Gigabit's markers carry the lane number in the data, which is why a swap there is a supported condition.
XAUI's /A/ columns solve a different problem: not which lane, but when.
4. RTL 1 — XGMII In, Four Lanes Out
// SYNTHESIZABLE.
//
// The XGMII Extender Sublayer's transmit half: take an XGMII column,
// convert each byte lane to an 8B/10B code group, and hand it to that
// lane's serialiser.
//
// THE MAPPING:
// XGMII byte lane n -> XAUI serial lane n. Always. Every column.
// There is no striping and no deal, because the receiver identifies
// a lane by WHICH WIRE it arrives on -- unlike Chapter 9.5's PCS
// lanes, which carry their own names.
//
// THE NUMBERS:
// 10 Gb/s / 32 bits = 312.5 M columns/s
// one byte per lane per column = 312.5 M code groups/s per lane
// x 10 bits (8B/10B) = 3125 Mbaud per lane
// 4 lanes x 3.125 x 8/10 = 10 Gb/s <- closes
// 1 UI = 320 ps
//
// XGMII CONTROL BITS BECOME CODE GROUPS. TXC[n] set means TXD[8n+7:8n]
// is a control character, which maps to an 8B/10B K code group. So the
// four control WIRES disappear into the encoding, exactly as Chapter
// 10.5's TX_CTL did.
package xaui_pkg;
localparam int unsigned LANES = 4;
localparam int unsigned CODE_BITS = 10;
localparam int unsigned LANE_MBAUD = 3125;
localparam int unsigned COLUMNS_PER_S_M = 312; // 312.5, truncated
// 1 UI = 320 ps. Clause 47 receivers absorb roughly 40 UI of skew:
// 40 x 320 ps = 12.8 ns
// which is the number Section 9 turns into a FIFO depth.
localparam int unsigned UI_PS = 320;
localparam int unsigned SKEW_UI = 40;
localparam int unsigned SKEW_PS = SKEW_UI * UI_PS; // 12800
// XAUI code groups. /K/ carries the comma, /A/ is the deskew marker,
// /R/ is the skip used for clock compensation.
localparam logic [7:0] K28_0 = 8'h1C; // /R/ skip
localparam logic [7:0] K28_3 = 8'h7C; // /A/ align
localparam logic [7:0] K28_5 = 8'hBC; // /K/ sync, contains the comma
localparam logic [7:0] K27_7 = 8'hFB; // /S/ start
localparam logic [7:0] K29_7 = 8'hFD; // /T/ terminate
localparam logic [7:0] K30_7 = 8'hFE; // /E/ error
// The idle sequence is A K R K R K R ... with an /A/ column roughly
// every 16 columns, so deskew markers arrive often enough to re-align
// quickly and rarely enough to cost almost nothing.
localparam int unsigned A_INTERVAL = 16;
typedef enum logic [2:0] {
COL_DATA, COL_IDLE_A, COL_IDLE_K, COL_IDLE_R,
COL_START, COL_TERMINATE, COL_ERROR
} column_kind_e;
endpackage
module xgmii_to_xaui_adapter
import xaui_pkg::*;
#(
parameter int unsigned CNT_W = 24
) (
input logic clk, // 312.5 MHz column clock
input logic rst_n,
// XGMII, as Chapter 9.4 defines it.
input logic [31:0] xgmii_d,
input logic [3:0] xgmii_c,
input logic xgmii_valid,
// To the four lane encoders.
output logic [LANES-1:0][7:0] lane_octet,
output logic [LANES-1:0] lane_is_k,
output logic lane_valid,
output column_kind_e column_kind,
output logic [CNT_W-1:0] c_columns,
output logic [CNT_W-1:0] c_data_columns,
output logic [CNT_W-1:0] c_idle_columns,
// A start character on a lane other than 0. Chapter 9.4 owns the
// rule; this module owns DETECTING it before it is serialised,
// because a serialised illegal column is four lanes of legal-looking
// code groups that mean nothing.
output logic start_off_lane0,
output logic [CNT_W-1:0] c_start_off_lane0
);
integer i;
always_comb begin
column_kind = COL_DATA;
if (xgmii_c[0] && (xgmii_d[7:0] == K27_7)) column_kind = COL_START;
else if (|xgmii_c && (xgmii_d[7:0] == K29_7)) column_kind = COL_TERMINATE;
else if (&xgmii_c && (xgmii_d[7:0] == K28_5)) column_kind = COL_IDLE_K;
else if (&xgmii_c && (xgmii_d[7:0] == K28_3)) column_kind = COL_IDLE_A;
else if (&xgmii_c && (xgmii_d[7:0] == K28_0)) column_kind = COL_IDLE_R;
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
lane_valid <= 1'b0; c_columns <= '0; c_data_columns <= '0;
c_idle_columns <= '0; start_off_lane0 <= 1'b0;
c_start_off_lane0 <= '0;
for (i = 0; i < LANES; i = i + 1) begin
lane_octet[i] <= 8'd0;
lane_is_k[i] <= 1'b0;
end
end else begin
lane_valid <= 1'b0;
start_off_lane0 <= 1'b0;
if (xgmii_valid) begin
// THE MAPPING. Byte lane n to serial lane n, and the control
// bit becomes the K/data distinction the encoder needs.
for (i = 0; i < LANES; i = i + 1) begin
lane_octet[i] <= xgmii_d[8*i +: 8];
lane_is_k[i] <= xgmii_c[i];
end
lane_valid <= 1'b1;
if (!(&c_columns)) c_columns <= c_columns + 1'b1;
if (|xgmii_c) begin
if (!(&c_idle_columns)) c_idle_columns <= c_idle_columns + 1'b1;
end else begin
if (!(&c_data_columns)) c_data_columns <= c_data_columns + 1'b1;
end
// A start character anywhere but lane 0 is illegal (Chapter
// 9.4 §4). Detecting it HERE matters, because once serialised
// it becomes four lanes of individually valid code groups and
// nothing downstream has any reason to object.
for (i = 1; i < LANES; i = i + 1) begin
if (xgmii_c[i] && (xgmii_d[8*i +: 8] == K27_7)) begin
start_off_lane0 <= 1'b1;
if (!(&c_start_off_lane0))
c_start_off_lane0 <= c_start_off_lane0 + 1'b1;
end
end
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that XGMII's four control wires disappear into the encoding, exactly as Chapter 10.5's TX_CTL did. TXC[n] does not travel — it selects whether lane n's byte becomes a K code group or a data code group, and the receiver recovers it from the 8B/10B decode. Four wires become zero, and the information survives.
Deliberately simplified: the column classifier examines lane 0 for most kinds. A production adapter checks all four lanes against the full column format, since a well-formed idle column requires the same character on all four.
Production implication: start_off_lane0 is detected before serialisation and that placement is the point. After the adapter, an illegal column has become four individually valid, individually disparity-correct code groups, and nothing downstream has any reason to reject them. The far end will reassemble a column with a start character in the wrong place and behave undefinedly, which is Chapter 9.4 §12's rejected property arriving in a new form: the rule must be enforced where it is still expressible.
5. RTL 2 — Four Encoders, Four Disparities
// SYNTHESIZABLE (with an 8B/10B encoder core per lane).
//
// Four independent 8B/10B encoders, one per XAUI lane.
//
// THE WORD THAT MATTERS IS "INDEPENDENT". Each lane has its OWN running
// disparity, because each lane is its own serial stream with its own
// DC-balance requirement. There is no shared disparity across a column,
// and a design that keeps one is wrong in a way that produces valid
// code groups and a slowly drifting DC level on three of four lanes.
//
// Chapter 10.5 owns 8B/10B's convergence behaviour on ONE lane. Here
// there are four of them, converging independently after a common
// alignment event -- so the convergence WINDOW is per lane, and the
// slowest lane sets when the link is usable.
module xaui_8b10b_lane_encoder
import xaui_pkg::*;
#(
parameter int unsigned CNT_W = 24
) (
input logic clk,
input logic rst_n,
input logic [LANES-1:0][7:0] lane_octet,
input logic [LANES-1:0] lane_is_k,
input logic lane_valid,
output logic [LANES-1:0][CODE_BITS-1:0] lane_code,
output logic [LANES-1:0] lane_code_valid,
// Per-lane running disparity, exported. Four numbers, not one.
output logic [LANES-1:0] lane_rd,
// A lane whose running disparity has been the same polarity for an
// implausibly long run. Not illegal, and a strong hint that the
// encoder's alternate-code selection is broken on that lane.
output logic [LANES-1:0] lane_rd_stuck,
output logic [LANES-1:0][CNT_W-1:0] c_lane_codes,
output logic [LANES-1:0][7:0] lane_rd_run
);
// A run of same-polarity disparity longer than this is a hint, not a
// violation -- legal data can produce long runs, but not this long
// by chance.
localparam int unsigned RD_RUN_ALARM = 8'd64;
// Placeholder for the 8B/10B encoder core. Chapter 3.5 owns the
// table; what matters here is that it CONSUMES and PRODUCES the
// running disparity, which is why each lane needs its own copy.
function automatic logic [CODE_BITS-1:0] encode_8b10b
(input logic [7:0] octet, input logic is_k, input logic rd_in);
encode_8b10b = {rd_in, is_k, octet};
endfunction
function automatic logic next_rd
(input logic [7:0] octet, input logic is_k, input logic rd_in);
next_rd = rd_in ^ (^octet) ^ is_k;
endfunction
integer i;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (i = 0; i < LANES; i = i + 1) begin
lane_code[i] <= '0;
lane_code_valid[i] <= 1'b0;
lane_rd[i] <= 1'b0;
lane_rd_stuck[i] <= 1'b0;
lane_rd_run[i] <= 8'd0;
c_lane_codes[i] <= '0;
end
end else begin
for (i = 0; i < LANES; i = i + 1) lane_code_valid[i] <= 1'b0;
if (lane_valid) begin
for (i = 0; i < LANES; i = i + 1) begin
lane_code[i] <= encode_8b10b(lane_octet[i], lane_is_k[i],
lane_rd[i]);
lane_code_valid[i] <= 1'b1;
// EACH LANE UPDATES ITS OWN DISPARITY. Sharing one across
// four lanes produces valid code groups on every lane and a
// DC level that drifts on three of them.
lane_rd[i] <= next_rd(lane_octet[i], lane_is_k[i], lane_rd[i]);
if (next_rd(lane_octet[i], lane_is_k[i], lane_rd[i]) == lane_rd[i]) begin
if (lane_rd_run[i] != 8'hFF) lane_rd_run[i] <= lane_rd_run[i] + 8'd1;
if (lane_rd_run[i] == 8'(RD_RUN_ALARM)) lane_rd_stuck[i] <= 1'b1;
end else begin
lane_rd_run[i] <= 8'd0;
lane_rd_stuck[i] <= 1'b0;
end
if (!(&c_lane_codes[i])) c_lane_codes[i] <= c_lane_codes[i] + 1'b1;
end
end
end
end
endmoduleClassification: synthesizable, with an 8B/10B encoder core per lane; the encode and disparity functions are placeholders for Chapter 3.5's table.
What it teaches: that running disparity is per lane and not per column, which is easy to get wrong because a column looks like the natural unit. Each XAUI lane is an independent, AC-coupled serial link with its own DC-balance requirement — sharing one disparity variable across four encoders produces code groups that are individually valid on every lane and a DC level that drifts on three of them, because three lanes' balance is being corrected using a fourth lane's data.
Deliberately simplified: the encoder and disparity functions are placeholders. The real 8B/10B table maps each octet to one of two code groups chosen by the current disparity, and the choice is what keeps the running total bounded.
Production implication: lane_rd_stuck is a hint rather than a violation, and that distinction is deliberate. Legal data can produce long same-polarity runs; sixty-four in a row cannot happen by chance on random data, and it is the signature of an encoder whose alternate-code selection is broken on that lane — which produces perfectly valid code groups and a lane that fails AC coupling after a few microseconds.
6. Why XAUI Needs Alignment Columns
XGMII's four byte lanes arrive together because they are length-matched traces clocked by one clock. XAUI's four lanes are four independent serial links, and nothing makes them agree.
Each lane has its own serialiser, its own transmission line, its own receiver and its own clock-data recovery loop. They leave the transmitter on the same column and arrive at different times, and the standard's receivers absorb roughly 40 UI of that difference:
40 × 320 ps = 12.8 ns
So the receiver needs a marker that was transmitted simultaneously on all four lanes, and that is the /A/ column: K28.3 on every lane at once.
And /A/ lives inside the idle sequence, which is the elegant part:
A K R K R K R K R K R K R K R K A K R K R …
An /A/ column roughly every sixteen columns, embedded in idle that had to be transmitted anyway. Frequent enough that a disturbed link re-aligns in well under a microsecond, and cheap enough to be free — it displaces idle, not data.
| Column | Character | Job |
|---|---|---|
/A/ | K28.3 | deskew — sent simultaneously on all four lanes |
/K/ | K28.5 | sync — carries the comma for code-group alignment |
/R/ | K28.0 | skip — inserted and deleted for clock compensation |
Three characters, three completely different jobs, all inside the idle stream.
7. RTL 3 — Generating the Idle That Carries Everything
// SYNTHESIZABLE.
//
// Generates XAUI's idle sequence, which is three mechanisms wearing one
// costume.
//
// A K R K R K R K R K R K R K R K A K R K R ...
//
// /A/ K28.3 -- transmitted SIMULTANEOUSLY on all four lanes, roughly
// every 16 columns. The deskew reference.
// /K/ K28.5 -- carries the comma, so per-lane code-group alignment
// has something to hunt for.
// /R/ K28.0 -- the skip column. Section 10's clock compensation
// inserts and deletes these, and ONLY these.
//
// WHY /R/ AND NOT /K/ OR /A/ IS THE DELETABLE ONE:
// /A/ is a timing reference -- deleting one loses a deskew
// opportunity, and deleting a pair changes the interval the far
// end measures against.
// /K/ carries the comma -- deleting them starves alignment recovery.
// /R/ carries NOTHING. It exists to be deletable, which is why the
// idle sequence is more than half /R/.
module xaui_align_sequencer
import xaui_pkg::*;
#(
parameter int unsigned CNT_W = 24
) (
input logic clk,
input logic rst_n,
input logic idle_request, // no data to send this column
input logic force_align, // e.g. after a link event
output logic [LANES-1:0][7:0] idle_octet,
output logic [LANES-1:0] idle_is_k,
output logic idle_valid,
output column_kind_e idle_kind,
// Exported so Section 10's compensator knows which columns it may
// delete and which it must not.
output logic is_deletable,
output logic [4:0] columns_since_a,
output logic [CNT_W-1:0] c_a_columns,
output logic [CNT_W-1:0] c_k_columns,
output logic [CNT_W-1:0] c_r_columns,
// The interval since the last /A/ exceeded the nominal. Legal after
// a compensation event; a fault if it persists, because the far end
// measures skew against these.
output logic a_interval_stretched
);
logic [4:0] since_a_q;
logic kr_phase_q; // alternates K and R between /A/ columns
assign columns_since_a = since_a_q;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
since_a_q <= 5'd0; kr_phase_q <= 1'b0;
idle_valid <= 1'b0; idle_kind <= COL_IDLE_K;
is_deletable <= 1'b0; a_interval_stretched <= 1'b0;
c_a_columns <= '0; c_k_columns <= '0; c_r_columns <= '0;
for (int i = 0; i < LANES; i = i + 1) begin
idle_octet[i] <= K28_5;
idle_is_k[i] <= 1'b1;
end
end else begin
idle_valid <= 1'b0;
is_deletable <= 1'b0;
a_interval_stretched <= 1'b0;
if (idle_request) begin
idle_valid <= 1'b1;
if (force_align || (since_a_q >= 5'(A_INTERVAL))) begin
// /A/ COLUMN. K28.3 on ALL FOUR LANES, at the same instant --
// which is the property that makes it a skew measurement
// rather than just another idle character.
for (int i = 0; i < LANES; i = i + 1) begin
idle_octet[i] <= K28_3;
idle_is_k[i] <= 1'b1;
end
idle_kind <= COL_IDLE_A;
is_deletable <= 1'b0; // NEVER deletable
since_a_q <= 5'd0;
kr_phase_q <= 1'b0;
if (!(&c_a_columns)) c_a_columns <= c_a_columns + 1'b1;
end else if (!kr_phase_q) begin
// /K/ -- carries the comma. Not deletable: alignment recovery
// needs a supply of them.
for (int i = 0; i < LANES; i = i + 1) begin
idle_octet[i] <= K28_5;
idle_is_k[i] <= 1'b1;
end
idle_kind <= COL_IDLE_K;
is_deletable <= 1'b0;
kr_phase_q <= 1'b1;
since_a_q <= since_a_q + 5'd1;
if (!(&c_k_columns)) c_k_columns <= c_k_columns + 1'b1;
end else begin
// /R/ -- carries nothing, and exists to be deletable. More
// than half of the idle sequence is /R/ for exactly this
// reason: clock compensation needs a supply of columns whose
// removal costs nothing.
for (int i = 0; i < LANES; i = i + 1) begin
idle_octet[i] <= K28_0;
idle_is_k[i] <= 1'b1;
end
idle_kind <= COL_IDLE_R;
is_deletable <= 1'b1; // THE deletable column
kr_phase_q <= 1'b0;
since_a_q <= since_a_q + 5'd1;
if (!(&c_r_columns)) c_r_columns <= c_r_columns + 1'b1;
end
// The /A/ interval is what the far end measures skew against.
// Stretching it is legal after a compensation event and a fault
// if it persists.
if (since_a_q > 5'(A_INTERVAL + 4)) a_interval_stretched <= 1'b1;
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that /R/ exists in order to be deleted, and the idle sequence's composition follows from that. /A/ is a timing reference and deleting one costs a deskew opportunity. /K/ carries the comma and deleting them starves alignment recovery. /R/ carries nothing at all — it is a placeholder whose only property is that removing it changes nothing — and more than half the idle sequence is /R/ precisely so that clock compensation always has something safe to consume.
Deliberately simplified: the sequence is a strict A K R K R … with a counter. Real transmitters randomise the idle to spread its spectrum, and the /A/ interval is a range rather than a constant.
Production implication: is_deletable is exported rather than recomputed downstream, and the reason is that getting it wrong is silent. A compensator that deletes an /A/ column removes a deskew reference the far end is counting; one that deletes a /K/ starves the comma supply and slows every future re-alignment. Both produce a link that works and re-aligns more slowly than it should, which nothing measures unless something is looking for it.
8. RTL 4 — Deskewing Four Lanes
// SYNTHESIZABLE.
//
// Aligns the four lanes using the /A/ column, and reports the measured
// skew.
//
// THE DEPTH, computed from the standard rather than guessed:
// 1 UI at 3.125 Gbaud = 320 ps
// clause 47 skew budget = ~40 UI
// 40 x 320 ps = 12.8 ns
// one code group = 10 UI = 3.2 ns
// 12.8 / 3.2 = 4 code groups
// so a 6-deep FIFO per lane covers the budget with margin
//
// AND THE MEASUREMENT IS FREE. The /A/ columns left the transmitter
// simultaneously, so the difference in their arrival times IS the skew
// -- obtained as a by-product of the alignment the receiver had to do
// anyway, exactly as Chapter 9.5 §7 gets it from alignment markers.
module xaui_lane_deskew_stage
import xaui_pkg::*;
#(
parameter int unsigned DEPTH = 6,
parameter int unsigned DW = 3,
parameter int unsigned CNT_W = 20
) (
input logic clk,
input logic rst_n,
input logic clear,
input logic [LANES-1:0] lane_valid,
input logic [LANES-1:0][7:0] lane_octet,
input logic [LANES-1:0] lane_is_k,
input logic [LANES-1:0] lane_a_seen, // /A/ on this lane
input logic [LANES-1:0] lane_synced, // comma alignment held
output logic deskew_done,
output logic [LANES-1:0][7:0] aligned_octet,
output logic [LANES-1:0] aligned_is_k,
output logic aligned_valid,
// The measured skew, per lane, in code groups. The number a board
// engineer needs and nothing else in the design produces.
output logic [LANES-1:0][DW-1:0] lane_depth,
output logic [DW-1:0] worst_depth,
// A lane arrived further behind than the buffer holds. REPORTED and
// deskew REFUSED -- aligning three lanes and guessing the fourth
// produces a stream that reassembles into plausible garbage.
output logic skew_exceeded,
output logic [1:0] skew_exceeded_lane,
output logic [CNT_W-1:0] c_deskews,
output logic ever_skew_exceeded,
// Deskew is only meaningful once every lane has code-group sync.
output logic waiting_for_sync
);
logic [DW-1:0] depth_q [LANES];
logic [LANES-1:0] seen_q;
logic hunting_q;
assign waiting_for_sync = !(&lane_synced);
integer i;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
deskew_done <= 1'b0; aligned_valid <= 1'b0; worst_depth <= '0;
skew_exceeded <= 1'b0; skew_exceeded_lane <= 2'd0;
c_deskews <= '0; ever_skew_exceeded <= 1'b0;
seen_q <= '0; hunting_q <= 1'b1;
for (i = 0; i < LANES; i = i + 1) begin
depth_q[i] <= '0;
lane_depth[i] <= '0;
end
end else if (clear || !(&lane_synced)) begin
// Losing code-group sync on ANY lane invalidates the deskew: the
// code-group boundary that /A/ was measured against is gone.
deskew_done <= 1'b0; hunting_q <= 1'b1; seen_q <= '0;
skew_exceeded <= 1'b0;
for (i = 0; i < LANES; i = i + 1) depth_q[i] <= '0;
// ever_skew_exceeded survives: a board is a board.
end else begin
aligned_valid <= 1'b0;
skew_exceeded <= 1'b0;
if (hunting_q) begin
for (i = 0; i < LANES; i = i + 1) begin
if (lane_valid[i]) begin
if (lane_a_seen[i] && !seen_q[i]) begin
// This lane's /A/. Its current depth IS how far behind
// the earliest lane it is.
seen_q[i] <= 1'b1;
lane_depth[i] <= depth_q[i];
if (depth_q[i] > worst_depth) worst_depth <= depth_q[i];
end else if (|seen_q) begin
if (depth_q[i] == DW'(DEPTH - 1)) begin
// OUT OF RANGE. Refuse; do not wrap. A wrapped buffer
// aligns on stale data and reports a plausible SMALL
// skew, which is the most convincing wrong answer this
// module can give.
skew_exceeded <= 1'b1;
skew_exceeded_lane <= 2'(i);
ever_skew_exceeded <= 1'b1;
hunting_q <= 1'b1;
seen_q <= '0;
end else begin
depth_q[i] <= depth_q[i] + 1'b1;
end
end
end
end
if (&seen_q) begin
// ALL FOUR. Three is not deskew -- an XGMII column needs all
// four bytes, so a missing lane means every column is wrong.
hunting_q <= 1'b0;
deskew_done <= 1'b1;
if (!(&c_deskews)) c_deskews <= c_deskews + 1'b1;
end
end else begin
aligned_octet <= lane_octet;
aligned_is_k <= lane_is_k;
aligned_valid <= &lane_valid;
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that losing code-group sync on any one lane invalidates the deskew for all four, and the ordering is not negotiable. Deskew is measured in code groups, and a code group only exists once comma alignment has found its boundary — so a lane that loses sync has lost the unit the measurement was expressed in. waiting_for_sync gates the whole stage, and a design that deskews first and syncs later is measuring an offset in a unit it does not yet have.
Deliberately simplified: one code group per lane per cycle and a restart on any excess. Production XGXS implementations hunt continuously and re-verify against a second /A/ column before declaring alignment.
Production implication: lane_depth is the measured skew and it is the module's most valuable output, exactly as in Chapter 9.5 §8. Every other signal says the link works; this one says by how much. A board with 1 code group of skew and a board with 5 both report deskew_done, both pass traffic, and the second is one connector, one temperature step or one layout revision from failing.
9. The Skew Budget, Computed
Turn the standard's tolerance into a FIFO depth, because the arithmetic is short and the conclusion is not obvious.
| Step | Working | Result |
|---|---|---|
| lane baud rate | — | 3.125 Gbaud |
| unit interval | 1 ÷ 3.125 GHz | 320 ps |
| skew budget | ~40 UI | 40 × 320 = 12.8 ns |
| one code group | 10 UI | 10 × 320 = 3.2 ns |
| budget in code groups | 12.8 ÷ 3.2 | 4 code groups |
| FIFO depth with margin | round up | 6 per lane |
Four code groups is a small number, and that is the point.
Compare it with Chapter 9.5's 100 Gigabit budget:
| XAUI | 100GBASE-R | |
|---|---|---|
| skew tolerated | 12.8 ns | 180 ns |
| in units of the lane's code/block | 4 code groups | 14 blocks |
| what the lanes traverse | traces on one board | fibres in a building |
| equivalent physical difference | ≈ 2.6 m of FR-4 | ≈ 37 m of fibre |
| lanes can be reordered | no | yes |
The fourth row explains the first. XAUI's lanes are four differential pairs going the same 20 to 50 cm across one board, so 12.8 ns is generous — it is about 2.6 m of FR-4 at roughly 150 ps per inch, on traces that are a few tens of centimetres long. 100 Gigabit's lanes may be separate fibres in separate bundles, so its budget has to be fourteen times larger.
And the smaller budget is what lets XAUI skip lane identification entirely. A lane cannot be more than four code groups late, so its identity is never in doubt — there is no possibility of confusing lane 2's /A/ with lane 2's next /A/. At 100 Gigabit, with 14 blocks of skew and 20 lanes, the markers must name themselves.
10. RTL 5 — Deleting the Column That Carries Nothing
// SYNTHESIZABLE.
//
// Clock compensation: absorb the frequency difference between the two
// ends by inserting and deleting /R/ columns.
//
// WHY IT IS NEEDED. The transmitting XGXS runs on ITS clock; the
// receiving one runs on ITS clock. Both are within tolerance and
// neither is exactly 156.25 MHz. Over time the receiver either
// accumulates columns it cannot forward or runs out of columns to
// forward -- Chapter 4.4's elastic buffer problem, at 312.5 M columns
// per second.
//
// THE MECHANISM, and it is the reason for this chapter's rejected
// property: the XGXS DELETES an /R/ column when its buffer fills and
// INSERTS one when it empties. So the column sequence leaving the far
// XGXS is NOT the sequence that entered the near one -- by design, on
// every conforming link, whenever the two clocks differ at all.
//
// AND THE GUARANTEE IT DOES MAKE is that FRAMES are unchanged. Not one
// byte of frame content, not one column between /S/ and /T/, is ever
// added or removed. Transparency is stated over frames; the stream is
// deliberately modified.
module xaui_clock_compensation
import xaui_pkg::*;
#(
parameter int unsigned FIFO_DEPTH = 32,
parameter int unsigned HIGH_MARK = 24,
parameter int unsigned LOW_MARK = 8,
parameter int unsigned CNT_W = 24
) (
input logic clk,
input logic rst_n,
input logic clear,
input logic col_valid,
input column_kind_e col_kind,
input logic col_is_deletable, // TRUE only for /R/
input logic in_frame,
input logic [5:0] fifo_level,
output logic forward_column,
output logic delete_column,
output logic insert_column,
output logic [CNT_W-1:0] c_deletions,
output logic [CNT_W-1:0] c_insertions,
// The buffer reached a limit with no deletable column available.
// This is the real failure: compensation had nowhere to act, so a
// column will be lost or duplicated somewhere it matters.
output logic compensation_starved,
output logic [CNT_W-1:0] c_starvations,
output logic ever_starved,
// Net columns added minus removed. A number that should hover near
// zero and drift slowly; a monotone trend means the clocks differ by
// more than the mechanism can absorb.
output logic signed [CNT_W-1:0] net_adjustment
);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
forward_column <= 1'b0; delete_column <= 1'b0; insert_column <= 1'b0;
c_deletions <= '0; c_insertions <= '0;
compensation_starved <= 1'b0; c_starvations <= '0;
ever_starved <= 1'b0; net_adjustment <= '0;
end else if (clear) begin
c_deletions <= '0; c_insertions <= '0; c_starvations <= '0;
net_adjustment <= '0;
// ever_starved survives.
end else begin
forward_column <= 1'b0;
delete_column <= 1'b0;
insert_column <= 1'b0;
compensation_starved <= 1'b0;
if (col_valid) begin
if (in_frame) begin
// NEVER touch a frame. Not one column between /S/ and /T/,
// whatever the buffer is doing. This is the guarantee that
// makes "transparent" true at the granularity it is stated.
forward_column <= 1'b1;
end else if (fifo_level > 6'(HIGH_MARK)) begin
if (col_is_deletable) begin
// DELETE an /R/. The stream is now shorter than it was,
// and nothing above notices, because /R/ carries nothing.
delete_column <= 1'b1;
net_adjustment <= net_adjustment - 1;
if (!(&c_deletions)) c_deletions <= c_deletions + 1'b1;
end else begin
// The buffer is filling and this column is /A/ or /K/,
// which must not be removed. Forward it and wait.
forward_column <= 1'b1;
if (fifo_level == 6'(FIFO_DEPTH - 1)) begin
compensation_starved <= 1'b1;
ever_starved <= 1'b1;
if (!(&c_starvations)) c_starvations <= c_starvations + 1'b1;
end
end
end else if (fifo_level < 6'(LOW_MARK)) begin
// INSERT an /R/. Also invisible above, for the same reason.
insert_column <= 1'b1;
forward_column <= 1'b1;
net_adjustment <= net_adjustment + 1;
if (!(&c_insertions)) c_insertions <= c_insertions + 1'b1;
end else begin
forward_column <= 1'b1;
end
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that the layer's transparency is stated over frames and deliberately not over the stream, and this module is where that distinction lives. Not one column between /S/ and /T/ is ever added or removed — the in_frame branch is unconditional, whatever the buffer is doing. Everything outside a frame is fair game, and /R/ in particular exists to be consumed.
Deliberately simplified: one deletable column kind and fixed high and low marks. Production compensators may delete or insert several columns at once when the idle sequence permits, and the marks are usually programmable to trade buffer depth against how often compensation acts.
Production implication: compensation_starved is the genuine failure this module can have, and it is not the same as the buffer filling. A full buffer with an /R/ available is handled; a full buffer arriving at an /A/ or /K/ column is compensation with nowhere to act — and the next thing to overflow will be a column that matters. A rising c_starvations on a link that still works means the idle sequence is not supplying enough /R/ columns for the clock difference, which is a design or configuration fact and not a fault in the link.
11. RTL 6 — Proving the Extender Is There and Working
// SYNTHESIZABLE.
//
// A loopback self-test for the XGXS pair, and its checker is built
// around what the extender ACTUALLY guarantees.
//
// THE GUARANTEE, stated precisely:
// FRAMES are preserved exactly. Every column between /S/ and /T/,
// in order, unmodified, none added and none removed.
// THE STREAM IS NOT. /R/ columns are inserted and deleted outside
// frames whenever the two ends' clocks differ -- which is always.
//
// So this checker compares FRAMES and counts IDLE ADJUSTMENTS. A test
// that compared the two streams bit for bit would fail on every
// conforming link, which is Section 14's rejected property in the form
// of a testbench rather than an assertion.
module xgxs_loopback_selftest
import xaui_pkg::*;
#(
parameter int unsigned CNT_W = 20,
parameter int unsigned PATTERN_FRAMES = 16'd1000
) (
input logic clk,
input logic rst_n,
input logic start,
// What was sent into the near XGXS.
input logic tx_col_valid,
input logic [31:0] tx_col_data,
input logic tx_in_frame,
// What emerged from the far XGXS.
input logic rx_col_valid,
input logic [31:0] rx_col_data,
input logic rx_in_frame,
output logic running,
output logic done,
output logic passed,
// FRAME comparison -- the real check.
output logic [CNT_W-1:0] c_frame_columns_sent,
output logic [CNT_W-1:0] c_frame_columns_received,
output logic [CNT_W-1:0] c_frame_column_mismatches,
// IDLE accounting -- expected to be non-zero, and reported rather
// than treated as an error.
output logic signed [CNT_W-1:0] idle_columns_net,
output logic idle_adjusted,
// The two verdicts, which are deliberately separate.
output logic frames_preserved,
output logic stream_modified
);
logic [15:0] frames_q;
logic [31:0] tx_fifo_q; // one-entry model of the frame path
logic tx_have_q;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
running <= 1'b0; done <= 1'b0; passed <= 1'b0;
frames_q <= '0; tx_fifo_q <= '0; tx_have_q <= 1'b0;
c_frame_columns_sent <= '0; c_frame_columns_received <= '0;
c_frame_column_mismatches <= '0;
idle_columns_net <= '0; idle_adjusted <= 1'b0;
frames_preserved <= 1'b0; stream_modified <= 1'b0;
end else if (start) begin
running <= 1'b1; done <= 1'b0; passed <= 1'b0;
frames_q <= '0; tx_have_q <= 1'b0;
c_frame_columns_sent <= '0; c_frame_columns_received <= '0;
c_frame_column_mismatches <= '0;
idle_columns_net <= '0; idle_adjusted <= 1'b0;
end else if (running) begin
idle_adjusted <= 1'b0;
// ---- FRAME columns: compared exactly ----
if (tx_col_valid && tx_in_frame) begin
tx_fifo_q <= tx_col_data;
tx_have_q <= 1'b1;
if (!(&c_frame_columns_sent))
c_frame_columns_sent <= c_frame_columns_sent + 1'b1;
end
if (rx_col_valid && rx_in_frame) begin
if (!(&c_frame_columns_received))
c_frame_columns_received <= c_frame_columns_received + 1'b1;
if (tx_have_q && (rx_col_data != tx_fifo_q)) begin
// A frame column that changed. THIS is a failure.
if (!(&c_frame_column_mismatches))
c_frame_column_mismatches <= c_frame_column_mismatches + 1'b1;
end
tx_have_q <= 1'b0;
end
// ---- IDLE columns: counted, never compared ----
if (tx_col_valid && !tx_in_frame) idle_columns_net <= idle_columns_net + 1;
if (rx_col_valid && !rx_in_frame) begin
idle_columns_net <= idle_columns_net - 1;
// A net difference means the compensator acted, which is the
// mechanism working. Recorded as an EVENT, not an error.
if (idle_columns_net != 0) idle_adjusted <= 1'b1;
end
if (rx_col_valid && rx_in_frame && !tx_in_frame) begin
frames_q <= frames_q + 16'd1;
end
if (frames_q == PATTERN_FRAMES) begin
running <= 1'b0;
done <= 1'b1;
// THE TWO VERDICTS, and they are independent.
// frames_preserved -- the guarantee the XGXS makes.
// stream_modified -- the mechanism it needed to make it.
// A healthy link has the first TRUE and the second ALSO TRUE.
frames_preserved <= (c_frame_column_mismatches == '0) &&
(c_frame_columns_sent == c_frame_columns_received);
stream_modified <= (idle_columns_net != 0);
passed <= (c_frame_column_mismatches == '0) &&
(c_frame_columns_sent == c_frame_columns_received);
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that frames_preserved and stream_modified are both expected to be true on a healthy link, and reporting them as two independent verdicts is the entire design. The XGXS guarantees the first and needs the second in order to deliver it — a compensator that never modified the stream would eventually overflow, and the overflow would land inside a frame.
Deliberately simplified: the frame comparison uses a one-entry model of the path. A real self-test compares against a much deeper reference or uses a known pattern with an embedded sequence number, because the XGXS's latency is many columns.
Production implication: passed is computed only from the frame comparison, and that is deliberate. A self-test whose pass criterion included stream equality would fail on every conforming link — which is not a hypothetical, because it is the natural way to write a loopback checker: send a sequence, capture a sequence, compare. The correct checker compares what the layer promised and counts what it changed, and the counting is diagnostic rather than a verdict.
12. RTL 7 — Conformance Across Four Lanes
// SYNTHESIZABLE.
//
// Checks the XAUI rules that only exist because there are four lanes.
//
// THE FOUR-LANE RULES, none of which has a single-lane analogue:
// 1. /A/ is transmitted on ALL FOUR lanes simultaneously. An /A/ on
// one lane and not the others is not an align column; it is a lane
// that has lost sync and is decoding rubbish that happens to look
// like K28.3.
// 2. Idle columns are the SAME character on all four lanes. A column
// with /K/ on two lanes and /R/ on two is not a legal column.
// 3. /S/ appears on lane 0 only (Chapter 9.4 §4), and after
// deskew that rule is checkable again.
// 4. Every lane must hold code-group sync. Three synced lanes is not
// 75% of a link -- an XGMII column needs all four bytes.
module xaui_conformance_monitor
import xaui_pkg::*;
#(
parameter int unsigned CNT_W = 20
) (
input logic clk,
input logic rst_n,
input logic clear,
input logic deskew_done,
input logic [LANES-1:0] lane_synced,
input logic [LANES-1:0][7:0] lane_octet,
input logic [LANES-1:0] lane_is_k,
input logic col_valid,
input logic in_frame,
output logic partial_align_column,
output logic mixed_idle_column,
output logic start_off_lane0,
output logic lane_sync_incomplete,
output logic [CNT_W-1:0] c_partial_align,
output logic [CNT_W-1:0] c_mixed_idle,
output logic [CNT_W-1:0] c_start_off_lane0,
output logic [CNT_W-1:0] c_sync_incomplete,
output logic [LANES-1:0] ever_lost_sync,
output logic first_violation_valid,
output logic [1:0] first_violation_kind,
output logic ever_violated
);
logic a_any_c, a_all_c;
logic idle_any_c, idle_same_c;
logic any_c;
logic [1:0] kind_c;
always_comb begin
a_any_c = 1'b0;
a_all_c = 1'b1;
for (int i = 0; i < LANES; i = i + 1) begin
if (lane_is_k[i] && (lane_octet[i] == K28_3)) a_any_c = 1'b1;
else a_all_c = 1'b0;
end
// A legal idle column carries the SAME control character on all
// four lanes. Mixed characters mean the lanes are not aligned to
// the same column, whatever the deskew stage believes.
idle_any_c = 1'b0;
idle_same_c = 1'b1;
for (int i = 0; i < LANES; i = i + 1) begin
if (lane_is_k[i]) idle_any_c = 1'b1;
if (lane_is_k[i] && (lane_octet[i] != lane_octet[0])) idle_same_c = 1'b0;
end
any_c = 1'b0;
kind_c = 2'd0;
if (partial_align_column) begin any_c = 1'b1; kind_c = 2'd0; end
else if (mixed_idle_column) begin any_c = 1'b1; kind_c = 2'd1; end
else if (start_off_lane0) begin any_c = 1'b1; kind_c = 2'd2; end
else if (lane_sync_incomplete) begin any_c = 1'b1; kind_c = 2'd3; end
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
partial_align_column <= 1'b0; mixed_idle_column <= 1'b0;
start_off_lane0 <= 1'b0; lane_sync_incomplete <= 1'b0;
c_partial_align <= '0; c_mixed_idle <= '0;
c_start_off_lane0 <= '0; c_sync_incomplete <= '0;
ever_lost_sync <= '0;
first_violation_valid <= 1'b0; first_violation_kind <= 2'd0;
ever_violated <= 1'b0;
end else if (clear) begin
c_partial_align <= '0; c_mixed_idle <= '0;
c_start_off_lane0 <= '0; c_sync_incomplete <= '0;
first_violation_valid <= 1'b0;
// ever_lost_sync and ever_violated survive.
end else begin
partial_align_column <= 1'b0;
mixed_idle_column <= 1'b0;
start_off_lane0 <= 1'b0;
lane_sync_incomplete <= 1'b0;
for (int i = 0; i < LANES; i = i + 1)
if (!lane_synced[i]) ever_lost_sync[i] <= 1'b1;
// Nothing is checked before deskew. Before it, every lane is
// presenting a code group from a different column, so every rule
// is violated simultaneously and none of it is information.
if (deskew_done && col_valid) begin
if (!(&lane_synced)) begin
lane_sync_incomplete <= 1'b1;
if (!(&c_sync_incomplete)) c_sync_incomplete <= c_sync_incomplete + 1'b1;
end
// /A/ ON SOME LANES BUT NOT ALL. Not a partial align column --
// there is no such thing. It is a lane decoding rubbish.
if (a_any_c && !a_all_c) begin
partial_align_column <= 1'b1;
if (!(&c_partial_align)) c_partial_align <= c_partial_align + 1'b1;
end
if (!in_frame && idle_any_c && !idle_same_c) begin
mixed_idle_column <= 1'b1;
if (!(&c_mixed_idle)) c_mixed_idle <= c_mixed_idle + 1'b1;
end
for (int i = 1; i < LANES; i = i + 1) begin
if (lane_is_k[i] && (lane_octet[i] == K27_7)) begin
start_off_lane0 <= 1'b1;
if (!(&c_start_off_lane0))
c_start_off_lane0 <= c_start_off_lane0 + 1'b1;
end
end
end
if (any_c) begin
ever_violated <= 1'b1;
if (!first_violation_valid) begin
first_violation_valid <= 1'b1;
first_violation_kind <= kind_c;
end
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that there is no such thing as a partial align column, and treating one as an alignment event is how a receiver locks onto rubbish. /A/ is transmitted on all four lanes simultaneously; /A/ on one lane and not the others means that lane has lost code-group sync and is decoding a bit-shifted stream that happens to produce K28.3. Acting on it deskews against a phantom reference.
Deliberately simplified: four rules and a two-bit violation kind. Production monitors also check the /A/ interval, the idle sequence's K/R alternation, and the column format for /S/ and /T/.
Production implication: every check is gated on deskew_done, for the same reason Chapter 10.5 §14's are gated on aligned. Before deskew, each lane is presenting a code group from a different column, so mixed idle columns, partial aligns and misplaced starts occur on essentially every column — all of it true, none of it useful, and it buries first_violation_kind under aftermath from a single root cause.
13. What the XGXS Hides, and What It Cannot
The extender pair's whole purpose is that neither end knows it is there. It succeeds at that, with three specific exceptions.
| Property | Preserved across the XGXS pair? |
|---|---|
| frame content, byte for byte | yes, exactly |
| column order within a frame | yes, exactly |
| the start character's lane | yes — lane 0 in, lane 0 out |
| the number of columns between frames | NO — /R/ inserted and deleted |
| the exact idle sequence | NO — same reason |
| latency | NO — the XGXS adds serialisation and deskew delay |
The first three are the guarantee and the last three are the price, and the interesting thing is that only one of them is usually thought about.
Latency, computed:
| Stage | Cost |
|---|---|
| 8B/10B encode | ≈ 1 code group = 3.2 ns |
| serialise | ≈ 1 code group |
| propagate, 50 cm of FR-4 | ≈ 3 ns |
| deserialise | ≈ 1 code group |
| comma alignment | one-time, at link-up |
| deskew buffer | up to DEPTH code groups = ≈ 19 ns |
| clock compensation buffer | up to half its depth |
| total, one direction | ≈ 40–60 ns |
Against Chapter 8.4's terms at 10 Gb/s:
| Term | Value |
|---|---|
| XGXS pair, one direction | ≈ 50 ns |
| serialising a 64-octet frame at 10 Gb/s | 64 × 8 ÷ 10 G = 51.2 ns |
| serialising a 1518-octet frame | 1214 ns |
| Chapter 9.6's KP4 FEC at 400 Gb/s | ≈ 100 ns |
The XGXS costs roughly the time it takes to serialise one minimum-length frame — which is far from negligible at 10 Gigabit and is the price of not routing 74 signals.
14. Properties Worth Asserting, and One Worth Refusing
The organising question here is one of granularity: at what level does this layer make its promise, and is the property being written at that level or at a finer one?
The XGMII-to-XAUI mapping
// P1. Byte lane n maps to serial lane n. Always, every column, with no
// striping -- because the receiver identifies lanes by their wires.
property p_lane_mapping_fixed;
@(posedge clk) disable iff (!rst_n)
lane_valid |-> (lane_octet[2] == $past(xgmii_d[23:16]));
endproperty
a_lane_mapping_fixed: assert property (p_lane_mapping_fixed);
// P2. An XGMII control bit becomes a K code group on its own lane.
// The four control wires disappear into the encoding.
property p_control_becomes_k;
@(posedge clk) disable iff (!rst_n)
lane_valid |-> (lane_is_k == $past(xgmii_c));
endproperty
a_control_becomes_k: assert property (p_control_becomes_k);
// P3. A start character off lane 0 is detected BEFORE serialisation,
// while it is still expressible. Afterwards it is four valid code
// groups nothing downstream can object to.
property p_start_off_lane0_detected;
@(posedge clk) disable iff (!rst_n)
(xgmii_valid && xgmii_c[2] && (xgmii_d[23:16] == K27_7))
|=> start_off_lane0;
endproperty
a_start_off_lane0_detected: assert property (p_start_off_lane0_detected);
// P4. Every column produces exactly one code group per lane. No
// column is dropped and none is duplicated in the adapter.
property p_one_code_per_lane_per_column;
@(posedge clk) disable iff (!rst_n)
xgmii_valid |=> lane_valid;
endproperty
a_one_code_per_column: assert property (p_one_code_per_lane_per_column);Per-lane encoding
// P5. Each lane's running disparity is updated from ITS OWN data.
// Sharing one across four produces valid code groups and a drifting
// DC level on three lanes.
property p_disparity_is_per_lane;
@(posedge clk) disable iff (!rst_n)
lane_valid |=> (lane_rd[1] == next_rd($past(lane_octet[1]),
$past(lane_is_k[1]),
$past(lane_rd[1])));
endproperty
a_disparity_per_lane: assert property (p_disparity_is_per_lane);
// P6. An implausibly long same-polarity run is reported as a hint.
property p_rd_stuck_reported;
@(posedge clk) disable iff (!rst_n)
(lane_rd_run[0] == 8'(RD_RUN_ALARM)) |=> lane_rd_stuck[0];
endproperty
a_rd_stuck_reported: assert property (p_rd_stuck_reported);The idle sequence
// P7. An /A/ column is the SAME character on all four lanes. There is
// no such thing as a partial align column.
property p_align_is_all_lanes;
@(posedge clk) disable iff (!rst_n)
(idle_valid && (idle_kind == COL_IDLE_A))
|-> ((idle_octet[0] == K28_3) && (idle_octet[1] == K28_3) &&
(idle_octet[2] == K28_3) && (idle_octet[3] == K28_3));
endproperty
a_align_all_lanes: assert property (p_align_is_all_lanes);
// P8. /A/ is NEVER marked deletable. It is a timing reference the far
// end measures skew against.
property p_align_not_deletable;
@(posedge clk) disable iff (!rst_n)
(idle_kind == COL_IDLE_A) |-> !is_deletable;
endproperty
a_align_not_deletable: assert property (p_align_not_deletable);
// P9. Nor is /K/ -- it carries the comma, and alignment recovery needs
// a supply of them.
property p_sync_not_deletable;
@(posedge clk) disable iff (!rst_n)
(idle_kind == COL_IDLE_K) |-> !is_deletable;
endproperty
a_sync_not_deletable: assert property (p_sync_not_deletable);
// P10. /R/ is the ONLY deletable column. It exists for that purpose.
property p_only_r_deletable;
@(posedge clk) disable iff (!rst_n)
is_deletable |-> (idle_kind == COL_IDLE_R);
endproperty
a_only_r_deletable: assert property (p_only_r_deletable);
// P11. The /A/ interval is bounded. The far end's deskew depends on
// them arriving regularly.
property p_align_interval_bounded;
@(posedge clk) disable iff (!rst_n)
idle_valid |-> (columns_since_a <= 5'(A_INTERVAL + 8));
endproperty
a_align_interval_bounded: assert property (p_align_interval_bounded);Deskew
// P12. Deskew requires code-group sync on EVERY lane first. Skew is
// measured in code groups, and a lane without sync has no code groups.
property p_deskew_needs_sync;
@(posedge clk) disable iff (!rst_n)
deskew_done |-> (&lane_synced);
endproperty
a_deskew_needs_sync: assert property (p_deskew_needs_sync);
// P13. And it requires all four /A/ characters. Three is not deskew --
// an XGMII column needs four bytes.
property p_deskew_needs_all_lanes;
@(posedge clk) disable iff (!rst_n)
$rose(deskew_done) |-> $past(&seen_q);
endproperty
a_deskew_needs_all_lanes: assert property (p_deskew_needs_all_lanes);
// P14. THE ALIASING PROPERTY. No recorded depth ever reaches DEPTH.
// A wrapped FIFO reports a PLAUSIBLE SMALL skew, which is the most
// convincing wrong answer this module can produce.
property p_depth_within_buffer;
@(posedge clk) disable iff (!rst_n)
deskew_done |-> (worst_depth < DW'(DEPTH));
endproperty
a_depth_within_buffer: assert property (p_depth_within_buffer);
// P15. An excess REFUSES deskew and names the lane.
property p_excess_refuses_and_names;
@(posedge clk) disable iff (!rst_n)
skew_exceeded |-> (!deskew_done && (skew_exceeded_lane < 2'(LANES)));
endproperty
a_excess_refuses: assert property (p_excess_refuses_and_names);
// P16. Losing sync on any lane invalidates the deskew.
property p_sync_loss_invalidates_deskew;
@(posedge clk) disable iff (!rst_n)
!(&lane_synced) |=> !deskew_done;
endproperty
a_sync_loss_invalidates: assert property (p_sync_loss_invalidates_deskew);Clock compensation — the granularity properties
// P17. THE GUARANTEE. No column inside a frame is ever deleted.
property p_never_delete_in_frame;
@(posedge clk) disable iff (!rst_n)
in_frame |-> !delete_column;
endproperty
a_never_delete_in_frame: assert property (p_never_delete_in_frame);
// P18. Nor inserted into one.
property p_never_insert_in_frame;
@(posedge clk) disable iff (!rst_n)
in_frame |-> !insert_column;
endproperty
a_never_insert_in_frame: assert property (p_never_insert_in_frame);
// P19. Only a deletable column is ever deleted.
property p_delete_only_deletable;
@(posedge clk) disable iff (!rst_n)
delete_column |-> col_is_deletable;
endproperty
a_delete_only_deletable: assert property (p_delete_only_deletable);
// P20. Delete and insert are mutually exclusive.
property p_adjust_exclusive;
@(posedge clk) disable iff (!rst_n)
!(delete_column && insert_column);
endproperty
a_adjust_exclusive: assert property (p_adjust_exclusive);
// P21. Starvation is REPORTED. The buffer reached a limit with no
// deletable column available, which is the real failure -- as distinct
// from the buffer being full, which is the mechanism working.
property p_starvation_reported;
@(posedge clk) disable iff (!rst_n)
(col_valid && !in_frame && (fifo_level == 6'(FIFO_DEPTH - 1)) &&
!col_is_deletable) |=> compensation_starved;
endproperty
a_starvation_reported: assert property (p_starvation_reported);
// P22. FRAMES are preserved across the pair -- the property the layer
// actually promises, at the granularity it promises it.
property p_frames_preserved;
@(posedge clk) disable iff (!rst_n)
done |-> (c_frame_column_mismatches == '0);
endproperty
a_frames_preserved: assert property (p_frames_preserved);15. Verification Scenarios
The adapter
- A data column — four bytes to four lanes,
lane_is_kall zero. - An idle
/K/column —K28.5on all four lanes,lane_is_kall one. - A start column —
/S/on lane 0, data on lanes 1 to 3,start_off_lane0low. - A start character on lane 2 —
start_off_lane0high, and the column is still serialised so downstream behaviour can be observed. - A terminate on each of the four lanes in turn — all legal, all mapped correctly.
xgmii_cset on lanes 0 and 2 only — the control bits map tolane_is_kper lane, not per column.- Column throughput — one output column per input column, no drops, no duplicates.
Per-lane encoding
- Four lanes carrying identical bytes — the four running disparities evolve identically, which is correct and looks like a shared variable.
- Four lanes carrying different bytes — the four disparities diverge. The scenario that distinguishes a per-lane implementation from a shared one.
- A shared-disparity mutation — P5 fires; three lanes' balance is corrected using a fourth lane's data.
- A lane fed a pattern that holds disparity at one polarity for 64 code groups —
lane_rd_stuck[n], the others clear.
The idle sequence
- A long idle period — the sequence
A K R K R …with an/A/roughly every 16 columns. /A/columns —is_deletablelow, always./K/columns —is_deletablelow./R/columns —is_deletablehigh, and they are the only ones.force_align— an/A/column is produced immediately regardless of the counter.- A stretched
/A/interval after several compensation events —a_interval_stretched; legal transiently, a fault if it persists.
Deskew
- Zero skew — all four
/A/characters in the same column; depths all zero; immediatedeskew_done. - Lane 1 late by 1 code group — depth 1,
deskew_done, no excess. - Lane 3 late by
DEPTH − 1— the boundary that must still align. - Lane 3 late by
DEPTH—skew_exceededwithskew_exceeded_lane = 3,deskew_donelow, and no wrap. - Three
/A/characters and the fourth never arriving —deskew_donenever asserts. Three lanes is not 75% of a link. - Lane 2 losing code-group sync after deskew —
deskew_donefalls; the measurement's unit is gone. /A/on lane 0 only —partial_align_column, and deskew is not attempted against it.- Re-deskew after a link bounce — depths re-measured;
ever_skew_exceededsurvives aclear.
Clock compensation
- Two clocks exactly equal —
c_deletionsandc_insertionsboth zero,net_adjustmentzero. - The transmitter 100 ppm fast — the buffer fills,
/R/columns are deleted,net_adjustmentdrifts negative, and no frame column is touched. - The transmitter 100 ppm slow —
/R/columns inserted,net_adjustmentpositive. - A high-water event arriving at an
/A/column — the column is forwarded, not deleted; the buffer keeps filling. - The buffer reaching its limit at an
/A/column —compensation_starved,ever_starvedsticky. The real failure. - A high-water event during a frame — nothing is deleted; the
in_framebranch is unconditional. P17. - A low-water event during a frame — nothing inserted. P18.
- An idle sequence with
/R/removed entirely (a deliberate mutation) —compensation_starvedwithin a few thousand columns under any clock offset.
Loopback and conformance
- A 1000-frame loopback with equal clocks —
frames_preservedtrue,stream_modifiedfalse. - The same with 100 ppm offset —
frames_preservedtrue,stream_modifiedtrue,passedtrue. Both verdicts, and the pass criterion uses only the first. - A single frame column corrupted —
c_frame_column_mismatchesnon-zero,passedfalse. - A mixed idle column —
/K/on two lanes,/R/on two —mixed_idle_column. - Checks before
deskew_done— no violations reported at all. The gate.
16. Debugging: Four Lanes, Four Places to Fail
| Observation | Likely cause | The distinguishing check |
|---|---|---|
deskew_done never asserts, one lane never synced | that lane's SerDes, trace or connector | ever_lost_sync[n] names it |
deskew_done low, skew_exceeded high | a lane routed too long | skew_exceeded_lane, then measure that trace |
| deskew done, small measured skew, 100% FCS failures | a wrapped deskew FIFO | worst_depth implausibly low; the aliasing signature |
partial_align_column rising | a lane decoding rubbish that resembles K28.3 | it has lost sync; check lane_synced |
mixed_idle_column rising | lanes aligned to different columns | deskew believed itself; re-check /A/ handling |
| frames corrupt, control stream legal | lanes swapped on the board | XAUI identifies lanes by wire; only a known pattern finds it |
| one lane's DC level drifting | shared running disparity | lane_rd_stuck[n] on three of four lanes |
compensation_starved rising | not enough /R/ in the idle for the clock offset | check the idle sequence and the clock tolerance |
| a frame column lost occasionally, no other errors | compensation acting inside a frame | P17 and P18; the in_frame gate |
| loopback passes bit-exact | the testbench has one clock | the compensator never ran; nothing is verified |
Four habits.
First, read lane_synced before anything else on a four-lane interface. Almost every downstream symptom — partial aligns, mixed idle columns, deskew failures — is a consequence of one lane having lost code-group sync, and reporting the consequences buries the cause.
Second, treat a small measured skew with a 100% error rate as aliasing. A wrapped FIFO reports a plausible number, because DEPTH + 1 code groups of delay looks like 1. The most convincing wrong answer this subsystem produces is a reassuring one, which is why P14 asserts that no depth ever reaches DEPTH.
Third, distinguish a full buffer from a starved compensator. A full buffer with an /R/ available is the mechanism working. A full buffer at an /A/ column is compensation with nowhere to act, and the next overflow lands somewhere that matters.
Fourth, distrust a bit-exact loopback result. If the two XGXS instances share a clock — which they do in almost every testbench — the compensator never ran, and the mechanism most likely to be wrong is the one never exercised.
17. Common Misconceptions
"XGMII is an interface you route between chips."
The wrong model: a connector-level interface like MII or RGMII.
What it costs: you cannot explain why XAUI exists, or why the standard defines two interfaces for one boundary.
The corrected model: XGMII is 74 signals — 32 + 4 + 1 in each direction — at 156.25 MHz DDR, and essentially nobody routes it between packages. It specifies where the reconciliation sublayer ends and the PCS begins, so two vendors' blocks can be described against one boundary; inside a chip, 74 wires on a die is nothing. XAUI exists for when that boundary crosses a package, and it carries the same 10 Gb/s on sixteen wires — a 4.6× reduction — with an extender sublayer at each end so that both sides still see XGMII.
"XAUI stripes data across four lanes."
The wrong model: a round-robin distribution like Chapter 9.5's.
What it costs: you expect lane identification in the data, and cannot explain why a swapped lane breaks a XAUI link and is absorbed at 100 Gigabit.
The corrected model: XGMII byte lane n goes to XAUI serial lane n, always — there is no striping decision at all. A lane is identified by which wire it arrives on, not by anything in the data, which is why /A/ answers when rather than which. 100 Gigabit's alignment markers name their lane and can therefore be reordered; XAUI's cannot, and that simplification is correct at 50 cm and wrong at 10 km.
"The XGXS is transparent, so what goes in comes out."
The wrong model: transparency means bit-exactness.
What it costs: Section 14's rejected property, and — worse — a "fix" that disables clock compensation and corrupts frames.
The corrected model: transparency is stated over frames. Not one column between /S/ and /T/ is ever added or removed. The stream is deliberately modified: /R/ columns are inserted and deleted to absorb the difference between two 156.25 MHz references that are never exactly equal. The stream modification is load-bearing — it is what makes the frame guarantee keepable — and a design that removes it overflows its buffer into a frame.
"Running disparity can be shared across the four lanes."
The wrong model: a column is the natural unit, so one disparity per column.
What it costs: three lanes whose DC balance is corrected using a fourth lane's data.
The corrected model: each XAUI lane is an independent, AC-coupled serial link with its own DC-balance requirement. Four encoders, four running disparities, updated from four different byte streams. A shared variable produces code groups that are individually valid on every lane and a DC level that drifts on three of them — which passes every code-group check and fails AC coupling after a few microseconds.
"Three of four lanes working is most of a link."
The wrong model: graceful degradation across parallel lanes.
What it costs: you misread a 100% error rate as a total failure rather than as one lane's fault.
The corrected model: an XGMII column is four bytes and recovering any column requires all four. One lane out of sync corrupts one byte of every column, so every frame fails its FCS and the error rate is 100% — indistinguishable, at the frame layer, from a completely dead link. The per-lane vectors — lane_synced, lane_depth, lane_rd_stuck, ever_lost_sync — are the only things that can attribute it, and they exist only below the combine.
18. Interview Reasoning
"Why does 10 Gigabit define two interfaces for one boundary?"
Because XGMII's width makes it unroutable and its width is not optional. 32 data + 4 control + 1 clock in each direction is 74 signals at 156.25 MHz DDR, and every one is single-ended and length-matched — Chapter 10.4's delay problem applied to thirty-six lines against one clock. XGMII specifies the functional boundary between the reconciliation sublayer and the PCS, which is exactly what is needed inside a chip where 74 wires cost nothing. XAUI is what crosses a package: four lanes per direction at 3.125 Gbaud with 8B/10B, 4 × 3.125 × 8/10 = 10 Gb/s, on sixteen wires — a 4.6× reduction. The finishing point: XAUI is not a different interface, it is XGMII with an extender sublayer on each end, so the RS and the PCS both still see XGMII and neither knows the serial link is there.
"What are XAUI's three idle characters for?"
Three different alignment problems, all solved inside the idle stream so all three are free. /K/ (K28.5) carries the comma, which is how each lane finds its code-group boundary — Chapter 10.5's problem, four times over. /A/ (K28.3) is transmitted simultaneously on all four lanes, so the difference in its arrival times is the inter-lane skew, measured rather than estimated; it appears roughly every 16 columns. /R/ (K28.0) is the skip column: it carries nothing at all and exists to be deleted and inserted for clock compensation. The strong answer says why /R/ and not the others: deleting an /A/ costs a deskew reference the far end is counting; deleting a /K/ starves comma supply for future re-alignment; /R/ carries no information, which is why more than half the idle sequence is /R/.
"How deep does a XAUI deskew buffer need to be?"
Compute it. One UI at 3.125 Gbaud is 1 ÷ 3.125 GHz = 320 ps; the receiver absorbs about 40 UI, so 40 × 320 = 12.8 ns; a code group is 10 UI = 3.2 ns; therefore 12.8 ÷ 3.2 = 4 code groups, and a 6-deep FIFO per lane covers it with margin. The strong answer contrasts it with Chapter 9.5's 180 ns at 100 Gigabit and explains the ratio: XAUI's lanes are four traces going the same 20 to 50 cm across one board, while 100 Gigabit's may be separate fibres in a building. And the finishing observation: the small budget is what lets XAUI skip lane identification entirely — a lane cannot be more than four code groups late, so its /A/ can never be confused with its next /A/, whereas 20 lanes with 14 blocks of skew need markers that name themselves.
"Would you assert that the XGXS pair is bit-exact?"
No — it is false on every conforming link, and the reason is granularity rather than content. The standard calls the extender transparent, and it is: frames cross unchanged, not one column between /S/ and /T/ added or removed. But the stream does not, because the two ends' 156.25 MHz references are never exactly equal and the XGXS absorbs the difference by inserting and deleting /R/ columns — Chapter 4.4's elastic buffering as a code group. So the property asserts a stronger guarantee than the layer owes, and it is false precisely because of the mechanism the layer needs in order to deliver the weaker one. The damage is specific: a team that writes it either concludes the extender is broken or disables compensation to satisfy it, at which point the buffer overflows into a frame and the guarantee that mattered is the one that breaks. Assert the guarantee at its own granularity: nothing inside a frame is ever added or deleted, only /R/ is ever deleted, insert and delete are exclusive, starvation is reported, and frames compare equal end to end. The test: at what granularity does this layer state its promise? A promise about frames says nothing about columns.
19. Understanding Check
Seventy-four against sixteen, and the gap is why there are two.
| XGMII | XAUI | |
|---|---|---|
| per direction | 32 + 4 + 1 = 37 | 4 lanes |
| both directions | 74 signals | 4 × 2 × 2 = 16 wires |
| clocking | 10 G ÷ 32 = 312.5 MT/s → 156.25 MHz DDR | 3.125 Gbaud per lane |
| encoding | none | 8B/10B |
| reduction | — | 4.6× |
XGMII specifies a functional boundary, not a connector — where the reconciliation sublayer ends and the PCS begins, so two vendors' blocks can be described against one interface. Inside a chip, 74 wires on a die is nothing.
XAUI exists for when the boundary crosses a package, and it is not a different interface: an extender sublayer at each end converts XGMII to XAUI and back, so both sides still see XGMII.
And XAUI's rate closes exactly: 4 lanes × 3.125 Gbaud × 8/10 = 10 Gb/s, with each lane carrying one XGMII byte lane at 312.5 M code groups/s.
20. What's Next
The claim this chapter defended: a layer described as transparent is transparent at exactly one granularity.
XGMII is 74 signals — 32 data, 4 control and a clock in each direction at 156.25 MHz DDR — which specifies a boundary well and crosses a package badly. XAUI carries the same 10 Gb/s on sixteen wires, four lanes per direction at 3.125 Gbaud with 8B/10B, and an extender sublayer at each end makes both sides believe they are still talking XGMII.
What four independent serial lanes cost is everything Chapter 10.5 built, four times over, plus a problem SGMII does not have. Per-lane comma alignment, per-lane running disparity — sharing one across four is a DC-balance failure that passes every code-group check — and deskew, because four lanes traverse four paths and arrive up to 40 × 320 ps = 12.8 ns apart, which is 4 code groups and therefore a 6-deep FIFO.
XAUI solves all three inside its idle sequence. /K/ carries the comma; /A/ is transmitted simultaneously on all four lanes and measures the skew directly; /R/ carries nothing and exists to be deleted — which is why more than half the idle is /R/.
And that last mechanism is the chapter's rejected property. The XGXS inserts and deletes /R/ columns to absorb the two ends' clock difference, so the stream that emerges is not the stream that entered — by design, on every conforming link, always. Its transparency is a guarantee about frames: not one column between /S/ and /T/ is ever touched. Asserting bit-exactness of the stream is a property one granularity too fine, false because of the mechanism that makes the real guarantee keepable — and a design "fixed" to satisfy it overflows into a frame.
Module 10 is complete. Six interfaces, one boundary, and a single question asked six ways: where does the clock come from, and what varies when the link's speed does? MII gave both clocks to the PHY and got a clock that stops. RMII gave the clock to nobody and got a clock nobody drives. GMII split them and got a timing relationship RTL cannot see. RGMII folded the bus and got a delay with no owner. SGMII serialised and got a fixed rate with no per-speed timing at all. XAUI serialised four times over and got deskew.
Chapter 11.1 — Link Discovery: FLP Bursts and Parallel Detection opens the module that owns the other end of the problem.
Every interface in Module 10 assumed a link that already existed at a known speed. Module 11 asks how two devices that have never met agree on one — and its first chapter is about what can be observed before any configuration is agreed at all. Chapter 9.2 §5 gave the FLP burst's structure; 11.1 goes into its timing tolerances, the link-integrity-pulse fallback that keeps a 1990 device working, the parallel-detection window and what it can and cannot determine, and the full ordering from a signal is present to abilities are available.
The full path is on the Ethernet curriculum index.
Continue learning
Related tutorials
- Related topic
10 Gigabit — 64B/66B, XGMII and the End of Shared Media
64B/66B cut line-code overhead from 25% to 3.125% by replacing a run-length guarantee with a probability, XGMII went 32 bits wide with per-lane control, and half duplex was removed from the standard outright.
- 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.
