Skip to content

PCIe · Module 25

PCIe Debugging — One Method, Applied to Any Failure

Seven of eight distinct root causes produce the same sentence. The method that separates them never starts with an analyzer — it starts with the last event you can prove happened.

Module 24 verified a design you built, with stimulus you chose, against a model you wrote. Debugging is the other situation entirely: a system you did not build, a failure you cannot reproduce at will, and a symptom instead of a testbench.

And the symptom is almost always useless on its own. "The device doesn't show up." "DMA hangs." "It works on that machine." Each of those sentences is produced by many different faults at different layers, and none of them names one.

1. Sources, Scope, and What Module 25 Owns

2. The Chain

Every step exists to shrink the candidate set, and each one is answerable with evidence rather than opinion.

StepThe questionWhy it shrinks the space
1. Symptomwhat was observed, precisely?"DMA hung" and "DMA returned wrong data" are different searches
2. Layerwhich layer can produce that symptom?eliminates two thirds of the design immediately
3. Last known goodwhat is the last event I can prove happened?converts a search into a state transition (§5)
4. Expected nextwhat should have happened after it?a transition has few, enumerable preconditions
5. Blockerwhich precondition or resource is absent?this is usually the answer
6. Experimentwhat single change distinguishes the remaining candidates?one measurement, not a capture session
7. Root cause

Step 3 is load-bearing and step 5 is where it pays off. Once you know the last proven event, "what should happen next" has a small answer — a state machine's exit condition, a credit, a Tag, a completion. Compare that with "DMA hung", whose candidate set is the whole device.

And step 6 deserves its own emphasis. A distinguishing experiment is one whose two outcomes point at different causes. "Capture a trace and look" is not distinguishing — it produces data with no hypothesis attached, which §3 argues is the most common way debugging time disappears.

3. Do Not Start With the Analyzer

4. Three Layers, Ordered Evidence

Chapter 24.1 §2 established the three contracts. In debugging they are an order, not a menu.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
1. Link state    is the Link usable at all?
2. Data Link     is packet delivery over this Link healthy?
3. Transaction   are requests, completions and configuration correct?
4. System        is the operation even being issued or recognized?

Why the order is not negotiable. A failure at a lower layer makes the layers above unprovable rather than false (24.1 §3). If the Link is dropping into Recovery, the completion timeouts above it are a consequence — investigating them first is time spent on a symptom.

And the fourth line matters more than it looks. A great many "PCIe problems" are the operation never being issued: no work queued, the driver not bound, bus mastering not enabled. Chapter 22.1 §6 measured a real system at 49.39% utilisation whose single largest blocking category was no_work — nothing to do with PCIe at all.

The practical form is a checklist you can answer in minutes, before opening anything:

LayerOne questionOne place to look
Linkis it in L0, and has it stayed there?LTSSM state + a Recovery entry count
Data Linkis replay/retry active?replay counter, error counters
Transactionare requests leaving and completions returning?outstanding count, completion count
Systemis work being issued at all?descriptor/doorbell activity, no_work cycles

5. Last Known Good

6. First Failure, Not Last Symptom

One fault produces a cascade, and the cascade is louder than its cause.

A credit leak (22.3 §8) produces, in order: transmission stalls → outstanding requests never complete → a completion timeout → a DMA descriptor never retires → the driver reports a hang → the user reports "the card is broken."

Six symptoms, one cause, and the reporting order is exactly backwards from the causal order.

So the instrumentation rule is: capture the first, count the rest (23.6 §7). Chapter 21.4 §11 measured the alternative — a status register overwritten by each new error reported a non-root cause 96.8% of the time.

And the debugging rule that follows: read the earliest evidence you have, not the loudest. If the design has a first-failure register, read it. If it does not, the timestamped log is more trustworthy than any current-state register — because a current-state register describes the aftermath.

§9's first_failure_capture is therefore the single highest-value diagnostic block in this chapter, and §13's cases all begin by reading it.

7. The Triage Table

Symptom in, layer and next experiment out. Every row routes to the chapter that owns the class.

