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.
// ── 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.
| Caller | cntxt | inst_name | Effective Scope / Search Path |
|---|---|---|---|
tb_top initial block | null | "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
| Parameter | Typical Mistake | Gotcha | Prevention |
|---|---|---|---|
| T (type) | #(int) set, #(bit[31:0]) get | G2 — Type Mismatch | Use typedef; keep same T in both set() and get() |
| cntxt | Wrong component used as anchor — scope prefix incorrect | G4 — Scope Mismatch | null from tb_top; this from components |
| inst_name (set) | Exact name used instead of wildcard — breaks on rename | G4 — Scope Mismatch | Use "*" for broad reach; verify with get_full_name() |
| field_name | Case difference, typo, trailing space between set/get | G3 — Field Name Error | Shared localparam string in a package |
| value (get output) | Return value ignored — null/garbage silently propagates | G5 — Null Handle | Always: if (!get(...)) uvm_fatal(...)` |
| Call timing | set() placed after super.build_phase() — cascade already ran | G1 — Phase Ordering | All 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().
// ❌ 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
endclassGotcha 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.
// ── 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() Type | get() Type | Match? | Fix |
|---|---|---|---|
int | int | YES | — |
int | bit [31:0] | NO | Use int in both |
virtual apb_if | virtual apb_if | YES | — |
virtual apb_if | virtual apb_if.master | NO | Remove modport from get() |
string | uvm_object | NO | Use same type or subtype |
apb_agent_config | apb_agent_config | YES | — |
apb_agent_config | uvm_object | NO | Type 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.
// ── 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.
// ── 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.
// ❌ 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.
## 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:
| Check | Look For | Failure Sign |
|---|---|---|
| 1. Was SET ever called? | Search for SET ... field: "your_key" above the MISS | No SET line → set() was never called or field_name differs |
| 2. Do types match? | Compare #(T) in SET vs GET lines | Different T → type mismatch (G2) |
| 3. Do field names match? | Compare field: "..." exactly | Any difference → field name error (G3) |
| 4. Does scope cover getter path? | Compare SET context pattern vs GET by: path | No pattern match → scope mismatch (G4) |
Debug Checklist — Use When get() Returns 0
- Enable
+UVM_CONFIG_DB_TRACEand 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 stringconstant for field names — eliminates typos at compile time.
// ── 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.
// ── 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
endclassPattern 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.
// ── 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
endclassPattern 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.
// ── 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
endfunctionPattern 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.
// ❌ 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);
endfunctionGotcha Location Reference — Where Each Bug Appears
| Component | What config_db Carries | Most Common Gotcha | Quick Fix |
|---|---|---|---|
| uvm_driver | Virtual interface (VIF) | G5 — null handle (get() return ignored) | if (!get) uvm_fatal` |
| uvm_monitor | Virtual interface (VIF) | G4 — scope mismatch (path changed after rename) | Print get_full_name(); widen wildcard |
| uvm_agent | Config object | G1 — phase ordering (set() after cascade) | Move set() before super.build_phase() |
| uvm_scoreboard | Integer knobs, bit flags | G2 — type mismatch (int vs bit[31:0]) | Shared localparam type + key constants |
| uvm_sequence | Runtime knobs in body() | G6 — phase race (set in run_phase concurrently) | Set knobs in build_phase instead |
| uvm_env | Sub-agent config objects | G3 — 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().
// ── 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
endmoduleExample 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.
// ── 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.
// ── 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.
// ── 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.
// ❌ 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 setBug 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.
// ── 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
// 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
| Rule | Practice | Why It Matters |
|---|---|---|
| BP-1 | Always call set() before super.build_phase() | Guarantees the value is in the database before any child component calls get() |
| BP-2 | Always check get() return value with uvm_fatal for critical resources | Crash at the failure site with a clear message; never proceed with null or uninitialized data |
| BP-3 | Use 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-4 | Store virtual interfaces as virtual intf_name without modport in config_db | Modport types are distinct from the base interface type — always causes MISS |
| BP-5 | Use localparam string constants for field names in a shared package | Typos become compile-time errors; case errors and trailing spaces are eliminated |
| BP-6 | Prefer uvm_test_top.* or null + "*" scope for VIFs set from tb_top | Robust against component renames and hierarchy refactors |
| BP-7 | Always 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-8 | Enable +UVM_CONFIG_DB_TRACE in all debug runs, filter to MISS with grep | Eliminates all six gotcha categories in under 2 minutes of reading trace output |
§14 — Summary
| Gotcha | Root Cause | Symptom | Fix |
|---|---|---|---|
| G1 — Phase Ordering | set() called after super.build_phase() — build cascade already consumed | uvm_fatal: "not found" even though set() is in the code | Move all set() calls before super.build_phase() |
| G2 — Type Mismatch | set #(T1) and get #(T2) where T1 ≠ T2 — separate databases | MISS in trace even though SET and GET both ran for same field_name | Use identical T in set() and get() |
| G3 — Field Name Error | Case difference, typo, or trailing space in field_name string | SET and GET visible in trace, correct type, still MISS | Use shared localparam string constants |
| G4 — Scope Mismatch | set() scope pattern does not cover getter's full path | MISS — trace shows SET scope vs GET path with no overlap | Print get_full_name() at getter; widen wildcard in set() |
| G5 — Null Handle | get() return value ignored — null propagates silently | Crash in run_phase on first signal access, far from real cause | Always check return; use uvm_fatal for critical resources |
| G6 — Modport Type | set() stores virtual intf.port, get() expects virtual intf | MISS visible in trace — T shows different modport specification | Store only full interface type in config_db; no modport |