会变的常量
parameter 是一个常量:编译器在仿真开始之前就确定它的值,并把它固化进设计里。特别之处在于,实例化这个模块的人可以修改这个值。同一个模块源文件,根据调用方怎样设置参数,可以生成大小各不相同的电路。
可复用的 IP 就是这样做出来的。一个带 WIDTH 和 DEPTH 参数的 FIFO 模块可以服务公司里的每一个团队。一个带 WIDTH 参数的计数器,不管是数到 16 还是数到 40 亿,用的都是同一份源代码。
基本的参数声明
这里有三处新语法:
- 参数块:位于模块名和端口列表之间的
#( parameter WIDTH = 8 )。默认值8就是没人覆盖时模块使用的值。 - 使用处:
output reg [WIDTH-1:0] count。参数只是一个常量,我们把它填进位范围里。同一份模块源代码,根据WIDTH的不同,输出 8 位或 16 位。 - 覆盖语法:实例化时写
counter #(.WIDTH(16)) dut16(...)。#(...)块放在模块名之后、实例名之前。没有提到的参数保持默认值。
localparam:内部常量
有些常量你不希望调用方覆盖,状态编码就是典型例子:
localparam 让 IDLE、RUNNING、DONE 在模块内部可用,但无法在实例化时被覆盖。这样做是对的:如果模块外部能改掉 IDLE 对应的状态值,那会非常危险。
需要调用方配置的东西(位宽、深度、行为选项)用 parameter,其他一律用 localparam。一种常见写法是从 parameter 派生出 localparam:
parameter WIDTH = 32;
localparam WIDTH_M1 = WIDTH - 1; // computed once; not overrideable
实战中的参数化位宽
参数最常见的用途是让总线位宽变得灵活。下面是一个适用于任意位宽的加法器:
一个源文件,两个实例,两种不同的位宽,完全不用复制粘贴。这就是参数带来的全部好处。
多个参数与按名覆盖
较大的模块往往有好几个参数。可以按名称、以任意顺序覆盖它们:
module fifo #(
parameter WIDTH = 8,
parameter DEPTH = 16,
parameter AFULL = DEPTH - 2
)(
input wire clk,
input wire reset,
// ... ports ...
);
// ...
endmodule
// At the call site:
fifo #(.WIDTH(32), .DEPTH(1024)) cmd_queue (.clk(clk), .reset(reset), ...);
不必覆盖每一个参数,没有提到的保持默认值。示例中 AFULL 的默认值是由 DEPTH 算出来的,这意味着只要覆盖了 DEPTH,AFULL 就会自动跟着变。如果你不希望调用方能单独覆盖 AFULL,同样的依赖关系也可以交给 localparam 来处理。
常见错误
覆盖时漏写 #。 counter (.WIDTH(16)) dut(...) 看起来像是覆盖参数,但 Verilog 会把 (.WIDTH(16)) 当成端口连接。正确写法是 counter #(.WIDTH(16)) dut(...)。
在不够“常量”的地方使用参数。 参数在展开(elaboration)阶段就已确定,那时任何信号都还不存在。你不能根据运行时的信号来设置参数:如果某个值取决于仿真时输入的状态,那它就不是参数,而是逻辑。
在端口列表里混淆 parameter 和 localparam。 只有 parameter 能放进顶部的 #(...) 块,localparam 写在模块体内部。如果写反了,编译器会提示你。
接下来
现在你已经能写出位宽在编译时确定的模块了。接下来的两篇文档会讲字面常量的写法(8'h1F、4'b1010、32'd100),以及信号没有驱动或驱动冲突时出现的 x 和 z 值。之后我们进入运算符:对你已经见过的向量和参数能做的一切运算。
常见问题
Verilog 中的 parameter 是什么?
parameter 是在模块内部声明的编译期常量。凡是允许使用常量的地方都能用它:总线位宽、存储器深度、状态编码、默认值。关键在于,模块的每个实例都可以覆盖这个参数,所以同一个源文件可以在一处生成 8 位计数器,在另一处生成 32 位计数器。
Verilog 中 parameter 和 localparam 有什么区别?
parameter 的值可以在实例化模块时从外部覆盖,localparam 的值不行,它只是内部常量。状态编码,以及模块作者不希望调用方随意改动的派生常量,都应该用 localparam。
Verilog 中如何覆盖参数?
实例化模块时,在实例名前面加上参数覆盖块:counter #(.WIDTH(16)) my_inst (.clk(clk), .count(count));。.WIDTH(16) 这种写法只设置指定的那个参数,没有列出的参数保持默认值。多个覆盖项在 #(...) 里用逗号分隔。
什么是参数化模块?
就是对外提供一个或多个可由调用方覆盖的 parameter 声明的模块。一个参数化的 FIFO 可以有 WIDTH 和 DEPTH 两个参数,于是同一份源代码在一处生成 32 位宽、16 深的 FIFO,在另一处生成 8 位宽、1024 深的 FIFO。可复用的 IP 模块库就是这样写出来的。