Stop Copy-Pasting Hardware: Master Generate Blocks in SystemVerilog

Every parameterized module you write eventually hits the same wall: you can make the port widths flexible with a parameter, but the logic inside is still written for a fixed size. Add a pipeline stage and you rewrite the always block. Change the adder width and you manually duplicate instantiations. The module is parameterized in name only.

Generate blocks solve this. They let the elaborator write the repetitive hardware for you — instantiating N copies of a submodule, building an N-wide logic chain, or selecting between two architectures based on a parameter. This post covers the three generate constructs you will use in every serious parameterized design: for-generate, and if-generate, with practical examples for each.

1. Generate Blocks Run at Elaboration, Not Simulation

The critical mental model shift: a generate for loop is not a software loop. It does not execute at runtime. It runs once, before simulation or synthesis begins, during a phase called elaboration. The elaborator unrolls the loop and creates a separate piece of hardware for each iteration — each with its own signals, flip-flops, and connections. The result is identical to writing that hardware by hand.

The example below shows a pipeline register written both ways. The manual version does not scale — adding a stage means editing the module. The generate version scales to any depth by changing a single parameter.

pipeline.sv
// Manual pipeline — does not scale, edit required per stage logic [3:0][7:0] pipe; always_ff @(posedge clk) pipe[0] <= d; always_ff @(posedge clk) pipe[1] <= pipe[0]; always_ff @(posedge clk) pipe[2] <= pipe[1]; always_ff @(posedge clk) pipe[3] <= pipe[2]; // ───────────────────────────────────────────────────────── // Generate pipeline — change DEPTH, the rest follows module pipeline #( parameter int DEPTH = 4, parameter int DATA_W = 8 )( input logic clk, rst, input logic [DATA_W1:0] d, output logic [DATA_W1:0] q ); logic [DEPTH:0][DATA_W1:0] stage; assign stage[0] = d; assign q = stage[DEPTH]; genvar i; generate for (i = 0; i < DEPTH; i++) begin : gen_stage always_ff @(posedge clk or posedge rst) begin if (rst) stage[i+1] <= ‘0; else stage[i+1] <= stage[i]; end end endgenerate endmodule

2. For-Generate: Building Bit-Slice Logic

The most common use of for-generate is building N-wide combinational logic from a single-bit operation applied across every bit position. Instead of writing one assign per bit, you write one assign inside a generate loop and let the elaborator replicate it N times, once per index value.

A Gray code encoder is the canonical example: the MSB passes through unchanged, and every other output bit is the XOR of two adjacent binary input bits. The pattern is identical for every bit — only the index changes. With generate, this becomes a single parameterized module that works for any width.

gray_encoder.sv
module gray_encoder #(parameter int N = 8)( input logic [N1:0] bin, output logic [N1:0] gray ); assign gray[N1] = bin[N1]; // MSB passes through unchanged genvar i; generate for (i = 0; i < N1; i++) begin : gen_xor assign gray[i] = bin[i+1] ^ bin[i]; end endgenerate endmodule
Always name your generate blocks: The : gen_xor label after begin is not optional decoration. It creates a hierarchical scope — Vivado uses it to reference generated instances in timing constraints (get_cells gen_xor[3]/...) and in simulation waveforms. Unnamed generate blocks make post-synthesis debug significantly harder.

3. For-Generate: Instantiating N Submodules

The second major use of for-generate is structural: instantiating a submodule N times and connecting the instances in a chain. This is how you build parameterized multi-bit structures from a single verified 1-bit building block. The elaborator creates N distinct instances, each with its own unique name derived from the generate block label and the loop index.

An N-bit ripple carry adder demonstrates the pattern clearly. A single full_adder module handles one bit. The generate loop chains N of them together, threading the carry signal from each instance into the next. Changing the parameter N changes the adder width — no other edit is needed.

ripple_adder.sv
module full_adder ( input logic a, b, cin, output logic sum, cout ); assign {cout, sum} = a + b + cin; endmodule // ───────────────────────────────────────────────────────── module ripple_adder #(parameter int N = 8)( input logic [N1:0] a, b, input logic cin, output logic [N1:0] sum, output logic cout ); logic [N:0] carry; assign carry[0] = cin; assign cout = carry[N]; genvar i; generate for (i = 0; i < N; i++) begin : gen_fa full_adder u_fa ( .a (a[i]), .b (b[i]), .cin (carry[i]), .sum (sum[i]), .cout(carry[i+1]) ); end endgenerate endmodule // Elaborated instance names: gen_fa[0].u_fa, gen_fa[1].u_fa, …, gen_fa[7].u_fa

4. If-Generate: Choosing the Architecture

If-generate selects between two (or more) hardware implementations based on a parameter evaluated at elaboration time. Only the selected branch is synthesized — the other branch does not exist in the netlist. This lets a single module file support multiple architectural variants without any runtime multiplexing overhead.

A common use is selecting between a registered and a combinational output path based on a REGISTERED parameter. The caller instantiates the same module name and simply overrides the parameter. Timing, area, and latency change without touching the module implementation.

output_stage.sv
module output_stage #( parameter int DATA_W = 8, parameter bit REGISTERED = 1 // 1 = register output, 0 = combinational )( input logic clk, rst, input logic [DATA_W1:0] d, output logic [DATA_W1:0] q ); generate if (REGISTERED) begin : gen_reg always_ff @(posedge clk or posedge rst) begin if (rst) q <= ‘0; else q <= d; end end else begin : gen_comb assign q = d; // zero-latency combinational path end endgenerate endmodule // Registered output — 1 cycle latency, meets tighter timing: // output_stage #(.REGISTERED(1)) u0 (.clk(clk), .rst(rst), .d(d), .q(q)); // // Combinational output — zero latency, longer path: // output_stage #(.REGISTERED(0)) u1 (.clk(clk), .rst(rst), .d(d), .q(q));
genvar is an elaboration-time constant, not a signal: You cannot read a genvar inside an always block, assign it to a port, or use it outside a generate loop. It exists only during loop unrolling. If you need a loop index at runtime — for example, in an FSM that iterates over N channels — you need a regular logic counter, not a genvar.

Final Thoughts: Parameters Without Generate Are Half-Finished

A parameter that only controls port widths is a weak abstraction. As soon as the internal logic needs to scale with that parameter — more pipeline stages, more submodule instances, more bit-slice operations — you need generate. Together, parameters and generate blocks form the complete tool for writing hardware that is genuinely reusable: change one number at the top, get a completely different but always-correct implementation out the bottom.

Start with for-generate any time you catch yourself copy-pasting logic with incrementing indices. Move to if-generate any time a module needs to support two architectures without two separate files. Name every generate block — your timing constraints and your future self will thank you.


Happy coding.
fpgawizard.com

error: Selection is disabled!