Checks in interfaces: SVA inside a UVM bench

Bind a checker of concurrent assertions into the design, send its failures to the UVM report server so they fail the test, switch it with the bench's checks_enable knob, and prove with a cover count that the hold rule had work to do.

Lesson
U7.4
Time
30 min
Runs on
Verilator with UVM
Check
5 questions

The design's output stage reads the input pins instead of its stage register, so two of three results are wrong. A bound checker catches both, at 35 and 65 ns. The regression still reports a pass: the checker's fail action called $warning, the UVM report server never saw it, and the summary read UVM_ERROR : 0. Same bug, same rule, three fail actions:

Fail actionWhat the log showsUVM summaryVerdict from UVM counts
$warning(...)[35] %Warning: ... tb.dut.chk.a_result: ..., again at 65UVM_ERROR : 0Passed
$error(...)The simulator's own error message, at 35 and again at 65UVM_ERROR : 0Passed, unless the regression also scans the log for simulator errors
uvm_report_error(...)UVM_ERROR main.sv(..) @ 35: reporter@@tb.dut.chk.a_result [SVA_RESULT] y=33, again at 65UVM_ERROR : 2, [SVA_RESULT] 2Failed

The first and third rows are measured on this bench. The second is the standard behaviour of $error; Verilator differs, as the simulator note under "Send failures to the report server" explains. Only the third reaches the report server, which is what a UVM test's verdict reads.

Where the checker sits

A failure's path from the pins to the verdictSolid arrows carry sampled values or messages; the test's arrow is a write through a virtual interface handle. The vendor checker is a second bound instance with the same rule.
A failure's path from the pins to the verdictSolid arrows carry sampled values or messages; the test's arrow is a write through a virtual interface handle. The vendor checker is a second bound instance with the same rule. pins incr_if pins valid, ready, x,out_valid, y checker dut.chk incr_checks, boundinto incr_pipe checker dut.vchk vendor checker, failswith $warning uvm Report server Counts by severityand ID tool output Simulator log Printed, nevercounted by UVM test Test Writes the knob;reads the counts sampled at posedgesampled at posedgeuvm_report_error$warningget_severity_countchk.checks_enable =knob
  1. Build: a handle and a knob0 ns (illustrative)

    bind placed chk inside the design instance dut, and the top module put dut.chk in uvm_config_db. The test gets that virtual interface and writes checks_enable in end_of_elaboration_phase, before the first clock edge at 5 ns.

    More context

    The interface is not a component, so it cannot read uvm_config_db itself. The test writes the field through its virtual interface handle.

    Follow the calls, values and waits
    1. data checks_enable: Test → dut.chk
  2. Sample and decide35 ns (illustrative)

    16 was accepted at 15 ns, so at 35 ns both checkers expect y = 17. They sample y = 33: the output stage copied the pins at 25 ns, when the driver already offered 32.

    More context

    Both checkers read the same sampled values at the same edge: they are two bound instances of the same rule.

    Follow the calls, values and waits
    1. data y = 33: incr_if pins → dut.chk
    2. data y = 33: incr_if pins → dut.vchk
  3. Two fail actions35 ns (illustrative)

    chk's fail action calls uvm_report_error: the server prints the message and counts one UVM_ERROR. vchk's calls $warning: the simulator prints a line and UVM counts nothing.

    More context

    Only uvm_report_* calls and the UVM macros reach the report server's counters; a simulator severity task does not.

    Follow the calls, values and waits
    1. call a_result fails, 35 ns: dut.chk → Report server
    2. call $warning: dut.vchk → Simulator log
  4. The same failure again65 ns (illustrative)

    The result of 32 is sampled as y = 49 against an expected 33. chk reports a second UVM_ERROR; vchk prints a second warning.

    More context

    At 95 ns both checkers pass: the pins happen to hold 48 still, so the faulty y = x + 1 gives 49, the right answer by luck.

    Follow the calls, values and waits
    1. data y = 49: incr_if pins → dut.chk
    2. data y = 49: incr_if pins → dut.vchk
    3. call a_result fails, 65 ns: dut.chk → Report server
    4. call $warning: dut.vchk → Simulator log
  5. The verdict115 ns (illustrative)

    report_phase asks the server for its error count: 2, from chk alone. The test prints FAILED. Had chk used $warning as well, the count would be 0 and the test would pass with a known bug.

    More context

    The run phase ends at 115 ns: the last item completes at 75 ns and the test waits 40 ns more before dropping its objection.

    Follow the calls, values and waits
    1. data UVM_ERROR = 2: Report server → Test
