Skip to content

RTL Design Patterns · Chapter 12 · Parameterized & Reusable RTL

Generate-Based Replication

A parameter names a family of designs; generate is how you build the member the parameter describes. It reads the parameter at elaboration time and stamps out actual, statically-named hardware. When you need eight synchronizers, one per crossing bit, a for-generate replicates the flop pair into eight named instances instead of hand-copying it and risking an index slip. When a block should carry an optional pipeline stage only in its high-speed build, an if-generate includes the stage when a mode parameter is set and excludes it otherwise. This is structural replication resolved before simulation starts, and it differs from a procedural for loop inside one always block, which describes behavior within a single process. This lesson builds both forms, shows how generated slices connect through arrays and carry chains, and covers the undriven branch that floats in the configuration you never simulated, in SystemVerilog, Verilog, and VHDL.

Intermediate16 min readRTL Design PatternsGenerateElaboration-Time ReplicationParameterizationBit-Sliced DatapathConditional Hardware

Chapter 12 · Section 12.2 · Parameterized & Reusable RTL

1. The Engineering Problem

You are building the destination-domain front end of a chip. Eight independent control bits cross from an unrelated clock domain, and every one of them needs a two-flop synchronizer — the exact structure from Chapter 11.2, back-to-back flops on the destination clock. The obvious move is to instantiate the synchronizer module eight times and wire each to its bit. That works, and for eight bits it is only tedious. But then the spec changes to sixteen bits, then a variant part needs four, and the synchronized-bit count becomes a parameter WIDTH that the integrator sets per instance. You cannot hand-write WIDTH copies of an instantiation when WIDTH is not known until someone elaborates the design.

This is the shape of a huge fraction of real RTL: a structure that must be replicated a parameter-determined number of times, or a feature that must be present in some configurations and absent in others. A byte-enabled register file has one write-mask lane per byte — WIDTH/8 of them. A ripple-carry adder is N full-adder slices chained carry-to-carry. A configurable datapath includes a saturation stage only when SATURATE is set. A DMA engine has NUM_CHANNELS identical channel state machines. In every case the count or the presence of hardware is a parameter, and you need the tool to build the right amount of real, named hardware from that parameter — before simulation, before synthesis, at elaboration time.

You cannot solve this with a runtime for loop inside an always block: that loop describes behavior within one block, it does not create eight separate module instances you can constrain, probe, or place. And you cannot solve it by copy-paste, because the count is not a constant you own. What you need is a construct that runs at compile time, reads the parameter, and emits the corresponding structure — WIDTH instances of the synchronizer, N chained adder slices, the saturation stage or not. That construct is generate, and it is the backbone of every reusable, parameterized block in a real IP library.

the-need.v — how many synchronizers? the parameter decides, at elaboration
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // WIDTH control bits cross from an async domain. WIDTH is set per instance.
   parameter WIDTH = 8;              // 4 on one part, 16 on another — not a constant we own
 
   wire [WIDTH-1:0] async_in;        // WIDTH bits, each needs its OWN 2FF synchronizer
   wire [WIDTH-1:0] sync_out;
 
   // WRONG — you cannot hand-copy WIDTH instantiations; WIDTH is unknown here:
   //   sync2 u0 (.d(async_in[0]), .q(sync_out[0]));
   //   sync2 u1 (.d(async_in[1]), .q(sync_out[1]));   // ... and when WIDTH=16? =4?
 
   // WRONG — a procedural for in an always block describes ONE block's behavior,
   //   it does NOT stamp out WIDTH separate, named synchronizer instances.
 
   // RIGHT — a for-generate REPLICATES the synchronizer WIDTH times at elaboration,
   //   creating WIDTH real, named instances: gen_sync[0].u_sync ... gen_sync[WIDTH-1].u_sync.

2. Mental Model

3. Pattern Anatomy

The pattern has two forms — replication and conditional inclusion — built from a small, shared vocabulary.

The for-generate — structural replication. A genvar (an elaboration-time index, not a runtime variable) drives a generate for loop whose body is wrapped in a named block. Each iteration stamps out one copy of that body — a submodule instance, or a piece of logic — and gives it a hierarchical name indexed by the genvar: gen_lbl[i].u_slice. The count comes from a parameter (N, WIDTH), so the number of instances is set at elaboration. This is how you build a bit-sliced datapath (N full-adder slices), a bank of registers (WIDTH flops with per-bit enables), or an array of synchronizers (one 2FF pair per crossing bit). The body is real hardware, replicated; it is not a loop that executes.

Connecting the slices — arrays and carry chains. Replicated slices are not islands; they connect. The universal technique is an array of signals indexed by the same genvar: lane i reads din[i] and drives dout[i]. When slices form a chain — a ripple-carry adder, a shift register, a pipeline — each slice's output feeds the next slice's input, so you declare an internal array one element longer than the slice count (carry[0..N]), tie carry[0] to the chain's input, connect slice i between carry[i] and carry[i+1], and take carry[N] as the chain's output. The genvar wires the topology; the array carries the signals between generated instances.

The if-generate and case-generate — conditional inclusion. if (COND) generate … else generate … endgenerate (SystemVerilog/Verilog) and if COND generate … else generate … end generate (VHDL) decide, from a parameter, which hardware is emitted. Use it to add a stage only when a feature is enabled (an output register only if REGISTERED, a saturation clamp only if SATURATE), or, with case-generate, to pick one of several implementations by a MODE parameter (a fast wide adder vs a small ripple adder). The branch not taken emits nothing. Every branch must present the same interface — every branch of an if/case-generate must drive every output the module declares — or a configuration you did not elaborate will float (this is §6 and §7).

genvar and named blocks — the addressing. A genvar is scoped to its generate loop and is resolved at elaboration; reusing one across loops or mis-scoping it is a classic error. The named block (: gen_lbl) is mandatory in practice: it gives the replicated instances their hierarchical names, which timing constraints, waveform probes, and assertions all reference. An unnamed generate block yields tool-assigned names that are fragile and non-portable.

Generate — one parameter, two forms, one compile-time factory

