Skip to content

UCIe · Module 27

RTL Review Checklist

The pre-tapeout RTL gate — how to read code against a decision record rather than line by line, the accepted-event and simultaneous-event mistakes that silently corrupt every counter downstream, unsigned underflow that turns a small bug into a permanent stall, FSM and reset defects that only appear after recovery, and how to review an assertion for vacuity rather than for existence.

27.1 closed the decisions. This gate asks whether they are actually in the code — and whether the code survives every event the architecture permits.

1. The One-Sentence Model

An RTL review asks two questions of every module: can each architecture decision be found in this code, and does this code behave correctly for every event the architecture allows — including the combinations nobody wrote a test for.

The first question is traceability. A decision recorded in a document and absent from the RTL was not implemented, whatever the document says (27.1 §16). A review that does not check this is checking style.

The second question is where the real defects live, and it has a specific shape. Almost every bug in this chapter is correct for the events the author imagined and wrong for a combination they did not — two events in the same cycle (§8), a subtraction that underflows once (§10), a state entered only after a recovery (§14). Each is invisible in the common case and permanent once it happens.

2. What This Gate Owns

GateOwnsNot this chapter
27.1 — Architecturewhich decisions exist and who owns them
27.2 — RTL (this chapter)whether the code implements them and survives permitted events
27.3 — Verificationwhether the DV plan can find what review cannotcoverage closure, checker independence
27.4 — Performancemeasured performance against budgetprofiling
27.5 — Integrationpackage-level contract closuresupplier sign-off
27.6 — Debugtriage when a link failsfirst-divergence procedure

Three boundaries, because this is the gate whose scope creeps most.

This gate does not re-open architecture. If the review finds that the retention policy is wrong, that is a 27.1 finding raised late, and it should be recorded as such rather than fixed quietly in RTL. A design where architecture decisions get made during RTL review has no architecture gate.

This gate does not replace verification, and the division is precise. Review finds defects that are visible in the code; DV finds defects that require state to expose. §8's lost decrement is visible in five lines. §14's unreachable state is visible in the case statement. Neither needs a simulation, and both routinely survive one.

And this gate does not chase style. Naming, formatting and structure matter for maintenance and are not why parts come back. Every item in §5–§21 is chosen because it produces silicon failures, not because it produces ugly code.

3. How to Read RTL in a Review

Line-by-line reading does not scale and does not find these bugs. Three passes do.

PassQuestionOutput
1 — traceabilityfor each 27.1 decision, where is it in the code?a decision → file:line map, with gaps
2 — event enumerationfor each state element, what events change it, and can they coincide?the same-cycle combinations (§8)
3 — boundary and extremeswhat happens at zero, at max, at reset, after recovery?§10's underflow, §14's states

Three readings.

Pass 1 is the one unique to this gate, and it is fast. Take 27.1's decision list; for each, name the file and line where it is enforced. A decision with no line is either unimplemented or implemented somewhere nobody expects — and both are findings. 27.1 §20's capability record makes this mechanical: every field should map to code.

Pass 2 finds the largest class of real bugs in this chapter, and it is a mechanical question rather than an insight. For each register: list the events that write it, then ask which pairs can occur in the same cycle. If the code handles them in separate if branches, the last assignment wins and an update is lost (§8).

And pass 3 is where reviewers stop too early. "What happens at zero" catches unsigned underflow (§10). "What happens after a recovery" catches states reachable only in that path (§14). Neither is exotic; both are simply outside the scenario the author had in mind while writing.

4. Sourcing and Scope

5. Area A — Decision Traceability

#Review itemWhat to find in the codeIf missing
A1retention policy across recovery (27.1 §16)the recovery branch, and what it does not touch26.5 §21
A2generation check on completiona comparison, not just a live bitstale completion retires a live entry
A3route/config epoch on live entriesa stored epoch and a check at use26.5 §19's rejection burst
A4derived outstanding depththe parameter, and a comment naming its derivation27.1 §17's 390× miss
A5credit conservationthe invariant, and an assertion carrying itslow leak, hang at hour nine
A6readiness conjunctionall terms, ordered26.5 §15's first-access failure
A7one reliability ownerretry logic present exactly oncedouble retry, or none

Three readings.

A1 is checked by looking for an absence, which is why it is easy to miss. The correct implementation is a recovery branch that does not clear the transaction table. A reviewer scanning for what the code does will not notice what it deliberately does not do — so the review item is phrased as "show me the recovery branch and tell me what it leaves alone."

A4 asks for a comment, and that is not a style request. A parameter without its derivation cannot be re-checked when the assumption changes (27.1 §17). localparam int N_ID = 8; is unreviewable. // N_ID >= target_rate × rt_latency = 6.25 ops/ns × 500 ns is reviewable, and it fails review immediately at 8 — which is the entire point.

