A value parameter lets one module definition generate different bit widths or depths for different instances.
SystemVerilog extends the same idea one step further: a parameter type lets one module, interface, or program definition generate different data types for different instances, instead of rewriting the whole block for every data type you need to support.
7 min read | Intermediate Level
What You'll Learn
- How to declare a
parameter typeon a module, and use it to type a port or an internal signal - How a value parameter can itself depend on a type parameter declared earlier in the same parameter list
- How to override a type parameter at instantiation, and what happens if that override breaks a dependent value parameter
- That the same mechanism works on interfaces, not just modules
Declaring a Type Parameter
A type parameter is declared with parameter type instead of a plain parameter. Its default is a data type rather than a value:
module generic_reg #(
parameter type T = logic [7:0],
parameter T RESET_VAL = '0
) (
input logic clk,
input logic rst_n,
input logic en,
input T d,
output T q
);
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) q <= RESET_VAL;
else if (en) q <= d;
endmodule
The ports d and q are both declared with type T. By default, that makes them an 8-bit logic vector, since that is T's default. RESET_VAL is an ordinary value parameter, but its own type is T instead of a fixed type like int. That is legal specifically because T was already declared earlier in the same parameter list. A later parameter can depend on the type of an earlier one.
Overriding a Type Parameter at Instantiation
A type parameter is overridden the same way a value parameter is, using #(.name(...)) at the instance - except the thing inside the parentheses is a type instead of a value:
module top;
logic clk, rst_n, en;
// Uses the default: T = logic [7:0], RESET_VAL = '0
generic_reg reg8 (.clk, .rst_n, .en, .d(8'h00), .q());
// Overrides T to a 16-bit vector; RESET_VAL now defaults to a 16-bit '0 automatically
generic_reg #(.T(logic [15:0])) reg16 (.clk, .rst_n, .en, .d(16'hBEEF), .q());
endmodule
reg16 doesn't need to override RESET_VAL at all. Because RESET_VAL's type tracks T, widening T to 16 bits automatically widens RESET_VAL's default to a 16-bit '0 as well. Any data type can be supplied this way, including a user-defined typedefed struct or enum - not just a built-in vector type.
Quick Check
Q: If generic_reg were instantiated as generic_reg #(.T(logic [15:0]), .RESET_VAL(16'hFFFF)) reg16 (...), would that compile?
Show Answer
Yes. Overriding T and explicitly overriding RESET_VAL at the same time is legal - it just means RESET_VAL no longer uses the type-dependent default, since it was given an explicit 16-bit value of its own instead.
Type Parameters Also Work on Interfaces
The same declaration works on an interface, letting one interface definition carry whatever payload type the design around it needs:
interface generic_if #(parameter type T = logic [31:0]);
T data;
logic valid;
logic ready;
endinterface
Connecting this to an 8-bit design and a 64-bit design just means instantiating generic_if twice, once with T left at its default and once overridden to logic [63:0] - no second copy of the interface source needs to exist.
The value-parameter side of interface parameterization - sizing a bus width that stays a plain vector - is covered in full in SystemVerilog Interface Parameters. This same parameter type mechanism is also what parameterized classes use, covered in SystemVerilog Parameterized Classes.
Restrictions Worth Knowing
- A type parameter can only ever be set to a data type, never a plain value, and the reverse is also true.
- Mixing up which kind of parameter a given name is produces a compile error.
- A type parameter cannot be overridden with a
defparamstatement. It can only be overridden at the point of instantiation. - A type parameter's override expression cannot be a hierarchical name. A plain type name, a
typedefed name, or a package-scoped type reference are all legal; a path through the design hierarchy is not.
Common Beginner Mistakes
Mistake #1: Overriding a type parameter without checking what depends on it
In the generic_reg example, RESET_VAL's default depends on T. Overriding T to an incompatible type - a class handle, for instance, where RESET_VAL's default of '0 no longer makes sense - causes elaboration to fail on the dependent parameter, not on the type override itself. The error points at the symptom, not the cause, which makes this easy to misdiagnose the first time it happens.
Mistake #2: Expecting a type parameter to flow into the connected module automatically
Just like a value parameter, overriding a type parameter on one instance does not change any other instance, and does not change a separate module that happens to be connected to this one. Each parameterized module, interface, or program is still an independent entity - connecting them doesn't share parameters between them unless each side is explicitly given a matching override.
Practice Exercise
Challenge: A Generic Comparator
Write a module generic_eq with a type parameter T defaulting to logic [7:0], two input ports a and b of type T, and one output port match that is high whenever a == b. Then instantiate it once with the default type, and once overridden to compare two 32-bit values.
Hint
match doesn't need to be type T itself - it's always a single bit, regardless of how wide a and b are.
Solution
module generic_eq #(
parameter type T = logic [7:0]
) (
input T a,
input T b,
output logic match
);
assign match = (a == b);
endmodule
module top;
logic [7:0] a8, b8;
logic [31:0] a32, b32;
logic m8, m32;
generic_eq cmp8 (.a(a8), .b(b8), .match(m8));
generic_eq #(.T(logic [31:0])) cmp32 (.a(a32), .b(b32), .match(m32));
endmodule
match stays a plain 1-bit logic in both instances. Only a and b change width, since only they are declared with type T.
Summary
In this tutorial, you learned:
parameter typedeclares a parameter whose default and override are both data types, usable directly as the type of a port or an internal signal- A value parameter declared later in the same list can depend on a type parameter declared earlier, and its default is recomputed if that type parameter is overridden
- Type parameters are overridden the same way value parameters are, with
#(.name(...))at the instance - Type parameters work the same way on modules, interfaces, and programs
- A type parameter can't be set with
defparam, and its override can't be a hierarchical name