How to Use UVM Callbacks With Configuration Fields
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.
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:
- Use the `uvm_do_callbacks macro to call the appropriate function from our base callback class
- 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