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
endclassThree 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
endclassNotice 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.
| Phase | Call order | What belongs here |
|---|---|---|
build_phase | Top-down (parent first) | Call create() for every sub-component; read settings from uvm_config_db. |
connect_phase | Bottom-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
endclassThe 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
endclassKeep 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 innew. - Using new() instead of the factory.
type_id::create()is what makes overrides possible; a directnewcannot 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.
Share your question in comments or talk to our mentor team for batch guidance.
Compile and simulate this in the browser — no install. Edit the code and re-run instantly.
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