Skip to content

PCIe · Module 27

Senior PCIe Questions — Isolating a Cause, Not Reciting a Mechanism

Senior interviews test isolation under ambiguity. Six causes produce one symptom and are separated by their signatures; a 60% credit-stall rate changed throughput by nothing; and an exclusive if/else made a tag pool report itself full.

Chapter 27.3 closed on a standard: at advanced level, "I would check the specification" is half an answer, and the other half is the derivation you can do without the value.

Senior level moves the question again. You are no longer asked what a mechanism is, or even how it works. You are given a symptom with several possible causes and asked how you would tell them apart — and the answer is judged on whether your method actually discriminates.

1. Sources, Scope, and What This Chapter Refuses to Do

2. What the Senior Level Actually Tests

Six things, and none of them is vocabulary.

Isolation under ambiguity. Given a symptom with several causes, can you name an observable that discriminates? §3 is the canonical case.

Ownership and concurrency reasoning. Who owns this resource, from when to when, and what happens when two events land in the same cycle? §8 and §9.

Reading RTL for correctness rather than for syntax. Given a block, can you find the case it gets wrong? §8, §10, §11.

Knowing what an instrument does not measure. A counter that reads zero, or reads large, often means something other than the obvious. §15 is measured proof.

Separating the layer that broke from the layer that reported it. A protocol bug, an RTL bug, a driver bug and a platform limit produce overlapping symptoms. §12.

And knowing the limits of your own knowledge precisely. Not "I'd look it up" — which value you would look up, and what you can derive without it.

A weak answer lists candidates — "could be MPS, could be credits, could be the host" — and stops. It is not wrong; it just does not converge. Ten minutes later the interviewer knows nothing about how you would proceed.

A slightly better answer proposes changing something and re-measuring. That is a real technique and it is expensive: each experiment costs a build or a reboot, and a change that produces no improvement teaches almost nothing, because most changes produce no improvement when you have not found the limit.

The reasoning path. Throughput on a requester-driven path is bounded by

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
throughput ≤ (outstanding_bytes) / (round_trip_time)
outstanding_bytes = outstanding_requests × bytes_per_request

So there are exactly three families of cause, and each has its own observable:

familyquestionobservable
not enough requests outstandingare we filling the pipe?tag/outstanding occupancy
requests too smallis each one carrying enough?MRRS, actual request size on the wire
round trip too longis each one taking too long?measured completion latency

Then the fourth question, which is upstream of all three: is anything even asking? A device that is not being given work is not limited by any of the above.

A strong answer names the occupancy counter first, because it splits the space in one read:

"I'd read outstanding-tag occupancy first. If it's pinned at the pool size, the requester is saturated and the limit is either request size or round-trip latency — and I'd separate those by looking at the actual request size on the wire and the measured completion latency. If occupancy is well below the pool size, the requester is starving, and the question moves upstream: software submission rate, descriptor supply, or a flow-control stall preventing issue."

§14 measured why occupancy is the right first read. Two causes produce identical throughput — request size too small, and outstanding depth too small, both at 12% of baseline — and their occupancy readings are 31.9 and 4.0. One number separates them.

The follow-up: "Occupancy is pinned at the pool size and the request size is already at the configured maximum. Now what?"

Then the round trip is the limit, and the remaining question is where in the round trip. §14's model separates a slow far side (mean completion latency rises) from a slow host memory path (latency rises with a long tail rather than uniformly), and that distinction points at completely different owners.

RTL implication. This entire method depends on the occupancy counter being correct, and §17 measured a common way to get it wrong that makes it read high when the pool is actually empty. An instrument you have not validated is not evidence — §11 is that question.

4. "Where would you put the boundary between the Root Complex and the Endpoint in that analysis?"

What is being tested: whether you know which side owns which resource.

The reasoning path. Almost every resource in §3 belongs to a specific side:

resourceowned by
outstanding tags for reads it issuesthe requester — the endpoint, for DMA
completion buffer space for those repliesthe requester
credits toward the receiverthe transmitter, granted by the receiver
address translation and its contextsthe host (26.1 §5)
host memory bandwidththe platform, past the Root Complex
descriptor supplysoftware

A strong answer uses that table to assign each hypothesis an owner before testing it, because it changes who you go and talk to. A tag-occupancy limit is the endpoint team's. A translation-context limit is the platform's. A descriptor-supply limit is the driver author's — and no hardware change will help.

