Skip to content

config_db Gotchas

Six gotchas that cause silent failures: phase ordering, type mismatch, name case, scope, null handles.

UVM Fundamentals · Module 9

The Six Silent Failure Categories

Every config_db failure has a root cause in one of these six categories. All of them share the same symptom: get() returns 0 and the variable keeps its uninitialized value — or worse, the component proceeds with the wrong data and produces incorrect results hours later in the regression.

G1 — Phase Ordering

set() called after the component's build_phase already ran. The database entry exists but the component missed it.

Symptom: uvm_fatal "not found in config_db" even though set() is in the code.

G2 — Type Mismatch

set() uses config_db #(int) but get() uses config_db #(bit [31:0]). Different T = different database.

Symptom: get() returns 0. Adding trace shows SET was called but GET is MISS.

G3 — Field Name Error

set("APB_VIF") vs get("apb_vif"). Field name comparison is case-sensitive and exact. A trailing space causes a miss.

Symptom: trace shows SET and GET both called, correct type, but still MISS.

G4 — Scope Mismatch

set() path pattern does not cover the getter's full path. Wildcard placement error or component renamed after override.

Symptom: trace shows SET scope and GET key — no pattern overlap.

G5 — Null Handle Silent Proceed

get() return value not checked. Variable stays null. Component proceeds with null interface — crashes 1000 lines later.

Symptom: null-handle runtime crash in run_phase, far from the actual failure site.

G6 — Wrong Phase for get()

get() called in connect_phase or run_phase for a value that was set() only in build_phase — works, but race condition if set() is in run_phase concurrently.

Symptom: intermittent failure depending on phase scheduling order.

set() and get() — Every Parameter, Every Gotcha Source

Every config_db miss traces back to a mismatch in one of four parameters between set() and get(). The API looks deceptively simple, but each parameter plays a specific role in how the database builds its internal scope key and resolves lookups. Understanding this precisely cuts debug time from hours to minutes.

SystemVerilog — set() and get() signatures, fully annotated
// ── set() — stores a value in the type-keyed global database ─────────────
static function bit set(
uvm_component cntxt,       // ① Anchor: cntxt.get_full_name() is the scope PREFIX
//   'this' in a component → scope is that component's path
//   'null' in tb_top/initial → inst_name IS the full pattern
string        inst_name,   // ② Pattern: appended to cntxt's path with a "."
//   Glob wildcards: '*' matches any chars including dots
//   "*.drv" matches any path ending in .drv
//   "" means only the cntxt's own path
string        field_name,  // ③ Key: case-sensitive, byte-for-byte exact string
//   "vif" ≠ "VIF" ≠ "vif " (trailing space!) ≠ "vif\n"
T             value        // ④ Value: stored keyed on {type T, scope, field_name}
//   Type T is part of the key — int ≠ bit[31:0]
);
 
// ── get() — retrieves value; returns 1 (HIT) or 0 (MISS) ────────────────
static function bit get(
uvm_component cntxt,       // ① Who is calling: 'this' in almost all cases
//   get_full_name() forms the base of the lookup path
string        inst_name,   // ② Suffix: usually "" — means search at cntxt's path
//   Non-empty: cntxt.full_name + "." + inst_name
string        field_name,  // ③ Key: must byte-for-byte match the set() field_name
inout T       value        // ④ Output: written ONLY on HIT
//   On MISS: variable is UNTOUCHED — not cleared, not zero
);
// Return value: 1 = HIT  |  0 = MISS.  ALWAYS check this.

How the Effective Scope Is Computed

The scope key stored in the database is computed differently for set() vs get(). A match occurs when the stored set() scope pattern glob-matches the get() computed path. They do not need to be the same string — only the pattern must cover the path.

Callercntxtinst_nameEffective Scope / Search Path
tb_top initial blocknull"uvm_test_top.*"uvm_test_top.* — inst_name is the full scope
uvm_test (set)this"*"uvm_test_top.* — cntxt path + "." + pattern
uvm_env (set)this"apb_agent.*"uvm_test_top.env.apb_agent.*
uvm_driver (get)this""uvm_test_top.env.apb_agent.drv — exact path, no suffix
uvm_driver (get, sub-lookup)this"sub_drv"uvm_test_top.env.apb_agent.drv.sub_drv

Parameter → Gotcha Reference

