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:
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
endclassInstance 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.
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
endmoduleProperty 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.
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=5000Methods — 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 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
endclassNotice 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.
// 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
endclassThree 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.
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.
// 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 unchangedPattern 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.
// 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.
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
endmoduleQuick Reference
| Keyword | What it does | Use for |
|---|---|---|
int / bit / string | Plain instance property | IDs, flags, names — values you set manually |
rand | Randomisable instance property | Addresses, data, burst types — anything the solver picks |
randc | Cyclic random — exhausts all values before repeating | Sequence numbers, scenario selectors, IDs |
function | Method with no time consumption; can return a value | display, compare, copy, compute, validate |
task | Method that can consume simulation time | drive, send, wait_for_response, receive |
function void | Function that returns nothing | display(), print(), reset() — side-effect only |
void'(...) | Discard a return value explicitly | void'(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.
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
endclassSimulation 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.
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
endmoduleWaveform 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.
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.
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)
endclass 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 errorclass 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!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)
endclass 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 expectInterview 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
- Default properties to
local; promote only with a reason. External access goes through methods; encapsulation survives refactoring. - Initialise every property explicitly in
new(). Don't rely on declaration-site initialisers; behaviour varies across tools. - Use
staticdeliberately. Default to instance; promote to static only when the shared-across-instances semantic is intentional and documented. - Use
constfor object identity and locked configuration. Compile-enforced immutability beats convention. - Function for zero-time queries; task for time-consuming operations. If the body has no timing controls, it's a function.
- Each function does one thing. Pure queries don't mutate; mutations don't return values; logging is its own method.
- Pass by value unless mutation is the explicit contract. Reach for
refwhen mutation is required;const reffor read-only efficiency on large data. - Never return handles to internal collections. Provide explicit operations (
add_x,get_count); never hand out the internal mailbox/queue handle. - Provide a
describe()method on every transaction class. Logs and assertion messages should never inline-format from raw property access. - Categorise properties up front: required, computed, lifecycle, static. Each category has its own conventions; mixing them produces confused class designs.