An interface class declares only pure virtual methods, parameters and typedefs. A class can implements any number of them while still extending one ordinary base class. That gives SystemVerilog multiple inheritance of APIs, not of data or code.
This matters for checkers. A protocol checker may already extend a vendor base class or a reporting base. With interface classes it can also promise "I observe transactions" and "I report results" without a shared ancestor that forces a fixed hierarchy on every checker in the project.
Need | virtual class | interface class |
|---|---|---|
Shared data or code | Yes | No (pure virtual methods only) |
How many per class | One (extends) | Any number (implements) |
Combine APIs | No | interface class C extends A, B; |
Typical use | Base transaction, base component | Observer, reporter, callback, comparable |
A small adder registers its sum and parity. Two checkers implement plugin_checker, which itself extends two contracts: txn_observer and reportable. A logger implements only txn_observer. The registry stores handles by contract and never names a concrete checker class.
class txn;
bit [7:0] a, b;
bit [8:0] sum;
bit parity;
endclass
// Two small contracts. A class can sign either, or both.
interface class txn_observer;
pure virtual function void observe(txn t);
endclass
interface class reportable;
pure virtual function string name();
pure virtual function int unsigned seen();
pure virtual function int unsigned errors();
endclass
// A checker is anything that both observes and reports: API inheritance only.
interface class plugin_checker extends txn_observer, reportable;
endclass
// Plug-in checkers: no common base class, only shared contracts.
class sum_checker implements plugin_checker;
local int unsigned n, errs;
virtual function void observe(txn t);
n++;
if (t.sum !== 9'(t.a) + 9'(t.b)) begin
errs++;
$display("sum_checker: %0d+%0d gave %0d", t.a, t.b, t.sum);
end
endfunction
virtual function string name(); return "sum_checker"; endfunction
virtual function int unsigned seen(); return n; endfunction
virtual function int unsigned errors(); return errs; endfunction
endclass
class parity_checker implements plugin_checker;
local int unsigned n, errs;
virtual function void observe(txn t);
n++;
if (t.parity !== ^t.sum) begin
errs++;
$display("parity_checker: sum %h parity %b", t.sum, t.parity);
end
endfunction
virtual function string name(); return "parity_checker"; endfunction
virtual function int unsigned seen(); return n; endfunction
virtual function int unsigned errors(); return errs; endfunction
endclass
// A logger only observes; it has nothing to report.
class txn_logger implements txn_observer;
int unsigned lines;
virtual function void observe(txn t); lines++; endfunction
endclass
// The registry knows the contracts, never the concrete classes.
class checker_registry;
local txn_observer observers[$];
local reportable reporters[$];
// A checker goes on both lists; plain upcasts, no $cast needed.
function void add_checker(plugin_checker c);
observers.push_back(c);
reporters.push_back(c);
endfunction
function void add_observer(txn_observer o);
observers.push_back(o);
endfunction
function void publish(txn t);
foreach (observers[i]) observers[i].observe(t);
endfunction
function int unsigned report(int unsigned expected_txns, int unsigned expected_checkers);
int unsigned bad = 0;
if (reporters.size() != expected_checkers) begin
$display("registry: %0d checkers registered, expected %0d", reporters.size(), expected_checkers);
bad++;
end
foreach (reporters[i]) begin
if (reporters[i].seen() != expected_txns) begin
$display("%s saw %0d of %0d", reporters[i].name(), reporters[i].seen(), expected_txns);
bad++;
end
bad += reporters[i].errors();
end
return bad;
endfunction
endclass
module add_par (
input logic clk,
input logic [7:0] a, b,
output logic [8:0] sum,
output logic parity
);
always_ff @(posedge clk) begin
sum <= a + b;
parity <= ^(9'(a) + 9'(b)); // widen first: a reduction operand is self-determined
end
endmodule
The registry's report is an activity gate as well as an error sum. It requires the expected number of checkers to be registered and each one to have seen every result. A checker that was never registered reports nothing and would otherwise make a regression greener.
Mutant runs. Removing reg_h.add_checker(pc) fails with registry: 1 checkers registered, expected 2. Writing the DUT's parity as ^(a + b) fails with parity errors: the operand of a reduction operator is self-determined, so a + b is evaluated at 8 bits and the carry is lost. That bug appeared while writing this example, and the parity checker caught it.
The monitor could call each checker by name. Then adding a checker means editing the monitor, and a project that does not want a checker must edit it out. With a registry, the monitor publishes to whatever registered, and the test or environment decides which checkers exist. The monitor, the registry and the checkers can live in different packages and be owned by different people.
The registry also gives one place to enforce end-of-test rules: every checker saw traffic, none reported errors, and the expected set was present. That is the same sanity gate a senior reviewer asks for, written once instead of in every test.
A natural registry would accept a txn_observer and use $cast to see whether the same object is also reportable. The LRM allows a cast between interface class types, checked at run time against the actual object. In our runs Verilator 5.048 returned 0 for that sideways cast even when the object implemented both, while a cast to a combined interface class such as plugin_checker worked. The bench avoids the sideways cast by giving checkers the combined contract and registering them through add_checker().
The same idea replaces callback base classes. A driver holds a queue of drive_cb handles and calls them at fixed points. Any class can implement the hook, including one that already extends something else.
// snippet
interface class drive_cb;
pure virtual function void pre_drive(beat b); // may corrupt b (a function cannot delay; use a task for that)
endclass
class error_injector extends base_component implements drive_cb;
virtual function void pre_drive(beat b);
if ($urandom_range(0, 99) < 2) b.data ^= 16'h0001;
endfunction
endclass
// in the driver: foreach (cbs[i]) cbs[i].pre_drive(b);Not every observer reports. The logger implements only txn_observer, so it can be published to but has nothing to report. Two queues let each object join exactly the roles it implements.
No. An interface class may contain pure virtual methods, typedefs and parameters (constants), but no data members. State belongs to the implementing class.