Verilog · Chapter 6.2 · Constant Variables
localparam in Verilog — Strictly-Internal Compile-Time Constants
The parameter keyword exposes configuration knobs that a module's users can change from outside. The localparam keyword does the opposite. It declares compile-time constants that are strictly internal to the module and cannot be overridden by any external instantiation. This page is a deep look at localparam and why it matters. You will see how it protects internal constants from accidental tampering, how it derives values such as bus widths from a parameter using the clog2 system function, and how it names state-encoding constants in finite-state machines. It also explains the design discipline of keeping user-facing knobs separate from internal magic numbers. Most reusable IP blocks use both keywords together for exactly this reason.
Foundation18 min readVeriloglocalparamConstants$clog2FSM Encoding
Chapter 6 · Section 6.2 · Constant Variables
1. The Engineering Problem
A team designs a parameterised FIFO with configurable depth. The first cut:
module fifo #(
parameter DEPTH = 16,
parameter ADDR_W = 4 // user-overridable, "should match DEPTH"
)(
input wire [ADDR_W-1:0] addr,
output wire [7:0] data_out
// ...
);
reg [7:0] mem [0:DEPTH-1];
// ...
endmoduleA user instantiates it with DEPTH = 32:
fifo #(.DEPTH(32), .ADDR_W(4)) u_fifo (...); // 32 entries, but only 4 address bits!The result: a 32-entry memory with only 16 addressable entries. The upper 16 entries are unreachable. addr overflows from 4'hF to 4'h0 and wraps around — every write to "address 16" silently overwrites address 0. The bug is invisible in code review (both parameters are explicit) and produces wrong results at runtime.
The right design encodes the constraint in the language: ADDR_W is derived from DEPTH, not user-configurable.
module fifo #(
parameter DEPTH = 16 // ONLY user-overridable parameter
)(
input wire [ADDR_W-1:0] addr,
output wire [7:0] data_out
);
localparam ADDR_W = $clog2(DEPTH); // derived from DEPTH, cannot be overridden
reg [7:0] mem [0:DEPTH-1];
// ...
endmoduleNow the user instantiation:
fifo #(.DEPTH(32)) u_fifo (...); // ADDR_W = $clog2(32) = 5, automaticallyThe user cannot override ADDR_W — it's a localparam, and any attempt produces a compile error. The address width always matches the depth. The bug is impossible.
This is the role of localparam: encode the design's internal invariants as language-enforced constraints. Where parameter says "this is a configuration knob the user can turn," localparam says "this is an internal constant the user has no business touching." The two keywords together form the design contract: knobs vs constants.
2. Anatomy of a localparam Declaration
The syntax mirrors parameter but with one difference: there is no port-list form. localparam declarations live in the module body only.
`default_nettype none
module example #(
parameter DEPTH = 16, // user-overridable
parameter MODE = 0
)(
input wire clk
);
// localparam declarations — module-body only
localparam ADDR_W = $clog2(DEPTH); // derived from DEPTH
localparam BURST_MAX = 8; // internal magic number
localparam [3:0] IDLE = 4'h0; // typed FSM state
localparam [3:0] ACTIVE = 4'h1;
localparam [3:0] WAIT = 4'h2;
localparam [3:0] DONE = 4'h3;
// ... use ADDR_W, BURST_MAX, IDLE, ACTIVE, WAIT, DONE anywhere ...
endmoduleThree things to note:
- No port-list form. Unlike
parameter,localparamcannot appear in the#(...)syntax. It must be declared inside the module body. - No default-value override path. The right-hand-side expression is the value; no external instantiation can change it.
- Right-hand side can reference parameters.
localparam ADDR_W = $clog2(DEPTH);is the canonical pattern — derive alocalparamvalue from aparameterso it always tracks.
The convention: ALL_CAPS names, same as parameter. The convention places localparam declarations after all parameter declarations in the module body, so the derivation order is clear.
3. Mental Model
The mental-model has three practical consequences:
localparamis the default choice for internal constants. If you're declaring a constant and the user has no business changing it, uselocalparam. Only when the constant is genuinely a knob does it becomeparameter.- Derive everything you can. A FIFO's address width, a memory's read-port count's bit width, a counter's max value — all derivable. Declaring them as
localparamremoves inconsistency risks. - Use named constants for FSM states. Raw
2'b00,2'b01is a code smell. Named constantsIDLE,ACTIVEself-document and survive refactoring.
4. The Two Keywords Side-by-Side
| Aspect | parameter | localparam |
|---|---|---|
| Compile-time constant | yes | yes |
| Overridable externally | yes (via instantiation) | no |
Declaration in port-list #(...) | yes (Verilog-2001) | no |
| Declaration in module body | yes (Verilog-1995 form) | yes |
| Default value required | recommended | required (no override mechanism) |
| Typical use | width, depth, mode, encoding | derived values, internal constants, FSM states |
| Synthesis behaviour | folded into expressions | folded into expressions |
| Simulation cost | zero (compile-time) | zero (compile-time) |
The single observable difference is overridability. Everything else — synthesis, simulation, scoping — is identical.
5. The $clog2() Pattern
The canonical use case. The IEEE 1364-2005 $clog2() system function returns the ceiling of log base 2 — the number of bits needed to represent values 0 through N-1. The pattern:
`default_nettype none
module memory #(
parameter DEPTH = 16
)(
input wire [ADDR_W-1:0] addr,
input wire wr_en,
input wire [7:0] wr_data,
output wire [7:0] rd_data
);
localparam ADDR_W = $clog2(DEPTH);
reg [7:0] mem [0:DEPTH-1];
always @(posedge clk) if (wr_en) mem[addr] <= wr_data;
assign rd_data = mem[addr];
endmoduleFor each common DEPTH, the resulting ADDR_W:
DEPTH | ADDR_W (= $clog2(DEPTH)) | Bits Needed |
|---|---|---|
| 1 | 0 | 1-entry — no address needed |
| 2 | 1 | 2 entries, 1 bit |
| 4 | 2 | 4 entries, 2 bits |
| 8 | 3 | 8 entries, 3 bits |
| 16 | 4 | 16 entries, 4 bits |
| 17 | 5 | needs 5 bits (since 2^4 = 16 < 17) |
| 32 | 5 | 32 entries, 5 bits |
| 256 | 8 | 256 entries, 8 bits |
| 1024 | 10 | 1K entries, 10 bits |
The function rounds up — $clog2(17) = 5 because 17 doesn't fit in 4 bits. For powers of 2, $clog2(N) = log2(N) exactly.
5.1 Variants
localparam ADDR_W = $clog2(DEPTH); // simple address-width derivation
localparam CNT_W = $clog2(DEPTH + 1); // count needs one extra value (e.g., for "full")
localparam REG_CNT = NUM_REGS * REGS_PER_BANK; // arithmetic combination of parameters
localparam BUS_WIDTH = DATA_WIDTH + ECC_BITS; // composite widthAny expression involving parameter values and constants is legal as the right-hand side of a localparam. The elaborator computes the value at compile time.
5.2 $clog2() edge cases
$clog2(0) // = 0 (some tools); 1 (other tools) — implementation-defined
$clog2(1) // = 0 (a 1-entry "memory" needs 0 address bits — only one address possible)
$clog2(2) // = 1
$clog2(2**31) // = 31 (handles large values fine on 32-bit systems)For DEPTH < 2, $clog2() may return 0 (in which case you'd declare a reg [-1:0] — undefined). The defensive pattern:
localparam ADDR_W = (DEPTH <= 1) ? 1 : $clog2(DEPTH); // minimum 1 bitMost production codebases ensure DEPTH ≥ 2 at the architectural level, so the edge case rarely matters in practice. The $clog2(1) = 0 corner is the only one worth knowing about.
6. The FSM State Encoding Pattern
The second canonical use. FSM state values declared as named localparams.
`default_nettype none
module simple_fsm (
input wire clk, rst_n, start, done,
output reg busy
);
// FSM state encoding — named constants for readability
localparam [1:0] IDLE = 2'b00;
localparam [1:0] ACTIVE = 2'b01;
localparam [1:0] WAIT = 2'b10;
localparam [1:0] FINISH = 2'b11;
reg [1:0] state_q, state_next;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) state_q <= IDLE;
else state_q <= state_next;
end
always @(*) begin
state_next = state_q;
case (state_q)
IDLE: if (start) state_next = ACTIVE;
ACTIVE: state_next = WAIT;
WAIT: if (done) state_next = FINISH;
FINISH: state_next = IDLE;
endcase
end
always @(*) busy = (state_q != IDLE);
endmoduleThe four localparam declarations name the states. Three benefits:
- Readability.
if (start) state_next = ACTIVE;reads better thanif (start) state_next = 2'b01;. - Refactoring safety. Changing the encoding (e.g., from binary to one-hot for power optimisation) requires updating only the
localparamdeclarations, not every reference. - Code review. Typing
2'b10instead of2'b11is hard to spot in code review; typingWAITinstead ofFINISHis obvious.
The convention: all state names declared as localparams with explicit width ([STATE_W-1:0]). The STATE_W itself is often a localparam derived from the state count: localparam STATE_W = $clog2(NUM_STATES);.
6.1 One-hot encoding variant
For power-optimised designs (avoiding multi-bit transitions per cycle), one-hot encoding uses one bit per state:
localparam [3:0] IDLE = 4'b0001; // bit 0 = IDLE
localparam [3:0] ACTIVE = 4'b0010; // bit 1 = ACTIVE
localparam [3:0] WAIT = 4'b0100; // bit 2 = WAIT
localparam [3:0] FINISH = 4'b1000; // bit 3 = FINISHThe transition between states changes exactly two bits (one from 1 → 0, one from 0 → 1), reducing dynamic power. The localparam form makes the encoding choice swappable — change the four declarations from binary to one-hot, and the rest of the FSM code stays unchanged.
7. Visual Explanation
Three figures cover the localparam patterns.
7.1 Visual A — protected internal constant
The interface boundary. parameter is exposed; localparam is internal.
parameter vs localparam — interface boundary
data flow7.2 Visual B — the derived-value cascade
A parameter flowing into multiple localparams, all of which derive from it.
7.3 Visual C — FSM state encoding swap
The named-state pattern lets you swap encodings without touching FSM logic.
8. The Parameterised FIFO — Complete Example
A real-world FIFO with parameters for user knobs and localparams for derived values.
`default_nettype none
module fifo #(
parameter DATA_WIDTH = 8, // user-overridable
parameter DEPTH = 16
)(
input wire clk,
input wire rst_n,
input wire push,
input wire pop,
input wire [DATA_WIDTH-1:0] data_in,
output wire [DATA_WIDTH-1:0] data_out,
output wire full,
output wire empty,
output wire [CNT_W-1:0] count
);
// Derived constants — strictly internal
localparam ADDR_W = $clog2(DEPTH); // bit width to address DEPTH entries
localparam CNT_W = $clog2(DEPTH + 1); // count needs DEPTH+1 representable values
// Internal state
reg [DATA_WIDTH-1:0] mem [0:DEPTH-1];
reg [ADDR_W-1:0] wr_ptr_q;
reg [ADDR_W-1:0] rd_ptr_q;
reg [CNT_W-1:0] count_q;
integer i;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
wr_ptr_q <= {ADDR_W{1'b0}};
rd_ptr_q <= {ADDR_W{1'b0}};
count_q <= {CNT_W{1'b0}};
for (i = 0; i < DEPTH; i = i + 1)
mem[i] <= {DATA_WIDTH{1'b0}};
end else begin
if (push && !pop) count_q <= count_q + 1'b1;
if (!push && pop) count_q <= count_q - 1'b1;
if (push) begin
mem[wr_ptr_q] <= data_in;
wr_ptr_q <= (wr_ptr_q == DEPTH-1) ? {ADDR_W{1'b0}} : wr_ptr_q + 1'b1;
end
if (pop) begin
rd_ptr_q <= (rd_ptr_q == DEPTH-1) ? {ADDR_W{1'b0}} : rd_ptr_q + 1'b1;
end
end
end
assign data_out = mem[rd_ptr_q];
assign full = (count_q == DEPTH);
assign empty = (count_q == {CNT_W{1'b0}});
assign count = count_q;
endmodule
// Instantiation examples:
// fifo #(.DATA_WIDTH(8), .DEPTH(4)) u_uart_rx_fifo (...);
// fifo #(.DATA_WIDTH(32), .DEPTH(64)) u_dma_buffer (...);
// fifo #(.DATA_WIDTH(64), .DEPTH(256)) u_packet_fifo (...);Two parameters expose the user knobs (data width, depth). Two localparams derive from DEPTH (address width, count width). The user cannot override ADDR_W or CNT_W because they're localparams — the language enforces the constraint.
The instantiations are simple: only the two parameters that matter to the user (data width, depth). Everything else follows automatically.
9. Simulation and Synthesis Behavior
localparam is resolved at elaboration time, identical to parameter. The elaborator substitutes the value into every expression that uses it; the simulator and synthesis tool see only the resolved constant.
The right-hand-side expression of a localparam can be any expression involving constants and parameters — $clog2(), arithmetic, conditional (? :), bitwise operators. The elaborator evaluates it once.
10. Common Mistakes
Five pitfalls that catch engineers using localparam.
10.1 Using parameter for derived values
module fifo #(
parameter DEPTH = 16,
parameter ADDR_W = 4 // BUG: should be localparam
);Exposing ADDR_W as a parameter invites the §1 mismatch bug. Use localparam ADDR_W = $clog2(DEPTH); instead.
10.2 Using localparam for genuinely user-configurable values
The opposite mistake. A FIFO's DATA_WIDTH is genuinely user-configurable; using localparam forces every variant to hardcode it. Use parameter for user knobs.
10.3 Forgetting that localparam cannot be overridden
module fifo;
localparam DEPTH = 16;
endmodule
// Trying to override
fifo #(.DEPTH(32)) u_fifo (...); // COMPILE ERRORThe compiler errors out: "DEPTH is a localparam and cannot be overridden." If you want the value to be overridable, change localparam to parameter.
10.4 Circular derivation
localparam A = B + 1;
localparam B = A * 2; // circular dependencyThe elaborator cannot resolve A and B simultaneously; compilation fails. Make sure every localparam's right-hand side references only parameters, constants, and previously-declared localparams.
10.5 Forward reference
module fifo;
localparam ADDR_W = $clog2(DEPTH); // BUG: DEPTH not declared yet
parameter DEPTH = 16;
endmoduleThe forward reference produces a compile error or undefined behaviour. Declare parameters before any localparam that references them; this is the convention every working codebase enforces.
11. Industry Use Cases
Three patterns where localparam is essential.
11.1 Derived widths in parameterised IP
Every parameterised IP block (FIFO, register file, arbiter, decoder) has localparams deriving from its parameters. The pattern is universal: a DEPTH parameter and a localparam ADDR_W = $clog2(DEPTH); derivation, ensuring the address width always matches the depth.
11.2 FSM state encoding
Every FSM uses localparam for its state values. The named-state pattern is the standard idiom across every working RTL house. Mixing binary, gray, and one-hot encodings within a project is supported by switching only the localparam declarations — the FSM logic stays unchanged.
11.3 Module-internal constants
Protocol-defined constants (e.g., AHB burst lengths, AXI4 burst sizes, USB descriptor values) that come from external specifications are localparam — the protocol fixes them; no instantiation should override them. Exposing them as parameter would mislead users into thinking they have flexibility they don't.
12. Debugging Lab
Three localparam debug post-mortems
13. Coding Guidelines
- Use
localparamfor derived values. Any constant computed from aparametershould belocalparam— the user shouldn't be able to override it independently. - Use
localparamfor FSM state encoding. Named states (IDLE, ACTIVE, DONE) survive code-review and refactoring better than raw binary literals. - Use
localparamfor module-internal magic numbers. Constants the user has no business overriding belong aslocalparam, notparameter. - Declare
localparams afterparameters. Convention: parameters first, then localparams that derive from them. ALL_CAPSnaming forlocalparam. Same convention asparameter.- Typed
localparams for FSM states.localparam [STATE_W-1:0] IDLE = 2'b00;makes the width explicit and prevents mismatched-width assignments. - Avoid circular dependencies. Each
localparam's right-hand side should reference onlyparameters, constants, and previously-declaredlocalparams. - Don't try to override a
localparam. Useparameterif you want overridable; uselocalparamonly when you don't.
14. Interview Q&A
15. Exercises
Three exercises that turn localparam usage into reflex.
Exercise 1 — Spot the misuse
For each declaration, recommend parameter, localparam, or "remove entirely."
module memory (
parameter DATA_WIDTH = 8, // ?
parameter NUM_ENTRIES = 16, // ?
parameter ADDR_W = 4, // ?
parameter signed ZERO = 32'h0000_0000, // ?
parameter CACHE_LINE_SIZE = 8, // ? (always 8 in this design)
localparam HALF_ENTRIES = NUM_ENTRIES / 2, // ?
localparam [1:0] IDLE = 2'b00 // ?
);Hints. User-configurable → parameter. Derived or design-constant → localparam. Used-as-constant-zero values rarely belong as parameter.
Exercise 2 — Convert FSM to named states
Convert the following FSM to use localparam-named state constants. The current encoding uses raw 3-bit binary values.
module traffic_light (
input wire clk, rst_n,
output reg green, yellow, red
);
reg [2:0] state_q;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) state_q <= 3'b000;
else case (state_q)
3'b000: state_q <= 3'b001;
3'b001: state_q <= 3'b010;
3'b010: state_q <= 3'b100;
3'b100: state_q <= 3'b000;
endcase
end
always @(*) begin
green = (state_q == 3'b001);
yellow = (state_q == 3'b010);
red = (state_q == 3'b100);
end
endmoduleHints. Identify the four states and name them (RESET, GREEN, YELLOW, RED). Declare each as localparam [2:0]. Replace every raw 3'bXXX with the named version.
Exercise 3 — Parameterise a register file
Write a register file module with DATA_WIDTH and NUM_REGS as parameters and ADDR_W as a derived localparam. The module should:
- Have a synchronous write port.
- Have an asynchronous read port.
- Reset every register to zero in the reset path.
Hints. Use localparam ADDR_W = $clog2(NUM_REGS); so the address bus width tracks NUM_REGS. Declare the storage as reg [DATA_WIDTH-1:0] rf [0:NUM_REGS-1];. Use a generate loop or integer for loop in the reset path.
16. Summary
localparam declares compile-time constants that cannot be overridden externally — the strict-internal counterpart to parameter. The keyword exists to protect internal constants from accidental tampering and to encode design invariants as language-enforced constraints.
The three canonical uses:
- Derived values.
localparam ADDR_W = $clog2(DEPTH);derives from aparameter, always tracks it, cannot be overridden independently. - FSM state encoding.
localparam IDLE = 2'b00;declares named state constants for readability and refactoring safety. - Internal magic numbers.
localparam BURST_MAX = 16;for constants the user has no business overriding.
The syntax:
- Module-body only (no port-list form).
- Right-hand side can be any expression involving
parameters, constants, and previously-declaredlocalparams. - Convention:
ALL_CAPSnames, declared after allparameters.
The keyword choice is interface design:
parameter— user-facing knob, part of the public API.localparam— strictly internal, protected from external override.
Both produce identical hardware after synthesis — they fold into expressions at elaboration time. The simulator and synthesis tool see only the resolved constants.
The day-to-day discipline:
- Default to
localparam. Useparameteronly when the value is genuinely a user knob. - Derive everything you can. A
parameterfor the user knob; one or morelocalparams for the derived widths and counts. - Use named state constants for FSMs. Raw binary literals are a code smell.
- Declare
parameters beforelocalparams. Forward references produce compile errors.
The next chapter (Chapter 7) covers compiler directives — the `-prefixed constructs (`define, `ifdef, `include, `timescale) that configure compilation rather than describe hardware. Together with parameter and localparam, they form the compile-time configuration vocabulary of the language.
Related Tutorials
- parameter — Chapter 6.1; the user-overridable companion to localparam.
- Constant Variables — Chapter 6; the parent overview.
- Scalar vs Vector vs Arrays — Chapter 5.2.4; the dimensions that localparam-derived widths configure.
- reg — Chapter 5.2.1; the runtime variable counterpart.
- Number Representation — Chapter 4.4; literal syntax for localparam values.