And A7 is checked across modules rather than within one. Retry logic in the adapter and in the protocol layer is a finding that no single-file review can produce. It is why pass 1 works from the decision list rather than from the file list.

6. Area B — Accepted-Event Semantics

The single most productive review question in this chapter: is this counting what was offered, or what was accepted?

#Review itemCorrectWrong
B1transaction countersvalid && readyvalid alone
B2resource allocationon acceptanceon request
B3credit consumptionon acceptanceon offering
B4service-rate measurementon semantic retirementon transport completion
B5arbiter fairness ageingreset on actual servicereset on grant (21.5 §29)
B6queue occupancyaccept minus retireenqueue minus dequeue-attempt

Two readings.

B1 and B5 look similar and fail differently. B1 overcounts, which inflates every rate derived from it. B5 hides starvation — a grant into a blocked path is not service, and resetting the age on grant makes a starving requester look healthy exactly as the system gets busier (21.5 §29).

And B4 is the one that corrupts an adaptive mechanism rather than a report. Counting transport completions as work means retries count as progress (26.3 §16) — so a degrading target appears to speed up, and a service-rate-aware scheduler sends it more work. The instrument becomes the amplifier.

7. Wrong RTL — Counting the Offer

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG. ILLUSTRATIVE. Counts every cycle the producer asserts valid. During
// backpressure the SAME transaction is counted once per stalled cycle. Written
// by someone who read "valid means a transaction is present" and stopped there.
always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n) txn_count_q <= '0;
  else if (in_valid) txn_count_q <= txn_count_q + 32'd1;   // BUG: not && ready
end

Architecture. One counter feeding throughput reporting and, downstream, a rate estimate.

State. txn_count_q.

Event. in_validan offering, not a transfer.

Contract. Every consumer of this counter assumes it counts transactions. It counts cycles of offering, and the two coincide only when ready is always high.

Failure — the arithmetic. One transaction stalled for 9 cycles before acceptance is counted 10 times. A link at 30 % acceptance reports roughly 3.3× its real transaction countand the error grows as the system gets more congested, so the counter is least trustworthy exactly when it is most consulted.

Root cause. valid means "I have something"; valid && ready means "it moved" (21.5 §11). The bug is one token wide and inverts the meaning of every derived metric.

Corrected.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// CORRECT. Count the TRANSFER, which is the accepted event.
always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n) txn_count_q <= '0;
  else if (in_valid && in_ready) txn_count_q <= txn_count_q + 32'd1;
end
 
// MANDATORY. English: the transaction count increases only on a cycle where a
// transfer actually occurred. Catches the offered/accepted confusion directly,
// including if someone later "optimises" the ready term away.
a_count_on_transfer_only: assert property (
  @(posedge clk) disable iff (!rst_n)
    (txn_count_q != $past(txn_count_q)) |-> $past(in_valid && in_ready)
);

DV/debug. The review signature is a grep: every counter increment guarded by valid without ready is a candidate finding. This is one of the few defect classes a reviewer can find mechanically across an entire codebase in minutes.

8. Area C — Simultaneous Events

Pass 2's question (§3): for each register, which writing events can coincide? The classic failure is an increment and a decrement in the same cycle, written as separate branches.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG. ILLUSTRATIVE. Occupancy tracking with allocation and release in
// separate branches. Correct whenever they do not coincide — which is most
// cycles, in most tests, at low load.
always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n)          occ_q <= '0;
  else if (alloc_fire) occ_q <= occ_q + 16'd1;
  else if (free_fire)  occ_q <= occ_q - 16'd1;    // BUG: unreachable when both
end

Architecture. One occupancy counter gating allocation.

State. occ_q.

Event. Two events, priority-encoded by else if.

Contract. Callers assume occ_q equals live entries. It does — until the first cycle in which both fire.

Failure — the timeline. Each coincidence loses one decrement, so the count drifts upward and never recovers.

Eventocc_qActual liveDrift
alloc110
alloc + free, same cycle21+1
free10+1
alloc + free, same cycle21+2
… thousands of cycles laterat limitnear zeropermanent

The end state is a permanent stall with an empty queue — allocation blocked by an occupancy counter that has drifted to its limit. At hour nine, in a soak test, on hardware.

Failure — the cycle timeline, at OCC_LIMIT = 64. The lethal property is that the drift is monotonic: there is no event anywhere in the design that reduces it.

Cyclealloc_firefree_fireocc_qActual liveDriftObservable
10010110normal
14001000normal
2051110+1nothing — the first coincidence
206–900mixedmixedtracks +1 high+1no symptom
90111+2still no symptom
~10⁶60~2+58allocation slows slightly
~10⁶ + δ1164~1+63can_alloc deasserts
after0 — blocked164, pinned0+64permanent stall, queue EMPTY

