Part 2 · OOP for Verification · Intermediate

Singletons & Config Objects

Protected constructor + static get(), the central config object pattern, global-access tradeoffs, and config vs plusargs.

The singleton: exactly one instance, by construction

Some objects must exist exactly once: the factory's registry, a global error counter, the central configuration. The singleton pattern enforces this in the type system rather than by convention. Two ingredients: a protected constructor — so no code outside the class (or its subclasses) can call new() — and a static get() accessor that lazily constructs the one instance on first call and returns the same handle forever after. Because static class members exist once per class regardless of instances, the static handle IS the single point of truth.

systemverilog
class tb_config;
  // ---- singleton machinery ----
  protected static tb_config m_inst;       // the one handle
  protected function new();  endfunction   // outsiders cannot new() this

  static function tb_config get();
    if (m_inst == null) m_inst = new();    // lazy, first-use construction
    return m_inst;
  endfunction

  // ---- the actual configuration payload ----
  int unsigned n_txns        = 100;
  bit          enable_cov    = 1;
  bit          err_inject    = 0;
  int unsigned timeout_ns    = 100_000;
  string       test_name     = "smoke";

  function string convert2string();
    return $sformatf("test=%s n=%0d cov=%0b err=%0b to=%0dns",
                     test_name, n_txns, enable_cov, err_inject, timeout_ns);
  endfunction
endclass

// anywhere in the bench:
//   tb_config cfg = tb_config::get();
//   if (cfg.err_inject) ...

Why protected, not local

A local constructor would also block outside construction, but it blocks subclasses too. protected keeps the door open for a derived config (a project-specific tb_config extension) while still preventing arbitrary instantiation. Note SystemVerilog will not stop a subclass from being constructed elsewhere through its own constructor — singleton-ness in SV is strong convention plus access control, not an airtight guarantee.


The central config object pattern

Rather than sprinkling knobs as plusargs reads and magic numbers across components, a testbench concentrates them in one config object that the test populates before build and every component reads at construction or start-of-run. The flow has a strict order: parse command line, fill config, build environment from config, run. Components never write the config after build — it is frozen input, not shared mutable state.

diagram
CONFIG OBJECT LIFECYCLE

  +ARGS on command line
       │
       ▼
  test start:  cfg = tb_config::get()
               $value$plusargs("n_txns=%d", cfg.n_txns)
               if ($test$plusargs("ERR")) cfg.err_inject = 1
       │
       ▼            cfg is now FROZEN (convention)
  build env:   gen  = new(cfg.n_txns ...)
               drv  = new(...);  if (cfg.err_inject) install callback
               cov  = cfg.enable_cov ? new(...) : null
       │
       ▼
  run:         components READ cfg; nobody writes it
       │
       ▼
  report:      print cfg.convert2string() in the log header
               (every log states the exact knob settings it ran with)
systemverilog
module test_top;
  initial begin
    tb_config cfg = tb_config::get();

    // command line → config, ONCE, up front
    void'($value$plusargs("N_TXNS=%d",   cfg.n_txns));
    void'($value$plusargs("TIMEOUT=%d",  cfg.timeout_ns));
    if ($test$plusargs("ERR_INJECT")) cfg.err_inject = 1;
    if ($test$plusargs("NO_COV"))     cfg.enable_cov = 0;

    $display("CONFIG: %s", cfg.convert2string());

    // build & run env — components call tb_config::get() themselves
    // or receive cfg in their constructors (preferred: explicit handle)
  end
endmodule

Tradeoffs: global access vs explicit handles, config vs plusargs

The cost of global access

  • Hidden coupling — any component can read (or worse, write) the singleton; the dependency does not appear in any constructor signature.

  • Test interference risk — a singleton persists across everything in one simulation; stale state from one phase leaks into the next.

  • Reuse friction — a block-level env hard-wired to a global config cannot be instantiated twice with different settings at chip level.

  • Middle path — keep the singleton as the root, but PASS cfg handles down explicitly at construction; components depend on a handle, not on the global accessor. This is exactly the tension uvm_config_db resolves with hierarchical, path-scoped lookup.

Config object vs raw plusargs

  • Plusargs scattered through components — untyped strings, no central record of available knobs, no log of effective values; grep is your documentation.

  • Config object — typed fields with defaults, parsed once, printable in one line, overridable programmatically by a test without any command line.

  • Rule of thumb — plusargs are the OUTER interface (regression scripts), the config object is the INNER one (components); the test converts between them in one place.

Interview angle

  • "Write a singleton in SV" — protected new + static get with lazy construction; expect the follow-up "why protected, not local".

  • "Why are singletons criticized?" — hidden coupling and untestable global state; mention the pass-the-handle mitigation.

  • "How does UVM solve config distribution?" — uvm_config_db: the same idea with hierarchical scoping instead of one flat global.

Key takeaways

  • Singleton = protected constructor + static handle + lazy static get() — one instance by access control.

  • Centralize knobs in a config object: parse plusargs once, freeze before build, print in the log header.

  • Prefer passing config handles down explicitly; reserve global get() for the composition root.

  • Plusargs are the script-facing interface; the config object is the component-facing one.

Common pitfalls

  • Components writing the config mid-run — global mutable state, irreproducible failures.

  • Reading plusargs deep inside components — knob inventory becomes undiscoverable and unlogged.

  • Singleton holding per-test state in multi-test flows — stale values leak across tests in one simulation.

  • local constructor instead of protected — blocks the derived project config you will inevitably need.