Assertions in procedures, sequences as events
Place a concurrent assertion inside the RTL's own always block so it follows the design's decode, know where its clock and enable come from, and let a sequence wake procedural code.
- Lesson
- A5.1
- Time
- 25 min
- Runs on
- Verilator
- Check
- 6 questions
An arbiter's RTL already decoded "the machine is busy" in its case statement. The verification engineer wrote the one-grant rule as a module-level assertion and copied that decode into its antecedent. When the RTL gained a second busy state, nobody updated the copy, and the rule silently stopped checking half of the busy cycles.
An assertion inside the decode
A concurrent assertion can sit inside an always or initial procedure. It is still a concurrent assertion: it samples values at the clock edge and reports like any other. What changes is when attempts start. An attempt starts only at the edges where the procedure's flow reaches the statement.
BUSY branch is the enabling condition of one_grant.always_ff @(posedge clk or negedge rst_n)
if (!rst_n) state <= IDLE;
else case (state)
IDLE: if (start) state <= BUSY;
BUSY: begin
one_grant: assert property ($onehot({gnt_a, gnt_b}));
if (last) state <= IDLE;
end
endcaseThe rule now follows the RTL's decode. A new busy state that reaches this branch is checked without anyone editing the assertion. The statement does not block the procedure either: it starts an attempt, and the block carries on with its own assignments at once.
The reset branch is part of the path as well. While rst_n is low, the procedure takes the reset branch and never reaches the assertion, so this one-edge rule needs no disable iff. The module-level copy below has to say disable iff (!rst_n) itself.
Where the clock comes from
A procedural assertion with no clock of its own takes one from the procedure's event control. always @(posedge clk) gives @(posedge clk). In the block above, rst_n is also read in the body, so it cannot be the clock, and the clock is posedge clk again.
The inference needs exactly one edge event that the body does not otherwise use. A sensitivity list without an edge, such as @(clk), gives no clock, and so do two candidate edges that the body never reads. Write @(posedge clk) inside the assertion then.
Sampled-value functions in procedural code use the same inferred clock. if ($rose(start)) compares the sampled values of two edges and counts each rise once, while if (start) counts every edge where the level is high. The first quiz question measures both.
Same rule, two places
Inside the procedure
BUSY: begin
one_grant: assert property
($onehot({gnt_a, gnt_b}));
...
Clock, reset and enable come from the block. It follows the RTL when the decode changes.
At module level
one_grant_m: assert property (
@(posedge clk) disable iff (!rst_n)
state == BUSY |-> $onehot({gnt_a, gnt_b}));
Everything is written out. It stays the same whatever the RTL does.
The example runs both on the same traffic, and they report the same failure. The choice is about what the rule describes. A rule about one branch of this implementation belongs next to that branch. A rule about the interface, which any implementation must obey, belongs at module level or in a bound checker, where an RTL rewrite cannot change what it checks.
Procedural placement has a cost as well. The rule now lives in the RTL file, where the RTL designer reviews, edits and sometimes deletes it, so agree with the design team on which rules live where.
Sequences as events
SVA can also drive procedural code. A named sequence with its own clock can be an event: always @(s_ab) wakes at every end point of s_ab, once per edge where a match completes.
a for a |-> ##2 b. Each pass is an end point of s_ab = a ##2 b; the program also prints each wake-up of always @(s_ab).- Edge 2 · t=15
aat 15 starts a match ofa ##2 b. - Edge 3 · t=25
aagain at 25: a second match overlaps the first. - Edge 4 · t=35
bat 35 completes the first match.always @(s_ab)wakes at 35. - Edge 5 · t=45
bat 45 completes the second match: another wake-up. - Edge 7 · t=65
aat 65 starts a third match. - Edge 9 · t=85
bis low at 85, so this match dies. The lane fails, and the block does not wake.
Read the lanes as text
- a @15, from edge 2 (t=15): passes at edge 4 (t=35).
- a @25, from edge 3 (t=25): passes at edge 5 (t=45).
- a @65, from edge 7 (t=65): fails at edge 9 (t=85).
Sampled values, edge 1 to 9: a 0 1 1 0 0 0 1 0 0; b 0 0 0 1 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 b_two_later 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 a = 0, b = 0;
sequence s_ab; @(posedge clk) a ##2 b; endsequence
b_two_later: assert property (@(posedge clk) a |-> ##2 b);
always @(s_ab) $display("s_ab ended at %0t", $time);
initial begin
@(negedge clk); a = 1; // a at 15 and 25
@(negedge clk);
@(negedge clk); a = 0; b = 1; // b at 35 and 45
@(negedge clk);
@(negedge clk); b = 0;
@(negedge clk); a = 1; // a at 75
@(negedge clk); a = 0; // no b at 85
@(negedge clk);
@(negedge clk);
$finish;
end
endmodule
The wake-up comes at the edge where a match ends, so the block runs once per completed pattern, however many edges the pattern spans.
In a testbench this turns a protocol pattern into a trigger. Count completed handshakes, sample coverage when a burst ends, or hold a stimulus thread until the DUT has finished one, with @(s_done); in an initial block. Keep these sequences fixed-length here.
Assertions can wake procedures from their action blocks too. else -> gnt_late; triggers a named event, and always @(gnt_late) can count it, print the state around it, or stop the stimulus.
Procedural assertions in loops, .triggered, and wait on a sequence are not covered yet.
Try it
Both forms failed only at 55, where both grants were high in BUSY. At 85 both were high again, but the machine was in IDLE: the procedure never reached the assertion, and the module-level antecedent was false.
Delete if (last) state <= IDLE; so the machine stays busy, and predict both lines.
procedural failed at: 55 module-level failed at: 55Check yourself
Remember
- A procedural concurrent assertion starts attempts only at edges where the procedure reaches it.
- Its clock comes from the one edge event the body does not otherwise use, such as
posedge clkin an always_ff with an asynchronous reset. - Put rules about one branch of this RTL inside it; put interface rules at module level or in a bound checker.
- On Verilator 5.052, keep procedural assertions to one-edge checks and write multi-edge rules at module level.
always @(seq)wakes once per end point of a fixed-length sequence; an action block can trigger an event.