Skip to content

PCIe · Module 31

"LTSSM Only Matters During Boot" — The State Machine Never Stops

The link left L0 thirty-seven times in a run that looked continuously up. A design that latched link-up at boot retired 13,417 operations under a stale epoch.

The belief: the link trains at boot, reaches L0, and stays there. The LTSSM is bring-up machinery — interesting when a card does not enumerate, irrelevant afterwards.

The state machine does not stop. It runs for as long as the link is powered, and its excursions out of L0 are normal operation, not failures. What makes them expensive is that most designs never wrote down what their own in-flight state means when one happens.

1. Why a Competent Engineer Believes It

Four reasons, and the first is that the observation is genuinely correct most of the time.

In a healthy system you only ever see L0. Read the LTSSM state register a thousand times and it says L0 a thousand times. The evidence for the myth is overwhelming and it is real — excursions are brief and rare, and sampling almost never catches one.

The LTSSM is taught as bring-up. Every explanation walks Detect → Polling → Configuration → L0 and stops, because that is the sequence that gets a link working (18.1). The states after L0 are covered later, in a different chapter, often by a different engineer.

Debugging attention is front-loaded. The LTSSM is what you inspect when a device does not appear. By the time the system works, the LTSSM has served its purpose — and that impression is what persists.

And nothing reports an excursion by default. A Recovery entry and exit that resolves correctly leaves the link working. There is no error, no interrupt, no log line — the mechanism did its job, and doing its job is invisible.

2. The Locally True Kernel

For bring-up, the LTSSM is a boot-time sequence: if the link is not in L0, nothing above it can work, and the training states are where you look.

That model is correct and it is 25.3's entire subject. It is also the right model for "prove L0 before investigating anything else" — which 30.6 §4 makes check number one, at a cost of 1.

The scope boundary is the word "only". The myth is not that the LTSSM matters at boot; it is that it matters only then. The moment you ask what happens to work that was in flight when the link retrained, the boot-time model has nothing to say — because in that model there is no "when the link retrained".

3. The Hidden Assumption

The myth assumes L0 is absorbing: once entered, never left.

It is not. L0 has exits, and taking them is ordinary:

exit from L0why it happensis it an error?
Recoveryretraining, error recovery, speed or width changeno — this is the mechanism working
L0s / L1power managementno
Hot Resetcommanded reset of the hierarchyno
Disabledcommandedno

Recovery is the one that matters for this chapter, because it happens at runtime, without software asking, and returns to L0 — so from above it looks like nothing happened (18.5).

The assumption underneath the assumption is worse. Even engineers who know Recovery exists often believe it is transparent: the link repairs itself, so nothing above needs to care. Recovery repairs a link-level condition. It says nothing about the lifetime of a semantic operation your design had in flight.

The replacement model:

The LTSSM runs continuously; L0 is a state it occupies, not a state it terminates in. Excursions are normal, and each one defines a boundary — what your design believed about in-flight work before it may not hold after.

This is the distinction the myth erases, and it is worth its own section.

questionanswered by
is the physical link usable right now?the LTSSM state
has the link ever been usable?a latched bit — which is what the myth builds
is my in-flight work still valid?nothing, unless the design says so

Row 3 is the gap. No mechanism tells your DMA engine that the completion it is waiting for belongs to a link generation that no longer exists. 25.7 §7 established the general principle — an asynchronous response must carry its own identity, including which use of that identity — and a link excursion is exactly the event that ends one use and begins another.

A latched "link up" bit answers row 2 and is routinely used to answer rows 1 and 3. That single substitution is §10's 13,417 stale retirements.

5. The Root-Cause Tree

