SystemVerilog and UVM Callbacks - Complete Guide

Callbacks provide hooks to inject custom behavior into existing code without modification. They're essential for error injection, coverage collection, and extending testbench functionality in reusable VIPs.

What are Callbacks?

Callbacks are predefined "hooks" that allow you to:

  • Extend functionality without modifying original code
  • Inject errors or modify data in specific tests
  • Add debug/coverage at strategic points
  • Keep VIP code clean while allowing customization
Key Benefit: Callbacks let test writers customize VIP behavior without touching the VIP source code.

Callback Flow

sequenceDiagram
    participant Test
    participant Driver
    participant Callback
    
    Test->>Callback: Register custom callback
    Test->>Driver: Start test
    Driver->>Driver: Get transaction
    Driver->>Callback: pre_drive() hook
    Callback->>Callback: Modify/inject error
    Callback-->>Driver: Return
    Driver->>Driver: Drive to DUT
    Driver->>Callback: post_drive() hook
    Callback->>Callback: Collect coverage
    Callback-->>Driver: Return

SystemVerilog Callbacks (OOP Approach)

Using polymorphism to implement callbacks:

Step 1: Define Callback Base Class

// Callback base class with virtual methods (hooks)
class driver_callback;

  // Pre-drive hook - called before driving transaction
  virtual task pre_drive(ref transaction tr);
    // Default: do nothing
  endtask

  // Post-drive hook - called after driving transaction
  virtual task post_drive(ref transaction tr);
    // Default: do nothing
  endtask

endclass

Step 2: Driver with Callback Support

class driver;

  // Queue of registered callbacks
  driver_callback callbacks[$];

  // Register a callback
  function void add_callback(driver_callback cb);
    callbacks.push_back(cb);
  endfunction

  // Main drive task
  task drive(transaction tr);
    // Call all pre_drive callbacks
    foreach (callbacks[i]) begin
      callbacks[i].pre_drive(tr);
    end

    // Actual driving logic
    $display("[DRV] Driving: addr=0x%0h data=0x%0h", tr.addr, tr.data);
    #10;

    // Call all post_drive callbacks
    foreach (callbacks[i]) begin
      callbacks[i].post_drive(tr);
    end
  endtask

endclass

Step 3: Custom Callback Implementation

// Error injection callback
class error_inject_callback extends driver_callback;

  int error_rate = 10;  // 10% error rate

  virtual task pre_drive(ref transaction tr);
    if ($urandom_range(100) < error_rate) begin
      $display("[CB] Injecting error: corrupting data");
      tr.data = tr.data ^ 32'hDEAD_BEEF;
    end
  endtask

endclass


// Coverage callback
class coverage_callback extends driver_callback;

  covergroup cg with function sample(transaction tr);
    addr_cp: coverpoint tr.addr[31:28] {
      bins low  = {[0:7]};
      bins high = {[8:15]};
    }
  endgroup

  function new();
    cg = new();
  endfunction

  virtual task post_drive(ref transaction tr);
    cg.sample(tr);
    $display("[CB] Coverage sampled: addr=0x%0h", tr.addr);
  endtask

endclass

Step 4: Test Using Callbacks

module tb;

  initial begin
    driver drv;
    transaction tr;
    error_inject_callback err_cb;
    coverage_callback cov_cb;

    drv = new();
    err_cb = new();
    cov_cb = new();

    // Register callbacks
    drv.add_callback(err_cb);
    drv.add_callback(cov_cb);

    // Run transactions
    repeat (5) begin
      tr = new();
      assert(tr.randomize());
      drv.drive(tr);
    end
  end

endmodule

UVM Callback Mechanism

UVM provides a structured callback framework with macros:

flowchart TD
    A["uvm_callback"] --> B["Define callback class"]
    B --> C["Register with `uvm_register_cb"]
    C --> D["Add callbacks in test"]
    D --> E["Execute via `uvm_do_callbacks"]
    
    style A fill:#dbeafe,stroke:#3b82f6
    style E fill:#d1fae5,stroke:#10b981

Step 1: Define UVM Callback Class

class my_driver_callback extends uvm_callback;

  `uvm_object_utils(my_driver_callback)

  function new(string name = "my_driver_callback");
    super.new(name);
  endfunction

  // Virtual callback methods
  virtual task pre_drive(my_driver drv, ref my_seq_item tr);
    // Default: empty
  endtask

  virtual task post_drive(my_driver drv, ref my_seq_item tr);
    // Default: empty
  endtask

endclass

Step 2: Driver with UVM Callbacks

class my_driver extends uvm_driver #(my_seq_item);

  `uvm_component_utils(my_driver)
  `uvm_register_cb(my_driver, my_driver_callback)

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  virtual task run_phase(uvm_phase phase);
    my_seq_item tr;

    forever begin
      seq_item_port.get_next_item(tr);

      // Execute pre_drive callbacks
      `uvm_do_callbacks(my_driver, my_driver_callback, pre_drive(this, tr))

      // Drive transaction
      drive_item(tr);

      // Execute post_drive callbacks
      `uvm_do_callbacks(my_driver, my_driver_callback, post_drive(this, tr))

      seq_item_port.item_done();
    end
  endtask

  virtual task drive_item(my_seq_item tr);
    `uvm_info(get_name(), $sformatf("Driving: %s", tr.convert2string()), UVM_MEDIUM)
    // Drive to interface...
    #10;
  endtask

