Holding until an event: until and throughout

Say that a condition holds until an event with the right operator: until or until_with, weak or strong, or throughout over a goto or a window, including the case where the event is already there.

Lesson
A3.2
Time
30 min
Runs on
Verilator
Check
5 questions

A bus bridge had the rule "once req rises, it stays high until gnt", written $rose(req) |-> req until gnt. A request that was dropped early passed anyway, because gnt from the previous transfer was still high at the edge where req rose. until ended the check at its very first edge, before it had looked at req at all.

Two questions for every hold

Every "p holds until q" rule has to answer two questions. Must p still hold at the edge where q arrives? And must q arrive before the test ends? Get either answer wrong, and the rule passes a real bug or fails on legal traffic. The four until operators are the four combinations of answers.

Operatorp needed at the edge of q?q never comes
p until qno (non-overlapping)no failure (weak)
p until_with qyes (overlapping)no failure (weak)
p s_until qnofails at the end of the run (strong)
p s_until_with qyesfails at the end of the run (strong)

Measured on Verilator 5.052; the end-of-run case is in the simulator note below.

In each form, p must hold at every edge from the start of the attempt until q is seen. The difference is only the last edge, and what happens if that edge never comes.

One trace, two operators

The two figures below follow three attempts on one trace. In A, v drops exactly where r arrives. In B, r is already high at the start. In C, r never comes before the test stops.

until: the edge of r is freeOne lane per start for start |-> v until r.start |-> v until rMeasured on Verilator 5.052: one copy of the assertion per lane
attempt startspassstill running at the end: weak, no failure
until: the edge of r is freestart @15, from edge 2 (t=15): passes at edge 4 (t=35). start @55, from edge 6 (t=55): passes at edge 6 (t=55). start @75, from edge 8 (t=75): is still running at the end of the run: weak, so no failure.edgetime152153254355456557658759851095end100clkstartvrattemptsstart @15start @15: passes at edge 4 (t=35)start @55start @55: passes at edge 6 (t=55)start @75start @75: is still running at the end of the run: weak, so no failure
  1. Edge 2 · t=15

    A starts at 15 with v high. v must hold until r is seen.

  2. Edge 4 · t=35

    r arrives at 35. until does not look at v on this edge, so A passes although v is already low.

  3. Edge 6 · t=55

    B starts at 55 with r already high. The attempt passes at once, and v is never checked.

  4. Edge 8 · t=75

    C starts at 75 with v high and keeps it high.

  5. End of the run · t=100

    r never came. until is weak, so C ends with no verdict.

Read the lanes as text
  • start @15, from edge 2 (t=15): passes at edge 4 (t=35).
  • start @55, from edge 6 (t=55): passes at edge 6 (t=55).
  • start @75, from edge 8 (t=75): is still running at the end of the run: weak, so no failure.

Sampled values, edge 1 to 10: start 0 1 0 0 0 1 0 1 0 0; v 0 1 1 0 0 0 0 1 1 1; r 0 0 0 1 0 1 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 hold_until 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 start = 0, v = 0, r = 0;

  hold_until: assert property (@(posedge clk) start |-> v until r);

  initial begin
    @(negedge clk); start = 1; v = 1;   // A starts at 15
    @(negedge clk); start = 0;
    @(negedge clk); v = 0; r = 1;       // r at 35, v already low
    @(negedge clk); r = 0;
    @(negedge clk); start = 1; r = 1;   // B starts at 55 with r already high
    @(negedge clk); start = 0; r = 0;
    @(negedge clk); start = 1; v = 1;   // C starts at 75; r never comes
    @(negedge clk); start = 0;
    @(negedge clk);
    @(negedge clk);
    $finish;                            // the test stops at 100
  end
endmodule
until_with: the edge of r is checked tooThe same trace for start |-> v until_with r.start |-> v until_with rMeasured on Verilator 5.052: one copy of the assertion per lane
attempt startsfailstill running at the end: weak, no failure
until_with: the edge of r is checked toostart @15, from edge 2 (t=15): fails at edge 4 (t=35). start @55, from edge 6 (t=55): fails at edge 6 (t=55). start @75, from edge 8 (t=75): is still running at the end of the run: weak, so no failure.edgetime152153254355456557658759851095end100clkstartvrattemptsstart @15start @15: fails at edge 4 (t=35)start @55start @55: fails at edge 6 (t=55)start @75start @75: is still running at the end of the run: weak, so no failure
  1. Edge 2 · t=15

    A starts at 15 with v high, exactly as in the first figure.

  2. Edge 4 · t=35

    At 35 r arrives, and until_with needs v on this edge as well. v is low, so A fails.

  3. Edge 6 · t=55

    B starts with r high, so its first edge is also its last. v is low there: B fails.

  4. End of the run · t=100

    C is still waiting at the end. until_with is weak too, so no verdict.

