OOP & Classes¶
UVM is, structurally, just a large SystemVerilog class library. Everything about how UVM "feels" to write comes directly from ordinary object-oriented concepts — nothing UVM-specific is happening at the language level.
Handles, not variables¶
A class variable is a handle (like a pointer/reference), not the object itself:
class packet;
bit [7:0] addr;
bit [7:0] data;
endclass
packet p1; // p1 is a handle, currently null — no object exists yet
p1 = new(); // now p1 points to a real object
packet p2 = p1; // p2 points to the SAME object — not a copy
p2.addr = 8'hFF;
$display(p1.addr); // prints FF — p1 and p2 are the same object
This trips up almost everyone coming from RTL/procedural code once: assignment between class handles never copies data, it always aliases. To actually copy an object's contents, you need an explicit copy() method (UVM objects that extend uvm_object get this via uvm_field_* macros or a hand-written do_copy).
Inheritance and polymorphism¶
class base_packet;
virtual function void print();
$display("base_packet");
endfunction
endclass
class ext_packet extends base_packet;
function void print();
$display("ext_packet");
endfunction
endclass
base_packet handle;
ext_packet ext = new();
handle = ext; // a base-type handle can point to a derived-type object
handle.print(); // prints "ext_packet" — NOT "base_packet"
That last line is the entire point of virtual methods, and it's the mechanism the UVM factory depends on: code written against a base-class handle (uvm_driver#(base_packet)) can transparently operate on derived objects substituted in later, without the calling code ever being rewritten. See The Factory.
virtual on methods vs. virtual on interfaces
These are two completely unrelated uses of the same keyword. virtual function/virtual task means "resolve which override to call at runtime" (OOP polymorphism). virtual interface means "a handle to a hardware interface instance" (used to let a class-based driver/monitor connect to module-based DUT signals). Don't conflate them — a common beginner confusion.
rand and constraints¶
class txn;
rand bit [7:0] addr;
rand bit [3:0] len;
constraint c_len { len inside {[1:8]}; }
constraint c_addr { addr % 4 == 0; } // word-aligned
endclass
txn t = new();
assert(t.randomize()); // solves all active constraints, assigns addr/len
randomize() returns 0 on failure (constraints are contradictory/unsatisfiable) — always check the return value, at minimum with an assert() as above, so a silently-failed randomization doesn't send garbage/stale values into the DUT and waste hours of debug on a "DUT bug" that's actually a testbench bug.
Constraints can be turned off per-instance and overridden by inheritance:
t.c_len.constraint_mode(0); // disable just this constraint for this object
t.addr.rand_mode(0); // stop randomizing addr entirely — treat it as a fixed value
rand_mode/constraint_mode and inheritance
Both are not re-entrant across a class hierarchy the way you might expect from a quick read: disabling a constraint on a base-class handle doesn't necessarily affect constraints added in an extended class, and vice versa — the scoping follows where the constraint block is declared, not the static/dynamic type of the handle you called it through. Test this explicitly if a derived sequence item behaves unexpectedly after selectively disabling constraints.
Static vs. instance members¶
class counter;
static int total_created = 0; // shared across ALL instances — one storage location
int id; // per-instance
function new();
id = total_created;
total_created++;
endfunction
endclass
static members are shared program-wide, not per-object — useful for a global transaction counter or a singleton-style registry, but a frequent source of subtle bugs if used for anything that should have been per-instance (e.g. accidentally making a scoreboard's error count static and having every test class share one counter across supposedly-independent test runs in the same simulation).