endclass

Step 3: Custom UVM Callback

// Error injection callback
class error_inject_cb extends my_driver_callback;

  `uvm_object_utils(error_inject_cb)

  int unsigned error_rate = 20;  // 20% error rate

  function new(string name = "error_inject_cb");
    super.new(name);
  endfunction

  virtual task pre_drive(my_driver drv, ref my_seq_item tr);
    if ($urandom_range(100) < error_rate) begin
      `uvm_info("ERR_CB", "Injecting CRC error", UVM_LOW)
      tr.crc_error = 1;
    end
  endtask

endclass


// Scoreboard bypass callback
class scoreboard_bypass_cb extends my_driver_callback;

  `uvm_object_utils(scoreboard_bypass_cb)

  function new(string name = "scoreboard_bypass_cb");
    super.new(name);
  endfunction

  virtual task post_drive(my_driver drv, ref my_seq_item tr);
    if (tr.is_config_write) begin
      `uvm_info("SB_CB", "Config write - bypassing scoreboard", UVM_MEDIUM)
      tr.skip_scoreboard = 1;
    end
  endtask

endclass

Step 4: Register Callbacks in Test

class error_injection_test extends base_test;

  `uvm_component_utils(error_injection_test)

  error_inject_cb err_cb;

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);

    // Create callback instance
    err_cb = error_inject_cb::type_id::create("err_cb");
    err_cb.error_rate = 50;  // 50% for this test

    // Register callback with driver
    uvm_callbacks #(my_driver, my_driver_callback)::add(env.agent.driver, err_cb);
  endfunction

endclass

UVM Callback Macros

MacroUsageDescription
`uvm_register_cbIn component classRegisters callback type with component
`uvm_do_callbacksIn component methodsExecutes all registered callbacks
`uvm_do_callbacks_exit_onIn component methodsStops if callback returns specified value
uvm_callbacks#()::addIn test/envAdds callback instance to component
uvm_callbacks#()::deleteIn test/envRemoves callback from component

Callbacks vs Factory Overrides

AspectCallbacksFactory Override
PurposeAdd behavior at specific pointsReplace entire component/object
ScopeTargeted hooksComplete replacement
MultipleCan have multiple callbacksOne override at a time
Use caseError injection, coverageDifferent driver/monitor behavior
VIP modificationNone requiredNone required
// Callback: Add error injection at hook point
uvm_callbacks#(my_driver, my_driver_callback)::add(drv, err_cb);

// Factory: Replace entire driver class
my_driver::type_id::set_type_override(custom_driver::get_type());

Common Use Cases

1. Error Injection

virtual task pre_drive(my_driver drv, ref my_seq_item tr);
  // Corrupt data randomly
  if ($urandom_range(100) < error_rate) begin
    tr.data[7:0] = ~tr.data[7:0];  // Bit flip
    tr.inject_error = 1;
  end
endtask

2. Protocol Violation

virtual task pre_drive(my_driver drv, ref my_seq_item tr);
  // Violate timing for negative testing
  if (inject_timing_error) begin
    tr.setup_time = 0;  // Zero setup time
  end
endtask

3. Debug Logging

virtual task post_drive(my_driver drv, ref my_seq_item tr);
  `uvm_info("DEBUG", $sformatf("Transaction driven: %s", tr.convert2string()), UVM_HIGH)
endtask

4. Functional Coverage

virtual task post_drive(my_driver drv, ref my_seq_item tr);
  cg.sample(tr.addr, tr.cmd, tr.size);
endtask

Best Practices

  • Plan hooks early: Add callback hooks during VIP development
  • Keep hooks strategic: pre/post for major operations
  • Use ref parameters: Allow callbacks to modify transactions
  • Document hooks: List available callbacks in VIP documentation
  • Test callbacks: Verify callback mechanism works correctly

Interview Questions

Q1: What are callbacks in UVM?

Answer: Callbacks are hooks that allow users to inject custom code at predefined points without modifying the original component. They enable error injection, coverage collection, and protocol modifications in tests.

Q2: What macros are used for UVM callbacks?

Answer: `uvm_register_cb registers a callback type with a component, `uvm_do_callbacks executes registered callbacks, and uvm_callbacks#()::add registers a callback instance.

Q3: When would you use callbacks vs factory override?

Answer: Use callbacks for targeted modifications at specific points (error injection, coverage). Use factory override to completely replace a component or object with a different implementation.

Key Takeaways

  • Callbacks provide hooks for customization without code modification
  • SystemVerilog callbacks use polymorphism (virtual methods)
  • UVM provides structured macros: `uvm_register_cb, `uvm_do_callbacks
  • Common uses: error injection, coverage, debug logging
  • Multiple callbacks can be registered and executed in sequence
  • Plan callback hooks during VIP development
Author
Mayur Kubavat
VLSI Design and Verification Engineer sharing knowledge about SystemVerilog, UVM, and hardware verification methodologies.

Comments (0)

Leave a Comment