9 min read | Intermediate Level

What You'll Learn

  • What makes a UVM verification component truly reusable
  • Guidelines for modeling data items, agents, and sequences for reuse
  • How to use the factory, config database, and callbacks to maximize flexibility
  • Common pitfalls that break reusability and how to avoid them

Most projects reuse verification components developed for a previous project or protocol. For a component to drop in cleanly, it must be designed with reusability in mind from the start. A driver written specifically for one protocol's signal timing cannot be transplanted into a different testbench without modification - unless it was built to be flexible. This guide consolidates the key guidelines for developing UVM verification components that are genuinely portable across projects and teams.

Why Reusability Matters in Industry

In production verification environments, VIPs (Verification IP) for standard protocols like AXI, PCIe, USB, and DDR are developed once and reused across dozens of SoC projects. A poorly structured component forces every downstream project to patch or rewrite it, multiplying verification effort. Well-designed reusable VIPs dramatically reduce bring-up time for new projects.

Guideline 1: Model Data Items Correctly

The transaction object - the data item passed between sequences, drivers, and monitors - is the foundation of a reusable component. Getting its structure right is critical.

Always Derive from uvm_sequence_item

Every user-defined transaction class must extend uvm_sequence_item, directly or indirectly. This gives the transaction access to the sequencer-driver handshake, the factory, and UVM's built-in utilities (print, compare, copy, pack/unpack).


class axi_transaction extends uvm_sequence_item;
  `uvm_object_utils(axi_transaction)

  // Protocol fields as rand variables - enables constrained randomization
  rand bit [31:0] addr;
  rand bit [31:0] data;
  rand bit        wr_rd;    // 1 = write, 0 = read
  rand bit [2:0]  burst_len;

  function new(string name = "axi_transaction");
    super.new(name);
  endfunction

  // Rest of the code (field macros or do_* methods)
endclass

Declare All Protocol Fields as rand

Declare all meaningful protocol fields as rand so that sequences can apply constraints without modifying the transaction class itself. Fields that should never change (like a fixed packet preamble) can be rand with a hard constraint in the class, or simply initialized to a constant in new().

Do not hardcode specific values into the transaction class. Hardcoded fields force every reuser to modify the class, breaking the component's portability.

Implement do_copy, do_compare, and do_print

UVM's uvm_object base class provides virtual do_* methods that the framework calls automatically. Implementing these correctly ensures your transaction works properly with scoreboards, coverage collectors, and debug utilities - including components written by other teams.


function void do_copy(uvm_object rhs);
  axi_transaction rhs_;
  super.do_copy(rhs);
  if (!$cast(rhs_, rhs))
    `uvm_fatal("TYPE_MISMATCH", "do_copy: incorrect type")
  this.addr      = rhs_.addr;
  this.data      = rhs_.data;
  this.wr_rd     = rhs_.wr_rd;
  this.burst_len = rhs_.burst_len;
endfunction

Using field automation macros like `uvm_field_int inside `uvm_object_utils_begin/end auto-generates do_copy, do_compare, do_print, and do_pack. Prefer this for straightforward transactions; write custom do_* methods only when the auto-generated behavior is insufficient.

Keep the Transaction Class Protocol-Agnostic Where Possible

Separate the logical transaction (address, data, operation type) from protocol-specific timing fields (wait states, parity, error injection flags). Use inheritance or composition so a downstream project can extend the base transaction to add protocol details without rewriting the core fields.


// Base transaction - reusable across bus protocols
class bus_transaction extends uvm_sequence_item;
  rand bit [31:0] addr;
  rand bit [31:0] data;
  rand bit        is_write;
  // ...
endclass

// Protocol-specific extension
class axi_transaction extends bus_transaction;
  rand bit [2:0] burst_len;
  rand bit [1:0] burst_type;
  // ...
endclass

Quick Check

Q: Why should transaction fields be declared rand even when a specific test always drives the same value?

Show Answer

Declaring fields rand allows sequences to apply randomize() with {} inline constraints without modifying the class. Tests that need a fixed value can apply a constraint; tests that need variation get it for free. Hardcoding inside the class removes that flexibility for all consumers.

Guideline 2: Structure the Agent for Portability

The agent is the primary packaging unit of a reusable VIP. A well-structured agent can be instantiated in any environment with minimal configuration.

Always Support Active and Passive Modes

The is_active field (inherited from uvm_agent) controls whether the agent drives the DUT or only observes it. A reusable agent must respect this flag - it must not instantiate the driver or sequencer when set to UVM_PASSIVE.


