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 Foundations14
    • What Is UVM?
    • Testbench Architecture and Environment Composition
      • The Four Layers
      • The Agent as the Reuse Unit
      • The Env Owns the Infrastructure
      • Coordinating Agents
      • Active-Passive Mixes and Composition Debug
  • UVM Phases & Simulation Lifecycle26
  • Factory & Configuration15
  • Transaction-Level Modeling (TLM)13
  • UVM Sequences & Stimulus21
  • UVM Agents & Protocol VIP21
  • Environment & Test Classes11
  • Checking & Coverage in UVM21
  • UVM Register Abstraction Layer (RAL)22
  • Advanced UVM in Practice13
  • Debug & Regression10
  • Coding Practice47
  • Verification in Industry Practice10
  • Senior Prep: Architecture, Leadership and Interviews32
Loading lesson

‹Testbench Architecture and Environment Composition
  1. Topics
  2. UVM Verification
  3. UVM Foundations
  4. Testbench Architecture and Environment Composition
  5. The Env Owns the Infrastructure

Part 1 · Foundations · Intermediate

The Env Owns the Infrastructure

Build top-down, connect bottom-up: the DMA env's creation and wiring, imp_decl for several inputs, analysis fan-out, and handing config to agents.

On this page

    A new engineer tidies dma_env by moving the three connect calls into build_phase, right after the create calls: "why spread it over two functions?" The first run dies at time 0 with a null-object access on apb.ap. The agent was created a line earlier, so how can its port be null? The answer is the whole reason UVM separates building from connecting.

    The idea: build top-down, connect bottom-up

    Building a testbench is like wiring a house. You cannot run a cable to a socket that has not been installed yet, so the walls go up first, room by room, and only then does the electrician connect everything. UVM does the same in two phases. In build_phase, each component creates its children, starting from the test and working down, so a parent always exists before its children and can configure them. In connect_phase, which runs only after the whole tree is built, components connect ports, starting from the leaves and working up.

    The bottom-up order matters for one common pattern: an agent copies its monitor's port into its own ap in its connect_phase, and the env connects that ap in its connect_phase. Because children connect before parents, the agent's ap is already assigned when the env uses it. In build_phase, by contrast, the env runs before its agents' own build_phase, so the monitor, and the port the agent forwards, do not exist yet. That is the hook's null.

    The picture: two passes over the tree

    Sequence diagram: during build, the env creates the agent and scoreboard, then the agent creates its monitor; during connect, the agent assigns ap from the monitor, then the env connects ap to the scoreboard.
    The env's build runs before the agent's, so the monitor and the agent's ap do not exist during the env's build. By the env's connect, every child has built and connected itself.

    Build it: the DMA env

    The bench's env is short because it does only two jobs. Build creates every child through the factory with a stable name; connect wires each agent's public port to a scoreboard input.

    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
    • Names are stable and meaningful: apb, in_agt, out_agt, sb. Config paths, factory instance overrides and every log line depend on them.

    • Every child comes from `type_id::create`, so a test can override any of them. dma_drain_test replaces sb with a derived scoreboard this way, with no env edit.

    • Connect is unconditional. No if on a scenario knob decides whether a checker hears traffic.

    Three streams into one scoreboard

    A class can have only one method called write, but the scoreboard needs three inputs. The uvm_analysis_imp_decl macro creates a new imp class whose write call is forwarded to a method with a suffix, so one scoreboard can implement write_apb, write_in and write_out:

    design.sv
    1
    2
    3
    4
    // snippet: from the bench, above dma_scoreboard
    `uvm_analysis_imp_decl(_apb)   // uvm_analysis_imp_apb calls write_apb()
    `uvm_analysis_imp_decl(_in)    // uvm_analysis_imp_in  calls write_in()
    `uvm_analysis_imp_decl(_out)   // uvm_analysis_imp_out calls write_out()

    The alternative is a uvm_tlm_analysis_fifo per stream and a run_phase thread in the scoreboard that pulls from them. FIFOs decouple timing (the scoreboard can wait for a matching item), at the cost of a thread and a FIFO to drain at the end of the test. The imp style is simpler when every input can be processed the moment it arrives, as here.

    One port, many listeners

    An analysis port can connect to any number of imps, and each write reaches all of them. Adding a coverage collector to the APB stream is one more line in connect_phase, and the monitor never knows:

    design.sv
    // snippet
    apb.ap.connect(sb.apb_export);
    apb.ap.connect(len_cov.analysis_export);   // a uvm_subscriber names its imp analysis_export

    Two facts about fan-out are worth knowing cold. A second connection never replaces the first; every connected imp receives every transaction. And the order in which subscribers are called is not something to rely on (the details are in analysis broadcast). An analysis port with no subscribers at all is legal, which is why a forgotten connection is silent.

    Handing configuration down: two styles

    An env usually gives each agent a config object. It can put the object in config_db for the agent to fetch in its build_phase, or assign it straight to the agent's handle right after create. Both are correct, because the agent's build_phase has not run yet at that point. config_db can be traced and printed (+UVM_CONFIG_DB_TRACE) and works across hierarchy levels; a direct assignment is checked by the compiler and costs nothing at run time. Pick one per bench and use it everywhere.

    Watch it run: a missing connection, made visible

    The passing run proves all three connections are live: the scoreboard's 8 words matched needs the KEY from the APB stream, every input word, and every output word. Now delete the first line of connect_phase, so the APB monitor's traffic never reaches the scoreboard. Nothing complains at elaboration, because an unconnected analysis port is legal. The scoreboard's key stays 0, so it predicts each output as the plain input word, and every compare fails:

    Diagram
    UVM_ERROR dma_tb.sv(396) @ ...: uvm_test_top.env.sb [SB_MISMATCH] expected <input word>, got <input word ^ a5a50f0f>
    ...
    UVM_INFO dma_tb.sv(411) @ ...: uvm_test_top.env.sb [SB] 0 words matched, 8 mismatched

    Every mismatch differs from its expected value by exactly the KEY, a5a50f0f. That pattern is the clue: the data path is fine, and the scoreboard is missing the register stream. A symptom that is "wrong by a constant" usually means a missing input, not a broken DUT.

    What breaks: the connect that ran too early

    Back to the hook. With the connects moved into build_phase, the env executes apb.ap.connect(...) immediately after creating apb. But apb.ap is assigned in the agent's connect_phase from mon.ap, and at this moment the agent's build_phase has not even run, so mon does not exist. apb.ap is null, and calling connect on it is a null-object access that stops the simulation at time 0.

    The engineer's next instinct is often to "fix" it by connecting to apb.mon.ap instead, which still fails for the same reason and also breaks the agent boundary. The real fix is to put connections back in connect_phase. The rule to remember: build creates, connect wires, and anything that needs a child's internals waits for connect.

    Interview

    "Why does UVM have separate build and connect phases?"

    A weak answer: "Build creates components and connect connects them." It restates the names.

    A strong answer: "Build runs top-down so each parent exists, and can configure its children, before they build. Connections need both ends to exist, and a parent's build runs before its children's, so connecting in build would hit null ports. Connect runs after the whole tree exists, bottom-up, which also lets an agent forward its monitor's port as its own before the env connects to it. UVM then checks bindings before end of elaboration, so a missing required connection fails early."

    Expect the follow-ups: Is an unconnected analysis port an error? (no; a non-analysis port with no connection is, at end of elaboration) and Imp-decl or analysis FIFOs in a scoreboard? (imps when each item can be handled on arrival; FIFOs when you need to wait or decouple timing).

    Check yourself The scoreboard reports 0 words matched, 8 mismatched, and every actual word equals the expected word XOR a5a50f0f. Where do you look first?

    The APB-to-scoreboard path: a missing apb.ap.connect, or a write_apb that does not recognise the KEY write. The constant difference equal to the KEY means the prediction used KEY = 0 while the DUT used the real key. The stream paths are working, since every word was compared.

    Check yourself Can the env assign in_agt.cfg = cfg.in_cfg; right after in_agt = axis_agent::type_id::create("in_agt", this);?

    Yes. The agent's build_phase runs after the env's has finished, so the handle is in place when the agent reads it. It is a legitimate alternative to config_db, type-checked by the compiler, though it does not show up in config_db traces.

    Next: the env is wired, but a DMA job still needs two agents to act in order: registers first, then data. Coordinating agents shows how.

    Related topics

    • Coordinating agents

    • TLM ports, exports and imps

    • Scoreboards with several imps

    Previous

    16 / 280

    Next: Coordinating Agents

    Up next

    Up nextCoordinating Agents

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • The Four Layers
    • Active-Passive Mixes and Composition Debug