Skip to content

SystemVerilog · Module 9

Classes & Objects — Basics

The anatomy of a class, how objects are allocated on the heap, handles as pointers, and the construction lifecycle from new() to garbage collection.

Module 9 · Page 9.2

Declaring a Class

When you write a class in SystemVerilog, nothing happens in simulation. The simulator reads your blueprint, checks it for errors, and then forgets about it until you actually create an object from it. Think of it as registering a shape — no material is used until you start stamping things out.

Here is the complete syntax, annotated so every line is clear:

Class Declaration — Full Syntax
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// keyword  class-name   optional: extends ParentClass
class BusTransaction;
 
    // ── Properties (variables owned by each object) ──────────
    bit [31:0]  addr;       // destination address
    bit [7:0]   data;       // payload byte
    bit          write;      // 1 = write,  0 = read
 
    // ── Constructor (runs automatically on new()) ─────────────
    function new();
        addr  = 32'h0;
        data  = 8'h0;
        write = 0;
    endfunction
 
    // ── Method (function defined inside the class) ────────────
    function void display();
        $display("[BusTx] addr=0x%08h  data=0x%02h  %s",
                  addr, data, write ? "WRITE" : "READ");
    endfunction
 
endclass   // every class ends here

A class can hold any number of properties and methods. Properties are the data. Methods are the behaviour. That's all there is to the structure — everything else in OOP is built on top of this one idea.

Creating Objects with new()

Declaring a class gives you a blueprint. Calling new() is what actually builds something from that blueprint. The simulator allocates memory, runs the constructor, and returns a reference to the new object. That reference is stored in your handle.

The two-step pattern you will write hundreds of times:

Handle Declaration → Object Creation
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Step 1 — Declare a handle (just a name, no memory yet)
BusTransaction txn;
//              ^^^  handle name
//                   At this point: txn == null
 
// Step 2 — Create the object (allocate memory, run constructor)
txn = new();
//    ^^^  calls the constructor, returns object reference
//         Now txn points to a real object in memory
 
// You can also do both on one line:
BusTransaction txn2 = new();

What new() Does Internally

  • 1 Allocates memory The simulator reserves enough memory to hold all the properties declared in the class — in this case, 32 bits for addr, 8 bits for data, and 1 bit for write.
  • 2 Runs the constructor If you wrote a function new(), it runs now. This is your chance to set initial values. If you did not write a constructor, SystemVerilog initialises everything to 0 / null automatically.
  • 3 Returns a reference new() gives back a reference (memory address) to the newly created object. This reference is stored in your handle variable. From this point forward, the handle is the only way to access the object.

Accessing Properties and Calling Methods

Once you have a live object, you access everything on it using the dot operator: handle.member. Same syntax for properties and methods — just add parentheses when calling a method.

Dot Operator — Properties & Methods
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
BusTransaction txn = new();
 
// ── Writing to a property ─────────────────────────────────────
txn.addr  = 32'hC000_0004;
txn.data  = 8'hFF;
txn.write = 1;
 
// ── Reading a property ────────────────────────────────────────
bit [31:0] saved_addr = txn.addr;   // copy the value out
 
// ── Calling a method ──────────────────────────────────────────
txn.display();
// Output: [BusTx] addr=0xc0000004  data=0xff  WRITE
 
// ── Passing an object to a function ──────────────────────────
task send(BusTransaction t);
    // use t.addr, t.data etc inside here
endtask
 
send(txn);   // passes the handle, not a copy of the object

Multiple Objects — Each One Is Independent

This is where OOP pays off fast. You can create as many objects as you want from one class, and each one gets its own private copy of every property. Changing one object's data has absolutely no effect on any other object.

Object Independence — Two Objects, Same Class
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
BusTransaction wr, rd;
 
// Create two completely independent objects
wr = new();
rd = new();
 
// Set different values on each
wr.addr  = 32'h1000_0000;
wr.data  = 8'hAB;
wr.write = 1;           // WRITE transaction
 
rd.addr  = 32'h2000_0000;
rd.data  = 8'h00;
rd.write = 0;           // READ transaction
 
// Modifying wr does not touch rd — they own separate memory
wr.addr = 32'hABCD_1234;
 
wr.display();
// [BusTx] addr=0xabcd1234  data=0xab  WRITE
 
rd.display();
// [BusTx] addr=0x20000000  data=0x00  READ  ← unchanged

Now scale that up. A queue of 500 transactions, all independent, all randomised, all ready to drive in a loop — and your code is still six lines long.

Creating Many Objects in a Loop
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
BusTransaction q[$];   // queue of handles
 
