Skip to main content

Parameters and generate

Data Types built a 4-bit ripple-carry adder by hand-instantiating four full_adder copies, one line per bit. That approach doesn't scale — an 8-bit, 32-bit, or 64-bit version would each need its own hand-written, near-identical module. parameter and generate are how Verilog makes a module's width (or repeated structure) a configurable, code-generated quantity instead.

parameter: compile-time constants​

module ripple_adder #(
parameter WIDTH = 4
) (
input [WIDTH-1:0] a,
input [WIDTH-1:0] b,
input cin,
output [WIDTH-1:0] sum,
output cout
);

A parameter declared in a module's port-like #( ) list is set once, at instantiation or elaboration time — never changes during simulation — and can be overridden per instance:

ripple_adder #(.WIDTH(8)) adder8 (.a(a8), .b(b8), .cin(cin8), .sum(sum8), .cout(cout8));
ripple_adder #(.WIDTH(32)) adder32 (.a(a32), .b(b32), .cin(cin32), .sum(sum32), .cout(cout32));

Two instances of the same module, each independently sized — this is the entire point of parameterization: write the adder's structure once, and let each instantiation site decide how wide a copy it needs. localparam is the same idea but explicitly not overridable from outside the module — used for constants that are derived from other parameters or are simply internal to the module's own logic, where allowing an external override would make no sense.

generate/genvar: code-generated structure​

A parameter alone controls widths and constants, but the 4-bit ripple-carry adder's fa0...fa3 instances were still hand-written, one per bit. generate, combined with a for loop over a special genvar loop variable, produces that repeated structure automatically, scaled to WIDTH:

module ripple_adder #(
parameter WIDTH = 4
) (
input [WIDTH-1:0] a,
input [WIDTH-1:0] b,
input cin,
output [WIDTH-1:0] sum,
output cout
);

wire [WIDTH:0] carry;
assign carry[0] = cin;
assign cout = carry[WIDTH];

genvar i;
generate
for (i = 0; i < WIDTH; i = i + 1) begin : fa_stage
full_adder fa (
.a(a[i]), .b(b[i]), .cin(carry[i]),
.sum(sum[i]), .cout(carry[i+1])
);
end
endgenerate

endmodule

For WIDTH=4, this elaborates into exactly the same four-instance, hand-wired circuit from Data Types — the diagram doesn't change at all, only the source code that produces it does:

Cin=0 and A0/B0 feed FA0, whose Cout feeds FA1 along with A1/B1, whose Cout feeds FA2 along with A2/B2, whose Cout feeds FA3 along with A3/B3; each stage also produces its own Sum output

For WIDTH=32, the identical source produces 32 chained full-adder instances without a single additional line of code — this is the entire value of generate: the description stays the same size regardless of how large the generated hardware gets.

genvar is not a real signal​

genvar (i above) exists purely at elaboration time, controlling how many times the loop body is generated — it has no simulation-time existence, is not a wire or register, and cannot be read or written by any procedural or dataflow statement. It's easy to mistake i for an ordinary loop variable from Loops, but that page's for loops unroll procedural statements inside an always block at compile time; a generate for loop unrolls entire structural instances (module copies, primitive gates, always blocks) at elaboration time — a related idea, applied one level higher, to hardware structure rather than statement repetition.

Conditional generate: if/case at elaboration time​

generate isn't limited to for loops — a generate if (or generate case) picks which structure to elaborate based on a parameter, deciding at elaboration time rather than describing a runtime mux:

generate
if (WIDTH <= 8)
ripple_adder_simple #(.WIDTH(WIDTH)) u_adder (.a(a), .b(b), .cin(cin), .sum(sum), .cout(cout));
else
ripple_adder_fast #(.WIDTH(WIDTH)) u_adder (.a(a), .b(b), .cin(cin), .sum(sum), .cout(cout));
endgenerate

Only one branch is ever actually elaborated into hardware — unlike a runtime if, which builds mux logic to select between both possibilities every time, a generate if picks exactly one of the two adder implementations at compile time based on WIDTH, and the losing branch's hardware is never built at all.

The named generate block​

begin : fa_stage names the generated block — required here so each of the WIDTH generated full_adder instances gets a distinguishable hierarchical name (fa_stage[0].fa, fa_stage[1].fa, and so on) for waveform viewing and debugging, the same reason instance names mattered back in Modules and Ports.

What's next​

generate scales structural repetition; the next page covers Verilog's other major scaling tool — arrays, specifically how a declaration like reg [7:0] mem [0:255];, first previewed in Data Types, is used to model actual ROM and RAM behaviorally.