Menu

Verilog 模块实例化:连接子模块

如何在一个模块中实例化另一个模块,按名称连接与按位置连接端口的区别,以及搭建真实设计时会用到的多实例写法。

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

模块中的模块

一个 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 在细化阶段执行,早于仿真开始:它不是运行时循环,而是给综合器用的代码生成器。

Coddy 编程语言插图

和 Coddy 一起学编程

开始