← Design Verification Interviews

Interview Question · Code Writing

Remembering the Request Data

L1 JuniorL2 Mid-Levelsvalocal variables

The Question

The interviewer asks

A block returns data + 1 three cycles after each valid, and valid can be high on consecutive cycles. This checker fails on a correct design. Rewrite it.

systemverilog
// out must equal data + 1, three cycles after each valid
always @(posedge clk) if (valid) saved <= data;
a_out: assert property (@(posedge clk) valid |-> ##3 (out == saved + 8'd1)) else errors++;
Reveal the Model Answer

One shared register can remember only the newest request, so a back-to-back request overwrites it before the first check runs. Give each attempt its own copy with a local variable: property p; logic [7:0] v; (valid, v = data) |-> ##3 (out == v + 1); endproperty. The assignment runs when valid matches, using the sampled data, and each overlapping attempt keeps its own v.

Three Requests, Three Copies of `id`One lane per request for (arvalid, id = arid) |-> ##2 (rvalid && rid == id). The capsule at the start of each lane is the value that attempt captured.(arvalid, id = arid) |-> ##2 (rvalid && rid == id)Measured on Verilator 5.052: one copy of the assertion per lane
attempt startspassfail
Three requests, three copies of `id`request @15, from edge 2 (t=15), captures id=3: passes at edge 4 (t=35). request @25, from edge 3 (t=25), captures id=4: passes at edge 5 (t=45). request @35, from edge 4 (t=35), captures id=5: fails at edge 6 (t=55).edgetime15215325435545655clkarvalidarid03459rvalidrid034attemptsrequest @15id=3request @15: passes at edge 4 (t=35)request @25id=4request @25: passes at edge 5 (t=45)request @35id=5request @35: fails at edge 6 (t=55)
  1. Edge 2 · t=15

    arvalid is high at 15, so an attempt starts and copies arid into its own id: 3.

  2. Edge 3 · t=25

    A second request starts a second attempt, which captures 4 in its own copy. The first attempt still holds 3.

  3. Edge 4 · t=35

    The third attempt captures 5. At the same edge the first attempt checks its response: rid is 3, equal to its id, so it passes.

  4. Edge 5 · t=45

    arid changes to 9 while arvalid is low. No attempt starts and no copy changes. The second attempt finds 4 and passes.

  5. Edge 6 · t=55

    The third attempt expected 5 but finds 4 again. It fails, and only it fails.

    More context

    A single testbench variable would have held 5 since edge 4, so the first two checks would have failed as well.

Read the lanes as text
  • request @15, from edge 2 (t=15), captures id=3: passes at edge 4 (t=35).
  • request @25, from edge 3 (t=25), captures id=4: passes at edge 5 (t=45).
  • request @35, from edge 4 (t=35), captures id=5: fails at edge 6 (t=55).

Sampled values, edge 1 to 6: arvalid 0 1 1 1 0 0; arid 0 3 4 5 9 9; rvalid 0 0 0 1 1 1; rid 0 0 0 3 4 4.

The program behind the lanes

The lanes come from running this program on Verilator 5.052. Each lane is a separate copy of id_back that can start only at that lane's edge, so every verdict and its edge are read from that copy's own output.

module tb;
  logic clk = 0;
  always #5 clk = ~clk;                 // rising edges at 5, 15, 25, ...
  logic       arvalid = 0, rvalid = 0;
  logic [3:0] arid = 0, rid = 0;

  id_back: assert property (@(posedge clk) (arvalid, id = arid) |-> ##2 (rvalid && rid == id));

  initial begin
    @(negedge clk); arvalid = 1; arid = 3;          // requests at 15, 25, 35
    @(negedge clk); arid = 4;
    @(negedge clk); arid = 5; rvalid = 1; rid = 3;  // response for 3 at 35
    @(negedge clk); arvalid = 0; arid = 9; rid = 4; // response for 4 at 45
    @(negedge clk); rid = 4;                        // 4 again at 55: 5 was due
    @(negedge clk); rvalid = 0;
    $finish;
  end
endmodule

Every attempt owns its copy from the edge it starts until it ends.

Negative control: the local-variable rule against a DUT with one pipeline stage too few.

RUNVerilator · simulator service
Edit the code, then run it
Expected: t=45 a_out failed t=55 a_out failed failures=2

Self-Check

Tick each key point you covered in your answer.

What Interviewers Listen For (Junior)

A good junior answer sees that the second request overwrites the shared register. It rewrites the check with a local variable in the property.

Cross-Questions

Where the interviewer goes next. Open a node to practise that follow-up.

WhyWhat IfProve ItBreak ItCompareScale It

  1. Remembering the Request Data
  2. CompareA Local Variable vs Past for Fixed LatencyL2 Mid-Level

    Compare (valid, v = data) |-> ##3 (out == v + 1) with valid |-> ##3 (out == $past(data, 3) + 1).

    Reveal the Model Answer

    For a fixed latency they give the same verdicts: $past(data, 3) at the check edge is the data sampled at the request edge. $past keeps a history of depth 3 for every assertion, while the local variable keeps one value per open attempt. The local variable also generalises to variable latency, where no fixed depth exists.

    Local variable$past(data, 3)
    KeepsOne value per open attemptA history of depth 3
    Fixed latencyWorksWorks, same verdicts
    Variable latencyWorksNo fixed depth to use

    Self-Check

    Tick each key point you covered in your answer.

    Misconception

    "$past(data, 3) is evaluated at the request edge."

    It is evaluated at the check edge and looks three ticks back. That matches the request only when the latency is exactly three.

    Source IEEE 1800-2017 §16.9.3

  3. WhyWhy Each Attempt Gets Its Own CopyL1 JuniorL2 Mid-Level

    Why did the language designers give every attempt its own copy of a local variable?

    Reveal the Model Answer

    Attempts overlap whenever the antecedent can match on consecutive edges, and each attempt describes a different transaction. A shared variable would make one transaction's check read another's data, which is exactly the false failure in the twin. Per-attempt copies make a property read as a statement about one transaction.

    Self-Check

    Tick each key point you covered in your answer.

    Half-Truth

    "Copies are made only when attempts overlap."

    Every attempt has its own variables by definition; overlap is just when it becomes visible.

    Source Mehta, SystemVerilog Assertions and Functional Coverage (3rd ed.), ch. 11 (PDF p210-213)

  4. What IfCapturing Data When the Latency VariesL2 Mid-LevelL3 Senior

    What if the response comes one to four cycles after the request, in order?

    Reveal the Model Answer

    The standard form carries the local variable across the window, (req, v = data) |-> ##[1:4] (rsp && rdata == v), but with overlapping requests one response can satisfy several attempts. A robust checker keeps an in-order queue of expected data in auxiliary code. It pushes on each request, pops on each response, and asserts the popped value. That queue is a small scoreboard written next to the property.

    Self-Check

    Tick each key point you covered in your answer.

    Missed Corner

    "The ranged form is enough; overlapping requests are rare."

    Back-to-back traffic is the stress case, and it is exactly where a ranged window lets one response answer two requests.

    Source Mehta, SystemVerilog Assertions and Functional Coverage (3rd ed.), §11.1 (PDF p222)

  5. Break ItWhy One Response Satisfies Every Open WindowL1 JuniorL2 Mid-Level

    Why can one ack pass several overlapping attempts of req |-> ##[1:4] ack?

    Reveal the Model Answer

    Each attempt only asks whether ack is true at some edge of its own window. The attempts are evaluated independently, and none of them uses up the ack. So that one edge satisfies every window that contains it. Counting responses needs state outside the property, a ticket or a queue.

    One Ack Passes Three AttemptsOne lane per request for req |-> ##[1:4] ack. Each attempt forks a thread per delay of the range.req |-> ##[1:4] ackMeasured on Verilator 5.052: one copy of the assertion per lane
    attempt startspassthread dies; the attempt goes onthread dropped: the attempt has already passed
    One ack passes three attemptsreq @15, from edge 2 (t=15): thread 1 (to edge 3 (t=25)) dies at edge 3 (t=25); thread 2 (to edge 4 (t=35)) dies at edge 4 (t=35); thread 3 (to edge 5 (t=45)) passes at edge 5 (t=45); thread 4 (to edge 6 (t=55)) is dropped at edge 5 (t=45), because the attempt has already passed. req @25, from edge 3 (t=25): thread 1 (to edge 4 (t=35)) dies at edge 4 (t=35); thread 2 (to edge 5 (t=45)) passes at edge 5 (t=45); thread 3 (to edge 6 (t=55)) is dropped at edge 5 (t=45), because the attempt has already passed; thread 4 (to edge 7 (t=65)) is dropped at edge 5 (t=45), because the attempt has already passed. req @35, from edge 4 (t=35): thread 1 (to edge 5 (t=45)) passes at edge 5 (t=45); thread 2 (to edge 6 (t=55)) is dropped at edge 5 (t=45), because the attempt has already passed; thread 3 (to edge 7 (t=65)) is dropped at edge 5 (t=45), because the attempt has already passed; thread 4 (to edge 8 (t=75)) is dropped at edge 5 (t=45), because the attempt has already passed.edgetime15215325435545655765875clkreqackattemptsreq @15req @15: dies at edge 3 (t=25)req @15: dies at edge 4 (t=35)req @15: passes at edge 5 (t=45)req @25req @25: dies at edge 4 (t=35)req @25: passes at edge 5 (t=45)req @35req @35: passes at edge 5 (t=45)
    1. Edge 2 · t=15

      The first request starts an attempt with four threads, one for each edge from 25 to 55.

    2. Edge 4 · t=35

      Two more requests have started their own attempts. No ack yet, so the earliest threads die, but every attempt still has threads looking.

    3. Edge 5 · t=45

      One ack at 45. Every attempt has a thread looking at this edge, so all three pass here, together.

      More context

      Nothing in ack says which request it answers. The two lost responses are never missed.

    4. Edge 8 · t=75

      The windows close with no further ack, and nothing fails.

    Read the lanes as text
    • req @15, from edge 2 (t=15): thread 1 (to edge 3 (t=25)) dies at edge 3 (t=25); thread 2 (to edge 4 (t=35)) dies at edge 4 (t=35); thread 3 (to edge 5 (t=45)) passes at edge 5 (t=45); thread 4 (to edge 6 (t=55)) is dropped at edge 5 (t=45), because the attempt has already passed.
    • req @25, from edge 3 (t=25): thread 1 (to edge 4 (t=35)) dies at edge 4 (t=35); thread 2 (to edge 5 (t=45)) passes at edge 5 (t=45); thread 3 (to edge 6 (t=55)) is dropped at edge 5 (t=45), because the attempt has already passed; thread 4 (to edge 7 (t=65)) is dropped at edge 5 (t=45), because the attempt has already passed.
    • req @35, from edge 4 (t=35): thread 1 (to edge 5 (t=45)) passes at edge 5 (t=45); thread 2 (to edge 6 (t=55)) is dropped at edge 5 (t=45), because the attempt has already passed; thread 3 (to edge 7 (t=65)) is dropped at edge 5 (t=45), because the attempt has already passed; thread 4 (to edge 8 (t=75)) is dropped at edge 5 (t=45), because the attempt has already passed.

    Sampled values, edge 1 to 8: req 0 1 1 1 0 0 0 0; ack 0 0 0 0 1 0 0 0.

    The program behind the lanes

    The lanes come from running this program on Verilator 5.052. Each lane is a separate copy of acked that can start only at that lane's edge, so every verdict and its edge are read from that copy's own output.

    module tb;
      logic clk = 0;
      always #5 clk = ~clk;                 // rising edges at 5, 15, 25, ...
      logic req = 0, ack = 0;
    
      // Ticket counters: a request takes ticket number issued, the next ack serves ticket served.
      int unsigned issued = 0, served = 0;
      always @(posedge clk) begin
        if (req) issued <= issued + 1;
        if (ack) served <= served + 1;
      end
    
      acked: assert property (@(posedge clk) req |-> ##[1:4] ack);
    
      initial begin
        @(negedge clk); req = 1;            // requests at 15, 25, 35
        repeat (2) @(negedge clk);
        @(negedge clk); req = 0; ack = 1;   // one ack, at 45
        @(negedge clk); ack = 0;
        repeat (3) @(negedge clk);          // edges at 55 ... 75
        $finish;
      end
    endmodule
    

    The lanes converge: one response, three passes.

    Self-Check

    Tick each key point you covered in your answer.

    Misconception

    "The first attempt uses up the ack, so the second one fails."

    Attempts share no state; an ack is a value at an edge, not a token that one attempt can take.

    Source IEEE 1800-2017 §16.12.7

  6. Prove ItSolve 6.6.9: The Right Data for the Right RequestSimulator-graded

    Show it, don't just say it: this Silicon Lab problem is graded by the simulator.

    Open Problem 6.6.9

  7. Scale ItOrdering Checks: Assertions or the Scoreboard?L3 Senior

    For a block with outstanding, tagged transactions, which ordering and completion rules do you put in assertions and which in the scoreboard?

    Reveal the Model Answer

    Assertions take the local, cycle-level rules: an ID is not reused while outstanding, every response matches an outstanding ID, deadlines are met. They report the exact cycle and also run in formal. The scoreboard takes end-to-end data correctness and anything needing a reference model. Overlap is deliberate: a lost response should be caught by both, at different distances from the bug.

    Self-Check

    Tick each key point you covered in your answer.

    What Interviewers Listen For (Senior)

    A good senior answer splits the rules by how local they are and by reuse in formal. It wants both the assertions and the scoreboard to catch a lost response, on purpose.

    Wrong Altitude

    "The scoreboard already checks everything, so assertions are redundant."

    A scoreboard reports a missing response at the end of the test, far from the cause. An assertion names the cycle, and only properties carry over to formal.

What People Get Wrong

Common wrong answers and why they fail. Run It shows the wrong belief failing on a real simulator.

Misconception

"A module register set when valid is high does the same job as a local variable."

There is one register for all attempts. In the twin the request at 25 overwrites it, and the correct output for the request at 15 fails; the local-variable version passes.

Run It

A correct DUT, two back-to-back requests, and a checker that remembers data in one register.

RUNVerilator · simulator service
Edit the code, then run it
Expected: failures=1 FAIL: the second request overwrote the shared register, so a correct DUT failed
See the Fix
module tb;
  logic clk = 0, valid = 0;
  logic [7:0] data = 0, out = 0, p1 = 0, p2 = 0;
  int fails = 0;
  always #5 clk = ~clk;
  // the DUT, correct: out = data + 1, three cycles later
  always @(posedge clk) begin p1 <= data + 8'd1; p2 <= p1; out <= p2; end
  // The fix: a local variable, one copy per attempt.
  property p_out;
    logic [7:0] v;
    @(posedge clk) (valid, v = data) |-> ##3 (out == v + 8'd1);
  endproperty
  a_out: assert property (p_out) else fails++;
  initial begin
    @(negedge clk) begin valid = 1; data = 8'h10; end
    @(negedge clk) data = 8'h20;        // a second request, back to back
    @(negedge clk) valid = 0;
    repeat (5) @(negedge clk);
    $display("failures=%0d", fails);
    if (fails == 0) $display("PASS: each attempt kept its own copy of data");
    $finish;
  end
endmodule

Prints: failures=0 PASS: each attempt kept its own copy of data

Misconception

"A local variable is shared by all attempts, like a static variable in a task."

Each attempt, and each thread of an attempt, gets its own copy, which is the whole point.

Source IEEE 1800-2017 §16.10

Tool as Truth

"Carry the variable across a window: (valid, v = data) |-> ##[1:4] (out == v + 1); every simulator runs that."

The standard allows it, but Verilator 5.052 rejects a local variable used across a ranged delay in the consequent.

Source ASIC Point engine notes (Verilator 5.052 SVA probes), row "local variable across a ranged delay"

Learn It, Then Prove It