Skip to content

SystemVerilog · Module 9

Static Properties & Methods

Class-wide storage, auto-ID generators, singletons, global verbosity controllers, and the static-vs-instance trap that catches every new OOP developer.

Module 9 · Page 9.7

Instance vs. Static — The Core Difference

Every property you have seen so far — addr, data, txn_id — is an instance property. Each object gets its own private copy. Change it on one object and no other object is affected.

A static property is completely different. There is exactly one copy in memory, shared by every object of that class. When one object changes it, every other object immediately sees the new value. It belongs to the class itself, not to any individual object.

Both t1 and t2 have their own private addr, data, and txn_id. But obj_count and next_id are static — one shared slot that both objects point to.

Declaring Static Properties

Add the keyword static before the data type. That is the only syntax difference. The property then lives at the class level, not the object level.

Static Property — Syntax and Behaviour
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class BusTxn;
 
    // ── Instance properties — each object owns its own copy ───
    rand bit [31:0] addr;
    rand bit [31:0] data;
    int               txn_id;
 
    // ── Static properties — ONE copy shared by all objects ────
    static int  obj_count = 0;   // how many BusTxn objects exist
    static int  next_id   = 1;   // auto-incrementing ID counter
 
    function new();
        txn_id    = next_id++;   // grab current ID, then increment
        obj_count++;             // one more object in the world
    endfunction
 
    function void display();
        $display("[BusTxn #%0d] addr=0x%08h  (total objects=%0d)",
                  txn_id, addr, obj_count);
    endfunction
 
endclass
 
module tb;
    initial begin
        BusTxn t1 = new();   // txn_id=1, obj_count=1
        BusTxn t2 = new();   // txn_id=2, obj_count=2
        BusTxn t3 = new();   // txn_id=3, obj_count=3
 
        t1.addr = 32'h1000_0000;
        t2.addr = 32'h2000_0000;
        t3.addr = 32'h3000_0000;
 
        t1.display();   // [BusTxn #1] addr=0x10000000  (total objects=3)
        t2.display();   // [BusTxn #2] addr=0x20000000  (total objects=3)
        t3.display();   // [BusTxn #3] addr=0x30000000  (total objects=3)
    end
endmodule

Notice how all three objects report total objects=3 — they all read from the same obj_count. Each object's addr is different because that is an instance property.

Declaring Static Methods

A static method belongs to the class, not to any object. You can call it without creating an object at all — just use the class name and ::.

Because a static method has no object context, it comes with one hard rule: it can only access static properties and other static methods. Instance properties and this do not exist inside a static method. The compiler will refuse to compile if you try.

Static Methods — No Object Needed
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class BusTxn;
 
    static int obj_count = 0;
    static int next_id   = 1;
 
    rand bit [31:0] addr;
    int               txn_id;
 
    function new();
        txn_id = next_id++;
        obj_count++;
    endfunction
 
    // ── Static method — callable without any object ───────────
    static function int get_count();
        return obj_count;   // OK — accessing static property
    endfunction
 
    static function void reset_counter();
        obj_count = 0;      // OK — modifying static property
        next_id   = 1;
        $display("[BusTxn] Counter reset");
    endfunction
 
    // ── What static methods CANNOT do ────────────────────────
    // static function void broken();
    //     addr = 0;       ← ERROR: instance property inside static method
    //     this.txn_id = 0; ← ERROR: 'this' does not exist here
    // endfunction
 
endclass
 
module tb;
    initial begin
        // Call static method BEFORE creating any object
        $display("Objects before: %0d", BusTxn::get_count());  // 0
 
        BusTxn t1 = new();
        BusTxn t2 = new();
 
        $display("Objects after:  %0d", BusTxn::get_count());  // 2
 
        // Reset and verify
        BusTxn::reset_counter();
        $display("After reset:    %0d", BusTxn::get_count());  // 0
    end
endmodule

Common Use Cases in Verification

Auto-Incrementing ID

Every new transaction gets a unique ID automatically. No external counter variable needed.

Object Counter

Track how many instances of a class were created. Useful for memory leak detection and coverage.

Global Verbosity

One static verbosity level controls all instances of a driver or monitor — change it once, affects all.

Shared Error Count

All scoreboard instances increment the same static error counter. One call to get the total at the end.

Singleton Instance

A static handle pointing to one shared instance — ensure a resource (like a logger) is only created once.

Global Config

Store protocol-wide defaults (e.g., default timeout, bus width) at class level rather than passing them into every constructor.

