Skip to content

PCIe · Module 30

RTL Checklist — When Two Legal Events Collide

Endpoint RTL rarely fails on steady-state equations. It fails when an allocation and a retirement land in the same cycle, when a tag is freed while its Completion is still coming, and when clean lint certifies a counter that loses every simultaneous update.

30.1 asked whether the architecture is decisive. This gate asks whether the RTL is faithful to it — under every simultaneous event, reset, error and lifetime corner the architecture permitted.

1. What This Gate Owns

Most hard RTL bugs are not wrong steady-state equations; they are wrong ownership transitions when two legal events happen together.

Three readings, and the third decides how to run the review.

The steady-state path is the part everyone reviews and the part that works. A counter that increments correctly, an FSM that advances correctly, a queue that fills and drains correctly — these are visible in a waveform and get checked. The failure is in the cycle where both things happen.

Which means the review question is combinatorial, not sequential. For each piece of state: enumerate the simultaneous events, and check the design handles the cross product rather than each event alone. §5 is that idea applied to a counter; §6 to a tag table; §9 to configuration.

And this is emphatically not a Verilog style guide. Naming, formatting and lint are §14's subject, and §14's point is that they prove none of this. Every item here ties to a PCIe subsystem mechanism.

This gate ownsOwned elsewhere
faithful implementation of 30.1's contractsthe contracts themselves — 30.1
event ownership, lifetimes, simultaneity, widths, reset scopewhether the environment can disprove the design — 30.3
SVA hooks the design must exposeSoC-facing contracts — 30.4
the mechanisms of 23.123.6 applied as gatesre-teaching those mechanisms

2. The Acceptance Event

The single highest-yield question in this checklist, and it is one sentence: what exact event means this happened?

ThingThe eventThe trap
request acceptedvalid && readyvalid alone
queue entry allocatedthe same accepted eventa separate decode of valid
tag allocatedthe accepted eventallocation on decision to send
Completion consumedconsumed by the sink, not arrivalarrival at the input
credit consumedat the defined consumption point (16.5)at request generation
descriptor retiredafter the write-back is visibleafter it is issued
DMA "done"the final byte retiredthe last request issued (29.1 §7)

The rule: one named wire per event, used everywhere. Every counter, allocation, trace write and assertion that concerns "a request was accepted" must reference the same signal. Two independent decodes of the same idea will diverge — under back-pressure, under reset, or after someone edits one of them.

3. Wrong RTL — valid Means Accepted

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG. ILLUSTRATIVE. The requester allocates on valid. It is correct whenever
// the consumer is never busy, which is every early simulation.
always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n) begin
    outstanding_q <= '0;
  end else begin
    // BUG: req_valid may be held for many cycles while req_ready is low.
    //      Every one of those cycles allocates again.
    if (req_valid) begin
      outstanding_q       <= outstanding_q + 1'b1;
      tag_live_q[next_tag] <= 1'b1;
      trace_q[wr_ptr_q]    <= req_desc;
      wr_ptr_q             <= wr_ptr_q + 1'b1;
    end
  end
end

Failure — the timeline. One request offered; the consumer is busy for four cycles.

Cyclereq_validreq_readyReal requestsoutstanding_qTrace entries
001000
1101 offered11
210still the same one22
310still the same one33
4111 accepted44
50144 — for one request
later1 Completion arrivesdecrements to 3
end of test0 outstanding in reality3 — never drains

First divergence: cycle 2 — the second allocation for a request that has not yet been accepted.

Root cause. valid is an offer; valid && ready is a transaction. The design treated a sustained offer as repeated events, and back-pressure is exactly when offers are sustained.

Three consequences, and the third is the dangerous one. The counter inflates and never drains, so tag exhaustion appears at a fraction of the real load. The trace fills with duplicates, so debug evidence is corrupted before the investigation starts. And the design works perfectly in any test where ready is always high — which is most early tests, so this survives to the lab.

Corrected — one wire, used everywhere.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// CORRECT. The accepted event, named once. Every consumer of "a request
// happened" references THIS wire — never re-derives it.
wire req_fire = req_valid && req_ready;
 
