PCIe · Module 18
Detect — Is Anything There?
Before training, before negotiation, before any bit is exchanged, the port must answer one question: is a receiver connected? Detect answers it electrically, in hardware, with no cooperation from the other end.
Chapter 18.1 framed the LTSSM as the machine that decides what the Link is trying to do. This is the first thing it tries.
Everything Module 17 built assumed a partner. Chapter 17.2's aligner needed a stream to align to. Chapter 17.3's negotiation needed someone to negotiate with.
At power-up none of that is known. Traces are routed, a connector may or may not have a card in it, and the port has no idea whether anything is on the other end.
And it cannot find out by transmitting and listening, because a receiver that is not there will not answer, and a receiver that is there is not yet doing anything either.
How does a port establish that a receiver physically exists before any signalling is possible, and what digital state must surround an operation that is fundamentally electrical?
1. The Verified Substates
2. Why This Cannot Be Answered by Transmitting
The obvious approach fails, and understanding why is the chapter.
"Send something and see if anyone replies" requires a partner that is already listening, already locked, and already willing to respond. At this point none of that is true — the far end may itself be in Detect, may be unpowered, or may not exist.
So detection has to work on a port that is doing nothing at all, which rules out every protocol-level mechanism and leaves only the electrical one: does the far end present the characteristics of a terminated receiver?
3. Detect Is Not Enumeration
The first terminology collision Chapter 18.1 §4 warned about, and here it is concrete.
| Detect (LTSSM) | Device discovery (7.3) | |
|---|---|---|
| Asks | is a receiver electrically present? | what Functions exist and how are they addressed? |
| Performed by | hardware, the PHY | software |
| Produces | a presence result | Vendor/Device IDs, bus numbers, BARs |
| Needs | nothing but electrical access | a fully trained Link |
| Happens | first | much later |
Detect learns nothing about identity. It does not know whether the thing on the other end is a network card or a GPU, what its Vendor ID is, or whether it has one Function or eight. It knows that something with a receiver's electrical signature is there.
And the ordering is strict. Configuration accesses are TLPs; TLPs need a trained Link; a trained Link needs Detect to have succeeded. A device whose Detect never succeeds is not a device that enumerates badly — it is electrically invisible, and no amount of software will find it.
4. Detect Is Not Signal Detect
A subtler and more damaging conflation, because both involve "is there something on this lane".
| Receiver detection | Signal detect / LOS | |
|---|---|---|
| Initiated by | the transmitter side, deliberately | continuous, receiver side |
| Asks | is a receiver terminated on the far end? | is the input above a threshold right now? |
| When | during Detect, as an operation | always, once powered |
| Answer means | a partner exists | someone is transmitting |
5. The Detect Subtree
Two things to read out of the figure, and one not to.
There is no path from "not present" to Polling. Every failing route leads back to waiting and trying again (§6). That absence is the most important feature of the diagram.
The interval between attempts is a real element, not padding — §1's source names a substate whose entire job is "Wait 12mS between Rx Detection attempts." Detection is not free and is not instantaneous, and a design that retried continuously would be doing electrical work at a rate nobody specified.
What not to read into it: this is the Detect subtree only. Entry conditions from other states, other exits, and the speed-change substates §1 lists as implementation detail are deliberately absent — Chapter 18.1 §6 owns the full map.
6. Retry, Not Fabricate
The behavioural rule that everything else in the chapter enforces.
receiver present → proceed toward Polling
receiver NOT present → wait, and try againNever: not present → proceed anyway.
7. Candidate Lanes Are Not a Width
Detection on a multi-lane port produces per-lane results, and it is tempting to read them as the Link's width. They are not.
Detect → which lanes have a receiver present ← candidates
Polling → which lanes can carry trained communication
Configuration → which lanes form one logical Link, and how wide it isEach stage can reduce the set. A lane with a receiver present may fail to achieve lock in Polling. A set of lanes that all pass Polling may still not form a legal Link width in Configuration (Chapter 18.4 §8).
So §12's mask is named candidate_lane_mask, deliberately, and this chapter makes no claim that it becomes the width. Chapter 17.3 §11 already showed that five usable lanes yield x4 — the reduction from candidates to a committed width is Configuration's, and doing it here would both be wrong and consume Chapter 18.4.
8. A Trace
Internal teaching signals, not PCIe pins. Two attempts, the first finding nothing.
step 1 2 3 4 5 6 7 8 9
state QUI ACT ACT WAIT WAIT ACT ACT ACT →POL
detect_start 0 1 0 0 0 1 0 0 0
detect_busy 0 1 1 0 0 1 1 1 0
detect_done 0 0 1 0 0 0 0 1 0
rx_present - - 0 - - - - 1 -
attempt_id 0 1 1 1 1 2 2 2 2
result_valid 0 0 1 0 0 0 0 1 1
candidate_mask 00 00 00 00 00 00 00 0F 0F
traffic_enabled 0 0 0 0 0 0 0 0 0Read steps 2–3. An attempt is issued and completes with rx_present = 0. The result is captured against attempt 1.
Read steps 4–5. The interval. Nothing is being transmitted and no detection is running — this is the wait §1's source names.
Read steps 6–8. A second attempt, this time finding a receiver. attempt_id is 2, so the result is unambiguously attributed (§10).
Read step 8's mask. Four lanes report present. That is a candidate set, not a width (§7).
And note traffic_enabled across the whole trace. It is zero everywhere — normal traffic is not legal in Detect, and Chapter 18.1 §13's gate enforces it.
9. The Boundary
10. RTL — Detection Command and Result Ownership
// SYNTHESIZABLE. Digital wrapper around the PHY's receiver-detection
// operation.
// THAT RECEIVER DETECTION IS A PHY OPERATION WITH A DEFINED NOT-PRESENT
// OUTCOME is vendor-verified (section 1). The attempt-ID mechanism, the
// no-overlap rule and the error reports are ILLUSTRATIVE IMPLEMENTATION
// POLICY. The electrical operation itself is NOT modelled (section 9).
package detect_pkg;
// An attempt identifier, wide enough that a stale result from an
// abandoned attempt cannot alias the current one within any plausible
// window. Two bits would wrap in four attempts; this is cheap insurance.
localparam int ATT_W = 4;
typedef struct packed {
logic valid;
logic [ATT_W-1:0] attempt;
logic present;
} detect_result_t;
endpackageimport detect_pkg::*;
module detect_attempt_ctrl #(
parameter int LANES = 16,
// GUARDED. $clog2(1) is zero; LANES = 1 is a legal configuration.
parameter int LANE_W = (LANES <= 1) ? 1 : $clog2(LANES)
) (
input logic clk,
input logic rst_n,
// ---- From the Detect state logic ---------------------------------------
input logic start_attempt,
output logic busy,
// ---- To the PHY macro ---------------------------------------------------
// A DECOUPLED command. The macro may not be able to accept immediately,
// and the request must survive that without being reissued.
output logic detect_cmd_valid,
input logic detect_cmd_ready,
// ---- From the PHY macro -------------------------------------------------
input logic detect_done,
input logic detect_present,
// ---- Result, attributed to a specific attempt --------------------------
output detect_result_t result,
output logic [ATT_W-1:0] attempt_id,
output logic err_start_while_busy,
output logic err_unowned_result
);
generate
if (LANES < 1) $error("LANES must be at least 1");
if (LANE_W < 1) $error("LANE_W must be at least 1");
endgenerate
typedef enum logic [1:0] { S_IDLE, S_ISSUE, S_WAIT, S_REPORT } st_e;
st_e st_q;
logic [ATT_W-1:0] att_q;
detect_result_t res_q;
logic esb_q, eur_q;
assign busy = (st_q != S_IDLE);
assign attempt_id = att_q;
assign result = res_q;
assign err_start_while_busy = esb_q;
assign err_unowned_result = eur_q;
// ==================================================================
// THE COMMAND IS A LEVEL, HELD UNTIL THE MACRO ACCEPTS IT.
//
// Pulsing it for one cycle loses the request whenever the macro is not
// ready that cycle -- and the machine then waits forever for a result
// to an operation that was never started (section 14, mutation 1).
// ==================================================================
assign detect_cmd_valid = (st_q == S_ISSUE);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
st_q <= S_IDLE;
att_q <= '0;
res_q <= '0;
esb_q <= 1'b0; eur_q <= 1'b0;
end else begin
unique case (st_q)
S_IDLE :
if (start_attempt) begin
// A NEW ATTEMPT GETS A NEW IDENTIFIER, allocated here and
// never again -- so a result can be checked against it.
att_q <= att_q + ATT_W'(1);
res_q.valid <= 1'b0;
st_q <= S_ISSUE;
end
// Hold the command until accepted. EXACTLY ONE acceptance per
// start, because the state leaves S_ISSUE on the handshake.
S_ISSUE :
if (detect_cmd_ready) st_q <= S_WAIT;
S_WAIT :
if (detect_done) begin
// ==========================================================
// THE RESULT IS CAPTURED, NOT SAMPLED LATER.
//
// detect_present is only meaningful in the cycle detect_done
// asserts. Reading it afterwards reads whatever the macro has
// moved on to -- section 14's mutation 8.
// ==========================================================
res_q.valid <= 1'b1;
res_q.attempt <= att_q;
res_q.present <= detect_present;
st_q <= S_REPORT;
end
S_REPORT :
st_q <= S_IDLE;
default : st_q <= S_IDLE;
endcase
// NO OVERLAPPING ATTEMPTS in this teaching model. A start while busy
// is a caller error and is REPORTED rather than silently starting a
// second operation whose result could not be attributed.
if (start_attempt && busy) esb_q <= 1'b1;
// A result arriving with no attempt outstanding cannot belong to
// anything. Reported, never captured.
if (detect_done && (st_q != S_WAIT)) eur_q <= 1'b1;
end
end
endmoduleClassification: synthesizable.
Architecture. A four-state wrapper with an attempt identifier allocated at start and a result captured in the cycle the macro reports done.
The attempt ID is not decoration. It is what makes "this result belongs to that request" checkable — by the design, by P4, and by the scoreboard. Without it, a late result from an abandoned attempt is indistinguishable from a fresh one.
Cycle behaviour.
| Situation | Result |
|---|---|
start_attempt while idle | new attempt ID; command asserted |
| macro not ready | command held, not reissued |
| macro accepts | one acceptance, exactly once |
detect_done while waiting | result captured with the current attempt ID |
detect_done while not waiting | reported, never captured |
start_attempt while busy | reported, no second operation |
| reset mid-attempt | attempt abandoned, result invalid |
Failure — five. Pulsing the command loses it when the macro stalls, and the machine then waits forever. Leaving S_ISSUE on something other than the handshake can issue twice (§14). Sampling detect_present after detect_done reads a signal that is no longer meaningful. Omitting the attempt ID makes a stale result indistinguishable from a current one. And allowing overlapping attempts creates results that cannot be attributed at all.
11. RTL — Detect Interval Timer
// SYNTHESIZABLE. The wait between detection attempts.
// THE INTERVAL IS A PARAMETER. Section 1's source states 12 ms for that
// vendor device; this chapter does not publish it as a PCIe constant, and
// a design must take the required interval from the specification.
// Contract identical to Chapter 18.1 section 11's timer primitive.
module detect_interval #(
parameter int CNT_W = 24
) (
input logic clk,
input logic rst_n,
input logic enable,
input logic restart,
input logic [CNT_W-1:0] limit, // interval, in local cycles
output logic [CNT_W-1:0] count,
output logic elapsed
);
generate if (CNT_W < 1) $error("CNT_W must be at least 1"); endgenerate
logic [CNT_W-1:0] cnt_q;
assign count = cnt_q;
// limit == 0 DISABLES, never expires instantly (Chapter 18.1 section 11).
assign elapsed = enable && (limit != '0) && (cnt_q >= limit);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) cnt_q <= '0;
else if (restart) cnt_q <= '0; // restart beats counting
else if (enable && !elapsed) cnt_q <= cnt_q + CNT_W'(1); // saturates
end
endmoduleClassification: synthesizable.
Same three declared contracts as Chapter 18.1 §11 — limit == 0 disables, restart beats counting, the counter saturates — reused deliberately rather than reimplemented with different semantics. A design with two timers that disagree about what limit = 0 means has a bug waiting in whichever one is configured last.
12. RTL — Candidate Lane Mask
import detect_pkg::*;
// SYNTHESIZABLE. Accumulate per-lane detection results into a candidate
// mask.
// THIS IS CANDIDATE PRESENCE, NOT NEGOTIATED WIDTH (section 7). Chapter
// 18.4 owns the reduction from candidates to a committed configuration.
module detect_lane_mask #(
parameter int LANES = 16,
parameter int LANE_W = (LANES <= 1) ? 1 : $clog2(LANES)
) (
input logic clk,
input logic rst_n,
// A new attempt invalidates everything the previous one found.
input logic attempt_begin,
input logic lane_result_valid,
input logic [LANE_W-1:0] lane_id,
input logic lane_present,
output logic [LANES-1:0] candidate_lane_mask,
output logic lane_range_error
);
generate
if (LANES < 1) $error("LANES must be at least 1");
if (LANE_W < 1) $error("LANE_W must be at least 1");
if ((LANES > 1) && ((1 << LANE_W) < LANES))
$error("LANE_W too narrow to index LANES");
endgenerate
// RANGE SAFETY on an externally-supplied lane id, checked one bit wider
// BEFORE any indexing. A LANE_W-wide value can name a lane that does not
// exist whenever LANES is not a power of two.
localparam int CHK = LANE_W + 1;
wire lane_legal = (CHK'(lane_id) < CHK'(LANES));
logic [LANES-1:0] mask_q;
logic err_q;
assign candidate_lane_mask = mask_q;
assign lane_range_error = err_q;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
mask_q <= '0; err_q <= 1'b0;
end else begin
// ==============================================================
// A NEW ATTEMPT CLEARS THE MASK, and this ordering is deliberate:
// checked FIRST, so a result arriving in the same cycle as a new
// attempt cannot be counted against the old one. Accumulating
// across attempts would report lanes that were present once, ever
// (section 14, mutation 6).
// ==============================================================
if (attempt_begin) begin
mask_q <= '0;
end else if (lane_result_valid && lane_legal) begin
// ONE BIT, THE NAMED ONE. Assigning the whole vector here is
// mutation 12, and it makes every lane share one lane's answer.
mask_q[lane_id] <= lane_present;
end
if (lane_result_valid && !lane_legal) err_q <= 1'b1;
end
end
endmoduleClassification: synthesizable.
Architecture. A bit per lane, cleared on a new attempt, written one bit at a time by a range-checked index.
The clear-on-new-attempt ordering is the interesting part. Checked before the result arm, so a lane result coinciding with a new attempt is attributed to the new attempt's (empty) mask rather than the old one's. Accumulating across attempts would produce a mask meaning "lanes that were ever present", which is not a question anybody asked.
Failure — four. Accumulating across attempts reports stale presence. Indexing before the range check sets a bit for a lane that does not exist, or aliases lane 0. Assigning the whole mask instead of one bit gives every lane one lane's result. And treating this mask as the width is §7's error, and belongs to Chapter 18.4.
13. Assertions
// SVA over detect_attempt_ctrl, detect_interval and detect_lane_mask.
// These assert the ATTEMPT-OWNERSHIP contract. They assert NOTHING about
// the electrical detection operation (section 9), nothing about Polling
// (Chapter 18.3), and nothing about a receiver eventually existing --
// which depends on whether a card is plugged in.
// ---- ENVIRONMENT ------------------------------------------------------
// A1: detect_present is meaningful only in the cycle detect_done asserts.
assume property (@(posedge clk) disable iff (!rst_n)
detect_done |-> !$isunknown(detect_present));
// A2: the macro asserts detect_done only for an operation it accepted.
assume property (@(posedge clk) disable iff (!rst_n)
detect_done |-> $past(detect_cmd_valid && detect_cmd_ready, 1, 1, 64));
// ---- COMMAND OWNERSHIP ------------------------------------------------
// P1: THE COMMAND IS STABLE UNDER STALL. It is a held level, not a pulse.
property p_cmd_held;
@(posedge clk) disable iff (!rst_n)
(detect_cmd_valid && !detect_cmd_ready) |=> detect_cmd_valid;
endproperty
a_held : assert property (p_cmd_held);
// P2: ONE START, AT MOST ONE COMMAND ACCEPTANCE. The chapter's central
// ownership property -- section 14's counterexample fails it.
// (g_starts and g_cmds are testbench counters.)
property p_one_command_per_start;
@(posedge clk) disable iff (!rst_n)
(g_cmds <= g_starts);
endproperty
a_once : assert property (p_one_command_per_start);
// P2b: and the command drops immediately on acceptance -- it cannot be
// accepted twice in consecutive cycles.
property p_cmd_drops_on_accept;
@(posedge clk) disable iff (!rst_n)
(detect_cmd_valid && detect_cmd_ready) |=> !detect_cmd_valid;
endproperty
a_drop : assert property (p_cmd_drops_on_accept);
// ---- RESULT OWNERSHIP -------------------------------------------------
// P3: a result is captured only while an attempt is outstanding.
property p_result_needs_attempt;
@(posedge clk) disable iff (!rst_n)
$rose(result.valid) |-> ($past(st_q) == S_WAIT);
endproperty
a_owned : assert property (p_result_needs_attempt);
// P4: THE RESULT NAMES THE ATTEMPT THAT PRODUCED IT. Without this, a late
// result from an abandoned attempt is indistinguishable from a fresh one.
property p_result_attributed;
@(posedge clk) disable iff (!rst_n)
$rose(result.valid) |-> (result.attempt == attempt_id);
endproperty
a_attributed : assert property (p_result_attributed);
// P5: the captured result is STABLE -- present is not re-sampled later.
property p_result_stable;
@(posedge clk) disable iff (!rst_n)
(result.valid && !start_attempt) |=> $stable(result.present);
endproperty
a_stable : assert property (p_result_stable);
// P6: A NOT-PRESENT RESULT CANNOT REPORT PRESENCE. The property that
// forbids fabricating a partner (section 6).
property p_absent_is_absent;
@(posedge clk) disable iff (!rst_n)
($rose(result.valid) && !$past(detect_present)) |-> !result.present;
endproperty
a_absent : assert property (p_absent_is_absent);
// P7: an unowned result is REPORTED and never captured.
property p_unowned_reported;
@(posedge clk) disable iff (!rst_n)
(detect_done && (st_q != S_WAIT)) |=> (err_unowned_result && !result.valid);
endproperty
a_unowned : assert property (p_unowned_reported);
// P8: NO OVERLAPPING ATTEMPTS -- a start while busy is refused and
// reported, not silently started.
property p_no_overlap;
@(posedge clk) disable iff (!rst_n)
(start_attempt && busy) |=> (err_start_while_busy && $stable(attempt_id));
endproperty
a_overlap : assert property (p_no_overlap);
// ---- LANE MASK --------------------------------------------------------
// P9: the mask changes only on a valid, in-range lane result or a new
// attempt.
property p_mask_change_owned;
@(posedge clk) disable iff (!rst_n)
(!$stable(candidate_lane_mask))
|-> $past(attempt_begin || (lane_result_valid && lane_legal));
endproperty
a_mask : assert property (p_mask_change_owned);
// P10: a result writes ONE lane's bit -- never the whole vector.
property p_one_lane_bit;
@(posedge clk) disable iff (!rst_n)
($past(lane_result_valid && lane_legal && !attempt_begin))
|-> ($countones(candidate_lane_mask ^ $past(candidate_lane_mask)) <= 1);
endproperty
a_one_bit : assert property (p_one_lane_bit);
// P11: an out-of-range lane id touches nothing and is reported.
property p_lane_range;
@(posedge clk) disable iff (!rst_n)
(lane_result_valid && !lane_legal)
|=> (lane_range_error && $stable(candidate_lane_mask));
endproperty
a_range : assert property (p_lane_range);
// P12: A NEW ATTEMPT CLEARS THE MASK -- no accumulation across attempts.
property p_mask_cleared;
@(posedge clk) disable iff (!rst_n)
attempt_begin |=> (candidate_lane_mask == '0);
endproperty
a_clear : assert property (p_mask_cleared);
// ---- TIMER ------------------------------------------------------------
// P13: the interval timer's three declared contracts (Chapter 18.1 §11).
property p_timer_restart;
@(posedge clk) disable iff (!rst_n) restart |=> (count == '0);
endproperty
a_restart : assert property (p_timer_restart);
property p_timer_zero_disables;
@(posedge clk) disable iff (!rst_n) (limit == '0) |-> !elapsed;
endproperty
a_zero : assert property (p_timer_zero_disables);
// ---- SCOPE ------------------------------------------------------------
// P14: NO NORMAL TRAFFIC IN DETECT. Bound against Chapter 18.1's gate.
property p_no_traffic_in_detect;
@(posedge clk) disable iff (!rst_n)
(dut_ltssm.cur_state == ST_DETECT) |-> !dut_gate.traffic_enabled;
endproperty
a_no_traffic : assert property (p_no_traffic_in_detect);
// P15: reset abandons any pending attempt and clears the result.
property p_reset;
@(posedge clk)
!rst_n |=> (!busy && !result.valid && (candidate_lane_mask == '0));
endproperty
a_reset : assert property (p_reset);P2 and P2b are the command-ownership pair. P2 is the counting statement — acceptances never exceed starts — and P2b is the structural one that makes it true: the command drops on the handshake. §14's counterexample fails both.
P3 and P4 are the result-ownership pair. P3 says a result requires an outstanding attempt; P4 says the result names it. A design can satisfy P3 and still misattribute if attempts can overlap, which is why P8 forbids that in this model.
P6 is short and is the most important property in the chapter. A not-present result cannot report presence. Everything else is bookkeeping; this one is the rule that a Link is never fabricated (§6).
No liveness is asserted. "A receiver eventually appears" depends on whether a card is plugged in.
14. Verification and Fault Injection
The scoreboard tracks attempts and results independently — counting starts, command acceptances and results, and maintaining its own expected mask — and never reads st_q, att_q or mask_q.
Command and result
- Receiver present, first attempt — verify one command, one result,
presenthigh. - Receiver absent — verify one result with
presentlow and no advance (P6). - Macro command stalled for many cycles — verify the command is held, and accepted exactly once (P1, P2b). Required.
detect_donein the earliest possible cycle after acceptance.detect_donewith no attempt outstanding — verify reported, not captured (P7).start_attemptwhile busy — verify reported, no second attempt (P8). Required.- Repeated absent attempts — verify each gets a distinct attempt ID.
- Reset mid-attempt — verify nothing is reported afterwards (P15).
Lane mask
LANES= 1, 2, 4, 8, 16 — including the$clog2(1)corner. Required.- All lanes present, then some absent — verify per-lane accuracy.
- A new attempt after a partial mask — verify it clears (P12). Required.
- An out-of-range lane id — verify nothing touched and reported (P11).
- A lane result coinciding with
attempt_begin— verify it is attributed to the new attempt.
Timer
limit = 0— verify disabled, not instant.restartand a tick in the same cycle — verify restart wins.
Mutations
| # | Mutation | Caught by | Lab symptom |
|---|---|---|---|
| 1 | command pulsed for one cycle | P1 | detection never starts if the macro stalls; stuck in Detect forever |
| 2 | same attempt issued twice | P2, P2b | two electrical operations per request; wasted time, confused results |
| 3 | result attributed to the wrong attempt | P4 | a stale "absent" answer blocks a partner that is present |
| 4 | absent interpreted as present | P6 | proceeds to Polling with no partner; fails looking like a training bug |
| 5 | stale result survives reset | P15 | first attempt after reset returns the previous run's answer |
| 6 | mask accumulates across attempts | P12 | lanes that were once present reported forever |
| 7 | out-of-range lane id aliases lane 0 | P11 | a phantom lane 0 candidate |
| 8 | detect_present sampled after done | P5 | intermittently wrong presence, timing-dependent |
| 9 | normal traffic enabled in Detect | P14 | packets pushed into a Link with no partner |
| 10 | not-present path advances to Polling | P6, and 18.3's entry condition | Polling failure reported for an empty slot |
| 11 | command retriggered while busy | P8 | overlapping operations; unattributable results |
| 12 | one lane result writes the whole mask | P10 | every lane reports one lane's answer |
15. Debugging
Symptom → state → signal → distinguishing experiment.
The LTSSM never leaves Detect
This is Detect doing its job, and the question is which half is failing.
- Is
detect_cmd_validever asserted? If not, the state logic is not requesting detection at all. - Is it ever accepted? A command asserted and never accepted means the macro is not ready — a PHY configuration or power-state problem, not a Link problem.
- Does
detect_donearrive? If a command was accepted and no result comes back, the operation is not completing. - What does
rx_presentsay? If it says absent, believe it — the answer is physical.
At step 4 the investigation leaves the digital domain entirely: card seating, slot power, connector contact, lane routing, and the far end's own power state. Nothing in the packet layers is relevant, and no amount of driver work will change the answer.
An endpoint works in one slot and not another
A physical difference between two paths, and Detect is the earliest place it shows.
Inspect the per-lane candidate mask in both slots. If the same card produces different masks, the difference is the slot, the riser, or the routing to it.
The distinguishing experiment: move a known-good card into the failing slot. If it also fails to detect, the slot is implicated; if it detects, the original card's transmitter or termination is.
An x8 card shows fewer candidate lanes than expected
A per-lane physical problem — and the mask says exactly which lanes.
Inspect which bits are clear, and whether they are contiguous. Lanes 4–7 all absent suggests a connector half or a routing group; a single scattered lane suggests one trace or one pin.
And do not call the mask a width. §7: this is candidate presence. A reduced mask does not yet mean a reduced Link — Chapter 18.4 decides that, and it may reduce further.
Detection is intermittent
Suspect the digital wrapper before the physical path (§14's counterexample).
Check whether more command acceptances occur than starts — that single count comparison distinguishes a double-issue bug from a marginal electrical connection, and it is cheap to instrument.
16. Common Misconceptions
- "Detect discovers the Device and Vendor ID." It learns nothing about identity. Discovery is software, much later, over a trained Link (§3).
- "Detect means signal detect." Receiver detection is a transmitter-initiated operation; signal detect is a continuous receiver-side threshold (§4).
- "A routed lane means a receiver is present." Routing is a board fact; presence is an electrical measurement (§2).
- "CDR lock is needed before receiver detection." The opposite order — detection precedes any signalling, and there is nothing to lock to yet (§4).
- "Detect is software-controlled." Hardware, at Link timescales (Chapter 18.1 §3).
- "A lane with no receiver can still be trained." It cannot, and attempting it wastes the whole training sequence (§6).
- "Detect determines the final x8 or x4 width." It produces candidates; Chapter 18.4 commits a width (§7).
- "The detection operation can be implemented in ordinary RTL." It is analog; the RTL is the wrapper (§9).
- "
receiver_presentcan be sampled whenever convenient." It is meaningful in the cycledoneasserts, and belongs to a specific attempt (§10, P5). - "With no partner, proceeding to Polling is harmless." It converts a clear diagnostic into a misleading one, and transmits into an unterminated line (§6).
- "Two detection attempts mean something is wrong." §1's source shows a first attempt, an interval, and a second if needed — retry is the design.
17. Understanding Check
18. What's Next
Detect answers the first question any Link must answer, and it answers it electrically: a receiver terminates the line, an empty trace does not, and the PHY can tell the difference before any signalling exists.
The digital contribution is ownership. One request, one operation; a result that names its attempt; a candidate mask that clears when a new attempt begins; and — most importantly — no path from "nobody there" to "proceed anyway."
Chapter 18.3 — Polling takes the next question, and it is a much harder one. A receiver being present says nothing about whether anything can be communicated over the lane. Timing must be recovered, boundaries found, and training information recognised repeatedly enough to be trusted — which is the step where signal quality stops being theoretical.
The idea to carry forward: a state machine's diagnostic value comes from stopping exactly where the truth is.