Read the diagram as text
  • incr_if pins: valid, ready, x, out_valid, y
  • dut.chk: incr_checks, bound into incr_pipe
  • dut.vchk: vendor checker, fails with $warning
  • Report server: Counts by severity and ID
  • Simulator log: Printed, never counted by UVM
  • Test: Writes the knob; reads the counts
  • incr_if pins → dut.chk: sampled at posedge
  • incr_if pins → dut.vchk: sampled at posedge
  • dut.chk → Report server: uvm_report_error
  • dut.vchk → Simulator log: $warning
  • Report server → Test: get_severity_count
  • Test → dut.chk: chk.checks_enable = knob

Only the path through the report server changes the verdict.

SystemVerilog does not allow concurrent assertions in a class, so the protocol rules live in an interface or module and the classes reach them through a handle. Here the checker is an interface with the design's signals as inputs. One bind line adds it to every incr_pipe without touching the design's source; A7.1 [packaging and binding](A7-1-packaging-and-binding.html) covers bind itself.

The other common home is the agent's own interface, incr_if here, with the agent writing vif.checks_enable: the assertions then travel with the agent to every bench that uses it. Choose a bound checker when the rules need the design's internal signals or you cannot edit the interface. It also suits checks that belong to the design rather than to the protocol.

The connection expressions resolve inside incr_pipe, where bus is its interface port. dut.chk names the bound instance.
bind incr_pipe incr_checks chk (.clk(bus.clk), .valid(bus.valid), .ready(bus.ready),
                                .out_valid(bus.out_valid), .x(bus.x), .y(bus.y));
// in the top module, before run_test:
uvm_config_db #(virtual incr_checks)::set(null, "uvm_test_top", "chk", dut.chk);

The two rules are ones U7.1's monitor states in class code: an offer that is not taken is held, and a result appears two edges after its input. a_hold uses |=> and $stable; a_result uses ##2 and $past. [A1.2](A1-2-clocks-and-sampling.html) explains the sampled values both read.

Send failures to the report server

A simulator severity task

a_result: assert property (...)
  else $error("a_result failed: y=%0d",
              $sampled(y));

The simulator reports it. UVM's counts, its summary and every check of them say nothing happened.

The report server

a_result: assert property (...)
  else uvm_report_error("SVA_RESULT",
         $sformatf("y=%0d", $sampled(y)), UVM_NONE,
         `__FILE__, `__LINE__, $sformatf("%m"));

One UVM_ERROR per failing attempt, under ID SVA_RESULT, with the assertion's path in the message.

uvm_report_error here is the global function from uvm_pkg, not a component method: a module or interface has no report object of its own. It forwards to uvm_root, which is why the message names the reporter reporter. The last argument is a context string, printed after @@; $sformatf("%m") in the action block gives the assertion's full path, tb.dut.chk.a_result. uvm_error("SVA_RESULT", msg) works too on this bench, but prints no path.

Each failure is one count, so two wrong results give UVM_ERROR : 2 and [SVA_RESULT] 2 in the summary. Because the reporter is uvm_root, severity or action settings made on an env or agent component do not reach these messages; make them on uvm_root, by ID.

One reporting function for every checker

A project usually wraps the call once. Every checker then uses the same IDs, and one function decides where they go:

The caller passes %m: inside the function it would name the package function.
package sva_report_pkg;
  import uvm_pkg::*;
  function automatic void sva_error(string id, string msg, string scope);
    uvm_report_error(id, msg, UVM_NONE, "", 0, scope);
  endfunction
endpackage
// in a checker:  else sva_error("SVA_RESULT", msg, $sformatf("%m"));

The same applies to a checker you receive from someone else. If it calls $error, ask for a hook or define that routes its failures, or the UVM verdict ignores it, as the vendor checker in the example shows.

One knob for monitor and interface checks

