CXL · Module 5
CXL Discovery
How software finds out what a CXL device is: walking PCIe's capability list to the CXL DVSEC, the exactly-one rule on the primary function, which functions are not CXL, and why the walk must be bounded. Six RTL models simulated, twelve mutations, twelve killed.
The link has trained (5.4) and decided what protocol it speaks (5.3). Hardware at both ends knows what it agreed to.
Software knows none of it. This chapter closes Module 5 with the last question: how does the host find out what it has, and what can go wrong in the finding out?
1. The Engineering Problem — Two Agreements, One of Them Invisible
Chapter 5.3's negotiation happened in ordered sets during link bring-up, at 2.5 GT/s, before any software was running. It produced a real agreement between two pieces of hardware.
Software cannot see ordered sets. It sees configuration space. So there has to be a second, independent mechanism by which the same facts become visible to a driver — and the two can disagree.
That gives discovery an unusual property: it is a read, not a negotiation. Nothing is decided here. Every failure in this chapter is a failure to read correctly — reading a structure that is not there, reading one that is too old, reading the right structure at the wrong time, or reading a list the device controls without bounding how far you will go.
2. The One-Sentence Model
CXL discovery is PCIe enumeration plus a bounded walk to a CXL-specific structure that must appear exactly once on the primary function — and everything software believes about the device is a read from that structure, so every question is "is this present, is it valid, and did I read it in the right order".
Call it present, valid, ordered. Those three are different questions and they fail differently.
3. What This Chapter Owns
| Question | Owned by |
|---|---|
| Why CXL rides on PCIe | 5.1 |
| Config space as a reuse class | 5.2 |
| The hardware protocol agreement | 5.3 |
| The operating point | 5.4 |
| How software learns any of it | this chapter |
4. The Discovery Sequence
Each step consumes the previous step's result. Acting on a later step without the earlier one means acting on a value that was never read — which is RTL 5 and mutations M9 and M10.
Note the second arrow. Enumeration succeeds on any PCIe host, including one that has never heard of CXL, which is Chapter 5.2's most valuable single reuse. Only the third arrow onwards is CXL-aware.
5. Link Training, Mode Establishment, and Discovery Are Three Different Things
These three get conflated constantly, including in otherwise careful writing. They differ in what is decided, when, by whom, and what fails if it goes wrong.
What each one decides, and when.
| Mechanism | Decides | When |
|---|---|---|
| Training | width and rate | first of all |
| Mode setup | the protocol | in Config, at 2.5 GT/s |
| Discovery | nothing — a read | once software runs |
What carries each one, and how it fails.
| Mechanism | Carried by | Fails as |
|---|---|---|
| Training | PCIe LTSSM (5.4) | no link, or a bad point |
| Mode setup | modified TS (5.3) | PCIe, not CXL |
| Discovery | config reads (here) | bad driver setup |
They also differ in who is talking: training is between the two link ends and the path between them, mode establishment is between the two link ends, and discovery is between host software and the device. And in what is CXL-specific: nothing in training, the payload but not the mechanism in mode establishment, the structure but not the walk in discovery.
Three rows deserve emphasis.
"Decides?" separates discovery from the other two. Training and mode establishment are negotiations that change what the hardware does. Discovery changes only what software believes. That is why a discovery bug produces a working link that software mishandles rather than a broken link.
The two agreements can disagree. The hardware negotiated in ordered sets; software reads config space. If the structure says something the negotiation did not produce, both are internally consistent and the system is wrong.
"CXL-specific?" is the same answer three times, and it is the module's thesis. In every row the mechanism is PCIe's and the content is CXL's. That is what Chapter 5.1 argued from economics and 5.2 inventoried structure by structure.
6. Teaching-model boundary
7. RTL 1 — Walking a List the Device Controls
Discovery reuses PCIe's mechanism: the host walks a linked list of capability structures. That list comes from the device. A host that walks it without a bound has handed a remote party control over whether its own enumeration terminates.
module dvsec_scan #(
parameter int unsigned MAX_STEPS = 8,
parameter bit UNBOUNDED = 1'b0 // 1 = the hanging shape
) (
input logic clk, rst_n, start,
input logic [7:0] next_ptr, // next capability offset; 0 = end of list
input logic entry_is_dvsec,
input logic [3:0] entry_revision,
output logic [7:0] cur_ptr_q, steps_q,
output logic scanning, found, done, gave_up,
output logic walked_past_bound_err, found_without_revision_err
);
localparam logic [3:0] MIN_REV = 4'd1;
localparam logic [1:0] S_IDLE = 2'd0, S_WALK = 2'd1, S_FOUND = 2'd2, S_GIVEUP = 2'd3;
logic [1:0] st_q;
logic over_budget, entry_usable;
assign over_budget = !UNBOUNDED && (steps_q >= MAX_STEPS[7:0]);
assign entry_usable = entry_is_dvsec && (entry_revision >= MIN_REV);
assign scanning = (st_q == S_WALK);
assign found = (st_q == S_FOUND);
assign gave_up = (st_q == S_GIVEUP);
assign done = found || gave_up;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
st_q <= S_IDLE; cur_ptr_q <= 8'd0; steps_q <= 8'd0;
walked_past_bound_err <= 1'b0; found_without_revision_err <= 1'b0;
end else begin
case (st_q)
S_IDLE : if (start) begin st_q <= S_WALK; steps_q <= 8'd0; end
S_WALK : begin
steps_q <= steps_q + 8'd1;
if (entry_usable) st_q <= S_FOUND;
else if (next_ptr == 8'd0) st_q <= S_GIVEUP; // clean end of list
else if (over_budget) st_q <= S_GIVEUP; // list is lying to us
else cur_ptr_q <= next_ptr;
end
default : ;
endcase
if (scanning && (steps_q > MAX_STEPS[7:0] + 8'd2)) walked_past_bound_err <= 1'b1;
if (found && !entry_usable && entry_is_dvsec) found_without_revision_err <= 1'b1;
end
end
endmoduleNote the two distinct exits to S_GIVEUP. next_ptr == 0 is a clean end of list — the device has no CXL structure and is a perfectly good PCIe device. over_budget is a malformed list — the device is telling us to keep walking and we have decided not to. Same terminal state, entirely different diagnosis, which is why RTL 6 counts the reason separately.
A well-formed device: the walk finds the structure in three steps
8 cycles bounded : scanning=0 gave_up=1 steps=9
unbounded : scanning=1 gave_up=0 steps=23The stimulus never sets next_ptr to zero — a cyclic or malformed list. The bounded scan stops after nine steps and reports that it gave up. The unbounded one was still walking when the simulation ended, and would walk forever.
This is not a corruption case that only a hostile device produces. Early silicon returns garbage from unimplemented registers, a partially-powered device returns all-ones, and a list can loop through an ordinary firmware bug. The host has no way to know in advance, so the bound is not paranoia — it is the only thing that makes enumeration a terminating operation.
8. RTL 2 — What a Device May Claim
Public material is explicit: CXL.io is mandatory; CXL.cache and CXL.mem are optional. So .io is a floor, and a device advertising .cache or .mem without it is describing a configuration that cannot exist — .io is what carries configuration in the first place.
module protocol_advertise #(
parameter bit TRUST_CLAIM = 1'b0 // 1 = the broken shape
) (
input logic clk, rst_n, valid,
input logic adv_io, adv_cache, adv_mem,
output logic en_io, en_cache, en_mem,
output logic well_formed, device_type_q,
output logic malformed_accepted_err, cache_without_io_err
);
// .io is the floor. Nothing else is meaningful without it.
assign well_formed = valid && adv_io;
assign en_io = valid && (TRUST_CLAIM ? adv_io : well_formed);
assign en_cache = valid && (TRUST_CLAIM ? adv_cache : (well_formed && adv_cache));
assign en_mem = valid && (TRUST_CLAIM ? adv_mem : (well_formed && adv_mem));
assign device_type_q = en_mem && !en_cache;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
malformed_accepted_err <= 1'b0; cache_without_io_err <= 1'b0;
end else if (valid) begin
if ((en_cache || en_mem || en_io) && !well_formed) malformed_accepted_err <= 1'b1;
if (en_cache && !en_io) cache_without_io_err <= 1'b1;
end
end
endmodule io cache mem | en_io en_cache en_mem well_formed | trust-claim en_cache
0 0 0 | 0 0 0 0 | 0
0 0 1 | 0 0 0 0 | 0
0 1 0 | 0 0 0 0 | 1
0 1 1 | 0 0 0 0 | 1
1 0 0 | 1 0 0 1 | 0
1 0 1 | 1 0 1 1 | 0
1 1 0 | 1 1 0 1 | 1
1 1 1 | 1 1 1 1 | 1Four of eight combinations are malformed, and all four have adv_io low. The correct design enables nothing for all four; the trusting variant enables .cache in two of them, on a device that cannot carry configuration traffic.
The full 2³ sweep is deliberate. A test that only exercises the well-formed half has not tested the floor at all, and it is the natural half to write, because it is the half real devices produce.
9. RTL 3 — Not Every Function Is a CXL Function
Public material describes a Non-CXL Function Map DVSEC advertising which functions are not CXL. Note the polarity: the map lists the exceptions, not the members.
That makes the default the interesting question. If the map is absent, what should a host assume?
module function_map #(
parameter bit ASSUME_ALL_CXL = 1'b0 // 1 = the broken shape
) (
input logic clk, rst_n, valid, map_present,
input logic [7:0] non_cxl_map, // bit n set = function n is NOT CXL
input logic [2:0] fn_sel,
output logic fn_is_cxl,
output logic [3:0] n_cxl_fns,
output logic treated_pcie_as_cxl_err, map_absent_assumed_err
);
logic [7:0] cxl_mask;
integer k;
// With no map present, only the primary function is known to be CXL.
assign cxl_mask = ASSUME_ALL_CXL ? 8'hFF : (map_present ? ~non_cxl_map : 8'h01);
assign fn_is_cxl = valid && cxl_mask[fn_sel];
always_comb begin
n_cxl_fns = 4'd0;
for (k = 0; k < 8; k = k + 1) n_cxl_fns = n_cxl_fns + {3'd0, cxl_mask[k]};
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
treated_pcie_as_cxl_err <= 1'b0; map_absent_assumed_err <= 1'b0;
end else if (valid) begin
if (map_present && non_cxl_map[fn_sel] && fn_is_cxl) treated_pcie_as_cxl_err <= 1'b1;
if (!map_present && (n_cxl_fns > 4'd1)) map_absent_assumed_err <= 1'b1;
end
end
endmodule fn is_cxl (map says non-CXL: 1,3) | assume-all-CXL is_cxl
0 1 | 1
1 0 | 1
2 1 | 1
3 0 | 1
...
CXL functions: correct=6 | assume-all=8
no map present : correct claims 1 function | assume-all claims 8The last line is the important one. With no map present, the correct design claims one function — the primary, which public material requires to carry the DVSEC — and the broken one claims all eight.
A missing map is not a permissive default. It is the absence of information, and the safe reading is the minimum the specification guarantees. This is Chapter 5.1's "the peer said no versus the peer has not answered" distinction, arriving as a data-structure default rather than a signal.
10. RTL 4 — Exactly One, on the Primary Function
"One instance of CXL DVSEC ID0 with Revision 1 or greater" is a cardinality rule, and cardinality rules fail in both directions.
module primary_function_rule #(
parameter bit FIRST_WINS = 1'b0 // 1 = the broken shape
) (
input logic clk, rst_n, valid, is_primary_fn,
input logic [2:0] n_id0_instances,
input logic [3:0] revision,
output logic cxl_device, cardinality_ok,
output logic zero_instances_err, duplicate_accepted_err, nonprimary_id0_err
);
localparam logic [3:0] MIN_REV = 4'd1;
assign cardinality_ok = valid && (n_id0_instances == 3'd1);
assign cxl_device = valid && is_primary_fn && (revision >= MIN_REV) &&
(FIRST_WINS ? (n_id0_instances >= 3'd1) : cardinality_ok);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
zero_instances_err <= 1'b0; duplicate_accepted_err <= 1'b0;
nonprimary_id0_err <= 1'b0;
end else if (valid) begin
if (is_primary_fn && (n_id0_instances == 3'd0)) zero_instances_err <= 1'b1;
// Taking the first of several is a silent choice between disagreeing
// structures, not a resolution.
if (cxl_device && (n_id0_instances > 3'd1)) duplicate_accepted_err <= 1'b1;
if (!is_primary_fn && cxl_device) nonprimary_id0_err <= 1'b1;
end
end
endmodule instances=0 : cxl_device=0 cardinality_ok=0 | first-wins cxl_device=0
instances=1 : cxl_device=1 cardinality_ok=1 | first-wins cxl_device=1
instances=2 : cxl_device=0 cardinality_ok=0 | first-wins cxl_device=1
instances=3 : cxl_device=0 cardinality_ok=0 | first-wins cxl_device=1
one instance, revision 0 : cxl_device=0 <-- present, too old
first-wins duplicate_accepted_err=1Zero and two are both violations, and only zero looks like one. A driver that takes the first of several instances gets a plausible answer and never reports a problem — but if the two structures disagree, which one it read is an accident of traversal order, and the same device may be configured differently on two hosts.
The revision 0 row is Chapter 5.2's presence-versus-validity distinction: the structure is there, at the right ID, on the right function, and is not the format about to be parsed.
11. RTL 5 — Discovery Is Ordered
module discovery_sequence #(
parameter bit ENABLE_EARLY = 1'b0 // 1 = the broken shape
) (
input logic clk, rst_n,
input logic enumerated, dvsec_located, caps_read, want_enable,
output logic [2:0] step_q,
output logic enabled,
output logic enable_before_read_err, read_before_locate_err,
output logic locate_before_enum_err
);
localparam logic [2:0] K_NONE = 3'd0, K_ENUM = 3'd1, K_LOCATED = 3'd2,
K_READ = 3'd3, K_ENABLED = 3'd4;
logic may_enable;
assign may_enable = ENABLE_EARLY ? 1'b1 : (step_q == K_READ);
assign enabled = (step_q == K_ENABLED);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
step_q <= K_NONE;
enable_before_read_err <= 1'b0; read_before_locate_err <= 1'b0;
locate_before_enum_err <= 1'b0;
end else begin
if (want_enable && may_enable && (step_q != K_ENABLED)) begin
step_q <= K_ENABLED;
if (step_q != K_READ) enable_before_read_err <= 1'b1;
end else begin
case (step_q)
K_NONE : if (enumerated) step_q <= K_ENUM;
K_ENUM : if (dvsec_located) step_q <= K_LOCATED;
K_LOCATED : if (caps_read) step_q <= K_READ;
default : ;
endcase
end
if (dvsec_located && !enumerated) locate_before_enum_err <= 1'b1;
if (caps_read && !dvsec_located) read_before_locate_err <= 1'b1;
end
end
endmodule want_enable with nothing done : correct=NONE | early enabled=1
after enumerate : ENUM
after locate : LOCATED
a cycle later, still not read : LOCATED
after read : READ
after enable : ENABLED enabled=1Line 1 is the whole module: software asked to enable before anything had been read, and the correct design stayed put while the permissive one enabled immediately — configuring a device from values it never fetched.
Line 4 — "a cycle later, still not read" — is a check added specifically because mutation M10 makes the LOCATED transition unconditional. Without a cycle in which caps_read is low and the machine is observed not to move, the shortcut is invisible.
12. RTL 6 — Outcomes, Reasons, and an Independent Reference
module discovery_outcome_counters (
input logic clk, rst_n, probe,
input logic got_cxl, // usable CXL structure found
input logic got_pcie_only, // enumerated, no CXL structure
input logic got_malformed, // structure present but unusable
input logic reason_old_rev, // a REASON for malformed, not an outcome
output logic [15:0] n_probe_q, n_cxl_q, n_pcie_q, n_malformed_q, n_old_rev_q,
output logic accounting_err, multi_outcome_err, reason_without_outcome_err
);
logic [2:0] hot;
assign hot = 3'd0 + got_cxl + got_pcie_only + got_malformed;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_probe_q <= '0; n_cxl_q <= '0; n_pcie_q <= '0;
n_malformed_q <= '0; n_old_rev_q <= '0;
accounting_err <= 1'b0; multi_outcome_err <= 1'b0;
reason_without_outcome_err <= 1'b0;
end else begin
if (probe) n_probe_q <= n_probe_q + 16'd1;
if (got_cxl) n_cxl_q <= n_cxl_q + 16'd1;
if (got_pcie_only) n_pcie_q <= n_pcie_q + 16'd1;
if (got_malformed) n_malformed_q <= n_malformed_q + 16'd1;
if (reason_old_rev) n_old_rev_q <= n_old_rev_q + 16'd1;
// Outcomes partition. The revision reason annotates malformed and must
// NOT appear in the sum.
if (n_probe_q != n_cxl_q + n_pcie_q + n_malformed_q) accounting_err <= 1'b1;
if (hot > 3'd1) multi_outcome_err <= 1'b1;
// INPUT ASSUMPTION, made explicit: a reason annotates an outcome, so
// reason_old_rev may only be asserted alongside got_malformed. Left
// implicit, this assumption silently makes "count the outcome" and
// "count outcome OR reason" the same function.
if (reason_old_rev && !got_malformed) reason_without_outcome_err <= 1'b1;
end
end
endmodule probes=50 cxl=38 pcie-only=6 malformed=6 (old-revision=3)
conservation probes == cxl+pcie+malformed : 50 == 50
old revision explains 3 of 6 malformed resultsThree outcomes partition; the revision reason annotates. This is Chapter 5.3's timeout distinction again — outcomes partition, reasons annotate — and mutation M11 breaks it in the same way by adding the reason to the sum.
13. Assertions
Concurrent SVA execution: NOT SUPPORTED BY Icarus Verilog. Not executed; each maps to a procedural stand-in and a mutation.
// SAFETY -------------------------------------------------------------------
// V1 — the capability walk never exceeds its step bound.
a_walk_bounded: assert property (@(posedge clk) disable iff (!rst_n)
scanning |-> (steps_q <= MAX_STEPS + 1));
// V2 — a structure is only "found" at or above the minimum revision.
a_found_is_usable: assert property (@(posedge clk) disable iff (!rst_n)
found |-> (entry_is_dvsec && (entry_revision >= MIN_REV)));
// V3 — nothing is enabled on a device that did not advertise .io.
a_io_is_the_floor: assert property (@(posedge clk) disable iff (!rst_n)
(en_io || en_cache || en_mem) |-> adv_io);
// V4 — a function the map excludes is never treated as CXL.
a_map_respected: assert property (@(posedge clk) disable iff (!rst_n)
(valid && map_present && non_cxl_map[fn_sel]) |-> !fn_is_cxl);
// V5 — with no function map, only the primary function is claimed.
a_absent_map_minimal: assert property (@(posedge clk) disable iff (!rst_n)
(valid && !map_present) |-> (n_cxl_fns == 1));
// V6 — exactly one DVSEC ID0 instance, on the primary function.
a_exactly_one: assert property (@(posedge clk) disable iff (!rst_n)
cxl_device |-> (is_primary_fn && (n_id0_instances == 1)
&& (revision >= MIN_REV)));
// V7 — discovery steps are strictly ordered.
a_ordered: assert property (@(posedge clk) disable iff (!rst_n)
enabled |-> ($past(step_q) == K_READ));
// V8 — a later step never precedes an earlier one.
a_no_reordering: assert property (@(posedge clk) disable iff (!rst_n)
(caps_read |-> dvsec_located) and (dvsec_located |-> enumerated));
// V9 — CONSERVATION: every probe reaches exactly one outcome.
a_outcomes_conserved: assert property (@(posedge clk) disable iff (!rst_n)
n_probe_q == n_cxl_q + n_pcie_q + n_malformed_q);
// INPUT ASSUMPTION ---------------------------------------------------------
// V10 — a reason is only reported alongside its outcome. Written as an ASSUME
// for formal and checked as an ASSERT in simulation: it constrains the
// environment, and leaving it implicit makes two different counter
// designs indistinguishable (see §12).
m_reason_has_outcome: assume property (@(posedge clk) disable iff (!rst_n)
reason_old_rev |-> got_malformed);
// LIVENESS ------------------------------------------------------------------
// V11 — the walk always terminates. Provable BECAUSE of V1; without the step
// bound this depends on the DEVICE providing a well-formed list, which
// is an assumption about a remote party the host cannot verify.
a_walk_terminates: assert property (@(posedge clk) disable iff (!rst_n)
scanning |-> s_eventually done);V10 is the only assume in Module 5, and it is here because §12 showed what an unstated assumption costs. The pairing of V1 and V11 is the module's recurring shape for the third time: the liveness property you want, discharged by a bound you can prove — and here the environment being bounded away is not a slow partner or a bad channel but the device's own data structure.
14. Mutation Testing
Twelve mutations. Clean code restored after each.
| ID | Mutation | Result |
|---|---|---|
| M1 | the capability walk has no bound | KILLED — walked_past_bound_err |
| M2 | revision not checked when found | KILLED — added stimulus |
| M3 | .io no longer required | KILLED — 8-combination sweep |
| M4 | .cache enabled from the claim alone | KILLED — 8-combination sweep |
| M5 | with no map, assume all functions CXL | KILLED — absent-map default |
| M6 | function map polarity inverted | KILLED — per-function check |
| M7 | duplicates accepted as one | KILLED — cardinality check |
| M8 | the DVSEC revision floor dropped | KILLED — revision check |
| M9 | enable permitted at any step | KILLED — enable_before_read_err |
| M10 | the read step is skipped | KILLED — no-advance check |
| M11 | the reason counted as a fourth outcome | KILLED — conservation |
| M12 | the reason double-counts its outcome | KILLED — independent reference |
12/12 killed, 0 escapedThree escaped on the first run, and each needed a different remedy.
M2 was a missing stimulus. Every scan used revision 1, so "is a DVSEC" and "is a usable DVSEC" produced identical results. Adding a scan of a list whose DVSEC is at revision 0 separated them.
M10 was a missing check of a very specific kind. The mutation makes the LOCATED step advance unconditionally, so the machine reaches the right final state one cycle early. Catching it required a cycle in which caps_read is low and the machine is observed not to move. Asserting that a state machine stays put is easy to omit, because every natural assertion is about transitions that happen.
M12 was an equivalent mutant conditioned on an unstated input assumption, resolved by §12's callout.
15. Debug Lab
Enumeration hangs on one device and the whole bus stalls
UNBOUNDED-LIST-WALK// Walk to the end of the capability list.
S_WALK : if (entry_is_dvsec) st_q <= S_FOUND;
else if (next_ptr == 0) st_q <= S_GIVEUP;
else cur_ptr_q <= next_ptr;A host boots normally with most cards and hangs during enumeration with one. No timeout, no error, no progress — and other devices behind the same bridge never enumerate either.
bounded : scanning=0 gave_up=1 steps=9
unbounded : scanning=1 gave_up=0 steps=23 <-- and countingThe walk terminates only when the device says so. If next_ptr never reaches zero — a cyclic list, a partially-powered device returning all-ones, unimplemented registers returning garbage — there is no exit.
The host has handed a remote party control over whether its own enumeration terminates, and it cannot verify the list in advance because reading the list is the operation.
assign over_budget = (steps_q >= MAX_STEPS);
S_WALK : if (entry_usable) st_q <= S_FOUND;
else if (next_ptr == 0) st_q <= S_GIVEUP; // clean end of list
else if (over_budget) st_q <= S_GIVEUP; // list is lying to us
else cur_ptr_q <= next_ptr;Any traversal of a remote data structure needs a step bound. The two exits to the same terminal state must be distinguished in reporting — a clean end of list is an ordinary PCIe device, a bound overrun is a malformed one, and merging them makes a firmware bug indistinguishable from a device that simply is not CXL. This is Chapter 5.3's timeout at a third layer: anything waiting on a remote party needs a bound that leads somewhere legal.
A driver configures CXL on an ordinary PCIe function
FUNCTION-MAP-DEFAULT-WRONG// It's a CXL device, so its functions are CXL functions.
assign cxl_mask = 8'hFF;A multi-function CXL device works for its memory function and fails on a management function on the same device. The management function returns errors for register accesses at offsets that are valid for CXL functions and mean nothing for it.
no map present : correct claims 1 function | assume-all claims 8
map says non-CXL: 1,3 : correct=6 CXL functions | assume-all=8
treated_pcie_as_cxl_err=1Two related mistakes. The Non-CXL Function Map was ignored, so functions explicitly excluded were driven as CXL functions. And the default with no map was "all", when the only function public material guarantees to carry the DVSEC is the primary one.
A missing map is the absence of information, not permission. Reading it as permission produces exactly the failure above on any device whose functions are not uniform.
// The map lists exceptions. With no map, claim only what is guaranteed.
assign cxl_mask = map_present ? ~non_cxl_map : 8'h01;
if (map_present && non_cxl_map[fn_sel] && fn_is_cxl) treated_pcie_as_cxl_err <= 1'b1;
if (!map_present && (n_cxl_fns > 4'd1)) map_absent_assumed_err <= 1'b1;Absent data is not permissive data. The safe default is the minimum the specification guarantees, and anything beyond it is an assumption that deserves its own diagnostic. Note also the map's polarity — it lists what is not CXL — so a reader who inverts it gets a complementary and entirely plausible answer, which mutation M6 exercises.
The same device is configured differently on two hosts
DUPLICATE-DVSEC-FIRST-WINS// Found a DVSEC ID0. Use it.
assign cxl_device = is_primary_fn && (n_id0_instances >= 3'd1);A device is configured with one protocol set on one host and a different one on another. Both hosts report success, neither reports an anomaly, and the device firmware is identical.
instances=2 : cxl_device=0 cardinality_ok=0 | first-wins cxl_device=1
duplicate_accepted_err=1The device presents more than one DVSEC ID0 instance — a firmware bug — and the host takes the first one it encounters. Public material requires one instance on the primary function, so "at least one" is not the rule.
If the instances disagree, which one is read depends on traversal order, so two correct hosts can reach different conclusions from the same device. Taking the first is not a resolution; it is a silent choice.
assign cardinality_ok = (n_id0_instances == 3'd1); // exactly one
assign cxl_device = is_primary_fn && cardinality_ok && (revision >= MIN_REV);
if (cxl_device && (n_id0_instances > 3'd1)) duplicate_accepted_err <= 1'b1;"Exactly one" fails in both directions, and only zero looks like a failure. Cardinality rules are routinely implemented as existence checks because the zero case is the one people picture. Whenever a specification says "one instance of", check both bounds — and treat a duplicate as a reportable device defect rather than something to resolve by convention, because any convention makes host behaviour depend on traversal order.
A device advertises .cache without .io and the host believes it
PROTOCOL-CLAIM-TRUSTED// Enable what the device advertises.
assign en_cache = adv_cache;
assign en_mem = adv_mem;An early-silicon device with a register-initialisation bug advertises .cache with .io clear. The host enables coherency, then fails to configure the device — because configuration travels over .io, which was never enabled.
io cache mem | en_io en_cache en_mem well_formed | trust-claim en_cache
0 1 0 | 0 0 0 0 | 1
malformed_accepted_err=1 cache_without_io_err=1Each advertised bit was acted on independently, with no check that the combination describes a device that can exist. CXL.io is mandatory and .cache/.mem are optional, so .io is a floor rather than a peer of the other two.
The host ends up in a state where it believes it has a coherent device and has no path to configure it — a failure that appears at configuration time, well after the read that caused it.
assign well_formed = valid && adv_io; // the floor
assign en_io = well_formed;
assign en_cache = well_formed && adv_cache;
assign en_mem = well_formed && adv_mem;Validate the combination, not the bits. Four of the eight .io/.cache/.mem combinations are malformed, and a test that only exercises the four well-formed ones — the natural half to write, because it is the half real devices produce — has not tested the floor at all. Any advertisement with a mandatory element needs a well-formedness check before any optional element is acted on.
A device is configured from values that were never read
ENABLE-BEFORE-READ// Software asked to enable. Enable.
assign may_enable = 1'b1;A device is enabled with a plausible but wrong protocol set. It works on the bench where the driver initialises slowly and fails on a fast host, or on one boot in twenty. The values written correspond to nothing the device advertised.
want_enable with nothing done : correct=NONE | early enabled=1
enable_before_read_err=1Enablement was not gated on the read having completed. Whatever was in the capability variables — reset values, a previous device's values, uninitialised memory — got written to the device.
The timing dependence is what makes it expensive: on a slow path the read usually finishes first by accident, so the bug reproduces only under conditions that look unrelated to discovery.
assign may_enable = (step_q == K_READ);
if (want_enable && may_enable && (step_q != K_ENABLED)) begin
step_q <= K_ENABLED;
if (step_q != K_READ) enable_before_read_err <= 1'b1;
endEach step of an ordered read must gate the next explicitly, not by expected timing. "It works because the read is fast" is not a design. Note the testbench requirement this creates: catching the related mutation M10 needed a cycle in which the input is low and the machine is observed not to advance — asserting that a state machine stays put is easy to omit, since every natural assertion is about transitions that happen.
A fleet reports 12% malformed devices and the cause is unrecoverable
REASON-FOLDED-INTO-OUTCOME// Count anything that went wrong with the structure.
if (got_malformed || reason_old_rev) n_malformed_q <= n_malformed_q + 16'd1;Fleet telemetry reports 12% of probes malformed. That figure is consistent with a firmware bug on one device family, with a population of older devices at a revision this host predates, and with a discovery bug on the host — and nothing in the data separates them.
probes=50 cxl=38 pcie-only=6 malformed=6 (old-revision=3)
old revision explains 3 of 6 malformed resultsThe reason a structure was unusable was folded into the outcome count. An old revision is a reason for malformed, like a timeout is a reason for PCIe fallback in Chapter 5.3. Merging them destroys the only information that distinguishes "these devices predate this host" from "these devices are broken".
The bug is unusually resistant to testing, because the reason only ever occurs alongside its outcome — so the OR is an identity and the counts are unchanged. It survived the conservation law for exactly that reason.
if (got_malformed) n_malformed_q <= n_malformed_q + 16'd1; // the outcome
if (reason_old_rev) n_old_rev_q <= n_old_rev_q + 16'd1; // the reason
// and make the assumption that hid the bug explicit:
if (reason_old_rev && !got_malformed) reason_without_outcome_err <= 1'b1;Outcomes partition; reasons annotate — the same rule as Chapter 5.3's timeouts. What is new here is why the bug is hard to catch: an unstated input assumption made two different designs produce identical numbers. A conservation law proves the parts are consistent with each other and cannot prove any part is correct; that needs a reference computed from the stimulus rather than from the design.
16. Verification Plan
| Item | Approach and goal |
|---|---|
| List walk | well-formed, empty and cyclic — terminates, reason distinguished |
| Revision gate | DVSEC revisions spanning the floor — found flips at it |
| Advertisement | all 8 io/cache/mem combinations — 4 malformed enable nothing |
| Function map | present, absent and inverted — absent default is primary only |
| Cardinality | 0, 1, 2, 3 instances — only 1 accepted, 0 and 2 both reported |
| Ordering | request each step out of order — later steps refused |
| Non-advance | hold an input low a cycle — machine observed not to move |
| Outcome counts | mixed population vs an independent reference |
| Input assumptions | drive the illegal combination — the violation is detected |
Rows 7 and 9 are the ones this chapter added to the module's standard plan. Row 7 catches shortcuts that reach the right state early; row 9 exists because §12 showed that an unstated assumption can make two designs indistinguishable.
17. Design Review
- Is every traversal of device-supplied data bounded, and are the bound overrun and the clean end reported separately?
- Is a "one instance of" rule implemented as an existence check?
- Is any structure with a revision field read without checking the revision?
- Does the absent-map, absent-structure, absent-anything default claim more than the specification guarantees?
- Is a mandatory element checked before any optional element is acted on?
- Is each step of an ordered read gated explicitly, or does it work because the read is fast?
- Do any two counters double-count because a reason implies its outcome?
- Which input assumptions are load-bearing, and which are written down?
18. How This Appears in Real Engineering
Enumeration hangs get blamed on the bridge. Debug Lab 1's symptom is that devices behind a bridge never appear, so investigation starts at the bridge. The cause is a single downstream device with a malformed capability list, and the fastest discriminator is whether removing that one card fixes everything.
Multi-function devices are where function-map bugs live. A single-function device never exercises Debug Lab 2, so the bug ships and surfaces on the first product with a management function alongside a memory function.
Early silicon produces every malformed case in this chapter. Duplicate structures, wrong revisions, .cache without .io, garbage capability pointers — all are ordinary bring-up conditions rather than exotic attacks. A host that assumes well-formed devices does most of its debugging on devices that are not.
Discovery bugs present as driver bugs. Because the link is genuinely up and healthy, everything points at software. The distinguishing question is whether the values written to the device correspond to anything it advertised — which requires logging both, and is rarely done until someone has spent a week on it.
19. Common Misconceptions
| Claim | Why it is wrong |
|---|---|
| "Discovery is how CXL is negotiated" | Negotiation happens in hardware during bring-up, in modified TS ordered sets (5.3). Discovery is a read by software afterwards. Two mechanisms, two times, two parties — see §5. |
| "If the DVSEC is present, the device is CXL" | Presence is not validity. Public material requires Revision 1 or greater, and exactly one instance on the primary function. |
| "At least one DVSEC ID0 is fine" | The rule is one. Duplicates make host behaviour depend on traversal order, so two correct hosts can configure the same device differently. |
| "Every function on a CXL device is a CXL function" | A Non-CXL Function Map DVSEC advertises which are not, and with no map the only function guaranteed to carry the DVSEC is the primary one. |
| "A device advertising .cache is a coherent device" | Not without .io. CXL.io is mandatory; an advertisement without it describes a device that cannot be configured at all. |
| "The capability list ends when the device says so" | Only if the device is well-formed. The walk needs a step bound, because reading the list is the only way to check it. |
| "A conservation law proves the counts are right" | It proves the parts are consistent with each other. Proving any part correct needs a reference computed from the stimulus, not from the design. |
| "A CXL-capable host is required to see a CXL device" | No — the device presents a Type 0 header and enumerates on any PCIe host. That is the reuse Module 5 is built on. |
20. Interview Reasoning
21. Exercises
-
Trace. A device presents a capability list of six entries where entry 4 is a DVSEC at revision 0 and entry 6 points back to entry 2. With
MAX_STEPS = 8, give the step count and terminal state, and state which of the two give-up reasons applies. Then repeat with the revision-0 entry corrected. -
Calculate. Of the eight
.io/.cache/.memcombinations, how many are well-formed? For a test plan that samples only well-formed advertisements, state which two mutations in §14 would survive and why. -
DV task. Write the check that catches a state machine advancing when it should not, and explain why an assertion over transitions cannot express it. Then give the equivalent SVA.
-
Debug task. A host hangs during enumeration on one of forty machines. You may capture one thing. Choose it, and give the decision tree from what you see to a root cause, distinguishing a malformed list from a bridge fault.
-
Design. Extend §12's counters with a fourth reason — "duplicate instances" — and state whether it enters the conservation sum, what input assumption it introduces, and how you would test that assumption is enforced.
-
Critique. Argue that the absent-function-map default should be "all functions are CXL". Give the strongest case, identify the device class that breaks it, and say which diagnostic would have revealed the mistake in the field.
22. Summary
Discovery is a read, not a negotiation, and every failure in it is a failure to read correctly.
- Three separate questions: present, valid, ordered. A structure can be present and too old, or valid and read at the wrong time.
- The primary function must carry exactly one CXL DVSEC ID0 at Revision 1 or greater. "Exactly one" fails in both directions and only zero looks like a failure.
- CXL.io is mandatory, so four of eight protocol advertisements are malformed — and those four are the half a natural test plan omits.
- A missing function map is the absence of information, not permission. The safe default is the minimum the specification guarantees.
- The capability walk must be bounded, because the list comes from the device and reading it is the only way to check it. Report the bound overrun separately from a clean end of list.
- Outcomes partition; reasons annotate — and a conservation law proves the parts consistent, never any part correct.
- Verification: five distinct causes make a mutation survive, and only two are testbench problems.
23. Module 5 Complete
Module 5 asked why CXL rides on PCIe and what the machinery that exploits that reuse actually has to do.
- 5.1 — the reuse buys zero dead pairs, not more coherent links, and compatibility is a design constraint expressed at reset, at link-up, and at error.
- 5.2 — "reuse" is four contracts, and the class of a structure describes what you still owe it. Only 3 of 13 structures can honestly be skipped on a clean PCIe regression.
- 5.3 — the protocol agreement uses PCIe's own alternate protocol negotiation in modified TS ordered sets at 2.5 GT/s, producing the intersection of what both ends offered and what the path can carry.
- 5.4 — training resolves to an operating point, not a status; degraded is an outcome, and recovery must narrow and then conclude.
- 5.5 — software learns all of it by a bounded read of PCIe's capability list to a CXL structure that must appear exactly once.
The thread through all five: the container is PCIe's and the content is CXL's. That is what made CXL deployable, and it is also why its characteristic bug is a well-formed read of the wrong thing — a shared container makes a wrong read return a plausible value instead of an error.
Three mechanisms in this module bound something outside the design's control — a partner that may never answer, a channel that may never recover, a list that may never end. Each converts a liveness property that cannot be proven into a safety property that can. That is the single most transferable idea in Module 5, and it has nothing to do with CXL.
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.
