How UVM Callback works?

In short: A UVM callback lets you change what a component (like a driver or monitor) does at chosen points, without touching that component’s source code. You define an empty hook method inside the component, register it, then plug in your own class later to inject errors, add checks, or collect coverage. It is the tool of choice when a base component is locked, such as a third-party VIP.

A callback is a small piece of code you attach to a component so it runs at a fixed spot during that component’s work. The base component stays exactly as written. You just supply the extra behaviour from outside. In UVM this is built on the uvm_callback base class and a set of macros that wire everything together.

A simple way to picture it

Think of a delivery driver who follows a fixed route every day. The delivery company does not want anyone rewriting the driver’s route. Instead, they leave a note slot on the dashboard. Before each stop, the driver checks the slot and does whatever the note says: skip a house, ring twice, or leave the parcel with a neighbour. The driver code never changes. You change behaviour only by dropping in a different note. A UVM callback is that note slot: the component reaches a fixed point, checks for attached callbacks, and runs whatever you plugged in.

When callbacks are the right choice

Callbacks shine when you need small, targeted changes to a component you should not or cannot edit. Common reasons include:

  • You are using an encrypted third-party VIP and only get published hook points such as “just after get_next_item” or “just before driving the bus”.
  • You want to inject an error into one test without breaking the clean behaviour every other test relies on.
  • You want a monitor to also feed a scoreboard or gather coverage, added per test rather than baked in.
  • The change is rare or minor, so building a whole new component through the factory would be more work than it is worth.

The building blocks of a UVM callback

A working callback needs four parts. Keep them in mind as you read the example below.

PartWhat it does
Callback classExtends uvm_callback and declares one or more virtual hook methods (empty by default).
`uvm_register_cbPlaced inside the component. Tells UVM which callback type is allowed on this component.
`uvm_do_callbacksPlaced at the hook point in the component. Runs every attached callback in order.
uvm_callbacks#(T,CB)::addCalled from a test to attach your callback object to a specific component instance.

A worked example: injecting an error from a test

This driver exposes one hook, inject_err. By default it does nothing, so normal tests drive clean traffic. One test attaches a callback that flips a bit, so only that test sees corrupted data. The base driver is never edited.

`include "uvm_macros.svh"
import uvm_pkg::*;

// 1) The callback class with an empty hook
typedef class spi_driver;
class spi_driver_cb extends uvm_callback;
  `uvm_object_utils(spi_driver_cb)
  function new(string name = "spi_driver_cb");
    super.new(name);
  endfunction

  // Hook methods must be virtual so a derived callback can override them
  virtual task pre_send(spi_driver drv, spi_item tr);
  endtask
endclass
// 2) The driver registers the callback and calls the hook
class spi_driver extends uvm_driver #(spi_item);
  `uvm_component_utils(spi_driver)
  `uvm_register_cb(spi_driver, spi_driver_cb)   // allow this callback type

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

  task run_phase(uvm_phase phase);
    forever begin
      seq_item_port.get_next_item(req);
      // Hook point: run every attached callback before we drive
      `uvm_do_callbacks(spi_driver, spi_driver_cb, pre_send(this, req))
      `uvm_info("SPI_DRV", $sformatf("driving addr=%0h data=%0h",
                req.addr, req.data), UVM_LOW)
      seq_item_port.item_done();
    end
  endtask
endclass
// 3) A concrete callback that corrupts one bit of data
class spi_bitflip_cb extends spi_driver_cb;
  `uvm_object_utils(spi_bitflip_cb)
  function new(string name = "spi_bitflip_cb");
    super.new(name);
  endfunction

  task pre_send(spi_driver drv, spi_item tr);
    tr.data[0] = ~tr.data[0];   // flip bit 0 on the way out
    `uvm_info("BITFLIP_CB", "injected single-bit error", UVM_LOW)
  endtask
