尝试做一个最简单的加法器sv验证系统

⚠️ 注意区分ifdefifndef的区别

最基础版本

学习sv验证一直不能很好的理解其中的思想,看了很多完整的工程,乍一眼觉得也就这么回事,但真要自己的写的时候却发现都是问题。不能一直总看别人怎么写,所以我打算从一个最简单的sv验证环境开始编写,尝试从头掌握sv验证环境的编写方法。本次设定的功能非常简单,就是一个组合逻辑的全加器,没有用到时钟和复位信号,尽可能减少一些不必要的复杂度,先理解整个sv框架是怎么搭起来的。dut是:

dut.sv

`ifndef DUT
`define DUT
module dut(
    input  logic [7:0] a,
    input  logic [7:0] b,
    input  logic       cin,
    output logic [7:0] sum,
    output logic       cout
);

    assign {cout, sum} = a + b + cin;
    
endmodule
`endif

由于没有用到时序逻辑,所以在interface中就不加clk和复位信号了,这样编写接口变得简单了很多。

后面测试时我也尝试把全加器用always进行了修改,不过从仿真结果来看差不多。

always@(*)  {cout, sum} = a + b + cin;

interface.sv

`ifndef DUT_IF
`define DUT_IF
interface dut_if;

    logic [7:0] a;
    logic [7:0] b;
    logic       cin;
    logic [7:0] sum;
    logic       cout;

    modport dut (
        input a,
        input b,
        input cin,
        output sum,
        output cout
    );
    modport tb (
        output a,
        output b,
        output cin,
        input  sum,
        input  cout
    );

endinterface
`endif

接口程序长上面的样子,已经是非常简单的写法了,由于没有时钟逻辑,所以没有用到时钟块。之后,将二者在顶层文件中进行封装

top.sv

`include "interface.sv"
`include "dut.sv"

