Skip to content
VLSI Mentor

CXL · Module 5

CXL Link Training

What the link settles on after negotiation: the 2.5 GT/s start, the 8.0 GT/s floor, native versus degraded widths and rates, and why recovery must be bounded and must degrade rather than retry. Six RTL models simulated, thirteen mutations, thirteen killed.

Chapter 5.3 answered what protocol the link will speak. It left the link at the Gen 1 rate, having just decided.

This chapter answers the other half: what operating point does the link actually settle on, and what happens when it cannot hold it?

A bring-up report says the link trained. That sentence is compatible with all of the following:

  • x16 at 64 GT/s, which is what the design was sized for.
  • x16 at 8 GT/s, which is a quarter of the bandwidth and completely legal.
  • x2 at 64 GT/s, because fourteen lanes did not train.
  • x16 at 64 GT/s after eleven recovery cycles, and it will drop again under load.
  • Nothing at all, because the link has been retrying at the same operating point for four hours and nobody set a bound.

All five are "trained". Only one is the result anyone wanted, and none of the other four report an error — which is why the operating point, not the training status, is the number that matters.

2. The One-Sentence Model

Training resolves to an operating point — a width and a rate together — that must clear a floor but need not reach the native set; and when the link cannot hold that point, the correct response is to narrow it and try again, never to retry the same point forever.

Call it narrow, then conclude. A recovery policy that can neither narrow nor conclude is the failure this chapter is built around.

3. What This Chapter Owns

QuestionOwned by
Why CXL rides on PCIe at all5.1
Native versus degraded, as a reuse class5.2
How the two ends agree on the protocol5.3
The operating point, and recoverythis chapter
How software learns what was agreed5.5

Chapter 5.2 introduced the native/degraded partition as an example of a revision-dependent reuse class. This chapter uses it as the operating-point space and adds what 5.2 did not need: the floor, recovery, and the cost of each axis.

4. Three Rates, Three Different Jobs

The single most common confusion in this area is treating "the rate" as one number. There are three, and they answer different questions.

RateValueQuestion it answers
Start rate2.5 GT/s (Gen 1)at what rate is the capability conversation held?
Floor8.0 GT/swhat must the link clear to be a CXL link at all?
Native set32.0 / 64.0 GT/swhat was the design sized for?

The start rate is below the floor, and that is not a contradiction. Chapter 5.3 explains why: the link must come up before capability is known, so the conversation happens at the slowest and most robust rate every PCIe device can do. The rate a link trains at and the rate it operates at are different questions, and a design that conflates them will either reject its own bring-up or accept an operating point it cannot use.

5. Teaching-model boundary

6. RTL 1 — Training as a State Machine

Architectural teaching FSM for CXL link training: down waits for start, training reaches a base link, CXL setup is entered only when negotiation agreed CXL, then the link is operational; an error enters a bounded recovery state which either retrains or, once the bound is exhausted, failsDOWNTRAINBASESETUPOPRECOVFAILEDstartstarttrain_oktrain_okCXL agreedCXL agreedPCIe onlyPCIe onlysetup donesetup donelink errorlink errorrecovered / dwellrecovered / dwellbound exhaustedbound exhausted

Three structural properties carry the chapter.

SETUP is on top of BASE, not beside it. There is no edge into SETUP that does not come from BASE, because CXL setup requires a working base link. Mutation M1 removes the SETUP step entirely; the testbench catches it by checking the route, not the destination.

BASE requires train_ok. Mutation M2 makes the transition unconditional, and the link reaches a "base" state that has not trained.

RECOV has two exits and a bound. One back to training, one to FAILED. A recovery state with only the first exit is Chapter 5.3's unbounded wait in a new costume.

