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 action | What the log shows | UVM summary | Verdict from UVM counts |
|---|---|---|---|
$warning(...) | [35] %Warning: ... tb.dut.chk.a_result: ..., again at 65 | UVM_ERROR : 0 | Passed |
$error(...) | The simulator's own error message, at 35 and again at 65 | UVM_ERROR : 0 | Passed, 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 65 | UVM_ERROR : 2, [SVA_RESULT] 2 | Failed |
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
- Build: a handle and a knob0 ns (illustrative)
bindplacedchkinside the design instancedut, and the top module putdut.chkinuvm_config_db. The test gets that virtual interface and writeschecks_enableinend_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_dbitself. The test writes the field through its virtual interface handle.Follow the calls, values and waits
- data checks_enable: Test → dut.chk
- 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
- data y = 33: incr_if pins → dut.chk
- data y = 33: incr_if pins → dut.vchk
- Two fail actions35 ns (illustrative)
chk's fail action callsuvm_report_error: the server prints the message and counts oneUVM_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
- call a_result fails, 35 ns: dut.chk → Report server
- call $warning: dut.vchk → Simulator log
- The same failure again65 ns (illustrative)
The result of 32 is sampled as y = 49 against an expected 33.
chkreports a secondUVM_ERROR;vchkprints 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
- data y = 49: incr_if pins → dut.chk
- data y = 49: incr_if pins → dut.vchk
- call a_result fails, 65 ns: dut.chk → Report server
- call $warning: dut.vchk → Simulator log
- The verdict115 ns (illustrative)
report_phaseasks the server for its error count: 2, fromchkalone. The test prints FAILED. Hadchkused$warningas 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
- 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.
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:
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).
The test sets the knob
uvm_config_db #(bit)::set(this, "", "checks_enable", 0), for the scope that reads it.A component reads it in build_phase
void'(uvm_config_db #(bit)::get(this, "", "checks_enable", checks_enable)).It writes the interface before the first edge
chk.checks_enable = checks_enableinend_of_elaboration_phase, at 0 ns. The first sampling edge is at 5 ns.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.
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 in | Good at | Cannot see |
|---|---|---|
| Bound interface (SVA) | Cycle rules on pins and on internal signals bind can reach; the same properties feed formal tools | Planned traffic, transaction meaning, results that leave the block |
| Monitor class check | Rules about one transaction's fields, with the transaction in hand | Edge-by-edge timing it does not sample |
| Scoreboard (U7.3) | End-to-end values against a model, missing and extra results | When, 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?
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] 2Check yourself
Remember
- Concurrent assertions live in an interface or module;
bindplaces the checker in the design and the top module puts its handle inuvm_config_db. - A fail action that calls
$erroror$warningbypasses the report server, so a UVM verdict ignores it; calluvm_report_errorwith%mas context, one count per failure. - An interface cannot read
uvm_config_db; a component readschecks_enableand writes it through the virtual interface before the first edge, and each property usesdisable 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
- The checker that fails nothing Fix · Easy