Verilator入门笔记

Verilator入门笔记

前言

verilator是用于verilog仿真的工具,它将HDL描述的代码编译为C++后运行,在效率上比传统的仿真器高出很多。同时作为开源工具,体量比较小,使用也比较灵活,不用每次都开一个大家伙。

但是,它的文档实在是写的太简略了,C++的类型和接口都没有详细的reference,只能自己去代码里翻。而且veripool.org的官方文档在国内连接也不是很稳定,有时候开了代理都上不去,verilator.org更是无法连接……所以总结一些摸索出的经验以备查阅。

verilator官方文档页面

安装过程略。

C++ wrapper

假设我们需要仿真的是一个加法器模块:

1
2
3
module adder(input [31:0] x,input [31:0] y,output [31:0] out);
    assign out=x+y;
endmodule

为了调用verilator进行仿真,我们还需要编写仿真的测试文件,即testbench,对于verilator而言也叫做C++ wrapper,其作用就是调用编译后的电路C++模型并执行。如下所示是一个简单的测试文件示例。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <verilated.h>
#include "Vadder.h"
#include <iostream>
int main(int argc, char **argv) {
    Verilated::commandArgs(argc, argv);
    Vadder *tb = new Vadder;
	uint32_t x=190,y=11;
	tb->x = 190;tb->y = 11;
	tb->eval();
	std::cout << (tb->out) << std::endl;
    tb->final();
	delete tb;
	return 0;
}

有这样几个细节需要注意:

  1. verilator对电路编译后生成的模型名称是V[module name],就是简单地在模块名称前方添加一个大写V。例如本例中模块名称是adder,则生成的就是Vadder。这个名称包含第二行的include和代码中用于实例化的类名称,以及默认情况下后续步骤会生成的可执行文件名称。
  2. 对输入端口进行赋值后需要调用tb->eval()进行计算。verilator不是精确时序的仿真器,不能指定延时,因此只会在每次调用eval后重新计算更新内部状态。

Verilating & Run

在编写完成verilog文件和C++ wrapper后即可调用verilator进行编译,官方说法是“verilating”。例如目前有如下两个文件:

1
2
3
.
├── adder.v
└── adder_tb.cpp

可以通过如下命令执行verilating:

1
verilator --cc adder.v  --exe adder_tb.cpp --build

其中–build参数是自动执行构建过程。如果不加这个参数,verilator只会生成对应的C++源文件和mk脚本,需要自行调用make进行构建。

verilator默认的生成目录是当前目录下的obj_dir,如果编译过程没有错误,将会在其中生成Vadder(或者Vadder.exe)可执行文件。运行该文件即可执行仿真,并查看输出结果。

1
2
3
4
5
6
#linux
$ ./obj_dir/Vadder
201
#win
> obj_dir\Vadder.exe
201

常用verilator参数

verilator支持的所有参数可以在文档页面查看,这里列出常用的几项。

添加查找目录

使用-y [dir]可以添加在编译时的模块查找路径。例如你的adder和其他公用模块都存放在/path/to/lib目录,而当前项目的top模块调用了adder的话,可以在verilator命令追加参数-y /path/to/lib,这样verilator就会正确识别到adder,并且top.v中无需显式include。

指定顶层模块

使用--top-module [module]可以显式指定顶层模块,一般在模块包含关系图中有多个树根的时候需要用到。

覆盖顶层参数

verilog的一项语法是支持通过参数实例化。在verilating时就可以通过-G[param]=[value]覆盖参数值。注意这里G和参数名称之间没有空格。

例如对于支持改变位宽的adder:

1
2
3
4
module adder#(parameter n=32)
(input [n-1:0] x, input [n-1:0] y, output [n-1:0] out);
    assign out=x+y;
endmodule

代码中指定参数n的默认值为32,这时在verilating时就可以通过-Gn=10来强制实例化为10位的adder。

不显示编译信息

严格而言这不是verilator的参数。verilator在生成完毕调用C++编译器时会在终端输出大量编译信息,影响观感,#可以通过输出重定向使得终端更加干净:

1
2
3
4
#linux
verilator --cc adder.v  --exe adder_tb.cpp --build >/dev/null
#win
verilator --cc adder.v  --exe adder_tb.cpp --build >NUL

这样终端输出就安静了许多。当然,这里只重定向标准输出流,编译的错误信息等属于错误流,仍然可以显示。

周期时序仿真 & 波形生成

verilator虽然不支持精确时序仿真,但仍然能够处理周期时序的情况。例如现有如下一个简单的脉冲计数器:

1
2
3
4
5
6
7
8
module counter(
    input clk,input rst,output reg [31:0] cnt
);
	always@(posedge clk or posedge rst)begin
        if(rst)cnt<=0;
        else if(clk)cnt<=cnt+1;
    end
endmodule

要对其进行周期时序仿真,首先在C++wrapper中需要启用相关功能。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <verilated.h>
#include "obj_dir/Vcounter.h"
#include "verilated_vcd_c.h" // 引入波形头文件
#include <iostream>

int main(int argc, char **argv) {
    Verilated::commandArgs(argc, argv);
    Vcounter *tb = new Vcounter;
    // 启用波形追踪
	Verilated::traceEverOn(true);
	VerilatedVcdC *tfp = new VerilatedVcdC;
    tb->trace(tfp,0);
    tfp->open("vtop.vcd");
    //仿真输入
	tb->rst=1;tb->clk=0;
    tb->eval();
    tfp->dump(0);
    tb->rst=0;
	bool clk = 0;
	for (int i = 1; i <= 10; ++i)
	{
		clk = !clk;
		tb->clk = clk;
		tb->eval();
		tfp->dump(i);//手动控制波形时间戳
	}
	tfp->close();
	delete tfp;
    tb->final();
	delete tb;
	return 0;
}

在编写完成wrapper后,verilating时还要追加--trace参数。

1
2
verilator --trace --cc counter.v  --exe counter_tb.cpp --build >/dev/null
./obj_dir/Vcounter

正常编译运行结束后,生成的vcd波形可以使用gtkwave查看。从左侧的面板可以选取和添加需要查看的信号。

默认情况波形数据包括所有层级、每个实例的内部信号,因此即使你的电路并不包含时钟/时序逻辑,也可以通过查看这些数据进行调试和排错,避免来回修改电路端口或插入多余的C++输出语句等操作。

注意: 启用波形追踪会显著降低仿真运行速度。

特殊数据输入/输出

在模块端口位宽较短时都可以通过直接将unsigned int/long long等赋值给它实现输入。对于输出数据,也可以通过强制类型转换为这些类型进行后续处理和输出。

浮点数

如果你编写了一个浮点数运算模块,用于处理32位IEEE754标准的浮点数据,在测试时直接赋值C++中的float变量是无法正确传递数据的。可以用强制无检查类型转换的方式将float类型转换为输入所需的32位二进制数据,输出时也可以通过同样的方法转换为浮点数类型(该方法来自stackoverflow):

1
2
3
4
5
float x=190,y=11;
tb->x = reinterpret_cast<uint32_t&>(x);
tb->y = reinterpret_cast<uint32_t&>(y);
tb->eval();
std::cout << reinterpret_cast<float&>(tb->out) << std::endl;

长位宽数据

内容较多,见另一篇笔记

Verilator c++ wrapper兼容不同名称的verilog模块

另一篇笔记

End

verilator的功能十分强大,这里只列出了目前接触的一部分。如果后续使用中了解了其他功能会追加更新。

0%