UVM
uvm_config_db Architecture
Architecture, get/set protocol, scoping rules, virtual interface pattern, configuration objects.
UVM Fundamentals · Module 8
The Problem config_db Solves
A driver needs a virtual interface handle to drive the DUT. The interface is instantiated in tb_top. The driver lives at uvm_test_top.env.apb_agent.drv — four levels deep inside the hierarchy. How does the virtual interface get from tb_top to the driver without a direct reference that would couple them together?
Option A — Constructor Arguments
Pass the interface through new(name, parent, vif) all the way down. Every intermediate class must carry the argument, know about the interface type, and forward it. Adding a second interface breaks everything.
Option B — Global Variables
Declare apb_if vif as a global or package-level variable. Works for one interface, fails for multiple instances. Not test-specific. Kills any possibility of reuse.
Option C — uvm_config_db
Store the virtual interface handle in config_db from tb_top. The driver retrieves it by name in its build_phase(). Neither side knows about the other. Hierarchy changes don't break the connection.
config_db is a global hierarchical key-value store. It decouples the provider of configuration data from the consumers, using path-based scoping to ensure each component gets exactly the data intended for it.
Architecture — What config_db Actually Is
uvm_config_db #(T) is a parameterised class. The type parameter T is the type of value being stored. Every distinct type has its own separate database — a uvm_config_db #(int) entry and a uvm_config_db #(virtual apb_if) entry with the same key are completely independent.
Figure 1 — config_db architecture. A parameterised global singleton stores (type, scope, key, value) tuples. set() deposits data from any component or module. get() retrieves by matching scope against the getter's full path.
There are four pieces of information in every database entry: the type (parameterised T), the scope (derived from who set it and the inst_name pattern), the key (a string field_name), and the value itself. A get() succeeds only when all four match.
The Set Protocol — Storing Values
The complete signature is:
static function void set(
uvm_component cntxt, // who is setting (null = from module scope)
string inst_name, // hierarchical path pattern (wildcards OK)
string field_name, // string key — must match get() exactly
T value // the data to store (type T = parameterised)
);Understanding Each Argument
| Argument | Type | Meaning and Usage |
|---|---|---|
| cntxt | uvm_component | The component doing the setting. Its get_full_name() path becomes the root of the scope. Pass null when calling from a module (tb_top) since modules are not uvm_components. |
| inst_name | string | A path pattern appended to cntxt's path. Determines which components can retrieve the value. Supports * wildcards. "uvm_test_top.*" (from null cntxt) makes the value visible to all components under the test root. |
| field_name | string | A string identifier — the lookup key. Must match the field_name in the corresponding get() call exactly, character for character. Case-sensitive. |
| value | T (parameterised) | The data to store. Type T is part of the key — a config_db #(int) set and a config_db #(string) get with the same field_name will NOT match. |
// ── From tb_top (module, NOT a component — use null cntxt) ───────────
initial begin
// Scope: "" + "uvm_test_top.*" = "uvm_test_top.*"
// Visible to: every component under uvm_test_top
uvm_config_db#(virtual apb_if)::set(
null, // cntxt: null because tb_top is a module
"uvm_test_top.*", // inst_name: wildcard — all components under root
"apb_vif", // field_name: the lookup key
apb_vif // value: the interface instance
);
run_test();
end
// ── From base_test (component — use 'this') ───────────────────────────
function void build_phase(uvm_phase phase);
super.build_phase(phase);
// Scope: "uvm_test_top" + "*.apb_agent" = "uvm_test_top.*.apb_agent"
// Visible to: any apb_agent component anywhere under the test
uvm_config_db#(int)::set(
this, // cntxt: 'this' = uvm_test_top (the test component)
"*.apb_agent", // inst_name: any apb_agent under this test
"num_txns", // field_name
200 // value
);
endfunction
// ── From my_env (narrower scope) ──────────────────────────────────────
function void build_phase(uvm_phase phase);
super.build_phase(phase);
// Scope: "uvm_test_top.env" + "apb_agent_0.*"
// Visible to: ONLY components under apb_agent_0 in THIS env
uvm_config_db#(bit)::set(
this, "apb_agent_0.*", "is_active", 1'b1
);
endfunctionThe Get Protocol — Retrieving Values
The complete get() signature mirrors set() — same four arguments, with value as an output:
static function bit get(
uvm_component cntxt, // the component doing the getting ('this')
string inst_name, // usually "" — appended to cntxt's path
string field_name, // must exactly match the set() field_name
ref T value // output — populated on success
); // returns 1 if found, 0 if not found — ALWAYS check the return value
// ── Standard get() pattern with mandatory error handling ─────────────
class apb_driver extends uvm_driver #(apb_seq_item);
`uvm_component_utils(apb_driver)
virtual apb_if vif;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
// cntxt = this (driver at "uvm_test_top.env.apb_agent.drv")
// inst_name = "" (no additional path — look up for 'this' component)
// field_name must match what was set() exactly
if (!uvm_config_db#(virtual apb_if)::get(this, "", "apb_vif", vif))
`uvm_fatal("CFG",
"apb_vif not found in config_db — check tb_top set() call")
// If get() returned 0, uvm_fatal stops simulation — no null pointer crash later
endfunction
endclass
// ── Getting a simple integer ──────────────────────────────────────────
int num_txns = 100; // default value
void'(uvm_config_db#(int)::get(this, "", "num_txns", num_txns));
// void'() suppresses "return value ignored" warning when absence is acceptableScoping Rules — How the Lookup Algorithm Works
When a component calls get(this, "", "apb_vif", vif), the config_db constructs a lookup key from:
// ── SET side: scope key construction ─────────────────────────────────
// set(cntxt, inst_name, field_name, value)
// Scope key = cntxt.get_full_name() + "." + inst_name
// = "" + "uvm_test_top.*" (null cntxt has empty path)
// = "uvm_test_top.*"
uvm_config_db#(virtual apb_if)::set(null, "uvm_test_top.*", "apb_vif", vif_h);
// ── GET side: lookup key construction ────────────────────────────────
// get(cntxt, inst_name, field_name, value)
// Lookup key = cntxt.get_full_name() + "." + inst_name (if inst_name not empty)
// = "uvm_test_top.env.apb_agent.drv" + "." + ""
// = "uvm_test_top.env.apb_agent.drv"
uvm_config_db#(virtual apb_if)::get(this, "", "apb_vif", vif);
// ── Matching: does "uvm_test_top.*" cover "uvm_test_top.env.apb_agent.drv"?
// Pattern: "uvm_test_top.*"
// Key: "uvm_test_top.env.apb_agent.drv"
// The wildcard * matches ".env.apb_agent.drv" → MATCH → get() returns 1 ✓
// ── Narrower scope example ────────────────────────────────────────────
// If set was called as:
uvm_config_db#(virtual apb_if)::set(null, "uvm_test_top.env.apb_agent_0.*", "apb_vif", vif0);
// Scope: "uvm_test_top.env.apb_agent_0.*"
// Covers: any component under apb_agent_0 only
// drv under apb_agent_1 → "uvm_test_top.env.apb_agent_1.drv" → NO MATCHFigure 2 — Scoping zones. Zone A covers all components (wildcard to root). Zone B covers only apb_agent_0 children (narrow scope). Zone C shows a missed get() — agent_1's driver is outside Zone B's scope.
Scope Resolution Priority
Two competing set() calls are resolved by a precedence number, not by pattern specificity and not by ordering. Getting this backwards is the single most common architectural misunderstanding of config_db, because it inverts the rule that makes top-down configuration work at all.
During build_phase, precedence is derived from the hierarchy depth of the setting context, computed as uvm_resource_base::default_precedence - cntxt.get_depth(). A shallower context yields a larger number, so:
A
set()from higher in the hierarchy beats aset()from lower in the hierarchy — regardless of which ran first, and regardless of which pattern is more specific.
That is the entire basis of layered configuration. The environment installs a default; the test overrides it; the test wins because it is above the environment, not because it ran later.
| Situation | Which entry wins | Why |
|---|---|---|
Test set() vs env set(), same key, both in build_phase | the test | shallower context ⇒ higher precedence |
Two set() calls from the same context in build_phase | the later one | equal precedence ⇒ last write wins |
Any set() calls made after build time | the later one | all use default_precedence ⇒ last-wins semantics |
| A broad wildcard vs a narrow path, set from the same context | the later one | specificity is not part of the resolution |
Because the depth-based rule applies only during build time, a configuration written in run_phase behaves differently from the same configuration written in build_phase: after build, every entry carries the default precedence and the last writer wins outright. Configuration that must be layered belongs in build_phase.
The Virtual Interface Pattern — The Most Important Use Case
Passing virtual interface handles from the top module to drivers and monitors is the single most universal application of config_db. Every UVM testbench with a real DUT connection uses this pattern.
// ────────────────────────────────────────────────────────────────────
// tb_top.sv — the module that owns the interface
// ────────────────────────────────────────────────────────────────────
module tb_top;
`include "uvm_macros.svh"
import uvm_pkg::*;
import test_pkg::*;
// Interface instances
apb_if apb_vif(); // creates the interface
my_dut dut (.apb(apb_vif)); // DUT connection
initial begin
// Store handle BEFORE run_test() — run_test() triggers build_phase
uvm_config_db#(virtual apb_if)::set(
null, // null because tb_top is a module, not uvm_component
"uvm_test_top.*", // visible to ALL components under the test root
"apb_vif", // the key — exactly this string must be used in get()
apb_vif // the interface instance — modport not needed here
);
run_test(); // UVM takes over — build_phase cascade begins
end
endmodule
// ────────────────────────────────────────────────────────────────────
// apb_driver.sv — the component that needs the interface
// ────────────────────────────────────────────────────────────────────
class apb_driver extends uvm_driver #(apb_seq_item);
`uvm_component_utils(apb_driver)
virtual apb_if vif; // will hold the interface handle after build_phase
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
// Retrieve the interface — fatal if not found
if (!uvm_config_db#(virtual apb_if)::get(
this, // 'this' = drv at "uvm_test_top.env.apb_agent.drv"
"", // no additional path — lookup for 'this' component
"apb_vif", // must match the set() key exactly
vif)) // populated on success
`uvm_fatal("CFG", "apb_vif not found — check tb_top set() call")
endfunction
task run_phase(uvm_phase phase);
// vif is now valid — safe to drive signals
@(posedge vif.clk);
vif.paddr = req.addr;
vif.pwdata = req.data;
endtask
endclassConfiguration Objects — Grouping Related Settings
Passing individual integers and bits through config_db becomes unmanageable when a component has ten configuration parameters. The clean solution: create a configuration class and pass the whole object as a single config_db entry.
// ── Step 1: Define the configuration class ────────────────────────────
class apb_agent_config extends uvm_object;
`uvm_object_utils(apb_agent_config) // register with factory (good practice)
// Interface handle — part of the config object
virtual apb_if vif;
// Behavioural parameters
int num_txns = 100;
bit is_active = 1; // 1 = active (driver+monitor), 0 = passive (monitor only)
int max_wait_cycles = 20;
function new(string name = "apb_agent_config");
super.new(name);
endfunction
endclass
// ── Step 2: Create and set from tb_top or test ────────────────────────
initial begin
apb_agent_config cfg = new("cfg"); // or use factory create
cfg.vif = apb_vif; // attach the virtual interface
cfg.num_txns = 500;
cfg.is_active = 1;
cfg.max_wait_cycles = 10;
uvm_config_db#(apb_agent_config)::set(
null, "uvm_test_top.env.apb_agent", "cfg", cfg
);
run_test();
end
// ── Step 3: Retrieve in the agent's build_phase ───────────────────────
class apb_agent extends uvm_agent;
`uvm_component_utils(apb_agent)
apb_agent_config cfg;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(apb_agent_config)::get(this, "", "cfg", cfg))
`uvm_fatal("CFG", "apb_agent_config not found")
// cfg is now populated — pass it down to driver and monitor
drv = apb_driver::type_id::create("drv", this);
uvm_config_db#(apb_agent_config)::set(this, "drv", "cfg", cfg);
endfunction
apb_driver drv;
endclassQuick Reference
| Pattern | set() call | get() call |
|---|---|---|
| Virtual interface (from module) | set(null, "uvm_test_top.*", "vif", vif_h) | get(this, "", "vif", vif) |
| Integer from test (global) | set(this, "*", "num_txns", 200) | get(this, "", "num_txns", n) |
| Config object to specific agent | set(null, "*.env.apb_agent", "cfg", cfg_h) | get(this, "", "cfg", cfg) |
| Set from env to its direct children | set(this, "drv", "cfg", cfg_h) | get(this, "", "cfg", cfg) (in drv) |
| Check with default (no fatal) | — | void'(get(this, "", "flag", flag_var)) |
// ── The four arguments ────────────────────────────────────────────────
// set(cntxt, inst_name, field_name, value)
// get(cntxt, inst_name, field_name, value) ← returns bit (1=found)
//
// cntxt : who is setting/getting. null from module. 'this' from component.
// inst_name : path suffix appended to cntxt's full_name. Wildcards OK.
// field_name: string key. Case-sensitive. Must match exactly.
// value : the data (type T = the parameterised class argument)
// ── Scope key formula ─────────────────────────────────────────────────
// set scope = cntxt.get_full_name() + "." + inst_name
// get key = cntxt.get_full_name() + "." + inst_name
// Match if: the set scope pattern covers the get key path
// ── Essential rules ───────────────────────────────────────────────────
// 1. set() before run_test() for interfaces (needed in build_phase)
// 2. Always check get() return value for critical data
// 3. Type T in set() and get() must be IDENTICAL
// 4. field_name must match EXACTLY — case, spelling, no spaces
// 5. For debug: add +UVM_CONFIG_DB_TRACE to simulation command
// ── Debug trace output format ─────────────────────────────────────────
// vsim +UVM_CONFIG_DB_TRACE work.tb_top
//
// UVM_INFO: SET uvm_config_db #(virtual apb_if) ...
// by: null context: uvm_test_top.* field: apb_vif
// UVM_INFO: GET uvm_config_db #(virtual apb_if) ...
// by: uvm_test_top.env.apb_agent.drv field: apb_vif result: HITCode Examples — config_db From Simple to Production
config_db is one of those mechanisms that looks simple in isolation but hides surprising behavior when components are deep in the hierarchy or when multiple tests share the same testbench. These four examples build from the minimal working pattern to the production configuration object pattern used on large SoC projects.
Example 1 — Beginner: Passing an Integer to a Component
`include "uvm_macros.svh"
import uvm_pkg::*;
// ── Consumer: component that reads config ──────────────────────────
class my_monitor extends uvm_monitor;
`uvm_component_utils(my_monitor)
int num_checks = 10; // default value
function new(string n, uvm_component p); super.new(n,p); endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
// Retrieve config — override default if found
if(!uvm_config_db#(int)::get(this, "", "num_checks", num_checks))
`uvm_info("CFG", "num_checks not found, using default=10", UVM_MEDIUM)
else
`uvm_info("CFG", $sformatf("num_checks=%0d", num_checks), UVM_MEDIUM)
endfunction
endclass
// ── Provider: test that sets config ────────────────────────────────
class basic_cfg_test extends uvm_test;
`uvm_component_utils(basic_cfg_test)
my_monitor mon;
function new(string n, uvm_component p); super.new(n,p); endfunction
function void build_phase(uvm_phase phase);
// Set BEFORE creating the component so it sees the value in its build_phase
uvm_config_db#(int)::set(this, "mon", "num_checks", 50);
super.build_phase(phase);
mon = my_monitor::type_id::create("mon", this);
endfunction
task run_phase(uvm_phase p);
p.raise_objection(this); p.drop_objection(this);
endtask
endclass
module basic_cfg_top;
initial run_test("basic_cfg_test");
endmodule
// Expected Output:
// UVM_INFO [CFG] num_checks=50 ← config_db value overrides defaultExample 2 — Intermediate: Virtual Interface via config_db
`include "uvm_macros.svh"
import uvm_pkg::*;
// Interface definition (normally in a separate .sv file) ──────────
interface apb_if (input logic clk);
logic[31:0] paddr;
logic[31:0] pwdata;
logic pwrite;
logic psel;
logic penable;
logic pready;
endinterface
// Driver reads vif from config_db ──────────────────────────────────
class apb_driver extends uvm_driver#(uvm_sequence_item);
`uvm_component_utils(apb_driver)
virtual apb_if vif;
function new(string n, uvm_component p); super.new(n,p); endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
// Fatal if vif not set — driver cannot function without it
if(!uvm_config_db#(virtual apb_if)::get(this, "", "vif", vif))
`uvm_fatal("CFG", "Virtual interface 'vif' not set in config_db")
else
`uvm_info("CFG", "vif connected successfully", UVM_MEDIUM)
endfunction
endclass
// Top module wires interface and sets it in config_db ──────────────
module apb_tb_top;
logic clk;
initial clk = 0;
always #5 clk = ~clk;
apb_if dut_if(.clk(clk)); // interface instance
initial begin
// Set BEFORE run_test — driver needs it in build_phase
uvm_config_db#(virtual apb_if)::set(null, "uvm_test_top.*", "vif", dut_if);
run_test("apb_test");
end
endmodule
// Key points:
// 1. null context + wildcard path = accessible from any component
// 2. set() called BEFORE run_test() so it's available in build_phase
// 3. uvm_fatal ensures the driver can't silently run with a null interfaceExample 3 — Verification: Configuration Object Pattern
`include "uvm_macros.svh"
import uvm_pkg::*;
// Config object: one object, multiple parameters ───────────────────
class apb_agent_config extends uvm_object;
`uvm_object_utils_begin(apb_agent_config)
`uvm_field_int(is_active, UVM_ALL_ON)
`uvm_field_int(num_txns, UVM_ALL_ON)
`uvm_field_int(bus_width, UVM_ALL_ON)
`uvm_object_utils_end
bit is_active = 1; // 1=active agent, 0=passive monitor only
int num_txns = 100; // number of transactions to generate
int bus_width = 32; // APB bus width in bits
function new(string n="apb_agent_config"); super.new(n); endfunction
endclass
class apb_agent extends uvm_agent;
`uvm_component_utils(apb_agent)
apb_agent_config cfg;
function new(string n, uvm_component p); super.new(n,p); endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if(!uvm_config_db#(apb_agent_config)::get(this, "", "cfg", cfg))
`uvm_fatal("CFG", "apb_agent_config not found")
`uvm_info("CFG", $sformatf(
"active=%0b txns=%0d width=%0d",
cfg.is_active, cfg.num_txns, cfg.bus_width), UVM_MEDIUM)
endfunction
endclass
class cfg_obj_test extends uvm_test;
`uvm_component_utils(cfg_obj_test)
apb_agent agent;
apb_agent_config cfg;
function new(string n, uvm_component p); super.new(n,p); endfunction
function void build_phase(uvm_phase phase);
cfg = apb_agent_config::type_id::create("cfg");
cfg.is_active = 1;
cfg.num_txns = 250;
cfg.bus_width = 32;
uvm_config_db#(apb_agent_config)::set(this, "agent", "cfg", cfg);
super.build_phase(phase);
agent = apb_agent::type_id::create("agent", this);
endfunction
task run_phase(uvm_phase p);
p.raise_objection(this); p.drop_objection(this);
endtask
endclass
module cfg_obj_top;
initial run_test("cfg_obj_test");
endmodule
// Expected Output:
// [CFG] active=1 txns=250 width=32 ← all three fields from config objectExample 4 — Tricky: Scoping Wildcard vs Exact Path
`include "uvm_macros.svh"
import uvm_pkg::*;
class scope_consumer extends uvm_component;
`uvm_component_utils(scope_consumer)
int timeout;
function new(string n, uvm_component p); super.new(n,p); endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
void'(uvm_config_db#(int)::get(this, "", "timeout", timeout));
$display(" [%s] timeout=%0d", get_full_name(), timeout);
endfunction
endclass
class scope_test extends uvm_test;
`uvm_component_utils(scope_test)
scope_consumer c1, c2;
function new(string n, uvm_component p); super.new(n,p); endfunction
function void build_phase(uvm_phase phase);
// Wildcard: applies to all children
uvm_config_db#(int)::set(this, "*", "timeout", 100);
// Exact path: overrides wildcard for c2 specifically
uvm_config_db#(int)::set(this, "c2", "timeout", 500);
super.build_phase(phase);
c1 = scope_consumer::type_id::create("c1", this);
c2 = scope_consumer::type_id::create("c2", this);
endfunction
task run_phase(uvm_phase p);
p.raise_objection(this); p.drop_objection(this);
endtask
endclass
module scope_top;
initial run_test("scope_test");
endmodule
// Expected Output:
// [uvm_test_top.c1] timeout=100 ← wildcard applied
// [uvm_test_top.c2] timeout=500 ← exact path overrides wildcard
//
// Key insight: More specific path wins. "c2" is more specific than "*".Common Bugs — config_db Failures That Look Like Something Else
config_db bugs are uniquely frustrating because they manifest as null pointer crashes, wrong values deep in run_phase, or components that silently use default values without ever warning you. The root cause is always set/get mismatch — but finding which mismatch requires knowing exactly what to look for.
⚠️ Bug 1 — Type Mismatch: set() and get() Use Different Types
Symptom: get() returns 0 (failure) even though set() was definitely called with the correct field name. The component uses its default value. No error message.
Root cause: The T parameter in uvm_config_db#(T) must match between set() and get(). If set() uses #(int) and get() uses #(integer) or #(bit[31:0]), they resolve to different database entries and the get() finds nothing.
// ❌ WRONG: type mismatch between set and get
// In test:
uvm_config_db#(int)::set(this, "mon", "timeout", 100); // stores as 'int'
// In monitor:
bit[31:0] timeout;
uvm_config_db#(bit[31:0])::get(this, "", "timeout", timeout);
// get() returns 0 — different type = different entry in database
// timeout remains at its initialization value, no warning
// ✓ CORRECT: matching types
uvm_config_db#(int)::set(this, "mon", "timeout", 100);
int timeout; // ← same type as set()
if(!uvm_config_db#(int)::get(this, "", "timeout", timeout))
`uvm_error("CFG", "timeout not found in config_db")
// Debug: add +UVM_CONFIG_DB_TRACE to simulation command
// It shows every set/get call with types and paths — type mismatches are obvious⚠️ Bug 2 — Wrong Context or Path: get() Scope Doesn't Match set() Scope
Symptom: get() returns 0 even though types match. The component receives a default value. Happens most often when a component is deeply nested or when the inst_path is wrong.
Root cause: UVM resolves get() by building a scope from the component's hierarchy: get_full_name() + "." + inst_name. If the set() scope doesn't match this computed string, get() finds nothing.
// ❌ WRONG: path doesn't reach the deep component
// Testbench topology: test → env → agent → driver
// Component full path: "uvm_test_top.env.agent.drv"
// In test.build_phase:
uvm_config_db#(int)::set(this, "drv", "speed", 100);
// set scope = "uvm_test_top.drv" ← WRONG, driver is at "...env.agent.drv"
// ✓ FIX 1: Use wildcard to reach any level
uvm_config_db#(int)::set(this, "env.agent.drv", "speed", 100);
// ✓ FIX 2: Global wildcard — reaches all components named "drv"
uvm_config_db#(int)::set(null, "uvm_test_top.*", "speed", 100);
// Debug: print the consumer's path INSIDE get():
// `uvm_info("CFG", $sformatf("my path: %s", get_full_name()), UVM_DEBUG)
// Use +UVM_CONFIG_DB_TRACE to see scope resolution in real time🔍 Debug Insight — The +UVM_CONFIG_DB_TRACE Lifesaver
Adding +UVM_CONFIG_DB_TRACE to your simulation command prints every set() and get() call with full type and scope information. When get() fails, you immediately see the scope mismatch. This single plusarg resolves 90% of config_db bugs without any code changes:
vsim ... +UVM_CONFIG_DB_TRACE work.tb_top # Example trace output: # UVM_INFO: set(uvm_test_top, "mon", "timeout", int, 100) # UVM_INFO: get(uvm_test_top.mon, "", "timeout", int) → MATCH: 100 # UVM_INFO: get(uvm_test_top.mon, "", "speed", int) → MISS (not found)
Ready-to-Run: Complete config_db Demo
This single-file testbench demonstrates all major config_db patterns: integer, string, wildcard scope, exact path override, and the configuration object pattern. Save as config_db_demo.sv and run it.
// ================================================================
// config_db_demo.sv — config_db Architecture Demonstration
// Covers: integer, string, scope, wildcard, config object
//
// Compile (Questa):
// vlog -sv -L uvm_1_2 config_db_demo.sv
// vsim -c -L uvm_1_2 config_db_demo_top -do "run -all; quit"
//
// Compile (VCS):
// vcs -sverilog -ntb_opts uvm-1.2 config_db_demo.sv && ./simv
//
// Compile (Xcelium):
// xrun -sv -uvm config_db_demo.sv
//
// Debug mode (any simulator):
// Add +UVM_CONFIG_DB_TRACE to see all set/get operations
// ================================================================
`include "uvm_macros.svh"
import uvm_pkg::*;
// ── Config object ──────────────────────────────────────────────────
class agent_cfg extends uvm_object;
`uvm_object_utils(agent_cfg)
int max_txns = 100;
string protocol = "APB";
bit is_active = 1;
function new(string n="agent_cfg"); super.new(n); endfunction
endclass
// ── Generic consumer component ─────────────────────────────────────
class demo_comp extends uvm_component;
`uvm_component_utils(demo_comp)
int num_pkts = 10; // integer config
string mode = "SLOW"; // string config
agent_cfg cfg; // object config
function new(string n, uvm_component p); super.new(n,p); endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
// Get all three config items
void'(uvm_config_db#(int)::get(this, "", "num_pkts", num_pkts));
void'(uvm_config_db#(string)::get(this, "", "mode", mode));
void'(uvm_config_db#(agent_cfg)::get(this, "", "cfg", cfg));
$display(" [%s] num_pkts=%0d mode=%s", get_name(), num_pkts, mode);
if(cfg != null)
$display(" [%s] cfg: max_txns=%0d protocol=%s active=%0b",
get_name(), cfg.max_txns, cfg.protocol, cfg.is_active);
else
$display(" [%s] cfg: not set", get_name());
endfunction
endclass
// ── Test: sets different configs for two components ─────────────────
class full_demo_test extends uvm_test;
`uvm_component_utils(full_demo_test)
demo_comp c_alpha, c_beta;
agent_cfg cfg_a;
function new(string n, uvm_component p); super.new(n,p); endfunction
function void build_phase(uvm_phase phase);
// ── Integer: wildcard applies to all children ──────────
uvm_config_db#(int)::set(this, "*", "num_pkts", 50);
// ── String: per-component exact path ───────────────────
uvm_config_db#(string)::set(this, "c_alpha", "mode", "FAST");
uvm_config_db#(string)::set(this, "c_beta", "mode", "SAFE");
// ── Config object: only c_alpha gets one ───────────────
cfg_a = agent_cfg::type_id::create("cfg_a");
cfg_a.max_txns = 200;
cfg_a.protocol = "AXI";
cfg_a.is_active = 1;
uvm_config_db#(agent_cfg)::set(this, "c_alpha", "cfg", cfg_a);
super.build_phase(phase);
c_alpha = demo_comp::type_id::create("c_alpha", this);
c_beta = demo_comp::type_id::create("c_beta", this);
endfunction
task run_phase(uvm_phase phase);
phase.raise_objection(this);
phase.drop_objection(this);
endtask
endclass
module config_db_demo_top;
initial run_test("full_demo_test");
endmodule
// ================================================================
// Expected Output:
//
// [c_alpha] num_pkts=50 mode=FAST
// [c_alpha] cfg: max_txns=200 protocol=AXI active=1
//
// [c_beta] num_pkts=50 mode=SAFE
// [c_beta] cfg: not set
//
// Analysis:
// num_pkts=50: wildcard "*" set applies to both
// mode=FAST/SAFE: exact paths set different values per component
// cfg: only c_alpha received it; c_beta gets null → "not set"
// ================================================================Runnable Proof — Four get() Calls, One of Them Deliberately Broken
Reading about type mismatch and scope mismatch does not build the reflex. Running a file in which three lookups succeed and one fails — for a reason you can point at — does.
// ================================================================
// config_db_proof.sv — three good lookups, one intentional failure.
// Questa : vlog -sv -L uvm_1_2 config_db_proof.sv
// vsim -c -L uvm_1_2 config_db_proof_top -do "run -all; quit"
// VCS : vcs -sverilog -ntb_opts uvm-1.2 config_db_proof.sv && ./simv
// Xcelium: xrun -sv -uvm config_db_proof.sv
// ================================================================
`include "uvm_macros.svh"
import uvm_pkg::*;
class leaf extends uvm_component;
`uvm_component_utils(leaf)
int good_int;
int wrong_type;
int wrong_path;
string label;
function new(string n, uvm_component p); super.new(n,p); endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
// (1) Matching type, matching scope, matching field name → succeeds.
if (!uvm_config_db#(int)::get(this, "", "good_int", good_int))
`uvm_fatal("CFG", "good_int not found")
// (2) Set as int, fetched as int, but through a DIFFERENT field name.
// Returns 0. The variable keeps its previous value — here, 0 —
// which is indistinguishable from a legitimately configured 0.
if (!uvm_config_db#(int)::get(this, "", "typo_int", wrong_path))
`uvm_warning("CFG", "typo_int not found (expected: name mismatch)")
// (3) Stored as int, requested as byte. Type T is part of the KEY, so
// this is a different entry entirely — not a conversion.
if (!uvm_config_db#(byte)::get(this, "", "good_int", wrong_type))
`uvm_warning("CFG", "good_int/byte not found (expected: type mismatch)")
// (4) The guarded pattern every real lookup should use.
if (!uvm_config_db#(string)::get(this, "", "label", label))
`uvm_fatal("CFG", $sformatf(
"%s: no 'label' visible at this scope. set() must run before this build_phase.",
get_full_name()))
`uvm_info("CFG", $sformatf("good_int=%0d wrong_path=%0d wrong_type=%0d label=%s",
good_int, wrong_path, wrong_type, label), UVM_LOW)
endfunction
endclass
class env extends uvm_env;
`uvm_component_utils(env)
leaf l;
function new(string n, uvm_component p); super.new(n,p); endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
// Env installs a DEFAULT. The test's competing set() is shallower and
// therefore higher precedence, so the test's value is what leaf sees.
uvm_config_db#(string)::set(this, "l", "label", "ENV_DEFAULT");
l = leaf::type_id::create("l", this);
endfunction
endclass
class cfg_test extends uvm_test;
`uvm_component_utils(cfg_test)
env e;
function new(string n, uvm_component p); super.new(n,p); endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
uvm_config_db#(int)::set(this, "e.l", "good_int", 42);
uvm_config_db#(string)::set(this, "e.l", "label", "TEST_WINS");
e = env::type_id::create("e", this);
endfunction
task run_phase(uvm_phase p); p.raise_objection(this); p.drop_objection(this); endtask
endclass
module config_db_proof_top;
initial run_test("cfg_test");
endmodule
// Expected:
// UVM_WARNING [CFG] typo_int not found (expected: name mismatch)
// UVM_WARNING [CFG] good_int/byte not found (expected: type mismatch)
// UVM_INFO [CFG] good_int=42 wrong_path=0 wrong_type=0 label=TEST_WINS
// ↑ not ENV_DEFAULTThe label result is the one to sit with. Both the test and the environment set it, the environment's call runs later in simulation time, and the test's value still wins — because during build_phase precedence comes from hierarchy depth, not from ordering.
And note what lookups (2) and (3) have in common: both return 0, and both leave the destination variable holding 0. A missing entry does not zero anything and does not warn on its own. The zero you observe is the value the variable already had, which is why an unguarded get() is a silent-failure generator.
A test's configuration was ignored, and moving one line to a different file fixed it
CONFIG-DB-PRECEDENCE-DEPTHA test set num_transactions to 5000 for a long soak run. The sequence ran 100 — the environment's default. The set() call was demonstrably executing; a uvm_info beside it printed on every run.
Reordering the calls made no difference. Making the test's path pattern narrower and more specific made no difference either.
class soak_test extends base_test;
function void build_phase(uvm_phase phase);
super.build_phase(phase); // builds env, which sets its default
// Intended: override the env's default for this test
uvm_config_db#(int)::set(e.seqr, "", "num_transactions", 5000);
endfunction
endclass
class my_env extends uvm_env;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
uvm_config_db#(int)::set(this, "seqr", "num_transactions", 100);
seqr = my_sequencer::type_id::create("seqr", this);
endfunction
endclass+UVM_CONFIG_DB_TRACE showed both writes and the read, and the read returned 100:
CFGDB/SET uvm_test_top.env.seqr num_transactions (int) = 100
CFGDB/SET uvm_test_top.env.seqr num_transactions (int) = 5000
CFGDB/GET uvm_test_top.env.seqr num_transactions (int) -> 100That output is what made the bug confusing: the 5000 write happened second and still lost. Under a last-wins model this is impossible, which is why the team spent a day suspecting the trace itself.
Two things combined, and the second is the real lesson.
First, the ordering: the test called super.build_phase() before its own set(), so the environment — and the sequencer's own get() — had already run. Even a correct precedence would not help a value written after it was read.
Second, and more importantly, the test's set() used e.seqr as the context. That makes the setting context the sequencer itself, at depth 3. The environment's set() used this — the env, at depth 2. During build_phase precedence is default_precedence - cntxt.get_depth(), so the env's write carried higher precedence than the test's. The test had unintentionally demoted itself below the component it was trying to configure.
This is why narrowing the pattern did not help: specificity plays no part in resolution.
Set from the test's own context, before the environment is built:
class soak_test extends base_test;
function void build_phase(uvm_phase phase);
// Context = this (the test, depth 1) → higher precedence than the env.
// Written before super so it is in place when the sequencer reads it.
uvm_config_db#(int)::set(this, "env.seqr", "num_transactions", 5000);
super.build_phase(phase);
endfunction
endclassTwo rules generalise from this. Configure downward, never sideways: the cntxt argument should be the component doing the configuring, with the path expressing the target — not the target passed as context. And write before you build: any set() intended to be visible to a component's build_phase must precede the super.build_phase() call that constructs it.
The guard worth adding is the fatal-on-miss pattern from the runnable example. It would not have caught this particular bug — the get() succeeded, it simply returned the other value — which is exactly why the second guard matters too: log the configured value at end_of_elaboration_phase, where a wrong-but-present value is visible as a number rather than as behaviour three hours into a regression.
Questions Engineers Actually Ask
Because a failed get() does nothing at all — including nothing to the variable you passed it.
It returns 0 and leaves the destination untouched, holding whatever it held before: 0 for an int, null for a virtual interface handle. An unguarded lookup therefore produces a plausible-looking default rather than an error, and the failure surfaces much later as a null-handle crash in the driver or as a test that quietly runs zero transactions.
The guard is one line and it should be reflexive:
if (!uvm_config_db#(virtual apb_if)::get(this, "", "apb_vif", vif))
`uvm_fatal("CFG", "apb_vif not found — check the set() in tb_top")The field name is only one of four things that must match. The full key is the type T, the resolved scope, and the field name — and the getter's own position determines whether a scope pattern applies to it.
The one that catches people is type. uvm_config_db#(int)::set(...) and uvm_config_db#(bit[31:0])::get(...) are different entries, not a conversion, and no warning connects them. uvm_config_db#(uvm_object) versus a specific object type behaves the same way.
+UVM_CONFIG_DB_TRACE prints the type alongside every set and get, which usually makes the mismatch obvious in one run.
Where This Is Specified
- IEEE 1800.2-2020 (UVM) —
uvm_config_db#(T). Theset()andget()signatures, thecntxt/inst_name/field_nametriple, and the parameterised type as part of the entry's identity. - IEEE 1800.2-2020 — configuration precedence. Derivation of a setting's precedence from the context's hierarchical depth during build time, and the reversion to a uniform default precedence afterwards.
- IEEE 1800.2-2020 —
uvm_resource_dband the resource pool. The storage layer beneathuvm_config_db, including precedence-ordered resolution of multiple matching entries. - IEEE 1800.2-2020 —
uvm_component::get_full_nameandget_depth. The hierarchical name matched by scope patterns, and the depth value that feeds the precedence calculation. - IEEE 1800.2-2020 — command-line processing.
+UVM_CONFIG_DB_TRACEand+UVM_RESOURCE_DB_TRACEas the supported way to observe set and get activity. - IEEE 1800.2-2020 — UVM phasing. The ordering of
build_phasetop-down construction, which is what makes "set before super.build_phase" a correctness requirement rather than a style preference.
Related Reading
- config_db set/get — the call-level mechanics of the two methods, if you want the API before the architecture.
- config_db Scope Rules — path patterns and wildcards in depth.
- config_db Failures and config_db Gotchas — the diagnostic catalogue for lookups that return nothing or the wrong thing.
- Resource vs config_db — what the layer underneath does, and when to address it directly.
- Hierarchical Configuration — layered defaults across a multi-agent environment, which is precedence applied at scale.
- Type vs Instance Override — the other customisation axis. If two candidate implementations differ by behaviour rather than by a value, the factory is the right tool and configuration is not.
Interview Questions — config_db Questions From Real Interviews
Beginner Level
- Q1 What is uvm_config_db and why do we use it instead of passing values through constructors? Answer:
uvm_config_dbis UVM's global key-value database for passing configuration from tests to components without creating direct references between them. We use it instead of constructors because: (1) Components should be reusable across projects — hardcoding configuration in constructors ties them to specific tests; (2) Deep hierarchies would require passing values through many intermediate layers; (3) Tests should be able to change component behavior without modifying VIP source code. config_db creates a clean separation between test configuration and component implementation. - Q2 What are the four arguments to uvm_config_db::set() and what does each do? Answer:
set(context, inst_name, field_name, value). (1) context — the component doing the setting (usuallythis); determines the base scope; (2) inst_name — path relative to context that determines which components can retrieve this value; use"*"for wildcard; (3) field_name — the key string, must match exactly in get(); (4) value — the data to store, type must match the template parameter T. The scope for resolution is built by combining context.get_full_name() + "." + inst_name.
Intermediate Level
- Q3 get() returns 0 (failure) even though you can see set() was called. List three possible causes. Answer: (1) Type mismatch: set() used
#(int)but get() uses#(integer)or vice versa — different template parameters create different database entries. (2) Scope mismatch: the inst_name in set() doesn't reach the component calling get() — component's full path doesn't match the computed scope. (3) Timing issue: set() was called after the component's build_phase already executed — config_db resolution happens at get() call time, and if build_phase ran before set() was called, the value doesn't exist yet. Always call set() before creating the component (before super.build_phase()).
Senior / Architect Level
- Q4 Both a test and a base_test set the same config key. Which value does the component receive and why? Answer: The most recently registered entry with the most specific scope wins. UVM resolves config_db by walking the hierarchy to find the best match — "best" means the most specific path that still covers the requesting component. If both entries have the same specificity (same path pattern), the one set last wins because config_db uses a FIFO with last-write-wins semantics for same-specificity entries. In practice: the derived test's set() should be called after super.build_phase() inherits the base_test's set(), ensuring the derived test can override the base. This is the standard pattern for test inheritance with config_db.
- Q5 You're passing a virtual interface via config_db but the driver gets a null handle. You confirmed the interface is set in tb_top. What's the most common cause and fix? Answer: The most common cause is that
run_test()was called before the virtual interface was set in config_db, or the interface was set with a scope that doesn't cover the driver's full path. Fix: in tb_top, calluvm_config_db#(virtual apb_if)::set(null, "uvm_test_top.*", "vif", dut_if)BEFORErun_test(). Usingnullas context and"uvm_test_top.*"makes it globally accessible to all descendants. Usingnullwithoutrun_test()prevents the interface from being set too late.
Best Practices — config_db Patterns That Production Teams Follow
| Rule | Correct Pattern | Why It Matters |
|---|---|---|
| Set before create | Call set() before super.build_phase() and component creation | Components resolve config in their build_phase — if set() arrives after, it's too late |
| Use uvm_fatal on missing critical config | if(!get(...)) uvm_fatal("CFG","...")` | Silent null handle = crash deep in run_phase with misleading error |
| Match types exactly | set() and get() must use identical T parameter | Type mismatch = silent failure with no error message |
| Use wildcard for virtual interfaces from tb_top | set(null, "uvm_test_top.*", "vif", ...) | All descendant agents/drivers can reach the interface |
| Prefer config objects over individual fields | One set() with a config object vs many individual set() calls | Fewer set/get calls, related parameters travel together, easier to extend |
| Always run with +UVM_CONFIG_DB_TRACE in debug | Add to simulation command during development | Instantly shows scope mismatches and type errors without code changes |
| Document the config schema in the agent header | Comment: "Requires config_db #(agent_cfg) at path ".*"" | Prevents integration errors when teams share VIPs |
💡 Pro Tip — The Config Object Pattern Scales Better
Early-stage testbenches use individual set() calls for each parameter. As the project grows, this creates maintenance chaos — adding one parameter requires updating every test. The config object pattern solves this permanently:
- Define all agent parameters in one
uvm_objectsubclass - Base test creates the config object with defaults
- Derived tests only modify what they need before passing it
- Adding a new parameter? Update the config class and base test only — derived tests automatically inherit the new field with its default value
Summary — config_db Is the Configuration Bus of Your Testbench
Every time a driver needs a virtual interface, a monitor needs a timeout value, or a scoreboard needs to know the bus width — config_db is the answer. It separates test configuration from component implementation, which is what makes VIPs reusable across projects without source modification.
| Concept | What It Does | When to Use |
|---|---|---|
| set(this, "child", "key", val) | Stores value scoped to a specific child path | Most common: parent-to-child configuration |
| set(this, "*", "key", val) | Stores value accessible to all descendants | Blanket configuration for an entire subtree |
| set(null, "uvm_test_top.*", "vif", ...) | Stores from module scope, global reach | Virtual interface connection from tb_top |
| get() returns 0 | Entry not found — type or scope mismatch | Use +UVM_CONFIG_DB_TRACE to diagnose |
| Config object pattern | One set() carries all related parameters | Any agent/component with 3+ parameters |
- 1 Set before the component builds. config_db is resolved at get() call time, which happens in build_phase. If set() hasn't been called yet, get() finds nothing. Always set before super.build_phase() or before run_test().
- 2 Type mismatch is the silent killer. set() and get() must use identical template parameters. A wrong type creates a completely separate database entry and get() never finds it. No error, no warning.
- 3 +UVM_CONFIG_DB_TRACE is mandatory for debug. One simulation flag shows every set/get operation with full type and scope information. Use it always during development. Gate it behind a plusarg in regression.
Part of UVM Fundamentals·Configuration with config_db·Lesson 29 of 33
View programContinue learning
Related tutorials
- Related topic
config_db Gotchas
Six gotchas that cause silent failures: phase ordering, type mismatch, name case, scope, null handles.
- Prerequisite
What UVM Is — and What It Is Not
Methodology, library, and framework — where the three layers differ.
- Related topic
UVM Class Library Hierarchy
uvm_void → uvm_object → uvm_component — what each level adds and when to extend which class.
- Related topic
Package Structure & Compilation
Standard VIP layout, multi-agent directory structure, eliminating circular dependencies.