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.

A failure reported at the end of its window
012345edge123456requestA-B-rvalidrequest A is lostA fails here; B is in flight

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.

PropertyRequest at edge 3: ack is checked at
req |-> ##2 ackedge 5 only
req |=> ##2 ackedge 6 only
req |-> ##[1:3] ackedges 4, 5 and 6, until one of them has ack
req |=> ##[0:2] ackedges 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.

Three requests, one rangeOne lane per request for 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.req |-> ##[1:3] ackMeasured on Verilator 5.052: one copy of the assertion per lane
attempt startspassfailthread dies; the attempt goes onthread dropped: the attempt has already passed
Three requests, one rangereq @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.edgetime15215325435545655765875clkreqackattemptsreq @15req @15: dies at edge 3 (t=25)req @15: dies at edge 4 (t=35)req @15: fails at edge 5 (t=45)req @25req @25: dies at edge 4 (t=35)req @25: dies at edge 5 (t=45)req @25: passes at edge 6 (t=55)req @45req @45: passes at edge 6 (t=55)
  1. Edge 2 · t=15

    req is high at 15, so this attempt forks three threads. Each one looks for ack at its own edge: 25, 35 or 45.

  2. Edge 3 · t=25

    A second request starts its own attempt with three new threads. The first thread from 15 finds ack low and dies, but its attempt goes on.

  3. Edge 4 · t=35

    Two more threads die, one in each attempt. The attempt from 15 now has a single thread left.

  4. Edge 5 · t=45

    The last thread from 15 finds ack low, so that attempt fails here, at the end of its window. A third request starts an attempt at the same edge.

  5. Edge 6 · t=55

    One ack passes 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 ack to 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.

and ends with its later operandOne lane per request for req |-> ((##1 ack) and (##3 stat)) ##1 done. done must come one edge after stat, the later operand.req |-> ((##1 ack) and (##3 stat)) ##1 doneMeasured on Verilator 5.052: one copy of the assertion per lane
attempt startspassfail
and ends with its later operandreq @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).edgetime1521532543554565576587598510951110512115131251413515145clkreqackstatdoneattemptsreq @15req @15: passes at edge 6 (t=55)req @65req @65: fails at edge 11 (t=105)req @115req @115: fails at edge 13 (t=125)
  1. Edge 2 · t=15

    A request at 15 starts both operands at once: ack is due at 25 and stat at 45.

  2. Edge 6 · t=55

    ack came at 25 and stat at 45, so the and ends at 45. done at 55 completes the attempt, which passes.

  3. Edge 9 · t=85

    The second request has ack at 75. done arrives at 85, one edge after ack, but the and has not ended yet, so this done counts for nothing.

  4. Edge 11 · t=105

    stat came at 95, so done was due at 105. It is low, and the attempt fails.

    More context

    done one edge after the earlier operand is not enough. What follows an and is measured from its later end.

  5. Edge 13 · t=125

    The third request has no ack at 125. One operand failed, so the and cannot match, and the attempt fails at once, without waiting for stat.

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.

RUNVerilator · simulator service
Edit the code, then run it
Expected: t=85 exact_two failed t=135 exact_two failed t=145 ack_soon failed t=165 done_next failed

Check yourself

Predict the outputA request at the edge at 15 is never acknowledged. Predict the exact output.
module tb;
  logic clk = 0;
  always #5 clk = ~clk;                  // rising edges at 5, 15, 25, ...
  logic req = 0, ack = 0;
  p: assert property (@(posedge clk) req |-> ##[1:3] ack)
    else $display("fail at %0t", $time);
  initial begin
    @(negedge clk); req = 1;             // request at 15
    @(negedge clk); req = 0;             // ack never comes
    #60 $finish;
  end
endmodule
Conceptarvalid && arready |-> ##[1:16] rvalid checks a request accepted at edge 10 that is never answered. Where is the failure reported?
ConceptWhich consequent passes on exactly the same traces as ##[1:2] v?
Read the waveformreq |-> ((##1 ack) and (##3 stat)) ##1 done checks this trace. What happens to the attempt from edge 1?
Sampled values at each rising edge
01234edge12345reqackstatdone

One request at edge 1.

Spot the bugEach option is the property for "after a request, rvalid rises within 1 to 3 edges". Which option fails on legal traffic?

Remember

  • ##n names 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 or of delays; and puts two obligations on one trigger and ends with the later one.
  • An unmet ##[1:$] never fails in an assert, because a sequence used as a property is weak by default.