Parameterized Verilog Modules

Creating more generic and reusable modules

32410

Introduction

When designing verilog modules, you can add instantiation parameters. These allow the module to be customized when it is instantiated, allowing you to create more reusable code.

Defining Parameterized Modules

Take the example of a register comprised of multiple D-FlipFlops. In some cases, a designiner might need a 32-bit register, a 96-bit register, or even a massive 512-bit register. Instead of writing modules for each different size of register required, in verilog the designer can write a single register module and configure the size when the module is instantiated.

The code shown below is for a 8-bit register with write enable:

module register_8_bit 
(
	input clk, rst, wen,
	input [7:0] D,
	output [7:0] Q
);

	reg [7:0] val;
	
	assign Q = val;
	
	always @(posedge clk)
	begin
		if(rst)
			val<=0;
		else if(wen) //if write enabled
			val<=D;
	end


endmodule

A Parameterized Version

To parameterize this design so it can be used for any size of register it can be modified into the following:

module register #(parameter WIDTH = 8)
(
	input clk, rst, wen,
	input [WIDTH-1:0] D,
	output [WIDTH-1:0] Q
);

	reg [WIDTH-1:0] val;

	assign Q = val;

	always@(posedge clk)
	begin
		if(rst)
			val<=0;
		else if(wen)
			val<=D;
	end

	
	

endmodule


When defining a parameterized module, the entires within the parenthesis preceded by a pound sign becomes the parameter list. Typically parameters should be given names with all caps, as this makes it easy to distinguish which variables in the module definition are parameter and which are not. All parameters must be also given a default value, this is so if a module is instantiated without changing a given parameter, there is a fallback value. In the example above, the default size for the register is defined to be 8 bits.

The general form for a parameterized module definition is shown below:

module an_example #(parameter PAR_A, PAR_B, PAR_C)
(
	input port_in,
	output port_out
);
	//module body
endmodule

In the module, anywhere a parameter variable is used, it will be replaced with the passed value for that variable. Note that for this module, the bus width of D, Q, and val are given as [WIDTH-1:0].

Instantiating Parameterized Modules

A parameterized module can be configured by passing values when instantiating the module; After the module name but before the instance name, place a pound-sign followed by a pair of parenthesis. Here the parameters of a module can be changed from their defaults. It is best practice to set parameters by using name reference, this also allows you to only change the parameters you need to. As with assigning ports by name, in the parameter list precede the parameter’s name with a period and place its value in the parenthesis following the name. The value given must be constant, it can’t be assigned from another type as the value cannot be known at synthesis time. Following the instance name, the port list can be defined as per usual instantiation.

The general format for setting parameters when instantiating a module is below

A_module #( .A_PARAMETER(a_constant_value), .ANOTHER_PARAMETER(7) ) an_instance_name
(
	.a_port(a_wire),
	.another_port(another_wire_or_constant)
)

Instantiation Examples

Below are some examples of instantiating the register modules defined earlier.

