基于SV简单的数字IC验证框架搭建


简介

    本文基于systemverilog搭建一个简单的验证框架(框架图如下所示),对于ic验证小白的入门指导。
    为什么要搭建这样一个验证平台,而不是对于DUT写个testbench就好了,对于这个问题,刚入门的我也有些疑惑。一般来说,我们的tsetbench只会设计DUT输入激励,关于DUT的输出相应我们一般都会直接通过仿真波形来人为查看结果是否正确,不正确再去改激励或者RTL设计,然后不断循环往复,直到所有结果与我们所预期那样,但这对于数字IC来说不切实际。为什么呢?因为一般来说数字ic所需要验证的端口会很庞大,你不可能人为的去一个一个设计激励并验证它是否正确,这将消耗大量时间。验证平台相当于创建一个随机激励并自动检测的机器,去检测你想检测的任何情况,并验证dut输出与你所预期是否相同。这或许在一些小型设计当中并不出众,一但设计规模大了之后,验证平台的效率会直线上升。
systemverilog_verification_structure


一、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就是一个包。
    本验证平台在事务类中定义了一个随机变量cin,这将在Generator随机化产生输入激励,变量sumcout不需要赋随机值,值传递DUT输出相应。事务中还封装了两个display函数,分别显示激励输入与相应输出。其实,transaction中还可以设置约束(constraint),不过本文中没有约束条件。

class transaction;//packet class
  
//simulus are declared with rand keyword
  rand 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 class
  mailbox gen2drv; //mailbox declaration
  
  function new(mailbox gen2drv); // creation of mailbox and constructor
    this.gen2drv = gen2drv;
  endfunction
  
  task main;
    repeat(1)begin
      trans = new();              //object for transaction class
      trans.randomize();          //randomization of transaction
      trans.display_in("Generator"); //checking purposew 
      gen2drv.put(trans);          //putting data into mailbox
    end
  endtask
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 interface
  
  mailbox gen2drv;  //handle of mailbox
  
  function new(virtual in_intf i_vif,mailbox gen2drv);
    this.i_vif=i_vif;
    this.gen2drv=gen2drv;
  endfunction
  
  task main;
    repeat(1)begin       
        transaction trans;  //handle of transaction class,to get the mailbox data
      	#1;
        gen2drv.get(trans);//getting trans data from mainbox
     
        i_vif.a   <= trans.a;
        i_vif.b   <= trans.b;
        i_vif.cin <= trans.cin;
        trans.display_in("Driver");
      end
  endtask
  
endclass

六、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 declaration
  
  mailbox mon2rml;  //declaration of mailbox
  
  function new(virtual in_intf i_vif,mailbox mon2rml);
    
    this.i_vif=i_vif;
    this.mon2rml=mon2rml;
       
  endfunction
  
  task main;
    repeat(1)begin
      transaction trans;  //handle of transaction class
      trans = new();      //consturctor or creating object for trans
      #2;
      trans.a     = i_vif.a  ;//sampling of data in monitor
      trans.b     = i_vif.b  ;
      trans.cin   = i_vif.cin;
      mon2rml.put(trans);
      trans.display_in("input_monitor");
    end
  endtask
  
endclass
class o_monitor;
  
  virtual out_intf o_vif; //virtual interface declaration
  
  mailbox mon2scb;  //declaration of mailbox
  
  function new(virtual out_intf o_vif,mailbox mon2scb);
    
    this.o_vif=o_vif;
    this.mon2scb=mon2scb;
       
  endfunction
  
  task main;
    repeat(1)begin

      transaction trans;  //handle of transaction class
      trans = 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");
    end
  endtask
  
endclass

七、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的完全功能。
    从下面代码中可以看出,定义了两个信箱mon2rmlrml2scb,一个用于接收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;
  endfunction
  
  task main;
    transaction r_trans;  //recv transaction from i_monitor
    transaction s_trans;  //send transaction to scoreboard      
    repeat(1)begin
        #3;
        mon2rml.get(r_trans);   // getting info from mailbox
        s_trans = new();
        
        //reference_model
        s_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  
  endtask
  
endclass

