UVM-02: Testbench Architecture — VLSI Trainers
VLSI Trainers UVM Series · UVM-02
UVM Series · UVM-02

Testbench Architecture

How a UVM testbench hierarchy is structured, the roles of test / env / agent, top-down build and bottom-up connect, deferred construction, the UVM phase system, naming and path conventions, and a complete block-level testbench skeleton.

📋 The Component Hierarchy

A UVM testbench is a tree of uvm_component objects. Every node in the tree except the root has a parent. The tree is built top-down during the build phase — each component creates its children, which in turn create their children, until the leaf nodes (drivers, monitors) are reached.

The standard hierarchy for a single-interface block-level testbench has five levels:

UVM Component Hierarchy — Five Levels L1 L2 L3 L4 L5 my_test (uvm_test) uvm_test_top — root of hierarchy my_env (uvm_env) uvm_test_top.env — contains agents + analysis components my_agent (uvm_agent) uvm_test_top.env.agent scoreboard uvm_test_top.env.sb cov_collector uvm_test_top.env.cov sequencer .agent.seqr driver .agent.drv monitor uvm_object (not components) sequence_item, sequences, config objects These are NOT part of the component hierarchy They are created and destroyed dynamically during run_phase vlsitrainers.com
Figure 1 — Five-level UVM component hierarchy for a single-interface block-level testbench. Each box shows the component name, base class, and full dot-path name. The component hierarchy is built once during build_phase and persists for the entire simulation. uvm_objects (sequence items, sequences, config) are not part of this tree.

📋 Has-A Relationships

The UVM hierarchy is built using has-a (composition) relationships, not is-a (inheritance) relationships. A my_env “has a” my_agent and “has a” scoreboard. The agent “has a” driver, “has a” monitor, “has a” sequencer.

Every component is declared as a member handle inside its parent, then constructed in the parent’s build_phase using the factory create() method. The parent passes itself as the parent argument, establishing the tree link:

class my_env extends uvm_env;
  `uvm_component_utils(my_env)

  my_agent      m_agent;       // handle — null until build_phase
  my_scoreboard m_sb;
  my_coverage   m_cov;

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

  function void build_phase(uvm_phase phase);
    // ① Factory creates child — passes "this" as parent
    m_agent = my_agent::type_id::create("agent", this);
    m_sb    = my_scoreboard::type_id::create("sb", this);
    m_cov   = my_coverage::type_id::create("cov", this);
  endfunction
endclass
Always use the factory type_id::create() — never call new() directly. Calling new() bypasses the factory entirely, which means factory overrides will not work. Every UVM component and object must be created through the factory so test-time substitution is possible.

📋 Naming and Path Conventions

Every uvm_component has a unique dot-separated path name that reflects its position in the hierarchy. The root test is always named uvm_test_top by the UVM infrastructure. Below it, the path is built from the name strings passed to create().

Path Names — From create() Arguments to Full Dot-Path create() call Full path name Used in config_db / uvm_info ID run_test(“my_test”) uvm_test_top Fixed name — always uvm_test_top my_env::type_id::create(“env”, this) uvm_test_top.env uvm_config_db #(T)::set(this, “env.*”, …) my_agent::type_id::create(“agent”, this) uvm_test_top.env.agent uvm_config_db #(T)::get(this, “”, “cfg”, …) my_driver::type_id::create(“drv”, this) uvm_test_top.env.agent.drv `uvm_info(“DRV”, …) prints path automatically vlsitrainers.com
Figure 2 — How create() name arguments build the full dot-path name. The string passed as the first argument to create() becomes the last segment of the path. This path is used by config_db lookups, uvm_info messages, and phase callbacks.

