Each class instance normally has its own independent copy of every internal variable and its own separate calls to every method. SystemVerilog's static keyword changes that default for a specific property or method — letting you share a single copy across every instance of a class instead, or call a method without ever needing an object at all.

10 min read | Intermediate Level

What You'll Learn

  • The default behavior: every class instance gets its own independent copy of each property
  • How a static property becomes one shared copy across every instance instead
  • What a static method can and can't access, compared to a regular method
  • Why a static method can never access a non-static (per-instance) member

The Default: Each Instance Gets Its Own Copy

Before introducing static, here's the default behavior it changes — each class instance holding its own independent copy of every internal variable:


class Packet;
	bit [15:0] 	addr;
	bit [7:0] 	data;

	function new (bit [15:0] ad, bit [7:0] d);
		addr = ad;
		data = d;
		$display ("addr=0x%0h data=0x%0h", addr, data);
	endfunction
endclass

module tb;
	initial begin
		Packet 	p1, p2, p3;
		p1 = new (16'hdead, 8'h12);
		p2 = new (16'hface, 8'hab);
		p3 = new (16'hcafe, 8'hfc);
	end
endmodule

Each of the class objects p1, p2, p3 has its own addr and data — changing one has no effect on the others.

Output

Output
ncsim> run
addr=0xdead data=0x12
addr=0xface data=0xab
addr=0xcafe data=0xfc
ncsim: *W,RNQUIE: Simulation is complete.

Static Variables

When a variable inside a class is declared static, that variable becomes the only copy shared across every instance of the class. To demonstrate, here's a static counter compared against a normal (non-static) counter, both incremented inside new() so they update every time an object is created:


class Packet;
	bit [15:0] 	addr;
	bit [7:0] 	data;
	static int 	static_ctr = 0;
	int 	ctr = 0;

	function new (bit [15:0] ad, bit [7:0] d);
		addr = ad;
		data = d;
		static_ctr++;
		ctr++;
		$display ("static_ctr=%0d ctr=%0d addr=0x%0h data=0x%0h", static_ctr, ctr, addr, data);
	endfunction
endclass

module tb;
	initial begin
		Packet 	p1, p2, p3;
		p1 = new (16'hdead, 8'h12);
		p2 = new (16'hface, 8'hab);
		p3 = new (16'hcafe, 8'hfc);
	end
endmodule

Output

Output
ncsim> run
static_ctr=1 ctr=1 addr=0xdead data=0x12
static_ctr=2 ctr=1 addr=0xface data=0xab
static_ctr=3 ctr=1 addr=0xcafe data=0xfc
ncsim: *W,RNQUIE: Simulation is complete.

static_ctr is shared between p1, p2, and p3, so it climbs to 3 as each new packet is created. ctr, on the other hand, is not static, so every object gets its own fresh copy — each one starts at 0 and is incremented exactly once inside its own constructor, which is why ctr shows 1 for every single object, never climbing higher.

Declaring a variable static is useful whenever you need to know the total number of objects (like packets) created so far, across the entire testbench, rather than just within one specific object.

Quick Check

Q: If a fourth object, p4, were created right after this code, what would static_ctr and ctr show for it?

Show Answer

static_ctr = 4, ctr = 1. static_ctr keeps counting up across every object ever created, while ctr always starts fresh at 0 for a brand new object and gets incremented exactly once in its own constructor call, landing on 1 every time.

Static Functions

A static method follows all the usual class scoping and access rules, with one key difference: it can be called even without any object of the class ever being instantiated. A static method has no access to non-static (per-instance) members, but it can directly access static class properties or call other static methods of the same class. Calling a static method by class name uses the scope resolution operator :::


class Packet;
	static int ctr=0;

   function new ();
      ctr++;
   endfunction

	static function get_pkt_ctr ();
		$display ("ctr=%0d", ctr);
	endfunction

endclass

module tb;
	Packet pkt [6];
	initial begin
		for (int i = 0; i < $size(pkt); i++) begin
			pkt[i] = new;
		end
		Packet::get_pkt_ctr(); 	// Static call using :: operator
		pkt[5].get_pkt_ctr(); 	// Normal call using instance
	end
endmodule

Output

Output
ncsim> run
ctr=6
ctr=6
ncsim: *W,RNQUIE: Simulation is complete.

