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.
| Step | The question | Why it shrinks the space |
|---|---|---|
| 1. Symptom | what was observed, precisely? | "DMA hung" and "DMA returned wrong data" are different searches |
| 2. Layer | which layer can produce that symptom? | eliminates two thirds of the design immediately |
| 3. Last known good | what is the last event I can prove happened? | converts a search into a state transition (§5) |
| 4. Expected next | what should have happened after it? | a transition has few, enumerable preconditions |
| 5. Blocker | which precondition or resource is absent? | this is usually the answer |
| 6. Experiment | what 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.
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:
| Layer | One question | One place to look |
|---|---|---|
| Link | is it in L0, and has it stayed there? | LTSSM state + a Recovery entry count |
| Data Link | is replay/retry active? | replay counter, error counters |
| Transaction | are requests leaving and completions returning? | outstanding count, completion count |
| System | is 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.
| Symptom | Likely layer | Proof to gather | Owned by |
|---|---|---|---|
| nothing in the OS device list, no link | Link / PHY | LTSSM state; did it leave Detect? | 25.3 |
| link up, nothing in the device list | Configuration | config-request and completion counters | 25.2 |
| link up, then drops repeatedly | Link state | Recovery entry count, residency | 25.4 |
| device visible, registers read as garbage | Configuration / decode | which aperture claimed the address | 25.5 |
| device visible, DMA never starts | System | bus mastering, descriptor activity | 25.6 |
| DMA starts, never completes | Transaction | outstanding count, completion count | 25.7 |
| everything stalls, link healthy, no errors | Flow Control | per-class credit, pending packets | 25.8 |
| intermittent, load-dependent corruption | Transaction / ownership | first-failure record, stall counters | 23.6 §13 |
| works on one machine, not another | System / topology | negotiated width and speed; switch in path | 25.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 cyclesFour 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
// 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;
endpackageimport 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
endmoduleimport 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
endmoduleimport 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
endmoduleimport 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
endmoduleimport 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
endmoduleimport 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
endmoduleClassification: 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
// ---- 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;
endpropertyTwenty-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
| # | Mutation | Symptom | Caught by |
|---|---|---|---|
| 1 | Debug from the last symptom | wrong first boundary in 4 of 15 cases (§11) | review |
| 2 | Capture an analyzer trace before forming a hypothesis | data with no question attached (§3) | review |
| 3 | First error overwritten by later ones | non-root cause 96.8% of the time | P2, P3 |
| 4 | Link state captured after the failure | an error in Recovery read as an error in L0 | P4 |
| 5 | Event history records offers rather than transfers | events that never happened in the record | P7 |
| 6 | Frozen history keeps writing | the window around the fault is overwritten | P8 |
| 7 | Frozen history drops silently | the window looks quieter than it was | P9 |
| 8 | History depth 1 breaks the pointer width | out-of-range write | P6 |
| 9 | History ring assumed power-of-two | wrap at the wrong index | P6 |
| 10 | No timestamps in the record | events cannot be ordered across instruments | review |
| 11 | Heartbeat reset by any activity | a spinning system reports progress | P10 |
| 12 | The stall budget described as a PCIe timeout | an unobserved mechanism claimed (§1) | review |
| 13 | Residency counters wrap | a stuck state reports a small number | P16 |
| 14 | Entry counter increments every resident cycle | one entry counted thousands of times | P13 |
| 15 | Each block computes its own state-entry pulse | the instruments disagree about transitions | P11 |
| 16 | Instruments freeze on different events | the snapshots describe different instants | P18 |
| 17 | Outstanding allowed to wrap negative | a lost retire becomes a huge count | P20 |
| 18 | One global error context shared across ports | port A's fault attributed to port B | review |
| 19 | Debug register gates a functional path | the measured design is not the shipping one | P19 |
| 20 | "Link is L0" taken as "the system is healthy" | three layers above it unexamined (§4) | review |
| 21 | No distinction between no-work and blocked-work | a source problem investigated as a PCIe problem | review |
| 22 | A downstream timeout blamed on the PHY immediately | the loudest symptom, not the earliest | review |
| 23 | A configuration failure diagnosed from DMA state | wrong layer, wrong chapter (§7) | review |
| 24 | Timestamps from different clock domains compared | a false ordering conclusion | review |
| 25 | Freeze priority undeclared | the reason register disagrees run to run | P17 |
| 26 | Counters cleared before the evidence is read | the fault is masked by the cleanup | P2 |
| 27 | A "proved" event taken from a driver print | plausible values mistaken for evidence (§5) | review |
| 28 | The triage table used as a diagnosis rather than a route | a class-specific fault half-solved here | review |
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.