class axi_agent extends uvm_agent;
  `uvm_component_utils(axi_agent)

  axi_driver    drv;
  axi_monitor   mon;
  axi_sequencer seqr;

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    mon = axi_monitor::type_id::create("mon", this);

    // Only build active components when driving the DUT
    if (get_is_active() == UVM_ACTIVE) begin
      drv  = axi_driver::type_id::create("drv", this);
      seqr = axi_sequencer::type_id::create("seqr", this);
    end
  endfunction

  // Rest of the code (connect_phase, etc.)
endclass

A passive agent is useful when a second instance of the VIP is monitoring a bus that another active agent is driving - common in multi-agent environments and full-chip testbenches.

Use a Dedicated Agent Configuration Object

All agent parameters - interface handle, active/passive mode, protocol variant, timing parameters - must live in a configuration object, not be hardcoded. Pass this object through the uvm_config_db so the environment can control the agent without subclassing it.


class axi_agent_cfg extends uvm_object;
  `uvm_object_utils(axi_agent_cfg)

  virtual axi_if  vif;           // Virtual interface handle
  uvm_active_passive_enum is_active = UVM_ACTIVE;
  int unsigned    num_outstanding = 8;  // Protocol parameter
  bit             en_coverage     = 1;  // Toggle coverage collection

  function new(string name = "axi_agent_cfg");
    super.new(name);
  endfunction
endclass

The agent fetches its configuration in build_phase:


function void build_phase(uvm_phase phase);
  axi_agent_cfg cfg;
  super.build_phase(phase);

  if (!uvm_config_db #(axi_agent_cfg)::get(this, "", "cfg", cfg))
    `uvm_fatal("NO_CFG", "axi_agent: configuration object not found")

  // Use cfg.vif, cfg.is_active, cfg.num_outstanding, etc.
endfunction

Never use uvm_config_db to pass individual parameters (one set() call per field). Bundle all agent parameters into a single configuration object. This makes the agent's interface explicit and prevents silent misconfiguration when parameters are added later.

Expose TLM Analysis Ports on the Agent

The monitor's analysis port should be promoted to the agent level. This allows scoreboards and coverage collectors in the environment to connect directly to the agent without reaching into its internals.


class axi_agent extends uvm_agent;
  uvm_analysis_port #(axi_transaction) ap; // Promoted from monitor

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    ap  = new("ap", this);
    mon = axi_monitor::type_id::create("mon", this);
    // ...
  endfunction

  function void connect_phase(uvm_phase phase);
    mon.ap.connect(this.ap); // Forward monitor's port outward
  endfunction
endclass

Quick Check

Q: An environment contains two AXI agents - one active (master) and one passive (slave-side monitor). What field determines which components each agent builds?

Show Answer

The is_active field in the agent configuration object (or set directly via agent.is_active). When UVM_PASSIVE, the agent skips building the driver and sequencer. The monitor is always built in both modes.

Guideline 3: Write Reusable Sequences

Sequences are the primary mechanism for stimulus reuse. A well-written sequence can be used as-is, extended, or composed into virtual sequences without modification.

Parameterize Sequences - Never Hardcode Values

Any value that varies between test scenarios - data payload, address ranges, burst counts - must be a rand field that tests constrain externally, not a constant embedded in the sequence body.


class axi_write_seq extends uvm_sequence #(axi_transaction);
  `uvm_object_utils(axi_write_seq)

  rand bit [31:0] base_addr;
  rand int unsigned num_transactions;

  constraint default_c {
    num_transactions inside {[1:16]};
    base_addr[1:0] == 2'b00; // Word-aligned
  }

  virtual task body();
    axi_transaction tx;
    repeat(num_transactions) begin
      `uvm_do_with(tx, { tx.addr == base_addr; tx.wr_rd == 1; })
    end
  endtask
endclass

A test applies its own constraints externally without touching the sequence class:


// In the test's run_phase - constrain and start the sequence
axi_write_seq seq;
seq = axi_write_seq::type_id::create("seq");
assert(seq.randomize() with { base_addr == 32'hA000_0000; num_transactions == 4; });
seq.start(env.agent.seqr);

Build Sequence Libraries, Not Monolithic Sequences

Break complex stimulus into small, single-purpose sequences that can be composed. A single 500-line sequence covering initialization, burst writes, and error injection is impossible to reuse partially. Three focused sequences can be combined in any order by any test.


// Small, single-purpose sequences
class axi_init_seq    extends uvm_sequence #(axi_transaction); ... endclass
class axi_write_seq   extends uvm_sequence #(axi_transaction); ... endclass
class axi_read_seq    extends uvm_sequence #(axi_transaction); ... endclass
class axi_err_inj_seq extends uvm_sequence #(axi_transaction); ... endclass

// Composed in a virtual sequence
class axi_full_test_seq extends uvm_sequence;
  virtual task body();
    `uvm_do(init_seq)
    `uvm_do(write_seq)
    `uvm_do(read_seq)
  endtask
endclass

Register sequences with `uvm_object_utils so the factory can override them. This allows a test to substitute a more constrained version of a sequence without modifying the environment.

Guideline 4: Use the Factory for All Component Creation

The UVM factory is the mechanism that makes component substitution possible without touching the original source files. A component that bypasses the factory cannot be overridden by downstream users.

Register Every Class with the Factory

Use `uvm_component_utils for components and `uvm_object_utils for objects and sequences. Without registration, factory override will silently fail.


class axi_driver extends uvm_driver #(axi_transaction);
  `uvm_component_utils(axi_driver)   // Registers with factory
  // ...
endclass

class axi_transaction extends uvm_sequence_item;
  `uvm_object_utils(axi_transaction) // Registers with factory
  // ...
endclass

Always Create Components and Objects via the Factory

Use type_id::create() everywhere - never new() directly. A direct new() call cannot be intercepted by a factory override.


// WRONG - bypasses the factory
axi_driver drv = new("drv", this);

// CORRECT - factory can substitute a different type
axi_driver drv = axi_driver::type_id::create("drv", this);

Using new() directly for components or transactions is one of the most common reusability mistakes. It makes the component impossible to extend or override without modifying the original source.

Factory Override in Practice

In a multi-project environment, a verification team ships an AXI VIP with a standard axi_driver. A project team working on a custom memory controller needs slightly different driving behavior. Rather than forking the VIP, they extend axi_driver, override the specific run_phase behavior, and register the override at the test level. The VIP source stays unchanged and continues to receive updates from the original team.

Guideline 5: Use Callbacks for Behavioral Extension

Callbacks provide a structured extension point without requiring subclassing. They allow a reuser to inject custom behavior at specific points in a component's execution - such as before or after driving a transaction - without touching the component's code.


// 1. Define the callback class
class axi_driver_cbs extends uvm_callback;
  `uvm_object_utils(axi_driver_cbs)

  // Called before driving each transaction
  virtual task pre_drive(axi_driver drv, axi_transaction tx);
  endtask

  // Called after driving each transaction
  virtual task post_drive(axi_driver drv, axi_transaction tx);
  endtask
endclass

// 2. Register and invoke callbacks in the driver
class axi_driver extends uvm_driver #(axi_transaction);
  `uvm_component_utils(axi_driver)
  `uvm_register_cb(axi_driver, axi_driver_cbs)

  virtual task run_phase(uvm_phase phase);
    forever begin
      seq_item_port.get_next_item(req);
      `uvm_do_callbacks(axi_driver, axi_driver_cbs, pre_drive(this, req))
      drive_transaction(req);
      `uvm_do_callbacks(axi_driver, axi_driver_cbs, post_drive(this, req))
      seq_item_port.item_done();
    end
  endtask

  // Rest of the code (drive_transaction task, etc.)
endclass

A test then adds a callback without modifying the driver:


// Custom callback for error injection
class err_inject_cb extends axi_driver_cbs;
  virtual task pre_drive(axi_driver drv, axi_transaction tx);
    if ($urandom_range(0,9) == 0)
      tx.addr[1:0] = 2'b11; // Force misaligned address
  endtask
endclass

// In the test:
err_inject_cb cb = new("cb");
uvm_callbacks #(axi_driver, axi_driver_cbs)::add(env.agent.drv, cb);

Callbacks are most valuable when you ship a VIP to other teams who cannot modify the source. They define the legal extension points upfront so extension happens in a controlled, documented way.

Guideline 6: Avoid Hardcoded Hierarchical Paths

Hardcoded path strings in uvm_config_db calls or uvm_root::find() calls tie a component to one specific testbench topology. When the hierarchy changes - because the component is reused in a new project with a different environment structure - those paths silently fail to resolve.

Use Relative Paths in Config Database Calls


// WRONG - hardcoded absolute path breaks in any other testbench
uvm_config_db #(axi_agent_cfg)::set(null, "uvm_test_top.env.axi_agent", "cfg", my_cfg);

// CORRECT - set from the context of the component that owns it
uvm_config_db #(axi_agent_cfg)::set(this, "axi_agent", "cfg", my_cfg);

Never Use uvm_top.find() Inside Reusable Components

Looking up sibling components by path creates hidden coupling to the testbench topology. Use TLM connections and configuration objects to communicate between components instead.

Guideline 7: Write Monitors That Check the Protocol

The monitor should capture every transaction observed on the interface and broadcast it via an analysis port. It must not contain scoreboard logic - that belongs in a separate subscriber. However, the monitor may include protocol-level assertions to catch bus violations immediately at the interface.


class axi_monitor extends uvm_monitor;
  `uvm_component_utils(axi_monitor)

  uvm_analysis_port #(axi_transaction) ap;
  virtual axi_if vif;

  virtual task run_phase(uvm_phase phase);
    axi_transaction tx;
    forever begin
      @(posedge vif.clk);
      if (vif.valid && vif.ready) begin
        tx = axi_transaction::type_id::create("tx");
        tx.addr = vif.addr;
        tx.data = vif.data;
        // Broadcast to all connected subscribers
        ap.write(tx);
      end
    end
  endtask

  // Rest of the code (constructor, build_phase, protocol assertions)
endclass

Do not put scoreboard comparisons inside the monitor. Scoreboards are project-specific; monitors are protocol-specific. Mixing them prevents the monitor from being reused in a different project with a different checking strategy.

Quick Check

Q: A monitor and a scoreboard are both observing AXI transactions. Which component should detect a protocol violation (e.g., an invalid burst length), and which should detect a data correctness error?

Show Answer

The monitor (or an embedded protocol checker/assertion) handles protocol violations - these are fixed rules of the AXI spec and belong in the VIP. The scoreboard handles data correctness - it compares what went in against what came out, and this logic is project-specific.

Guideline 8: Parameterize Coverage for Reuse

Coverage groups embedded in a subscriber or monitor should be toggleable via the configuration object. Not every project needs every coverage point - forcing them on wastes simulation cycles and produces irrelevant coverage reports for downstream users.


class axi_coverage extends uvm_subscriber #(axi_transaction);
  `uvm_component_utils(axi_coverage)

  axi_agent_cfg cfg;
  axi_transaction tx;

  covergroup axi_cg;
    cp_op:    coverpoint tx.wr_rd   { bins write = {1}; bins read = {0}; }
    cp_burst: coverpoint tx.burst_len;
    cp_cross: cross cp_op, cp_burst;
  endgroup

  function void write(axi_transaction t);
    if (cfg.en_coverage) begin
      tx = t;
      axi_cg.sample();
    end
  endfunction

  // Rest of the code (constructor, build_phase to fetch cfg)
endclass

Guideline 9: Document the VIP Interface Contract

A reusable component is only as useful as its documentation. At a minimum, document:

  • Configuration object fields - what each field controls and its legal range
  • Required uvm_config_db entries - what the component expects to find and under what path string
  • TLM ports and their transaction types - so integrators know what to connect
  • Available factory overrides - list the classes that are registered and can be swapped
  • Callback hooks - document each callback method's purpose and legal actions
  • Virtual interface requirements - interface name, clock assumptions, reset polarity

A simple README.md or header comment block in the package file describing these six items saves every downstream user hours of reading source code.

Common Mistakes

Mistake #1: Hardcoding the virtual interface in the driver or monitor

Wrong: Assigning vif directly inside the component's constructor or from a package-level global variable.
Right: Always retrieve the virtual interface through uvm_config_db in build_phase using the configuration object.

Why? Global interface handles tie the component to one specific testbench. Config database retrieval works regardless of where the component is instantiated.

Mistake #2: Printing protocol-specific messages with $display instead of UVM reporting.

Wrong:


$display("AXI write to addr %0h", tx.addr);

Right:


`uvm_info("AXI_DRV", $sformatf("Write to addr %0h", tx.addr), UVM_HIGH)

Why? UVM's reporting system allows verbosity control, filtering, and log capture. Raw $display cannot be silenced by environment-level verbosity settings, polluting regression logs.

Mistake #3: Making the sequencer type-specific to one transaction class with no extension path.

Why? If a project needs a transaction with additional fields, they must subclass the transaction. The sequencer should be parameterized with the base transaction type so extended types work without replacing the sequencer.

Mistake #4: Putting the reset handler inside the driver's run_phase without a callback hook.

Why? Different projects have different reset timing requirements. If the reset behavior is embedded with no override point, every project must fork the driver to adjust it. A pre_reset or post_reset callback makes this pluggable.

Summary

In this guide, you learned:

  • Transaction classes must extend uvm_sequence_item, use rand fields, and implement do_* methods for full framework integration
  • Agents must support active/passive modes and receive all configuration through a dedicated config object via uvm_config_db
  • Sequences should be parameterized and composable - small, single-purpose sequences assembled by virtual sequences
  • All components and objects must be registered with and created through the UVM factory
  • Callbacks expose controlled extension points without requiring the reuser to modify source
  • Monitors capture and broadcast; scoreboards check - never mix them
  • Coverage collection and verbose reporting should be toggleable through configuration
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5