always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n) begin
    outstanding_q <= '0; wr_ptr_q <= '0;
  end else if (req_fire) begin
    outstanding_q        <= outstanding_q + 1'b1;
    tag_live_q[alloc_tag] <= 1'b1;
    trace_q[wr_ptr_q]     <= req_desc;
    wr_ptr_q              <= wr_ptr_q + 1'b1;
  end
end
 
// MANDATORY. English: payload and control must not change while an offer is
// pending. Catches the other half of the same misunderstanding — a producer
// that treats a stalled offer as retractable.
a_stable_while_stalled: assert property (
  @(posedge clk) disable iff (!rst_n)
    (req_valid && !req_ready) |=> (req_valid && $stable(req_desc))
);

Six lenses. Architecture: one canonical event so no two consumers disagree. State: the counter, tag bitmap and trace pointer, all advanced by the same wire. Event: valid && ready, once. Contract: the producer must hold valid and keep the payload stable until accepted — which is what a_stable_while_stalled enforces, and which is the assumption the consumer is entitled to make. Failure: re-deriving the event in a second always block, which drifts the moment someone edits one. DV/debug: the mutation test in 30.3 §8 — change req_fire back to req_valid and confirm the environment fails.

4. Two Non-Blocking Assignments to One Register

The classic. Two legal events, two separate if statements, and the second silently wins.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG. ILLUSTRATIVE. Both branches are correct in isolation and the code
// reads as obviously right.
always_ff @(posedge clk) begin
  if (alloc_fire)  count_q <= count_q + 1'b1;
  if (retire_fire) count_q <= count_q - 1'b1;   // same cycle: THIS one wins
end

When both fire in the same cycle, the count should be unchanged. It decrements. The error is −1 per coincidence, and coincidences get more frequent as the design gets busier — so the drift is proportional to load, and it presents as "we lose outstanding slots under stress."

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// CORRECT. ILLUSTRATIVE. One next-state expression, width-safe, bounded, and
// with the "both" case handled by construction rather than by ordering.
localparam int N_TAG = 64;
localparam int CW    = $clog2(N_TAG + 1);        // +1 — the count reaches N_TAG
 