Path names are used everywhere in UVM. When you call uvm_config_db::set(), you specify a path pattern to determine which components receive the value. When `uvm_info prints a message, the component’s path is included automatically. When a phase error occurs, the path tells you exactly which component caused the problem.

Choose component names that match the path you want in config_db lookups. If you name your agent "apb_agent", then config_db patterns like "*.apb_agent.*" will match its children. Generic names like "agent" work but can cause ambiguity in multi-agent environments. Consistent naming is critical for config_db to work correctly.

📋 UVM Phase System

UVM controls the lifecycle of every testbench through a phase system. Phases execute in a defined order, ensuring that components are fully built before they are connected, fully connected before they run, and able to report results after running. There are 12 standard phases organised into three groups.

build_phase
top-down
connect_phase
bottom-up
end_of_elab
bottom-up
start_of_sim
bottom-up
run_phase
concurrent
extract_phase
bottom-up
check_phase
bottom-up
report_phase
bottom-up
final_phase
top-down
PhaseTypeDirectionWhat happens
build_phaseFunctionTop-downComponents create child components. config_db values are retrieved. Factory overrides applied.
connect_phaseFunctionBottom-upTLM ports connected between components. Analysis ports connected to subscribers. Virtual interfaces assigned from config_db.
end_of_elaboration_phaseFunctionBottom-upHierarchy is final. Print topology, check configuration. No construction or connections allowed.
start_of_simulation_phaseFunctionBottom-upLast setup before time starts. Print configuration, seed info.
run_phaseTaskConcurrentMain simulation. All components run simultaneously. Ends when all objections dropped.
extract_phaseFunctionBottom-upExtract results from simulation — read final register values, collect statistics.
check_phaseFunctionBottom-upCheck for errors — unmatched transactions, protocol violations, coverage holes.
report_phaseFunctionBottom-upPrint results — pass/fail, coverage summary, statistics.
final_phaseFunctionTop-downFinal cleanup before simulation exits.
Function phases vs task phases. Every phase except run_phase is a function — it executes in zero simulation time. run_phase is a task — it can consume simulation time and run concurrently in all components. This is why you can only call #delay or @(event) inside run_phase, never inside build_phase or connect_phase.

📋 Build Phase — Top-Down Construction

The build phase runs top-down: the test’s build_phase runs first, then env’s, then agent’s, then driver/monitor/sequencer. This order is mandatory and enforced by UVM. You cannot change it.

Top-down ordering exists for a specific reason: the parent needs to configure the child before the child builds. The test creates and configures config objects, places them in config_db, then creates the env. When the env’s build_phase runs, it retrieves the config from config_db (already set by the test), uses it to decide what to build, and creates agents. Each agent then retrieves its own config and builds its sub-components.

Build Phase Execution Order — Top-Down ① test build_phase runs first ② env build_phase gets cfg, builds ③ agent build_phase gets cfg, builds ④ driver / monitor / seqr build_phase (leaf nodes) runs last What the test’s build_phase does — sets everything up for levels below ① Apply factory overrides (if any) — must be before creating child objects ② Create agent config objects, set virtual interface handles in them ③ Place config objects into config_db so env/agent can retrieve them ④ Create the top-level env (this triggers env’s build_phase, which triggers agent’s, and so on) vlsitrainers.com
Figure 3 — Build phase execution order. The test builds first, placing configuration into config_db before creating the env. When the env’s build_phase runs, the configuration is already available. This deferred construction pattern is why config_db works — values are always set before they are retrieved.
class base_test extends uvm_test;
  `uvm_component_utils(base_test)

  my_env        m_env;
  my_env_config m_cfg;

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

  function void build_phase(uvm_phase phase);
    // ① Create config, assign virtual interface from SV top
    m_cfg = my_env_config::type_id::create("m_cfg");
    if (!uvm_config_db #(virtual dut_if)::get(
          this, "", "dut_vif", m_cfg.vif))
      `uvm_fatal("CFG", "No virtual interface!")

    // ② Place config so env can retrieve it
    uvm_config_db #(my_env_config)::set(
      this, "m_env", "cfg", m_cfg);

    // ③ Build env — triggers env's build_phase
    m_env = my_env::type_id::create("m_env", this);
  endfunction

  task run_phase(uvm_phase phase);
    my_sequence seq;
    phase.raise_objection(this);
    seq = my_sequence::type_id::create("seq");
    seq.start(m_env.m_agent.m_seqr);  // start on sequencer
    phase.drop_objection(this);
  endtask
endclass

📋 Connect Phase — Bottom-Up Wiring

The connect phase runs bottom-up — the opposite direction to build. It runs after all build_phases have completed, meaning the entire component tree is fully constructed. Only then can connections between components be made safely (you cannot connect to a component that hasn’t been built yet).

Three types of connections happen in connect_phase:

class my_env extends uvm_env;
  `uvm_component_utils(my_env)

  my_agent      m_agent;
  my_scoreboard m_sb;
  my_coverage   m_cov;

  function void connect_phase(uvm_phase phase);
    // Connect monitor's analysis port → scoreboard + coverage
    m_agent.m_monitor.ap.connect(m_sb.analysis_export);
    m_agent.m_monitor.ap.connect(m_cov.analysis_export);
  endfunction
endclass
Connect phase runs bottom-up so children can expose their ports to parents. The driver is at the bottom of the hierarchy — its seq_item_port is connected to the sequencer’s seq_item_export inside the agent’s connect_phase. Only after the agent finishes connecting does the env’s connect_phase run — by then the agent’s internal connections are complete and the env can safely connect the monitor’s analysis port.

📋 Run Phase and Objections

