Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here6
  • SystemVerilog Language Foundations28
  • Object-Oriented SystemVerilog for Verification30
  • Constraint Randomization — Interview-Critical Deep Dive36
  • SystemVerilog Assertions — Interview-Critical Deep Dive40
  • Functional Coverage in SystemVerilog22
  • Testbench Architecture in SystemVerilog23
  • Advanced Language & Integration25
  • Senior & Interview Preparation21
  • Principal-Level Verification28
    • Reusable Testbench Architecture
      • Policy Classes
      • Abstract BFM
      • Parameterized Interfaces
      • Interface Classes
      • Layered Stimulus
    • Checking Out-of-Order Traffic
    • Assertion Depth
    • Coverage Modelling for Real Features
    • Capstone: Out-of-Order Memory Controller
    • Principal Judgment
Loading lesson

‹Reusable Testbench Architecture
  1. Topics
  2. SystemVerilog
  3. Principal-Level Verification
  4. Reusable Testbench Architecture
  5. Interface Classes

Part 9 · Principal-Level Verification · Advanced

Interface Classes

Contracts with interface class and implements for plug-in checkers and callbacks, multiple API inheritance, and a registry that knows only the contracts.

On this page

    Contracts without a common base

    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

    Runnable example: a checker registry

    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.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    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.

    Why a registry instead of direct calls

    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.

    Casting between contracts

    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().

    Callbacks with interface classes

    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.

    class.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 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);

    Common pitfall

    Adding a method to a widely implemented interface class. Every non-virtual class that implements it stops compiling until it provides the new method (Verilator: "implements 'A' but is missing implementation for 'f'"). Keep contracts small and stable, and add a new interface class for a new role instead of growing an old one. A virtual class can defer a method by redeclaring it pure virtual.

    Interview

    Likely question: “What is the difference between an abstract class and an interface class in SystemVerilog?” Answer: an abstract (virtual) class can hold data and implemented methods and is extended once; an interface class holds only pure virtual methods, types and parameters, and a class can implement many. Use interface classes for roles such as observer or reporter that cut across your class hierarchy.

    Check yourself Why does the registry keep two queues instead of one queue of plugin_checker handles?

    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.

    Check yourself Can an interface class declare a property such as int count?

    No. An interface class may contain pure virtual methods, typedefs and parameters (constants), but no data members. State belongs to the implementing class.

    Related topics

    • Previous: Parameterized Interfaces

    • Next: Layered Stimulus

    • Contracts and polymorphic containers (Part 4)

    • Callbacks and Hooks

    Previous

    235 / 259

    Next: Layered Stimulus

    Up next

    Up nextLayered Stimulus

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Policy Classes
    • Abstract BFM