模块是 Verilog 的基本单元
Verilog 中的一切都放在 module 里。一个模块封装一块电路:它声明哪些信号输入、哪些信号输出,以及它们之间有哪些导线、寄存器和逻辑。你见过的每一块芯片,都是一棵由模块层层实例化其他模块构成的树,一直到最底层厂商提供的门级原语。
模块的结构总是一样的:
module name(port_list);
// declarations: wire, reg, parameter
// body: assigns, instantiations, always blocks
endmodule
我们会分几个阶段,逐步搭建出一个完整的模块。
最小的有用模块:二输入与门
我们需要一个简单、自成一体的例子。二输入与门再合适不过:两个输入,一个输出,一行逻辑。
点击“运行”。你应该会看到与门真值表的四行。下面我们逐一分析每个部分。
读懂模块声明
module and_gate(
input wire a,
input wire b,
output wire y
);
module and_gate声明一个名为and_gate的模块。其他模块通过这个名字来实例化它。- 括号里的列表是端口列表:从外部能看到的信号。
input wire a:a是输入端口,类型是wire(由外部驱动)。output wire y:y是输出端口,由模块内部的某个东西驱动。
如果想写得简洁一些,可以用 input a 代替 input wire a:只写方向时,默认类型就是 wire。不过写清楚是一个值得养成的习惯。模块端口介绍了端口的所有写法。
模块体
assign y = a & b;
这是一个连续赋值。它的意思是:“只要 a 或 b 变化,就把 y 重新计算为两者按位相与的结果。”这里没有时钟,也没有时序,这个关系始终成立。这就是纯粹的组合逻辑。
endmodule 结束这个块,模块就写完了。
testbench
and_gate 不能单独运行。你需要第二个模块来驱动它的输入、观察它的输出。这就是 testbench,按照惯例我们把它命名为 test、tb 或 <design>_tb。
module test;
reg a, b;
wire y;
and_gate dut(.a(a), .b(b), .y(y));
...
endmodule
有三点值得注意:
- 没有端口列表。 testbench 是仿真的顶层,外面没有任何东西。
reg a, b和wire y。 被测设计(DUT)的输入在 testbench 中是reg,因为我们是在过程块中驱动它们。输出是wire,因为由 DUT 驱动它。and_gate dut(.a(a), .b(b), .y(y))。 这就是实例化。我们复制出一个and_gate,并把它命名为dut(一个常用的名字,意思是“被测设计”)。.a(a)的意思是“把实例上名为a的端口连接到本地名为a的信号”。模块实例化会深入讲解。
激励
initial begin
a = 0; b = 0; #1 $display("a=%b b=%b y=%b", a, b, y);
a = 0; b = 1; #1 $display("a=%b b=%b y=%b", a, b, y);
a = 1; b = 0; #1 $display("a=%b b=%b y=%b", a, b, y);
a = 1; b = 1; #1 $display("a=%b b=%b y=%b", a, b, y);
$finish;
end
initial 块在仿真开始时运行一次。在它内部:
a = 0; b = 0;驱动输入。这些是阻塞赋值:顺序很重要,每一条都在下一条之前完成。#1让仿真时间前进 1 个单位。需要它是为了让y在输入变化后有时间稳定下来。没有#1,$display会打印y的旧值。$display(...)向仿真控制台打印内容。格式字符串的用法和 C 的printf类似:%b是二进制,%d是十进制,%h是十六进制,%t是仿真时间。$finish结束仿真。没有它,仿真器会一直推进时间,等待一个永远不会到来的事件。
试着改一改
把模块改成或门(把 & 改成 |),然后重新运行。真值表就变了。再试试异或门:
骨架相同,只是运算符不同。组合逻辑的全部玩法就是这样:声明端口,写 assign,遍历输入,观察输出。
有两个输出的模块
模块可以有不止一个输出。下面是一个半加器:两个输入,输出和与进位:
两条 assign 语句写在一起,却是并行发生的:并不存在“先算和,再算进位”。两者始终同时成立。这就是我们在硬件与软件中讲过的并发性的具体体现。
你现在学会了什么
你已经看到了 Verilog 文件的完整骨架:一个声明了端口和模块体的设计模块,加上一个 testbench 模块,它实例化设计、在 initial 块中驱动输入并报告结果。你今后读到的几乎每一个 Verilog 源文件都符合这个模板。语言的其余部分,不过是往里面填充更丰富的逻辑、多位信号、时钟驱动的行为和更大的 testbench。
下一篇:注释和代码风格,让你的模块在不断变大时依然易读。
常见问题
最简单的 Verilog 模块是什么样的?
最小的合法 Verilog 模块就是 module name; endmodule:没有端口,也没有模块体。最小的有用模块是只有一个输出的模块:module and_gate(input wire a, input wire b, output wire y); assign y = a & b; endmodule。这是一段真正的组合逻辑,可以放进任何更大的设计中。
如何运行一个 Verilog 模块?
模块不能单独运行,它是电路描述,不是程序。你需要写一个 testbench 模块来实例化你的设计并驱动它的输入,然后用 iverilog -o sim design.v test.v 把两者一起编译,再运行 vvp sim。点击本页浏览器编辑器中的“运行”,它会帮你完成这两步。
Verilog 中的 testbench 是什么?
testbench 是第二个模块,通常没有端口,它的任务是检验你的设计。它实例化设计,通过 initial 块改变设计的输入,用 $display 或 $monitor 观察输出,完成后调用 $finish。testbench 不可综合,它只用来验证行为。
为什么我的 Verilog 代码需要 $finish?
因为硬件永远不会停下来。仿真器模拟时间的流逝,如果没有显式的 $finish,它会一直推进时间,永远等待新的事件。$finish 告诉仿真器“我们做完了,正常退出吧”。在 testbench 中,它是 initial 块的最后一行:先运行测试,再结束。