Assertions and protocol checks

Check the handshake rule at the edge where it breaks, with one immediate assertion and one concurrent assertion.

Lesson
6.6
Time
25 min
Runs on
Verilator
Check
5 questions

A scoreboard reported a data mismatch 3,000 cycles into a test. The real cause was much earlier: the source changed rdata while a beat was stalled, so the sink captured a different value from the one the scoreboard expected. Tracing back from the mismatch took a day. A two-line assertion would have stopped the test at the exact edge where the rule broke.

Data that moves during a stall
0123rvalidrreadyrdata222324hold_offerokFAILokrdata changed while stalled

The assertion fails at the first edge after the illegal change, thousands of cycles before the scoreboard would.

Two kinds of assertion

An assertion states a condition that must hold and reports when it does not. SystemVerilog has two kinds, and a testbench uses both.

An immediate assertion is a statement in procedural code, such as assert (exp_q.size() > 0) else $error(...);. It checks its condition once, at the moment the statement runs, like an if with a built-in failure report.

A concurrent assertion runs on its own, beside the design, and is checked at every clock edge. It can describe behaviour over several cycles, such as "if an offer is stalled at this edge, it must still be there at the next one".

An immediate assertion in the scoreboard

Immediate assertions suit checks that belong to a particular moment in testbench code. At each transfer, the scoreboard can assert that an expectation is waiting, which catches a response that nobody asked for.

The else part is the action block. It runs only on failure, and it should print the time and the values that matter, as Module 1 recommends for every message. A label, as in have_exp: assert ..., names the assertion in reports.

A concurrent assertion for the handshake

The source's rule from lesson 6.5 becomes one property: (rvalid && !rready) |=> (rvalid && $stable(rdata)). Each part has a precise meaning.

PartMeaning
@(posedge clk)check at every rising edge
disable iff (!rst_n)ignore the property while reset is active
rvalid && !rreadythe trigger, called the antecedent: a beat is offered but stalled
|=>then, at the next edge, the consequent that follows must hold
rvalid && $stable(rdata)the offer is still there and rdata equals its value at the previous edge

Reading the hold property.

|-> would check the consequent at the same edge as the trigger; |=> checks it one edge later, which is what the rule needs. At edges where the trigger is false, the property passes without checking anything.

Sampled values, again

A concurrent assertion reads every signal in the Preponed region, the same pre-edge values a #1step clocking block samples. It evaluates in the Observed region, after the design has settled, so it never races the DUT.

$stable(rdata) compares the sampled value at this edge with the sampled value at the previous edge. Related functions such as $past, $rose and $fell work the same way, on sampled values.

Make sure it can fail

An assertion that never fails might be correct, or it might be checking nothing. Run it once against stimulus that breaks the rule on purpose, as the example does, and confirm that it reports the right edge.

Checks only that valid holds

assert property (@(posedge clk)
  (rvalid && !rready) |=> rvalid);

Passes even when rdata changes during the stall, which is the bug in the opening story.

Checks valid and data

assert property (@(posedge clk)
  disable iff (!rst_n)
  (rvalid && !rready) |=> (rvalid && $stable(rdata)));

Fails at the first edge after rdata moves during a stall.

Also make sure the trigger happens at all. A property whose antecedent is never true passes on every edge; a counter or a cover statement shows whether stalls really occurred.

Reading a failure report

A concurrent assertion reports its failure at the edge where the consequent was false, which for |=> is one edge after the trigger. Look one cycle earlier in the waveform to find the stalled offer that started the check.

Values printed in the action block are the current values, which may already have moved on. Print $sampled(rdata) to show the value the assertion actually used.

Choosing between the two kinds

Use an immediate assertion for a condition that belongs to one moment in procedural code, such as a scoreboard lookup or a configuration check. It costs nothing between the times its statement runs.

Use a concurrent assertion for a rule about signals over clock cycles, such as a handshake. It runs on every edge, whether or not any testbench code is looking, which is exactly what makes it catch problems early.

Where to go next

This lesson used one property and two operators. The Assertions track covers sequences, the full set of operators, sampled-value functions, local variables and checkers in depth.

Keep protocol assertions in the interface or a bound checker module, next to the signals they watch, so that every test using the interface gets them without extra work.

Try it

The first beat was held through its stalls and passed. The second changed rdata twice while stalled, and hold_offer reported each change at the next rising edge. The third beat arrived with no expectation queued, so the immediate assertion reported it.

Change the property's |=> to |-> and predict which failures still appear, and when.

RUNVerilator · simulator service
Edit the code, then run it
Expected: t=75 hold_offer failed t=85 hold_offer failed t=105 have_exp failed: unexpected transfer 33

Check yourself

Predict the outputStimulus changes at falling edges; rising edges are at 5, 15, 25 and so on. Predict the exact output.
module tb;
  logic clk = 0;
  always #5 clk = ~clk;
  logic v = 0, r = 0;
  logic [7:0] d = 0;
  a: assert property (@(posedge clk) (v && !r) |=> (v && $stable(d)))
    else $display("fail at %0t", $time);
  initial begin
    @(negedge clk); v = 1; d = 8'h05;
    @(negedge clk);
    @(negedge clk); d = 8'h06;
    @(negedge clk); r = 1;
    @(negedge clk); v = 0; r = 0;
    #20 $finish;
  end
endmodule
ConceptWhat is the difference between a |-> b and a |=> b in a property clocked on posedge clk?
ConceptA concurrent assertion and the DUT both look at rdata at the same rising edge. Which value does the assertion use?
Read the waveformThe property (rvalid && !rready) |=> (rvalid && $stable(rdata)) checks this trace. At which edge does it first fail?
Sampled values at each rising edge
01234rvalidrreadyrdata5a6b

One value per rising edge.

Spot the bugEach option is the property expression inside assert property (@(posedge clk) ...). It must catch a source that changes rdata during a stall, and stay silent for a source that holds it. Which option misses the bug?

Remember

  • An immediate assertion checks a condition when a procedural statement runs; a concurrent assertion checks a property at every clock edge.
  • (rvalid && !rready) |=> (rvalid && $stable(rdata)) states the source's hold rule.
  • |-> checks at the same edge as the trigger; |=> checks at the next edge.
  • Concurrent assertions use Preponed samples and evaluate in Observed, so they do not race the design.
  • Prove each assertion can fail, and check that its trigger actually occurs.

Practice