Skip to main content

The Singleton Pattern

The word "singleton" has already come up three times in this curriculum without ever being explained on its own terms: Base Classes called uvm_root "a singleton — not something you construct yourself"; User-Defined Phases built a custom phase class using exactly this pattern without naming it; and Printer & Comparer Policies used uvm_default_table_printer as one, in passing. This page names the pattern directly, shows it as plain SystemVerilog with nothing UVM-specific about it, and — more usefully — draws the line between when it's the right tool and when uvm_config_db already is.

The pattern itself: a private constructor plus a static accessor​

class pwm_global_config;
local static pwm_global_config m_inst;
int unsigned default_period = 1000;

local function new();
endfunction

static function pwm_global_config get();
if (m_inst == null)
m_inst = new();
return m_inst;
endfunction
endclass

Two things make this a singleton rather than an ordinary class:

  • new() is local — a compile error, not a runtime check, stops any code outside this class from constructing one directly. pwm_global_config cfg = new(); from anywhere else in the testbench simply won't compile.
  • get() is static, called on the class itself (pwm_global_config::get()), not on an instance — since by definition, nothing has an instance to call it on until get() itself creates the first one.

The first call to get() anywhere in the simulation constructs the one and only instance (m_inst == null is only ever true once); every call after that, from anywhere, returns the exact same handle. This is precisely the shape User-Defined Phases's fifo_calibration_phase::get() already used — a custom phase needs to be the literal same object everywhere it's referenced in the schedule, for the same reason a global config object needs to be the literal same object everywhere it's read.

Where UVM itself already relies on this​

uvm_root::get(), uvm_factory::get() (the factory handle used throughout The UVM Factory is exactly this), and the default printer objects from Printer & Comparer Policies are all singletons built the same way. None of them are UVM-specific magic — they're this exact pattern, applied to base-library infrastructure that genuinely needs to be a single, shared, global instance.

IEEE 1800.2 also standardizes a single entry point for reaching several of these at once: uvm_coreservice_t::get() returns another singleton whose own methods (get_factory(), get_root(), get_report_server(), and others) hand back the same underlying singletons named above. It exists so testbench code has one documented, standard-mandated way to reach core UVM services, rather than depending on how a specific simulator happens to expose the global factory/uvm_top variables — functionally, it's a singleton whose job is handing out references to other singletons.

Contrast with uvm_config_db: "the tree" vs. "exactly one"​

uvm_config_db (see Resource DB & Config DB) and a singleton solve superficially similar problems — both let unrelated parts of a testbench reach shared data without a direct handle passed between them — but the shape of what each one models is genuinely different:

  • uvm_config_db is scoped to the component hierarchy. Different components, at different paths, can legitimately get() different values for the same field name, depending on which set() calls' wildcard scope actually reaches them.
  • A singleton has no concept of hierarchy or scope at all — there is exactly one instance, and every single caller everywhere gets the identical object, with identical state, no matter where in the tree (or outside it entirely) the call happens.

That difference points directly at when each one fits: configuration that's meant to vary by hierarchical location — is_active, a virtual interface, per-agent settings — belongs in uvm_config_db. A genuinely global, cross-cutting concern with no meaningful "which component asked" dimension at all — a shared random-seed manager, a process-wide coverage aggregator, a piece of state that isn't even attached to any uvm_component in the tree — is what a singleton is actually for.

Singleton state doesn't reset between tests the way uvm_config_db entries do

uvm_config_db settings are naturally scoped to whatever test's build_phase set them, since a fresh component tree gets built per test. A singleton's static state has no such boundary — if more than one test class runs within the same compiled simulation, a singleton constructed by an earlier test can still be sitting there, holding stale state, when a later test's code calls the same get(). This kind of cross-test leakage is easy to miss precisely because nothing about the singleton's own code looks wrong in isolation — the bug only shows up as a mysterious value that shouldn't be there yet, and +UVM_CONFIG_DB_TRACE won't show it, since it isn't a config_db access at all.

What's next​

Both major mechanisms of the "advanced topics" pair — backdoor access and the singleton pattern — are now covered. The next section turns from mechanism to discipline: what actually makes a verification component reusable by someone other than the person who wrote it.