When designing RTL, there are many times when you need modules that perform similar functions but differ only in bit width or pipeline depth.
The worst habit at this point is hard coding, which involves copying existing code and simply changing file names and numbers. This approach makes maintenance a nightmare as the project grows, because making a single change requires tracking down and modifying all ten copied files.
In this article, we will learn how to design reusable IP by utilizing the parameterand generate block, a powerful feature of Verilog called “code that writes code.”
1. Types of constants: parameter vs `define vs localparam
There are several ways to define unchanging values (constants), but for reusability, the scope must be clearly distinguished.
① `define (Global Macro)
- Characteristic: Similar to #define in C. It is a preprocessor at compile time.
- Disadvantage: It applies globally. If a name defined in project_A overlaps with a name defined in project_B, a conflict will occur.
- Usage: Restricted to constants shared across the chip (e.g., chip ID, global address map). Never use for internal module settings.
② parameter (Interface Constant)
- Characteristic: The value can be changed externally when instantiating a module.
- Usage: This is the key to reusability. It is used to externally determine the "personality" of a module, such as bit width and FIFO depth.
③ localparam (Internal Constant)
- Characteristic: Used only within the module and cannot be changed from outside.
- Usage: Used to define secondary constants (e.g. state values of a State Machine) calculated based on parameters.
// Good example: flexible design with parameters
module generic_fifo #(
parameter DATA_WIDTH = 32, // External settings possible
parameter FIFO_DEPTH = 1024, // External settings possible
// localparam is automatically calculated by parameter (cannot be modified externally)
localparam ADDR_WIDTH = $clog2(FIFO_DEPTH)
) (
input wire [DATA_WIDTH-1:0] wdata,
...
);2. RTL Generate statement: ‘Generate’ hardware
The generate block dynamically generates code during Elaboration Time (pre-synthesis). Unlike software's if and for statements, it doesn't loop during execution, but rather copies or erases circuits.
① generate for: Create a repeating structure
Use it when you want to copy the same circuit N times. It reduces 100 lines of code by hand to 3 lines.
② generate if / case: Create a conditional circuit
Depending on parameter values, specific circuits can be included or removed entirely. This optimizes area by removing unnecessary logic during the synthesis stage.
3. Practical Pattern ①: N-Stage Pipeline (Generate For)
Sometimes data needs to move far, so we need to put in three pipeline registers, and sometimes we need to put in five. This is the code that controls this with parameters.
module delay_line #(
parameter WIDTH = 8,
parameter DEPTH = 3 // Pipeline number
) (
input wire clk,
input wire [WIDTH-1:0] d_in,
output wire [WIDTH-1:0] d_out
);
// Declare registers as a two-dimensional array
reg [WIDTH-1:0] pipeline [0:DEPTH-1];
genvar i; // Declare loop variable for generate
generate
for (i = 0; i < DEPTH; i = i + 1) begin : gen_pipe
always @(posedge clk) begin
if (i == 0)
pipeline[i] <= d_in; // First stage
else
pipeline[i] <= pipeline[i-1]; // Remaining stages
end
end
endgenerate
assign d_out = pipeline[DEPTH-1];
endmodule- Analysis: If DEPTH is set to 5, the synthesis tool will automatically unroll and implement five register blocks named gen_pipe[0] to gen_pipe[4].
4. Practical Pattern ②: Feature Toggle (Generate If)
When designing IP, some clients want a CRC check function, while others request it be omitted, citing a smaller footprint. Instead of creating two lines of code, we can solve this problem with generate if.
module uart_tx #(
parameter ENABLE_CRC = 1 // 1: Include CRC, 0: Remove CRC
) (
input wire clk,
input wire [7:0] tx_data,
output wire tx_out
);
wire [7:0] final_data;
generate
if (ENABLE_CRC == 1) begin : gen_crc_logic
// Instantiating the CRC module
wire [7:0] crc_val;
crc_calc u_crc ( .data(tx_data), .crc(crc_val) );
// Add CRC to the end of the data (example)
assign final_data = tx_data ^ crc_val;
end else begin : gen_no_crc
// CRC logic is not synthesized at all (area 0)
assign final_data = tx_data;
end
endgenerate
// ... UART Transmit Logic using final_data ...
endmodule- Key: If ENABLE_CRC is 0, the u_crc module disappears from the netlist entirely. This allows for efficient gate count management.
5. Tip: Using the $clog2 System Function
The most challenging part of parameterized design is calculating the address bit width. A depth of 1024 requires 10 bits, and a depth of 2048 requires 11 bits. True automation requires a formula to handle this calculation, rather than relying on human intervention.
// Supports Verilog-2005 and later
parameter DEPTH = 512;
localparam ADDR_WIDTH = $clog2(DEPTH); // log2(512) = 9 automatically calculatedCaution: It is well supported by modern tools such as Vivado, but it may not be supported by very old legacy tools, so you need to check.
6. Conclusion: Lazy engineers are great engineers.
Great RTL engineers are "lazy." They hate writing the same code twice, so they create perfectly parameterized modules, even if it means a little more effort the first time.
- Parameters make the size and depth of modules flexible.
- Automate repetitive coding with generate for.
- Generate if cleanly removes unnecessary logic.
The modules you create will become your own powerful asset that you can use in Project A, Project B, and even 10 years from now.
References: chipverify