How to Avoid Parameter Creep for Parameterizable Agents and Interfaces

For configurable protocols, it is useful to have a single agent which can adapt to any protocol configuration. If the agent and the interface are parameterized, having a large number of configuration options will require using many parameters. This can quickly lead to parameter creep: explicitly specifying and propagating all the parameters throughout the environment.

In this post I’ll show how to avoid parameter creep when writing parameterizable agents and interfaces. As an example, let’s consider a hypothetical protocol which is used to send a number of data items to a certain address. Here it is a short excerpt from the agent, which has three parameters:

class my_agent#(parameter addr_width = 1, parameter data_width = 1, parameter payload_length = 1) extends uvm_agent;
  `uvm_component_param_utils(my_agent#(addr_width, data_width, payload_length))

  virtual my_interface#(addr_width, data_width, payload_length) vif;

  my_driver#(addr_width, data_width, payload_length) driver;
  ................................

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

    if (!uvm_config_db#(virtual my_interface#(addr_width, data_width, payload_length))::get(this, "", "if", vif))
      `uvm_fatal(get_name(), "Error retrieving the virtual interface handle!")

    driver = my_driver#(addr_width, data_width, payload_length)::type_id::create("driver", this);
    ................................
  endfunction
endclass

Try to imagine how the code will look like if the number of parameters would increase to 10 or 15! The bigger the number of parameters, the harder it will be to read and maintain the code.

Solution

To avoid the problem, a single packed structure can be used as a parameter instead of a large number of different parameters. A possible implementation of the structure is the following:

typedef struct packed {
  byte unsigned addr_width;
  byte unsigned data_width;
} layer1_t;
 
typedef struct packed {
  int unsigned payload_length;
} layer2_t;
 
typedef struct packed {
   layer1_t layer1;
   layer2_t layer2;
} my_config_t;

We can now create a few configurations, which can be used for parameterizing the agents and the interfaces:

parameter my_config_t cfg_a = '{ '{ addr_width: 4, data_width:  8 }, '{ payload_length: 2 } };
parameter my_config_t cfg_b = '{ '{ addr_width: 8, data_width: 16 }, '{ payload_length: 4 } };

The interface and the sequence item can then be implemented. Note that a default value for the configuration must be supplied for the interface and for any parameterized class definition that is related to the agent.

interface my_if#(parameter my_config_t cfg = cfg_a) (input logic clk);
  logic                             valid;
  logic [cfg.layer1.addr_width-1:0] addr;
  logic [cfg.layer1.data_width-1:0] data;
endinterface
 