module wrap_registers
(
	input clk,
	input rst,
	input [63:0] data_in
);

	//NOTE, the data output of all the registers below
	//is left unconnected for this example

	register_8_bit A_rg
	(
		.clk(clk),
		.rst(rst),
		.wen(1'b1),
		.D(data_in[7:0]),
		.Q()
	);

	//this synthesizes exactly the same as A_rg
	register #( .WIDTH(8) ) B_rg
	(
		.clk(clk),
		.rst(rst),
		.wen(1'b1),
		.D(data_in[7:0]),
		.Q()
	);
	
	//this also synthesizes exactly the same as A_rg and B_rg
	//because the default width is 8
	register C_rg
	(
		.clk(clk),
		.rst(rst),
		.wen(1'b1),
		.D(data_in[7:0]),
		.Q()
	);

	//this creates a 64-bit register
	register #( .WIDTH(64) ) D_rg
	(
		.clk(clk),
		.rst(rst),
		.wen(1'b1),
		.D(data_in),
		.Q()
	);

endmodule

Using Parameters with Generate Statements

The following module is a parameterized shift register with both parameters for bit-width (the width of each register) and depth (number of register stages). It illustrates a more complex use of parameters, as well as being used in conjunction with the generate statement. The use of generate statements and parameters can make very flexible and large circuits with a small amount of code, however care must be taken when using either and especially when using both. Look at the example module and notice the the index calculations for the register interconnects, one small error and the module may fail to synthesize, or make incorrect connections.

module shift_reg #(parameter WIDTH = 8, DEPTH = 8)
(
	input clk, rst, en,
	input [WIDTH-1:0] shift_in,
	output [WIDTH-1:0] shift_out
);

	//interconnect between stages
	//need WIDTH*(DEPTH-1) wires in-between stages and 2*WIDTH for input and output
	wire [(1+DEPTH)*WIDTH-1:0] con;
	
	//shift in input
	assign con[WIDTH-1:0] = shift_in;
	
	//shift out output
	assign shift_out = con[(1+DEPTH)*WIDTH-1 -:WIDTH];
	
	
	//generate DEPTH number of registers which are WIDTH wide
	//connect inputs and output to correct indicies of interconnect
	genvar j;
	generate
		for(j=1;j<= DEPTH;j=j+1)
		begin: shift_rg
			register #(.WIDTH(WIDTH)) inst
			(
				.clk(clk),
				.rst(rst),
				.wen(en),
				.D(con[WIDTH*j-1 -:WIDTH]),
				.Q(con[WIDTH*(j+1)-1 -:WIDTH])
			);
		end
	endgenerate
	
endmodule

Instantiation Examples

Examples of instantiating the given shift register module with different parameters is shown below.

module shift_wrap
(
	input clk, rst,
	input [63:0] data_in
);

	//Using default parameters, 8 deep and 8 wide
	shift_reg exA
	(
		.clk(clk),
		.rst(rst),
		.shift_in(data_in[7:0]),
		.shift_out()
	);
	
	//Makes same as above, explicitly defined parameters
	shift_reg #( .WIDTH(8), .DEPTH(8) ) exB
	(
		.clk(clk),
		.rst(rst),
		.shift_in(data_in[7:0]),
		.shift_out()
	);

	//input/output wires and internal registers are 32-bit wide
	//since DEPTH not specifed, takes default value of 8-registers deep
	shift_reg #( .WIDTH(32) ) exC
	(
		.clk(clk),
		.rst(rst),
		.shift_in(data_in[31:0]),
		.shift_out()
	);
	
	//width is default value of 8-bits
	//4 registers in the pipeline
	shift_reg #( .DEPTH(4) ) exD
	(
		.clk(clk),
		.rst(rst),
		.shift_in(data_in[7:0]),
		.shift_out()
	);
	
	//24-wide and 22 deep
	shift_reg #(.WIDTH(24), .DEPTH(22) ) exE
	(
		.clk(clk),
		.rst(rst),
		.shift_in(data_in[23:0]),
		.shift_out()
	);


endmodule

Using Parameters as constant values

The previous examples showed the use of parameters to set the width of a bus and the depth of a shift register. Often you may want to use a parameter to set a constant value that affects the behavior of your module. Some examples are a comparator with one input as a constant value, a counter that rolls over at a value that is not a power of 2, or a register that resets to a specific value. It’s worth remembering that parameters are NOT inputs to your modules; They are constant values passed prior to synthesis and used to determine the resulting circuit.

A Note on Bit-Width

When using parameters as values, bit-widths must be considered; If a constant is being assigned to a wire that has too few bits to contain it, it will be truncated. It is often best to consider the possibility of wildly different values being passed as parameters when a module is instantiated.

Take the example of the module defined below. It is a comparator which compares the single input to the constant parameter. It is written in such a way that the input value can only be 8-bits wide; If the constant value is greater than 255, the ‘less-than’ input will always be high.

module const_comp #(parameter COMP_VAL = 0)
(
	input [7:0] val_in,
	output gt,lt,eq
);
	
	assign gt = val_in>COMP_VAL;
	assign lt = val_in<COMP_VAL;
	assign eq = val_in==COMP_VAL;

endmodule

A couple strategies to account for the possible bit-width of parameters are discussed below.

Adding a ‘Width’ Parameter

The simplest method is to add an additional parameter stating the width required in the module. This places the responsibility on the user who instantiates the module to provide the necessary width for correct operation.

module const_comp #(parameter COMP_VAL = 0, WIDTH = 8)
(
	input [WIDTH-1:0] val_in,
	output gt,lt,eq
);
	
	assign gt = val_in>COMP_VAL;
	assign lt = val_in<COMP_VAL;
	assign eq = val_in==COMP_VAL;

endmodule

Using $clog2 to determine width during synthesis

Another way is to calculate the neccessary size when synthesizing the module. For our example, we can use the ceiling of log2(COMP_VAL). Many synthesis tools support the function $clog2() to facilitate this.

module const_comp #(parameter COMP_VAL = 0)
(
	input [$clog2(COMP_VAL)-1:0] val_in,
	output gt,lt,eq
);
	
	assign gt = val_in>COMP_VAL;
	assign lt = val_in<COMP_VAL;
	assign eq = val_in==COMP_VAL;

endmodule

Using clog2 works well when you need to calulate the bit-width required to hold/compare a parameter. However as always, when using arithmetic operations consider bit growth and possible truncation of the result.