data flow
Generate — one parameter, two forms, one compile-time factoryparameter (N,WIDTH, MODE)names the family membergenvar i (elaboration index)genvar i(elaboration…compile-time, not a runtime varfor-generatestamp N named instances: gen_lbl[i]array / carrychaincarry[i] -> carry[i+1] wires slicesif /case-generateinclude or exclude hardware by paramelaboratednetlistfixed hardware the clock then drives
A parameter names which family member to build. A genvar drives a for-generate that stamps out N named instances (gen_lbl[i]), and an array or carry chain wires those slices together. Independently, an if/case-generate reads a parameter and includes or excludes hardware. Both run ONCE at elaboration and produce a fixed netlist — the hardware the clock later drives. Nothing here is a runtime loop or a runtime branch; it is a compile-time factory. The same two forms exist in SystemVerilog, Verilog, and VHDL.

The distinction from a procedural loop closes the anatomy, because it is the concept engineers most often blur. A for (i=0; i<N; i=i+1) inside a single always @(*) or always_ff block is behavioral: the loop is unrolled within that one block to describe its logic compactly — one block, described with a loop. A generate for is structural: it stamps out N separate blocks or instances into the netlist, each independently named. Both can produce a bit-sliced result, but only the generate-for gives you N addressable instances (for per-lane constraints, per-instance attributes like the synchronizer's ASYNC_REG, or hierarchical assertions). Reach for the procedural for when you are describing one block's behavior over its bits; reach for the generate-for when you want N replicated instances of a structure.

4. Real RTL Implementation

This is the core of the page. We build three things — (a) a for-generate replicated structure (an N-bit synchronizer bank, one 2FF pair per crossing bit, per-lane connected), (b) an if/case-generate optional feature (an output pipeline stage included only when a MODE parameter selects it), and (c) the if-generate-undriven-output bug, buggy vs fixed — and each is shown in SystemVerilog, Verilog, and VHDL, with both an RTL block and a self-checking testbench. The idea is identical in all three; the syntax differs, and seeing them side by side is what makes the pattern language-independent.

Two disciplines run through every block. First, every for-generate range covers exactly the intended slicesfor (i = 0; i < N; i++) visits 0 … N-1, no more, no fewer. Second, every if/case-generate branch drives every output — the branch you did not elaborate is the branch you did not test, and an output a branch forgets to drive floats in that configuration (pattern (c)).

4a. A for-generate replicated structure — an N-bit synchronizer bank

Start with replication. We take the two-flop synchronizer atom (one crossing bit) and use a for-generate to stamp out WIDTH copies — one independent synchronizer per crossing bit — each a named instance so a per-instance ASYNC_REG-style attribute or timing constraint can target it. The per-lane connection is trivial because each lane is independent: lane i synchronizes async_in[i] to sync_out[i].

sync_bank.sv — for-generate replicates a 2FF synchronizer per crossing bit
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // The atom: one two-flop synchronizer (destination-clock, async reset).
   module sync2 (
       input  logic clk,        // destination clock — both flops sample here
       input  logic rst_n,      // async, active-low reset
       input  logic d,          // async crossing bit (may be mid-transition)
       output logic q           // resolved, safe in the destination domain
   );
       logic meta;              // first flop: the one that may go metastable
       always_ff @(posedge clk or negedge rst_n) begin
           if (!rst_n) begin meta <= 1'b0; q <= 1'b0; end
           else        begin meta <= d;    q <= meta;  end
       end
   endmodule
 
   // The bank: WIDTH independent synchronizers, one per bit — replicated.
   module sync_bank #(
       parameter int WIDTH = 8                       // #crossing bits — set per instance
   )(
       input  logic             clk,
       input  logic             rst_n,
       input  logic [WIDTH-1:0] async_in,            // WIDTH async bits, in parallel
       output logic [WIDTH-1:0] sync_out             // WIDTH resolved bits
   );
       genvar i;                                     // ELABORATION-time index, not runtime
       generate
           for (i = 0; i < WIDTH; i++) begin : gen_lane   // named block => gen_lane[i]
               // Each iteration STAMPS OUT one real sync2 instance, addressable as
               // gen_lane[i].u_sync — a per-instance constraint/attribute can target it.
               sync2 u_sync (
                   .clk   (clk),
                   .rst_n (rst_n),
                   .d     (async_in[i]),             // per-lane connect: lane i reads bit i
                   .q     (sync_out[i])              // ... and drives bit i
               );
           end
       endgenerate
   endmodule

The single line that matters is begin : gen_lane — the label. Without it the WIDTH synchronizer instances have tool-assigned names and gen_lane[i].u_sync (the path a constraint or a waveform probe needs) does not exist. The testbench instantiates the bank at several WIDTH values (a parameterized structure must be verified generic), drives each lane, waits the two-cycle synchronizer latency, and asserts every lane resolved to its input — and it checks that the hierarchical instance names exist by driving lanes independently and confirming no cross-lane bleed.

sync_bank_tb.sv — self-checking at multiple WIDTH; every lane resolves, no cross-lane bleed
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module sync_bank_tb;
       localparam int WIDTH = 8;                     // re-elaborate at 1, 4, 8, 16 to prove generic
       logic             clk = 0, rst_n = 0;
       logic [WIDTH-1:0] async_in, sync_out;
       int               errors = 0;
 
       sync_bank #(.WIDTH(WIDTH)) dut (.clk(clk), .rst_n(rst_n),
                                       .async_in(async_in), .sync_out(sync_out));
 
       always #5 clk = ~clk;                         // 100 MHz destination clock
 
       initial begin
           async_in = '0; rst_n = 0; repeat (2) @(posedge clk);
           rst_n = 1;
           for (int t = 0; t < 8; t++) begin
               async_in = $urandom;                  // random pattern across all WIDTH lanes
               repeat (3) @(posedge clk);            // wait past the 2FF latency
               #1;
               // Invariant: every lane's output equals its input (each lane independent).
               assert (sync_out === async_in)
                   else begin $error("t=%0d in=%b out=%b", t, async_in, sync_out); errors++; end
           end
           // Directed: drive ONLY lane 3 high; confirm no other lane bleeds (instances distinct).
           async_in = '0; async_in[3] = 1'b1; repeat (3) @(posedge clk); #1;
           assert (sync_out === (WIDTH'(1) << 3))
               else begin $error("cross-lane bleed: out=%b", sync_out); errors++; end
           if (errors == 0) $display("PASS: sync_bank WIDTH=%0d all lanes resolve, no bleed", WIDTH);
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

The Verilog form is identical in intent; the differences are reg/wire typing, $random instead of $urandom, and the classic generate/endgenerate wrapper. The genvar and the named block work the same way — gen_lane[i].u_sync is still the hierarchical address.

sync_bank.v — for-generate replicates the synchronizer WIDTH times in Verilog
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module sync2 (
       input  wire clk,
       input  wire rst_n,
       input  wire d,
       output reg  q
   );
       reg meta;
       always @(posedge clk or negedge rst_n) begin
           if (!rst_n) begin meta <= 1'b0; q <= 1'b0; end
           else        begin meta <= d;    q <= meta;  end
       end
   endmodule
 
   module sync_bank #(
       parameter WIDTH = 8
   )(
       input  wire             clk,
       input  wire             rst_n,
       input  wire [WIDTH-1:0] async_in,
       output wire [WIDTH-1:0] sync_out
   );
       genvar i;                                     // elaboration-time index
       generate
           for (i = 0; i < WIDTH; i = i + 1) begin : gen_lane   // named => gen_lane[i]
               // One real sync2 per bit — gen_lane[i].u_sync is the hierarchical name.
               sync2 u_sync (
                   .clk   (clk),
                   .rst_n (rst_n),
                   .d     (async_in[i]),             // lane i in
                   .q     (sync_out[i])              // lane i out
               );
           end
       endgenerate
   endmodule
sync_bank_tb.v — self-checking; wait past 2FF latency, assert every lane resolves
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module sync_bank_tb;
       parameter WIDTH = 8;                          // re-run at 1, 4, 16 to prove generic
       reg              clk, rst_n;
       reg  [WIDTH-1:0] async_in;
       wire [WIDTH-1:0] sync_out;
       integer          t, errors;
 
       sync_bank #(.WIDTH(WIDTH)) dut (.clk(clk), .rst_n(rst_n),
                                       .async_in(async_in), .sync_out(sync_out));
 
       initial clk = 0;
       always #5 clk = ~clk;
 
       initial begin
           errors = 0; async_in = {WIDTH{1'b0}}; rst_n = 0;
           repeat (2) @(posedge clk); rst_n = 1;
           for (t = 0; t < 8; t = t + 1) begin
               async_in = $random;                   // random across all lanes
               repeat (3) @(posedge clk);            // past the 2FF latency
               #1;
               if (sync_out !== async_in) begin
                   $display("FAIL: t=%0d in=%b out=%b", t, async_in, sync_out);
                   errors = errors + 1;
               end
           end
           async_in = {WIDTH{1'b0}}; async_in[3] = 1'b1;   // drive only lane 3
           repeat (3) @(posedge clk); #1;
           if (sync_out !== ({{(WIDTH-1){1'b0}}, 1'b1} << 3))
               begin $display("FAIL: cross-lane bleed out=%b", sync_out); errors = errors + 1; end
           if (errors == 0) $display("PASS: sync_bank all lanes resolve, no bleed");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

In VHDL the replication is a for … generate with a generate label (gen_lane) and the component/entity instantiated inside it. The label plus the loop index names each instance — gen_lane(i).u_sync — exactly as the SystemVerilog named block does. The per-lane connection uses the array index directly.

sync_bank.vhd — for..generate replicates the synchronizer per bit (labelled)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
 
   entity sync2 is
       port (
           clk   : in  std_logic;
           rst_n : in  std_logic;
           d     : in  std_logic;
           q     : out std_logic
       );
   end entity;
 
   architecture rtl of sync2 is
       signal meta : std_logic;
   begin
       process (clk, rst_n)
       begin
           if rst_n = '0' then
               meta <= '0'; q <= '0';                -- async, active-low reset
           elsif rising_edge(clk) then
               meta <= d; q <= meta;                 -- two flops on the destination clock
           end if;
       end process;
   end architecture;
 
   library ieee;
   use ieee.std_logic_1164.all;
 
   entity sync_bank is
       generic ( WIDTH : positive := 8 );            -- generic == Verilog parameter
       port (
           clk      : in  std_logic;
           rst_n    : in  std_logic;
           async_in : in  std_logic_vector(WIDTH-1 downto 0);
           sync_out : out std_logic_vector(WIDTH-1 downto 0)
       );
   end entity;
 
   architecture rtl of sync_bank is
   begin
       -- for..generate: one sync2 instance per bit. gen_lane is the generate LABEL;
       -- gen_lane(i).u_sync is the hierarchical instance name a constraint targets.
       gen_lane : for i in 0 to WIDTH-1 generate
           u_sync : entity work.sync2
               port map (
                   clk   => clk,
                   rst_n => rst_n,
                   d     => async_in(i),             -- lane i in
                   q     => sync_out(i)              -- lane i out
               );
       end generate;
   end architecture;

The VHDL testbench re-runs the same self-check idea with assert … report … severity error, driving all lanes and then a single lane to confirm each generated instance is independent.

sync_bank_tb.vhd — self-checking VHDL bank testbench (assert report severity)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity sync_bank_tb is
   end entity;
 
   architecture sim of sync_bank_tb is
       constant WIDTH : positive := 8;               -- re-elaborate at 1, 4, 16 to prove generic
       signal clk      : std_logic := '0';
       signal rst_n    : std_logic := '0';
       signal async_in : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
       signal sync_out : std_logic_vector(WIDTH-1 downto 0);
   begin
       dut : entity work.sync_bank
           generic map (WIDTH => WIDTH)
           port map (clk => clk, rst_n => rst_n, async_in => async_in, sync_out => sync_out);
 
       clk <= not clk after 5 ns;
 
       stim : process
           variable v : unsigned(WIDTH-1 downto 0) := (others => '0');
       begin
           rst_n <= '0'; wait for 20 ns; rst_n <= '1';
           for t in 0 to 7 loop
               v := to_unsigned((t*29 + 7) mod (2**WIDTH), WIDTH);
               async_in <= std_logic_vector(v);
               wait for 30 ns;                       -- past the 2FF latency
               -- Invariant: every lane resolves to its input (independent lanes).
               assert sync_out = std_logic_vector(v)
                   report "sync_bank lane mismatch" severity error;
           end loop;
           async_in <= (others => '0');
           async_in(3) <= '1';                       -- drive only lane 3
           wait for 30 ns;
           assert sync_out = std_logic_vector(to_unsigned(8, WIDTH))
               report "cross-lane bleed" severity error;
           report "sync_bank self-check complete" severity note;
           wait;
       end process;
   end architecture;

4b. An if/case-generate optional feature — a mode-selected output stage

Now conditional inclusion. A datapath produces a combinational result; some configurations must register that result (a pipelined, high-throughput variant), others must pass it combinationally (a low-latency variant). The presence of the output flop is a parameter — MODE = FAST (registered) or SLOW (combinational). An if-generate includes the register stage or not. The load-bearing rule, previewed here and dramatized in §7: both branches must drive dout — the registered branch through a flop, the combinational branch directly — so dout is defined in every configuration.

opt_stage.sv — if-generate includes an output register only in FAST mode
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module opt_stage #(
       parameter int WIDTH = 16,
       parameter bit FAST  = 1'b1                     // 1 = registered (pipelined), 0 = combinational
   )(
       input  logic             clk,
       input  logic             rst_n,
       input  logic [WIDTH-1:0] a, b,
       output logic [WIDTH-1:0] dout                  // MUST be driven in BOTH branches
   );
       logic [WIDTH-1:0] result;
       assign result = a + b;                         // the shared combinational core
 
       generate
           if (FAST) begin : gen_fast
               // FAST: include a pipeline register — dout is the flopped result.
               always_ff @(posedge clk or negedge rst_n)
                   if (!rst_n) dout <= '0;
                   else        dout <= result;
           end else begin : gen_slow
               // SLOW: NO register emitted — dout is combinational. The else branch
               // STILL drives dout (every branch drives every output).
               assign dout = result;
           end
       endgenerate
   endmodule

The if (FAST) decides, at elaboration, whether the flop exists — in SLOW mode the always_ff contributes no hardware at all. The critical discipline is that the else branch is not empty: it drives dout combinationally. Omit that assign and SLOW-mode dout floats (§7). The testbench instantiates the module in each mode and asserts the right behavior for each — FAST: dout equals last cycle's a+b (one-cycle latency); SLOW: dout equals this cycle's a+b (zero latency) — proving both branches were elaborated and both drive dout.

opt_stage_tb.sv — verify BOTH modes: FAST is one-cycle latency, SLOW is combinational
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module opt_stage_tb;
       localparam int WIDTH = 16;
       // Instantiate BOTH configurations so BOTH generate branches are exercised.
       logic clk = 0, rst_n = 0;
       logic [WIDTH-1:0] a, b, dout_fast, dout_slow;
       int   errors = 0;
 
       opt_stage #(.WIDTH(WIDTH), .FAST(1'b1)) dut_fast
           (.clk(clk), .rst_n(rst_n), .a(a), .b(b), .dout(dout_fast));
       opt_stage #(.WIDTH(WIDTH), .FAST(1'b0)) dut_slow
           (.clk(clk), .rst_n(rst_n), .a(a), .b(b), .dout(dout_slow));
 
       always #5 clk = ~clk;
 
       initial begin
           a = 0; b = 0; rst_n = 0; repeat (2) @(posedge clk); rst_n = 1;
           for (int t = 0; t < 8; t++) begin
               logic [WIDTH-1:0] exp_comb;
               @(negedge clk);
               a = $urandom; b = $urandom;
               exp_comb = a + b;
               #1;
               // SLOW: combinational — dout tracks THIS cycle's sum immediately.
               assert (dout_slow === exp_comb)
                   else begin $error("SLOW t=%0d dout=%h exp=%h", t, dout_slow, exp_comb); errors++; end
               @(posedge clk); #1;
               // FAST: registered — dout equals the sum captured at this edge.
               assert (dout_fast === exp_comb)
                   else begin $error("FAST t=%0d dout=%h exp=%h", t, dout_fast, exp_comb); errors++; end
           end
           if (errors == 0) $display("PASS: opt_stage correct in BOTH FAST and SLOW modes");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

The Verilog form uses generate if … else with reg for the flopped output and a wire path for the combinational one; because a signal cannot be both reg and wire, the idiom drives an internal reg dout in FAST and assigns it in SLOW via an output reg that the combinational branch loads in an always @(*) — keeping both branches driving dout.

opt_stage.v — if-generate optional register in Verilog (both branches drive dout)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module opt_stage #(
       parameter WIDTH = 16,
       parameter FAST  = 1                            // 1 = registered, 0 = combinational
   )(
       input  wire             clk,
       input  wire             rst_n,
       input  wire [WIDTH-1:0] a, b,
       output reg  [WIDTH-1:0] dout                   // driven in BOTH branches
   );
       wire [WIDTH-1:0] result = a + b;               // shared core
 
       generate
           if (FAST) begin : gen_fast
               // FAST: pipeline register — dout flopped.
               always @(posedge clk or negedge rst_n)
                   if (!rst_n) dout <= {WIDTH{1'b0}};
                   else        dout <= result;
           end else begin : gen_slow
               // SLOW: no register — dout combinational, but STILL driven.
               always @(*) dout = result;
           end
       endgenerate
   endmodule
opt_stage_tb.v — self-checking; both modes instantiated, each checked
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module opt_stage_tb;
       parameter WIDTH = 16;
       reg  clk, rst_n;
       reg  [WIDTH-1:0] a, b;
       wire [WIDTH-1:0] dout_fast, dout_slow;
       reg  [WIDTH-1:0] exp_comb;
       integer t, errors;
 
       opt_stage #(.WIDTH(WIDTH), .FAST(1)) dut_fast
           (.clk(clk), .rst_n(rst_n), .a(a), .b(b), .dout(dout_fast));
       opt_stage #(.WIDTH(WIDTH), .FAST(0)) dut_slow
           (.clk(clk), .rst_n(rst_n), .a(a), .b(b), .dout(dout_slow));
 
       initial clk = 0;
       always #5 clk = ~clk;
 
       initial begin
           errors = 0; a = 0; b = 0; rst_n = 0;
           repeat (2) @(posedge clk); rst_n = 1;
           for (t = 0; t < 8; t = t + 1) begin
               @(negedge clk);
               a = $random; b = $random;
               exp_comb = a + b;
               #1;
               if (dout_slow !== exp_comb) begin      // SLOW combinational
                   $display("FAIL SLOW t=%0d dout=%h exp=%h", t, dout_slow, exp_comb);
                   errors = errors + 1;
               end
               @(posedge clk); #1;
               if (dout_fast !== exp_comb) begin      // FAST registered
                   $display("FAIL FAST t=%0d dout=%h exp=%h", t, dout_fast, exp_comb);
                   errors = errors + 1;
               end
           end
           if (errors == 0) $display("PASS: opt_stage correct in BOTH modes");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

In VHDL the conditional inclusion is an if … generate with else generate (VHDL-2008), each arm labelled. The registered arm uses a clocked process; the combinational arm a concurrent assignment. Both arms drive dout, so the interface is complete in either configuration.

opt_stage.vhd — if..generate optional register in VHDL (both arms drive dout)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity opt_stage is
       generic (
           WIDTH : positive := 16;
           FAST  : boolean  := true                   -- true = registered, false = combinational
       );
       port (
           clk   : in  std_logic;
           rst_n : in  std_logic;
           a, b  : in  std_logic_vector(WIDTH-1 downto 0);
           dout  : out std_logic_vector(WIDTH-1 downto 0)   -- driven in BOTH arms
       );
   end entity;
 
   architecture rtl of opt_stage is
       signal result : std_logic_vector(WIDTH-1 downto 0);
   begin
       result <= std_logic_vector(unsigned(a) + unsigned(b));   -- shared core
 
       gen_fast : if FAST generate
           -- FAST arm: include a pipeline register.
           process (clk, rst_n)
           begin
               if rst_n = '0' then
                   dout <= (others => '0');
               elsif rising_edge(clk) then
                   dout <= result;
               end if;
           end process;
       else generate
           -- SLOW arm: no register, but STILL drives dout (concurrent assignment).
           dout <= result;
       end generate;
   end architecture;
opt_stage_tb.vhd — self-checking; FAST (registered) and SLOW (combinational) both checked
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity opt_stage_tb is
   end entity;
 
   architecture sim of opt_stage_tb is
       constant WIDTH : positive := 16;
       signal clk   : std_logic := '0';
       signal rst_n : std_logic := '0';
       signal a, b  : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
       signal dout_fast, dout_slow : std_logic_vector(WIDTH-1 downto 0);
   begin
       dut_fast : entity work.opt_stage
           generic map (WIDTH => WIDTH, FAST => true)
           port map (clk => clk, rst_n => rst_n, a => a, b => b, dout => dout_fast);
       dut_slow : entity work.opt_stage
           generic map (WIDTH => WIDTH, FAST => false)
           port map (clk => clk, rst_n => rst_n, a => a, b => b, dout => dout_slow);
 
       clk <= not clk after 5 ns;
 
       stim : process
           variable exp : std_logic_vector(WIDTH-1 downto 0);
       begin
           rst_n <= '0'; wait for 20 ns; rst_n <= '1';
           for t in 0 to 7 loop
               wait until falling_edge(clk);
               a <= std_logic_vector(to_unsigned((t*23+1) mod 2**WIDTH, WIDTH));
               b <= std_logic_vector(to_unsigned((t*11+5) mod 2**WIDTH, WIDTH));
               wait for 1 ns;
               exp := std_logic_vector(unsigned(a) + unsigned(b));
               -- SLOW: combinational — tracks this cycle's sum.
               assert dout_slow = exp report "SLOW mismatch" severity error;
               wait until rising_edge(clk); wait for 1 ns;
               -- FAST: registered — equals the sum captured at the edge.
               assert dout_fast = exp report "FAST mismatch" severity error;
           end loop;
           report "opt_stage self-check complete" severity note;
           wait;
       end process;
   end architecture;

4c. The if-generate-undriven-output bug — buggy vs fixed, in all three HDLs

Pattern (c) is the signature failure, shown here as buggy vs fixed RTL in each language, then dramatized narratively in §7. The bug is the same everywhere: an if/case-generate branch drives an output in the configuration the author verified and forgets to drive it in the other. Here the module has a valid output alongside dout. The FAST branch drives both. The SLOW branch drives dout but forgets valid — so in SLOW mode valid is never assigned and floats: a latch is inferred (or it reads X). The FAST tests all pass; SLOW was never elaborated by the author, so the bug ships.

opt_bug.sv — BUGGY (else branch leaves valid undriven) vs FIXED (both branches drive both)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: the FAST branch drives dout AND valid; the SLOW branch drives only
   //        dout and FORGETS valid → in SLOW mode, valid is never assigned →
   //        latch inferred / X. FAST-mode tests pass; SLOW mode floats.
   module opt_valid_bad #(
       parameter int WIDTH = 16,
       parameter bit FAST  = 1'b1
   )(
       input  logic clk, rst_n,
       input  logic [WIDTH-1:0] a, b,
       output logic [WIDTH-1:0] dout,
       output logic             valid          // must be driven in EVERY config
   );
       logic [WIDTH-1:0] result;
       assign result = a + b;
       generate
           if (FAST) begin : gen_fast
               always_ff @(posedge clk or negedge rst_n)
                   if (!rst_n) begin dout <= '0; valid <= 1'b0; end
                   else        begin dout <= result; valid <= 1'b1; end
           end else begin : gen_slow
               assign dout = result;            // drives dout ...
               // BUG: valid is NEVER driven in this branch → floats in SLOW mode.
           end
       endgenerate
   endmodule
 
   // FIXED: every branch drives EVERY output — give each branch a complete,
   //        consistent interface (or assign a default before the generate).
   module opt_valid_good #(
       parameter int WIDTH = 16,
       parameter bit FAST  = 1'b1
   )(
       input  logic clk, rst_n,
       input  logic [WIDTH-1:0] a, b,
       output logic [WIDTH-1:0] dout,
       output logic             valid
   );
       logic [WIDTH-1:0] result;
       assign result = a + b;
       generate
           if (FAST) begin : gen_fast
               always_ff @(posedge clk or negedge rst_n)
                   if (!rst_n) begin dout <= '0; valid <= 1'b0; end
                   else        begin dout <= result; valid <= 1'b1; end
           end else begin : gen_slow
               assign dout  = result;
               assign valid = 1'b1;             // the missing driver — SLOW is combinational-valid
           end
       endgenerate
   endmodule
opt_bug_tb.sv — instantiate BOTH modes; only sweeping SLOW exposes the floating valid
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module opt_bug_tb;
       localparam int WIDTH = 16;
       logic clk = 0, rst_n = 0;
       logic [WIDTH-1:0] a, b, d_fast, d_slow;
       logic v_fast, v_slow;
       int   errors = 0;
 
       // Point BOTH duts at opt_valid_good to PASS; at _bad to see SLOW's valid float.
       opt_valid_good #(.WIDTH(WIDTH), .FAST(1'b1)) dut_fast
           (.clk(clk), .rst_n(rst_n), .a(a), .b(b), .dout(d_fast), .valid(v_fast));
       opt_valid_good #(.WIDTH(WIDTH), .FAST(1'b0)) dut_slow
           (.clk(clk), .rst_n(rst_n), .a(a), .b(b), .dout(d_slow), .valid(v_slow));
 
       always #5 clk = ~clk;
 
       initial begin
           a = 0; b = 0; rst_n = 0; repeat (2) @(posedge clk); rst_n = 1;
           for (int t = 0; t < 6; t++) begin
               @(negedge clk); a = $urandom; b = $urandom; #1;
               // The check the FAST-only author skipped: valid must be DEFINED in SLOW too.
               assert (v_slow === 1'b1)
                   else begin $error("SLOW valid floats: v_slow=%b", v_slow); errors++; end
               @(posedge clk); #1;
               assert (v_fast === 1'b1)
                   else begin $error("FAST valid=%b", v_fast); errors++; end
           end
           if (errors == 0) $display("PASS: valid driven in BOTH FAST and SLOW configurations");
           else             $display("FAIL: %0d (a generate branch left valid undriven)", errors);
           $finish;
       end
   endmodule

The Verilog buggy/fixed pair is the same story: the SLOW branch drives dout and omits valid, so valid (an output reg) is never loaded in that configuration and infers a latch. Instantiating and sweeping the SLOW mode is what exposes it — the FAST-only test the author ran never touched it.

opt_bug.v — BUGGY (SLOW omits valid) vs FIXED (every branch drives every output)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: SLOW branch forgets valid → latch inferred on valid in SLOW mode.
   module opt_valid_bad #(
       parameter WIDTH = 16,
       parameter FAST  = 1
   )(
       input  wire clk, rst_n,
       input  wire [WIDTH-1:0] a, b,
       output reg  [WIDTH-1:0] dout,
       output reg              valid
   );
       wire [WIDTH-1:0] result = a + b;
       generate
           if (FAST) begin : gen_fast
               always @(posedge clk or negedge rst_n)
                   if (!rst_n) begin dout <= {WIDTH{1'b0}}; valid <= 1'b0; end
                   else        begin dout <= result;       valid <= 1'b1; end
           end else begin : gen_slow
               always @(*) dout = result;       // drives dout ...
               // BUG: valid never assigned here → latch in SLOW.
           end
       endgenerate
   endmodule
 
   // FIXED: both branches drive both outputs.
   module opt_valid_good #(
       parameter WIDTH = 16,
       parameter FAST  = 1
   )(
       input  wire clk, rst_n,
       input  wire [WIDTH-1:0] a, b,
       output reg  [WIDTH-1:0] dout,
       output reg              valid
   );
       wire [WIDTH-1:0] result = a + b;
       generate
           if (FAST) begin : gen_fast
               always @(posedge clk or negedge rst_n)
                   if (!rst_n) begin dout <= {WIDTH{1'b0}}; valid <= 1'b0; end
                   else        begin dout <= result;       valid <= 1'b1; end
           end else begin : gen_slow
               always @(*) begin
                   dout  = result;
                   valid = 1'b1;                 // the missing driver
               end
           end
       endgenerate
   endmodule
opt_bug_tb.v — self-checking; sweeping SLOW catches the undriven valid
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module opt_bug_tb;
       parameter WIDTH = 16;
       reg  clk, rst_n;
       reg  [WIDTH-1:0] a, b;
       wire [WIDTH-1:0] d_fast, d_slow;
       wire v_fast, v_slow;
       integer t, errors;
 
       // Bind to _good to PASS; to _bad to observe valid float in SLOW.
       opt_valid_good #(.WIDTH(WIDTH), .FAST(1)) dut_fast
           (.clk(clk), .rst_n(rst_n), .a(a), .b(b), .dout(d_fast), .valid(v_fast));
       opt_valid_good #(.WIDTH(WIDTH), .FAST(0)) dut_slow
           (.clk(clk), .rst_n(rst_n), .a(a), .b(b), .dout(d_slow), .valid(v_slow));
 
       initial clk = 0;
       always #5 clk = ~clk;
 
       initial begin
           errors = 0; a = 0; b = 0; rst_n = 0;
           repeat (2) @(posedge clk); rst_n = 1;
           for (t = 0; t < 6; t = t + 1) begin
               @(negedge clk); a = $random; b = $random; #1;
               if (v_slow !== 1'b1) begin           // SLOW valid must be defined
                   $display("FAIL: SLOW valid floats v_slow=%b", v_slow);
                   errors = errors + 1;
               end
               @(posedge clk); #1;
               if (v_fast !== 1'b1) begin
                   $display("FAIL: FAST valid=%b", v_fast);
                   errors = errors + 1;
               end
           end
           if (errors == 0) $display("PASS: valid driven in both configs");
           else             $display("FAIL: %0d mismatches (undriven generate branch)", errors);
           $finish;
       end
   endmodule

In VHDL the same bug appears when one generate arm assigns an output and the other omits it. Because a combinational process (or a missing concurrent assignment) that never drives valid in the SLOW arm leaves it unresolved, the fix is identical: every arm must drive every output — give each arm a complete interface, or default valid before the generate.

opt_bug.vhd — BUGGY (else arm omits valid) vs FIXED (both arms drive both)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   -- BUGGY: the else arm drives dout but NOT valid → valid undriven in SLOW.
   entity opt_valid_bad is
       generic ( WIDTH : positive := 16; FAST : boolean := true );
       port (
           clk, rst_n : in  std_logic;
           a, b       : in  std_logic_vector(WIDTH-1 downto 0);
           dout       : out std_logic_vector(WIDTH-1 downto 0);
           valid      : out std_logic
       );
   end entity;
   architecture rtl of opt_valid_bad is
       signal result : std_logic_vector(WIDTH-1 downto 0);
   begin
       result <= std_logic_vector(unsigned(a) + unsigned(b));
       gen_fast : if FAST generate
           process (clk, rst_n) begin
               if rst_n = '0' then dout <= (others => '0'); valid <= '0';
               elsif rising_edge(clk) then dout <= result; valid <= '1';
               end if;
           end process;
       else generate
           dout <= result;                          -- drives dout ...
           -- BUG: valid is never driven in this arm → undriven in SLOW.
       end generate;
   end architecture;
 
   -- FIXED: every arm drives every output.
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
   entity opt_valid_good is
       generic ( WIDTH : positive := 16; FAST : boolean := true );
       port (
           clk, rst_n : in  std_logic;
           a, b       : in  std_logic_vector(WIDTH-1 downto 0);
           dout       : out std_logic_vector(WIDTH-1 downto 0);
           valid      : out std_logic
       );
   end entity;
   architecture rtl of opt_valid_good is
       signal result : std_logic_vector(WIDTH-1 downto 0);
   begin
       result <= std_logic_vector(unsigned(a) + unsigned(b));
       gen_fast : if FAST generate
           process (clk, rst_n) begin
               if rst_n = '0' then dout <= (others => '0'); valid <= '0';
               elsif rising_edge(clk) then dout <= result; valid <= '1';
               end if;
           end process;
       else generate
           dout  <= result;
           valid <= '1';                            -- the missing driver
       end generate;
   end architecture;
opt_bug_tb.vhd — self-checking; sweep SLOW arm, assert valid is defined
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity opt_bug_tb is
   end entity;
 
   architecture sim of opt_bug_tb is
       constant WIDTH : positive := 16;
       signal clk   : std_logic := '0';
       signal rst_n : std_logic := '0';
       signal a, b  : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
       signal d_fast, d_slow : std_logic_vector(WIDTH-1 downto 0);
       signal v_fast, v_slow : std_logic;
   begin
       -- Bind to opt_valid_good to PASS; to _bad to observe valid undriven in SLOW.
       dut_fast : entity work.opt_valid_good
           generic map (WIDTH => WIDTH, FAST => true)
           port map (clk => clk, rst_n => rst_n, a => a, b => b, dout => d_fast, valid => v_fast);
       dut_slow : entity work.opt_valid_good
           generic map (WIDTH => WIDTH, FAST => false)
           port map (clk => clk, rst_n => rst_n, a => a, b => b, dout => d_slow, valid => v_slow);
 
       clk <= not clk after 5 ns;
 
       stim : process
       begin
           rst_n <= '0'; wait for 20 ns; rst_n <= '1';
           for t in 0 to 5 loop
               wait until falling_edge(clk);
               a <= std_logic_vector(to_unsigned((t*17+3) mod 2**WIDTH, WIDTH));
               b <= std_logic_vector(to_unsigned((t*9+1)  mod 2**WIDTH, WIDTH));
               wait for 1 ns;
               -- The check the FAST-only author skipped: valid defined in SLOW too.
               assert v_slow = '1' report "SLOW valid undriven" severity error;
               wait until rising_edge(clk); wait for 1 ns;
               assert v_fast = '1' report "FAST valid wrong" severity error;
           end loop;
           report "opt_bug self-check complete" severity note;
           wait;
       end process;
   end architecture;

Across all three languages the lesson is one sentence: a for-generate range must cover exactly the intended slices, and every if/case-generate branch must drive every output — the branch you did not elaborate is the branch you did not test.

5. Verification Strategy

A generated design has a correctness question that a fixed design does not: is it correct at every parameter and mode setting, not just the one the author happened to elaborate? Generate resolves at compile time, so a branch you never elaborated is a branch the simulator never saw. The single discipline that captures a correct generated design is:

The design is correct only if it is verified generic — re-elaborated and checked at multiple N/WIDTH values and in every if/case-generate mode — because each parameter setting is, in effect, a different netlist.

Everything below is that discipline, made checkable, and it maps directly onto the testbenches in §4.

  • Multiple-N sweep for the for-generate (self-checking). Re-elaborate the replicated structure at N = 1, 4, 16 (and, where relevant, a non-power-of-two like N = 3) and re-run the same self-checking testbench each time. N = 1 is the corner that exposes a range that assumes N > 1; a large N exposes an off-by-one that leaves the top or bottom slice unconnected. The sync_bank testbenches in §4a do exactly this — drive all lanes, assert every lane resolves, then drive a single lane to confirm the generated instances are independent (no cross-lane bleed from an aliased index).
  • The replication invariant. Stated as a property that must hold every run: lane i reads exactly din[i] and drives exactly dout[i], and there are exactly N lanes — no more, no fewer. Its violation is an off-by-one in the generate range (a missed top/bottom slice, or an over-instantiated aliased lane), and the single-lane directed test is what surfaces it.
  • Every-mode coverage for the if/case-generate. Instantiate the module once per mode (FAST and SLOW; or every MODE value of a case-generate) in the same testbench and assert the correct behavior in each — one-cycle latency in the registered mode, zero latency in the combinational mode. This is the check that catches pattern (c): the §4c testbenches instantiate both modes and assert valid is defined in both, so the SLOW-mode floating output the FAST-only author missed is caught. Verifying only the mode you built is the exact hole the bug lives in.
  • Undriven-output detection. For every if/case-generate output, in every branch, assert the output is a defined 0/1 (never X/Z) at the point it should be valid. A branch that forgets to drive an output shows up as X (or a latch that holds) in that configuration; the assert (v_slow === 1'b1) / assert v_slow = '1' checks are precisely this. Lint reinforces it: an "output undriven in configuration" or "latch inferred" warning on a generate branch is the static signature of the same bug.
  • Hierarchical-name existence. Because generated instances are named (gen_lane[i].u_sync), verification and constraints depend on those names existing. Confirm the hierarchy by probing or force-checking a generated instance by path, and by driving each lane independently (as the directed single-lane test does) — if the label is missing or the genvar mis-scoped, the per-instance path does not resolve and the independence check fails.
  • Expected waveform. For the replicated bank, a correct run shows each lane's output following its own input after the fixed structural latency (two destination-clock edges for the synchronizer), with no lane tracking another lane's data. For the if-generate, the FAST instance's output lags its input by one clock while the SLOW instance's tracks combinationally — and a floating valid in the untested mode shows as a red X on the waveform in that configuration, the visual signature of the §7 bug.

6. Common Mistakes

for-generate off-by-one / wrong range. The signature replication error. for (i = 1; i < N; i++) silently drops lane 0; for (i = 0; i <= N; i++) over-instantiates and either fails to elaborate (index out of range) or, worse, aliases an extra lane onto a valid signal. The range must cover exactly 0 … N-1. Verify it by sweeping N (especially N = 1, where an off-by-one is most visible) and by driving each lane independently to confirm there are exactly N distinct, correctly-connected slices.

if/case-generate that leaves an output undriven in one configuration. The signature failure of this page (full post-mortem in §7; buggy-vs-fixed RTL in §4c). A branch drives an output in the configuration the author verified and forgets it in another; that output floats — a latch is inferred or it reads X — only in the mode nobody elaborated. The fix is a discipline: every branch drives every output. Give each branch a complete, consistent interface, or assign a safe default to every output before the generate so no branch can leave one undriven.

Confusing a generate-for (structural) with a procedural for (behavioral). Using a generate for when you meant "describe one block's behavior over its bits" needlessly creates N named instances (and can misbehave if the body was written as if it were procedural); using a procedural for inside an always block when you needed N addressable instances (for per-instance constraints or attributes, like the synchronizer's ASYNC_REG) leaves you with one block you cannot target per-lane. Choose by intent: N instances → generate-for; one block described compactly → procedural for.

Forgetting the named generate block. Omitting the : gen_lbl label (or the VHDL generate label) leaves the replicated instances with tool-assigned, non-portable names, so gen_lbl[i].u_slice — the path that timing constraints, waveform probes, and hierarchical assertions reference — does not exist. Always name generate blocks; the name is the address of the hardware, not decoration.

genvar reused or mis-scoped. A genvar belongs to its generate loop and is resolved at elaboration. Reusing one genvar across two loops, or referencing it outside its generate scope, is a compile-time error or a silently wrong index. Declare a genvar per loop (or reuse deliberately and only within valid scope), and never treat it as a runtime variable — it does not exist at simulation time.

Assuming a parameter default is enough. A generated design that is only ever elaborated at its default parameters is only ever tested there. A range or branch that is wrong at N = 1, at a non-power-of-two N, or in the non-default mode ships because it was never elaborated. Verify generic: sweep N and exercise every mode (§5) — the setting you skip is the netlist you did not build.

7. DebugLab

The SLOW-mode block that shipped with a floating valid — the branch nobody elaborated

The engineering lesson: generate builds hardware conditionally at elaboration, so a branch you did not elaborate is a branch you did not test — and an if/case-generate branch that forgets to drive an output floats that output in the configuration nobody built. The tell is a bug that appears only in one configuration ("perfect in FAST, valid floats in SLOW"): configuration-specific failure in a parameterized block means a branch presents a different interface than its sibling. Two habits make it impossible, and they are identical across SystemVerilog, Verilog, and VHDL: every for-generate range must cover exactly the intended slices, and every if/case-generate branch must drive every output (a complete interface per branch, or a default before the generate); and verify at multiple parameter and mode settings, because the netlist you did not elaborate is the netlist you did not verify.

8. Interview Q&A

9. Exercises

Design and reasoning exercises — no syntax drills. Each rehearses the "am I replicating structure or describing one block, and does every branch drive every output?" habit.

Exercise 1 — Generate-for or procedural-for?

For each task, state whether you would use a for-generate (structural, N named instances) or a procedural for (behavioral, one block over its bits), and why in one line: (a) instantiate one two-flop synchronizer per bit of a WIDTH-bit control bus, each needing a per-instance ASYNC_REG attribute; (b) compute the bitwise parity of a WIDTH-bit word inside one combinational block; (c) build an N-slice ripple-carry adder whose slices chain carry-to-carry; (d) zero-extend or sign-extend a vector inside one always @(*). (Hint: do you need N addressable instances, or one block described compactly?)

Exercise 2 — Find the off-by-one before it elaborates

An engineer writes a replicated N-lane structure as generate for (i = 1; i <= N; i = i + 1) begin : gen_lane … lane[i] … end. State (i) exactly which lane is dropped and which index is out of range, (ii) at what value of N the bug is most visible in a directed test and why, and (iii) the two testbench checks (a multiple-N sweep and a single-lane directed drive) that would have caught it. Then give the corrected range.

Exercise 3 — Complete every branch's interface

A configurable block has outputs dout, valid, and err. Its FAST if-generate branch drives all three; its SLOW branch drives dout and valid but not err. Describe (i) what happens to err in the SLOW configuration and how it manifests in simulation vs on an FPGA, (ii) why the FAST regression never caught it, and (iii) two distinct fixes — one that completes the SLOW branch's interface, one that defaults all outputs before the generate — and the trade-off between them. State the single verification change that guarantees this class of bug is caught.

Exercise 4 — Pick the implementation by mode

You must build a datapath adder that maps to a small ripple adder in an area-critical MODE, a carry-lookahead adder in a speed-critical MODE, and a DSP-mapped add in a third. Describe how a case-generate selects among the three at elaboration, what each unselected arm contributes to the netlist, the one interface rule every arm must obey, and how you would verify all three configurations from a single self-checking testbench without editing the design between runs.

Continue in RTL Design Patterns (forward links unlock as they ship):

  • Parameter Patterns — Chapter 12.1; the direct prerequisite — how a parameter names a family of designs; generate is what turns that parameter into the actual, named hardware.
  • Parameterized FIFO / Memory — Chapter 12.3 (this batch); a depth- and width-generic buffer whose replicated storage and mode-selected read timing are pure generate.
  • Width-Generic Datapath — Chapter 12; a datapath whose slice count and optional stages are generate-built end to end.
  • Configurable IP with Assertions — Chapter 12; guarding parameter/mode combinations and asserting every generated configuration's contract.

In-track patterns this one replicates:

  • The Two-Flop Synchronizer — Chapter 11.2; the per-bit structure this page replicates into a WIDTH-bit synchronizer bank with a for-generate.
  • Adders & Subtractors — Chapter; the bit-sliced, carry-chained datapath that a for-generate builds slice-by-slice through a carry array.
  • Shift Registers — Chapter; a chain where each stage feeds the next — the canonical generate-connected pipeline.
  • Register Files — Chapter; a bank of registers, replicated per entry, with parameterized depth and width.
  • Multiplexers (2:1, N:1, one-hot) — Chapter 1.1; the N-input mux whose AND-OR is naturally generate-built, and where the same latch-from-incomplete-assignment lesson first appears.

Backward / method:

  • The RTL Design Mindset — Chapter 0.2; the Interface → State → Datapath → Control → Verification lenses — generate is how you make the datapath parameter-generic at the structural level.
  • What Is an RTL Design Pattern? — Chapter 0.1; why replication earns the name "pattern" — a recurring need, a reusable form, a synthesis reality, and a signature failure.

Verilog v1 prerequisites (the grammar this pattern transcribes into):

  • Generate For — the for-generate construct behind the replicated structure, and where the genvar and named block live.
  • Generate If — the if-generate construct behind the optional-feature form, and where conditional inclusion is taught.
  • Generate Case — the case-generate that picks one of several implementations by a parameter.
  • Generate Blocks — the elaboration-time generate mechanics and named-block scoping this page builds on.
  • Parameters — the parameters (N, WIDTH, MODE) that drive every generate on this page.
  • For Loops — the procedural for that this page contrasts with the structural generate-for.

11. Summary

  • generate builds hardware at elaboration time; it is not a runtime loop. A parameter names a family of designs; generate reads the parameter before simulation and stamps out the actual, named hardware — replicating structure or including/excluding it. What the clock later drives is the fixed netlist generate emitted.
  • for-generate replicates STRUCTURE; a procedural for describes BEHAVIOR. generate for (genvar i…) begin : gen_lbl creates N separate, named instances (gen_lbl[i].u_slice) — a bit-sliced datapath, a bank of synchronizers, a register file — each addressable for per-instance constraints and attributes. A for inside one always block describes that one block's logic compactly. Choose by intent: N instances vs one block over its bits.
  • if/case-generate includes or excludes hardware by a parameter. if (COND) generate … else generate and case-generate decide, once, which hardware exists; the unselected branch contributes zero gates. It is how one file becomes several distinct designs — an optional pipeline stage, a mode-selected implementation.
  • Named blocks are the address of the hardware, and slices connect through arrays. The : gen_lbl label (VHDL generate label) gives replicated instances the hierarchical names that constraints, probes, and assertions reference; chained slices wire together through a genvar-indexed signal array (a carry chain declared one element longer than the slice count). Drop the label and per-instance references break.
  • Every for-generate range must cover exactly the intended slices, and every if/case-generate branch must drive every output — verify generic. An off-by-one range drops or aliases a lane; a branch that forgets an output floats it in the configuration nobody elaborated (the §7 latch-in-SLOW-mode bug). Sweep N = 1, 4, 16 and exercise every mode with a self-checking testbench — shown here in SystemVerilog, Verilog, and VHDL — because the netlist you did not elaborate is the netlist you did not test.