Factory Pattern in UVM: From Theory to Implementation

The Factory pattern is the backbone of UVM's flexibility. Every type_id::create() call you write relies on it. But what's actually happening under the hood? This post explores the theory behind factory patterns, how UVM implements them, and why it matters for your testbench.

The Problem: Why Factories Exist

Consider a simple UVM environment. You have a driver, and you instantiate it directly:

Example: Hardcoded Instantiation (The Problem)

class apb_env extends uvm_env;
  apb_driver drv;
  
  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    drv = new("drv", this);  // Hardcoded to apb_driver
  endfunction
endclass

This works, but creates tight coupling. The environment is permanently bound to apb_driver. Now consider these scenarios:

  • Debug mode: You need apb_debug_driver with extra logging
  • Error injection: You need apb_error_driver that corrupts transactions
  • Performance: You need apb_fast_driver that skips protocol delays

With hardcoded new(), you must edit apb_env for each variant. This violates two SOLID principles:

  • Open-Closed Principle: Classes should be open for extension, closed for modification
  • Dependency Inversion: Depend on abstractions, not concrete implementations

The Factory pattern solves this by decoupling object creation from object usage.

Gang of Four: Factory Patterns

The "Gang of Four" (GoF) book describes three factory-related patterns. Understanding these helps you appreciate UVM's design choices.

1. Simple Factory

A single class with a method that creates objects based on input parameters. Not a formal GoF pattern, but commonly used.

classDiagram
    class SimpleFactory {
        +create(type) : Product
    }
    class Product
    class ConcreteProductA
    class ConcreteProductB
    
    Product <|-- ConcreteProductA
    Product <|-- ConcreteProductB
    SimpleFactory ..> Product : creates

Example: Simple Factory in SystemVerilog

class driver_factory;
  static function uvm_driver create(string driver_type);
    case (driver_type)
      "apb":   return new apb_driver();
      "axi":   return new axi_driver();
      "ahb":   return new ahb_driver();
      default: return null;
    endcase
  endfunction
endclass

// Usage
drv = driver_factory::create("apb");

Limitation: Adding new types requires modifying the factory's case statement.

2. Factory Method

Define an interface for creating objects, but let subclasses decide which class to instantiate. The creation is deferred to subclasses.

classDiagram
    class Creator {
        +factoryMethod()* : Product
        +operation()
    }
    class ConcreteCreatorA {
        +factoryMethod() : Product
    }
    class ConcreteCreatorB {
        +factoryMethod() : Product
    }
    class Product
    class ConcreteProductA
    class ConcreteProductB
    
    Creator <|-- ConcreteCreatorA
    Creator <|-- ConcreteCreatorB
    Product <|-- ConcreteProductA
    Product <|-- ConcreteProductB
    ConcreteCreatorA ..> ConcreteProductA : creates
    ConcreteCreatorB ..> ConcreteProductB : creates

Example: Factory Method in SystemVerilog

virtual class base_env extends uvm_env;
  // Factory method - subclasses override this
  pure virtual function uvm_driver create_driver();
  
  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    drv = create_driver();  // Delegates to subclass
  endfunction
endclass

class apb_env extends base_env;
  virtual function uvm_driver create_driver();
    return apb_driver::type_id::create("drv", this);
  endfunction
endclass

class axi_env extends base_env;
  virtual function uvm_driver create_driver();
    return axi_driver::type_id::create("drv", this);
  endfunction
endclass

Benefit: New products don't require modifying existing code—just add new subclasses.

3. Abstract Factory

Provide an interface for creating families of related objects without specifying concrete classes.

classDiagram
    class AbstractFactory {
        +createDriver()* : Driver
        +createMonitor()* : Monitor
        +createSequencer()* : Sequencer
    }
    class APBFactory {
        +createDriver() : APBDriver
        +createMonitor() : APBMonitor
        +createSequencer() : APBSequencer
    }
    class AXIFactory {
        +createDriver() : AXIDriver
        +createMonitor() : AXIMonitor
        +createSequencer() : AXISequencer
    }
    
    AbstractFactory <|-- APBFactory
    AbstractFactory <|-- AXIFactory

Example: Abstract Factory for VIP Components

virtual class vip_factory;
  pure virtual function uvm_driver    create_driver(string name, uvm_component parent);
  pure virtual function uvm_monitor   create_monitor(string name, uvm_component parent);
  pure virtual function uvm_sequencer create_sequencer(string name, uvm_component parent);
endclass

class apb_vip_factory extends vip_factory;
  virtual function uvm_driver create_driver(string name, uvm_component parent);
    return apb_driver::type_id::create(name, parent);
  endfunction
  
  virtual function uvm_monitor create_monitor(string name, uvm_component parent);
    return apb_monitor::type_id::create(name, parent);
  endfunction
  
  virtual function uvm_sequencer create_sequencer(string name, uvm_component parent);
    return apb_sequencer::type_id::create(name, parent);
  endfunction
