Module 1: Foundations and Architecture
UVM Architecture Overview
Learning objectives
- Explain the core mental model behind UVM Architecture Overview
- Apply UVM Architecture Overview within Foundations and Architecture
- Identify important boundaries, trade-offs, and failure modes
- Produce concrete evidence from the practice exercise
Related: UVM What and Why | TLM Basics | UVM Testbench Structure | UVM Active vs Passive Agents | UVM Reusability Principles
Why a Layered Architecture?
A verification testbench has multiple concerns: generating stimulus, driving signals, observing outputs, checking correctness, collecting coverage. If these are all mixed together (as in traditional testbenches), changing one concern breaks others.
UVM enforces a layered architecture where each concern maps to a dedicated component type. Each layer communicates with adjacent layers through well-defined interfaces (TLM ports), not by reaching into each other's internals.
Analogy: Think of a restaurant. The customer (test) tells the waiter (sequencer) what they want. The waiter passes the order to the chef (driver) who actually makes the food (drives signals). A food critic (monitor) observes what comes out. A health inspector (scoreboard) checks whether the food meets standards. Each person has one job, and they communicate through defined channels — the customer does not walk into the kitchen.
The Component Hierarchy
Every UVM testbench follows this structural pattern:
┌─────────────────────────────────────────────────────────────────┐
│ uvm_test (top) │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ uvm_env │ │
│ │ ┌─────────────────────────────┐ ┌────────────────────┐ │ │
│ │ │ uvm_agent │ │ uvm_scoreboard │ │ │
│ │ │ ┌───────────────────────┐ │ │ │ │ │
│ │ │ │ uvm_sequencer │ │ │ reference model │ │ │
│ │ │ │ (stimulus routing) │ │ │ + comparison │ │ │
│ │ │ └──────────┬────────────┘ │ │ │ │ │
│ │ │ │ │ └────────▲───────────┘ │ │
│ │ │ ┌──────────▼────────────┐ │ │ │ │
│ │ │ │ uvm_driver │ │ ┌────────┴───────────┐ │ │
│ │ │ │ (pin wiggling) │ │ │ uvm_monitor │ │ │
│ │ │ └──────────┬────────────┘ │ │ (signal capture) │ │ │
│ │ │ │ │ └────────┬───────────┘ │ │
│ │ └─────────────┼───────────────┘ │ │ │
│ │ │ │ │ │
│ └────────────────┼───────────────────────────┼──────────────┘ │
│ │ │ │
└───────────────────┼───────────────────────────┼──────────────────┘
│ │
┌─────▼───────────────────────────▼─────┐
│ SystemVerilog Interface │
│ (virtual interface / BFM) │
└─────────────────┬───────────────────────┘
│
┌──────▼──────┐
│ DUT │
└─────────────┘What Each Component Does
uvm_test — The top-level entry point. Each test class configures the environment and launches sequences. You write one test class per test scenario. The test does NOT drive signals — it selects which sequences to run.
uvm_env — A container that holds agents, scoreboards, coverage collectors, and sub-environments. The env defines the testbench topology. For a simple IP, one env suffices. For an SoC, you nest multiple envs.
uvm_agent — Encapsulates all verification logic for one protocol interface. Contains a sequencer, driver, and monitor (in active mode) or just a monitor (in passive mode). The agent is the unit of reuse — you build an AXI agent once and reuse it across every project that has AXI.
uvm_sequencer — Routes sequence items (transactions) from sequences to the driver. Acts as an arbiter when multiple sequences compete. The sequencer does not know or care what the transactions contain — it just manages the handshake.
uvm_driver — Converts abstract transactions into pin-level signal activity. The driver contains protocol-specific timing: setup times, handshake protocols, wait states. It pulls transactions from the sequencer and "drives" them onto the DUT's interface.
uvm_monitor — Observes the DUT's interface signals and reconstructs transactions from what it sees. The monitor is passive — it never drives signals. It broadcasts observed transactions to subscribers (scoreboards, coverage collectors) via analysis ports.
uvm_scoreboard — Receives transactions from monitors, computes expected results (using a reference model), and compares actual vs. expected. Reports mismatches as errors.
The Three Key Phases
UVM simulation proceeds through phases — ordered steps that ensure components are built, connected, and run in the right order. The three most important phases are:
build_phase: Constructing the Tree (Top-Down)
class my_env extends uvm_env;
`uvm_component_utils(my_env)
my_agent m_agent;
my_scoreboard m_sb;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
// build_phase: create child components
function void build_phase(uvm_phase phase);
super.build_phase(phase);
// Factory creates the agent — allows type overrides
m_agent = my_agent::type_id::create("m_agent", this);
m_sb = my_scoreboard::type_id::create("m_sb", this);
`uvm_info(get_type_name(), "Environment built", UVM_MEDIUM)
endfunction
endclassWhy top-down? The test's build_phase runs first, then the env's, then the agent's, then the driver's/monitor's. This matters because a parent can configure its children before they build themselves. For example, the test can set is_active on an agent before the agent's build_phase decides whether to create a driver.
build_phase execution order:
1. uvm_test::build_phase() ← creates env
2. my_env::build_phase() ← creates agent, scoreboard
3. my_agent::build_phase() ← creates driver, monitor, sequencer
4. my_driver::build_phase()
4. my_monitor::build_phase()
4. my_sequencer::build_phase()
3. my_scoreboard::build_phase()connect_phase: Wiring TLM Ports (Bottom-Up)
After all components exist, connect_phase hooks them together using TLM ports:
class my_agent extends uvm_agent;
`uvm_component_utils(my_agent)
my_sequencer m_seqr;
my_driver m_drv;
my_monitor m_mon;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
m_mon = my_monitor::type_id::create("m_mon", this);
if (get_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
// connect_phase: wire the ports
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
if (get_is_active() == UVM_ACTIVE) begin
// Driver pulls transactions from sequencer
m_drv.seq_item_port.connect(m_seqr.seq_item_export);
end
// Monitor's analysis port is connected at the env level
endfunction
endclassWhy bottom-up? Connect phase runs children before parents. This lets parent components connect ports that children have already created. The env connects the monitor's analysis port to the scoreboard — the agent connects driver to sequencer internally.
connect_phase execution order:
4. my_driver::connect_phase()
4. my_monitor::connect_phase()
4. my_sequencer::connect_phase()
3. my_agent::connect_phase() ← connects driver ↔ sequencer
3. my_scoreboard::connect_phase()
2. my_env::connect_phase() ← connects monitor → scoreboard
1. uvm_test::connect_phase()run_phase: The Actual Simulation
run_phase is a task (not a function) — it consumes simulation time. All components' run_phases execute in parallel:
class my_driver extends uvm_driver #(my_transaction);
`uvm_component_utils(my_driver)
virtual my_if vif;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
task run_phase(uvm_phase phase);
my_transaction txn;
forever begin
// Pull next transaction from sequencer
seq_item_port.get_next_item(txn);
// Drive it onto the interface (consumes time)
@(posedge vif.clk);
vif.valid <= 1'b1;
vif.addr <= txn.addr;
vif.data <= txn.data;
@(posedge vif.clk);
while (!vif.ready) @(posedge vif.clk);
vif.valid <= 1'b0;
// Tell sequencer we are done
seq_item_port.item_done();
end
endtask
endclassrun_phase execution: ALL components run simultaneously
Time ──────────────────────────────────────────────────▶
Driver: [get_txn][drive][drive][get_txn][drive]...
Monitor: [sample][sample][sample][sample][sample]...
Sequencer: [arbitrate][route][arbitrate][route]........
Scoreboard: [wait][compare][wait][compare][wait]........
All running as concurrent processes (fork-join_none internally)Complete Phase List
UVM has more phases than just the big three. Here is the full set:
┌──────────────────────────────────┐
│ build_phase (top-down, function, no time) │
│ connect_phase (bottom-up, function, no time) │
│ end_of_elaboration_phase (bottom-up, function) │
│ start_of_simulation_phase (bottom-up, function) │
├──────────────────────────────────┤
│ run_phase (parallel, task, consumes time) │
│ ├── reset_phase │
│ ├── configure_phase │
│ ├── main_phase │
│ └── shutdown_phase │
├──────────────────────────────────┤
│ extract_phase (bottom-up, function) │
│ check_phase (bottom-up, function) │
│ report_phase (bottom-up, function) │
│ final_phase (top-down, function) │
└──────────────────────────────────┘In practice, most testbenches use only build_phase, connect_phase, and run_phase. The sub-phases of run_phase (reset, configure, main, shutdown) are available for more structured test flows but are optional.
The UVM Factory
You may have noticed type_id::create() instead of new(). This is the UVM factory — a design pattern that allows type substitution without modifying existing code.
// Normal SystemVerilog — hardcoded type
my_driver drv = new("drv", this);
// UVM factory — type can be overridden
my_driver drv = my_driver::type_id::create("drv", this);Why this matters: Suppose you have a base AXI driver that works for most tests. One specific test needs a modified driver that injects errors. With the factory:
class my_error_test extends uvm_test;
`uvm_component_utils(my_error_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
// Override: wherever axi_driver is created, use axi_error_driver instead
set_type_override_by_type(
axi_driver::get_type(),
axi_error_driver::get_type()
);
// The env and agent code are UNCHANGED — they still call
// axi_driver::type_id::create(), but the factory returns
// an axi_error_driver instance
endfunction
endclassThe factory is how UVM achieves test-level customization without modifying reusable components.
Putting It All Together: A Minimal Complete Hierarchy
// ── Test ──────────────────────────────────────────────────
class base_test extends uvm_test;
`uvm_component_utils(base_test)
my_env m_env;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(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);
phase.drop_objection(this);
endtask
endclass
// ── Environment ──────────────────────────────────────────
class my_env extends uvm_env;
`uvm_component_utils(my_env)
my_agent m_agent;
my_scoreboard m_sb;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
m_agent = my_agent::type_id::create("m_agent", this);
m_sb = my_scoreboard::type_id::create("m_sb", this);
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
// Monitor broadcasts to scoreboard
m_agent.m_mon.ap.connect(m_sb.analysis_export);
endfunction
endclass
// ── Scoreboard ───────────────────────────────────────────
class my_scoreboard extends uvm_scoreboard;
`uvm_component_utils(my_scoreboard)
uvm_analysis_imp #(my_transaction, my_scoreboard) analysis_export;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
analysis_export = new("analysis_export", this);
endfunction
// Called automatically when monitor broadcasts a transaction
function void write(my_transaction txn);
// Compare txn against expected
`uvm_info(get_type_name(),
$sformatf("Received: %s", txn.convert2string()),
UVM_MEDIUM)
endfunction
endclassPhase Objections: Controlling Simulation End
Notice phase.raise_objection() and phase.drop_objection() in the test. This is how UVM knows when simulation should end.
The run_phase does not end until all objections are dropped. The pattern:
task run_phase(uvm_phase phase);
phase.raise_objection(this); // "I'm not done yet"
// ... do work (run sequences, wait for events) ...
phase.drop_objection(this); // "I'm done — simulation can end"
endtaskWithout objections, run_phase would end immediately (time 0) because no component is holding simulation open. With objections, UVM waits until the last objection is dropped, then proceeds to extract/check/report phases.
Best practice: Raise and drop objections only in the test (or occasionally in sequences). Do not scatter objections across drivers and monitors — it makes simulation termination unpredictable.
Key Takeaways
- UVM testbenches follow a strict hierarchy: test → env → agent → driver/monitor/sequencer.
- build_phase (top-down) creates components; connect_phase (bottom-up) wires TLM ports; run_phase (parallel) runs the actual simulation.
- The factory (
type_id::create) enables test-level customization without modifying reusable code. - Objections control when simulation ends — raise before work, drop when done.
- All components'
run_phasetasks execute in parallel — the driver drives while the monitor samples while the scoreboard checks.
Next: TLM Basics — how components communicate through Transaction-Level Modeling.
Practice lab
Create a minimal executable SystemVerilog or UVM artifact that demonstrates UVM Architecture Overview. Record the expected behavior, inject one deliberate defect, and capture the evidence that identifies the failure.
Review questions
- What problem does UVM Architecture Overview solve, and what assumptions does it rely on?
- Which boundary or failure case is easiest to miss, and how would you expose it?
- What alternative design would you consider, and what trade-off would change the decision?
- What artifact, trace, test, or metric proves that your implementation is correct?
Completion evidence
- A working artifact, annotated trace, or reproducible experiment
- At least one normal case and one deliberately failing or boundary case
- A concise explanation of the design choice and its trade-offs
- Saved output showing how correctness was evaluated