模块中的模块
一个 Verilog 设计是一棵模块树。顶层模块(你的测试平台,或者芯片的顶层封装)实例化下一层的模块,这些模块又实例化更下一层的模块,一直到厂商提供的门级原语。实例化就是表达这种嵌套的语法。
你已经见过它的样子了,我们在 你的第一个模块 中用过:
and_gate dut(.a(a), .b(b), .y(y));
这一行生成了一个 and_gate 实例,把它命名为 dut,并把它的端口连接到本地信号上。我们逐一拆解每个部分。
实例化的结构
module_name instance_name (port_connections);
module_name必须和项目中某处module声明的名字一致。Verilog 区分大小写。instance_name是你起的标签,通常描述这个实例所扮演的角色。在层次路径和波形视图中都会用到它。port_connections把实例的端口连到本地信号上。有两种写法。
按名称连接端口(推荐)
按名称连接的写法如下:
my_module instance_name(
.clk (clk),
.reset (reset_n),
.data_in(in_bus),
.data_out(out_bus),
.valid (out_valid)
);
每一对 .port(signal) 的意思是“把这个实例中名为 port 的端口连接到名为 signal 的本地信号”。顺序无关紧要。如果你给模块声明新增了一个端口,只要给新端口设置默认值,或者更新每个实例化的地方,已有的实例化就不会出问题。
两条实用提示:
- 端口名(括号左边)必须和模块的声明完全一致。
- 信号名(括号里面)属于实例化所在的作用域,通常是父模块。
如果某个端口不连接,就让括号里空着:.optional_port()。在实例内部,这个信号处于悬空状态(z)。少数综合工具会给出警告,大多数都接受。
按位置连接端口(避免使用)
更简短的写法按端口列表的顺序列出信号:
my_module instance_name(clk, reset_n, in_bus, out_bus, out_valid);
这样写更短,但很脆弱。一旦调整了模块的端口列表顺序(重构时确实会发生),所有按位置连接的实例化都会悄无声息地连错线。除非端口列表只有一两个成员,并且几乎不可能改变,否则不要用按位置连接。
仍然可以接受的场景:端口顺序本身就是接口一部分的小型工具模块。一个双输入门按位置连接没问题;一个有 30 个端口的存储器控制器这样写就是在自找麻烦。
完整的层次化示例
这是一个真正的三层层次结构:test → full_adder → 两个 half_adder 实例。每个实例都有自己的一份 half_adder 内部门电路;综合工具会为每次实例化生成一个电路。
同一模块的多个实例
当你把同一个模块实例化多次时,每个实例都是独立的硬件。它们不共享状态,也不共享门电路。可以把每个实例想象成从设计中复制出来的一份全新副本。
adder add0(.a(a0), .b(b0), .sum(s0));
adder add1(.a(a1), .b(b1), .sum(s1));
adder add2(.a(a2), .b(b2), .sum(s2));
adder add3(.a(a3), .b(b3), .sum(s3));
这是四个并行运行的独立加法器。如果 adder 中包含一个寄存器,每个实例都会有自己的一份寄存器和自己的状态。
generate 循环:批量生成重复硬件
手写四个实例还行,手写 64 个就太繁琐了。generate 块可以让细化器替你完成这些重复输入:
这里有三处新语法:
genvar i声明一个可在generate中使用的循环变量。它不是运行时信号,只存在于细化阶段。generate ... endgenerate把循环包起来。有些工具接受不写generate关键字的 generate 循环,但写上它能让意图一目了然。begin : invert_loop给 generate 作用域加上标签。这个标签会成为每个生成实例的层次名的一部分(dut.invert_loop[0].u_inv、dut.invert_loop[1].u_inv等)。
综合器会展开循环,生成 WIDTH 个 bit_inverter 副本。每个副本都是独立的硬件。
实例化时覆盖参数
如果模块有参数,可以在模块名和实例名之间用 #(.PARAM(value)) 覆盖它们:
counter #(.WIDTH(16)) c16 (.clk(clk), .count(out16));
counter #(.WIDTH(32)) c32 (.clk(clk), .count(out32));
两个实例使用同一份 counter 源码,但位宽不同。我们在 参数 中讲过这种语法,它能顺畅地用在实例化中。
层次名
有了层次结构之后,每个信号都有一条层次路径:
test.dut.ha0.sum
读作:在 test 模块中,dut 实例内部的 ha0 实例里,名为 sum 的信号。你会在波形查看器、错误信息中看到这样的路径,偶尔也会在测试平台里用 $display 深入访问某个子模块时用到:
$display("internal carry1 = %b", dut.carry1);
这样的层次引用只用于测试平台和调试:可综合的 RTL 不会去访问其他模块的内部。
常见错误
端口名不匹配。 .clk_in(clk) 把本地的 clk 连到名为 clk_in 的端口。如果模块的端口实际叫 clk,解析器会报错(有些工具的提示比其他工具更清楚)。
端口位宽不匹配。 把 4 位信号连到 8 位端口,会悄悄地做零扩展;反过来则会悄悄地截断。大多数工具会给出警告;如果你没看到警告,就再仔细找找。
忘了参数前的 #。 counter (.WIDTH(8)) c(.clk(clk)) 看起来像是参数覆盖,但其实不是:解析器会试图把 (.WIDTH(8)) 当作端口连接,然后报错。正确写法是:counter #(.WIDTH(8)) c(.clk(clk))。
重复使用实例名。 同一个作用域中的两个实例不能同名。错误信息通常很清楚;让你栽跟头的是复制粘贴的诱惑。
接下来
现在你可以把模块连接成真正的层次结构了。下一篇文档 连续赋值 补全 Verilog 的结构化部分,深入讲解从第一章起我们就一直在随手使用的 assign 语句。
常见问题
Verilog 中如何实例化一个模块?
先写模块名,再写实例名,然后是一个括号括起来的端口连接列表:my_module instance_name(.port(signal), ...);。最常见的风格是按名称连接(.port(signal)),按端口名匹配,与顺序无关。更简短的按位置连接(my_module instance(signal1, signal2))依赖端口列表的顺序,维护起来很危险。
按名称连接和按位置连接端口有什么区别?
按位置连接时,信号的排列顺序和模块端口列表的顺序相同:第一个信号连到第一个端口,第二个连到第二个,依此类推。按名称连接使用 .port_name(signal_name),按名字匹配。按名称连接写起来更长,但不怕端口顺序调整,而且在调用处就能看出含义。端口超过两三个时,就用按名称连接。
同一个 Verilog 模块可以实例化多次吗?
可以,这正是实例化的意义所在。每个实例都是独立的硬件,有自己的状态。如果你有一个 adder 模块,可以在一个 SIMD 单元中把它实例化 64 次,每个实例的输入都不同。当各个实例相似且带有索引时,generate 循环是标准写法。
Verilog 中的 generate 块是什么?
generate ... endgenerate 是一种在编译时批量生成重复硬件的结构。generate 中的 for 循环会把循环体里的内容生成 N 个实例。generate 在细化阶段执行,早于仿真开始:它不是运行时循环,而是给综合器用的代码生成器。