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:

ModifierAccessUse Case
localOnly within the classInternal implementation details
protectedClass and subclassesData shared with derived classes
(default)Public - anywherePublic 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

Author
Mayur Kubavat
VLSI Design and Verification Engineer sharing knowledge about SystemVerilog, UVM, and hardware verification methodologies.

Comments (0)

Leave a Comment