What You'll Learn
- How module parameters are declared and overridden at instantiation
- How local parameters and specify parameters differ from module parameters
- How to build parameterized counters from the examples
- Common parameter mistakes, and how to avoid them
What are Verilog Parameters ?
Parameters are Verilog constructs that allow a module to be reused with a different specification. For example, a 4-bit adder can be parameterized to accept a value for the number of bits and new parameter values can be passed in during module instantiation. So, an N-bit adder can become a 4-bit, 8-bit or 16-bit adder. They are like arguments to a function that are passed in during a function call.
parameter MSB = 7; // MSB is a parameter with a constant value 7
parameter REAL = 4.5; // REAL holds a real number
parameter FIFO_DEPTH = 256,
MAX_WIDTH = 32; // Declares two parameters
parameter [7:0] f_const = 2'd3; // 2 bit value is converted to 8 bits; 8'd3
Parameters are basically constants and hence it's illegal to modify their value at runtime. It is illegal to redeclare a name that is already used by a net, variable or another parameter.
There are two major types of parameters, module and specify and both accepts a range specification. But, they are normally made as wide as the value to be stored requires them to be and hence a range specification is not necessary.
Module Parameters
Module parameters can be used to override parameter definitions within a module and this makes the module have a different set of parameters at compile time. A parameter can be modified with the defparam statement or in the module instance statement. It is a common practice to use uppercase letters in names for the parameter to make them instantly noticeable.
The module shown below uses parameters to specify the bus width, data width and the depth of FIFO within the design, and can be overriden with new values when the module is instantiated or by using defparam statements.
// Verilog 1995 style port declaration
module design_ip ( addr,
wdata,
write,
sel,
rdata);
parameter BUS_WIDTH = 32,
DATA_WIDTH = 64,
FIFO_DEPTH = 512;
input addr;
input wdata;
input write;
input sel;
output rdata;
wire [BUS_WIDTH-1:0] addr;
wire [DATA_WIDTH-1:0] wdata;
reg [DATA_WIDTH-1:0] rdata;
reg [7:0] fifo [0:FIFO_DEPTH-1];
// Design code goes here ...
endmodule
In the new ANSI style of Verilog port declaration, you may declare parameters as show below.
module design_ip
#(parameter BUS_WIDTH=32,
parameter DATA_WIDTH=64) (
input [BUS_WIDTH-1:0] addr,
// Other port declarations
);
Overriding Parameters
Parameters can be overridden with new values during module instantiation. The first part instantiates the module called design_ip by the name d0 where new parameters are passed in within #( ).
The second part uses a Verilog construct called defparam to set the new parameter values. The first method is the most commonly used way to pass new parameters in RTL designs. The second method is commonly used in testbench simulations to quickly update the design parameters without having to reinstantiate the module.
module tb;
// Module instantiation override
design_ip #(.BUS_WIDTH(64), .DATA_WIDTH(128)) d0 ( [port list]);
// Use of defparam to override
defparam d0.FIFO_DEPTH = 128;
endmodule
Where is This Used?
In a System-on-Chip (SoC), different peripherals might use different data widths (e.g., an 8-bit SPI controller vs. a 64-bit DDR4 memory interface). Instead of writing two separate modules, you write one with a parameter WIDTH = 32. Another example is an AXI Interconnect or a Data Mux. You can instantiate the same multiplexer code for an 8-bit bus in one part of the chip and a 128-bit bus in another. The main benefit is that it reduces code duplication and the risk of "copy-paste" bugs.
Parameter Example
The module counter has two parameters N and DOWN declared to have a default value of 2 and 0 respectively. N controls the number of bits in the output effectively controlling the width of the counter. By default it is a 2-bit counter. Parameter DOWN controls whether the counter should increment or decrement. By default, the counter will increment because the parameter is set to 0.
2-bit up-counter
module counter
#( parameter N = 2,
parameter DOWN = 0)
( input clk,
input rstn,
input en,
output reg [N-1:0] out);
always @ (posedge clk) begin
if (!rstn) begin
out <= 0;
end else begin
if (en)
if (DOWN)
out <= out - 1;
else
out <= out + 1;
else
out <= out;
end
end
endmodule
The module counter is instantiated with N as 2 even though it is not required because the default value is anyway 2. DOWN is not passed in during module instantiation and hence takes the default value of 0 making it an up-counter.
module design_top ( input clk,
input rstn,
input en,
output [1:0] out);
counter #(.N(2)) u0 ( .clk(clk),
.rstn(rstn),
.en(en));
endmodule
See that default parameters are used to implement the counter where N equals two making it a 2-bit counter and DOWN equals zero making it an up-counter. The output from counter is left unconnected at the top level.