First divergence: cycle 205 — the first cycle in which both events fire. Every symptom is ~10⁶ cycles later, which is far outside any trace window (21.7 §21) and long after the causal event is unrecoverable.

And the signature at the stall is diagnostic if you know to look for it: occupancy at its limit while the queue is empty. Those two facts are contradictory, and their contradiction names the bug — which is exactly what a_occupancy_conserved states, and why it fires at cycle 205 instead of at 10⁶.

Root cause. else if is a priority encoder, and these events are not prioritised — they are independent. The frequency of coincidence rises with load, so the bug accelerates exactly when the system is busiest.

Corrected.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// CORRECT. ONE next-state expression that nets both events. Cast to a signed
// width wide enough that the intermediate cannot wrap, then assign.
always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n) occ_q <= '0;
  else        occ_q <= occ_q + 16'(alloc_fire) - 16'(free_fire);
end
 
// MANDATORY. English: occupancy always equals allocations minus frees. This is
// the invariant the counter claims to represent, and it catches drift on the
// FIRST coincidence rather than at hour nine.
a_occupancy_conserved: assert property (
  @(posedge clk) disable iff (!rst_n)
    occ_q == (alloc_total_q - free_total_q)
);
 
// MANDATORY. English: allocation never occurs when occupancy is at the limit.
// Catches the gate being bypassed, which is how ids get reused while live.
a_no_alloc_when_full: assert property (
  @(posedge clk) disable iff (!rst_n)
    alloc_fire |-> (occ_q < OCC_LIMIT)
);

Contract. One register, one next-state expression is the general rule, and it is worth stating as a review standard rather than as advice. Any register written from more than one if branch is a pass-2 finding until proven otherwise.

DV/debug. a_occupancy_conserved fires on the first coincidence. Without it, the only symptom is a stall thousands of cycles later, by which point the causal event is long out of any trace window (21.7 §21).

9. Area D — Width, Overflow and Signedness

#Review itemAsk
D1counter widthhow long until it wraps at max rate?
D2wrap vs saturatewhich, and is it stated?
D3saturation visibilityis saturation reported? (26.4 §12)
D4unsigned subtractioncan the operand order ever invert? (§10)
D5intermediate widthdoes a + b wrap before assignment?
D6comparison signednessare both operands the same signedness?
D7index width vs array sizecan the index exceed the array?

Three readings.

D2 has no universally correct answer, and the finding is the absence of a decision. Diagnostic counters should saturate — a wrapped counter produces an apparent negative rate and sends debug in the wrong direction. Sequence numbers must wrap, and their comparison must handle it. A counter whose intent is unstated is a finding regardless of which behaviour it has.

D3 is what makes saturation honest. A silently pinned counter is a diagnostic that lies while looking healthy (26.4 §12). One sticky overflow bit for a whole counter block is enough.

And D4 is the highest-severity item in this area, because unsigned underflow does not degrade gracefully — it jumps to the maximum value in one cycle (§10).

10. Wrong RTL — Unsigned Underflow

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG. ILLUSTRATIVE. Available credits computed by subtraction. Correct
// whenever the invariant holds — and this code is what turns a ONE-CYCLE
// invariant violation into a PERMANENT wedge.
logic [7:0] credits_avail;
 
