Remembering data with local variables

Capture a value when a request is accepted and check it against the response, with a local variable that every attempt keeps for itself.

Lesson
A4.1
Time
25 min
Runs on
Verilator
Check
6 questions

A memory controller passed its read tests for weeks, until a test issued requests back to back. Under that load, each response carried the data for the previous address. The scoreboard reported a pile of mismatches much later. A property that remembers each request's ID and checks its own response names the first wrong beat.

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.

Why a property needs memory

A response must match its request: the same ID, data from the requested address. Those values exist only at the request, and the check happens later.

With a fixed latency, $past(arid, 2) reaches back to the request. A local variable does the job more directly. It holds a value captured at one edge of an attempt for use later in the same attempt, and it can carry several values together.

Capturing with a match item

Declare local variables inside the property or sequence, before its clocking event: logic [3:0] id;. Assign them by listing the assignment after a condition in parentheses, (arvalid, a = araddr, id = arid). This match item runs at the edge where the condition matches, with that edge's sampled values.

Later parts of the attempt read the copy: (arvalid, id = arid) |-> ##2 (rvalid && rid == id). Every attempt that starts has its own id, so overlapping requests never mix, as the lanes above show.

Capture at the accepting edge

A request that waits for arready is offered at several edges but accepted at one. A rule that captures at every offer starts an attempt for each stalled edge, and each one expects a response that will never come.

Captured at each offer

(arvalid, id = arid)
  |-> ##2 (rvalid && rid == id)

One stalled edge, one false failure: the response is two edges after acceptance, not after the offer.

Captured at acceptance

(arvalid && arready, id = arid)
  |-> ##2 (rvalid && rid == id)

Exactly one attempt per accepted request, and it passes.

Measured on a request stalled for one edge: the left rule fails once, the right one never. The quiz below runs the same pair.

Where a local variable lives

QuestionAnswer
Who can read it?Only the property or sequence that declares it, in the same attempt
How many copies exist?One per attempt that is running, so three overlapping requests make three
When does it disappear?When its attempt passes or fails
Can the fail action print it?No. $display(..., id) in the action block does not compile (5.052: "Can't find definition of variable: 'id'")
Where does a value for later checks go?Into auxiliary code, such as a counter or a model

Scope rules. The action block runs outside the attempt, so print the design-side values with $sampled(...) instead.

Reading a design signal where a local variable was meant is the classic slip. rid == arid at the response compares with whatever request is on the bus now, which only works when nothing overlaps.

Local variable, $past or auxiliary code?

Three tools carry a value forward, and each has its place. $past(x, n) reads a fixed distance back. It needs no declaration, but it reads that edge whether or not a request was accepted there, so the rule has to repeat the acceptance condition through $past as well.

A local variable captures on a condition, carries several values together and gives every attempt its own copy. Auxiliary code, such as a counter or a small model, holds values that must outlive one attempt or be shared between assertions.

Start with the simplest tool that states the rule, and move up only when the rule needs it. A fixed-latency ID check is one $past. A check that also needs the address and the data is a local variable. Responses paired by their order need counters, as lesson A4.2 shows.

Delays that vary

A fixed latency, even one set per configuration with a parameter (##LAT), works with a local variable directly. When the latency varies from request to request, the response edge is unknown. For responses that return in request order, lesson A4.2 checks each request at its deadline instead, which needs only fixed delays.

Choosing what to capture

Capture only what the check needs, at the edge where the request is accepted. Name each variable after its meaning, such as a for the address and id for the ID.

Make the fail message carry the response side, for example "response for ID 4 carried data 95". With the request edge read off the waveform, that explains the bug on its own.

Try it

The first three requests were answered correctly, each checked by its own attempt even though they overlapped. The last three, made while bug was set, got data for the previous address, and each failure names the response's ID.

The fail action prints $sampled(rid), not id: the local variable is not visible there. Replace a in the consequent with $past(araddr, 2) and predict whether the output changes.

RUNVerilator · simulator service
Edit the code, then run it
Expected: t=65 response for ID 3 is wrong: rdata=85 t=75 response for ID 4 is wrong: rdata=95 t=85 response for ID 5 is wrong: rdata=e5

Check yourself

Predict the outputRequests with IDs 4 and 7 arrive at edges 15 and 25; both responses carry ID 4. Predict the exact 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;
  property id_back;
    logic [3:0] id;
    @(posedge clk) (arvalid, id = arid) |-> ##2 (rvalid && rid == id);
  endproperty
  p: assert property (id_back) else $display("fail at %0t", $time);
  initial begin
    @(negedge clk); arvalid = 1; arid = 4;     // requests at 15 (ID 4) and 25 (ID 7)
    @(negedge clk); arid = 7;
    @(negedge clk); arvalid = 0; rvalid = 1; rid = 4;
    @(negedge clk); rid = 4;                   // the second response repeats ID 4
    @(negedge clk); rvalid = 0;
    #20 $finish;
  end
endmodule
Predict the outputThe request with ID 6 is offered at 15, stalled, and accepted at 25. The DUT answers two edges after acceptance. Predict the exact output.
module tb;
  logic clk = 0;
  always #5 clk = ~clk;                  // rising edges at 5, 15, 25, ...
  logic       arvalid = 0, arready = 0;
  logic [3:0] arid = 0;
  // DUT: the response comes two edges after the accepting edge.
  logic       v1 = 0, v2 = 0;
  logic [3:0] i1 = 0, i2 = 0;
  always @(posedge clk) begin v1 <= arvalid && arready; i1 <= arid; v2 <= v1; i2 <= i1; end
  wire       rvalid = v2;
  wire [3:0] rid    = i2;
  int at_offer_fails, at_accept_fails;
  property at_offer;
    logic [3:0] id;
    @(posedge clk) (arvalid, id = arid) |-> ##2 (rvalid && rid == id);
  endproperty
  property at_accept;
    logic [3:0] id;
    @(posedge clk) (arvalid && arready, id = arid) |-> ##2 (rvalid && rid == id);
  endproperty
  offer:  assert property (at_offer)  else at_offer_fails++;
  accept: assert property (at_accept) else at_accept_fails++;
  initial begin
    @(negedge clk); arvalid = 1; arid = 6;   // offered at 15, not ready
    @(negedge clk); arready = 1;             // accepted at 25
    @(negedge clk); arvalid = 0; arready = 0;
    repeat (3) @(negedge clk);
    $display("offer=%0d accept=%0d", at_offer_fails, at_accept_fails);
    $finish;
  end
endmodule
ConceptTwo requests arrive on consecutive edges. How many copies of the local variable id exist while both are waiting?
ConceptResponses always come exactly two edges after the request. Which statement is true?
Read the waveform(arvalid, id = arid) |-> ##2 (rvalid && rid == id) checks this trace. Which attempt fails?
Requests and responses
01234edge12345arvalidarid26rvalidrid02

Values sampled at each rising edge.

Spot the bugEach option checks that a response two edges after a request carries the request's ID. Which option fails on legal back-to-back traffic?

Remember

  • A local variable captures a value at one edge of an attempt for use later in the same attempt.
  • Assign it in a match item, (arvalid && arready, id = arid), at the edge where the request is accepted.
  • Every attempt has its own copy, so overlapping requests never mix.
  • The action block cannot see local variables; print design-side values with $sampled.
  • With a fixed delay, $past does the same job; for varying delays, check at a deadline (A4.2).

Practice