Capstone: an assertion suite

Plan and write an assertion suite for the problem bank's FIFO that fires on every faulty design and stays silent on the correct one.

Lesson
A8.1
Time
30 min
Runs on
Verilator
Check
5 questions

A FIFO kept read_valid high after its first read. The scoreboard compared read_data only while read_valid was 1, and the stale item matched what it expected, so the fault passed for weeks. One rule, "no edge raises read_valid unless the edge before it accepted a read", fails at the first edge where the flag stays high with nothing to answer.

A FIFO whose read_valid sticks at 1
012345edge123456read_enused210read_validno_extra failsno_extra fails

Sampled values at each rising edge; used is the count before the edge. Reads are accepted at edges 2 and 5, so only edges 3 and 6 may show read_valid.

What the Verify problems ask

The problems that follow this lesson give you a design, a stimulus that you do not control, and a bank of variants: one correct design and several faulty ones. You write the assertions.

Your suite is scored on two counts. It must report at least one failure on every faulty variant, and it must report none on the correct design. A suite that fails the correct design is wrong, however many faults it catches.

The FIFO under test

The design is hardware_fifo, the problem bank's FIFO, which Module 7 built and checked with a scoreboard. It has DEPTH entries of WIDTH bits. The data pins are 32 bits wide, and only the low WIDTH bits are stored.

At each rising edge, both requests are decided from the number of items held before the edge. A write is accepted when the FIFO is not full, and a read when it is not empty. An accepted read is answered after that edge: read_valid is 1 for one cycle, and read_data holds the oldest item.

reset is synchronous and active high. At a reset edge the FIFO empties, clears read_valid and accepts nothing. The problem bank's fixture also pulses an initialize pin once at the start, which clears everything as a reset does.

From spec to rule list

Start from the specification, one sentence at a time, exactly as lesson A1.1 did. Each sentence becomes one labelled assertion, and each assertion uses the smallest set of operators that states it.

FIFO spec sentenceLabelAssertion
full and empty are never both 1.not_both!(full && empty)
The flags match the number of items held.flags_matchfull == (n == DEPTH) && empty == (n == 0)
An accepted read is answered at the next edge.answeredtake_r |=> read_valid
No edge raises read_valid without a read to answer.no_extra!take_r |=> !read_valid
The answer is the oldest item held.in_ordertake_r |=> read_data == exp_data
Reset empties the FIFO and cancels any answer.reset_clearsreset |=> empty && !read_valid

The rule list for hardware_fifo. n, take_r and exp_data come from a small auxiliary model, described below.

Every rule except the last carries disable iff (initialize || reset), because a reset edge legally breaks it. The reset rule checks reset itself, so only initialize disables it, as lesson A3.5 recommended.

Two rules look one edge ahead with |=>, because the FIFO registers its answer. A rule written with |-> would compare the answer at the same edge as the read, and fail on the correct design.

What a fault bank contains

Each faulty variant breaks one sentence of the specification, in the way real designs break. Knowing the usual faults helps you check that your rule list covers them.

Typical faultRule that exposes it
one bit of read_data flippedin_order
read_valid stuck at 1 after a readno_extra
read_valid never risesanswered
every answer one edge lateanswered, no_extra, in_order
a read and a write together return the new itemin_order
a pointer that wraps at a power of two instead of at DEPTHin_order, once the stimulus completes a lap

Faults of the kind the problem bank contains, and the rules that expose them.

A fault can only be caught where the stimulus exposes it. A pointer that wraps in the wrong place does no harm until the FIFO has taken more writes than it has slots.

A model for the data rules

The rules need to know how many items the FIFO should hold and which item a read should return. A few lines of auxiliary code keep that: a count n, a queue of the values written, and exp_data, the item the last accepted read removed.

The model decides acceptance from its own count, take_r = read_en && n > 0, exactly as the specification does. It never reads the FIFO's internal used or its pointers, even though a bound module could. A model that copies the design's state copies its bugs too.

Update the model with nonblocking assignments at the clock, as lesson A2.2 advised. At the edge after a read, the sampled exp_data then holds the item that read removed, ready to compare with read_data.

Checking the suite itself

Every assertion should be shown to fail at least once, on purpose. Bind the suite to a faulty variant, or force a violation in a small test, and check that the right rule fires at the right edge.

Check that every antecedent matches during the tests, as lesson A1.3 showed. A rule that never triggers scores nothing, because it cannot catch a fault.

A rule that only works on clean traffic