八、Scoreboard

    计分板(Scoreboard)是用于比较参考模型的数值与DUT运算后的值是否相同。一般来说来自参考模型的数值会比来自DUT输出的值要快,所以在不断循环产生约束随机激励的验证平台,来自参考模型的数值会不断累积,这时就需要在计分板中定义一个队列,用于存储先带到来的参考模型数值,一旦检测到DUT有一个输出到达,立即将队列中数值弹出进行比对,如此循环。本验证平台所设计的时等所有模块都执行完毕,在开启下一次的随机测试,所以没有设计队列由于缓存。
    由下面的代码可以看出,scoreboard模块只是设置了两个mailbox(rml2scbmon2scb),一个用于接收来自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;
  endfunction
  
  task main;
    repeat(1)begin
      transaction rml_tr; //handle of transaction class
      transaction o_mon_tr;        
      #4;
      rml2scb.get(rml_tr);   // getting info from mailbox
      mon2scb.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("---------------------------------------------------------------");
    end
  endtask
  
endclass

九、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 drv
  mailbox mon2rml;//i_mon to refe_model
  mailbox rml2scb;//refe_model to scoreboard
  mailbox mon2scb;//o_mon to scoreboard
  
  virtual 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);
    
  endfunction
  
  task test();
  	fork
      gen.main();
      driv.main();
      i_mon.main();
      rml.main(); 
      o_mon.main();	
      scb.main();
    join
  endtask
  
  task run;
    repeat(1)begin
    	test();
        #5;
    end
    $finish;
    
  endtask
  
endclass

十、Test

    Test如下所示只定义了一个程序块(program),声明并创建了一个env对象,并启动env里的run任务。

`include "environment.sv"
program test(in_intf  i_intf,out_intf o_intf);
	environment env;
    
  	initial begin
      env = new(i_intf,o_intf);
      env.run();
    end
  
endprogram  

十一、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。最后编译仿真结果如下图所示:
simulation
    由上图可以看出,generator产生随机激励为 a = 0, b = 1, cin = 1,然后DUT输出的结果是 sum = 0, cout = 1,reference_model算出来的结果也是 sum = 0, cout = 1,这就证实DUT本次验证例子正确,但还不能证明DUT功能设计正确,只有随机约束随机出所有可能的值且都验证正确时,才能算DUT功能设计正确。

  • 28
    点赞
  • 182
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
在异步FIFO验证中,可以使用SystemVerilog(SV)进行时序验证。以下是一些验证步骤的一般指南: 1. 设计规范:首先,你需要了解异步FIFO的设计规范。这包括FIFO的接口信号、时序要求和操作行为等方面的规定。 2. 编写测试环境:使用SV编写测试环境,包括FIFO的模型和其他必要的组件。测试环境应该能够生成激励信号以驱动FIFO,并捕获和分析FIFO的响应。 3. 设计测试用例:根据设计规范,设计一系列测试用例来验证FIFO的时序行为。测试用例应该涵盖各种情况,例如写入和读取操作的顺序、时序差异、写入和读取的数据量等。 4. 驱动信号:在测试环境中生成适当的信号以驱动FIFO进行操作。这包括写入数据、读取数据、控制信号(如使能和复位)等。 5. 断言验证:使用SV的断言机制编写断言来验证FIFO的行为是否符合设计规范。断言可以检查各种时序属性,如读写操作的顺序、数据完整性和时序关系等。 6. 波形分析:分析仿真波形,确保FIFO的行为符合预期。检查波形中的读取和写入操作是否按照正确的时序进行,以及数据是否正确传输和保存。 7. 覆盖率分析:使用覆盖率工具来评估测试用例的覆盖率,确保已经测试了FIFO的各种时序情况。覆盖率报告可以帮助你确定是否需要进一步调整测试用例或增加额外的测试。 8. 调试:如果发现测试用例中存在问题或FIFO的行为不正确,使用调试技术来定位和修复问题。这包括波形调试、打印调试信息、断言调试等。 以上是一般的步骤,实际的异步FIFO验证可能会根据具体设计和要求有所不同。这些步骤可以帮助你建立一个验证框架,并确保FIFO在时序方面的正确性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Iceeeewoo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值