Debugging common UVM failures is mostly a matter of recognising a handful of patterns: a null handle, a test that ends at time zero, a test that hangs until the timeout, a broken driver-sequencer handshake, a factory override that does nothing, and a configuration lookup that fails. Each has a small set of usual causes and a quick way to confirm which one you have.
This article is organised by symptom. For each one it lists the likely causes in the order worth checking them, and points to the UVM switch or call that confirms the diagnosis without adding print statements.
9 min read | Intermediate
What You'll Learn
- Diagnose null handle errors in components and sequences
- Find out why a test ends immediately or never ends
- Recognise broken driver-sequencer handshakes
- Work out why a factory override or a config_db setting has no effect
First Steps for Any Failure
- Read the first error, not the last. One real problem often causes a cascade of later errors. Rerun with
+UVM_MAX_QUIT_COUNT=1,NOso the simulation stops at the first one. - Check the warnings above it. A warning about a failed lookup or an unregistered type early in the log is frequently the actual cause of a fatal much later.
- Use the built-in traces before adding prints.
+UVM_CONFIG_DB_TRACE,+UVM_OBJECTION_TRACEand+UVM_PHASE_TRACEanswer most "why did this happen" questions without recompiling. They are described in UVM Command Line Options and Built-in Debug.
Symptom: Null Handle Error
The simulator stops with a null object access, pointing at a line in a component or sequence. Find which handle on that line is null, then check these causes in order.
A virtual interface that was never received
The most common case. The driver or monitor called uvm_config_db::get() for its interface, the call failed, nothing checked the result, and the first access to vif in run_phase crashes. Always check the result where it is fetched, so the error appears at the real cause instead of much later:
if (!uvm_config_db #(virtual bus_if)::get(this, "", "vif", vif))
`uvm_fatal("NOVIF", {"Virtual interface not set for ", get_full_name()})
Then find out why the lookup failed, as described further down in this article under config_db.
A component used before it was created
Children are created in build_phase and connected in connect_phase. A null handle in connect_phase usually means the child was never created: a missing type_id::create(), a create guarded by a configuration bit that was off, or a passive agent whose driver doesn't exist. uvm_top.print_topology() at the end of elaboration shows what was really built.
A sequence with no sequencer
A sequence started with a null sequencer can't send items. Inside it, m_sequencer and p_sequencer are null, and so is anything looked up through them. Check the start() call, and that the path to the sequencer exists in an active agent.
Symptom: The Test Ends Immediately
The log shows the test starting and the report summary at time 0, or very early, with none of the expected traffic. The run phase ended because no objection was raised while it mattered.
- The test starts its sequence in
run_phasewithoutphase.raise_objection(this)first. - The objection is raised, but after a delay or after a blocking call, so the phase had already ended.
- The sequence runs as a default sequence and relies on its own objection, but never raises one.
+UVM_OBJECTION_TRACE shows every raise and drop with the time and the object responsible. An empty trace for the run phase confirms the cause. The rules are in UVM Objection.
Symptom: The Test Hangs Until the Timeout
The test runs until the global timeout ends it with a fatal error. Something is waiting for an event that never comes, and something else is holding an objection while it waits.
Start with +UVM_OBJECTION_TRACE. The objection that is never dropped tells you which component or sequence is stuck. Typical causes:
- The driver never calls
item_done()on some path, often an error path or a protocol case the driver doesn't handle, so the sequence waits infinish_item()forever. - The driver is waiting for a DUT signal that never arrives: a missing clock, a reset never released, or a
readythe DUT never raises. - A sequence waits in
get_response()for a response the driver never sends. - A sequencer was left locked or grabbed, so every other sequence on it is blocked.
- A never-ending sequence, such as a responder, raised an objection it can never drop.
While debugging a hang, shorten the timeout with +UVM_TIMEOUT so each attempt doesn't take the full default time to fail.
Quick Check
Q: The objection trace shows the test raised one objection and never dropped it. The test's run_phase starts a sequence and drops the objection after start() returns. Where do you look next?
Show Answer
At why start() never returns. The sequence is blocked: usually in finish_item() because the driver never called item_done(), or in get_response() waiting for a response that never comes. Check where the driver is waiting at the time of the hang.
Symptom: Handshake Errors from the Sequencer
The sequencer checks that the driver uses the handshake correctly and reports an error when it doesn't. The usual messages say that item_done() was called with no outstanding request, or that get_next_item() was called twice without an item_done() in between.
- item_done() with nothing outstanding: the driver called
item_done()twice for one item, often once in a normal path and again in an error path, or after usingget(), which already completes the item. - get_next_item() twice: a code path skips
item_done(), for example an earlycontinuein the driver loop. - Mixing styles: one item fetched with
get_next_item()and finished withput(), or fetched withget()and then finished withitem_done().
Each fetch must be paired with exactly one completion of the matching style, as described in Using get_next_item() and Using get() and put().
Symptom: A Factory Override Does Nothing
The test sets an override, but the original class still runs. print_topology() shows the old type, and the factory's print() shows the override registered. The usual reasons:
- The object is created with
new(). Onlytype_id::create()goes through the factory, so anew()call ignores every override. - The override is set too late. It must be in place before the object is created. A test that calls
super.build_phase()and creates the environment before setting the override is too late for the components it contains. - The instance path is wrong. An instance override path must match the full hierarchical name of the component being created. Copy it from
print_topology()rather than typing it. - The replacement doesn't extend the original. An override must be a subclass of the type it replaces, or the factory rejects it.
factory.debug_create_by_type() and debug_create_by_name() run the same search as a create, without creating anything, and report which override would apply. More on overrides in UVM Factory Override.
Symptom: A config_db Lookup Fails
A get() returns 0 even though a matching set() exists. +UVM_CONFIG_DB_TRACE lists both, so compare them field by field:
- Type: the type parameter must be identical on both sides.
intandbit [31:0]are different types, and so are two parameterizations of the same interface. - Field name: the strings must match exactly, including case.
- Scope: the set's context plus instance path must match the get's full path. A scope like
"m_agent"reaches the agent but not its children;"m_agent*"reaches both. - Timing: the set must happen before the get. A set in a parent's
connect_phaseis too late for a child'sbuild_phase.
uvm_config_db Examples walks through these cases with the trace output.
Common Mistakes
Mistake #1: Chasing the last error in the log
Later errors are often consequences of the first one. Stop at the first error and fix that before looking further.
Mistake #2: Unchecked config_db gets
An unchecked get() turns a clear configuration error into a confusing null handle crash later. Check every get() that the component cannot work without, and fail with a message naming the component.
Mistake #3: Adding print statements first
The built-in traces already report objections, configuration lookups and phases. Use them before editing and recompiling the testbench.
Mistake #4: Debugging hangs with the full default timeout
Every attempt then takes as long as the timeout allows. Set a short +UVM_TIMEOUT while debugging and restore it afterwards.
Summary
In this article, you learned:
- Null handles usually come from an unreceived virtual interface, an uncreated component or a sequence without a sequencer
- A test that ends at once has no objection raised; one that hangs has an objection never dropped, usually by a blocked sequence
- Handshake errors come from unpaired or mixed
get_next_item(),get(),item_done()andput()calls - Overrides fail when objects are created with
new(), when the override is set too late, or when the path is wrong - config_db lookups fail on type, name, scope or timing, and the trace shows which