You want the APB agent to check its interface handle once, so you add a uvm_config_db#(virtual apb_if)::get(this, "", "vif", vif) to apb_agent::build_phase with a fatal on failure. The driver and the monitor have always found that same handle. The agent does not: the test dies at time 0 with your new fatal. The handle is in the database, under the right name and type. The path is the problem.
uvm_config_db is a notice board shared by the whole testbench. A set(context, inst_path, field, value) pins a note addressed to every component whose full name matches the path (the context's full name, a dot, then inst_path). A get(this, "", field, value) looks for notes addressed to the caller's own full name, with that field name and exactly that type. It returns 1 if it found one and 0 if not.
Top-down build is what makes the board reliable: a parent's build finishes before its children's builds start, and the top module's initial block runs before run_test. So a note pinned by a parent in its build_phase is always there when the child looks. The other half is the address. A path ending in .* reaches everything inside a box, not the box itself.
// snippet: from the course DMA bench
module top;
import uvm_pkg::*;
import dma_pkg::*;
logic clk = 0;
always #5 clk = ~clk; // 100 MHz
rst_if rst (clk);
wire rst_n = rst.rst_n;
initial begin
rst.rst_n = 1'b0;
repeat (4) @(posedge clk);
rst.rst_n <= 1'b1;
end
apb_if apb (clk, rst_n);
axis_if s_ax (clk, rst_n);
axis_if m_ax (clk, rst_n);
irq_if irqi (clk);
// The output side accepts every beat unless a test adds a reactive slave
// (axis_sink_agent), whose driver then drives TREADY.
initial m_ax.tready = 1'b1;
dma_dut dut (
.clk, .rst_n,
.paddr(apb.paddr), .psel(apb.psel), .penable(apb.penable), .pwrite(apb.pwrite),
.pwdata(apb.pwdata), .prdata(apb.prdata), .pready(apb.pready), .pslverr(apb.pslverr),
.s_tdata(s_ax.tdata), .s_tvalid(s_ax.tvalid), .s_tlast(s_ax.tlast), .s_tready(s_ax.tready),
.m_tdata(m_ax.tdata), .m_tvalid(m_ax.tvalid), .m_tlast(m_ax.tlast), .m_tready(m_ax.tready),
.irq(irqi.irq)
);
initial begin
uvm_config_db#(virtual apb_if)::set(null, "uvm_test_top.env.apb.*", "vif", apb);
uvm_config_db#(virtual axis_if)::set(null, "uvm_test_top.env.in_agt.*", "vif", s_ax);
uvm_config_db#(virtual axis_if)::set(null, "uvm_test_top.env.out_agt.*", "vif", m_ax);
uvm_config_db#(virtual irq_if)::set(null, "uvm_test_top", "irq_vif", irqi);
uvm_config_db#(virtual irq_if)::set(null, "uvm_test_top.env.irq.*", "vif", irqi);
uvm_config_db#(virtual rst_if)::set(null, "uvm_test_top*", "rst_vif", rst);
run_test("dma_smoke_test");
end
endmoduleEach interface goes to the components that use it. "uvm_test_top.env.apb.*" matches uvm_test_top.env.apb.drv and uvm_test_top.env.apb.mon, which each read vif in their build_phase with a fatal on failure. It does not match uvm_test_top.env.apb, and that is the opening bug: the agent's own get misses.
There are three fixes, each with a cost. Set on "uvm_test_top.env.apb*" (no dot), which reaches the agent and its children, but also any sibling whose name starts with apb. Set a second time on the exact path "uvm_test_top.env.apb". Or the common production style: the agent gets the handle once and hands it to its children through an agent configuration object. What not to do is widen the set to "*", which hands every bus's handle to every component that asks for vif: the APB monitor would then match the stream interface's set as well, and only the type parameter would keep it from picking the wrong one.
Every get in the bench follows one shape: check the return value and stop with a message that names the component. A get whose result is ignored leaves a null handle that fails much later, on the first vif. access, far from the cause.
// snippet
if (!uvm_config_db#(virtual apb_if)::get(this, "", "vif", vif))
`uvm_fatal("NOVIF", $sformatf("apb_if not set for %s", get_full_name()))A get matches only if | Typical mismatch |
|---|---|
The type parameter is identical |
|
The setter's path matches the getter's full name |
|
The field name is byte-identical |
|
The set happened before the get | Set in the child's own build, or in connect, after the get ran |
During build, a set from higher in the hierarchy wins (the precedence is 1000 minus the setter's depth, so the top module and the test beat the env), and between equal precedences, and for any set made after build, the last one wins. Path specificity never decides: a matching wildcard from the test beats an exact path from the env. The full rules, with a worked example, are in config_db resolution order.
The env pins one note of its own: is_active = UVM_PASSIVE for out_agt, because the DUT drives that port. Each agent then builds a driver and sequencer only when get_is_active() says UVM_ACTIVE, and guards the matching connect the same way:
// snippet: the shape of both bench agents
function void build_phase(uvm_phase phase);
super.build_phase(phase); // uvm_agent reads is_active here
mon = axis_monitor::type_id::create("mon", this);
if (get_is_active() == UVM_ACTIVE) begin
sqr = axis_sequencer::type_id::create("sqr", this);
drv = axis_driver::type_id::create("drv", this);
end
endfunction
function void connect_phase(uvm_phase phase);
ap = mon.ap;
if (get_is_active() == UVM_ACTIVE) // same condition as the create
drv.seq_item_port.connect(sqr.seq_item_export);
endfunctionTwo details matter. The agent never reads is_active from config_db itself; uvm_agent::build_phase does it, which is why the super call is not optional (What super.build_phase() Really Does shows the silent failure). And every conditional create needs the same condition around every use in connect, or the passive instance dereferences a driver that was never built.
out_agt axis_agent - @272
mon axis_monitor - @674
ap uvm_analysis_port - @682The passive agent in the smoke test's topology: one monitor, no driver, no sequencer. When you change configuration and want to see what was actually read, three tools help: +UVM_CONFIG_DB_TRACE on the command line logs every set and get; print_config() on a component lists what is visible to it; and check_config_usage(), called in end_of_elaboration, reports settings that were set but never read, which is usually a misspelled path or field.
Back to the opening. The fatal fired in uvm_test_top.env.apb, the database held a virtual apb_if named vif, and the engineer's first instinct was a type mismatch. +UVM_CONFIG_DB_TRACE showed the set's scope as uvm_test_top.env.apb.* and the failed lookup as uvm_test_top.env.apb: the path, not the type. They added an exact-path set for the agent and kept the wildcard for its children.
The same shape shows up the other way round: a set on "env.agt" that the driver at env.agt.drv never sees. Read the set's path as a pattern against the getter's full name, character by character, and the answer is usually obvious.
"in_agt.*". The agent's build_phase reads it with get(this, "", "cfg", cfg). What happens?The get fails: the set's pattern is uvm_test_top.env.in_agt.*, which matches the agent's children but not uvm_test_top.env.in_agt itself. Set on the exact path "in_agt", which is right for an object only the agent reads.
dma_env to set out_agt's is_active *before* creating out_agt, and also safe to set it after?The agent reads the value in its own build_phase, which runs after the env's build_phase returns. Any set made anywhere in the env's build, before or after the create, is in place by then.
A connect that uses the driver or sequencer without the get_is_active() == UVM_ACTIVE guard. The passive build skipped those creates, so connect must skip their uses under the same condition.
Next: with every component built and configured, Connect Phase: Bottom-Up TLM Wiring wires them together.