Skip to content
VLSI Mentor

CXL · Module 30

RTL Review Checklist

A working pre-tapeout RTL review document. Nine review dimensions — handshake acceptance, transition completeness, single-driver discipline, identity lifetime, arithmetic width, recovery completeness, retry state, combinational completeness and behavioural telemetry — each with the defect, the code that produces it, what escapes, and the telemetry that exposes it in silicon.

30.1 asked what mechanism enforces the architecture's invariants. This chapter asks whether the RTL that was written is the mechanism the architecture asked for, and it is the first review where the answer is a line of code rather than a paragraph.

The review question this chapter turns on, asked once per dimension:

What does this line actually do, in every cycle, under every combination of its inputs — including the ones the testbench never drove?

An architecture review can be conducted on intent. An RTL review cannot. Every finding in this chapter is a construct that is individually legal, compiles without a warning, passes a nominal test, and is wrong.

1. How To Use This Chapter

Each of the nine review dimensions below is a working review item, and every one answers the same eight questions:

FacetWhat it settles
Under reviewthe construct being examined
Invariant at riskthe property that breaks if it is wrong
Where it livesthe specific line, not the module
Evidence to demandwhat the reviewer should ask to see
What escapesthe bug that reaches silicon
How DV proves itthe stimulus that would falsify it
Telemetrywhat exposes it after tapeout
Misleading evidencewhat makes the broken code look correct

The last facet is again the hard one, and in RTL it is harder than in architecture. A broken architecture produces a reassuring document. Broken RTL produces a reassuring waveform, a clean lint run, and a passing regression — three forms of evidence that engineers are trained to trust.

2. The One-Sentence Model

An RTL review is sound when the RTL has been read, when every counter counts acceptance rather than presentation, when every state encoding has a way home, when every register has exactly one assignment per cycle, when every identity is unique while it is live, and when every arithmetic intermediate is wide enough to hold its own result — and "the RTL was reviewed" is bit 0.

3. What This Chapter Owns

GroundOwner
Reviewing the architecture before RTL exists30.1
Reviewing the verification environment that judges this RTL30.3
Reviewing coherency invariants across agents30.4
What a deployment committed to29.5
Reviewing the RTL against the architecturethis chapter

The boundary with 30.3 is worth stating precisely, because the two chapters share a vocabulary. This chapter reviews the design. 30.3 reviews the environment that judges the design. A defect this chapter would call "the counter counts the wrong event" becomes, in 30.3, "the checker never asked which event the counter counted" — the same escape, seen from the other side of the testbench.

4. Teaching-Model Boundary And Source Discipline

Every RTL block in this chapter is a teaching model. Each isolates one review dimension so it can be examined, mutated and broken on purpose. None is a production CXL controller, an implementation of any specification flow, or a complete design.

Nothing here states a normative CXL detail. No opcode, packet layout, bit position, field width, response encoding, snoop encoding, retry rule, timeout constant, latency figure or register definition from the specification appears anywhere in this chapter. The review dimensions — acceptance versus presentation, transition completeness, single-driver discipline, identity lifetime, arithmetic width — are general RTL properties that any synchronous design must satisfy, and they are examined here in their general form deliberately, so the review technique transfers to any block a reviewer is handed.

Claim classHow it is marked
General RTL reasoningstated plainly
Teaching abstractiondeclared in the RTL header
Illustrative parameterevery concrete figure in a model or table
Simulator-derived resultquoted from a run and asserted
Derived arithmeticshown with its inputs

Each model is built twice. A parameter selects between the robust build, which is what the review should require, and a weak build containing exactly the defect under discussion, written as it appears in real code rather than as a caricature. Every section's headline number is the gap between the two.

5. Review Item 1 — Does The Counter Count Acceptance Or Presentation?

Under review. Every counter, every credit decrement, every pointer advance and every state transition that fires on a valid signal.

Invariant at risk. One transaction is one event. If the count is wrong then every credit, every occupancy figure, every performance number and every conservation equation built on it is wrong by the same amount, and the amount depends on how stalled the interface was.

Where it lives. The condition on a single if.

The distinction. A request is presented when valid is high. It is accepted when valid and ready are both high. A producer that must hold a request up while the consumer stalls holds valid high for every one of those cycles, and a counter gated on valid alone counts each of them.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// RTL 1 - counting on PRESENTATION instead of on ACCEPTANCE.
//
// The single most common RTL review finding. A request is PRESENTED when valid
// is high. It is ACCEPTED when valid AND ready are both high. A counter driven
// by valid alone counts every cycle the producer holds the request up while the
// consumer is stalling - so one transaction is counted many times.
//
//   BAD  : if (valid)            n <= n + 1;
//   GOOD : if (valid && ready)   n <= n + 1;
//
// TEACHING MODEL. Isolates one RTL invariant; not a production block.
module handshake_acceptance #(parameter int COUNT_ON_VALID = 0) (
  input  logic clk, rst_n,
  input  logic       valid, ready,
  output logic       accepted,
  output logic [7:0] n_counted, n_truly_accepted, n_stall_cycles,
  output logic       hs_err
);
  assign accepted = valid && ready;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      n_counted <= 8'd0; n_truly_accepted <= 8'd0; n_stall_cycles <= 8'd0;
    end else begin
      // The build under review.
      if (COUNT_ON_VALID != 0) begin
        if (valid) n_counted <= n_counted + 8'd1;
      end else begin
        if (accepted) n_counted <= n_counted + 8'd1;
      end
      // The truth, computed the same way in BOTH builds so the model can detect
      // its own weak build.
      if (accepted)          n_truly_accepted <= n_truly_accepted + 8'd1;
      if (valid && !ready)   n_stall_cycles   <= n_stall_cycles + 8'd1;
    end
  end

  // SAFETY: the published count must equal the number of accepted handshakes.
  assign hs_err = (n_counted != n_truly_accepted);
endmodule

The measurement. One transaction, presented for four cycles with ready low for the first three:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
1 transaction, 3 stall cycles : good=1 bad=4 truth=1 stalls=3

One transaction counted four times. The error is not a fixed offset — it is the stall depth plus one, which means it grows with congestion and disappears in a lightly loaded test. A nominal regression on an unloaded interface cannot find this defect, because with ready tied high the two builds agree exactly.

Evidence to demand. Ask to see the counter's enable condition, and ask what the count reads after a deliberately backpressured burst. A reviewer who accepts "it counts transactions" without reading the enable has reviewed nothing.

What escapes. Credit accounting that over-counts consumption and stalls the interface below its real capacity; a performance counter that reports a throughput the design never achieved; an occupancy figure that never returns to zero.

How DV proves it. Drive one transaction with ready low for N cycles and check the count is exactly one for every N. This is the test in section 16, and it is three lines long.

Telemetry. Publish both accepted and stall_cycles. Their ratio is the interface's efficiency, and a design whose counter is gated on valid reports a stall count that is inconsistent with its own transaction count.

Misleading evidence. A waveform in which the counter increments exactly when valid rises, which is what the reviewer expects to see and is also exactly what the defect produces on a transaction that is accepted immediately.

A block diagram of one transaction presented for four cycles with ready low for three of them. A counter gated on valid alone reports four transactions; a counter gated on valid and ready reports one, which matches the independently maintained truth.1 transaction4 cycles of validgated on validpresentationgated on validand readyacceptancecounts 43 stalls countedcounts 1matches truth12

Figure 1 — the error is the stall depth. Both counters see the same interface. The upper path counts every cycle the request was held up; the lower path counts the one cycle it was taken. On an interface that never stalls the two are identical, which is why this defect survives a nominal regression and appears the first time the consumer is slow.

6. Review Item 2 — Does Every Encoding Have A Way Home?

Under review. Every case statement on a state variable, and every state register whose width admits more encodings than the design defines.

Invariant at risk. The design never occupies an encoding that is not a state; and if it somehow does, it leaves.

Where it lives. The presence or absence of a default arm — and, separately, the transitions the table permits that the architecture forbids.

Two distinct failures live here. The first is reachability: a three-bit register holds eight encodings and the design defines four, so four encodings exist that no transition targets. The second is permission: a transition table that moves from IDLE straight to RUN is perfectly legal Verilog and may be architecturally forbidden, and nothing in the language will say so.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// RTL 2 - state-transition completeness and illegal transitions.
//
// A case statement without a default leaves unlisted encodings undefined; in
// simulation the state holds, in synthesis it may become a latch or an
// unreachable trap. Worse, a legal-looking transition table can permit a
// transition the architecture forbids, and nothing reports it.
//
//   BAD  : case (st) ... endcase                 // no default
//   GOOD : case (st) ... default: st <= IDLE;    // and an illegal-transition
//                                                // monitor beside it
//
// TEACHING MODEL.
module transition_completeness #(parameter int NO_DEFAULT_ARM = 0) (
  input  logic clk, rst_n,
  input  logic       go, done, fault, poke_illegal,
  output logic [2:0] state,
  output logic       in_legal_state, illegal_transition,
  output logic [7:0] n_transitions, n_illegal, n_recovered,
  output logic       st_err
);
  localparam logic [2:0] IDLE = 3'd0, ARMED = 3'd1, RUN = 3'd2, DRAIN = 3'd3;
  // 3'd4..3'd7 are NOT states. Reaching one is the failure this model is about.
  logic [2:0] st, prev;

  assign state          = st;
  assign in_legal_state = (st <= DRAIN);
  // An illegal transition is one the architecture forbids: IDLE straight to RUN
  // without arming, or any move out of an encoding that is not a state.
  assign illegal_transition = (prev == IDLE && st == RUN) || (prev > DRAIN);
  // SAFETY: the design must never sit in an encoding that is not a state.
  assign st_err = !in_legal_state;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      st <= IDLE; prev <= IDLE;
      n_transitions <= 8'd0; n_illegal <= 8'd0; n_recovered <= 8'd0;
    end else begin
      prev <= st;
      if (st != prev)            n_transitions <= n_transitions + 8'd1;
      if (illegal_transition)    n_illegal     <= n_illegal + 8'd1;
      if (!in_legal_state)       n_recovered   <= n_recovered + 8'd1;

      // `poke_illegal` models a corrupted or glitched encoding arriving from
      // anywhere - an upset, a bad reset release, a CDC escape.
      if (poke_illegal) begin
        st <= 3'd6;                       // not a state
      end else begin
        case (st)
          IDLE : if (go)    st <= ARMED;
          ARMED: if (go)    st <= RUN;
                 else if (fault) st <= IDLE;
          RUN  : if (done)  st <= DRAIN;
                 else if (fault) st <= DRAIN;
          DRAIN: st <= IDLE;
          default: begin
            // The whole review point. With the default arm present the design
            // recovers to a known state; without it, it stays lost.
            if (NO_DEFAULT_ARM == 0) st <= IDLE;
          end
        endcase
      end
    end
  end
endmodule

The measurement. With the state register forced to encoding 6 — modelling an upset, a bad reset release or a CDC escape — and one clock allowed to pass:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
poked to 6 : good_state=6 bad_state=6 legal_good=0 legal_bad=0
one cycle on : good=0 bad=6

Both builds are equally lost, and only one comes back. The build with the default arm returns to IDLE on the next edge. The build without it stays at 6 forever — and every subsequent case evaluation falls through, so the design is not merely in a wrong state, it is inert.

Evidence to demand. Ask for the synthesis report's state-machine extraction and compare the encodings it found against the encodings the architecture defines. Then ask which transitions the table permits that the architecture forbids, and where the monitor is that would report one.

What escapes. A block that is permanently unresponsive after a single transient, with no error signal, no interrupt, and a state register a debugger can read that shows an encoding nobody recognises.