repeat (500) begin
    BusTransaction t = new();   // fresh object each iteration
    void'(t.randomize());
    q.push_back(t);
end
 
// Drive all 500 — each one has its own addr/data/write values
foreach (q[i])
    q[i].display();

Handle vs. Object — The Distinction That Matters

We touched on this in 9.1. Here it is in full detail, because getting this wrong is the source of some of the most confusing bugs in verification code.

Scenario 1 — Two handles, one object

Shallow Copy — Both Handles Point to Same Object
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
BusTransaction a = new();
BusTransaction b;
 
a.addr = 32'hAAAA_0000;
 
b = a;   // b now points to the SAME object as a (not a copy)
 
b.addr = 32'hBBBB_0000;   // modifies the shared object
 
$display("a.addr = 0x%08h", a.addr);
// a.addr = 0xbbbb0000   ← a sees the change too!
 
// Both a and b point to the same memory location.
// This is called a SHALLOW COPY.

If you want two independent copies, you need a deep copy — create a new object and copy each property manually. This is usually done with a copy() method. We cover that fully in Page 9.14 — Handles: Copy, Clone, Compare.

Scenario 2 — The null handle crash

Null Handle — What It Is and How to Avoid the Crash
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
BusTransaction txn;    // handle declared — value is null
 
// ── WRONG: calling new() was forgotten ───────────────────────
txn.addr = 32'h1000;
// RUNTIME ERROR: null object dereference — simulation stops
 
// ── RIGHT: always call new() before use ──────────────────────
txn = new();
txn.addr = 32'h1000;   // perfectly fine now
 
// ── Safe pattern: check for null before using ────────────────
if (txn == null)
    $fatal(1, "txn was never created — call new() first");
else
    txn.display();

Object Lifetime and Garbage Collection

An object lives as long as at least one handle is pointing to it. The moment no handle references an object, the simulator's garbage collector can reclaim that memory. You don't need to manually free objects in SystemVerilog — there is no delete or free().

Object Lifetime — When Objects Die
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
BusTransaction txn = new();   // Object A created
txn.addr = 32'h1000;
 
txn = new();   // Object B created — txn now points to B
                // Object A has no handles left → eligible for GC
                // (you can no longer reach Object A)
 
txn = null;   // Object B also now has no handles → eligible for GC
 
// ── Keeping objects alive in a queue ────────────────────────
BusTransaction q[$];
 
begin
    BusTransaction t = new();
    t.addr = 32'hCAFE;
    q.push_back(t);
end
// Even though local 't' goes out of scope here,
// the object is still alive because q[0] holds a reference to it.

Full Working Example — APB Transaction

Here is everything from this page combined into one complete, runnable example. You can drop this directly into your simulator.

Full Example — APB Transaction Class
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ──────────────────────────────────────────────────────────────
// APB Transaction class
// ──────────────────────────────────────────────────────────────
class ApbTransaction;
 
    // Properties
    rand bit [31:0] addr;
    rand bit [31:0] data;
    rand bit         write;     // 1=write, 0=read
    int              txn_id;
 
    // Constraint: keep address in valid APB range
    constraint c_addr { addr inside {[32'h4000_0000 : 32'h4FFF_FFFF]}; }
 
    // Constructor
    function new(int id = 0);
        txn_id = id;
    endfunction
 
    // Display method
    function void display();
        $display("[APB #%0d] %s  addr=0x%08h  data=0x%08h",
                  txn_id,
                  write ? "WR" : "RD",
                  addr, data);
    endfunction
 
endclass
 
// ──────────────────────────────────────────────────────────────
// Testbench
// ──────────────────────────────────────────────────────────────
module tb;
 
    ApbTransaction txn_q[$];   // queue to hold all transactions
 
    initial begin
 
        // Create 10 randomised transactions
        for (int i = 0; i < 10; i++) begin
            ApbTransaction t = new(i);      // new object each iteration
            if (!t.randomize())
                $fatal(1, "Randomise failed on txn %0d", i);
            txn_q.push_back(t);
        end
 
        // Print all 10
        foreach (txn_q[i])
            txn_q[i].display();
 
        // Verify independence — modify txn_q[0] only
        txn_q[0].addr = 32'hABCD_1234;
        $display("\n--- After modifying txn_q[0].addr ---");
        txn_q[0].display();
        txn_q[1].display();   // still has its original addr
 
    end
endmodule

Quick Reference

OperationSyntaxNotes
Declare a classclass MyClass; ... endclassBlueprint only — no memory used
Declare a handleMyClass h;Handle is null until new() is called
Create an objecth = new();Allocates memory, runs constructor
Declare + createMyClass h = new();One-liner — common shorthand
Access a propertyh.property_nameRead or write with dot operator
Call a methodh.method_name(args)Parentheses always required
Check for nullif (h == null)Always check before first use in tasks
Release a handleh = null;Object GC'd if no other handles exist

