使用 Icarus Verilog 开发
在学校课程中,电子钟、CPU 等数字系统的设计需要使用 Verilog 编写。Quartus II 虽然是一款功能强大的 Verilog IDE,但是不像 VS Code 这样可以接入各种插件,对于像笔者这样的初学者使用不是很友好。(个人感觉 Quartus II 完全就不是现代开发工具……)
然后发现,计算机组成原理课设的资料里给了 Icarus Verilog 这个工具,看起来挺好用的。
Icarus Verilog(通常简称为iverilog)是一款基于 C++ 开发的轻量免费的 Verilog 编译器。可以将 Verilog 编译成多种目标语言,并且可以配合 GTKWave 实现波形仿真。使用方法和命令格式与 gcc 很像,需要输入命令行来使用该工具。有了 iverilog,我们就可以在 VS Code 中编写 Verilog,这样就可以接入其他各种便捷的辅助插件了!
基础使用方法
该部分讲解如何编译源代码以及调用 GTKWave 查看波形。
Icarus Verilog 共包含两个工具:iverilog、vvp. 在其安装文件中还包含一个波形查看器: gtkwave.
iverilog 命令可以编译 .v/.sv 等源文件,生成输出文件。testbench 等包含顶层激励的仿真设计经 iverilog 编译出的输出文件可以作为仿真的输入。vvp 命令用来执行编译后的仿真程序,通过在 testbench 里编写 $dumpfile $dumpvars 转储语句可以生成 .vcd 波形文件。gtkwave 命令用来查看 .vcd 文件存储的波形。
大概逻辑是这样:
.v 文件 —— iverilog ——> 输出文件 —— vvp ——> .vcd 文件 —— gtkwave ——> 查看仿真波形
编译源代码
编译源代码分为两种情况:1、编译的文件没有引用其他文件编写的模块,即单文件编译;2、编写的文件引用了其他文件编写的模块,即多文件编译。接下来分别说明两种情况的编译方法。
编译使用的是 iverilog 命令,该命令结构如下:
iverilog <参数列表> <文件名>
参数列表里最常用的是 -o,用来指定输出文件的名称。
单文件编译
如果一个 Verilog 文件内部已经包含了完整的模块定义,没有调用其他文件中的模块,那么直接编译这一个文件即可。
下面给出一个最简单的示例,其中模块定义和测试代码都放在同一个文件中。

module and_gate(
input a,
input b,
output y
);
assign y = a & b;
endmodule
module and_gate_tb;
reg a;
reg b;
wire y;
and_gate uut(
.a(a),
.b(b),
.y(y)
);
initial begin
a = 0; b = 0;
#10 a = 0; b = 1;
#10 a = 1; b = 0;
#10 a = 1; b = 1;
#10 $finish;
end
endmodule
假设上述代码保存在 and_gate_tb.v 中,那么可以使用下面的命令进行编译:
iverilog -o and_gate_tb.out and_gate_tb.v

其中 -o 用于指定输出文件名,and_gate_tb.out 是编译后生成的仿真文件。

多文件编译
当一个文件中使用了其他文件定义的模块时,编译时不能只传入当前文件,而应将所有相关源文件同时加入命令。
例如,将与门模块和测试代码拆分成两个文件。

and_gate.v
module and_gate(
input a,
input b,
output y
);
assign y = a & b;
endmodule
and_gate_tb.v
module and_gate_tb;
reg a;
reg b;
wire y;
and_gate uut(
.a(a),
.b(b),
.y(y)
);
initial begin
a = 0; b = 0;
#10 a = 0; b = 1;
#10 a = 1; b = 0;
#10 a = 1; b = 1;
#10 $finish;
end
endmodule
由于 and_gate_tb.v 中实例化了 and_gate 模块,因此编译时需要把两个文件都写上:
iverilog -o and_gate_tb.out and_gate_tb.v and_gate.v


