Part 2 · OOP for Verification · Intermediate

Object-Oriented SystemVerilog for Verification

Classes, handles, inheritance, polymorphism, virtual interfaces, interprocess communication, and the OOP patterns every modern testbench is built on.

Why testbenches are class-based

RTL lives in modules — static structures elaborated once at time zero, with fixed instance counts and fixed connectivity for the entire simulation. A testbench has the opposite needs: it must create thousands of transactions on the fly , randomize them, pass them between threads, mutate them per test without editing source, and throw them away when done. That demands dynamic objects — created at runtime with new(), referenced through handles, reclaimed automatically when no handle points to them.

Classes also bring inheritance and polymorphism : a base driver written against a base transaction can be reused for an extended transaction without touching the driver's source. That single property — extend behavior without modifying proven code — is why every methodology from VMM to UVM is class-based. Master plain SystemVerilog OOP first and UVM becomes a library of familiar patterns rather than magic.

Topic map

diagram
┌──────────────────────────────────────────────────────────────────────┐
│  OOP SECTION — topic map                                              │
├──────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  1. CLASSES & HANDLES                                                │
│     declaration │ new() │ handle vs object │ copy │ this │ lifetime  │
│                                                                      │
│  2. INHERITANCE & POLYMORPHISM                                       │
│     extends │ virtual methods │ $cast │ abstract │ containers │ traps│
│                                                                      │
│  3. CLASS FEATURES                                                   │
│     static │ parameterized │ const/local/protected │ extern          │
│                                                                      │
│  4. VIRTUAL INTERFACES                                               │
│     class ↔ module bridge │ vif passing │ clocking blocks            │
│                                                                      │
│  5. INTERPROCESS COMMUNICATION                                       │
│     mailbox │ semaphore │ event │ producer-consumer pipelines        │
│                                                                      │
│  6. OOP PATTERNS                                                     │
│     factory │ singleton │ callback │ layered TB architecture         │
│                                                                      │
└──────────────────────────────────────────────────────────────────────┘

Topics and sub-topics

  1. Classes, Objects & Handles — declaration, new(), handle semantics, copy, this, object lifetime.

  2. Inheritance & Polymorphism — extends, virtual dispatch, $cast, abstract classes, polymorphic containers, interview traps.

  3. Class Features — static members, parameterized classes, access qualifiers, extern methods.

  4. Virtual Interfaces — connecting the dynamic class world to static DUT pins.

  5. Interprocess Communication — mailboxes, semaphores, events between testbench threads.

  6. OOP Patterns — factory, singleton, callbacks, and layered testbench architecture.


Class world vs module world

A simulation has two parallel worlds. The module world is built at elaboration and never changes shape. The class world is built at runtime, grows and shrinks, and reaches the pins only through a virtual interface bridge. Keeping the boundary in your head explains most OOP rules — why classes cannot contain always blocks, why drivers need a vif handle, and why objects need no fixed instance path.

diagram
MODULE WORLD (static)              CLASS WORLD (dynamic)
  ─────────────────────              ─────────────────────
  elaborated at time 0               constructed at runtime
  fixed instance tree                objects come and go
  wires, always blocks               handles, methods, tasks
  hierarchical paths                 no fixed paths — handles only

  ┌────────────────────┐            ┌──────────────────────────┐
  │ top                │            │  test (initial block)    │
  │  ├── dut           │            │    env = new();          │
  │  │    (RTL)        │            │    ├── driver  obj       │
  │  └── intf          │◄──vif──────│    ├── monitor obj       │
  │       (signals,    │   bridge   │    └── scoreboard obj    │
  │        clocking)   │            │  txn objects ×1000s      │
  └────────────────────┘            └──────────────────────────┘

  vif = the ONLY doorway between the two worlds:
  class driver;  virtual dut_if vif;  // handle to a static interface
    task drive(); vif.cb.data <= txn.data; endtask
  endclass

What each world is good at

  • Modules — synthesizable structure, pin-accurate timing, fixed hierarchy that tools can elaborate and optimize.

  • Classes — randomizable data, runtime creation, inheritance for reuse, containers and IPC for thread coordination.

  • Interfaces sit between — declared like modules, handed to classes as virtual interface handles.

  • Interview angle: 'why can't a class drive DUT pins directly?' — classes have no elaboration-time connectivity; they need a vif handle to a static interface.

Key takeaways

  • Modules are static structure; classes are dynamic data and behavior — testbenches need both.

  • Objects are created with new() at runtime and reached only through handles.

  • Inheritance + virtual methods let you extend a proven testbench without editing its source.

  • The virtual interface is the single bridge from the class world to DUT pins.

Common pitfalls

  • Writing a testbench as one giant module with tasks — no reuse, no per-test customization, no randstate isolation.

  • Treating a handle as the object itself — assignment copies the reference, not the data.

  • Forgetting virtual on base-class methods — derived overrides silently never run through base handles.

  • Storing pin references in classes without a virtual interface — illegal or unportable hierarchical paths.