Skip to content

SystemVerilog · Module 9

Properties & Methods

Per-instance data fields, methods that operate on them, and the difference between functions and tasks inside a class.

Module 9 · Page 9.3

Properties — The Data Inside a Class

A property is just a variable declared inside a class. Each object you create gets its own private copy of every property. That's the whole point — 500 objects from the same class means 500 completely independent sets of values.

You can use any data type as a property — integers, bit vectors, strings, enums, arrays, even handles to other classes. Here is a realistic transaction class showing the full range:

Properties — All Common Types
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class AhbTransaction;
 
    // ── Basic types ───────────────────────────────────────────
    bit [31:0]  haddr;       // 32-bit address
    bit [31:0]  hwdata;      // write data
    bit [31:0]  hrdata;      // read data (filled after drive)
    bit          hwrite;      // 1=write, 0=read
    int          txn_id;      // unique identifier
    string       label;       // optional debug name
 
    // ── Enum type as a property ───────────────────────────────
    typedef enum bit [2:0] {
        SINGLE = 3'b000,
        INCR   = 3'b001,
        WRAP4  = 3'b010,
        INCR4  = 3'b011
    } burst_e;
 
    burst_e  hburst;
 
    // ── Array as a property ───────────────────────────────────
    bit [31:0]  payload[];   // dynamic array of data beats
 
    // ── Handle to another class as a property ─────────────────
    // (forward declaration shown here — covered in Page 9.15)
    // AhbResponse  resp;       // handle to a response object
 
endclass

Instance Properties — Each Object Owns Its Own Copy

By default, every property you declare is an instance property instance. The word "instance" just means it belongs to one specific object. Change it on one object and every other object is completely unaffected.

rand Properties — For Constrained Random Verification

The rand rand keyword marks a property for constrained random generation. When you call randomize() on the object, the solver picks a new legal value for every rand property, respecting any constraints you have written.

randc randc stands for random cyclic. The solver cycles through every possible value exactly once before repeating — useful for things like transaction IDs or test scenario selectors where you want guaranteed coverage with no repeats until the full set is exhausted.

rand vs randc vs plain property
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class Packet;
 
    // rand — new random value every randomize() call
    rand bit [7:0]  data;
 
    // randc — cycles through all 256 values before repeating
    randc bit [3:0] seq_id;
 
    // plain — randomize() ignores this; you set it manually
    bit               valid;
    int               pkt_count;
 
    constraint c_data { data inside {[8'h00 : 8'h7F]}; }
 
endclass
 
module tb;
    Packet p = new();
    initial begin
        repeat(5) begin
            void'(p.randomize());
            // data : random value in 0x00–0x7F each time
            // seq_id: 0,3,11,7,14... (no repeats until 0-15 exhausted)
            // valid : unchanged (not rand) — still whatever you last set
            $display("data=%0d  seq_id=%0d", p.data, p.seq_id);
        end
    end
endmodule

Property Initialisation

Properties can be initialised either inline at declaration or inside the constructor. Both work. Inline is fine for simple constant defaults. The constructor is better when the initial value depends on an argument or needs some computation.

Property Initialisation — Inline vs Constructor
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class Transaction;
 
    // Inline initialisation — runs before the constructor body
    int           timeout_cycles = 1000;
    bit           enable         = 1;
    string        name           = "unnamed";
 
    // Constructor can override or depend on a passed argument
    function new(string n = "tx", int timeout = 1000);
        name           = n;
        timeout_cycles = timeout;
        // 'enable' keeps its inline default of 1
    endfunction
 
endclass
 
// Three objects — each with its own independent values
Transaction t1 = new();                    // name="tx",  timeout=1000
Transaction t2 = new("burst_tx");          // name="burst_tx"
Transaction t3 = new("slow_tx", 5000);    // timeout=5000

Methods — The Behaviour Inside a Class

A method is a function or task defined inside a class. It has automatic access to all of the class's own properties — no need to pass them as arguments. That is the big difference from a standalone function.

The rule for choosing between a function and a task is the same as outside a class: function

  • Returns a value (or void)
  • Cannot consume simulation time
  • No delay, wait, or @event inside
  • Use for: compute, display, check, copy task
  • Cannot return a value (use ref args)
  • Can consume simulation time
  • Can contain @clk, #delay, wait()
  • Use for: drive, wait, send, receive

Writing Functions Inside a Class

Functions are the workhorse of class methods. You use them for anything that computes a result or operates on data without needing simulation time.

