A singleton object is an instance of a class that is designed to have only one instance for the whole simulation. Every part of the verification environment that asks for the object gets the same instance, so it acts as a shared, global store.

Singletons are often used for global settings, configurations and resources that many components need. They can also store design RTL parameters that shape the testbench configuration.

8 min read | Intermediate Level

What You'll Learn

  • How a singleton class is built with a static handle and a static get() function
  • How different components share data through a singleton
  • Which UVM classes are singletons themselves
  • How a singleton can hold design parameters for a large SoC testbench

How a Singleton Works

A singleton class has three parts:

  • A static local handle of its own type, which holds the only instance. Because it is static, there is one handle for the whole class, not one per object.
  • A static function, usually called get(), which creates the instance on the first call and returns the same instance on every later call.
  • Ideally, a protected constructor, so that no other code can call new() and create a second instance.
uvm singleton object

Singleton Object Example


// Define a class that you want to make a singleton
class mySingleton;
	function new (string name = "mySingleton");
		this.name = name;
   	endfunction

   	static local mySingleton m_sg;       // Singleton object
   	string name;                         // My test variable

	// Has a static method that returns the instance of the class
   	static function mySingleton get();
      	if (m_sg == null)
         	m_sg = new ();
      	return m_sg;
   	endfunction
endclass

The variable m_sg is static and local, so it can only be accessed inside the class. The static get() function checks whether m_sg already holds an instance. If it does, it returns that instance, otherwise it creates a new one first.

Both the test and the environment below call mySingleton::get() in their build_phase().


class my_env extends uvm_env;
   `uvm_component_utils (my_env)

   function new (string name="my_env", uvm_component parent);
      super.new (name, parent);
   endfunction

   virtual function void build_phase (uvm_phase phase);
      mySingleton  m_sg = mySingleton::get();

      super.build_phase (phase);

      `uvm_info ("ENV", $sformatf ("%s", m_sg.name), UVM_MEDIUM)
   endfunction

endclass


class my_test extends uvm_test;
   `uvm_component_utils (my_test)

   function new (string name = "my_test", uvm_component parent);
      super.new (name, parent);
   endfunction

   my_env m_env;

   virtual function void build_phase (uvm_phase phase);
      mySingleton m_sg = mySingleton::get();

      super.build_phase (phase);

      m_sg.name = "Hey, this works well !";
      `uvm_info ("TEST", $sformatf ("%s", m_sg.name), UVM_MEDIUM) 

      m_env = my_env::type_id::create ("m_env", this);

   endfunction
endclass

The first call, in the test, creates the object. The test then sets name. The call in the environment returns the same object, so it prints the string set by the test.

Output
CDNS-UVM-1.1d (15.10-s004)
(C) 2007-2013 Mentor Graphics Corporation
(C) 2007-2013 Cadence Design Systems, Inc.
(C) 2006-2013 Synopsys, Inc.
(C) 2011-2013 Cypress Semiconductor Corp.
----------------------------------------------------------------
UVM_INFO @ 0: reporter [RNTST] Running test my_test...
UVM_INFO tb_top.sv(45) @ 0: uvm_test_top [TEST] Hey, this works well !
UVM_INFO tb_top.sv(29) @ 0: uvm_test_top.m_env [ENV] Hey, this works well !

--- UVM Report catcher Summary ---

The test message appears first because build_phase() runs top-down: the test builds before the environment it creates.

The constructor of mySingleton is public, so any code could still call new() and create a second object. Declaring it as protected function new (...) prevents this, while get() can still call it from inside the class.

Singletons in UVM

UVM itself uses singletons for services that must be unique in a simulation:

SingletonAccessPurpose
uvm_rootuvm_root::get()Top of the component hierarchy, also known as uvm_top
uvm_factoryuvm_factory::get()Creates objects and applies overrides
uvm_report_serveruvm_report_server::get_server()Collects and counts all report messages
uvm_event_pooluvm_event_pool::get_global_pool()Global pool of named events

Use Case Example

An interconnect is the backbone of an SoC, because many cores, IPs and memory blocks connect to it. Each master and slave can have a different data bus width and different requirements. The testbench should be organized so that a change in these design parameters needs as little testbench effort as possible.

