How to Use UVM Callbacks With Configuration Fields

How to Use UVM Callbacks With Configuration Fields

Posted by

In computer programming, a callback is basically a function which you can pass to some existing code to be called at some specific event.

UVM offers all the pieces of the puzzle to easily implement callbacks in any verification environment in just a few simple steps. In this post I will show you how I use UVM callbacks to trigger actions when environment configuration fields change.


Let’s consider the following scenario:

We have a highly flexible environment which can support, at any moment in time, only one interface for accessing the registers: I2C or APB. Switching between the two is possible at runtime and it depends solely on the number of bits used by the address bus. For an address width equal to 7 or 10 bits the I2C interface is used. For any other value of the address width the APB interface is used.

UVM callbacks trigger automatic reconfiguration
Actions triggered by changing the address width from environment configuration class

The target is to implement with UVM callbacks the following actions whenever the field address_width from the configuration environment class changes:

  • Reconfigure the I2C agent
  • Reconfigure the APB agent
  • Reconfigure the DUT model

 

Step #1: Create a base callback class

The first thing that we need to do is to define a basic callback class in which to specify what functions will be called back.

This is easily accomplished by defining the callback class as a child of uvm_callback:

1
2
3
4
5
6
7
8
9
10
11
virtual class acme_callback_addr_width extends uvm_callback;

   function new(string name = "");
      super.new(name);
   endfunction
   
   pure virtual function void addr_width_changed(
      int unsigned old_addr_width,
      int unsigned new_addr_width);
   
endclass

For our example, we need the callback class named acme_callback_addr to have a function named addr_change() which will be called every time the the field addr_width changes.

To make life easier, we should add as arguments of this function the old and the new value of the addr_width field – this will come in handy in just a little bit.

 

Step #2: Define a type for the UVM callbacks pool

As you will see in the next steps, we will interact in several places with a class called uvm_callbacks#(T, CB). This is the class through which we can add, or remove, callbacks – it is basically a pool of callbacks that we want to maintain in our environment.

Because it is a parameterized class, the name ends up to be quite long. In order to avoid this “ugly” code, we can hide it behind a typedef:

1
typedef uvm_callbacks#(acme_env_config, acme_callback_addr_width) acme_addr_width_cb_pool;

Of course, you can do without this step, but then you will have to use that long parameterized class name.

 

Step #3: Define when the callbacks will be called

In our scenario, we need to trigger the callbacks when addr_width field of the environment configuration class changes – so there is no better place to do this than in the setter function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  class acme_env_config extends uvm_component;
   ...
   `uvm_register_cb(acme_env_config, acme_callback_addr_width)
   ...
   function void set_addr_width(int unsigned addr_width);

      `uvm_do_callbacks(
         acme_env_config,
         acme_callback_addr_width,
         addr_width_changed(this.addr_width, addr_width));
     
      this.addr_width = addr_width;
   endfunction
   ...
   endclass

There are two important aspects to pay attention to here:

  1. Use the `uvm_do_callbacks macro to call the appropriate function from our base callback class
  2. Use the `uvm_register_cb macro to register the callback class (acme_callback_addr_width) with the given object type (acme_env_config)

 

Step #4: Define custom callback functions

Callbacks are a great way to easily implement the design principle of separation of concerns. So for this reason, we will implement a different callback class for each of the three actions that we want to trigger when the address width from the environment configuration class changes.

 

Step #4.1. Reconfigure the I2C agent

Whenever the new address width is either 7 or 10 bits then we need to correctly configure the I2C agent.

For this we need in our callback a pointer to the I2C agent configuration object. Once we have access to this, the implementation is quite straight forward:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class acme_callback_addr_width_config_i2c extends acme_callback_addr_width;
 
  //pointer to the I2C agent configuration object
  i2c_agent_config agent_config;

  `uvm_object_utils(acme_callback_addr_width_config_i2c)
   
  function new(string name = "");
    super.new(name);
  endfunction
   
  virtual function void addr_width_changed(int unsigned old_addr_width, int unsigned new_addr_width);
    if(old_addr_width != new_addr_width) begin
      if(new_addr_width == 7) begin
        agent_config.set_addr_width(I2C_ADDR_7BIT);
      end
      else if(new_addr_width == 10) begin
        agent_config.set_addr_width(I2C_ADDR_10BIT);
      end
    end
  endfunction
   
endclass

 

Step #4.2. Reconfigure the APB agent

The logic for reconfiguring the APB agent is even simpler than the one for I2C. This is because the addr_width of the APB agent is of type int unsigned, not an enumerated type as for I2C agent:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class acme_callback_addr_width_config_apb extends acme_callback_addr_width;
 
  //pointer to the APB agent configuration object
  apb_agent_config agent_config;

  `uvm_object_utils(acme_callback_addr_width_config_apb)
   
  function new(string name = "");
    super.new(name);
  endfunction
   
  virtual function void addr_width_changed(int unsigned old_addr_width, int unsigned new_addr_width);
    if(old_addr_width != new_addr_width) begin
      if(!(new_addr_width inside {7, 10})) begin
        agent_config.set_addr_width(new_addr_width);
      end
    end
  endfunction
   
endclass

 

Step #4.3. Reconfigure the DUT model

The job of the last callback that we need to define is to reconfigure the DUT model. This might mean many things, based on the complexity.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class acme_callback_addr_width_config_model extends acme_callback_addr_width;
 
  //pointer to the DUT model
  acme_model model;

  `uvm_object_utils(acme_callback_addr_width_config_model)
   
  function new(string name = "");
    super.new(name);
  endfunction
   
  virtual function void addr_width_changed(int unsigned old_addr_width, int unsigned new_addr_width);
    if((old_addr_width inside {7, 10}) && !(new_addr_width inside {7, 10})) begin
      model.switch_from_i2c_to_apb();
    end
    else if(!(old_addr_width inside {7, 10}) && (new_addr_width inside {7, 10})) begin
      model.switch_from_apb_to_i2c();
    end
  endfunction
   
endclass

 

Step #5: Hook up the callbacks

The final step is to hook up all the callbacks that we declared. This is actually the easiest step: we create an object for each of the three callbacks, connect the pointers and add them to the callbacks pool:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class acme_env extends uvm_component;
  ...
  virtual function void connect_phase(uvm_phase phase);
    super.connect_phase(phase);
   
    //hook up the callback for I2C reconfiguration
    begin
      acme_callback_addr_width_config_i2c callback = acme_callback_addr_width_config_i2c::type_id::create("callback_i2c");
      callback.agent_config = i2c_agt.agent_config;
       
      acme_addr_width_cb_pool::add(env_config, callback);
    end
     
    //hook up the callback for APB reconfiguration
    begin
      acme_callback_addr_width_config_apb callback = acme_callback_addr_width_config_apb::type_id::create("callback_apb");
      callback.agent_config = apb_agt.agent_config;
       
      acme_addr_width_cb_pool::add(env_config, callback);
    end
     
    //hook up the callback for DUT model reconfiguration
    begin
      acme_callback_addr_width_config_model callback = acme_callback_addr_width_config_model::type_id::create("callback_model");
      callback.model = model;
       
      acme_addr_width_cb_pool::add(env_config, callback);
    end
  endfunction

endclass

That’s it: we’re all done!

You can also try to run this code on EDA Playground.


 

Extra

UVM already comes with everything in place (from step #1 up to step #3) for hooking up callbacks which are triggered when a register field gets updated. You can read more about this in How to Use Register Callbacks in uvm_reg Library

Cristian Slav

Leave a Reply

Your email address will not be published.