endclass
// 4) The test attaches the callback to one driver instance
class err_inject_test extends uvm_test;
  `uvm_component_utils(err_inject_test)
  spi_env env;

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

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    env = spi_env::type_id::create("env", this);
  endfunction

  function void end_of_elaboration_phase(uvm_phase phase);
    spi_bitflip_cb cb = spi_bitflip_cb::type_id::create("cb");
    // attach cb to this specific driver only
    uvm_callbacks#(spi_driver, spi_driver_cb)::add(env.agt.drv, cb);
  endfunction
endclass

Running the test prints a driving line for each item. When the bit-flip callback is attached, its message appears just before each drive and the data value shown has bit 0 inverted. A run of the same test with no callback attached would omit the injected lines and print the data exactly as randomized:

# Sample log (spi_env with bitflip callback attached)
UVM_INFO @ 0: reporter [RNTST] Running test err_inject_test...
UVM_INFO bitflip_cb.sv [BITFLIP_CB] injected single-bit error
UVM_INFO spi_driver.sv [SPI_DRV] driving addr=1a data=c5
UVM_INFO bitflip_cb.sv [BITFLIP_CB] injected single-bit error
UVM_INFO spi_driver.sv [SPI_DRV] driving addr=2f data=90
UVM_INFO bitflip_cb.sv [BITFLIP_CB] injected single-bit error
UVM_INFO spi_driver.sv [SPI_DRV] driving addr=07 data=3b
# Note: each data value has bit 0 flipped versus what the sequence randomized.
# With no callback attached, the BITFLIP_CB lines disappear and data matches the sequence.

Because the hook lives inside the driver and the behaviour lives in a separate callback class, the same driver runs clean in every other test. Only err_inject_test sees the flipped bit. To turn it off again mid-test you can call uvm_callbacks#(spi_driver, spi_driver_cb)::delete(env.agt.drv, cb).

Expected output, in plain words

When you run err_inject_test, the driver prints a “driving” line for each item. On every item the bit-flip callback also prints its “injected single-bit error” line just before, and the data value shown in the driver log has bit 0 inverted compared with what the sequence randomized. Run a plain test with no callback attached and the “injected” lines disappear while the data matches the sequence exactly. Since I do not run a simulator here, treat this as the behaviour the code is written to produce; run it in your own tool or on EDA Playground to confirm the exact logs.

Common mistakes to avoid

  • Forgetting `uvm_register_cb. Without it, `uvm_do_callbacks has nothing registered and your hook silently never fires.
  • Non-virtual hook methods. If the hook in the base callback is not virtual, your derived override is ignored.
  • Adding the callback too late. Attach it in build_phase or end_of_elaboration_phase so it is in place before run_phase starts using it.
  • Attaching to the wrong instance. add takes a specific component handle; pass the exact driver you mean, or use the type-wide add form if you truly want every instance.
  • Doing heavy work in a hook. Keep callbacks light. Large behaviour changes usually belong in a factory override, not a callback.

Keep learning

Callbacks are one of two reuse tools in UVM. See how they compare with the other in Callbacks vs Factory, and read more on the concept of the UVM factory. To see where the driver and monitor sit, review the UVM testbench architecture. For interview prep, browse our interview questions and the full UVM guides.

Frequently asked questions

What is a callback in UVM?

A callback is a mechanism that lets you run your own code at a fixed point inside a component, such as a driver or monitor, without editing that component. You define a hook method in a class based on uvm_callback, register it in the component, and attach your version from a test.

When should I use a callback instead of the factory?

Use a callback for small, targeted tweaks like error injection or extra coverage, especially on a locked or third-party component. Use the factory when you need to replace a component with a different version that changes behaviour more broadly.

Why is my callback method never called?

The two most common causes are a missing `uvm_register_cb in the component, or a hook method that is not declared virtual so your override is skipped. Also check that you attached the callback before run_phase started.

Can I attach a callback to only one component instance?

Yes. uvm_callbacks#(T,CB)::add takes a specific component handle, so you can target a single driver or monitor. There is also a type-wide form that applies to every instance of that component type.

How do I remove a callback during a test?

Call uvm_callbacks#(T,CB)::delete with the same component handle and callback object you used to add it. After deletion the hook runs its default empty behaviour again.

Do callbacks work on monitors as well as drivers?

Yes. Any component can register a callback type and place hook points. Monitors often use callbacks to pass observed transactions to a scoreboard or to sample coverage per test.

Similar Posts