Skip to content

SystemVerilog · Module 9

Introduction to OOP in SystemVerilog

Why OOP belongs in verification, what classes give you over structs and modules, and the OO mindset every modern UVM environment is built on.

Module 9 · Page 9.1

The Problem That OOP Solves

Let's say you are verifying an APB slave. You have five test scenarios. Each one needs an address, some data, a direction, and a few status flags. So you start writing variables.

SystemVerilog — Without OOP (the messy way)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Five transactions. Five of everything.
bit [31:0] trans1_addr, trans2_addr, trans3_addr, trans4_addr, trans5_addr;
bit [7:0]  trans1_data, trans2_data, trans3_data, trans4_data, trans5_data;
bit         trans1_write, trans2_write, trans3_write, trans4_write, trans5_write;
bit         trans1_valid, trans2_valid, trans3_valid, trans4_valid, trans5_valid;
bit [1:0]  trans1_resp, trans2_resp, trans3_resp, trans4_resp, trans5_resp;
 
// Now send the first one...
trans1_addr  = 32'h4000_0000;
trans1_data  = 8'hAB;
trans1_write = 1;
trans1_valid = 1;
 
// You need 1000 transactions? Good luck.

This works for five transactions. It falls apart completely at fifty. You end up with hundreds of variables, no structure, and code only you can read — and even you can't read it after two weeks.

Now here is the same thing written with a class:

SystemVerilog — With OOP (the clean way)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Define the blueprint once
class ApbTransaction;
    rand bit [31:0] addr;
    rand bit [7:0]  data;
    rand bit         write;
    bit              valid;
    bit [1:0]        resp;
endclass
 
// Create as many as you need — instantly
ApbTransaction trans_q[$];
 
repeat (1000) begin
    ApbTransaction t = new();
    void'(t.randomize());
    trans_q.push_back(t);
end
 
// 1000 transactions. Zero extra variables.

That is the core value of OOP in verification. One clean blueprint. As many objects as you need. No mess.

What is Object-Oriented Programming?

OOP is a way of organising code around objects instead of just functions and variables. Each object is a self-contained unit that holds data (called properties) and knows what to do with that data (called methods).

The simplest way to think about it: a class is the blueprint. An object is something you build from that blueprint. You write the blueprint once. You can build a hundred objects from it. Each object has the same structure but its own independent data.

In SystemVerilog, you write a class to describe what a transaction looks like. Then every time you need a new transaction, you create a new object from that class. All objects share the same structure but hold different values.

The Six Pillars of OOP

OOP is built on six core ideas. You will not use all six on day one, but they are all covered in this module. Here is a quick preview so you know where things are heading.

1 — Encapsulation

Bundle data and the code that works with it into one place. Decide what the outside world can see and what it cannot. → Page 9.5: Encapsulation

2 — Abstraction

Show only what matters. Hide the internal complexity. A caller just calls pkt.send() without knowing the 40 lines behind it.

3 — Inheritance

Build a new class on top of an existing one. The child gets everything the parent has — and adds its own bits. → Page 9.8: Inheritance

4 — Polymorphism

One interface, many implementations. The same method call routes to the right code at runtime depending on the actual object type. → Page 9.10: Polymorphism

5 — Reusability

Write a class once. Use it across dozens of tests and multiple projects. Extend it without touching the original.

6 — Modularity

Break a complex testbench into small, focused classes. Each class has one job. Easy to test, debug, and maintain.

Your First Class — Every Part Explained

Here is a complete class from scratch. Every part is labelled. Read through it once before reading the explanation below.

SystemVerilog — First complete class
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─── 1. Class definition (the blueprint) ──────────────────────────
class Packet;
 
    // ─── 2. Properties (data the object holds) ────────────────────
    int          packet_id;
    bit [7:0]    data;
    bit          parity;
 
    // ─── 3. Method: display the packet ────────────────────────────
    function void display();
        $display("Packet %0d: data=0x%h  parity=%b",
                 packet_id, data, parity);
    endfunction
 
    // ─── 4. Method: compute even parity ───────────────────────────
    function void calc_parity();
        parity = ^data;   // XOR of all 8 bits
    endfunction
 
endclass
 
// ─── 5. Using the class inside a testbench ────────────────────────
module tb;
 
    Packet pkt;              // ← handle only — no object yet, pkt is null
 
    initial begin
        pkt           = new();   // ← object created in memory
        pkt.packet_id = 1;
        pkt.data      = 8'hA5;
        pkt.calc_parity();        // calls the method on this object
        pkt.display();            // Packet 1: data=0xa5  parity=1
    end
 
