The DMA team builds a two-DMA subsystem by instantiating the block env twice. The first run fills the log with X on the middle bus. Both dma0's output and dma1's input agent are driving it: dma0 because it is the DUT's output, dma1's agent because nobody told it that, at this level, its input is fed by another DUT. The class was right. Its configuration was missing.
An agent is built by someone else, at a path it did not choose, for a context it cannot see. How does it find out what it is supposed to be?
Configuration is how a parent tells a child about the world before the child is built. The parent leaves an envelope in uvm_config_db addressed to the child's hierarchical path; the child opens it in its build_phase. There are two ways to fill the envelope. Knobs are individual typed entries: the bench sets is_active for out_agt as one uvm_active_passive_enum entry, and each virtual interface as one virtual apb_if entry. A config object is one uvm_object holding every setting for a component, passed as a single handle.
Knobs are fine for two or three settings. Past that they scatter: twelve set calls with twelve string names, any of which can be misspelt, and no single place that says what the agent's settings are. A config object turns those into fields with types and defaults, can carry constraints and a validate() method, and travels as one handle. Commercial VIPs almost always use one per agent instance.
Configuration flows down the tree, one level translating it for the next. The subsystem env reads a high-level fact ("dma1's input is fed internally") from its config object and turns it into the low-level knob the agent understands. The agent never learns why it is passive.
In dma_env::build_phase (shown in the previous lesson), one line makes the output agent passive: uvm_config_db#(uvm_active_passive_enum)::set(this, "out_agt", "is_active", UVM_PASSIVE);. The context this plus the relative path "out_agt" addresses exactly env.out_agt. The virtual interfaces are knobs too, set from the top module with a null context and full paths such as "uvm_test_top.env.apb.*"; the driver and monitor each get theirs with get(this, "", "vif", vif) and stop with a fatal if it is missing.
The subsystem env has a small config object of its own. It is not randomized and holds two switches, each with a sensible default:
// 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
endclassThe env that consumes it shows the pattern to copy: get first, create a default only if nothing was set. A test that wants a different topology sets its own object; a test that does not care gets the defaults without writing a line.
// snippet: from the course DMA bench
class dma_subsys_env extends uvm_env;
`uvm_component_utils(dma_subsys_env)
dma_subsys_cfg cfg;
dma_env dma0, dma1; // the block env, reused as is
dma_e2e_scoreboard e2e;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(dma_subsys_cfg)::get(this, "", "cfg", cfg))
cfg = dma_subsys_cfg::type_id::create("cfg"); // defaults if the test set none
if (cfg.chain_internal)
uvm_config_db#(uvm_active_passive_enum)::set(this, "dma1.in_agt", "is_active", UVM_PASSIVE);
dma0 = dma_env::type_id::create("dma0", this);
dma1 = dma_env::type_id::create("dma1", this);
if (cfg.has_e2e_checker) e2e = dma_e2e_scoreboard::type_id::create("e2e", this);
endfunction
function void connect_phase(uvm_phase phase);
if (cfg.has_e2e_checker) begin
dma0.apb.ap.connect(e2e.key0_export);
dma1.apb.ap.connect(e2e.key1_export);
dma0.in_agt.ap.connect(e2e.first_export);
dma1.out_agt.ap.connect(e2e.last_export);
end
endfunction
endclassTwo details matter. The object is created with the factory (type_id::create), so a test could override the config class itself. And the env sets the agent knob after reading its own config and before creating the child envs, which is the order in which the facts become known.
dma_job_cfg describes one DMA job (key, length, interrupt enable) with constraints. dma_rand_test looks for one in config_db and randomizes a fresh one only if none was set, so a derived test can pin a job without touching the loop. The full treatment, including polymorphic config and validation, is in Config objects; the point here is the shape: the owner that consumes the object is the one that decides between "use what I was given" and "make my own".
The bench keeps its agents on knobs to stay small. A production APB agent would carry one object per instance. This sketch shows what goes in it and how the agent uses it:
// snippet
class apb_agent_cfg extends uvm_object;
uvm_active_passive_enum is_active = UVM_ACTIVE;
virtual apb_if vif; // one home for the interface handle
bit has_checks = 1; // protocol checks in the monitor
bit has_coverage = 0;
int unsigned max_wait = 16; // PREADY timeout, in clocks
`uvm_object_utils(apb_agent_cfg)
function new(string name = "apb_agent_cfg");
super.new(name);
endfunction
function bit validate(output string why);
if (is_active == UVM_ACTIVE && vif == null) begin why = "active agent with no vif"; return 0; end
if (max_wait == 0) begin why = "max_wait must be at least 1"; return 0; end
return 1;
endfunction
endclass
// Inside the agent:
// build_phase: get "cfg" (fatal if missing), call cfg.validate(), hand cfg.vif to children.
// get_is_active() is virtual: override it to return cfg.is_active,
// so the mode has one source of truth.Pick one home for each setting. If the vif lives in the config object, the driver and monitor get it from there, not from a second config_db entry that might disagree. If the mode lives in the object, override get_is_active() to return cfg.is_active, and every piece of code that asks the agent its mode gets the same answer.
During build_phase, a set from higher in the hierarchy wins: the test (depth 1) beats the env (depth 2), whatever paths they use. Between sets at equal precedence, and for every set made after build, the last set wins. That is why the get-or-create pattern above is safe: a test-supplied object always beats anything the env might set. The full rules, with worked cases, are in config_db resolution; turn on +UVM_CONFIG_DB_TRACE when a get returns something you did not expect.
Run dma_subsys_test with the subsystem file (scripts/data/uvm-course/bench/dma_subsys_tb.sv). The test sets no dma_subsys_cfg, so the env creates the default: end-to-end checker on, dma1's input passive.
UVM_INFO dma_tb.sv(411) @ 875000: uvm_test_top.env.dma0.sb [SB] 10 words matched, 0 mismatched
UVM_INFO dma_tb.sv(411) @ 875000: uvm_test_top.env.dma1.sb [SB] 10 words matched, 0 mismatched
UVM_INFO dma_tb.sv(1178) @ 875000: uvm_test_top.env.e2e [E2E] 10 words matched end to end, 0 mismatched
UVM_ERROR : 0
UVM_FATAL : 0dma1.sb matched 10 words although dma1's input agent drives nothing. Its passive monitor watched dma0's output on the shared middle bus and fed dma1's scoreboard. That is configuration doing its job.
The e2e line exists because has_e2e_checker defaulted to 1. A test that sets a cfg with it cleared would get the same two block lines and no third.
A team converted their stream agents to config objects. To save lines, the env created one axis_agent_cfg, set it for both in_agt and out_agt, and then wrote cfg.is_active = UVM_PASSIVE for the output side. Every test hung. The input agent had no driver, so no word ever entered the DMA, no interrupt came, and the run ended on the global timeout. The topology print told the story to anyone who looked: neither agent had a driver.
The root cause is that a config object is passed by handle. Both agents held the same object, so the line meant for one changed both. The fix is one object per agent instance (create two, or clone() a template and then change the copy). The same trap catches shared objects changed at run time: a test that edits cfg.max_wait mid-run changes it for every agent holding that handle, which is sometimes what you want and must always be deliberate.
apb_agent_cfg and sets it for apb. A test also sets one, during its own build_phase. Which does the agent get, and is the env's code still wrong?The agent gets the test's object: the test is higher in the hierarchy, so its build-time set wins regardless of order. The env's code is still poor style. It does work every run that nobody uses, and it hides intent: a reader cannot tell that a test is allowed to replace the object. Get first, and create a default only when the get fails.
dma_subsys_env set is_active for dma1.in_agt itself, instead of letting the test do it?Because "dma1's input is driven by dma0" is a fact about the subsystem's topology, which the subsystem env owns. Tests choose scenarios; they should not need to know the path of an agent two levels down. The env translates its own config switch, chain_internal, into the agent's knob.
cfg.validate(), which fails. Fatal, error or warning?Fatal. An invalid configuration means the bench is not the bench the test thinks it is; continuing produces failures that look like DUT bugs. Stopping at time 0 with a message that names the bad field costs seconds; debugging a misconfigured run costs hours.
Next: the configuration said passive. In Mode-gated build you will see exactly where the agent reads that, what it builds as a result, and how a derived agent can lose its monitor by skipping one line.