endclass

// Usage: Swap entire VIP family by changing factory
class generic_env extends uvm_env;
  vip_factory factory;
  
  function void build_phase(uvm_phase phase);
    drv = factory.create_driver("drv", this);
    mon = factory.create_monitor("mon", this);
    sqr = factory.create_sequencer("sqr", this);
  endfunction
endclass

Use case: When you have related components that must be used together (driver + monitor + sequencer for a specific protocol).

UVM's Factory Architecture

UVM implements a centralized, type-based factory that combines aspects of all three patterns. Let's break it down.

The Registration Mechanism

Every UVM class registers itself with the factory using macros:

class my_driver extends uvm_driver #(my_txn);
  `uvm_component_utils(my_driver)  // Registers with factory
  // ...
endclass

class my_txn extends uvm_sequence_item;
  `uvm_object_utils(my_txn)  // For objects (no parent)
  // ...
endclass

But what do these macros actually do?

Macro Expansion: What uvm_component_utils Actually Does

// `uvm_component_utils(my_driver) expands to approximately:

// 1. Typedef for the proxy type
typedef uvm_component_registry #(my_driver, "my_driver") type_id;

// 2. Static method to get the type
static function type_id get_type();
  return type_id::get();
endfunction

// 3. Virtual method to get type name
virtual function string get_type_name();
  return "my_driver";
endfunction

The key is uvm_component_registry—a proxy class that knows how to create instances of your class.

The Proxy Pattern Inside UVM Factory

UVM uses the Proxy pattern to enable type-based creation. Each registered class gets a proxy that acts as its factory representative.

sequenceDiagram
    participant Test
    participant Factory as uvm_factory
    participant Proxy as type_id (proxy)
    participant Driver as my_driver
    
    Test->>Proxy: my_driver::type_id::create("drv", parent)
    Proxy->>Factory: create_component_by_type(proxy, name, parent)
    Factory->>Factory: Check for overrides
    Factory->>Proxy: Call proxy.create_component()
    Proxy->>Driver: new(name, parent)
    Driver-->>Test: Return instance

Simplified Proxy Implementation

class uvm_component_registry #(type T=uvm_component, string Tname="") 
    extends uvm_object_wrapper;
  
  typedef uvm_component_registry #(T, Tname) this_type;
  
  // Singleton proxy instance
  static this_type me = get();
  
  // Get the singleton proxy
  static function this_type get();
    if (me == null) begin
      me = new();
      // Register with global factory
      uvm_factory::get().register(me);
    end
    return me;
  endfunction
  
  // Create method - called by factory
  virtual function uvm_component create_component(
      string name, uvm_component parent);
    T obj = new(name, parent);  // Actual instantiation
    return obj;
  endfunction
  
  // Convenience method for users
  static function T create(string name, uvm_component parent);
    uvm_component obj;
    obj = uvm_factory::get().create_component_by_type(
        get(), "", name, parent);
    $cast(create, obj);
  endfunction
  
endclass

The Global Factory Singleton

UVM maintains a single global factory instance (uvm_factory) that:

  • Stores all registered type proxies
  • Maintains override tables (type and instance)
  • Resolves creation requests to the correct proxy

Factory's Internal Data Structures

class uvm_default_factory extends uvm_factory;
  
  // Registry: type_name -> proxy
  protected uvm_object_wrapper m_type_names[string];
  
  // Type overrides: original_type -> override_type
  protected uvm_factory_override m_type_overrides[$];
  
  // Instance overrides: {path, original_type} -> override_type  
  protected uvm_factory_override m_inst_overrides[$];
  
  // Creation flow
  virtual function uvm_component create_component_by_type(
      uvm_object_wrapper requested_type,
      string parent_inst_path,
      string name,
      uvm_component parent);
    
    uvm_object_wrapper override_type;
    
    // 1. Check instance overrides first (higher priority)
    override_type = find_inst_override(requested_type, parent_inst_path);
    
    // 2. If no instance override, check type overrides
    if (override_type == null)
      override_type = find_type_override(requested_type);
    
    // 3. Use original if no override found
    if (override_type == null)
      override_type = requested_type;
    
    // 4. Create via the proxy
    return override_type.create_component(name, parent);
  endfunction
  
endclass

Override Mechanisms

The real power of UVM's factory is the ability to swap implementations without modifying source code. There are two override types:

Type Override

Replace all instances of a type globally:

Type Override Examples

