Why assertions

Turn sentences from a protocol spec into assertions that check every clock edge, and know when to use an immediate assertion and when a concurrent one.

Lesson
A1.1
Time
20 min
Runs on
Verilator
Check
5 questions

An interconnect dropped rvalid for one cycle while a read beat was stalled, then raised it again with the same data. The scoreboard still received every beat with the right value, so every test passed. In silicon, a downstream block that sampled the first offer lost the beat. The rule "once rvalid is high, it stays high until the transfer" was in the spec, but nothing in the testbench checked it.

One cycle of rvalid missing during a stall
01234edge12345rvalidrreadyrdatab2attempt starts: stalled offerhold_valid fails

The data arrives in the end, so a scoreboard is satisfied. Only a rule about each edge sees the gap.

An assertion is a spec sentence that runs

An assertion states a condition the design must meet and reports every time it does not. Written well, it reads like the sentence in the spec it came from, and it checks that sentence on every relevant clock edge of every test.

Spec sentenceAssertion
Once rvalid is high, it stays high until a transfer.rvalid && !rready |=> rvalid
rlast is only meaningful while rvalid is high.rlast |-> rvalid
Every transfer must have an expected value waiting.assert (exp_q.size() > 0) at each transfer
A read is answered within 16 cycles.arvalid && arready |-> ##[1:16] rvalid

Read-channel rules and the assertions that check them. Later lessons explain each operator.

A scoreboard checks that the right data arrived. Assertions check how it arrived: the protocol rules at each edge. A test needs both, because traffic can carry the right data in a way the spec forbids.

What the scoreboard sees, and what the assertion seesThe opening story as two observers of one read channel. Arrows show what each box observes: the monitor sees completed transfers, the assertion sees the pins at every edge.
What the scoreboard sees, and what the assertion seesThe opening story as two observers of one read channel. Arrows show what each box observes: the monitor sees completed transfers, the assertion sees the pins at every edge. design Interconnect Offers beat b2 onrvalid, rdata testbench Monitor Records eachtransfer testbench Scoreboard Compares data withthe expected value assertion hold_valid rvalid && !rready |=>rvalid component Failure report Names the rule andthe edge transfersbeatpins, every edgefails
  1. A stalled offer

    The interconnect offers b2 and the sink is not ready. The assertion samples the pins at this edge and starts an attempt: rvalid must still be high at the next edge.

    Follow the calls, values and waits
    1. data b2 offered, sink stalls: Interconnect → hold_valid
  2. rvalid drops for one edge

    At the next edge rvalid is low while the beat is unfinished. The attempt fails and reports the rule and the time.

    Follow the calls, values and waits
    1. data rvalid low mid-stall: Interconnect → hold_valid
    2. data hold_valid failed: hold_valid → Failure report
  3. The beat transfers anyway

    Later, b2 transfers with the right data. The monitor records one transfer and the scoreboard finds a match: from where it stands, nothing went wrong.

    More context

    A downstream block that sampled the first offer would have lost the beat. Only a rule about each edge can see that.

    Follow the calls, values and waits
    1. data beat b2: Interconnect → Monitor
    2. data beat b2: Monitor → Scoreboard
Read the diagram as text
  • Interconnect: Offers beat b2 on rvalid, rdata
  • Monitor: Records each transfer
  • Scoreboard: Compares data with the expected value
  • hold_valid: rvalid && !rready |=> rvalid
  • Failure report: Names the rule and the edge
  • Interconnect → Monitor: transfers
  • Monitor → Scoreboard: beat
  • Interconnect → hold_valid: pins, every edge
  • hold_valid → Failure report: fails

The scoreboard checks what arrived; the assertion checks how it arrived.

Immediate assertions

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

Immediate assertions suit checks that belong to one moment in testbench code: a scoreboard lookup, a configuration value, a function argument. They know nothing about clock edges or about the cycles before and after.

The standard also has deferred forms, assert #0 and assert final, meant for combinational checks whose inputs settle during a time step. They are not covered yet.