assign credits_avail = credit_limit_q - credits_used_q;   // BUG: unsigned
assign can_send      = (credits_avail != 8'd0);

Architecture. A subtraction feeding a send gate.

State. Two registers; the subtraction is combinational.

Event. Evaluated continuously.

Contract. It assumes credits_used_q <= credit_limit_q always. That is exactly the invariant the credit logic is supposed to maintain — so this code is correct precisely when nothing has gone wrong, and catastrophic the moment something has.

Failure — the arithmetic. If credits_used_q reaches 5 while credit_limit_q is 4 — one double-count, or one credit return applied twice — then 4 - 5 in 8-bit unsigned is 255, not −1.

limitusedcredits_availcan_sendEffect
431yesnormal
440nocorrect backpressure
45255yessends with no credits
46254yesoverruns the receiver

The design transitions from "one credit over-counted" to "unlimited sending" in a single cycle — and the receiver's buffer overruns, which is data loss rather than a stall.

Failure — the second variant. If the sense were reversed and a counter underflowed toward zero, the value pins at maximum and can_send never deasserts. Both directions of this bug produce the worst available outcome, which is why the review item is unconditional.

Root cause. An invariant was used as a precondition without being enforced. The subtraction is a correct implementation of a statement that is only true while the rest of the design is correct.

Corrected.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// CORRECT. Never rely on an invariant you have not enforced. Clamp explicitly,
// and make the violation VISIBLE rather than absorbing it silently.
logic [7:0] credits_avail;
logic       credit_invariant_violated_q;   // sticky — see the assertion below
 
assign credits_avail = (credits_used_q >= credit_limit_q)
                     ? 8'd0                                  // clamp, never wrap
                     : (credit_limit_q - credits_used_q);
assign can_send      = (credits_avail != 8'd0);
 
always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n) credit_invariant_violated_q <= 1'b0;
  else if (credits_used_q > credit_limit_q)
    credit_invariant_violated_q <= 1'b1;    // sticky: it happened at least once
end
 
// MANDATORY. English: credits in use never exceed the configured limit. This
// is the credit-conservation invariant from 27.1 area D2, and it catches the
// ROOT cause one cycle after it occurs — rather than the overrun that follows.
a_credit_invariant: assert property (
  @(posedge clk) disable iff (!rst_n)
    (credits_used_q <= credit_limit_q)
);
 
// MANDATORY. English: a send never occurs without an available credit.
// Catches the §10 wedge directly, and stays valid even if the clamp is removed.
a_no_send_without_credit: assert property (
  @(posedge clk) disable iff (!rst_n)
    send_fire |-> (credits_avail != 8'd0)
);

Contract. The clamp prevents the catastrophe; the sticky bit and assertion prevent the silence. Clamping alone would be worse than the bug in one specific way — it would hide a credit-accounting error indefinitely, and the design would run slightly wrong forever with no evidence.

DV/debug. credit_invariant_violated_q readable from silicon converts "the link wedged" into "credit accounting broke, at least once, before it wedged"which is a different and far shorter investigation (21.7 §14).

11. Area E — Reset

#Review itemAsk
E1which reset drives this register?is it the right scope (26.5 §7)?
E2what must NOT be reset by a link reset?obligations, generation, epochs, first-fault capture
E3reset polarity and async/sync consistencymixed styles in one always block
E4reset release synchronisationis release synchronised to each domain?
E5is every state element reset that needs to be?including counters used before first write
E6is anything reset that should persist?sticky diagnostics, high-water marks
E7diagnostic clear separate from functional reset(25.10 §14)

Three readings.

E1 and E2 are the highest-consequence items, and they are checked by reading the sensitivity list and the reset expression together. if (!rst_n || !link_rst_n) covering a transaction table is 26.5 §9's corruption, visible in one line — and it looks tidy, which is why it survives.

E4 is the item most often assumed rather than checked. Asynchronous assertion with synchronised release is standard practice; a release that is not synchronised into each clock domain can let different flops in the same domain leave reset on different cycles, producing a startup state no test constructs and no analysis predicts.

And E6 inverts the usual question in a way reviewers find unnatural. "Is anything reset that should persist?" catches a sticky first-fault register cleared by a link reset — which means the reset destroys the evidence for the fault that caused it (21.7 §14).

12. Area F — FSM Hygiene

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG. ILLUSTRATIVE. A link-control FSM with three realistic defects.
// It simulates correctly, and it is one upset away from a permanent hang.
typedef enum logic [2:0] { L_RESET, L_TRAIN, L_ACTIVE, L_RECOVER } lnk_e;
lnk_e lnk_q;
 
always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n) lnk_q <= L_RESET;
  else begin
    // BUG 1: 3-bit enum, 4 defined values — FOUR encodings are undefined, and
    //        there is no default, so the FSM holds an illegal state forever.
    case (lnk_q)
      L_RESET:   if (start)        lnk_q <= L_TRAIN;
      L_TRAIN:   if (train_ok)     lnk_q <= L_ACTIVE;
      L_ACTIVE:  if (err)          lnk_q <= L_RECOVER;
      // BUG 2: no timeout on TRAIN or RECOVER — a peer that never responds
      //        produces an unbounded, unreported wait (26.2 §10).
      L_RECOVER: if (recover_done) lnk_q <= L_ACTIVE;
      // BUG 3: no default. Synthesis may infer a latch-like hold, and an
      //        illegal state has no path back to L_RESET.
    endcase
  end
end

Architecture. Four states, one transition each.

State. lnk_q, three bits, eight encodings, four defined.

Event. Each transition has exactly one trigger and no escape.

Contract. Callers assume the FSM always makes progress or reports why it cannot. It does neither in three cases.

Failure. BUG 2 is the one that reaches silicon most often. A peer that never completes recovery leaves the FSM in L_RECOVER forever, with no error and no timeout — indistinguishable from a slow link, for as long as anyone is willing to look (26.2 §10). BUG 1 and BUG 3 together mean any upset into an undefined encoding is permanent.

Corrected.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// CORRECT. Bounded waits with NAMED failures, a total case, and a defined
// recovery path from any illegal encoding.
typedef enum logic [2:0] {
  L_RESET, L_TRAIN, L_ACTIVE, L_RECOVER, L_TRAIN_FAIL, L_RECOVER_FAIL
} lnk_e;
 
lnk_e        lnk_q;
logic [15:0] wait_q;
 
always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n) begin
    lnk_q  <= L_RESET;
    wait_q <= '0;
  end else begin
    // unique case flags overlapping/missing coverage in simulation, and the
    // default guarantees a path home from ANY encoding, defined or not.
    unique case (lnk_q)
      L_RESET:   if (start) begin lnk_q <= L_TRAIN; wait_q <= '0; end
 
      L_TRAIN:   if (train_ok)                 lnk_q  <= L_ACTIVE;
                 else if (wait_q == TRAIN_TO)  lnk_q  <= L_TRAIN_FAIL;
                 else                          wait_q <= wait_q + 16'd1;
 
      L_ACTIVE:  if (err) begin lnk_q <= L_RECOVER; wait_q <= '0; end
 
      L_RECOVER: if (recover_done)             lnk_q  <= L_ACTIVE;
                 else if (wait_q == RECOV_TO)  lnk_q  <= L_RECOVER_FAIL;
                 else                          wait_q <= wait_q + 16'd1;
 
      L_TRAIN_FAIL,
      L_RECOVER_FAIL: if (diag_clear)          lnk_q  <= L_RESET;
 
      default:                                 lnk_q  <= L_RESET;   // BUG 3 fix
    endcase
  end
