Module 7: Advanced RTL Design Techniques ยท 7.1 Parameterized & Scalable Modules

generate if and generate case

In the previous article a parameter changed the width of a module. In this one a parameter chooses between two different circuits.

You know generate for from Module 5: it stamps out N copies of the same structure. Sometimes you do not want N copies of the same thing. You want one parameter to decide between, say, a ripple-carry adder for small widths and a faster structure for large ones, chosen automatically at instantiation. That is what generate if and generate case do. Same keyword, but instead of repeating structure they select it.

generate if: a fork decided at elaboration

module adder_auto #(parameter WIDTH = 8) (
    input  wire [WIDTH-1:0] a, b,
    input  wire             cin,
    output wire [WIDTH-1:0] sum,
    output wire             cout
);
    generate
        if (WIDTH <= 8) begin : gen_ripple
            // Small width: plain ripple carry is fine, minimal area
            assign {cout, sum} = a + b + cin;
        end else begin : gen_fast
            // Large width: split into two halves for a carry-select style speedup
            wire [WIDTH/2-1:0] lower_sum;
            wire                lower_cout;
            wire [WIDTH/2-1:0] upper_sum0, upper_sum1;
            wire                upper_cout0, upper_cout1;

            assign {lower_cout, lower_sum} = a[WIDTH/2-1:0] + b[WIDTH/2-1:0] + cin;
            assign {upper_cout0, upper_sum0} = a[WIDTH-1:WIDTH/2] + b[WIDTH-1:WIDTH/2] + 1'b0;
            assign {upper_cout1, upper_sum1} = a[WIDTH-1:WIDTH/2] + b[WIDTH-1:WIDTH/2] + 1'b1;

            assign sum  = {lower_cout ? upper_sum1 : upper_sum0, lower_sum};
            assign cout = lower_cout ? upper_cout1 : upper_cout0;
        end
    endgenerate
endmodule

Instantiate this at WIDTH = 4 and only the gen_ripple branch exists in the netlist. The whole gen_fast branch is dropped before synthesis starts, as if you had never written it. At WIDTH = 32 it is the other way round. The decision is made at elaboration, from the parameter value alone, so there is no multiplexer, no runtime check and no gates spent on the branch you did not take.

This is worth holding onto: if inside an always block builds a mux, because the condition can change at runtime. generate if builds nothing for the untaken branch, because the condition is fixed when the module is instantiated and can never change afterwards.

The block names matter here too

As with generate for in Module 5, the : gen_ripple and : gen_fast labels give each branch a hierarchical name. When you are debugging and need to look at a signal inside whichever branch was elaborated, the path looks like this:

tb.u_dut.gen_fast.upper_sum0

If the instance took the gen_ripple branch, that path does not exist in the simulation hierarchy at all, which is itself a handy check that the branch you expected got selected.

generate case: more than two options

With three or more structural variants, generate case reads better than a chain of generate if/else if:

module encoder_selectable #(parameter STYLE = "PRIORITY") (
    input  wire [7:0] req,
    output reg  [2:0] enc,
    output reg        valid
);
    generate
        case (STYLE)
            "PRIORITY": begin : gen_priority
                always @(*) begin
                    enc = 3'd0; valid = 1'b0;
                    if      (req[7]) begin enc = 3'd7; valid = 1'b1; end
                    else if (req[6]) begin enc = 3'd6; valid = 1'b1; end
                    else if (req[5]) begin enc = 3'd5; valid = 1'b1; end
                    // ... remaining bits omitted for brevity
                    else if (req[0]) begin enc = 3'd0; valid = 1'b1; end
                end
            end
            "ONEHOT": begin : gen_onehot
                always @(*) begin
                    enc = 3'd0; valid = |req;
                    case (1'b1)
                        req[0]: enc = 3'd0;
                        req[1]: enc = 3'd1;
                        req[2]: enc = 3'd2;
                        req[3]: enc = 3'd3;
                        req[4]: enc = 3'd4;
                        req[5]: enc = 3'd5;
                        req[6]: enc = 3'd6;
                        req[7]: enc = 3'd7;
                        default: ;
                    endcase
                end
            end
            default: begin : gen_invalid
                // Unrecognized STYLE parameter: fail loudly at elaboration
                initial begin
                    $display("ERROR: encoder_selectable STYLE=%s is not a recognized style", STYLE);
                    $finish;
                end
            end
        endcase
    endgenerate
endmodule

The parameter here is a string rather than a number. Verilog allows this, and at the instantiation site encoder_selectable #(.STYLE("ONEHOT")) u_enc (...) tells you what you are getting without looking up what STYLE = 1 or STYLE = 2 means.

The default branch is not dead code, it is a guard. If someone instantiates the module with a typo such as STYLE = "PRIORTY", the default branch catches it and fails at elaboration with a clear message, rather than silently generating no logic and leaving you to find the problem in simulation, or after tape-out.

When to use it

generate if and generate case earn their place in one situation: when a parameter should produce different hardware structures, not different widths of the same structure. The usual cases are a small-versus-large-width architecture switch like the adder above, a debug-versus-production build where assertion and monitor logic is compiled in only for simulation, and an FPGA-versus-ASIC target switch where one platform gets a vendor primitive and the other gets a generic RTL fallback. Vendor IP does this constantly: a memory controller might use generate case on a TARGET_DEVICE parameter to instantiate a hard block on one branch and portable RTL on another, behind the same interface.

Practice Exercise

Write a parameterized shifter module with a MODE string parameter that selects between "LOGICAL" (vacated bits filled with 0) and "ARITHMETIC" (vacated bits filled with the sign bit) right shift, using generate case. Include a default branch that fails at elaboration if MODE is not recognized.

Solution
`timescale 1ns / 1ps
`default_nettype none

module shifter #(parameter WIDTH = 8, parameter MODE = "LOGICAL") (
    input  wire [WIDTH-1:0] data_in,
    input  wire [$clog2(WIDTH)-1:0] shamt,
    output wire [WIDTH-1:0] data_out
);
    generate
        case (MODE)
            "LOGICAL": begin : gen_logical
                assign data_out = data_in >> shamt;
            end
            "ARITHMETIC": begin : gen_arithmetic
                assign data_out = $signed(data_in) >>> shamt;
            end
            default: begin : gen_invalid
                initial begin
                    $display("ERROR: shifter MODE=%s is not recognized", MODE);
                    $finish;
                end
            end
        endcase
    endgenerate
endmodule

Instantiate shifter #(.WIDTH(8), .MODE("ARITHMETIC")) and only the gen_arithmetic branch, a signed >>> shift, exists in the elaborated design. With .MODE("LOGICAL") you get the plain >> branch. Misspell the mode string and elaboration stops with a message instead of building nothing.

That is the structural side of parameterization: generate if and generate case pick a structure once, at elaboration, and the branch not taken costs nothing. In the next article we put all of this to work on something bigger than an adder, a parameterized memory model that we will reuse for the rest of the module.

โ† Why Parameterize Everything A Parameterized Memory Model โ†’