Singleton Pattern in UVM: The Hidden Singletons You Use Every Day

Every time you call uvm_config_db#(int)::set(...), print a message through `uvm_info, or create a component with type_id::create(), you're relying on a Singleton. In fact, a typical UVM testbench silently depends on at least five singletons before your test even starts running. This post uncovers these hidden singletons, explains the theory behind the pattern, and shows you how to build your own.

The Hidden Singletons

Run any UVM test and trace what happens before your build_phase even executes. The framework has already created these single-instance objects behind the scenes:

SingletonWhat It DoesHow You Use It (Often Without Knowing)
uvm_rootTop of the component hierarchyEvery component's parent chain ends here
uvm_factoryCentral type registry and override enginetype_id::create() calls this internally
uvm_report_serverGlobal message handlerEvery `uvm_info, `uvm_error, `uvm_fatal
uvm_coreservice_tMeta-singleton managing other singletonsCoordinates factory, root, and report server
uvm_cmdline_processorCommand-line argument parser+UVM_TESTNAME, +UVM_VERBOSITY, etc.

These aren't singletons by accident. Each one manages global, shared state that must be consistent across your entire testbench. Two factory instances would mean two separate type registries. Two report servers would mean split message counts. Two roots would break the component hierarchy entirely.

The Singleton pattern ensures exactly one instance exists and provides a global access point to it. Let's see how.

Gang of Four: The Singleton Pattern

The Gang of Four defines Singleton as:

Ensure a class has only one instance, and provide a global point of access to it.

It's deceptively simple. The structure has just one class:

classDiagram
    class Singleton {
        -static instance : Singleton
        -Singleton()
        +static get() : Singleton
        +operation()
    }
    Singleton --> Singleton : returns single instance

Three mechanisms work together:

  1. Private/local constructor — Prevents external code from calling new()
  2. Static instance variable — Holds the single instance
  3. Static accessor method — Creates the instance on first call, returns it on every subsequent call

Classic Singleton in SystemVerilog

class my_singleton;
  // 1. Static variable holds the single instance
  static local my_singleton m_inst;
  
  // 2. Local constructor — cannot be called externally
  local function new();
    // initialization
  endfunction
  
  // 3. Static accessor — the only way to get the instance
  static function my_singleton get();
    if (m_inst == null)
      m_inst = new();  // Created only once
    return m_inst;
  endfunction
  
  // Instance methods
  function void do_something();
    // ...
  endfunction
endclass

// Usage
my_singleton s1 = my_singleton::get();
my_singleton s2 = my_singleton::get();
// s1 == s2 → always the same object

The if (m_inst == null) check is called lazy initialization: the instance isn't created until someone actually needs it. This is the approach UVM uses for all its singletons.

Why Not Just Use a Global Variable?

SystemVerilog supports global variables. You could do this:

// Don't do this
my_manager global_mgr = new();

// Anywhere in the testbench:
global_mgr.do_something();

This has three problems:

  • No instantiation control — Anyone can create another my_manager and break the "single instance" invariant
  • Initialization order — Static variables initialize before time 0 in unpredictable order. If global_mgr depends on another global, it might not exist yet
  • No encapsulation — The variable is exposed directly with no access control

The Singleton pattern solves all three: the local constructor enforces one instance, lazy initialization avoids ordering issues, and the get() method encapsulates access.

UVM's Singleton Implementations

UVM doesn't use a single singleton pattern. Different singletons use different strategies depending on their role. Let's examine the three most important ones.

uvm_coreservice_t — The Singleton That Manages Singletons

This is UVM's meta-singleton. Rather than having each singleton manage itself independently, uvm_coreservice_t acts as a central registry for core services.

classDiagram
    class uvm_coreservice_t {
        -static inst : uvm_coreservice_t
        +static get() : uvm_coreservice_t
        +get_factory() : uvm_factory
        +get_root() : uvm_root
        +get_report_server() : uvm_report_server
        +set_factory(uvm_factory f)
        +set_report_server(uvm_report_server s)
    }
    class uvm_default_coreservice_t {
        +get_factory() : uvm_factory
        +get_root() : uvm_root
        +get_report_server() : uvm_report_server
    }
    class uvm_factory
    class uvm_root
    class uvm_report_server
    
    uvm_coreservice_t <|-- uvm_default_coreservice_t
    uvm_default_coreservice_t --> uvm_factory : creates & holds
    uvm_default_coreservice_t --> uvm_root : creates & holds
    uvm_default_coreservice_t --> uvm_report_server : creates & holds

Core Service Implementation

class uvm_coreservice_t;
  static local uvm_coreservice_t inst;
  
  // The single access point
  static function uvm_coreservice_t get();
    if (inst == null)
      inst = new();  // Lazy initialization
    return inst;
  endfunction
  
  // Virtual methods — overridable for customization
  virtual function uvm_factory get_factory();
  virtual function void set_factory(uvm_factory f);
  virtual function uvm_root get_root();
  virtual function uvm_report_server get_report_server();
  virtual function void set_report_server(uvm_report_server server);
endclass

Why a meta-singleton? It provides a single point of customization. If you want to replace the default factory with a custom one, you do it through core service rather than modifying each singleton independently.

uvm_root — The Hierarchy Anchor

uvm_root is the invisible parent at the top of every UVM component tree. When you write uvm_test_top.env.agent.driver, there's actually a uvm_root above uvm_test_top.

graph TD
    A["uvm_root (__top__)"] --> B["uvm_test_top"]
    B --> C["env"]
    C --> D["agent"]
    D --> E["driver"]
    D --> F["monitor"]
    D --> G["sequencer"]
    style A fill:#334155,stroke:#22d3ee,color:#f1f5f9

uvm_root's Singleton Access

class uvm_root extends uvm_component;
  static local uvm_root m_inst;
  
  // Public accessor delegates to core service
  static function uvm_root get();
    uvm_coreservice_t cs = uvm_coreservice_t::get();
    return cs.get_root();
  endfunction
  
  // Internal creation — called once by core service
  static function uvm_root m_uvm_get_root();
    if (m_inst == null) begin
      m_inst = new();
      void'(uvm_domain::get_common_domain());
      m_inst.m_domain = uvm_domain::get_uvm_domain();
    end
    return m_inst;
  endfunction
  
  // Constructor — names itself "__top__" with null parent
  function new();
    super.new("__top__", null);
  endfunction
endclass

// Access the singleton
uvm_root top = uvm_root::get();

Notice the two-tier access: the public get() goes through uvm_coreservice_t, while the internal m_uvm_get_root() does the actual creation. This separation allows the core service to control when and how uvm_root is created.

uvm_factory — The Type Registry

You already know the factory from the Factory Pattern post. But the factory itself is a singleton—there's exactly one registry for all type proxies and overrides.

Factory Singleton Access

// Abstract base class
virtual class uvm_factory;
  // Convenience accessor
  static function uvm_factory get();
    uvm_coreservice_t s = uvm_coreservice_t::get();
    return s.get_factory();
  endfunction
  
  // Pure virtual interface
  pure virtual function void register(uvm_object_wrapper obj);
  pure virtual function uvm_component create_component_by_type(...);
  pure virtual function uvm_object create_object_by_type(...);
endclass

// Concrete implementation
class uvm_default_factory extends uvm_factory;
  protected uvm_object_wrapper m_type_names[string];
  protected uvm_factory_override m_type_overrides[$];
  // ... single instance holds ALL registrations
endclass

The factory uses an abstract base + concrete implementation pattern. This means you could replace uvm_default_factory with your own implementation via uvm_coreservice_t::set_factory()—for example, to add logging every time a component is created.

uvm_report_server — The Message Hub

Every `uvm_info, `uvm_warning, `uvm_error, and `uvm_fatal eventually reaches the report server singleton. It maintains global message counts—that's how UVM knows to fail a test when error count exceeds the threshold.

class uvm_report_server;
  // Singleton access
  static function uvm_report_server get_server();
    uvm_coreservice_t cs = uvm_coreservice_t::get();
    return cs.get_report_server();
  endfunction
endclass

// Common usage: check error counts at end of test
uvm_report_server rs = uvm_report_server::get_server();
int error_count = rs.get_severity_count(UVM_ERROR);
if (error_count > 0)
  `uvm_info("TEST", $sformatf("%0d errors detected", error_count), UVM_NONE)