ParameterTypical MistakeGotchaPrevention
T (type)#(int) set, #(bit[31:0]) getG2 — Type MismatchUse typedef; keep same T in both set() and get()
cntxtWrong component used as anchor — scope prefix incorrectG4 — Scope Mismatchnull from tb_top; this from components
inst_name (set)Exact name used instead of wildcard — breaks on renameG4 — Scope MismatchUse "*" for broad reach; verify with get_full_name()
field_nameCase difference, typo, trailing space between set/getG3 — Field Name ErrorShared localparam string in a package
value (get output)Return value ignored — null/garbage silently propagatesG5 — Null HandleAlways: if (!get(...)) uvm_fatal(...)`
Call timingset() placed after super.build_phase() — cascade already ranG1 — Phase OrderingAll set() before super.build_phase() or before run_test()

Gotcha 1 — Phase Ordering: get() Before set()

The build_phase cascade runs top-down: test → env → agent → driver. If a test calls set() inside build_phase() but after super.build_phase(), the cascade has already finished and the driver's build_phase() has already called get() — and found nothing. ❌ WRONG — set() AFTER super.build_phase()error_test.build_phase()super.build_phase() ← FIRSTenv.build_phase()agent.build_phase()drv: get() → MISS ✗set() called here — TOO LATE✓ CORRECT — set() BEFORE super.build_phase()error_test.build_phase()set() called here — FIRST ✓super.build_phase() ← SECONDenv.build_phase()agent.build_phase()drv: get() → HIT ✓TIMING RULES SUMMARYset() in tb_top initial:→ Before run_test() — safe for all componentsset() in test.build_phase:→ Before super.build_phase() — reaches all childrenset() in env.build_phase:→ Before super.build_phase() — reaches env's childrenset() in run_phase (for run_phase consumers):→ Race condition risk if getter is in parallel run_phaseset() after super.build_phase():→ Too late for build_phase consumers — MISS Figure 1 — Phase ordering failure (left) vs correct ordering (right). The build_phase cascade runs top-down. set() must appear before the cascade reaches the component that calls get().

SystemVerilog — phase ordering gotcha and fix
// ❌ WRONG — set() AFTER super.build_phase() ─────────────────────────
class my_test extends base_test;
function void build_phase(uvm_phase phase);
super.build_phase(phase);   // ← triggers env→agent→driver build cascade
// driver already ran get("vif") and got MISS — now setting is pointless
uvm_config_db#(virtual apb_if)::set(this, "*.drv", "vif", apb_vif);
endfunction
endclass
// Simulator output: uvm_fatal CFG: "vif not found in config_db"
 
// ✓ CORRECT — set() BEFORE super.build_phase() ────────────────────────
class my_test extends base_test;
function void build_phase(uvm_phase phase);
// ALL set() calls go here — before the cascade
uvm_config_db#(virtual apb_if)::set(this, "*.drv", "vif", apb_vif);
uvm_config_db#(int)::set(this, "*", "num_txns", 200);
 
super.build_phase(phase);   // ← cascade starts — all components find their data
endfunction
endclass

Gotcha 2 — Type Mismatch: Wrong T Parameter

uvm_config_db #(T) is parameterised. Every distinct type T maintains a completely separate database. A set #(int) and a get #(bit [31:0]) with the same field_name will never match — even though int and bit [31:0] can hold the same values.

SystemVerilog — type mismatch examples
// ── Scenario 1: int vs. bit mismatch ─────────────────────────────────
// tb_top.sv:
uvm_config_db#(int)::set(null, "uvm_test_top.*", "timeout", 1000);
 
// apb_monitor.sv:
bit [31:0] timeout_val;
if (!uvm_config_db#(bit [31:0])::get(this, "", "timeout", timeout_val))
`uvm_fatal("CFG", "timeout not found")
// Fails! config_db #(int) ≠ config_db #(bit[31:0]) — different databases
// Fix: use int in both set() and get(), or cast consistently
 
// ── Scenario 2: virtual interface modport mismatch ────────────────────
// tb_top.sv — stores the full interface:
uvm_config_db#(virtual apb_if)::set(null, "uvm_test_top.*", "vif", apb_vif);
 
