verilog-parametros-modulos-genericos

Parameters in Verilog: Generic and Configurable Modules

  • 5 min

A parameter is an elaboration-time constant that allows you to configure the structure of a module before synthesizing it.

Imagine this situation: You’ve designed a perfect 8-bit counter. It works great. Ten minutes later, you realize you need a 16-bit counter for another part of the project.

What do you do? Do you copy the counter_8bit.v file, paste it, rename it to counter_16bit.v, and manually change all the [7:0] to [15:0]?

Wrong! That’s a maintenance nightmare. If you later find a bug, you’ll have to fix it in 20 different files.

In object-oriented programming, there are “Templates” or “Generics.” In Verilog, we have Parameters.

Today we’re going to learn how to write reusable code, creating elastic modules that adapt to what we need when we instantiate them.

What is a parameter?

A Parameter is a constant that is local to a module, but can be modified when instantiating that module from the outside.

It’s important to understand the difference with an input (input):

  • Input: It’s a signal that changes while the circuit is running (Runtime). E.g., A pressed button.
  • Parameter: It’s a value defined before manufacturing the circuit (Synthesis time). It defines the physical structure of the chip. Once synthesized, it cannot be changed.

Think of parameters like the arguments of a class constructor. They define how the object is born.

Defining a generic module

To use parameters, we add a special #( ... ) section before defining the module ports.

Let’s create the famous N-bit Counter.

module generic_counter #(
    // Define the parameters and their DEFAULT values
    parameter BITS = 8  // If you don't tell me, I'll be 8 bits
)(
    input wire clk,
    input wire rst,
    output wire [BITS-1 : 0] count // The width depends on the parameter
);

    reg [BITS-1 : 0] internal_register;

    always @(posedge clk) begin
        if (rst) 
            internal_register <= 0;
        else 
            internal_register <= internal_register + 1;
    end

    assign count = internal_register;

endmodule
Copied!

Notice the line output wire [BITS-1 : 0]. If BITS is 8, Verilog will interpret [7:0]. If BITS is 32, Verilog will interpret [31:0].

The bus width is parametric: it is fixed for each instance during elaboration, before generating the hardware.

Configuring each instance

Now, from our top_system, we can create copies of this counter with different sizes. We use the syntax #( .PARAMETER(value) ).

module top_level (
    input wire clk,
    input wire rst,
    output wire [7:0] leds,
    output wire slow_output
);

    // INSTANCE 1: Using the default value (8 bits)
    generic_counter small_counter (
        .clk(clk),
        .rst(rst),
        .count(leds) // Fits with 8-bit output
    );

    // INSTANCE 2: Overriding the parameter to 32 bits
    wire [31:0] big_count;
    
    generic_counter #(
        .BITS(32) // <--- Here we configure this specific instance
    ) huge_counter (
        .clk(clk),
        .rst(rst),
        .count(big_count)
    );

    assign slow_output = big_count[31];

endmodule
Copied!

The synthesizer is smart enough to physically create two different circuits: one small with 8 flip-flops and another large with 32 flip-flops, both generated from the same source code.

parameter vs localparam

Sometimes we need constants that help make the code readable (like FSM states: IDLE, RUN…), but we do not want anyone to change them from the outside. They are private constants.

For that, we have localparam.

  • parameter: Public. Modifiable at instantiation.
  • localparam: Private. Fixed within the module.
module my_state_machine #(
    parameter BUS_WIDTH = 8 // Configurable
)(
    input clk
);

    // Internal constants (No one can change them from outside)
    localparam STATE_IDLE = 0;
    localparam STATE_RUN  = 1;
    
    // ...
endmodule
Copied!

Calculations with parameters

The truly powerful thing about parameters is that we can use mathematical formulas.

Imagine you want to make a Frequency Divider (Prescaler) to get 1 Hz. Normally you calculate by hand: “I have 12 MHz, I need to count to 6 million… that’s 23 bits…”.

What if you change to a 50 MHz board? You have to recalculate everything. Better let Verilog calculate for us.

We’ll use the system function $clog2() (Ceiling Logarithm base 2), which tells us how many bits are needed to represent a number.

module strobe_generator #(
    parameter INPUT_FREQUENCY = 12000000, // 12 MHz by default
    parameter OUTPUT_FREQUENCY  = 1       // 1 Hz by default
)(
    input wire clk,
    input wire rst,
    output reg strobe
);

    // 1. Calculate the count top automatically
    localparam MAX_COUNT = (INPUT_FREQUENCY / OUTPUT_FREQUENCY);

    // 2. Calculate the required bits automatically
    // If MAX_COUNT is 12,000,000, $clog2 will return 24.
    localparam N_BITS = (MAX_COUNT <= 1) ? 1 : $clog2(MAX_COUNT);

    reg [N_BITS-1 : 0] counter = 0;

    always @(posedge clk) begin
        if (rst) begin
            counter <= 0;
            strobe <= 0;
        end else begin
            strobe <= 0;
            if (counter == MAX_COUNT - 1) begin
                counter <= 0;
                strobe <= 1;
            end else begin
                counter <= counter + 1;
            end
        end
    end

endmodule
Copied!

Now, instantiating a 9600 baud generator for a UART is as easy as:

strobe_generator #(
    .INPUT_FREQUENCY(12000000),
    .OUTPUT_FREQUENCY(9600)
) baud_gen ( ... );
Copied!

If the frequencies are not evenly divisible, integer division introduces a small error. Check that it stays within the protocol’s tolerance.

Functions like $clog2 or divisions in parameters do not consume resources on the FPGA. The computer calculates them once during compilation and “pastes” the constant result into the design.