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. Reporting Assertion Failures through uvm_report

Part 13 · Senior Prep · Intermediate

Reporting Assertion Failures through uvm_report

Routing assertion failures into the UVM verdict with stable IDs, bounding floods with the quit count, and scoping assertion waivers.

On this page

    Under fault 6, DONE stuck at 1, the bound invariant fired on the first busy cycle and then on every busy cycle after it: 57 UVM_ERROR lines for one stuck flag, interleaved with everything else the bench printed. In an earlier lesson of the industry section, the same kind of checker written with plain $error stopped Verilator at the first failure and printed no UVM summary at all. Neither log is what a triage script, or a person at 2 a.m., wants. How should assertion failures enter the UVM verdict, and how do you keep one root cause from becoming 57 lines?

    The idea: an assertion failure is a check result like any other

    The UVM report server is where the bench decides pass or fail. It counts by severity and ID, it runs report catchers (waivers), it applies severity overrides, it stops the run at the quit count, and the verdict server prints PASS or FAIL from its counts. A failure that bypasses it, such as a plain $error, is invisible to all of that: it may stop the simulator, or it may scroll past while the summary says UVM_ERROR : 0. So the rule is simple: every check failure, from a scoreboard or from an assertion, reaches the report server. Think of one inbox for a support team. Tickets that arrive by side channels get answered by whoever happens to see them, and are never counted.

    The picture: two paths out of an action block

    Flowchart: a failing assertion goes through a macro either to uvm_error with a rule ID, or to $error plus a package counter that a check_phase converts into uvm_error; both reach the report server, which produces the verdict and per-ID summary.
    Either way, the failure ends up counted by the report server, so the verdict, waivers, quit count and triage treat it like a scoreboard error.

    Build it: route, name, and bound the failures

    Route: one macro, two builds

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // snippet: failure routing in the rule checker
    `ifdef DMA_CHK_PLAIN
      `define CHK_FAIL(ID, MSG) begin $error("[%s] %s", ID, MSG); dma_chk_pkg::n_fail++; end
    `else
      `define CHK_FAIL(ID, MSG) `uvm_error(ID, MSG)
    `endif
    
    a_done_not_busy: assert property (@(posedge clk) disable iff (!rst_n) !(done && busy))
      else `CHK_FAIL("DMA_DONE_BUSY", $sformatf("%m: DONE and BUSY both set"))

    The default build calls uvm_error. A build that must stay UVM-free (a checker shared with a team that has no UVM, or an emulation flow) defines DMA_CHK_PLAIN: the failure prints with $error and increments a counter, and one small component makes the count fail the test. On a simulator that stops at $error, run it with the tool's option to continue, or the counter never gets read.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // snippet: for checkers that must stay UVM-free, a component turns the count into a UVM error
    class chk_fail_counter extends uvm_component;
      `uvm_component_utils(chk_fail_counter)
      function new(string name, uvm_component parent);
        super.new(name, parent);
      endfunction
      function void check_phase(uvm_phase phase);
        if (dma_chk_pkg::n_fail != 0)
          `uvm_error("SVA_FAIL", $sformatf("%0d assertion failure(s) reported by plain $error; see the log above", dma_chk_pkg::n_fail))
      endfunction
    endclass

    A third route needs no code in the checker at all: the regression script reads the simulator's own assertion summary as well as the UVM summary, and fails the run if either reports failures. It is the cheapest option and a good safety net, but it acts after the run, so waivers, quit count and the verdict line cannot use it.

    Name: stable IDs and a location in the message

    • The ID is the rule, not the instance: DMA_DONE_BUSY, DMA_SHORT_PKT. Triage buckets, waivers and trend charts key off it, so it must not change from run to run.

    • The location goes in the message. A uvm_error called from a module has no component, so the report's context is the generic reporter. Put %m in the message; in the scratch log it expands to the assertion's own scope, top.dut.u_rules.a_done_not_busy..., which names the instance.

    • Values go in the message too: the LEN, the count, the state. "input TLAST before word LEN=8" saves a waveform session; "assertion failed" does not.

    Bound: one root cause should not be 57 lines

    Two tools help, and they solve different problems. A quit count stops the run after N errors of any kind: set_max_quit_count(5) on the report server in the test, or +UVM_MAX_QUIT_COUNT=5,NO on the command line. It keeps the log short and the farm free, and the first error is still first. A property that fires once per episode fixes the shape of the check itself: $rose(done && busy) reports when the illegal state begins, not on every cycle it lasts. Use the quit count in regression and the property shape when a condition, once broken, stays broken.

    Watch it run: fault 6 with a quit count of 5 (scratch run)

    Diagram
    +UVM_TESTNAME=sva_quit_test +FAULT=6
    FAULT 6 injected
    UVM_ERROR sva_tb.sv(36) @ 165000: reporter [DMA_DONE_BUSY] top.dut.u_rules.a_done_not_busy.unnamedblk3: DONE and BUSY both set
    UVM_ERROR sva_tb.sv(36) @ 175000: reporter [DMA_DONE_BUSY] top.dut.u_rules.a_done_not_busy.unnamedblk3: DONE and BUSY both set
    UVM_ERROR sva_tb.sv(36) @ 185000: reporter [DMA_DONE_BUSY] top.dut.u_rules.a_done_not_busy.unnamedblk3: DONE and BUSY both set
    UVM_ERROR sva_tb.sv(36) @ 195000: reporter [DMA_DONE_BUSY] top.dut.u_rules.a_done_not_busy.unnamedblk3: DONE and BUSY both set
    UVM_ERROR sva_tb.sv(36) @ 205000: reporter [DMA_DONE_BUSY] top.dut.u_rules.a_done_not_busy.unnamedblk3: DONE and BUSY both set
    Quit count reached!
    Quit count :     5 of     5
    UVM_ERROR :    5
    UVM_FATAL :    0
    [DMA_DONE_BUSY]     5
    TEST VERDICT: FAIL (UVM_ERROR=5, UVM_FATAL=0)
    • The first error is at 165 ns, the first clock on which the DMA is busy in the first job. The run stops 40 ns later instead of at 1,305 ns with 57 errors.

    • The verdict line still prints, because the verdict server prints from report_summarize, which runs on the way out after a quit.

    • The per-ID summary names the rule, so a triage script buckets the run as DMA_DONE_BUSY without reading a waveform.

    What breaks: a waiver that cannot be scoped

    The bench's DMA-142 waiver is scoped to one component: uvm_report_cb::add(env.sb, catcher). Try the same with an assertion ID and there is no component to attach it to, because every module-side uvm_error reports through the root context. A catcher added with uvm_report_cb::add(null, catcher) sees every message in the bench, so a waiver keyed only on DMA_DONE_BUSY silences that rule on every DMA in a subsystem, including the one where it is a real bug. Scope assertion waivers by message pattern instead, which is another reason to put %m in every message: a pattern such as "top.dut0.u_rules*" names one instance. Better still, if a usage rule is expected to fire, switch it off with its knob before it fires, as lesson 2 does, rather than catching it afterwards.

    The classic version of this failure is older and simpler: a checker using plain $error on a simulator that continues past it. Every assertion failure prints, the UVM summary says UVM_ERROR : 0, and the regression, which reads only the summary, calls it a pass. Assertion VIP and bind shows both behaviours on real runs.

    Interview

    "Can a UVM test pass while assertions are failing?"

    A weak answer: "No, an assertion failure is an error."

    A strong answer: "Yes, easily. A plain $error from an assertion is a simulator message, not a UVM report, so the report server never counts it; on a simulator that continues, the summary says zero errors and a script that reads the summary passes the run. I route action blocks through a macro that calls uvm_error with a stable rule ID and the instance path in the message, so failures count in the verdict and can be triaged and, if needed, waived by pattern. For checkers that must stay UVM-free, the macro also bumps a package counter and a component fails the test in check_phase, and the regression reads the simulator's assertion summary as a backstop. Then I bound floods with a quit count, and with $rose-style properties where a broken state persists."

    Follow-ups: Why is the report context 'reporter' for these errors? (module code has no component; uvm_error falls back to the root). Fatal or error for an assertion? (error: a fatal stops the run at the first hit and loses the rest; fatal is for a broken bench).

    Check yourself A regression with 400 tests shows DMA_DONE_BUSY in 3 runs, each with exactly 5 errors. What do you suspect, and what do you look at first?

    Exactly 5 suggests the quit count stopped each run, so the real number of failing cycles is unknown, and the runs never reached their own checks. Look at the first error's time and the instance path in its message, then the seed: three runs with the same first-error signature are one bug. The quit count did its job, keeping the logs short and the first error first.

    Check yourself A teammate maps every assertion failure to uvm_fatal "so nobody can miss it". What is lost?

    The run stops at the first failure, so independent failures later in the run are never seen, check_phase and the scoreboard's leftover checks never run, and one flaky protocol check can hide a data bug behind it. Errors with a quit count give the same visibility and keep the rest of the evidence. Fatal is for a bench that cannot continue, such as a missing virtual interface.

    Next: the assertions are in place, controlled, measured and reported. The Leadership and Senior Judgment topic moves from the bench to the team that runs it.

    Related topics

    • Up: UVM with SVA and Formal

    • Report catchers

    • Report server and verdict

    • Regression triage by signature

    Previous

    261 / 280

    Next: Leadership and Senior Judgment

    Up next

    Up nextLeadership and Senior Judgment

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Where Assertions Live
    • Assertion Control from the Test
    • Cover Properties and Coverage Closure