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

endclass

Why 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

endclass

Why 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

endclass
run_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

endclass

The 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

endclass

Phase 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"
endtask

Without 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

  1. UVM testbenches follow a strict hierarchy: test → env → agent → driver/monitor/sequencer.
  2. build_phase (top-down) creates components; connect_phase (bottom-up) wires TLM ports; run_phase (parallel) runs the actual simulation.
  3. The factory (type_id::create) enables test-level customization without modifying reusable code.
  4. Objections control when simulation ends — raise before work, drop when done.
  5. All components' run_phase tasks 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

  1. What problem does UVM Architecture Overview solve, and what assumptions does it rely on?
  2. Which boundary or failure case is easiest to miss, and how would you expose it?
  3. What alternative design would you consider, and what trade-off would change the decision?
  4. 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