本文主要是介绍基于SV简单的数字IC验证框架搭建,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
基于SV简单的数字IC验证框架搭建
- 简介
- 一、DUT
- 二、Interface
- 三、Transaction
- 四、Generator
- 五、Driver
- 六、Monitor
- 七、Reference_model
- 八、Scoreboard
- 九、Environment
- 十、Test
- 十一、Testbench
- 十二、输出结果
简介
本文基于systemverilog搭建一个简单的验证框架(框架图如下所示),对于ic验证小白的入门指导。
为什么要搭建这样一个验证平台,而不是对于DUT写个testbench就好了,对于这个问题,刚入门的我也有些疑惑。一般来说,我们的tsetbench只会设计DUT输入激励,关于DUT的输出相应我们一般都会直接通过仿真波形来人为查看结果是否正确,不正确再去改激励或者RTL设计,然后不断循环往复,直到所有结果与我们所预期那样,但这对于数字IC来说不切实际。为什么呢?因为一般来说数字ic所需要验证的端口会很庞大,你不可能人为的去一个一个设计激励并验证它是否正确,这将消耗大量时间。验证平台相当于创建一个随机激励并自动检测的机器,去检测你想检测的任何情况,并验证dut输出与你所预期是否相同。这或许在一些小型设计当中并不出众,一但设计规模大了之后,验证平台的效率会直线上升。
一、DUT
DUT全称为Design Under Test,本设计由于是刚入门验证小白搭建验证平台,所以DUT设计只是一个全加器,目的是为了解整个验证框。架设计RTL代码如下所示。其中a与b为两个加数,cin为进位输入,cout为进位输出,sum为加法和。真值表如下图所示:
module adder(a,b,cin,sum,cout);input a,b,cin;
output sum,cout;assign {cout,sum} = a + b + cin;endmodule
二、Interface
大家在设计多层RTL代码时可以发现,有些信号可能需要流经几个设计层次,它必须一遍又一遍地被声明和连接。最糟糕是如果想添加一个新的信号,又需要在多个文件中定义与连接。这会大大增加连线出错几率,并且使得跟踪、调试和维护变得更加繁琐。
而System Verilog使用接口(interface)为块之间的通信建模,接口可以看作一捆智能的连线,它可以包含任务、函数、参数、变量、功能覆盖率和断言,使我们在模块内通过接口监控和记录事务。由于该信息被封装在接口中,因此无论它具有多少端口,连接到设计也变得更加容易。
在模块中(module)声明interface
是可以的,但是在类中(class)直接声明会报错,在类中声明interface
应该在前面加上virtual
,如果不使用关键字 “virtual
” 那么在多次调用该接口时,因为在其中的一个实例中对接口中某一信号的修改会影响其他实例接口,如果使用了 “virtual
” 关键字,那么每个实例是独立的。所以我们要习惯在除了模块中的其他地方声明都应使用virtual。在本验证框架中,为了直观看出效果,定义了两个接口,一个是用于DUT输入(in_intf
),还有一个用于DUT输出(out_intf
)。两个接口定义如下:
interface in_intf();logic a;logic b;logic cin;
endinterface
interface out_intf();logic sum;logic cout;
endinterface
三、Transaction
OOP的核心概念就是把数据和相关的方法封装成一个类,事务(transaction
)就是基于这样的思想建立起来的。一个简单的DUT监视器可能只在接口上采样几个数值,如果将他们简单地保存在整数变量中然后传递给下一级,这样可能在一开始节省一点时间,但最终你还是需要将这些数值组合到一起以构成一个完整的事务。这些事务中的几个可能需要被组合成更高级别的事务,例如DMA事务。所以,应该立刻将这些接口数值封装成一个事务类,这样,你就可以在保存数据的同时保存相关的信息(端口号、接收时间),然后将该对象传递给测试平台的其他部分。
一般来说,物理协议中的数据交换都是以帧或者包为单位的,通常在一帧或者一个包中要定义好各项参数,每个包的大小不一样。很少会有协议是以bit或者byte为单位来进行数据交换的。以以太网为例,每个包的大小至少是64byte。这个包中要包括源地址、目的地址、包的类型、整个包的CRC校验数据等。transaction就是用于模拟这种实际情况,一笔transaction就是一个包。
本验证平台在事务类中定义了一个随机变量a
、b
与cin
,这将在Generator
随机化产生输入激励,变量sum
与cout
不需要赋随机值,值传递DUT输出相应。事务中还封装了两个display函数,分别显示激励输入与相应输出。其实,transaction
中还可以设置约束(constraint
),不过本文中没有约束条件。
class transaction;//packet class//simulus are declared with rand keywordrand bit a;rand bit b;rand bit cin;bit sum;bit cout;function void display_in(string name);$display("-------------------------");$display("[%0t]ns %s ",$time,name);//$display("-------------------------");$display("a = %0d, b = %0d, cin = %0d",a,b,cin);$display("-------------------------"); endfunction function void display_out(string name);$display("-------------------------");$display("[%0t]ns %s ",$time,name);//$display("-------------------------");$display("sum = %0d, cout = %0d",sum,cout);$display("-------------------------");endfunction
endclass
四、Generator
当RTL 设计越来越大时,要产生一个完整的激励集来测试的设计功能也变得越来越困难了。可以编写一个定向测试集来检查某些功能项,但当一个项目的功能项成倍增加时,编写足够多的定向测试集就不可能了。解决办法就是采用受约束的随机测试法(CRT)自动产生测试集。定向测试集能找到你所认为可能存在的Bug,CRT方法通过随机激励,可以找到你都无法确定的Bug。可以通过约束来选择测试方案,只产生有效的激励,以及测试感兴趣功能项,本验证平台Generator就是用来产生受约束的随机激励。
Generator为DUT产生受约束随机激励类,代码如下所示,首先创建generator的new函数,并声明传入参数类型信箱(mailbox
),mailbox为线程间的通信,除此之外还有事件(event
)和旗语(semaphore
),通过 this.gen2drv = gen2drv;
将该类中变量赋值为传入参数,信箱通过put与get将事务(transaction
)send与recive。随后创建main任务,方便在env类中直接调用此任务。main任务中首先声明transaction句柄,然后创建一个transaction对象(trans = new();
),然后将事务随机化(trans.randomize();
),并用信箱发送随机激励后的transaction(gen2drv.put(trans);
)。
class generator;transaction trans ;//Handle of Transaction classmailbox gen2drv; //mailbox declarationfunction new(mailbox gen2drv); // creation of mailbox and constructorthis.gen2drv = gen2drv;endfunctiontask main;repeat(1)begintrans = new(); //object for transaction classtrans.randomize(); //randomization of transactiontrans.display_in("Generator"); //checking purposew gen2drv.put(trans); //putting data into mailboxendendtask
endclass
五、Driver
Driver为驱动模块,目的是将Generator产生的激励传输到DUT中。当时我有个疑惑,为什么不将Generator模块与Driver放到一起,这样看起来不更简洁美观吗。但是验证测试平台是基于system verilog的面向对象编程,也就是说将重复造作的行为放到一个类中,而Driver只是驱动事务到输入接口(in_intf
)上、Generator只是产生随机激励,如果想改约束直接改Generator中设计就行,不用动Driver模块,如果放到一起会大大增加工作量,以上仅是我个人理解。
Driver代码如下所示,在new
函数内参数i_vif
为驱动到DUT端口的激励,gen2drv
为接受generator
传输过来的transaction
。main任务中还有个#1;
的延时,为了还原真实传输路径,实现从激励产生模块到接受模块数据传输延时。实际上也可以设计event
在generator与driver模块中,当generator模块生成激励并put到信箱中,触发event
,driver中检测到generator的触发event
,开始接收信箱中数据,并继续下一步传输与处理。这里用延时代表driver
已经接收到generator
生成激励信号。之后将激励传输到DUT 接口中(i_vif.a <= trans.a; i_vif.b <= trans.b; i_vif.cin <= trans.cin;
)。
class driver;virtual in_intf i_vif; //vif is a handle of virtual interfacemailbox gen2drv; //handle of mailboxfunction new(virtual in_intf i_vif,mailbox gen2drv);this.i_vif=i_vif;this.gen2drv=gen2drv;endfunctiontask main;repeat(1)begin transaction trans; //handle of transaction class,to get the mailbox data#1;gen2drv.get(trans);//getting trans data from mainboxi_vif.a <= trans.a;i_vif.b <= trans.b;i_vif.cin <= trans.cin;trans.display_in("Driver");endendtaskendclass
六、Monitor
验证平台必须时刻监测DUT的行为,只有知道DUT的输入与输出信号变化之后,才能根据这些信号变化来判定DUT的行为是否正确。
Monitor为验证平台的监控模块,即监控DUT的输入与输出,这里为了好区别,分别设计DUT输入监控(i_monitor
)与DUT输出监控(o_monitor
)。输出监控很好理解,那为什么还要多设计一个输入监控,为什么不直接将driver里的事务驱动到后面的reference_model模块呢?书上是这样说的:第一,在一个大型的项目中,driver根据某一协议发送数据,而monitor根据这种协议收集数据,如果driver和monitor由不同的人员实现,那么可以大大减少其中任何一方对协议理解的错误;第二,在实现代码重用时,使用monitor是非常有必要的。
下面的代码中,在i_monitor
传入DUT输入接口(i_vif
),并将从接口上检测到的数据,封装到transaction
中,通过mailbox
发送给reference_model
模块;在o_monitor
传入DUT输出接口(o_vif
),并将从接口上检测到的数据,封装到transaction
中,通过mailbox
发送给scoreboard
模块。
class i_monitor;virtual in_intf i_vif; //virtual interface declarationmailbox mon2rml; //declaration of mailboxfunction new(virtual in_intf i_vif,mailbox mon2rml);this.i_vif=i_vif;this.mon2rml=mon2rml;endfunctiontask main;repeat(1)begintransaction trans; //handle of transaction classtrans = new(); //consturctor or creating object for trans#2;trans.a = i_vif.a ;//sampling of data in monitortrans.b = i_vif.b ;trans.cin = i_vif.cin;mon2rml.put(trans);trans.display_in("input_monitor");endendtaskendclass
class o_monitor;virtual out_intf o_vif; //virtual interface declarationmailbox mon2scb; //declaration of mailboxfunction new(virtual out_intf o_vif,mailbox mon2scb);this.o_vif=o_vif;this.mon2scb=mon2scb;endfunctiontask main;repeat(1)begintransaction trans; //handle of transaction classtrans = new(); //consturctor or creating object for trans#2;trans.sum = o_vif.sum;trans.cout = o_vif.cout;mon2scb.put(trans);trans.display_out("out_monitor");//trans.display("output_Monitor");endendtaskendclass
七、Reference_model
Reference_model模块用于完成和DUT相同的功能。reference_model的输出被scoreboard接收,用于和DUT的输出相比较。DUT如果很复杂,那么reference_model也会相当复杂。DUT是用Verilog写成的时序电路,而reference model则可以直接使用systemverilog高级语言的特性,同时还可以通过DPI等接口调用其他语言来完成与DUT相同的功能。虽然本文的DUT比较简单,reference_model也很简单,但对于ip级与系统级的reference_model来说,整个验证模块应该算reference_model是最难的,毕竟要完全复现DUT的完全功能。
从下面代码中可以看出,定义了两个信箱mon2rml
与rml2scb
,一个用于接收i_monitor
发送来的事务,一个用于给最终的scoreboard
模块。这里接收driver所驱动的随机数a、b、cin,这里参考模型是根据真值表画出卡罗图、算出sum与cout与其关系建立起来的,然后将通过参考模型计算后的数值发送给scoreboard进行与DUT输出比对,查看DUT功能是否真确。
class reference_model;mailbox mon2rml; mailbox rml2scb;function new(mailbox mon2rml,mailbox rml2scb);this.mon2rml = mon2rml;this.rml2scb = rml2scb;endfunctiontask main;transaction r_trans; //recv transaction from i_monitortransaction s_trans; //send transaction to scoreboard repeat(1)begin#3;mon2rml.get(r_trans); // getting info from mailboxs_trans = new();//reference_models_trans.sum = r_trans.a ^ r_trans.b ^ r_trans.cin;s_trans.cout = (r_trans.a&r_trans.b)|(r_trans.b&r_trans.cin)|(r_trans.a&r_trans.cin);rml2scb.put(s_trans);r_trans.display_in("reference_model");s_trans.display_out("reference_model");end endtaskendclass
八、Scoreboard
计分板(Scoreboard)是用于比较参考模型的数值与DUT运算后的值是否相同。一般来说来自参考模型的数值会比来自DUT输出的值要快,所以在不断循环产生约束随机激励的验证平台,来自参考模型的数值会不断累积,这时就需要在计分板中定义一个队列,用于存储先带到来的参考模型数值,一旦检测到DUT有一个输出到达,立即将队列中数值弹出进行比对,如此循环。本验证平台所设计的时等所有模块都执行完毕,在开启下一次的随机测试,所以没有设计队列由于缓存。
由下面的代码可以看出,scoreboard模块只是设置了两个mailbox(rml2scb
、mon2scb
),一个用于接收来自reference_model
,一个用于接收o_monitor
。然后比对两transaction中的sum与cout输出是否相同,并打印结果。
class scoreboard;mailbox rml2scb; mailbox mon2scb; function new(mailbox rml2scb,mailbox mon2scb);this.rml2scb = rml2scb;this.mon2scb = mon2scb;endfunctiontask main;repeat(1)begintransaction rml_tr; //handle of transaction classtransaction o_mon_tr; #4;rml2scb.get(rml_tr); // getting info from mailboxmon2scb.get(o_mon_tr);rml_tr.display_out("scoreboard_reference");o_mon_tr.display_out("scoreboard_actrue");if(rml_tr.sum==o_mon_tr.sum | rml_tr.cout==o_mon_tr.cout) //reference model$display("Result is as Expected");else$display("Error Result!");$display("---------------------------------------------------------------");endendtaskendclass
九、Environment
Environment类将前面所有的类包含到其类中,统一创建接口与信箱,为前面所有模块中的数据传输建立连接,至此,所有模块才算真正紧密联系。然后定义个run任务,将前面所有类中的main任务并行执行。这里run任务只循环执行一次,为了突出效果,可以将repeat(1)中参数改高点可以看出效果。
`include "transaction.sv"
`include "generator.sv"
`include "driver.sv"
`include "i_monitor.sv"
`include "o_monitor.sv"
`include "reference_model.sv"
`include "scoreboard.sv"class environment;generator gen ;driver driv ;i_monitor i_mon;o_monitor o_mon;reference_model rml ;scoreboard scb ;mailbox gen2drv;//gen to drvmailbox mon2rml;//i_mon to refe_modelmailbox rml2scb;//refe_model to scoreboardmailbox mon2scb;//o_mon to scoreboardvirtual in_intf i_intf;virtual out_intf o_intf;function new(virtual in_intf i_intf,virtual out_intf o_intf);this.i_intf = i_intf;this.o_intf = o_intf;gen2drv = new();mon2rml = new();rml2scb = new();mon2scb = new();gen = new(gen2drv);driv = new(i_intf,gen2drv);i_mon = new(i_intf,mon2rml);rml = new(mon2rml,rml2scb);o_mon = new(o_intf,mon2scb);scb = new(rml2scb,mon2scb);endfunctiontask test();forkgen.main();driv.main();i_mon.main();rml.main(); o_mon.main(); scb.main();joinendtasktask run;repeat(1)begintest();#5;end$finish;endtaskendclass
十、Test
Test如下所示只定义了一个程序块(program),声明并创建了一个env对象,并启动env里的run任务。
`include "environment.sv"
program test(in_intf i_intf,out_intf o_intf);environment env;initial beginenv = new(i_intf,o_intf);env.run();endendprogram
十一、Testbench
Testbench例化了addr模块,并将接口连接到模块端口,且例化了test程序,启动自动执行env中run任务。
`include "input_interface.sv"
`include "output_interface.sv"
`include "test.sv"module tbench_top;in_intf i_intf();out_intf o_intf();test t1(i_intf,o_intf);adder h1(.a(i_intf.a),.b(i_intf.b),.cin(i_intf.cin),.sum(out_intf.sum),.cout(out_intf.cout));endmodule
十二、输出结果
本验证平台可以在EDA playground上搭建验证,EDA playground上可以在线编译仿真SV、UVM等数字IC所需验证环境,而且方便快捷,真是学习数字IC验证的神仙网站!本验证例子EDA playground网址为systemveriog_verification。最后编译仿真结果如下图所示:
由上图可以看出,generator产生随机激励为 a = 0, b = 1, cin = 1,然后DUT输出的结果是 sum = 0, cout = 1,reference_model算出来的结果也是 sum = 0, cout = 1,这就证实DUT本次验证例子正确,但还不能证明DUT功能设计正确,只有随机约束随机出所有可能的值且都验证正确时,才能算DUT功能设计正确。
这篇关于基于SV简单的数字IC验证框架搭建的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!