Verification Usage — Class Patterns in Real Testbenches

Class basics — declaration, new(), properties, methods — combine into the three foundational verification patterns: the transaction (a passive data record), the component (an active processor with its own loop), and the scoreboard entry (a queueable expected value). Master these three and you have the structural foundation for every UVM testbench that follows.

SystemVerilog — The three foundational verification class patterns
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Pattern 1: TRANSACTION — passive data record ──────────────
class axi_xact;
    rand bit [31:0] addr;
    rand bit [63:0] data;
    rand bit [7:0]  burst_len;
    bit            is_write;
 
    constraint addr_aligned { addr[2:0] == 3'b000; }
    constraint burst_legal { burst_len inside {1, 2, 4, 8, 16}; }
 
    function string describe();
        return $sformatf("AXI %s @0x%08h len=%0d data=0x%016h",
                         is_write ? "WR" : "RD", addr, burst_len, data);
    endfunction
endclass
 
// ── Pattern 2: COMPONENT — active processor with its own loop ──
class axi_monitor;
    virtual axi_if vif;
    mailbox #(axi_xact) outbox;
    int observed_count;
 
    function new(virtual axi_if v, mailbox #(axi_xact) o);
        vif = v;
        outbox = o;
        observed_count = 0;
    endfunction
 
    task run();
        forever begin
            @(posedge vif.clk);
            if (vif.awvalid && vif.awready) begin
                axi_xact tr = new;           // fresh object per observation
                tr.addr      = vif.awaddr;
                tr.is_write  = 1;
                tr.burst_len = vif.awlen;
                // ... capture data on subsequent W beats
                outbox.put(tr);
                observed_count++;
            end
        end
    endtask
endclass
 
// ── Pattern 3: SCOREBOARD ENTRY — queueable expected-value record ──
class axi_scoreboard;
    mailbox #(axi_xact) expected_q;
    mailbox #(axi_xact) observed_q;
    int match_count, mismatch_count;
 
    function new();
        expected_q = new(0);   // unbounded
        observed_q = new(0);
        match_count = 0;
        mismatch_count = 0;
    endfunction
 
    task check_loop();
        forever begin
            axi_xact exp_tr, obs_tr;
            expected_q.get(exp_tr);
            observed_q.get(obs_tr);
            if (exp_tr.addr === obs_tr.addr &&
                exp_tr.data === obs_tr.data) begin
                match_count++;
                $display("[%0t] SCB MATCH: %s", $time, exp_tr.describe());
            end else begin
                mismatch_count++;
                $display("[%0t] SCB MISMATCH: exp=%s obs=%s",
                         $time, exp_tr.describe(), obs_tr.describe());
            end
        end
    endtask
endclass

Simulation Behavior — How the Simulator Tracks Objects

Behind every new() and every handle assignment, the simulator maintains a reference count and a pool of live objects. Understanding what happens at the engine level helps you debug memory growth, identify reference cycles, and reason about object lifetime in long-running regressions.

SystemVerilog — Tracing handle reference behaviour
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class tracked_obj;
    static int live_count;
    int id;
 
    function new(int i);
        id = i;
        live_count++;
        $display("[%0t] obj#%0d created (live=%0d)", $time, id, live_count);
    endfunction
 
    function void manual_release();
        live_count--;
        $display("[%0t] obj#%0d manually released (live=%0d)",
                 $time, id, live_count);
    endfunction
endclass
 
initial begin
    tracked_obj a, b, c;
 
    a = new(1);     // [0] obj#1 created (live=1)
    b = new(2);     // [0] obj#2 created (live=2)
    c = a;             // no allocation; c and a both point to obj#1
                       // live_count unchanged — same single object, two handles
 
    a = null;        // a drops reference; obj#1 still kept alive by c
    a = new(3);     // [0] obj#3 created (live=3)
 
    // Now live: obj#1 (via c), obj#2 (via b), obj#3 (via a)
 
    b = null;        // obj#2 has no live handles → eligible for GC
                       // live_count says 3 because no manual_release was called
 
    #10ns;
    $display("At t=10ns: live_count=%0d (heap may have GC'd obj#2)", live_count);
end

Waveform Analysis — Tracking Class Activity Without Class Visibility

Waveform viewers can't display class objects directly. But the activity those objects generate — bus transactions, scoreboard checks, counter updates — does appear in the waveform if you mirror class events into module-level signals. This is the bridge between dynamic class behaviour and the static-signal world that Verdi understands.

SystemVerilog — Mirror class state into module signals
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module tb;
    // Static signals — visible in Verdi/DVE waveform
    logic [31:0] last_xact_addr;
    logic [63:0] last_xact_data;
    int           xacts_sent;
    int           scb_matches;
    int           scb_mismatches;
 
    axi_driver     drv;
    axi_scoreboard scb;
 
    initial begin
        drv = new();
        scb = new();
 
        // Every transaction sent updates the mirror signals
        forever begin
            axi_xact tr = new();
            assert (tr.randomize());
            last_xact_addr = tr.addr;       // → waveform
            last_xact_data = tr.data;
            xacts_sent++;
            drv.send(tr);
        end
    end
 
    // Periodic scoreboard status snapshot
    always @(posedge clk) begin
        scb_matches    = scb.match_count;
        scb_mismatches = scb.mismatch_count;
    end
endmodule
 
// In Verdi, browse:
//   tb.last_xact_addr   — most recent transaction's address
//   tb.xacts_sent       — running count of sends
//   tb.scb_mismatches   — alarm when this changes

Industry Insights — How Senior Teams Use Classes

Debugging Academy — 5 Real Class-Based Testbench Bugs

Each lab is a real failure mode from production projects. Buggy code, symptom, root cause, fix.

1

DEBUG
Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Generator keeps a "current" handle and modifies it per transaction
class generator;
    packet current;
 
    task run();
        current = new;
        forever begin
            assert (current.randomize());
            outbox.put(current);              // queues the SAME handle every loop
            @(posedge clk);
        end
    endtask
endclass
2

DEBUG
Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class statistics;
    int total_count;
    function void bump();
        total_count++;            // ← reads/writes through this handle
    endfunction
endclass
 
module tb;
    statistics stats;             // ← declared but never new()'d
    initial stats.bump();         // crash or silent garbage
endmodule
3

DEBUG
Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class queue_manager;
    mailbox #(packet) mbox;
    function new();
        // Forgot to allocate mbox
    endfunction
 
    task push(packet p);
        mbox.put(p);              // crash: mbox is null
    endtask
endclass
 
module tb;
    queue_manager qm = new;
    initial qm.push(new());      // crash
endmodule
4

DEBUG
Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class counter;
    static int count;             // ← static — shared across ALL instances
    function void increment();
        count++;
    endfunction
endclass
 
initial begin
    counter c1 = new;
    counter c2 = new;
    c1.increment();
    c1.increment();
    $display("c2.count=%0d", c2.count);   // prints 2, not 0
end
5

DEBUG
Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Producer fires a million transactions; consumer drains slowly
initial begin
    repeat (1_000_000) begin
        packet tr = new;
        assert (tr.randomize());
        outbox.put(tr);           // queues every transaction
    end
end
 
// Consumer can only process 1000/second
initial begin
    forever begin
        packet rx;
        outbox.get(rx);
        slow_process(rx);
    end
end

Interview Q&A — 12 Questions on Classes & Objects

Drawn from real interviews at chip-design and verification companies. Try to answer before reading each response.

A class declaration is a type definition — it describes what properties and methods objects of this class will have, but creates nothing at runtime. An object is a runtime instance — created by new(), lives in the heap, has its own copy of every non-static property. One class declaration can serve as the blueprint for zero, one, or millions of objects over a simulation. Class is to object as module is to module instance — but objects exist dynamically rather than statically.

Best Practices — Class & Object Rules to Walk Away With

  1. One class per file, file name matches class name. Production VIP convention; makes grep, blame, and dependency tracking work.
  2. Always declare classes inside a package, never in a module. Module-internal classes are not reusable.
  3. Pair handle declaration with construction. my_class h = new(); at declaration, or h = new(); immediately after.
  4. Initialise every dynamic property in the constructor. Mailboxes, queues, dynamic arrays, associative arrays, and nested class handles all start null — allocate them in new().
  5. Allocate a fresh object every time you queue a transaction. Never put-and-mutate; always allocate-and-put.
  6. Use static deliberately. Default to instance properties; promote to static only when the shared-across-instances semantic is intentional and documented.
  7. Provide a describe() method on every transaction and component class. Logs, scoreboard messages, debug prints all use it — never inline-format from raw property access.
  8. Use bounded mailboxes for producer-consumer queues. Memory growth is bounded; producer naturally throttles to consumer rate.
  9. Mirror class state into module signals for waveform visibility. Verdi can't browse class memory; mirror signals can be alarmed and time-aligned.
  10. Master plain SV classes before reaching for UVM. Transaction + driver + monitor + scoreboard in plain SV is the foundation; UVM is "the foundation plus a factory, plus phasing, plus config_db."