Pattern 1 — Auto-Incrementing ID Generator

The most common static pattern in verification. Every new transaction object gets a globally unique ID without any code in the testbench to track the counter. The class handles it internally.

Pattern 1 — Auto-Incrementing Unique ID
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class Txn;
 
    static int next_uid = 1000;  // start IDs at 1000 for readability
 
    int    uid;    // instance — each object gets its own unique value
    string kind;
 
    function new(string kind = "generic");
        uid        = next_uid++;   // atomically grab then increment
        this.kind  = kind;
    endfunction
 
    function void display();
        $display("[UID:%0d] %s", uid, kind);
    endfunction
 
endclass
 
module tb;
    initial begin
        Txn a = new("write");
        Txn b = new("read");
        Txn c = new("write");
 
        a.display();   // [UID:1000] write
        b.display();   // [UID:1001] read
        c.display();   // [UID:1002] write
 
        // Guaranteed unique — no collisions, no manual tracking
    end
endmodule

Pattern 2 — Shared Verbosity Level

Imagine you have 50 driver objects in a large testbench. You want to turn on debug logging for all of them with one line of code — not by looping through every handle. A static verbosity property solves this cleanly.

Pattern 2 — Static Verbosity Shared Across All Instances
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class ApbDriver;
 
    // All drivers share this — change once, affects all
    static int verbosity = 0;   // 0=quiet, 1=normal, 2=debug
 
    string name;
 
    function new(string n);
        name = n;
    endfunction
 
    task drive(bit [31:0] addr, bit [31:0] data);
        if (verbosity >= 1)
            $display("[%s] Driving addr=0x%08h  data=0x%08h",
                      name, addr, data);
        if (verbosity >= 2)
            $display("[%s] DEBUG: bus timing details would go here", name);
        // ... drive signals ...
    endtask
 
    static function void set_verbosity(int v);
        verbosity = v;
        $display("[ApbDriver] Verbosity set to %0d (affects ALL drivers)", v);
    endfunction
 
endclass
 
module tb;
    ApbDriver d1 = new("drv_0");
    ApbDriver d2 = new("drv_1");
    ApbDriver d3 = new("drv_2");
 
    initial begin
        // Turn on debug for ALL drivers — one line, no loop
        ApbDriver::set_verbosity(2);
 
        d1.drive(32'h4000_0000, 32'hFF00_FF00);
        d2.drive(32'h5000_0004, 32'h0A0B_0C0D);
        d3.drive(32'h6000_0008, 32'h1234_5678);
    end
endmodule

Pattern 3 — Singleton Instance

Some objects should only exist once in the entire simulation — a shared logger, a global error tracker, a central configuration store. The singleton pattern uses a static handle to guarantee this.

Pattern 3 — Singleton (One Shared Instance)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class SimLogger;
 
    // Static handle — points to the one shared instance
    static SimLogger instance;
 
    int   error_count;
    int   warning_count;
 
    // Private constructor — prevents external new()
    local function new();
        error_count   = 0;
        warning_count = 0;
    endfunction
 
    // Static factory — returns existing instance or creates one
    static function SimLogger get();
        if (instance == null)
            instance = new();   // only created once
        return instance;
    endfunction
 
    function void log_error(string msg);
        error_count++;
        $error("[LOGGER] %s  (total errors: %0d)", msg, error_count);
    endfunction
 
    function void report();
        $display("[LOGGER] Final: errors=%0d  warnings=%0d",
                  error_count, warning_count);
    endfunction
 
endclass
 
module tb;
    initial begin
        // Anyone can call SimLogger::get() — always same object
        SimLogger::get().log_error("Scoreboard: addr mismatch on txn #5");
        SimLogger::get().log_error("Monitor: unexpected response on chan 2");
 
        // Report at end of test
        SimLogger::get().report();
        // [LOGGER] Final: errors=2  warnings=0
    end
endmodule

A Common Confusion — static Lifetime vs static Member

SystemVerilog uses the word static in two completely different contexts and they are often confused by engineers new to the language.

ContextWhat it meansExample
Static class member (this page)A property or method that belongs to the class, not to any object. One shared copy.static int count; inside a class
Static lifetime on a task/function (Module 6)The local variables inside a task/function persist between calls rather than being created fresh each time. Opposite of automatic.function static int counter();

Full Working Example — AXI Transaction with All Static Patterns

One complete class that combines auto-ID, object counter, shared verbosity, and a static utility method — all in one place.

