SystemVerilog Classes and Objects - Complete Guide
Classes are the foundation of object-oriented programming in SystemVerilog. They enable you to create reusable, modular verification components by encapsulating data and behavior together. This guide covers everything you need to know about classes and objects - from basic syntax to advanced patterns.
What is a Class?
A class is a user-defined data type that bundles properties (data) and methods (functions/tasks) into a single unit. Unlike modules, classes are dynamic - objects are created at runtime and managed in heap memory.
classDiagram
class Transaction {
+bit[31:0] addr
+bit[63:0] data
+op_type_e op
+new()
+display()
+copy()
}
class Packet {
+bit[7:0] payload[]
+bit[15:0] length
+bit[31:0] crc
+new()
+calc_crc()
+pack()
}
Class Declaration
The basic syntax for declaring a class:
class Transaction;
// Properties (member variables)
bit [31:0] addr;
bit [63:0] data;
bit write;
int id;
// Methods (member functions)
function void display();
$display("[%0d] %s addr=0x%08h data=0x%016h",
id, write ? "WR" : "RD", addr, data);
endfunction
endclass
Objects and Handles
A class variable is called a handle - it holds a reference to an object, not the object itself. Objects are created using the new constructor.
// Declare handle (initially null)
Transaction tr;
// Create object and assign to handle
tr = new();
// Access members through handle
tr.addr = 32'h1000;
tr.data = 64'hDEAD_BEEF;
tr.display();
Handle vs Object
flowchart LR
subgraph Stack
H1["tr1 (handle)"]
H2["tr2 (handle)"]
end
subgraph Heap
O1["Transaction Object\naddr=0x1000\ndata=0xBEEF"]
end
H1 --> O1
H2 --> O1
Multiple handles can point to the same object. When you assign one handle to another, you copy the reference, not the object:
Transaction tr1, tr2;
tr1 = new(); // Create object
tr1.addr = 32'h1000;
tr2 = tr1; // tr2 points to SAME object
tr2.addr = 32'h2000; // Modifies the shared object
$display("tr1.addr = 0x%h", tr1.addr); // Prints 0x2000!
Constructors
The constructor is a special function named new that initializes objects. It's called automatically when you create an object.
Default Constructor
class Transaction;
bit [31:0] addr;
bit [63:0] data;
// Explicit default constructor
function new();
addr = 0;
data = 0;
$display("Transaction created");
endfunction
endclass
Parameterized Constructor
class Transaction;
bit [31:0] addr;
bit [63:0] data;
string name;
// Constructor with parameters
function new(string name = "Transaction", bit [31:0] addr = 0);
this.name = name;
this.addr = addr;
this.data = '0;
endfunction
endclass
// Usage
Transaction tr1 = new(); // Uses defaults
Transaction tr2 = new("Config", 32'hFF00); // Custom values
Transaction tr3 = new(.addr(32'h1000)); // Named argument
The 'this' Keyword
The this keyword refers to the current object instance. It's essential when parameter names match property names:
class Packet;
int length;
int id;
function new(int length, int id);
this.length = length; // this.length = property, length = parameter
this.id = id;
endfunction
function void set_length(int length);
this.length = length;
endfunction
// Return reference to self for method chaining
function Packet configure(int length, int id);
this.length = length;
this.id = id;
return this;
endfunction
endclass
Properties
Properties are variables declared inside a class. They can be any SystemVerilog data type.
class AdvancedTransaction;
// Scalar types
bit [31:0] addr;
logic [7:0] byte_enable;
int burst_length;
real timestamp;
string name;
// Enumerated type
typedef enum {READ, WRITE, IDLE} op_e;
op_e operation;
// Arrays
bit [7:0] payload[]; // Dynamic array
bit [31:0] fixed_data[4]; // Fixed array
int queue[$]; // Queue
int assoc[string]; // Associative array
// Nested object (composition)
Address addr_obj;
// Class handle array
Transaction history[$];
endclass
Methods
Methods are functions or tasks declared inside a class. They operate on the object's properties.
Functions vs Tasks
class Driver;
virtual interface axi_if vif;
int packets_sent;
// Function: No time consumption, can return value
function int get_count();
return packets_sent;
endfunction
// Function with void return
function void reset_count();
packets_sent = 0;
endfunction
// Task: Can consume time, use for driving signals
task drive(Transaction tr);
@(posedge vif.clk);
vif.addr <= tr.addr;
vif.data <= tr.data;
vif.valid <= 1'b1;
@(posedge vif.clk);
vif.valid <= 1'b0;
packets_sent++;
endtask
// Task with delay
task wait_cycles(int n);
repeat(n) @(posedge vif.clk);
endtask
endclass
Method Arguments
class Calculator;
// Default arguments
function int add(int a, int b = 0, int c = 0);
return a + b + c;
endfunction
// Reference arguments (modified in place)
function void swap(ref int a, ref int b);
int temp = a;
a = b;
b = temp;
endfunction
// Input (default), output, and inout
function void process(input int in_val,
output int out_val,
inout int io_val);
out_val = in_val * 2;
io_val = io_val + in_val;
endfunction
endclass
Static Members
Static members belong to the class itself, not individual objects. They're shared across all instances.
Static Properties
class Transaction;
// Instance properties (each object has its own)
bit [31:0] addr;
int id;
// Static property (shared by all objects)
static int count = 0;
static int max_id = 0;
function new();
count++; // Increment shared counter
this.id = count; // Assign unique ID
if(id > max_id) max_id = id;
endfunction
// Static method to access static data
static function int get_count();
return count;
endfunction
static function void reset_count();
count = 0;
max_id = 0;
endfunction
endclass
// Usage
initial begin
Transaction t1 = new(); // count = 1, id = 1
Transaction t2 = new(); // count = 2, id = 2
Transaction t3 = new(); // count = 3, id = 3
// Access static member through class name
$display("Total transactions: %0d", Transaction::count);
$display("Max ID: %0d", Transaction::get_count());
// Or through any instance
$display("Count via t1: %0d", t1.count);
end
Static Methods
Static methods can only access static members (no this reference):
class Logger;
static string log_file = "sim.log";
static int verbosity = 1;
static int msg_count = 0;
// Static method - no access to instance data
static function void log(string msg, int level = 1);
if(level <= verbosity) begin
$display("[%0t] %s", $time, msg);
msg_count++;
end
endfunction
static function void set_verbosity(int v);
verbosity = v;
endfunction
static function int get_msg_count();
return msg_count;
endfunction
endclass
// Use without creating object
initial begin
Logger::set_verbosity(2);
Logger::log("Simulation started");
Logger::log("Debug info", 2);
end
Access Modifiers
SystemVerilog provides access control for class members:
| Modifier | Access | Use Case |
|---|---|---|
local | Only within the class | Internal implementation details |
protected | Class and subclasses | Data shared with derived classes |
| (default) | Public - anywhere | Public interface |
class BankAccount;
// Public - accessible anywhere
string owner;
// Protected - accessible in subclasses
protected real balance;
// Local - only in this class
local string pin;
local int failed_attempts;
function new(string owner, string pin);
this.owner = owner;
this.pin = pin;
this.balance = 0;
this.failed_attempts = 0;
endfunction
// Public interface
function real get_balance(string pin);
if(verify_pin(pin))
return balance;
return -1;
endfunction
function bit deposit(real amount);
if(amount > 0) begin
balance += amount;
return 1;
end
return 0;
endfunction
// Local helper - internal only
local function bit verify_pin(string entered_pin);
if(entered_pin == pin) begin
failed_attempts = 0;
return 1;
end
failed_attempts++;
if(failed_attempts >= 3)
$display("Account locked!");
return 0;
endfunction
endclass
Parameterized Classes
Classes can be parameterized for generic, reusable components:
// Generic FIFO class
class Fifo #(type T = int, int DEPTH = 16);
local T queue[$];
local int max_depth = DEPTH;
function bit push(T item);
if(queue.size() < max_depth) begin
queue.push_back(item);
return 1;
end
return 0; // Full
endfunction
function bit pop(ref T item);
if(queue.size() > 0) begin
item = queue.pop_front();
return 1;
end
return 0; // Empty
endfunction
function bit is_empty();
return queue.size() == 0;
endfunction
function bit is_full();
return queue.size() >= max_depth;
endfunction
function int size();
return queue.size();
endfunction
endclass
// Usage with different types
Fifo #(Transaction, 32) tx_fifo; // FIFO of Transactions, depth 32
Fifo #(bit[7:0], 256) byte_fifo; // FIFO of bytes, depth 256
Fifo #(string) name_fifo; // FIFO of strings, default depth 16
initial begin
Transaction tr;
tx_fifo = new();
tr = new();
tr.addr = 32'h1000;
tx_fifo.push(tr);
tx_fifo.pop(tr);
$display("Popped addr: 0x%h", tr.addr);
end
Multiple Parameters
class Memory #(type ADDR_T = bit[31:0],
type DATA_T = bit[63:0],
int SIZE = 1024);
local DATA_T mem[ADDR_T];
function void write(ADDR_T addr, DATA_T data);
mem[addr] = data;
endfunction
function DATA_T read(ADDR_T addr);
if(mem.exists(addr))
return mem[addr];
return '0;
endfunction
endclass
// 32-bit address, 64-bit data (default)
Memory #() mem64;
// 16-bit address, 8-bit data, 256 entries
Memory #(bit[15:0], bit[7:0], 256) mem8;
Object Copying
Understanding shallow vs deep copy is critical when working with objects.
Shallow Copy
Copies only the immediate properties. Nested objects are shared:
class Address;
bit [31:0] addr;
bit [3:0] region;
endclass
class Transaction;
int id;
Address addr_obj; // Nested object handle
function new();
addr_obj = new();
endfunction
// Shallow copy using built-in new
function Transaction shallow_copy();
Transaction t = new this; // Copies handles, not objects
return t;
endfunction
endclass
initial begin
Transaction t1 = new();
t1.id = 1;
t1.addr_obj.addr = 32'h1000;
// Shallow copy - addr_obj is SHARED
Transaction t2 = new t1; // or t1.shallow_copy()
t2.id = 2;
t2.addr_obj.addr = 32'h2000;
$display("t1.addr = 0x%h", t1.addr_obj.addr); // Prints 0x2000!
end
Deep Copy
Recursively copies all nested objects:
class Address;
bit [31:0] addr;
bit [3:0] region;
function Address copy();
Address a = new();
a.addr = this.addr;
a.region = this.region;
return a;
endfunction
endclass
class Transaction;
int id;
Address addr_obj;
bit [7:0] payload[];
function new();
addr_obj = new();
endfunction
// Deep copy - creates independent copy
function Transaction copy();
Transaction t = new();
t.id = this.id;
t.addr_obj = this.addr_obj.copy(); // Copy nested object
t.payload = new[this.payload.size()](this.payload); // Copy array
return t;
endfunction
endclass
initial begin
Transaction t1 = new();
t1.id = 1;
t1.addr_obj.addr = 32'h1000;
t1.payload = new[4];
t1.payload = '{8'hAA, 8'hBB, 8'hCC, 8'hDD};
// Deep copy - completely independent
Transaction t2 = t1.copy();
t2.id = 2;
t2.addr_obj.addr = 32'h2000;
t2.payload[0] = 8'hFF;
$display("t1.addr = 0x%h", t1.addr_obj.addr); // Still 0x1000
$display("t1.payload[0] = 0x%h", t1.payload[0]); // Still 0xAA
end
Null Handles and Safety
Uninitialized handles are null. Accessing members through null causes runtime errors:
Transaction tr; // tr is null
// This will cause a runtime error!
// tr.addr = 32'h1000; // ERROR: null object access
// Always check for null
if(tr != null) begin
tr.addr = 32'h1000;
end else begin
$error("Transaction handle is null!");
end
// Or create the object first
if(tr == null) tr = new();
tr.addr = 32'h1000;
Safe Access Pattern
class SafeContainer;
Transaction tr;
function void set_transaction(Transaction t);
if(t == null) begin
`uvm_error("NULL", "Cannot set null transaction")
return;
end
this.tr = t;
endfunction
function Transaction get_transaction();
if(tr == null) begin
tr = new(); // Lazy initialization
end
return tr;
endfunction
endclass
Object Lifetime
SystemVerilog uses automatic garbage collection. Objects are destroyed when no handles reference them:
initial begin
Transaction tr1 = new(); // Object A created
Transaction tr2 = new(); // Object B created
tr1 = tr2; // Object A has no references -> garbage collected
tr2 = null; // tr1 still references Object B
tr1 = null; // Object B has no references -> garbage collected
end
Comparing Objects
Handles can be compared for equality (same object) or content comparison:
class Transaction;
bit [31:0] addr;
bit [63:0] data;
// Content comparison
function bit equals(Transaction other);
if(other == null) return 0;
return (this.addr == other.addr) &&
(this.data == other.data);
endfunction
endclass
initial begin
Transaction t1 = new();
Transaction t2 = new();
Transaction t3;
t1.addr = 32'h1000;
t2.addr = 32'h1000;
t3 = t1;
// Handle comparison - are they the same object?
$display("t1 == t2: %0b", t1 == t2); // 0 (different objects)
$display("t1 == t3: %0b", t1 == t3); // 1 (same object)
// Content comparison - do they have same values?
$display("t1.equals(t2): %0b", t1.equals(t2)); // 1 (same content)
end
Complete Example: Packet Class
typedef enum {ETH, IP, TCP, UDP} protocol_e;
class Packet;
// Static members
static int packet_count = 0;
static int total_bytes = 0;
// Instance properties
protected int id;
protected protocol_e protocol;
protected bit [47:0] src_mac;
protected bit [47:0] dst_mac;
protected bit [7:0] payload[];
protected bit [31:0] crc;
// Constructor
function new(protocol_e proto = ETH);
packet_count++;
this.id = packet_count;
this.protocol = proto;
this.src_mac = '0;
this.dst_mac = '0;
this.payload = new[0];
this.crc = 0;
endfunction
// Setters with validation
function bit set_payload(bit [7:0] data[]);
if(data.size() > 1500) begin
$error("Payload too large: %0d bytes", data.size());
return 0;
end
this.payload = new[data.size()](data);
total_bytes += data.size();
calc_crc();
return 1;
endfunction
function void set_addresses(bit [47:0] src, bit [47:0] dst);
this.src_mac = src;
this.dst_mac = dst;
endfunction
// Getters
function int get_id();
return id;
endfunction
function int get_length();
return payload.size();
endfunction
// CRC calculation
protected function void calc_crc();
crc = 32'hFFFFFFFF;
foreach(payload[i])
crc = crc ^ {24'b0, payload[i]};
endfunction
function bit verify_crc();
bit [31:0] calc = 32'hFFFFFFFF;
foreach(payload[i])
calc = calc ^ {24'b0, payload[i]};
return (calc == crc);
endfunction
// Deep copy
function Packet copy();
Packet p = new(this.protocol);
packet_count--; // Undo increment from new()
p.id = this.id;
p.src_mac = this.src_mac;
p.dst_mac = this.dst_mac;
p.payload = new[this.payload.size()](this.payload);
p.crc = this.crc;
return p;
endfunction
// Display
function void display(string prefix = "");
$display("%sPacket #%0d [%s]", prefix, id, protocol.name());
$display("%s Src: %012h Dst: %012h", prefix, src_mac, dst_mac);
$display("%s Payload: %0d bytes CRC: %08h", prefix, payload.size(), crc);
endfunction
// Static methods
static function int get_packet_count();
return packet_count;
endfunction
static function int get_total_bytes();
return total_bytes;
endfunction
static function void print_stats();
$display("=== Packet Statistics ===");
$display("Total packets: %0d", packet_count);
$display("Total bytes: %0d", total_bytes);
endfunction
endclass
// Usage
initial begin
Packet p1, p2;
bit [7:0] data[] = '{8'h01, 8'h02, 8'h03, 8'h04};
p1 = new(IP);
p1.set_addresses(48'hAABBCCDDEEFF, 48'h112233445566);
p1.set_payload(data);
p1.display();
p2 = p1.copy();
p2.display("Copy: ");
Packet::print_stats();
end
Key Takeaways
- Classes encapsulate data (properties) and behavior (methods) together
- Handles are references to objects - assigning handles copies the reference, not the object
- Constructors (
new) initialize objects and can take parameters - Static members are shared across all instances - use for counters, configuration
- Access modifiers:
local(class only),protected(class + subclasses), public (default) - Parameterized classes enable generic, reusable components
- Deep copy creates independent objects; shallow copy shares nested objects
- Always check for null handles before accessing members
Classes are fundamental to verification - UVM is built entirely on SystemVerilog classes. Master these concepts before diving into inheritance and polymorphism.
Part of the SystemVerilog Reference. Next: Inheritance & Abstract Classes
Comments (0)
Leave a Comment