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:
ErrorInjectionCallback.pre_driveexecutes before driving.MyDriver.driveperforms its core driving function.MonitorCallback.post_driveexecutes 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
- Modularity: Callbacks separate the core functionality of a component from additional behavior, making the code cleaner and more modular.
- Reusability: Once written, callback classes can be reused across different testbenches and test cases, reducing code duplication.
- Extendability: New behaviors can be added by creating additional callback classes, without changing the original component code.
- Runtime Flexibility: Callbacks allow for behavior changes during runtime, making test cases more dynamic and adaptable.
Example Use Cases for Callbacks
- Error Injection: Inject faults or corruptions into the data being driven to simulate error conditions.
- Monitoring: Add monitoring behavior to capture and log data, check protocol compliance, or gather statistics.
- Custom Checks: Define specific checks that need to be performed conditionally without modifying the main checkers or scoreboards.
- Coverage Collection: Collect coverage data selectively based on certain conditions or requirements in specific test scenarios.