stagewhat happens
misconception"the LTSSM only matters at boot"
hidden assumptionL0 is absorbing; Recovery is transparent to layers above
architecture decisionno defined lifetime for in-flight state across a link excursion; "link up" is a boot-time fact
RTL decisionlatch link_up once; outstanding tables, tags and descriptors survive Recovery untouched
first divergencea Completion issued before the excursion is matched against a request issued after it
visible symptomrare data corruption or a spuriously completed operation, hours or days apart
likely wrong diagnosis"cosmic ray", "marginal timing", "the far device is flaky"
correct diagnosisin-flight state outlived the link generation it belonged to
corrected modelan excursion is an epoch boundary; work in flight across it is invalidated explicitly

The wrong diagnosis is the most expensive in this module. "Marginal timing" and "flaky device" are unfalsifiable and lead to hardware substitution, margin testing, and eventually to shipping with a known-intermittent defect. 28.4's low-rate argument applies here with more force: 13,417 events in 591,531 operations is 2.3%, and a fault at that rate survives every qualification programme.

6. The Minimal Counterexample

One Recovery. One operation. No load, no concurrency beyond that.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
t0   engine issues a Memory Read, tag 7
t1   link enters Recovery  (a normal, self-resolving excursion)
t2   link returns to L0
t3   engine issues a NEW Memory Read, tag 7
     — legal: the first request was abandoned when the link dropped,
       and the tag was reclaimed
t4   a Completion arrives carrying tag 7
     — it belongs to the request from t0
t5   the engine matches it to the request from t3

Under the myth, t1–t2 did not happen — the link was "up" throughout, so there is no reason to treat t4's Completion as anything but the answer to t3.

The tag is the entire identity of the Completion (10.2), and it matches. Nothing on the wire is malformed. Nothing is out of specification. The correlation is simply wrong, and the only thing that could have prevented it is a design-side notion that t1 ended one epoch and began another.

7. The LTSSM, Drawn as a Running Machine

An LTSSM diagram. Detect leads to Polling, which leads to Configuration, which leads to L0. From L0 the machine can enter Recovery and return to L0, enter L0s and return, enter L1 and return, or go to Hot Reset. The boot path is shown as running once; the transitions around L0 are shown as running for the life of the link.DetectPollingConfigurationL0 -occupiedRecoveryL0sL1HotResetruns onceruns onceat bring-upat bring-uplink usablelink usableretrain / error / speed changeretrain / error/ speed changeEPOCH BOUNDARYEPOCHBOUNDARYidleidletraffictrafficlow powerlow powerwakewakecommandedcommanded
Figure 1 — the LTSSM with the runtime excursions emphasised rather than the boot path. The training sequence on the left runs once. Everything around L0 runs for the life of the link: Recovery for retraining and error recovery, the low-power states, and the commanded resets. Each return arrow into L0 is an epoch boundary for any design that had work in flight.

Two readings.

The three muted states run once. Everything else runs for the life of the link — which is the inversion the myth performs, treating the boot path as the interesting part.

The Recovery → L0 arrow is labelled EPOCH BOUNDARY. That is not a PCIe term; it is the design-side consequence, and §11 is the RTL of it.

8. The RTL the Myth Produces

The wrong RTL. Written by someone for whom the LTSSM finished at boot.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — link_up is a latched boot-time fact, and outstanding state has no
// relationship to it. This is the natural implementation if L0 is absorbing.
module outstanding_wrong #(
  parameter int unsigned NTAG = 32
)(
  input  logic clk,
  input  logic rst_n,
  input  logic                       link_in_l0,      // live from the core
  input  logic                       alloc_req,
  output logic                       alloc_gnt,
  output logic [$clog2(NTAG)-1:0]    alloc_tag,
  input  logic                       cpl_valid,
  input  logic [$clog2(NTAG)-1:0]    cpl_tag,
  output logic                       cpl_accept,
  output logic                       link_up
);
 
  logic [NTAG-1:0] busy;
 
  // "The link came up at boot." Latched once, never cleared. Every consumer
  // of link_up now believes the link has been continuously usable.
  always_ff @(posedge clk or negedge rst_n)
    if (!rst_n)          link_up <= 1'b0;
    else if (link_in_l0) link_up <= 1'b1;
 
  always_comb begin
    alloc_gnt  = alloc_req && (busy != '1);
    alloc_tag  = '0;
    for (int i = NTAG-1; i >= 0; i--) if (!busy[i]) alloc_tag = $clog2(NTAG)'(i);
    // A Completion is matched on TAG ALONE. Under the myth this is complete:
    // the tag is unique among outstanding requests, and there is only ever
    // one link generation.
    cpl_accept = cpl_valid && busy[cpl_tag];
  end
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) busy <= '0;
    else begin
      if (alloc_req && alloc_gnt) busy[alloc_tag] <= 1'b1;
      if (cpl_accept)             busy[cpl_tag]   <= 1'b0;
      // Nothing happens on a link excursion. There is no branch for it,
      // because in this model there is no such event.
    end
  end
 
