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);

    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);

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.


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;
class my_packet#(parameter my_config_t cfg = cfg_a) extends uvm_sequence_item;
  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 = "");;

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;
  virtual my_if#(cfg) vif;
  my_driver#(cfg) driver;
  virtual function void build_phase(uvm_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);

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);
  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");

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 repo.

Read, post and discuss to keep the community alive!


Leave a Comment:

Your comment will be visible after approval.

(will not be published)