end
 
// MANDATORY. English: the FSM never remains in a wait state longer than its
// configured bound. Catches BUG 2 — the unbounded, unreported wait.
a_train_bounded: assert property (
  @(posedge clk) disable iff (!rst_n)
    (lnk_q == L_TRAIN) |-> ##[1:TRAIN_TO+1] (lnk_q != L_TRAIN)
);
 
// MANDATORY. English: the FSM is only ever in a defined state. Catches BUG 1
// in simulation, and is a natural formal target.
a_fsm_state_legal: assert property (
  @(posedge clk) disable iff (!rst_n)
    (lnk_q inside {L_RESET, L_TRAIN, L_ACTIVE, L_RECOVER,
                   L_TRAIN_FAIL, L_RECOVER_FAIL})
);

Contract. The timeout values must come from the peer's stated worst case, not from measurement (26.2 §12) — a timeout raised each time it fires is a timeout that cannot detect the bug it exists for (21.6 §29).

DV/debug. The failure states are named and distinct, so a stuck link reports L_TRAIN_FAIL or L_RECOVER_FAIL rather than being silent. diag_clear rather than a functional reset returns them (E7), so the fault record survives re-arming.

13. Area G — Clock Domain Crossings in Code

#Review itemAsk
G1is every crossing identified?which signals cross, and in which direction
G2level or pulse?only levels cross safely (26.5 §10)
G3is the payload synchronised, or held stable?held stable — never bit-synchronised
G4is edge detection on a stable stage?not on the first synchroniser flop
G5is there an acknowledgement?or is the source guessing?
G6four-phase completenessis the deassert wait present? (26.5 §12)
G7reset consistency across domainscan one side reset alone?

Three readings.

G3 is the item where well-intentioned code is most often wrong. Synchronising a multi-bit bus flop by flop feels safer than not synchronising it. It is the actual bug — independent synchronisers can resolve on different cycles, producing a value that was never sent. The correct structure holds the payload stable by protocol and synchronises only the control signal.

G4 is a one-line finding with a real consequence. Edge detection on sync1 samples a potentially metastable value. The extra stage exists solely so that edge detection operates on something stable, and removing it is a natural-looking optimisation.

And G6 catches the bug that only appears under load. Without waiting for the acknowledgement to drop, two back-to-back events merge into one at a slow destination (26.5 §12) — invisible at low rates, and a lost event at high ones.

14. Area H — States Reachable Only After Recovery

Pass 3's question (§3). The most under-reviewed region of any link design is the state space entered only after an error.

#Review itemAsk
H1what is outstanding when recovery starts?and what happens to it (A1)
H2can an ID be reallocated while the peer holds it?26.5 §9
H3does an ID allocator restart after a link event?it must not
H4are epochs re-checked after recovery?or assumed still valid
H5can two recoveries overlap?is the second handled or dropped
H6is a completion for a pre-recovery transaction accepted?per A1's policy — consistently
H7does first-fault capture survive it?(E6)

Three readings.

H3 is a single-line check with a corruption-class consequence. An allocator reset by a link event restarts at zero while the peer still holds the old identities — and the reuse window is every link reset (26.5 §9).