U7.1 gave the monitor a checks_enable switch read from uvm_config_db. The interface checks need the same switch, so that an error-injection test can turn them off. An interface is not a component: it cannot look up its own path in the database. A component that owns the configuration reads the knob and writes it through the virtual interface, and every property starts with disable iff (!checks_enable).

  1. The test sets the knob

    uvm_config_db #(bit)::set(this, "", "checks_enable", 0), for the scope that reads it.

  2. A component reads it in build_phase

    void'(uvm_config_db #(bit)::get(this, "", "checks_enable", checks_enable)).

  3. It writes the interface before the first edge

    chk.checks_enable = checks_enable in end_of_elaboration_phase, at 0 ns. The first sampling edge is at 5 ns.

  4. The properties stand down

    With the knob at 0, on this bench: no failures, no passes and no cover counts for the whole run.

disable iff acts on the knob's current value, not a sampled one, so a change during the run also abandons attempts already in flight. Change it at a quiet point and hold it across clock edges. $assertoff is the other standard switch: it stops new attempts of the assertions or scopes it names and leaves running attempts alone, while $assertkill also aborts them.

Prove each rule had work to do

Zero failures can mean the rule held, or that its antecedent never matched. a_hold passes vacuously at every edge with no stall (A1.3 [implication and vacuity](A1-3-implication-and-vacuity.html)), and pass actions run for vacuous passes too, so a pass count shows nothing. c_stall: cover property (valid && !ready) asks the other question: did a stall happen at all? Turn its count into part of the verdict.

In report_phase, before the error count is read. A test that turned the checks off does not demand their coverage.
if (checks_enable && chk.stalls_seen == 0)
  `uvm_error("SVA_COVER", "the hold rule was never exercised")

On a design that is never busy, every input is taken at once and a_hold never has an antecedent match. This line then fails the test; try it in the first quiz question. a_result needs a count of its own antecedent, accepted inputs, in the same way; the transfer exercise asks for it. In a full bench the cover results also go to the coverage database and the verification plan (U15.1). This line is the minimum: a test that claims to check a rule shows that the rule was reached.

Assertion, monitor check or scoreboard

Check inGood atCannot see
Bound interface (SVA)Cycle rules on pins and on internal signals bind can reach; the same properties feed formal toolsPlanned traffic, transaction meaning, results that leave the block
Monitor class checkRules about one transaction's fields, with the transaction in handEdge-by-edge timing it does not sample
Scoreboard (U7.3)End-to-end values against a model, missing and extra resultsWhen, within a cycle, a rule broke

Each layer reports through UVM and honours checks_enable, so one test can switch them off together.

Here the scoreboard of U7.3 would also find the wrong results, but only at the output, as a value mismatch. a_result names the edge and the assertion. Like the monitor, the checker has no idea what was planned: a transfer that never happened gives it nothing to check, so keep U7.3's planned-count guard.

Try it

The design's bug corrupts the first two results. Both checkers fail at 35 and 65 ns and pass at 95 ns, when the pins happen to hold 48 still. chk's failures are UVM errors; vchk's are simulator warnings. The verdict counts two errors, both from chk, and the stall cover saw four edges, so the hold rule was exercised.

Fix the design: change bus.y <= bus.x + 1 back to bus.y <= stage, and every count except the stall cover drops to 0. Then put the bug back and change chk's uvm_report_error calls to $warning: the verdict reads PASSED with four failures in the log.

Transfer exercise: add an accepts_seen counter for a_result's antecedent, valid && ready, and fold it into the SVA_COVER check. Then add a rule that out_valid is never high without an accepted input two edges earlier: which design fault would only that rule catch, and what would show it was exercised?

RUNVerilator + UVM 2020.3.1 · simulator service
Edit the code, then run it
Expected among the UVM messages: UVM_ERROR main.sv(103) @ 35: reporter@@tb.dut.chk.a_result [SVA_RESULT] y=33, expected 17 [35] %Warning: main.sv:119: tb.dut.vchk.a_result: vendor a_result failed: y=33 UVM_ERROR main.sv(103) @ 65: reporter@@tb.dut.chk.a_result [SVA_RESULT] y=49, expected 33 [65] %Warning: main.sv:119: tb.dut.vchk.a_result: vendor a_result failed: y=49 checker: hold 0, result 2 failures, 4 stalls covered; vendor checker: 2 failures UVM errors 2: TEST FAILED UVM_WARNING : 0 UVM_ERROR : 2 [SVA_RESULT] 2

Check yourself

Predict the outputThis bench uses the example's checker and verdict with a correct design that is never busy (BUSY = 0). The sequence sends 16, 32 and 48. What line does the $display print? UVM's own messages are not part of the answer.
`timescale 1ns/1ns
`include "uvm_macros.svh"
import uvm_pkg::*;

// The incrementer from U4.3 with a result port. It captures x at a rising edge k where valid and
// ready are both 1 and is busy for the next BUSY edges. It drives y = x + 1 and out_valid after
// edge k+1, for one cycle, so a monitor sampling at rising edges sees the result at edge k+2.
interface incr_if (input logic clk);
  logic       valid = 0;
  logic [7:0] x = 0;
  logic       ready;
  logic       out_valid = 0;
  logic [7:0] y = 0;
endinterface

module incr_pipe #(parameter int BUSY = 2) (incr_if bus);
  logic [1:0] busy = 0;
  logic       staged = 0;
  logic [7:0] stage = 0;
  assign bus.ready = (busy == 0);
  always @(posedge bus.clk) begin
    bus.out_valid <= staged;
    bus.y         <= stage;
    staged        <= bus.valid && bus.ready;
    stage         <= bus.x + 1;
    if (bus.valid && bus.ready) busy <= BUSY;
    else if (busy != 0)         busy <= busy - 1;
  end
endmodule

// What the sequence sends, and what the monitor sees going in.
class incr_item extends uvm_sequence_item;
  `uvm_object_utils(incr_item)
  bit [7:0] x;
  function new(string name = "incr_item"); super.new(name); endfunction