endmodule

Let's walk through what each part does:

The class keyword

class Packet; declares the blueprint. Everything between class and endclass belongs to this class. The name Packet is what you use later to declare handles and create objects.

Properties

Properties are just variables that live inside the class. Every object you create from Packet gets its own independent copy of packet_id, data, and parity. Changing one object's data has no effect on another object's data.

& 4. Methods

Methods are functions (or tasks) defined inside the class. They have direct access to all the class properties without needing to pass them as arguments. When you call pkt.calc_parity(), the method reads data and writes parity from the same object automatically.

Handle vs. Object — the most important distinction

This is where nearly every beginner makes their first mistake.

Packet pkt; gives you a handle. Think of it as a name tag. Before new() is called, the handle points to nothing — it is null. The new() call actually builds the object in simulation memory and makes pkt point to it.

Classes vs. Modules vs. Structs

If you come from an RTL background, you are used to modules and maybe structs. Here is a quick reference so you always know which one to use.

FeatureModuleStructClass
PurposeDescribe hardwareGroup data fieldsVerification objects with behaviour
Synthesisable?YesYesNo — testbench only
Dynamic creation?No — fixed at elaborationNoYes — new() at any time
Has methods?Tasks and functions onlyNoYes — full task and function support
Inheritance?NoNoYes — extends keyword
Randomisation?NoLimitedYes — rand, randc, constraints
Use it forRTL — adders, FSMs, countersPacked register fields, packet headersTransactions, drivers, monitors, scoreboards

Simple rule: if it goes into silicon, use a module. If it is a named group of data with no behaviour, use a struct. If it is something your testbench creates, randomises, and drives — use a class.

Why OOP Actually Matters in Verification

The six pillars sound great on paper. Here is what they actually buy you on a real project.

Your testbench can scale

A flat procedural testbench with 2000 lines works fine at IP level. At subsystem level, you need ten of them stitched together. Without classes, you copy-paste and manually synchronise everything. With classes, you reuse the same driver, monitor, and scoreboard objects across all ten environments — just pointing them at different interfaces.

Bugs become easier to find

When every transaction is an object with a display() method and a unique ID, your error messages tell you exactly which transaction failed and what its fields looked like. Compare that to fishing through raw waveforms and matching timestamps by hand.

UVM is built entirely on OOP

Every component in UVM — driver, monitor, scoreboard, sequencer — is a class. Every transaction is a class. The factory, the phasing mechanism, the register abstraction layer — all classes. Learning OOP in SystemVerilog is not a step before UVM. It is the same thing.

What the Rest of This Module Covers

This page was just the warm-up. Here is the roadmap for the full OOP module:

Quick Reference — OOP Syntax at a Glance

Bookmark this. You will come back to it when you forget a keyword.

SystemVerilog — OOP syntax quick reference
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Declare a class ───────────────────────────────────────────
class MyClass;
endclass
 
// ── Declare a handle (no object yet) ──────────────────────────
MyClass handle;
 
// ── Create an object ──────────────────────────────────────────
handle = new();
 
// ── Access a property ─────────────────────────────────────────
handle.property_name = 5;
 
// ── Call a method ─────────────────────────────────────────────
handle.method_name();
 
// ── Inherit from a parent ─────────────────────────────────────
class ChildClass extends ParentClass;
endclass
 
// ── Call parent constructor from child ────────────────────────
function new();
    super.new();
endfunction
 
// ── Mark a method as overridable ──────────────────────────────
virtual function void display();
endfunction
 
// ── Abstract class (cannot be instantiated directly) ──────────
virtual class BaseClass;
    pure virtual function void do_something();
endclass

Verification Usage — Building a Minimal Class-Based Testbench

The full UVM framework is covered later in the curriculum — but you don't need UVM to start using classes for verification. A surprising amount of real testbench machinery — packet generators, scoreboards, coverage objects — can be written with plain SystemVerilog classes. Once you've internalised these patterns, UVM stops being a framework you "learn" and becomes a library of conventions you already understand.