H5 is asked because the answer is usually "that can't happen". A second error during recovery is exactly the case that occurs on a marginal link — the condition under which recovery is happening at all. If the second event is dropped, the design is unrecovered and believes it is recovering.

And H6 requires the code to be consistent with A1's documented answer, not merely self-consistent. A design that retains obligations but silently discards late completions has implemented neither policy — and that mixture is not in anyone's specification.

15. Area I — Parameters and Their Derivations

#Review itemAsk
I1is every sizing parameter derived?from what, in a comment (A4)
I2what latency does it assume?and who owns that number (27.1 §9)
I3has it been re-checked against silicon latency?or only against a model (27.1 §17)
I4do corner values work?parameter = 1, = max
I5are widths derived from the parameter?or hardcoded alongside it
I6is there a compile-time check?$error in a generate/initial for illegal combinations

Two readings.

I5 is the defect that survives every functional test at the default configuration and breaks the first derivative product. localparam N_CH = 16; beside logic [3:0] ch_sel; works — until someone sets N_CH = 32 and the selector silently truncates. Widths must be derived ($clog2(N_CH)), and a hardcoded width beside a parameter is a finding on sight.

And I6 is cheap insurance that reviewers rarely ask for. A compile-time $error for illegal parameter combinations turns a subtle runtime misbehaviour into a build failure — and it costs three lines.

16. Area J — Assertion Review

27.1 commissioned a list (27.1 §21). This gate checks the assertions exist, say what they mean, and can actually fail.

#Review itemAsk
J1is there an English statement?in a comment, above the property
J2|-> or |=>?same cycle, or next — is it what the English says?
J3can it be vacuous?does the antecedent ever occur?
J4disable iff scopeis it disabling more than intended?
J5$past depth and resetis $past valid that early?
J6is it bounded?an unbounded ##[1:$] never fails in simulation
J7which bug does it catch?if the answer is unclear, it is decoration
J8formal suitabilityis it bound over an array that will not converge?

Three readings.

J3 is the item that turns an assertion count into an assertion review. A property whose antecedent never occurs passes forever and proves nothing. The check is a coverage question — cover property on the antecedent — and it belongs beside every non-trivial assertion.

J4 is the quietest defect in this area. disable iff (!rst_n || any_error) is common and often disables the assertion exactly when the interesting behaviour happens. The legitimate use is disabling a property while the design is deliberately in a state where it does not apply (26.3 §14) — and the review question is whether the disable term is that, or a way to stop a failing assertion from failing.

And J7 is the acceptance test for the whole area. An assertion that cannot be traced to a specific bug it would catch is decoration, and decoration costs simulation time and review attention without buying anything.

17. Wrong SVA — Three Realistic Assertion Defects

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG #1. Vacuous. `req_special` is a mode never enabled in this product.
// The property passes in every regression and has never been evaluated.
a_special_response: assert property (
  @(posedge clk) disable iff (!rst_n)
    req_special |-> ##[1:8] resp_valid
);
// Fix: pair every non-trivial assertion with a cover on its antecedent, so
// "never failed" is distinguishable from "never ran".
c_special_seen: cover property (@(posedge clk) req_special);
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG #2. Over-broad disable. The intent was to ignore the property during
// reset. As written it is also disabled during every error condition — which
// is precisely when the retention behaviour matters (§14).
a_obligation_retained: assert property (
  @(posedge clk) disable iff (!rst_n || err_active)      // BUG: err_active
    (recovery_done && txn_live_q[chk]) |=> txn_live_q[chk]
);
// Fix: disable only for reset. If the property genuinely does not apply in
// some state, encode that in the ANTECEDENT where it is visible and reviewable.
a_obligation_retained_fixed: assert property (
  @(posedge clk) disable iff (!rst_n)
    (recovery_done && txn_live_q[chk]) |=> txn_live_q[chk]
);
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG #3. Unbounded consequent. This can never fail in a finite simulation:
// the tool waits forever for the response, and the test ends first.
a_response_eventually: assert property (
  @(posedge clk) disable iff (!rst_n)
    req_fire |-> ##[1:$] resp_fire                       // BUG: unbounded
);
// Fix: bound it with the architecture's stated maximum. A bound that must be
// raised each time it fires is not a bound (21.6 §29) — if the number is
// unknown, that is a 27.1 finding, not an assertion style choice.
a_response_bounded: assert property (
  @(posedge clk) disable iff (!rst_n)
    req_fire |-> ##[1:MAX_RESP_LATENCY] resp_fire
);

Architecture. Three properties that a reviewer counting assertions would score as three. A reviewer applying J3, J4 and J6 scores them as zero.

State. None added — which is why these defects are free to accumulate.

Event. #1 never triggers, #2 is disabled when it matters, #3 never completes.

Contract. Each claims to enforce something. None does, and their presence actively reduces scrutiny elsewhere because the property is "covered".

