Classes and Objects in SystemVerilog

Understanding Object-Oriented Programming (OOP) is the foundation of modern verification. This tutorial will take you from zero to hero in SystemVerilog classes.

Why Do We Need Classes?

In the early days of verification, engineers used simple Verilog tasks and functions. But as chip designs became more complex (think of your smartphone's processor with billions of transistors!), we needed a better way to organize our testbench code.

Consider this scenario: You're verifying a memory controller that handles read and write transactions. Each transaction has an address, data, and transaction type. Without classes, you'd have to pass these as separate variables everywhere:

Without Classes (Messy Approach)
task drive_transaction(
    input [31:0] addr,
    input [63:0] data,
    input [1:0]  trans_type,
    input [3:0]  burst_length,
    input        lock,
    // ... and 10 more signals!
);
    // Too many parameters to manage!
endtask

This approach becomes a nightmare when you have 20+ parameters. Classes solve this by bundling related data and operations together in a single, reusable package.

What is a Class?

A class is a blueprint or template that defines the structure and behavior of objects. Think of it like the design specifications for a car - it describes what a car should have (engine, wheels, doors) and what it can do (start, stop, accelerate).

An object is a specific instance created from that blueprint. Just like how many Maruti Swift cars can be manufactured from the same design, many objects can be created from the same class.

Anatomy of a SystemVerilog Class

Basic Class Structure
class Transaction;
    // ═══════════════════════════════════════
    // PROPERTIES (Data Members)
    // These store the data for each object
    // ═══════════════════════════════════════
    bit [31:0] addr;           // Address
    bit [63:0] data;           // Data payload
    bit [1:0]  trans_type;     // 0=READ, 1=WRITE
    bit [3:0]  burst_length;   // Burst size
    
    // ═══════════════════════════════════════
    // CONSTRUCTOR
    // Called automatically when object is created
    // ═══════════════════════════════════════
    function new();
        addr = 0;
        data = 0;
        trans_type = 0;
        burst_length = 1;
        $display("Transaction object created!");
    endfunction
    
    // ═══════════════════════════════════════
    // METHODS (Member Functions)
    // Define what the object can do
    // ═══════════════════════════════════════
    function void display();
        $display("═══════════════════════════════");
        $display(" Transaction Details");
        $display("═══════════════════════════════");
        $display(" Address     : 0x%08h", addr);
        $display(" Data        : 0x%016h", data);
        $display(" Type        : %s", (trans_type == 0) ? "READ" : "WRITE");
        $display(" Burst Length: %0d", burst_length);
        $display("═══════════════════════════════");
    endfunction
    
    function bit is_valid();
        // Address should be word-aligned (last 2 bits = 0)
        return (addr[1:0] == 2'b00);
    endfunction
    
endclass

Creating and Using Objects

In SystemVerilog, creating an object is a two-step process:

  1. Declare a handle - A variable that can point to an object
  2. Construct the object - Use new() to create actual object in memory
Creating Objects
module testbench;
    
    initial begin
        Transaction txn;     // Step 1: Declare handle (points to null)
        
        // At this point, txn = null (no object exists)
        if (txn == null)
            $display("Handle is null - no object yet!");
        
        txn = new();         // Step 2: Create object
        
        // Now txn points to a valid Transaction object
        txn.addr = 32'h1000_0000;
        txn.data = 64'hDEAD_BEEF_CAFE_BABE;
        txn.trans_type = 1;  // WRITE
        txn.burst_length = 4;
        
        txn.display();        // Call method
        
        if (txn.is_valid())
            $display("Transaction is valid!");
        else
            $display("ERROR: Transaction address not aligned!");
    end
    
endmodule

Output:

Simulation Output
Handle is null - no object yet!
Transaction object created!
═══════════════════════════════
 Transaction Details
═══════════════════════════════
 Address     : 0x10000000
 Data        : 0xdeadbeefcafebabe
 Type        : WRITE
 Burst Length: 4
═══════════════════════════════
Transaction is valid!

Constructors with Parameters

Often, you want to initialize an object with specific values right when it's created. You can do this by passing arguments to the constructor:

Parameterized Constructor
class Packet;
    bit [7:0]  src_addr;
    bit [7:0]  dst_addr;
    bit [31:0] payload;
    string     packet_name;
    
    // Constructor with default values
    function new(bit [7:0] src = 8'h00, 
                 bit [7:0] dst = 8'hFF,
                 string name = "unnamed");
        this.src_addr = src;
        this.dst_addr = dst;
        this.payload = 0;
        this.packet_name = name;
        $display("[%s] Packet created: src=0x%02h, dst=0x%02h", 
                 name, src, dst);
    endfunction
    
endclass

// Usage examples:
initial begin
    Packet p1, p2, p3;
    
    p1 = new();                           // Uses all defaults
    p2 = new(8'h10, 8'h20);               // Custom src and dst
    p3 = new(8'h55, 8'hAA, "ConfigPkt");  // All custom
end

The 'this' Keyword

Notice the this keyword in the constructor above. It refers to the current object and is useful when parameter names match property names:

Using 'this' Keyword
class Employee;
    string name;
    int salary;
    
    function new(string name, int salary);
        this.name = name;       // this.name = property, name = parameter
        this.salary = salary;
    endfunction
endclass

Static vs Instance Members

By default, each object has its own copy of all class properties. But sometimes, you want a variable that's shared across ALL objects of that class. These are called static members.

Static Members Example
class Transaction;
    // Instance variable - each object has its own
    bit [31:0] addr;
    bit [63:0] data;
    
    // Static variable - shared by ALL objects
    static int transaction_count = 0;
    
    function new();
        transaction_count++;  // Increment shared counter
        $display("Transaction #%0d created", transaction_count);
    endfunction
    
    // Static method - can be called without object
    static function int get_total_count();
        return transaction_count;
    endfunction
    
endclass

module test;
    initial begin
        Transaction t1, t2, t3;
        
        t1 = new();  // Transaction #1 created
        t2 = new();  // Transaction #2 created
        t3 = new();  // Transaction #3 created
        
        // Access static method using class name
        $display("Total transactions: %0d", 
                 Transaction::get_total_count());  // Output: 3
    end
endmodule

When to Use Static Members?

  • Counting total objects created (like above)
  • Configuration settings shared across all objects
  • Utility methods that don't need object data
  • Constants that should be same for all objects

Object Handles and Assignment

This is where many freshers get confused. When you assign one object handle to another, you're NOT copying the object - you're just making both handles point to the same object!

Handle Assignment (Shallow Copy)
initial begin
    Transaction txn1, txn2;
    
    txn1 = new();
    txn1.addr = 32'h1000;
    txn1.data = 64'hAAAA;
    
    txn2 = txn1;  // Both point to SAME object!
    
    txn2.addr = 32'h2000;  // This changes the address
    
    $display("txn1.addr = 0x%h", txn1.addr);  // Output: 0x2000 (SAME!)
    $display("txn2.addr = 0x%h", txn2.addr);  // Output: 0x2000
    
    // Both handles point to same object in memory
    if (txn1 == txn2)
        $display("Same object!");
end

Creating a True Copy (Deep Copy)

If you need separate objects with same values, use a copy method:

Deep Copy Method
class Transaction;
    bit [31:0] addr;
    bit [63:0] data;
    
    function Transaction copy();
        Transaction new_txn = new();
        new_txn.addr = this.addr;
        new_txn.data = this.data;
        return new_txn;
    endfunction
endclass

initial begin
    Transaction txn1, txn2;
    
    txn1 = new();
    txn1.addr = 32'h1000;
    
    txn2 = txn1.copy();  // Creates NEW object
    txn2.addr = 32'h2000;
    
    $display("txn1.addr = 0x%h", txn1.addr);  // Output: 0x1000
    $display("txn2.addr = 0x%h", txn2.addr);  // Output: 0x2000
    
    // Now they're different objects
    if (txn1 != txn2)
        $display("Different objects!");
end

Complete Practical Example

Let's create a complete, realistic example - an APB Transaction class that you might use in an actual verification project:

APB Transaction Class - Real World Example
class APBTransaction;
    // ═══════════════════════════════════════════════════════
    // Transaction Properties
    // ═══════════════════════════════════════════════════════
    rand bit [31:0] paddr;      // Address
    rand bit [31:0] pwdata;     // Write data
    rand bit        pwrite;     // 1=Write, 0=Read
    rand bit [2:0]  pprot;      // Protection type
    rand bit [3:0]  pstrb;      // Byte strobe
    
    // Response (filled after transaction completes)
    bit [31:0] prdata;          // Read data
    bit        pslverr;         // Slave error
    
    // Tracking
    static int trans_count = 0;
    int trans_id;
    time start_time;
    time end_time;
    
    // ═══════════════════════════════════════════════════════
    // Constraints
    // ═══════════════════════════════════════════════════════
    constraint addr_align_c {
        paddr[1:0] == 2'b00;  // Word aligned
    }
    
    constraint addr_range_c {
        paddr inside {[32'h0000_0000:32'h0000_FFFF]};  // 64KB range
    }
    
    constraint strobe_valid_c {
        pstrb != 4'b0000;  // At least one byte active
    }
    
    // ═══════════════════════════════════════════════════════
    // Constructor
    // ═══════════════════════════════════════════════════════
    function new(string name = "APB_TXN");
        trans_count++;
        trans_id = trans_count;
        start_time = $time;
        $display("[%0t] %s: Transaction #%0d created", $time, name, trans_id);
    endfunction
    
    // ═══════════════════════════════════════════════════════
    // Display Method
    // ═══════════════════════════════════════════════════════
    function void display(string tag = "");
        $display("╔══════════════════════════════════════════╗");
        $display("║  APB Transaction #%04d  %s", trans_id, tag);
        $display("╠══════════════════════════════════════════╣");
        $display("║  PADDR   : 0x%08h                    ║", paddr);
        $display("║  Type    : %s                         ║", pwrite ? "WRITE" : "READ ");
        if (pwrite)
            $display("║  PWDATA  : 0x%08h                    ║", pwdata);
        else
            $display("║  PRDATA  : 0x%08h                    ║", prdata);
        $display("║  PSTRB   : 4'b%04b                        ║", pstrb);
        $display("║  PSLVERR : %0b                              ║", pslverr);
        $display("╚══════════════════════════════════════════╝");
    endfunction
    
    // ═══════════════════════════════════════════════════════
    // Compare Method (for scoreboard)
    // ═══════════════════════════════════════════════════════
    function bit compare(APBTransaction other);
        if (other == null) return 0;
        
        if (paddr != other.paddr) begin
            $display("MISMATCH: Address 0x%h vs 0x%h", paddr, other.paddr);
            return 0;
        end
        
        if (!pwrite && prdata != other.prdata) begin
            $display("MISMATCH: Read data 0x%h vs 0x%h", prdata, other.prdata);
            return 0;
        end
        
        return 1;  // Match!
    endfunction
    
    // ═══════════════════════════════════════════════════════
    // Copy Method
    // ═══════════════════════════════════════════════════════
    function APBTransaction copy();
        APBTransaction txn = new();
        txn.paddr = this.paddr;
        txn.pwdata = this.pwdata;
        txn.pwrite = this.pwrite;
        txn.pprot = this.pprot;
        txn.pstrb = this.pstrb;
        return txn;
    endfunction
    
endclass

// ═══════════════════════════════════════════════════════════
// Testbench
// ═══════════════════════════════════════════════════════════
module tb;
    initial begin
        APBTransaction write_txn, read_txn;
        
        // Create write transaction
        write_txn = new("WRITE_TXN");
        if (!write_txn.randomize() with { pwrite == 1; })
            $fatal("Randomization failed!");
        write_txn.display("[STIMULUS]");
        
        // Create read transaction
        read_txn = new("READ_TXN");
        if (!read_txn.randomize() with { pwrite == 0; paddr == write_txn.paddr; })
            $fatal("Randomization failed!");
        read_txn.prdata = 32'hCAFE_BABE;  // Simulated response
        read_txn.display("[RESPONSE]");
        
        // Show total
        $display("\nTotal transactions created: %0d", 
                 APBTransaction::trans_count);
    end
endmodule

Key Points to Remember

  • Class = Blueprint, Object = Instance - A class defines structure, objects are actual instances
  • Handles are pointers - Declaring Transaction t; creates a null handle, not an object
  • new() creates objects - Without new(), no object exists in memory
  • Assignment copies handles, not objects - Use a copy() method for true copies
  • Static members are shared - Use for counters, configurations, utility methods
  • this refers to current object - Useful when parameter names match property names

Common Interview Questions

  1. What is the difference between a class and an object?

    A class is a template/blueprint that defines properties and methods. An object is a specific instance created from that class using new().

  2. What happens if you try to access an object before calling new()?

    You'll get a null pointer error because the handle points to null - no object exists yet.

  3. Difference between static and non-static members?

    Static members are shared across all objects of the class. Non-static (instance) members are unique to each object.

  4. When would you use the 'this' keyword?

    When you need to distinguish between a class property and a local variable/parameter with the same name.