This is a deepdive how UVM implements callbacks. It’s useful to notifiy user code with specific conditions deep inside a UVC.

Callback use-model Link to heading

Starting with the callback definition where it extends uvm_callback and define callback task.

class my_callback extends uvm_callback;
  
  virtual task my_callback_task(); endtask
endclass

Then instrument the code with calls to uvm_do_callbacks

class my_uvm_component;
  `uvm_register_cb(my_uvm_component,my_callback)
  
  
  task run_phase(uvm_phase phase);
      ...
      `uvm_do_callbacks(my_uvm_component,my_callback,my_callback_task());
       ...
  endtask
encclass

Finally use code can extends the callback defined above my_callback and implement the callback task my_callback_task

class some_random_user_code extends my_callback;
    
  task my_callback_task;
    `uvm_info("Yo sup from my_callback_task",UVM_LOW);
  endtask

endclass

and finally user callback is registered

  some_random_user_code cb;
  cb = some_random_user_code::type_id::create("cb1", this);

  uvm_callbacks#(my_uvm_component,my_callback)::add(<INST>,cb);

So, the 2 macros are used to implement the callbacks. Let’s dig deeper.

uvm_register_cb Link to heading

uvm_register_cb is defined in macros/uvm_callback_defines.svh. that expands to a call to m_register_pair

`define uvm_register_cb(T,CB) \
  static local bit m_register_cb_``CB = uvm_callbacks#(T,CB)::m_register_pair(`"T`",`"CB`");

m_register_pair links the cb to class type passed to tname. Note that it calls get() which returns the singleton of uvm_callbacks class as shown below.

  static function bit m_register_pair(string tname="", cbname="");
    this_type inst = get();

    m_typename = tname;
    super_type::m_typename = tname;
    m_typeid.typename = tname;

    m_cb_typename = cbname;
    m_cb_typeid.typename = cbname;

    inst.m_registered = 1; 

    return 1;
  endfunction

  static function this_type get();
    m_user_inst = this_user_type::get();
    m_super_inst = this_super_type::get();
    m_s_typeid = uvm_typeid#(ST)::get();
    if(m_d_inst == null) begin
      m_d_inst = new;
    end
    return m_d_inst;
  endfunction

uvm_do_callbacks Link to heading

As documentation shows, uvm_do_callbacks is called to execute the registered callbacks.

//-----------------------------------------------------------------------------
// MACRO -- NODOCS -- `uvm_do_callbacks
//
//| `uvm_do_callbacks(T,CB,METHOD)
//
// Calls the given ~METHOD~ of all callbacks of type ~CB~ registered with
// the calling object (i.e. ~this~ object), which is or is based on type ~T~.
//
// This macro executes all of the callbacks associated with the calling
// object (i.e. ~this~ object). The macro takes three arguments:
//
// - CB is the class type of the callback objects to execute. The class
//   type must have a function signature that matches the METHOD argument.
//
// - T is the type associated with the callback. Typically, an instance
//   of type T is passed as one the arguments in the ~METHOD~ call.
//
// - METHOD is the method call to invoke, with all required arguments as
//   if they were invoked directly.
//
// For example, given the following callback class definition:
//
//| virtual class mycb extends uvm_cb;
//|   pure function void my_function (mycomp comp, int addr, int data);
//| endclass
//
// A component would invoke the macro as
//
//| task mycomp::run_phase(uvm_phase phase); 
//|    int curr_addr, curr_data;
//|    ...
//|    `uvm_do_callbacks(mycb, mycomp, my_function(this, curr_addr, curr_data))
//|    ...
//| endtask
//-----------------------------------------------------------------------------


// @uvm-ieee 1800.2-2020 auto B.4.3
`define uvm_do_callbacks(T,CB,METHOD) \
  `uvm_do_obj_callbacks(T,CB,this,METHOD)

Looking deeper into uvm_do_obj_callbacks, which uses uvm_callback_iter iter to loop over registered callbacks and executes them with cb.METHOD.