Class Functions — Return Values, void, and Getters
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class EthernetFrame;
 
    rand bit [7:0]  payload [];   // dynamic array
    rand bit [47:0] dst_mac;
    rand bit [47:0] src_mac;
    bit [31:0]       crc;
    int               frame_id;
 
    // ── void function — no return value, just does something ──
    function void display();
        $display("[Frame #%0d] dst=%h  src=%h  len=%0d  crc=0x%08h",
                  frame_id, dst_mac, src_mac,
                  payload.size(), crc);
    endfunction
 
    // ── function that returns a value ─────────────────────────
    function int get_length();
        return payload.size();   // accesses property directly
    endfunction
 
    // ── function that computes and stores internally ───────────
    function void compute_crc();
        bit [31:0] acc = 32'hFFFF_FFFF;
        foreach (payload[i])
            acc = acc ^ (payload[i] << (i % 24));
        crc = ~acc;   // writes directly to class property
    endfunction
 
    // ── function that returns a pass/fail bit ─────────────────
    function bit is_valid();
        if (payload.size() < 46)  return 0;   // min Ethernet payload
        if (payload.size() > 1500) return 0;   // max payload
        return 1;
    endfunction
 
    // ── string function (useful for logging) ──────────────────
    function string to_string();
        return $sformatf("Frame#%0d len=%0d valid=%0b",
                          frame_id, get_length(), is_valid());
    endfunction
 
endclass

Notice how compute_crc() reads payload and writes crc directly, without either being passed as a parameter. Methods always have full access to the class's own properties. This is what makes classes so much cleaner than passing a dozen variables into a standalone function.

Writing Tasks Inside a Class

Tasks go inside a class whenever you need to consume simulation time — waiting for a clock edge, applying a delay, or driving signals through a virtual interface. A driver class, for example, almost always has a drive() task.

Task Methods — Driving and Waiting
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// A simplified driver class to show task methods
class ApbDriver;
 
    virtual apb_if vif;          // virtual interface to DUT
    int            drive_count;   // tracks how many txns we've driven
 
    function new(virtual apb_if vi);
        vif         = vi;
        drive_count = 0;
    endfunction
 
    // ── Task: waits for clock edges (consumes sim time) ───────
    task wait_cycles(int n = 1);
        repeat(n) @(posedge vif.pclk);
    endtask
 
    // ── Task: drives one APB write transaction ────────────────
    task write(bit [31:0] addr, bit [31:0] data);
        @(posedge vif.pclk);
        vif.psel   <= 1;
        vif.paddr  <= addr;
        vif.pwdata <= data;
        vif.pwrite <= 1;
        vif.penable <= 0;
 
        @(posedge vif.pclk);
        vif.penable <= 1;
 
        @(posedge vif.pclk);
        vif.psel    <= 0;
        vif.penable <= 0;
 
        drive_count++;   // update class property directly
        $display("[APBDriver] WR addr=0x%08h data=0x%08h (txn #%0d)",
                  addr, data, drive_count);
    endtask
 
    // ── Task: drives one APB read, returns data via output arg
    task read(bit [31:0] addr, output bit [31:0] rdata);
        @(posedge vif.pclk);
        vif.psel   <= 1;
        vif.paddr  <= addr;
        vif.pwrite <= 0;
        vif.penable <= 0;
 
        @(posedge vif.pclk);
        vif.penable <= 1;
 
        @(posedge vif.pclk);
        rdata = vif.prdata;    // capture read data
        vif.psel    <= 0;
        vif.penable <= 0;
 
        drive_count++;
    endtask
 
endclass

Three Method Patterns You Will Write Every Day

After a few months in verification you will notice you write the same three types of methods on nearly every class. Here they are, with the exact patterns to use.

Pattern 1 — display() / print()

Every transaction class should have a display() function. When a test fails, this is what prints to the log. The cleaner you write this, the faster you debug.

Pattern 1 — display() method
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
function void display(string prefix = "");
    $display("%s[Txn #%0d] %s addr=0x%08h data=0x%08h",
              prefix,
              txn_id,
              write ? "WR" : "RD",
              addr,
              data);
endfunction
 
// Called as:
t.display();                   // [Txn #3] WR addr=0x40001000 data=0x000000FF
t.display("[SCOREBOARD] ");    // [SCOREBOARD] [Txn #3] ...

Pattern 2 — copy() / clone()

When you need a genuinely independent duplicate of an object, you write a copy() function. Remember from page 9.2 — assigning a handle copies the reference, not the data. copy() fixes that.

Pattern 2 — copy() method
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Inside BusTransaction class:
 
function BusTransaction copy();
    BusTransaction c = new();  // create a fresh object
    c.addr   = this.addr;      // copy each field manually
    c.data   = this.data;
    c.write  = this.write;
    c.txn_id = this.txn_id;
    return c;                   // return the new independent object