SymptomLikely layerProof to gatherOwned by
nothing in the OS device list, no linkLink / PHYLTSSM state; did it leave Detect?25.3
link up, nothing in the device listConfigurationconfig-request and completion counters25.2
link up, then drops repeatedlyLink stateRecovery entry count, residency25.4
device visible, registers read as garbageConfiguration / decodewhich aperture claimed the address25.5
device visible, DMA never startsSystembus mastering, descriptor activity25.6
DMA starts, never completesTransactionoutstanding count, completion count25.7
everything stalls, link healthy, no errorsFlow Controlper-class credit, pending packets25.8
intermittent, load-dependent corruptionTransaction / ownershipfirst-failure record, stall counters23.6 §13
works on one machine, not anotherSystem / topologynegotiated width and speed; switch in path25.3, 25.9

Two rows deserve comment.

Row 3 versus row 1. "No link" and "link that keeps dropping" are different chapters because they are different questions — one asks why a transition never happened, the other asks why a state does not persist (25.4 §2).

And row 8 is the one people misroute. Load-dependent corruption is almost never a protocol fault; it is an ownership fault (24.1 §7 measured every such defect in Modules 22–23 needing a stall to appear). The proof is the first-failure record plus stall counters, not a bus trace.

8. The Waveform

Last known good, expected next, and the blockers eliminated

10 cycles
Ten cycles at a requester. A request is accepted at cycle 0 and appears on the wire at cycle 1. The outstanding count rises to one at cycle 1 and stays there. No completion is ever seen. Link L0 and credit ok remain asserted throughout and work pending remains asserted, so none of those is the blocker. A watchdog asserts at cycle 8.last known good: request on the wirelast known good: request onthe wireexpected next: a completionexpected next: a completionlink L0 and credit fine — not the blockerlink L0 and credit fine —not the blockerclklink_l0credit_okreq_acceptreq_on_wireoutstandingcpl_validwatchdogt0t1t2t3t4t5t6t7t8t9
Figure 1 — the method applied to one hang. A request is accepted and transmitted, and the outstanding count rises and never falls. Link state, credit availability and pending work are all healthy throughout, which eliminates three candidate blockers by inspection. The last provable event is the request on the wire; the expected next event is a completion; the watchdog marks where the evidence runs out.

Four things to read out of the figure.

link_l0 and credit_ok are drawn deliberately. They are high for the whole window, which eliminates the Link and Flow Control layers by inspection — two of §4's four rows, answered before any capture.

outstanding rises and never falls. That is the last known-good event and the missing next one, in one signal (§5).

cpl_valid never asserts. The request left and was not answered, which routes to 25.7 — and note the figure does not attempt to say why, because that is a different chapter.

And the watchdog at cycle 8 is where evidence ends, not where the fault is. It is a debug heuristic (§1), and treating its threshold as a protocol timeout is §12's mutation 12.

9. RTL — The Diagnostic Instruments

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Diagnostic event types.
// INTERNAL DEBUG FORMAT -- PCIe defines no such record (§1).
package pcie_dbg_pkg;
 
  parameter int EVENT_W   = 8;
  parameter int STATE_W   = 4;
  parameter int PORT_W    = 3;
  parameter int CTX_W     = 32;
  parameter int HIST_DEPTH = 32;
  parameter int HIST_W    = (HIST_DEPTH <= 1) ? 1 : $clog2(HIST_DEPTH);
 
  typedef enum logic [EVENT_W-1:0] {
    EV_LINK_STATE_CHANGE = 8'd0,
    EV_CFG_REQ_ACCEPTED  = 8'd1,
    EV_CFG_CPL_RETURNED  = 8'd2,
    EV_DESC_ACCEPTED     = 8'd3,
    EV_REQ_TRANSMITTED   = 8'd4,
    EV_CPL_RECEIVED      = 8'd5,
    EV_TXN_RETIRED       = 8'd6,
    EV_STATUS_PUBLISHED  = 8'd7,
    EV_REPLAY            = 8'd8,
    EV_ERROR             = 8'd9
  } dbg_event_e;
 
  typedef struct packed {
    dbg_event_e        ev;
    logic [STATE_W-1:0] link_state;
    logic [PORT_W-1:0]  port;
    logic [CTX_W-1:0]   context;      // identity, offset, or cause code
    logic [CTX_W-1:0]   cycle;
  } dbg_record_t;
 
endpackage
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import pcie_dbg_pkg::*;
 