Read the lanes as text
  • start @15, from edge 2 (t=15): fails at edge 4 (t=35).
  • start @55, from edge 6 (t=55): fails at edge 6 (t=55).
  • start @75, from edge 8 (t=75): is still running at the end of the run: weak, so no failure.

Sampled values, edge 1 to 10: start 0 1 0 0 0 1 0 1 0 0; v 0 1 1 0 0 0 0 1 1 1; r 0 0 0 1 0 1 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 hold_until_with 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 start = 0, v = 0, r = 0;

  hold_until_with: assert property (@(posedge clk) start |-> v until_with r);

  initial begin
    @(negedge clk); start = 1; v = 1;   // A starts at 15
    @(negedge clk); start = 0;
    @(negedge clk); v = 0; r = 1;       // r at 35, v already low
    @(negedge clk); r = 0;
    @(negedge clk); start = 1; r = 1;   // B starts at 55 with r already high
    @(negedge clk); start = 0; r = 0;
    @(negedge clk); start = 1; v = 1;   // C starts at 75; r never comes
    @(negedge clk); start = 0;
    @(negedge clk);
    @(negedge clk);
    $finish;                            // the test stops at 100
  end
endmodule

Lane A is the whole difference between the two. For a hold that must cover the transfer itself, such as data that must be right when rready is seen, only the overlapping form works: rvalid && !rready |=> (rvalid && $stable(rdata)) until_with rready.

The hold rule, edge by edge

  1. Edge 1: the offer stalls

    The antecedent rvalid && !rready matches, so an attempt starts. Because of |=>, the hold begins at the next edge.

  2. Edge 2: still stalled

    rvalid is high and rdata equals its value at edge 1, so $stable holds. rready is still low, so the attempt keeps going.

  3. Edge 3: the transfer

    rready is seen, so this is the last edge the attempt checks. Because the operator is until_with, rvalid and stable data must hold here as well.

Each stalled edge also starts its own attempt, so a three-edge stall is checked by overlapping attempts that all end at the transfer. A data change anywhere in the stall fails at least one of them.

When the event is already there

Lane B is the trap from the opening story. If q is true at the first edge of the attempt, until passes without checking anything, and until_with checks p on that single edge only. A leftover gnt, a sticky done or a level that stays high between transfers all end the hold before it starts.

Decide whether the first edge counts. If the event must be a new one, skip that edge: req throughout (##1 gnt[->1]) waits for the first gnt after the start edge. Writing the event as an edge, req until $rose(gnt), has the same effect when gnt is a level that stays high.

throughout

c throughout s requires the boolean c at every edge of a match of the sequence s, from its first edge to its last. s can be fixed, en throughout (##2 done), or a window, en throughout (##[1:3] d). Unlike the operands of until, which are properties, the left side here must be a plain boolean expression.

throughout over a windowOne lane per start for s |-> en throughout (##[1:3] d). Each delay of the window is a thread.s |-> en throughout (##[1:3] d)Measured on Verilator 5.052: one copy of the assertion per lane
attempt startspassfailthread dies; the attempt goes onthread dropped: the attempt has already passed
throughout over a windows @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)) passes at edge 4 (t=35); thread 3 (to edge 5 (t=45)) is dropped at edge 4 (t=35), because the attempt has already passed. s @55, from edge 6 (t=55): thread 1 (to edge 7 (t=65)) dies at edge 7 (t=65); thread 2 (to edge 8 (t=75)) fails at edge 8 (t=75); thread 3 (to edge 8 (t=75)) fails at edge 8 (t=75).edgetime152153254355456557658759851095clksendattemptss @15s @15: dies at edge 3 (t=25)s @15: passes at edge 4 (t=35)s @55s @55: dies at edge 7 (t=65)s @55: fails at edge 8 (t=75)s @55: fails at edge 8 (t=75)
  1. Edge 2 · t=15

    The start at 15 forks three threads, one per delay. Each needs en on every edge it covers.

  2. Edge 4 · t=35

    d arrives at 35 with en still high, so the two-edge thread passes and takes the whole attempt with it.

  3. Edge 6 · t=55

    A second start at 55 forks three new threads.

  4. Edge 7 · t=65

    No d at 65, so the shortest thread dies.

  5. Edge 8 · t=75

    en drops at 75. Both remaining threads fail on this edge, and so does the attempt, although d comes at 85.

Read the lanes as text
  • s @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)) passes at edge 4 (t=35); thread 3 (to edge 5 (t=45)) is dropped at edge 4 (t=35), because the attempt has already passed.
  • s @55, from edge 6 (t=55): thread 1 (to edge 7 (t=65)) dies at edge 7 (t=65); thread 2 (to edge 8 (t=75)) fails at edge 8 (t=75); thread 3 (to edge 8 (t=75)) fails at edge 8 (t=75).