If two report servers existed, some components would report to one and some to the other. Error counts would be split, and your test might falsely pass because neither server saw enough errors individually.

Building Your Own Singleton

When should you create a singleton in your testbench? When you have shared state that must be globally consistent. Here are two practical examples.

Example 1: Global Scoreboard Registry

In a multi-agent environment, you might want a central place to track all scoreboards and their pass/fail counts:

class scoreboard_registry;
  static local scoreboard_registry m_inst;
  
  // Registry of all scoreboards
  protected uvm_scoreboard m_scoreboards[string];
  protected int m_match_count[string];
  protected int m_mismatch_count[string];
  
  local function new();
  endfunction
  
  static function scoreboard_registry get();
    if (m_inst == null)
      m_inst = new();
    return m_inst;
  endfunction
  
  // Register a scoreboard
  function void register(string name, uvm_scoreboard sb);
    m_scoreboards[name] = sb;
    m_match_count[name] = 0;
    m_mismatch_count[name] = 0;
  endfunction
  
  // Record results from any scoreboard
  function void record_match(string name);
    m_match_count[name]++;
  endfunction
  
  function void record_mismatch(string name);
    m_mismatch_count[name]++;
  endfunction
  
  // Print summary across ALL scoreboards
  function void print_summary();
    foreach (m_scoreboards[name]) begin
      `uvm_info("SCRBD_REG", $sformatf(
        "%s: %0d matches, %0d mismatches",
        name, m_match_count[name], m_mismatch_count[name]),
        UVM_LOW)
    end
  endfunction
endclass

// Usage from any scoreboard component
scoreboard_registry::get().register("apb_scrbd", this);
scoreboard_registry::get().record_match("apb_scrbd");

// In test's report_phase
scoreboard_registry::get().print_summary();

Example 2: Shared Resource Manager

When multiple agents share a bus or memory resource and need to coordinate access:

class resource_manager;
  static local resource_manager m_inst;
  
  // Track which agent owns each address range
  protected string m_owners[int unsigned];
  protected semaphore m_lock;
  
  local function new();
    m_lock = new(1);  // Binary semaphore
  endfunction
  
  static function resource_manager get();
    if (m_inst == null)
      m_inst = new();
    return m_inst;
  endfunction
  
  // Request exclusive access to an address range
  task acquire(string agent_name, int unsigned base_addr, int unsigned size);
    m_lock.get();
    foreach (m_owners[addr]) begin
      if (addr >= base_addr && addr < base_addr + size) begin
        if (m_owners[addr] != "" && m_owners[addr] != agent_name) begin
          m_lock.put();
          `uvm_error("RES_MGR", $sformatf(
            "%s cannot acquire 0x%0h — owned by %s",
            agent_name, addr, m_owners[addr]))
          return;
        end
      end
    end
    for (int unsigned a = base_addr; a < base_addr + size; a++)
      m_owners[a] = agent_name;
    m_lock.put();
  endtask
  
  // Release an address range
  function void release(string agent_name, int unsigned base_addr, int unsigned size);
    for (int unsigned a = base_addr; a < base_addr + size; a++) begin
      if (m_owners.exists(a) && m_owners[a] == agent_name)
        m_owners.delete(a);
    end
  endfunction