// SYNTHESIZABLE. FIRST-FAILURE CAPTURE (§6). The single highest-value
// block in this chapter. 21.4 §11 measured last-write-wins naming a
// non-root cause 96.8% of the time; this latches once and locks.
module first_failure_capture (
  input  logic clk,
  input  logic rst_n,
  input  logic              err_fire,
  input  logic [EVENT_W-1:0] err_code,
  input  logic [STATE_W-1:0] link_state,
  input  logic [CTX_W-1:0]   context,
  input  logic [CTX_W-1:0]   cycle,
  input  logic              dbg_clear,        // EXPLICIT clear only
 
  output logic              captured,
  output logic [EVENT_W-1:0] first_code,
  output logic [STATE_W-1:0] first_link_state,
  output logic [CTX_W-1:0]   first_context,
  output logic [CTX_W-1:0]   first_cycle,
  output logic [31:0]        later_count
);
  logic cap_q; logic [EVENT_W-1:0] code_q; logic [STATE_W-1:0] ls_q;
  logic [CTX_W-1:0] ctx_q, cyc_q; logic [31:0] later_q;
 
  assign captured=cap_q; assign first_code=code_q; assign first_link_state=ls_q;
  assign first_context=ctx_q; assign first_cycle=cyc_q; assign later_count=later_q;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n || dbg_clear) begin
      cap_q<=1'b0; code_q<='0; ls_q<='0; ctx_q<='0; cyc_q<='0; later_q<='0;
    end else if (err_fire) begin
      if (!cap_q) begin
        // Capture the LINK STATE too: an error during Recovery means
        // something very different from the same error in L0 (§4).
        cap_q<=1'b1; code_q<=err_code; ls_q<=link_state;
        ctx_q<=context; cyc_q<=cycle;
      end else if (!(&later_q)) later_q <= later_q + 32'd1;
    end
  end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import pcie_dbg_pkg::*;
 