Sampled values, edge 1 to 10: s 0 1 0 0 0 1 0 0 0 0; en 0 1 1 1 0 1 1 0 0 0; d 0 0 0 1 0 0 0 0 1 0.

The program behind the lanes

The lanes come from running this program on Verilator 5.052. Each lane is a separate copy of en_until_d 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 s = 0, en = 0, d = 0;

  en_until_d: assert property (@(posedge clk) s |-> en throughout (##[1:3] d));

  initial begin
    @(negedge clk); s = 1; en = 1;      // start at 15
    @(negedge clk); s = 0;
    @(negedge clk); d = 1;              // d at 35, en still high
    @(negedge clk); d = 0; en = 0;
    @(negedge clk); s = 1; en = 1;      // start at 55
    @(negedge clk); s = 0;
    @(negedge clk); en = 0;             // en drops at 75
    @(negedge clk); d = 1;              // d at 85: too late
    @(negedge clk); d = 0;
    @(negedge clk);
    $finish;
  end
endmodule

A window under throughout is a bounded hold: "en stays high, and d comes within three edges". It fails at a definite edge, which makes it the simulation-friendly cousin of s_until.

Over a goto, v throughout r[->1] gives the same verdicts as v until_with r. The example measures all five operators on one trace.

Because throughout builds a sequence, the standard also lets more sequence follow it. That use is not covered yet.

Choosing the operator

The spec saysWrite
p holds while waiting; the event edge itself is freep until q
p holds through the event edge as wellp until_with q or p throughout q[->1]
the event must come within N edgesp throughout (##[0:N] q)
the event must come before the test endsp s_until q, p s_until_with q, with a drain
the event may already be true at the startskip that edge: p throughout (##1 q[->1])

Prefer a bound when the specification gives one; it fails at a definite edge.

Keep "what must hold while waiting" and "how long the wait may last" as two rules. A weak hold rule plus a bounded latency rule, such as rvalid && !rready |-> ##[0:16] rready, gives two failure messages, and each names the requirement that broke. One strong rule that mixes both reports only at the end of the run, and says less.

Try it

Attempts A and B are the lanes of the figures. The non-overlapping forms pass both; the overlapping forms, and throughout over a goto, fail both. Attempt C here gets its r at 95 with v still high, so every operator passes it.

Delete the line that raises r at 95. The weak forms then stay silent, and Verilator stops at $finish with an error for the two strong ones.

RUNVerilator · simulator service
Edit the code, then run it
Expected: until failed at: none until_with failed at: 35 55 s_until failed at: none s_until_with failed at: 35 55 throughout failed at: 35 55

Check yourself

Predict the outputA beat stalls at 45, and at 55 r rises while v drops. Predict the exact output.
module tb;
  logic clk = 0;
  always #5 clk = ~clk;                  // rising edges at 5, 15, 25, ...
  logic v = 0, r = 0;
  p: assert property (@(posedge clk) v && !r |=> v until_with r)
    else $display("fail at %0t", $time);
  initial begin
    @(negedge clk); v = 1;               // stalled at 15
    @(negedge clk); r = 1;               // transfer at 25
    @(negedge clk); v = 0; r = 0;
    @(negedge clk); v = 1;               // stalled at 45
    @(negedge clk); v = 0; r = 1;        // valid gone at the ready edge, 55
    @(negedge clk); r = 0;
    #20 $finish;
  end
endmodule
ConceptSpec: "After a refresh request, the bank stays idle until refresh_done, and refresh_done must come before the test ends. The bank may leave idle on the done cycle itself." Which operator fits?
Read the waveformstart |-> v until r and start |-> v until_with r both check this trace. Which statement is true for the attempt started at edge 2?
One attempt
01234edge12345startvr

v is high on edges 2 to 4, r arrives at edge 4.

Spot the bugEach option is the hold property for a stalled beat. It must also catch data that changes exactly at the transfer edge. Which option misses that?
Spot the bugEach option checks "once req rises, it stays high until a gnt". In this test, gnt from the previous transfer is still high on the edge where req rises. Which option lets a request that drops early pass?

Remember

  • until leaves the edge of q free; until_with checks p there too.
  • The s_ forms also fail if q never comes before the end of the run; the plain forms are weak.
  • If q is already true at the first edge, until passes at once: decide whether that edge counts.
  • c throughout s needs c on every edge of s; over q[->1] it matches until_with, over a window it is a bounded hold.
  • Prefer a bounded hold when the specification gives a limit.

Practice