|
接着上一篇文章,我们来继续探索Verilator中C++ testbench的编写方式和验证模式。
编写Makefile
在每次用verilator仿真时,我们都要输入相同的命令,因此可以利用Make来快速build和run simulation。
MODULE=alu
.PHONY:sim
sim: waveform.vcd
.PHONY:verilate
verilate: .stamp.verilate
.PHONY:build
build: obj_dir/Valu
.PHONY:waves
waves: waveform.vcd
@echo
@echo "### WAVES ###"
gtkwave waveform.vcd
waveform.vcd: ./obj_dir/V$(MODULE)
@echo
@echo "### SIMULATING ###"
@./obj_dir/V$(MODULE)
./obj_dir/V$(MODULE): .stamp.verilate
@echo
@echo "### BUILDING SIM ###"
make -C obj_dir -f V$(MODULE).mk V$(MODULE)
.stamp.verilate: $(MODULE).sv tb_$(MODULE).cpp
@echo
@echo "### VERILATING ###"
verilator -Wall --trace -cc $(MODULE).sv --exe tb_$(MODULE).cpp
@touch .stamp.verilate
.PHONY:lint
lint: $(MODULE).sv
verilator --lint-only $(MODULE).sv
.PHONY: clean
clean:
rm -rf .stamp.*;
rm -rf ./obj_dir
rm -rf waveform.vcd
之前在编写Makefile时,一直不是很理解.PHONY是做什么的,今天顺手查了一下。简单来讲,比如你的目录下有一个文件名字叫做clean,但是Makefile中没有添加.PHONY: clean这条语句的话,那么当在终端执行make clean时,它就会去执行目录中clean文件,而不是做rm的操作,但是如果在Makefile中有.PHONY: clean这条语句的话,就算目录中有clean文件,make clean也不会去执行那个文件,而是会做Makefile中的rm操作。
当编写好Makefile文件并保存后,我们就能够通过在终端中运行 make sim 快速重建整个仿真,使用 make waves 打开 GTKWave,使用 make verilate 验证设计,或使用 make build 构建仿真可执行文件。Makefile中有一个附加的 make lint ,这主要用来快速解析 Verilog/SystemVerilog 源文件并检查代码错误。即使没有使用 Verilator 进行后续的仿真,这也可用于检查源代码。
Verilator是一个two state simulator,这意味着它只支持逻辑值 1 和 0,不支持 X(并且对 Z 的支持有限)。Verilator因此默认将所有信号初始化为0,从之前的仿真结果可以看出。

