UVM env: build_phase, connect_phase & analysis ports

In a UVM testbench the uvm_env is the component that owns and connects everything that surrounds your DUT: the agents that drive and sample pins, the scoreboard that checks results, coverage collectors, and sometimes smaller environments nested inside it. A test decides which env to build and how to configure it; the env decides what components exist and how they are wired together. Getting the UVM env right is what lets a verification environment be reused across blocks, projects, and even a full SoC.

Before you run any of this: the UVM snippets below are library code and are shown as syntax references only. To compile and simulate them you need a UVM-capable simulator such as industry-standard simulation tools, VCS, or industry-standard simulators, or the free online option EDA Playground. A plain Verilog simulator without the UVM package will not elaborate them. Further down there is one self-contained SystemVerilog example that demonstrates the same build-then-connect idea and runs with no UVM library at all.

What a uvm_env actually is

A user environment is simply a class that extends the base uvm_env component and registers itself with the factory. By itself it does nothing useful — its whole job is to be a parent for other components. The minimal skeleton every env starts from looks like this:

class alu_env extends uvm_env;
  `uvm_component_utils(alu_env)

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction
endclass

Three things are mandatory. extends uvm_env gives the class the standard phasing (build, connect, run, and the rest). The `uvm_component_utils macro registers the type with the factory so it can be created by name and overridden later. And the new constructor takes a name and a parent handle and passes them straight to super.new — that is how the component tree knows where this env lives in the hierarchy.

Instantiating agents and a scoreboard in build_phase

Components are never constructed in new. They are constructed inside build_phase, which UVM calls top-down: the env is built before its children, so the env can read configuration and decide what to create before those children exist. Declare each sub-component as a class member, then build it with the factory:

class alu_env extends uvm_env;
  `uvm_component_utils(alu_env)

  alu_agent      agent;       // reusable driver + monitor + sequencer bundle
  alu_scoreboard scoreboard;  // checks DUT output against a reference

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    agent      = alu_agent::type_id::create("agent", this);
    scoreboard = alu_scoreboard::type_id::create("scoreboard", this);
  endfunction
endclass

Notice type_id::create("agent", this) instead of new(). Using the factory is what makes the env reusable: a test can register a factory override so that the same env builds, say, an error-injecting agent instead of the default one — without editing a single line of the env. The this argument makes the env the parent, so the full instance path becomes uvm_test_top.m_env.agent. Always call super.build_phase(phase) first.

PhaseCall orderWhat belongs here
build_phaseTop-down (parent first)Call create() for every sub-component; read settings from uvm_config_db.
connect_phaseBottom-up (child first)Join TLM/analysis ports to exports; assign virtual interface handles.

Connecting analysis ports in connect_phase

Once all components exist, the env wires them together in connect_phase. The most common wiring in any env is the analysis port: a monitor broadcasts every transaction it observes, and one or more subscribers receive a copy. The monitor declares a uvm_analysis_port; the scoreboard declares a uvm_analysis_imp and implements a write() method that UVM calls for each transaction.

class alu_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(alu_scoreboard)

  // the imp acts as the "export" the monitor's port connects into
  uvm_analysis_imp #(alu_txn, alu_scoreboard) item_export;

  function new(string name, uvm_component parent);
    super.new(name, parent);
    item_export = new("item_export", this);
  endfunction

  // UVM calls write() once per transaction received on the imp
  virtual function void write(alu_txn t);
    `uvm_info("SCB", $sformatf("checking result = %0d", t.result), UVM_LOW)
  endfunction
endclass

The env then joins them. Connection always flows port.connect(export) — the producer connects into the consumer:

virtual function void connect_phase(uvm_phase phase);
  super.connect_phase(phase);
  // monitor sits inside the agent; its port feeds the scoreboard imp
  agent.monitor.item_collected_port.connect(scoreboard.item_export);
endfunction

A single uvm_analysis_imp is fine when the scoreboard listens to one source. When it must receive from two different ports — an expected stream and an actual stream, for example — each needs its own write method, which you get with the `uvm_analysis_imp_decl(_actual) macro. That generates a uvm_analysis_imp_actual type whose method is named write_actual, so the two subscriptions never collide.

Layering sub-environments

Because an env is just a component, an env can contain other envs. This is how block-level environments are reused at the SoC level: you do not rewrite the ALU checking logic, you instantiate the whole alu_env inside a larger soc_env and build a memory env beside it. The parent only constructs its sub-envs; each sub-env builds and connects its own internals.

class soc_env extends uvm_env;
  `uvm_component_utils(soc_env)

  alu_env alu;   // reused block-level env, unchanged
  mem_env mem;   // a second block-level env

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    alu = alu_env::type_id::create("alu", this);
    mem = mem_env::type_id::create("mem", this);
  endfunction

  virtual function void connect_phase(uvm_phase phase);
    super.connect_phase(phase);
    // only cross-env, SoC-level wiring lives here
  endfunction