The run_phase is a task — the only phase that consumes simulation time. It executes concurrently in all components that implement it. The driver’s run_phase, monitor’s run_phase, scoreboard’s run_phase all run simultaneously using SystemVerilog’s automatic fork/join semantics.

Simulation time in run_phase ends when all phase objections are dropped. An objection is a counter — as long as the count is non-zero, the phase continues. This allows any component to extend the simulation by raising an objection, and signal completion by dropping it.

Objection Lifecycle — How run_phase Duration is Controlled raise_objection test starts raise_objection seq 2 starts drop_objection seq 2 done drop_objection test sequence done count=1 count=2 count=1 run_phase ends (count=0 + drain) vlsitrainers.com
Figure 4 — Objection lifecycle. The test raises an objection when it starts and drops it when done. Any other component can independently raise/drop objections. run_phase ends only when the total count reaches zero. A drain time can be set to allow in-flight transactions to complete before the phase actually ends.

📋 Inside the Agent

The agent is the most important structural unit in UVM. It encapsulates all the components needed to drive and observe one protocol interface. A well-designed agent can be plugged into any environment that uses the same interface.

An agent has two modes controlled by the is_active field (inherited from uvm_agent):

class my_agent extends uvm_agent;
  `uvm_component_utils(my_agent)

  my_driver    m_drv;
  my_sequencer m_seqr;
  my_monitor   m_mon;
  my_agent_cfg m_cfg;

  function void build_phase(uvm_phase phase);
    // Retrieve config from config_db
    if (!uvm_config_db #(my_agent_cfg)::get(
          this, "", "cfg", m_cfg))
      `uvm_fatal("CFG", "No agent config!")

    // Monitor always built — active or passive
    m_mon = my_monitor::type_id::create("m_mon", this);

    // Sequencer + driver only in active mode
    if (m_cfg.is_active == UVM_ACTIVE) begin
      m_seqr = my_sequencer::type_id::create("m_seqr", this);
      m_drv  = my_driver::type_id::create("m_drv", this);
    end
  endfunction

  function void connect_phase(uvm_phase phase);
    // Connect driver seq_item_port to sequencer
    if (m_cfg.is_active == UVM_ACTIVE)
      m_drv.seq_item_port.connect(m_seqr.seq_item_export);
  endfunction
endclass
The monitor is always built — even in passive mode. This is intentional. An environment that switches an agent from active to passive (e.g. for integration-level reuse) still needs the monitor for observation. Only the driver and sequencer are conditionally built based on is_active.

📋 Block-Level Testbench Skeleton

The complete file structure for a typical block-level UVM testbench. Each file contains one class. All files are compiled as a package.

FileContainsExtends
my_seq_item.svTransaction class with rand fieldsuvm_sequence_item
my_sequence.svBase sequence, body() generates itemsuvm_sequence#(my_seq_item)
my_driver.svrun_phase drives DUT via virtual interfaceuvm_driver#(my_seq_item)
my_monitor.svrun_phase observes DUT, publishes analysis_portuvm_monitor
my_sequencer.svTypedef — usually just a type aliasuvm_sequencer#(my_seq_item)
my_agent_cfg.svConfig: vif handle, is_active flaguvm_object
my_agent.svbuild + connect: seqr/drv/mon, conditional on is_activeuvm_agent
my_env_cfg.svTop-level config: agent config, enable flagsuvm_object
my_scoreboard.svanalysis_imp, write(), compare logicuvm_scoreboard
my_env.svbuild: creates agent + sb + cov; connect: ap wiringuvm_env
base_test.svbuild: config + env; run: start root sequenceuvm_test
my_pkg.svPackage: `include all the above in order
tb_top.svDUT + interface + clock; initial run_test()module (static)

📋 Quick Reference

ItemKey fact
Hierarchy rootuvm_test_top — always this name, created by run_test()
Has-a relationshipParent declares child handle, creates it via factory in build_phase
Factory creationChildClass::type_id::create("name", this) — never use new()
Path name formatuvm_test_top.env.agent.driver — built from create() name args
build_phase directionTop-down — test first, leaf components last
connect_phase directionBottom-up — leaf components first, test last
run_phase typeTask — only phase that consumes simulation time
run_phase concurrencyRuns simultaneously in all components that implement it
run_phase end conditionAll phase objections dropped (count = 0)
raise/drop objectionphase.raise_objection(this) / phase.drop_objection(this)
Active agentContains sequencer + driver + monitor. is_active == UVM_ACTIVE
Passive agentContains monitor only. is_active == UVM_PASSIVE
Monitor built whenAlways — both active and passive agents
config_db set locationTest’s build_phase — before creating the env that will retrieve it
connect_phase typical workanalysis_port.connect(), driver seq_item_port.connect()
Scroll to Top