Full Example — AXI Transaction with Static Members
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class AxiTxn;
 
    // ── Static (class-level) ──────────────────────────────────
    static int next_id      = 1;
    static int total_count  = 0;
    static int error_count  = 0;
    static int verbosity    = 0;
 
    // ── Instance (per-object) ─────────────────────────────────
    rand bit [31:0] addr;
    rand bit [31:0] data;
    bit               write;
    bit [1:0]        resp;
    int               uid;
 
    constraint c_align { addr[1:0] == 2'b00; }
 
    function new(bit wr = 0);
        uid         = next_id++;
        write       = wr;
        total_count++;
        if (verbosity >= 2)
            $display("[AxiTxn] Created UID=%0d (total=%0d)",
                      uid, total_count);
    endfunction
 
    function void set_resp(bit [1:0] r);
        resp = r;
        if (r != 2'b00) begin
            error_count++;           // shared error counter
            if (verbosity >= 1)
                $display("[AxiTxn UID:%0d] Error response 0x%02b",
                          uid, r);
        end
    endfunction
 
    function void display();
        $display("[AXI UID:%0d] %s  addr=0x%08h  data=0x%08h  resp=%02b",
                  uid, write ? "WR" : "RD", addr, data, resp);
    endfunction
 
    // ── Static utility methods ────────────────────────────────
    static function void set_verbosity(int v);
        verbosity = v;
    endfunction
 
    static function void print_stats();
        $display("[AxiTxn] Stats: created=%0d  errors=%0d  next_id=%0d",
                  total_count, error_count, next_id);
    endfunction
 
    static function void reset_stats();
        total_count = 0;
        error_count = 0;
        next_id     = 1;
    endfunction
 
endclass
 
module tb;
    initial begin
 
        AxiTxn::set_verbosity(1);   // turn on normal logging globally
 
        AxiTxn t1 = new(1);   // write
        AxiTxn t2 = new(0);   // read
        AxiTxn t3 = new(1);   // write
 
        void'(t1.randomize()); t1.set_resp(2'b00);  // OKAY
        void'(t2.randomize()); t2.set_resp(2'b10);  // SLVERR — counts error
        void'(t3.randomize()); t3.set_resp(2'b00);  // OKAY
 
        t1.display();
        t2.display();
        t3.display();
 
        AxiTxn::print_stats();
        // [AxiTxn] Stats: created=3  errors=1  next_id=4
 
    end
endmodule

Quick Reference

FeatureInstance (default)Static
MemoryOne copy per objectOne copy for the entire class
Access viahandle.propertyClassName::property or handle.property
Calling a methodNeed an object: t.display()No object needed: AxiTxn::get_count()
Can access this?YesNo — no object context
Can access instance props?YesNo — only other static members
Typical useaddr, data, payload — per-transaction dataCounters, IDs, verbosity, shared config, singleton

Verification Usage — Where Static Earns Its Keep in Real Testbenches

Static properties and methods are not theoretical curiosities — they sit at the heart of three patterns you will see in every production UVM environment. The cleanest way to understand when to reach for static is to look at the canonical roles it plays in real verification IP.

Pattern A — Unique Transaction IDs (auto-ID generator)

Every transaction class in a serious testbench needs a globally unique ID for logging, scoreboard correlation, and waveform tagging. Without static, you would need to thread a counter through every create() call. With static, each transaction stamps itself at construction time.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<code>class axi_transaction;
  static int next_id = 0;          // class-wide counter
  int        id;                    // per-instance copy
  rand bit [31:0] addr;
  rand bit [31:0] data;
 
  function new();
    id = next_id++;                 // atomic stamp + increment
  endfunction
 
  function void display();
    $display("[AXI TXN #%0d] addr=0x%08h data=0x%08h", id, addr, data);
  endfunction
endclass
 
// 10,000 transactions across the entire sim → IDs 0..9999, no collisions.</code>

Pattern B — Global Verbosity / Severity Control

Every component (driver, monitor, scoreboard, sequencer) needs to honour a single global verbosity setting. A static field on a logger class — toggled once from the top of the test — fans out to thousands of call sites with zero handle plumbing.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<code>class tb_logger;
  typedef enum { LOW, MED, HIGH, DEBUG } verbosity_e;
  static verbosity_e level = MED;
 
  static function void log(verbosity_e v, string msg);
    if (v <= level) $display("[%0t] %s", $time, msg);
  endfunction
endclass
 
// Test setup:
initial tb_logger::level = tb_logger::HIGH;
 
