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;
endmoduleNotice 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];
endmoduleThe 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;
// ...
endmoduleCalculations 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
endmoduleNow, instantiating a 9600 baud generator for a UART is as easy as:
strobe_generator #(
.INPUT_FREQUENCY(12000000),
.OUTPUT_FREQUENCY(9600)
) baud_gen ( ... );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.