Menu

Verilog 参数:让模块可配置

如何用 parameter 和 localparam 定义编译期常量、把位宽和深度参数化,并在实例化模块时覆盖这些值。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

会变的常量

parameter 是一个常量:编译器在仿真开始之前就确定它的值,并把它固化进设计里。特别之处在于,实例化这个模块的人可以修改这个值。同一个模块源文件,根据调用方怎样设置参数,可以生成大小各不相同的电路。

可复用的 IP 就是这样做出来的。一个带 WIDTH 和 DEPTH 参数的 FIFO 模块可以服务公司里的每一个团队。一个带 WIDTH 参数的计数器,不管是数到 16 还是数到 40 亿,用的都是同一份源代码。

基本的参数声明

这里有三处新语法:

  1. 参数块:位于模块名和端口列表之间的 #( parameter WIDTH = 8 )。默认值 8 就是没人覆盖时模块使用的值。
  2. 使用处:output reg [WIDTH-1:0] count。参数只是一个常量,我们把它填进位范围里。同一份模块源代码,根据 WIDTH 的不同,输出 8 位或 16 位。
  3. 覆盖语法:实例化时写 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 模块库就是这样写出来的。

Coddy 编程语言插图

和 Coddy 一起学编程

开始