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
- Gang of Four: The Singleton Pattern
- UVM's Singleton Implementations
- Building Your Own Singleton
- Singleton vs Static Classes
- Quick Reference
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:
| Singleton | What It Does | How You Use It (Often Without Knowing) |
|---|---|---|
uvm_root | Top of the component hierarchy | Every component's parent chain ends here |
uvm_factory | Central type registry and override engine | type_id::create() calls this internally |
uvm_report_server | Global message handler | Every `uvm_info, `uvm_error, `uvm_fatal |
uvm_coreservice_t | Meta-singleton managing other singletons | Coordinates factory, root, and report server |
uvm_cmdline_processor | Command-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:
- Private/local constructor — Prevents external code from calling
new() - Static instance variable — Holds the single instance
- 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_managerand break the "single instance" invariant - Initialization order — Static variables initialize before time 0 in unpredictable order. If
global_mgrdepends 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
| Aspect | Singleton | Static Class |
|---|---|---|
| Instance exists? | Yes — one object in memory | No — just a namespace for static methods |
| Access pattern | Class::get().method() | Class::method() |
| State stored in | Instance variables | Static variables only |
| Can be extended? | Yes (if constructor is protected) | Limited — subclass inherits static methods |
| Can be swapped? | Yes — replace instance via setter | No — static methods can't be overridden at runtime |
| UVM examples | uvm_root, uvm_factory, uvm_report_server | uvm_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 Class | Pattern | Access | Replaceable? |
|---|---|---|---|
uvm_root | Singleton | uvm_root::get() | No |
uvm_factory | Singleton | uvm_factory::get() | Yes, via set_factory() |
uvm_report_server | Singleton | uvm_report_server::get_server() | Yes, via set_report_server() |
uvm_coreservice_t | Meta-Singleton | uvm_coreservice_t::get() | No |
uvm_cmdline_processor | Singleton | uvm_cmdline_processor::get_inst() | No |
uvm_config_db | Static Class | uvm_config_db#(T)::set/get() | No |
uvm_resource_db | Static Class | uvm_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
| Mistake | Fix |
|---|---|
Calling new() directly on a singleton | Use get() — constructor should be local |
Storing singleton handle in uvm_config_db | Unnecessary — use Class::get() directly |
| Creating a singleton for per-agent state | Use uvm_config_db or pass handles instead |
Confusing uvm_config_db for a singleton | It's a static class — no instance exists |
Forgetting local on constructor | Without 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
Comments (0)
Leave a Comment