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:
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.