Concurrent assertions

A concurrent assertion runs on its own, beside the design. hold_valid: assert property (@(posedge clk) rvalid && !rready |=> rvalid); is checked at every rising edge of clk, for the whole simulation, whether or not any testbench code is looking.

Concurrent assertions can describe behaviour across clock cycles, which is what most protocol rules are about. They read every signal from just before the edge, so they never race the design; the next lesson shows exactly how.

Each assertion has a label, such as hold_valid, that names it in every report. Choose labels that say the rule, because a failure report is often the first thing someone reads about a bug.

Action blocks and severity

After the property comes an optional pass action and, after else, a fail action. The fail action usually calls a severity task: $error for a broken rule, $fatal when continuing makes no sense, $warning or $info for softer notes.

Without a fail action, a failing assertion calls $error with a message of the simulator's own wording. Writing your own message with the time and the values involved makes the report useful on its own.

Assert, assume and cover

assert property says a rule must hold, and reports when it does not. assume property says the environment will behave in a certain way; formal tools use it to rule out impossible inputs, and simulators check it like an assertion.

cover property records that something happened, such as a stall of three cycles or an error response. Covers answer a different question. They ask whether the tests ever reached a situation, not whether a rule was broken.

Where assertions live

Designers put assertions inside RTL to state their own assumptions, such as "this FIFO pointer never passes that one". Verification engineers put protocol rules in the interface or in a separate checker module bound to the design.

Both kinds matter. Internal assertions catch a broken invariant at its source, while protocol assertions check every block that uses the interface, including blocks bought from someone else.

What assertions buy you

An assertion fails at the cycle where the rule broke, not thousands of cycles later at the scoreboard. That alone often turns a day of tracing back through a waveform into a few minutes.

Assertions can watch signals inside the DUT that never reach its outputs, through bind, which lesson A7.1 covers. The same properties also feed formal verification tools, which try to prove them for every possible input instead of only the ones a test happens to drive.

Try it

The concurrent assertions caught the dropped rvalid at the edge after the stall and the stray rlast at the edge where it appeared. The immediate assertion in the scoreboard caught the beat that arrived with nothing expected.

Each report names the rule and the time, so it points straight at the edge to inspect. Delete the line that drops rvalid and predict which reports remain.

RUNVerilator · simulator service
Edit the code, then run it
Expected: t=45 hold_valid failed t=55 have_exp failed: no expectation for b2 t=65 last_needs_valid failed

Check yourself

Predict the outputRising edges are at 5, 15, 25 and so on, and stimulus changes at falling edges. Predict the exact output.
module tb;
  logic clk = 0;
  always #5 clk = ~clk;                  // rising edges at 5, 15, 25, ...
  logic rvalid = 0, rlast = 0;
  last_ok: assert property (@(posedge clk) rlast |-> rvalid)
    else $display("fail at %0t", $time);
  initial begin
    @(negedge clk); rvalid = 1;
    @(negedge clk); rlast = 1;
    @(negedge clk); rvalid = 0;
    @(negedge clk); rlast = 0;
    #20 $finish;
  end
endmodule
ConceptWhich check is best written as a concurrent assertion rather than an immediate one?
ConceptA test passes and the scoreboard received every beat with the right data. What can an assertion still catch?
Read the waveformhold_valid: assert property (@(posedge clk) rvalid && !rready |=> rvalid); checks this trace. At which edge does it fail?
Sampled values at each rising edge
01234edge12345rvalidrready

A transfer happens where both are 1.

Spot the bugEach option is the property for the rule "rlast is only allowed while rvalid is high". Which option is wrong?

Remember

  • An assertion turns a spec sentence into a check that runs on every relevant edge of every test.
  • Immediate assertions check a condition once, when a procedural statement runs.
  • Concurrent assertions run beside the design and describe behaviour across clock edges.
  • Label every assertion after its rule, and write a fail message with the time and the values.
  • Assertions catch protocol breaks that still deliver the right data to the scoreboard.