endclass

// From any agent's sequence
resource_manager::get().acquire("agent_0", 32'h1000, 32'h100);
// ... perform transactions ...
resource_manager::get().release("agent_0", 32'h1000, 32'h100);

Without a singleton, each agent would need a handle to the resource manager passed through the hierarchy. The singleton makes coordination implicit—any component can access it without wiring.

Singleton vs Static Classes

This is the most common confusion in UVM. uvm_config_db and uvm_resource_db look like singletons—they have global access and shared state—but they're actually static classes. Understanding the difference matters.

Static Class: Never Instantiated

// uvm_config_db is a STATIC CLASS — no instance exists
class uvm_config_db #(type T=int) extends uvm_resource_db #(T);
  // Protected constructor — never called
  protected function new(); endfunction
  
  // ALL methods are static
  static function void set(
      uvm_component cntxt, string inst_name,
      string field_name, T value);
    // ...
  endfunction
  
  static function bit get(
      uvm_component cntxt, string inst_name,
      string field_name, inout T value);
    // ...
  endfunction
endclass

// Called with :: — no object involved
uvm_config_db#(int)::set(null, "*", "timeout", 1000);

Singleton: Instantiated Once

// uvm_factory is a SINGLETON — one instance exists
class uvm_factory;
  static function uvm_factory get();
    // Returns actual object
  endfunction