当工程包含多个源文件时,也可以继续按照这种方式把相关文件依次追加到命令后面。若遗漏了某个被引用模块所在的文件,编译时通常会报出模块未定义之类的错误。
执行仿真
编译完成后,就可以使用 vvp 来执行刚刚生成的仿真文件:
vvp and_gate_tb.out
这条命令会按 testbench 中定义的激励顺序运行整个仿真流程。若 testbench 中包含 $display 等输出语句,终端里会直接打印对应结果。
例如我在 and_gate_tb.v 里添加了一些 $display 语句:
module and_gate_tb;
reg a;
reg b;
wire y;
and_gate uut(
.a(a),
.b(b),
.y(y)
);
initial begin
$display("start sim..."); // 提示仿真开始
a = 0; b = 0;
#10 a = 0; b = 1;
$display("a=%d, b=%d, y=%d", a, b, y); // 打印 a b y
#10 a = 1; b = 0;
$display("a=%d, b=%d, y=%d", a, b, y); // 打印 a b y
#10 a = 1; b = 1;
$display("a=%d, b=%d, y=%d", a, b, y); // 打印 a b y
#10 $display("finish sim..."); // 提示仿真结束
$finish;
end
endmodule
如果你不知道 $display 怎么用,可以把 $display 跟 C 语言的 printf 函数类比一下,它们的参数结构都是一样的。$display(<format>, <var1>, <var2>, ...) 这样的形式。
输出结果:

如果希望进一步查看波形,还需要在 testbench 中加入波形转储语句。例如:
initial begin
$dumpfile("and_gate_tb.vcd");
$dumpvars(0, and_gate_tb);
end
其中,$dumpfile 用于指定生成的波形文件名,$dumpvars 用于指定需要记录的信号范围。加入这段代码后,再次执行:
vvp and_gate_tb.out

仿真结束后,当前目录下会生成 and_gate_tb.vcd 文件。接着可以使用 GTKWave 打开它:
gtkwave and_gate_tb.vcd
这样就可以在图形界面中查看各个信号随时间变化的波形。若执行 vvp 后没有生成 .vcd 文件,通常需要优先检查 testbench 中是否正确写入了 $dumpfile 和 $dumpvars。
查看波形
执行 gtkwave 后会进入如下界面,依次点击模块、选择波形、添加波形,即可在右侧查看到选择的波形。

至此 iverilog 的基础操作就完成了。学会这些操作就足够应付作业了。
$monitor 的用法
$display 和 $monitor 都是用来打印日志的系统任务。$monitor 顾名思义,在调用后监视其绑定的信号,一旦这些信号发生变化,$monitor 就会打印一次日志。这比 $display 要方便很多。
p.s. 直到数字系统设计期末备考的时候我才知道有 $monitor 这个功能,于是然后就把这段加上了
简单写个使用示例:(Gemini 生成)
module tb_monitor;
reg clk;
reg [3:0] counter;
initial begin
clk = 0;
counter = 0;
// 启动监视器。只要 clk 或 counter 发生变化,这行就会自动打印
$monitor("Time = %0t | clk = %b | counter = %d", $time, clk, counter);
// 仿真运行一段时间后停止
#50 $finish;
end
// 产生时钟
always #5 clk = ~clk;
// 计数器逻辑
always @(posedge clk) begin
counter <= counter + 1;
end
endmodule
全局只有一个 $monitor 生效,如果代码里定义了多个,只有最后一个 $monitor 会生效。
Icarus Verilog 下载方式
打开iVerilog官网下载:http://bleyer.org/icarus/

选择自己需要的版本下载。
启动安装程序,一直点“下一步”即可。注意勾上环境变量。

最后输入以下命令测试安装是否成功。
where iverilog
where vvp
where gtkwave

若打印出向上面那样的路径名则说明安装成功。
附录:格式控制符
- 格式控制符
在使用 $display 和 $monitor 时,常使用以下格式符来规定输出的进制或类型:
%b或%B:二进制 (Binary)%d或%D:十进制 (Decimal)%h或%H:十六进制 (Hexadecimal)%o或%O:八进制 (Octal)%t或%T:仿真时间 (Time)%s或%S:字符串 (String)%c或%C:ASCII字符 (Character)