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
    • Debug Toolkit and Playbooks
      • The UVM Debug Toolkit
      • Hang Playbook
      • Scoreboard Mismatch Playbook
      • False-Pass Playbook
      • Early-End Playbook
    • Regression and Sign-off
  • Coding Practice47
  • Verification in Industry Practice10
  • Senior Prep: Architecture, Leadership and Interviews32
Loading lesson

‹Debug Toolkit and Playbooks
  1. Topics
  2. UVM Verification
  3. Debug & Regression
  4. Debug Toolkit and Playbooks
  5. The UVM Debug Toolkit

Part 11 · Debug & Regression · Intermediate

The UVM Debug Toolkit

Topology, factory, config_db tracing, objection and phase tracing, per-ID verbosity, recording and the verdict line: which instrument answers which question, with real logs.

On this page

    It is late, a nightly test is red, and the log is 40,000 lines long. A colleague suggests turning on every trace and reading the lot. Another suggests adding $display calls and rebuilding. Both waste an hour. UVM already ships an instrument for each question you are likely to ask: what was built, which class the factory made, whether a configuration value reached its target, who is holding the test open, what crossed one interface. The skill is knowing which instrument answers which question, and how to switch it on when the command line is not available.

    This lesson is the toolbox for every playbook that follows. You will see each instrument run on the DMA bench, with its real output.

    Why every instrument answers exactly one question

    A doctor does not order every scan for a cough. They ask a question (is it the lungs?) and pick the test that answers it. Debug works the same way. Each UVM instrument looks at one layer of the testbench: the component tree (topology), the factory (which class was made for which request), the resource pool (config_db sets and gets), the phase machinery (objections and phase transitions), the report system (verbosity per message ID and per component) and the transaction stream (recording). Switching all of them on at once buries the answer in noise; switching on the right one gives you a dozen lines that settle the question.

    Most instruments have two switches: a plusarg read by the library at startup, and a programmatic call you put in a test. The plusargs are the everyday tool on a commercial simulator, because they need no rebuild. The calls matter when you cannot rebuild the command line (a regression launcher that fixes it), when you want the instrument on for one component only, and on builds where plusargs are not seen at all, like this course's Verilator build.

    Which instrument for which symptom?

    Symptom

    Question

    Command line

    In code (runs everywhere)

    Override seems ignored; wrong class built

    What did the factory make?

    none (+UVM_FACTORY_TRACE does not exist)

    uvm_root::get().print_topology(), uvm_factory::get().print(), debug_create_by_type()

    Null virtual interface; a knob has no effect

    Did my set reach the getter?

    +UVM_CONFIG_DB_TRACE, +UVM_RESOURCE_DB_TRACE

    uvm_config_db_options::turn_on_tracing(), check_config_usage(), uvm_config_db#(T)::dump()

    Test hangs, or ends at time 0

    Who holds the phase open?

    +UVM_OBJECTION_TRACE

    phase.get_objection().trace_mode(1), display_objections()

    A runtime phase seems skipped; order surprise

    Which phase runs when?

    +UVM_PHASE_TRACE

    no public call

    Need detail from one component only

    What crossed this interface?

    +uvm_set_verbosity=<comp>,<id>,<verbosity>,<phase>, +UVM_VERBOSITY=UVM_HIGH

    set_report_id_verbosity(), set_report_id_verbosity_hier(), set_report_verbosity_level_hier()

    Need items next to waveforms

    Which item ran when?

    +uvm_set_config_int=<comp>,recording_detail,400

    uvm_config_db#(int)::set(this, "env.in_agt.*", "recording_detail", UVM_FULL)

    Did the run pass at all?

    One line a script can trust

    none

    a report server that prints a verdict in report_summarize

    uvm_config_db#(T)::dump() prints the whole resource pool, whatever T is, so it answers "what is in there", not "what did this get see". For the second question use the trace, which logs every get with the scope that asked and whether it found anything.

    When does each instrument fire?

    Flowchart: config tracing starts in the test constructor, sets and gets are traced in build, topology and factory are printed at end of elaboration, objections, verbosity and recording act in run, display_objections fires just before a timeout, and the verdict prints in report_summarize.
    Where each instrument belongs in the phase order. Turn config tracing on before build, read the static picture at end of elaboration, and trace the dynamic picture during run.

    Warning

    This course's Verilator build does not see most plusargs. The bench is built with UVM_NO_DPI, so the library's command-line processor has no arguments to read: +UVM_OBJECTION_TRACE, +UVM_CONFIG_DB_TRACE, +UVM_PHASE_TRACE, +uvm_set_verbosity and +UVM_TIMEOUT are silently ignored. Only +UVM_TESTNAME still works, through a fallback that reads it with $value$plusargs. The library announces the mode at the top of every log, as shown below. On EDA Playground with a commercial simulator, the plusargs work as documented. Everything below therefore uses the programmatic switches, which run on every simulator.

    Diagram
    UVM_WARNING base/uvm_test_runner.svh(207) @ 0: reporter [NO_DPI_USED] We are thinking of removing support for UVM_NO_DPI.  Please try this test without it and evaluate the impact
    UVM_INFO base/uvm_test_runner.svh(210) @ 0: reporter [NO_DPI_TSTNAME] UVM_NO_DPI defined--getting UVM_TESTNAME directly, without DPI

    Build it: one debug test that switches the instruments on in code

    The scratch test below extends dma_truncate_test, which type-overrides the stream driver, so the factory has something to report. Each line turns on one instrument at the point in the phase order where it belongs. Nothing in the bench changes: this is the shape of a debug test you keep next to a regression and point at a failing scenario.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // snippet: scratch test built on the course bench (dbg_tb.sv includes dma_tb.sv)
    class dbg_toolkit_test extends dma_truncate_test;
      `uvm_component_utils(dbg_toolkit_test)
      function new(string name, uvm_component parent);
        super.new(name, parent);
        install_verdict_server();                    // one PASS/FAIL line at the very end
        uvm_config_db_options::turn_on_tracing();    // = +UVM_CONFIG_DB_TRACE, from here on
      endfunction
      function void end_of_elaboration_phase(uvm_phase phase);
        uvm_factory f = uvm_factory::get();
        super.end_of_elaboration_phase(phase);       // the base test prints the topology
        uvm_config_db_options::turn_off_tracing();   // build is over: stop the noise
        f.print();                                   // overrides and registered types
        f.debug_create_by_type(axis_driver::get_type(), "uvm_test_top.env.in_agt", "drv");
        env.apb.mon.set_report_id_verbosity("APB_MON", UVM_HIGH);   // one ID, one component
      endfunction
      task run_phase(uvm_phase phase);
        phase.get_objection().trace_mode(1);         // = +UVM_OBJECTION_TRACE
        super.run_phase(phase);
      endtask
    endclass

    Watch it run

    Topology: what was really built

    The topology is the first thing to read when a component "does nothing". It shows the type of every instance, so a factory override that worked is visible at a glance: in_agt.drv is an axis_truncating_driver, and out_agt has only a monitor, so it really is passive. A component that is missing from this table was never created, and no amount of tracing inside it will help.

    Diagram
    UVM_INFO base/uvm_root.svh(531) @ 0: reporter [UVMTOP] UVM testbench topology:
    ----------------------------------------------------------------
    Name                         Type                    Size  Value
    ----------------------------------------------------------------
    <unnamed>                    uvm_root                -     @70
      uvm_test_top               dbg_toolkit_test        -     @180
        env                      dma_env                 -     @252
          apb                    apb_agent               -     @270
            drv                  apb_driver              -     @502
            mon                  apb_monitor             -     @338
            sqr                  uvm_sequencer           -     @357
          in_agt                 axis_agent              -     @278
            drv                  axis_truncating_driver  -     @716
            mon                  axis_monitor            -     @552
            sqr                  uvm_sequencer           -     @571
          out_agt                axis_agent              -     @286
            mon                  axis_monitor            -     @766
          sb                     dma_scoreboard          -     @294
    (ports, exports and sequencer fields left out)

    The factory: overrides and one request explained

    print() lists the registered overrides and, with the default argument, the user types the factory knows. debug_create_by_type() replays one request (a type plus the instance path that would ask for it) and states the result. In this library's output the table of considered overrides came back empty even though a type override applies; the Result line is the part to trust, because it is exactly what create() would return for that path.

    Diagram
    UVM_INFO base/uvm_factory.svh(2050) @ 0: reporter [UVM/FACTORY/PRINT]
    #### Factory Configuration (*)
    
    No instance overrides are registered with this factory
    
    Type Overrides:
    
      Requested Type  Override Type
      --------------  ----------------------
      axis_driver     axis_truncating_driver
    
    All types registered with the factory: 282 total
    (the list of user types follows)
    
    UVM_INFO base/uvm_factory.svh(2235) @ 0: reporter [UVM/FACTORY/DUMP]
    #### Factory Override Information (*)
    
    Given a request for an object of type 'axis_driver' with an instance
    path of 'uvm_test_top.env.in_agt.drv' the factory encountered
    
    no relevant overrides.
    
    Result:
    
      The factory will produce an object of type 'axis_truncating_driver'

    Config tracing: every set and every get

    With tracing on, every set and get prints its scope, its field name, who asked, and whether it found a value. A null virtual interface becomes a one-line answer: find the get for vif from the failing component and read whether it says (failed lookup). Two warnings about the trace. Most of its lines are the library's own lookups (check_connection_relationships, wait_for_sequences_count), which fail by design. And it only sees what happens after it is turned on: the top module's vif sets happen before run_test creates the test, so they are not in this trace.

    Diagram
    UVM_INFO base/uvm_config_db_implementation.svh(485) @ 0: reporter [CFGDB/GET] Configuration scope='uvm_test_top' name='irq_vif' (type PARAMTYPEDTYPE 'T') read accessor=uvm_test_top = (PARAMTYPEDTYPE 'T') 612379584
    UVM_INFO base/uvm_config_db_implementation.svh(485) @ 0: reporter [CFGDB/SET] Configuration scope='uvm_test_top.env.out_agt' name='is_active' (type PARAMTYPEDTYPE 'T') set accessor=uvm_test_top.env = (PARAMTYPEDTYPE 'T') 0
    UVM_INFO base/uvm_config_db_implementation.svh(485) @ 0: reporter [CFGDB/GET] Configuration scope='uvm_test_top.env.sb.apb_export' name='check_connection_relationships' (type PARAMTYPEDTYPE 'T') read accessor=uvm_test_top.env.sb.apb_export = null (failed lookup)
    UVM_INFO base/uvm_config_db_implementation.svh(485) @ 0: reporter [CFGDB/GET] Configuration scope='uvm_test_top.env.apb.mon' name='vif' (type PARAMTYPEDTYPE 'T') read accessor=uvm_test_top.env.apb.mon = (PARAMTYPEDTYPE 'T') 612379456
    UVM_INFO base/uvm_config_db_implementation.svh(485) @ 0: reporter [CFGDB/GET] Configuration scope='uvm_test_top.env.apb.sqr' name='wait_for_sequences_count' (type PARAMTYPEDTYPE 'T') read accessor=uvm_test_top.env.apb.sqr = null (failed lookup)

    Objections and per-ID verbosity

    The objection trace shows every raise and drop with the object that did it and the running totals, so "who is holding the run phase open" becomes a read, not a guess. In the same run, APB_MON messages (written at UVM_HIGH) appear from the APB monitor only: every register access is visible, while every other component stays quiet at its default verbosity.

    Diagram
    UVM_INFO @ 0: run_objection [OBJTN_TRC] Object uvm_test_top raised 1 run_objection objection(s): count=1  total=1
    UVM_INFO @ 0: run_objection [OBJTN_TRC] Object uvm_top added 1 run_objection objection(s) to its total (raised from source object uvm_test_top): count=0  total=1
    UVM_INFO dma_tb.sv(151) @ 75000: uvm_test_top.env.apb.mon [APB_MON] WR addr=0c data=3fbcbf2f
    UVM_INFO dma_tb.sv(151) @ 115000: uvm_test_top.env.apb.mon [APB_MON] WR addr=08 data=00000008
    UVM_INFO dma_tb.sv(151) @ 155000: uvm_test_top.env.apb.mon [APB_MON] WR addr=00 data=00000003
    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(151) @ 345000: uvm_test_top.env.apb.mon [APB_MON] RD addr=04 rdata=00000004
    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(151) @ 385000: uvm_test_top.env.apb.mon [APB_MON] WR addr=04 data=00000006
    UVM_INFO @ 485000: run_objection [OBJTN_TRC] Object uvm_test_top dropped 1 run_objection objection(s): count=0  total=0
    UVM_INFO @ 485000: run_objection [OBJTN_TRC] Object uvm_test_top all_dropped 1 run_objection objection(s): count=0  total=0
    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)

    Config written but never read

    A misspelled field name or scope does not fail. The set lands in the pool and nobody ever asks for it. check_config_usage(), called at end of elaboration, lists every resource with writes and no reads. A second scratch test set recording_detail correctly and a field called max_beats that no component reads:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // snippet: scratch test (dbg_tb.sv)
    class dbg_config_test extends dma_smoke_test;
      `uvm_component_utils(dbg_config_test)
      function new(string name, uvm_component parent);
        super.new(name, parent);
      endfunction
      function void build_phase(uvm_phase phase);
        uvm_config_db#(int)::set(this, "env.in_agt.*", "recording_detail", UVM_FULL);
        uvm_config_db#(int)::set(this, "env.in_agt.sqr", "max_beats", 4);   // nobody gets max_beats
        super.build_phase(phase);
      endfunction
      function void end_of_elaboration_phase(uvm_phase phase);
        uvm_root::get().check_config_usage();
      endfunction
    endclass
    Diagram
    UVM_INFO @ 0: reporter [CFGNRD]  ::: The following resources have at least one write and no reads :::
    UVM_INFO base/uvm_resource_pool.svh(1292) @ 0: reporter [UVM/RESOURCE_POOL/PRINT_QUEUE] (uvm_queue#(uvm_resource_base)@708) {
      [0]: {
        name: max_beats
        value: 4
        scope: uvm_test_top.env.in_agt.sqr
        precedence: 'd999
        accesses: {
          [uvm_test_top]: reads: 0 @ 0  writes: 1 @ 0
        }
      }
      [1]: {
        name: rst_vif
        value: 2571119744
        scope: uvm_test_top*
        precedence: 'd1000
        ...
      [2]: {
        name: vif
        value: 2571119552
        scope: uvm_test_top.env.irq.*
        precedence: 'd1000
        ...

    max_beats is the planted mistake. The other two entries are real findings about the bench: the top module sets rst_vif and the interrupt agent's vif for every test, and the smoke test has no component that reads them. That is harmless here, but on a big bench this list is where stale and misspelled configuration hides.

    Transaction recording

    With recording_detail set, the sequencer records every item it hands out. Commercial simulators send the records to their waveform database, where items appear as boxes above the signals. The reference library's default database is a text file, tr_db.log:

    Diagram
    CREATE_STREAM @155000 {NAME:pkt T:TVM SCOPE:uvm_test_top.env.in_agt.sqr STREAM:904}
        OPEN_RECORDER @155000 {TXH:906 STREAM:904 NAME:pkt TIME:155000 TYPE="Begin_No_Parent, Link"}
        OPEN_RECORDER @155000 {TXH:913 STREAM:904 NAME:it TIME:155000 TYPE="Begin_End, Link"}
          SET_ATTR @155000 {TXH:913 NAME:label VALUE:Transactions   RADIX:UVM_STRING BITS=20}
      LINK @155000 {TXH1:906 TXH2:913 RELATION=child}
          SET_ATTR @205000 {TXH:913 NAME:inst_id VALUE:909   RADIX:UVM_DEC BITS=32}
        CLOSE_RECORDER @205000 {TXH:913 TIME=205000}

    Only the begin and end times and the parent link were recorded, with no data fields, because axis_item has no do_record() and no field macros. Recording shows what you taught the item to record.

    Phase tracing

    +UVM_PHASE_TRACE logs every phase as it is scheduled, started, skipped and completed. There is no public call to turn it on from code; the scratch run below set the library's internal uvm_phase::m_phase_trace bit, which is fine for a one-off look but is not an API to put in a bench. The lines that matter are the SKIP lines: a runtime phase in which nobody raised an objection is skipped in zero time, which is why a sequence started from main_phase without an objection is killed before it sends anything.

    Diagram
    UVM_INFO base/uvm_phase_hopper.svh(444) @ 0: reporter [PH/TRC/STRT] Phase 'common.run' (id=207) Starting phase
    UVM_INFO base/uvm_phase_hopper.svh(444) @ 0: reporter [PH/TRC/STRT] Phase 'uvm.uvm_sched.main' (id=123) Starting phase
    UVM_INFO base/uvm_phase_hopper.svh(526) @ 0: reporter [PH/TRC/SKIP] Phase 'uvm.uvm_sched.main' (id=123) No objections raised, skipping phase
    UVM_INFO base/uvm_phase_hopper.svh(738) @ 655000: reporter [PH/TRC/DONE] Phase 'common.run' (id=207) Completed phase
    UVM_INFO base/uvm_phase_hopper.svh(444) @ 655000: reporter [PH/TRC/STRT] Phase 'common.check' (id=217) Starting phase

    What breaks: the trace that sent someone the wrong way

    A newcomer to the bench chased a null virtual interface with config tracing switched on in the test's constructor. The trace showed no set for vif anywhere, so they concluded the top module never set it and spent an hour on the top module. The sets were there all along: they run in the top's initial block before run_test, which is before the test exists, so a trace turned on by the test cannot see them. The get lines were the useful ones, and they showed the failing component asking under a different path from the one the top had used.

    The fix was procedural: trace from time 0 (the plusarg does that on a commercial simulator), or read the get lines and compare the scope with the path in the top's set. The broader lesson is that every instrument has a blind spot, and you need to know it before you trust a silence.

    Interview

    "A component's virtual interface is null at run time. How do you debug it?"

    A weak answer: "Add $display calls in the top and in the component to print the handle."

    A strong answer: "First print_topology() to confirm the component exists with the full path I expect. Then config tracing, +UVM_CONFIG_DB_TRACE or uvm_config_db_options::turn_on_tracing(), and I read the get from that component: its scope, the field name and whether it failed. The usual causes are a wildcard that does not match the getter's full name (env.agt.* does not match env.agt), a field-name typo, a type mismatch (virtual apb_if set, something else got), or a set made after the get. check_config_usage() at end of elaboration lists sets nobody read, which catches the typo directly."

    Follow-ups: Why does `dump()` not answer it? (it prints the whole pool, not who asked for what). Your trace is empty even with the plusarg. (the build may not pass arguments to the library, as with UVM_NO_DPI; switch tracing on in code).

    Check yourself You add a type override in your test's build_phase, after super.build_phase(), and the topology still shows the original driver. Which instrument confirms the cause, and what is it?

    The topology already shows the symptom. uvm_factory::get().print() shows the override is registered, and debug_create_by_type() for that path shows it would now apply. The cause is ordering: the base test's build_phase created the env, and the env created the agents, before the override existed. Register overrides before calling super.build_phase(), as dma_truncate_test does.

    Check yourself You need the APB monitor's per-transfer messages in a regression where the command line is fixed by the launcher, without flooding the log from every other component. What do you add, and where?

    In the test, at end of elaboration (once the component exists): env.apb.mon.set_report_id_verbosity("APB_MON", UVM_HIGH). It raises the verbosity of that one ID on that one component. set_report_verbosity_level_hier on the agent would raise everything under it, which is broader than needed.

    Check yourself The objection trace shows uvm_test_top raised at time 0 and nothing else, and the run ends on PH_TIMEOUT. What does that tell you?

    The test is still holding its objection: it never reached its drop_objection, so it is blocked on something it waits for. The next step is to find what the test is waiting on, which is the subject of the hang playbook.

    Next: the most common reason to reach for these instruments is a test that never ends. Hang Playbook turns a two-hour hang into a five-microsecond diagnosis.

    Related topics

    • Factory override debug patterns

    • Phase debugging

    • Reporting and verbosity

    • Command-line control

    Previous

    185 / 280

    Next: Hang Playbook

    Up next

    Up nextHang Playbook

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Scoreboard Mismatch Playbook
    • False-Pass Playbook
    • Early-End Playbook