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.
(arvalid, id = arid) |-> ##2 (rvalid && rid == id). The capsule at the start of each lane is the value that attempt captured.- Edge 2 · t=15
arvalidis high at 15, so an attempt starts and copiesaridinto its ownid: 3. - Edge 3 · t=25
A second request starts a second attempt, which captures 4 in its own copy. The first attempt still holds 3.
- Edge 4 · t=35
The third attempt captures 5. At the same edge the first attempt checks its response:
ridis 3, equal to itsid, so it passes. - Edge 5 · t=45
aridchanges to 9 whilearvalidis low. No attempt starts and no copy changes. The second attempt finds 4 and passes. - 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
| Question | Answer |
|---|---|
| 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.
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=e5Check yourself
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,
$pastdoes the same job; for varying delays, check at a deadline (A4.2).
Practice
- The right data for the right request Verify · Medium