Failure. #2 is the most dangerous of the three: it is the assertion for 26.5 §21's retention policy, disabled during exactly the error conditions under which recovery occurs. The one bug it exists to catch is the one it cannot see.

DV/debug. The general review practice is worth stating as a rule: every non-trivial assertion needs a matching cover on its antecedent, and an assertion that has never failed in development is suspicious rather than reassuring. Deliberately breaking the RTL to confirm a property fires is the cheapest way to know it works (25.8 §22).

18. Area K — X-Propagation and Simulation/Synthesis Mismatch

The last review area, and the one whose defects are invisible in RTL simulation by construction: the places where simulation is more forgiving than silicon, or more punishing.

#Review itemAsk
K1uninitialised state used before first writeis it reset, or does it rely on simulation initialisation?
K2X-optimism in if/casedoes an X condition take a definite branch in simulation?
K3unique/priority in synthesisis a case truly one-hot, or only in the tests you ran?
K4casez/casex don't-caresdoes casex match X in a way silicon cannot?
K5initial blocks in synthesisable RTLdo they define behaviour that silicon will not reproduce?
K6inferred latchesany combinational block with an incomplete assignment
K7$random, delays, non-synthesisable constructspresent in synthesisable scope?

Three readings, and K2 and K3 are the pair that most reliably surprises people.

K2 — X-optimism means simulation can be kinder than silicon. If a condition evaluates to X, a simulator's if takes the else branch and continues, producing plausible behaviour. Silicon has no X — it has a real, definite value that may take the other branch. So a design with an uninitialised control signal can simulate cleanly for a year and take a different path in a real part. The review question is K1's: is every state element that is read before being written actually reset?

K3 is the mirror image — synthesis can be kinder than simulation, and then silicon is not. unique case tells the tool that the cases are mutually exclusive and complete. Simulation checks that claim and warns; synthesis believes it and optimises accordingly, removing the priority logic that would have made an unexpected combination safe. If the claim is false in a state no test reached, the simulator would have told you and the silicon will not. So unique is a promise, and the review item is "what makes this promise true?" — not "does it simulate?"

And K6 is the classic, worth one grep rather than one debate. A combinational always_comb with a path that assigns nothing infers a latch. always_comb will warn where always @(*) may not, which is one concrete reason the review standard should require always_combnot for style, but because it makes an entire defect class visible to the tool.

The general principle behind the whole area, and the reason it belongs at this gate rather than to DV: a defect that RTL simulation cannot express will not be found by more simulation. These seven items are found by reading, by lint, and by gate-level or X-pessimistic simulation — and the review is the cheapest of the three.

19. The Consolidated Review Sheet

AreaThe one question that finds the mostConsequence if skipped
A — traceabilitywhere is each 27.1 decision in the code?a decision that was never implemented
B — accepted eventsvalid or valid && ready?every derived metric wrong
C — simultaneous eventscan these two events coincide?permanent counter drift (§8)
D — width and signcan this subtraction underflow?one-cycle jump to unlimited sending (§10)
E — resetwhat must this reset NOT clear?26.5 §9's corruption
F — FSMis every wait bounded, and is there a default?silent permanent hang (§12)
G — CDClevel or pulse; payload held or synchronised?intermittent lost events
H — post-recoverycan an ID be reused while the peer holds it?corruption after every recovery
I — parametersderived from what, owned by whom?27.1 §17's 390× miss
J — assertionswhich bug does this catch, and can it fire?assertion count without coverage
K — X and sim/synthis anything read before it is written?a path silicon takes and simulation never did (§18)

And the ordering is deliberate. A first, because a decision missing from the code makes every other area moot. B and C next, because they are mechanical, fast, and corrupt everything downstream. D through H are the correctness core. J and K last — reviewing assertions is only meaningful once you know what the code is supposed to do, and K's defects are the ones no amount of RTL simulation will surface (§18).

20. How the RTL Review Itself Fails

Failure modeLooks likeWhy it is fatal
line-by-line readingthoroughruns out of attention before the state machines (§3)
style findings crowding out defectsmany commentsthe expensive findings get equal weight with naming
reviewing files instead of decisionsfull coverage of the module listmisses cross-module items like A7's double retry
counting assertions"well verified"§17 — three properties, zero coverage
reviewing only what changedefficienta new event combination breaks unchanged code

Three readings.

The last row is the one that catches experienced teams. A diff-based review is efficient and structurally blind to §8's defect class: adding a new event that can coincide with an existing one breaks code that did not change. The affected lines are not in the diff, and the reviewer never looks at them.

Style findings are not wrong; they are miscategorised. A review that produces forty naming comments and no event-coincidence findings has spent its attention badly — and the author, reasonably, treats the whole review as low-value.

