Part 4 · Assertions (SVA) · Intermediate

Data Integrity Drills

Delayed-equality pipeline checks with $past, FIFO in/out matching with local variables, tagged request/response matching, and write-readback consistency.

Drill 1 — output equals input delayed N cycles

Problem

Interviewer: 'A 3-stage pipeline: dout must equal din from 3 cycles earlier. First write the free-running version, then the version where data only moves when valid is high.'

Think it through

  • Free-running: a plain invariant with $past(din, 3). Choose WHERE to stand: stand at the output and look back. Standing at the input (din |-> ##3 ...) needs a local variable to carry the value forward — more machinery for the same check.

  • $past(din, 3) is unknown/garbage for the first 3 cycles after reset — gate the check so early attempts don't fire (here: a cycle counter or simply riding on disable iff if reset is long enough; the explicit gate is the defensible answer).

  • Valid-qualified: $past takes a GATING expression as its 4th argument — $past(din, 3, , valid) means 'din three VALID-cycles ago'. This is the senior answer; juniors try to hand-build shift registers.

systemverilog
// Free-running pipeline, with startup gate
logic [1:0] warm;
always_ff @(posedge clk or negedge rst_n)
  if (!rst_n) warm <= '0;
  else if (warm != 3) warm <= warm + 1;

a_pipe3: assert property (
  @(posedge clk) disable iff (!rst_n)
  (warm == 3) |-> (dout == $past(din, 3)));

// Valid-gated pipeline: compare against the 3rd-last VALID din,
// and only when the output side presents a valid beat
a_pipe3_valid: assert property (
  @(posedge clk) disable iff (!rst_n)
  vld_out |-> (dout == $past(din, 3, , vld_in)));
diagram
PASS trace (free-running, N=3)      FAIL trace
cycle :  0   1   2   3   4   5      cycle :  0   1   2   3   4   5
din   :  A   B   C   D   E   F      din   :  A   B   C   D   E   F
dout  :  -   -   -   A   B   C      dout  :  -   -   -   A   X7  C

At 3: dout==A==din@0  PASS         At 4: dout==X7, $past(din,3)
At 4: dout==B==din@1  PASS         is din@1==B  FAIL at cycle 4.
(warm gate suppresses cycles 0-2)   Corruption localized to the beat.

Common wrong answers

  • din_changed |-> ##3 (dout == din) — by the time the consequent samples, din holds the CURRENT value, not the value from 3 cycles ago. You must either look back with $past or capture forward with a local variable.

  • No startup gate — first cycles compare against X/stale history; on 4-state simulation, X == X comparisons can even pass silently and hide the gap.

  • Hand-rolling a shift register for the valid-gated case without knowing $past's gating argument — works, but signals unfamiliarity with the tool.


Drill 2 — FIFO in/out data matching with local variables

Problem

Interviewer: 'Data written into a FIFO must come out unchanged and in order. Show me the local-variable assertion for the simple case, and tell me exactly where it stops working.'

Think it through

  • Local variable captures wr_data at the write; the consequent waits for the matching read and compares. The capture syntax (expr, lv = sig) attaches the assignment to a sequence point.

  • Which read matches? For in-order single-entry traffic, 'the first read after my write' — rd_en[->1].

  • The limit: with multiple writes in flight, EACH write's attempt waits for the FIRST subsequent read — two writes can both compare against the same read. Local variables don't queue. State this limit unprompted; it is the real test.

systemverilog
// Valid while at most ONE entry is in flight
property p_fifo_d1;
  logic [7:0] d;
  @(posedge clk) disable iff (!rst_n)
  (wr_en, d = wr_data) |=> rd_en[->1] ##0 (rd_data == d);
endproperty
a_fifo_d1: assert property (p_fifo_d1);
diagram
PASS trace                          WHERE IT BREAKS (2 in flight)
cycle  :  0  1  2  3  4             cycle  :  0  1  2  3  4
wr_en  :  0  1  0  0  0             wr_en  :  0  1  1  0  0
wr_data:  -  A  -  -  -             wr_data:  -  A  B  -  -
rd_en  :  0  0  0  1  0             rd_en  :  0  0  0  1  1
rd_data:  -  -  -  A  -             rd_data:  -  -  -  A  B

write A @1, read A @3  PASS        Attempt(A): first read @3  A 
                                    Attempt(B): first read @3 too!
                                    compares B vs A  FALSE FAIL.
                                    Local vars don't queue.

Variation — the scalable fix: auxiliary model

systemverilog
// Reference queue beside the assertions — handles any depth
logic [7:0] model_q [$];
always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n) model_q.delete();
  else begin
    if (wr_en) model_q.push_back(wr_data);
    if (rd_en) begin
      a_order: assert (model_q.size() > 0 && rd_data == model_q[0])
        else $error("FIFO data/order mismatch");
      void'(model_q.pop_front());
    end
  end
end

Common wrong answers

  • Claiming the local-variable property handles a deep FIFO — the multi-attempt aliasing above is the classic trap; the interviewer is waiting for you to find it.

  • rd_en ##0 rd_data == d without [->1] — only checks a read in the very next cycle; gaps fail.

  • Comparing with |=> rd_data == d (no read qualifier) — compares against bus garbage on non-read cycles.


Drill 3 — tagged request/response matching

Problem