// @uvm-ieee 1800.2-2020 auto B.4.4
`define uvm_do_obj_callbacks(T,CB,OBJ,METHOD) \
   begin \
     uvm_callback_iter#(T,CB) iter = new(OBJ); \
     CB cb = iter.first(); \
     while(cb != null) begin \
       `uvm_cb_trace_noobj(cb,$sformatf(`"Executing callback method 'METHOD' for callback %s (CB) from %s (T)`",cb.get_name(), OBJ.get_full_name())) \
       cb.METHOD; \
       cb = iter.next(); \
     end \
   end

uvm_callback_iter is just wrapper on top methods defined in uvm_callback such as get_first

// @uvm-ieee 1800.2-2020 auto D.1.1
class uvm_callback_iter#(type T = uvm_object, type CB = uvm_callback) extends uvm_void;

   local int m_i;
   local T   m_obj;
   local CB  m_cb;

   // Function -- NODOCS -- new
   //
   // Creates a new callback iterator object. It is required that the object
   // context be provided.

   // @uvm-ieee 1800.2-2020 auto D.1.2.1
   function new(T obj);
      m_obj = obj;
   endfunction

   // Function -- NODOCS -- first
   //
   // Returns the first valid (enabled) callback of the callback type (or
   // a derivative) that is in the queue of the context object. If the
   // queue is empty then ~null~ is returned.

   // @uvm-ieee 1800.2-2020 auto D.1.2.2
   function CB first();
      m_cb = uvm_callbacks#(T,CB)::get_first(m_i, m_obj);
      return m_cb;
   endfunction

get_first calls m_get_q and loops over it to return cb.

  // Function -- NODOCS -- get_first
  //
  // Returns the first enabled callback of type CB which resides in the queue for ~obj~.
  // If ~obj~ is ~null~ then the typewide queue for T is searched. ~itr~ is the iterator;
  // it will be updated with a value that can be supplied to <get_next> to get the next
  // callback object.
  //
  // If the queue is empty then ~null~ is returned. 
  //
  // The iterator class <uvm_callback_iter> may be used as an alternative, simplified,
  // iterator interface.

  // @uvm-ieee 1800.2-2020 auto 10.7.2.4.1
  static function CB get_first (ref int itr, input T obj);
    uvm_queue#(uvm_callback) q;
    CB cb;
    void'(get());
    m_get_q(q,obj);
    for(itr = 0; itr<q.size(); ++itr) begin
      
      if($cast(cb, q.get(itr)) && cb.callback_mode()) begin
         
        return cb;
      end

    end

    return null;
  endfunction

add Link to heading

This is probably the most important part of callback mechanism, but it is simple really. The callback is added to q based on order(UVM_APPEND).

  // Group -- NODOCS -- Add/delete interface

  // Function -- NODOCS -- add
  //
  // Registers the given callback object, ~cb~, with the given
  // ~obj~ handle. The ~obj~ handle can be ~null~, which allows 
  // registration of callbacks without an object context. If
  // ~ordering~ is UVM_APPEND (default), the callback will be executed
  // after previously added callbacks, else  the callback
  // will be executed ahead of previously added callbacks. The ~cb~
  // is the callback handle; it must be non-~null~, and if the callback
  // has already been added to the object instance then a warning is
  // issued. Note that the CB parameter is optional. For example, the 
  // following are equivalent:
  //
  //| uvm_callbacks#(my_comp)::add(comp_a, cb);
  //| uvm_callbacks#(my_comp, my_callback)::add(comp_a,cb);

  // @uvm-ieee 1800.2-2020 auto 10.7.2.3.1
  static function void add(T obj, uvm_callback cb, uvm_apprepend ordering=UVM_APPEND);
    uvm_queue#(uvm_callback) q;

    ...
    ...

      //check if already exists in the queue
      if(m_cb_find(q,cb) != -1) begin
        uvm_report_warning("CBPREG", { "Callback object ", cb.get_name(), " is already registered",
                           " with object ", obj.get_full_name() }, UVM_NONE);
      end
      else begin
        if(ordering == UVM_APPEND) begin
          
          q.push_back(cb);
        end

        else begin
          
          q.push_front(cb);
        end

      end
    end
  endfunction