Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here4
  • UVM Foundations18
  • UVM Phases & Simulation Lifecycle94
  • Factory & Configuration37
  • Transaction-Level Modeling (TLM)42
  • UVM Sequences & Stimulus44
  • UVM Agents & Protocol VIP71
  • Environment & Test Classes55
    • Environment Architecture
      • The Environment and Its Config Object
      • Vertical Reuse: From Block to Subsystem
      • Integrating Third-Party VIP
      • Where the Virtual Sequencer Lives (and Whether You Need One)
    • Environment Composition Hub: Scaling Reusable Env Topologies
    • Scoreboard + Coverage Hub: Integrated Checking Pipelines
    • Tests: Base Classes, Selection and Startup
    • Config Distribution Hub: Test-to-Env Policy Propagation
    • Virtual Sequencer Hub: Env-Level Multi-Agent Coordination
    • Plusargs & Test Selection Hub: Runtime Control Without Recompile
    • End-of-Test Hub: Objections, Readiness, and Fail-Fast Shutdown
    • Top Module Hub: Startup Contracts from HDL to UVM
  • Checking & Coverage in UVM37
  • UVM Register Abstraction Layer (RAL)53
  • Advanced UVM in Practice44
  • Debug & Regression10
  • Coding Practice47
  • Senior Prep: Architecture, Leadership and Interviews92
  • Verification in Industry Practice10
Loading lesson

‹Environment Architecture
  1. Topics
  2. UVM Verification
  3. Environment & Test Classes
  4. Environment Architecture
  5. The Environment and Its Config Object

Part 7 · Environment & Tests · Intermediate

The Environment and Its Config Object

What dma_env builds and wires, env config objects with dma_subsys_cfg as the worked example (has_e2e_checker, chain_internal, get-or-create defaults), what belongs in env cfg vs agent cfg vs the test, and config_db vs handle passing.

On this page

    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?

    The idea: the env owns topology, and a config object describes it

    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.

    The picture

    Flowchart: the test sets the cfg in config_db; the env gets it or creates defaults; chain_internal makes dma1.in_agt passive; the env creates dma0 and dma1; has_e2e_checker decides whether e2e is created.
    How dma_subsys_env uses its config object. The test sets it once; the env reads it (or makes defaults), turns each field into a build decision, and only then creates its children.

    Build it

    What dma_env builds and wires

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    // 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
    endclass
    • build_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.

    dma_subsys_cfg: a worked env config

    The subsystem env has two options, and they live in one small class:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    // 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
    endclass
    • Typed 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.

    A config object for the block env

    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.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    // 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
    endclass

    What belongs where

    Decision

    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

    has_e2e_checker, chain_internal, has_scoreboard

    How one interface behaves

    Agent config

    is_active, the virtual interface, protocol timeouts, coverage on/off

    Pin-level connection

    The top module

    Which interface instance is vif for which agent

    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.

    Passing agent configs: config_db or a handle

    config_db set from the env

    Assign the handle after create

    How

    uvm_config_db#(axis_cfg)::set(this, "in_agt", "cfg", cfg.in_cfg) before creating the agent

    in_agt = axis_agent::type_id::create(...); in_agt.cfg = cfg.in_cfg;

    Why it works

    The agent's get in its own build_phase finds it

    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 +UVM_CONFIG_DB_TRACE

    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.

    Watch it run

    The subsystem topology shows both config fields at work. Port rows trimmed:

    Diagram
    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      -     @291
    • dma1.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.

    What breaks: one config object, two environments

    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.

    Interview

    "What goes in an environment config object, and how does it reach the components?"

    A weak answer: "All the testbench settings, put in config_db with a wildcard so everyone sees them." A wildcard set of a big object is how options leak into components that should not see them.

    A strong answer: "The env config describes topology: has_* switches for optional checkers and agents, active or passive per agent, and handles to the agent configs. Scenario values stay in the test. The test creates and fills it in build_phase and sets it once on the env's path; the env gets it, or creates defaults if the get fails, so it works with no test config. It reads it before creating children, translates fields into child settings such as is_active, and passes each agent its own config, by config_db or by assigning the handle right after create. Each sub-env gets its own object; they are shared by handle."

    Follow-ups: Why get-or-create rather than fatal on a missing config? (any test, and the next integration level, can use the env without knowing its options). The env sets is_active for an agent and the test sets it too. Which wins? (the test: during build the higher setter wins).

    Check yourself 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.

    Check yourself A test changes 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.

    Check yourself Why does 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.

    Related topics

    • Vertical Reuse: From Block to Subsystem

    • config_db resolution order

    • Typed config patterns

    • Agent configuration

    Previous

    312 / 658

    Next: Multi-Agent Build: Deterministic Construction at Scale

    Up next

    Up nextMulti-Agent Build: Deterministic Construction at Scale

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Vertical Reuse: From Block to Subsystem
    • Integrating Third-Party VIP
    • Where the Virtual Sequencer Lives (and Whether You Need One)