4-bit down-counter
In this case, the module counter is instantiated with N as 4 making it a 4-bit counter. DOWN is passed a value of 1 during module instantiation and hence an down-counter is implemented.
module design_top ( input clk,
input rstn,
input en,
output [3:0] out);
counter #(.N(4), .DOWN(1))
u1 ( .clk(clk),
.rstn(rstn),
.en(en));
endmodule

Local Parameters
Local parameters are used to define constants within a module using the keyword localparam. Unlike regular parameters, localparams cannot be modified or overridden from outside the module using defparam. This feature ensures that the values assigned to localparams remain constant throughout the module's lifetime, providing a way to protect critical design values from accidental changes during instantiation.
Industry Application
In a complex Finite State Machine (FSM), such as a DDR4 Memory Controller or a PCIe Physical Layer, the specific bit patterns for states (IDLE, READ, WRITE, WAIT) are internal implementation details.
Why localparam? If an external user accidentally overrides your IDLE state from 3'b000 to 3'b111 during instantiation, the entire state machine logic will break. Using localparam "locks" these values inside the module.
module adder #(parameter WIDTH = 8) (
input wire [WIDTH-1:0] a,
input wire [WIDTH-1:0] b,
output wire [WIDTH-1:0] sum
);
// Define a local parameter that cannot be modified externally
localparam LENGTH = 4;
assign sum = a + b;
endmodule
module tb;
reg [15:0] a;
reg [15:0] b;
wire [15:0] sum;
// Instantiate the adder module
adder #(.WIDTH(16)) add_instance (
.a(a),
.b(b),
.sum(sum)
);
// The following line would cause an error because LENGTH is a localparam
// adder #(.WIDTH(16), .LENGTH(5)) add_instance2 ( ... ); // This will fail
endmodule
An attempt to override a localparam will throw an elaboration error.
adder #(.WIDTH(16), .LENGTH(5)) add_instance2 (
|
xmelab: *E,CUPASNA (./testbench.sv,28|30): Illegal override of local parameter 'LENGTH'.
However, local parameters can be assigned constant expressions containing parameters, which can be modified with defparam statements or module instance parameter value assignments.
module adder #(parameter WIDTH = 8, PARAM_LENGTH = 4) (
input wire [WIDTH-1:0] a,
input wire [WIDTH-1:0] b,
output wire [WIDTH-1:0] sum
);
localparam LENGTH = 2 + PARAM_LENGTH;
assign sum = a + b;
endmodule
module tb;
// Statements
// Regular parameter PARAM_LENGTH can be changed even though it is assigned to a localparam
adder #(.WIDTH(16), .PARAM_LENGTH(5)) add_instance2 ( ... );
endmodule
Bit-selects and part-selects of local parameters that are not of type real shall be allowed.
module adder #(parameter WIDTH = 8, PARAM_LENGTH = 4) (
input wire [WIDTH-1:0] a,
input wire [WIDTH-1:0] b,
output wire [WIDTH-1:0] sum
);
// Assign a part-select of original parameter
localparam LENGTH = 2 + PARAM_LENGTH[1:0];
assign sum = a + b;
initial begin
$display("Length = %0d", LENGTH);
end
endmodule
module tb;
// Statements
// Update parameter length to 6, and localparam should get 2 + 2'b10 = 4
adder #(.WIDTH(16), .PARAM_LENGTH(6)) add_instance2 ( ... );
endmodule
xcelium> run Length = 4 xmsim: *W,RNQUIE: Simulation is complete.
Specify parameters
These are primarily used for providing timing and delay values and are declared using the specparam keyword. It is allowed to be used both within the specify block and the main module body.
// Use of specify block
specify
specparam t_rise = 200, t_fall = 150;
specparam clk_to_q = 70, d_to_q = 100;
endspecify
// Within main module
module my_block ( ... );
specparam dhold = 2.0;
specparam ddly = 1.5;
parameter WIDTH = 32;
endmodule
Read more on Verilog Specify blocks !
Difference between specify and module parameters
While module parameters provide flexibility in configuring modules during instantiation, specify parameters focus on accurately modeling timing characteristics essential for reliable circuit operation.
| Specify parameter | Module parameter |
|---|---|
Declared by specparam | Declared by parameter |
Can be declared inside specify block or within main module | Can only be declared within the main module |
| May be assigned specparams and parameters | May not be assigned specparams |
| SDF can be used to override values | Instance declaration parameter values or defparam can be used to override |
Common Beginner Mistakes
Mistake #1: Confusing parameter with localparam
Beginners often use parameter for internal constants that should never be changed by an outside module. This exposes internal logic to accidental overrides. Use localparam for constants like state machine encodings or fixed mathematical values that must remain constant regardless of the module's instantiation.
Mistake #2: Positional Parameter Overrides
When instantiating a module, using positional overrides like module_name #(16, 8) inst_name is dangerous. If the original module's parameter order changes, your design will break or behave unpredictably. Always use named association: module_name #(.WIDTH(16), .DEPTH(8)) inst_name.
Mistake #3: Hard-coding Bit-Widths Instead of Using Expressions
A common error is defining a parameter WIDTH = 8 but then hard-coding a signal as reg [7:0] data. If the parameter is updated to 16, the signal remains 8 bits, causing truncation. Always derive related widths from the parameter: reg [WIDTH-1:0] data.
Mistake #4: Attempting to Change Parameters at Runtime
Newcomers often mistake parameters for variables (like reg or integer). Parameters are "elaboration-time" constants; they are set when the hardware is "built" and cannot be modified while the simulation or the chip is running. If you need a value to change during operation, use a register-based configuration bus instead.
Mistake #5: Circular or Complex Parameter Dependencies
Defining parameters that rely on complex calculations or other parameters across multiple files can lead to compilation errors if the order of inclusion is wrong. Ensure that all dependent parameters are defined in a clear, hierarchical order or within a single header file that is included with `include.
Best Practice: Use parameter for values you want to configure during instantiation (like bus widths) and localparam for values internal to the module's logic. This "encapsulation" makes your IP cores much more robust and easier for others to use without breaking them.
Practice Exercise
Challenge: A Parameterized Register
Write a register module preg with parameters WIDTH (default 8) and RESET_VAL (default 0), a clock, an active-low synchronous reset, a data input and an output. Then instantiate it as a 4-bit register that resets to 4'hA.
Hint
Size every port and signal from WIDTH, and override both parameters by name in the instance.
Solution
module preg #(parameter WIDTH = 8, parameter RESET_VAL = 0)
(input clk, rstn,
input [WIDTH-1:0] d,
output reg [WIDTH-1:0] q);
always @(posedge clk)
if (!rstn) q <= RESET_VAL; // Reset value comes from the parameter
else q <= d;
endmodule
// 4-bit instance that resets to 4'hA
preg #(.WIDTH(4), .RESET_VAL(4'hA)) u_p (.clk(clk), .rstn(rstn), .d(d4), .q(q4));
With named overrides, the order of the parameters does not matter and adding a new parameter later does not break the instance. Without any override, the same module is an 8-bit register that resets to 0.
Summary
In this tutorial, you learned:
- Parameters are constants fixed at compile time, which let one module be reused with different widths, depths or values
- A parameter is overridden per instance with
#( )or withdefparam; named overrides are safer than positional ones localparamvalues cannot be overridden from outside, but can be calculated from parametersspecparamholds timing values and can be used in specify blocks and in the module body- Derive widths from parameters, as in
[WIDTH-1:0], instead of hard-coding them