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.
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.
Symptom | Question | Command line | In code (runs everywhere) |
|---|---|---|---|
Override seems ignored; wrong class built | What did the factory make? | none ( |
|
Null virtual interface; a knob has no effect | Did my set reach the getter? |
|
|
Test hangs, or ends at time 0 | Who holds the phase open? |
|
|
A runtime phase seems skipped; order surprise | Which phase runs when? |
| no public call |
Need detail from one component only | What crossed this interface? |
|
|
Need items next to waveforms | Which item ran when? |
|
|
Did the run pass at all? | One line a script can trust | none | a report server that prints a verdict in |
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.
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 DPIThe 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.
// 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
endclassThe 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.
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)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.
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'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.
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)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.
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)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:
// 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
endclassUVM_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.
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:
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.
+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.
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 phaseA 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.
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.
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.
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.