【Verilog】参数化设计指令

1. 参数化设计

我到现在仍然记得多年前第一次学习C语言的宏定义时,课本上所举的例子:#define PI 3.14159。
当时内心只有1个想法:挖槽,这TM真的好方便啊!!!假设我要写一个涉及到圆或者球的函数时,那么这个圆周率π我是肯定要用很多次的啊。先不说我每次使用时能不能记住π约等于3.14159这个数,就是多输入几次同样的数据,我整个人也烦躁不。更不用说,如果把π的精度提高,比如从3.14159改成3.1415926585,那么我不是在函数中每个使用到的地方都要改?这工作量不巨大?这谁能忍?
所以这样看来C语言的宏定义这种东西用起来还真的是挺方便的(缺点就不谈了,毕竟小白),一个是可维护性强,另一个就是能增加代码的可读性(一串数字你不知道是啥,一个英文单词你还能不知道是啥吗?)。那么在FPGA设计的Verilog语言中有没有类似的操作?嘿嘿,当然有啦!
在Verilog的设计中,我们一般使用 parameter 、localparam 和`define这三种方法来实现常量的参数化设计(可以理解为宏定义)。三种方法在使用上有一些小区别,接下来通过一些场景来详细说明。

2. Parameter

Parameter一般适用于被调用的子模块,将子模块的特定常量参数化,即可实现定制子模块。

2.1 维护简单

假设有如下的module,实现对a,b,c三个变量赋值同样的数100

module test(
        input        clk
);
 
wire          a,b,c;
 
assign        a = 100;
assign        b = 100;
assign        z = 100;
 
endmodule

我们不先考虑这个module的现实性,只看这个模块中写起来不方便的地方:需要对3个wire变量赋值同一个数100。假设以后维护的时候,需要把这个100改成200,那么就需要同时修改3个地方,3个变量还好,工作量也不是很大嘛。那假如有100个变量都用到这个值,那么就需要改100个地方,那手不得改断?
所以我们可以考虑使用 parameter 参数来把100这个常量参数化,如下:

module test(
        input        clk
);
 
parameter        NUM = 100;        //可以制定位宽,或者编译器直接分配位宽
wire            a,b,c;
 
assign        a = NUM ;
assign        b = NUM ;
assign        c = NUM ;
 
endmodule

这样设计的话,下次需要改动100这个常量为200,就只要改一行代码了:

 parameter        NUM = 200; 

2.2 易理解

假如你现在需要写一个模块,需要实现的功能:实现3个计时器,第一个1s,第二个0.1s,第三个0.01s。如下:

module test(
	input	clk		,		//假设时钟50M
	input	rst_n			//低电平有效的同步复位
);
 
 
always@(posedge clk)begin
	if(!rst_n)
		cnt1 <= 0;
	else if(cnt1 == 50_000_000 - 1)	
        cnt1 <= 0;
    else
		cnt1 <= cnt1 + 1;
end
 
always@(posedge clk)begin
	if(!rst_n)
		cnt2 <= 0;
	else if(cnt2 == 5_000_000 - 1)
        cnt2 <= 0;	
    else
		cnt2 <= cnt2 + 1;
end
 
always@(posedge clk)begin
	if(!rst_n)
		cnt3 <= 0;
	else if(cnt3 == 500_000 - 1)
        cnt3 <= 0;
    else	
		cnt3 <= cnt3 + 1;
end
 
endmodule

可以看到其中出现了三个数字:50000000、5000000、500000,分别是当前时钟下计数1s\0.1s\0.01s所需要的最大时钟个数。实际上,忽略注释的话这看起来并不方便。我们现在使用parameter来将这三个数字参数化:

module test(
	input	clk		,				//假设时钟50M
	input	rst_n					//低电平有效的同步复位
);
 
parameter	CNT1_MAX = 'd50_000_000;	  //位宽可以自己分配,或者编译器自动分配
parameter	CNT2_MAX = 'd5_000_000;       //位宽可以自己分配,或者编译器自动分配
parameter	CNT3_MAX = 'd500_000;         //位宽可以自己分配,或者编译器自动分配
 
always@(posedge clk)begin
	if(!rst_n)
		cnt1 <= 0;
	else if(cnt1 == CNT1_MAX - 1)	
        cnt1 <= 0;
    else
		cnt1 <= cnt1 + 1;
end
 
always@(posedge clk)begin
	if(!rst_n)
		cnt2 <= 0;
	else if(cnt2 == CNT2_MAX - 1)	
        cnt2 <= 0;
    else
		cnt2 <= cnt1 + 1;
end
 
always@(posedge clk)begin
	if(!rst_n)
		cnt3 <= 0;
	else if(cnt3 == CNT3_MAX - 1)	
        cnt3 <= 0;
    else
		cnt3 <= cnt1 + 1;
end
 
endmodule

我们分别使用CNT1_MAX、CNT2_MAX、CNT3_MAX这3个参数来代替三个计时器的最大计数上限,从名字上就能看出这个参数是要干什么的,有效地提高了我们程序的可读性。

2.3 复用方便 

假设现在需要写一个module A,其中需要使用到2个串口做接收(其中1个波特率115200,另一个波特率4800)。幸好我以前写过串口接收模块,那么直接调用不就好啦!
哎,不对哦,怎么波特率是115200,我之前这个模块是波特率9600的啊?没事,之前的文件我改一下就是的,给它改成波特率115200。
哎,不对,怎么要两个串口,波特率还不一样啊?没事,我把之前的串口接收模块文件复制一份,再把波特率改成4800。这样我在module A分别例化两个串口接受模块就好了啊!嘿嘿不愧是我。
这种方法当然可以实现功能,就是开发效率低下,用起来麻烦点罢了。有没有高效点的办法?
当然有咯!把之前写好的串口接收子模块,改一下,预留一个波特率参数到module外:

//串口接收模块
module uart_rx
#(
    parameter			BPS = 'd9_600				//发送波特率
)    
(    
//系统接口
    input				sys_clk			,            //50M系统时钟
    input				sys_rst_n		,            //系统复位
//UART接收线    
    input				uart_rxd		,            //接收数据线
//用户接口    
    output reg			uart_rx_done	,            //数据接收完成标志,当其为高电平时,代表接收数据有效
    output reg [7:0]	uart_rx_data                 //接收到的数据,在uart_rx_done为高电平时有效
);
 
//---------------具体代码省略--------------------------
 
endmodule

接着调用这个子模块两次,两次的参数分别设置需要的参数就可以了:

module test(
	input	clk		,				//假设时钟50M
	input	rst_n					//低电平有效的同步复位
	//---------------具体代码省略--------------------------
);
 
parameter	BPS1 = 'd115200;	//串口接收模块1的波特率
parameter	BPS2 = 'd4800;		//串口接收模块2的波特率
 
//---------------具体代码省略--------------------------
 
//例化第1个串口接收模块,波特率:115200
uart_rx
#(
    .BPS				(BPS1			)			
)    
uart_rx_inst1(    
    .sys_clk			(sys_clk		),    
    .sys_rst_n			(sys_rst_n		),    
    .uart_rxd			(uart_rxd1		),    
    .uart_rx_done		(uart_rx_done1	),    
    .uart_rx_data    	(uart_rx_data1	)     
);
 
//例化第2个串口接收模块,波特率:4800
uart_rx
#(
    .BPS				(BPS2			)			
)    
uart_rx_inst2(    
    .sys_clk			(sys_clk		),    
    .sys_rst_n			(sys_rst_n		),    
    .uart_rxd			(uart_rxd2		),    
    .uart_rx_done		(uart_rx_done2	),    
    .uart_rx_data    	(uart_rx_data2	)     
);
 
endmodule

  可以看到通过例化2个串口接收子模块来实现需要的功能。第1个例化将波特率设为115200, 第2个例化将波特率设为4800。

2.4 使用方法 

parameter参数的实现可以在module内部,如下:

//串口接收模块
module uart_rx    
(    
    input				sys_clk			,            //50M系统时钟
    input				sys_rst_n		,            //系统复位  
    input				uart_rxd		,            //接收数据线    
    output reg			uart_rx_done	,            //数据接收完成标志,当其为高电平时,代表接收数据有效
    output reg [7:0]	uart_rx_data                 //接收到的数据,在uart_rx_done为高电平时有效
);
 
//---------------具体代码省略--------------------------
 
parameter	BPS = 'd9_600;							//发送波特率
parameter	CLK = 'd50_000_000;						//系统时钟频率
 
endmodule

也可以放在端口,如下:

//串口接收模块
module uart_rx #(
	parameter	BPS = 'd9_600		,						//发送波特率
	parameter	CLK = 'd50_000_000	,						//系统时钟频率
)    
(    
    input				sys_clk			,            //50M系统时钟
    input				sys_rst_n		,            //系统复位  
    input				uart_rxd		,            //接收数据线    
    output reg			uart_rx_done	,            //数据接收完成标志,当其为高电平时,代表接收数据有效
    output reg [7:0]	uart_rx_data                 //接收到的数据,在uart_rx_done为高电平时有效
);
 
//---------------具体代码省略--------------------------
 
endmodule

但是不能一部分放在端口,另一部分放在模块内部,这样编译器会报错(好像是警告,会导致模块内的parameter无法被重定义,即当作一个本地参数)。

2.5 调用方法

可以使用2种方法调用含有parameter参数的子模块。

方法1:如果参数的定义是在模块内部,则使用defparam来对参数进行重定义:

module test(
	input	clk		,				//假设时钟50M
	input	rst_n					//低电平有效的同步复位
	//---------------具体代码省略--------------------------
);
 
parameter	BPS1 = 'd115200;	//串口接收模块1的波特率
parameter	BPS2 = 'd4800;		//串口接收模块2的波特率
 
//---------------具体代码省略--------------------------
 
//例化第1个串口接收模块,波特率:115200
defparam	uart_rx_inst1.BPS = BPS1;			//.表示一种层级结构,即被例化模块uart_rx_inst1中的BPS参数
 
uart_rx	uart_rx_inst1(    
    .sys_clk			(sys_clk		),    
    .sys_rst_n			(sys_rst_n		),    
    .uart_rxd			(uart_rxd1		),    
    .uart_rx_done		(uart_rx_done1	),    
    .uart_rx_data    	(uart_rx_data1	)     
);
 
//例化第2个串口接收模块,波特率:4800
defparam	uart_rx_inst2.BPS = BPS2;			//.表示一种层级结构,即被例化模块uart_rx_inst1中的BPS参数
 
uart_rx	uart_rx_inst2(        
    .sys_clk			(sys_clk		),    
    .sys_rst_n			(sys_rst_n		),    
    .uart_rxd			(uart_rxd2		),    
    .uart_rx_done		(uart_rx_done2	),    
    .uart_rx_data    	(uart_rx_data2	)     
);
 
endmodule

方法2:参数定义在端口,则参数的重定义类似信号的例化:

module test(
	input	clk		,				//假设时钟50M
	input	rst_n					//低电平有效的同步复位
	//---------------具体代码省略--------------------------
);
 
parameter	BPS1 = 'd115200;	//串口接收模块1的波特率
parameter	BPS2 = 'd4800;		//串口接收模块2的波特率
 
//---------------具体代码省略--------------------------
 
//例化第1个串口接收模块,波特率:115200
uart_rx
#(
    .BPS				(BPS1			)			
)    
uart_rx_inst1(    
    .sys_clk			(sys_clk		),    
    .sys_rst_n			(sys_rst_n		),    
    .uart_rxd			(uart_rxd1		),    
    .uart_rx_done		(uart_rx_done1	),    
    .uart_rx_data    	(uart_rx_data1	)     
);
 
//例化第2个串口接收模块,波特率:4800
uart_rx
#(
    .BPS				(BPS2			)			
)    
uart_rx_inst2(    
    .sys_clk			(sys_clk		),    
    .sys_rst_n			(sys_rst_n		),    
    .uart_rxd			(uart_rxd2		),    
    .uart_rx_done		(uart_rx_done2	),    
    .uart_rx_data    	(uart_rx_data2	)     
);
 
endmodule

3. localparam

参数的声明方式不仅有parameter这种方式,还有一种localparam的语法,不过它当然与parameter有所区别:localparam模块内有效的定义,是局部变量,不可用于参数传递,也不能被重定义。

那么适用于哪一类使用场景?当模块内的常量希望被参数化,但是又不需要被其他模块重定义时。

比如状态机的状态变量,状态机动辄数个状态,若全部使用parameter表示,则被外部调用时又需要重定义一次,属实是增加无效工作量。所以我们可以使用localparam来定义状态机的状态变量:

module test(
	input	clk		,				//假设时钟50M
	input	rst_n					//低电平有效的同步复位
	//---------------具体代码省略--------------------------
);
 
localparam	BPS1  = 3'b001;		//初始状态
localparam	WRITE = 3'b010;		//写状态
localparam	READ  = 3'b100;		//读状态
 
//---------------具体代码省略--------------------------
 
endmodule

或者在模块存在数个指令时,可使用localparam来提高可读性:

module test(
	input	clk		,				//假设时钟50M
	input	rst_n					//低电平有效的同步复位
	//---------------具体代码省略--------------------------
);
 
localparam	RESET = 4'b0001;		//复位命令
localparam	WRITE = 4'b0010;		//写命令
localparam	READ  = 4'b0100;		//读命令
localparam	SEND  = 4'b1000;		//发送命令
 
//---------------具体代码省略--------------------------
 
always@(posedge clk)begin
	if(!rst_n)
		cmd <= RESET;				//复位时执行复位命令
	else
		cmd <= SEND;				//否则执行发送命令
end
 
endmodule

4. `define