// SYNTHESIZABLE. Bounded event history (§5). Enough to answer "what is
// the last thing that provably happened", not a protocol analyzer.
// Freezes on failure so the window around the fault survives.
module dbg_event_history (
  input  logic clk,
  input  logic rst_n,
  input  logic        push,
  input  dbg_record_t rec,
  input  logic        freeze,          // level: stop writing, keep contents
  input  logic        dbg_clear,
 
  output dbg_record_t history [HIST_DEPTH],
  output logic [HIST_W-1:0] wr_ptr,
  output logic              wrapped,
  output logic              frozen,
  output logic [31:0]       dropped_while_frozen
);
  dbg_record_t h_q [HIST_DEPTH];
  logic [HIST_W-1:0] wr_q;
  logic wrap_q, froz_q;
  logic [31:0] drop_q;
 
  always_comb for (int i=0;i<HIST_DEPTH;i++) history[i]=h_q[i];
  assign wr_ptr=wr_q; assign wrapped=wrap_q; assign frozen=froz_q;
  assign dropped_while_frozen=drop_q;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n || dbg_clear) begin
      wr_q<='0; wrap_q<=1'b0; froz_q<=1'b0; drop_q<='0;
      for (int i=0;i<HIST_DEPTH;i++) h_q[i]<='0;
    end else begin
      if (freeze) froz_q <= 1'b1;
      if (push) begin
        if (froz_q) begin
          // Count what we could not record. A frozen buffer that silently
          // drops makes the window look quieter than it was.
          if (!(&drop_q)) drop_q <= drop_q + 32'd1;
        end else begin
          h_q[wr_q] <= rec;
          // Guarded so HIST_DEPTH = 1 is legal: the pointer is always 0.
          if (HIST_DEPTH == 1) wrap_q <= 1'b1;
          else if (wr_q == HIST_W'(HIST_DEPTH-1)) begin wr_q<='0; wrap_q<=1'b1; end
          else wr_q <= wr_q + HIST_W'(1);
        end
      end
    end
  end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import pcie_dbg_pkg::*;
 
// SYNTHESIZABLE. PROGRESS HEARTBEAT (§4's fourth row).
// "Meaningful progress" is a DECLARED list of events -- a clock that keeps
// toggling is not progress, and neither is a counter that keeps counting.
module progress_heartbeat #(parameter int unsigned STALL_BUDGET = 65536) (
  input  logic clk,
  input  logic rst_n,
  input  logic progress_event,        // OR of the declared meaningful events
  input  logic dbg_clear,
 
  output logic [31:0] progress_epoch,
  output logic [31:0] cycles_since_progress,
  output logic        no_progress,     // sticky: exceeded the DEBUG budget
  output logic [31:0] worst_gap
);
  logic [31:0] ep_q, since_q, worst_q;
  logic        np_q;
 
  assign progress_epoch=ep_q; assign cycles_since_progress=since_q;
  assign no_progress=np_q;    assign worst_gap=worst_q;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n || dbg_clear) begin ep_q<='0; since_q<='0; np_q<=1'b0; worst_q<='0; end
    else if (progress_event) begin
      ep_q <= ep_q + 32'd1;
      if (since_q > worst_q) worst_q <= since_q;
      since_q <= '0;
    end else begin
      since_q <= since_q + 32'd1;
      // A DEBUG HEURISTIC, not a PCIe timeout (§1). It says "nothing
      // meaningful has advanced for this long", which is a fact about the
      // design, not a protocol violation.
      if (since_q >= 32'(STALL_BUDGET)) np_q <= 1'b1;
    end
  end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import pcie_dbg_pkg::*;
 
// SYNTHESIZABLE. State residency counters (§4).
// Distinguishes "sat in one state forever" from "cycled rapidly" -- two
// completely different faults with the same external symptom.
module state_residency #(parameter int N_STATE = 16) (
  input  logic clk,
  input  logic rst_n,
  input  logic [STATE_W-1:0] state,
  input  logic dbg_clear,
 
  output logic [31:0] residency [N_STATE],
  output logic [31:0] entries   [N_STATE],
  output logic        state_enter,
  output logic        saturated
);
  logic [31:0] res_q [N_STATE], ent_q [N_STATE];
  logic [STATE_W-1:0] prev_q;
  logic sat_q;
 
  // ONE definition of state entry, shared by every consumer -- computing
  // it separately in each debug block is how they disagree (25.4 §5).
  assign state_enter = (state != prev_q);
  assign saturated   = sat_q;
  always_comb for (int i=0;i<N_STATE;i++) begin residency[i]=res_q[i]; entries[i]=ent_q[i]; end
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n || dbg_clear) begin
      prev_q<='0; sat_q<=1'b0;
      for (int i=0;i<N_STATE;i++) begin res_q[i]<='0; ent_q[i]<='0; end
    end else begin
      prev_q <= state;
      if (state < STATE_W'(N_STATE)) begin           // range-safe (23.6 pattern 11)
        if (state_enter) begin
          if (!(&ent_q[state])) ent_q[state] <= ent_q[state] + 32'd1;
        end
        if (&res_q[state]) sat_q <= 1'b1;            // saturate, report
        else res_q[state] <= res_q[state] + 32'd1;
      end
    end
  end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import pcie_dbg_pkg::*;
 
// SYNTHESIZABLE. Outstanding-work snapshot -- the "last known good vs
// expected next" instrument for the transaction layer (§5).
module outstanding_snapshot (
  input  logic clk,
  input  logic rst_n,
  input  logic req_transmitted,
  input  logic cpl_received,
  input  logic txn_retired,
  input  logic freeze,
  input  logic dbg_clear,
 
  output logic [15:0] outstanding,
  output logic [31:0] req_count,
  output logic [31:0] cpl_count,
  output logic [31:0] retired_count,
  output logic [15:0] frozen_outstanding,
  output logic        err_negative_outstanding    // sticky
);
  logic [15:0] out_q, froz_q;
  logic [31:0] rq_q, cp_q, rt_q;
  logic e_q;
 
  assign outstanding=out_q; assign req_count=rq_q; assign cpl_count=cp_q;
  assign retired_count=rt_q; assign frozen_outstanding=froz_q;
  assign err_negative_outstanding=e_q;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n || dbg_clear) begin
      out_q<='0; rq_q<='0; cp_q<='0; rt_q<='0; froz_q<='0; e_q<=1'b0;
    end else begin
      // NEXT-STATE arithmetic so a same-cycle transmit and retire is exact
      // (23.6 pattern 9).
      automatic logic [16:0] nxt = {1'b0,out_q} + 17'(req_transmitted) - 17'(txn_retired);
      if (nxt[16]) e_q <= 1'b1;                     // went negative: a lost retire
      else out_q <= nxt[15:0];
      if (req_transmitted && !(&rq_q)) rq_q <= rq_q + 32'd1;
      if (cpl_received    && !(&cp_q)) cp_q <= cp_q + 32'd1;
      if (txn_retired     && !(&rt_q)) rt_q <= rt_q + 32'd1;
      if (freeze) froz_q <= out_q;                  // the value AT the failure
    end
  end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import pcie_dbg_pkg::*;
 
// SYNTHESIZABLE. The debug top level -- one place that decides WHEN to
// freeze, so every instrument freezes on the same event (§10's audit).
module pcie_debug_top #(parameter int unsigned STALL_BUDGET = 65536) (
  input  logic clk,
  input  logic rst_n,
  input  logic        err_fire,
  input  logic        no_progress_in,
  input  logic        host_freeze_req,
  input  logic        dbg_clear,
 
  output logic        freeze,
  output logic [1:0]  freeze_reason      // 0 none, 1 error, 2 no-progress, 3 host
);
  logic froz_q; logic [1:0] rsn_q;
  assign freeze = froz_q; assign freeze_reason = rsn_q;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n || dbg_clear) begin froz_q<=1'b0; rsn_q<=2'd0; end
    else if (!froz_q) begin
      // PRIORITY, declared: an error is more informative than a timeout,
      // and both beat a host request (§10).
      if (err_fire)            begin froz_q<=1'b1; rsn_q<=2'd1; end
      else if (no_progress_in) begin froz_q<=1'b1; rsn_q<=2'd2; end
      else if (host_freeze_req)begin froz_q<=1'b1; rsn_q<=2'd3; end
    end
  end
