With the rule checker bound, sva_truncate_test fails. The test forces TLAST on beat 3 of an 8-word job, exactly as it should, and a_full_packet reports DMA_SHORT_PKT. A teammate's first fix is one line at the top of the test's run_phase: $assertoff(0, top.dut);. It does not compile, because the test is a class in a package. When it is moved somewhere it does compile, it turns off every assertion in the DMA, including the DONE and BUSY invariant, for the rest of the run. What is the narrowest switch a test can use, and how does class code reach it?
Every way of silencing a check answers three questions. Scope: which rules, on which instances? Time: from when to when, and does it reset for the next test? Trace: does the log say the check was off? A good switch is narrow on all three: one rule, one instance, one test, printed in the log. Think of a conference room: muting one microphone during a coughing fit is fine; cutting the whole PA system and forgetting to turn it back on is how the next speaker goes unheard.
Switch | Scope | Reached from a test by | Use it for |
|---|---|---|---|
| One property, during reset | Nothing: it is in the property | Reset. Never use test control for reset noise |
Per-rule knob in the checker | One rule, one instance or a wildcard |
| Error injection that breaks one usage rule |
Enable bit in an interface | Every property in that interface instance | The virtual interface handle: | Assertions written inside the bench's own interfaces |
| A hierarchy, by assertion and directive type | A | Killing attempts in flight; vendor flows built on the system tasks |
Severity override or report catcher | One message ID, after the failure |
| Known issues with a ticket, scoped and counted |
The list is in order of preference for a reason. Knobs act before the property evaluates, so a disabled rule costs nothing and leaves nothing in the log except the line that says it is off. Report-level demotions act after a failure, so the failure still happens and still lands in the log as an INFO line, which is how the waiver that hid a real bug went unnoticed.
The checker owns one bit per usage rule and folds it into disable iff. It reads the bit from config_db, keyed by its own instance path, after reset. That timing matters: tests set knobs in build_phase, which runs at time 0, and an initial block in a module can run before UVM has built anything.
// snippet: the knob read inside dma_rules_chk
bit short_pkt_en = 1'b1;
// %m inside a function names the function; drop the last part to get the instance path.
function automatic string inst_path();
string s = $sformatf("%m");
for (int i = s.len() - 1; i > 0; i--) if (s[i] == ".") return s.substr(0, i - 1);
return s;
endfunction
initial begin
@(posedge rst_n); // tests set knobs in build_phase, at time 0
void'(uvm_config_db#(bit)::get(null, inst_path(), "short_pkt_en", short_pkt_en));
`uvm_info("DMA_CHK", $sformatf("%s: short_pkt_en=%0d", inst_path(), short_pkt_en), UVM_LOW)
endconfig_db does not need a component to look up a value; any string path works. Keying by the checker's own instance path means two DMAs in a subsystem bench can have different settings, and a wildcard such as "*.u_rules" reaches all of them. The test side is one line:
// snippet: from the scratch bench
class sva_truncate_knob_test extends sva_truncate_test;
`uvm_component_utils(sva_truncate_knob_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
// null context: the path is a design hierarchy, not a component path
uvm_config_db#(bit)::set(null, "top.dut.u_rules", "short_pkt_en", 1'b0);
endfunction
endclassFor assertions written inside the bench's own interfaces, the equivalent is a bit chk_en = 1; in the interface, folded into each disable iff, which a test sets through the virtual interface it already has. It is coarser (every property in that interface instance) but needs no config_db lookup.
The assertion control tasks take hierarchical names, and code in a package cannot contain hierarchical references, so a test class cannot name top.dut.u_rules. The general form is $assertcontrol(control_type, assertion_type, directive_type, levels, list); $assertoff, $asserton and $assertkill are shorthands for three of its control types.
control_type | Effect |
|---|---|
3 On / 4 Off | Start or stop new attempts; attempts already running finish |
5 Kill | Stop new attempts and abort the ones in flight |
6 PassOn / 7 PassOff | Run or skip pass action blocks |
8 FailOn / 9 FailOff | Run or skip fail action blocks (the check still evaluates) |
10 NonvacuousOn / 11 VacuousOff | Run or skip pass actions on vacuous successes |
1 Lock / 2 Unlock | Freeze the current on/off state against later control calls |
assertion_type is a bit mask (1 concurrent, 2 simple immediate, 4 and 8 deferred immediate, 16 expect, and so on) and directive_type another (1 assert, 2 cover, 4 assume), so $assertcontrol(4, 1, 1) turns off concurrent asserts while leaving covers running. When you need these, put the call in a module that can name the hierarchy and let the test trigger it:
// snippet: a module-side helper in the top, for when you need the system tasks
module assert_ctl_helper;
import uvm_pkg::*;
initial begin
uvm_event off_ev = uvm_event_pool::get_global("rules_off");
uvm_event on_ev = uvm_event_pool::get_global("rules_on");
forever begin
off_ev.wait_trigger();
$assertcontrol(5, 1, 1, 0, top.dut.u_rules); // 5 = Kill: off, and drop attempts in flight
on_ev.wait_trigger();
$assertcontrol(3, 1, 1, 0, top.dut.u_rules); // 3 = On
end
end
endmoduleTwo limits are worth knowing in an interview. The state is global simulation state: it does not reset between phases, and nothing restores it at the end of a test, so every Off needs its On. And scoping support varies by tool: in a scratch run on Verilator 5.048, $assertoff(0, top.u1) also silenced the identical checker top.u2, because that tool applied the call to every assertion. A knob behaves the same on every simulator.
+UVM_TESTNAME=sva_truncate_test (rule left on)
UVM_INFO sva_tb.sv(30) @ 35000: reporter [DMA_CHK] top.dut.u_rules: short_pkt_en=1
UVM_INFO dma_tb.sv(520) @ 275000: uvm_test_top.env.in_agt.drv [TRUNC] TLAST forced on beat 3
UVM_ERROR sva_tb.sv(41) @ 305000: reporter [DMA_SHORT_PKT] top.dut.u_rules.a_full_packet.unnamedblk4: input TLAST before word LEN=8
UVM_INFO dma_tb.sv(761) @ 385000: uvm_test_top [TRUNC] DMA reported ERR for the short packet, as expected
TEST VERDICT: FAIL (UVM_ERROR=1, UVM_FATAL=0)
+UVM_TESTNAME=sva_truncate_knob_test (knob off for this instance)
UVM_INFO sva_tb.sv(30) @ 35000: reporter [DMA_CHK] top.dut.u_rules: short_pkt_en=0
UVM_INFO dma_tb.sv(520) @ 275000: uvm_test_top.env.in_agt.drv [TRUNC] TLAST forced on beat 3
UVM_INFO dma_tb.sv(761) @ 385000: uvm_test_top [TRUNC] DMA reported ERR for the short packet, as expected
UVM_INFO dma_tb.sv(411) @ 485000: uvm_test_top.env.sb [SB] 4 words matched, 0 mismatched
TEST VERDICT: PASS (UVM_ERROR=0, UVM_FATAL=0)The first line of each run states the knob. A reviewer reading any log can see which rules were off without opening the test.
Only one rule moved. a_done_not_busy stayed on in the knob test; the same test under fault 6 still reports 15 DMA_DONE_BUSY errors.
The DUT's behaviour is still checked: the test itself verifies ERR, and the scoreboard matched the 4 words that went through.
The first version of the knob test passed this as the context: uvm_config_db#(bit)::set(this, "top.dut.u_rules", ...). It compiled, ran, and failed exactly like the rule-on test. The scratch log shows why in its first line:
+UVM_TESTNAME=sva_truncate_ctx_test (set with 'this' as the context)
UVM_INFO sva_tb.sv(30) @ 35000: reporter [DMA_CHK] top.dut.u_rules: short_pkt_en=1
UVM_ERROR sva_tb.sv(41) @ 305000: reporter [DMA_SHORT_PKT] top.dut.u_rules.a_full_packet.unnamedblk4: input TLAST before word LEN=8
TEST VERDICT: FAIL (UVM_ERROR=1, UVM_FATAL=0)A set with a component context prefixes the path with that component's full name, so the value was stored for uvm_test_top.top.dut.u_rules, which nothing ever reads. Inside the component tree that prefix is what you want; for a design path it is wrong. Use null (or uvm_root::get()) as the context, and make the checker print what it read. Without that print, the failure would have looked like a broken knob in the checker rather than a wrong path in the test. A first draft of the checker had a second version of the same bug: it looked itself up with $sformatf("%m") inside its initial block, which returns top.dut.u_rules.unnamedblk1, not the instance path. The print caught that one too.
top.dut0 and top.dut1, each with a bound u_rules. An error-injection test truncates packets only on dma0. What does the test set?uvm_config_db#(bit)::set(null, "top.dut0.u_rules", "short_pkt_en", 0). dma1's checker reads its own path, finds nothing, and keeps the rule on, so dma1 is still checked. A wildcard such as "*.u_rules" would switch off both and hide a real short packet on dma1.
initial block?Tests set their knobs in build_phase, which runs at time 0 but after run_test starts. A module's initial block can run before that, read nothing, and keep the default. Waiting for the end of reset guarantees the build is complete. (A uvm_event or a config_db wait would also work, but reset is a natural point that every checker already sees.)
$assertcontrol(4, 1, 1, 0, top.dut.u_rules) at 300 ns and never turns it back on. What happens to the next test in the regression?Nothing, if each test is a separate simulation, which is the usual case: the state dies with the run. Within the same run, the rules stay off until the end, including through any later phases and checks. That is why every Off has its On in the same test, and why the helper should print each change.
A concurrent assertion starts a new attempt on every clock edge where its antecedent can match, in every instance it is bound into. A rule with a long or open-ended window (##[1:$]) keeps many attempts alive at once, so a chip with sixty DMA instances and a dozen such rules can spend more time in assertion threads than in the testbench. Two habits keep it honest. Measure first: the simulator's profiler reports time per assertion, and one or two rules are usually most of it. Then trim by test, not by deleting: the same knobs that switch a rule off for an error test can switch an expensive rule off in long soak runs and leave it on in the short functional regression, and the knob-state line every test prints is the audit trail that shows which rules were live in which run.
Next: a switch decides whether a rule runs. Cover Properties and Coverage Closure asks whether the rule ever had anything to check.