How DV proves it. Force the state register to every undefined encoding in turn and check the design is back in a legal state within one cycle. This is a directed test that costs six lines and cannot be reached by any amount of constrained-random stimulus, because random stimulus can only drive inputs and this failure is not driven by an input.

Telemetry. A sticky illegal_state_seen bit and a recovery counter. Both should read zero forever, and the second is the only evidence that the first ever fired.

Misleading evidence. A simulation in which the missing default is invisible, because simulation semantics hold the register and a held register looks like a design that is simply idle. In synthesis the same omission can become a latch or an unreachable trap, and the two behaviours differ.

7. Review Item 3 — How Many Assignments Reach This Register In One Cycle?

Under review. Every register assigned in more than one place inside a clocked block.

Invariant at risk. The register's value after the edge is a function of all the events of that cycle, not of whichever one happens to appear last in the source.

Where it lives. Two consecutive if statements.

The construct. Two separate if statements each assigning the same register are correct whenever only one of them fires. When both fire, the non-blocking assignments are both scheduled and the last one in source order wins. The other is not merged, not summed and not flagged — it is discarded.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// RTL 3 - two non-blocking assignments to one register in one cycle, and the
// increment/decrement collision that hides inside it.
//
// Two separate `if` statements each assigning the same register look correct in
// isolation and are correct whenever only one fires. When both fire, the LAST
// one wins and the other is silently lost.
//
//   BAD  : if (push) occ <= occ + 1;
//          if (pop)  occ <= occ - 1;      // simultaneous push+pop LOSES the push
//   GOOD : one assignment computed from both events
//
// TEACHING MODEL.
module nba_collision #(parameter int TWO_ASSIGNMENTS = 0) (
  input  logic clk, rst_n,
  input  logic       push, pop,
  output logic [7:0] occupancy, truth, n_push, n_pop, n_both,
  output logic       coll_err
);
  logic [7:0] occ_q, tru_q;

  assign occupancy = occ_q;
  assign truth     = tru_q;
  // SAFETY: the published occupancy must track the independently maintained
  // truth, which is computed with one assignment in both builds.
  assign coll_err  = (occ_q != tru_q);

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      occ_q <= 8'd0; tru_q <= 8'd0;
      n_push <= 8'd0; n_pop <= 8'd0; n_both <= 8'd0;
    end else begin
      if (push)        n_push <= n_push + 8'd1;
      if (pop)         n_pop  <= n_pop + 8'd1;
      if (push && pop) n_both <= n_both + 8'd1;

      if (TWO_ASSIGNMENTS != 0) begin
        // The antipattern, written exactly as it appears in real code.
        if (push) occ_q <= occ_q + 8'd1;
        if (pop)  occ_q <= (occ_q == 8'd0) ? 8'd0 : occ_q - 8'd1;
      end else begin
        // The robust form: ONE assignment, both events accounted for.
        case ({push, pop})
          2'b10:   occ_q <= occ_q + 8'd1;
          2'b01:   occ_q <= (occ_q == 8'd0) ? 8'd0 : occ_q - 8'd1;
          default: occ_q <= occ_q;            // 00 idle, 11 nets to zero
        endcase
      end

      // The truth, maintained identically in both builds.
      case ({push, pop})
        2'b10:   tru_q <= tru_q + 8'd1;
        2'b01:   tru_q <= (tru_q == 8'd0) ? 8'd0 : tru_q - 8'd1;
        default: tru_q <= tru_q;
      endcase
    end
  end
endmodule

The measurement. A simultaneous push and pop against an occupancy of 3, followed by three more pops:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
simultaneous push+pop : good=3 bad=2 truth=3
after three more : good=3 bad=0

The first line is the defect and the second line is why it is dangerous. The simultaneous cycle loses the push, so occupancy reads 2 where the truth is 3. The three subsequent pops then drain the weak build to zero while the storage still holds an entry. The counter and the storage have separated, and nothing reports it.

Evidence to demand. For every register assigned more than once in a clocked block, ask what the value is when both conditions hold, and ask whether the testbench ever drove them together. The second question usually answers the first.

What escapes. A FIFO whose occupancy drifts from its true fill level by one count per simultaneous push-pop, presenting weeks later as an overflow with the level indicator reading half full, or a full indicator on an empty queue.

How DV proves it. Drive push and pop in the same cycle and check occupancy is unchanged. Testing push and pop separately cannot find this, and separate testing is the natural thing to write.

Telemetry. Count simultaneous events explicitly — the n_both counter in the model. A design that never sees a simultaneous push and pop in silicon has not proved the case is correct, it has proved the case was not exercised, and those are different findings.

Misleading evidence. The individual statements, read in isolation. Each is correct. The defect exists only in their composition, which is why line-by-line review misses it and a review that asks "what fires together" catches it.

The general form. The same shape appears wherever a resource is credited and debited by different events: a credit counter incremented by returns and decremented by sends, a reference count, an outstanding-transaction tally. 30.1 found the identical defect at architecture level, where a completion and an abandonment in the same cycle were collapsed with an OR. This is that defect one abstraction level down.

8. Review Item 4 — Is This Identity Free, And How Long Does It Stay Valid?

Under review. Every tag allocator, transaction-id generator, sequence-number source and credit-slot index.

Invariant at risk. Two properties, and they are usually reviewed as one:

  • Uniqueness. An identity is never issued while the previous holder is still outstanding.
  • Lifetime. The published identity is meaningful for a defined window, and a consumer that samples it outside that window gets a different value.