SystemVerilog — Minimal class-based testbench without UVM
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Transaction class — encapsulates one bus operation ─────────
class apb_xact;
    rand bit [31:0] paddr;
    rand bit [31:0] pwdata;
    rand bit        pwrite;
 
    constraint addr_aligned { paddr[1:0] == 2'b00; }
 
    function string to_string();
        return $sformatf("APB %s @0x%08h data=0x%08h",
                         pwrite ? "WR" : "RD", paddr, pwdata);
    endfunction
endclass
 
// ── Driver class — pulls transactions, drives the bus ─────────
class apb_driver;
    mailbox #(apb_xact) inbox;
    virtual apb_if vif;
 
    task run();
        forever begin
            apb_xact tr;
            inbox.get(tr);
            @(posedge vif.clk);
            vif.paddr   <= tr.paddr;
            vif.pwdata  <= tr.pwdata;
            vif.psel    <= 1;
            @(posedge vif.clk);
            vif.penable <= 1;
            while (!vif.pready) @(posedge vif.clk);
            vif.psel    <= 0;
            vif.penable <= 0;
            $display("[%0t] DRV: %s", $time, tr.to_string());
        end
    endtask
endclass
 
// ── Test top — composition: env has-a driver, has-a generator ──
module tb;
    // Interfaces stay in module-world; classes hold handles to them
    apb_if u_apb(.clk);
    apb_slave u_dut(.apb(u_apb));
 
    initial begin
        // Build the testbench: instantiate the driver and the inbox
        apb_driver drv     = new;
        mailbox #(apb_xact) bx = new;
        drv.inbox = bx;
        drv.vif   = u_apb;
        fork drv.run(); join_none
 
        // Generate 10 random transactions
        repeat (10) begin
            apb_xact tr = new;
            assert (tr.randomize());
            bx.put(tr);
        end
 
        #1us; $finish;
    end
endmodule

Simulation Behavior — Object Lifecycle in the Simulator

A class object lives in the simulator's heap memory. The simulator allocates it when new() runs, tracks every handle that points to it, and frees its memory when the last handle drops the reference. Understanding when allocation, reference changes, and garbage collection happen is the difference between a clean testbench and one that mysteriously consumes 20 GB of RAM mid-regression.

SystemVerilog — Tracing object lifetime
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class packet;
    static int alive_count;
    int id;
 
    function new(int i);
        id = i;
        alive_count++;
        $display("[%0t] packet#%0d created (alive=%0d)",
                 $time, id, alive_count);
    endfunction
 
    // SystemVerilog doesn't have destructors, but you can model
    // "release" via a dedicated drop method that nulls the handle
    function void drop();
        alive_count--;
        $display("[%0t] packet#%0d dropped (alive=%0d)",
                 $time, id, alive_count);
    endfunction
endclass
 
initial begin
    packet p1, p2;
 
    p1 = new(1);                  // [0] packet#1 created (alive=1)
    p2 = new(2);                  // [0] packet#2 created (alive=2)
 
    // Handle reassignment — same object, two pointers
    p1 = p2;                       // packet#1 now has zero handles → eligible for GC
                                   // But alive_count still says 2 because no drop() was called
 
    p2 = null;                    // One handle dropped; packet#2 still has p1 pointing to it
 
    p1 = null;                    // Last handle dropped; packet#2 now garbage
 
    // SV's GC is implementation-defined; the simulator may free immediately
    // or batch up frees. alive_count is a manual indicator, not GC truth.
end

Waveform Analysis — Why Classes Don't Appear in the Viewer

Verdi and DVE show signals — wires and variables that have a hierarchical path in the elaborated design. Classes don't have a hierarchical path. An object is created at runtime, lives in the heap, and has no fixed location in the design tree. You cannot browse to tb.drv.pending_tr.paddr in a waveform viewer the way you can to tb.u_apb.paddr — the first path goes through dynamic class memory; the second through static module signals. This trips up every engineer transitioning from RTL debug to verification debug.

SystemVerilog — How to 'see' class state in a waveform context
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Pattern: snapshot class state into a module-level signal ───
module tb;
    // Static signals — these DO appear in waveform
    logic [31:0] last_paddr;
    logic [31:0] last_pwdata;
    int           xact_count;
 
    initial begin
        apb_driver drv = new;
 
        // As the driver processes each transaction, mirror its data into signals
        forever begin
            apb_xact tr;
            drv.inbox.get(tr);
            last_paddr  = tr.paddr;       // snapshot to signal — visible in Verdi
            last_pwdata = tr.pwdata;
            xact_count++;
            drv.process(tr);
        end
    end
endmodule
 
// In Verdi, browse to:
//   tb.last_paddr      ← shows the most recent transaction's address
//   tb.last_pwdata     ← shows the most recent transaction's data
//   tb.xact_count      ← shows total transactions processed
//
// You CANNOT directly browse drv.* — it's a class handle pointing to
// dynamic memory with no hierarchical path.

Industry Insights — How Senior Teams Use OOP in Verification

Debugging Academy — 5 Real Class-Based Testbench Bugs

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

1

DEBUG
Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class scoreboard;
    int hits, misses;
    function void record(bit hit);
        if (hit) hits++; else misses++;
    endfunction
endclass
 
module tb;
    scoreboard scb;   // ← declared, NOT instantiated
 
    initial begin
        scb.record(1);   // crash: null reference
    end
endmodule
2

DEBUG
Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class packet;
    int data[4];
endclass
 
initial begin
    packet original = new;
    packet copy     = original;   // ← NOT a copy — same handle, same object
 
    original.data[0] = 42;
    copy.data[0]     = 99;        // overwrites original.data[0]
 
    $display("original=%0d  copy=%0d", original.data[0], copy.data[0]);
    // Prints: original=99  copy=99 — both see the same memory
end
3

DEBUG
Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module tb_top;
    // Class declared inside the module — not reusable elsewhere
    class packet;
        int data;
    endclass
 
    initial begin
        packet p = new;
    end
endmodule
 
// Another file:
module monitor;
    initial begin
        packet p = new;   // ← compile error: packet undefined
    end
endmodule
4

DEBUG
Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Generator sends 10 transactions through a mailbox
initial begin
    packet tr = new;   // ← single object reused for every send
 
    repeat (10) begin
        assert (tr.randomize());
        bx.put(tr);    // puts the SAME handle 10 times
    end
end
 
// Receiver gets all 10 — but they're all the same object,
// holding only the last randomized values
initial begin
    repeat (10) begin
        packet rx;
        bx.get(rx);
        $display("Got: data=%h", rx.data);  // all 10 print the same data
    end
end
5

DEBUG
Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// memory_controller.sv — meant to be synthesisable RTL
module memory_controller(...);
    logic [31:0] addr_reg;
 
    // Helper "class" added by an engineer who thought it would be clean
    class addr_decoder;
        function bit is_io(bit [31:0] a);
            return (a[31:24] == 8'hFE);
        endfunction
    endclass
 
    addr_decoder decoder = new;
 
    always_ff @(posedge clk)
        if (decoder.is_io(addr_reg)) ...   // never reaches netlist
endmodule

Interview Q&A — 12 Questions on OOP Foundations

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

Verilog was designed for hardware modelling — static, gate-level structures. As chip complexity grew (from thousands of gates in the 1980s to billions today), the verification environment that exercised the design became its own large software project. Verilog's static module hierarchy is too rigid for dynamic test stimulus, reusable testbench components, and inheritance-based VIP. OOP — added in IEEE 1800-2005 — gave verification engineers a proper toolkit: dynamic objects, encapsulation, inheritance, polymorphism. The RTL part of SystemVerilog stayed Verilog-compatible; OOP lives strictly in the verification side.

Best Practices — OOP Rules to Walk Away With

  1. Always declare classes inside a package. Module-internal class declarations don't scale beyond the prototype stage.
  2. Pair handle declaration with construction. my_class h = new();, or explicitly new() in the next line. Never declare a handle and use it before initialising.
  3. Allocate a fresh object for every queued send. Never put the same handle in a mailbox/queue twice with mutations in between.
  4. Default fields to local. Promote to protected only when a subclass genuinely needs the access; expose through methods otherwise.
  5. Use inheritance only when subclass is-a superclass. If the only reason to extend is "to reuse this helper method," use composition or a package function instead.
  6. Mirror class-internal state into module signals for waveform visibility. Verdi can't browse class memory; module-level signals can.
  7. Log every important class state change with $display or ``uvm_info`. The transcript is your primary class-debug tool.
  8. Add explicit cleanup methods for non-memory resources. File handles, mailbox slots, semaphore tokens — call close() or release before dropping the last reference.
  9. Lint for "class in RTL file." CI rule rejects any class declaration in files under rtl/. Catches misclassified code at commit, not synthesis.
  10. Master plain SV classes before reaching for UVM. If you can't write a working three-class testbench (transaction, driver, monitor) without UVM, you'll struggle to debug a UVM env that uses the same patterns underneath.