Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here4
  • UVM Foundations14
  • UVM Phases & Simulation Lifecycle26
  • Factory & Configuration15
  • Transaction-Level Modeling (TLM)13
  • UVM Sequences & Stimulus21
  • UVM Agents & Protocol VIP21
  • Environment & Test Classes11
  • Checking & Coverage in UVM21
  • UVM Register Abstraction Layer (RAL)22
  • Advanced UVM in Practice13
  • Debug & Regression10
  • Coding Practice47
  • Verification in Industry Practice10
  • Senior Prep: Architecture, Leadership and Interviews32
    • Verification Architecture and Planning
    • UVM with SVA and Formal
      • Where Assertions Live
      • Assertion Control from the Test
      • Cover Properties and Coverage Closure
      • Reusing Properties in Formal
      • Reporting Assertion Failures through uvm_report
    • Leadership and Senior Judgment
    • Interview Bank
Loading lesson

‹UVM with SVA and Formal
  1. Topics
  2. UVM Verification
  3. Senior Prep: Architecture, Leadership and Interviews
  4. UVM with SVA and Formal
  5. Assertion Control from the Test

Part 13 · Senior Prep · Intermediate

Assertion Control from the Test

Switching one rule off for one test and one instance: config_db knobs into bound checkers, interface enables, and the limits of $assertoff from class code.

On this page

    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?

    The idea: a switch has a scope, a time and a trace

    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.

    The picture: the switches, narrowest first

    Switch

    Scope

    Reached from a test by

    Use it for

    disable iff (!rst_n)

    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

    uvm_config_db with a hierarchical path

    Error injection that breaks one usage rule

    Enable bit in an interface

    Every property in that interface instance

    The virtual interface handle: vif.chk_en = 0

    Assertions written inside the bench's own interfaces

    $assertcontrol in a module helper

    A hierarchy, by assertion and directive type

    A uvm_event the helper waits on

    Killing attempts in flight; vendor flows built on the system tasks

    Severity override or report catcher

    One message ID, after the failure

    set_report_severity_id_override, uvm_report_cb::add

    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.

    Build it: a per-rule knob, set by the test

    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.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 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)
      end

    config_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:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 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
    endclass

    For 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.

    Where `$assertoff` stops working from class code

    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:

    assert_ctl_helper.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 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
    endmodule

    Two 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.

    Watch it run (scratch runs)

    Diagram
    +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.

    What breaks: the knob that never arrived

    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:

    Diagram
    +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.

    Interview

    "An error-injection test trips a protocol assertion on purpose. How do you handle it?"

    A weak answer: "Call $assertoff at the start of the test," or "demote the error in that test."

    A strong answer: "I switch off that one rule, for that test, for the instance being injected, and leave everything else on. The checker owns a knob per usage rule, folded into disable iff, and reads it from config_db by its own instance path after reset; the test sets it with a null context, because the path is a design path, and the checker prints the value it read. I avoid $assertoff from tests: a package class cannot name the hierarchy, the state is global and sticky, and scoping varies by tool. Severity demotion is a last resort, because the failure still happens and can hide a real one."

    Follow-ups: What is the difference between Off and Kill? (Off stops new attempts; Kill also aborts attempts in flight). How do you stop someone copying the knob into the base test? (print knob state in every log, review base-test changes, and count covers that never fire).

    Check yourself A subsystem bench has two DMAs, 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.

    Check yourself Why read the knob after reset rather than at time 0 in the checker's 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.)

    Check yourself A test calls a helper that runs $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.

    What assertions cost, and when to trim them

    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.

    Related topics

    • Up: UVM with SVA and Formal

    • Assertion VIP and bind (config-bit enable)

    • Factory and configuration

    • Checking expected faults

    Previous

    258 / 280

    Next: Cover Properties and Coverage Closure

    Up next

    Up nextCover Properties and Coverage Closure

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Reusing Properties in Formal
    • Reporting Assertion Failures through uvm_report