// apb_driver.sv — tries to get with modport:
virtual apb_if.master drv_vif;
if (!uvm_config_db#(virtual apb_if.master)::get(this, "", "vif", drv_vif))
`uvm_fatal("CFG", "vif not found")
// Fails! virtual apb_if ≠ virtual apb_if.master — different types
// Fix: store and retrieve as 'virtual apb_if' (no modport), assign modport in driver
 
// ── Correct approach: consistent types, assign modport locally ────────
virtual apb_if vif;
if (!uvm_config_db#(virtual apb_if)::get(this, "", "vif", vif))
`uvm_fatal("CFG", "vif not found")
// Now vif is valid. Access modport implicitly via port directions in the driver.
set() Typeget() TypeMatch?Fix
intintYES
intbit [31:0]NOUse int in both
virtual apb_ifvirtual apb_ifYES
virtual apb_ifvirtual apb_if.masterNORemove modport from get()
stringuvm_objectNOUse same type or subtype
apb_agent_configapb_agent_configYES
apb_agent_configuvm_objectNOType must match exactly

Gotcha 3 — Field Name: Case-Sensitive Exact Match

The field_name string is compared with strcmp() — byte-for-byte, case-sensitive. A single character difference between set() and get() produces a silent miss. This is the hardest gotcha to spot because both set() and get() appear to be using the same string.

SystemVerilog — field name mismatch examples
// ── Case mismatch ─────────────────────────────────────────────────────
uvm_config_db#(virtual apb_if)::set(null, "uvm_test_top.*", "APB_VIF", vif);
uvm_config_db#(virtual apb_if)::get(this, "", "apb_vif", vif);   // MISS — wrong case
 
// ── Trailing space (invisible in code review) ──────────────────────────
uvm_config_db#(int)::set(null, "uvm_test_top.*", "num_txns ", 100);  // note space
uvm_config_db#(int)::get(this, "", "num_txns", n);             // MISS — no space
 
// ── Typo ──────────────────────────────────────────────────────────────
uvm_config_db#(bit)::set(null, "uvm_test_top.*", "is_activee", 1);  // double-e
uvm_config_db#(bit)::get(this, "", "is_active", flag);          // MISS
 
// ── Prevention: use a shared constant string ──────────────────────────
package cfg_keys;
localparam string APB_VIF    = "apb_vif";
localparam string NUM_TXNS   = "num_txns";
localparam string IS_ACTIVE  = "is_active";
endpackage
 
// Both set() and get() reference the same constant — typos caught at compile time
uvm_config_db#(virtual apb_if)::set(null, "uvm_test_top.*", cfg_keys::APB_VIF, vif);
uvm_config_db#(virtual apb_if)::get(this, "", cfg_keys::APB_VIF, vif);

Gotcha 4 — Scope Mismatch: Path Pattern Errors

The scope pattern in set() is matched against the getter's full path using shell-style glob matching. The wildcard * matches any sequence of characters including dots. Getting the pattern wrong by one character produces a silent miss — no warning, no error.

SystemVerilog — scope pattern errors and fixes
// ── Component renamed — scope no longer matches ───────────────────────
// tb_top sets using old name:
uvm_config_db#(virtual apb_if)::set(null, "uvm_test_top.env.apb_agent.drv", "vif", vif);
 
// Agent was refactored and driver is now named "driver" not "drv":
// create("driver", this)  ←  path is now "uvm_test_top.env.apb_agent.driver"
// "uvm_test_top.env.apb_agent.drv" ≠ "uvm_test_top.env.apb_agent.driver" → MISS
// Fix: use wildcards for component-name-independent patterns
 
uvm_config_db#(virtual apb_if)::set(null, "uvm_test_top.*", "vif", vif);
// "uvm_test_top.*" matches any path starting with "uvm_test_top." → robust
 
// ── Wrong root when cntxt is not null ────────────────────────────────
// Inside my_env (full_name = "uvm_test_top.env"):
uvm_config_db#(int)::set(this, "apb_agent.*", "num_txns", 100);
// Scope = "uvm_test_top.env" + "." + "apb_agent.*"
// = "uvm_test_top.env.apb_agent.*"
// Covers: any component directly under apb_agent
 
// But actual agent is named "apb_agent_0" not "apb_agent" →
// Getter at "uvm_test_top.env.apb_agent_0.drv" → NO MATCH
// Fix: use wildcard in agent name
uvm_config_db#(int)::set(this, "apb_agent_*.*", "num_txns", 100);
// or even broader:
uvm_config_db#(int)::set(this, "*", "num_txns", 100);
// Scope: "uvm_test_top.env.*" — covers all children of env
 
// ── Debug: print the actual getter path ───────────────────────────────
// In the driver's build_phase, before get():
`uvm_info("PATH", $sformatf("My full path: %s", get_full_name()), UVM_LOW)
// Then compare with the inst_name pattern used in set()

Gotcha 5 — The Null Handle Silent Proceed

When get() returns 0 (miss), the output variable is not modified — it keeps whatever value it had before the call. For a virtual apb_if vif declared at class scope, the initial value is null.

If the get() return value is ignored, the driver proceeds with vif = null. The driver's build_phase completes without error. The connect_phase completes without error. The run_phase starts. The first time the driver writes vif.paddr = req.addr, the simulator crashes with a null-pointer runtime error — nowhere near the actual cause.

SystemVerilog — null handle trap and defensive patterns
// ❌ DANGEROUS — silently proceeds with null vif ───────────────────────
function void build_phase(uvm_phase phase);
super.build_phase(phase);
uvm_config_db#(virtual apb_if)::get(this, "", "vif", vif);
// Return value ignored — vif stays null if miss — crash happens in run_phase
endfunction
 
// ✓ SAFE — fatal on miss (recommended for all critical resources) ──────
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(virtual apb_if)::get(this, "", "vif", vif))
`uvm_fatal("CFG", $sformatf(
"vif not found — check tb_top set() call. I am at: %s",
get_full_name()))
// Crash here — at the right place with a clear message
endfunction
 
// ✓ OPTIONAL — warn on miss (for non-critical configuration) ──────────
int num_txns = 100;   // default — used if config_db entry not found
if (!uvm_config_db#(int)::get(this, "", "num_txns", num_txns))
`uvm_info("CFG", "num_txns not in config_db, using default 100", UVM_MEDIUM)
 
// ✓ SILENT — for truly optional parameters (suppress warning) ─────────
void'(uvm_config_db#(int)::get(this, "", "debug_level", debug_level));

+UVM_CONFIG_DB_TRACE — Reading the Debug Output

Enable +UVM_CONFIG_DB_TRACE on the command line to get a real-time log of every set() and get() call. This is the fastest way to diagnose any config_db failure because it shows both sides of the exchange and whether they matched. vsim +UVM_CONFIG_DB_TRACE work.tb_topUVM_INFO @ 0ns: SET uvm_config_db #(virtual apb_if) by: null context: uvm_test_top.* field: "apb_vif"UVM_INFO @ 0ns: GET uvm_config_db #(virtual apb_if) by: uvm_test_top.env.apb_agent.drv field: "apb_vif" result: HITUVM_INFO @ 0ns: GET uvm_config_db #(virtual apb_if) by: uvm_test_top.env.ahb_agent.drv field: "apb_vif" result: MISSUVM_INFO @ 0ns: GET uvm_config_db #(virtual apb_if.master) by: uvm_test_top.env.apb_agent.drv field: "apb_vif" result: MISS (type mismatch)SET — value stored in databaseScope: "uvm_test_top." Key: "apb_vif"GET result: HIT — vif populated successfullyGetter path ".apb_agent.drv" matches scope "uvm_test_top."Type matches. Field name matches. → SUCCESS.GET result: MISS — scope does not matchGetter is ahb_agent.drv but SET scope is uvm_test_top.Wait — "uvm_test_top." SHOULD match ".ahb_agent.drv"!GET result: MISS — type mismatchSET used "virtual apb_if"GET used "virtual apb_if.master" — different type!HIT= get() succeeded, value populated. MISS= get() returned 0. Figure 2 — Annotated +UVM_CONFIG_DB_TRACE output. Each SET shows what was stored. Each GET shows who requested it and whether the result was HIT or MISS. The type parameter appears explicitly — making type mismatches visible at a glance.

Shell — enabling CONFIG_DB_TRACE on different simulators
## Questa / ModelSim
vsim +UVM_CONFIG_DB_TRACE work.tb_top
 
## VCS
vcs +UVM_CONFIG_DB_TRACE ... && simv +UVM_CONFIG_DB_TRACE
 
## Xcelium
xrun -uvm_config_db_trace work.tb_top
 
## Combined: factory + config_db trace (maximum debug)
vsim +UVM_CONFIG_DB_TRACE +UVM_FACTORY_OVERRIDE_TRACE work.tb_top
 
## Filter output to only see MISS results (grep after the fact):
vsim +UVM_CONFIG_DB_TRACE work.tb_top |& grep -A3 "MISS"

How to Read a MISS

When you see a MISS, check four things in the trace output in this order:

CheckLook ForFailure Sign
1. Was SET ever called?Search for SET ... field: "your_key" above the MISSNo SET line → set() was never called or field_name differs
2. Do types match?Compare #(T) in SET vs GET linesDifferent T → type mismatch (G2)
3. Do field names match?Compare field: "..." exactlyAny difference → field name error (G3)
4. Does scope cover getter path?Compare SET context pattern vs GET by: pathNo pattern match → scope mismatch (G4)

Debug Checklist — Use When get() Returns 0

  • Enable +UVM_CONFIG_DB_TRACE and run the simulation.
  • Search the trace log for SET ... field: "your_key" — confirm set() was called.
  • If no SET line: set() was never called, wrong field_name, or wrong type T in set().
  • If SET exists: compare #(T) type parameter in SET vs GET — must be byte-for-byte identical.
  • Compare field: "..." strings in SET and GET — case-sensitive exact match required.
  • Compare SET context+inst_name scope vs GET by: path — scope must cover getter path.
  • Add ``uvm_info("PATH", get_full_name(), UVM_LOW)` in the getter's build_phase to see the exact path at runtime.
  • Verify set() is called BEFORE super.build_phase() in the test/env where set() lives.
  • Verify set() is called BEFORE run_test() in tb_top for interface handles.
  • Use a shared localparam string constant for field names — eliminates typos at compile time.
SystemVerilog — config_db gotcha prevention pattern
// ── Prevention pattern — use this in every project ────────────────────
 
// 1. Centralise key strings
package cfg_keys_pkg;
localparam string APB_VIF   = "apb_vif";
localparam string NUM_TXNS  = "num_txns";
localparam string AGENT_CFG = "agent_cfg";
endpackage
 
// 2. Set before run_test() or before super.build_phase()
initial begin
uvm_config_db#(virtual apb_if)::set(
null, "uvm_test_top.*", cfg_keys_pkg::APB_VIF, apb_vif);
run_test();
end
 
// 3. Always check return value with uvm_fatal for critical data
virtual apb_if vif;
if (!uvm_config_db#(virtual apb_if)::get(this, "", cfg_keys_pkg::APB_VIF, vif))
`uvm_fatal("CFG", $sformatf(
"%s: apb_vif not found. Full path: %s. Check tb_top set() call.",
get_type_name(), get_full_name()))
 
// 4. Debug command — add when anything is suspicious
// vsim +UVM_CONFIG_DB_TRACE work.tb_top |& grep -B2 -A3 "MISS"

Where These Gotchas Appear in Real Verification Projects

In theory, config_db is three lines: set in tb_top, get in the driver, done. In practice, a real VIP has twelve agents, four configuration objects, a shared base test that 30 directed tests inherit from, and sequences that need runtime knobs in their body(). Each combination has a specific failure surface. Here is a map of where each gotcha category reliably bites in production testbenches.

Pattern 1 — Virtual Interface Distribution

The most common config_db use case. tb_top holds the actual interface instances and must distribute virtual handles to drivers and monitors before any component tries to use them.

SystemVerilog — canonical VIF distribution pattern
// ── tb_top.sv — THE right place to set virtual interfaces ────────────
module tb_top;
logic clk;
apb_if  apb_vif(clk);    // instance in tb_top
axi_if  axi_vif(clk);
 
initial begin
// Set before run_test() — the only 100% safe position
uvm_config_db#(virtual apb_if)::set(null, "uvm_test_top.*", "apb_vif", apb_vif);
uvm_config_db#(virtual axi_if)::set(null, "uvm_test_top.*", "axi_vif", axi_vif);
run_test();
end
endmodule
 
// ── apb_driver.sv — gets its VIF in build_phase ───────────────────────
class apb_driver extends uvm_driver#(apb_seq_item);
`uvm_component_utils(apb_driver)
virtual apb_if vif;   // null until build_phase
 
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(virtual apb_if)::get(this, "", "apb_vif", vif))
`uvm_fatal("VIF", $sformatf(
"%s: apb_vif not found. tb_top set() before run_test()? Path: %s",
get_type_name(), get_full_name()))
endfunction
 
task run_phase(uvm_phase phase);
seq_item_port.get_next_item(req);
// vif is guaranteed valid here — uvm_fatal fired otherwise
vif.paddr  <= req.addr;
vif.pwdata <= req.data;
endtask
endclass

Pattern 2 — Configuration Object from Test to Agent

For anything more complex than a single integer, pass a configuration object. One set(), one get(), and the entire agent config travels as a single handle — no field-name errors, no type mismatches across multiple set() calls.

SystemVerilog — config object pattern (production-ready)
// ── apb_agent_cfg.sv — all agent knobs in one object ─────────────────
class apb_agent_cfg extends uvm_object;
`uvm_object_utils(apb_agent_cfg)
bit         is_active  = 1;
int         num_txns   = 100;
bit         has_checks = 1;
int         baud_rate  = 115200;
endclass
 
// ── base_test.sv — creates and distributes the config ─────────────────
class base_test extends uvm_test;
apb_agent_cfg agent_cfg;
 
function void build_phase(uvm_phase phase);
agent_cfg = apb_agent_cfg::type_id::create("agent_cfg");
agent_cfg.is_active = 1;
 
// Single set() — type is apb_agent_cfg, not int or bit
uvm_config_db#(apb_agent_cfg)::set(this, "env.apb_agent", "cfg", agent_cfg);
super.build_phase(phase);   // ← cascade — agent gets it in its build_phase
endfunction
endclass
 
// ── error_test.sv — overrides specific fields before passing ──────────
class error_test extends base_test;
function void build_phase(uvm_phase phase);
super.build_phase(phase);  // base creates and sets agent_cfg
// Modify AFTER retrieval — but only if this test's scope also sets it
agent_cfg.has_checks = 0;   // override a field — no new set() needed
endfunction
endclass
 
// ── apb_agent.sv — retrieves config object in build_phase ────────────
class apb_agent extends uvm_agent;
apb_agent_cfg cfg;
 
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(apb_agent_cfg)::get(this, "", "cfg", cfg))
`uvm_fatal("CFG", "apb_agent_cfg not found in config_db")
// Pass it down to driver and monitor via config_db or direct handle
endfunction
endclass

Pattern 3 — Scoreboard and Monitor Configuration

Scoreboards and monitors need test-controlled knobs — error tolerance thresholds, protocol-specific timeouts, coverage enable flags. These are typically integers or bits set by the test and retrieved in the component's build_phase. The type mismatch gotcha (G2) is most common here because two engineers independently write the set() and get() without coordinating the type.

SystemVerilog — scoreboard configuration with type-safe keys
// ── sb_cfg_keys.sv — shared key constants prevent ALL name errors ─────
package sb_cfg_keys;
localparam string PKT_TIMEOUT  = "pkt_timeout_ns";
localparam string MAX_ERRORS   = "max_errors";
localparam string COV_ENABLE   = "cov_enable";
endpackage
 
// ── test.sv — sets scoreboard knobs ───────────────────────────────────
import sb_cfg_keys::*;
function void build_phase(uvm_phase phase);
uvm_config_db#(int)::set(this, "env.sb", PKT_TIMEOUT, 500);
uvm_config_db#(int)::set(this, "env.sb", MAX_ERRORS,  0);
uvm_config_db#(bit)::set(this, "env.sb", COV_ENABLE,  1);
super.build_phase(phase);
endfunction
 
// ── apb_scoreboard.sv — retrieves with same type ─────────────────────
import sb_cfg_keys::*;
int  pkt_timeout_ns = 200;  // safe defaults
int  max_errors     = 5;
bit  cov_enable     = 0;
 
function void build_phase(uvm_phase phase);
super.build_phase(phase);
void'(uvm_config_db#(int)::get(this, "", PKT_TIMEOUT, pkt_timeout_ns));
void'(uvm_config_db#(int)::get(this, "", MAX_ERRORS,  max_errors));
void'(uvm_config_db#(bit)::get(this, "", COV_ENABLE,  cov_enable));
// void' suppresses lint; defaults are safe so MISS is OK here
endfunction

Pattern 4 — Sequence Knobs in run_phase (Race Condition Risk)

Sequences run in run_phase. If a test calls set() in run_phase and a sequence calls get() in its body(), there is a race — both run concurrently in the same time step. This is Gotcha 6 and is the hardest to reproduce deterministically.

SystemVerilog — sequence-level knob: safe vs race-condition pattern
// ❌ RACE — test sets in run_phase, sequence gets in run_phase body() ──
// apb_test.sv run_phase:
task run_phase(uvm_phase phase);
phase.raise_objection(this);
uvm_config_db#(int)::set(null, "*", "burst_len", 16); // ← run_phase set()
my_seq.start(sequencer);   // sequence starts — but body() may have already begun
phase.drop_objection(this);
endtask
 
// apb_seq.sv body():
task body();
int burst_len = 4;   // default
void'(uvm_config_db#(int)::get(null, "*", "burst_len", burst_len));
// In Questa, start() is blocking — sequence starts after set() — OK.
// But in parallel sequences, this is non-deterministic.
endtask
 
// ✓ SAFE — pass knobs through build_phase, not run_phase ─────────────
function void build_phase(uvm_phase phase);
uvm_config_db#(int)::set(this, "*", "burst_len", 16); // ← build_phase = safe
super.build_phase(phase);
endfunction

Gotcha Location Reference — Where Each Bug Appears

ComponentWhat config_db CarriesMost Common GotchaQuick Fix
uvm_driverVirtual interface (VIF)G5 — null handle (get() return ignored)if (!get) uvm_fatal`
uvm_monitorVirtual interface (VIF)G4 — scope mismatch (path changed after rename)Print get_full_name(); widen wildcard
uvm_agentConfig objectG1 — phase ordering (set() after cascade)Move set() before super.build_phase()
uvm_scoreboardInteger knobs, bit flagsG2 — type mismatch (int vs bit[31:0])Shared localparam type + key constants
uvm_sequenceRuntime knobs in body()G6 — phase race (set in run_phase concurrently)Set knobs in build_phase instead
uvm_envSub-agent config objectsG3 — field name error (two devs out of sync)Shared package with localparam key strings