// Anywhere in the env:
tb_logger::log(tb_logger::DEBUG, "AXI handshake observed");</code>

Pattern C — Singleton Configuration Database

UVM's uvm_config_db and uvm_factory are, under the hood, singletons — one object instance per simulation, accessed through a static get() method. The pattern below is what you would build if you were writing your own minimal UVM-style env.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<code>class env_config;
  local static env_config m_inst;       // hidden, class-wide
  rand bit         coverage_on  = 1'b1;
  rand int         drain_cycles = 100;
 
  protected function new(); endfunction  // no public construction
 
  static function env_config get();
    if (m_inst == null) m_inst = new();
    return m_inst;
  endfunction
endclass
 
// Driver, monitor, scoreboard all share one config:
env_config cfg = env_config::get();
if (cfg.coverage_on) ...</code>

Simulation Behavior — How the Simulator Allocates and Schedules Static

Understanding what the simulator actually does with static demystifies most of the surprises around lifetime, initialization, and method dispatch.

Storage allocation happens once, at elaboration

When the simulator loads your compiled design, every static property gets exactly one memory cell allocated, regardless of how many instances of the class are eventually created — including zero. The storage is live before initial blocks even start.

Initializer evaluation order is undefined across files

A static initializer like static int count = 0; runs once before initial begins, but the order in which static initializers from different files run is implementation-defined. Do not write static initializers that depend on each other across compilation units.

Static methods compile to plain function calls

An instance method like obj.foo() conceptually passes a hidden this pointer. A static method like my_class::foo() has no this, so the simulator compiles it as a direct function call — no dispatch table lookup, no implicit pointer push. This is one of the few places where static is measurably faster than instance access.

The lifetime is the entire simulation

Static properties persist from $time = 0 until $finish. They survive every garbage-collection sweep that reclaims regular class instances, because the storage is not owned by any single object.

✅ Safe Static-Initialization Pattern
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<code>class packet;
  // Self-contained, no cross-file deps
  static int tx_count = 0;
  static int rx_count = 0;
endclass</code>
❌ Order-Dependent Static Initialization
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<code>// file_a.sv
class a_cls;
  static int x = b_cls::y + 1;   // order-defined !
endclass
 
// file_b.sv
class b_cls;
  static int y = 10;
endclass</code>

Waveform Analysis — Making Static State Visible

Class-internal state — including static fields — does not naturally appear in the waveform viewer. This is the single biggest blind spot when debugging class-based testbenches. The fix is to deliberately mirror the critical static fields to module-scope signals.

The visibility problem

You can $display a static like packet::tx_count from anywhere in your testbench, but you cannot place it in the waveform window and watch it ramp over time. The waveform viewer only sees module-scope variables and interface signals.

The mirroring fix

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<code>module tb;
  // Module-scope signals visible in waveform:
  int wave_tx_count;
  int wave_rx_count;
 
  // Static-mirror process — runs every simulation cycle:
  always @(packet::tx_count) wave_tx_count = packet::tx_count;
  always @(packet::rx_count) wave_rx_count = packet::rx_count;
endmodule</code>

Now you can drag wave_tx_count into the waveform viewer and watch transaction generation as a stepping integer that ramps from 0 upward. The same mirror trick works for any static — coverage counters, configuration knobs, singleton state.

ASCII view of a static-counter waveform

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<code>           0ns      100ns     200ns     300ns     400ns     500ns
clk        _|^|_|^|_|^|_|^|_|^|_|^|_|^|_|^|_|^|_|^|_|^|_|^|_|^|_|^|_
tx_count   ===0=====1=====2=====3=====4=====5=====6=====7=====8=====
rx_count   ===0=====0=====0=====1=====2=====3=====4=====5=====6=====
                          ^                 ^
                          |                 |
                  first packet driven    scoreboard catches up</code>

Industry Insights — Hard-Won Lessons From Production Testbenches

Debugging Academy — Five Real Bugs Caused by Static

Each lab below is drawn from a real production debugging session. Read the symptom, study the buggy code, and try to spot the defect before reading the fix.

1

"My counter shows 1 even on the first transaction"

DEBUG
Symptom

Every transaction's id field is one higher than expected — the first transaction reports id=1, not 0.

Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<code>class packet;
  static int next_id = 0;
  int id;
  function new();
    next_id++;          // BUG: post-increment used as standalone statement
    id = next_id;       // assigns the *new* value, not the old one
  endfunction
endclass</code>
Root Cause

