An engineer on the subsystem team wants dma1's input agent passive in a new test. They add one line to the test, config_db set on "env.dma*.in_agt", because the wildcard "looks right". The test crashes at 275 ns with a fatal from deep inside the sequence library. Nobody changed the env. What happened, and how do you stop it happening across three levels of environment and a hundred tests?
Configuration is how the same env behaves differently at block, subsystem and chip level. Left alone, it grows into dozens of loose config_db keys set from wherever was convenient, and the only way to learn what a run used is to read every set in the codebase. The senior pattern replaces that with a configuration tree that mirrors the env tree: one config object per env, nested the way the envs nest, built and decided by the test, and handed down with one config_db set per level.
A building's electrical panel is the model. Every circuit has one labelled breaker in one place; nobody switches a kitchen light off from a junction box in the attic. A loose config_db set from a test into the depths of an agent is that attic junction box: it works until someone else wires a circuit through it.
One config object per env level, nested: the subsystem config holds the block configs.
The parent sets, then creates. A set must run before the child's build_phase reads it.
The test decides; components only read. Only the test parses plusargs. A shared config object is read-only after it is set.
Validate in one place, before anything is built, so an illegal combination fails at time zero with a clear message.
`config_db` carries handles, not a hundred scalars: virtual interfaces and config objects. The precedence rule (higher setter wins during build, then the last set) is taught in config_db resolution; this lesson assumes it.
The bench already has the top of the tree. dma_subsys_cfg holds the two decisions that differ between subsystem configurations, and dma_subsys_env reads it with a default when no test set one:
// snippet: from the course DMA bench
// Two DMAs in a chain (dma0's output feeds dma1's input), verified by reusing the
// block env twice, unchanged, plus an end-to-end checker.
class dma_subsys_cfg extends uvm_object;
bit has_e2e_checker = 1; // the end-to-end scoreboard
bit chain_internal = 1; // dma1's input is driven by dma0, so its agent is passive
`uvm_object_utils(dma_subsys_cfg)
function new(string name = "dma_subsys_cfg");
super.new(name);
endfunction
endclassWhat is missing is the block level. dma_env hard-codes its one decision (output agent passive) and has no switches at all, so dma_subsys_env reaches into it with a path-based config_db set on "dma1.in_agt". That works for one decision. With ten it becomes the attic junction box. The proposal below adds a block config and nests two of them in the subsystem config. It is a design sketch, not bench code:
// snippet: a configuration tree for the DMA envs (a proposal, not in the bench)
class dma_env_cfg extends uvm_object;
`uvm_object_utils(dma_env_cfg)
uvm_active_passive_enum apb_mode = UVM_ACTIVE;
uvm_active_passive_enum in_mode = UVM_ACTIVE;
bit has_scoreboard = 1;
bit has_coverage = 1;
function new(string name = "dma_env_cfg");
super.new(name);
endfunction
function void validate(string who);
if (has_scoreboard && apb_mode == UVM_PASSIVE && in_mode == UVM_PASSIVE)
`uvm_info("CFG", {who, ": fully passive with a scoreboard: inputs must come from monitors"}, UVM_LOW)
endfunction
endclass
class dma_subsys_tree_cfg extends uvm_object;
`uvm_object_utils(dma_subsys_tree_cfg)
bit has_e2e_checker = 1;
bit chain_internal = 1;
dma_env_cfg dma[2];
function new(string name = "dma_subsys_tree_cfg");
super.new(name);
foreach (dma[i]) dma[i] = dma_env_cfg::type_id::create($sformatf("dma%0d_cfg", i));
endfunction
// Level policy lives here, in one place, not in tests.
function void apply_chain();
if (chain_internal) dma[1].in_mode = UVM_PASSIVE;
endfunction
function void apply_soc(); // the CPU owns both register buses
foreach (dma[i]) dma[i].apb_mode = UVM_PASSIVE;
endfunction
endclass
// In the subsystem env's build_phase, before creating the children:
// uvm_config_db#(dma_env_cfg)::set(this, "dma0", "cfg", cfg.dma[0]);
// uvm_config_db#(dma_env_cfg)::set(this, "dma1", "cfg", cfg.dma[1]);
// and in dma_env's build_phase, the agent modes come from its cfg:
// uvm_config_db#(uvm_active_passive_enum)::set(this, "apb", "is_active", cfg.apb_mode);
// uvm_config_db#(uvm_active_passive_enum)::set(this, "in_agt", "is_active", cfg.in_mode);Nested objects are created in `new`, so policy functions never touch a null handle.
Level policy is a named function (apply_chain, apply_soc). A test calls one; it does not poke six fields.
Each env passes its children their branch with an exact path on its own direct children. No wildcards cross more than one level.
The `is_active` sets remain, because uvm_agent reads its mode from config_db, but each one is made by the agent's own parent, from a field with a name.
When a configuration question comes up, turn on tracing before reading code: +UVM_CONFIG_DB_TRACE on the command line, or uvm_config_db_options::turn_on_tracing() from a test's constructor (the scratch run below uses the call, because this Verilator build has no DPI for the command-line processor). This is the unchanged subsystem bench with tracing on, filtered to the lines that matter (the printed object is shortened to its two fields):
+UVM_TESTNAME=dma_cfg_trace_test (scratch run: dma_subsys_test with tracing turned on)
UVM_INFO base/uvm_config_db_implementation.svh(485) @ 0: reporter [CFGDB/SET] Configuration scope='uvm_test_top.env' name='cfg' (type PARAMTYPEDTYPE 'T') set accessor=uvm_test_top = (PARAMTYPEDTYPE 'T') '{has_e2e_checker:'h1, chain_internal:'h1, ...}
UVM_INFO base/uvm_config_db_implementation.svh(485) @ 0: reporter [CFGDB/GET] Configuration scope='uvm_test_top.env' name='cfg' (type PARAMTYPEDTYPE 'T') read accessor=uvm_test_top.env = (PARAMTYPEDTYPE 'T') '{has_e2e_checker:'h1, chain_internal:'h1, ...}
UVM_INFO base/uvm_config_db_implementation.svh(485) @ 0: reporter [CFGDB/SET] Configuration scope='uvm_test_top.env.dma1.in_agt' name='is_active' (type PARAMTYPEDTYPE 'T') set accessor=uvm_test_top.env = (PARAMTYPEDTYPE 'T') 0
UVM_INFO base/uvm_config_db_implementation.svh(485) @ 0: reporter [CFGDB/SET] Configuration scope='uvm_test_top.env.dma0.out_agt' name='is_active' (type PARAMTYPEDTYPE 'T') set accessor=uvm_test_top.env.dma0 = (PARAMTYPEDTYPE 'T') 0
UVM_INFO base/uvm_config_db_implementation.svh(485) @ 0: reporter [CFGDB/SET] Configuration scope='uvm_test_top.env.dma1.out_agt' name='is_active' (type PARAMTYPEDTYPE 'T') set accessor=uvm_test_top.env.dma1 = (PARAMTYPEDTYPE 'T') 0Every set names its accessor, the component that made it. dma1.in_agt was made passive by uvm_test_top.env, the subsystem env, which is exactly where the decision belongs. Value 0 is UVM_PASSIVE.
The config object is set by the test and read by the env, once each, with its field values printed. If the env had read before the test set, the GET would show the default object.
The trace is noisy. The same run prints 79 trace lines, most of them failed lookups the library makes for its own options (check_connection_relationships, wait_for_sequences_count). Grep for your field name.
Back to the opening. The test meant to make only dma1's input passive, and wrote this scratch test:
// snippet: scratch test, extends dma_subsys_test
function void build_phase(uvm_phase phase);
uvm_config_db#(uvm_active_passive_enum)::set(this, "env.dma*.in_agt", "is_active", UVM_PASSIVE);
super.build_phase(phase);
endfunction+UVM_TESTNAME=dma_cfg_wild_test (scratch run)
UVM_FATAL @ 275000: reporter@@pkt [SEQ] neither the item's sequencer nor dedicated sequencer has been supplied to start item in pkt
UVM_FATAL : 1dma* matches dma0 too. The test's set outranks anything the env sets during build, so dma0.in_agt was built passive, with a monitor and no sequencer: the topology print in the same log shows in_agt under dma0 with only mon. The test then started its packet sequence on env.dma0.in_agt.sqr, a null handle, and the sequence library died at the first start_item. The crash is the lucky outcome; had the stimulus come from elsewhere, the run would simply have checked nothing.
The tree prevents this by construction. The test calls cfg.apply_chain(), which names dma[1] explicitly, and the only is_active sets are made by each agent's parent on its own children. When something does go wrong, the triage order is short: turn on the trace, find the SET whose scope matches the component, read its accessor, and check the topology print for what was actually built.
build_phase, dma1's env sets cfg.has_coverage = 0 for itself. What happens to dma0?It loses coverage too, because both envs hold the same object. Shared config objects are read-only after the set. Give each instance its own branch (cfg.dma[0], cfg.dma[1]), or clone before modifying, and decide per-instance policy in the test or the parent, not in the child.
cfg returning a default object, and the test's SET appears later in the log. What is wrong, and what is the fix?The set ran after the env's build_phase read it, typically because the test created the env before setting the config, or set it in a later phase. Set first, then create: in the test's build_phase, build and set the config object before dma_subsys_env::type_id::create.
Next: configuration decides which agents go passive. Passive Agents and Who Owns the Check decides what keeps checking when they do.