endmodule

Why it passes basic tests. Every functional test runs on a link that stays in L0. The excursion branch is not merely untested — it does not exist, so there is nothing for a test to fail against. Error-injection suites that force a retrain usually do so with the engine idle, which is precisely the condition under which the bug cannot occur.

Three defects.

link_up is a latch, not a state. It answers "has the link ever worked", and every consumer reads it as "is the link working". §4's row 2 substituted for row 1.

Outstanding state is untouched by excursions. A request issued before Recovery is still busy after it, and its tag will be matched by any arriving Completion.

And there is no generation. The tag is the whole identity, so a pre-excursion Completion is indistinguishable from a post-excursion one — §6's counterexample, in RTL.

9. The Failure Timeline

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
cycle      0   link_in_l0 = 1.  link_up latches to 1 and never changes again.
cycle    100   engine allocates tag 7, issues MemRd A.  busy[7] = 1
cycle    120   link_in_l0 falls — Recovery entered.
               The wrong RTL does nothing: busy[7] stays 1, link_up stays 1.
               The engine has no reason to stop issuing.
cycle    121   engine allocates tag 9, issues MemRd B — into a link that
               is not currently usable. The TL will stall or drop it.
cycle    920   link_in_l0 rises — back in L0. 800 cycles of excursion.
               Nothing in the design observed the boundary.
cycle    925   request A's completion timeout fires (correctly).
               busy[7] is cleared and tag 7 returns to the pool.
cycle    940   engine allocates tag 7 again, issues MemRd C.  busy[7] = 1
cycle    963   a Completion arrives carrying tag 7.
               It is A's completion — issued before the excursion, delayed,
               and delivered after it.
cycle    964   cpl_accept = 1.  busy[7] cleared.  C is retired using A's data.
               (first divergence at 964; the CAUSE was at cycle 120)
cycle   1200   the consumer of C's buffer reads bytes belonging to A

The first divergence is cycle 964, and the cause is 844 cycles earlier — which is why the symptom points nowhere useful.

Why the wrong diagnosis wins. Everything at cycle 964 is legal: a well-formed Completion carrying a tag that was genuinely outstanding, accepted by logic that is doing exactly what it was written to do. The link is in L0 and has been for 44 cycles. There is no error, no timeout, no status bit. The corruption surfaces at cycle 1200 in a completely different subsystem.

And §10 measured the rate: 13,417 in 591,531 operations — 2.3%.

10. Measured — What Runtime Excursions Cost

design policyexcursionsissuedretiredretired under a stale epoch
"link up" latched once at boot37591,531591,15713,417
epoch invalidates in-flight work37591,531577,7400

Three readings.

Thirty-seven excursions in a run that looked continuously up. The link was in L0 for the overwhelming majority of the time, and any sampling-based monitor would have reported it as stable.

13,417 stale retirements is 2.3% of operations. Low enough to survive qualification; high enough to appear in the field. This is the same low-rate danger 31.5 §9 identified — a fault that occurred every time would have been found immediately.

