Part 1 · Foundations · Intermediate
Layered Testbench Architecture: Test, Env, Agent, DUT
The standard UVM layering pattern — where protocol VIP, scoreboards, virtual sequencers, and the DUT interface belong, and why layering is really about isolating change.
The four-layer model
Almost every UVM testbench, from a tiny block to a full SoC, is organized into four conceptual layers. Each layer has one job and hides the layer below it.
Test — selects the scenario, applies factory overrides, sets plusarg-driven modes, and starts the top sequence. Thin and disposable.
Environment — composes agents, scoreboards, predictors, coverage collectors, and the register model. Reusable infrastructure with minimal scenario logic.
Agent — encapsulates a driver + sequencer + monitor for exactly one interface instance, plus its configuration.
Interface / DUT — pin-level connectivity exposed to the class world through a virtual interface from the static top module.
+-------------------- uvm_test --------------------+
| picks scenario, overrides, starts top sequence |
+--------------------------+-----------------------+
|
+-------------------- uvm_env ---------------------+
| agents + scoreboard + coverage + reg_model |
+------+------------------+-----------------+------+
| | |
uvm_agent scoreboard reg_model (RAL)
+----+-----+
| | |
driver sqr monitor
| |
+--+----------+--+
| virtual iface |
+-------+--------+
|
+--+--+
| DUT |
+-----+The agent: the reusable unit of protocol knowledge
The agent is the heart of reuse. It bundles everything needed to talk to one interface and exposes an is_active knob so the same class can either drive traffic or just observe.
class apb_agent extends uvm_agent;
`uvm_component_utils(apb_agent)
apb_driver drv;
apb_sequencer sqr;
apb_monitor mon;
apb_config cfg;
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#(apb_config)::get(this, "", "cfg", cfg))
`uvm_fatal("NOCFG", "apb_config not set")
mon = apb_monitor::type_id::create("mon", this); // always built
if (cfg.is_active == UVM_ACTIVE) begin // only when active
drv = apb_driver ::type_id::create("drv", this);
sqr = apb_sequencer::type_id::create("sqr", this);
end
endfunction
endclassACTIVE agent: builds driver + sequencer + monitor and drives the bus.
PASSIVE agent: builds only the monitor — used to observe an interface already driven elsewhere.
The same agent class is reused in block-level (active) and SoC-level (passive) testbenches.
Env and the virtual sequencer
The environment owns the reusable infrastructure and, when there are multiple agents, a virtual sequencer that holds handles to each agent's sequencer. Virtual sequences then coordinate cross-interface scenarios without any agent knowing about the others.
class soc_env extends uvm_env;
`uvm_component_utils(soc_env)
apb_agent apb_agt;
axi_agent axi_agt;
soc_scoreboard scb;
soc_vsequencer v_sqr;
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
apb_agt.mon.ap.connect(scb.apb_imp);
axi_agt.mon.ap.connect(scb.axi_imp);
v_sqr.apb_sqr = apb_agt.sqr; // virtual sequencer maps real sequencers
v_sqr.axi_sqr = axi_agt.sqr;
endfunction
endclassWhy this layering pays off
Tests are thin orchestrators; the env holds everything reusable.
Multiple agents in one env = a multi-interface block or subsystem.
The virtual sequencer coordinates across agents without breaking agent encapsulation.
Key takeaways
Layering exists for change isolation: swap tests without rewriting agents, reuse agents across envs.
Keep protocol knowledge inside agents and integration knowledge inside the env.
is_active lets one agent serve both active driving and passive monitoring roles.
Common pitfalls
A monolithic env that hard-codes one test's sequence flow instead of staying generic.
A scoreboard reaching into driver internals instead of subscribing to the monitor's analysis port.
Putting scenario logic in the env, so every new test forces an env edit.