The follow-up: "The endpoint's occupancy is pinned and its request size is maximal. The host claims the platform is fine. How do you settle it?"

Measure the round trip at the endpoint. The endpoint knows when it issued a request and when the Completion returned; that interval includes everything past its own pins. If it is long and the endpoint's own queues are empty, the endpoint has exonerated itself with a measurement rather than an assertion — and 22.2 owns the discipline of stating exactly which event pair you measured.

5. "Throughput is fine and latency is terrible. Same subsystem. Explain."

What is being tested: whether you treat throughput and latency as one quantity.

The reasoning path. They are different questions about the same pipe, and a deep pipeline optimises one at the expense of the other. A requester with a large outstanding pool and large requests fills the link — and any individual transaction waits behind everything already queued.

A strong answer distinguishes the two measurement definitions before diagnosing:

  • throughput is bytes retired per unit time, measured over a window;
  • latency is the interval between one request's issue and its completion.

And then names the mechanism: queueing. Adding outstanding depth raises throughput and raises per-transaction latency at the same time. If the workload is latency-sensitive — a control read, a synchronisation flag — the fix is not more depth, it is a separate, shallow path for that traffic.

The follow-up: "Would you give control traffic its own virtual channel?"

The honest answer names the trade rather than picking. A separate channel isolates the small traffic from the bulk queue; it also costs buffering at the receiver and adds arbitration. This chapter does not claim a universal answer, and a candidate who does is signalling they have not built one.

6. "A device works with the IOMMU disabled and fails with it enabled. Is PCIe broken?"

What is being tested: whether you can tell the layer that broke from the layer that reported it.

The reasoning path. Ask what changed. The link state did not; the TLPs did not; the LTSSM has been in L0 throughout. What changed is what happens to the address after it crosses the Root Port (26.1 §5).

A strong answer:

"Nothing in the PCIe layer changed, so 'PCIe is broken' has no evidence behind it. The device is issuing well-formed Memory Writes to an I/O virtual address that is either unmapped or mapped without the permission it needs. I'd look for translation faults first — and I'd check whether faults are being reported at all before concluding none occurred."

That last clause is the senior part. 26.1 §14 measured a design that forwarded denied translations anyway: the permission check ran, its result was discarded, and 2,103 accesses proceeded on a translation that had been refused. An absence of reported faults is not evidence that none occurred.

The follow-up: "The driver says it's mapping the buffer correctly. How do you check without trusting either side?"

Compare the address the device emitted against the address the translation received. Those are two independent observations, and if they disagree, the question is answered without either party's cooperation.

What is being tested: whether you use direction to name a resource.

The reasoning path. The two directions consume different resources because they are different transaction types:

directiontransactionconsumesreceives a Completion?
device reads host memoryMemory Read — Non-PostedTags, completion-buffer spaceyes
device writes host memoryMemory Write — Postedposted creditno

A strong answer turns the symptom into a resource hypothesis immediately: a read-only failure points at Tags or completion space; a write-only failure points at posted credit or at the write path itself. And it notes what the asymmetry rules out — a fault in anything both directions share, which includes the link, the BAR decode and the address routing.

The follow-up: "Writes are Posted. How does the device know they landed?"

It does not, from the link. A Posted write receives no Completion (12.2), so the device learns of success only through a status structure it writes afterwards and the ordering rules that make it visible in the right order (13.4). A candidate who says "it gets an acknowledgement" is confusing the Data Link Layer's ACK with an end-to-end receipt25.9 §8 measured a switch dropping 10% of traffic internally while the ingress trace showed 20,000 successful ACKs.

8. Whiteboard RTL — the tag allocator, and the cycle everyone gets wrong