training_fsm.sv — architectural teaching FSM
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module training_fsm #(
  parameter int unsigned MAX_RECOVERY  = 3,
  parameter int unsigned REC_DWELL     = 4,     // bound on ONE recovery attempt
  parameter bit          RETRY_FOREVER = 1'b0   // 1 = the unbounded shape
) (
  input  logic       clk, rst_n, start,
  input  logic       train_ok,       // base training reached a usable state
  input  logic       cxl_agreed,     // negotiation resolved to CXL
  input  logic       link_error, recovered,
  output logic [2:0] state_q,
  output logic       operational, failed, in_recovery,
  output logic [7:0] n_recovery_q, rec_dwell_q,
  output logic       cxl_before_base_err, op_without_training_err,
  output logic       unbounded_recovery_err
);
  localparam logic [2:0] S_DOWN = 3'd0, S_TRAIN = 3'd1, S_BASE = 3'd2,
                         S_SETUP = 3'd3, S_OP = 3'd4, S_REC = 3'd5, S_FAIL = 3'd6;
  logic [2:0] nxt;
  logic       may_retry, dwell_done;
 
  assign may_retry   = RETRY_FOREVER || (n_recovery_q < MAX_RECOVERY[7:0]);
  // An ATTEMPT COUNTER IS NOT A BOUND if the attempt itself can hang. Counting
  // entries into recovery bounds nothing while the machine sits in recovery,
  // so one attempt needs its own dwell bound before the attempt count can
  // advance at all.
  assign dwell_done  = (rec_dwell_q >= REC_DWELL[7:0]);
  assign operational = (state_q == S_OP);
  assign failed      = (state_q == S_FAIL);
  assign in_recovery = (state_q == S_REC);
 
  always_comb begin
    nxt = state_q;
    case (state_q)
      S_DOWN  : if (start)      nxt = S_TRAIN;
      // Base training is PCIe's. Nothing CXL happens here.
      S_TRAIN : if (train_ok)   nxt = S_BASE;
      // The base link exists. CXL setup rides on top of it, never beside it.
      S_BASE  : nxt = cxl_agreed ? S_SETUP : S_OP;
      S_SETUP : nxt = S_OP;
      S_OP    : if (link_error) nxt = S_REC;
      // Recovery re-enters training. It is bounded, and exhausting the bound
      // is a FAILURE, not a silent forever-retry.
      S_REC   : if (recovered)       nxt = S_TRAIN;
                else if (dwell_done) nxt = may_retry ? S_TRAIN : S_FAIL;
      S_FAIL  : ;
      default : nxt = S_FAIL;
    endcase
  end
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      state_q <= S_DOWN; n_recovery_q <= 8'd0; rec_dwell_q <= 8'd0;
      cxl_before_base_err <= 1'b0; op_without_training_err <= 1'b0;
      unbounded_recovery_err <= 1'b0;
    end else begin
      state_q     <= nxt;
      rec_dwell_q <= (nxt == S_REC) ? (rec_dwell_q + 8'd1) : 8'd0;
      if ((state_q != S_REC) && (nxt == S_REC)) n_recovery_q <= n_recovery_q + 8'd1;
 
      if ((nxt == S_SETUP) && (state_q != S_BASE)) cxl_before_base_err <= 1'b1;
      if ((nxt == S_OP) &&
          !((state_q == S_BASE) || (state_q == S_SETUP) || (state_q == S_OP)))
        op_without_training_err <= 1'b1;
      // Stated about the DWELL, which is what actually advances. A check
      // written against n_recovery_q cannot fail while the machine is stuck,
      // because the thing it counts is exactly what has stopped happening.
      if (in_recovery && (rec_dwell_q > REC_DWELL[7:0] + 8'd2))
        unbounded_recovery_err <= 1'b1;
    end
  end
endmodule

7. Waveform — Success, and Recovery

A clean bring-up: train, set CXL up on top, operate

11 cycles
The machine waits in DOWN until start, spends four cycles in TRAIN until train_ok is asserted at cycle six, then moves through BASE and SETUP before reaching the operational state at cycle ninePCIe trainingPCIe trainingbase linkbase linkCXL setupCXL setupoperationaloperationaltrain_ok — base training succeededtrain_ok — base trainingsucceededCXL set up ON TOP of the base linkCXL set up ON TOP of thebase linkclkstarttrain_okcxl_agreedstateDOWNDOWNTRAINTRAINTRAINTRAINTRAINBASESETUPOPOPoperationalt0t1t2t3t4t5t6t7t8t9t10
Icarus Verilog 13.0, EXP1. train_ok is deliberately held low for four cycles — see §14.

Recovery: an error re-enters training and returns to operational

12 cycles
An error at cycle one drops the link from operational into recovery where the recovery count increments to one, the recovered signal at cycle three sends it back through training, base and setup, and it is operational again by cycle sevenoperatingoperatingrecoveryrecoveryre-trainingre-trainingoperating againoperating againrecovery entered — attempt count advances oncerecovery entered — attemptcount advances oncerecovery RE-ENTERS training, it does not resumerecovery RE-ENTERStraining, it does notresumeclklink_errorrecoveredstateOPOPRECOVRECOVTRAINBASESETUPOPOPOPOPOPoperationaln_recovery001111111111t0t1t2t3t4t5t6t7t8t9t10t11
Icarus Verilog 13.0, EXP2. Note that recovery costs the full training sequence, not a resume.

Two things in the second trace are worth naming.

Recovery re-enters training; it does not resume. The link goes back through TRAIN, BASE and SETUP — five cycles here, and in real hardware a full retrain. That is why a link that recovers frequently can be operational almost never, which §13 measures.

n_recovery advances exactly once per entry. That is correct, and it is also precisely the property that made the first version of the bound useless.

8. RTL 2 — The Start Rate and the Floor

rate_ladder.sv — two different rate questions
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module rate_ladder #(
  parameter bit ALLOW_BELOW_FLOOR = 1'b0   // 1 = the broken shape
) (
  input  logic       clk, rst_n,
  input  logic       negotiated,     // alternate protocol negotiation done
  input  logic [2:0] rate_code,      // 0:2.5 1:5 2:8 3:16 4:32 5:64
  input  logic       rate_valid,
  output logic       rate_legal, is_native_rate,
  output logic       below_floor_err, start_rate_err
);
  localparam logic [2:0] START_RATE = 3'd0;   // 2.5 GT/s
  localparam logic [2:0] FLOOR      = 3'd2;   // 8.0 GT/s
 
  assign is_native_rate = rate_valid && (rate_code >= 3'd4);
  // Before negotiation the link is at the start rate. After it, the rate must
  // be at or above the floor.
  assign rate_legal     = rate_valid &&
                          (negotiated ? (ALLOW_BELOW_FLOOR ? 1'b1
                                                           : (rate_code >= FLOOR))
                                      : (rate_code == START_RATE));
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      below_floor_err <= 1'b0; start_rate_err <= 1'b0;
    end else if (rate_valid) begin
      if (negotiated && rate_legal && (rate_code < FLOOR)) below_floor_err <= 1'b1;
      if (!negotiated && (rate_code != START_RATE))        start_rate_err  <= 1'b1;
    end
  end
endmodule
Icarus Verilog 13.0 — EXP4
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  pre-negotiation  2.5 GT/s : legal=1
  post-negotiation rate[0]  : legal=0 native=0 | allow-below-floor legal=1
  post-negotiation rate[1]  : legal=0 native=0 | allow-below-floor legal=1
  post-negotiation rate[2]  : legal=1 native=0 | allow-below-floor legal=1
  post-negotiation rate[3]  : legal=1 native=0 | allow-below-floor legal=1
  post-negotiation rate[4]  : legal=1 native=1 | allow-below-floor legal=1
  post-negotiation rate[5]  : legal=1 native=1 | allow-below-floor legal=1

Rate 0 is legal before negotiation and illegal after it. The same value, the same signal, opposite verdicts — because rate_legal is not a property of the rate, it is a property of the rate at a point in bring-up. A design that evaluates it context-free will either reject its own start rate or accept an operating point below the floor, and both mistakes are one missing term.

Note also the four legal post-negotiation rates split 2–2 between degraded and native. Clearing the floor is not the same as being fast, and a report that says "the link is legal" has said almost nothing about performance.

width_negotiate.sv — degradation is an outcome, not a failure
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module width_negotiate #(
  parameter bit DEGRADED_IS_FAILURE = 1'b0   // 1 = the broken shape
) (
  input  logic       clk, rst_n,
  input  logic [4:0] lanes_requested, lanes_trained,
  input  logic       valid,
  output logic [4:0] width_out,
  output logic       width_legal, is_native_width, degraded, train_ok,
  output logic       overclaim_err, degraded_rejected_err
);
  // You cannot operate wider than the lanes that actually trained.
  assign width_out       = (lanes_trained < lanes_requested) ? lanes_trained
                                                             : lanes_requested;
  assign is_native_width = valid && (width_out >= 5'd4);   // x4, x8, x16
  assign degraded        = valid && (width_out >= 5'd1) && (width_out < 5'd4);
  assign width_legal     = valid && (width_out >= 5'd1);
  // The broken shape refuses to come up narrow at all.
  assign train_ok        = width_legal &&
                           (DEGRADED_IS_FAILURE ? is_native_width : 1'b1);
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      overclaim_err <= 1'b0; degraded_rejected_err <= 1'b0;
    end else if (valid) begin
      if (width_out > lanes_trained) overclaim_err <= 1'b1;
      // Diagnostic on the broken variant: a usable narrow link was refused.
      if (DEGRADED_IS_FAILURE && degraded && !train_ok) degraded_rejected_err <= 1'b1;
    end
  end
endmodule
Icarus Verilog 13.0 — EXP5
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  request x16 trained 16 : width=16 native=1 degraded=0 train_ok=1
  request x16 trained  8 : width=8 native=1 degraded=0 train_ok=1
  request x16 trained  2 : width=2 native=0 degraded=1 train_ok=1 | reject-degraded train_ok=0
  request  x4 trained 16 : width=4  <-- never wider than requested

Row 2 is the one people misread: x8 from a requested x16 is still a native width. Half the bandwidth, and not degraded mode — because native/degraded classifies the width itself, not the shortfall against what was asked for. Row 3 is degraded, and it is still train_ok on the correct design.

The min in width_out is bidirectional on purpose. It clamps down when fewer lanes trained than were requested, and clamps up — that is, refuses to widen — when more trained than were requested. Mutation M7 replaces it with lanes_requested and the design claims lanes that never trained.

10. RTL 4 — Recovery Must Narrow, Then Conclude

Public material does not specify a recovery policy, so this module models the invariants any such policy must satisfy rather than a specified behaviour: bounded attempts at each operating point, degradation when the bound is hit, and a conclusion when there is nothing left to narrow.

recovery_policy.sv — the shape of a policy that terminates
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module recovery_policy #(
  parameter int unsigned MAX_AT_POINT  = 2,
  parameter bit          NEVER_DEGRADE = 1'b0   // 1 = the broken shape
) (
  input  logic       clk, rst_n, error_seen, retrain_ok,
  input  logic [2:0] rate_in,
  output logic [2:0] rate_out,
  output logic [7:0] tries_at_point_q, n_degrade_q,
  output logic       give_up, stuck_err, degraded_past_floor_err
);
  localparam logic [2:0] FLOOR = 3'd2;   // 8.0 GT/s
  logic [2:0] rate_q;
  logic       exhausted;
 
  assign exhausted = (tries_at_point_q >= MAX_AT_POINT[7:0]);
  assign rate_out  = rate_q;
  // Give up only when the operating point cannot be narrowed any further.
  // NEVER_DEGRADE models the real bug shape: retry at the SAME point forever,
  // neither narrowing nor concluding.
  assign give_up   = exhausted && !NEVER_DEGRADE && (rate_q <= FLOOR);
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      rate_q <= 3'd5; tries_at_point_q <= 8'd0; n_degrade_q <= 8'd0;
      stuck_err <= 1'b0; degraded_past_floor_err <= 1'b0;
    end else begin
      if (retrain_ok) begin
        tries_at_point_q <= 8'd0;
      end else if (error_seen) begin
        if (exhausted && !NEVER_DEGRADE && (rate_q > FLOOR)) begin
          // Narrow the operating point and reset the attempt count.
          rate_q           <= rate_q - 3'd1;
          n_degrade_q      <= n_degrade_q + 8'd1;
          tries_at_point_q <= 8'd0;
        end else begin
          tries_at_point_q <= tries_at_point_q + 8'd1;
        end
      end
 
      if ((tries_at_point_q > MAX_AT_POINT[7:0] + 8'd1) && !give_up) stuck_err <= 1'b1;
      if (rate_q < FLOOR) degraded_past_floor_err <= 1'b1;
    end
  end
endmodule
Icarus Verilog 13.0 — EXP6
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  step  error  rate_out  tries  degrades  give_up | never-degrade rate  give_up
  2      1      4        0      1         0       | 5              0
  5      1      3        0      2         0       | 5              0
  8      1      2        0      3         0       | 5              0
  11     1      2        3      3         1       | 5              0
  14     1      2        6      3         1       | 5              0
  correct policy : rate reached 2 after 3 degrades, give_up=1
  never-degrade  : rate still 5, tries=16, give_up=0

The correct policy walks the rate down 5 → 4 → 3 → 2, stops at the floor, and concludes. The broken one is still at rate 5 after sixteen attempts with no conclusion — not slower, non-terminating.

Note degraded_past_floor_err. Degradation has a limit: a CXL link that cannot hold 8.0 GT/s is not a slower CXL link, it is not a CXL link. Mutation M9 removes the floor check from the degrade path and the policy walks below it.

11. RTL 5 — The Operating Point Is the Pair

operating_point.sv — width and rate are one decision
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module operating_point (
  input  logic        clk, rst_n,
  input  logic [4:0]  width,
  input  logic [2:0]  rate_code,
  input  logic        valid,
  output logic [15:0] bw_proxy,
  output logic        point_native, point_legal,
  output logic        illegal_point_used_err, bw_mismatch_err
);
  logic [15:0] rate_units;
  logic        w_ok, r_ok, w_native, r_native;
 
  // Teaching proxy: 2.5/5/8/16/32/64 -> 1/2/3/6/13/26 units.
  always_comb begin
    case (rate_code)
      3'd0: rate_units = 16'd1;  3'd1: rate_units = 16'd2;
      3'd2: rate_units = 16'd3;  3'd3: rate_units = 16'd6;
      3'd4: rate_units = 16'd13; 3'd5: rate_units = 16'd26;
      default: rate_units = 16'd0;
    endcase
  end
 
  assign w_ok     = (width >= 5'd1) && (width <= 5'd16);
  assign r_ok     = (rate_code >= 3'd2);           // at or above the 8 GT/s floor
  assign w_native = (width >= 5'd4);
  assign r_native = (rate_code >= 3'd4);
 
  assign point_legal  = valid && w_ok && r_ok;
  // Native requires BOTH axes native -- the same AND as Chapter 5.2.
  assign point_native = point_legal && w_native && r_native;
  assign bw_proxy     = point_legal ? ({11'd0, width} * rate_units) : 16'd0;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      illegal_point_used_err <= 1'b0; bw_mismatch_err <= 1'b0;
    end else if (valid) begin
      if (!point_legal && (bw_proxy != 16'd0)) illegal_point_used_err <= 1'b1;
      // Independent restatement: bandwidth is the product, always.
      if (point_legal && (bw_proxy != ({11'd0, width} * rate_units)))
        bw_mismatch_err <= 1'b1;
    end
  end
endmodule
Icarus Verilog 13.0 — EXP7
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  over 4 widths x 4 rates: native=6 legal-but-degraded=10 illegal=0
  x16 @ rate5 : bw=416  <-- the reference point
  x8  @ rate5 : bw=208  (half the width)
  x16 @ rate4 : bw=208  (one rate step down)

Two different degradations, identical bandwidth. Half the width and one rate step down both land on 208 — which means "the link degraded" tells you nothing about how much was lost until you know which axis, and the two have very different causes. A narrow link points at lanes: a broken trace, a bad connector pin, a marginal via. A slow link points at the channel as a whole: insertion loss, crosstalk, equalisation.

Note also native = 6 of 16. This is the same AND-on-both-axes as Chapter 5.2, and mutation M11 replaces it with OR — pushing the native count from 6 to 14, which is exactly the over-claim a datasheet reader makes.

12. Where the Bandwidth Actually Goes

Using the §11 proxy, with x16 at 64 GT/s as the reference:

Pointof referenceclass
x16 @ 64100%native
x16 @ 3250%native
x8 @ 6450%native
x4 @ 6425%native
x16 @ 1623%degraded
x16 @ 812%degraded
x1 @ 646%degraded

The last row is the one to sit with. x1 at 64 GT/s clears the rate floor, is a perfectly legal operating point, and delivers about 6% of the reference. Nothing in "the link trained and is legal" distinguishes it from the first row, which is §1's entire complaint restated as a number.

13. RTL 6 — Where Training Time Goes

training_residency.sv — with a conservation law
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module training_residency (
  input  logic        clk, rst_n, tick,
  input  logic        in_train, in_setup, in_op, in_rec,
  output logic [15:0] t_total_q, t_train_q, t_setup_q, t_op_q, t_rec_q,
  output logic        accounting_err, multi_state_err
);
  logic [2:0] hot;
  assign hot = 3'd0 + in_train + in_setup + in_op + in_rec;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      t_total_q <= '0; t_train_q <= '0; t_setup_q <= '0; t_op_q <= '0; t_rec_q <= '0;
      accounting_err <= 1'b0; multi_state_err <= 1'b0;
    end else begin
      if (tick) begin
        t_total_q <= t_total_q + 16'd1;
        if (in_train) t_train_q <= t_train_q + 16'd1;
        if (in_setup) t_setup_q <= t_setup_q + 16'd1;
        if (in_op)    t_op_q    <= t_op_q    + 16'd1;
        if (in_rec)   t_rec_q   <= t_rec_q   + 16'd1;
      end
      if (t_total_q != t_train_q + t_setup_q + t_op_q + t_rec_q) accounting_err <= 1'b1;
      if (hot > 3'd1) multi_state_err <= 1'b1;
    end
  end
endmodule
Icarus Verilog 13.0 — EXP8, 100 ticks
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  total=100 train=30 setup=8 op=50 recovery=12
  conservation total == train+setup+op+recovery : 100 == 100
  fraction of link life actually operational: 50%

Half the link's life was not operational, and no error was reported at any point — every recovery succeeded. This is Chapter 5.1's silent-downgrade problem in the time domain: a link that recovers successfully and often is a link that is mostly retraining, and "recovery succeeded" is the wrong thing to count.

The conservation law is what makes the number trustworthy. Mutation M13 drops recovery time from the sum, which is exactly the mistake that would produce a report claiming 100% accounted-for while 12% of the time is unattributed.

14. Assertions

Concurrent SVA execution: NOT SUPPORTED BY Icarus Verilog. Not executed; each maps to a procedural stand-in and a mutation.

cxl_link_training_sva.sv — bind-ready properties
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SAFETY -------------------------------------------------------------------
// V1 — CXL setup is entered only from the base link state.
a_setup_on_base: assert property (@(posedge clk) disable iff (!rst_n)
  (state_q == S_SETUP) |-> ($past(state_q) == S_BASE));
 
// V2 — the base state is never entered before training succeeded.
a_base_needs_trainok: assert property (@(posedge clk) disable iff (!rst_n)
  (state_q == S_BASE) |-> $past(train_ok));
 
// V3 — operational is reached only from base, setup, or itself.
a_op_route: assert property (@(posedge clk) disable iff (!rst_n)
  operational |-> $past(state_q) inside {S_BASE, S_SETUP, S_OP});
 
// V4 — after negotiation the rate is at or above the floor.
a_rate_floor: assert property (@(posedge clk) disable iff (!rst_n)
  (rate_valid && negotiated && rate_legal) |-> (rate_code >= FLOOR));
 
// V5 — before negotiation the link is at the start rate.
a_start_rate: assert property (@(posedge clk) disable iff (!rst_n)
  (rate_valid && !negotiated) |-> (rate_code == START_RATE));
 
// V6 — the operating width never exceeds the lanes that trained.
a_width_bounded: assert property (@(posedge clk) disable iff (!rst_n)
  valid |-> (width_out <= lanes_trained));
 
// V7 — a point is native only if BOTH axes are native.
a_native_both: assert property (@(posedge clk) disable iff (!rst_n)
  point_native |-> (w_native && r_native));
 
// V8 — the rate never degrades below the floor.
a_never_below_floor: assert property (@(posedge clk) disable iff (!rst_n)
  rate_q >= FLOOR);
 
// V9 — CONSERVATION: every tick is in exactly one training state.
a_residency_conserved: assert property (@(posedge clk) disable iff (!rst_n)
  t_total_q == t_train_q + t_setup_q + t_op_q + t_rec_q);
 
// V10 — one recovery attempt is bounded by its dwell.
a_dwell_bounded: assert property (@(posedge clk) disable iff (!rst_n)
  in_recovery |-> (rec_dwell_q <= REC_DWELL + 1));
 
// LIVENESS ------------------------------------------------------------------
// V11 — recovery always terminates: retrain or fail, never neither.
//       Provable BECAUSE of V10. Without the dwell bound this needs an
//       environment assumption that the link eventually recovers, which is
//       exactly the assumption a failing link violates.
a_recovery_terminates: assert property (@(posedge clk) disable iff (!rst_n)
  in_recovery |-> s_eventually (operational || failed));

V10 and V11 repeat Chapter 5.3's pattern: the liveness property you want, discharged by a bound you can prove. What is new here is that the bound has to be on the inner attempt, not the outer count — V11 would not follow from a bound on n_recovery_q alone, for the reason §6's callout gives.

15. Mutation Testing

Thirteen mutations. Clean code restored after each.

IDMutationResult
M1CXL setup skipped even when agreedKILLED — route check
M2base reached without training succeedingKILLED — route check
M3recovery retries regardless of the boundKILLED — did not give up
M4the recovery dwell never advancesKILLED — did not give up
M5no rate floor after negotiationKILLED — rate table
M616 GT/s reclassified as nativeKILLED — rate table
M7operate at the requested width regardlessKILLED — overclaim_err
M8x2 reclassified as a native widthKILLED — positive test
M9degrade below the rate floorKILLED — degraded_past_floor_err
M10the attempt count never advancesKILLED — never degraded
M11one native axis is enoughKILLED — point classification
M12bandwidth is the sum, not the productKILLED — bw_mismatch_err
M13recovery time omitted from conservationKILLED — conservation
Mutation run — final
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
13/13 killed, 0 escaped

Five escaped on the first run, and one of them was different in kind from anything in the previous chapters.

M1, M2, M6 and M11 were missing checks — the same "printed, not asserted" gap that Chapter 5.2 and Chapter 5.3 each hit. Four scenarios displayed their traces and asserted only the final outcome. Fixing M1 and M2 required a distinction the earlier chapters did not need:

Asserting the outcome does not check the route. Every shortcut bug in this chapter reaches the right terminal state by a wrong path, so the testbench has to record which states were visited, not only where it ended.

M2 additionally needed a stimulus change. With train_ok rising on the same cycle a shortcut would enter BASE, there is no window in which "BASE without train_ok" is observable — the route check was correct and could not see anything. Holding train_ok low for four cycles opened the window. It is the Chapter 5.3 identity-value problem in the time domain: a stimulus edge that coincides with the event you are checking hides the difference.

The fifth escape was not a testbench problem at all. It is in §6's callout: the FSM's bound was written against a counter that stops advancing precisely when the design gets stuck, and the diagnostic was written against the same counter. The design and its checker shared a blind spot, which no amount of stimulus would have exposed — only running the bounded and unbounded variants side by side and noticing they behaved identically.

16. Debug Lab

1

A link retries for hours and reports no error

RECOVERY-NEVER-CONCLUDES
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Keep trying to recover.
S_REC : if (recovered) nxt = S_TRAIN;
Symptom

A port on a marginal channel goes down and never comes back. No error is logged, no failure is reported, and the port draws power and produces nothing. Compared against a design with a bound, on the same stimulus:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  bounded      : state=FAILED failed=1 n_recovery=3
  retry-forever: state=OP     failed=0 n_recovery=3
Root Cause

The recovery state has one exit and it depends on success. If recovery never succeeds — the channel is genuinely bad, the partner is gone, a retimer has failed — there is no path out.

The correct behaviour is not to try harder. It is to narrow the operating point and try again, and to conclude when there is nothing left to narrow. A link that cannot hold 64 GT/s may hold 8; a link that can hold nothing should say so.

Fix
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
S_REC : if (recovered)       nxt = S_TRAIN;
        else if (dwell_done) nxt = may_retry ? S_TRAIN : S_FAIL;
Lesson

A recovery policy must be able to do three things: retry, narrow, and give up. Missing any one of them produces a link that either fails too eagerly or never fails at all, and the second is worse because it reports nothing. This is Chapter 5.3's unbounded-wait lesson at a different layer — anything that waits on the physical world needs a bound that leads somewhere.

2

The retry bound is present and does nothing

COUNTER-MEASURES-WHAT-STOPPED
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
assign may_retry = (n_recovery_q < MAX_RECOVERY);
// n_recovery_q increments only on ENTERING recovery:
if ((state_q != S_REC) && (nxt == S_REC)) n_recovery_q <= n_recovery_q + 8'd1;
// and the diagnostic is written against the same counter:
if (in_recovery && (n_recovery_q > MAX_RECOVERY + 1)) unbounded_recovery_err <= 1'b1;
Symptom

A design with an explicit retry bound behaves identically to one without. Both sit in recovery indefinitely, and the bound's own error flag never fires.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  bounded      : state=RECOV failed=0 n_recovery=2
  retry-forever: state=RECOV failed=0 n_recovery=2
  unbounded_recovery_err=0
Root Cause

n_recovery_q counts entries into recovery. A link that enters recovery once and never completes the attempt stays there, so the counter stops advancing — and it stops advancing for exactly the reason the bound was supposed to catch. The counter measures the thing that has stopped happening.

The diagnostic was written against the same counter, so the checker had the same blind spot as the design. Two independent-looking safeguards, one shared assumption.

Fix
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Bound ONE attempt, then let the attempt count advance.
assign dwell_done = (rec_dwell_q >= REC_DWELL);
rec_dwell_q <= (nxt == S_REC) ? (rec_dwell_q + 8'd1) : 8'd0;
S_REC : if (recovered)       nxt = S_TRAIN;
        else if (dwell_done) nxt = may_retry ? S_TRAIN : S_FAIL;
// restate the diagnostic against the thing that actually advances:
if (in_recovery && (rec_dwell_q > REC_DWELL + 2)) unbounded_recovery_err <= 1'b1;
Lesson

A bound on retries needs a bound on each try, or the outer bound is decorative. And a diagnostic derived from the same quantity as the mechanism it checks inherits its failure modes — the check must be stated against something that keeps moving when the design is stuck. Ask of any counter-based bound: what advances this, and can the failure I am guarding against stop it?

3

A link with fourteen bad lanes refuses to come up at all

DEGRADED-TREATED-AS-FAILURE
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Only a native width is acceptable.
assign train_ok = width_legal && is_native_width;
Symptom

A board with damage on most lanes brings up nothing. The same board in a PCIe-only slot comes up narrow and works. The failure is reported as a link-training failure with no indication that a usable narrow link was available.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  request x16 trained 2 : width=2 native=0 degraded=1 train_ok=1 | reject-degraded train_ok=0
  degraded_rejected_err=1
Root Cause

Degraded mode was treated as a failure condition rather than an outcome. Public material describes x2 and x1 widths in degraded mode — these are supported operating points, not errors.

Refusing them turns a recoverable situation (a slow link plus a clear diagnostic pointing at specific lanes) into an unrecoverable one (no link, and no information about why). The system loses both the service and the evidence.

Fix
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
assign train_ok = width_legal;              // narrow is still up
assign degraded = width_legal && !is_native_width;   // and say so, separately
Lesson

Come up degraded and report it; do not refuse to come up. The reporting half is not optional — a narrow link that does not announce itself is Chapter 5.1's silent downgrade. The pattern throughout Module 5 is the same: the fallback path is legal, and it must be visible.

4

A link operates at a width whose lanes never trained

WIDTH-OVERCLAIM
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// We asked for x16, so use x16.
assign width_out = lanes_requested;
Symptom

A link reports x16 and delivers roughly half the expected bandwidth, with correctable errors clustered on a subset of lanes. The error rate scales with load rather than being constant.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  request x16 trained 8 : width=8   (correct)
  overclaim_err=1                    (mutation M7)
Root Cause

The operating width was taken from the request rather than from what actually trained. Traffic is striped across lanes that are not carrying anything, so a fraction of every transfer goes into lanes that did not come up.

It presents as an error-rate problem because that is the visible symptom, and it sends debugging to the channel — but the channel is fine on the eight lanes that trained and irrelevant on the eight that did not.

Fix
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// The min in both directions: never wider than trained, never wider than asked.
assign width_out = (lanes_trained < lanes_requested) ? lanes_trained : lanes_requested;
if (width_out > lanes_trained) overclaim_err <= 1'b1;
Lesson

A request is not a result. The same distinction runs through Chapter 5.3's negotiation — what is enabled is the intersection, never the request — and it appears here as a width. Any signal named *_requested should be treated as an upper bound, and the assertion result <= requested is cheap and catches the whole family.

5

The link is legal, and delivers 6% of the expected bandwidth

LEGAL-IS-NOT-THE-OPERATING-POINT
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Training done. Report status.
assign link_ok = rate_legal && width_legal;
Symptom

Bring-up reports success on every port. Application throughput on a subset of ports is an order of magnitude below the pilot, and no port reports an error or a degraded state.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  x16 @ rate5 : bw=416   <-- the reference point
  x1  @ rate5 : bw=26    (6% of reference)  legal=1 native=0
Root Cause

link_ok reports legality, and both operating points are legal. x1 at the top rate clears the rate floor and is a supported width in degraded mode — the design is behaving exactly as specified.

The problem is that legality is a binary floor check and the operating point is a two-dimensional result. Collapsing the second into the first throws away everything anyone wanted to know.

Fix
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
assign link_ok       = rate_legal && width_legal;             // the floor
assign point_native  = is_native_width && is_native_rate;     // the target
assign bw_proxy      = width * rate_units;                    // the number
assign under_target  = link_ok && (bw_proxy < expected_bw);   // the alarm
Lesson

Report the operating point, not the training status. A boolean cannot carry a two-dimensional result, and the four ways to be "legal but wrong" in §12 are indistinguishable from the top row without the width, the rate, and a comparison against what was expected. Note also that the two axes have different root causes — narrow points at lanes, slow points at the channel — so reporting them merged loses the diagnosis as well as the magnitude.

6

Every recovery succeeds and the link is operational half the time

SUCCESSFUL-RECOVERY-IS-STILL-DOWNTIME
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Count failures.
if (recovery_failed) n_problem_q <= n_problem_q + 16'd1;
Symptom

A fleet reports zero recovery failures and consistently poor throughput on a subset of links. Every recovery on those links succeeds. Latency-sensitive workloads see periodic stalls that do not correlate with anything in the error logs.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  total=100 train=30 setup=8 op=50 recovery=12
  fraction of link life actually operational: 50%
  recovery failures: 0
Root Cause

Only failed recoveries were counted. A link that drops and successfully recovers every few seconds registers zero problems while spending a large fraction of its life not carrying traffic — and because recovery re-enters training rather than resuming (§7), each event costs the full training sequence.

Successful recovery is not free. It is downtime that reports success.

Fix
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Measure residency, and conserve it.
if (tick) begin
  t_total_q <= t_total_q + 16'd1;
  if (in_train) t_train_q <= t_train_q + 16'd1;
  if (in_rec)   t_rec_q   <= t_rec_q   + 16'd1;
  if (in_op)    t_op_q    <= t_op_q    + 16'd1;
end
if (t_total_q != t_train_q + t_setup_q + t_op_q + t_rec_q) accounting_err <= 1'b1;
Lesson

Count time, not events. Recovery count and recovery cost are different measurements, and only the second predicts what a workload experiences. The conservation law is what makes the number defensible — without it, a residency report can add to less than the total and nobody notices, which is exactly what mutation M13 does.

17. Verification Plan

ItemApproach and goal
Training routerecord visited states — TRAIN, BASE, SETUP each asserted
Guard windowshold train_ok low across the shortcut — guard observable
Rate contextcross negotiated x all rates — legality flips at the floor
Native ratesassert the class per rate — boundary at 32 GT/s exact
Width clamptrained above, below and equal to requested
Degraded acceptx2, x1 on a healthy channel — up, and reported degraded
Recovery policyrepeated failures to the floor — degrades, then concludes
Bound integritybounded and unbounded side by side — they must differ
Operating pointscross width x rate — native count = 6 of 16
Residencymixed trace with recovery — conserves every tick

Row 8 is the one that would have caught §6's shared blind spot. Running a bounded design and an unbounded one on the same stimulus and asserting that they diverge is a cheap and unusually effective check, because it tests the bound rather than the behaviour the bound is supposed to constrain.

18. Design Review

  • Can recovery retry, narrow, and give up? Missing any one produces a link that never fails or fails too soon.
  • What advances every counter used as a bound, and can the failure being guarded against stop it?
  • Is any diagnostic derived from the same quantity as the mechanism it checks?
  • Is a degraded width accepted and reported, or refused?
  • Is the operating width clamped to the lanes that trained, in both directions?
  • Does anything report a two-dimensional operating point as a boolean?
  • Is rate legality evaluated with the negotiation context, or context-free?
  • Is recovery time measured, or only recovery failures?
  • Does degradation stop at the floor?

19. How This Appears in Real Engineering

Bring-up reports say "trained" and stop. §1's five outcomes all produce that word. Teams that add the operating point and a comparison against expectation find whole classes of problem that were previously invisible — usually on a subset of boards nobody had reason to look at.

Recovery loops are found by power, not by logs. A port stuck in the Debug Lab 1 pattern logs nothing. It is typically noticed because a system draws more power than expected, or because someone happens to look at a link that should be carrying traffic.

Width and rate degradation get triaged to the same team, wrongly. Narrow points at specific lanes — a trace, a connector pin, a via. Slow points at the channel as a whole — insertion loss, crosstalk, equalisation. Reporting a merged "degraded" flag sends both to whoever owns signal integrity, and half of those investigations are looking at the wrong thing.

The floor is a compatibility cliff, not a gradient. A link that cannot hold 8.0 GT/s is not a slow CXL link; it is not a CXL link. That discontinuity surprises people who expect graceful degradation all the way down, and it is why degraded_past_floor_err exists as a distinct check.

20. Common Misconceptions

ClaimWhy it is wrong
"CXL defines its own link training state machine"It does not. Training is PCIe's; what is CXL-specific is the alternate protocol negotiation carried within it and the constraints on the resulting operating point.
"The link trains at 8 GT/s minimum"It starts training at 2.5 GT/s (Gen 1) and must reach 8.0 GT/s or higher after negotiation. Start rate and operating rate are different questions.
"x8 from a requested x16 is degraded mode"No — x16, x8 and x4 are native widths. Native/degraded classifies the width itself, not the shortfall against the request.
"Degraded mode means something is broken"Degraded mode is a supported operating point. Refusing to come up narrow turns a recoverable situation into an unrecoverable one.
"A legal link is a good link"x1 at 64 GT/s is legal and delivers about 6% of an x16 at 64 GT/s. Legality is a floor check; the operating point is a two-dimensional result.
"Successful recovery costs nothing"Recovery re-enters training, it does not resume. A link that recovers often can be operational half the time with zero recovery failures logged.
"A retry counter makes recovery bounded"Only if each attempt is itself bounded. A counter that advances on entering recovery stops advancing exactly when the design gets stuck.
"Degradation can always go one step further"It stops at the floor. Below 8.0 GT/s there is no CXL link to degrade to.

21. Interview Reasoning

22. Exercises

  1. Calculate. Using §12's proxy, find every operating point delivering between 20% and 60% of the x16-at-64 reference. Group them by which axis was degraded, and state which group a lane-level fault would produce.

  2. Trace. Using §6's FSM with MAX_RECOVERY = 3 and REC_DWELL = 4, write the state sequence for a link that errors immediately after each successful retrain and never recovers within the dwell. State the cycle at which it reaches FAILED, and how that changes if only the attempt count is bounded.

  3. DV task. Write the check that would have caught the shared blind spot in §6 — a bound whose counter stops advancing when the design gets stuck. Explain why no amount of stimulus on the bounded design alone would have found it.

  4. Debug task. A fleet shows: 100% links legal, 0 recovery failures, 40% of links below expected throughput, all on one board revision. Give your investigation order, and say which measurement distinguishes a width problem from a rate problem before you touch any hardware.

  5. Design. Extend §10's policy to degrade width as well as rate. State the ordering you would use between the two axes, justify it from §12's table, and say which new diagnostic the second axis requires.

  6. Critique. Argue that the 8.0 GT/s floor should not exist and that CXL should degrade to any PCIe rate. Give the strongest case, then identify what the floor buys and which failure it prevents.

23. Summary

Training resolves to an operating point — a width and a rate together — and "the link trained" says almost nothing about it.

  • Three rates, three jobs: 2.5 GT/s start, 8.0 GT/s floor, 32/64 GT/s native. The start rate is below the floor, and that is by design.
  • Native widths x16/x8/x4; degraded x2/x1. Native classifies the width itself, not the shortfall against the request — x8 from a requested x16 is native.
  • Degraded is an outcome, not a failure. Come up narrow and report it; refusing loses both the service and the evidence.
  • 6 of 16 width-by-rate points are native, because the axes combine with AND. x1 at 64 GT/s is legal and delivers about 6% of the reference.
  • Recovery must retry, narrow, and conclude. It re-enters training rather than resuming, so successful recovery is still downtime — measure residency, not events.
  • A bound on retries needs a bound on each try, and a diagnostic derived from the same counter as the mechanism inherits its blind spot. Run bounded and unbounded designs side by side and assert they diverge.
  • Verification lesson: asserting the outcome does not check the route, and a stimulus edge coinciding with the checked event hides the difference.

Chapter 5.5 closes the module with the last question: once the link has decided and settled, how does software find out what it got?

Standards & specifications

Governing standard
CXL Specification (CXL Consortium)(opens CXL Consortium in a new tab)

Defines CXL.io, CXL.cache and CXL.mem, and the coherence and memory-pooling behaviour built on them. System design and deployment topology are not mandated.

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 CXL curriculum.