如果我们将X赋值给wire或reg变量时,默认情况下他们会是0。但是,我们可以通过命令行参数更改此行为,可以让 Verilator 将所有信号初始化为 1,或者更好的是,初始化为随机值。一旦我们将其添加到testbench,这能够检查复位信号是否有效。为了让testbench将信号初始化为随机值,要在实例化DUT对象之前调用 Verilated::commandArgs(argc, argv);
int main(int argc, char** argv, char** env) {
Verilated::commandArgs(argc, argv);
Valu *dut = new Valu;
<...>
修改Makefile的verilate命令如下:
verilator -Wall --trace --x-assign unique --x-initial unique -cc $(MODULE).sv --exe tb_$(MODULE).cpp最后,需要将 +verilator+rand+reset+2 传递给仿真可执行文件,以将运行时信号初始化为随机。 修改Makefile 中的sim命令如下:
@./obj_dir/V$(MODULE) +verilator+rand+reset+2现在,如果我们 make clean 并 make waves,将看到信号在仿真开始时被初始化为随机值:

DUT 复位信号设置
修改testbench的主循环:
while (sim_time < MAX_SIM_TIME) {
dut->rst = 0;
if(sim_time >=3 && sim_time < 6){
dut->rst = 1;
dut->a_in = 0;
dut->b_in = 0;
dut->op_in = 0;
dut->in_valid = 0;
}
dut->clk ^= 1;
dut->eval();
m_trace->dump(sim_time);
sim_time++;
}
在终端输入 make waves,观察波形,

基本验证
接下来需要在 C++ testbench 主循环中插入激励和验证代码来驱动和检查 DUT。
时钟上升沿计数
首先,创建一个新变量来统计时钟上升沿。该变量与 sim_time 的类型相同:
vluint64_t sim_time = 0;
vluint64_t posedge_cnt = 0;接下来在主循环中添加程序:在每次clk从0翻转为1时,就将posedge_cnt变量加1。
dut->clk ^= 1; // Invert clock
dut->eval(); // Evaluate dut on the current edge
if(dut->clk == 1){
posedge_cnt++; // Increment posedge counter if clk is 1
}
m_trace->dump(sim_time); // Dump to waveform.vcd
sim_time++; // Advance simulation time在 eval 和 dump 之间添加该计数器的程序,在 Verilog 中完成这个计数过程的代码如下:
initial posedge_cnt <= &#39;0;
always_ff @ (posedge clk) begin
posedge_cnt <= posedge_cnt + 1&#39;b1;
end
DUT激励和验证
让我们先忽略 a、b 和 opt_in 以及数据输出,首先看一下模块的输入 valid 信号是否正确传播到output。在alu.sv的模块设计中,输入 in_valid 信号经过两级寄存器接到输出信号 out_valid 信号上。代码经简化如下:
always_ff @ (posedge clk) begin
in_valid_r <= in_valid;
out_valid <= out_valid_r;
end因此,如果在第 5 个时钟上升沿将 in_valid 信号置1,在两个时钟周期后,也就是在第 7 个时钟上升沿看到 out_valid 上的 1。
while (sim_time < MAX_SIM_TIME) {
dut_reset(dut, sim_time);
dut->clk ^= 1;
dut->eval();
dut->in_valid = 0;
if(dut->clk == 1){
posedge_cnt++; // Increment posedge counter if clk is 1
if (posedge_cnt == 5){
dut->in_valid = 1;
}
else {
dut->in_valid = 0;
}
if (posedge_cnt == 7){
if (dut->out_valid != 1)
std::cout << &#34;ERROR!&#34; << std::endl;
}
}
m_trace->dump(sim_time);
sim_time++;
}
修改过 testbench后,新的波形如下,可以看到out_valid信号是延迟in_valid信号两个周期的。

断言式验证
在第 5 个clk上升沿将 in_valid 置1并检查 out_valid 是否为 1 是可以的,但如果想在更多时钟沿验证准确性,需要添加更多的检查。此外,在上面程序汇总我们只检查了out_valid 在第7个时钟沿是否为 1,并没有检查后面它是否回到0, out_valid 可能停留在 1 并且testbench不会报错。因此,验证代码可以通过编写一些 C++ 代码来持续监控 in_valid 和 out_valid ,类似于 SystemVerilog 断言的工作方式。
我们可以写一个如下的函数:
#define VERIF_START_TIME 7
void check_out_valid(Valu *dut, vluint64_t &sim_time){
static unsigned char in_valid = 0; //in valid from current cycle
static unsigned char in_valid_d = 0; //delayed in_valid
static unsigned char out_valid_exp = 0; //expected out_valid value
if (sim_time >= VERIF_START_TIME) {
// note the order!
out_valid_exp = in_valid_d;
in_valid_d = in_valid;
in_valid = dut->in_valid;
if (out_valid_exp != dut->out_valid) {
std::cout << &#34;ERROR: out_valid mismatch, &#34;
<< &#34;exp: &#34; << (int)(out_valid_exp)
<< &#34; recv: &#34; << (int)(dut->out_valid)
<< &#34; simtime: &#34; << sim_time << std::endl;
}
}
}VERIF_START_TIME 来确保我们不会在reset之前或reset期间进行验证,以防止错误检测。 rst 在 6ps 时返回到 0(sim_time=6时),因此我们应该从 sim_time=7 开始检查。检查部分的代码非常简单——它只是对 in_valid 和 out_valid 之间的 register pipeline 进行建模。将上面的函数添加到while循环中。
while (sim_time < MAX_SIM_TIME) {
dut_reset(dut, sim_time);
dut->clk ^= 1;
dut->eval();
if (dut->clk == 1){
dut->in_valid = 0;
posedge_cnt++;
if (posedge_cnt == 5){
dut->in_valid = 1;
}
check_out_valid(dut, sim_time);
}
m_trace->dump(sim_time);
sim_time++;
}如果现在运行仿真,应该不会得到任何错误,因为我们已经知道valid信号传播是正确的。然而,为了绝对确保验证代码有效,可以进入 alu.sv 修改代码并将 out_valid 设置为永久为 1(当然这是错的,这里只是为了验证):
always_ff @ (posedge clk, posedge rst) begin
if (rst) begin
out <= &#39;0;
out_valid <= &#39;0;
end else begin
out <= result;
out_valid <= 1&#39;b1; //**** this should be in_valid_r ****//
end
end
运行仿真,会在终端得到如下结果:


随机valid信号
我们也可以将对 in_valid 的单个赋值替换为随机将其设置为 1 或 0 的值。需要include C++ header cstdlib,
#include <cstdlib>
并在自定义 set_rnd_out_valid 函数中使用伪随机数生成函数 rand() 生成随机 1 和 0:
void set_rnd_out_valid(Valu *dut, vluint64_t &sim_time){
if (sim_time >= VERIF_START_TIME) {
dut->in_valid = rand() % 2; // generate values 0 and 1
}
}我们还需要通过调用 srand 为随机数生成器提供种子,它可以放在 main 函数的开头:
int main(int argc, char** argv, char** env) {
srand (time(NULL));
Verilated::commandArgs(argc, argv);
Valu *dut = new Valu;
<...>另外,我们将 MAX_SIM_TIME 增加到更大的值,例如 300,以仿真更长时间来验证波形:
#define MAX_SIM_TIME 300然后,在终端运行make waves来执行仿真,得到如下波形:

结论
C++ testbench的编写方式不同于在 Verilog/SystemVerilog 中testbench的编写方式,但从本文中给出的示例中,可以看到在 C++ 中编写测试模块与用 Verilog 编写的各个功能块是非常相似的。因此,如果想将 Verilog 测试平台编写技能应用到 C++,深入了解 C++ 调用的正确顺序以创建时钟边沿、激励/检查信号和波形值至关重要。虽然我们的当前版本的testbench仍然非常基础,但它已经开始像一些更高级的验证环境。testbench现在将所有信号初始化为随机值,并包含随机激励和对至少一个输出的连续断言式监控。
原文作者:Norbert Kremeris
原文链接:Verilator Pt.2: Basics of SystemVerilog verification using C++ :: It&#39;s Embedded! (itsembedded.com) |
|