UVM verification components (UVCs) package a complete protocol agent - driver, monitor, sequencer, and supporting infrastructure - into a self-contained unit that can be dropped into any testbench. Once you have a set of UVCs for your DUT's interfaces, the task shifts from building components to integrating and configuring them correctly. This guide walks through each step of that process: reviewing a UVC's interface contract, wiring it into an environment, driving it with sequences, and adding project-specific checking and coverage on top.
10 min read | Intermediate Level
What You'll Learn
- How to review and configure a third-party or reused UVC before integration
- How to instantiate and connect UVCs correctly inside a
uvm_env - How to write and organize sequences that drive UVCs from a test
- How to add a virtual sequencer, scoreboard, and coverage extensions to a UVC-based testbench
UVCs in a Typical SoC Verification Flow
A modern SoC verification environment typically integrates multiple UVCs - one per bus interface on the DUT. An SoC with AXI master and slave ports, a UART interface, and an interrupt controller might bring in three or four separate UVCs, each developed independently. The environment's job is to configure and connect them into a coherent testbench without modifying any UVC internals.
Step 1: Review the UVC's Interface Contract
Before writing a single line of integration code, understand what the UVC expects from its host environment. A well-documented UVC (see the companion guide on Developing Reusable Verification Components) will provide this information explicitly. If it doesn't, read the source and identify these four things:
- Configuration object type and fields - what class does the UVC expect from
uvm_config_db, and what does each field control? - Virtual interface type and name - what interface must the testbench top pass in, and under what path string?
- TLM analysis ports - what ports does the agent expose for scoreboards and coverage collectors to connect to?
- Available sequences - what sequences does the UVC ship with, and which should you extend vs use directly?
Most UVCs expose a single configuration object and a single analysis port at the agent level. If a UVC requires you to reach into its internal hierarchy to access its monitor or driver directly, it was not built for reuse - treat that as a red flag and push back on the UVC provider.
Example: Reading a UVC Configuration Object
Suppose you are integrating an AXI UVC. Its configuration class might look like this:
// Provided by the UVC - do not modify
class axi_agent_cfg extends uvm_object;
`uvm_object_utils(axi_agent_cfg)
virtual axi_if vif; // Must be connected by testbench top
uvm_active_passive_enum is_active = UVM_ACTIVE;
int unsigned max_outstanding = 8; // Protocol parameter
bit en_coverage = 1; // Toggle coverage on/off
bit en_protocol_checks = 1; // Toggle protocol assertions
// Rest of the code (constructor, field macros)
endclass
This tells you exactly what you need to supply: a virtual interface handle, an active/passive mode decision, and toggles for coverage and protocol checking. Everything else defaults safely.
Step 2: Always Instantiate UVCs Inside a uvm_env
UVCs must be instantiated inside a uvm_env container, not inside tests. This is one of the most important structural decisions in a UVM testbench.
If you instantiate agents directly inside uvm_test, every test must know how to build and configure the environment topology. Changing the topology - adding an agent, swapping a UVC version - requires editing every test file. With a uvm_env as the container:
- Tests interact only with sequences and configuration objects - they never touch agents directly
- Topology changes are isolated to the environment class
- Multiple tests share the same environment class without modification
- The environment can itself be reused as a sub-environment in a larger system-level testbench
Never instantiate uvm_agent subclasses directly inside uvm_test. A test that owns agents cannot be reused, and an environment that doesn't exist cannot be shared.
Building the Environment
The environment's build_phase creates all UVC instances and sets their configuration objects before any component's build_phase runs lower in the hierarchy.
class chip_env extends uvm_env;
`uvm_component_utils(chip_env)
axi_agent axi_mst; // AXI master UVC (active)
axi_agent axi_slv; // AXI slave UVC (passive - observe only)
uart_agent uart; // UART UVC
chip_scoreboard scb; // Project-specific scoreboard
function void build_phase(uvm_phase phase);
axi_agent_cfg mst_cfg, slv_cfg;
uart_agent_cfg uart_cfg;
super.build_phase(phase);
// Create configuration objects via factory
mst_cfg = axi_agent_cfg::type_id::create("mst_cfg");
slv_cfg = axi_agent_cfg::type_id::create("slv_cfg");
uart_cfg = uart_agent_cfg::type_id::create("uart_cfg");
// Slave side only observes - no driver or sequencer needed
slv_cfg.is_active = UVM_PASSIVE;
// Retrieve virtual interfaces set by the testbench top module
if (!uvm_config_db #(virtual axi_if)::get(this, "", "axi_mst_if", mst_cfg.vif))
`uvm_fatal("NO_VIF", "AXI master virtual interface not found")
if (!uvm_config_db #(virtual axi_if)::get(this, "", "axi_slv_if", slv_cfg.vif))
`uvm_fatal("NO_VIF", "AXI slave virtual interface not found")
if (!uvm_config_db #(virtual uart_if)::get(this, "", "uart_if", uart_cfg.vif))
`uvm_fatal("NO_VIF", "UART virtual interface not found")
// Push each config object to its agent before build_phase propagates
uvm_config_db #(axi_agent_cfg)::set(this, "axi_mst", "cfg", mst_cfg);
uvm_config_db #(axi_agent_cfg)::set(this, "axi_slv", "cfg", slv_cfg);
uvm_config_db #(uart_agent_cfg)::set(this, "uart", "cfg", uart_cfg);
// Instantiate UVCs and scoreboard via factory
axi_mst = axi_agent::type_id::create("axi_mst", this);
axi_slv = axi_agent::type_id::create("axi_slv", this);
uart = uart_agent::type_id::create("uart", this);
scb = chip_scoreboard::type_id::create("scb", this);
endfunction
// Rest of the code (connect_phase)
endclass
The environment retrieves virtual interfaces from uvm_config_db and places them into configuration objects before pushing those objects down to the agents. This keeps the testbench top's interface wiring isolated from UVC internals.
Step 3: Connect TLM Ports in connect_phase
After all components are built, connect_phase wires each UVC's analysis ports to the scoreboard and any coverage subscribers. The environment owns all connections - neither the UVC nor the scoreboard should hold a direct reference to the other.
function void connect_phase(uvm_phase phase);
// AXI master transactions feed the scoreboard's stimulus input
axi_mst.ap.connect(scb.axi_mst_export);
// AXI slave transactions feed the scoreboard's response input
axi_slv.ap.connect(scb.axi_slv_export);
// UART transactions feed the scoreboard's UART input
uart.ap.connect(scb.uart_export);
// Wire real sequencers into the virtual sequencer
vseqr.axi_seqr = axi_mst.seqr;
vseqr.uart_seqr = uart.seqr;
endfunction
If the UVC ships with its own coverage subscriber, connect it the same way:
// Connect UVC's built-in coverage collector to the master's analysis port
axi_mst.ap.connect(axi_mst_cov.analysis_export);
Quick Check
Q: The scoreboard needs transactions from both the AXI master and AXI slave agents. Which phase makes these connections, and which component owns them?
Show Answer
connect_phase, owned by the uvm_env. The environment has handles to both agents and the scoreboard, making it the correct place to wire them together. Neither the agent nor the scoreboard should hold a direct reference to the other.
Step 4: Configure UVCs from the Test
Tests should not rebuild or rewire the environment. Instead, a test customizes behavior by modifying configuration objects before build_phase runs, or by selecting which sequences to run in run_phase.
Using an Environment-Level Configuration Object
For a well-structured testbench, wrap all agent configuration objects inside a single top-level environment configuration class. The test creates and populates this object, then passes it to the environment via uvm_config_db before the environment builds.
class chip_env_cfg extends uvm_object;
`uvm_object_utils(chip_env_cfg)
axi_agent_cfg axi_mst_cfg;
axi_agent_cfg axi_slv_cfg;
uart_agent_cfg uart_cfg;
function new(string name = "chip_env_cfg");
super.new(name);
// Create sub-configs with sensible defaults
axi_mst_cfg = axi_agent_cfg::type_id::create("axi_mst_cfg");
axi_slv_cfg = axi_agent_cfg::type_id::create("axi_slv_cfg");
uart_cfg = uart_agent_cfg::type_id::create("uart_cfg");
axi_slv_cfg.is_active = UVM_PASSIVE; // Slave always passive
endfunction
endclass
chip_env's build_phase retrieves this object with a matching get() call, before it builds any of the sub-configs shown in Step 2 - if the test supplied one, its values take priority over the environment's own defaults:
function void chip_env::build_phase(uvm_phase phase);
chip_env_cfg env_cfg;
super.build_phase(phase);
if (!uvm_config_db #(chip_env_cfg)::get(this, "", "cfg", env_cfg))
env_cfg = chip_env_cfg::type_id::create("env_cfg"); // No override from the test - use defaults
mst_cfg = env_cfg.axi_mst_cfg;
slv_cfg = env_cfg.axi_slv_cfg;
uart_cfg = env_cfg.uart_cfg;
// ... continue with vif retrieval and uvm_config_db::set() calls as shown in Step 2
endfunction
A test that needs to disable coverage for a fast regression simply modifies the config object - no environment changes required:
class fast_regression_test extends chip_base_test;
`uvm_component_utils(fast_regression_test)
function void build_phase(uvm_phase phase);
chip_env_cfg cfg = chip_env_cfg::type_id::create("cfg");
cfg.axi_mst_cfg.en_coverage = 0; // Skip coverage for speed
cfg.uart_cfg.en_coverage = 0;
uvm_config_db #(chip_env_cfg)::set(this, "env", "cfg", cfg);
super.build_phase(phase);
endfunction
endclass
Create a chip_base_test that builds the default configuration and starts a default idle sequence. All project tests extend this base and override only what they need, preventing every test from duplicating environment setup boilerplate.
Configuration-Driven Testbench Flexibility
In regression suites for complex SoCs, the same environment class is reused by hundreds of tests. Fast smoke tests disable coverage and protocol checking for speed; nightly regression tests enable everything. A single configuration object field controls this - no environment source changes are needed between runs.
Step 5: Drive UVCs with Sequences
Tests drive the DUT by starting sequences on the UVC's sequencer - never by calling driver methods or accessing the virtual interface directly. The sequencer handle is accessed through the environment and agent hierarchy.
Starting a Sequence from a Test
class axi_write_test extends chip_base_test;
`uvm_component_utils(axi_write_test)
virtual task run_phase(uvm_phase phase);
axi_write_seq seq;
phase.raise_objection(this);
seq = axi_write_seq::type_id::create("seq");
// Constrain externally - sequence class is not modified
assert(seq.randomize() with {
base_addr == 32'h8000_0000;
num_transactions == 16;
});
seq.start(env.axi_mst.seqr);
phase.drop_objection(this);
endtask
endclass
Coordinating Multiple UVCs with a Virtual Sequencer
When a test must coordinate stimulus across multiple interfaces - sending an AXI write then triggering a UART response - use a virtual sequencer. It holds handles to each UVC's real sequencer and lets a virtual sequence orchestrate them from one place. See UVM Virtual Sequencer for full details.
// Virtual sequencer - holds references to real sequencers
class chip_vseqr extends uvm_sequencer;
`uvm_component_utils(chip_vseqr)
axi_sequencer axi_seqr;
uart_sequencer uart_seqr;
// Rest of the code (constructor)
endclass
A virtual sequence then drives both interfaces in coordination:
class chip_init_vseq extends uvm_sequence;
`uvm_object_utils(chip_init_vseq)
`uvm_declare_p_sequencer(chip_vseqr)
virtual task body();
axi_init_seq axi_seq;
uart_idle_seq uart_seq;
// Initialize AXI first, then bring UART out of reset
`uvm_do_on(axi_seq, p_sequencer.axi_seqr)
`uvm_do_on(uart_seq, p_sequencer.uart_seqr)
endtask
endclass
Quick Check
Q: A test needs to send an AXI burst and simultaneously monitor the UART output for a response. Why is a virtual sequence the right tool for this, rather than two separate fork/join blocks in the test's run_phase?
Show Answer
A virtual sequence encapsulates multi-interface coordination in a reusable, factory-overridable object. It can be called from any test, composed with other virtual sequences, and overridden via the factory. A fork/join block embedded in a test cannot be reused, composed, or overridden without copying and editing the test itself.
Step 6: Add Project-Specific Checking
UVCs include protocol-level checking (valid burst lengths, correct handshake timing) built into the monitor. What they cannot contain is project-specific functional correctness checking - that depends on your DUT's specification and belongs in a scoreboard you write. See UVM Scoreboard for implementation details.
`uvm_analysis_imp_decl(_axi_mst)
`uvm_analysis_imp_decl(_axi_slv)
class chip_scoreboard extends uvm_scoreboard;
`uvm_component_utils(chip_scoreboard)
uvm_analysis_imp_axi_mst #(axi_transaction, chip_scoreboard) axi_mst_export;
uvm_analysis_imp_axi_slv #(axi_transaction, chip_scoreboard) axi_slv_export;
axi_transaction expected_q[$]; // Reference model queue
// Stimulus side: compute and enqueue expected response
function void write_axi_mst(axi_transaction tx);
axi_transaction exp = predict_response(tx);
expected_q.push_back(exp);
endfunction
// Response side: compare against expected
function void write_axi_slv(axi_transaction tx);
axi_transaction exp;
if (expected_q.size() == 0) begin
`uvm_error("SCB", "Unexpected transaction - expected queue is empty")
return;
end
exp = expected_q.pop_front();
if (!tx.compare(exp))
`uvm_error("SCB", $sformatf("Mismatch: got %0h, expected %0h", tx.data, exp.data))
else
`uvm_info("SCB", "Transaction matched", UVM_HIGH)
endfunction
// Rest of the code (predict_response, build_phase, constructor)
endclass
Do not implement functional correctness checking inside the UVC's monitor. The monitor is protocol-specific and reusable across projects; the scoreboard is project-specific. Mixing them prevents the monitor from being reused unchanged in the next project.
Step 7: Add Functional Coverage Extensions
UVCs may ship with protocol coverage (e.g., all burst lengths exercised). Project-specific coverage - which DUT features were exercised, which state machine transitions were triggered - must be added by you as a separate uvm_subscriber.
class chip_axi_coverage extends uvm_subscriber #(axi_transaction);
`uvm_component_utils(chip_axi_coverage)
axi_transaction tx;
// Project-specific: which memory regions and operations were covered
covergroup mem_region_cg;
cp_region: coverpoint tx.addr[31:28] {
bins boot_rom = {4'h0};
bins sram = {4'h2};
bins periph = {4'hA};
bins illegal = default;
}
cp_op: coverpoint tx.wr_rd { bins wr = {1}; bins rd = {0}; }
cp_cross: cross cp_region, cp_op;
endgroup
function new(string name, uvm_component parent);
super.new(name, parent);
mem_region_cg = new();
endfunction
function void write(axi_transaction t);
tx = t;
mem_region_cg.sample();
endfunction
endclass
Wire it into the environment's connect_phase alongside the scoreboard:
axi_mst.ap.connect(axi_cov.analysis_export);
Keep UVC-level coverage (protocol compliance) inside the UVC and project-level coverage (DUT feature exercise) in your own subscriber. This separation ensures the UVC's coverage closure data remains valid across projects, and your project coverage can evolve independently.
Step 8: Structure Tests Around Scenarios, Not Signals
A well-integrated UVC-based testbench means tests are written entirely in terms of transactions and sequences - never in terms of signal toggling or clock cycles. If a test contains references to vif.addr or @(posedge clk), the UVC abstraction has been bypassed.
Organize tests in four layers:
- Directed tests - specific sequence constraints targeting a defined corner case
- Constrained-random tests - randomized sequences with broad constraints, relying on coverage-driven closure
- Stress tests - back-to-back maximum-burst sequences with multiple agents active simultaneously
- Error injection tests - UVC callbacks that inject protocol errors to verify DUT error handling
// Constrained-random test - no signal-level access anywhere
class axi_random_test extends chip_base_test;
`uvm_component_utils(axi_random_test)
virtual task run_phase(uvm_phase phase);
axi_write_seq wr_seq;
axi_read_seq rd_seq;
phase.raise_objection(this);
repeat(100) begin
// Write: random burst size 1-8
`uvm_do_on_with(wr_seq, env.vseqr.axi_seqr,
{ wr_seq.num_transactions inside {[1:8]}; })
// Read back from same address
`uvm_do_on_with(rd_seq, env.vseqr.axi_seqr,
{ rd_seq.base_addr == wr_seq.base_addr; })
end
phase.drop_objection(this);
endtask
endclass
Test Layering in Production Regression Suites
In high-volume regression suites, directed tests handle known-difficult corner cases - reset during burst, maximum outstanding transactions, address boundary crossings - while constrained-random tests, often run with thousands of different seeds, handle broad coverage closure. The UVC's sequence library provides the building blocks; each test just selects and constrains them.
Common Mistakes
Mistake #1: Instantiating agents directly inside uvm_test.
Tests that own topology cannot be shared across projects or testbench configurations. Every topology change requires editing every test. Always put agents inside uvm_env.
Mistake #2: Setting virtual interface handles with absolute hierarchical paths in uvm_config_db.
Wrong: uvm_config_db::set(null, "uvm_test_top.env.axi_mst", "vif", axi_if_inst);
Right: Set the interface into a configuration object, pass the config to the environment, and let the environment distribute it to each agent using a relative path.
Absolute paths break when the environment hierarchy is renamed or nested inside a larger environment.
Mistake #3: Starting sequences directly on an agent's internal sequencer from the test.
Tests that directly reference env.axi_mst.seqr are coupled to the agent's internal structure. If the agent is renamed or the environment topology changes, every such test must be updated. Go through the virtual sequencer instead.
Mistake #4: Putting project-specific correctness checking inside the monitor's run_phase.
Monitors must be passive observers that broadcast transactions. Protocol-rule checks belong in the monitor, but data-correctness checking is project-specific and breaks the UVC's portability to the next project. Use a separate scoreboard.
Summary
In this guide, you learned:
- Always read a UVC's configuration object, virtual interface requirements, and TLM ports before writing any integration code
- Instantiate all UVCs inside a
uvm_env- never directly inside a test - to keep topology changes isolated and tests reusable - Retrieve virtual interfaces at the environment level and pass them into UVC configuration objects via
uvm_config_db - Wire analysis ports to scoreboards and coverage subscribers in
connect_phase, owned by the environment - Drive UVCs exclusively through sequences and a virtual sequencer - never through direct signal access
- Project-specific checking belongs in a scoreboard; project-specific coverage belongs in a separate subscriber - neither belongs inside the UVC
- Organize tests by scenario type (directed, constrained-random, stress, error injection) using sequences as the building blocks