Classes model behavior that changes at runtime; modules and interfaces model structure fixed at elaboration. The virtual interface is the one bridge between the two worlds.
A module is a static hardware container, fixed at elaboration with no inheritance. A class is a dynamic software object created with new, supporting inheritance, polymorphism and randomization.
Testbenches need runtime flexibility: thousands of transactions created and destroyed, behavior swapped by extension without touching the driver. DUTs stay modules because hardware is static structure with timing.
Level | What happens | When it bites |
|---|---|---|
Handle assignment ( | One object, two names | Editing through either name changes both |
Shallow copy ( | Two objects sharing nested ones | Editing the copy payload corrupts the original |
Deep copy (hand-written | Fully independent objects | Monitors must use it before reusing a transaction |
The listing below is class declarations only, with no top module, so this course's checks do not run it. It shows the shared-payload bug and the recursive fix.
// not-run: fragment: class declarations only, no top module
class payload;
bit [7:0] len;
function payload copy();
payload c = new();
c.len = len;
return c;
endfunction
endclass
class packet;
bit [7:0] hdr;
payload pl;
function packet copy(); // deep copy: written by hand
packet c = new();
c.hdr = hdr;
c.pl = (pl == null) ? null : pl.copy();
return c;
endfunction
endclass
// p2 = new p1; shallow: p2.pl and p1.pl are the same objectWithout classes, plain assignment already copies by value. The testbench below proves it on packed headers, which is exactly the guarantee class handles do not give.
typedef struct packed { logic [7:0] hdr; logic [7:0] len; } hdr_t;
module tb_copy_plain;
hdr_t a, b;
int errors = 0;
initial begin
a.hdr = 8'hA5; a.len = 8'd16;
b = a;
b.len = 8'd99;
if (a.len !== 8'd16) errors++; // original untouched
if (b.hdr !== 8'hA5) errors++; // header carried over
if (errors == 0) $display("PASS: plain assignment copies and the original stays intact");
else $fatal(1, "FAIL: copy semantics wrong");
$finish;
end
endmoduleA virtual method dispatches on the object type, not the handle type. Components hold base handles while tests inject derived objects with overridden behavior, and the driver is never edited.
Without virtual, the base version runs even when the object is derived. The override is silently ignored, which is the worst kind of stimulus bug.
The listing below is class declarations only, with no top module, so this course's checks do not run it.
// not-run: fragment: class declarations only, no top module
class base_txn;
rand bit [7:0] data;
virtual function void apply();
// normal drive would go here
endfunction
endclass
class corrupt_txn extends base_txn;
function void apply(); // override: corrupt on purpose
data = data ^ 8'hFF;
endfunction
endclass
// driver code, never edited:
// base_txn t; gen2drv.get(t); t.apply();$cast is a checked downcast from a base handle to a derived handle. As a function it returns 0 on failure so the code can branch; as a task it raises a run-time error on failure, which is not fatal.
Its second use converts an integral value to an enum, where direct assignment is a compile error. Upcasts never need a cast since every derived object is already a base object.
// snippet
// base_txn b; err_txn e;
// function form: branch on the actual type
// if ($cast(e, b)) $display("error txn");
// task form: failure is a testbench bug, fail loudly
// $cast(e, b);
// enum conversion: op_e op; $cast(op, rdata);A static property has one copy shared by all instances, alive from time zero. The canonical uses are transaction ID stampers and debug counters callable without an instance.
Static state is global state in disguise: it couples tests invisibly and goes stale across restarts. Use it for IDs and counters, never as a configuration back-channel.
// snippet
// class fifo_txn;
// static int next_id = 0; // one copy for every instance
// int id;
// function new();
// id = next_id++; // free unique ID per transaction
// endfunction
// endclassA class with type or value parameters is written once and specialized per transaction type. Each specialization is a distinct, incompatible type.
The standard trick for a common handle is a non-parameterized base class that the parameterized class extends. The built-in mailbox with a type argument is the example everyone already uses.
An interface bundles a signal group into one named port. A 40-signal bus becomes one connection line, and protocol assets travel with it: clocking blocks, modports, assertions and tasks.
Modports are compile-time direction contracts per connection point. The compiler rejects a module driving a signal its modport declares as input, which kills contention bugs before simulation.
They are compile-time only. Code holding the raw interface bypasses the contract entirely.
input #1step samples in the Preponed region, the value just before the edge, exactly what a flop captures. output with a small delay drives past the hold window.
#1step differs from #0: #0 samples after the edge updates may have landed, so it can read post-edge values. Together the skews remove sampling and drive races for every signal accessed through the clocking block.
CLOCKING BLOCK TIMING
setup edge hold
────────────────┬──┬──┬────────────────────► time
│ ▲ │
input #1step ──┘ │ └── output #2ns drives here
samples here │ (after hold window)
(Preponed: value clk
before the edge) risesThe listing below is an interface with no module or testbench, so this course's checks do not run it.
// not-run: fragment: an interface only, no top module
interface fifo_if (input logic clk, input logic rst_n);
logic push, pop, full, empty;
logic [7:0] wdata, rdata;
clocking cb @(posedge clk);
default input #1step output #2ns;
output push, pop, wdata;
input full, empty, rdata;
endclocking
modport DUT (input push, pop, wdata, clk, rst_n,
output full, empty, rdata);
modport TB (clocking cb, input rst_n);
endinterfacePre-edge sampling needs no clocking block to prove. In the testbench below, active-region reads always precede NBA updates, so the sampled value matches what a flop would capture.
module tb_preedge;
timeunit 1ns; timeprecision 1ps;
logic clk = 0;
always #5 clk = ~clk;
logic sig = 0;
logic pre_val, post_val;
int errors = 0;
always_ff @(posedge clk) sig <= ~sig;
initial begin
@(posedge clk);
pre_val = sig; // active region: pre-edge value
#1;
post_val = sig; // NBA settled: post-edge value
if (pre_val !== 1'b0) errors++;
if (post_val !== 1'b1) errors++;
@(posedge clk);
pre_val = sig; #1; post_val = sig;
if (pre_val !== 1'b1) errors++;
if (post_val !== 1'b0) errors++;
if (errors == 0) $display("PASS: edge sampling sees pre-edge values like a flop");
else $fatal(1, "FAIL: sampling order wrong");
$finish;
end
endmoduleClasses have no elaboration-time existence, so they cannot have ports. A virtual interface is a runtime handle to a real interface instance, and the one sanctioned bridge into the static module world.
The standard chain is constructor injection: top builds the real interface and passes the handle down through environment, agent, driver and monitor. UVM uses config-db lookup for the same journey.
Each agent needs its own handle bound to its own instance. Binding every agent to instance zero drives one physical port from all agents while the other ports sit idle.
The symptom is triple traffic on port zero and silence elsewhere. Per-instance keys in UVM, or per-agent constructor arguments outside it, prevent it.
The listing below is a class that needs the fifo_if interface above and has no top module, so this course's checks do not run it. It names the null-handle defense every driver needs.
// not-run: fragment: needs the fifo_if interface above and has no top module
class driver;
virtual fifo_if vif; // null until assigned
function new(virtual fifo_if vif);
this.vif = vif;
endfunction
task automatic run();
if (vif == null) $display("vif never assigned");
else $display("vif bound");
endtask
endclassvirtual, a base handle holds a derived object and calls the overridden method. Which version runs?The base version. Dispatch follows the declared handle type, silently ignoring the override with no warning.
Triple traffic on port zero and nothing on the other ports. Each agent needs its own handle bound to its own instance.