Repetition and counting events

Say that a level holds for n edges with [*n], know what a repetition range at the end of a sequence really checks, and count protocol events with a small auxiliary model.

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

A four-beat burst must end with rlast on its fourth beat. The first check, req |-> ##4 rlast, looked four edges after the request and passed in every test without backpressure. When a sink started stalling the channel, it failed on correct bursts. After it was relaxed to stop the noise, a DUT that ended stalled bursts one beat early passed straight through.

Counting edges or counting transfers
012345edge123456reqrvalid && rreadyrlastattempt starts##4: rlast required here, fails4th transfer: rlast correct

One stall at edge 3 moves the fourth transfer to edge 6, but ##4 still looks at edge 5.

Consecutive repetition

b[*n] means b holds at n consecutive edges, the same as b ##1 b ##1 ... written out. $rose(rvalid) |-> rvalid[*4] says that once rvalid rises, it stays high for at least four edges, which every four-beat burst needs.

Whole sequences can repeat too: (rvalid ##1 !rvalid)[*2] describes valid going high and low twice in a row. Use this sparingly, because repeated sequences quickly become hard to read in a waveform.

A range at the end checks only its lower bound

b[*m:n] allows from m to n repetitions. At the end of a consequent it behaves like a delay range: the attempt passes at its first match, the shortest one. req |=> busy[*2:4] therefore passes as soon as busy has been high for two edges, and it still passes when busy stays high for six.

req |=> busy[*2:4] on two requests
012345678910edge (time)152535455565758595105115reqbusypasses: two edges of busysix edges: no failureone edge only: fails

Measured in the predict question below: the only failure is at 115. Six edges of busy are not reported.

So the upper bound 4 checks nothing here. If the spec says "busy for two to four edges", the "at most four" half needs a rule of its own.

The upper bound needs its own rule

A fixed repetition can be a trigger. busy[*4] |=> !busy matches wherever busy has been high for four edges in a row, and then requires it low at the next edge. Each edge starts a new attempt, so a long stretch of busy starts several overlapping ones.

Four edges of busy, then it must dropOne lane per starting edge for busy[*4] |=> !busy, on a stretch of six busy edges from 25 to 75.busy[*4] |=> !busyMeasured on Verilator 5.052: one copy of the assertion per lane
attempt startspassfail
Four edges of busy, then it must dropfrom @25, from edge 3 (t=25): fails at edge 7 (t=65). from @35, from edge 4 (t=35): fails at edge 8 (t=75). from @45, from edge 5 (t=45): passes at edge 9 (t=85).edgetime152153254355456557658759851095111051211513125clkbusyattemptsfrom @25from @25: fails at edge 7 (t=65)from @35from @35: fails at edge 8 (t=75)from @45from @45: passes at edge 9 (t=85)
  1. Edge 3 · t=25

    busy rises at 25, and an attempt starts matching four edges of it: 25, 35, 45 and 55.

  2. Edge 6 · t=55

    Four attempts are in flight, from 25, 35, 45 and 55; the figure follows the first three.

  3. Edge 7 · t=65

    The attempt from 25 has seen four edges of busy, so busy must be low at 65. It is high, and that attempt fails.

  4. Edge 8 · t=75

    The attempt from 35 fails at 75 for the same reason: the fifth and sixth edges each add one failure.

  5. Edge 9 · t=85

    busy drops at 85, and the attempt from 45 passes there. Attempts from later edges never see four edges of busy, so they succeed vacuously.

    More context

    A stretch of six busy edges gives two failures. Debug from the first one; the second reports the same stretch.

Read the lanes as text
  • from @25, from edge 3 (t=25): fails at edge 7 (t=65).
  • from @35, from edge 4 (t=35): fails at edge 8 (t=75).
  • from @45, from edge 5 (t=45): passes at edge 9 (t=85).

Sampled values, edge 1 to 13: busy 0 0 1 1 1 1 1 1 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 at_most_4 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, busy = 0;

  at_most_4: assert property (@(posedge clk) busy[*4] |=> !busy);

  initial begin
    @(negedge clk); req = 1;            // request sampled at 15
    @(negedge clk); req = 0; busy = 1;  // busy sampled at 25, 35, ... 75
    repeat (6) @(negedge clk);
    busy = 0;
    @(negedge clk); req = 1;            // request sampled at 95
    @(negedge clk); req = 0; busy = 1;  // busy only at 105
    @(negedge clk); busy = 0;
    repeat (2) @(negedge clk);          // edges at 115 and 125
    $finish;
  end
endmodule

One failure per edge beyond the limit, and a pass for the attempt whose fourth edge is the last one.

Counting edges or counting events

Ask what the spec counts. Repetition counts clock edges, so it suits levels: "valid stays high for four edges", "busy never lasts more than four". A delay counts edges too.

Protocol rules usually count events: transfers, beats, grants. Written as an edge count, such a rule is right only while nothing stalls, which is exactly the trap in the opening story.

The standard has event-counting forms, the goto repetition [->n] and the non-consecutive repetition [=n]. They are not covered yet: Verilator 5.052 misses goto failures and rejects [=n]. Count the events in a few lines of ordinary code instead.

Counting with auxiliary code

A counter that resets on each request and increments on each transfer turns "the fourth transfer carries rlast" into xfer && beat == 3 |-> rlast, with stalls anywhere.

Such auxiliary code is common in real assertion suites. Keep it small and obviously correct, update it with nonblocking assignments at the clock, and remember that assertions see its value from before the edge, like any other signal.

Count edges

assert property (@(posedge clk)
  req |-> ##4 rlast);

Correct only without stalls; fails good bursts and can pass bad ones.

Count transfers

// beat: transfers since the request
assert property (@(posedge clk)
  xfer && beat == 3 |-> rlast);
assert property (@(posedge clk)
  xfer && beat < 3 |-> !rlast);

Checks the fourth transfer, wherever stalls put it.

A counter turns transfers into an edge conditionBoxes are the design's pins, the auxiliary counter and the two rules from the example. Arrows show which value each box reads.
A counter turns transfers into an edge conditionBoxes are the design's pins, the auxiliary counter and the two rules from the example. Arrows show which value each box reads. design Read channel pins req, xfer = rvalid &&rready, rlast auxiliary beat counter 0 on req, +1 on eachxfer assertion last_on_4th xfer && beat == 3|-> rlast assertion no_early xfer && beat < 3 |->!rlast component Failure report One line per brokenrule req, xferbeatbeatfailsfails
  1. A request

    A request resets beat to 0. The counter is ordinary code, updated with a nonblocking assignment at the clock.

    Follow the calls, values and waits
    1. data req: beat = 0: Read channel pins → beat counter
  2. Early transfers

    Each transfer adds one. While beat is below 3, no_early requires rlast low on every transfer.

    Follow the calls, values and waits
    1. data two transfers: beat = 2: Read channel pins → beat counter
    2. data beat = 2 sampled: beat counter → no_early
  3. A stall

    A stalled edge has no transfer, so beat holds. An edge count moves on here; the counter does not.

  4. The fourth transfer

    At the fourth transfer the sampled beat is 3, so last_on_4th requires rlast, wherever the stalls put that edge.

    More context

    The assertion reads beat from before the edge, so at the fourth transfer it sees 3, the count of the transfers before this one.

    Follow the calls, values and waits
    1. data beat = 3 sampled: beat counter → last_on_4th
  5. A burst with rlast too early

    In the faulty burst, rlast came on the third transfer. no_early fails at 185, and last_on_4th fails at 195 because rlast is gone at the fourth.

    Follow the calls, values and waits
    1. data no_early at 185: no_early → Failure report
    2. data last_on_4th at 195: last_on_4th → Failure report
Read the diagram as text
  • Read channel pins: req, xfer = rvalid && rready, rlast
  • beat counter: 0 on req, +1 on each xfer
  • last_on_4th: xfer && beat == 3 |-> rlast
  • no_early: xfer && beat < 3 |-> !rlast
  • Failure report: One line per broken rule
  • Read channel pins → beat counter: req, xfer
  • beat counter → last_on_4th: beat
  • beat counter → no_early: beat
  • last_on_4th → Failure report: fails
  • no_early → Failure report: fails

Two rules over one counter: one for the beat that must carry rlast, one for every beat that must not.

The second rule matters as much as the first. Without !rlast on earlier beats, a design that raises rlast on every beat passes.

Try it

The fixed-delay check failed the correct stalled burst at 115. On the faulty burst, the stall happened to put rlast exactly four edges after the request, so the fixed delay passed it. The transfer-counting rules caught the early rlast at 185 and the missing one at 195.

four_edges passed every burst, as it should. Change it to rvalid[*5] and predict which bursts fail it.

RUNVerilator · simulator service
Edit the code, then run it
Expected: t=115 fixed_delay failed t=185 no_early failed t=195 last_on_4th failed

Check yourself

Predict the outputbusy is high for six edges after the first request and for one edge after the second. Predict the exact output.
module tb;
  logic clk = 0;
  always #5 clk = ~clk;                  // rising edges at 5, 15, 25, ...
  logic req = 0, busy = 0;
  p: assert property (@(posedge clk) req |=> busy[*2:4])
    else $display("fail at %0t", $time);
  initial begin
    @(negedge clk); req = 1;             // request at 15
    @(negedge clk); req = 0; busy = 1;   // busy at 25 ... 75
    repeat (6) @(negedge clk);
    busy = 0;
    @(negedge clk); req = 1;             // request at 95
    @(negedge clk); req = 0; busy = 1;   // busy at 105 only
    @(negedge clk); busy = 0;
    repeat (2) @(negedge clk);
    $finish;
  end
endmodule
ConceptWhat does req |=> busy[*2:4], at the end of the consequent, actually require?
ConceptAfter a request, the fourth transfer must carry rlast, however many stalls occur. beat counts the transfers since the request. Which check states the rule?
Read the waveformbusy[*4] |=> !busy checks this trace. Which failures does it report?
Sampled values at each rising edge
01234567edge12345678busy

busy is high for five edges, 2 to 6.

Spot the bugEach option is the property for "once rvalid rises, it stays high for at least four edges". Which option misses a burst that drops after three?

Remember

  • b[*n] is b at n consecutive edges; use it for levels that must hold.
  • A repetition range at the end of a sequence passes at its shortest match, so its upper bound checks nothing.
  • Check an upper bound with its own rule, such as busy[*4] |=> !busy.
  • Protocol rules count events such as transfers, not clock edges.
  • A small auxiliary counter turns an event-counting rule into a plain implication; check the earlier events too.