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、总结
- parameter一般推荐在端口定义,而不是模块内定义,这样源代码及被调用代码看起来都比较直观
- localparam无法在被调用时更改参数值,只能通过修改子模块源代码的方式修改,适用于固定的本地常量,一般是提高代码的维护性及可读性
- `define作用于整个工程,可以跨文件使用,一般使用一个参数文件(头文件)统一预定义所有被2个或2个以上module调用的参数,方便管理(作用类似C语言头文件)
6、其他
- 创作不易,如果本文对您有帮助,还请多多点赞、评论和收藏。您的支持是我持续更新的最大动力!
- 关于本文,您有什么想法均可在评论区留言交流。
- 自身能力不足,如有错误还请多多指出!