Interviewer: 'Requests carry a 4-bit tag; responses return the tag. Every request must get a response with ITS tag within 20 cycles. Out-of-order responses are allowed. Why do local variables work here when they failed for the FIFO?'

Think it through

  • Each $rose-of-request attempt captures ITS OWN tag into a private local variable — per-attempt privacy, which broke the FIFO drill, is exactly what makes tag matching work.

  • The consequent waits for a response carrying THAT tag: (rsp_vld && rsp_tag == t)[->1], bounded to 20 cycles with intersect or a ranged delay.

  • Out-of-order is free: each attempt only watches for its own tag and ignores others.

systemverilog
property p_tag_match;
  logic [3:0] t;
  @(posedge clk) disable iff (!rst_n)
  (req_vld, t = req_tag) |->
    ##[1:20] (rsp_vld && (rsp_tag == t));
endproperty
a_tag_match: assert property (p_tag_match);

// companion: no response with a tag nobody requested needs
// an aux scoreboard of live tags (bit [15:0] live; set on req,
// clear on rsp) — pure SVA cannot express 'no unmatched rsp'.
diagram
PASS trace (out-of-order)
cycle   :  0  1  2  3  4  5
req_vld :  0  1  1  0  0  0
req_tag :  -  5  9  -  -  -
rsp_vld :  0  0  0  1  1  0
rsp_tag :  -  -  -  9  5  -

Attempt(tag5)@1: sees rsp tag 9 @3 (ignored), tag 5 @4  PASS
Attempt(tag9)@2: sees rsp tag 9 @3  PASS. Order irrelevant.

FAIL trace: rsp_tag @4 is 7 instead of 5; attempt(tag5)
scans cycles 2..21, never sees tag 5  FAIL at cycle 21.

Common wrong answers

  • Comparing rsp_tag == req_tag directly in the consequent — req_tag has long since changed; the whole point of the local variable is freezing the value at request time.

  • ##[1:20] rsp_vld ##0 (rsp_tag == t) — binds the tag check to the FIRST response in the window, whatever its tag; out-of-order traffic falsely fails. Keep tag and valid in ONE boolean.

  • Asserting tag uniqueness implicitly — if two live requests share a tag, both attempts match the same response; note the protocol must forbid tag reuse, or add an aux check.


Drill 4 — write-readback consistency

Problem

Interviewer: 'A register file: after a write to address A, the next read of A must return the written data — unless another write to A happened in between. Write the property; the in-between clause is the hard part.'

Think it through

  • Capture BOTH address and data in local variables at the write.

  • The 'unless overwritten' clause lives in the antecedent path: between the write and the read there must be NO other write to the same address — a throughout-style constraint on the waiting period.

  • Structure: write, then (no-write-to-A)* gap, then read-of-A — all as the antecedent sequence — and the consequent is just the data compare. Putting the gap in the antecedent means overwritten cases simply never trigger (vacuous), which matches the spec's 'unless'.

systemverilog
property p_wr_rd_consistency;
  logic [4:0] a;  logic [31:0] d;
  @(posedge clk) disable iff (!rst_n)
  ( (wr_en, a = wr_addr, d = wr_data)
    ##1 (!(wr_en && (wr_addr == a)))[*0:$]
    ##0 (rd_en && (rd_addr == a)) )
  |-> (rd_data == d);
endproperty
a_wr_rd: assert property (p_wr_rd_consistency);
// (assumes same-cycle read data; for registered read data,
//  consequent becomes |=> (rd_data == d) — say which spec you assumed)
diagram
PASS trace
cycle  :  0    1     2     3     4
wr_en  :  0    1     0     1     0
wr_addr:  -    A5    -     B0    -
wr_data:  -    11    -     22    -
rd_en  :  0    0     0     0     1
rd_addr:  -    -     -     -     A5
rd_data:  -    -     -     -     11
write A5=11 @1; intervening write @3 is to B0 (different addr,
gap term still true); read A5 @4 returns 11  PASS

FAIL trace: same, but rd_data@4 = 33  compare 33 vs 11  FAIL.
OVERWRITE trace: write A5=22 @3  gap term false at 3, the @1
attempt dies WITHOUT obligation (vacuous) — exactly the 'unless'.

Common wrong answers

  • wr_en |-> ##[1:$] (rd_en && rd_addr == a && rd_data == d) — no overwrite clause: stale-data failures after a legal overwrite; also bakes the data compare into the search, so a WRONG readback is silently skipped while the property keeps hunting for a matching one. Compare in the consequent, search in the antecedent.

  • Putting the no-overwrite condition in the consequent — turns legal overwrites into failures instead of vacuous passes.

  • Forgetting [*0:$] allows a zero-length gap — back-to-back write-then-read next cycle must still be checked.

Key takeaways

  • Stand at the output and look back with $past, or stand at the input and carry values forward with local variables — pick one and gate startup.

  • Local variables are per-attempt: perfect for tag matching, structurally wrong for shared counts or queued FIFO data.

  • Encode 'unless X happened in between' as a gap term in the ANTECEDENT, so the exception becomes vacuous rather than failing.

Common pitfalls

  • Comparing live signals in delayed consequents — the value moved on; freeze it in a local or use $past.

  • $past in the first cycles after reset — gate it; X-vs-X equality can silently pass.

  • Data compare inside the antecedent search — mismatches get skipped, not reported.

  • Assuming one local-variable property scales to N in-flight items — aliasing across attempts; switch to an aux queue model.