UCIe · Module 27
Debug Review Checklist
The pre-tapeout observability gate — why first-failure capture and last-error-wins differ by an entire debug campaign, the trace ring that records before the trigger rather than after, which counter discriminates which hypothesis, and why 'we'll use firmware logs' is a FAIL rather than a plan.
27.5 asked whether the pieces work together. This gate asks what you will know when they do not — and it is held before tapeout, because that is the last moment the answer can change.
1. The One-Sentence Model
Debug capability is an architectural feature. If you wait for the bug to decide what you wish you could observe, tapeout has already answered no.
That is the whole gate. Every other review in Module 27 examines something the design does; this one examines what the design reveals — and unlike a functional bug, an observability gap cannot be patched, worked around in firmware, or fixed in the next drop. It is a respin.
And the gate has a specific technical centre (§6): the first recorded error is almost never the first divergence. A credit return goes missing; credits drift down; a timeout eventually fires. The register says TIMEOUT. The cause was thousands of cycles earlier and is unrecorded. Everything in §7–§15 exists to close that gap.
2. What This Gate Owns
| Gate | Asks | Not this chapter |
|---|---|---|
| 27.1 — Architecture | what are we building? | area H commissioned there |
| 27.2 — RTL | did we implement it safely? | code review of the capture logic |
| 27.3 — Verification | can we prove it correct? | pre-silicon closure |
| 27.4 — Performance | can it meet the workload? | the counters' performance use |
| 27.5 — Integration | can the pieces work together? | cross-die contracts |
| 27.6 — Debug (this chapter) | can we find the first divergence without guessing? | — |
Three boundaries.
This is not a debug tutorial. 21.7 and 25.9 teach how to debug; this gate reviews whether the silicon will let you. The distinction is temporal: those chapters run after tapeout, this one runs before it, and its findings are the only ones that can still be acted on.
It shares counters with 27.4 and reviews them for a different property. 27.4 asks do these counters attribute lost cycles? This gate asks does each counter discriminate between two hypotheses? — and a counter that measures something real but distinguishes nothing is a 27.4 asset and a 27.6 finding (§13).
And 27.1 §13 commissioned area H months ago. If that commissioning did not happen, this gate cannot repair it — the review can only record how much of the design is unobservable and what that costs. §21 is what that conversation looks like.
3. Gate Decisions for Observability
| Decision | In this gate |
|---|---|
| PASS | first-divergence evidence exists for every known risk · capture survives the resets that occur around it · the mechanisms have themselves been tested (§18) · reachable in the lab (§20) |
| CONDITIONAL PASS | a low-likelihood risk has partial observability, with a named mitigation — a bench procedure, an emulation-based fallback — and no downstream gate assumes full visibility |
| FAIL | a known risk with no first-divergence evidence · last-error-wins on a critical status · trace records only after the trigger · capture cleared by a reset that occurs in the failure path · debug mechanisms never validated · unreachable through the assembled package |
Three readings.
"For every known risk" is what makes this gate tractable. You cannot make everything observable. You can enumerate the risks the earlier gates surfaced — 27.1 §19's three unanswered questions, 27.5 §8's retention disagreement, 27.4 §17's window ceiling — and require evidence for each. A risk that a previous gate named and this gate cannot observe is a FAIL, not a judgement call.
The CONDITIONAL PASS is narrower here than elsewhere, because there is no closure date that helps: after tapeout the mechanism either exists or it does not. The only legitimate conditional is a mitigation — a bench technique, an emulation fallback, an FPGA prototype that carries the missing instrument — and it must be named and costed now.
And the last FAIL row catches a real and expensive mistake. Debug registers that exist in RTL and cannot be reached through the assembled package are not observable (27.5 §17 H7). That is discovered in the lab, and the RTL was correct all along.
4. Statement Versus Evidence
| Review statement (worthless) | Evidence (accepted) |
|---|---|
| "we have error registers" | first-fault sticky with class, state, semantic ID, generation, epochs and timestamp — and the overflow count (§7) |
| "we'll use firmware logs" | nothing for a lost hardware event (§21) |
| "there's a trace buffer" | the pre-trigger depth, the post-trigger budget, the wrap flag and the freeze (§10) |
| "we have counters" | the counter→hypothesis map: which two explanations does each separate? (§13) |
| "software can read the state" | the atomic snapshot mechanism and its documented read protocol (§14) |
| "we tested the debug logic" | the injection results: second fault, trace wrap, trigger near wrap, reset during trace, saturation (§18) |
| "we can dump registers" | the sequence proving evidence is captured before any reset (§16) |
Three readings.
Row 2 is this chapter's flagship (§21), and the reason it fails is structural rather than a matter of log quality. A log records what software was told. A lost credit return was never reported to software — no interrupt, no status change, no event. You cannot log an event that did not happen, so the log's first entry is the timeout, which is the symptom.
Row 4 is the item that separates useful instrumentation from telemetry. Every counter should answer "which two hypotheses does this discriminate?" A counter that measures something real and separates nothing is cost without capability.
And row 6 is the item almost nobody has. The debug mechanisms are logic, and untested logic does not work. A trace ring whose trigger lands one entry before the wrap point, a first-fault register hit by two faults in consecutive cycles — these are the conditions that occur in the failure you built them for.
5. Sourcing and Scope
6. First Divergence Is Not First Error
The distinction that organises the whole gate.
| First divergence | First recorded error | |
|---|---|---|
| what it is | the first moment observed behaviour left the contract | the first condition the design was built to detect |
| when | early | often much later |
| example | a credit return is not generated | a timeout fires |
| what it tells you | the subsystem and the mechanism | that something is wrong |
| typically recorded? | no, unless designed for | yes |
Worked example — the gap made concrete.
| Cycle | What happens | Recorded by a typical design |
|---|---|---|
| 1 000 | a credit return is not generated for one entry | nothing |
| 1 001–40 000 | traffic continues; credits drift down by one | nothing |
| 40 000 | credits reach zero; the link stalls | nothing — a stall is not an error |
| 40 000–44 000 | no progress | nothing |
| 44 000 | a transaction times out | ERR_TIMEOUT |
| 44 001+ | dependent timeouts cascade | ERR_FATAL overwrites it (§9) |
The register says ERR_FATAL. The divergence was at cycle 1 000, 43 000 cycles earlier, at a specific boundary, and nothing recorded it.
Three readings.
The gap between divergence and error is the amount of debugging you are signing up for, and it is set at design time. Closing it is not about recording more — it is about recording invariant violations rather than consequences. A credit-conservation check (25.6 §10) fires at cycle 1 000; a timeout fires at 44 000. Same bug, two orders of magnitude apart in usefulness.
Which yields the design rule this gate applies everywhere: capture invariant violations, not symptoms. A timeout is a symptom of a dozen different root causes; a conservation violation is one cause, and it names the boundary.
And it explains why a trace ring must record before the trigger (§12). At cycle 44 000 the interesting history is 43 000 cycles old. A buffer that starts recording at the trigger captures only the cascade.
7. Area A — First-Failure Capture
| # | Must be true | Evidence that counts | Weak evidence | FAIL if |
|---|---|---|---|---|
| A1 | capture is FIRST, not last | the !valid guard in RTL | "we log errors" | last-error-wins (§9) |
| A2 | an error class is recorded, not a single bit | the class encoding | one error flag | cannot triage |
| A3 | semantic ID and generation are captured | the fields | — | cannot correlate (27.5 §21) |
| A4 | config epoch and route epoch are captured | the fields | — | stale-config cases undiagnosable |
| A5 | a timestamp is captured | the counter value | — | cannot order across dies |
| A6 | an overflow/second-fault count exists | the counter | — | cannot tell one fault from a storm |
| A7 | a compact resource snapshot accompanies it | credits, occupancy, state (§15) | — | context lost |
| A8 | clearing is explicit and separate from functional reset | the clear control | — | cannot re-arm live (27.5 §18 I4) |
| A9 | it survives every reset in the failure path | the retention statement + test | — | the reset destroys the cause |
Three readings.
A6 is the field people omit and then desperately need. First-fault capture alone cannot distinguish "one thing went wrong" from "nine thousand things went wrong and this was the first." A count of subsequent faults turns an ambiguous register into a story — and it costs one counter.
A9 is where first-fault capture is most often defeated in practice. The design captures correctly; then the link resets as part of recovery, and the capture is cleared by the same reset. The failure path itself erases the evidence, and the field reports only failures that did not trigger a reset.
And A4 is what makes the modern failure classes findable at all. A fault whose cause is a stale configuration epoch or a route change (26.5 §19) is undiagnosable without those two fields — the error class will say something plausible and wrong.
8. Illustrative RTL — the First-Fault Register
// ILLUSTRATIVE. First-fault capture. The single guard `!ff_valid_q` is what
// makes this FIRST rather than LAST, and it is the whole difference between
// §9's useless register and a usable one.
typedef enum logic [3:0] {
FC_NONE, FC_CREDIT_INVARIANT, FC_ORPHAN_CMPL, FC_STALE_GEN,
FC_ID_REUSE_LIVE, FC_EPOCH_STALE, FC_ROUTE_MISMATCH, FC_TIMEOUT,
FC_FSM_ILLEGAL, FC_PARITY, FC_OVERFLOW, FC_UNDERFLOW
} fault_class_e;
logic ff_valid_q;
fault_class_e ff_class_q;
logic [SEM_W-1:0] ff_sem_id_q;
logic [GEN_W-1:0] ff_gen_q;
logic [EPOCH_W-1:0] ff_cfg_epoch_q, ff_route_epoch_q;
logic [2:0] ff_lnk_state_q;
logic [31:0] ff_timestamp_q;
logic [15:0] ff_credits_q, ff_outstanding_q;
logic [15:0] ff_subsequent_q; // A6 — one fault, or a storm?
logic [31:0] cycle_q;
always_ff @(posedge clk or negedge rst_n) begin
// NOTE the reset expression: functional/link resets are DELIBERATELY absent.
// Only a full reset or an explicit debug clear may erase evidence (A8, A9).
if (!rst_n || ff_clear) begin
ff_valid_q <= 1'b0;
ff_class_q <= FC_NONE;
ff_subsequent_q <= '0;
cycle_q <= '0;
end else begin
cycle_q <= cycle_q + 32'd1;
if (fault_detected) begin
if (!ff_valid_q) begin
// FIRST fault: capture everything needed to locate the divergence.
ff_valid_q <= 1'b1;
ff_class_q <= fault_class;
ff_sem_id_q <= fault_sem_id;
ff_gen_q <= fault_gen;
ff_cfg_epoch_q <= cfg_epoch; // A4
ff_route_epoch_q <= route_epoch; // A4
ff_lnk_state_q <= link_state;
ff_timestamp_q <= cycle_q; // A5
ff_credits_q <= credits_avail; // A7 — context at the moment
ff_outstanding_q <= outstanding_cnt; // A7
end else if (ff_subsequent_q != 16'hFFFF) begin
// Later faults: COUNT them, never overwrite. Saturate rather than wrap.
ff_subsequent_q <= ff_subsequent_q + 16'd1;
end
end
end
end
// MANDATORY. English: once first-fault capture is valid, its class never
// changes until an explicit clear. This is A1 expressed as a property, and it
// catches anyone "improving" the register into last-error-wins (§9).
a_first_fault_stable: assert property (
@(posedge clk) disable iff (!rst_n)
(ff_valid_q && !ff_clear) |=> (ff_valid_q && $stable(ff_class_q))
);
// MANDATORY. English: a fault always leaves a record — either it captures, or
// it increments the subsequent count. Catches a fault path that reports
// nothing at all, which is worse than reporting the wrong thing.
a_fault_always_recorded: assert property (
@(posedge clk) disable iff (!rst_n)
fault_detected |=> (ff_valid_q &&
($changed(ff_class_q) || $changed(ff_subsequent_q) ||
(ff_subsequent_q == 16'hFFFF)))
);Architecture. One sticky record plus a count of everything after it. Ten fields, chosen so that a single register dump answers what, to which transaction, under which configuration, when, and in what context.
State. ff_valid_q is the guard; the rest is captured once. ff_subsequent_q is what distinguishes a single event from a storm (A6), and it saturates rather than wrapping so a huge count stays huge.
Event. Capture on the first fault_detected. Every subsequent fault increments only.
Contract. The reset expression is a contract, not a style choice — link resets and functional resets are deliberately absent (A9), so a recovery in the failure path cannot erase the cause. ff_clear must be a debug-only control that does not disturb functional state (A8), or you cannot re-arm on a live system.
Failure. Three realistic mistakes. Omitting ff_subsequent_q makes one fault and ten thousand indistinguishable. Including link_rst_n in the reset erases evidence in exactly the failure path you care about. And capturing only the class — without ID, generation and epochs — produces a register that names a symptom and locates nothing.
DV/debug. a_first_fault_stable catches the regression back to last-error-wins, which is a natural-looking "improvement". And §18 requires this logic to be validated by injection: two faults in consecutive cycles, and a fault during a link reset.
9. Wrong RTL — Last Error Wins
// WRONG. ILLUSTRATIVE. Every error overwrites the status register. Written by
// someone who reasoned that the most recent error is the most relevant — which
// is true for a stateless peripheral and false for a cascading link failure.
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
err_code_q <= FC_NONE;
err_valid_q <= 1'b0;
end else if (fault_detected) begin
err_code_q <= fault_class; // BUG 1: overwrites unconditionally
err_valid_q <= 1'b1; // BUG 2: no count of prior faults
end
// BUG 3: no ID, generation, epoch or timestamp — nothing to correlate.
endArchitecture. One register, one valid bit. Simple, tidy, and it destroys the only information that mattered.
State. A single class, continuously overwritten.
Event. Every fault.
Contract. A debug engineer reads this register expecting the reason the link failed. It contains the last consequence before someone looked.
Failure — the timeline. §6's scenario, with this register.
| Cycle | Event | err_code_q | What is now lost |
|---|---|---|---|
| 1 000 | credit-conservation violation | FC_CREDIT_INVARIANT | — |
| 40 000 | credits exhausted, link stalls | FC_CREDIT_INVARIANT | — |
| 44 000 | transaction timeout | FC_TIMEOUT | the root cause, overwritten |
| 44 010 | second timeout | FC_TIMEOUT | — |
| 44 100 | dependent unit reports orphan | FC_ORPHAN_CMPL | — |
| 45 000 | fatal escalation | FC_TIMEOUT (last one) | — |
| lab, next day | engineer reads the register | FC_TIMEOUT | everything |
Root cause. The design recorded the most recent event when the first one was the information. In a cascading failure — which is what link failures are — the last error is almost always a generic escalation, and generic escalations are consistent with dozens of root causes.
And the cost is measurable in weeks, not hours. FC_TIMEOUT sends the team to look at latency, arbitration, the far end, the workload — every hypothesis except credit accounting, because nothing points there. The correct first-fault register would have said FC_CREDIT_INVARIANT at cycle 1 000, with the semantic ID and the credit count at that moment.
The review question that catches it is one line: "is this register first-write-wins or last-write-wins?" — and the answer is visible in the RTL in five seconds. §8 is the correction, and §3 makes this a FAIL on a critical status register rather than an improvement request.
10. Area B — Trace Buffer
| # | Must be true | Evidence that counts | FAIL if |
|---|---|---|---|
| B1 | the ring records continuously, BEFORE any trigger | the always-writing structure | arms on the trigger (§12) |
| B2 | post-trigger capture is bounded and small | the post-trigger count | records only the cascade |
| B3 | it freezes and stays frozen | the freeze condition | overwritten while being read |
| B4 | a wrap indicator exists | the sticky wrapped flag | cannot tell 100 entries from 100 000 |
| B5 | a valid entry count exists | the count | cannot tell full from partial |
| B6 | entries carry a timestamp | the field | cannot order or correlate |
| B7 | entries carry identity and epochs | the fields | cannot follow a transaction |
| B8 | the event set is chosen to show divergence, not volume | the event list | a firehose of noise |
Three readings.
B1 is the item, and §12 is why. At the moment a trigger fires, the interesting history is already in the past. A buffer that begins recording at the trigger captures the consequences of a divergence that happened long before.
B4 is small and decisive. A dump of 1 024 entries with no wrap flag could be 1 024 events total or the last 1 024 of a million. Those two readings support opposite conclusions, and the flag costs one bit.
And B8 is where trace design goes wrong in the other direction. A ring that records every transaction covers microseconds and wraps away the divergence. A ring that records state transitions, credit events, error conditions, epoch changes and recovery entry/exit covers far longer and contains the events that matter. The review item is the event list, and its justification.
11. Illustrative RTL — the Trace Ring
// ILLUSTRATIVE (§8). A circular pre-trigger history with a bounded post-trigger
// tail and a hard freeze. The three review-critical properties are: it writes
// ALWAYS, the wrap flag is sticky, and after freeze nothing mutates.
localparam int TR_DEPTH = 1024;
localparam int TR_POST = 64; // post-trigger entries, deliberately small
typedef struct packed {
logic [31:0] ts;
logic [3:0] evt; // state change / credit / error / epoch...
logic [SEM_W-1:0] sem_id;
logic [GEN_W-1:0] gen;
logic [EPOCH_W-1:0] cfg_epoch;
logic [15:0] aux; // credits, occupancy, sub-state
} tr_entry_t;
tr_entry_t tr_mem [TR_DEPTH];
logic [$clog2(TR_DEPTH)-1:0] wr_ptr_q;
logic wrapped_q; // STICKY (B4)
logic frozen_q;
logic [$clog2(TR_POST):0] post_cnt_q;
logic [$clog2(TR_DEPTH):0] valid_cnt_q; // B5
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n || tr_arm) begin
wr_ptr_q <= '0; wrapped_q <= 1'b0; frozen_q <= 1'b0;
post_cnt_q <= '0; valid_cnt_q <= '0;
// NOTE: the memory is deliberately NOT cleared. valid_cnt_q and wrapped_q
// describe what is meaningful, so clearing 1024 entries buys nothing and
// costs a long, awkward reset sequence.
end else if (!frozen_q) begin
// ALWAYS recording. This is B1, and it is the difference from §12.
if (tr_event) begin
tr_mem[wr_ptr_q] <= '{ ts: cycle_q,
evt: tr_evt_code,
sem_id: tr_sem_id,
gen: tr_gen,
cfg_epoch: cfg_epoch,
aux: tr_aux };
if (wr_ptr_q == TR_DEPTH-1) begin
wr_ptr_q <= '0;
wrapped_q <= 1'b1; // sticky: never cleared except on arm
end else begin
wr_ptr_q <= wr_ptr_q + 1'b1;
end
if (valid_cnt_q != TR_DEPTH) valid_cnt_q <= valid_cnt_q + 1'b1;
end
// After the trigger, keep a BOUNDED tail so the cascade's start is visible,
// then freeze. Counting ENTRIES, not cycles: a quiet link would otherwise
// freeze having recorded nothing after the trigger.
if (tr_triggered) begin
if (post_cnt_q == TR_POST) frozen_q <= 1'b1;
else if (tr_event) post_cnt_q <= post_cnt_q + 1'b1;
end
end
// else: FROZEN. No branch may write tr_mem, wr_ptr_q or wrapped_q (B3).
end
// MANDATORY. English: once frozen, the write pointer never moves. Catches a
// later edit that adds a write path outside the !frozen_q guard — which would
// overwrite the evidence while software is reading it.
a_trace_frozen_stable: assert property (
@(posedge clk) disable iff (!rst_n)
frozen_q |=> ($stable(wr_ptr_q) && $stable(wrapped_q) && frozen_q)
);
// MANDATORY. English: the wrapped flag is sticky until an explicit re-arm.
// Catches the flag being cleared as a side effect, which would make a full
// buffer look like a partial one.
a_wrapped_sticky: assert property (
@(posedge clk) disable iff (!rst_n || tr_arm)
wrapped_q |=> wrapped_q
);Architecture. A continuously-written ring, a sticky wrap flag, a bounded entry-counted post-trigger tail, and a hard freeze. Depth TR_DEPTH of history and only TR_POST of aftermath — the asymmetry is deliberate and is the design.
State. Pointer, wrap flag, freeze, post count, valid count. valid_cnt_q and wrapped_q together tell software exactly what the dump means, which is why the memory need not be cleared on arm.
Event. Written on tr_event — the selected event set (B8), not every transaction.
Contract. The post-trigger tail counts entries, not cycles. A cycle-counted tail on a link that stalls after the trigger freezes having recorded nothing, which is the common case: the trigger often is the moment traffic stops.
Failure. Four realistic mistakes. A non-sticky wrap flag makes a wrapped buffer look partial. A cycle-counted post-trigger tail captures nothing on a stalled link. A write path outside the freeze guard overwrites evidence during the dump. And a too-large TR_POST wraps the pre-trigger history away with cascade entries — which converts a correct ring into §12's broken one.
DV/debug. §18 requires this to be injected: trigger at entry 0, trigger one entry before wrap, trigger after several wraps, and a reset during capture. The last is where wrapped_q and valid_cnt_q are most likely to disagree.
12. Wrong Trace — Recording Starts at the Trigger
// WRONG. ILLUSTRATIVE. Recording begins when the error is detected. This looks
// efficient — why record when nothing is wrong? — and captures only the
// aftermath of a divergence that already happened.
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
rec_en_q <= 1'b0; wr_ptr_q <= '0;
end else begin
if (error_detected) rec_en_q <= 1'b1; // BUG: arms ON the error
if (rec_en_q && tr_event) begin
tr_mem[wr_ptr_q] <= tr_entry;
wr_ptr_q <= wr_ptr_q + 1'b1; // BUG 2: no freeze; wraps forever
end
end
endArchitecture. A buffer that is idle until something goes wrong.
State. An enable and a pointer. No history exists at the moment history becomes valuable.
Event. Arms on error_detected — which by §6 is typically thousands of cycles after the divergence.
Contract. A debug engineer expects the trace to contain the events leading to the failure. It contains the events following it.
Failure — the timeline.
| Cycle | Event | Trace contains |
|---|---|---|
| 1 000 | credit-conservation violation — the divergence | nothing; not recording |
| 1 001–43 999 | credits drift; the link degrades | nothing |
| 44 000 | timeout → error_detected → recording begins | empty, now arming |
| 44 001+ | dependent timeouts, orphans, escalation | the cascade |
| dump | — | 1 024 entries, all consequences |
Every recorded entry is derivative. The root cause occurred 43 000 cycles before the first entry, and no amount of analysis of the cascade recovers it.
And there is a second, subtler defect: no freeze (B3). The buffer keeps writing while software reads it, so a slow dump returns a mixture of entries from different wrap generations — which is worse than an empty buffer, because it looks like data.
Root cause. The trigger was treated as the start of the interesting period when it is the end of it (§6). The fix is architectural, not a tuning change: record always, keep a bounded tail, freeze.
DV/debug. The review question is one line: "does the ring record before the trigger?" — visible in the RTL immediately. §3 makes this a FAIL, because a post-trigger-only trace cannot produce first-divergence evidence for any risk.
13. Area C — Counters That Discriminate
A counter earns its area by separating two hypotheses. The review artefact is the counter→hypothesis map.
| Counter | Separates | From |
|---|---|---|
| offered vs accepted | the source is not offering | downstream is refusing |
| accepted vs transmitted | admission is the limit | the wire is |
| transmitted vs retried | useful work | retransmission (27.4 §11) |
| delivered vs retired | transport completed | semantic completion |
| no-credit cycles | flow control | anything else |
| stalled-on-no-free-ID | the outstanding window (27.4 §17) | congestion |
| credit-return generated vs applied | our side failed to return | the peer failed to apply |
| recovery enter / exit counts | one long recovery | many short ones |
| obligations outstanding at recovery | retained | discarded (27.5 §8) |
| first-fault subsequent count | one fault | a storm (A6) |
Three readings.
Row 7 is the counter that would have made §6's example a five-minute diagnosis. Credit returns generated on one side versus applied on the other localises a missing return to a specific direction and boundary (25.9 §12) — and it is exactly the event that firmware logs cannot see (§21).
Row 9 is 27.5 §8's discriminator, and it must exist on both sides or it discriminates nothing.
And the review test for every proposed counter is a single question: "which two explanations does this separate?" If the answer is vague, the counter is telemetry — it consumes area, appears in dashboards, and does not shorten any investigation.
14. Area D — Atomic Snapshot
| # | Must be true | Evidence that counts | FAIL if |
|---|---|---|---|
| D1 | state is captured in one cycle | the single-cycle copy | sequential live reads |
| D2 | capture is triggered by the fault, not only by software | the trigger path | the state has moved on by the time software arrives |
| D3 | an epoch/valid indicates a complete capture | the fields | torn reads unnoticed |
| D4 | the snapshot spans the whole diagnostic set | the field list | partial context |
| D5 | saturation is reported | the sticky bit | a pinned counter reads as a plateau |
| D6 | the read protocol is documented | the register-map note | software trusts a torn read |
Two readings.
D2 is the item unique to debug snapshots, as opposed to 27.4 §12's performance snapshots. A performance snapshot can be taken on demand. A debug snapshot must be taken at the moment of the fault — by the time software notices and reads, credits have recovered, occupancy has drained and the link has retrained. The state that explained the fault is gone.
And D1's failure mode produces impossible states rather than wrong ones, if anyone checks. A sequential read across a live system can yield more delivered than accepted — visibly absurd. The dangerous case is a small skew that yields a plausible, wrong picture and sends the investigation somewhere reasonable and incorrect.
15. Illustrative RTL — the Fault-Triggered Snapshot
// ILLUSTRATIVE (§8). Captured BY THE FAULT (D2), in ONE cycle (D1). This is
// the context that makes §8's first-fault class actionable: not just WHAT went
// wrong, but what the machine looked like at that instant.
typedef struct packed {
logic [2:0] lnk_state;
logic [EPOCH_W-1:0] cfg_epoch, route_epoch;
logic [15:0] credits_avail, credits_outstanding;
logic [15:0] occupancy, occ_high_water;
logic [31:0] no_credit_cyc, no_free_id_cyc;
logic [31:0] cr_ret_generated, cr_ret_applied; // §13 row 7
logic [15:0] obligations_outstanding; // §13 row 9
logic [31:0] timestamp;
} dbg_snap_t;
dbg_snap_t snap_q;
logic snap_valid_q;
logic [15:0] snap_epoch_q;
logic snap_saturated_q;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n || snap_clear) begin
snap_valid_q <= 1'b0; snap_epoch_q <= '0; snap_saturated_q <= 1'b0;
end else begin
// Capture on the FIRST fault, or on an explicit software request. Every
// field is sampled in the SAME cycle — a multi-cycle copy would reintroduce
// exactly the tear this exists to prevent (D1).
if ((fault_detected && !snap_valid_q) || snap_req) begin
snap_q <= '{ lnk_state: link_state,
cfg_epoch: cfg_epoch,
route_epoch: route_epoch,
credits_avail: credits_avail,
credits_outstanding: credits_outstanding,
occupancy: occupancy_q,
occ_high_water: occ_high_water_q,
no_credit_cyc: no_credit_q[31:0],
no_free_id_cyc: no_free_id_q[31:0],
cr_ret_generated: cr_gen_q[31:0],
cr_ret_applied: cr_app_q[31:0],
obligations_outstanding: obligations_q,
timestamp: cycle_q };
snap_valid_q <= 1'b1;
snap_epoch_q <= snap_epoch_q + 16'd1;
if (ctr_saturated) snap_saturated_q <= 1'b1; // D5
end
end
end
// MANDATORY. English: a captured snapshot never changes until explicitly
// cleared. Catches a later "refresh on every fault" change, which would
// replace the first fault's context with the cascade's (§9's error, again).
a_snapshot_stable: assert property (
@(posedge clk) disable iff (!rst_n)
(snap_valid_q && !snap_clear && !snap_req) |=> $stable(snap_q)
);
// MANDATORY. English: the first fault always produces a snapshot. Catches a
// fault path that records a class with no context, which names a symptom and
// locates nothing.
a_fault_captures_context: assert property (
@(posedge clk) disable iff (!rst_n)
(fault_detected && !$past(snap_valid_q)) |=> snap_valid_q
);Architecture. One structure, captured in one cycle, by the fault. Thirteen fields chosen so that the four most common root-cause families — credit accounting, the outstanding window, configuration staleness and retention — are each distinguishable from the dump alone.
State. One snapshot plus an epoch and a valid bit. snap_epoch_q lets software detect a re-capture between reads (D3, 27.4 §13's protocol).
Event. First fault, or explicit request. Note the !snap_valid_q guard: like §8, this is first-wins — a refresh-on-every-fault design replaces the first fault's context with the cascade's, which is §9's error in a different register.
Contract. Every field is sampled in the same cycle from the same clock domain. A field pulled from another die's domain must be a synchronised, stable value (27.5 §13 E3) — capturing a raw cross-domain bus here produces a value that was never real.
Failure. The realistic mistakes are capturing only on software request (D2 — the state has moved on), capturing over multiple cycles (D1 — a torn picture), and refreshing on every fault (the cascade overwrites the cause).
DV/debug. The pair cr_ret_generated / cr_ret_applied in the snapshot is what turns §6's 43 000-cycle mystery into a one-read answer: a difference of exactly one, at the moment of the first fault, names the boundary and the mechanism.
16. Area E — Reset Must Not Destroy Evidence
| # | Must be true | Evidence that counts | FAIL if |
|---|---|---|---|
| E1 | first-fault survives link reset and transport recovery | the reset expression + a test | the failure path erases the cause |
| E2 | the trace survives the resets in the failure path | the retention statement | history lost |
| E3 | the snapshot survives them | the reset expression | context lost |
| E4 | software captures evidence BEFORE any reset | the documented recovery sequence | automatic reset first |
| E5 | diagnostic clear is separate from functional reset | the two controls | cannot re-arm live (27.5 §18 I4) |
| E6 | what survives which reset is written down | the retention table (26.5 §8) | assumed |
Three readings.
E4 is a software review item in a hardware gate, and it belongs here because nobody else asks it. A recovery routine that resets the link before dumping evidence destroys the record every single time — and the hardware was correct. The required sequence is: capture → dump trace → read snapshot → then reset.
E1 is checked by reading one line: the reset expression on the capture registers. if (!rst_n || !link_rst_n) on a first-fault register is the failure, visible instantly (26.5 §9 is the same defect on transaction state).
And E6 exists because retention is an agreement. Debug hardware, link logic and firmware are frequently owned by three different groups. A retention table nobody wrote is three groups' three assumptions, which is 27.5 §8's pattern applied to evidence.
17. Area F — Cross-Die Correlation
Inherited from 27.5 §21, owned here.
| # | Must be true | FAIL if |
|---|---|---|
| F1 | a common time base or shared milestone counter | two dies' traces cannot be interleaved |
| F2 | semantic ID and generation in both sides' records | cannot follow one transaction |
| F3 | config epoch and route epoch in both | stale-config cases undiagnosable |
| F4 | obligations_outstanding_at_recovery on both | 27.5 §8 undiagnosable |
| F5 | credit-return generated/applied on both | §13 row 7 — direction unresolvable |
| F6 | first-fault on each die, independently clearable | one side's cause lost |
| F7 | the contract version readable from silicon | which contract is present is unknown |
And F1 is a hard prerequisite, not a nice-to-have. Without a correlated time base, two dies' traces are two unrelated lists — and every cross-die failure is a correlation problem. This is a design-time decision that cannot be added when the first silicon bug appears, which is precisely the shape §1 describes.
18. Area G — Validating the Debug Mechanisms
The debug logic is logic. Untested logic does not work — and it will be exercised for the first time during the failure it exists for.
| Injection | Must produce |
|---|---|
| a single fault | correct class, ID, epochs, timestamp, context |
| two faults in consecutive cycles | first captured; subsequent count = 1 |
| a fault storm | first captured; count saturates and reports saturation |
| trigger at trace entry 0 | usable dump; valid_cnt correct |
| trigger one entry before wrap | usable dump; wrapped correct |
| trigger after several wraps | wrapped set; the most recent history present |
| a link reset during capture | first-fault and trace SURVIVE (§16) |
| a fault during trace freeze | freeze holds; no mutation |
| counter saturation | sticky saturation reported, not a silent plateau |
| a stale-epoch request | captured with the epoch fields populated |
| a fault on the far die | both sides' records correlate (§17) |
Three readings.
Rows 5 and 7 are where debug hardware most often fails, and both are corner cases of the mechanism rather than of the design. A trigger one entry before wrap exercises the pointer, the wrap flag and the valid count simultaneously. A link reset during capture is not a corner case at all — it is what happens in most real failures, because faults trigger recovery.
The evidence is the injection report, in the same form 27.3 §22 requires for mutation testing: eleven rows, eleven expected outcomes, eleven observed. "We tested the debug logic" is a review statement (§4).
And this belongs to this gate rather than to 27.3 for a specific reason. A DV plan naturally verifies functional requirements; debug registers are frequently outside the functional requirement set entirely, so nobody's plan covers them — and a gap here is invisible until silicon.
19. Area H — Observability Cost
| # | Must be true | Evidence that counts | FAIL if |
|---|---|---|---|
| H1 | the area cost is estimated | flops/SRAM count for the debug set | unbudgeted |
| H2 | the trace depth is justified | depth vs the event rate it must span | arbitrary depth |
| H3 | the power cost is considered | the always-on portion | ignored |
| H4 | capture paths are not on critical timing paths | the timing plan's treatment | debug logic constrains the design |
| H5 | what was cut is recorded, with its risk | the descope list | silent removal |
| H6 | the cost is compared against the debug risk | the argument | cost with no benefit stated |
Three readings.
H2 turns trace depth from a guess into a derivation. Depth × event rate = time span, and the span must cover the gap between divergence and error for the risks §3 enumerated. A 1 024-entry ring at one event per hundred cycles spans ~100 000 cycles — enough for §6's example. At one event per cycle it spans 1 024 cycles and would have wrapped the divergence away. That arithmetic is the justification, and it also drives B8's event selection.
H5 is the row that protects the next review. Observability cut for area is a decision with a named risk, and recording it means the eventual field failure is a known accepted risk rather than a surprise. Silent removal is how a gate that passed produces an undebuggable part.
And H4 is the boundary with physical design (§5). This gate requires that debug logic is not on critical paths and that its cost is budgeted; it does not do timing closure or congestion analysis. Those belong to the PD flow and its owners.
20. Area I — Lab Reproducibility and Access
| # | Must be true | FAIL if |
|---|---|---|
| I1 | every debug register is reachable through the assembled package | unreachable in the lab (27.5 §17 H7) |
| I2 | the access path works when the link is down | debug requires the thing that is broken |
| I3 | the register map documents the read protocol | torn reads trusted |
| I4 | a documented evidence-collection sequence exists | ad-hoc dumps, evidence lost (E4) |
| I5 | failing conditions are reproducible, or the evidence is sufficient without reproduction | "it happened once" |
| I6 | the same instruments exist in emulation/FPGA | pre-silicon debug cannot rehearse |
Two readings.
I2 is the item that turns a good design into an unusable one. If the only path to the debug registers is over the link that has failed, the registers do not exist for the failure that matters. A sideband or independent management path is the standard answer, and the review question is simply "how do I read this with the link down?"
And I6 is cheap and disproportionately valuable. If the same first-fault, trace and snapshot mechanisms exist in emulation, the debug procedure is rehearsed before silicon — and §18's injection campaign runs there naturally. A team that has never dumped its own trace ring will do it for the first time under pressure.
21. Flagship — "We'll Use Firmware Logs"
The pre-tapeout review failure that defines this gate.
What the team presents. Comprehensive firmware logging: every command, every completion, every error interrupt, every state change firmware observes, with timestamps. Megabytes of it, well structured, searchable. The proposal: logs are the debug strategy; dedicated hardware capture is not needed.
What the reviewer asks. "A credit return goes missing at cycle 1 000 and the link times out at cycle 44 000. What in your logs tells you which credit-return boundary failed?"
The answer: nothing.
| Question | Firmware log | Hardware capture |
|---|---|---|
| was a credit return generated? | not visible — no software event | cr_ret_generated (§13) |
| was it applied at the peer? | not visible | cr_ret_applied |
| when did credits start drifting? | not visible | trace ring (§11) |
| what was the first invariant violation? | not visible | first-fault (§8) |
| what was the state at that moment? | not visible | snapshot (§15) |
| what did software see? | the timeout, at cycle 44 000 | — |
Gate decision: FAIL.
Four readings, and the reasoning generalises well beyond credits.
The limitation is structural, not a matter of log quality. A log records what software was told. A credit return that was never generated produced no interrupt, no status change, no event — there was nothing to tell. You cannot log the absence of something that was never reported, so the first log entry is necessarily the timeout: the symptom, 43 000 cycles late (§6).
Better logging cannot fix it, and that is the part teams resist. More verbose logs, higher-frequency polling, timestamps at finer resolution — none of them observe an event that hardware never surfaced. The only fix is hardware that notices the invariant violation when it happens.
And it is a FAIL rather than a CONDITIONAL PASS (§3) because no mitigation exists after tapeout. There is no bench technique, no firmware change and no software workaround that recovers an unrecorded hardware event. The conditional-pass option existed at 27.1 when the counters could still be added; here the only honest outcome is FAIL with a named cost.
The constructive form of the finding is a short list, not a rejection. Credit-return generated/applied counters on both sides; a credit-conservation check feeding first-fault capture; a trace ring covering the drift window; and a fault-triggered snapshot. That is a modest amount of logic (§19), and it converts §6's 43 000-cycle gap into a single register read.
22. The Debug Gate Checklist
Forty-four items.
First-fault capture
| # | Question | FAIL if |
|---|---|---|
| 1 | Is capture first-write-wins? | last-error-wins (§9) |
| 2 | Is an error class recorded, not one bit? | cannot triage |
| 3 | Are semantic ID and generation captured? | cannot correlate |
| 4 | Are config epoch and route epoch captured? | stale-config undiagnosable |
| 5 | Is a timestamp captured? | cannot order across dies |
| 6 | Is there a subsequent-fault count? | one fault vs a storm indistinguishable |
| 7 | Is a resource context snapshot captured with it? | class without context |
| 8 | Is clearing explicit and separate from functional reset? | cannot re-arm live |
| 9 | Read the reset expression — does capture survive link reset? | failure path erases the cause |
Trace
| # | Question | FAIL if |
|---|---|---|
| 10 | Does the ring record before the trigger? | arms on trigger (§12) |
| 11 | Is the post-trigger tail bounded and small? | pre-trigger history wrapped away |
| 12 | Is the post-trigger tail counted in entries, not cycles? | records nothing on a stalled link |
| 13 | Does it freeze, and stay frozen? | overwritten during the dump |
| 14 | Is there a sticky wrap flag? | 1 024 entries vs a million indistinguishable |
| 15 | Is there a valid entry count? | full vs partial unknown |
| 16 | Do entries carry timestamp, identity and epochs? | cannot follow a transaction |
| 17 | Is the event set justified against the span it must cover? | a firehose that wraps in microseconds |
Counters
| # | Question | FAIL if |
|---|---|---|
| 18 | For each counter: which two hypotheses does it separate? | vague → telemetry |
| 19 | Are offered/accepted/transmitted/retried/delivered/retired all present? | stage divergence unlocatable |
| 20 | Are credit-return generated and applied counted separately? | §21 — direction unresolvable |
| 21 | Is stalled-on-no-free-ID counted? | 27.4 §17 invisible |
| 22 | Are recovery enter and exit counted separately? | one long vs many short |
| 23 | Is obligations-outstanding-at-recovery counted on both dies? | 27.5 §8 undiagnosable |
| 24 | Are counters saturating, with saturation reported? | a pinned counter reads as a plateau |
Snapshot
| # | Question | FAIL if |
|---|---|---|
| 25 | Is the snapshot captured in one cycle? | torn state |
| 26 | Is it triggered by the fault, not only by software? | state has moved on |
| 27 | Is it first-wins, not refreshed by the cascade? | cause overwritten |
| 28 | Is there an epoch/valid so software detects re-capture? | torn reads unnoticed |
| 29 | Are all fields from the same clock domain, or synchronised? | a value that was never real |
Reset and evidence
| # | Question | FAIL if |
|---|---|---|
| 30 | Does the trace survive the resets in the failure path? | history lost |
| 31 | Does the snapshot survive them? | context lost |
| 32 | Does the software recovery sequence capture before resetting? | evidence destroyed every time |
| 33 | Is what-survives-which-reset written down? | three groups, three assumptions |
Cross-die
| # | Question | FAIL if |
|---|---|---|
| 34 | Is there a common time base across dies? | traces cannot be interleaved |
| 35 | Do both sides record identity, generation and epochs? | cannot follow a transaction |
| 36 | Is the contract version readable from silicon? | which contract is present is unknown |
Validation, cost, access
| # | Question | FAIL if |
|---|---|---|
| 37 | Has two faults in consecutive cycles been injected? | untested mechanism |
| 38 | Has trigger one entry before wrap been injected? | pointer/flag interaction untested |
| 39 | Has a link reset during capture been injected? | the common real case untested |
| 40 | Is there an injection report with expected vs observed? | "we tested it" |
| 41 | Is the area/power cost estimated and budgeted? | unbudgeted |
| 42 | Is trace depth justified by depth × event rate vs the required span? | arbitrary |
| 43 | Is every debug register reachable through the assembled package? | unreachable in the lab |
| 44 | Does the access path work when the link is down? | debug needs the broken thing |
Run items 1, 9, 10, 18, 20, 32 and 44 first. Each is a single question with a visible answer, and each can fail the gate on its own — first-vs-last capture, the reset expression, pre-vs-post trigger, counter discrimination, credit-return direction, the software sequence, and access with the link down.
23. How the Debug Review Itself Fails
| Failure mode | Looks like | Why it is fatal |
|---|---|---|
| counting mechanisms instead of risks | "we have 40 debug registers" | §3 — the question is coverage of known risks |
| accepting logs as observability | a comprehensive logging plan | §21 — logs cannot record unreported events |
| not reading the reset expressions | capture exists | the failure path erases it (§16) |
| never validating the debug logic | it is in the RTL | §18 — first exercise is the real failure |
| deferring to "we'll add it if needed" | flexible | after tapeout there is no "if needed" |
| reviewing RTL without package access | registers exist | unreachable in the lab (§20 I1) |
Three readings.
Row 1 is the most common shape. A count of registers says nothing about whether the risks the earlier gates named are observable. §3's PASS criterion is explicitly per-risk, which makes the review a mapping exercise: 27.1 §19's three questions, 27.5 §8's retention disagreement, 27.4 §17's window — what evidence exists for each?
Row 5 is the one that sounds most reasonable in the room. "We'll add instrumentation if we find we need it" is a perfectly good policy for firmware and has no meaning for silicon. The moment you know you need it is the moment you cannot have it — which is §1, stated as a schedule decision.
And row 3 is a ten-second check that reviewers skip because it feels like 27.2's job. It is not: the scope of a reset over evidence registers is an observability decision, and its consequence is that the field reports only failures that did not trigger a reset.
24. Red Flags
| Heard in review | Usually means | Ask |
|---|---|---|
| "we'll use firmware logs" | §21 — unreported events are invisible | "what tells you which credit boundary failed?" |
| "we have an error register" | possibly last-error-wins | "first-write or last-write?" |
| "there's a trace buffer" | possibly post-trigger only | "does it record before the trigger?" |
| "we can dump 1 024 entries" | wrap flag unknown | "how do I know it wrapped?" |
| "we have plenty of counters" | telemetry, not discrimination | "which two hypotheses does each separate?" |
| "software reads the status" | possibly torn, possibly too late | "captured by the fault, or by the read?" |
| "reset clears the errors" | §16 — the failure path erases evidence | "which resets clear the capture?" |
| "the driver resets and retries" | evidence destroyed before dump | "what is captured before the reset?" |
| "the debug logic is simple" | §18 — never validated | "what happens on a trigger one entry before wrap?" |
| "we'll add it if we need it" | there is no later | "which risk are we accepting?" |
| "JTAG can read everything" | maybe not with the link down | "how do I read this when the link is dead?" |
And the single most useful question in the room is the first one, generalised: "walk me through the failure we most fear, cycle by cycle, and tell me what we would read." Any step where the answer is "nothing" is a finding.
25. Common Misconceptions
"The error register tells you why the link failed." §9: with last-error-wins it tells you the final escalation — usually TIMEOUT, which is consistent with dozens of root causes.
"The first error is the root cause." §6: the first recorded error is often 43 000 cycles after the first divergence. Capture invariant violations, not symptoms.
"Firmware logs are enough." §21: a log records what software was told. A credit return that was never generated produced no event to log.
"More verbose logging would have caught it." §21: verbosity does not create an observation of something hardware never surfaced.
"The trace buffer captures the failure." §12: if it arms on the trigger, every entry is a consequence.
"A 1 024-entry dump is 1 024 events." §10 B4: without a wrap flag it could be the last 1 024 of a million.
"More counters means better observability." §13: a counter that separates no hypotheses is telemetry. The test is which two explanations it distinguishes.
"Software can read the state after the fault." §14 D2: by then credits have recovered and the link has retrained. Capture must be triggered by the fault.
"Reset clears errors — that's what reset is for." §16 E1: if it clears first-fault capture, the recovery erases the cause of the failure that triggered it.
"The debug logic is too simple to need testing." §18: a trigger one entry before wrap, and a link reset during capture, are the conditions that occur in the failure you built it for.
"We'll add instrumentation if we need it." §23: for silicon, the moment you know you need it is the moment you cannot have it.
26. Understanding Check
27. Summary
Seven things.
Debug capability is architectural (§1). The moment you know you need an instrument is the moment you can no longer add one — which is why this gate runs before tapeout and why "we'll add it if needed" has no meaning here.
The first recorded error is not the first divergence (§6). Credit return missing at cycle 1 000, TIMEOUT at 44 000. Capture invariant violations, not symptoms.
First-wins, not last-wins (§7–§9). With a class, semantic ID, generation, both epochs, a timestamp, a subsequent-fault count, and a context snapshot — and a reset expression that excludes the resets in the failure path.
The ring records before the trigger (§10–§12), with a bounded entry-counted tail, a hard freeze, a sticky wrap flag and a valid count. A post-trigger-only trace records the cascade.
Counters must discriminate (§13). Which two hypotheses does this separate? — and credit-return generated versus applied, plus obligations-outstanding-at-recovery on both dies, are the two that close this module's recurring failures.
Validate the debug logic (§18). Trigger one entry before wrap; a link reset during capture; two faults in consecutive cycles. Eleven injections, expected versus observed — because the mechanism's first real exercise is the failure it exists for.
And "we'll use firmware logs" is a FAIL (§21). A log records what software was told, and the events that matter most were never reported. No mitigation exists after tapeout.