endmodule

Classification: all seven synthesizable debug hooks.

Failure — seven. A status register overwritten by every error (96.8% wrong root cause). An event history that records offers rather than transfers. A frozen buffer that silently drops. A heartbeat reset by irrelevant activity. Residency counters that wrap. Each block computing its own "state entry". And a debug threshold described as a PCIe timeout.

10. Same-Cycle Audit and Assertions

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---- FIRST FAILURE (§6) -----------------------------------------
// P1: the first error is latched exactly once.
property p_first_capture_once;
  @(posedge clk) disable iff (!rst_n)
    ($rose(captured)) |-> $past(err_fire && !captured);
endproperty
 
// P2: the record is immutable until an EXPLICIT clear.
property p_first_record_sticky;
  @(posedge clk) disable iff (!rst_n)
    (captured && !dbg_clear) |=> (captured && $stable(first_code)
                                            && $stable(first_context));
endproperty
 
// P3: later errors are counted, never stored over the first. 21.4 §11:
// last-write-wins named a non-root cause 96.8% of the time.
property p_later_counted_not_stored;
  @(posedge clk) disable iff (!rst_n)
    (captured && err_fire && !dbg_clear) |=> $stable(first_cycle);
endproperty
 
// P4: the captured link state is the one at the failure, not afterwards.
property p_link_state_captured_at_failure;
  @(posedge clk) disable iff (!rst_n)
    ($rose(captured)) |-> (first_link_state == $past(link_state));
endproperty
 
// ---- EVENT HISTORY (§5) -----------------------------------------
// P5: an event is recorded once per push.
property p_push_once;
  @(posedge clk) disable iff (!rst_n)
    (push && !frozen) |=> (wr_ptr != $past(wr_ptr)) || wrapped;
endproperty
 