§9 — Code Examples

Example 1 — Beginner: Phase Ordering, Side by Side

The most common gotcha. Two tests, same DUT, same environment — one works and one doesn't. The only difference is where set() appears relative to super.build_phase().

SystemVerilog — phase ordering: broken vs correct
// ── broken_test.sv (fails silently) ───────────────────────────────────
class broken_test extends base_test;
`uvm_component_utils(broken_test)
function void build_phase(uvm_phase phase);
super.build_phase(phase);   // ← env→agent→drv build cascade fires HERE
// driver.build_phase() already ran — get("vif") already returned 0
uvm_config_db#(virtual apb_if)::set(this, "*", "vif", vif);
endfunction
endclass
 
// ── correct_test.sv (passes) ───────────────────────────────────────────
class correct_test extends base_test;
`uvm_component_utils(correct_test)
function void build_phase(uvm_phase phase);
// set() before cascade — every component that needs vif will find it
uvm_config_db#(virtual apb_if)::set(this, "*", "vif", vif);
super.build_phase(phase);   // ← cascade starts, driver gets HIT
endfunction
endclass
 
// ── tb_top.sv — safest: set() before run_test() ───────────────────────
module tb_top;
apb_if apb_vif(clk);
initial begin
// set before run_test → arrives before ANY component's build_phase
uvm_config_db#(virtual apb_if)::set(null, "uvm_test_top.*", "vif", apb_vif);
run_test();
end
endmodule

