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
    • Classes, Objects & Handles
    • Inheritance and Polymorphism
    • Static, Encapsulation & Parameterized Classes
    • Virtual Interfaces: the Class-to-Pins Bridge
    • Events, Semaphores, Mailboxes and Processes
    • OOP Patterns for Testbenches
      • The Transaction Base Class
      • The Factory Pattern from Scratch
      • Singletons and Config Objects
      • Callbacks and Hooks
      • Building a Component Hierarchy by Hand
  • 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
Loading lesson

‹OOP Patterns for Testbenches
  1. Topics
  2. SystemVerilog
  3. Object-Oriented SystemVerilog for Verification
  4. OOP Patterns for Testbenches
  5. Callbacks and Hooks

Part 2 · OOP for Verification · Intermediate

Callbacks and Hooks

Driver hooks with stacked callbacks for error injection and sampling, without driver edits.

On this page

    Why this matters

    Every error test edited the signed-off driver, and each edit risked breaking the clean tests. Callbacks let a test add the error without touching the driver.

    After this lesson you can: give a driver hook points and a callback queue, write callbacks for error injection and sampling, and predict their order.

    Extension without edits

    A signed-off driver stays untouched while tests add behavior. The driver publishes hook points, and each test installs callback objects that run there.

    Subclassing forks verified code per test, and a factory override swaps the whole class. Callbacks add small behaviors onto one driver instead, and several stack on the same run.

    1. Install: the test pushes callbacks onto the driver queue before the run starts.

    2. Pre hooks: each callback sees the live transaction before the pins move.

    3. Drive: the frozen driver core moves the possibly mutated transaction to the pins.

    4. Post hooks: each callback samples exactly what was driven.

    Pre hooks may change the transaction; the core drives it; post hooks see exactly what went out.

    Question

    Factory override

    Callback

    What changes

    The created type

    Behavior at a hook point

    Installed

    Before build

    Before run

    Stacks with others

    No, one type wins

    Yes, in queue order

    Hooks, queue and iteration

    One base class declares empty virtual hooks. The driver holds a queue of that base type and iterates it at each hook with foreach.

    Empty beats pure virtual. Each callback overrides only the hooks it uses instead of stubbing every one.

    Hooks receive the driver handle and the live transaction. A callback can read component state and mutate the in-flight object, and hooks are tasks so delays stay legal.

    Coverage sampling is the classic post-drive callback: its hook calls a covergroup's sample(). Covergroups are taught in the Coverage part; the bench below records the same sampling intent with plain counters.

    Common mistakes

    Common pitfall

    Pure virtual hooks force every callback to implement every hook. The pattern then dies by boilerplate, and authors go back to editing the driver.

    Interview

    Likely questions: “inject errors without modifying the driver”, “why empty virtual instead of pure virtual”, and “callback versus driver subclass”.

    • Sketch: hook base, callback queue, one foreach per hook.

    • Rationale: empty hooks keep single-purpose callbacks small.

    • Tradeoff: subclassing forks code per test, while callbacks stack on one driver.

    Check yourself Coverage samples in post_drive while errors inject in pre_drive. Can sampling see clean data the pins never got?

    No. Every pre_drive runs before the pins move and every post_drive runs after, so the sampler always sees exactly what was driven.

    Check yourself Two callbacks override the same hook. Which runs first?

    The one installed first. A shared hook runs in queue order, so install injection before any same-hook monitor.

    Check yourself Why are the base hooks empty virtual instead of pure virtual?

    A callback then overrides only the hooks it uses. pure virtual would force every callback to implement every hook.

    Testbench

    Counter-based callbacks with no covergroup and no interface timing, proving hook order on eight transactions.

    design.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
    typedef class driver;
    
    class bus_txn;
      bit [31:0] data;
    
      function string convert2string();
        return $sformatf("data=%08h", data);
      endfunction
    endclass
    
    class driver_cb;
      virtual task pre_drive(driver drv, bus_txn t);
      endtask
      virtual task post_drive(driver drv, bus_txn t);
      endtask
    endclass
    
    class driver;
      driver_cb cbs[$];
      int unsigned n_driven;
      bit [31:0] last_data;
    
      function void add_cb(driver_cb cb);
        cbs.push_back(cb);
      endfunction
    
      task run(int unsigned n, bit [31:0] start_data);
        bus_txn t;
        bit [31:0] d;
        d = start_data;
        repeat (n) begin
          t = new();
          t.data = d;
          d = d + 1;
          foreach (cbs[i]) cbs[i].pre_drive(this, t);
          n_driven++;
          last_data = t.data;
          foreach (cbs[i]) cbs[i].post_drive(this, t);
        end
      endtask
    endclass
    
    class err_inject_cb extends driver_cb;
      int unsigned corrupted;
      int unsigned seen;
    
      virtual task pre_drive(driver drv, bus_txn t);
        seen++;
        if ((seen % 4) == 0) begin
          t.data = t.data ^ 32'h80000000;
          corrupted++;
        end
      endtask
    endclass
    
    class count_cb extends driver_cb;
      int unsigned n_post;
      int unsigned n_hi;
    
      virtual task post_drive(driver drv, bus_txn t);
        n_post++;
        if (t.data[31] != 0) n_hi++;
      endtask
    endclass
    
    Terminal
    PASS: pre hooks corrupt before pins, post hooks sample after

    Project step

    Add error injection to PacketFIFO's driver without editing it: give the driver a queue of callbacks with an empty pre_send hook, and write bad_check_cb, which flips the check byte of every second packet.

    One way to write it; the driver logs the bytes instead of driving pins, so the hook order is easy to see:

    design.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
    typedef bit [7:0] bytes_t[$];
    
    // Hooks a test can attach to the driver without editing it.
    class pkt_driver_cb;
      virtual function void pre_send(int n, ref bytes_t b);   // empty: override what you need
      endfunction
    endclass
    
    // Error injection: corrupt the check byte of every second packet.
    class bad_check_cb extends pkt_driver_cb;
      virtual function void pre_send(int n, ref bytes_t b);
        if (n % 2 == 1) b[b.size() - 1] = ~b[b.size() - 1];
      endfunction
    endclass
    
    class pkt_driver;
      pkt_driver_cb cbs[$];
      bytes_t       wire_log[$];                 // what went out, one entry per packet
      int           n_sent;
    
      function void add_cb(pkt_driver_cb cb);
        cbs.push_back(cb);
      endfunction
    
      function void send(bytes_t b);
        foreach (cbs[i]) cbs[i].pre_send(n_sent, b);   // hooks run in the order added
        wire_log.push_back(b);                   // stands in for driving the pins
        n_sent++;
      endfunction
    endclass
    
    Terminal
    packet 0: check byte bb ok
    packet 1: check byte 44 bad
    packet 2: check byte bb ok
    packet 3: check byte 44 bad
    PASS: the callback corrupted packets 1 and 3 without a change to pkt_driver

    Remember

    • Base hooks are empty virtual methods; callbacks override only what they need.

    • Pre hooks may change the transaction; post hooks see what was driven.

    • Hooks run in the order the callbacks were added.

    Rule of thumb: the test adds behavior; the driver's source stays frozen.

    How this was checked

    Run with Verilator 5.048 (--binary --timing) on 2026-10-18; every bench printed the PASS line shown, with no warnings. These class benches were run on Verilator only.

    Covergroups are not run in this course's checks: Verilator 5.048 compiles them but ignores the bins (warning COVERIGN), and Icarus 13 does not support them.

    Related topics

    • Next: Building a Component Hierarchy by Hand

    Previous

    63 / 259

    Next: Building a Component Hierarchy by Hand

    Up next

    Up nextBuilding a Component Hierarchy by Hand

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • The Transaction Base Class
    • The Factory Pattern from Scratch