Both calls — Packet::get_pkt_ctr() and pkt[5].get_pkt_ctr() — print the exact same value, because there's only one shared ctr regardless of which syntax is used to reach it. Calling a static method through a specific instance doesn't give it any special access to that instance's own data — it's still the same static method operating on the same shared state either way.

Now let's add a non-static member called mode and try to reference it from the static function:


class Packet;
	static int ctr=0;
   bit [1:0] mode;

   function new ();
      ctr++;
   endfunction

	static function get_pkt_ctr ();
		$display ("ctr=%0d mode=%0d", ctr, mode);
	endfunction
endclass

This isn't allowed, and results in a compile-time error:

Output
                $display ("ctr=%0d mode=%0d", ctr, mode);
                                                      |
ncvlog: *E,CLSNSU (static-function.sv,10|40): A static class method cannot access non static class members.

This makes sense once you consider that a static method can be called with no object ever having been created — get_pkt_ctr() has no specific instance to pull mode from, since mode only exists once an actual object exists to hold it.

A static method can never be declared virtual either — the same restriction covered for constructors in SystemVerilog Class Constructor. Virtual dispatch depends on an object's actual runtime type, but a static method call may not even involve an object at all.

Quick Check

Q: Could get_pkt_ctr() call a different static method of the same class from inside itself?

Show Answer

Yes. A static method can freely access static properties and call other static methods of the same class — the restriction is specifically about non-static (per-instance) members, not about static methods calling each other.

Where Are Static Variables Used?

A shared static counter is a common way to track a running total across an entire test — for example, the total number of transactions generated so far by every generator instance combined, rather than just one specific generator's own count.

Common Beginner Mistakes

Mistake #1: Assuming every property is shared across instances by default

As the first example shows, the default behavior is the opposite — every property gets its own independent copy per object. Why? Only properties explicitly declared static are shared; everything else follows the normal one-copy-per-object rule.

Mistake #2: Trying to access a non-static member from inside a static method

As the mode example shows, this is a compile-time error. Why? A static method can be called with no object ever created, so it has no specific instance to resolve a non-static member against — there's simply nothing for mode to mean in that context.

Mistake #3: Assuming instance-based and class-based static calls behave differently

Packet::get_pkt_ctr() and pkt[5].get_pkt_ctr() produce identical results. Why? A static method operates on shared class-level state regardless of the syntax used to call it — calling it "through" a specific object doesn't give it any special access to that object's own data.

Mistake #4: Declaring a static method as virtual

This isn't allowed, for the same reason a constructor can't be virtual. Why? Virtual dispatch depends on resolving a call based on an actual object's runtime type, but a static method call doesn't necessarily involve any object at all.

Practice Exercise

Challenge: Track a Running Byte Total

Extend the Packet class with a static int total_bytes = 0; property, incremented in the constructor by however many bytes each packet's data field represents (assume 1 byte per packet, for simplicity). Add a static function called report() that prints the total number of packets created and the total bytes across all of them.

Hint

Follow the same pattern as static_ctr — increment total_bytes inside the constructor, alongside the existing counter.

Solution

class Packet;
	bit [15:0] 	addr;
	bit [7:0] 	data;
	static int 	static_ctr = 0;
	static int 	total_bytes = 0;

	function new (bit [15:0] ad, bit [7:0] d);
		addr = ad;
		data = d;
		static_ctr++;
		total_bytes++;    // 1 byte per packet, for simplicity
	endfunction

	static function report ();
		$display ("Packets created: %0d, Total bytes: %0d", static_ctr, total_bytes);
	endfunction
endclass

module tb;
	initial begin
		Packet 	p1, p2, p3;
		p1 = new (16'hdead, 8'h12);
		p2 = new (16'hface, 8'hab);
		p3 = new (16'hcafe, 8'hfc);

		Packet::report ();
	end
endmodule

This prints Packets created: 3, Total bytes: 3. Both static_ctr and total_bytes are shared across every Packet object, so report() can summarize the entire run without needing a reference to any specific packet at all.

Summary

In this tutorial, you learned:

  • By default, every class instance gets its own independent copy of each property
  • A property declared static becomes one shared copy across every instance of the class instead
  • A static method can be called even with no object created, using ClassName::method_name(), and behaves identically whether called that way or through a specific instance
  • A static method can never access non-static members, and can never be declared virtual
  • A shared static counter is a common way to track a running total across an entire testbench, independent of any one specific object
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5