class my_packet#(parameter my_config_t cfg = cfg_a) extends uvm_sequence_item;
  `uvm_object_param_utils(my_packet#(cfg))
 
  rand bit [cfg.layer1.addr_width-1:0] addr;
  rand bit [cfg.layer1.data_width-1:0] payload[cfg.layer2.payload_length];
 
  function new(string name = "");
    super.new(name);
  endfunction
endclass

The code for the agent is much cleaner. If more configuration options are needed, the structure has to be updated, but no changes will be required in the agent.

class my_agent#(parameter my_config_t cfg = cfg_a) extends uvm_agent;
  `uvm_component_param_utils(my_agent#(cfg))
 
  virtual my_if#(cfg) vif;
 
  my_driver#(cfg) driver;
  ................................
 
  virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
 
    if (!uvm_config_db#(virtual my_if#(cfg))::get(this, "", "if", vif))
      `uvm_fatal(get_name(), "Error retrieving the virtual interface handle!")
 
    driver = my_driver#(cfg)::type_id::create("driver", this);
    ................................
  endfunction
endclass

Using the agent is straightforward. First, it needs to be created:

my_agent#(cfg_b) agent_b;
................................
agent_b = my_agent#(cfg_b)::type_id::create("agent_b", this);

Then, the interface must be instantiated and set in the UVM config db:

my_if#(cfg_b) if_b(clk);
initial
  uvm_config_db#(virtual my_if#(cfg_b))::set(null, "*agent_b*", "if", if_b);

Finally, a sequence can be started on the agent’s sequencer:

my_sequence#(if_cfg_b) seq_b = my_sequence#(if_cfg_b)::type_id::create("seq_b");
seq_b.start(agent_b.sequencer);

Comparison with Other Methods

Here it is a comparison between the classic way (using multiple parameters), the method I propose in this article and the accessor-class based solution.

Feature name Multiple parameters method Proposed method Accessor class method
1. Simulation speed impact None None Requires config_db accesses
2. Impact of adding/removing parameters Very High Low Medium
3. Support for arbitrary sized data Yes Yes No
4. Effort to add support for protocol layering High Low Medium
5. Debugging effort Hard Easy Medium
6. Requires proxy-entities No No Yes

As you can see, there are a few advantages when writing the agent and the interface using the proposed method:

1. Any of the agent’s parameterized classes has direct access to all configuration options, eliminating the need of accessing the UVM config db, which may affect the simulation speed. In contrast, the accessor class method needs a configuration object which is retrieved from the UVM config db.
2. Adding or removing configuration options will only require updating the structure and the actual piece of code which uses these configuration options. The accessor class method also requires updates to the accessor class. For the multiple parameters method, updates are needed throughout the environment, wherever the parameters are used.
3. No maximum size is required for the bus, allowing future updates. In comparison, the accessor class method has a maximum bus width defined in the accessor class which will need to be changed.
4. Protocol layering can be supported by nesting structures, allowing a clear separation between the physical bus properties and the packets’ structure. This is hard to do for the multiple parameters method, where the layer separation can’t be easily determined.
5. Errors are easy to spot: trying to drive a packet having the wrong configuration will result in a compilation error. When using the accessor class method, these checks are only performed at runtime. For the multiple parameters method a compilation error will also be triggered, but the large number of parameters can make debugging a hopeless task.
6. No proxy entities (such as accessor classes) are needed, reducing the necessary amount of code.

The Complete Code Example

If you want to run a simulation, you can download a complete working example from article’s GitHub repository.

Read, post and discuss to keep the community alive!

Comments

7 Responses

  1. Great! I have been researching all the possible methods and yours seem to be the best. You should also try contacting Dave Rich about his inputs on this method (he published more elaborate ways to do the same). You can also publish this as a small paper in DVCON.

  2. Hi Horia Razvan,
    Thanks This was helpful.

    I would also like to know how to set multiple instances of the interface to each agent.
    For example arb_req_agent_0 has req
    So in the top module I have

    genvar i;
    generate
    for(i=0;i<= `NUM_INST i++) begin: req_inst
    arb_req_if req_if();
    initial begin
    uvm_config_db#(virtual arb_req_if)::set(uvm_root::get(),*,$sformatf(req_if_%0d,i),req_if);
    end
    end
    endgenerate
    1.Now I want that each agent has access to its particular interface. For example arb_req_agent_0 has access to req_if_0 and so on. How do I modify my path in the * portion of the config db to achieve this? Instead of * can i write $sformatf("*.arb_req_agent_%0d",i) to set the interface for the proper agent.

    Note that in the env I have the agents created arb_req_agent_%0d

    2. How do I pass each interface to each agent created

    1. Hi Roopa,

      Please check again the code example on the GitHub repository, which is now updated with an array of agents and their corresponding interfaces.

      The changes required for an array of agents are minimal:

      //------------------------------------------------------------------------------
      // Test
      //------------------------------------------------------------------------------
      
      class my_test extends uvm_test;
        my_agent#(cfg_c) agent_cs[4];
      
        virtual function void build_phase(uvm_phase phase);
          ................................
          foreach (agent_cs[i])
            agent_cs[i] = my_agent#(cfg_c)::type_id::create($sformatf("agent_c_%0d", i), this);
        endfunction
      endclass
      
      //------------------------------------------------------------------------------
      // Top
      //------------------------------------------------------------------------------
      
      module my_tb;
        my_if#(cfg_c) if_cs[4](clk);
      
        initial begin
          static virtual my_if#(cfg_c) vif_cs[4] = if_cs;
          foreach (vif_cs[i])
            uvm_config_db#(virtual my_if#(cfg_c))::set(null, $sformatf("*agent_c_%0d*", i), "if", vif_cs[i]);
          ................................
        end
      endmodule
      
  3. Hi Horia-Răzvan Enescu,

    I used similar method in my TB but i’m dealing with a case where I’m having to maintain an array of agents with different parameterizations. This method doesn’t work in that case. I have some ideas of my own but wondering your take on this problem.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Subscribe to our newsletter

Do you want to be up to date with our latest articles?