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.

ArgumentRole in set / get
cntxtBase component. Its full hierarchical name is prepended to inst_name to form the scope.
inst_nameRelative path below cntxt. May contain * wildcards. This is the scope.
field_nameThe label the receiver asks for by name.
valueData 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
endclass

Notice 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 setWho 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. set with #(int) and get with #(bit) are different stores, so the get just misses.
  • Scope string errors. set uses a path relative to cntxt; a component gets with "" for its own scope. Confusing the two is the usual cause of a missing-config fatal.
  • Calling get for a value that is set later in time. Config for build-time decisions must be set before the target component’s build_phase runs.
  • 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.

Have questions about this topic?
Share your question in comments or talk to our mentor team for batch guidance.
▶ Try it in the ChipXpert Lab

Compile and simulate this in the browser — no install. Edit the code and re-run instantly.

Open in Verification Lab

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
Next batch starts 18th November 2026
Start your VLSI career with ChipXpert

Live online and classroom batches in Hyderabad & Bengaluru. Fill in your details and a counsellor will call you back.

  • Real EDA tools in your browser: industry-standard EDAReal EDA tools in your browser
  • Recorded sessions on the elearn portalRecorded sessions
  • Placement assistance: resume, mock interviewsPlacement assistance
  • Merit scholarship up to 60% · EMI optionsScholarship up to 60%
Hi! Ask me about courses, fees, batches or discounts. ×
BENGALURU
HYDERABAD