Example 2 — Intermediate: Catching Type Mismatch with TRACE

This shows what you actually see on the terminal when a type mismatch occurs. The trace output is the fastest way to isolate which gotcha category hit you.

SystemVerilog — type mismatch detection workflow
// ── tb_top stores with type 'int' ─────────────────────────────────────
uvm_config_db#(int)::set(null, "uvm_test_top.*", "pkt_count", 500);
 
// ── monitor tries to get with 'bit [31:0]' ────────────────────────────
bit [31:0] pkt_count;
if (!uvm_config_db#(bit [31:0])::get(this, "", "pkt_count", pkt_count))
`uvm_fatal("CFG", "pkt_count not found")  // ← fires, even though set() ran
 
// ── What +UVM_CONFIG_DB_TRACE shows ──────────────────────────────────
//
//  SET uvm_config_db #(int)            by: null
//      context: uvm_test_top.*  field: "pkt_count"
//
//  GET uvm_config_db #(bit[31:0])      ← different type!
//      by: uvm_test_top.env.mon  field: "pkt_count"
//      result: MISS
//
// The #(T) parameter printed by SET and GET are different.
// That is the entire diagnosis. Change get() to use 'int'.
 
// ── Fix: use the same type everywhere ────────────────────────────────
int pkt_count;
if (!uvm_config_db#(int)::get(this, "", "pkt_count", pkt_count))
`uvm_fatal("CFG", "pkt_count not found")
`uvm_info("CFG", $sformatf("pkt_count = %0d", pkt_count), UVM_LOW)

Example 3 — Verification: Scope Isolation for Two Agents

A common scenario in mixed-protocol environments — two agents of the same type, each needing a different virtual interface. Scope patterns make this precise.

SystemVerilog — two-agent scope isolation
// ── tb_top.sv — two APB interfaces, two agents ────────────────────────
apb_if apb_vif_0(clk);   // agent_0's interface
apb_if apb_vif_1(clk);   // agent_1's interface
 
initial begin
// Scope tied to each agent by name — not wildcard
uvm_config_db#(virtual apb_if)::set(
null, "uvm_test_top.env.agent_0.*", "vif", apb_vif_0);
uvm_config_db#(virtual apb_if)::set(
null, "uvm_test_top.env.agent_1.*", "vif", apb_vif_1);
run_test();
end
 
// ── apb_env.sv — creates two agents ──────────────────────────────────
agent_0 = apb_agent::type_id::create("agent_0", this);
agent_1 = apb_agent::type_id::create("agent_1", this);
 
// ── apb_driver.sv — same get() in both drivers ────────────────────────
virtual apb_if vif;
if (!uvm_config_db#(virtual apb_if)::get(this, "", "vif", vif))
`uvm_fatal("CFG", $sformatf("vif not found at %s", get_full_name()))
// driver inside agent_0 gets apb_vif_0, driver inside agent_1 gets apb_vif_1
// Same get() call — resolved differently by scope match. No index needed.

Example 4 — Tricky: Multiple set() Calls, Last Write Wins

When two set() calls share an overlapping scope for the same key and type, the last set() call wins. This trips engineers who expect the more specific path to take priority.

SystemVerilog — last set() wins, not most specific
// ── tb_top.sv ──────────────────────────────────────────────────────────
// Intention: agents globally get 100 txns, but agent_0 gets 200 specifically
uvm_config_db#(int)::set(null, "uvm_test_top.*", "num_txns", 100); // broad
uvm_config_db#(int)::set(null, "uvm_test_top.env.agent_0.*", "num_txns", 200); // specific
 
// What does agent_0's driver get?
// Answer: 200  ← The more specific path was called second, so it overwrites.
// But flip the order:
uvm_config_db#(int)::set(null, "uvm_test_top.env.agent_0.*", "num_txns", 200);
uvm_config_db#(int)::set(null, "uvm_test_top.*", "num_txns", 100); // broad last
 
// Now agent_0's driver gets 100 — the broad wildcard overwrites the specific.
// Rule: there is NO pattern-specificity priority in config_db.
// The last matching set() called BEFORE the component's build_phase wins.
 
// ── Correct pattern: base first, overrides after ──────────────────────
function void build_phase(uvm_phase phase);
// 1. Set defaults broad
uvm_config_db#(int)::set(this, "*", "num_txns", 100);
// 2. Override specific agent — called after, so wins for that scope
uvm_config_db#(int)::set(this, "env.agent_0.*", "num_txns", 200);
super.build_phase(phase);
endfunction

§10 — Bugs & Debugging

Bug 1 — Conditional set() in Base Test Creates Intermittent Failures

⚠️ Production Bug — 1% Regression Failure Rate, No Error Message

A base_test has an if (mode == "error") branch. That branch calls set("is_error_inject", 1). The else branch doesn't call set() at all. The driver's get() returns 0 silently when the else branch runs — the driver keeps its uninitialised default. Tests in the error mode regression fail with a null pointer miles away from the actual root cause.

SystemVerilog — conditional set() gotcha and fix
// ❌ BUGGY — only some branches call set() ────────────────────────────
class base_test extends uvm_test;
string mode = "normal";
function void build_phase(uvm_phase phase);
if (mode == "error")
uvm_config_db#(bit)::set(this, "*.drv", "inject_err", 1);
// else: no set() — driver's get("inject_err") returns 0
// driver proceeds with uninitialised inject_err value (0 by default in SV)
// tests pass — but the driver is not explicitly controlled
super.build_phase(phase);
endfunction
endclass
 
// ✓ FIXED — always set(), value depends on mode ────────────────────────
class base_test extends uvm_test;
string mode = "normal";
function void build_phase(uvm_phase phase);
// Always set — value is what changes based on mode
uvm_config_db#(bit)::set(this, "*.drv", "inject_err", (mode == "error") ? 1 : 0);
super.build_phase(phase);
endfunction
endclass
 
// ── In the driver — always use uvm_fatal for bit-controlled behaviour ─
bit inject_err = 0;   // safe default
void'(uvm_config_db#(bit)::get(this, "", "inject_err", inject_err));
// Suppress the return check for optional flags — use default if not set

Bug 2 — Virtual Interface Stored with Modport, Retrieved Without

⚠️ Production Bug — Modport Type Mismatch, Extremely Hard to Spot

This one looks visually identical in a code review. The engineer stores virtual apb_if.master in tb_top (because the modport signal group is connected there), then the driver tries to get virtual apb_if. Different types — silent MISS. The driver proceeds with null. Run_phase crash looks like a random simulation hang.

SystemVerilog — modport type mismatch bug and fix
// ── apb_if.sv ─────────────────────────────────────────────────────────
interface apb_if(input logic clk);
logic [31:0] paddr;
logic        pwrite;
logic [31:0] pwdata;
logic [31:0] prdata;
modport master(output paddr, pwrite, pwdata, input prdata);
modport slave(input paddr, pwrite, pwdata, output prdata);
endinterface
 
// ── tb_top.sv (BUGGY) ─────────────────────────────────────────────────
apb_if apb_vif(clk);
virtual apb_if.master master_vif = apb_vif;   // intermediate assignment
uvm_config_db#(virtual apb_if.master)::set(
null, "uvm_test_top.*", "vif", master_vif); // stored as modport type!
 