Where it lives. The allocation condition, and the output's timing contract.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// RTL 4 - identity reuse. A tag allocator that hands out an identity which is
// still outstanding creates two live transactions that are indistinguishable,
// and every response after that may be matched to the wrong one.
//
//   BAD  : next_tag = next_tag + 1;            // wraps onto a live tag
//   GOOD : allocate only from the free set, and refuse when it is empty
//
// TEACHING MODEL. Sequential.
//   State remembered : one in-use bit per tag.
//   Safety           : a tag is never allocated while already in use.
//   Reset semantics  : async reset frees every tag; a surviving in-use bit
//                      leaks a tag permanently.
module id_reuse #(parameter int WRAP_WITHOUT_CHECKING = 0) (
  input  logic clk, rst_n,
  input  logic       alloc_req, free_req,
  input  logic [1:0] free_tag,
  output logic [1:0] granted_tag, last_granted,
  output logic       grant_valid, no_tags,
  output logic [3:0] in_use,
  output logic [7:0] n_granted, n_refused, n_double_alloc,
  output logic       id_err
);
  logic [3:0] use_q, use_next;
  logic [1:0] next_q, last_q;
  logic       would_collide;

  // ONE assignment to the in-use vector, computed from BOTH events. Written as
  // two independent `if` statements - a set for the grant and a clear for the
  // free - the later one wins and a free arriving with a grant for the SAME tag
  // silently un-marks a tag that was just handed out. STATED PRIORITY: the
  // grant wins, because a requester cannot have finished a transaction it is
  // being handed in this very cycle.
  always_comb begin
    use_next = use_q;
    if (free_req)    use_next[free_tag] = 1'b0;
    if (grant_valid) use_next[next_q]   = 1'b1;
  end

  assign in_use      = use_q;
  assign no_tags     = (use_q == 4'hF);
  assign would_collide = use_q[next_q];
  // The BAD build hands out next_q regardless; the GOOD build refuses when the
  // candidate is already in use.
  assign grant_valid = (WRAP_WITHOUT_CHECKING != 0) ? alloc_req
                                                    : (alloc_req && !would_collide);
  // `granted_tag` is PULSE-QUALIFIED: it is only meaningful in the cycle
  // `grant_valid` is high, because `next_q` advances on the grant. Reading it a
  // cycle later returns the NEXT tag, not the one granted. `last_granted` is the
  // HELD form, valid until the next grant - which is what a consumer that
  // samples after the pulse must use. Confusing the two is its own review point.
  assign granted_tag = next_q;
  assign last_granted = last_q;
  // SAFETY VIOLATION: a tag was granted while it was still outstanding.
  assign id_err = grant_valid && would_collide;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      use_q <= 4'd0; next_q <= 2'd0; last_q <= 2'd0;
      n_granted <= 8'd0; n_refused <= 8'd0; n_double_alloc <= 8'd0;
    end else begin
      use_q <= use_next;
      if (grant_valid) begin
        last_q        <= next_q;          // hold the tag actually granted
        next_q        <= next_q + 2'd1;
        n_granted     <= n_granted + 8'd1;
      end else if (alloc_req) begin
        n_refused <= n_refused + 8'd1;
      end
      if (id_err)   n_double_alloc <= n_double_alloc + 8'd1;
    end
  end
endmodule

Uniqueness

The measurement. Four tags allocated from a four-deep space, then a fifth request with nothing freed:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
4 granted : in_use=1111 no_tags=1 granted=4
5th request : good_valid=0 bad_valid=1 err_good=0 err_bad=1

The robust build refuses. The weak build grants tag 0 — which is still outstanding — creating two live transactions with the same identity. Every response from that point can be matched to the wrong one, and the matching logic has no way to know.

What escapes. Data returned to the wrong requester. This is the worst class of escape in the chapter because it is silent, it corrupts rather than hangs, and it is separated from its cause by however long the first transaction had been outstanding.

Lifetime — the review point this chapter was built around

granted_tag is pulse-qualified. It is a combinational view of the allocator's next-tag pointer, and that pointer advances on the grant. It is meaningful only in the cycle grant_valid is high. A consumer that samples it one cycle later — which is the natural thing to do after seeing a pulse — receives the next tag, not the one that was granted.

last_granted is held. It is a register that captures the tag actually granted and holds it until the next grant. It is the output a consumer that samples after the pulse must use.

Both outputs are individually correct. Neither is an arithmetic error. The defect is an interface contract that was never written down, and the failure mode is a transaction tagged with an identity the allocator never issued for it.

A waveform over eight cycles of a tag allocator. A grant pulse in cycle two hands out tag zero while the pulse-qualified output already shows one. The held output captures zero and keeps it. A consumer sampling the pulse-qualified output one cycle late reads one, an identity that was never granted.grant: tag 0grant: tag 0late sample reads 1late sample reads 1grant: tag 1grant: tag 1clkgrant_validgranted_tag00011122last_grant00000011in_use00011133late_readt0t1t2t3t4t5t6t7
Figure 2 — a teaching waveform, not normative CXL timing. In cycle 2 the grant fires and tag 0 is allocated; the in_use row confirms it one cycle later. The granted_tag row already reads 1 in cycle 3, because the allocator's pointer advanced on the grant — so a consumer that latches on the cycle after the pulse captures identity 1, which the allocator has not issued to anybody. The last_grant row is the held form: it captures 0 at the grant and keeps it until the next one. Nothing in this waveform is an arithmetic error. Both rows are correct; only one of them answers the question the consumer is asking.

The five questions this section exists to make reflexive. For every published value in a design under review:

  1. When is it valid? Name the cycles, not the signal.
  2. What qualifies it? Which signal's assertion makes it meaningful.
  3. Is it pulse-qualified or held? These are different contracts with different consumers.
  4. Can a consumer legally sample it later? If yes, there must be a held form.
  5. What state transition invalidates it? For a pulse-qualified value, name the thing that moves underneath it.

A value can be logically correct and have an unsafe temporal interface contract. That sentence is the whole of this section. The reviewer's instinct is to check the arithmetic; the arithmetic here is flawless.

Evidence to demand. For every output, the cycle window in which it is meaningful, written down. If the module's documentation does not distinguish pulse-qualified outputs from held ones, the distinction exists anyway and somebody will get it wrong.

How DV proves it. Assert the pulse-qualified output while its qualifier is high, and the held output after. The testbench that found this had made exactly the opposite mistake, twice — section 19 records it as a testbench defect, which is what it was.

Telemetry. A tag-reuse counter that must read permanently zero, and a free-list occupancy high-water mark. The first catches the uniqueness escape; nothing in silicon catches the lifetime escape, which is why it must be caught in review.

9. Review Item 5 — What Width Is The Intermediate?

Under review. Every multiplication, every shift, every sum of more than two terms, every part-select of an expression, and every mixed signed-unsigned comparison.

Invariant at risk. The computed value fits in the net that holds it.

Where it lives. A declaration and an expression, usually on different lines and often in different files.

The rule. The product of two N-bit values needs 2N bits. Assigned to an N-bit net it truncates silently, and the result is a small plausible number where a large one belonged — which is worse than a large implausible one, because a plausible number propagates.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// RTL 5 - arithmetic intermediate width. The product of two N-bit values needs
// 2N bits. Assigning it to an N-bit net truncates silently, and the result is a
// small plausible number where a large one belonged.
//
//   BAD  : logic [15:0] p; assign p = a * b;              // wraps at 65536
//   GOOD : logic [31:0] q; assign q = {16'd0,a} * {16'd0,b};
//          assign p = (q > 32'd9999) ? 16'd9999 : q[15:0];
//
// The same review point covers part-selects of an expression, signed/unsigned
// mixing, and loop counters narrower than their bound.
//
// TEACHING MODEL.
module width_discipline #(parameter int NARROW_INTERMEDIATE = 0) (
  input  logic clk, rst_n,
  input  logic        evaluate,
  input  logic [15:0] a, b,
  output logic [15:0] product, truth_lo, overflowed_by,
  output logic [31:0] wide_product,
  output logic        fits_16, would_wrap,
  output logic [7:0]  n_evals, n_wrapped,
  output logic        width_err
);
  logic [15:0] narrow_product;
  // The full-width intermediate. Computed in BOTH builds so the model can
  // detect its own weak build.
  assign wide_product = {16'd0, a} * {16'd0, b};
  // The antipattern: a 16-bit result register fed by a 16x16 multiply.
  assign narrow_product = a * b;
  assign would_wrap = (wide_product > 32'd65535);
  assign fits_16    = !would_wrap;
  assign truth_lo   = wide_product[15:0];
  assign product = (NARROW_INTERMEDIATE != 0)
                 ? narrow_product
                 : (would_wrap ? 16'hFFFF : wide_product[15:0]);
  assign overflowed_by = would_wrap ? (wide_product[31:16]) : 16'd0;
  // SAFETY: a build that wraps must not report a value smaller than the truth
  // while claiming the result fits.
  assign width_err = (NARROW_INTERMEDIATE != 0) && would_wrap && (product < truth_lo + 16'd1)
                     && (product != 16'hFFFF);

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      n_evals <= 8'd0; n_wrapped <= 8'd0;
    end else if (evaluate) begin
      n_evals <= n_evals + 8'd1;
      if (would_wrap) n_wrapped <= n_wrapped + 8'd1;
    end
  end
endmodule

The measurement. Four evaluations, chosen to sit either side of the boundary:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
400 x 400 : wide=160000 good=65535 bad=28928 would_wrap=1
255 x 257 = 65535 : good=65535 bad=65535 would_wrap=0
256 x 257 = 65792 : good=65535 bad=256
300 x 300 = 90000 : good=65535 bad=24464

Read the second line first. At exactly 65,535 the two builds agree, because the boundary case is the last value that fits. A test suite built from round numbers and a maximum lands here and reports both builds correct.

Then read the fourth. 256 × 257 is 65,792 — 257 over the boundary — and the truncated build reports 256. Not a large wrong number. Not an X. A small, entirely reasonable-looking 256, in a design where 256 is a value that occurs naturally.

Compiler silence is not width proof

Icarus Verilog does not warn on this multiply under -Wall. Neither, in the general case, do the other tools a reviewer is likely to be relying on. The absence of a diagnostic proves nothing about:

QuestionWhat no-warning proves
the expression's widthnothing
the intermediate's widthnothing
the destination's widthnothing
the signedness of either operandnothing
whether a cast was correctnothing
whether truncation occurrednothing

The most expensive width defect in this chapter produces no diagnostic at all. That is the finding, and it is recorded in section 19 as a compiler-warning result rather than as an absence of one, because "we saw no warnings" is not evidence and "we confirmed the tool does not warn on this construct" is.

Evidence to demand. For each arithmetic expression, the widths of both operands, the width of the expression's evaluation context, and the width of the destination — stated as three numbers. If the reviewer cannot get three numbers, the expression has not been reviewed.

What escapes. A size calculation that wraps and allocates a buffer far too small; an address computation that aliases; a rate calculation that reports a plausible fraction of the real number and is believed.

How DV proves it. Drive the operands at the boundary and one step past it: at exactly the maximum that fits, one over, and well over. The boundary case is the one that agrees, so a test that drives only the boundary reports both builds correct.

Telemetry. A saturation counter on the robust build. A design that clamps is telling you it hit the limit; a design that wraps is telling you nothing, which is exactly the difference between the two builds.

Misleading evidence. Explicit zero-extension that looks load-bearing and is not. The model writes {16'd0, a} * {16'd0, b}, and in that specific expression the widening is redundant — the 32-bit assignment context already extends both operands. It is still correct practice, because it stops the expression's behaviour from depending on where the result is assigned. Section 18 records a mutation that exploited exactly this and had to be withdrawn as equivalent.

10. Review Item 6 — Does Recovery Clear The Accounting As Well As The Storage?

Under review. Every reset, flush, error-recovery path, mode change and soft-reset that clears one structure.

Invariant at risk. After recovery, every derived quantity agrees with the state it is derived from.

Where it lives. The body of the recovery branch — specifically, what is not in it.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// RTL 6 - clearing the storage is not clearing the ACCOUNTING.
//
// A flush operation that empties a table but leaves its occupancy counter
// behind makes the design believe in entries that no longer exist. The table is
// empty and unusable, and every allocation decision built on the counter is
// wrong. The same defect appears wherever a reset, a flush, an error-recovery
// path or a mode change clears one and not the other.
//
//   BAD  : if (flush) table <= '0;                      // counter untouched
//   GOOD : if (flush) begin table <= '0; count <= 0; end
//
// Note the asynchronous reset clears BOTH in both builds. That is deliberate:
// an uninitialised counter is X rather than stale, and X is a different (and
// more easily caught) failure. This model isolates the harder case where every
// register is properly initialised and the two are still allowed to diverge.
//
// TEACHING MODEL.
//   Safety : the occupancy counter always equals the number of set bits.
module reset_completeness #(parameter int PARTIAL_FLUSH = 0) (
  input  logic clk, rst_n,
  input  logic       alloc, free_slot, flush,
  input  logic [1:0] slot,
  output logic [3:0] table_bits,
  output logic [7:0] occupancy, popcount,
  output logic       consistent,
  output logic [7:0] n_allocs, n_flushes,
  output logic       rst_err
);
  logic [3:0] tab_q;
  logic [7:0] occ_q;
  integer i;
  logic [7:0] pc;

  always_comb begin
    pc = 8'd0;
    for (i = 0; i < 4; i = i + 1) if (tab_q[i]) pc = pc + 8'd1;
  end

  assign table_bits = tab_q;
  assign occupancy  = occ_q;
  assign popcount   = pc;
  assign consistent = (occ_q == pc);
  // SAFETY VIOLATION: the accounting disagrees with the storage.
  assign rst_err    = !consistent;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      // Both builds initialise completely. The divergence is introduced by the
      // FLUSH path below, not by reset.
      tab_q <= 4'd0; occ_q <= 8'd0;
      n_allocs <= 8'd0; n_flushes <= 8'd0;
    end else if (flush) begin
      tab_q     <= 4'd0;
      n_flushes <= n_flushes + 8'd1;
      // The whole review point.
      if (PARTIAL_FLUSH == 0) occ_q <= 8'd0;
    end else begin
      if (alloc && !tab_q[slot]) begin
        tab_q[slot] <= 1'b1;
        occ_q       <= occ_q + 8'd1;
        n_allocs    <= n_allocs + 8'd1;
      end else if (free_slot && tab_q[slot]) begin
        tab_q[slot] <= 1'b0;
        occ_q       <= (occ_q == 8'd0) ? 8'd0 : occ_q - 8'd1;
      end
    end
  end
endmodule

The measurement. Three slots allocated, then a flush:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
3 allocated : table=0111 occ=3 popcount=3 consistent=1
after flush : good occ=0 pc=0 | bad occ=3 pc=0

The table is empty and the counter says three. Every allocation decision from that point is made against a capacity the design does not have. The structure is not corrupt and not unreachable — it is empty and believed full to three, which is a working design that has silently lost a quarter of its capacity.

Defined stale state is not X

The asynchronous reset clears both the table and the counter in both builds. That is deliberate, and it is a correction: this model originally introduced the divergence at reset, so the weak build's counter was never assigned at power-on and was X for the entire run.

Those are different defects with different detectability.

XStale
ValueX3
Propagatesyes, visiblyno
Survives a looknoyes
Caught byalmost anythinga consistency check

An X is comparatively easy to catch: it propagates, it turns comparisons unknown, and a checker that rejects X finds it immediately. A properly initialised but stale value is plausible. It reads as a number a reviewer would expect, it survives a waveform inspection, and it is wrong.

The harder case is the one worth teaching, so the model was rebuilt to produce it: every register is correctly initialised, reset is complete in both builds, and the divergence is introduced by a flush — a realistic operation that empties storage and forgets to clear the accounting.

How it was found. By the X/Z rejection in the check harness, which reported "X/Z in result" rather than a value mismatch. A checker written as a plain equality would have compared X against 3, failed, and been "fixed" by adjusting the expected number — hiding the real finding behind a green test.

Evidence to demand. For every recovery path, the complete list of state it clears, checked against the complete list of state that exists. The gap is the finding.

What escapes. Capacity that shrinks after every error-recovery event, so a design that has recovered four times has lost its entire table and hangs — with a counter that reads exactly full and a structure that is entirely empty.

How DV proves it. A consistency assertion that holds continuously: the accounting equals the thing it accounts for. Not checked at the end of the test — checked every cycle, because the window after a flush is where it is false.

Telemetry. Publish the counter and the popcount and compare them in hardware. It costs a comparator and it is one of the few self-checks worth leaving in production silicon, because it catches an entire class of recovery-path defects with one bit.

A block diagram of a flush path. The flush clears the table in both builds. The complete flush also clears the occupancy counter and consistency holds. The partial flush leaves the counter at three while the popcount is zero, so the design believes in three entries that no longer exist.flushtable clearedcounter clearedtoocompletecounter untouchedpartialocc 3, popcount 0believes in 3 ghostsocc 0, popcount 0consistent12

Figure 3 — the storage and the accounting are two structures. Both builds clear the table, and a reviewer watching the table alone sees two identical designs. The difference is entirely in what the flush branch does not contain, and it is visible only by comparing the counter against a popcount the design does not otherwise need.

11. Review Item 7 — Does The Retry Replay The Original?

Under review. Every retry, replay, re-arbitration and error-recovery re-issue.

Invariant at risk. A retried request is the same request — same payload, same identity, counted once.

Where it lives. The source the replay reads from.

The construct. On a nack, the design re-issues. The question is from where. A replay that reads the current input bus replays whatever happens to be on the bus at that moment, and by then the bus holds a different request.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// RTL 7 - retry state. A retry must re-present the ORIGINAL request, exactly
// once, without losing it and without counting it twice.
//
//   BAD  : on retry, re-issue from the current input bus
//          - by then the bus holds a different request
//   GOOD : hold the original in a retry register and replay THAT
//
// TEACHING MODEL. Sequential.
//   State remembered : the held request payload and a retry-pending flag.
//   Safety           : a retried request carries the original payload.
//   Liveness         : a retried request is eventually issued - ASSUMING the
//                      retry budget is not exhausted.
module retry_state #(parameter int REPLAY_FROM_BUS = 0) (
  input  logic clk, rst_n,
  input  logic       issue, nack, bus_valid,
  input  logic [7:0] bus_payload,
  input  logic [3:0] retry_limit,
  output logic [7:0] issued_payload, held_payload,
  output logic [3:0] retry_count,
  output logic       retry_pending, budget_spent, replay_valid,
  output logic [7:0] n_issued, n_retried, n_corrupted,
  output logic       retry_err
);
  logic [7:0] held_q, issued_q;
  logic [3:0] cnt_q;
  logic       pend_q;

  assign held_payload  = held_q;
  assign issued_payload= issued_q;
  assign retry_count   = cnt_q;
  assign retry_pending = pend_q;
  assign budget_spent  = (cnt_q >= retry_limit) && (retry_limit != 4'd0);
  assign replay_valid  = pend_q && !budget_spent;
  // SAFETY VIOLATION: a replay carried a payload that is not the original.
  assign retry_err = replay_valid && (issued_q != held_q) && (n_retried != 8'd0);

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      held_q <= 8'd0; issued_q <= 8'd0; cnt_q <= 4'd0; pend_q <= 1'b0;
      n_issued <= 8'd0; n_retried <= 8'd0; n_corrupted <= 8'd0;
    end else begin
      if (issue && bus_valid && !pend_q) begin
        held_q   <= bus_payload;      // capture the ORIGINAL
        issued_q <= bus_payload;
        cnt_q    <= 4'd0;
        n_issued <= n_issued + 8'd1;
      end else if (nack && !budget_spent) begin
        pend_q   <= 1'b1;
        cnt_q    <= cnt_q + 4'd1;
        n_retried<= n_retried + 8'd1;
        // The review point. The robust build replays the HELD payload; the weak
        // build re-reads the bus, which by now holds something else entirely.
        issued_q <= (REPLAY_FROM_BUS != 0) ? bus_payload : held_q;
      end else if (pend_q && !nack) begin
        pend_q <= 1'b0;
      end
      if (retry_err) n_corrupted <= n_corrupted + 8'd1;
    end
  end
endmodule

The measurement. Payload a5 issued, then nacked one cycle after the bus has moved on to 3c:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
issued : held=a5 issued=a5 n_issued=1
after nack : good_issued=a5 bad_issued=3c held=a5 bus=3c

The weak build replays 3c, a request that was never issued and was never nacked. The original a5 is lost entirely. There is no error, no counter, and no way for the responder to know that the transaction it is now completing is not the one it rejected.

Three properties, reviewed separately

PropertyFails as
Fidelity — is the payload the original?silent corruption
Exactly-once — is it issued twice?duplicate work, double counting
Boundedness — is the count bounded?livelock

The model publishes all three, and the third one carries an explicit environmental assumption: the liveness claim is a retried request is eventually issued, assuming the retry budget is not exhausted. With retry_limit at zero the budget never spends, replay_valid stays high, and the design retries indefinitely — and no safety property is violated, which is what makes livelock hard. The measurement:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
budget : count=3 limit=3 spent=1 replay_valid=0
limit 0 : count=1 spent=0 replay_valid=1

Evidence to demand. The register the replay reads from, and the condition under which it was written. If the replay reads a module input, the review is over and the finding is written.

What escapes. A transaction that completes carrying another transaction's payload, with correct-looking counts on both sides.

How DV proves it. Issue, change the bus, then nack — in that order, with the bus change strictly between. A test that nacks while the bus still holds the original cannot find this, and holding the bus steady is the natural thing to do in a directed test.

Telemetry. Retries by cause, retries per transaction as a distribution rather than a total, and a budget-exhaustion counter. The distribution matters more than the total: a thousand retries spread over a thousand transactions is a busy fabric, and a thousand retries on one transaction is a livelock the total cannot distinguish.

Misleading evidence. A retry counter that increments correctly in the weak build. It does — the count is right and the payload is wrong, and a reviewer checking the counter has checked the wrong thing.

12. Review Item 8 — Does The Combinational Block Assign On Every Path?

Under review. Every always_comb and always @* block containing an if without a matching else, or a case without a default.

Invariant at risk. The output is a function of the current inputs only.

Where it lives. The missing else.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// RTL 8 - combinational completeness. A combinational block that does not
// assign every output on every path holds the previous value: in simulation it
// looks like memory, in synthesis it IS memory - an inferred latch.
//
//   BAD  : always_comb begin
//            if (sel) out = a;              // no else - `out` holds
//          end
//   GOOD : always_comb begin
//            out = default_value;           // assign first, refine after
//            if (sel) out = a;
//          end
//
// TEACHING MODEL. The weak build is written with the omission intact so the
// stale value is observable.
module comb_completeness #(parameter int INCOMPLETE_ASSIGN = 0) (
  input  logic clk, rst_n,
  input  logic        sel_a, sel_b,
  input  logic [15:0] a, b, default_val,
  output logic [15:0] out, expected,
  output logic        holds_stale,
  output logic [7:0]  n_evals, n_stale,
  output logic        comb_err
);
  logic [15:0] out_bad, out_good, prev_q;

  // The antipattern, preserved exactly. With neither select asserted `out_bad`
  // is not assigned on this path and retains whatever it held.
  always @* begin
    if (sel_a)      out_bad = a;
    else if (sel_b) out_bad = b;
  end

  // The robust form: a default assignment before any refinement.
  always @* begin
    out_good = default_val;
    if (sel_a)      out_good = a;
    else if (sel_b) out_good = b;
  end

  assign out      = (INCOMPLETE_ASSIGN != 0) ? out_bad : out_good;
  // The independent expectation, written as a single expression rather than as
  // a procedural block, so it cannot share the omission under review.
  assign expected = sel_a ? a : (sel_b ? b : default_val);
  assign holds_stale = (out != expected);
  // SAFETY VIOLATION: the output does not match the function it claims to compute.
  assign comb_err = holds_stale;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      n_evals <= 8'd0; n_stale <= 8'd0; prev_q <= 16'd0;
    end else begin
      n_evals <= n_evals + 8'd1;
      prev_q  <= out;
      if (holds_stale) n_stale <= n_stale + 8'd1;
    end
  end
endmodule

The measurement. Both selects deasserted, after a has been driven to 111 and the default is 999:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
neither select : good=999 bad=222 expected=999
after touching a : bad=111

The second line is the one that unmasks it. With neither select asserted the weak build holds 222 — the value from the previous evaluation. Then a changes, the block re-evaluates, and the output becomes 111 — a value that depends on an input the current selects do not select. The output is not a function of its inputs; it is a function of its input history.

Evidence to demand. The synthesis report's latch inference list, and an explanation for every entry. A design with intentional latches has a short list and a reason for each. A design with accidental latches has a long list and a reviewer who has not read it.

What escapes. A combinational path that behaves correctly in simulation, infers a latch in synthesis, and then fails timing analysis in a way that is reported as a timing problem rather than a coding problem — so the fix applied is a constraint rather than an else.

How DV proves it. Drive the un-selected case, then change a deselected input and check the output does not move. The second half is the test; the first half alone shows a stale value that could be coincidentally correct.

Telemetry. None. This defect has no silicon telemetry, which is why the synthesis report is the review artefact and why a latch-inference list nobody reads is a review that did not happen.

Misleading evidence. Simulation. The block behaves as memory in simulation and as memory in synthesis, and the two memories are not the same memory — the simulator's is a variable and the synthesiser's is a latch with a real enable and real timing.

13. Review Item 9 — Does The Parameter Move Any Telemetry?

Under review. Every parameter, define, strap, fuse and configuration register that alters behaviour.

Invariant at risk. The configuration of the running design is observable from outside it.

Where it lives. Not in a line — in the absence of one.

The claim. If a parameter alters what the design does, something observable must change with it. A behavioural parameter that moves no counter, no state and no output cannot be verified as configured, cannot be confirmed in silicon, and two teams can hold opposite beliefs about which build is running with the same evidence in front of them.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// RTL 9 - a parameter that changes behaviour and moves no telemetry.
//
// A review point that is easy to state and easy to miss: if a parameter alters
// what the design DOES, something observable must change with it. A behavioural
// parameter that moves no counter, no state and no output cannot be verified as
// configured, cannot be confirmed in silicon, and two teams can hold opposite
// beliefs about the build with the same evidence.
//
//   BAD  : parameter changes an internal decision, reports nothing
//   GOOD : the decision is reflected in a mode output and a counter
//
// TEACHING MODEL.
module parameter_telemetry #(parameter int SILENT_PARAMETER = 0) (
  input  logic clk, rst_n,
  input  logic       op_valid, aggressive_mode,
  input  logic [7:0] threshold,
  input  logic [7:0] value,
  output logic [7:0] mode_report, n_ops, n_taken, n_skipped,
  output logic       took_action, mode_observable,
  output logic       tel_err
);
  logic acted;
  // The behavioural decision the parameter influences.
  assign acted = op_valid && (aggressive_mode ? (value >= (threshold >> 1))
                                              : (value >= threshold));
  assign took_action = acted;
  // The robust build publishes WHICH rule it applied. The silent build reports a
  // constant, so no observer can tell the two configurations apart.
  assign mode_report = (SILENT_PARAMETER != 0) ? 8'd0
                                               : (aggressive_mode ? 8'd2 : 8'd1);
  assign mode_observable = (mode_report != 8'd0);
  // SAFETY-OF-REVIEW violation: behaviour is configurable and nothing reports it.
  assign tel_err = op_valid && !mode_observable;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      n_ops <= 8'd0; n_taken <= 8'd0; n_skipped <= 8'd0;
    end else if (op_valid) begin
      n_ops <= n_ops + 8'd1;
      if (acted) n_taken   <= n_taken + 8'd1;
      else       n_skipped <= n_skipped + 8'd1;
    end
  end
endmodule

The measurement. The same value, evaluated under both modes:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
value 60, normal mode : acted=0 mode_report=1 observable=1
value 60, aggressive : acted=1 good_mode=2 bad_mode=0
after three clocked ops : ops=3 taken=2 skipped=1

The behaviour genuinely differs — the same input produces no action in one mode and an action in the other. The robust build publishes which rule it applied. The silent build reports a constant, so an observer looking at the mode output cannot tell the two configurations apart, and the only way to know which build is running is to read the source that built it.

Evidence to demand. For every behavioural parameter, the observable that changes with it. "It is set at build time" is not an answer; the question is how a person holding the chip finds out.

What escapes. A fleet running two configurations that nobody can distinguish, where a bug that reproduces on one build and not the other cannot be localised because the build cannot be read back.

How DV proves it. Instantiate both configurations in the same testbench and assert the observable differs. This is a configuration-contrast test, and it is the one most environments do not have — most build the alternative configuration, run the same regression, and never compare the two.

Telemetry. This item is telemetry. A configuration-report register, readable at runtime, carrying every behavioural parameter the design was built with.

Misleading evidence. A parameter that appears in the module header and changes nothing observable. It looks configured. It is configured. Nothing downstream can prove it.

14. The Review Assembled

Nine dimensions, one summary — and the same trap 30.1 found at architecture level.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// RTL 10 - an RTL review assembled. Nine review dimensions, one summary.
// "The RTL was reviewed" is bit 0: a meeting happened, and one sixth of a review.
module rtl_review_signoff #(parameter int REVIEWED_IS_CORRECT = 0) (
  input  logic clk, rst_n,
  input  logic        review,
  input  logic        rtl_reviewed, counts_on_accept, transitions_complete,
  input  logic        single_driver, identities_unique, widths_sufficient,
  output logic [5:0]  fail_mask,
  output logic [15:0] conditions_met, sound_pct,
  output logic        sound,
  output logic [7:0]  n_reviews, n_sound, n_claimed,
  output logic        signoff_err
);
  logic [31:0] s_q;
  logic        truly_sound, claimed;
  assign fail_mask[0] = ~rtl_reviewed;
  assign fail_mask[1] = ~counts_on_accept;
  assign fail_mask[2] = ~transitions_complete;
  assign fail_mask[3] = ~single_driver;
  assign fail_mask[4] = ~identities_unique;
  assign fail_mask[5] = ~widths_sufficient;
  assign conditions_met = {15'd0, rtl_reviewed} + {15'd0, counts_on_accept}
                        + {15'd0, transitions_complete} + {15'd0, single_driver}
                        + {15'd0, identities_unique} + {15'd0, widths_sufficient};
  assign s_q = ({16'd0, conditions_met} * 32'd100) / 32'd6;
  // No clamp: six one-bit values over six cannot exceed a hundred.
  assign sound_pct = s_q[15:0];
  assign truly_sound = (fail_mask == 6'd0);
  assign claimed = (REVIEWED_IS_CORRECT != 0) ? rtl_reviewed : truly_sound;
  assign sound = claimed;
  assign signoff_err = review && !truly_sound && claimed;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      n_reviews <= 8'd0; n_sound <= 8'd0; n_claimed <= 8'd0;
    end else if (review) begin
      n_reviews <= n_reviews + 8'd1;
      if (truly_sound) n_sound <= n_sound + 8'd1;
      if (claimed)     n_claimed <= n_claimed + 8'd1;
    end
  end
endmodule

The measurement. Two sign-off views of the same block:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
counting on valid : mask=000010 met=5 sound=83%
a meeting and nothing else : mask=111110 met=1 sound=16%

The first line is a real review with one finding open. Five of six conditions met, one bit set — the counter counts presentation. The block is not ready, and it is one fix away.

The second line is the failure this whole module is about. The RTL was reviewed. A meeting happened, people read the code, and not one of the other five conditions was established. Sixteen percent of a review, reported as a review.

"The RTL was reviewed" is bit 0. It is a necessary condition and it carries one sixth of the weight, and a sign-off process that treats it as the whole is the process that produces the second line.

A flowchart for an RTL review. The RTL was read, then counters count acceptance, transitions are complete, every register has one driver, identities are unique while live, and arithmetic widths are sufficient. Any failure ends in a review that is not sound; passing all six ends in a sound review.yesyesyesyesyesRTL readcounters countacceptance?transitionscomplete?one driver perregister?identitiesunique whilelive?widthssufficient?review soundany no: read, notreviewed
Figure 4 — the RTL review as a flow. The first decision is the weak one and the only one many reviews reach: the RTL was read, at length, by people who know it. The five below it are ordered by how much of the design each carries — acceptance first because a miscount corrupts every downstream figure, then the three structural invariants, then the arithmetic. Any failure ends in a review that is not sound; passing all six ends in a sound review.

15. Quantitative Reasoning

Every figure here is a teaching parameter or a value derived from one and asserted by the testbench. None is a measurement of a real system, and none is a CXL specification number.

The handshake miscount is the stall depth plus one. One transaction presented for four cycles with three stall cycles is counted four times. The general form is count = 1 + stalls, so the error is zero on an idle interface and unbounded on a congested one — the defect's magnitude is a function of the traffic, which is why it hides in a nominal regression.

A three-bit state register holds eight encodings for four states. Four encodings — half the space — are defined by nothing. A one-hot encoding of the same four states uses four bits and leaves twelve of sixteen encodings undefined, which is a larger absolute hole and a smaller practical risk, because a one-hot register's illegal encodings are detectable with a single popcount check. The encoding choice changes what the default arm is for, and both choices need one.

The truncation boundary, derived. A 16 × 16 multiply produces up to 32 bits; the exact boundary is 65,535, and 255 × 257 = 65,535 is the largest product of two values in this range that still fits. One step past it, 256 × 257 = 65,792, and the truncated build reports 65,792 − 65,536 = 256. At 300 × 300 = 90,000 it reports 90,000 − 65,536 = 24,464; at 400 × 400 = 160,000 it reports 160,000 − 131,072 = 28,928.

None of those three wrong answers looks wrong. 256 is a power of two, 24,464 is an ordinary number, 28,928 is an ordinary number. A saturating build reports 65,535 every time, which is visibly a limit and therefore visibly a problem.

Tag-space sizing, derived. To keep R transactions in flight over a round trip of T cycles at an issue rate of one per I cycles, the allocator needs at least T / I identities. At an illustrative 600-cycle round trip and one issue every 8 cycles that is 75 tags, so a 7-bit tag space. A 2-bit space — the model's — supports four, which is why its fifth request is the interesting one. Under-sizing the tag space does not corrupt anything in the robust build; it stalls. Under-sizing it in the weak build corrupts, because the weak build wraps instead of refusing.

The held identity costs ceil(log2(N)) flops. For the model's four tags that is two flops. For the 75-tag allocator above it is seven. Seven flops is the entire price of the interface contract in section 8, and the defect it prevents is data returned to the wrong requester.

The in-use vector costs one bit per identity — 4 bits in the model, 75 in the sized example — and the allocation check is a single indexed read of it. The refusal path is a mux. The robust build is not more expensive than the wrapping build in any way a synthesiser would notice.

Phantom occupancy after a partial flush. Three of four slots allocated, then flushed: the counter reads 3 against a real occupancy of 0, so 75 percent of the table is believed occupied by entries that do not exist. Four such events consume the entire structure. The consistency check that catches it is a 3-bit popcount and a comparator.

The retry budget is a bound on work, not on time. With a limit of 3 the count reaches 3 and budget_spent asserts. With a limit of 0 the budget never spends: replay_valid stays high and the design retries indefinitely, and no safety property is violated by that — the payload is still correct, the identity is still unique, nothing is double-counted. Livelock is a liveness failure, and safety assertions do not see it.

The sign-off arithmetic. Six conditions; five met is 5 × 100 / 6 = 83 percent under integer division, and one met is 100 / 6 = 16 percent. The second number is what "the RTL was reviewed" is worth on its own.

16. Verification Method

Order of work

compile → inspect warnings → legal baseline → reset → boundaries → simultaneous events → abuse and error cases → configuration contrasts → PASS → mutation campaign

A mutation campaign on failing RTL is invalid, and this chapter produced the cleanest demonstration of that rule in the batch. Section 18 records it: a broken baseline returned 45 of 45 killed, a better-looking number than the correct campaign, earned entirely by failing for the same reason every time.

Independent oracles

Expected values are reasoned from the specification of the model, never copied from its implementation.

ModelOracle
handshake1 transaction over 4 valid cycles → accepted count is 1, stall count is 3
transitionspoke to 6 → both builds illegal; one cycle on → IDLE with the default arm, 6 without
collisionoccupancy 3, simultaneous push and pop → still 3
identity4 tags, 4 grants, 5th refused; granted tag during the pulse is 0, next pointer after it is 1
width400 × 400 = 160,000 by hand; truncated = 160,000 − 131,072 = 28,928
flush3 allocated → popcount 3; after flush → popcount 0, so the counter must be 0
retryissue a5, bus moves to 3c, nack → replayed payload must still be a5
combinationalneither select → the default 999, and it must not move when a changes
telemetryvalue 60, threshold 100 → no action; aggressive halves it to 50 → action
sign-offfive of six conditions → 83 percent; one of six → 16 percent

If the oracle and the design disagree, either could be wrong. chkv prints both numbers for exactly that reason, and in this chapter it caught one case where the design was right and my expectation was wrong — the operation count in section 13, which I had traced over #1 steps rather than over clock edges.

X and Z rejected explicitly

chk(c, …) tests c !== 1'b1, so an X-valued condition fails rather than passing. chkv(got, exp, …) reduces the result and reports an explicit X/Z failure before comparing.

This is not a formality in this chapter. The X/Z rejection is what found the flush model's real subject: it reported "X/Z in result" where a plain equality would have reported a value mismatch, and a value mismatch is the kind of failure an author fixes by changing the expected number. Section 19 records what that would have hidden.

Pulses are latched, never sampled

Every safety output — hs_err, st_err, coll_err, id_err, width_err, rst_err, retry_err, comb_err, tel_err, signoff_err — is caught by a continuous always @(posedge clk) monitor into a sticky bit, and the sticky bits are asserted outside any conditional. A check nested inside if (pulse) cannot fail when the defect is that the pulse never fires.

One of this chapter's three testbench defects was the inverse of that: a monitor that was correct but had no clock edge inside the window it was meant to observe, so the sticky bit never latched because the violation never reached a posedge. A sticky monitor still needs a clock during the window, and that is not obvious until it bites.

Both builds are always instantiated

Every model here has both its robust and weak build wired up and contrasted in the same simulation. This is carried from 29.5, where three sequential models had only their measured build and a mutation on a parameter-selected branch was the only thing that found it.

Safety, liveness and performance kept apart

Safety — the published count equals the accepted count; the design never sits in a non-state; occupancy tracks the truth; an identity is never granted while live; the accounting equals the storage; a replay carries the original payload; a combinational output is a function of its inputs. None of these requires an assumption.

Liveness — a retried request is eventually issued, assuming the retry budget is not exhausted. The model proves the withdrawal: at retry_limit zero the request retries forever and no safety property fires.

Performance — a four-deep tag space, a three-retry budget, a four-slot table. These are targets. A design that misses one is slow or small, not incorrect.

17. Assertions

The testbenches carry 328 checks149 across the first five models and 179 across the last five.

Every output of every model is asserted as a value, in both builds. The output-listing gate reports zero unasserted output nets across the ten models.

Every simulator-derived value printed to the reader is asserted. The displayed-value gate scans 87 printed references and reports zero unasserted derived values. Two printed names are testbench-driven inputs — the retry budget and limit in section 11 — and are reported apart, because those are constants the author set rather than results the simulator produced.

Reset is verified with live state, not only at time zero: with a handshake in progress, with tags outstanding, with a table partly allocated, with a retry pending and with an occupancy above zero.

Simultaneous events are driven: push with pop, issue with nack, valid with ready falling, a flush concurrent with an allocation, and a grant with a free of the very same identity — the case that found the defect in section 19.

Abuse cases are driven and asserted to be no-ops or refusals: ready high with no valid, a pop of an empty structure, a free of a tag that is not in use, an allocate of a slot already allocated, a free of a slot not allocated, a nack with nothing issued, a zero operand into the multiplier, an illegal IDLE-to-RUN transition, an operation with no op_valid, and — the one that mattered — a fresh issue inside the single cycle a retry is pending.

Boundaries are driven at the edge and one step past it: exactly 65,535 and one above, a full tag space and one request past it, an exhausted retry budget and a budget of zero, a full table and an empty one, and each threshold in section 13 at exactly its value.

18. Mutation Testing

83 mutations attempted. 1 withdrawn as equivalent. 82 non-equivalent mutations injected, 82 killed. Zero unexplained survivors.

Reported separatelyCount
Mutants attempted83
Withdrawn as equivalent1
Non-equivalent mutants82
Killed82
Unexplained survivors0

By model:

ModelDimensionMutations
m1handshake acceptance8
m2transition completeness8
m3single-driver discipline6
m4identity uniqueness and lifetime9
m5arithmetic width6
m6recovery completeness9
m7retry state9
m8combinational completeness7
m9behavioural telemetry7
m10review sign-off13

Four survivors on the first run, every one classified before anything was changed.

Survivor 1 — missing checker

m2, the illegal-transition detector: st == RUN changed to st == ARMED. The separating question has an answer — the legal IDLE-to-ARMED move would be reported as illegal — so the mutation is not equivalent. It survived because the testbench asserted illegal_transition low only during the poked non-state and never during a legal transition. The checker was added.

Survivor 2 — equivalent, and withdrawn

m5, the widened multiply: {16'd0,a} * {16'd0,b} changed to {16'd0,a} * b.

The separating question has no answer. The assignment context is 32 bits, so the unsigned 16-bit b is zero-extended to 32 regardless of whether the source says so. The two expressions are identical under Verilog's width rules. Withdrawn. Not counted as a kill.

This is the first equivalent mutant in the batch that comes from language semantics rather than from arithmetic. Nothing about the model is redundant; the mutation looks like it narrows an operand and does not, because the expression's width is set by where its result goes. A reviewer reading that diff would reasonably expect it to truncate — which is precisely why the explicit widening is still correct practice. It is not load-bearing in this expression, and it becomes load-bearing the moment the result is assigned somewhere narrower. Writing the intent explicitly is what stops the expression's behaviour from depending on its context.

Survivor 3 — coincidental result

m5, the wrap counter inverted. As the stimulus stood, two wrapping cases out of four evaluations is exactly half, so an inverted counter reaches the same total. A fifth evaluation breaks the split and the mutation dies.

This is a survivor of the stimulus, not of the checker. The checker was correct and the arithmetic conspired against it.

Survivor 4 — stimulus gap, abuse case

m7, the issue guard: !pend_q dropped. The separating question has an answer — issuing a fresh request while a retry is still pending would overwrite the held payload, and the retry would then replay the new request instead of the one that was nacked. It survived because the testbench never issued while a retry was pending.

This is the same shape as three of 30.1's survivors: the mechanism was exercised thoroughly under legal traffic, and nothing asked what happens when the requester does something the protocol forbids while the design is mid-recovery.

The campaign-validity incident

The first attempt at survivor 4's abuse case broke the baseline. The intruding issue was driven after the one-cycle pending window had already closed, so the design legitimately accepted it, and four assertions failed.

The mutation run against that broken baseline reported 45 of 45 killed.

It was not 45 of 45. A failing baseline fails every mutation for the same reason it fails unmutated, and every one is recorded as a kill it did not earn.

The number that comes back looks better than the correct one. That is what makes this the hardest campaign error to notice: a rising kill rate reads as progress. A higher kill count is not automatically stronger evidence — it is stronger evidence only if the baseline it was measured against was green.

The stimulus was corrected — the pending window is exactly one cycle wide, and the intrusion has to land inside it with no intervening clock edge — the baseline returned to green, and only then was the campaign re-run.

Mutation testing is invalid unless the unmutated baseline passes first. This is now a permanent rule for every chapter in this track, and it is the single most important process finding of the batch.

Survivor classification comes before any fix

Never add an assertion for a survivor before classifying it. The classes, and what each one demands:

ClassMeans, and what to do
Equivalentno input tells the two apart — withdraw it, never count a kill
Stimulus gapthe case is never driven — extend the stimulus
Missing checkerthe case is driven and nothing looks — add the checker
Vacuous checkerthe check cannot fail — fix the check, not the design
Unreachableits guard never holds — fix the guard
Maskedanother mechanism hides it — expose it, or say why you cannot
Coincidentalthe arithmetic happens to agree — change the stimulus
Missing configthe build that differs is never built — instantiate it
Otheranything else — state it precisely

A survivor is a question about the environment, not a hole to be filled. Adding an assertion first destroys the evidence that would have told you which class it was.

The tooling finding

Survivor 3 should have been caught in advance. splitcheck located counter pairs by name suffix, and this chapter named its counters …n and …nw, so the tool matched nothing and reported a confident zero — the third such silent failure in three chapters.

It has been rewritten to derive pairs from the model source rather than from names: counters incremented at the shallowest level inside an event guard are totals, counters incremented under a further condition are sub-counts, and both are mapped to testbench nets through the module's own instantiations. Name-independent.

A first attempt over-corrected, pairing every asserted integer, and produced 16 hits here and 38 on another chapter — unusable. The corrected version flags exactly the two real splits in this chapter and zero on six others.

One scare had to be disproved properly. An intermediate version flagged three pairs in 30.1, which is already published. Rather than argue they were false positives, all five corresponding counter mutations were injected into 30.1 and every one died — confirming that testbench distinguishes them and that 30.1 needs no change. The tool was then tightened so it no longer misreads a counter inside a multi-line guarded block as unconditional.

The general lesson belongs to 30.3: a checker that infers its subject from a naming convention will keep failing this way, and a structural check that reports zero must be independently confirmed to have actually read its input.

19. Baseline Defects Found Before Mutation

RTL defect 1 — a pulse-qualified output read as a held one

Symptom. After the first grant, granted_tag reported 1 rather than the 0 it had just handed out.

Root cause. granted_tag was a combinational view of the allocator's next-tag pointer, which advances on the grant. It is meaningful only in the cycle grant_valid is high. A consumer that samples it one cycle later — the natural thing to do after seeing a pulse — receives the next tag, not the granted one.

Fix. Keep the pulse-qualified output and add a registered last_granted that holds the tag actually granted until the next grant. Both are now published and both are asserted, and the distinction became a review point in its own right — section 8.

Why it matters. This is not an arithmetic error. Both outputs are individually correct. The defect is an interface contract that was never written down, and the failure mode is a transaction tagged with an identity the allocator never issued for it.

RTL defect 2 — a free and a grant of the same identity, and the free won

Symptom. None, under the stimulus as it stood. The case was never driven.

Root cause. The in-use vector was written by two independent statements — a bit-set under the grant and a bit-clear under the free:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if (grant_valid) use_q[next_q]   <= 1'b1;
...
if (free_req)    use_q[free_tag] <= 1'b0;

Both are correct alone, and they name different indices — until the indices are equal. A free arriving in the same cycle as a grant for that same tag silently un-marked a tag that had just been handed out, so a later wrap could reissue it. That is the exact safety hole this model exists to rule out, sitting in the build that is supposed to be correct.

Fix. One assignment computed from both events, with the priority written down: the grant wins, because a requester cannot have finished a transaction it is being handed in this very cycle.

Verification. The simultaneous case is now driven and asserted, and a mutation that swaps the priority is killed.

How it was found. Not by the stimulus, not by the campaign, and not by any of the structural gates. It was found by a static sweep of every model in the batch for a register written by two independent top-level statements — the exact shape this batch teaches — run as part of the final adversarial review. Five of the thirty models in the batch had it, and in every one the simultaneous same-target case was undriven and the priority unwritten.

The general lesson. A bit-set and a bit-clear on the same vector look independent because they usually name different indices. They stop being independent the moment the two indices are equal, and nothing in the source says whether that can happen. The rule from 30.2 section 7 — one assignment computed from all events, or a written priority — applies to indexed writes exactly as it does to whole registers.

RTL defect 3 — an uninitialised counter is X, not stale

Symptom. The check harness reported X/Z in result on the partial-reset build's occupancy counter, twice.

Root cause. The weak build's reset skipped the counter entirely, so it was never assigned at power-on and stayed X for the whole run. The model had been written to demonstrate a stale counter and actually demonstrated an unknown one.

Fix. Reset now clears both the table and the counter in both builds, and the divergence is introduced by a flush instead — a realistic operation that empties storage and forgets to clear the accounting. The weak build now carries a defined, plausible, wrong value.

Found by: the X/Z rejection in chkv. A checker written as a plain equality would have compared X against 3, failed, and been "fixed" by adjusting the expected number — hiding the real finding behind a green test.

Testbench defects — three, all sampling

WhereFault
identity, first-grant checksampled a pulse-qualified output after the pulse — the inverse of RTL defect 1
identity, weak-build checkthe same mistake again, on the other build
combinational, monitor checkno clock edge occurred inside the stale window, so the continuous monitor never observed the violation

The third is the subtle one. The monitor was correct, the sticky bit was correct, and the window it was meant to observe contained no posedge — so nothing was ever latched. A sticky monitor is only as good as the clock inside the window it watches.

Wrong oracle — one

The operation count in section 13 was traced over #1 steps rather than over clock edges: three posedges occurred with op_valid high, not four. Retraced by hand to 3 operations, 2 taken, 1 skipped, and the design was right.

Compiler-warning findings

Under -Wall the ten models produce no truncation, select-width, signedness, cast, multiplication-width, shift-width or loop-width warnings. The only warning is a missing timescale on models containing no delay constructs at all — inspected and recorded as benign rather than silenced, because adding a simulation directive to published teaching RTL would be noise in the lesson.

The important result here is a negative one, and it is evidence rather than an absence of evidence. The width model deliberately contains a 16 × 16 multiply assigned to a 16-bit net, and Icarus does not warn on it. That silence was confirmed by running the tool against the construct, not assumed. Treat compiler warnings as verification evidence and compiler silence as nothing at all.

Simulator constraints

Icarus Verilog 13.0 rejects ref task arguments, carried forward from 29.5 and 30.1. The check harness passes values rather than references as a result, which is why chkv takes a 16-bit argument and wider quantities are checked in halves.

20. Synthesis And Implementation Reality

The acceptance gate costs one AND gate. valid && ready instead of valid is a two-input gate on a counter enable, off the critical path in every design where the counter is not itself the bottleneck. The robust build is not more expensive than the broken one, which removes the only argument that is ever made for the broken one.

The default arm costs nothing in a fully-specified encoding and a small amount otherwise. With four states in three bits the synthesiser must decode four undefined encodings to route them home; with a one-hot encoding the same recovery is a popcount comparison. Neither cost is a reason to omit it, and the omission's cost is a block that never comes back.

The single-assignment form is usually cheaper than the two-assignment form, which surprises people. Two sequential if statements synthesise to a priority mux; the case on the concatenated events synthesises to a parallel one. The robust form is the faster form here, and the broken form's only advantage is that it reads more naturally line by line.

The in-use vector is one bit per identity and the collision check is an indexed read. For the illustrative 75-tag allocator that is 75 flops plus a 7-to-1 indexed read, against a wrapping allocator's 7 flops. The 75 flops are the mechanism, and they buy the uniqueness invariant outright.

The held identity is ceil(log2(N)) flops — 7 for that allocator — and it buys the lifetime invariant. It is the cheapest correctness mechanism anywhere in this chapter.

Width discipline is not free and is nearly free. A 16 × 16 multiply produces a 32-bit result whether the source says so or not; the saturating build adds a 32-bit comparator and a 16-bit mux. The truncating build saves the comparator and the mux and loses the answer.

The consistency check between accounting and storage costs a popcount. For a 4-entry table that is three half-adders; for a 64-entry table it is a small adder tree, and it sits outside the datapath so it costs area rather than timing. This is one of the few self-checks worth leaving in production silicon, because it catches an entire class of recovery-path defects with one output bit.

The retry hold register is as wide as the payload. For an 8-bit payload that is 8 flops; for a real request it is the full request width, which can be substantial. That cost is the usual reason the weak build exists — somebody decided the bus would still be holding the request, and for most of the design's life it is.

Latch inference is the one item here with no area trade-off at all. The else costs nothing. What it costs to omit is a latch with real setup and hold requirements in a path the timing constraints assume is combinational.

No area, frequency or power figures appear in this chapter, because none was measured.

21. Silicon Observability

TelemetryWhat it exposes
accepted transactions and stall cycles, published separatelya counter gated on valid reports a stall count inconsistent with its own transaction count
illegal-state seen, stickya transient that put the design in a non-state — must read permanently zero
illegal-state recoverieshow often it happened; the only evidence the sticky bit ever fired more than once
simultaneous push-and-pop countwhether the simultaneous case was ever exercised in the field
occupancy against an independent popcountthe accounting-versus-storage divergence, evaluable in hardware
tag reuse detecteda safety escape — must read permanently zero
free-list high-water markhow close the identity space came to exhaustion, not where it sits
saturations on arithmeticthe design telling you it hit a limit; the wrapping build cannot populate this
retries per transaction, as a distributionlivelock, which the retry total cannot distinguish from a busy fabric
budget exhaustion counttransactions abandoned to the retry limit
configuration report registerwhich behavioural build is actually running

Three counters here are "must be permanently zero" counters — illegal-state seen, tag reuse detected, and payload-corruption on replay. Each costs almost nothing, never fires in a correct design, and captures a safety escape that would otherwise present as data corruption far from its cause.

The pattern to read on the handshake pair: a transaction count that exceeds the accepted count by roughly the stall count is the section 5 defect, visible from the outside with no access to the RTL. The two numbers being consistent is the check, and either number alone proves nothing.

A saturation counter that reads zero can mean two things — the design never reached the limit, or the design wraps and cannot count. Distinguishing them requires knowing which build is running, which is section 13's whole point.

Retries per transaction as a total is the wrong statistic. A thousand retries over a thousand transactions is a congested fabric. A thousand retries on one transaction is a livelock. The total is identical and the two systems are not.

The accounting-versus-storage comparison is the one to keep if only one survives area review. It is a popcount and a comparator, it is valid continuously, and it catches every defect in section 10's class — flush, reset, error recovery and mode change — with a single bit.

22. DebugLabs

Lab 1 — Throughput is half the model and the transaction counter agrees with the model

Symptom. A block is specified at one transaction per two cycles and measures one per four in the lab. Its own transaction counter reports the expected rate.

Evidence. The counter's rate matches the specification; the observed data rate does not. The interface is visibly backpressured on the analyser.

Hypothesis. The counter is not counting what the data path is doing.

Investigation. Compare the transaction counter against bytes moved divided by transaction size. They disagree by a factor that tracks the measured backpressure.

Root cause. The counter is gated on valid alone. Every stall cycle is counted as a transaction, so the counter reports the rate the producer attempted rather than the rate the interface achieved.

Fix. Gate the counter on valid && ready.

Prevention. A review item that reads every counter's enable condition, and a DV check that drives one transaction against N stall cycles and requires a count of exactly one.

Silicon observability. Publish accepted and stalled separately. Their inconsistency is the signature, and it is readable without any access to the design.

Lab 2 — A block stops responding after an ESD event and its state register reads 6

Symptom. One instance in a hundred stops responding after an electrical event. It does not hang the system; it simply never asserts anything again.

Evidence. A debugger read of the state register returns 6. The design's documentation lists four states, 0 through 3.

Hypothesis. The design is in an encoding that is not a state, and has no transition out of it.

Investigation. Check the case statement for a default arm. There is none. Every evaluation falls through and the register holds.

Root cause. A three-bit register admits eight encodings for four states. A transient put it in one of the four undefined ones and nothing routes it home.

Fix. A default arm returning to IDLE, plus a sticky illegal-state bit and a recovery counter.

Prevention. Force the state register to every undefined encoding in a directed test and require a legal state within one cycle. No amount of random input stimulus reaches this, because the failure is not driven by an input.

Silicon observability. The sticky bit and the recovery counter. Without them the only evidence is a debugger read of a register in a part that has already failed.

Lab 3 — A FIFO reports full while empty, weeks into deployment

Symptom. A queue asserts full with no entries in it. A reset clears the condition and it returns after days.

Evidence. The occupancy counter reads maximum; a scan of the storage shows nothing valid.

Hypothesis. The counter and the storage have separated.

Investigation. Correlate the drift against traffic. It advances by one every time push and pop coincide.

Root cause. Two separate if statements assign the occupancy register. On a simultaneous push and pop the second scheduled assignment wins and the push is lost.

Fix. One assignment computed from both events.

Prevention. A DV check that drives push and pop together and requires occupancy unchanged — and a review rule that every register assigned more than once in a clocked block is a finding until explained.

Silicon observability. Count simultaneous events explicitly. A design that has never seen one has not proved the case correct.

Lab 4 — Data is returned to the wrong requester under load

Symptom. Under sustained load, responses are occasionally delivered to a requester that did not issue them. Light load is clean.

Evidence. Both transactions carry the same tag. The tag allocator reports no error.

Hypothesis. An identity was allocated while still outstanding.

Investigation. Instrument the allocator's in-use vector. Under load it reaches all-ones and the next allocation proceeds anyway.

Root cause. The allocator increments a pointer and hands out whatever it lands on. It never checks the in-use bit, so at wrap it reissues a live identity.

Fix. Allocate from the free set and refuse when it is empty. A refusal is backpressure; a collision is corruption.

Prevention. A tag-reuse assertion that must never fire, and a test that fills the identity space and requests one more.

Silicon observability. A tag-reuse counter that must read permanently zero, and a free-list high-water mark. The high-water mark is the early warning; the reuse counter is the escape.

Lab 5 — A buffer is allocated a quarter of the size it needs and nothing overflowed

Symptom. A size computation returns 256 for an object that is manifestly larger. No overflow is reported anywhere.

Evidence. The inputs to the computation are 256 and 257. The result register is 16 bits wide.

Hypothesis. The product wrapped.

Investigation. 256 × 257 = 65,792, which is 65,536 + 256. The 16-bit register holds 256.

Root cause. A 16 × 16 multiply assigned to a 16-bit net. No compiler warning was issued and none was expected once the tool was checked.

Fix. A 32-bit intermediate and an explicit saturate-or-error policy on the narrowing.

Prevention. A review rule requiring three numbers for every arithmetic expression — operand widths, evaluation-context width, destination width — and boundary tests at the limit and one step past it. The limit itself agrees in both builds, so a test that drives only the limit reports success.

Silicon observability. A saturation counter. A wrapping design cannot populate one, which is itself the diagnosis.

Lab 6 — Capacity shrinks by a quarter after every error recovery

Symptom. A structure that holds four entries holds three after the first error-recovery event, two after the second, and hangs after the fourth.

Evidence. The occupancy counter reads 3 immediately after a recovery in which the table was emptied.

Hypothesis. Recovery clears the storage and not the accounting.

Investigation. Read the flush branch. It assigns the table and not the counter.

Root cause. A partial recovery path. Every register is correctly initialised at reset, which is why this survives power-on testing entirely.

Fix. Clear both in the recovery branch, and add a continuous consistency assertion.

Prevention. For every recovery path, enumerate the state it clears against the state that exists. The gap is the finding, and it is found by reading, not by testing.

Silicon observability. Publish the counter and an independent popcount and compare them in hardware. One bit catches the entire class.

Lab 7 — A responder completes a transaction it never received

Symptom. A completion arrives for a request the responder has no record of. The original request is never completed.

Evidence. The retry counter is correct. The payload on the replay is not the payload that was nacked.

Hypothesis. The replay is reading the wrong source.

Investigation. Capture the input bus at the nack. It holds a different request, and that is the request being replayed.

Root cause. The replay re-reads the input bus instead of a held register. The original was never captured.

Fix. Capture the request into a retry register on issue and replay from that register.

Prevention. Issue, change the bus, then nack — in that order. A test that holds the bus steady across the nack cannot find this, and holding it steady is what a directed test naturally does.

Silicon observability. A payload-comparison bit on replay, which must read permanently zero. The retry counter is correct in the failing design, so checking it is checking the wrong thing.

Lab 8 — A path fails timing and the fix is a constraint

Symptom. A combinational path misses timing. A multicycle constraint is applied, timing closes, and the block behaves incorrectly in a corner nobody tests.

Evidence. The synthesis report lists an inferred latch on that path. Nobody read the list.

Hypothesis. The path is not combinational.

Investigation. The always @* block assigns the output under two conditions and not on the fall-through path. With neither select asserted the output holds — and then moves when a deselected input changes.

Root cause. A missing else. In simulation the block looks like an idle design; in synthesis it is a latch with a real enable.

Fix. Assign a default first, refine after.

Prevention. Read the latch-inference list and justify every entry. This defect has no silicon telemetry at all, which makes the synthesis report the only review artefact that can catch it.

Silicon observability. None. That is the finding, and it is why the review gate is the synthesis report rather than a counter.

23. The Review, As A Working Checklist

AskAccept only
What is this counter's enable condition?valid && ready, or a stated reason it is not
Which encodings are not states?an enumeration, and a default arm that handles them
Which transitions does the table permit that the architecture forbids?a monitor beside the table, not an assurance
How many assignments reach this register in one cycle?one, or an explanation of which wins and why that is right
What happens when both these events fire together?a driven test, not an argument
Can this identity be allocated while live?a free-set check and a refusal path
When is this output valid?a named cycle window, in writing
Is this output pulse-qualified or held?one of the two, stated, with the other provided if consumers need it
What are the three widths in this expression?three numbers
Did the tool warn?evidence the tool would warn on this construct
What does this recovery path clear?the complete list, checked against the complete state
Where does the replay read from?a held register, never a module input
Is there an else on every path in this block?the latch-inference list, read
What observable changes with this parameter?a named output or counter

Every row is a question with a wrong answer that sounds fine. "It counts transactions", "the encoding can't happen", "those never fire together", "the bus is still holding it", "the tool didn't complain" — each of those is a sentence that ends a review, and each of them is in this chapter as a defect.

24. How This Appears In Real Engineering

The handshake defect is the most common RTL review finding there is, and it survives because it is correct on an unloaded interface, which is the interface most unit tests present.

The missing default survives lint in many flows, because lint rules about full case statements are routinely waived on state machines with fully-enumerated encodings — and "fully enumerated" means every state is listed, not every encoding.

The double-assignment defect is written by experienced engineers deliberately, because two separate if statements express two independent concerns cleanly and the interaction is invisible until the two concerns coincide.

The identity-lifetime defect is a documentation failure that presents as an RTL failure. Two teams, each correct, integrate two modules and produce corruption. The reviewer who catches it is the one who asks when an output is valid rather than what it computes.

The width defect is where "no warnings" does the most damage. A design that has been linted, synthesised and simulated with no diagnostics has been examined by three tools, none of which was asked this question.

The partial-recovery defect appears late, because recovery paths are exercised by error injection and error injection is usually the last thing written and the first thing cut.

The retry-source defect is a race with the rest of the system, so it reproduces under load and not in a directed test, and the directed test is the one that gets written.

The latch defect reaches timing closure, where it is diagnosed as a timing problem, and the fix applied is a constraint rather than an else.

The telemetry defect is never diagnosed at all. It produces a fleet nobody can partition and bugs nobody can localise, and it is attributed to the bugs rather than to the parameter.

25. Common Misconceptions

"The counter counts transactions." It counts whatever its enable condition is true for. Read the condition.

"That encoding can't happen." It cannot be reached by a transition. It can be reached by an upset, a bad reset release, a CDC escape or a scan artefact, and the default arm costs nothing.

"Those two events never fire together." Then the assertion that says so is free. An event that cannot happen and an event nobody tested look identical in a regression report.

"The compiler would have warned." The most expensive width defect in this chapter produces no diagnostic under -Wall. Compiler silence proves nothing about width, signedness, casts or truncation. Confirm the tool warns on the construct before treating its silence as evidence.

"Reset clears everything." Reset clears what the reset branch assigns. Flush, error recovery and mode change are separate branches with separate omissions, and reset being complete is exactly what makes the others survive power-on testing.

"An uninitialised counter is the same bug as a stale one." An X propagates and is caught by almost anything. A defined, plausible, wrong value survives inspection. They have different detectability and the harder one is the one that ships.

"The mutation score went up, so the testbench improved." Not if the baseline was failing. A failing baseline kills every mutation for free, and the number it returns is higher than the honest one.

"A survivor means we need another assertion." A survivor means you do not yet know why it survived. Classify first. An equivalent mutant is withdrawn, and an assertion added to kill it is an assertion that proves nothing.

"The retry counter is right, so the retry is right." The count is right and the payload is wrong. Check the thing that can be wrong, not the thing that is easy to check.

"It simulates correctly." Simulation holds an unassigned variable. Synthesis builds a latch. They are not the same memory, and the second one has timing.

26. Interview And Design-Review Questions

RTL and microarchitecture

1. What is the difference between a request being presented and being accepted? Presented is valid high. Accepted is valid && ready high. A counter gated on the first counts every stall cycle as a transaction.

2. A counter reports four transactions and one occurred. What do you check first? Its enable condition, then the stall depth. The error is the stall count plus one.

3. Why does the handshake defect not appear in unit test? With ready tied high the two builds are identical. The defect's magnitude is a function of backpressure, and a unit test usually has none.

4. A three-bit register holds four states. How many encodings are undefined? Four. Every one of them needs a route home, and no transition targets any of them.

5. What does a case without a default do in simulation versus synthesis? In simulation the register holds. In synthesis it may become a latch or an unreachable trap. The two behaviours differ, which is why simulation cannot clear this finding.

6. Why can constrained-random stimulus not reach an illegal state encoding? Random stimulus drives inputs. Reaching a non-state requires corrupting the register directly, which no input does.

7. Name two distinct failures that live in one transition table. Reachability — encodings with no route home. Permission — transitions the table allows and the architecture forbids. The second is legal Verilog and needs a monitor.

8. Two if statements assign one register. What happens when both fire? The last in source order wins; the other is discarded silently.

9. Is the single-assignment form more expensive? Usually less. Two sequential if statements synthesise to a priority mux; a case on the concatenated events synthesises to a parallel one.

10. Occupancy reads 2 after a simultaneous push and pop against a true value of 3. What happens next? Three pops drain it to zero while an entry remains. The counter and the storage have separated, and the design will later report full while empty.

11. What invariant does a tag allocator owe? No identity is issued while its previous holder is outstanding. Refusal when the space is exhausted is correct behaviour; wrapping is corruption.

12. What is the cost of the uniqueness mechanism? One in-use bit per identity plus an indexed read. For 75 tags, 75 flops. Nothing a synthesiser would notice.

13. What is a pulse-qualified output? One that is meaningful only while its qualifier is asserted. Sampling it outside that window returns a different, and usually valid-looking, value.

14. Give the five questions you ask about every published value. When is it valid; what qualifies it; pulse-qualified or held; can a consumer legally sample later; what transition invalidates it.

15. Both outputs of the allocator are correct and one of them corrupts. Explain. granted_tag is correct during the pulse and last_granted is correct after it. The defect is an undocumented temporal contract, not an arithmetic error.

16. How wide must the product of two 16-bit values be? 32 bits. Assigned to 16 it truncates modulo 65,536.

17. 256 × 257 truncated to 16 bits gives what, and why is that the dangerous answer? 256. Because it is a power of two and an entirely ordinary value in most designs, so it propagates instead of being noticed.

18. Why is a saturating build safer than a wrapping one even though both are wrong? A saturating build reports its limit, which is visibly a limit. A wrapping build reports a plausible number. One of them can populate a counter.

19. Does explicit zero-extension in a 32-bit assignment context change anything? Not in that expression — the context already extends both operands. It changes everything the moment the result is assigned somewhere narrower, which is why it is still correct practice.

20. A flush empties a table and the counter reads 3. What has the design lost? Three quarters of a four-entry structure, permanently, with no error and no indication.

21. Why does the partial-flush defect survive power-on testing? Reset is complete in both builds. The divergence is introduced by a path that power-on testing never runs.

22. What must a replay replay? The original payload, from a held register. Reading the input bus replays whatever is on the bus at the moment of the nack.

23. Name the three properties a retry mechanism owes. Fidelity, exactly-once, and boundedness. They fail as corruption, double accounting and livelock respectively.

24. What does an always_comb block with an if and no else produce? A held value in simulation and a latch in synthesis. The output becomes a function of input history rather than of current inputs.

25. What is the tell that a stale combinational output is a latch and not a coincidence? Change a deselected input and watch the output move. A coincidentally-correct value does not respond to an input it does not select.

26. Why must a behavioural parameter move telemetry? Otherwise the running configuration is unknowable from outside, and two teams can hold opposite beliefs about which build is deployed with the same evidence.

Verification

27. What single condition makes a mutation campaign invalid? A failing baseline. Every mutation then fails for the same reason it fails unmutated, and every failure is recorded as a kill.

28. A campaign against a broken baseline returned 45 of 45 and the correct one returns fewer. Which is stronger evidence? The smaller number. A higher kill count is stronger evidence only if the baseline was green.

29. Why is that error hard to catch? Because the wrong number looks better. A rising kill rate reads as progress.

30. A mutant survives. What is the first thing you do? Classify it. Adding an assertion first destroys the evidence that would have told you which class it was.

31. Name six survivor classes. Equivalent, stimulus gap, missing checker, vacuous checker, unreachable checker, masked, coincidental result, missing configuration. Equivalent mutants are withdrawn, never counted.

32. A mutation narrows an operand in a 32-bit assignment context and nothing changes. Which class? Equivalent — the context zero-extends it regardless. Withdrawn, not counted as a kill.

33. Two of four evaluations wrap, and an inverted wrap counter reaches the same total. Which class? Coincidental result. The fix is a fifth evaluation, not an assertion.

34. Your testbench never issues while a retry is pending, and a guard mutation survives. Which class? Stimulus gap — specifically an abuse case, where the requester violates the protocol during recovery.

35. Why did chkv's X/Z rejection matter more than any assertion in this chapter? It reported "X/Z in result" instead of a value mismatch. A plain equality would have compared X against 3 and been fixed by changing the expected number, hiding the real finding.

36. Your sticky monitor never latched and the violation occurred. What is the most likely cause? No clock edge inside the window it watches. The monitor was right and had nothing to sample.

37. Why must a safety check sit outside any conditional? Because a check nested inside if (pulse) cannot fail when the defect is that the pulse never fires.

38. What is a configuration-contrast test and why is it usually missing? Instantiating both configurations in one testbench and asserting the observable differs. Most environments build the alternative and run the same regression against it without comparing the two.

39. A structural checking tool reports zero. What must you confirm? That it actually read its input. This chapter's tool matched nothing because of a net-name convention and reported a confident zero — the third such failure in three chapters.

40. You suspect a tool's findings against already-published work are false positives. How do you settle it? Inject the corresponding mutations and watch them die. Arguing is cheaper and proves nothing.

41. Which boundary value in the width model reports both builds correct? Exactly 65,535. The boundary is the case that agrees, so a test at the boundary alone reports success.

Synthesis and silicon

42. Which defect in this chapter has no silicon telemetry? The inferred latch. That is why its review gate is the synthesis report and why an unread latch-inference list is a review that did not happen.

43. Which counters must read permanently zero? Illegal-state seen, tag reuse detected, and payload corruption on replay. Each is nearly free and each captures a safety escape that presents far from its cause.

44. A saturation counter reads zero. What are the two explanations? The limit was never reached, or the design wraps and cannot count. Distinguishing them requires knowing which build is running.

45. Why is retries-per-transaction better telemetry than total retries? A thousand retries over a thousand transactions is congestion; a thousand on one is livelock. The total cannot tell them apart.

46. If you could keep three counters from this chapter, which? The accepted-versus-stalled pair, the occupancy-versus-popcount comparison, and the tag-reuse counter. The first is readable from outside the design, the second catches an entire class of recovery defects with one bit, and the third catches the silent corruption.

27. Exercises

1 — RTL review. You are handed a module whose specification says "counts completed transactions" and whose counter is enabled by resp_valid. Write the review finding: name the invariant at risk, the exact condition you would require instead, the stimulus that falsifies the current code, and the telemetry pair that would expose it in silicon.

2 — Quantitative. A design multiplies a 12-bit length by a 10-bit count and stores the result in a 16-bit register. Compute the maximum true product, state whether it fits, find the smallest pair of operands that wraps, and give the value the register reports for that pair. Then state what the saturating build would report and why that is the safer wrong answer.

3 — Checker design. Write the continuous assertion that would have caught the partial-flush defect in section 10, and state explicitly why it must be evaluated every cycle rather than at the end of the test.

4 — Mutation classification. A mutation changes occ_q <= (occ_q == 0) ? 0 : occ_q - 1 to occ_q <= occ_q - 1 and survives. Give the separating question, classify the survivor, and state the change you would make — to the stimulus, the checker, or neither.

5 — Waveform diagnosis. Using Figure 2, state what a consumer latching on the cycle after grant_valid captures, what the allocator believes it granted, and the single cycle in which the two beliefs diverge. Then state what the consumer should have latched instead and how many flops that costs.

6 — Interface contract. Take any module you have written and write down, for every output, the cycle window in which it is meaningful and whether it is pulse-qualified or held. Identify every output for which you cannot answer without reading the implementation — each one is a review finding.

7 — Coverage planning. Define a functional coverage model for the retry mechanism that would find the replay-from-bus defect without anybody suspecting it. Specify the bins, the cross, and identify the bin the failing design can still hit (so it is not the tell) and the bin it cannot.

8 — Campaign validity. You inherit a regression reporting a 100 percent mutation kill rate. Describe, in order, the three checks you would run to establish whether that number means anything, and state what each one would look like if the campaign were invalid.

28. Summary

A counter gated on presentation counts the stall depth plus one, and the error grows with congestion and vanishes in a nominal test.

Every encoding needs a way home — a three-bit register holds four encodings the design never names, and no input drives it there.

One register, one assignment per cycle, or the last one in source order wins and the other is discarded silently.

An identity must be unique while it is live, and it must have a stated lifetime. Both outputs of a correct allocator are correct, and one of them corrupts if the consumer reads it a cycle late.

A value can be logically correct and have an unsafe temporal interface contract. Ask when it is valid, what qualifies it, whether it is pulse-qualified or held, whether a consumer may sample it later, and what invalidates it.

Compiler silence is not width proof. It proves nothing about expression width, intermediate width, destination width, signedness, cast correctness or truncation — and the most expensive defect here produces no diagnostic at all.

Clearing the storage is not clearing the accounting, and a defined, plausible, stale counter is a harder failure than an X because it survives inspection.

A replay must replay the original, from a held register, because by the time the nack arrives the bus holds something else.

A combinational block must assign on every path, or the output is a function of input history and the synthesiser builds a latch nobody constrained.

A behavioural parameter that moves no telemetry cannot be verified, confirmed or debugged, and produces a fleet nobody can partition.

Mutation testing is invalid unless the unmutated baseline passes first — a failing baseline reported 45 of 45 here, a better number than the honest campaign returned.

Six conditions, and "the RTL was reviewed" is one of them. A real review with one finding open is 83 percent. A meeting and nothing else is 16.

Continue learning

Standards & specifications

Governing standard
CXL Specification (CXL Consortium)(opens CXL Consortium in a new tab)

Defines CXL.io, CXL.cache and CXL.mem, and the coherence and memory-pooling behaviour built on them. System design and deployment topology are not mandated.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the CXL curriculum.