The counter is incremented before being copied. The very first packet sees next_id == 1, not 0.

Fix

id = next_id++; — use the post-increment expression so the old value is captured before the bump.

2

"I reset my static in the constructor and the counter never advances"

DEBUG
Symptom

Every transaction reports id = 0. The counter is frozen.

Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<code>class packet;
  static int next_id;
  int id;
  function new();
    next_id = 0;         // BUG: resets the class-wide counter every construction
    id = next_id++;
  endfunction
endclass</code>
Root Cause

Resetting a static inside the constructor wipes the shared counter every time a new instance is built — so the first packet bumps it to 1, the next packet resets it back to 0 and bumps to 1 again, and so on.

Fix

Initialize at declaration: static int next_id = 0; — runs once at elaboration, not per-instance.

3

"My regression runs out of memory after 8 hours"

DEBUG
Symptom

Long regressions die with an out-of-memory error. Heap profiling shows millions of packet objects still live.

Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<code>class packet_log;
  static packet history[$];   // unbounded static queue
 
  static function void record(packet p);
    history.push_back(p);     // BUG: nothing ever pops
  endfunction
endclass</code>
Root Cause

The static queue holds a live handle to every packet ever created. Because the queue itself is static, it is never reclaimed — so neither are any of the packets it points to. Classic memory leak.

Fix

Either bound the queue (if (history.size() > 1000) history.pop_front();) or store only a compact summary (id, time, type) instead of the full handle.

4

"Compiler error: 'this' is not allowed in static method"

DEBUG
Symptom

The line below produces Error: 'this' may not be used inside a static method.

Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<code>class driver;
  int        local_id;
  static int active_count;
 
  static function void enter();
    active_count++;
    this.local_id = active_count;   // BUG: 'this' has no meaning here
  endfunction
endclass</code>
Root Cause

A static method has no associated instance — there is literally no this to reference. The error is the compiler doing exactly what it should.

Fix

Either (a) make the method non-static if it really does need per-instance state, or (b) pass the target object in as an argument: static function void enter(driver d); d.local_id = ++active_count; endfunction.

5

"My package-level static reads as 'x' in test 1 but works in test 2"

DEBUG
Symptom

A test that reads a static field across compilation units sees x on the first run after a fresh compile, then works correctly on every later run.

Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<code>// file_setup.sv  — compiled separately
class env_config;
  static int    drain_cycles = compute_drain();    // depends on extern
  static function int compute_drain();
    return ext_pkg::default_drain;                 // cross-package read
  endfunction
endclass</code>
Root Cause

Static initializers in different compilation units fire in implementation-defined order. If env_config::drain_cycles initializes before ext_pkg has finished its own static setup, it reads an uninitialized value.

Fix

Move cross-unit dependencies into an explicit initialisation step called from the test's initial block, instead of relying on inline static initializers.

Interview Q&A — Twelve Questions You Will Be Asked

An instance property has one independent storage cell per object — modifying it on one object does not affect any other. A static property is shared by every object of the class (and accessible without any object via class_name::field); changes are visible to all instances. Static is allocated once at elaboration and persists for the whole simulation.

Best Practices — Ten Rules for Disciplined Use of Static

  1. Use static only for genuinely class-wide concepts. Unique IDs, global verbosity, shared singletons. If the value differs per object, it is not static.
  2. Initialize statics at declaration, never in the constructor. static int count = 0; runs once at elaboration; count = 0; inside new() wipes the counter on every construction.
  3. Prefer class_name::method() over handle.method() for static calls. The :: syntax signals intent — readers immediately see the call is not per-instance.
  4. Never store this inside an unbounded static collection. A static queue that push_backs without ever popping leaks every object it has seen — the GC cannot reclaim them while the queue holds the handle.
  5. Use the singleton pattern for shared configuration, not as a default. Singletons make testing harder (hidden global state). Reach for one only when every component truly needs the same instance.
  6. Never use this in a static method. It is a compile error. If you need instance state, take the handle as an argument or make the method non-static.
  7. Never mark a static method virtual. The two are mutually exclusive — virtual dispatch needs an instance, static has none.
  8. Keep all class code (and therefore static) out of synthesisable RTL. Classes are simulation-only. Synthesis tools will fail at read-source.
  9. Mirror critical statics to module-scope signals for waveform visibility. One always line per static is cheap insurance against next-week debugging.
  10. For multi-test regressions sharing one elaboration, reset statics explicitly at test entry. Static state outliving a test is the most common cause of "passes alone, fails in regression."