endfunction
 
// Called as:
BusTransaction orig  = new();
BusTransaction clone = orig.copy();   // completely independent
 
clone.addr = 32'h1234_5678;   // only affects clone
orig.display();               // orig.addr unchanged

Pattern 3 — compare() / is_equal()

Scoreboards need to compare expected and actual transactions. A compare() function gives you a clean, readable way to do that — and it logs exactly which field mismatched when the check fails.

Pattern 3 — compare() method
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Inside BusTransaction class:
 
function bit compare(BusTransaction other);
    bit match = 1;
 
    if (this.addr !== other.addr) begin
        $error("ADDR mismatch: exp=0x%08h  got=0x%08h",
                this.addr, other.addr);
        match = 0;
    end
 
    if (this.data !== other.data) begin
        $error("DATA mismatch: exp=0x%08h  got=0x%08h",
                this.data, other.data);
        match = 0;
    end
 
    if (this.write !== other.write) begin
        $error("WRITE mismatch: exp=%0b  got=%0b",
                this.write, other.write);
        match = 0;
    end
 
    return match;
endfunction
 
// Inside a scoreboard:
if (!expected.compare(actual))
    $error("[SCOREBOARD] Transaction mismatch on txn #%0d", expected.txn_id);
else
    $display("[SCOREBOARD] PASS txn #%0d", expected.txn_id);

Full Working Example — AXI4-Lite Transaction

Here is a complete, self-contained transaction class with all the patterns from this page combined. You can use this as a starting template for your own projects.