endclass

// get() returns an object handle
uvm_factory f = uvm_factory::get();
f.print();

The Key Differences

AspectSingletonStatic Class
Instance exists?Yes — one object in memoryNo — just a namespace for static methods
Access patternClass::get().method()Class::method()
State stored inInstance variablesStatic variables only
Can be extended?Yes (if constructor is protected)Limited — subclass inherits static methods
Can be swapped?Yes — replace instance via setterNo — static methods can't be overridden at runtime
UVM examplesuvm_root, uvm_factory, uvm_report_serveruvm_config_db, uvm_resource_db

The practical consequence: you can replace UVM's factory or report server with a custom implementation (because they're singletons with virtual methods), but you cannot replace uvm_config_db's behavior without modifying the UVM source (because it's a static class).

// You CAN do this — swap factory implementation
uvm_coreservice_t cs = uvm_coreservice_t::get();
cs.set_factory(my_custom_factory::new());

// You CAN do this — swap report server
cs.set_report_server(my_custom_report_server::new());

// You CANNOT do this — config_db is static, not swappable
// uvm_config_db is hardwired

When to Use Which

  • Use a Singleton when you need an instance with state, virtual methods, or the ability to swap implementations (e.g., resource manager, scoreboard registry)
  • Use a Static Class when you just need a globally accessible utility with no need for polymorphism (e.g., helper functions, lookup tables)

Quick Reference

UVM ClassPatternAccessReplaceable?
uvm_rootSingletonuvm_root::get()No
uvm_factorySingletonuvm_factory::get()Yes, via set_factory()
uvm_report_serverSingletonuvm_report_server::get_server()Yes, via set_report_server()
uvm_coreservice_tMeta-Singletonuvm_coreservice_t::get()No
uvm_cmdline_processorSingletonuvm_cmdline_processor::get_inst()No
uvm_config_dbStatic Classuvm_config_db#(T)::set/get()No
uvm_resource_dbStatic Classuvm_resource_db#(T)::set/get()No

Singleton Template

class my_singleton;
  static local my_singleton m_inst;
  
  local function new();
    // initialization
  endfunction
  
  static function my_singleton get();
    if (m_inst == null)
      m_inst = new();
    return m_inst;
  endfunction
  
  // Your methods here
endclass

Common Mistakes

MistakeFix
Calling new() directly on a singletonUse get() — constructor should be local
Storing singleton handle in uvm_config_dbUnnecessary — use Class::get() directly
Creating a singleton for per-agent stateUse uvm_config_db or pass handles instead
Confusing uvm_config_db for a singletonIt's a static class — no instance exists
Forgetting local on constructorWithout it, anyone can call new() and break the pattern

Previous: Factory Pattern — type_id::create(), overrides, and the proxy mechanism

Next: Builder Pattern — Complex transaction construction and fluent interfaces

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

Comments (0)

Leave a Comment