module top;
    // 实例化接口
    dut_if dut_if_inst();

    // DUT 实例化
    dut dut (
        .a(dut_if_inst.a),
        .b(dut_if_inst.b),
        .cin(dut_if_inst.cin),
        .sum(dut_if_inst.sum),
        .cout(dut_if_inst.cout)
    );

    initial begin
        // 初始化输入
        dut_if_inst.a = 0;
        dut_if_inst.b = 0;
        dut_if_inst.cin = 0;

        // 测试用例1:简单相加
        #10; // 延迟以便观察波形
        dut_if_inst.a = 8'd5;
        dut_if_inst.b = 8'd3;
        dut_if_inst.cin = 1'b0;

        #10;
        if ({dut_if_inst.cout, dut_if_inst.sum} == 9'd8)
            $display("Test case 1 passed");
        else
            $error("Test case 1 failed");

        // 测试用例2:进位
        #10;
        dut_if_inst.a = 8'd255;
        dut_if_inst.b = 8'd1;
        dut_if_inst.cin = 1'b0;

        #10;
        if ({dut_if_inst.cout, dut_if_inst.sum} == 9'd256)
            $display("Test case 2 passed");
        else
            $error("Test case 2 failed");

        // 测试用例3:进位
        #10;
        dut_if_inst.a = 8'd50;
        dut_if_inst.b = 8'd1;
        dut_if_inst.cin = 1'b1;

        #10;
        if ({dut_if_inst.cout, dut_if_inst.sum} == 9'd52)
            $display("Test case 3 passed");
        else
            $error("Test case 3 failed");

    end

    // 添加波形输出
    initial begin
        $dumpfile("dump.vcd");
        $dumpvars(0, top);
    end

endmodule

仿真结果

在这里插入图片描述

最基础的版本和用verilog编写testbench并无本质的区别,只不过用interface对接口进行了封装,并未体现sv的模块化验证的思想。所以,接下来我们尝试加入一些驱动、发射模块,对这个验证框架进行完善。

增加generator、driver并用environment进行封装

在用modulesim进行仿真时,把不同的模块放在不同的文件里总是报错,尤其是牵扯到ifdef或者ifndef时,我在测试时总是一头雾水,报错也不知道如何修改。索性现在我把所有模块都放放在了一个文件中:

  • 程序

    // DUT模块
    module dut(
        input  logic [7:0] a,
        input  logic [7:0] b,
        input  logic       cin,
        output logic [7:0] sum,
        output logic       cout
    );
        assign {cout, sum} = a + b + cin;
    endmodule
    
    // 接口定义
    interface dut_if;
        logic [7:0] a;
        logic [7:0] b;
        logic       cin;
        logic [7:0] sum;
        logic       cout;
    
        modport tb (
            output a, b, cin,
            input  sum, cout
        );
    
        modport dut (
            input  a, b, cin,
            output sum, cout
        );
    endinterface
    
    //transaction类
    class transaction;
        rand bit [7:0] a;
        rand bit [7:0] b;
        rand bit       cin;
    
        bit      [7:0] sum;
        bit            cout;
        bit      [7:0] expected_sum;
        bit            expected_cout;
    
        constraint c_valid {
            a inside {[0:255]};
            b inside {[0:255]};
            cin inside {[0:1]};
        }
    
        function void calculate_expected();
            {expected_cout, expected_sum} = a + b + cin;
        endfunction
    
        function bit compare();
            return (sum == expected_sum) && (cout == expected_cout);
        endfunction
    
        function void display();
            $display("a = %h, b = %h, cin = %b | Expected: sum = %h, cout = %b | Actual: sum = %h, cout = %b", 
                     a, b, cin, expected_sum, expected_cout, sum, cout);
        endfunction
    endclass
    
    //generator类
    class generator;
        mailbox #(transaction) gen2drv;
    
        function new(mailbox #(transaction) gen2drv_new);
            if(gen2drv_new == null) begin
                $display("%0t -> generator : ERROR -> gen2drv is null", $time);
                $finish;
            end else begin
                this.gen2drv = gen2drv_new;
            end
        endfunction
    
        task run(int num_tr = 200);
            transaction tr;
            repeat(num_tr) begin
                tr = new();
                assert(tr.randomize()) else $error("Randomization failed");
                tr.calculate_expected(); // 计算预期输出
                $display("%0t -> generator : Generated new transaction", $time);
                tr.display();
                gen2drv.put(tr);
            end
        endtask
    endclass
    
    //driver类
    class driver;
        virtual dut_if.tb intf;
        mailbox #(transaction) gen2drv;
    
        function new(virtual dut_if.tb intf, mailbox #(transaction) gen2drv_new);
            this.intf = intf;
            if(gen2drv_new == null) begin
                $display("%0t -> driver : ERROR -> gen2drv is null", $time);
                $finish;
            end
            else
                this.gen2drv = gen2drv_new;
        endfunction 
    
        task run();
            transaction tr;
    
            forever begin
                gen2drv.get(tr);
                intf.a   = tr.a;
                intf.b   = tr.b;
                intf.cin = tr.cin;
    
                #1; // 给DUT一个小的延迟来稳定输出
                tr.sum = intf.sum;
                tr.cout = intf.cout;
                
                if (tr.compare()) begin
                    $display("%0t -> driver : PASS", $time);
                end else begin
                    $display("%0t -> driver : FAIL", $time);
                end
                tr.display();
            end
        endtask
    endclass
    
    // Environment类
    class environment;
        generator gen;
        driver    drv;
        mailbox #(transaction) gen2drv;
        virtual dut_if.tb vif;
    
        function new(virtual dut_if.tb vif);
            this.vif = vif;
            gen2drv = new();
            gen = new(gen2drv);
            drv = new(vif, gen2drv);
        endfunction
    
        task run();
            fork
                gen.run();
                drv.run();
            join_any
        endtask
    endclass
    
    // Top模块
    module top;
        dut_if intf();
        
        dut DUT (
            .a(intf.a),
            .b(intf.b),
            .cin(intf.cin),
            .sum(intf.sum),
            .cout(intf.cout)
        );
    
        initial begin
            environment env;
            env = new(intf.tb);
            env.run();
            #10000; // 运行一段时间后结束仿真
            $finish;
        end
    
        initial begin
            $dumpfile("dump.vcd");
            $dumpvars(0, top);
        end
    endmodule
    

这个程序是对的,结果可以运行,并且符合要求。

部分结果

在这里插入图片描述

既然程序能够运行,那就可以对这个程序进行记录,总结这个程序是如何搭建起来的了.

transaction模块

事务模块相当于火药,用于表示dut接收到的一些列信号组合,编写它有一定的规律,最重要的就是随机化和约束。要使用随机化以及约束需要randomize进行调用,本程序使用断言进行测试。另外,在事务里也可以定一些函数,用来进行数据显示之类的,以后要用到事务就可以直接显示信号数值,用以检验。

generator模块

按照我自己的习惯,编写完事务,接下来就是发射模块。它的含义就是将事务打包一下,将火药变成子弹。这时就要使用到sv一个很重要的概念——邮箱mailbox——它负责建立各个模块之间的联系通道。在现在的的程序中,建立了一个gen到drv的邮箱,叫gen2drv,声明之后,就要紧接着再加上一个构造函数,本程序中是下面的这个样子:

function new(mailbox #(transaction) gen2drv_new);
     if(gen2drv_new == null) begin
          $display("%0t -> generator : ERROR -> gen2drv is null", $time);
          $finish;
     end else begin
          this.gen2drv = gen2drv_new;
     end
endfunction

用于执行必要的初始化操作。比如在本例中,检查传入的mailbox是否为空,并据此终止仿真或继续执行。

this.gen2drv = gen2drv_new; 这句话的作用是将传入构造函数的参数 gen2drv_new 的值赋给当前实例的 gen2drv 成员变量。这是初始化类成员变量的常用方式,确保创建的每个对象都有其自己的 gen2drv 数据。

task run(int num_tr = 200);
     transaction tr;
     repeat(num_tr) begin
          tr = new();
.          assert(tr.randomize()) else $error("Randomization failed");
          tr.calculate_expected(); // 计算预期输出
          $display("%0t -> generator : Generated new transaction", $time);
          tr.display();
          gen2drv.put(tr);
     end
endtask

在每一个gen或者drv以及接下来的monitor或scoreboard中,都有自己的一个任务,表示这个模块主要要做什么。比如这里的gen就需要产生200个测试数据,将transaction打包发送出去200次。按照我的习惯,以及学习其他的一些完整工程,设定发射多少次的测试数据就可以在generator模块里进行设定(当然,这个名字怎么取你随意,反正就是把事务打包的模块)。

driver模块

driver模块就是枪了。将打包好的子弹发送给dut,和generator模块相似,需要接收从gen发来的邮箱,因此也需要声明一个gen2drv,和generator建立起联系。同时,作为与dut直接接触的模块,它需要直接使用interface,这也是本程序中接口第一次登上舞台。

task run();
     transaction tr;

     forever begin
          gen2drv.get(tr);
          intf.a   = tr.a;
          intf.b   = tr.b;
          intf.cin = tr.cin;

          #1; // 给DUT一个小的延迟来稳定输出
          tr.sum = intf.sum;
          tr.cout = intf.cout;
            
          if (tr.compare()) begin
              $display("%0t -> driver : PASS", $time);
          end else begin
              $display("%0t -> driver : FAIL", $time);
          end
          tr.display();
     end
endtask

driver模块的任务就是将子弹发射出去,所以使用gen2drv.get(tr);从generator模块持续获得打包好的transaction。注意,这里要用接口的方法去和dut建立起链接,例如intf.a = tr.a;,并且本程序还设定会接收dut返回的sumcout信号,用以临时充当检测器的效果。

environment模块

这就是枪开火的环境了,它的任务比较单一(只是程序看起来这样,实际上它很重要),就是让gen、drv模块依次执行各自设定好的任务,为它们发挥作用提供一个场地。

task run();
     fork
          gen.run();
          drv.run();
     join_any
endtask

毕竟,打枪不能随便打,必须得在适当的场所。

如此一来,一个临时性的验证环境就搭建好了。在top模块里,我们需要把dut和测试体系例化出来,如果是一个时序逻辑,我们还可以在top模块里添加一些clk信号的变化,就和用verilog编写testbench类似。

再增加记分板和监视器

有了上面的基础,我们现在就尝试进一步对这个验证程序进行完善,毕竟不能只驱动dut,我们还需要测试它输出的结果对不对。

完整工程第一版

  • 第一版程序

    // DUT模块
    module dut(
        input  logic [7:0] a,
        input  logic [7:0] b,
        input  logic       cin,
        output logic [7:0] sum,
        output logic       cout
    );
        assign {cout, sum} = a + b + cin;
    endmodule
    
    // 接口定义
    interface dut_if;
        logic [7:0] a;
        logic [7:0] b;
        logic       cin;
        logic [7:0] sum;
        logic       cout;
    
        modport tb (
            output a, b, cin,
            input  sum, cout
        );
    
        modport dut (
            input  a, b, cin,
            output sum, cout
        );
    endinterface
    
    // transaction类
    class transaction;
        rand bit [7:0] a;
        rand bit [7:0] b;
        rand bit       cin;
    
        bit      [7:0] sum;
        bit            cout;
        bit      [7:0] expected_sum;
        bit            expected_cout;
    
        constraint c_valid {
            a inside {[0:255]};
            b inside {[0:255]};
            cin inside {[0:1]};
        }
    
        function void calculate_expected();
            {expected_cout, expected_sum} = a + b + cin;
        endfunction
    
        function bit compare();
            return (sum == expected_sum) && (cout == expected_cout);
        endfunction
    
        function void display();
            $display("a = %h, b = %h, cin = %b | Expected: sum = %h, cout = %b | Actual: sum = %h, cout = %b", 
                     a, b, cin, expected_sum, expected_cout, sum, cout);
        endfunction
    endclass
    
    // generator类
    class generator;
        mailbox #(transaction) gen2drv;
    
        function new(mailbox #(transaction) gen2drv_new);
            if(gen2drv_new == null) begin
                $display("%0t -> generator : ERROR -> gen2drv is null", $time);
                $finish;
            end else begin
                this.gen2drv = gen2drv_new;
            end
        endfunction
    
        task run(int num_tr = 400);
            transaction tr;
            repeat(num_tr) begin
                tr = new();
                assert(tr.randomize()) else $error("Randomization failed");
                tr.calculate_expected(); // 计算预期输出
                $display("%0t -> generator : Generated new transaction", $time);
                tr.display();
                gen2drv.put(tr);
            end
        endtask
    endclass
    
    // driver类
    class driver;
        virtual dut_if.tb intf;
        mailbox #(transaction) gen2drv;
    
        function new(virtual dut_if.tb intf, mailbox #(transaction) gen2drv_new);
            this.intf = intf;
            if(gen2drv_new == null) begin
                $display("%0t -> driver : ERROR -> gen2drv is null", $time);
                $finish;
            end
            else
                this.gen2drv = gen2drv_new;
        endfunction 
    
        task run();
            transaction tr;
    
            forever begin
                gen2drv.get(tr);
                intf.a   = tr.a;
                intf.b   = tr.b;
                intf.cin = tr.cin;
    
                #1; // 给DUT一个小的延迟来稳定输出
                tr.sum = intf.sum;
                tr.cout = intf.cout;
                
                $display("%0t -> driver : Received DUT output", $time);
                tr.display();
            end
        endtask
    endclass
    
    // Monitor类
    class monitor;
        virtual dut_if.dut intf;
        mailbox #(transaction) mon2sb;
    
        function new(virtual dut_if.dut intf, mailbox #(transaction) mon2sb_new);
            this.intf = intf;
            if(mon2sb_new == null) begin
                $display("%0t -> monitor : ERROR -> mon2sb is null", $time);
                $finish;
            end else begin
                this.mon2sb = mon2sb_new;
            end
        endfunction
    
        task run();
            transaction tr;
            forever begin
                tr = new();
                #1; // 等待DUT稳定输出
                tr.a   = intf.a;
                tr.b   = intf.b;
                tr.cin = intf.cin;
                tr.sum = intf.sum;
                tr.cout = intf.cout;
                tr.calculate_expected(); // 计算期望结果
    
                $display("%0t -> monitor : Captured transaction", $time);
                tr.display();
                mon2sb.put(tr);
            end
        endtask
    endclass
    
    // Scoreboard类
    class scoreboard;
        mailbox #(transaction) mon2sb;
    
        function new(mailbox #(transaction) mon2sb_new);
            if(mon2sb_new == null) begin
                $display("%0t -> scoreboard : ERROR -> mon2sb is null", $time);
                $finish;
            end else begin
                this.mon2sb = mon2sb_new;
            end
        endfunction
    
        task run();
            transaction tr;
            forever begin
                mon2sb.get(tr);
                if (tr.compare()) begin
                    $display("%0t -> scoreboard : PASS", $time);
                end else begin
                    $display("%0t -> scoreboard : FAIL", $time);
                end
                tr.display();
            end
        endtask
    endclass
    
    // Environment类
    class environment;
        generator gen;
        driver    drv;
        monitor   mon;
        scoreboard sb;
        mailbox #(transaction) gen2drv;
        mailbox #(transaction) mon2sb;
        virtual dut_if.tb vif;
        virtual dut_if.dut dif;
    
        function new(virtual dut_if.tb vif, virtual dut_if.dut dif);
            this.vif = vif;
            this.dif = dif;
            gen2drv = new();
            mon2sb = new();
            gen = new(gen2drv);
            drv = new(vif, gen2drv);
            mon = new(dif, mon2sb);
            sb = new(mon2sb);
        endfunction
    
        task run();
            fork
                gen.run();
                drv.run();
                mon.run();
                sb.run();
            join_any
        endtask
    endclass
    
    // Top模块
    module top;
        dut_if intf();
    
        dut DUT (
            .a(intf.a),
            .b(intf.b),
            .cin(intf.cin),
            .sum(intf.sum),
            .cout(intf.cout)
        );
    
        initial begin
            environment env;
            env = new(intf.tb, intf.dut);
            env.run();
            #100000; // 运行一段时间后结束仿真
            $finish;
        end
    
        initial begin
            $dumpfile("dump.vcd");
            $dumpvars(0, top);
        end
    endmodule
    
    

第一版程序能够运行,但仍存在一些bug。目前的错误在于监视器和驱动之间无法匹配,无论增加多少次事务数量,仿真结果都是错。超过设定的仿真次数时,程序变卡着不动,这时才会显示pass,很明显无法达到验证要求。

部分结果

在这里插入图片描述

但我们仔细分析这个程序结果可以发现,设定的actual 的数据值延后于expected值,表明dut在获得输入进行输出时,抓信号的模块没有成功抓到稳定输出的结果。可能的原因在于dut的输出还没有稳定便进行信号索取,尝试可以适当增加延迟增加dut稳定时间。接下来,在driver和monitor模块中增加了一些延迟:

第二版程序部分

第二版程序对于第一版程序来说几乎没有任何的修改,仅在monitor模块的#3; // 将原本的1改为3这一句那里做了调整,将原本的等待1个时间改为了3个。

......monitor模块其余不变

task run();
        transaction tr;
        forever begin
            tr = new();
            #3; // 将原本的1改为3
            tr.a   = intf.a;
            tr.b   = intf.b;
            tr.cin = intf.cin;
            tr.sum = intf.sum;
            tr.cout = intf.cout;
            tr.calculate_expected(); // 计算期望结果

            $display("%0t -> monitor : Captured transaction", $time);
            tr.display();
            mon2sb.put(tr);
        end
    endtask
    
    ......其余不变

新的输出结果

在这里插入图片描述

成功让信号pass.并且可以看到,dut的稳定数据输出终于被成功捕捉,表示本次验证环境被搭建出来了。

monitor模块与scoreboard模块

新增加的这两个模块和前面讲的类似,由于dut功能简单,所以每一个模块的任务也很单一,monitor模块就是持续把dut输出的结果捕捉到,然后送给记分板,由记分板都数据结果进行判断,与预期的结果进行比较,从而判断是否满足了设计要求。

新的问题

虽然现在信号可以pass了,但仔细观察输出可以发现,部分结果没有完整显示,例如:
在这里插入图片描述

第129号测试数据只捕捉到driver模块,没有捕捉到monitor以及scoreboard模块信号,导致输出结果不全。大概率在测试逻辑上仍存在部分问题。后面可以尝试增加一些事件来增加判断的准确性。

总结

这是一个非常简单的dut验证环境搭建,由于没有用到时序逻辑,所以和真正的芯片系统验证还有非常大的差距,但对于我理解验证环境来说却能起到很好的效果。

经过这么一整套组合拳下来,我个人感觉,sv验证环境说起来就是把用verilog编写testbench更详细化,并且更加模块化。比如,最开始什么都不加,我只用到了interfere,顶层的验证环境和verilog编写testbench几乎没差,后面逐渐增加模块,也有种杀鸡用牛刀的感觉。这是因为dut本身太简单了,甚至几乎不需要单独编写一个测试模块就可以看出来它对不对。而如果核心dut功能复杂,牵扯到时序、总线等等问题,一般的testbench便很难满足要求了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值