Full Class — AXI4-Lite Transaction with All Patterns
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class Axi4LiteTxn;
 
    // ── Properties ────────────────────────────────────────────
    typedef enum { AXI_READ, AXI_WRITE } dir_e;
 
    rand dir_e           direction;
    rand bit [31:0]    addr;
    rand bit [31:0]    wdata;
    rand bit [3:0]     wstrb;   // write strobe (byte enables)
    bit [31:0]         rdata;   // filled after read
    bit [1:0]          resp;    // 00=OKAY 10=SLVERR
    int                 txn_id;
 
    // ── Constraints ───────────────────────────────────────────
    constraint c_align  { addr[1:0] == 2'b00; }  // word-aligned
    constraint c_range  { addr inside {[32'h4000_0000 : 32'h4000_FFFF]}; }
    constraint c_strb   { (direction == AXI_READ) -> (wstrb == 4'b0000); }
 
    // ── Constructor ───────────────────────────────────────────
    function new(int id = 0);
        txn_id = id;
        resp   = 2'b00;
    endfunction
 
    // ── display() ─────────────────────────────────────────────
    function void display(string prefix = "");
        if (direction == AXI_WRITE)
            $display("%s[AXI #%0d] WR  addr=0x%08h  data=0x%08h  strb=%04b",
                      prefix, txn_id, addr, wdata, wstrb);
        else
            $display("%s[AXI #%0d] RD  addr=0x%08h  rdata=0x%08h  resp=%02b",
                      prefix, txn_id, addr, rdata, resp);
    endfunction
 
    // ── is_okay() — quick response check ──────────────────────
    function bit is_okay();
        return (resp == 2'b00);
    endfunction
 
    // ── copy() ────────────────────────────────────────────────
    function Axi4LiteTxn copy();
        Axi4LiteTxn c = new(txn_id);
        c.direction = direction;
        c.addr      = addr;
        c.wdata     = wdata;
        c.wstrb     = wstrb;
        c.rdata     = rdata;
        c.resp      = resp;
        return c;
    endfunction
 
    // ── compare() ─────────────────────────────────────────────
    function bit compare(Axi4LiteTxn other);
        bit ok = 1;
        if (addr      !== other.addr)      begin $error("addr mismatch");  ok=0; end
        if (direction !== other.direction) begin $error("dir mismatch");   ok=0; end
        if (direction == AXI_WRITE && wdata !== other.wdata)
            begin $error("wdata mismatch"); ok=0; end
        return ok;
    endfunction
 
endclass
 
// ── Quick testbench ───────────────────────────────────────────
module tb;
    Axi4LiteTxn q[$];
 
    initial begin
        // Generate 8 random transactions
        for (int i = 0; i < 8; i++) begin
            Axi4LiteTxn t = new(i);
            if (!t.randomize()) $fatal(1, "randomize failed");
            q.push_back(t);
        end
 
        // Print all
        foreach(q[i]) q[i].display("GEN: ");
 
        // Clone one and verify independence
        Axi4LiteTxn clone = q[0].copy();
        clone.addr = 32'h4000_FF00;
        $display("\nOriginal:"); q[0].display();
        $display("Clone:   ");  clone.display();
 
        // Compare original vs clone (addr differs — should FAIL)
        if (!q[0].compare(clone))
            $display("Compare correctly detected a mismatch.");
    end
endmodule

Quick Reference

KeywordWhat it doesUse for
int / bit / stringPlain instance propertyIDs, flags, names — values you set manually
randRandomisable instance propertyAddresses, data, burst types — anything the solver picks
randcCyclic random — exhausts all values before repeatingSequence numbers, scenario selectors, IDs
functionMethod with no time consumption; can return a valuedisplay, compare, copy, compute, validate
taskMethod that can consume simulation timedrive, send, wait_for_response, receive
function voidFunction that returns nothingdisplay(), print(), reset() — side-effect only
void'(...)Discard a return value explicitlyvoid'(randomize()) — suppress unused-return warning

Verification Usage — Properties & Methods That Earn Their Keep

In a production testbench, every property and method is a deliberate choice — what state needs to persist, what state needs to be queried, what state needs to mutate. The patterns below show how senior verification engineers structure transactions, scoreboard entries, and reference models so that the rest of the testbench stays maintainable.

SystemVerilog — A production-grade transaction class
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class axi_xact;
    // ── Required properties (random) — the "user input" ───────────
    rand bit [31:0] addr;
    rand bit [63:0] data;
    rand bit            is_write;
    rand bit [2:0]  burst_size;
 
    // ── Computed properties — derived from required, set by methods ──
    bit [7:0] crc;
    time       sent_time;
    int        retry_count;
 
    // ── Static properties — class-wide statistics ─────────────────
    static int total_created;
    static int total_failed;
 
    function new();
        total_created++;
        retry_count = 0;
    endfunction
 
    // ── Pure-query method (function) — no side effects ───────────
    function bit is_aligned();
        return (addr[burst_size-1:0] == '0);
    endfunction
 
    // ── Mutating method (function, returns nothing) ──────────────
    function void compute_crc();
        crc = 0;
        foreach (data[i])
            crc = crc ^ data[i];
    endfunction
 
    // ── Display method — pure, returns formatted string ──────────
    function string describe();
        return $sformatf("AXI %s @0x%08h data=0x%016h crc=0x%02h",
                         is_write ? "WR" : "RD", addr, data, crc);
    endfunction
 
    // ── Task — drives the bus, consumes time ─────────────────────
    task drive(virtual axi_if vif);
        @(posedge vif.clk);
        vif.awaddr  <= addr;
        vif.awvalid <= 1;
        sent_time = $time;
        while (!vif.awready) @(posedge vif.clk);
        vif.awvalid <= 0;
    endtask
 
    // ── Static method — class-wide query, no this ────────────────
    static function void print_statistics();
        $display("AXI xacts: created=%0d failed=%0d",
                 total_created, total_failed);
    endfunction
endclass

Simulation Behavior — Where Method Calls Land in the Scheduler

A method call is just a function/task call — the simulator does not allocate a new thread, does not change scheduling regions, and does not pause unless the method body itself has timing controls. Understanding what region a method call lands in lets you reason about TB-DUT races, assertion firing order, and concurrency interactions in class-based code.

SystemVerilog — Method call scheduling regions
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class monitor;
    int hit_count;
 
    // Function method — runs in caller's region, no time advances
    function void record_hit();
        hit_count++;
    endfunction
 
    // Task method — runs in caller's region but can suspend
    task wait_for_hits(int threshold);
        while (hit_count < threshold) @(posedge clk);
    endtask
endclass
 
module tb;
    monitor m;
    logic [7:0] data_in;
 
    initial begin
        m = new;
 
        // Function call inside always — Active region, same time slot
        always @(posedge clk)
            if (data_in > 8'h80)
                m.record_hit();         // no time advance; counts immediately
    end
 
    initial begin
        // Task call — caller suspends until task completes
        m.wait_for_hits(10);          // initial pauses here until threshold
        $display("Got 10 hits at time %0t", $time);
    end
endmodule

Waveform Analysis — Making Property Changes Visible

Class properties don't appear in waveform viewers — they live in heap memory with no hierarchical path. The standard pattern: mirror the property of interest into a module-level signal, updated by the class method that mutates the property. Verdi then shows the property's history as a normal signal trace.

SystemVerilog — Mirror class property to a tracked signal
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class arbiter_state;
    int grant_count [4];   // per-channel grant counts
 
    function void record_grant(int channel);
        grant_count[channel]++;
    endfunction
endclass
 
module tb;
    // Module-level signals — visible in waveform
    int tb_grants_ch0, tb_grants_ch1, tb_grants_ch2, tb_grants_ch3;
 
    arbiter_state st;
 
    initial begin
        st = new;
 
        forever begin
            @(posedge grant_valid);
            st.record_grant(grant_channel);
 
            // Mirror per-channel counts immediately after every grant
            tb_grants_ch0 = st.grant_count[0];
            tb_grants_ch1 = st.grant_count[1];
            tb_grants_ch2 = st.grant_count[2];
            tb_grants_ch3 = st.grant_count[3];
        end
    end
endmodule
 
// In Verdi, browse to:
//   tb.tb_grants_ch0 .. tb_grants_ch3
// Each shows a monotonic count timeline. Mismatch between channels (one
// channel starving) is immediately visible without grepping logs.

Industry Insights — How Senior Teams Structure Properties & Methods

Debugging Academy — 5 Real Property & Method 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
class packet;
    static bit [31:0] sequence_id;   // ← static shared across all packets
 
    function new();
        sequence_id++;               // intended to give each packet a unique ID
    endfunction
endclass
 
initial begin
    packet p1 = new;
    packet p2 = new;
    $display("p1.sequence_id=%0d p2.sequence_id=%0d",
             p1.sequence_id, p2.sequence_id);
    // Prints: p1.sequence_id=2 p2.sequence_id=2 (both see the latest)
end
2

DEBUG
Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class crc_helper;
    bit [7:0] poly = 8'h07;
 
    // Author wrote task out of habit; body has no timing controls
    task compute_crc(bit [63:0] data, output bit [7:0] result);
        result = 0;
        foreach (data[i]) result ^= data[i];
    endtask
endclass
 
// Caller tries to use in an assignment
crc_helper c = new;
assign computed_crc = c.compute_crc(payload);   // ← compile error
3

DEBUG
Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class transformer;
    function void rotate(ref bit [7:0] data);
        data = {data[6:0], data[7]};
    endfunction
endclass
 
// Caller passes a "constant"
transformer t = new;
bit [7:0] my_value = 8'hAA;
t.rotate(my_value);
$display("my_value=%h", my_value);   // prints 55, not AA
// Caller didn't expect their value to be mutated!
4

DEBUG
Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class counter;
    static int total_instances = 0;     // ← intended to count all instances ever
    int my_count = 100;                 // intended per-instance starting value
 
    function new();
        total_instances = total_instances + 1;
        // my_count is "auto-initialised" — but to what?
    endfunction
endclass
 
initial begin
    counter c1 = new;
    c1.my_count = 200;
    counter c2 = new;
    $display("c2.my_count=%0d", c2.my_count);
    // Prints 100 (correct) or 200 (depends on tool/version)
end
5

DEBUG
Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class scoreboard;
    local mailbox #(packet) expected_q;
 
    function new();
        expected_q = new(0);
    endfunction
 
    // Convenience getter — returns the queue handle
    function mailbox #(packet) get_expected();
        return expected_q;
    endfunction
endclass
 
// Caller misuses the getter
scoreboard scb = new;
mailbox #(packet) mb = scb.get_expected();
mb.put(some_packet);                         // mutates scoreboard's internal queue
// Now scoreboard has a transaction it didn't expect

Interview Q&A — 12 Questions on Properties & Methods

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

A property is data — a variable declared inside a class that holds state. A method is behaviour — a function or task declared inside a class that operates on the object's properties (and may take additional arguments). Properties are what the object knows; methods are what the object does. An object's identity is the combination of its property values and the methods callable on it.

Best Practices — Property & Method Rules to Walk Away With

  1. Default properties to local; promote only with a reason. External access goes through methods; encapsulation survives refactoring.
  2. Initialise every property explicitly in new(). Don't rely on declaration-site initialisers; behaviour varies across tools.
  3. Use static deliberately. Default to instance; promote to static only when the shared-across-instances semantic is intentional and documented.
  4. Use const for object identity and locked configuration. Compile-enforced immutability beats convention.
  5. Function for zero-time queries; task for time-consuming operations. If the body has no timing controls, it's a function.
  6. Each function does one thing. Pure queries don't mutate; mutations don't return values; logging is its own method.
  7. Pass by value unless mutation is the explicit contract. Reach for ref when mutation is required; const ref for read-only efficiency on large data.
  8. Never return handles to internal collections. Provide explicit operations (add_x, get_count); never hand out the internal mailbox/queue handle.
  9. Provide a describe() method on every transaction class. Logs and assertion messages should never inline-format from raw property access.
  10. Categorise properties up front: required, computed, lifecycle, static. Each category has its own conventions; mixing them produces confused class designs.