Parameterized UVM classes use SystemVerilog parameters, most often a type or a bit width, so one agent or environment can serve several DUT configurations. They need a different factory registration macro, `uvm_component_param_utils or `uvm_object_param_utils, and a few extra steps when the parameterized class is a test that run_test() has to create by name.
Parameters are not always the right tool. A value that can be known at run time, such as a count or a mode, is better held in a configuration object. Parameters are needed only for things that can't change after elaboration: the width of a vector, or the type of a data member. This article covers how to register such classes correctly and what changes in the factory when you do.
8 min read | Intermediate
What You'll Learn
- Register parameterized components and objects with the factory
- Explain why name-based factory calls don't work for them
- Make a parameterized test creatable by
run_test()and+UVM_TESTNAME - Keep long parameter lists consistent across many classes
Registering a Parameterized Class
The ordinary `uvm_component_utils macro registers a class under a fixed name. A parameterized class is really a family of classes, one for each combination of parameter values, and the name alone can't tell them apart. For these classes UVM provides separate macros:
class bus_driver #(int ADDR_W = 32, int DATA_W = 32) extends uvm_driver #(bus_item #(ADDR_W, DATA_W));
`uvm_component_param_utils(bus_driver #(ADDR_W, DATA_W))
// Rest of the code (constructor, build_phase, run_phase)
endclass
class bus_item #(int ADDR_W = 32, int DATA_W = 32) extends uvm_sequence_item;
`uvm_object_param_utils(bus_item #(ADDR_W, DATA_W))
rand bit [ADDR_W-1:0] addr;
rand bit [DATA_W-1:0] data;
// Rest of the code (constructor)
endclass
Pass the class name with its parameters to the macro, as shown. Creating objects works as usual, through the type that goes with that exact specialization:
typedef bus_driver #(16, 32) bus_driver_16_32; // a convenient short name
m_drv = bus_driver_16_32::type_id::create("m_drv", this);
Overrides are done on specializations too: bus_driver #(16, 32)::type_id::set_type_override(err_driver #(16, 32)::get_type()) replaces only that specialization.
What the Param Macros Don't Do
The parameterized macros leave out the string name that the normal macros store with the type, and they don't define get_type_name(). The consequence, stated in the UVM reference, is that name-based factory methods can't find parameterized classes. Name-based lookup is no longer recommended in any case: the type-based calls, type_id::create() and type_id::set_type_override(), are checked by the compiler, while a misspelt name only fails at run time.
This matters in three places:
+uvm_set_type_overrideand+uvm_set_inst_overrideon the command line can't name a parameterized class.get_type_name()returns the default, so log messages that use it say the same thing for every specialization. If you want the parameters in the log, useget_full_name()or add them to your messages yourself.run_test("name")and+UVM_TESTNAMElook the test up by name, so a parameterized test needs extra help, described next.
A Parameterized Test That run_test() Can Create
Sometimes the test itself is parameterized, for example a test of an ALU whose data width is a top-level parameter. The test must register with the name-based factory so that run_test() can find it. The param macro doesn't do that, so register by hand, giving each specialization a unique name through the registry's second parameter:
class alu_basic_test #(int DATA_W = 8) extends uvm_test;
// Same as `uvm_component_param_utils, but with a name for the name-based factory
typedef uvm_component_registry #(alu_basic_test #(DATA_W), "alu_basic_test_8") type_id;
static function type_id get_type();
return type_id::get();
endfunction
virtual function uvm_object_wrapper get_object_type();
return type_id::get();
endfunction
// Rest of the code (constructor, build_phase, run_phase)
endclass
The registry takes the type being registered and its name. The name must be unique for each value of the parameters, and it must be tied to the particular value used. The top module does that with a typedef that creates the specialization, so it exists when the factory is queried:
module tb;
parameter int DATA_W = 8;
alu #(DATA_W) dut (/* connections */);
typedef alu_basic_test #(DATA_W) test_t; // ties "alu_basic_test_8" to DATA_W = 8
initial begin
uvm_config_db #(virtual alu_if #(DATA_W))::set(null, "uvm_test_top", "vif", alu_vif);
run_test("alu_basic_test_8"); // or +UVM_TESTNAME=alu_basic_test_8
end
endmodule
The literal name belongs to the value the test is compiled with. If you compile with a different width, change the name too, so the name doesn't claim a width it doesn't have, and if two specializations of the test exist in one simulation, each needs its own name. Generating the name from the parameter value with a macro keeps the two in step.
Quick Check
Q: A test class uses `uvm_component_param_utils and the top module calls run_test("alu_basic_test"). What happens?
Show Answer
run_test() looks for the name in the name-based factory, where the class was never registered, so it reports that the test can't be found and the simulation ends at time zero. Register the test by hand with a name, as in the example above.
Parameterized Interfaces
An interface with a parameter is another type. virtual alu_if #(8) and virtual alu_if #(16) can't be assigned to each other, so the uvm_config_db type parameter must match on both sides, including the parameter value. A driver that expects virtual alu_if #(DATA_W) gets it only if the set() used the same specialization.
Keeping Parameter Lists Consistent
When several classes share the same parameters, adding one more means editing every class, every typedef and every instantiation, and one miss gives a confusing error. A common way around this is a set of macros that hold the list in one place:
`define PARAMS_DECLARE #(int ADDR_W = 16, int DATA_W = 8)
`define PARAMS_MAP #(.ADDR_W(ADDR_W), .DATA_W(DATA_W))
class bus_agent `PARAMS_DECLARE extends uvm_agent;
bus_driver `PARAMS_MAP m_drv;
// ...
endclass
The macros keep the list in a single, well-defined place. A package of shared parameters, imported by the classes that need it, is a simpler alternative when the values really are the same everywhere.
Parameters or Configuration Objects?
| Value | Use |
|---|---|
| Bit-vector width, type of a data member | Parameter: only parameters can change these |
| Count, delay, mode, enable bit | Configuration object, set through uvm_config_db |
| A vector width that only varies a little | Declare the widest and ignore unused bits, with the real width in a configuration object |
The last row trades a little memory for much simpler code, since no class in the testbench has to be parameterized. It is worth considering before parameterizing a whole environment.
Common Mistakes
Mistake #1: Using the plain utility macro on a parameterized class
The UVM reference says parameterized classes must be registered with the param macros, to avoid errors and simulator differences. A plain `uvm_component_utils on a parameterized class is not supported.
Mistake #2: Passing a parameterized test's name that was never registered
run_test() and +UVM_TESTNAME need the name-based registration described above. Without it the test isn't found.
Mistake #3: Mismatched specializations in config_db
A set() with virtual alu_if #(8) is invisible to a get() for virtual alu_if #(16). The parameter values are part of the type.
Mistake #4: Using name-based overrides for parameterized classes
Command-line and string-based overrides don't work for them. Use the type-based calls in code.
Summary
In this article, you learned:
- Parameterized classes register with
`uvm_component_param_utilsand`uvm_object_param_utils, passing the class with its parameters - These macros leave out the string name, so name-based factory calls can't find them
- A parameterized test needs a hand-written registry with a unique name for
run_test(), plus a typedef in the top module - Interface parameters are part of the type, so config_db types must match exactly
- Use parameters only for widths and types, and a configuration object for everything else