class error_injection_test extends base_test;
  
  function void build_phase(uvm_phase phase);
    // Method 1: Using type_id
    apb_driver::type_id::set_type_override(
        apb_error_driver::get_type());
    
    // Method 2: Using factory directly
    uvm_factory::get().set_type_override_by_type(
        apb_driver::get_type(),
        apb_error_driver::get_type());
    
    // Method 3: Using type names (strings)
    uvm_factory::get().set_type_override_by_name(
        "apb_driver",
        "apb_error_driver");
    
    super.build_phase(phase);
  endfunction
endclass

Instance Override

Replace a specific instance based on its hierarchical path:

Instance Override Examples

class targeted_error_test extends base_test;
  
  function void build_phase(uvm_phase phase);
    // Only override driver in agent[0], leave agent[1] normal
    apb_driver::type_id::set_inst_override(
        apb_error_driver::get_type(),
        "uvm_test_top.env.agent[0].driver");
    
    // Wildcards supported
    apb_driver::type_id::set_inst_override(
        apb_debug_driver::get_type(),
        "uvm_test_top.*.debug_agent.driver");
    
    super.build_phase(phase);
  endfunction
endclass

Override Priority

When multiple overrides apply, UVM resolves them in this order:

1. Instance override — Highest priority. Matches the most specific hierarchical path.
2. Type override — Applied globally. If multiple exist, last registered wins.
3. Original type — Default when no overrides match.

Instance overrides always win because they target a specific location in your testbench hierarchy, while type overrides are a blanket replacement everywhere.

Override Chaining

Overrides can chain: if A is overridden by B, and B is overridden by C, creating A gives you C.

Override Chaining Example

// In base_test
apb_driver::type_id::set_type_override(apb_debug_driver::get_type());

// In derived_test (extends base_test)
apb_debug_driver::type_id::set_type_override(apb_full_debug_driver::get_type());

// Result: apb_driver::type_id::create() returns apb_full_debug_driver

Advanced: Factory Internals

Debugging Factory Issues

When overrides don't work as expected, use factory debug methods:

Factory Debug Commands

// Print all registered types
uvm_factory::get().print();

// Print with verbosity (0=types, 1=+overrides, 2=+all instances)
uvm_factory::get().print(1);

// Debug specific creation
uvm_factory::get().debug_create_by_type(
    apb_driver::get_type(),
    "uvm_test_top.env.agent.driver");

Sample print() output:

#### Factory Configuration (*)

  No instance overrides are registered with this factory
  
  Type Overrides:
    Requested Type  Override Type
    --------------  -------------
    apb_driver      apb_error_driver

  All types registered with the factory: 42 total
    apb_driver
    apb_error_driver
    apb_monitor
    ...

Parameterized Classes

Parameterized classes require special registration:

Registering Parameterized Classes

class generic_driver #(type T=uvm_sequence_item) extends uvm_driver #(T);
  
  typedef generic_driver #(T) this_type;
  
  // Use uvm_component_param_utils for parameterized classes
  `uvm_component_param_utils(generic_driver #(T))
  
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction
endclass

// Each parameterization is a separate factory registration
generic_driver #(apb_txn)::type_id::set_type_override(
    custom_apb_driver::get_type());

Factory with Field Automation

When using field macros, ensure consistent registration:

Field Automation with Factory

class my_txn extends uvm_sequence_item;
  rand bit [31:0] addr;
  rand bit [31:0] data;
  rand bit        write;
  
  `uvm_object_utils_begin(my_txn)
    `uvm_field_int(addr,  UVM_ALL_ON)
    `uvm_field_int(data,  UVM_ALL_ON)
    `uvm_field_int(write, UVM_ALL_ON)
  `uvm_object_utils_end
  
  function new(string name="my_txn");
    super.new(name);
  endfunction
endclass

// Override works with field automation
my_txn::type_id::set_type_override(my_extended_txn::get_type());

Quick Reference

OperationCode
Register component`uvm_component_utils(class_name)
Register object`uvm_object_utils(class_name)
Register parameterized`uvm_component_param_utils(class#(T))
Create componentclass::type_id::create("name", parent)
Create objectclass::type_id::create("name")
Type overrideorig::type_id::set_type_override(new::get_type())
Instance overrideorig::type_id::set_inst_override(new::get_type(), "path")
Print factoryuvm_factory::get().print()
Debug creationuvm_factory::get().debug_create_by_type(...)

Common Mistakes

MistakeFix
Using new() directlyUse type_id::create()
Override after super.build_phase()Override before super.build_phase()
Wrong path in instance overrideUse get_full_name() to verify paths
Forgetting `uvm_*_utilsAlways register classes with factory

Next: Singleton Pattern - uvm_root, uvm_config_db, and global resources

Author
Milan Kubavat
Sharing knowledge about silicon verification, hardware design, and engineering insights.

Comments (0)

Leave a Comment