Callbacks in System Verilog

What is a Callback?

A callback is a function or task defined in one part of the code but called from another. Callbacks allow additional functionality to be “hooked” into a component at runtime without modifying the core code. In verification environments, callbacks are typically used to customize or add behavior, such as injecting errors or altering data flow, for specific test cases.

For example, in a System Verilog testbench, a callback might be used to alter the behavior of a driver, monitor, or checker in different simulation scenarios. This approach keeps the main component code clean and general, while allowing dynamic customization via the callback mechanism.

How Callbacks Work in SystemVerilog

Callbacks in SystemVerilog are generally implemented using virtual interfaces or polymorphism. By creating a base class with virtual functions, you can define one or more callback methods, then allow extended classes to override these methods with specific behaviors. The primary class then calls these methods through polymorphic instances, invoking the callback behavior dynamically at runtime.

Here’s a step-by-step outline of how to set up and use callbacks in SystemVerilog.

Step 1: Define a Callback Class

First, create a callback class that contains virtual tasks or functions. These methods will be overridden later to provide the specific behavior for the callback.

class MyCallback;
    // Virtual function to be overridden in extended callback classes
    virtual task pre_drive(int data);
    endtask

    virtual task post_drive(int data);
    endtask
endclass

In this example, MyCallback is a base class with two virtual tasks, pre_drive and post_drive. These tasks will be called before and after the main driver’s drive task, respectively.

Step 2: Implement Callback Classes

Now, create one or more classes that extend the base callback class. Override the virtual methods in each extended class to specify custom behavior.

class ErrorInjectionCallback extends MyCallback;
    virtual task pre_drive(int data);
        $display("Injecting error before drive: %0d", data);
    endtask
endclass

class MonitorCallback extends MyCallback;
    virtual task post_drive(int data);
        $display("Monitoring data after drive: %0d", data);
    endtask
endclass

Here, ErrorInjectionCallback overrides the pre_drive task, and MonitorCallback overrides the post_drive task. You can now attach these callback classes to a component in your testbench to perform specific actions before and after the drive.

Step 3: Define a Component with Callback Support

In the driver or component class, create a mechanism for registering and invoking the callbacks. This can be done by creating an array of callbacks and iterating through them during execution.

class MyDriver;
    MyCallback callbacks[$];  // Array of callback objects

    // Method to add a callback to the driver
    function void register_callback(MyCallback cb);
        callbacks.push_back(cb);
    endfunction

    // Drive task where callbacks are called
    task drive(int data);
        // Pre-drive callback
        foreach (callbacks[i]) begin
            callbacks[i].pre_drive(data);
        end

        $display("Driving data: %0d", data);

        // Post-drive callback
        foreach (callbacks[i]) begin
            callbacks[i].post_drive(data);
        end
    endtask
endclass

In this MyDriver class, the callbacks array holds instances of MyCallback (or classes derived from it). The register_callback function adds a callback to this array, and the drive task calls pre_drive and post_drive on each registered callback.

Step 4: Register and Use Callbacks

With everything set up, you can create instances of the callback classes and register them with the driver. Then, when you call the drive task, the callbacks will execute as defined.

module testbench;
    initial begin
        int test_data = 10;
        MyDriver driver = new();
        
        // Create callback instances
        ErrorInjectionCallback error_cb = new();
        MonitorCallback monitor_cb = new();

        // Register callbacks with the driver
        driver.register_callback(error_cb);
        driver.register_callback(monitor_cb);

        // Run the drive task with callbacks
        driver.drive(test_data);
    end
endmodule

In this example, both ErrorInjectionCallback and MonitorCallback are registered with the driver. When driver.drive(test_data); is called, the following occurs:

  1. ErrorInjectionCallback.pre_drive executes before driving.
  2. MyDriver.drive performs its core driving function.
  3. MonitorCallback.post_drive executes after driving.

After simulating above codes, the output will be:

Output:

Injecting error before drive: 10
Driving data: 10
Monitoring data after drive: 10

The callbacks add error injection and monitoring behaviors without altering the driver’s core functionality, allowing for flexible and reusable design.

Benefits of Callbacks

  1. Modularity: Callbacks separate the core functionality of a component from additional behavior, making the code cleaner and more modular.
  2. Reusability: Once written, callback classes can be reused across different testbenches and test cases, reducing code duplication.
  3. Extendability: New behaviors can be added by creating additional callback classes, without changing the original component code.
  4. Runtime Flexibility: Callbacks allow for behavior changes during runtime, making test cases more dynamic and adaptable.

Example Use Cases for Callbacks

  1. Error Injection: Inject faults or corruptions into the data being driven to simulate error conditions.
  2. Monitoring: Add monitoring behavior to capture and log data, check protocol compliance, or gather statistics.
  3. Custom Checks: Define specific checks that need to be performed conditionally without modifying the main checkers or scoreboards.
  4. Coverage Collection: Collect coverage data selectively based on certain conditions or requirements in specific test scenarios.