The block env makes one decision on its own: the output stream agent is passive, because the DUT drives that port. The subsystem needs a different decision for one of the two copies (dma1's input is driven by dma0, so its agent must be passive too), and an option to leave out the end-to-end checker for a fast run. You could add a config_db string for each choice. Twenty strings later, a typo in one of them is silently ignored and a checker you thought was on has been off for a month. Where should an environment's options live?
An environment (env) is the reusable assembly of a testbench: it creates agents and checkers and connects them. It owns topology, meaning what exists and how it is wired, but not the scenario. An env config object is a plain uvm_object that holds the env's options as typed fields: which optional components to build, which agents are active, and handles to the agents' own config objects. The test creates it, fills it in, and puts it in config_db once; the env reads it once, at the start of build_phase.
A car's build sheet is the analogy. The factory builds the same chassis every time, and a sheet travelling with it says: sunroof yes, tow hitch no, diesel engine. The workers read the sheet, not twenty sticky notes on the doors. If the customer hands in no sheet, the factory builds the standard model. That last rule, defaults when nothing was provided, is what makes an env usable by any test.
// snippet: from the course DMA bench
class dma_env extends uvm_env;
`uvm_component_utils(dma_env)
apb_agent apb;
axis_agent in_agt, out_agt;
dma_scoreboard sb;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
// The output side is observed only: the DUT drives it.
uvm_config_db#(uvm_active_passive_enum)::set(this, "out_agt", "is_active", UVM_PASSIVE);
apb = apb_agent::type_id::create("apb", this);
in_agt = axis_agent::type_id::create("in_agt", this);
out_agt = axis_agent::type_id::create("out_agt", this);
sb = dma_scoreboard::type_id::create("sb", this);
endfunction
function void connect_phase(uvm_phase phase);
apb.ap.connect(sb.apb_export);
in_agt.ap.connect(sb.in_export);
out_agt.ap.connect(sb.out_export);
endfunction
endclassbuild_phase sets out_agt passive in config_db before creating it, then creates the APB agent, two stream agents and the scoreboard through the factory, so a test can replace any of them.
connect_phase connects each agent's analysis port to a scoreboard export. It runs after every component is built, which is why connections never go in build_phase.
What is hard-wired: the scoreboard always exists, and out_agt is always passive. For one block that is fine. The moment someone needs a run without the scoreboard, or an active output agent, those become options, and options want a config object.
The subsystem env has two options, and they live in one small class:
// 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
endclass
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
endclassTyped fields with defaults. has_e2e_checker and chain_internal both default to 1, the normal subsystem configuration. A test changes only what is different.
Get, or create defaults. If no test set a config, the env creates one and carries on. dma_subsys_test does set one (with the defaults), but the env would build the same bench without it. An env that fatals without a config is an env every test must remember to configure.
Read before creating children. Each field becomes a build decision: chain_internal becomes an is_active setting for dma1.in_agt, set before dma1 is created; has_e2e_checker decides whether e2e exists, and connect_phase checks the same field before wiring it.
Field names describe the bench, not the code. chain_internal says dma1's input is driven by dma0; the env translates that into the agent setting. A test never needs to know the path dma1.in_agt.
Why does the env never set its own default config in config_db and then get it? Because during build, a set from higher in the hierarchy wins: the test (depth 1) beats the env (depth 2) whatever the paths look like. If the env set a default first, the test's set would still win, which is harmless; but if the env simply created its own object and ignored config_db, the test's settings would be lost. get, then create only on failure, is the one pattern that is always right.
The bench's block env has no config object. Here is what one looks like when the hard-wired choices become options, together with an env that reads it. The agents still take is_active through config_db, because uvm_agent reads it there in super.build_phase.
// snippet
class dma_env_cfg extends uvm_object;
`uvm_object_utils(dma_env_cfg)
bit has_scoreboard = 1;
bit has_irq_agent = 0;
uvm_active_passive_enum in_active = UVM_ACTIVE;
uvm_active_passive_enum out_active = UVM_PASSIVE; // the DUT drives the output
function new(string name = "dma_env_cfg");
super.new(name);
endfunction
endclass
class dma_cfg_env extends uvm_env;
`uvm_component_utils(dma_cfg_env)
dma_env_cfg cfg;
apb_agent apb;
axis_agent in_agt, out_agt;
irq_agent irq;
dma_scoreboard sb;
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_env_cfg)::get(this, "", "cfg", cfg))
cfg = dma_env_cfg::type_id::create("cfg"); // the standard model
uvm_config_db#(uvm_active_passive_enum)::set(this, "in_agt", "is_active", cfg.in_active);
uvm_config_db#(uvm_active_passive_enum)::set(this, "out_agt", "is_active", cfg.out_active);
apb = apb_agent::type_id::create("apb", this);
in_agt = axis_agent::type_id::create("in_agt", this);
out_agt = axis_agent::type_id::create("out_agt", this);
if (cfg.has_irq_agent) irq = irq_agent::type_id::create("irq", this);
if (cfg.has_scoreboard) sb = dma_scoreboard::type_id::create("sb", this);
endfunction
function void connect_phase(uvm_phase phase);
if (cfg.has_scoreboard) begin
apb.ap.connect(sb.apb_export);
in_agt.ap.connect(sb.in_export);
out_agt.ap.connect(sb.out_export);
end
endfunction
endclassDecision | Lives in | Example on the bench |
|---|---|---|
Which scenario runs, its values, its overrides | The test | Three random jobs; the truncating driver |
What the env builds and how it is wired | Env config |
|
How one interface behaves | Agent config |
|
Pin-level connection | The top module | Which interface instance is |
A real env config usually also holds handles to the agent configs (cfg.apb_cfg, cfg.in_cfg), so the test fills one tree and sets one object. The env then passes each agent its piece.
config_db set from the env | Assign the handle after create | |
|---|---|---|
How |
|
|
Why it works | The agent's | The agent's build_phase has not run yet; it runs after the env's returns |
Typo or wrong path | Silent until the agent's get fails | Compile error |
Test can override per instance | Yes, a test-level set wins during build | No, unless the env reads config_db first |
Visible in | Yes | No |
Both are common. Handle assignment is faster and type-checked; config_db keeps agents self-contained (an agent that gets its config works in any env) and lets a test reach past the env. Many teams use config_db for the top-level env config and agent configs, and direct handles for everything below the agent.
The subsystem topology shows both config fields at work. Port rows trimmed:
uvm_test_top dma_subsys_test - @182
env dma_subsys_env - @257
dma0 dma_env - @275
apb apb_agent - @339
in_agt axis_agent - @347
drv axis_driver - @718
mon axis_monitor - @578
sqr uvm_sequencer - @595
out_agt axis_agent - @355
mon axis_monitor - @756
sb dma_scoreboard - @363
dma1 dma_env - @283
apb apb_agent - @780
in_agt axis_agent - @788
mon axis_monitor - @1019
out_agt axis_agent - @796
mon axis_monitor - @1042
sb dma_scoreboard - @804
e2e dma_e2e_scoreboard - @291dma1.in_agt has only a monitor: chain_internal made it passive. dma0.in_agt, built from the same class by the same env code, has a driver and a sequencer.
e2e exists because has_e2e_checker is 1. Set it to 0 in the test and the row, and its report line, disappear.
Both out_agts are passive because dma_env itself says so; the subsystem env did not have to repeat it.
A team gives their block env a config object like dma_env_cfg and builds a subsystem from two copies of it. To save a few lines, the subsystem env creates one dma_env_cfg, sets it for both dma0 and dma1, and then writes cfg.in_active = UVM_PASSIVE to make dma1's input passive. Every subsystem test now dies at time 0 with a null-object access on env.dma0.in_agt.sqr.
There was only one object. Both envs held a handle to it, so making dma1's input passive made dma0's passive too, and dma0's input agent built no sequencer. A config object is shared by handle, not copied by config_db. The fix is one object per instance: create (or clone) a config for each sub-env and set each on its own path. The topology print shows the symptom at a glance: neither in_agt has a driver.
dma_subsys_test sets its config with chain_internal = 0, but leaves dma0 feeding dma1 as in the subsystem top. What does the env build, and what happens on the middle bus?The env skips the passive setting, so dma1.in_agt is active and builds a driver and a sequencer on the middle bus, which dma0's outputs already drive. The driver writes tvalid <= 0 at time 0, fighting the DUT. Depending on the simulator that is an elaboration error about mixed drivers or a corrupted bus. The config field exists precisely because the env cannot know from inside whether that bus is driven by another DUT.
cfg.has_e2e_checker to 0 in its run_phase. What happens?Nothing useful. The env read the field in build_phase and already created the checker and, in connect_phase, wired it. Changing the field later only makes the config object lie about the bench. Topology fields must be final before the env's build_phase runs.
dma_subsys_env set is_active for dma1.in_agt itself, instead of the test setting it on the full path?Encapsulation. The test should say what the bench is (dma1 is fed internally), not how the env is built inside. If the env later renames in_agt or adds a level, only the env changes. A test-level set would still win if someone needed it, because during build the test's set has higher precedence.
Next: the subsystem env is built from two copies of the block env. Vertical Reuse: From Block to Subsystem shows the top, the checkers and the run.