我是
雪天鱼
,一名FPGA爱好者,研究方向是FPGA架构探索和数字IC设计。
欢迎来关注我的B站账号,我将定期更新IC设计教程。
B站账号:
雪天鱼
,
https://space.bilibili.com/397002941?spm_id_from=333.1007.0.0
1 准备工作
先从GitHub下载实验代码
git clone https://github.com/n-kremeris/verilator_basics
git checkout verilator_pt1
2 我们的DUT
以一个用SystemVerilog编写的简单ALU来作为DUT(device under test)来学习Verilator是如何工作的。下面是ALU的源码:
/****** alu.sv ******/
typedef enum logic [1:0] {
add = 2'h1,
sub = 2'h2,
nop = 2'h0
} operation_t /*verilator public*/;
module alu #(
parameter WIDTH = 6
) (
input clk,
input rst,
input operation_t op_in,
input [WIDTH-1:0] a_in,
input [WIDTH-1:0] b_in,
input in_valid,
output logic [WIDTH-1:0] out,
output logic out_valid
);
operation_t op_in_r;
logic [WIDTH-1:0] a_in_r;
logic [WIDTH-1:0] b_in_r;
logic in_valid_r;
logic [WIDTH-1:0] result;
// Register all inputs
always_ff @ (posedge clk, posedge rst) begin
if (rst) begin
op_in_r <= '0;
a_in_r <= '0;
b_in_r <= '0;
in_valid_r <= '0;
end else begin
op_in_r <= op_in;
a_in_r <= a_in;
b_in_r <= b_in;
in_valid_r <= in_valid;
end
end
// Compute the result
always_comb begin
result = '0;
if (in_valid_r) begin
case (op_in_r)
add: result = a_in_r + b_in_r;
sub: result = a_in_r + (~b_in_r+1'b1);
default: result = '0;
endcase
end
end
// Register outputs
always_ff @ (posedge clk, posedge rst) begin
if (rst) begin
out <= '0;
out_valid <= '0;
end else begin
out <= result;
out_valid <= in_valid_r;
end
end
endmodule;
创建一个工作目录,并将上述代码保存为
alu.sv
.
这个ALU非常简单。只支持两种操作:加法和减法。操作经过两个周期计算出最终结果,下面是期望波形:
3 将 SystemVerilog 转换为 C++
由于Verilator要求C++ testbench 被编译成本地系统二进制。然而,我们不能将SystemVerilog编写的 ALU添加进C++ testbench:我们首先需要使用 Verilator 将 SystemVerilog 代码转换为C++,或者说
"Verilate"
,执行一行代码即可:
verilator --cc alu.sv
通过执行该指令,会生成一个名为
obj_dir
的子目录,里面是转换所生成的文件:
jccao@jccao-vm:~/jccao/files/code/verilator/part1$ ls obj_dir/
Valu___024root__DepSet_h7172bd91__0.cpp Valu_classes.mk
Valu___024root__DepSet_h7172bd91__0__Slow.cpp Valu.cpp
Valu___024root__DepSet_ha59b247d__0.cpp Valu.h
Valu___024root__DepSet_ha59b247d__0__Slow.cpp Valu.mk
Valu___024root.h Valu__Syms.cpp
Valu___024root__Slow.cpp Valu__Syms.h
Valu___024unit__DepSet_h45503383__0__Slow.cpp Valu__ver.d
Valu___024unit.h Valu__verFiles.dat
Valu___024unit__Slow.cpp
这里需要关注的文件有:
-
Valu.cpp
,
Valu.h
: alu转换后对应的C++实现源文件以及头文件,其中
Valu.h
包含了转换后的 “ALU “类定义–也就是我们将在C++ testbench 中作为DUT “实例化 “的东西。 -
Valu.mk
: 用于编译可执行的仿真文件 -
Valu___024unit.h
: 这是 “ALU “类的内部头文件,包含了
operation_t
数据类型定义。
4 简单 testbench 设计
testbench 命名为
tb_alu.cpp
,最简单的 testbench 代码如下所示:
#include <stdlib.h>
#include <iostream>
#include <verilated.h>
#include <verilated_vcd_c.h>
#include "Valu.h"
#include "Valu___024unit.h"
#define MAX_SIM_TIME 20 // 仿真总时钟边沿数
vluint64_t sim_time = 0; // 用于计数时钟边沿
int main(int argc, char** argv, char** env) {
Valu *dut = new Valu; // 例化转换后的 ALU 模块
// 接下来的四行代码用于设置波形存储为VCD文件
Verilated::traceEverOn(true);
VerilatedVcdC *m_trace = new VerilatedVcdC;
dut->trace(m_trace, 5);
m_trace->open("waveform.vcd");
// 实际进行仿真的代码
while (sim_time < MAX_SIM_TIME) {
dut->clk ^= 1;
dut->eval();
m_trace->dump(sim_time);
sim_time++; // 更新仿真时间
}
m_trace->close();
delete dut;
exit(EXIT_SUCCESS);
}
-
testbench 需要包含
<verilated.h>
和
<verilated_vcd_c.h>
,这两个头文件在安装好verilator就有了,前者里面包含了常用API,后者包含将波形写入
VCD(value change dump)
文件的API -
刚说过
"Valu.h"
包含了 verilated 后 ALU模块的类定义,
"Valu___024unit.h"
包含
operation_t
数据类型定义,被ALU类所需要,所以需要在Testbench中包含这两个头文件 -
dut->trace(m_trace, 5);
的意思是将 m_trace 传递给 dut,5表示跟踪深度限制在DUT的5级以内,这个5级目前我理解为DUT的子模块层级。
while (sim_time < MAX_SIM_TIME) {
dut->clk ^= 1;
dut->eval();
m_trace->dump(sim_time);
sim_time++;
}
该部分是实际启动仿真的代码。
-
dut->clk ^= 1
的意思是 clk 与 1 异或,翻转时钟 -
dut->eval()
eval()函数更新电路的状态,可理解为仿真 ALU 模型中的所有信号 -
m_trace->dump(sim_time)
将所有被追踪的信号写入波形中
5 生成仿真执行文件
完成 testbench 编写后,我们需要 build 可执行文件来运行这个仿真。
这是因为 Verilator 不同于
Modelsim
这样的仿真软件,内置仿真器进行仿真,Verilator本身就是只进行转换和编译工作,即
verilate
, 仿真工作由C++可执行文件完成的,所以我们需要用
make
编译 testbench得到可执行文件。
testbench 和转换后的HDL编写的模块本质上是一个C++应用程序,它可以在你的计算机上建立和运行。运行编译后的可执行文件就是模拟你的设计
,用GNU编译器集(GCC)构建Verilator可执行文件。
现在编译我们的测试文件并进行仿真,此时需要运行 Verilator 并重新生成包含测试用例的 .mk 文件:
$ verilator -Wall --trace -cc alu.sv --exe tb_alu.cpp
- -Wall 表示开启 C++ 所有警告
- –trace 表示开启波形跟踪
- -cc 转换.sv为cpp
- –exe 根据后面指定的testbench 生成用于仿真的可执行文件
随后我们进行编译:
make -C obj_dir -f Valu.mk Valu
-
-C obj_dir
告诉
make
工作目录为
obj_dir
-
makefile 通过
-f
指定 -
最后的
Valu
则是 target,也就是编译生成的testbench可执行文件的name
编译成功的话,可以在
obj_dir
目录下找到
Valu
可执行文件
6 运行 testbench
用
./obj_dir/Valu
命令执行可执行文件进行仿真,此时会生成波形图
waveform.vcd
,我们只需要执行
gtkwave waveform.vcd
即可查看波形图。
至此你已经完成了Verilator的一个基础仿真实验,也对Verilator有了一定的认知。
gtkwave安装:
sudo apt-get install gtkwave
7 问题思考
可以看到我们的仿真没有任何’x’,或未知值。这是因为
Verilator
是一个双状态的仿真工具,默认情况下,所有的信号都被初始化为0。这对于提高仿真速度来说是有效的,因为2个状态比4个状态要少,但是如果我们想检查我们的复位逻辑工作得如何,就不是很好。我们将在后面进一步探讨这个问题。