// no reset guard, antecedent too wide
assert property (@(posedge clk)
  read_en |=> read_valid);

Fails the correct FIFO whenever a read is refused because it is empty, or arrives with a reset.

A rule stated exactly

assert property (@(posedge clk)
  disable iff (initialize || reset)
  take_r |=> read_valid);

Triggers only on accepted reads, and only outside reset.

Working inside the simulator's limits

The problems run on the same simulator as this track, so the same runnable subset applies. Put any waiting at the end of the consequent with ##[m:n], until_with or s_eventually, and keep antecedents of fixed length. Write one implication per rule, and count events with auxiliary counters rather than gotos. A submission outside that subset is refused before it runs.

Report each failure through the fail action, as the examples do. The problems count failures per variant, so each assertion only needs to fail; it does not need to print anything in particular.

An order of work

  1. List the rules

    Write one line per spec sentence, in plain words, before writing any SystemVerilog.

  2. Write and label the assertions

    Use the smallest operator set for each rule, with a reset guard only where reset can break it.

  3. Run on the correct design

    The suite must be silent. Any failure here is a rule that disagrees with the spec, so fix the rule first.

  4. Run on known faults

    Break the design on purpose, one sentence at a time, and check that the matching rule fires at the right edge.

  5. Count the triggers

    Check that every antecedent matched at least once, so that no rule passed only vacuously.

Reading a variant's result

When a variant is caught, look at which rule fired first and at what edge. The first failure usually names the fault, while later failures are its consequences, such as the wrong data that follows a pointer wrapping in the wrong place.

When a variant is missed, ask which spec sentence it breaks and whether your suite states that sentence at all. Missing rules are far more common than wrongly written ones.

Try it

The same suite, bound to all four FIFOs, stayed silent on the correct one. Fault 2 left read_valid high after the last read, so no_extra failed at the two edges before the reset cleared it. Fault 4 answered late: answered failed for the two reads that did not follow another read, and in_order for all five.

Fault 6 sent the fourth write to a spare slot, so the read of 0x15 returned the stale 0x10 and only in_order saw it. Delete the op(1, 1, 'h15) line and predict the fault 6 line.

RUNVerilator · simulator service
Edit the code, then run it
Expected: fault 0: not_both=0 flags_match=0 answered=0 no_extra=0 in_order=0 reset_clears=0 fault 2: not_both=0 flags_match=0 answered=0 no_extra=2 in_order=0 reset_clears=0 fault 4: not_both=0 flags_match=0 answered=2 no_extra=1 in_order=5 reset_clears=0 fault 6: not_both=0 flags_match=0 answered=0 no_extra=0 in_order=1 reset_clears=0

Check yourself

Predict the outputA read is accepted at the edge at 15, and its answer appears one edge late, at 35. Predict the exact output.
module tb;
  bit clk = 0;
  always #5 clk = ~clk;                  // rising edges at 5, 15, 25, ...
  bit take_r = 0, read_valid = 0;        // stimulus changes at falling edges
  int f_answer, f_extra;
  answered: assert property (@(posedge clk) take_r |=> read_valid) else f_answer++;
  no_extra: assert property (@(posedge clk) !take_r |=> !read_valid) else f_extra++;
  initial begin
    @(negedge clk); take_r = 1;          // a read is accepted at 15
    @(negedge clk); take_r = 0;
    @(negedge clk); read_valid = 1;      // its answer is seen at 35
    @(negedge clk); read_valid = 0;
    #2 $display("answered=%0d no_extra=%0d", f_answer, f_extra);
    $finish;
  end
endmodule
ConceptA suite catches every faulty FIFO, but also reports one failure on the correct FIFO. What does that tell you?
ConceptYour in_order rule compares read_data with an auxiliary model. Where should the model get its information?
Read the waveformThis suite has answered, no_extra and in_order. On this faulty FIFO, which rule fails first, and where?
Faulty FIFO, with the model's expected answer
01234edge12345take_rread_validread_data001122exp_data001133

Sampled values at each rising edge. take_r and exp_data come from the model.

Spot the bugEach option is the property inside one rule. The fault bank's FIFO answers every read one edge late. Which rule fails to catch it?

Remember

  • A Verify suite must fail on every faulty variant and stay silent on the correct design.
  • Write one labelled assertion per spec sentence, with the smallest set of operators.
  • Build the model from the ports with its own count, never from the design's internals.
  • Show every assertion failing on purpose, and check that every antecedent triggers.
  • The first failure names the fault; later failures are usually its consequences.

Practice