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

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

Tags
IC验证
Published
August 3, 2024
Author
内容摘要
标签
是否记得
知道但不熟
⚠️
注意区分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

仿真结果

notion image
最基础的版本和用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
这个程序是对的,结果可以运行,并且符合要求。

部分结果

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

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,很明显无法达到验证要求。

部分结果

notion image
但我们仔细分析这个程序结果可以发现,设定的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 ......其余不变

新的输出结果

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

monitor模块与scoreboard模块

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

新的问题

虽然现在信号可以pass了,但仔细观察输出可以发现,部分结果没有完整显示,例如:
notion image
第129号测试数据只捕捉到driver模块,没有捕捉到monitor以及scoreboard模块信号,导致输出结果不全。大概率在测试逻辑上仍存在部分问题。后面可以尝试增加一些事件来增加判断的准确性。

总结

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