A singleton can act as a central database of design parameters, with functions in the same class to query them. Imagine the masters and slaves as cars of different types and makes, each described by a structure:


typedef struct {
	string 		brand;
	e_type 		car_type;
	e_engine	engine;
	bit [15:0] 	length;
	bit  		has_lcd;
	...
} st_car;

A list of these structures holds the details of every car, and can later be fed into the database. In a real project, a Perl or Python script can generate this list from the design files.


st_car 	car_list [10] = '{
 	'{"honda", SEDAN, V6, 450, ...}, 
 	'{"bmw", COUPE, V8, 300, ... }
  			...
};

The central database is a singleton class that stores one base_car object per entry:


class base_car;
	string 		brand;
	e_type 		car_type;
	...
endclass

class car_db;
	static local car_db 	m_inst;
	base_car  				car_list [$];
	
	static function car_db get();
		if (m_inst == null) begin
			m_inst = new();
		end
		return m_inst;
	endfunction
	
	// functions to query the list for different usage
endclass

The top environment gets the database with get() and fills it from the list:


class top_env;
	...
	car_db 	m_car_db;
	
	virtual function void build_env ();
		
		// Get database instance
		m_car_db = car_db::get();
		
		// For each row in the structure, create a class to store the row
		// and insert into the database queue
		foreach (car_list[i]) begin
			base_car  bc = new ();
			bc.brand = car_list[i].brand;
			bc.car_type = car_list[i].car_type;
			...
			m_car_db.car_list.push_back (bc);
		end
	endfunction
endclass

Any component can now call car_db::get() and query the database, without needing a handle passed down the hierarchy. All query functions are kept in one class. When a design parameter changes, only the structure list needs to be updated, and new variables can be added to base_car to store values computed from existing ones.

Quick Check

Q: The environment's build_phase() sets m_sg.name = "from env" after its `uvm_info. A scoreboard created by the environment prints name in its own build_phase(). What does it print?

Show Answer

from env. The scoreboard builds after the environment, and all of them share one object, so it sees the last value written.

Where is This Used?

SoC testbenches often keep a singleton memory map or design parameter database, generated from the RTL or IP-XACT files. Sequences use it to pick legal addresses, and scoreboards use it to work out which slave a transaction should reach.

Common Mistakes

Mistake #1: Leaving the constructor public

With a public new(), code such as m_sg = new(); creates a separate object that does not share any data with the singleton. Make the constructor protected so that get() is the only way to obtain the object.

Mistake #2: Depending on who sets values first

All components share one object, so a value read in one component depends on whether another component has already written it. Set values in the test or top environment, before the components that read them are built.

Mistake #3: Using a singleton for per-instance settings

A singleton holds one copy of each value. Settings that differ between two agents, such as an active or passive mode, belong in each agent's configuration object, passed through uvm_config_db.

Practice Exercise

Challenge: A Global Error Counter

Write a singleton class err_counter with a protected constructor, a static get() function, a function incr() that adds one to a counter, and a function count() that returns it. Then show how a scoreboard and a monitor would both increment the same counter.

Hint

Follow the three parts of a singleton: a static local handle, a static get() and a protected new().

Solution

class err_counter;
  static local err_counter m_inst;
  local int m_count;

  protected function new ();
  endfunction

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

  function void incr ();
    m_count++;
  endfunction

  function int count ();
    return m_count;
  endfunction
endclass

// In the scoreboard and in the monitor
err_counter m_errs = err_counter::get ();
...
m_errs.incr ();

// In the test, for example in report_phase()
err_counter m_errs = err_counter::get ();
`uvm_info ("TEST", $sformatf ("Total errors: %0d", m_errs.count ()), UVM_LOW)

Both components call get(), so both increment the same m_count. A line such as err_counter c = new(); outside the class now fails to compile.

Summary

In this tutorial, you learned:

  • A singleton has a static local handle to its only instance and a static get() function
  • get() creates the instance on the first call and returns the same instance afterwards
  • A protected constructor stops other code from creating extra instances
  • UVM uses singletons for uvm_root, the factory, the report server and the global event pool
  • Singletons suit global data, such as design parameters, but not per-instance settings
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5