And the second row retired 13,417 fewer operations. That is not lost throughput; it is work correctly abandoned rather than incorrectly completed. A design that "performs better" by retiring operations it cannot vouch for has traded correctness for a number, which is the shape 26.3 §17 and 28.1 §12 both measured in other contexts.

11. The Corrected RTL

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// The excursion is an EPOCH BOUNDARY. Outstanding state carries the epoch it
// was created in, and a Completion is qualified on (tag, epoch) rather than
// on tag alone — 25.7 §7's identity law, with the link generation as the use.
module outstanding_correct #(
  parameter int unsigned NTAG  = 32,
  parameter int unsigned EPO_W = 4
)(
  input  logic clk,
  input  logic rst_n,
  input  logic                       link_in_l0,       // LIVE, not latched
  input  logic                       alloc_req,
  output logic                       alloc_gnt,
  output logic [$clog2(NTAG)-1:0]    alloc_tag,
  input  logic                       cpl_valid,
  input  logic [$clog2(NTAG)-1:0]    cpl_tag,
  input  logic [EPO_W-1:0]           cpl_epoch,
  output logic                       cpl_accept,
  output logic                       cpl_stale,
  output logic [EPO_W-1:0]           cur_epoch,
  output logic                       link_usable_now,
  output logic                       link_ever_up,
  output logic [31:0]                c_excursions, c_invalidated, c_stale
);
 
  logic [NTAG-1:0]  busy;
  logic [EPO_W-1:0] tag_epoch [NTAG];
  logic             l0_d, excursion_start;
 
  always_comb begin
    // Two DIFFERENT facts, exposed separately. §4's rows 1 and 2 are not the
    // same question, and collapsing them is the myth.
    link_usable_now = link_in_l0;
 
    // Falling edge of L0 — the excursion begins here, and in-flight work is
    // suspect from this moment rather than from the return to L0.
    excursion_start = l0_d && !link_in_l0;
 
    // No new work is issued while the link is not usable. The wrong RTL has
    // no such gate and allocates into an unusable link (§9 cycle 121).
    alloc_gnt = alloc_req && (busy != '1) && link_in_l0;
    alloc_tag = '0;
    for (int i = NTAG-1; i >= 0; i--) if (!busy[i]) alloc_tag = $clog2(NTAG)'(i);
 
    // A Completion needs a live tag AND a matching epoch. Tag alone cannot
    // distinguish A's completion from C's request (§6).
    cpl_stale  = cpl_valid && (!busy[cpl_tag] || (tag_epoch[cpl_tag] != cpl_epoch));
    cpl_accept = cpl_valid && !cpl_stale;
  end
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      busy <= '0; cur_epoch <= '0; l0_d <= 1'b0; link_ever_up <= 1'b0;
      c_excursions <= '0; c_invalidated <= '0; c_stale <= '0;
      for (int i = 0; i < NTAG; i++) tag_epoch[i] <= '0;
    end else begin
      l0_d <= link_in_l0;
      if (link_in_l0) link_ever_up <= 1'b1;
 
      if (excursion_start) begin
        // The epoch advances at the START of the excursion. Advancing on the
        // RETURN to L0 leaves a window in which pre-excursion completions
        // still match — the whole excursion is that window (§12 audit A).
        cur_epoch    <= cur_epoch + 1'b1;
        c_excursions <= c_excursions + 32'd1;
        for (int i = 0; i < NTAG; i++)
          if (busy[i]) c_invalidated <= c_invalidated + 32'd1;
        busy <= '0;                       // in-flight work is abandoned
      end else begin
        if (alloc_req && alloc_gnt) begin
          busy[alloc_tag]      <= 1'b1;
          tag_epoch[alloc_tag] <= cur_epoch;
        end
        if (cpl_accept) busy[cpl_tag] <= 1'b0;
      end
 
      // A stale completion is DISCARDED and COUNTED. Discarding it silently
      // makes it indistinguishable from never having arrived, which is the
      // one thing that would hide this bug from the instrument built to
      // find it (25.1 §6).
      if (cpl_stale && c_stale != 32'hFFFF_FFFF) c_stale <= c_stale + 32'd1;
    end
  end
 
endmodule

The six lenses.

ARCHITECTURE. The module exists because a link excursion ends the validity of in-flight correlations and nothing else in the system says so (§4 row 3). The epoch is that statement.

STATE. busy (which tags are outstanding), tag_epoch (which link generation each belongs to), cur_epoch, and l0_d for edge detection. link_usable_now and link_ever_up are deliberately separate outputs.

EVENT. The falling edge of link_in_l0 advances the epoch and abandons in-flight work. Allocation stamps the current epoch; a Completion is qualified against it.

CONTRACT. The DMA engine relies on: an accepted Completion belongs to the request that is currently outstanding under that tag. Without the epoch that guarantee does not exist across an excursion, and the engine has no way to know.

FAILURE. Advancing the epoch on the return to L0 instead of the departure leaves the entire excursion as a window in which pre-excursion completions still match — §12 audit A.

DV / DEBUG. §13's p2_cpl_needs_matching_epoch is the direct check; c_stale and c_invalidated are what make the excursion visible to a human afterwards.

12. Same-Cycle Audit

13. Assertions

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P1 — no request is issued while the link is not usable. Catches the
// latched-link_up substitution of §4 directly (§9 cycle 121).
property p1_no_alloc_without_l0;
  @(posedge clk) disable iff (!rst_n)
    alloc_gnt |-> link_in_l0;
endproperty
a_p1: assert property (p1_no_alloc_without_l0);
 
// P2 — a Completion is accepted only with a live tag AND a matching epoch.
// This is the identity law of 25.7 §7 with the link generation as the "use".
// Catches §6's counterexample and §9's cycle 964.
property p2_cpl_needs_matching_epoch;
  @(posedge clk) disable iff (!rst_n)
    cpl_accept |-> (busy[cpl_tag] && (tag_epoch[cpl_tag] == cpl_epoch));
endproperty
a_p2: assert property (p2_cpl_needs_matching_epoch);
 
// P3 — the epoch advances on the DEPARTURE from L0, not the return.
// Advancing on return leaves the entire excursion as a matching window.
// |=> because the advance lands on the next edge via NBA.
property p3_epoch_advances_on_departure;
  @(posedge clk) disable iff (!rst_n)
    ($fell(link_in_l0)) |=> (cur_epoch == $past(cur_epoch) + 1'b1);
endproperty
a_p3: assert property (p3_epoch_advances_on_departure);
 
// P4 — the epoch does not change for any other reason. A design that
// advanced it spuriously would reject valid Completions and lose work,
// which is the overcorrection failure.
property p4_epoch_stable_otherwise;
  @(posedge clk) disable iff (!rst_n)
    (!$fell(link_in_l0)) |=> $stable(cur_epoch);
endproperty
a_p4: assert property (p4_epoch_stable_otherwise);
 
// P5 — in-flight work does not survive an excursion.
property p5_excursion_invalidates;
  @(posedge clk) disable iff (!rst_n)
    ($fell(link_in_l0)) |=> (busy == '0);
endproperty
a_p5: assert property (p5_excursion_invalidates);
 
// P6 — a stale Completion is COUNTED, never silently dropped. Without this
// the design's only evidence of the excursion's cost is invisible, and a
// discarded-and-unrecorded event is indistinguishable from one that never
// arrived (25.1 §6).
property p6_stale_counted;
  @(posedge clk) disable iff (!rst_n)
    cpl_stale |=> (c_stale > $past(c_stale));
endproperty
a_p6: assert property (p6_stale_counted);
 
// P7 — the two link facts remain distinct. link_ever_up may be 1 while
// link_usable_now is 0; a design that ties them has rebuilt the myth.
property p7_link_facts_distinct;
  @(posedge clk) disable iff (!rst_n)
    (!link_in_l0 && link_ever_up) |-> !link_usable_now;
endproperty
a_p7: assert property (p7_link_facts_distinct);
 
// Vacuity guards. Every property above except P1 and P7 is antecedented on
// an excursion. A testbench that never leaves L0 — which is the natural one
// to write under the myth — satisfies P2 through P6 without evaluating a
// single consequent.
c1_excursion:        cover property (@(posedge clk) disable iff (!rst_n) $fell(link_in_l0));
c2_excursion_w_work: cover property (@(posedge clk) disable iff (!rst_n)
                       $fell(link_in_l0) && (busy != '0));
c3_stale_cpl:        cover property (@(posedge clk) disable iff (!rst_n) cpl_stale);
c4_realloc_same_tag: cover property (@(posedge clk) disable iff (!rst_n)
                       $fell(link_in_l0) ##[1:$] (alloc_gnt && (alloc_tag == $past(cpl_tag))));

c2_excursion_w_work is the cover that matters. An excursion with the engine idle exercises nothing — and an error-injection suite that forces a retrain between transfers, which is the natural way to write one, produces exactly that. The bug requires an excursion with work in flight, and §10's model needed the operation lifetime extended past the excursion length before it could measure anything at all.

14. If DV Believes the Same Myth

The testbench never leaves L0, so the entire failure class is unreachable.

DV artefactwhat the myth makes it doconsequence
link BFMdrives link_in_l0 high after bring-up and holds itc1_excursion never covers; P2–P6 vacuous
error injectionforces a retrain between transfersc2_excursion_w_work never covers — the one condition that matters
scoreboardmatches Completions to requests by tagreproduces the DUT's bug exactly and agrees with it
reference modelno notion of a link generationcannot express "this completion is stale"
coverageLTSSM states visited during bring-upshows Recovery covered because bring-up passed through it
assertions"every Completion matches an outstanding tag"encodes the myth as a checked property

The coverage row is the subtle one. A coverage report showing Recovery as visited is true and useless — the state was entered during training, with nothing in flight. "State visited" and "state visited under load with outstanding work" are different bins, and only the second has anything to do with this bug.

The scoreboard row is the fatal one. A scoreboard matching on tag alone will pass §9's timeline and, worse, fail against the corrected RTL — because the corrected design rejects a Completion the scoreboard believes is valid. This is how a correct implementation gets reported as a regression.

What a correct testbench does differently. The link BFM injects excursions during traffic; the reference model stamps each request with the link generation it observed; and the scoreboard's match key is (tag, generation). The last change is the DV expression of §3's replacement model.

15. Debugging

On instrument choice. An analyzer can see the excursion — the link states are on the wire (25.9 §6). What it cannot see is your outstanding table, so it can tell you that an excursion happened and not what your design did about it. The excursion counter is cheaper and the stale counter is the one that names the fault.

16. Review and Interview

The review gate this myth corrupts. An RTL or integration review that accepts "the link is up and stable" has established nothing about lifetimes.

The review question: "What happens to every piece of outstanding state when the link leaves L0 — and what event makes that state reusable again?"

The second clause is where designs fail. A design that abandons in-flight work but reclaims its tags on a timeout, with no generation, has moved the race rather than removed it (25.7 §7 measured that exact window in a different context).

The interview exchange.

Weak answer: "The LTSSM handles link training. Once you're in L0 the link is up and the transaction layer takes over."

Why it sounds plausible: it is an accurate description of bring-up, and it is what every introduction teaches.

Interviewer follow-up: "The link has been in L0 for three days. How many times has it left L0 in that period?"

Where the weak model breaks: the expected answer under the myth is zero, and the honest answer is "I don't know, and neither does anyone here unless we counted." §10 measured 37 in a run that looked continuous.

Strong answer: "The LTSSM runs continuously — L0 is a state it occupies, not one it terminates in. Recovery, L0s, L1 and speed changes are all runtime exits, and Recovery in particular happens without software asking. What matters for my design isn't whether an excursion occurred; it's that each one is a boundary for in-flight state. I'd stamp outstanding requests with a link generation and qualify Completions on it, so a reply issued before an excursion can't be matched to a request issued after it."

Senior follow-up: "You advance the generation. On the departure from L0 or the return?"the departure, because the excursion itself is the window; advancing on return leaves every cycle of it exposed (P3, §12 audit A).

17. Misconceptions Inside the Misconception

"Recovery is an error, so if it's happening we have a problem." Why it sounds plausible: the word suggests something went wrong, and it sits next to Hot Reset and Disabled in the state diagram. What really happens: Recovery is the mechanism working — retraining, speed changes and error recovery all use it (18.5). §10's 37 excursions are not 37 faults. What it causes: two opposite errors — either alarm at a normal event, or dismissal of the excursion count as noise. The count is neither an alarm nor noise; it is the input to the lifetime question.

"Recovery is transparent to layers above." Why it sounds plausible: the link repairs itself and traffic resumes, which is exactly what transparent means at the link layer. What really happens: it is transparent to the link, and says nothing about the lifetime of a semantic operation your design had in flight (§3). What it causes: the precise substitution this chapter is about — link-level transparency read as system-level transparency.

"A completion timeout will catch it." Why it sounds plausible: an abandoned request does eventually time out, and timeouts exist for exactly this. What really happens: the timeout fires, reclaims the tag, and creates the reuse window (§9 cycles 925–940). It is part of the mechanism, not a defence against it. What it causes: a design that relies on the timeout as protection and is exposed by it.

"We tested Recovery." Why it sounds plausible: the coverage report shows the state visited. What really happens: it was visited during bring-up with nothing in flight (§14). c2_excursion_w_work is a different bin, and §10's model needed its operation lifetime extended past the excursion before it could measure anything. What it causes: the strongest possible false confidence — a green coverage item for the exact scenario that was never exercised.

18. Understanding Check

Q1. Your link has been in L0 for three days and a corruption occurred twice. What do you measure first, and what does its absence tell you?

How many times the link has left L0 (§15). §10 measured 37 excursions in a run that presented as continuously up, so "it has been in L0" is a statement about sampling rather than about the link. If no such counter exists, that absence is itself the finding — the design has no record of the boundary events that would explain a rare correlation failure. With the counter, correlate excursion timestamps against corruption timestamps; clustering makes the diagnosis. Without it the investigation defaults to marginal timing and flaky hardware, which is §5's wrong diagnosis and is unfalsifiable.

Q2. Explain why a completion timeout is part of the mechanism rather than a defence against it.

Because reclaiming the tag is what creates the reuse window (§9 cycles 925–940, §17). After an excursion the abandoned request times out — correctly — and its tag returns to the pool. A new request takes that tag. The old request's Completion, delayed by the excursion, then arrives carrying an identity that now belongs to somebody else. The timeout did exactly its job; the missing piece is a generation that distinguishes the two uses of the tag. This is 25.7 §7's window, reached through a link excursion instead of a slow completer.

Q3. Should the epoch advance when the link leaves L0 or when it returns? Give the failure of the wrong choice.

On the departure (P3, §12 audit A). Advancing on the return leaves the entire excursion as a window in which pre-excursion Completions still match the current epoch — and the excursion is 800 cycles in §10's model, which is where the delayed Completions are. A Completion that arrives during the excursion, or in the first cycles after it, would be accepted against state that the excursion invalidated. Advancing on departure means in-flight work is suspect from the first cycle the link is unusable, which is when it actually became suspect.

Q4. A coverage report shows the Recovery state as covered. Why might that be worthless for this bug?

Because Recovery was almost certainly entered during bring-up, with nothing in flight (§14, c2_excursion_w_work). "State visited" and "state visited with outstanding work" are different conditions, and only the second can produce the failure. §10's own model demonstrates the same trap: with a 200-step operation lifetime and an 800-step excursion, every operation retired before the epoch could change and the stale counter read zero for both policies — the mutation measured nothing. The lifetime had to exceed the excursion before the bug existed. A testbench that injects retrains between transfers reproduces exactly that blind spot.

Q5. State the replacement model, and use it to explain why "link up" is the wrong signal to gate work on.

"The LTSSM runs continuously; L0 is a state it occupies, not one it terminates in. Excursions are normal, and each defines a boundary — what your design believed about in-flight work before it may not hold after" (§3). "Link up" as a latched bit answers "has the link ever been usable" (§4 row 2), which is a boot-time fact that never becomes false. Gating work on it means issuing requests into a link that is not currently usable (§9 cycle 121, P1). The design needs two separate facts: link_usable_now, which gates issue, and the epoch, which qualifies correlation — and P7 asserts they stay distinct, because tying them together rebuilds the myth in RTL.

19. Module 31 Complete

Six myths, and each was a locally useful simplification applied outside its scope.

ChapterBeliefLocally true kernelHidden assumptionReplacement model
31.1"just a faster PCI"the software model was preserveda shared medium, made fasterpoint-to-point links; capacity scales, flows are independent
31.2"memory-mapped only"memory dominates by volumethe transaction type can be inferredthe type is in the header; four other families arrive
31.3"BARs contain memory"offsets backed by storage behave as memoryan address window implies storagea BAR claims addresses; behaviour is per offset
31.4"DMA bypasses the protocol"it bypasses the CPUCPU-bypass implies transport-bypassDMA changes who initiates, not what travels
31.5"MSI is a software interrupt"from the handler, it is an interruptnotification is delivered independently of dataan MSI is a Posted write, ordered like any write
31.6 (this)"LTSSM only matters at boot"for bring-up, it is a boot sequenceL0 is absorbing; Recovery is transparent abovethe FSM runs continuously; each excursion is a lifetime boundary

Every row has the same shape, and it is the module's transferable result:

A dangerous misconception is a useful model with its scope removed. The correction is never "that's wrong" — it is "that's true here, and here is the boundary."

The review map, connecting Module 31 back to Module 30:

ChapterReview gate the myth corruptsThe question that exposes it
31.1architecture — topology and bandwidth"where do these flows converge?"
31.2RTL — receive decode"what is the default case for an unrecognised type?"
31.3architecture — the register map"for every offset, does a read have a side effect?"
31.4performance (30.5)"how many TLPs is a transfer, and what does it consume?"
31.5verification — interrupt coverage"what makes the interrupt eligible to be issued?"
31.6integration — lifetimes"what happens to outstanding state when the link leaves L0?"

Three findings recur across all six, and they are worth carrying out of the curriculum entirely.

A fault with a low rate is more dangerous than one with a high rate. §10 measured 2.3%; 31.5 §9 measured 48.8% and 31.3 §10 measured a rate proportional to duplication. The 100% cases are found in the first test.

A testbench that shares the misconception certifies it. Every chapter in this module has a §"If DV believes the same myth" section, and in each one the scoreboard agrees with the wrong RTL and disagrees with the right one — which is the mechanism by which correct designs get reported as regressions.

And the instrument usually has the same blind spot as the mental model. An interrupt monitor that fires on irq_valid cannot see an ordering race; a coverage bin that counts "Recovery visited" cannot see an excursion under load; an analyzer cannot see internal ownership at all. The fix is always an oracle that shares no machinery with the thing under test — which is the single most transferable idea in the PCIe curriculum, and it appeared first in 25.6 and last here.

This is the final chapter of the PCIe curriculum. It ends on a state machine that never stops running, which is a reasonable place to leave it: the link is not a thing that was set up once, and neither is an engineer's model of it.