The four combinations must all be handled, and the trap is that two of them look mutually exclusive and are not.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// A tag pool with an occupancy counter. IMPLEMENTATION POLICY: pool size,
// and the assumption that at most one allocate and one free occur per cycle.
module tag_pool #(
  parameter int unsigned NTAG = 32
)(
  input  logic clk,
  input  logic rst_n,
  input  logic                          alloc_req,
  output logic                          alloc_gnt,
  output logic [$clog2(NTAG)-1:0]       alloc_tag,
  input  logic                          free_valid,
  input  logic [$clog2(NTAG)-1:0]       free_tag,
  output logic [$clog2(NTAG+1)-1:0]     occupancy,
  output logic                          err_double_alloc,
  output logic                          err_double_free
);
 
  logic [NTAG-1:0] busy;
  logic [$clog2(NTAG)-1:0] pick;
 
  always_comb begin
    pick = '0;
    for (int i = NTAG-1; i >= 0; i--) if (!busy[i]) pick = $clog2(NTAG)'(i);
    // The grant is computed from the CURRENT busy vector. A tag freed this
    // cycle is deliberately NOT offered this cycle: its owner's completion
    // is still being processed, and offering it here reissues a tag whose
    // previous replies may still be in flight (25.7 §7).
    alloc_gnt = alloc_req && (busy != '1);
    alloc_tag = pick;
 
    err_double_alloc = alloc_req && alloc_gnt && busy[pick];
    err_double_free  = free_valid && !busy[free_tag];
  end
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      busy <= '0;
      occupancy <= '0;
    end else begin
      if (alloc_gnt)  busy[alloc_tag] <= 1'b1;
      if (free_valid) busy[free_tag]  <= 1'b0;
 
      // ALL FOUR combinations, explicitly. This is the answer to the question.
      unique case ({alloc_gnt, free_valid})
        2'b10: occupancy <= occupancy + 1'b1;
        2'b01: occupancy <= occupancy - 1'b1;
        2'b11: occupancy <= occupancy;          // net zero — NOT "no update"
        2'b00: occupancy <= occupancy;
      endcase
    end
  end
 
endmodule

Walk the cycle explicitly, which is what the interviewer is listening for:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
OLD STATE        busy has 15 of 16 set; occupancy = 15
SAMPLED          alloc_req = 1, and free_valid = 1 for tag 3
COMBINATIONAL    busy != '1 (one free), so alloc_gnt = 1 and pick = the free index
                 pick != 3, because busy[3] is still 1 this cycle
NEXT STATE       busy[pick] -> 1, busy[3] -> 0, occupancy -> 15
NBA              all three land together at the edge
NEW STATE        occupancy = 15, and the pool holds a different set of tags

The wrong answers are 14 and 16, and §17 measured both. An exclusive if (alloc) ... else if (free) ... drops the free whenever it coincides with an allocate; occupancy drifts upward without bound, and the pool eventually reports itself full while every tag is genuinely free.

Why simple tests miss it: the two events must coincide, which requires the pool to be busy. §17 measured simultaneous allocate-and-free occurring in 23.3% of cycles at load — and essentially never at low rate.

9. "Now add a timeout. What breaks?"

What is being tested: whether you see that reclamation creates a new hazard.

The reasoning path. A timeout must reclaim the tag — a requester that never reclaims starves itself (25.7 §13 measured 366 requests issued against a healthy 31,195). But reclaiming makes the tag available while the abandoned request's Completion may still be in flight, and (Requester ID, Tag) is the entire identity of a Completion (10.2).

A strong answer names the fix and the reason:

"The tag needs a generation number that advances every time it is reclaimed. A Completion is then qualified on (RID, Tag, generation), and a late reply for the previous use fails the check and is discarded and counted. Without it, a Completion that was legitimately issued and legitimately delivered lands in the wrong request's buffer."

And the senior detail: 25.7 §13 measured that removing the generation bump corrupts identically to removing the check — 39,574 misdeliveries, 1,907,096 bytes in both cases — while the late-Completion counter reads zero in the bump case, because the field it compares stopped changing. The diagnostic and the fault are the same event.

The follow-up: "Your late-Completion counter reads zero. Are you satisfied?"

The expected answer is no, and the reason is the one above.

10. Whiteboard RTL — the outstanding table, and completion-context matching

A tag is a name. The context is what the name refers to, and matching them is where the second class of senior RTL bugs lives.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// The requester's outstanding table. Holds what a timeout cannot record:
// bytes received so far, and whether ANY completion matched (25.7 §3).
module outstanding_table #(
  parameter int unsigned NTAG = 32,
  parameter int unsigned GEN_W = 4
)(
  input  logic clk,
  input  logic rst_n,
  input  logic                        alloc,
  input  logic [$clog2(NTAG)-1:0]     alloc_tag,
  input  logic [31:0]                 alloc_bytes,
  input  logic [GEN_W-1:0]            alloc_gen,
  input  logic                        cpl_valid,
  input  logic [$clog2(NTAG)-1:0]     cpl_tag,
  input  logic [GEN_W-1:0]            cpl_gen,
  input  logic [15:0]                 cpl_bytes,
  output logic                        cpl_accept,
  output logic                        cpl_stale,
  output logic                        req_retire,
  output logic [$clog2(NTAG)-1:0]     retire_tag,
  output logic [31:0]                 stale_count
);
 
  typedef struct packed {
    logic            live;
    logic [31:0]     want;
    logic [31:0]     got;
    logic            saw_any;
    logic [GEN_W-1:0] gen;
  } ent_t;
 
  ent_t tbl [NTAG];
 
  always_comb begin
    // A Completion is accepted only if the entry is live AND the generation
    // matches. Matching on tag alone is the bug of §9 — a late reply for a
    // previous use of this tag is indistinguishable from the current one.
    cpl_stale  = cpl_valid && (!tbl[cpl_tag].live || (tbl[cpl_tag].gen != cpl_gen));
    cpl_accept = cpl_valid && !cpl_stale;
 
    // Retirement is on BYTE COUNT, never on "a completion arrived": a read
    // may be answered by several split Completions (13.3), and retiring on
    // the first frees the tag with data still in flight.
    req_retire = cpl_accept &&
                 ((tbl[cpl_tag].got + 32'(cpl_bytes)) >= tbl[cpl_tag].want);
    retire_tag = cpl_tag;
  end
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      for (int i = 0; i < NTAG; i++) tbl[i] <= '0;
      stale_count <= '0;
    end else begin
      if (alloc) begin
        // Allocation clears EVERY field. Leaving `got` or `saw_any` behind
        // makes the new request inherit the previous one's accounting, and
        // the resulting timeout is misclassified (25.7 §3).
        tbl[alloc_tag].live    <= 1'b1;
        tbl[alloc_tag].want    <= alloc_bytes;
        tbl[alloc_tag].got     <= '0;
        tbl[alloc_tag].saw_any <= 1'b0;
        tbl[alloc_tag].gen     <= alloc_gen;
      end
      if (cpl_accept) begin
        tbl[cpl_tag].saw_any <= 1'b1;
        tbl[cpl_tag].got     <= tbl[cpl_tag].got + 32'(cpl_bytes);
        if (req_retire) tbl[cpl_tag].live <= 1'b0;
      end
      if (cpl_stale && stale_count != 32'hFFFF_FFFF)
        stale_count <= stale_count + 32'd1;
    end
  end
 
endmodule

The same-cycle case to reason through, because interviewers ask it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
OLD STATE     tbl[7].live = 1, got = 192, want = 256, gen = 3
SAMPLED       cpl_valid = 1, cpl_tag = 7, cpl_gen = 3, cpl_bytes = 64
              AND alloc = 1 with alloc_tag = 7   <-- can this happen?

It must not, and the interface has to say so. Tag 7 is live, so the allocator cannot have offered it — alloc_gnt in §8 is computed from the current busy vector, and busy[7] is 1. The exclusion is a contract provided by the allocator, and stating that contract out loud is the senior answer. A design that allows both would have the allocation clear got in the same cycle the final Completion credits it, losing the retirement.

11. "Your occupancy counter says the pool is full and the pool is empty. Where do you look?"

What is being tested: whether you suspect the instrument.

The reasoning path. There are two independent representations of the same fact — the busy vector and the occupancy counter — and they can disagree. When two representations of one truth disagree, at least one is wrong, and the cheap check is which one matches reality.

A strong answer:

"I'd compare occupancy against a popcount of the busy vector. They're derived from the same events, so they should agree by construction — and if they don't, the counter's update logic is wrong, not the pool. The classic cause is an exclusive if/else that drops the free whenever it coincides with an allocate."

§17 measured that exact bug drifting occupancy upward in 200,000 of 200,000 cycles, ending at 46,601 against a true occupancy of 13.

The assertion that catches it is one line, and it is the strongest single check in this chapter:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// The counter and the vector are two views of one fact. This holds by
// construction in a correct design, so it costs nothing and catches every
// counter-update bug immediately — including the same-cycle case.
property p_occupancy_matches_vector;
  @(posedge clk) disable iff (!rst_n)
    (occupancy == $countones(busy));
endproperty
a_occ: assert property (p_occupancy_matches_vector);

The follow-up: "You can't afford a popcount in silicon. Now what?"

Then you assert it in verification and expose the busy vector to a debug register in silicon, so the comparison can be made once by software rather than every cycle by hardware. The invariant does not have to be checked continuously to be useful — it has to be checkable.

12. "Protocol bug, RTL bug, driver bug, or platform limit?"

What is being tested: whether you can classify before you investigate.

The four classes and what distinguishes them:

classtell
protocola TLP violates a rule — malformed, wrong status, illegal ordering. Visible on the link.
RTLthe transactions are legal and the device's internal state is wrong. Usually invisible on the link (25.9 §13).
driver/firmwarethe hardware did exactly what it was told. Reproduces with a different driver, or disappears with one.
platformnothing is wrong; something is slower or arranged differently. Moves when the device moves.
integrationeach component is correct in isolation and the composition is not.

A strong answer picks the discriminating experiment rather than the first hypothesis:

"I'd start by asking what the transactions on the link look like. If they're malformed or the status is wrong, it's a protocol or RTL bug and the trace localises it. If the transactions are all legal and the data is wrong, the analyzer is done helping — 25.9 measured three of five fault classes producing a byte-identical trace — and I need internal state. If it reproduces on one platform and not another with the same device and driver, it's platform. If it changes with the driver version, it's software."

The senior detail is knowing which questions the analyzer cannot answer. Internal descriptor ownership, tag allocator state, queue occupancy, the BAR-local target and host software intent never become packets, so no capture at any depth contains them.

13. "A completion arrives for a tag you never issued. What are the possibilities?"

What is being tested: whether you enumerate systematically.

The reasoning path. There are only a few ways this can happen, and they have different owners:

  1. A late Completion for a previous use of the tag — the timeout reclaimed it, and there is no generation check (§9). The most likely cause by far.
  2. A Completion for another requester — the Requester ID is not being compared, so somebody else's reply is being accepted.
  3. The tag was freed early — a split series freed on the first Completion instead of the last, so the remaining replies are orphans (13.3).
  4. The table was cleared — a reset invalidated the entry while a legitimate reply was in flight.
  5. A genuinely malformed Completion — the least likely, and the only one that is a protocol bug.

A strong answer orders these by likelihood and names the observable for each. The three separate rejection counters — foreign Requester ID, stale generation, tag not live — distinguish them in one read, and merging them into a single "rejects" number destroys exactly that distinction.

14. Measured — the six causes of one symptom

causeB/step% of baselinetag occupancyMRRScredit stallssoftware idlemean Cpl latency
healthy54.4100%31.9 / 3251200300
request size too small6.812%31.96400300
outstanding depth too small6.812%4.0 / 451200300
completion latency high9.117%32.0512001800
host memory slow13.725%32.051200300
software submission rate low12.824%7.5 / 325120390,000300

Three readings.

Rows 2 and 3 have identical throughput and are separated by occupancy alone — 31.9 versus 4.0. This is the single most useful fact in the chapter.

Rows 4 and 5 both show saturated occupancy and are separated by completion latency: a slow far side raises it uniformly; a slow host memory path raises the tail while the mean stays put. That distinction points at different owners.

And row 6 is the one that no hardware change fixes. Occupancy at 7.5 of 32 with a large software-idle count means the engine is waiting for work. A candidate who proposes increasing the tag pool here has misread the measurement.

15. Measured — a credit stall is not a credit bottleneck

Tag-limited ceiling = 32 × 512 / 300 = 54.6 B/step.

credit blockingB/step% of basecredit stallstag occupancy
0%54.4100%031.9
30%54.3100%119,92731.8
60%54.099%239,58031.7
90%46.686%359,86327.3
97%15.328%388,0469.0
99%5.09%396,0663.0

Blocking 60% of submission opportunities changed throughput by 1%. With a 300-step round trip and 32 tags, a tag frees roughly every nine steps — there are far more submission opportunities than tags to spend on them.

The debugging consequence is the point:

A nonzero credit-stall counter is not evidence that credits are the bottleneck. It is evidence only when the stall coincides with tag occupancy below the pool size.

Rows 5 and 6 are where credits actually bind, and note their signature: occupancy collapses to 9.0 and 3.0. That is the same signature as "software isn't submitting" — and separating those two requires the credit-stall counter and the occupancy counter together. Neither alone is sufficient.

16. "Interrupt fires and the status record is stale. Explain and fix."

What is being tested: whether you know an interrupt carries no information.

The reasoning path. MSI/MSI-X is a notification: it tells the host that something happened. The host then reads a status structure in memory to learn what. If the notification can overtake the record, the host reads the previous result.

A strong answer states the ordering obligation and where it comes from:

"The status write and the interrupt are both Memory Writes, and nothing makes them ordered unless the design makes them ordered. The interrupt must wait for the status write to be accepted, not merely issued. The ordering rules that make that meaningful are in 13.4 — but the design obligation is to sequence them, not to hope."

And the senior detail: this is characteristically intermittent, appearing only when the status path is slower than the interrupt path. A test on an idle system never sees it. 26.3 §16 measured 99,604 early interrupts in one run under load.

The follow-up: "Would a memory barrier in the driver fix it?"

No — the race is between two device-originated writes, and a barrier on the host orders the host's accesses. The fix is in the device's sequencing.

17. Measured — the same-cycle occupancy bug

occupancy implementationfinaltruecycles wrong
case ({alloc,free}) — all four combinations13130
if (alloc) ... else if (free) ...46,60113199,998
increment wins when both occur46,60113199,998

Both broken forms drift upward without bound. The pool eventually reports itself full while every tag is genuinely free — a hang whose register dump shows a healthy link, no errors, and an occupancy counter that is simply wrong.

And the bug requires the two events to coincide, which requires the pool to be busy. At low request rates it essentially never fires, which is why it survives directed testing and appears at load.

18. Invariants Worth Asserting

Five, chosen because each catches a distinct failure class rather than a variant of one.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 1. CONSERVATION — the counter and the vector are two views of one fact.
//    Catches every counter-update bug, including the same-cycle case (§17).
//    Assumption: both are maintained from the same events.
property p_occupancy_conserved;
  @(posedge clk) disable iff (!rst_n) (occupancy == $countones(busy));
endproperty
 
// 2. OWNERSHIP — a tag is never allocated while it is already outstanding.
//    Catches an allocator that offers a tag freed in the same cycle (§8).
property p_no_double_alloc;
  @(posedge clk) disable iff (!rst_n) alloc_gnt |-> !busy[alloc_tag];
endproperty
 
// 3. IDENTITY — a Completion is credited only to the request that made it.
//    Catches the reclaim/reissue window of §9. The generation is what makes
//    a late reply for a PREVIOUS use of the tag distinguishable.
property p_completion_identity;
  @(posedge clk) disable iff (!rst_n)
    cpl_accept |-> (tbl[cpl_tag].live && (tbl[cpl_tag].gen == cpl_gen));
endproperty
 
// 4. DATA INTEGRITY — a request retires only when its byte count is satisfied.
//    Catches retiring on the first of several split Completions (13.3).
property p_retire_on_bytes;
  @(posedge clk) disable iff (!rst_n)
    req_retire |-> ((tbl[cpl_tag].got + cpl_bytes) >= tbl[cpl_tag].want);
endproperty
 
// 5. LIVENESS — an allocated tag is eventually released.
//    Catches leaks, including the error-completion path that forgets to free.
//    Assumption: the timeout mechanism is present and enabled.
property p_no_tag_leak;
  @(posedge clk) disable iff (!rst_n)
    alloc_gnt |-> s_eventually !busy[$past(alloc_tag)];
endproperty

Property 1 is the one to volunteer in an interview. It is a single line, it costs nothing in verification, and it catches the bug §17 measured — which is the bug most likely to be in the room.

19. Bug Bank — the failure classes senior candidates are shown

broken ruleobservable symptomwhy simple tests miss itcheck that catches it
Exclusive if/else on allocate/freepool reports full; engine hangsneeds both events in one cycle — only at load (§17)p_occupancy_conserved
Allocator offers a tag freed this cyclemisdelivered Completionsneeds a reply still in flightp_no_double_alloc
No generation on the taglate reply credited to the new requestneeds a timeout and a reissuep_completion_identity
Generation present, never bumpedidentical corruption, counter reads zerothe diagnostic is the fault (25.7 §13)an oracle independent of the generation
Retire on first split Completiontag freed with data in flightneeds a read large enough to splitp_retire_on_bytes
Allocation leaves got unclearedtimeouts misclassified as partialneeds tag reuseclear-on-alloc assertion
Error Completion does not free the tagslow leak; engine stalls after hoursneeds an error path exercisedp_no_tag_leak
Interrupt not sequenced after statushost reads a stale recordneeds the status path to be the slower oneordering assertion (§16)
Rejection reasons merged into one countercannot distinguish stale from foreignnothing fails; the information is gonethree separate counters (§13)
Occupancy counter used as flow controla wrong counter becomes a wrong decisionworks until the counter driftsp_occupancy_conserved

20. The Traps

"Increase the outstanding depth." Only if occupancy is pinned (§14). If occupancy is at 7.5 of 32, the pool is not the limit and a deeper pool changes nothing.

"We have credit stalls, so credits are the bottleneck." §15 measured 239,580 credit stalls with throughput at 99% of baseline. Read occupancy alongside it.

"The link is at the right width and speed, so PCIe is fine." It means the physical negotiation succeeded. It says nothing about transaction sizing, outstanding depth, translation, or the host.

"The analyzer will show us." For an internal ownership bug it will not (25.9 §13). Knowing which questions it cannot answer is part of the skill.

"Throughput is fine, so latency is fine." They trade against each other (§5).

"The interrupt fired, so the data is ready." The interrupt carries no information (§16).

"It's a PCIe problem." Four other classes produce overlapping symptoms (§12), and naming the class before investigating is most of the work.

21. When You Do Not Know — the Senior Standard

At beginner level, "I'd check the specification" is a complete answer for a value.

At advanced level it is half an answer — the other half is the derivation you can do without it (27.3 §14).

At senior level there is a third part: say what you would do if the value turned out to be either extreme. An engineer who knows the shape of the answer without the number is more useful than one who knows the number and not the shape.

Worked example. Asked for a Completion Timeout range:

"I don't have the range memorised and I'd read it from the specification and from the device's own capability register, because it's configurable and the device may not support the full set. What I can reason about without it: the timeout has to be longer than the worst-case legitimate round trip through the deepest path in the topology, or you abandon requests that were going to complete — and every abandonment creates a reclaim/reissue window. It also has to be short enough that a genuinely lost request doesn't hold a tag for so long that the requester starves. If the configured value is very long, I'd expect tag starvation under fault conditions; if it's very short, I'd expect misdeliveries from late Completions unless the generation check is solid."

That answer contains no value and demonstrates more understanding than one that does.

22. Readiness Markers

You are ready for this round when you can, without notes:

  • name the observable that separates a depth limit from a size limit, and say why (§3, §14);
  • explain why a credit-stall counter alone proves nothing (§15);
  • write the four-way occupancy update and walk the same-cycle case (§8);
  • explain why a reclaimed tag needs a generation, and why the generation's counter can lie (§9);
  • state which resources belong to the requester, the completer, the host and software (§4);
  • classify a symptom into protocol / RTL / driver / platform / integration before proposing a fix (§12);
  • name three questions a protocol analyzer cannot answer (§12);
  • and give a useful answer to a question whose specification value you do not know (§21).

23. What Comes Next

Module 27 is complete.

ChapterThe level
27.1 Beginnerwhat the mechanisms are
27.2 Intermediatehow enumeration, BARs and TLPs work
27.3 Advancedwhy flow control, the LTSSM and MSI-X are built that way
27.4 (this)which cause produced this symptom, and how you would prove it

The escalation across the four rounds is one thing: how much ambiguity the question carries. A beginner question has one answer. An advanced question has one answer that requires a mechanism to derive. A senior question has several plausible answers and is scored on the method that eliminates them — which is why every measured result in this chapter is about a signature rather than a value.

Module 28 changes the frame entirely. Instead of reasoning inside PCIe, it asks what PCIe is for by comparing it with the interconnects it is not — AXI, Ethernet, USB and CXL. The comparisons are where the design decisions become visible, because every difference between two interconnects is a trade somebody made deliberately.