Delays and ranges: one attempt, many threads
Write timing with ##n and ##[m:n], read the threads a range forks inside one attempt, and put two obligations on one trigger with and.
- Lesson
- A2.1
- Time
- 30 min
- Runs on
- Verilator
- Check
- 5 questions
A rule said every read is answered within 16 cycles: arvalid && arready |-> ##[1:16] rvalid. When a response went missing, the failure appeared 16 edges after the lost request, and the waveform at that edge showed a different request being served. The engineer spent an afternoon debugging the wrong transaction.
Drawn with a window of four edges. The report comes when the last chance for A has passed, which is where request B is being served.
Fixed delays
##n moves the check n edges forward. req |-> ##2 ack requires ack exactly two edges after the request. ##0 joins two parts of a sequence at the same edge, and a |=> b is shorthand for a |-> ##1 b.
| Property | Request at edge 3: ack is checked at |
|---|---|
req |-> ##2 ack | edge 5 only |
req |=> ##2 ack | edge 6 only |
req |-> ##[1:3] ack | edges 4, 5 and 6, until one of them has ack |
req |=> ##[0:2] ack | edges 4, 5 and 6, the same window |
A fixed delay names one edge; a range names a window.
A fixed delay is right when the spec fixes the latency, such as a pipeline with three stages. Most handshakes allow a window instead, and a fixed delay then rejects legal designs that answer early or late.
A range forks threads
##[m:n] allows any delay from m to n edges. Inside one attempt, the range starts one thread per delay, and each thread looks at its own edge. The attempt passes at the first thread that matches, and the remaining threads are dropped.
req |-> ##[1:3] ack. Each attempt forks one thread per delay in the range. The attempts at the other edges succeed vacuously and are left out.- Edge 2 · t=15
reqis high at 15, so this attempt forks three threads. Each one looks forackat its own edge: 25, 35 or 45. - Edge 3 · t=25
A second request starts its own attempt with three new threads. The first thread from 15 finds
acklow and dies, but its attempt goes on. - Edge 4 · t=35
Two more threads die, one in each attempt. The attempt from 15 now has a single thread left.
- Edge 5 · t=45
The last thread from 15 finds
acklow, so that attempt fails here, at the end of its window. A third request starts an attempt at the same edge. - Edge 6 · t=55
One
ackpasses two attempts. It is the last chance for the attempt from 25, which passes late, and the first chance for the attempt from 45, which passes early.More context
Once an attempt passes, its other threads are dropped: nothing at 65 or 75 can change its result. Nothing ties this
ackto one request, which is why lesson A4.1 pairs them by ID.
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)) fails at edge 5 (t=45).
- 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)) dies at edge 5 (t=45); thread 3 (to edge 6 (t=55)) passes at edge 6 (t=55).
- req @45, from edge 5 (t=45): thread 1 (to edge 6 (t=55)) passes at edge 6 (t=55); thread 2 (to edge 7 (t=65)) is dropped at edge 6 (t=55), because the attempt has already passed; thread 3 (to edge 8 (t=75)) is dropped at edge 6 (t=55), because the attempt has already passed.
Sampled values, edge 1 to 8: req 0 1 1 0 1 0 0 0; ack 0 0 0 0 0 1 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 ack_soon 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;
ack_soon: assert property (@(posedge clk) req |-> ##[1:3] ack);
initial begin
@(negedge clk); req = 1; // requests sampled at 15 and 25
@(negedge clk);
@(negedge clk); req = 0;
@(negedge clk); req = 1; // request sampled at 45
@(negedge clk); req = 0; ack = 1; // one ack, sampled at 55
@(negedge clk); ack = 0;
repeat (2) @(negedge clk); // edges at 65 and 75
$finish;
end
endmodule
Any thread that finds ack passes its attempt. An attempt fails only when its last thread finds nothing.
Where a failure is reported
An attempt fails only when its last thread fails, so a missing response is reported at the end of the window. With ##[1:16], that is 16 edges after the request, however early the bug happened.
Two habits make such reports easy to read. Make the window as tight as the spec allows, so the report lands close to its cause. Print the attempt's start in the fail message, or capture an ID as lesson A4.1 shows, so the report names the request it belongs to.
A window wider than the spec is a quiet weakness too. A design that answers in 12 cycles passes a 16-cycle window even when the spec says 8. A window narrower than the spec is the opposite mistake: it fails legal designs, and the usual reaction is to switch the rule off.
A range is an or of delays
A sequence or matches when either operand matches. req |-> (##1 v) or (##2 v) passes when v comes one or two edges after the request, which is exactly what req |-> ##[1:2] v says. A range is the compact way to write alternatives that differ only in delay.
Two obligations from one trigger: and
A read request may need two independent answers: ack at the next edge and a status word three edges later. The sequence and says both: (##1 ack) and (##3 stat). Both operands start at the same edge, both must match, and the composite ends where the later one ends. Whatever follows starts from there.
req |-> ((##1 ack) and (##3 stat)) ##1 done. done must come one edge after stat, the later operand.- Edge 2 · t=15
A request at 15 starts both operands at once:
ackis due at 25 andstatat 45. - Edge 6 · t=55
ackcame at 25 andstatat 45, so theandends at 45.doneat 55 completes the attempt, which passes. - Edge 9 · t=85
The second request has
ackat 75.donearrives at 85, one edge afterack, but theandhas not ended yet, so thisdonecounts for nothing. - Edge 11 · t=105
statcame at 95, sodonewas due at 105. It is low, and the attempt fails.More context
doneone edge after the earlier operand is not enough. What follows anandis measured from its later end. - Edge 13 · t=125
The third request has no
ackat 125. One operand failed, so theandcannot match, and the attempt fails at once, without waiting forstat.
Read the lanes as text
- req @15, from edge 2 (t=15): passes at edge 6 (t=55).
- req @65, from edge 7 (t=65): fails at edge 11 (t=105).
- req @115, from edge 12 (t=115): fails at edge 13 (t=125).
Sampled values, edge 1 to 15: req 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0; ack 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0; stat 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1; done 0 0 0 0 0 1 0 0 1 0 0 0 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 both_then_done 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, stat = 0, done = 0;
both_then_done: assert property (@(posedge clk) req |-> ((##1 ack) and (##3 stat)) ##1 done);
initial begin
@(negedge clk); req = 1; // request at 15
@(negedge clk); req = 0; ack = 1; // ack at 25
@(negedge clk); ack = 0;
@(negedge clk); stat = 1; // stat at 45
@(negedge clk); stat = 0; done = 1; // done at 55: in time
@(negedge clk); done = 0; req = 1; // request at 65
@(negedge clk); req = 0; ack = 1; // ack at 75
@(negedge clk); ack = 0; done = 1; // done at 85: too early
@(negedge clk); done = 0; stat = 1; // stat at 95
@(negedge clk); stat = 0; // no done at 105
@(negedge clk); req = 1; // request at 115
@(negedge clk); req = 0; // no ack at 125
@(negedge clk);
@(negedge clk); stat = 1; // stat at 145
@(negedge clk); stat = 0;
$finish;
end
endmodule
Both operands start together. The attempt fails as soon as either operand fails, or when what follows the later end fails.
&& and and are different tools. ack && stat is one condition at one edge. and joins two sequences that may end at different edges.
With fixed delays, the same rule can be written as one chain, ##1 ack ##2 stat ##1 done. The and form keeps the two answers independent: each delay can change without rewriting the other, and the code reads like the spec.
Open windows, and what comes later
##[1:$] has no upper end: each attempt keeps one live thread per edge until a match, so a long-silent signal costs memory and time in every attempt. In simulation it also never fails, because a sequence used as an asserted property is weak by default (IEEE 1800-2017 16.12.2). The weakness comes from that default, not from the operator; lesson A3.1 shows the strong forms.
Ranges followed by more sequence, ranges in an antecedent and first_match are not covered yet: Verilator 5.052 misses some of their failures. End a sequence with its range, and check what follows from the event itself, as ack |=> done does in the example.
Try it
The first request is answered in two edges, so all three rules pass. The second is answered in one edge: legal by the spec, but exact_two fails at 85. The third is answered after four edges, so ack_soon fails at 145, the end of its window, and exact_two fails earlier, at 135.
The late ack at 155 has no done after it, and done_next reports that at 165. Move the last ack one edge earlier and predict which lines remain.
t=85 exact_two failed t=135 exact_two failed t=145 ack_soon failed t=165 done_next failedCheck yourself
Remember
##nnames one edge;##[m:n]names a window and forks one thread per delay.- An attempt passes at its first matching thread and fails only when its last thread fails, at the end of the window.
- Keep windows as tight as the spec allows, and say in the fail message which request failed.
- A range is an
orof delays;andputs two obligations on one trigger and ends with the later one. - An unmet
##[1:$]never fails in anassert, because a sequence used as a property is weak by default.