CXL · Module 10
Device-Type Selection Discipline
Selecting a device type is selecting a mode space: a two-engine device is four devices, each with its own obligations. Negotiation that can only reduce, disables that must quiesce, and state that must not outlive its protocol. Seven RTL models, twenty-six mutations, twenty-six killed.
10.1, 10.2 and 10.3 built the three device types and showed what each obliges. This chapter closes Module 10 with the question those three raise and none of them answers.
It is not which type should I build. 6.4 and 6.5 answered that from requirements, before any silicon existed. This is the question that appears after the answer:
What is this device when part of it is turned off?
1. The Engineering Problem — A Device That Ships as Four Devices
A team builds a Type 2 accelerator. It is verified as a Type 2, characterised as a Type 2, and documented as a Type 2.
Then a platform ships it with .mem disabled, because that platform has no use for the device's memory and the firmware team saw a way to save a configuration step. Nobody made a decision; a bit was left clear.
That configuration is a different device. It behaves as a Type 1 — the cache engine is still present, still holding host lines, still carrying every eviction obligation from 10.1. It was never verified in that mode, its obligations were never checked in that mode, and its telemetry attributes all of its uptime to a type it is not currently being.
The device did not fail. It was correct as a Type 2 and is now running as something nobody tested.
2. The One-Sentence Model
You do not select a device type once; you select every mode the device can enter. A device with two optional engines is four devices, each with a distinct effective type and a distinct set of obligations — and the ones nobody chose are the ones nobody verified.
Call it the mode space. The discipline is not choosing well at the start. It is knowing what you have chosen, in every configuration the device can reach.
3. What This Chapter Owns
This is the fourth chapter in this curriculum to touch selection, so the boundaries need stating precisely.
| Ground | Owner |
|---|---|
| Deriving a protocol set from requirements | 6.4 |
| The pre-silicon selection sequence and its cost model | 6.5 |
| What each type obliges | 10.1 · 10.2 · 10.3 |
| The mode space: what the device becomes when configured | this chapter |
6.5 established that three states must stay distinct — what silicon contains, what the link negotiated, what the platform enabled. It made that a discipline. This chapter makes it hardware, and then asks the question that follows from it: if those three can differ, the device has a space of configurations, and every point in it is a device somebody must have verified.
4. Four Devices, One Die
Architectural. The arrows are configuration relationships, not signals.
Only one of those four boxes is usually verified. The device was designed as a Type 2, so the "both" mode got the test plan, the characterisation and the documentation. The other three are reachable, are different devices, and were reached for the first time in a customer's rack.
5. Teaching-model boundary
6. RTL 1 — A Two-Engine Device Is Four Devices
// Mode index: {mem_en, cache_en}. Only combinations the silicon supports
// are reachable at all.
wire [1:0] mode = {mem_en, cache_en};
assign reachable_modes = ONLY_SHIPPED ? 3'd1
: (HAS_CACHE && HAS_MEM) ? 3'd4
: (HAS_CACHE || HAS_MEM) ? 3'd2 : 3'd1;=== EXP1: a two-engine device is FOUR devices ===
reachable modes : both engines=4 cache only=2 mem only=2
silicon with both engines can enter four modes : ok
neither enabled: io-only, not a CXL device type : ok
cache only: it BEHAVES as Type 1 : ok
memory only: it BEHAVES as Type 3 : ok
both: Type 2 : ok
visited=1111 changes=3 coverage complete=1
all four modes were entered : ok
5 steady cycles in one mode : changes went 3 -> 4
a steady mode added exactly one change, not one per cycle : ok
same enables applied to single-engine silicon : cache-only flag=1 mem-only flag=1
both single-engine devices caught being asked for an impossible mode : okFour modes, and the middle two are the interesting ones. A Type 2 running with .mem off is not a Type 1 — it behaves as one, which is a different and more dangerous statement. Its silicon still contains a memory engine; only its obligations have changed.
Note the last line. The same platform request — enable both — was applied to three different silicon configurations. It is legal for the two-engine device and illegal for both single-engine devices, and the checker caught each of them being asked for a mode it cannot support. That is one enable sequence and three different correct answers, which is exactly why a single as-shipped test proves so little.
Coverage is status, not a violation
The first version of this module latched an error when a reachable mode had not yet been entered — and it fired immediately, because at reset no mode has been entered. That is a coverage property, not a safety property, and expressing it as an error made a correct device fail its own regression from the first cycle.
The corrected design splits them:
// Entering a mode this silicon cannot support is a real violation;
// not having entered one YET is merely coverage, and is reported as
// status rather than latched as an error.
if (!all_modes[mode]) unreachable_mode_err <= 1'b1;coverage_complete became a status output the testbench checks at the end. Confusing a coverage goal with a safety property is the same category error as 9.6's warning about performance goals in assertions — and it produces the same outcome, a team that learns to ignore a failing check.
7. RTL 2 — Negotiation Reduces, and Can Never Add
// Each stage is an intersection, never a union.
assign negotiated = NEGOTIATE_UP ? link_offers : (link_offers & CONTAINS);
assign enabled = platform_wants & negotiated;=== EXP2: negotiation reduces, and can never add ===
contains 11, link offers 11, platform wants 11 : negotiated=11 enabled=11
contains 01 (cache only), link offers 11 : negotiated=01 enabled=01 | up-variant negotiated=11
negotiation intersected with what the silicon contains : ok
the negotiate-up variant granted memory it does not have : ok
and was caught adding a capability : ok
platform wants 01 only : enabled=01
the platform reduced it further : ok6.5's three states, as hardware. Each stage is an intersection: silicon is a ceiling, negotiation may only reduce it, enablement may only reduce that again.
The NEGOTIATE_UP variant is the failure worth naming, because it is the shape a real bug takes. Nobody writes "grant capabilities we do not have"; they write a union instead of an intersection, or they take the link's offer as authoritative because the link is the thing doing the negotiating. The result is a device advertising a memory engine it does not contain, which is 10.1's declared-type defect arriving through a different door.
The monotonic property is what makes this checkable in one line: at no stage may a capability appear that the previous stage lacked.
Each arrow narrows the set; none of them may widen it. The failure in the last two rows is never written deliberately — it appears as a union where an intersection belonged, or as treating the link's offer as authoritative because the link is the thing doing the negotiating.
8. RTL 3 — Turning a Protocol Off Is Not the Inverse of Turning It On
wire quiesced = (held_lines == 8'd0);
assign disable_done = draining_q && (DISABLE_IMMEDIATE || quiesced);=== EXP3: turning a protocol off is not the inverse of turning it on ===
disable requested, 3 lines still held : done=0 protocol on=1 wait=4 | immediate on=0
the protocol is still on and the disable has not completed : ok
it has been quiescing for exactly 4 cycles : ok
the immediate variant switched the protocol off at once : ok
and was caught disabling while it still held lines : ok
quiesced : protocol on=0 max quiesce wait=4
the protocol went off only after the lines were returned : okEnabling a protocol is a bit. Disabling one is a sequence.
The asymmetry is total and it is the most-missed fact in this chapter. Enabling .cache gives the device permission to start holding host lines — nothing is owed to anyone at that moment. Disabling .cache while it holds three modified lines destroys the only copy of that data, exactly as 10.1's silent eviction does, but at configuration time where nobody is looking for it.
This is the third appearance of the same structure in this batch — 9.6's fence, 10.2's ownership handover, and now a protocol disable. All three are barriers whose cost is the state they must drain, and all three have a fast, wrong variant that skips the drain.
9. Waveform — Quiesce, Then Disable, Then Clear
A disable that takes four cycles to become a disable
10 cyclesTeaching-model timing derived from the simplified RTL in this chapter. Not CXL wire timing.
Three cycles separate the request from the effect, and the ordering is the lesson. disable_req pulses at c4 and protocol_on is still high through c7. The lines are returned at c7, the disable completes, the protocol goes off at c8, and only at c9 do the state entries clear.
Every one of those steps is required and they cannot be reordered. Clearing the state before quiescing loses it; disabling before quiescing loses it; leaving the state after disabling is §10's defect. A configuration write that appears instantaneous to software is a multi-cycle obligation in hardware.
10. RTL 4 — State Must Not Outlive Its Protocol
if (falling && CLEAR_ON_DISABLE) entries_q <= 8'd0;
else if (install && proto_en) entries_q <= entries_q + 8'd1;
...
if (probe_hit && !proto_en) begin
n_residual_hit_q <= n_residual_hit_q + 16'd1;
residual_err <= 1'b1;
end=== EXP4: state must not outlive the protocol it belongs to ===
protocol on, 3 entries installed : entries=3 present=1
protocol off : correct entries=0 | leave-behind variant entries=3
the correct design cleared its state on disable : ok
the leave-behind variant kept all three : ok
probe while protocol off : correct hit=0 | variant hit=1 residual flag=1
the variant answered from state belonging to a disabled protocol : ok
install attempted while protocol off : entries=0
no state was recorded while the protocol was disabled : okResidual state is the defect that makes a disabled protocol act enabled — once, at the worst possible moment.
The engine is off. Software believes it is off. Then something consults a table that still has entries in it, gets a hit, and the device behaves as though the protocol were running. It happens exactly once per stale entry, it is not reproducible without recreating the enable/disable history, and it looks like a completely unrelated fault.
Both directions matter and only one is obvious. Clearing on disable is the one people implement. Refusing to install while the protocol is off is the one they forget, and it produces the same stale entries by the other route.
11. RTL 5 — Obligations Follow the Mode, Not the Silicon
This is the chapter's central mechanism, and the one that would have prevented §1's scenario.
wire [1:0] sel = USE_SILICON ? SILICON_TYPE : effective_type;
// Type 1 must notify evictions and refuse host memory.
// Type 2 must notify evictions and check permission on its own memory.
// Type 3 must do neither -- it has no cache at all.
assign obligations = (sel == 2'd1) ? 3'b011 :
(sel == 2'd2) ? 3'b101 :
(sel == 2'd3) ? 3'b000 : 3'b000;=== EXP5: obligations come from the CURRENT mode, not the silicon ===
effective Type 2 : obligations=101 (permission-check, refuse-mem, evict-notify)
effective Type 1 : obligations=011
a Type 1 must refuse host memory and notify evictions : ok
effective Type 3 : obligations=000
a Type 3 has neither obligation -- it has no cache : ok
degraded to Type 1, host memory SERVED : correct flag=1 | silicon-typed flag=0
the mode-driven matrix caught the missed obligation : ok
the silicon-typed variant did not even look for it : ok
eviction with NO notification, effective Type 1 : flag=1Read the obligation sets: they are not nested. A Type 1 must refuse host-memory accesses and a Type 2 must not — a Type 2 serves them. So a Type 2 degraded to Type 1 acquires an obligation it did not have when it was verified, and the correct behaviour in one mode is a violation in the other.
That is why "we tested the superset" is wrong here. There is no superset. The modes have overlapping but distinct obligation sets, and testing the richest configuration does not cover the poorer ones.
The USE_SILICON variant is §1's bug, in nine characters. It selects obligations from the silicon's type rather than the current mode, so a degraded Type 2 is still checked as a Type 2 — and the missed refusal is not merely unreported, it is not looked for. The correct matrix caught it; the silicon-typed one had no opinion.
12. RTL 6 — Choosing a Type Is Choosing a Verification Surface
=== EXP6: choosing a type is choosing a verification surface ===
single-engine device : modes=2 crosses=0 cost=6 additive=6 gap=0
two-engine device : modes=4 crosses=4 cost=28 additive=12 gap=16 | ignore-cross cost=12
four modes and four interactions cost 28 : ok
16 of that is interaction the additive view cannot see : ok
the additive variant reports 12 -- less than half : okSix versus twenty-eight, and the additive view sees twelve. More than half the verification surface of a two-engine device is interaction that neither engine's own plan contains — 10.2 named the four crosses concretely: local access against host-holds, both directions pending, a handover with work in flight, and an identity number live in both roles.
This refines 6.5's cost model rather than repeating it. 6.5 priced protocols pre-silicon and found a 9-unit interaction term on 41. This prices modes and their crosses, which is the post-silicon surface, and finds the interaction is the majority of it. The two agree on the shape and disagree on the magnitude for a good reason: 6.5 counted the engines, and this counts the configurations they can be in.
13. RTL 7 — How Long Was It Each Thing?
=== EXP7: how long was it each thing? ===
time as type1=15 type2=10 type3=15 changes=2 active=40
oracle: type1=15 type2=10 type3=15 changes=2
time in each effective type matched an independent oracle : ok
the device really did spend time as something other than Type 2 : ok
as-shipped variant : type2 time=40 type1 time=0
the as-shipped variant attributed everything to Type 2 : okThe device spent 25 of 40 cycles as something other than what it shipped as, and the as-shipped variant reports 40 out of 40 as Type 2.
That variant is not a strawman — it is what almost every device does, because telemetry is written once, against the device's nameplate type. A field investigation using it will attribute every failure to the wrong configuration, and the mode the device was actually in when it failed does not appear anywhere in the data.
Time-in-mode is the counter that makes a degraded configuration visible from the field. Without it, the only evidence that a device ever ran as something else is the absence of an explanation.
14. Quantitative Reasoning
Illustrative, with stated assumptions and teaching units.
The mode space grows exponentially
modes = 2 ^ (optional engines)| Optional engines | Modes | Effective types reachable |
|---|---|---|
| 0 | 1 | one |
| 1 | 2 | io-only, and one type |
| 2 | 4 | io-only, Type 1, Type 3, Type 2 |
Two engines is four devices. The exponent is what makes this worth a chapter: the design added one engine and doubled the number of devices somebody has to verify.
What the surface actually costs
Using §12's teaching units of 3 per mode and 4 per cross:
one engine : 2 modes x 3 + 0 crosses x 4 = 6
two engines : 4 modes x 3 + 4 crosses x 4 = 28A 4.7× increase in verification surface for one additional engine, of which 16 of 28 — 57% — is interaction that neither engine's plan covers. An additive estimate gives 12 and is wrong by more than a factor of two, always in the optimistic direction.
The cost of an unverified mode
If a two-engine device verifies only its as-shipped mode, it has covered 1 of 4 modes and 0 of 4 crosses:
covered : 3 of 28 units = 11%
unverified: 25 of 28 units = 89%That is the honest number behind §1. The device was not lightly tested; it was thoroughly tested in one of four configurations, and the configuration a customer selected was among the other three.
What a disable costs
From §8, the quiesce took 4 cycles for 3 held lines. Scaled with 9.6's 125-outstanding figure, disabling a protocol at full occupancy costs roughly a full round trip during which the device serves nothing — and unlike a fence, it happens under a configuration write that software expects to be instantaneous.
A disable is not a register write; it is a barrier with a register write at the end of it.
15. Assertions
Icarus Verilog 13.0 does not support concurrent SVA here, so every property is synthesisable checker logic verified in simulation. 53 assertions.
Safety
| Property | Intent |
|---|---|
| Mode legality | never enter a mode the silicon cannot support |
| Monotonic negotiation | no stage grants what the previous stage lacked |
| Quiesce before disable | a protocol goes off only when it owns no state |
| No residual state | state does not outlive its protocol, in either direction |
| Obligations follow the mode | the current effective type selects the obligation set |
| Surface not understated | a device with interactions is not costed as additive |
| Time conservation | attributed time never exceeds active time |
Liveness
| Property | Assumption it needs |
|---|---|
| A requested disable eventually completes | held state is eventually returned |
| A quiescing protocol eventually goes off | the drain makes progress |
Coverage — status, not assertions
| Goal | Reported by |
|---|---|
| Every reachable mode entered | coverage_complete |
| Every mode's obligations exercised | per-mode obligation checks |
| Time observed in more than one mode | time-in-type counters |
The third table is the one this chapter argues about. These are goals, reported as status and checked at end of test — not latched errors. Expressing "a mode has not been entered yet" as a violation makes a correct device fail from its first cycle, and §6 describes exactly that mistake being made and corrected.
16. Mutation Testing
Twenty-six mutations. Twenty-six killed.
| Mutation | Result |
|---|---|
| Every mode assumed reachable on any silicon | killed |
| Unsupported mode entry not reported | killed |
| Two-engine mode space under-counted | killed |
| Every mode recorded as mode zero | killed |
| Mode changes counted when nothing changed | killed |
| Negotiation unions instead of intersecting | killed |
| Platform enables past what was negotiated | killed |
| Capability growth not reported | killed |
| Protocol disabled without quiescing | killed |
| Protocol never actually turns off | killed |
| Worst quiesce wait under-reported by one | killed |
| Dirty disable not reported | killed |
| State never cleared on disable | killed |
| A probe hits whether state exists or not | killed |
| Residual-state hit not reported | killed |
| State installed while the protocol is off | killed |
| Obligations taken from the silicon type | killed |
| A Type 1 no longer required to refuse host memory | killed |
| Missed eviction notification not reported | killed |
| Missed refusal obligation not reported | killed |
| Interaction cost dropped entirely | killed |
| Understated verification surface not reported | killed |
| Interaction gap always reported as zero | killed |
| All time attributed to the as-shipped type | killed |
| Every active cycle counted as a mode change | killed |
| Time-attribution conservation law disabled | killed |
The first run scored 21 of 26, and one escape taught something the previous three chapters had not.
A mutation that escaped by coincidence
Inverting the mode-change condition from != to == should be trivially detectable. It escaped — and when I ran it in isolation, the mutated design reported exactly the same count as the correct one, 3.
It was not equivalent; the two designs count entirely different things. My stimulus happened to produce the same total from both. A single number matched by accident, and the assertion could not tell the difference.
The fix was a stimulus that makes the two diverge structurally rather than numerically — hold one mode steady and assert the counter adds exactly one:
// Hold one mode steady: a change counter must count TRANSITIONS, so a
// steady mode must add exactly one (the transition into it) and no more.
nc_before = ms_nc;
ms_ce=0; ms_me=0; repeat(5) step;
chk((ms_nc - nc_before) == 16'd1, "a steady mode added exactly one change, not one per cycle");The general lesson is uncomfortable: a passing assertion on a mutated design is not always evidence the assertion is weak. Sometimes it is evidence the stimulus produced a coincidence, and the only defence is a test whose result differs by construction rather than by arithmetic.
The other four
One flag was already set by an earlier violation. The missed-eviction mutation escaped because the missed-refusal test — a different obligation entirely — had already latched the same instance's error flag. Splitting it onto its own instance fixed it, and the lesson generalises: a shared sticky flag makes every check after the first one vacuous.
Two were untested paths: installing state while the protocol is off, and holding a mode steady.
One law was unreachable by construction, and the honest fix was to make it reachable rather than to delete it — an ATTRIBUTE_ALWAYS parameter used only on an abuse instance, which attributes time on every cycle while activity is asserted on only some.
17. Verification Plan
| Area | Approach |
|---|---|
| Mode space | Walk every mode; steady-state hold; single-engine silicon driven with the same enables |
| Negotiation | Full and reduced silicon; a negotiate-up variant |
| Disable | Request with state held, then quiesced; a disable-immediate variant |
| Residual state | Clear on disable; probe while off; install while off; a leave-behind variant |
| Obligations | All three effective types; a violation per obligation on separate instances; a silicon-typed variant |
| Surface | Single-engine and two-engine; an additive variant |
| Telemetry | Independent oracle; an as-shipped variant; a conservation abuse instance |
The coverage cross is mode × transition direction × state held: four modes, entering and leaving, with and without outstanding state. The leaving a mode with state held points are where every defect in this chapter lives, and none of them is reachable by a test that configures the device once at boot.
18. Silicon Observability
| Counter | Diagnoses |
|---|---|
| time in each effective type | what the device actually was, in the field |
| mode changes | whether it is being reconfigured at all |
| current effective type | which obligation set applies right now |
| quiesce waits and maximum | what a disable costs |
| negotiation reductions | silicon, link and platform disagreeing |
| modes entered | coverage, from the field rather than the lab |
unreachable_mode | correctness alarm — must be zero forever |
grew_capability | correctness alarm — must be zero forever |
disabled_dirty | correctness alarm — must be zero forever |
residual_hit | correctness alarm — must be zero forever |
obligation_missed | correctness alarm — must be zero forever |
The first row is the one this chapter exists to argue for. A device that reports only its nameplate type produces field data in which a degraded configuration is invisible, and every failure in it is attributed to a configuration the device was not in.
modes_entered from the field is the counter nobody builds and everybody needs. It answers a question no lab can: which of the configurations you shipped are customers actually using? If three of four modes are in use and one of four was verified, that is a fact worth having before the escalation rather than after.
19. Debug Lab
Verified as a Type 2, deployed as something else
UNVERIFIED-MODE// test plan: configure both engines, run the Type 2 suiteA device passes a thorough qualification and fails at one customer with coherence errors on host lines. The configuration differs from the lab only in a single disabled capability, and the device is behaving exactly as designed — for a mode nobody ran.
reachable modes : both engines=4 cache only=2 mem only=2
all four modes were entered : okEnumerate the reachable modes and compare against the modes the test plan covers. With two optional engines there are four; a plan naming one has covered 11% of the verification surface by §14's units.
Verification targeted the as-shipped configuration. Every other reachable mode is a different device with a different obligation set, and the first time three of them ran was in the field.
Enumerate the mode space in RTL, expose coverage_complete, and make the regression walk every mode. Then instrument time-in-mode so the field reports which ones are actually used.
Treat "which modes can this silicon enter?" as a required review question. The count is 2^optional_engines and it is never one.
A device advertising an engine it does not contain
NEGOTIATE-UPassign negotiated = link_offers; // the link decidesEnumeration succeeds and the device presents a memory capability. Software enables it. Accesses to the advertised memory return nothing coherent, and the investigation goes to the host and the switch because the device reported itself confidently.
contains 01 (cache only), link offers 11 : negotiated=01 | up-variant negotiated=11
the negotiate-up variant granted memory it does not have : ok
and was caught adding a capability : okCheck that each stage is a subset of the previous one. negotiated & ~contains must be zero, and enabled & ~negotiated must be zero.
Negotiation was treated as authoritative rather than as a reduction. Nobody writes "grant what we lack" — they write a union instead of an intersection, or take the link's offer directly because the link is what negotiates.
assign negotiated = link_offers & CONTAINS;
assign enabled = platform_wants & negotiated;Every stage is an intersection, and the monotonic property is asserted continuously.
Test with silicon that lacks a capability the link offers. Testing only fully-populated silicon leaves the intersection unverified.
A configuration write that destroyed data
DISABLE-WITHOUT-QUIESCEif (disable_req) proto_on_q <= 1'b0; // it's a config bitData written by the device is missing after a runtime reconfiguration. No error is reported. It correlates with how much work was in flight when the capability was disabled, so a quiescent-system test never sees it.
disable requested, 3 lines still held : done=0 protocol on=1 wait=4 | immediate on=0
the immediate variant switched the protocol off at once : ok
and was caught disabling while it still held lines : okCheck whether any coherent state was held at the moment the protocol went off. It must be zero.
Enabling is a bit; disabling is a sequence. Turning .cache off while it holds modified lines destroys the only copy of that data — 10.1's silent eviction, arriving through a configuration write instead of a capacity conflict.
Third appearance of this structure in the batch, after 9.6's fence and 10.2's handover: a barrier whose cost is the state it drains, with a fast wrong variant that skips it.
wire quiesced = (held_lines == 8'd0);
assign disable_done = draining_q && quiesced;
if (disable_done && !quiesced) disabled_dirty_err <= 1'b1;Request the disable with state deliberately held and assert the protocol stays on until it drains. A disable on an idle device passes on both designs.
A disabled engine that answered once
RESIDUAL-STATE// protocol disabled -- stop using the table, no need to clear itA one-off inexplicable behaviour after a reconfiguration: the device acts as though a disabled protocol were running, exactly once. Not reproducible without recreating the enable/disable history, so it is written off as a glitch.
probe while protocol off : correct hit=0 | variant hit=1 residual flag=1
the variant answered from state belonging to a disabled protocol : ok
install attempted while protocol off : entries=0Probe the engine's structures with the protocol off. Any hit means state outlived its protocol.
State was left behind on disable. Something later consulted it, got a hit, and the device behaved as though the engine were on — once per stale entry.
Both directions are needed and only one is obvious: clear on disable, and refuse to install while off. The second produces identical stale entries by another route and is routinely omitted.
if (falling && CLEAR_ON_DISABLE) entries_q <= 8'd0;
else if (install && proto_en) entries_q <= entries_q + 8'd1;
if (probe_hit && !proto_en) residual_err <= 1'b1;Assert every disabled engine's structures are empty, continuously. A one-shot check after disable misses state installed afterwards.
The test suite did not even look for the violation
SILICON-TYPED-OBLIGATIONSwire [1:0] sel = SILICON_TYPE; // we're a Type 2 deviceA degraded configuration passes its checks and violates an obligation in the field. The regression reports no failure — not because the violation was tolerated, but because nothing was watching for it.
degraded to Type 1, host memory SERVED : correct flag=1 | silicon-typed flag=0
the mode-driven matrix caught the missed obligation : ok
the silicon-typed variant did not even look for it : okCompare the obligation set the device is enforcing against the one its current effective type requires. If the two differ, the enforcement is keyed to the wrong thing.
Obligations were selected from the silicon's type rather than the current mode. The sets are not nested: a Type 1 must refuse host-memory accesses and a Type 2 must serve them, so a degraded Type 2 acquires an obligation it never had.
"We tested the superset" is therefore wrong — there is no superset.
wire [1:0] sel = effective_type;Derive obligations from the enable state, and select the test suite the same way.
Write the obligation matrix as a table indexed by effective type, and make both the RTL and the test plan read from it. Two sources of truth will drift.
Every failure attributed to a configuration the device was not in
AS-SHIPPED-TELEMETRY// this is a Type 2 device
time_as_type2_q <= time_as_type2_q + 1;Field data shows a device operating as a Type 2 for 100% of its uptime. Failures do not correlate with anything. The mode the device was actually in when it failed appears nowhere in the data.
time as type1=15 type2=10 type3=15 changes=2 active=40
as-shipped variant : type2 time=40 type1 time=0
the as-shipped variant attributed everything to Type 2 : okCheck whether time-in-mode is indexed by the current effective type or by a constant. A device reporting 100% in one mode across a fleet is reporting its nameplate, not its behaviour.
Telemetry was written once against the device's nameplate type. In the measured run the device spent 25 of 40 cycles as something else, and the counter attributed all 40 to Type 2.
time_in_type_q[effective_type] <= time_in_type_q[effective_type] + 16'd1;Index by the current effective type, and add a mode-change counter alongside.
Conservation: total time attributed must equal time active. A device reporting more time in one mode than it was active is miscounting somewhere.
A verification estimate wrong by more than double
ADDITIVE-SURFACE// effort = cache plan + memory plan
assign total_cost = additive_cost;A two-engine programme's verification schedule is set from the sum of two single-engine plans, and overruns by more than a factor of two. The overrun is entirely in bugs that require both engines active.
two-engine device : modes=4 crosses=4 cost=28 additive=12 gap=16
16 of that is interaction the additive view cannot see : ok
the additive variant reports 12 -- less than half : okCount the pairwise interactions, not just the engines. 10.2 names four for a Type 2, and none of them appears in either engine's own plan.
The verification surface was estimated additively. Modes multiply and interactions are a separate term, so 57% of the surface was invisible to the estimate — always in the optimistic direction.
assign total_cost = modes * MODE_COST + crosses * CROSS_COST;
if ((crosses != 0) && (total_cost == additive_cost)) understated_err <= 1'b1;Estimate modes and crosses separately, and require the cross count to be stated explicitly.
Make "list the pairwise interactions" a required output of planning. An empty list on a two-engine device is a finding, not an estimate.
A correct device failed its own regression from the first cycle
COVERAGE-AS-ERRORif ((visited_q | ~all_modes) != 4'hF) unvisited_shipped_err <= 1'b1;The regression fails immediately on a design that is behaving correctly. The error asserts at reset and stays asserted until every mode has been walked, so it is red for most of every run.
visited=1111 changes=3 coverage complete=1
every reachable mode was entered -- coverage is complete : okAsk whether the property is "the device did something wrong" or "the test has not finished exercising it". At reset no mode has been entered, and that is not a defect.
A coverage goal was latched as a safety error. The two have different lifetimes: a safety property must hold at every instant, and a coverage goal is evaluated once at the end.
Same category error as putting a performance goal in an assertion, which 9.6 warns about — and the same outcome, a team that learns to ignore a failing check.
assign coverage_complete = ((visited_q | ~all_modes) == 4'hF); // status
if (!all_modes[mode]) unreachable_mode_err <= 1'b1; // safetySplit them: coverage becomes a status output checked at end of test; the safety property is entering a mode the silicon cannot support.
For every check ask when it must hold. "At every instant" is safety; "by the end" is coverage; "usually" is a performance goal. Mixing the three degrades the credibility of all of them.
20. Design Review
- How many modes can this silicon enter, and which are verified?
- Is the effective type derived from the enable state, or stored?
- Can any stage grant a capability the previous stage lacked?
- What must quiesce before each protocol can be disabled?
- What is the worst-case quiesce time, and what waits on it?
- Are disabled engines' structures empty, and can anything install into them?
- Are obligations selected by current mode or by silicon type?
- Which obligations differ between the modes this device can enter?
- How many pairwise interactions does this device have, and are they in the plan?
- Does telemetry report the current effective type or the nameplate?
- Which checks are safety, which are coverage, and which are performance goals?
21. How This Appears in Real Engineering
Architecture. The mode count is 2^optional_engines, and it is a required output of the type decision rather than an afterthought.
RTL. Effective type derived, negotiation as intersection, quiesce before disable, and no structure that outlives its protocol.
DV. The plan is indexed by mode, not by device. The crosses are enumerated explicitly, and an empty cross list on a two-engine device is a finding.
Firmware. Every enable combination it can produce is a device somebody must have verified — and a disable is a sequence with a wait, not a register write.
Post-silicon. Time-in-mode and modes-entered are the two counters that make field configurations visible; without them a degraded device is indistinguishable from a nominal one in the data.
Programme management. The verification surface is modes times cost plus crosses times cost. The additive estimate is optimistic by more than half.
22. Common Misconceptions
| Belief | Correction |
|---|---|
| We picked a device type | You picked a mode space; the type varies with configuration |
A Type 2 with .mem off is a Type 1 | It behaves as one; its silicon and its history differ |
| We tested the superset | The obligation sets are not nested — there is no superset |
| Disabling is the inverse of enabling | Enabling is a bit; disabling is a barrier |
| A disabled engine is inert | Its residual state can answer once, at the worst moment |
| Negotiation decides capabilities | It can only reduce what the silicon contains |
| Verification effort is the sum of the engines | Modes multiply and interactions are a separate 57% |
| Telemetry should report the device type | It must report the current effective type |
| A mutation that escapes means a weak assertion | Sometimes the stimulus produced a coincidence |
23. Interview Reasoning
24. Exercises
-
Calculation. A device has three optional engines. Compute the number of reachable modes and the number of pairwise interactions. Using §12's units of 3 per mode and 4 per cross, compute the total surface and the fraction an additive estimate would miss.
-
Analysis. A fleet reports every device operating as its nameplate type 100% of the time, and failures correlate with nothing. Explain what that data cannot distinguish, name the two counters that would resolve it, and state what you would expect them to show.
-
RTL task. Extend
quiesce_gateto handle two independent protocols with separate held-state counts, where either may be disabled while the other stays on. State the new invariant this creates, and the failure mode if one protocol's quiesce is allowed to observe the other's state. -
Assertion task. Write the property that proves capability negotiation is monotonic across all three stages. Then explain why testing it only on fully-populated silicon leaves it unverified, and what silicon configuration the test actually needs.
-
Waveform analysis. Using §9's trace, identify the exact cycles of each phase — request, quiesce, disable, clear — and state what would go wrong if the clear happened one cycle earlier. Then describe what the trace would look like if the held count never reached zero, and which counter would report it.
-
Debug task. A device behaves correctly for months, then exhibits a single unexplained coherence event immediately after a reconfiguration, and never repeats it. Give your investigation order across this chapter's five alarms, and the single test that reproduces it deterministically.
-
Design review. A colleague proposes shipping a Type 2 device with a firmware option to disable
.mem, arguing it adds flexibility at zero hardware cost. Give the strongest version of that argument, then state what it commits the programme to in verification surface, what obligations change in the degraded mode, and what telemetry must exist before it can be supported in the field.
25. Summary
You do not select a device type once; you select every mode the device can enter.
- A device with two optional engines is four devices —
2^optional_engines— each with a distinct effective type and a distinct obligation set. - The obligation sets are not nested. A Type 1 must refuse host-memory accesses and a Type 2 must serve them, so "we tested the superset" is not available.
- Negotiation reduces and can never add. Silicon is a ceiling, negotiation intersects it, enablement intersects that — and the monotonic property is one line to assert.
- Enabling is a bit; disabling is a barrier. The measured quiesce took 4 cycles for 3 held lines, and disabling without it destroys data under a configuration write nobody watches.
- State must not outlive its protocol — in both directions. Clear on disable and refuse to install while off, or a disabled engine answers once, at the worst moment.
- Obligations follow the current mode, not the silicon type. The silicon-typed variant did not merely tolerate a violation; it never looked for one.
- 57% of a two-engine device's verification surface is interaction — 16 units of 28 — and an additive estimate is optimistic by more than half.
- Verifying only the as-shipped mode covers about 11% of that surface.
- Telemetry must report the current effective type. The device spent 25 of 40 cycles as something other than its nameplate, and the as-shipped counter reported 40 of 40.
- Verification: 26 of 26 mutations killed, 53 assertions. One mutation escaped by coincidence — a mutated counter that happened to produce the same number — and one escaped because a shared sticky flag had already been set by an unrelated violation. Both required fixing the stimulus, not the checker. And the first version of the mode tracker latched a coverage goal as a safety error, failing a correct device from its first cycle.
Module 10 — CXL Device Types is complete. 10.1 built the borrower, 10.3 the lender, 10.2 the device that is both and pays for it, and this chapter the discipline of knowing which one you are running at any moment.
Next: Module 11 — Memory Expansion, which turns from the device class to the system it exists to build, beginning with why a socket runs out of memory before it runs out of work.
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.
