4.1 模块(Module)
一、模块的概念
模块(Module)是Verilog中最基本的设计单元。 如果把一个数字电路比作一台机器,那么模块就是这台机器中的各个零件------每个零件都有特定的功能、明确的接口,零件之间通过接口连接组成完整的系统。
一个模块可以是一个简单的逻辑门,也可以是一个复杂的CPU核心。模块通过端口 与外部环境交互,但内部的具体实现细节对外部是隐藏的。
这种封装特性带来的好处是:你可以修改模块的内部实现(比如从门级改成数据流级),只要端口保持不变,整个设计中的其他部分就不需要做任何修改。
二、模块的语法结构
一个完整的Verilog模块由以下几个部分组成:
module <模块名> ( <端口列表> );
// 1. 端口声明(input / output / inout)
// 2. 数据类型声明(wire / reg / parameter)
// 3. 功能描述(assign / always / 模块实例调用)
endmodule
各部分说明:

验证: 在你的8位ALU工程中,alu.v 就是一个典型的模块定义,aludefines.vh 是参数头文件,alu_tb.v 是另一个模块(Testbench)
与C语言的类比 :module 相当于C语言中的struct或class,endmodule 相当于}。模块名相当于类名,端口列表相当于函数参数列表。
三、模块的四个抽象层次
Verilog允许在模块内部使用不同的抽象层次来描述功能,从最高抽象到最低抽象依次为:

一个核心理解: 同一个模块,对外接口(端口)保持不变,内部可以使用不同的抽象层次来实现。这是Verilog设计复用的核心优势。
之前写的8位ALU,在always块里用了case语句(行为级)和assign风格的赋值(数据流级),其实就是混合抽象层次的设计,这在RTL设计中非常普遍。
4.2 端口(Ports)
端口是模块与外部环境交互的通道,相当于芯片的引脚(Pin)
一、端口列表
模块定义中的端口列表,就是把所有输入输出信号罗列在模块名后面的括号里:
// 有端口列表的模块
module full_adder(sum, c_out, a, b, c_in);
// ...
endmodule
// 没有端口列表的模块(仿真顶层模块常用)
module testbench;
// 没有端口,因为不需要被其他模块调用
endmodule
仿真顶层模块通常没有端口 ,因为它是最顶层,不被任何其他模块调用。你的alu_tb.v就是一个没有端口的模块。
二、端口声明
端口列表中列出的信号,必须在模块内部进行端口声明,指定其方向。

重点规则(容易混淆):
-
input 和 inout 端口不能声明为
reg类型------因为输入端口反映的是外部信号的变化,不保存数值。 -
output 端口如果需要在
always块中被赋值,必须声明为reg类型。module D_FF(q, d, clk, reset);
output q; // 默认是wire
input d, clk, reset;
reg q; // 改成reg,因为需要在always块中赋值
// ...
endmodule
端口声明的两种写法:
// 写法1:传统风格(先列端口,再声明)
module FA(sum, c_out, a, b, c_in);
output sum, c_out;
input a, b, c_in;
// ...
endmodule
// 写法2:ANSI C风格(在端口列表中直接声明)------更简洁,推荐!
module FA(
output reg sum,
output c_out,
input a, b, c_in
);
// ...
endmodule
ANSI C风格目前更常用,因为它减少了重复代码。
三、端口连接规则
当你在一个模块中实例化另一个模块时,端口之间的连接必须遵守以下规则:

初学者常犯错误 :在顶层模块中试图把设计模块的 output 端口连接到 reg 变量上。记住------输出端口在顶层模块中必须用 wire 接收。
未连接端口:
如果某些端口不需要连接,可以悬空:
// output端口c_out悬空
full_adder fa0(SUM, , A, B, C_IN);
// 命名连接方式悬空
full_adder fa0(
.sum(SUM),
.c_out(), // 悬空
.a(A),
.b(B),
.c_in(C_IN)
);
input 端口不建议悬空,否则会处于高阻态z,可能导致不可预知的行为。如果要固定值,可以连接常量:.clk(1'b0)。
四、端口与外部信号的连接方式
模块实例化时,有两种方式连接端口:
方式1:顺序端口连接(按位置):
这种方法要求实例化时的信号顺序与模块定义时的端口顺序严格一致。
// 模块定义
module full_adder(sum, c_out, a, b, c_in);
// ...
endmodule
// 顺序连接
full_adder fa0(SUM, C_OUT, A, B, C_IN);
// 顺序必须:sum→SUM, c_out→C_OUT, a→A, b→B, c_in→C_IN
优点 :写法简洁。
缺点:顺序容易搞错,不易维护,端口列表改动时需要同步修改所有实例化
正如中科大教程中提到的:某位同学在综合实验中有个模块有12个端口,想添加一个新端口时根本不知道应该插在哪个位置------这就是顺序连接的典型问题。
方式2:命名端口连接(按名字)------推荐!
这种方法通过端口名来匹配信号,顺序无关紧要。
full_adder fa0(
.sum(SUM),
.c_out(C_OUT),
.a(A),
.b(B),
.c_in(C_IN)
);
优点:可读性强,顺序随意,端口列表变化时只需增删对应行,不影响其他连接。
更简化的写法(当外部信号名与端口名完全一致时):
// 如果外部信号也叫 clk
full_adder fa0(
.clk, // 等价于 .clk(clk)
.sum(SUM),
.c_out(C_OUT)
);
推荐:日常编码中请务必使用命名端口连接。
4.3 层次命名(Hierarchical Names)
一、什么是层次命名?
在Verilog中,每个模块实例、信号或变量都处于设计层次中的唯一位置 。层次命名允许你通过完整的路径名来访问设计中的任何信号或实例。
层次名由一串用点 . 分隔的标识符组成:
顶层模块名.子模块实例名.子子模块实例名.信号名
二、举例说明
假设你有这样的设计层次:
stimulus(顶层模块)
├── q, qbar, reset, set ← stimulus中定义的信号
└── m1(SR_latch的实例)
├── Q, Qbar, S, R ← m1中定义的信号
├── n1(与非门实例)
└── n2(与非门实例)
所有标识符的层次名如下:

关键用途: 在仿真调试时,你可以通过完整的层次名访问任意模块内部的任意信号,而不受作用域的限制。
三、显示层次名
在 $display 等系统任务中,可以使用 %m 格式符显示当前所在的层次路径:
module M;
initial
$display("Current hierarchy: %m");
endmodule
module top;
M m1();
M m2();
endmodule
// 输出:
// Current hierarchy: top.m1
// Current hierarchy: top.m2
这在调试多个实例时特别有用------可以看到信息是从哪个具体实例输出的。
4.4 本章小结
本章核心要点:
-
模块 是Verilog的基本设计单元,以
module开始,以endmodule结束。只有module、模块名、endmodule是必须的,其他部分都是可选的。 -
端口 是模块与外部通信的接口,分
input、output、inout三种类型。output端口如需在always块中赋值,必须声明为reg类型;input端口不能是reg。 -
端口连接规则 :从模块外部看,
input可接wire/reg,output必须接wire,inout必须接wire。 -
模块实例化 有两种方式:顺序连接 (按位置,不推荐)和命名连接(按名字,推荐)。
-
层次命名 允许通过路径名(如
top.u1.u2.signal)访问设计中的任何信号,在调试时非常有用。