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.
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
endmoduleNotice 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.
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
endmoduleCommon 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.
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
endmodulePattern 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.
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
endmodulePattern 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.
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
endmoduleA 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.
| Context | What it means | Example |
|---|---|---|
| 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.
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
endmoduleQuick Reference
| Feature | Instance (default) | Static |
|---|---|---|
| Memory | One copy per object | One copy for the entire class |
| Access via | handle.property | ClassName::property or handle.property |
| Calling a method | Need an object: t.display() | No object needed: AxiTxn::get_count() |
| Can access this? | Yes | No — no object context |
| Can access instance props? | Yes | No — only other static members |
| Typical use | addr, data, payload — per-transaction data | Counters, 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.
<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.
<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.
<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.
<code>class packet;
// Self-contained, no cross-file deps
static int tx_count = 0;
static int rx_count = 0;
endclass</code><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
<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
<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.
"My counter shows 1 even on the first transaction"
DEBUGEvery transaction's id field is one higher than expected — the first transaction reports id=1, not 0.
<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>The counter is incremented before being copied. The very first packet sees next_id == 1, not 0.
id = next_id++; — use the post-increment expression so the old value is captured before the bump.
"I reset my static in the constructor and the counter never advances"
DEBUGEvery transaction reports id = 0. The counter is frozen.
<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>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.
Initialize at declaration: static int next_id = 0; — runs once at elaboration, not per-instance.
"My regression runs out of memory after 8 hours"
DEBUGLong regressions die with an out-of-memory error. Heap profiling shows millions of packet objects still live.
<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>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.
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.
"Compiler error: 'this' is not allowed in static method"
DEBUGThe line below produces Error: 'this' may not be used inside a static method.
<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>A static method has no associated instance — there is literally no this to reference. The error is the compiler doing exactly what it should.
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.
"My package-level static reads as 'x' in test 1 but works in test 2"
DEBUGA 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.
<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>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.
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
- Use
staticonly for genuinely class-wide concepts. Unique IDs, global verbosity, shared singletons. If the value differs per object, it is not static. - Initialize statics at declaration, never in the constructor.
static int count = 0;runs once at elaboration;count = 0;insidenew()wipes the counter on every construction. - Prefer
class_name::method()overhandle.method()for static calls. The::syntax signals intent — readers immediately see the call is not per-instance. - Never store
thisinside an unbounded static collection. A static queue thatpush_backs without ever popping leaks every object it has seen — the GC cannot reclaim them while the queue holds the handle. - 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.
- Never use
thisin 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. - Never mark a static method
virtual. The two are mutually exclusive — virtual dispatch needs an instance, static has none. - Keep all
classcode (and therefore static) out of synthesisable RTL. Classes are simulation-only. Synthesis tools will fail at read-source. - Mirror critical statics to module-scope signals for waveform visibility. One
alwaysline per static is cheap insurance against next-week debugging. - 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."