IE盒子

搜索
查看: 104|回复: 1

[Verilator Part2] 使用 C++ 进行 SystemVerilog 验证

[复制链接]

4

主题

8

帖子

16

积分

新手上路

Rank: 1

积分
16
发表于 2022-12-17 19:33:27 | 显示全部楼层 |阅读模式
接着上一篇文章,我们来继续探索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 <= '0;
always_ff @ (posedge clk) begin
    posedge_cnt <= posedge_cnt + 1'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 << "ERROR!" << 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 << "ERROR: out_valid mismatch, "
                << "exp: " << (int)(out_valid_exp)
                << " recv: " << (int)(dut->out_valid)
                << " simtime: " << 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       <= '0;
                out_valid <= '0;
        end else begin
                out       <= result;
                out_valid <= 1'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's Embedded! (itsembedded.com)
回复

使用道具 举报

4

主题

14

帖子

27

积分

新手上路

Rank: 1

积分
27
发表于 4 天前 | 显示全部楼层
非常好,顶一下
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表