endclass

// What the monitor sees coming out.
class incr_result extends uvm_sequence_item;
  `uvm_object_utils(incr_result)
  bit [7:0] y;
  function new(string name = "incr_result"); super.new(name); endfunction
endclass

// Sends the values in the queue, one item each.
class incr_seq extends uvm_sequence #(incr_item);
  `uvm_object_utils(incr_seq)
  bit [7:0] values[$];
  function new(string name = "incr_seq"); super.new(name); values = '{16, 32, 48}; endfunction
  task body();
    foreach (values[i]) begin
      incr_item it = incr_item::type_id::create("it");
      start_item(it);
      it.x = values[i];
      finish_item(it);
    end
  endtask
endclass

// The driver from U4.3: offers an item at a falling edge, holds it until an edge with ready.
class incr_driver extends uvm_driver #(incr_item);
  `uvm_component_utils(incr_driver)
  virtual incr_if vif;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  task run_phase(uvm_phase phase);
    forever begin
      @(negedge vif.clk);
      seq_item_port.try_next_item(req);
      if (req == null) begin
        vif.valid <= 0;
        continue;
      end
      vif.valid <= 1;
      vif.x     <= req.x;
      do @(posedge vif.clk); while (!vif.ready);
      seq_item_port.item_done();
    end
  endtask
endclass

// Checks on the incrementer's pins. bind (at the end) places one instance inside every incr_pipe.
interface incr_checks (input logic clk, valid, ready, out_valid, input logic [7:0] x, y);
  import uvm_pkg::*;
  bit checks_enable = 1;     // the knob: written by the test before the first clock edge
  int hold_failures, result_failures;
  int stalls_seen;           // what c_stall counts: edges where the hold rule had work to do

  // An offer the design does not take is still offered, unchanged, at the next edge.
  a_hold: assert property (@(posedge clk) disable iff (!checks_enable)
                           valid && !ready |=> valid && $stable(x))
    else begin
      hold_failures++;
      uvm_report_error("SVA_HOLD", $sformatf("offer changed while stalled: valid=%0d x=%0d",
                       $sampled(valid), $sampled(x)), UVM_NONE, `__FILE__, `__LINE__, $sformatf("%m"));
    end

  // Two edges after the design takes x, the output pins show x + 1.
  a_result: assert property (@(posedge clk) disable iff (!checks_enable)
                             valid && ready |-> ##2 out_valid && y == $past(x, 2) + 8'd1)
    else begin
      result_failures++;
      uvm_report_error("SVA_RESULT", $sformatf("y=%0d, expected %0d", $sampled(y), $past(x, 2) + 8'd1),
                       UVM_NONE, `__FILE__, `__LINE__, $sformatf("%m"));
    end

  // Was the hold rule ever exercised? Its passes cannot tell: most are vacuous (A1.3).
  c_stall: cover property (@(posedge clk) disable iff (!checks_enable) valid && !ready);
  always @(posedge clk) if (checks_enable && valid && !ready) stalls_seen++;   // c_stall, counted
endinterface

bind incr_pipe incr_checks chk (.clk(bus.clk), .valid(bus.valid), .ready(bus.ready),
                                .out_valid(bus.out_valid), .x(bus.x), .y(bus.y));

class predict_test extends uvm_test;
  `uvm_component_utils(predict_test)
  uvm_sequencer #(incr_item) sqr;
  incr_driver drv;
  virtual incr_checks chk;
  bit checks_enable = 1;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    virtual incr_if vif;
    sqr = uvm_sequencer #(incr_item)::type_id::create("sqr", this);
    drv = incr_driver::type_id::create("drv", this);
    if (!uvm_config_db #(virtual incr_if)::get(this, "", "vif", vif))
      `uvm_fatal("NOVIF", "no virtual interface")
    if (!uvm_config_db #(virtual incr_checks)::get(this, "", "chk", chk))
      `uvm_fatal("NOCHK", "no checker interface")
    void'(uvm_config_db #(bit)::get(this, "", "checks_enable", checks_enable));
    drv.vif = vif;
  endfunction
  function void connect_phase(uvm_phase phase);
    drv.seq_item_port.connect(sqr.seq_item_export);
  endfunction
  // The interface is not a component and cannot look up its own knob: the test writes it.
  function void end_of_elaboration_phase(uvm_phase phase);
    chk.checks_enable = checks_enable;
  endfunction
  task run_phase(uvm_phase phase);
    incr_seq seq = incr_seq::type_id::create("seq");
    phase.raise_objection(this);
    seq.start(sqr);
    #40;                                     // the last result is checked two edges after its input
    phase.drop_objection(this);
  endtask

  function void report_phase(uvm_phase phase);
    uvm_report_server svr = uvm_report_server::get_server();
    string verdict = "PASSED";
    if (checks_enable && chk.stalls_seen == 0)
      `uvm_error("SVA_COVER", "the hold rule was never exercised")
    if (svr.get_severity_count(UVM_ERROR) + svr.get_severity_count(UVM_FATAL) > 0) verdict = "FAILED";
    $display("hold %0d, result %0d failures, %0d stalls covered; UVM errors %0d: TEST %s",
             chk.hold_failures, chk.result_failures, chk.stalls_seen, svr.get_severity_count(UVM_ERROR), verdict);
  endfunction
endclass

module tb;
  bit clk;
  always #5 clk = ~clk;
  incr_if bus (clk);
  incr_pipe #(.BUSY(0)) dut (bus);
  initial begin
    uvm_root::get().set_timeout(2000ns, 1);
    uvm_config_db #(virtual incr_if)::set(null, "uvm_test_top", "vif", bus);
    uvm_config_db #(virtual incr_checks)::set(null, "uvm_test_top", "chk", dut.chk);
    run_test("predict_test");
  end
endmodule
Spot the bugAn error-injection test drives a value that changes while the design stalls, so it must run with the interface checks off. The test reads checks_enable with uvm_config_db #(bit)::get(this, "", ...) in its own build_phase and writes it into chk. Each option is the line placed just before that get. Which one leaves the checks on?
ConceptA purchased protocol checker is bound into the design. Its fail actions call $error, and the test is run on a simulator that continues after $error. One rule fails twice. What does the UVM summary report for it?
ConceptAn assertion in the bound interface reports with uvm_report_error(id, msg, UVM_NONE, file, line, $sformatf("%m")). Why does its message read reporter@@tb.dut.chk.a_result?
ConceptA regression shows a_hold with zero failures in every test. What must you check before counting the hold rule as verified?

Remember

  • Concurrent assertions live in an interface or module; bind places the checker in the design and the top module puts its handle in uvm_config_db.
  • A fail action that calls $error or $warning bypasses the report server, so a UVM verdict ignores it; call uvm_report_error with %m as context, one count per failure.
  • An interface cannot read uvm_config_db; a component reads checks_enable and writes it through the virtual interface before the first edge, and each property uses disable iff (!checks_enable).
  • Zero failures can be vacuous: count each rule's antecedent with a cover and fail the test when an enabled rule was never exercised.

Practice