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,NO so 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_TRACE and +UVM_PHASE_TRACE answer 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_phase without phase.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 in finish_item() forever.
  • The driver is waiting for a DUT signal that never arrives: a missing clock, a reset never released, or a ready the 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 using get(), which already completes the item.
  • get_next_item() twice: a code path skips item_done(), for example an early continue in the driver loop.
  • Mixing styles: one item fetched with get_next_item() and finished with put(), or fetched with get() and then finished with item_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(). Only type_id::create() goes through the factory, so a new() 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. int and bit [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_phase is too late for a child's build_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() and put() 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
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5