logic [CW-1:0] count_q;
wire  [CW:0]   count_nxt =                        // one extra bit for the check
        {1'b0, count_q} + {{CW{1'b0}}, alloc_fire} - {{CW{1'b0}}, retire_fire};
 
always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n)      count_q <= '0;
  else             count_q <= count_nxt[CW-1:0];
end
 
// MANDATORY. English: the count never wraps in either direction. Underflow is
// the more dangerous case — an unsigned count_q of 0 minus 1 becomes the
// maximum, and every "is there room?" test then answers yes forever.
a_count_no_wrap: assert property (
  @(posedge clk) disable iff (!rst_n)
    (count_q <= CW'(N_TAG)) and
    (retire_fire |-> (count_q != '0 || alloc_fire))
);

Six lenses. Architecture: simultaneity is expressed arithmetically, so no ordering question exists. State: one counter, one derivation. Event: both fire signals in one expression. Contract: callers may assume count_q is the true live count in every cycle — including cycles where both events occur. Failure: the wrong version drifts under load and, if it ever underflows, count_q becomes N_TAG-ish and the allocator believes it is full forever — or, with the comparison the other way, believes it has infinite room. DV/debug: coverage must include the both-fire-same-cycle bin explicitly; it will not arise reliably from random stimulus at low occupancy.

And $clog2(N_TAG + 1) is not a typo. A 64-entry structure needs a count that can represent 65 values, 0 through 64. $clog2(64) is 6 bits and cannot hold 64. This single off-by-one is one of the most common width bugs in queue logic, and it manifests as "the last entry can never be used" or as a wrap at full.

5. Tag Lifetime

A tag is not a number; it is a lease. The review reconstructs the lease's full life:

PhaseEventQuestion
allocatedreq_fireis the tag marked live in the same cycle it is used?
livecan anything free it besides retirement?
Completion matchedCompletion accepted and tag livewhat if it is not live?
partially completesplit Completion (13.3)are bytes_left and status updated, not the live bit?
retiredfinal Completion consumedis live cleared exactly once?
reusableafter retirementcan a stale Completion still arrive?

The last row is the one that turns a tag into a generation. If the answer is "yes, after a local timeout", then the tag number alone cannot distinguish this lease from the previous one — and §7 is the timeline.

6. RTL — A Tag Table

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE. A tag table sized for review, not for completeness. Everything
// here exists to make the lease explicit: who holds it, since when, under which
// generation, and how much of it is left.
localparam int N_TAG = 64;
localparam int GW    = 4;                         // generation width — see §7
 
typedef struct packed {
  logic          live;
  logic [GW-1:0] gen;
  logic [15:0]   bytes_left;
  logic [1:0]    status_acc;     // accumulated, never last-write-wins
  logic [7:0]    cfg_epoch;      // which active config this was issued under
} tag_ctx_t;
 
tag_ctx_t      ctx_q [N_TAG];
logic [GW-1:0] gen_q [N_TAG];    // NEXT generation for each tag
logic [31:0]   stale_cpl_q;      // Completions dropped as stale — observability
 
wire req_fire = req_valid && req_ready;
wire cpl_fire = cpl_valid && cpl_ready;
 
// A Completion is ours only if the tag is live AND the generation matches.
// Both conditions. The tag alone is not an identity across reuse.
wire cpl_match = cpl_fire
              && ctx_q[cpl_tag].live
              && (ctx_q[cpl_tag].gen == cpl_gen);
 
// Final Completion for this request: this Completion consumes the remainder.
wire cpl_final = cpl_match && (cpl_bytes >= ctx_q[cpl_tag].bytes_left);
 
always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n) begin
    for (int t = 0; t < N_TAG; t++) begin
      ctx_q[t] <= '0;
      gen_q[t] <= '0;
    end
    stale_cpl_q <= '0;
  end else begin
    // Allocation and retirement are written in ONE place per field, so an
    // allocation of tag X and a retirement of tag Y in the same cycle cannot
    // interfere — and an allocation and retirement of the SAME tag in one
    // cycle is impossible by construction, because a tag cannot be allocated
    // while it is live.
    if (req_fire) begin
      ctx_q[alloc_tag].live       <= 1'b1;
      ctx_q[alloc_tag].gen        <= gen_q[alloc_tag];
      ctx_q[alloc_tag].bytes_left <= req_bytes;
      ctx_q[alloc_tag].status_acc <= 2'b00;
      ctx_q[alloc_tag].cfg_epoch  <= cfg_epoch_active;
    end
 
    if (cpl_match) begin
      // Status ACCUMULATES — a later good Completion must not erase an earlier
      // error. Last-write-wins here silently converts a failed transfer into a
      // successful one.
      ctx_q[cpl_tag].status_acc <= ctx_q[cpl_tag].status_acc | cpl_status;
 
      if (cpl_final) begin
        ctx_q[cpl_tag].live       <= 1'b0;
        ctx_q[cpl_tag].bytes_left <= '0;
        // The generation advances at RETIREMENT, so any Completion that arrives
        // after this point carries the old generation and cannot match.
        gen_q[cpl_tag]            <= gen_q[cpl_tag] + GW'(1);
      end else begin
        ctx_q[cpl_tag].bytes_left <= ctx_q[cpl_tag].bytes_left - cpl_bytes;
      end
    end else if (cpl_fire) begin
      // Not ours. Counted, never applied. An unobservable invariant is an
      // assumption (30.1 §11).
      stale_cpl_q <= stale_cpl_q + 32'd1;
    end
  end
end

Architecture. One context per tag plus a next generation per tag. The generation is stored separately from the context because it must survive the context being cleared — that is the whole mechanism.

State. live, gen, bytes_left, status_acc, cfg_epoch. status_acc uses OR-accumulate, which is the difference between reporting a partially-failed transfer and silently reporting success.

Event. Allocation on req_fire; retirement on cpl_final; generation advance on retirement, not on allocation. Advancing on allocation would leave a window where the old generation is still current.

Contract. The Completion path must supply cpl_genan interface requirement this module cannot enforce alone, exactly the kind of cross-boundary contract 30.1 §4 makes a matrix row. If the responder cannot echo a generation, the design must instead guarantee no stale Completion is possible, and that guarantee needs its own argument.

Failure. cpl_match without the generation term → §7. Retiring on the first Completion rather than the final one → data loss on splits. Status last-write-wins → a failed transfer reported as good.

DV/debug. stale_cpl_q non-zero is a fact, not an alarm: it proves the defence engaged. A design that claims stale Completions are impossible and has no counter has an assumption, not an invariant.

7. Wrong RTL — Tag Freed While the Request Is Live

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG. ILLUSTRATIVE. A local timeout frees the tag so the requester can make
// progress. Reasonable-sounding, and it creates an aliasing window.
if (local_timeout[t]) begin
  ctx_q[t].live <= 1'b0;          // BUG 1: freed, with no generation advance
  // BUG 2: the peer was never told. Its Completion is still coming.
end

Failure — the timeline.

TimeEventTag 7Consequence
0request A issued with tag 7live, gen 2
500 nsA's Completion delayed (congestion — 29.6)live
1 µslocal timeout frees tag 7not live, gen still 2A abandoned locally
1.1 µsrequest B allocated tag 7live, gen still 2same identity as A
1.4 µsA's Completion arrives — tag 7, gen 2matches BB retires on A's data
1.5 µsB's real Completion arrives — tag 7, gen 2B no longer livedropped or double-retires
latercorrupt data, no error

First divergence: 1.4 µs. Root cause: the tag was reused without the identity changing. BUG 1 is the missing generation advance; BUG 2 is the deeper architectural point — freeing a tag locally does not free it at the peer, and this is exactly the situation 30.1 §6's reset matrix row 1 exists to force a decision about.

The corrected policy, and its three acceptable forms. Advance the generation when the tag is freed by timeout, so a late Completion cannot match (the §6 code does this at retirement; a timeout path must do the same). Or quarantine the tag for longer than the maximum Completion latency before reuse. Or do not free on local timeout at all — escalate instead. All three are defensible; silently reusing is not.

And the exact timeout value is deliberately not stated here. 30.1 §11 assigns the timeout policy to architecture with a citation; this gate checks that the RTL implements whatever was decided, and that reuse is safe under it.

8. Reset — Per Register, Not Per Block

The anti-pattern is a single reset branch clearing everything, which encodes 30.1 §7's "reset clears everything" into silicon.

Ask of every registerFail criterion
which reset clears it?"the reset"
does it survive link Recovery (18.5)?unexamined
is it async-asserted / sync-deasserted?mixed conventions in one module
does clearing it orphan work at the peer?§7's BUG 2
are statistics cleared by a functional reset?evidence destroyed by an unrelated event
is first-fault preserved?last-error-wins (30.1 §13 Q40)

And the practical review technique is mechanical: list every register in the module, then for each of the four reset types mark clear / retain / reconstruct. Registers that end up in a different column from their neighbours are the interesting ones — they are usually right, and they are usually the ones a single reset branch got wrong.

9. Configuration — Shadow and Commit

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE. Software writes fields one at a time; the datapath must never
// observe a half-updated configuration. Shadow, validate, commit atomically,
// and stamp an epoch so in-flight work knows which config it belongs to.
logic [31:0] cfg_shadow_q, cfg_active_q;
logic [7:0]  cfg_epoch_active_q;
logic        cfg_valid_q;
 
wire cfg_consistent = check_cfg(cfg_shadow_q);      // pure function, no state
 
always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n) begin
    cfg_shadow_q <= CFG_RESET; cfg_active_q <= CFG_RESET;
    cfg_epoch_active_q <= '0;  cfg_valid_q  <= 1'b0;
  end else begin
    // Software may write the shadow at any time. The datapath never reads it.
    if (cfg_wr_fire) cfg_shadow_q <= cfg_wr_data;
 
    // Commit is a single event, gated on quiescence AND validity. Committing
    // with work in flight is the failure this whole structure prevents.
    if (cfg_commit_fire && cfg_consistent && (outstanding_q == '0)) begin
      cfg_active_q       <= cfg_shadow_q;
      cfg_epoch_active_q <= cfg_epoch_active_q + 8'd1;
      cfg_valid_q        <= 1'b1;
    end
  end
end
 
// MANDATORY. English: the active configuration is stable while any request is
// outstanding. This is the property the datapath depends on, stated directly.
a_cfg_stable_when_busy: assert property (
  @(posedge clk) disable iff (!rst_n)
    (outstanding_q != '0) |=> $stable(cfg_active_q)
);

Six lenses. Architecture: separate requested from active so a partial write is never observable. State: shadow, active, epoch, valid. Event: commit is one gated event; cfg_wr_fire never touches active. Contract: software must write the shadow then commit — a two-step protocol that must appear in the programming model, or drivers will write and expect immediate effect. Failure: committing without the quiescence check lets a request issued under config N retire under config N+1, which is why cfg_epoch is in §6's context. DV/debug: the negative test is commit with work outstanding and confirm nothing changes; the assertion is the same statement.

And the quiescence requirement is a real cost worth naming. Requiring outstanding_q == 0 means a reconfiguration waits for a drain. If the architecture cannot accept that latency, the alternative is per-request cfg_epoch carried end to end — which is more RTL, and it is a decision 30.1 should have made, not one to discover here.

10. Widths, Parameters and Arithmetic

ItemThe trapCorrect
$clog2(N) for a counta 64-entry structure needs 65 values$clog2(N + 1) (§4)
$clog2(1)evaluates to 0 — a zero-width signalspecial-case, or $clog2(N) > 0 ? ... : 1
unsigned subtraction0 - 1 becomes the maximum, and "room available?" answers yes foreverguard, saturate, or use a signed next-state
byte counterswrap after a plausible run time48 bits and saturating (29.2 §6)
generation widthwrap inside one Completion's flight timederive it (29.5 §8)
FIFO pointersfull and empty indistinguishableextra bit, or a separate count
truncating casts16'(x) silently drops bitssize explicitly at the source
parameter-derived slices[N-1:0] with N = 0elaboration-time $error

The reusable technique is the elaboration check (29.3 §7): a configuration that cannot be correct should fail the build, not the lab. Every parameterised structure should carry one.

11. Combinational and FSM Review

CheckTie to this subsystem
every branch assigns every output, or defaults precedea missing branch in the Completion decoder infers a latch on bytes_left
unique/priority used only where genuinely truea "unique" Completion-status case that is not unique masks an illegal status
no combinational path from ready back to valida ready = f(valid) loop deadlocks the whole requester
FSM has an explicit default returning to a safe statea one-hot state corrupted by an X during reset never recovers
functions used in RTL are purecheck_cfg (§9) must not have side effects, or commit becomes order-dependent
no X propagating from uninitialised memorytag context read before written after reset

And the ready/valid loop deserves its own emphasis because it is the one that produces a hang rather than a wrong value. The rule is directional: valid may not depend combinationally on ready. A producer that waits to see ready before asserting valid, facing a consumer that waits for valid before asserting ready, is a deadlock that no amount of stimulus will shake loose.

12. Clock and Reset Crossings

If the subsystem has more than one clock, every crossing is a review item.

CrossingRequirement
configuration writescommit as one event — a multi-bit config crossing bit-by-bit is a partial config (§9)
status / countersvalue must be sampled coherently, or a reader sees a mix of two states
single-cycle event pulsestreat as suspicious — a pulse narrower than the destination clock is lost
interrupt / doorbell eventsmust not be lost; handshake or toggle, never a bare pulse
queue ownership across domainsone owner per domain, with an explicit handoff
reset deassertionsynchronised per domain; a shared async deassert produces different first cycles

The single-cycle pulse is the highest-yield item. A doorbell or completion event crossing as a one-cycle pulse into a slower domain is lost, not delayed — and the symptom is an occasional missing interrupt or a stuck queue, appearing at a rate that depends on the clock ratio. Ratio-dependent bugs are the hardest class to reproduce, because changing frequency to investigate changes the bug.

13. Assertions the RTL Must Carry

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// English: a tag is never allocated while it is already live. The primary
// integrity property of the tag table, and an excellent formal target.
a_no_double_alloc: assert property (
  @(posedge clk) disable iff (!rst_n)
    req_fire |-> !ctx_q[alloc_tag].live
);
 
// English: a Completion is only applied to a live request with a matching
// generation. Catches §7 directly.
a_cpl_only_when_live: assert property (
  @(posedge clk) disable iff (!rst_n)
    cpl_applied |-> (ctx_q[cpl_tag].live && (ctx_q[cpl_tag].gen == cpl_gen))
);
 
// English: a request retires exactly once. Two-cycle window because retirement
// and a subsequent Completion are separate events.
a_retire_once: assert property (
  @(posedge clk) disable iff (!rst_n)
    cpl_final |=> !ctx_q[$past(cpl_tag)].live
);
 
// English: outstanding count equals the number of live tags. An identity that
// ties two representations together — the classic place they drift.
a_count_matches_table: assert property (
  @(posedge clk) disable iff (!rst_n)
    count_q == CW'($countones(live_vector))
);
 
// English: first fault is sticky until explicitly cleared.
a_first_fault_sticky: assert property (
  @(posedge clk) disable iff (!rst_n || err_clear)
    $rose(first_fault_valid) |=> always first_fault_valid
);

Three readings.

a_count_matches_table is the highest-value one and the least often written. Designs carry a count and a bitmap because each is cheap for a different question — and nothing keeps them consistent. The identity catches §4's two-NBA drift, §3's over-allocation, and any future path that updates one and not the other.

a_first_fault_sticky needs err_clear in the disable term, for the same reason 29.4 §8 needed stat_clear: a legitimate clear must not fail the assertion, or the assertion gets deleted.

And the first four are strong formal targets — small state, bounded arithmetic, and each states design intent rather than restating the code. a_no_double_alloc in particular is usually provable in minutes and is worth more than any amount of random stimulus.

14. Lint Is Not Sign-Off

Clean lint proves a real and useful class of things: syntax, inferred latches, width mismatches, unreachable code, incomplete sensitivity.

It proves none of this:

Not proven by lintWhere it is caught
the acceptance event is the right event§3 — valid lints perfectly
simultaneous updates net correctly§4 — two NBAs lint clean
tag lifetime is safe under reuse§7
reset scope matches the architecture§8
configuration is atomic with respect to traffic§9
ordering assumptions hold30.1 §9

The demonstration to keep in mind. §4's wrong version — two if statements, two non-blocking assignments — is perfectly clean under lint. Correct syntax, no latch, no width mismatch, no unreachable branch. It also loses one count on every coincidence, at a rate proportional to load. Lint has no opinion about whether the second assignment should have won.

So the review posture is: lint is a precondition, not evidence. A design that fails lint is not ready to review. A design that passes lint has earned the right to be reviewed for the things in this chapter.

15. The RTL Checklist — 48 Questions

Acceptance and events

#QuestionWhyEvidenceFAIL if
1Is there one named wire per protocol event, used everywhere?§2the wire, and all its usestwo decodes of the same idea
2Is allocation on valid && ready, never valid?§3the expressionvalid alone, unjustified
3Is "DMA done" the final byte retired, not the last request issued?29.1 §7the retirement eventissue-side signal
4Is a Completion "consumed" at the sink, not at arrival?back-pressure makes them differthe consume eventarrival used
5Is payload stable while an offer is stalled?§3's assertiona_stable_while_stalledno assertion
6Are credits consumed at the architecturally-defined point?16.5the point named and matchedgeneration-time

Counters and arithmetic

#QuestionWhyEvidenceFAIL if
7Does every counter use one next-state expression?§4the expressiontwo NBAs
8Are simultaneous increment and decrement correct?§4the both-fire coverage binuntested
9Is $clog2(N + 1) used for counts of N items?§4, §10the declaration$clog2(N)
10Can any unsigned counter underflow?§10guard or saturatebare subtraction
11Are byte counters wide enough and saturating?29.2 §6width + saturation32-bit wrapping
12Is $clog2(1) == 0 handled for degenerate parameters?§10elaboration checkzero-width signal
13Are all parameterised structures guarded by an elaboration check?29.3 §7$error on bad configsnone
14Do casts size explicitly rather than truncate silently?§10explicit widthsbare 16'(x) on wide data

Queues and occupancy

#QuestionWhyEvidenceFAIL if
15Is occupancy bounded by construction and asserted?§13bound + assertioncomment only
16Are FIFO full and empty distinguishable?§10extra bit or countambiguous pointers
17Is the back-pressure threshold derived from committed bytes?29.3 §5the inequalitya percentage
18Is there a drop/overflow counter, even where overflow is impossible?unobservable invariant = assumptionthe counternone
19Can one class's queue block another's progress?29.6 §9separate pools or a floorshared, unexamined

Tags and identity

#QuestionWhyEvidenceFAIL if
20Is a tag ever allocated while live?§13a_no_double_allocno check
21Is there a generation, and is its width derived?§6, §7the derivationabsent or arbitrary
22Does the generation advance at retirement?§6the code pathat allocation
23Can a local timeout free a tag whose Completion may still arrive?§7quarantine, generation advance, or no local freesilent reuse
24Is count_q tied to the live bitmap by an assertion?§13a_count_matches_tabletwo independent representations
25Are stale Completions counted rather than silently dropped?§6stale_cpl_qdropped silently

Completions

#QuestionWhyEvidenceFAIL if
26Is correlation by tag, never by arrival order?30.1 §9 · 13.4the match expressionFIFO matching
27Are split Completions handled with bytes_left?13.3the decrement pathretire on first
28Does status accumulate rather than last-write-wins?§6the OR-accumulateoverwrite
29Does retirement happen exactly once per request?§13a_retire_onceno check
30What happens to a Completion for a non-live tag?§6counted, not appliedapplied, or silently dropped

Reset and recovery

#QuestionWhyEvidenceFAIL if
31Is each register's reset scope stated individually?§8the per-register tableone reset branch
32Does anything clear on link Recovery that the peer still holds?§7, 30.1 §6the matrix rowunexamined
33Are statistics cleared by functional resets?evidence lossseparate clearshared
34Is first fault preserved across the event that caused it?§13a_first_fault_stickylast-error-wins
35Is reset deassertion synchronised per clock domain?§12the synchronisershared async deassert

Configuration

#QuestionWhyEvidenceFAIL if
36Are requested and active configuration separate?§9shadow + activeone register
37Is commit a single gated event?§9the commit expressionfield-by-field
38Is the active config stable while work is outstanding?§9a_cfg_stable_when_busyno check
39Is the commit protocol in the programming model?drivers must knowthe documentRTL-only

Combinational, FSM, CDC

#QuestionWhyEvidenceFAIL if
40Any combinational path from ready to valid?§11 — deadlock, not a wrong valuethe analysispresent
41Does every FSM have a safe default?§11the default branchnone
42Are unique/priority genuinely true?§11the argumentused for optimisation only
43Are single-cycle pulses crossed safely?§12handshake or togglebare pulse
44Are multi-bit config values crossed as one event?§12the crossing schemebit-by-bit

Observability and DV hooks

#QuestionWhyEvidenceFAIL if
45Are the 30.1 §13 Q11 observables actually implemented?architecture named themthe registersnamed but absent
46Is there an outstanding high-water mark?one register, ends a class of investigationthe registernone
47Are all assertions in §13 present and enabled?§13the assertionscommented out
48Does the design expose what 30.3 needs to check independently?a checker with no visibility cannot checkthe interfaceinternal-only state

16. Misconceptions

"Lint is clean, so the RTL is clean." §14: §4's two-NBA counter lints perfectly and loses a count on every coincidence.

"valid means the transaction happened." §3: it means an offer. Under back-pressure the offer persists, and the design counted four requests for one.

"It works in simulation." §3: with ready always high, the bug cannot appear. Most early tests hold ready high.

"A tag is just a number." §7: after reuse, the number is ambiguous. Identity requires a generation unless reuse is provably safe.

"Reset clears everything." §8 and 30.1 §7: which reset, and does the peer agree?

"Both assignments are correct, so the code is correct." §4: individually correct, jointly wrong — and the second one silently wins.

"Faster is safer for a single-cycle pulse." §12: it is lost, not delayed, and the loss rate depends on the clock ratio — so investigating by changing frequency changes the bug.

"We'll add assertions in DV." §13, and §15 Q48: assertions on internal state must live with the RTL; a testbench cannot see ctx_q.

17. Understanding Check

Q1. A design allocates on req_valid. Why does it pass every early test and fail in the lab?

Because valid is an offer and valid && ready is a transaction, and early tests hold ready high (§2, §3). With no back-pressure the two are identical. Under back-pressure a sustained offer is counted repeatedly: §3's timeline shows four allocations for one request across cycles 1–4, and the counter never drains — one Completion decrements it to 3 and it stays there. Three consequences follow, and the third is the worst: tag exhaustion appears at a fraction of the real load; the trace ring fills with duplicates, so debug evidence is corrupted before anyone starts investigating; and the design is correct in exactly the conditions under which it is usually tested. The fix is one named wire — req_fire = req_valid && req_ready — referenced by every consumer, never re-derived, plus a_stable_while_stalled to enforce the producer's half of the contract.

Q2. Two if statements each assign count_q non-blockingly. What is wrong and how does it present?

When both fire in the same cycle the second wins, so the count decrements when it should be unchanged (§4). The error is −1 per coincidence, and coincidences become more frequent as the design gets busier — so the drift is proportional to load and presents as "we lose outstanding slots under stress," which sounds like a capacity problem rather than an arithmetic one. The fix is one next-state expression covering all four cases by construction. Two details go with it: $clog2(N + 1), because a 64-entry structure needs 65 representable values and $clog2(64) is 6 bits; and an underflow guard, because an unsigned count_q going 0 − 1 becomes the maximum and every "is there room?" test then answers yes forever. Coverage must include the both-fire-same-cycle bin explicitly — random stimulus at low occupancy will not produce it reliably.

Q3. A local timeout frees tag 7 so the requester can proceed. What breaks?

The tag is reused with the same identity, so a late Completion for the abandoned request matches the new one (§7). Timeline: A holds tag 7; congestion delays its Completion; at 1 µs the local timeout frees the tag with no generation advance; B allocates tag 7 at 1.1 µs; A's Completion arrives at 1.4 µs, matches B, and B retires on A's data. B's real Completion then arrives against a dead entry. Corrupt data, no error.

Two bugs, and the second is architectural. The missing generation advance is the RTL defect; freeing a tag locally does not free it at the peer is the design defect — the peer never agreed to anything. Three fixes are defensible: advance the generation on the timeout path so a late Completion cannot match; quarantine the tag for longer than the maximum Completion latency; or do not free on local timeout at all and escalate instead. Silently reusing is the only unacceptable option. The timeout value is 30.1 §13 Q11's to decide; this gate checks that reuse is safe under whatever was decided.

Q4. Lint is clean and the block passes its directed tests. What has that established?

A precondition, not evidence (§14). Lint proves syntax, inferred latches, width mismatches, unreachable code and incomplete sensitivity — all real and all necessary. It has no opinion about which of two non-blocking assignments should win, so §4's broken counter passes it perfectly. It also cannot see that the acceptance event is the wrong event (§3), that a tag's lifetime is unsafe under reuse (§7), that reset scope contradicts the architecture (§8), or that configuration is not atomic with respect to traffic (§9).

Directed tests add little here for a structural reason: this chapter's failures are simultaneity failures, and directed tests exercise events one at a time by design. The evidence that counts is the cross product — the both-fire coverage bin, the stalled-offer case, tag reuse after a timeout, commit with work outstanding — plus the assertions in §13, of which a_count_matches_table earns its place by tying the count and the live bitmap together. Designs carry both representations because each is cheap for a different question, and nothing otherwise keeps them consistent.

18. What Comes Next

This gate asked whether the RTL is faithful. The next asks a harder question about the environment that judged it.

30.3 tests whether verification is capable of disproving the design, or has merely accumulated passing tests — including the case where the predictor's expected data descends from the DUT's own output, and every test agrees with itself forever.