endclass

Keep a clear rule: a sub-env wires its own components internally; the parent env only makes connections that cross between its children. That separation is what keeps the block-level env usable on its own and inside bigger systems.

The build-then-connect pattern in plain SystemVerilog

The two-step rhythm — construct every component first, then wire them — is the single most important idea behind a UVM env, and you can see it without any UVM library at all. The example below models an env, an agent, a monitor, and a scoreboard as plain classes. The monitor holds a scoreboard handle that plays the role of an analysis port, and the env builds the pieces before connecting them. It runs on any SystemVerilog simulator.

// tb.sv -- the UVM env "build then connect" pattern, plain SystemVerilog, no UVM
module tb;

  // Plays the role of uvm_analysis_imp: receives transactions via write()
  class scoreboard;
    int received;
    function void write(int txn);
      received++;
      $display("[SCB] got txn=%0d (total=%0d)", txn, received);
    endfunction
  endclass

  // Holds an analysis-port-like handle to a subscriber
  class monitor;
    scoreboard ap;
    function void observe(int txn);
      if (ap != null) ap.write(txn);
    endfunction
  endclass

  // An agent groups a monitor, like a uvm_agent inside the env
  class agent;
    monitor mon;
    function void build();     // mirrors build_phase
      mon = new();
    endfunction
  endclass

  // The env: build all children first, THEN connect them
  class env;
    agent      agt;
    scoreboard scb;
    function void build();     // top-down construction
      agt = new();
      scb = new();
      agt.build();
    endfunction
    function void connect();   // wiring, after everything exists
      agt.mon.ap = scb;        // monitor.port.connect(scoreboard)
    endfunction
  endclass

  env e;
  initial begin
    e = new();
    e.build();                 // construct ...
    e.connect();               // ... then connect, like UVM phases
    e.agt.mon.observe(10);
    e.agt.mon.observe(20);
    e.agt.mon.observe(30);
    $display("[TB] scoreboard saw %0d transactions", e.scb.received);
    $finish;
  end
endmodule
[SCB] got txn=10 (total=1)
[SCB] got txn=20 (total=2)
[SCB] got txn=30 (total=3)
[TB] scoreboard saw 3 transactions

Trace the order: if connect() ran before build(), agt.mon would still be null and the wiring would crash. That is exactly why UVM splits construction and connection into separate phases — and why your real env must obey the same discipline.

Common env mistakes

  • Constructing in new(). Build components in build_phase, never in the constructor — config is not available yet in new.
  • Using new() instead of the factory. type_id::create() is what makes overrides possible; a direct new cannot be replaced by a test.
  • Connecting in the wrong phase. Analysis and TLM connections belong in connect_phase, after all handles exist.
  • Reversing the connection. It is always port.connect(export), producer into consumer, not the other way around.

Quick check

Why are agents and the scoreboard created in build_phase rather than in new()?

Because UVM calls build_phase top-down after configuration is available, so a parent can read settings from uvm_config_db and decide what to create before its children are built. The constructor runs too early for that.

What does type_id::create() give you that a plain new() does not?

It builds the component through the UVM factory, so a test can register a type or instance override and swap in a different implementation without editing the env.

In which phase do you connect a monitor’s analysis port to a scoreboard, and in which direction?

In connect_phase, which runs bottom-up after all components exist. The direction is port.connect(export) — the monitor’s uvm_analysis_port connects into the scoreboard’s uvm_analysis_imp.

How do you reuse a block-level env inside a larger SoC env?

Declare the block env as a member of the SoC env and build it with type_id::create() in the SoC env’s build_phase. Each sub-env wires its own internals; the parent only makes connections that cross between its children.

Have questions about this topic?
Share your question in comments or talk to our mentor team for batch guidance.
▶ Try it in the ChipXpert Lab

Compile and simulate this in the browser — no install. Edit the code and re-run instantly.

Open in Verification Lab

Learn Verilog with mentors, labs and placement support

This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.

Explore VLSI Courses
Next batch starts 18th November 2026
Start your VLSI career with ChipXpert

Live online and classroom batches in Hyderabad & Bengaluru. Fill in your details and a counsellor will call you back.

  • Real EDA tools in your browser: industry-standard EDAReal EDA tools in your browser
  • Recorded sessions on the elearn portalRecorded sessions
  • Placement assistance: resume, mock interviewsPlacement assistance
  • Merit scholarship up to 60% · EMI optionsScholarship up to 60%
Hi! Ask me about courses, fees, batches or discounts. ×
BENGALURU
HYDERABAD