UVM config_db: set, get, virtual interfaces and wildcards
Every UVM testbench runs into the same awkward problem. The component that creates a piece of configuration, usually the test at the top, is nowhere near the component that needs it, like a driver buried three levels down inside an agent. You cannot simply pass the value through a constructor argument, because UVM builds components top-down through build_phase and you rarely hold a direct handle to the deep component. The uvm config db exists to solve exactly this: it is a shared, global storage area where one component writes a value under a named key and any other component reads it back later, without the two ever referencing each other directly.
A note on running these examples: the UVM snippets below are written for a UVM-capable simulator (industry-standard simulation tools, VCS or industry-standard simulators, or EDA Playground online) and show canonical UVM structure. They depend on the UVM base-class library, so free tools like Icarus Verilog or Verilator will not elaborate them as-is. Only the final self-contained example uses plain SystemVerilog and runs anywhere.
The two calls: set and get
Almost everything you do with uvm_config_db comes down to two static, parameterised methods. set publishes a value; get retrieves it. Because the class is parameterised by type, uvm_config_db #(int) and uvm_config_db #(virtual dut_if) are different stores, and a type mismatch between set and get simply looks like a missing entry.
| Argument | Role in set / get |
|---|---|
cntxt | Base component. Its full hierarchical name is prepended to inst_name to form the scope. |
inst_name | Relative path below cntxt. May contain * wildcards. This is the scope. |
field_name | The label the receiver asks for by name. |
value | Data stored (on set) or the variable written (on get). |
A crucial habit from day one: get returns a 1-bit status. It is 1 on a hit and 0 on a miss. Always check it, because a silent miss leaves your variable holding junk.
// Producer: the test publishes a value
class my_test extends uvm_test;
`uvm_component_utils(my_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
// store an int under scope "env.agent.driver", field "num_pkts"
uvm_config_db #(int)::set(this, "env.agent.driver", "num_pkts", 10);
endfunction
endclass
// Consumer: the driver reads it back
class my_driver extends uvm_driver #(my_seq_item);
`uvm_component_utils(my_driver)
int num_pkts;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
// inst_name "" means "my own scope" -- the driver asks about itself
if (!uvm_config_db #(int)::get(this, "", "num_pkts", num_pkts))
`uvm_fatal("CFG", "num_pkts was not set for this driver")
endfunction
endclassNotice the asymmetry that trips up beginners. The test sets with scope "env.agent.driver" relative to itself. The driver gets with an empty inst_name because, from the driver’s point of view, it is asking about its own full path. The two strings differ, yet they resolve to the same scope.
Passing a virtual interface down the hierarchy
This is the single most common use of the config_db, and the reason it is introduced so early. The real DUT interface is instantiated in a static module at the top. Class-based UVM components cannot reach into that module hierarchy, so the top module publishes a virtual interface handle into the config_db before calling run_test, and the driver and monitor fetch it during their build phase.
// Top module -- static world
module top;
import uvm_pkg::*;
`include "uvm_macros.svh"
dut_if vif(); // the physical interface instance
initial begin
// cntxt = null means the scope is absolute from the root
uvm_config_db #(virtual dut_if)::set(null, "uvm_test_top.env.agent.*",
"vif", vif);
run_test("my_test");
end
endmodule
// Inside the driver's build_phase -- class world
virtual dut_if vif;
if (!uvm_config_db #(virtual dut_if)::get(this, "", "vif", vif))
`uvm_fatal("NOVIF", "virtual interface handle not found in config_db")Two details matter here. First, cntxt is null in the top module because there is no component context; the scope string is then treated as an absolute path from the UVM root. Second, the scope ends in agent.*, so both the driver and the monitor under that agent can fetch the same handle with the field name "vif".
Passing a configuration object
Publishing one int at a time does not scale. The idiomatic approach is to bundle related settings into a uvm_object and pass that single handle down. Because you are passing a handle, every component that gets it shares the same object, which is usually what you want.
class agent_cfg extends uvm_object;
`uvm_object_utils(agent_cfg)
bit is_active = 1;
int num_pkts = 16;
function new(string name = "agent_cfg");
super.new(name);
endfunction
endclass
// In the test: build it, tune it, publish it
agent_cfg cfg = agent_cfg::type_id::create("cfg");
cfg.is_active = 0;
uvm_config_db #(agent_cfg)::set(this, "env.agent", "cfg", cfg);
// In the agent: fetch it before deciding whether to build a sequencer/driver
agent_cfg cfg;
if (!uvm_config_db #(agent_cfg)::get(this, "", "cfg", cfg))
`uvm_fatal("CFG", "agent_cfg object missing")
// now use cfg.is_active, cfg.num_pkts ...Wildcard scopes and precedence
The * wildcard in a scope string lets one set reach many components. A ? matches a single character; * matches any run of characters, including dots, so it can cross hierarchy levels. This is powerful and occasionally surprising.
| Scope passed to set | Who can get the field |
|---|---|
"env.agent.driver" | Only that one driver. |
"env.agent.*" | Every component directly under the agent. |
"*" | Every component in the testbench (a global default). |
When more than one stored entry matches a get, UVM does not pick randomly. Precedence goes to the entry published by the component highest in the hierarchy (closest to the test), so a parent can deliberately override what a child set for itself. Remaining ties resolve in favour of the most recent set. The practical lesson: set broad defaults with "*" from the test, then override specific components with precise scopes.
What is really happening: a runnable model
Strip away the UVM machinery and the config_db is a global associative array keyed by strings, plus a lookup that returns a status bit. The example below is plain SystemVerilog with no UVM library, so it runs on any simulator. It models the core mechanic: decoupled set/get and a miss that the caller must handle.
module tb;
// uvm_config_db is, at heart, a global associative array that lets a
// test write a value under a hierarchical key and lets a component read
// it back later -- the two never reference each other directly.
int store [string];
task automatic cfg_set(string key, int value);
store[key] = value;
$display("[set] %-32s = %0d", key, value);
endtask
// cfg_get mimics uvm_config_db#(T)::get -- it returns 1 and the value on
// a hit and 0 on a miss, so the caller reacts instead of using garbage.
function automatic bit cfg_get(string key, output int value);
if (store.exists(key)) begin
value = store[key];
return 1;
end
return 0;
endfunction
int v;
initial begin
cfg_set("env.agent.driver.num_pkts", 10);
cfg_set("env.agent.is_active", 1);
if (cfg_get("env.agent.driver.num_pkts", v))
$display("[get] driver reads num_pkts = %0d", v);
if (cfg_get("env.agent.is_active", v))
$display("[get] agent reads is_active = %0d", v);
// A miss must be handled -- this is why get returns a status bit.
if (!cfg_get("env.agent.driver.timeout", v))
$display("[get] timeout not set -> driver keeps its own default");
$finish;
end
endmodule[set] env.agent.driver.num_pkts = 10 [set] env.agent.is_active = 1 [get] driver reads num_pkts = 10 [get] agent reads is_active = 1 [get] timeout not set -> driver keeps its own default
Common mistakes to avoid
- Ignoring the return value of
get. A miss then flows silently into your logic as a stale or default value. - Type mismatch.
setwith#(int)andgetwith#(bit)are different stores, so the get just misses. - Scope string errors.
setuses a path relative tocntxt; a component gets with""for its own scope. Confusing the two is the usual cause of a missing-config fatal. - Calling
getfor a value that is set later in time. Config for build-time decisions must be set before the target component’sbuild_phaseruns. - Over-using
"*". A broad wildcard can leak a value into components you never intended; prefer the narrowest scope that works.
Quick check
Why can’t you just pass configuration through a constructor instead of using the config_db?
UVM components are built top-down during the build phase and a parent usually has no direct handle to the deep component that needs the value. The config_db decouples the producer from the consumer so neither needs a reference to the other.
A driver reads its own config. What should inst_name be in its get call?
An empty string "". That means “my own scope”, because the driver is asking about its own full hierarchical path. The matching set uses a scope relative to the component that published the value.
Why must the top module pass cntxt as null when setting the virtual interface?
The top is a static module, not a UVM component, so there is no component context. Passing null makes the scope string an absolute path from the UVM root, for example "uvm_test_top.env.agent.*".
Two entries match the same get. Which one wins?
The entry set by the component highest in the hierarchy (closest to the test) takes precedence, letting a parent override a child. If that still ties, the most recent set wins.
Share your question in comments or talk to our mentor team for batch guidance.
Compile and simulate this in the browser — no install. Edit the code and re-run instantly.
Learn Verilog with mentors, labs and placement support
This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.
Explore VLSI Courses