// ── apb_driver.sv (BUGGY) ─────────────────────────────────────────────
virtual apb_if vif;       // no modport — different type
if (!uvm_config_db#(virtual apb_if)::get(this, "", "vif", vif))
`uvm_fatal("CFG", "vif not found")   // ← fires, because types differ
 
// ── Fix: ALWAYS store as the full interface type (no modport) ─────────
// tb_top.sv — correct
uvm_config_db#(virtual apb_if)::set(
null, "uvm_test_top.*", "vif", apb_vif);  // full interface, no modport
 
// apb_driver.sv — correct (modport access via direction enforcement)
virtual apb_if vif;   // full interface handle
if (!uvm_config_db#(virtual apb_if)::get(this, "", "vif", vif))
`uvm_fatal("CFG", "vif not found")
// Use vif.paddr (works fine — port direction not enforced at runtime)

§11 — Ready-to-Run Code

A self-contained demo that deliberately triggers Gotcha 1 (phase ordering) and Gotcha 2 (type mismatch), then runs the corrected version. Run this first to see what the failure and fix look like before applying patterns to your own project.

config_db_gotcha_demo.sv — compile and run
// config_db_gotcha_demo.sv
// Demonstrates phase-ordering miss and type-mismatch miss.
// Compile: vlog -sv config_db_gotcha_demo.sv
// Run:     vsim -c work.tb_top_demo +UVM_TESTNAME=gotcha_test +UVM_CONFIG_DB_TRACE -do "run -all; quit"
 
`include "uvm_macros.svh"
import uvm_pkg::*;
 
// ── Simple virtual interface ──────────────────────────────────────────
interface demo_if;
logic [7:0] data;
endinterface
 
// ── Leaf component: tries to retrieve both values ────────────────────
class leaf_comp extends uvm_component;
`uvm_component_utils(leaf_comp)
virtual demo_if vif;
int timeout = -1;
 
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
 
function void build_phase(uvm_phase phase);
super.build_phase(phase);
 
// Get virtual interface
if (!uvm_config_db#(virtual demo_if)::get(this, "", "vif", vif))
`uvm_fatal("CFG", $sformatf("vif not found at %s", get_full_name()))
else
`uvm_info("CFG", "vif: HIT", UVM_LOW)
 
// Get timeout (set() uses int, get() uses bit[31:0] — type mismatch!)
bit [31:0] timeout_raw;
if (!uvm_config_db#(bit [31:0])::get(this, "", "timeout", timeout_raw))
`uvm_warning("CFG", "timeout: MISS (check +UVM_CONFIG_DB_TRACE)")
else
`uvm_info("CFG", $sformatf("timeout = %0d", timeout_raw), UVM_LOW)
endfunction
endclass
 
// ── Test: shows phase-ordering bug then fix ───────────────────────────
class gotcha_test extends uvm_test;
`uvm_component_utils(gotcha_test)
leaf_comp leaf;
 
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
 
function void build_phase(uvm_phase phase);
// set() BEFORE super.build_phase → vif arrives in time
// set timeout with int — leaf will try bit[31:0] → MISS
uvm_config_db#(int)::set(this, "*", "timeout", 1000);
 
super.build_phase(phase);
leaf = leaf_comp::type_id::create("leaf", this);
endfunction
endclass
 
// ── tb_top_demo ───────────────────────────────────────────────────────
module tb_top_demo;
demo_if dif();
initial begin
// set vif before run_test — always safe
uvm_config_db#(virtual demo_if)::set(null, "uvm_test_top.*", "vif", dif);
run_test();
end
endmodule
 
// Expected output (with +UVM_CONFIG_DB_TRACE):
//   SET uvm_config_db #(int) ... field: "timeout"
//   SET uvm_config_db #(virtual demo_if) ... field: "vif"
//   GET uvm_config_db #(virtual demo_if) ... result: HIT
//   GET uvm_config_db #(bit[31:0]) ... result: MISS   ← type mismatch visible
//   UVM_WARNING CFG: timeout: MISS (check +UVM_CONFIG_DB_TRACE)
//
// Fix: change leaf's get() to use int instead of bit[31:0].

§12 — Interview Questions

Beginner Level

Intermediate Level

Senior / Architect Level

§13 — Best Practices

RulePracticeWhy It Matters
BP-1Always call set() before super.build_phase()Guarantees the value is in the database before any child component calls get()
BP-2Always check get() return value with uvm_fatal for critical resourcesCrash at the failure site with a clear message; never proceed with null or uninitialized data
BP-3Use identical T type in set() and get() — never mix int/bit[31:0]Different types create different databases; the mismatch is silent at compile and run time
BP-4Store virtual interfaces as virtual intf_name without modport in config_dbModport types are distinct from the base interface type — always causes MISS
BP-5Use localparam string constants for field names in a shared packageTypos become compile-time errors; case errors and trailing spaces are eliminated
BP-6Prefer uvm_test_top.* or null + "*" scope for VIFs set from tb_topRobust against component renames and hierarchy refactors
BP-7Always set() unconditionally — use the condition to choose the value, not whether to call set()Conditional set() branches create intermittent failures that only appear in specific test modes
BP-8Enable +UVM_CONFIG_DB_TRACE in all debug runs, filter to MISS with grepEliminates all six gotcha categories in under 2 minutes of reading trace output

§14 — Summary

GotchaRoot CauseSymptomFix
G1 — Phase Orderingset() called after super.build_phase() — build cascade already consumeduvm_fatal: "not found" even though set() is in the codeMove all set() calls before super.build_phase()
G2 — Type Mismatchset #(T1) and get #(T2) where T1 ≠ T2 — separate databasesMISS in trace even though SET and GET both ran for same field_nameUse identical T in set() and get()
G3 — Field Name ErrorCase difference, typo, or trailing space in field_name stringSET and GET visible in trace, correct type, still MISSUse shared localparam string constants
G4 — Scope Mismatchset() scope pattern does not cover getter's full pathMISS — trace shows SET scope vs GET path with no overlapPrint get_full_name() at getter; widen wildcard in set()
G5 — Null Handleget() return value ignored — null propagates silentlyCrash in run_phase on first signal access, far from real causeAlways check return; use uvm_fatal for critical resources
G6 — Modport Typeset() stores virtual intf.port, get() expects virtual intfMISS visible in trace — T shows different modport specificationStore only full interface type in config_db; no modport