Your stream agent is a hit, and three projects want it. One has a 64-bit stream with TKEEP. One has 32 bits like the DMA but needs TUSER. The third wants the same agent on eight ports with different timeouts on each. You could fork the agent three times; within a year the forks will have three different bug lists. Or you could make it configurable, and then the question is how: which differences become SystemVerilog parameters, which become fields in a configuration object, and which should not be options at all?
Some differences change the shape of the code: how many bits a field has, how many wires the interface has, what type a transaction is. SystemVerilog fixes those at elaboration, so they are parameters. Other differences change behaviour: whether the agent drives, how long it waits before calling a timeout, whether coverage is on. Those can change per test or per instance without recompiling, so they are configuration knobs, fields of a config object read in build_phase.
A car factory is a fair picture. The chassis length and the engine mount are decided when the production line is set up; changing them means retooling (recompiling). The seat position, the radio station and the drive mode are chosen by each driver every morning (runtime config). Putting a seat position in the tooling, or the chassis length in a menu, makes both jobs harder.
A parameterized agent carries the width through every class that touches data: item, driver, monitor, sequencer, agent. Each is registered with the _param_utils macros, because the plain macros register a type name, and a parameterized class has no single name.
// snippet: the stream agent parameterized by data width
class axis_item_p #(int DW = 32) extends uvm_sequence_item;
`uvm_object_param_utils(axis_item_p #(DW))
rand bit [DW-1:0] data;
rand bit [DW/8-1:0] keep; // one TKEEP bit per byte
rand bit last;
constraint c_keep { soft keep == '1; } // full beats unless a test asks otherwise
function new(string name = "axis_item_p");
super.new(name);
endfunction
endclass
class axis_agent_p #(int DW = 32) extends uvm_agent;
`uvm_component_param_utils(axis_agent_p #(DW))
axis_monitor_p #(DW) mon;
axis_driver_p #(DW) drv;
uvm_sequencer #(axis_item_p #(DW)) sqr;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
virtual function string get_type_name(); // the param macros do not define it
return $sformatf("axis_agent_p#(%0d)", DW);
endfunction
// build_phase and connect_phase as in axis_agent, with the #(DW) types
endclass
// In the VIP package: name the specializations projects actually use.
typedef axis_agent_p #(32) axis32_agent;
typedef axis_agent_p #(64) axis64_agent;The _param_utils macros register the class by type only. Three consequences follow, and each one surprises someone on every project:
Operation | Plain class | Parameterized class |
|---|---|---|
| Works | Works, with the full specialization: |
Override by type name (string), including | Works | Silently does nothing: no component is ever requested by that name |
Name in | The class name | The nearest base class's name ( |
Override across specializations | n/a | Impossible: |
The same "every specialization is a different type" rule applies to the virtual interface: a config_db set of virtual axis_if #(64) is invisible to a get of virtual axis_if #(32). That trap, and the abstract-BFM pattern that removes it, are covered in Interface abstraction.
Many commercial VIPs avoid class parameters altogether. Every data field is sized to a compile-time maximum (often a compile-time define such as AXIS_MAX_DW), and the active width is a config field. One compile then serves every port, overrides by name work, and the topology prints real names. The price is that width correctness moves from the compiler to your code: the driver must zero unused bits, the monitor must mask them, and validate() must reject a width above the maximum. Both strategies are legitimate. Pick one per VIP, document it, and do not mix them inside one agent.
Runtime options live in one config object per agent, which the env or test creates, fills and sets before the agent is built. The object is the VIP's public API, so each field gets a default, a documented range and a check. Checking in build_phase makes a bad value fail at time 0 with a message that names the field, instead of failing an hour later as a mysterious hang.
// snippet: a stream agent config with validation
class axis_cfg extends uvm_object;
`uvm_object_utils(axis_cfg)
uvm_active_passive_enum is_active = UVM_ACTIVE;
int unsigned data_width = 32; // runtime width, max-width strategy only
int unsigned ready_timeout = 1000; // clocks before a stall is an error
bit coverage_on = 1;
bit checks_on = 1;
function new(string name = "axis_cfg");
super.new(name);
endfunction
virtual function void validate(uvm_component who);
if (!(data_width inside {8, 16, 32, 64}))
`uvm_fatal_context("CFG", $sformatf("data_width=%0d: must be 8, 16, 32 or 64", data_width), who)
if (ready_timeout < 16)
`uvm_fatal_context("CFG", $sformatf("ready_timeout=%0d: minimum is 16", ready_timeout), who)
endfunction
endclassTier the fields. Stable fields (mode, timeouts, check and coverage switches) are promised across releases. Advanced fields carry a warning in the user guide. Internal hooks stay out of the config object entirely, because whatever is in it, someone will set. And whatever range validate() enforces is the range the user guide states, in the same words.
A scratch run builds two parameterized components side by side: p_comp#(8) registered with the macro alone, and p_comp_named#(16) with a get_type_name() override like the one in the listing. It was started with +uvm_set_type_override=p_comp,p_comp_named. The topology it printed:
Name Type Size Value
----------------------------------------------
<unnamed> uvm_root - @75
uvm_test_top param_topo_test - @189
pa uvm_component - @259
pb p_comp_named#(16) - @267pa is listed as uvm_component: with no get_type_name() of its own, the class reports its nearest base class's name. A parameterized agent would show up as uvm_agent, which looks plausible and is wrong, so nobody questions it.
pb shows its real specialization, thanks to the override.
The command-line override by name changed nothing and printed no message at all: pa was built as p_comp#(8). A by-name override for a class that is only registered by type is silently ignored.
A project wanted a longer ready timeout on one slow port, and its test did uvm_config_db#(int)::set(this, "env.slow_agt*", "ready_timeout", 5000);. The VIP, at that time, read each knob as its own config_db entry with uvm_config_db#(int unsigned)::get(...). The types did not match, so the get found nothing and the agent kept its default of 1000. The test failed with a timeout error on exactly the port it had tried to fix, and two engineers spent an afternoon proving the set was at the right path.
The fix was structural: the VIP moved every knob into axis_cfg, set as one object. A misspelt or mistyped field in a config object is a compile error; a mistyped config_db key is silence. Separate config_db entries per knob are still common in older VIP, which is exactly why +UVM_CONFIG_DB_TRACE exists and why it is the first switch to try when a setting seems ignored.
+uvm_set_type_override=axis_agent_p,my_axis_agent on the command line. What happens?Nothing, and silently: a parameterized class registered with uvm_component_param_utils has no name in the factory, and components are requested by type, so no request ever matches the name axis_agent_p. The original type is built with no warning. Use a type-based override in code with the full specialization instead.
A config field. It changes behaviour, not types or pin counts, and it differs per instance, which a parameter could only do with a separate specialization per value. As a cfg field it gets a default, a range check in validate(), and one compile serves every instance.
Next: width and knobs are the same for every protocol. What differs is the protocol itself: Protocol agent patterns compares what APB, AXI and AXI-Stream agents must each get right.