// P6: the write pointer is always in range -- HIST_DEPTH = 1 included.
property p_ptr_in_range;
  @(posedge clk) disable iff (!rst_n) (wr_ptr < HIST_W'(HIST_DEPTH));
endproperty
 
// P7: history records TRANSFERS, not offers -- the same rule as every
// counter in Modules 22-24.
property p_history_on_transfer;
  @(posedge clk) disable iff (!rst_n)
    push |-> event_transferred;
endproperty
 
// P8: a frozen history is IMMUTABLE.
property p_frozen_immutable;
  @(posedge clk) disable iff (!rst_n)
    frozen |=> $stable(history[0]);
endproperty
 
// P9: events arriving while frozen are COUNTED, not silently discarded.
property p_frozen_drops_counted;
  @(posedge clk) disable iff (!rst_n)
    (frozen && push) |=> (dropped_while_frozen == $past(dropped_while_frozen) + 1);
endproperty
 
// ---- PROGRESS AND RESIDENCY (§4) --------------------------------
// P10: the progress epoch advances only on a DECLARED progress event --
// not on clock activity, not on a counter ticking.
property p_progress_on_declared_event;
  @(posedge clk) disable iff (!rst_n)
    (progress_epoch != $past(progress_epoch)) |-> $past(progress_event);
endproperty
 
// P11: the state-entry pulse is exactly one cycle.
property p_state_enter_one_cycle;
  @(posedge clk) disable iff (!rst_n)
    state_enter |=> !state_enter || (state != $past(state));
endproperty
 
// P12: residency accrues to the state actually occupied.
property p_residency_correct_state;
  @(posedge clk) disable iff (!rst_n)
    (residency[state] != $past(residency[state])) |-> $past(state == state);
endproperty
 
// P13: entry counters increment once per entry, not per resident cycle.
property p_entries_once_per_entry;
  @(posedge clk) disable iff (!rst_n)
    (entries[0] != $past(entries[0])) |-> $past(state_enter);
endproperty
 
// P14: progress resets the stall counter and records the gap.
property p_progress_resets_gap;
  @(posedge clk) disable iff (!rst_n)
    progress_event |=> (cycles_since_progress == '0);
endproperty
 
// P15: the no-progress flag is a DEBUG HEURISTIC and is sticky.
property p_no_progress_sticky;
  @(posedge clk) disable iff (!rst_n)
    no_progress |=> (no_progress || dbg_clear);
endproperty
 
// P16: counters saturate rather than wrap -- a wrapped diagnostic makes a
// busy system look idle (22.1 §11 measured 232 after saturating at 255).
property p_counters_saturate;
  @(posedge clk) disable iff (!rst_n)
    (req_count == 32'hFFFF_FFFF) |=> (req_count == 32'hFFFF_FFFF);
endproperty
 
// ---- FREEZE AND COHERENCE ---------------------------------------
// P17: freeze reason follows the declared priority (§10's audit).
property p_freeze_priority;
  @(posedge clk) disable iff (!rst_n)
    (err_fire && no_progress_in && !freeze) |=> (freeze_reason == 2'd1);
endproperty
 
// P18: every instrument freezes on the SAME event -- one freeze signal.
property p_single_freeze_source;
  @(posedge clk) disable iff (!rst_n)
    freeze |-> (history_frozen && outstanding_frozen);
endproperty
 
// P19: no diagnostic output drives functional logic.
property p_diagnostics_non_functional;
  @(posedge clk) disable iff (!rst_n)
    $stable({req_transmitted, cpl_received}) or !$stable({req_count, cpl_count});
endproperty
 
// P20: outstanding never goes negative -- a retire with no transmit is a
// lost-ownership bug, reported rather than wrapped.
property p_outstanding_non_negative;
  @(posedge clk) disable iff (!rst_n) !err_negative_outstanding;
endproperty
 
// P21: the frozen outstanding value is the one at the failure instant.
property p_frozen_outstanding_at_failure;
  @(posedge clk) disable iff (!rst_n)
    $rose(freeze) |=> (frozen_outstanding == $past(outstanding, 2));
endproperty
 
// P22: EVIDENCE -- the instruments were actually exercised.
property c_error_captured;
  @(posedge clk) disable iff (!rst_n) captured;
endproperty

Twenty-two properties. P1–P4 are the first-failure contract, which §13's cases all depend on. P10 is the one people get wrong: a heartbeat driven by "anything happened" reports progress in a system that is spinning, which is §12's mutation 11.

11. Measured Behaviour

12. Verification — Mutations

#MutationSymptomCaught by
1Debug from the last symptomwrong first boundary in 4 of 15 cases (§11)review
2Capture an analyzer trace before forming a hypothesisdata with no question attached (§3)review
3First error overwritten by later onesnon-root cause 96.8% of the timeP2, P3
4Link state captured after the failurean error in Recovery read as an error in L0P4
5Event history records offers rather than transfersevents that never happened in the recordP7
6Frozen history keeps writingthe window around the fault is overwrittenP8
7Frozen history drops silentlythe window looks quieter than it wasP9
8History depth 1 breaks the pointer widthout-of-range writeP6
9History ring assumed power-of-twowrap at the wrong indexP6
10No timestamps in the recordevents cannot be ordered across instrumentsreview
11Heartbeat reset by any activitya spinning system reports progressP10
12The stall budget described as a PCIe timeoutan unobserved mechanism claimed (§1)review
13Residency counters wrapa stuck state reports a small numberP16
14Entry counter increments every resident cycleone entry counted thousands of timesP13
15Each block computes its own state-entry pulsethe instruments disagree about transitionsP11
16Instruments freeze on different eventsthe snapshots describe different instantsP18
17Outstanding allowed to wrap negativea lost retire becomes a huge countP20
18One global error context shared across portsport A's fault attributed to port Breview
19Debug register gates a functional paththe measured design is not the shipping oneP19
20"Link is L0" taken as "the system is healthy"three layers above it unexamined (§4)review
21No distinction between no-work and blocked-worka source problem investigated as a PCIe problemreview
22A downstream timeout blamed on the PHY immediatelythe loudest symptom, not the earliestreview
23A configuration failure diagnosed from DMA statewrong layer, wrong chapter (§7)review
24Timestamps from different clock domains compareda false ordering conclusionreview
25Freeze priority undeclaredthe reason register disagrees run to runP17
26Counters cleared before the evidence is readthe fault is masked by the cleanupP2
27A "proved" event taken from a driver printplausible values mistaken for evidence (§5)review
28The triage table used as a diagnosis rather than a routea class-specific fault half-solved herereview

Two counterexamples worth stating explicitly.

Mutation 1 is the default behaviour of every team under pressure, and §11 quantifies its cost. The symptom is "DMA hung", so the DMA engine is examined first. In 4 of 15 synthetic cases the true fault was a layer below — a link that never trained, a config path that never delivered, a credit that never returned — and each of those produces the DMA symptom faithfully. The chain's second step, which layer can produce this symptom, costs one minute and eliminates the wrong search.

Mutation 11 is subtle and it disables the most useful watchdog you have. A heartbeat driven by "any interface activity" is reset by retries, by polling, by an interrupt storm — all of which are things a hung system does energetically. The result is a no-progress flag that never fires on exactly the failures it exists to catch. §9's heartbeat takes a declared list of meaningful events, and P10 asserts nothing else advances it.

13. Eight Worked Cases

Each follows §2's chain. Each stops where another chapter takes over.

Case 1 — "No device in the OS list." Last known good: power and clock. Expected next: the LTSSM leaves Detect. Evidence: LTSSM state and its residency counter (§9). Experiment: does residency in Detect grow without bound, or does it cycle? Conclusion: never leaves Detect → 25.3; leaves and returns → 25.4.

Case 2 — "Link is up, still nothing in the OS list." Last known good: L0 reached, provable from the LTSSM. Expected next: a configuration request arrives. Evidence: the config-request counter (25.2 §5). Experiment: is the counter zero, or non-zero with no completions? Conclusion: zero → the request never reached the device (host or topology); non-zero → the device's response path → 25.2.

Case 3 — "Device appears, DMA never starts." Last known good: the Function enumerated. Expected next: descriptors accepted and requests transmitted. Evidence: bus mastering enabled (9.5 §8) and the descriptor counter. Experiment: read the Command register. Conclusion: bus mastering off → not an enumeration failure at all; on with no descriptors → the source, not PCIe (22.1 §6's no_work).

Case 4 — "DMA starts and hangs." Last known good: a request transmitted — req_count incremented. Expected next: a completion. Evidence: outstanding non-zero and unchanging, cpl_count flat, credit_ok high, link_l0 high (§8's figure). Experiment: eliminate credit and Link by inspection, then check whether the completion ever reached the wire. Conclusion: routes to 25.7.

Case 5 — "Everything stalls, no errors, Link healthy." Last known good: the Link in L0, packets pending. Expected next: transmission. Evidence: per-class credit availability with the pendency qualification (22.3 §5). Experiment: read the six pools separately and the head packet's class. Conclusion: one class at zero with a packet pending → 25.8.

Case 6 — "Intermittent corruption, only under load." Last known good: transactions completing correctly at low rate. Expected next: the same at high rate. Evidence: the first-failure record plus stall counters. Experiment: add backpressure deterministically. Conclusion: an ownership fault, not a protocol fault — 24.1 §7 measured every such defect in Modules 22–23 requiring a stall, and 23.6 §13 routes by pattern.

Case 7 — "Works on machine A, fails on machine B." Last known good: enumeration on both. Expected next: identical behaviour. Evidence: negotiated generation and width on each (17.3), and whether a switch is in the path. Experiment: compare negotiated — not capable — width and speed. Conclusion: a width or speed difference explains throughput and latency changes (22.6 §3); identical negotiation points at topology or platform.

Case 8 — "Six blocks are reporting errors." Last known good: whatever the earliest timestamp shows. Expected next:Evidence: the first-failure register (§6). Experiment: none yet — read the first record before touching anything. Conclusion: the six reports are one cause and five consequences 96.8% of the time (21.4 §11); debug the first.

14. Misconceptions

"Start by capturing a trace." A capture without a hypothesis is data, not evidence (§3).

"The symptom names the layer." Seven of eight distinct root causes produce "the device doesn't work" (25.2 §11).

"The latest error tells you what broke." It is a non-root cause 96.8% of the time (§6).

"The Link is in L0, so PCIe is fine." L0 is one of four evidence layers (§4).

"The stalled block is the broken block." Backpressure propagates (23.1 §4).

"A debug watchdog firing is a protocol violation." It is a local heuristic (§1, §9).

"If it works at low rate it is not a real bug." Load dependence is the signature of an ownership fault (§13, case 6).

"Nothing is happening, so nothing is wrong with PCIe." No-work and blocked-work are different findings (22.1 §6).

"The driver printed the value, so it read correctly." A plausible value is not proof (23.2 §16).

"More instrumentation is always better." An instrument that resets on irrelevant activity is worse than none (§12, mutation 11).

"Clear the counters and reproduce." Clearing before reading destroys the first-failure evidence (mutation 26).

"Capable width x16 means the link is x16." Read the negotiated width (17.3).

15. Understanding Check

Q1. "DMA is hung." What are the first two questions, and why in that order? Is the Link in L0? and is the request visible on the wire? (§3). The first eliminates two entire layers in one read; the second splits local causes (source, credit, Tag, TX path) from remote ones (completion path). §11 measured that skipping this ordering pointed at the wrong boundary in 4 of 15 cases, because a link fault, a config fault and a credit block all present as "DMA hung."

Q2. Six blocks report errors within a few microseconds. Which do you read? The first-failure record (§6). A cascade ends with consequences, and 21.4 §11 measured last-write-wins status naming a non-root cause 96.8% of the time. If the design has no sticky first-failure capture, the timestamped log is more trustworthy than any current-state register — because current state describes the aftermath.

Q3. What does "last known good" mean, and what does it exclude? The last event you can prove happened — from a counter, a sticky bit or a captured record (§5). It excludes anything inferred: a driver print, a plausible-looking register value, or "it must have worked because the next thing started." Chapter 23.2 §16's device returned entirely plausible values for addresses belonging to a different device.

Q4. Your progress heartbeat never fires on a hung system. What is likely wrong? It is reset by activity that is not progress (§9, mutation 11) — retries, polling, an interrupt storm. A hung system does all of those energetically. The heartbeat must advance only on a declared list of meaningful events (a state change, a completion retired, a descriptor retired), and P10 asserts nothing else advances it.

Q5. Why does §9's design have one freeze signal rather than one per instrument? Because the snapshots must describe the same instant (§10, P18). Instruments freezing on their own conditions produce a history from one cycle, an outstanding count from another and a residency table from a third — and correlating them then requires exactly the reasoning the freeze was supposed to remove. One source, one declared priority (error > no-progress > host request).

Q6. When is an analyzer the right tool? After the chain has produced a hypothesis (§3). Once you know the transaction, the direction and the time window, an analyzer answers a specific question extremely well. Chapter 25.9 owns using it; the claim here is only that its value scales with the precision of the question you bring to it.

16. What's Next

The method is the chapter. The next three apply it, each one layer lower.

Chapter 25.2 Enumeration Failures takes §13's case 2: the Link is operational and no usable Function appears. Its central instrument is a milestone register — because §11 measured that seven of eight distinct root causes produce the same OS-visible symptom.

Chapter 25.3 Link Training Failures takes case 1: the Link never reaches L0, and the question becomes which state's exit condition is missing.

Chapter 25.4 LTSSM Issues goes a level deeper still — not which state but which contract inside that state: entry actions, guards, timers and the priority between simultaneous exits.

And the rest of Module 25 takes the remaining rows of §7's table. 25.5 BAR, 25.6 DMA, 25.7 completion timeouts, 25.8 credit deadlocks, 25.9 the analyzer. Every one of them begins by asking §5's question, which is why this chapter comes first.