Verilog · Chapter 6.1 · Constant Variables
parameter in Verilog — User-Overridable Compile-Time Constants
The parameter keyword is the entry point to parameterised RTL design, the discipline that turns a single source module into many hardware variants at instantiation time. Every reusable IP block, such as a FIFO, counter, register file, arbiter, decoder, or ALU, has parameters for width, depth, mode, and encoding. Without parameters, every variant would need its own source-code copy and the maintenance debt would compound until the project collapsed under its own weight. This lesson is the deep drill: every declaration form in the port list or in the body, every override syntax whether named or ordered, the hierarchical-passing pattern that propagates parameters through multiple module levels, typed parameters with explicit width and signedness, the difference between a parameter and the strictly internal localparam, and the deprecated defparam that every code review should reject.
Foundation22 min readVerilogparameterOverrideParameterisationIPReuse
Chapter 6 · Section 6.1 · Constant Variables
1. The Engineering Problem
A team has three counters in their design:
// Counter 1 — 8-bit byte counter for UART
module uart_byte_counter (
input wire clk, rst_n, en,
output reg [7:0] count_q
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) count_q <= 8'h00;
else if (en) count_q <= count_q + 8'h01;
end
endmodule
// Counter 2 — 32-bit cycle counter
module cycle_counter (
input wire clk, rst_n, en,
output reg [31:0] count_q
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) count_q <= 32'h00000000;
else if (en) count_q <= count_q + 32'h00000001;
end
endmodule
// Counter 3 — 12-bit ADC sample counter
module sample_counter (...); // same shape, 12-bitThree modules, three files, three sets of bugs to fix when the team finds an issue. The team's tech lead writes a parameterised version:
module up_counter #(
parameter WIDTH = 8 // user-overridable, default 8
)(
input wire clk, rst_n, en,
output reg [WIDTH-1:0] count_q
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) count_q <= {WIDTH{1'b0}};
else if (en) count_q <= count_q + 1'b1;
end
endmoduleOne module. The three counters become three instantiations:
up_counter #(.WIDTH(8)) u_uart_cnt (.clk(clk), .rst_n(rst_n), .en(uart_en), .count_q(uart_cnt));
up_counter #(.WIDTH(32)) u_cycle_cnt (.clk(clk), .rst_n(rst_n), .en(1'b1), .count_q(cycle_cnt));
up_counter #(.WIDTH(12)) u_adc_cnt (.clk(clk), .rst_n(rst_n), .en(adc_smp), .count_q(adc_cnt));Same source. Three different widths. Bug fixes apply once and propagate to all three. New features (e.g., adding a synchronous load) land in one place. The team's productivity multiplies.
This is the parameterisation pattern — and the parameter keyword is the language mechanism that makes it work. This page covers everything you need to know to write parameterised modules and instantiate them correctly.
2. Anatomy of a parameter Declaration
Verilog supports two syntactic forms for declaring parameters: port-list style (Verilog-2001, preferred) and in-body style (Verilog-1995, legacy).
2.1 Port-list style (preferred)
`default_nettype none
module fifo #(
parameter WIDTH = 8, // simple parameter, default 8
parameter DEPTH = 16, // simple parameter
parameter [3:0] MODE = 4'h0, // typed parameter (4-bit value)
parameter signed [31:0] OFFSET = -1 // typed signed parameter
)(
input wire clk,
input wire rst_n,
output reg [WIDTH-1:0] q
);
// ... use WIDTH, DEPTH, MODE, OFFSET anywhere ...
endmoduleThe parameters live between module name and the port list, inside #(...). Each parameter has:
- Name — the identifier (
WIDTH,DEPTH,MODE). - Default value — what to use if the parent doesn't override.
- Optional type — width and signedness specifier (
[3:0],signed [31:0]).
The convention: ALL_CAPS names for parameters, no exceptions.
2.2 In-body style (legacy)
`default_nettype none
module fifo (
input wire clk,
input wire rst_n,
output reg [WIDTH-1:0] q
);
parameter WIDTH = 8; // in-body parameter declaration
parameter DEPTH = 16;
// ... use WIDTH, DEPTH ...
endmoduleThe parameters appear after the port list, inside the module body. Functionally equivalent to the port-list style for override purposes — a parent module can override either form via the same #(.NAME(value)) syntax at instantiation.
The in-body form is the only option in pre-2001 Verilog codebases; modern code uses port-list style for module-public parameters. Mixing both forms in one module is legal but rarely useful — most modules use either all-port-list or all-in-body.
2.3 The default value
Every parameter should have a default value:
parameter WIDTH = 8; // default 8 — used if not overridden
parameter MODE = 0;
parameter [15:0] OFFSET = 16'h00; // typed, default 0Omitting the default is legal but discouraged — a parameter declared parameter WIDTH; requires every instantiation to override it, which defeats the purpose of "configurable with a sensible default." Some tools also produce warnings for parameters without defaults.
3. Mental Model
The mental-model has three practical consequences:
- Pick parameter names that match the design intent.
WIDTH,DEPTH,MODEare universal;DATA_WIDTH,ADDR_DEPTH,BURST_MODEare more specific. Match the granularity to the codebase convention. - Provide defaults that are useful. The default should produce a working instance — e.g., the smallest reasonable width, the most-common mode. If the default is "user must override," remove the default and let the instantiation fail at elaboration with a clear error.
- Document the parameter contract. The parameter is part of the module's public API; the comment on its declaration is part of the API contract. "Default 8-bit; supports 1, 2, 4, 8, 16, 32, 64-bit widths" is the right level of detail.
4. Parameter Override Syntax
The parent module overrides parameters at instantiation. Two syntactic forms exist.
4.1 Named override (preferred)
`default_nettype none
module top;
wire clk, rst_n;
wire [31:0] cycle_count;
wire [11:0] adc_count;
// Named override — clear, order-independent
up_counter #(.WIDTH(32)) u_cycle (
.clk (clk),
.rst_n (rst_n),
.en (1'b1),
.count_q (cycle_count)
);
up_counter #(.WIDTH(12)) u_adc (
.clk (clk),
.rst_n (rst_n),
.en (adc_sample_en),
.count_q (adc_count)
);
endmoduleThe #(.NAME(value)) syntax names each parameter explicitly. Three benefits:
- Order-independent. Adding a new parameter to
up_counterdoesn't break existing instantiations — the new parameter's default applies unless the instantiation also names it. - Self-documenting. The name at the call site explains what each override controls.
- Refactor-safe. Renaming a parameter in the module breaks compilation at every instantiation, making the migration visible.
Use the named form everywhere. It is the modern Verilog standard and required by most style guides.
4.2 Ordered override (legacy, fragile)
`default_nettype none
module top;
// Ordered override — positional, fragile
up_counter #(32) u_cycle (.clk(clk), .rst_n(rst_n), .en(1'b1), .count_q(cycle_count));
up_counter #(12) u_adc (.clk(clk), .rst_n(rst_n), .en(en), .count_q(adc_count));
endmoduleThe #(value1, value2, ...) syntax assigns values to parameters by position in the module's declaration order. Two problems:
- Fragile to parameter list changes. Adding a new parameter to
up_counterbetween existing ones shifts every subsequent override silently — every instantiation now sets the wrong parameters. - Opaque at the call site. Reading
up_counter #(32, 16, 1)requires consulting the module's declaration to know what each value means.
The ordered form is legal Verilog but forbidden in most working codebases. Use named overrides exclusively.
4.3 Multi-parameter override
// Named — adding a new parameter doesn't break this
fifo #(
.DATA_WIDTH (32),
.DEPTH (64),
.MODE (2)
) u_fifo (
.clk (clk),
.rst_n (rst_n),
.data_in (din),
.data_out(dout)
);
// Ordered — fragile
fifo #(32, 64, 2) u_fifo (...);Named overrides scale cleanly to many parameters. Ordered overrides become unreadable past 2-3 parameters and brittle when the module evolves.
5. Hierarchical Parameter Passing
A common pattern: parameters propagate through multiple levels of module hierarchy.
`default_nettype none
module top #(parameter DATA_WIDTH = 32, parameter DEPTH = 256) (
input wire clk, rst_n,
// ... ports ...
);
// Pass DATA_WIDTH and DEPTH to the fifo
fifo #(
.WIDTH (DATA_WIDTH),
.DEPTH (DEPTH)
) u_fifo (
.clk (clk),
.rst_n (rst_n)
// ... ports ...
);
endmodule
module fifo #(parameter WIDTH = 8, parameter DEPTH = 16) (
input wire clk, rst_n
// ... ports ...
);
// Forward WIDTH and DEPTH to the storage
fifo_storage #(
.W (WIDTH),
.D (DEPTH)
) u_storage (
.clk (clk),
.rst_n (rst_n)
// ... ports ...
);
endmodule
module fifo_storage #(parameter W = 8, parameter D = 16) (
input wire clk, rst_n
);
reg [W-1:0] mem [0:D-1];
// ... storage logic ...
endmoduleEach level declares its own parameters. The override at the top (DATA_WIDTH = 32) propagates through fifo to fifo_storage. The parameter names can differ at each level (DATA_WIDTH → WIDTH → W) — only the override syntax #(.NAME(expression)) connects them.
The convention: name the parameter to match the level. The top module's DATA_WIDTH is descriptive; the storage module's W is terse because it's an internal sub-module. Either approach is fine — pick the granularity that matches the codebase.
6. Typed Parameters
A parameter can have an explicit type — width and signedness — beyond the inferred type from its default value.
`default_nettype none
module example #(
parameter WIDTH = 8, // inferred from default (integer)
parameter [7:0] BYTE_VAL = 8'hAA, // 8-bit unsigned
parameter signed [15:0] OFFSET = -100, // 16-bit signed
parameter [31:0] DEFAULT_ADDR = 32'h0000_1000 // 32-bit value
)(
input wire clk
);
// ... use parameters ...
endmoduleWhen to use typed parameters:
- Bit-vector constants that should have a specific width regardless of the default value's bit width.
parameter [7:0] BYTE_VAL = 8'hAA;is exactly 8 bits;parameter BYTE_VAL = 8'hAA;(untyped) might be promoted to a wider integer in arithmetic. - Signed values.
parameter signed [15:0] OFFSET = -1;makes the sign-extension behaviour explicit. The untyped form might be ambiguous. - Override safety. A typed parameter rejects overrides that don't match the type —
BYTE_VAL #(.BYTE_VAL(1024))would warn because 1024 doesn't fit in 8 bits.
The untyped form (parameter WIDTH = 8;) is inferred from the default value's type — IEEE 1364-2005 treats it as a 32-bit signed integer for purposes of arithmetic. For most "configuration knob" parameters this works fine; for bit-pattern constants, use the typed form.
7. Visual Explanation
Three figures cover the parameterisation pattern.
7.1 Visual A — parameter declaration and override flow
The interaction between module declaration and module instantiation.
parameter declaration and override flow
data flow7.2 Visual B — named vs ordered override
The structural difference between the two override syntaxes.
7.3 Visual C — hierarchical parameter passing
The cascade pattern. A top-level parameter propagates through multiple module levels.
8. The Parameter-Driven Generate Block
A parameter can drive a generate block — producing a different hardware structure depending on the parameter's value.
`default_nettype none
module configurable_adder #(parameter MODE = 0) (
input wire [7:0] a,
input wire [7:0] b,
output wire [7:0] y
);
// MODE = 0 → ripple-carry adder
// MODE = 1 → carry-lookahead adder
// MODE = 2 → carry-skip adder
generate
if (MODE == 0) begin : g_ripple
ripple_carry_adder u_add (.a(a), .b(b), .y(y));
end
else if (MODE == 1) begin : g_cla
carry_lookahead_adder u_add (.a(a), .b(b), .y(y));
end
else if (MODE == 2) begin : g_skip
carry_skip_adder u_add (.a(a), .b(b), .y(y));
end
else begin : g_default
assign y = a + b; // fallback: synthesis tool's default
end
endgenerate
endmoduleThe generate block expands at elaboration time based on the parameter's value. Only one of the four code paths is included in the netlist — the others are stripped before synthesis. The synthesis tool sees a single adder structure determined by MODE.
This is the canonical pattern for mode-dependent hardware — the same module produces different gates depending on a parameter. Chapter 14.7 covers generate blocks in detail.
9. The Parameterised Register File
A canonical real-world parameter use: a register file with configurable size and read-port count.
`default_nettype none
module regfile #(
parameter DATA_WIDTH = 32,
parameter NUM_REGS = 32,
parameter NUM_READ_PORTS = 2
)(
input wire clk,
input wire rst_n,
input wire wr_en,
input wire [ADDR_W-1:0] wr_addr,
input wire [DATA_WIDTH-1:0] wr_data,
input wire [NUM_READ_PORTS-1:0][ADDR_W-1:0] rd_addr,
output reg [NUM_READ_PORTS-1:0][DATA_WIDTH-1:0] rd_data
);
localparam ADDR_W = $clog2(NUM_REGS);
reg [DATA_WIDTH-1:0] rf [0:NUM_REGS-1];
integer i;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (i = 0; i < NUM_REGS; i = i + 1)
rf[i] <= {DATA_WIDTH{1'b0}};
end
else if (wr_en) begin
rf[wr_addr] <= wr_data;
end
end
// Asynchronous read for each read port
genvar p;
generate
for (p = 0; p < NUM_READ_PORTS; p = p + 1) begin : g_rd_port
always @(*) rd_data[p] = rf[rd_addr[p]];
end
endgenerate
endmodule
// Instantiation examples:
// regfile #(.DATA_WIDTH(32), .NUM_REGS(32), .NUM_READ_PORTS(2)) u_riscv_rf (...);
// regfile #(.DATA_WIDTH(64), .NUM_REGS(16), .NUM_READ_PORTS(3)) u_dsp_rf (...);
// regfile #(.DATA_WIDTH(8), .NUM_REGS(256), .NUM_READ_PORTS(1)) u_misc_rf (...);Three parameters, all overridable: data width, register count, read-port count. The synthesis tool produces a different hardware block for each instantiation — different flop counts, different mux structures, different read-port replications. Same source.
10. The defparam Override (Forbidden)
Verilog-1995 introduced a third override mechanism — defparam — that allows changing a parameter's value from outside the instantiation:
`default_nettype none
module top;
sub u_sub (...);
// defparam override — DEPRECATED
defparam u_sub.WIDTH = 32;
endmodule
module sub;
parameter WIDTH = 8;
// ...
endmoduleThe defparam statement reaches into u_sub's scope and overrides its WIDTH parameter to 32. The override works in pure Verilog-2001, but every modern synthesis flow and most style guides forbid it for three reasons:
- Action-at-a-distance. The override happens in
top, but the affected parameter is insub. Readingsub's code doesn't reveal the override. - Hierarchical naming requirement. The path
u_sub.WIDTHbinds to the specific instance name. Renaming the instance intopsilently breaks the override. - Deprecated in SystemVerilog. SystemVerilog removed
defparam; most synthesis tools reject it; the construct is forbidden in any forward-looking codebase.
The replacement is the instantiation-time override from §4.1:
sub #(.WIDTH(32)) u_sub (...); // clear, scope-correct, modernIf you encounter defparam in an existing codebase, the right action is to rewrite the affected instantiations to use the inline override syntax. The 6 (constant-variables) overview's §9 covers this in detail.
11. Simulation and Synthesis Behavior
Parameters are resolved at elaboration time — before simulation starts and before synthesis maps to gates. The elaborator:
- Walks the module hierarchy from top to bottom.
- For each instantiation, applies the override values to the instantiated module's parameters.
- Substitutes the resolved values into every expression in the instantiated module.
- The result is a "flattened" hierarchy where every module instance has concrete parameter values.
By the time simulation starts, parameters appear as literal values in the compiled simulation code. The simulator's event queue never schedules an update on a parameter — they are constants from the simulator's perspective. The same is true at synthesis time — the synthesis tool sees the concrete values and produces gates accordingly.
12. Common Mistakes
Six pitfalls that catch engineers using parameter.
12.1 Using ordered override
fifo #(32, 64, 2) u_fifo (...); // BAD — positional, fragileAdding a new parameter to fifo (e.g., between WIDTH and DEPTH) shifts the override positions silently — u_fifo now sets the wrong parameters. Use named overrides: fifo #(.WIDTH(32), .DEPTH(64), .MODE(2)) u_fifo (...).
12.2 Forgetting to override
fifo u_fifo (...); // uses ALL defaultsLegal but probably wrong — the FIFO instantiates with default WIDTH = 8 and DEPTH = 16 regardless of what you wanted. The compile-time check is "the parameters have defaults"; there's no check that you intended those defaults. Lint may warn for important parameters.
12.3 Mismatched parameter types
parameter [7:0] BYTE = 8'hAA;
// ... in the instantiating module ...
fifo #(.BYTE(1024)) u_fifo (...); // 1024 doesn't fit in 8 bitsThe synthesis tool typically truncates 1024 (which is 11-bit) to 8 bits, producing BYTE = 8'h00 (or similar). Lint warns; some tools error out. Make sure the override value fits the parameter's declared type.
12.4 Using parameter for internal magic numbers
module fifo #(parameter BURST_MAX = 16) (...); // BURST_MAX is internal, should be localparamA constant like BURST_MAX that the user has no business changing should be localparam. Exposing it as parameter invites bad overrides. The 6.2 sub-page covers localparam in depth.
12.5 Hierarchical parameters not propagating
module top #(parameter WIDTH = 32);
sub u_sub (...); // BUG: WIDTH not passed
endmodule
module sub #(parameter WIDTH = 8);
// uses default WIDTH = 8 — top's WIDTH is ignored
endmoduleThe top module's WIDTH = 32 doesn't automatically propagate to sub. The instantiation must pass it explicitly: sub #(.WIDTH(WIDTH)) u_sub (...). This is the most common parameter bug in multi-level designs.
12.6 Using defparam
defparam u_sub.WIDTH = 32; // FORBIDDENSee §10. Use inline override #(.WIDTH(32)) instead.
13. Industry Use Cases
Three patterns where parameter is the right tool.
13.1 Width-configurable IP blocks
Every commercial IP (Synopsys DesignWare, Cadence IP, ARM cores, Xilinx Vivado IP) is parameterised for width. The user picks 8/16/32/64-bit and the IP produces the right gates.
13.2 Mode-dependent designs
A module that can run in multiple modes (e.g., a UART that supports 5/6/7/8-bit data, 1/2 stop bits, parity on/off) uses parameters for each mode flag. The generate block expands the right hardware for each mode.
13.3 Customer-specific configurations
A chip company sells the same RTL to multiple customers. Each customer has slightly different requirements (one wants a 32-bit data bus, another wants 64). Parameters allow one source codebase with per-customer configurations.
14. Debugging Lab
Three parameter debug post-mortems
15. Coding Guidelines
- Use port-list parameter declaration (Verilog-2001 style).
module name #(parameter X = 8) (...);is the modern convention. - Always provide default values. Even if every instantiation overrides them, the default documents the module's intended use.
- Use named overrides exclusively.
#(.NAME(value))is robust;#(value)is fragile. ALL_CAPSfor parameter names. Convention:WIDTH,DEPTH,MODE,RESET_VAL.- Use typed parameters for bit-vector constants.
parameter [7:0] DEFAULT_VAL = 8'hAA;makes the width explicit. - Pass parameters explicitly through every hierarchy level. Top-module parameters don't automatically propagate.
- Never use
defparam. Deprecated, action-at-a-distance, rejected by SystemVerilog. - Use
localparamfor derived values,parameterfor user knobs. The 6.2 sub-page covers this in depth.
16. Interview Q&A
17. Exercises
Three exercises that turn parameter usage into reflex.
Exercise 1 — Convert to parameterised
Convert the following module to support arbitrary widths via a WIDTH parameter.
module saturating_counter (
input wire clk, rst_n, en,
output reg [7:0] count_q
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) count_q <= 8'h00;
else if (en && count_q < 8'hFF) count_q <= count_q + 8'h01;
end
endmoduleHints. Default WIDTH = 8. Use {WIDTH{1'b1}} for "all ones." The < 8'hFF comparison becomes < {WIDTH{1'b1}}.
Exercise 2 — Hierarchical override
Given the following hierarchy, write the instantiation in top that produces a 64-bit data path throughout.
module top (...);
middle u_mid (...);
endmodule
module middle #(parameter MW = 16) (...);
bottom #(.BW(MW)) u_btm (...);
endmodule
module bottom #(parameter BW = 8) (
output wire [BW-1:0] dout
);
// ...
endmoduleHints. Add a parameter to top and pass it down through middle. The override must explicitly happen at each instantiation.
Exercise 3 — Spot the bugs
The following module has three parameter-related bugs. Identify and fix each.
module buggy (
input wire clk,
output wire [WIDTH-1:0] data_out
);
parameter WIDTH = 32, DEPTH = 16; // bug 1: in-body declaration order
sub_module u_sub (.data_out(data_out)); // bug 2: missing override
localparam ADDR_W = 4; // bug 3: derived value not actually derived
endmodule
module sub_module (
output reg [SUB_WIDTH-1:0] data_out
);
parameter SUB_WIDTH = 8;
endmoduleWhat to produce. (a) Identify each bug. (b) Show the fix. (c) Explain why the broken version compiled but produced wrong results.
18. Summary
The parameter keyword declares a user-overridable compile-time constant — the foundation of parameterised RTL design. Every reusable IP block uses parameters for width, depth, mode, encoding, and any other knob the user might want to configure at instantiation time.
The declaration:
- Port-list (Verilog-2001, preferred) —
module foo #(parameter WIDTH = 8) (...); - In-body (Verilog-1995, legacy) —
module foo (...); parameter WIDTH = 8; ... - Typed —
parameter [7:0] BYTE_VAL = 8'hAA;for explicit width.
The override:
- Named (preferred) —
module_name #(.PARAM(value)) - Ordered (legacy, fragile) —
module_name #(value1, value2) defparam(FORBIDDEN) — deprecated, never use.
The patterns:
- Width / depth parameterisation — one source produces N hardware variants.
- Mode-dependent generate —
generateblock expands different hardware per parameter value. - Hierarchical passing — propagate parameters through every level of the design.
The day-to-day discipline:
- Always use named overrides at every instantiation.
- Always provide default values for parameters.
ALL_CAPSfor parameter names.- Typed parameters for bit-vector constants.
- Pass parameters explicitly through every hierarchy level — they don't propagate automatically.
- Never use
defparam. - Use
localparamfor derived values;parameterfor user knobs.
The next sub-page is 6.2 localparam — the strictly-internal compile-time constant that complements parameter. After 6.2, Chapter 6 closes and Chapter 7 picks up with compiler directives (the `-prefixed constructs).
Related Tutorials
- Constant Variables — Chapter 6; the parent overview that introduces both
parameterandlocalparam. - Scalar vs Vector vs Arrays — Chapter 5.2.4; the dimensions that parameter-derived widths configure.
- reg — Chapter 5.2.1; the runtime variable counterpart.
- RTL Designing — Chapter 3; the broader RTL framing that motivates parameterised reuse.
- Number Representation — Chapter 4.4; literal syntax for parameter values.