`define实质上是一条编译指令,功能使用文本宏来替代常量,用法类似C语言的define。一般适用于跨文件调用,当然也可以只在本地文件使用(不过这种情况我一般推荐使用localparam)。

在稍微复杂一点的FPGA工程设计中,难免会出现数个module,而我们一般是推荐一个module使用一个文件管理,所以一般的FPGA工程会有数个文件。假设工程中存在大量参数是多个module都需要使用到的,比如VGA时序相关参数。这样我们就可以单独使用一个文件(命名为:vga_para.v)来定义这些参数:
 

//行同步参数定义
`define		H_SYNC 		10'd96 		//行同步
`define		H_BACK 		10'd40 		//行时序后沿
`define		H_LEFT 		10'd8		//行时序左边框
`define		H_VALID		10'd640		//行有效数据
`define		H_RIGHT		10'd8 		//行时序右边框
`define		H_FRONT		10'd8 		//行时序前沿
`define		H_TOTAL		10'd800		//行扫描周期

然后假设某个模块需要使用这些参数时,需要使用该指令:`include "vga_para.v"   将参数文件调用,然后就可以直接使用了(需要注意的是,使用时需要加`,如:不能使用H_SYNC,而是要用`H_SYNC

5. 总结

  1. parameter一般推荐在端口定义,而不是模块内定义,这样源代码及被调用代码看起来都比较直观
  2. localparam无法在被调用时更改参数值,只能通过修改子模块源代码的方式修改,适用于固定的本地常量,一般是提高代码的维护性及可读性
  3. `define作用于整个工程,可以跨文件使用,一般使用一个参数文件(头文件)统一预定义所有被2个或2个以上module调用的参数,方便管理(作用类似C语言头文件)
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值