And "reviewing files" misses precisely the items that matter most across a system. A7's "is retry implemented exactly once?" cannot be answered from any single file — which is why pass 1 works from the decision list rather than the file list (§3).

21. Red Flags

Seen in code or reviewUsually meansAsk
else if chain on independent events§8's lost update"can these coincide?"
a - b on unsigned, no clamp§10's wedge"can b exceed a, ever?"
case with no default§12's BUG 3"what happens on an illegal encoding?"
a wait state with no timer§12's BUG 2"how does this ever report failure?"
localparam with no derivation comment27.1 §17"derived from what, owned by whom?"
a hardcoded width beside a parameterI5"what happens at 32?"
disable iff with a term beyond reset§17 WRONG #2"is this disabled when it matters?"
##[1:$]§17 WRONG #3"what is the architectural bound?"
an assertion that has never failedJ3"has its antecedent ever occurred?"
"that path can't be taken"H5"prevented by what?"
reset expression with two reset termsE1/E2"which scope, and what must survive?"
a multi-bit signal through synchronisersG3"is it stable by protocol instead?"
always @(*) instead of always_combK6 — latch inference goes unwarned"does every path assign?"
unique case on a claim, not a proofK3 — synthesis believes it"what makes this promise true?"

Two readings.

Every row is greppable or visually obvious, which is the point. An RTL review's leverage comes from pattern recognition, not from comprehension — you cannot hold a large module in your head, and you do not need to.

And two rows deserve the most patience, because both are frequently correct: "that path can't be taken" and the two-term reset expression. A reset that legitimately covers two scopes exists. The review question is not skepticism but specificity — prevented by what, and what survives? (27.1 §23).

22. Common Misconceptions

"An RTL review checks code quality." §1: it checks that architecture decisions are present in the code and that the code survives every permitted event. Quality matters and is not why parts come back.

"Read it line by line to be thorough." §3, §20: line-by-line reading runs out of attention before the state machines. Three targeted passes — traceability, event enumeration, extremes — find more in less time.

"if (valid) is fine, ready is implied." §7: it counts cycles of offering. At 30 % acceptance the count is 3.3× too high, and the error grows with congestion.

"An else if is just a coding style." §8: it is a priority encoder. On independent events it silently loses an update, drifts upward, and wedges at hour nine.

"Underflow just wraps, it's a small error." §10: 4 - 5 in 8-bit unsigned is 255. The design goes from correct backpressure to unlimited sending in one cycle — data loss, not a stall.

"Clamping the subtraction fixes it." §10 Contract: clamping alone hides a credit-accounting error indefinitely. The clamp needs a sticky violation bit and an assertion, or the design runs slightly wrong forever with no evidence.

"A default case is defensive clutter." §12: without it an upset into an undefined encoding is permanent, with no path back to reset.

"Synchronise the bus to be safe." §13, G3: independent synchronisers can resolve on different cycles, producing a value that was never sent. Hold the payload stable by protocol; synchronise only the control signal.

"We have 200 assertions." §17: three properties can be vacuous, over-disabled and unbounded — a count of three and a value of zero. cover on the antecedent is what makes the count mean anything.

"If it simulates cleanly it is fine." §18: simulation has X and silicon does not. An uninitialised control signal can take the else branch for a year in simulation and the other branch in a real part — and unique case is a promise synthesis believes, not a check it performs.

"Reviewing the diff is enough." §20: adding an event that can coincide with an existing one breaks unchanged code, and those lines are not in the diff.

23. Understanding Check

24. Summary

Seven things.

Two questions, asked of every module (§1): can each architecture decision be found in this code, and does it survive every event the architecture permits — including combinations nobody tested.

Three passes, not line-by-line (§3): traceability against 27.1's decision list, event enumeration per register, and extremes — zero, max, reset, post-recovery.

Accepted events and simultaneous events are the mechanical wins (§6–§8). valid without ready inflates every derived metric; an else if on independent events loses an update and wedges at hour nine.

Unsigned underflow is the highest-severity single line (§10). 4 - 5 is 255 — correct backpressure to unlimited sending in one cycle, with data loss at the far end. Clamp, make it sticky, and assert the invariant.

FSMs need bounded waits, a total case and named failures (§12); post-recovery is the least-reviewed state space in any link design (§14), and ID reuse while the peer still holds an identity is corruption on every recovery.

And two areas close the gate. X-propagation and sim/synth mismatch (§18) — simulation is X-optimistic where silicon is definite, and unique is a promise the tool believes; a defect RTL simulation cannot express will not be found by more simulation. And assertions are reviewed for effect, not counted (§16–§17). Vacuous, over-disabled, unbounded — three properties, zero coverage. The acceptance test is which bug does this catch, and can it fire?