路科验证V0课程实验指导
2022年9月28日 更新
开启更多功能,提升办公效能

设计模块介绍

实验中的待测试模块(DUT)是一个16输入,16输出的路由器,这个路由器的功能是把数据通过各个输入端(Input)发送到任意输出端(Output)。下面是最终完成的整个验证平台示意图。

打开实验文件,你将会看到如下实验代码的结构,其中rtl中为设计代码,labs中为原始代码,solutions中为参考答案。

实验一

在Lab1中我们应该掌握以下内容:

  1. 用SV给待测试模块(DUT)搭建最简单的测试平台(Testbench)。
  1. 用SV写一个任务(Task)来重置(Reset)DUT。
  1. 编译(Compile)和仿真(Simulate)这个SV程序。


我们需要建立的几个文件:顶层(Top)文件:router_test_top.sv ,接口(Interface)文件:router_io.sv ,待测试(DUT)文件:router.v , 测试(Test)文件:test.sv。在该实验完成时,你将得到如下验证结构。

任务一:创建SV接口(interface)文件

  1. 创建router_io.sv文件,并用编辑器打开它。
  1. 以下是需要连接到DUT的信号。

interface router_io(input bit clock);

logic reset_n ;

logic [1 5 :0] din ;

...

logic [1 5 :0 ] frameo_n ;

endinterface: router_io


注意:在这一部分的所有接口都是异步且没有方向的(例如input,output,inout就是有方向)。接口的方向只能在针对同步信号的时钟模块(Clocking block)或是针对异步信号的(modport)中被说明。


在下一步中,我们会去创建同步(synchronous)信号给测试程序(test program)从而可以驱动(drive)和采样(sample)DUT中的信号。


  1. 声明一个由时钟上升沿所驱动的时钟模块(Clocking block,以后简称CB)。这个时钟模块将会被测试程序用来实施同步驱动(drive)和采样(sample)。这个CB中的所有信号的方向(direction)必须和DUT中的信号方向一致。

 


  1. 如果需要的话,可以对input和output的(skew)添加说明。

  1. 最后创建一个modport TB(),来连接这个测试程序。在它的参数(argument)列表中,应该引用我们之前创建的时钟模块CB和其它所有可能会用到的异步(asynchronous)信号。

  1. 保存并关闭router_io.sv文件。

任务二:创建SV测试程序文件并重置(reset)路由器。

  1. 创建测试程序文件test.sv,并用编辑器打开它。
  1. 在这个文件中,引用接口模块中的modport TB作为参数(argument),来将interface和test program连接在一起。

program automatic test (router_io . TB rtr_io) ;

endprogram: test


  1. 在这个程序(program)模块中,定义一个任务(Task):Reset(),实现重置DUT的功能。



reset_n既可以是同步信号也可以是异步信号

  1. 在初始化模块中(initial begin),调用reset()任务来重置DUT。


  1. 保存和关闭test.sv文件。

任务三:创建SV测试的壳文件(即TOP文件)

  1. 创建和打开router_test_top.sv文件。
  1. 输入如下基本结构。

  1. 给Top文件中添加接口的实例化(instance)

  1. 实例化这个测试程序。(通过将测试程序在top中的例化t 和接口在top中的例化top_io相联系,将test程序和Top连接在一起)

  1. 将待测试模块和top_io连在一起,实现DUT与Top的连接。

此时你有没有发现呢?Top文件中经过三次例化成功将DUT文件,test program文件,interface文件包含了起来。

  1. 添加`timescale和$timefornat到Top中。

  1. 保存并关闭router_test_top.sv文件。

任务四:编译(compile)和仿真(simulate)

此时,我们一共拥有四个文件:待测试文件:router.v,接口文件:router_io.sv ,测试文件:test.sv , 顶层文件:router_test_top.sv。

接下来我们就将文件库中的文件进行编译和仿真,一起来学习一下在Questasim中如何建立一个项目、添加设计文件和验证文件、完成编译和开始仿真吧。

  • 建立项目
  • 新建项目名称File -> New -> Project,在project name处填入名称例如lab1


  • 添加文件和编译
  • 在project窗口单击右键,选择Add to Project -> Existing File,找到下载的lab1实验文件。


  • 编译文件。

  • 仿真与添加波形
  • 在Library -> Work中找到顶层router_test_top,点击右键选择“Simulate without Optimization”,这种仿真模式是为了消除仿真器可能会加入的一些优化处理。


  • 选中top_io,在objects一栏中可以看到它的端口和内部信号。



  • 接下来在instance ->router_test_top ->top_io上点击右键,选择Add Wave,这是为了在仿真时存储波形。
  • 在命令窗口中敲入命令“run 1us”或者在工具栏中运行1us。

  • 观察波形


到这里我们整个验证的最基本框架就建立起来,以后我们会不断向里面填充东西,并将它们标准化,在整个过程结束后我们可以建立起一个完整的验证平台。


至此,Lab1我们就顺利完成了。

实验二

Lab2的学习目标是:

  1. 拓展lab1中的测试平台,从一个输入端向一个输出端发送数据包。
  1. 用新的测试平台来编译和仿真设计文件。

在这次Lab中,你将继续去搭建测试平台上的相关组件(component):激励产生器(Stimulus Generator),驱动器(Driver)等。你会使用一些的子程序(routine)去把一个数据包从输入端口3发送到输出端口7,并观察到这个数据包的有效负载(payload)。在该实验完成时,你将得到如下验证结构。


任务一: 声明程序的全局变量

根据路由器的需求说明(包括使用哪个输入端口和哪个输出端口,以及发送什么样的数据),来发送一个数据包。为了让这些变量更容易被参考到,你将会把它们声明为程序的全局变量(program globals)。

  1. 使用编辑器打开已存在的test.v文件。
  1. 对这个数据包声明程序全局变量:

bit [3:0] sa; // source address (input port)

bit[3:0] da; // destination address (output port)

logic[7:0] payload[$];

任务二:产生数据包

在lab1中,你通过调用reset()子程序来配置(Configure)待测试模块。接着你创建一个Task:  gen()来继续发展这个测试平台。(gen()是一个可以产生测试激励的子程序)

  1. 在程序的initial块中的reset()后面,调用gen()任务。
  1. 在任务reset()代码块后面,声明gen()任务。

  1. 在gen()任务的体(body)中
  1. 设置sa(源地址)为3,da(目标地址)为7。
  1. 用2到4范围内的随机字节来填满有效负载序列(payload queue)。

当一切完成后,你的代码将是下面的样子:

任务三:创建发送数据包的程序(routine)

在数据包的信息产生后,你准备好创建Transactor和Driver来发送数据包。

把一个数据包发送通过路由器的行为,可以通过以下三个进程来完成:

  1. 发送Destination Address(目标地址)
  1. 发送padding bits。
  1. 发送payload。

其中每一个进程将发展成为driver routine中的一个独立部分(device)。Driver中各部件和Transactor的区别是:当Transactor调用 device driver时,device driver和硬件信号是直接相互作用的。

 

这些抽象的分层可以使testbench的子程序(routine)更容易管理,更好复用,更加可靠。

以下的步骤可以帮助你构建这些子程序

  1. 在initial块中,在gen()后面立即调用send()任务来发送数据包。
  1. 在调用send()后仿真10个时钟周期。
  1. 在program中添加一个对任务send()的声明。
  1. 在send()任务中,包括如下操作。
  1. 调用send_addrs()
  1. 调用send_pad()
  1. 调用send_payload()
  1. 创建send_addrs()任务。这个Driver的部件将会驱动4位的地址进入Router。
  1. 在send_addrs()中,包括这样的操作。
  1. 驱动frame_n信号作为每一个路由器的配置内容。
  1. 驱动目标地址通过din信号传送。
  1. 把din作为单bit的连续信号。使用一个环结构来驱动din,以每周期一bit的速度循环四个周期。
  1. 创建send_pad()任务。这个device driver可以驱动五个pad bits进入路由器。
  1. 在send_pad()任务中,会驱动frame_n,valid_n和din信号作为每一个路由器的配置(specification)。
  1. 创建send_payload()任务。这个device driver发送payload到路由器中。
  1. 在send_payload()任务的体中,有如下操作:
  1. 写一个可以执行“payload.size()”次的循环。
  1. 这个循环内的payload[$]序列中,每8-bit数据被以一个周期一个bit的速度进行传送。
  1. 记着在每次路由器的配置中驱动valid_n。
  1. 在每次路由器的配置中,在发送到这个数据包的最后一位时,将frame_n信号反转至1’b1。
  1. 当数据包发送完成后,将valid_n信号返回到1’b1。
  1. 保存和关闭test.v文件。

任务四:对program进行编译和纠错(Debug)

任务五:拓展program到可以发送21个数据包

  1. 修改program模块,使用相同的sa=3和da=7,发送21个数据包。
  1. 编译(Compile),仿真(Simulate),查看波形。
  1. 同学们尝试着多次重新启动仿真,可以使用“restart”命令来重启,再对比连续两次生成的随机数据,看看它们之间是否相同呢?然后再在仿真器命令行处使用命令“vsim -novopt -solvefaildebug -sv_seed 0 work.router_test_top”来加载仿真。这里我们多传递了两个必须的仿真参数,-solvefaildebug是为了调试随机变量的,而-sv_seed NUM则是为了传递随机的种子。那么使用这个命令再看,是否与之前没有使用-sv_seed 0的命令产生了相同的数据呢?最后,请改为使用“vsim -novopt -solvefaildebug -sv_seed random work.router_test_top”命令再来比较前后两次的数据,是否相同呢?那么,你对-sv_seed random的仿真选项的认识是什么?


至此,Lab2我们就顺利完成了。

实验三

今天的Lab3的学习目标是:

  1. 构造一个从路由器输出端进行取样的监视器(Monitor)。
  1. 构造一个可以验证路由器输出的比较器(Checker)。
  1. 运行Driver和Monitor程序,检验Checker能否正确比数。 


在该实验完成时,你将得到如下验证结构。

任务一:创建顶层测试环境Top-Level Test Environment

  1. 编辑已存在的test.sv文件。
  1. 给pkt2cmp_payload[$]序列添加一个全局声明(global-declaration),位宽为8bit(logic[7:0])。这个序列将会被用来存储来自于DUT的取样数据。
  1. 为了能够进行自我核对,修改程序使recv()和send()可以并行进行,并在其后添加一个自我核对程序check()。

任务二:实现一个监视器Monitor

  1. 声明一个recv()命令。
  1. 在recv()体中调用get_payload()来获取有效负载。(在目前这是这是recv()程序中的唯一内容,在后面的实验中我们会在添加一些内容。)
  1. 声明get_payload()任务。
  1. 在get_payload()中,删除掉pkt2cmp_payload[$]的内容。(删掉来自之前数据包的可能剩余物是十分有必要的)
  1. 继续在get_payload()之中,等待输出端frame信号的下降沿。

  1. 通过在路由器的输出端口取样来继续完善get_payload():
  1. 保持循环直到frame信号被检测到。
  1. 在循环中,至少8个时钟周期内聚合起一个字节(8bit)的数据。接着将每一个8位数据存储到pkt2cmp_payload[$]序列中去。
  1. 如果payload没有连成一个byte,就打印错误信息并终止仿真。

任务三:发展比较器(Checker)

这一步是为了发展一个比较器从而去检查路由器的输出。

  1. 创建一个compare()函数,它可以返回一个bit的值并且可以进行数据比较。
  1. 在compare()中,比较payload[$]队列(用来参考的数据)和pkt2cmp_payload[$] 队列取样的数据)中存储的数据来验证接收到的payload是否正确。
  1. 如果payload[$]序列和pkt2cmp_payload[$]队列的长度不匹配的话,设置描述该错误的语句,并返回0值结束该子程序。
  1. 如果在payload[$]队列和pkt2cmp_payload[$]队列的数据不匹配,设置描述该错误的语句,并返回0值和结束该子程序。(你可以用“==”来直接比较两个序列)。
  1. 如果在payload[$]队列和pkt2cmp_payload[$]队列中的数据是完全一致的,设置描述比较成功的语句,并返回1值和结束该子程序。
  1. 创建check()任务。
  1. 在check()体中,声明一个string(字符串)变量和一个计数器。
  1. 在check()体内,调用compare函数来核对接收到的数据。
  1. 如果一个错误被发现了,则打印错误信息并终止仿真。
  1. 如果核对是成功的,则打印一个信息来表明比较成功和查到的数据包的数量。
  1. 保存并关闭test.v文件。

任务四:编译和仿真

任务五:测试所有端口

  1. 修改你的测试程序来随机产生sa(源地址)和da(目标地址)。
  1. 将你的Testbench拓展到可以发送2000个数据包。
  1. 编译和仿真你的程序。
  1. 确定仿真依然可以完全成功。

实验四

Lab4的学习目标是:

  1. 将数据信息封装进入Packet类中
  1. 利用随机化(randomization)在packet类中随机产生源地址,目标地址和payload。
  1. 创建两个packet对象(object),一个包用来在DUT输入端输入,另一个包用来和DUT输出的数据相参照。
  1. 将compare()方法嵌入packet类,用来验证DUT工作的正确性。


在这一次的实验中你将会把数据包的相关信息封装到一个类的结构中去。你将会在Generator中产生随机的Packet对象然后发送,接收,最后核对DUT使用这些Packet对象使用的正确性。


 加入Packet object的测试平台结构如下图

任务一:创建一个数据包类(class)文件

(用packet类来封装数据包的信息)

  1. 用编辑器打开Packet.sv文件。
  1. 用下面的方式声明一个类的定义:      

class Packet;

endclass: Packet

  1.  通过在类语句(statement)的前面和后面使用宏作为标记来防止多次编译。


  1.  在Packet类的体中,创建如下的属性:

(不同的数据包对象将会被gen()程序产生,我们需要区分这些不同的数据包对象,因此我们引入string name。) 

任务二:定义数据包的属性约束

  1. 在属性声明之后,添加一个约束模块来限制sa和da在0至15之内,和payload.size()在2至4之内。

任务三:定义数据包类方法的雏形

  1. 在数据包类的体中,添加以下的方法声明:

任务四:定义数据包类new()构建函数

new()函数,即构建函数,是被用来初始化对象并为它开辟空间。对于这个Packet对象来说,大部分性质将会调用randomize()来进行设置,但是name这一性质需要在new()中初始化。

  1. 在Class()体的外部,实现new()方法。确定通过::这一接口,可以使这个构建函数映射到开始的那个类中。
  1. 在结构体内部

这个机制能让这个string到达packet class的所有其他方法中去。

任务五:定义Packet的方法compare()

为了自我验证,经常会去比较两个数据对象的内容。在数据对象中建立compare方法是一个很好的主意。

  1. 从test.v中剪切和粘贴compare( )到Packet类中。
  1. 参考上面用 :: 标记的的方式,完成compare()方法。
  1. 修改argument列表来包含一个Packethandle:

function bit Packet::compare(Packet pkt2cmp, ref string message);

  1. 在compare( )方法内部,参照这个类的性质来改写pkt2cmp.payload。

任务六:定义数据包的方法display()

能打印出一个数据包的内容,在debug过程中十分有用。为了让这一方式简化,一个display的方法应该被定义在这个Packet类中。这样就可以在控制台上打印出Packet对象的内容。

  1. 在类外,创建display()方法.

  1. 在方法内,打印你想输出的内容。
  1. 保存并关闭packet.sv文件。 

任务七:修改test.sv来使用Packet类

这个packet类现在封装了路由器数据包的信息。

你将会产生随机Packet对象,接着会在这些随机数据包对象的基础上通过路由器发送和接受数据包。

  1. 用编辑器打开test.sv。
  1. 在program块内部,添加一个include语句,从而将Packet class包含进来。
  1. 创建和构造两个program全局数据包对象,pkt2send和pkt2cmp。 

任务八:修改gen()任务去产生数据包对象

  1. 在gen()任务内,删掉所有已存在的代码。
  1. 声明一个静态int变量能够记录Generator产生了多少个数据包。
  1. 将pkt2send的name设置为一个唯一的字符串变量。
  1. 随机化数据包对象pkt2send。如果随机化失败,打印一条错误信息后停止程序并终止仿真。
  1. 更新所有program全局变量。

这一步让pkt2send对象的内容对所有部件(component)是可见的。这些program的全局变量是一个暂时的解决方案。在下一个lab中,我们将删除这些变量,并将它们移植到testbench中不同部件的内部。

当你完成这些时,你的代码应该如下所示:

任务九:修改recv()任务

在recv()任务中,我们从路由器输出端取样payload时,需要将它们集合到一个数据包类中(pkt2cmp)。这个数据包对象将会被用来检查pkt2send对象。

在recv()任务中:

在调用get_payload任务前要做以下的事情:

  1. 创建一个静态int型变量pkt_cnt来记录接收到的数据包的数量。

在调用get_payload之后要做的事情:

  1. 连接pkt2cmp.da和程序全局变量da。
  1. 连接pkt2cmp.payload和pkt2cmp_payload。
  1. 为pkt2cmp对象设置一个唯一的name。

当完成上面的操作后,这个recv()任务应该是像这样的:

任务十:修改check()任务

在check()任务中,你将把compare()放入Packet对象中去验证发送和接收到的内容是否正确。为了debug,你应该在这个packet对象中使用display()方法。

  1. 将compare()方法放入packet对象中,从而取代以前的compare()。(你想比较的两个对象是全局变量pkt2send和pkt2cmp。)
  1. 当探查到错误时,使用display()方法。

当完成操作后,这个check()的程序应该如下所示:

任务十一:检查和保存文件

  1. 确保你已经删除掉了test.sv文件中的compare()程序。
  1. 保存和关闭test.v文件。

任务十二:编译和仿真


实验五

Lab5的学习目标是:

  1. 创建一个Generator transactor的类。
  1. 创建一个Driver的类。
  1. 创建一个Reciever的类。
  1. 拓展这个测试平台,并将Driver和Monitor同时连接到所有输入端和输出端。

在lab4中,你创建了一个封装好的数据包。但是,在一个时间内只能把数据包从一个输入端驱动到另一个输出端。

在这次的lab中,你会把generator,driver,monitor和checker等程序封装进Generator类,Driver类,Receiver类,Scoreboard类中。同时,你会创建一个testbench架构,从而将上面所有部分都容纳进来。

为了促进Packet对象从一个组件传到另一个组件,你将会使用Mailbox作为一个交流机制。

最终结构如下图所示:

任务一:发展Driver类

在之前的实验中,我们已经用一个Driverbase类封装了driver程序和program的全局变量。你将扩展这个基础类从而去产生一个新的Driver类。

  1. 用编辑器打开已经存在的DriverBase.sv 文件。
  1. 检查Driverbase类。

在这个基础类中,下面的属性需要被声明。

 

对于driver中的每一个方法,有一个if-$display()在子程序的开始处。在debug时,它可以帮助打印出子程序所执行的序列。TRACE_ON是一个program全局变量,你稍后将会在test.sv文件中声明它和控制它。

  1. 关闭DriverBase.sv文件。
  1. 用编辑器打开Driver.sv文件。

这个Driver类源于DriverBase类。其中In_box将会用来从Generator向Driver发送Packet对象。Out_box将用来从Driver向Scoreboard发送Packet对象。In_box和out_box都是pkt_mbox的类型。

下面是一个定义在router_test.h文件中的邮箱(mailbox)类型:


typedcef class Packet;

typedef mailbox # (Packet) pkt_mbox;


这个sem[ ]序列将会被当作一个防止多个输入端同时给同一个输出端发送数据包的仲裁机制。new( )方法在参数列表中有五个变量。name,sem[], in_box,  out_box, port_id。start( )方法可以从in_box中获取Packet对象。

任务二:将Driver类中的new( )填写完整

  1. 在new( )中,调用name,rtr_io参数的super.new( )。
  1. 在调用super.new()后添加一个踪迹(tracing)语句.

  1. assign 属性sa和port_id。
  1. 通过assign属性sem[],in_box,out_box等,来完成new()函数的拓展。

任务三:将Driver类中的方法start()填写完整

在这个Driver中,start()方法是在一个无限的循环中执行的。在这个循环的每一次执行中,都会从In_box中获取一个packet对象。这个数据包对象的内容将会通过send()发送至DUT。一旦这个packet对象的发送过程完成后,这个packet对象会被发送到计分板。

Driver的对象开始后,期望testbench中的所有其它组件可以同时运行。Start()方法中的trace语句都要在一个non-blocking的fork-join结构中。

  1.  在已存在的start()体内,添加一个trace语句。
  1. 在trace语句后,创建一个non-blocking的fork-join模块。
  1. 在fork-join结构里面,创建一个单独的无限循环。
  1.  每一个循环中将会做以下事情:
  1.  从in_box中获取packet对象。
  1. 如果sa与获取的packet对象的this.sa中的不匹配,继续下一次循环。
  1. 如果获取的packet sa与this.sa匹配,用pkt2send中的内容更新da和payload类。
  1.  为了到达正确的目的地址,使用sem[ ]序列去仲裁。
  1. 一旦仲裁成功,调用send()去驱动数据包通过DUT。
  1. 当send()完成后,发送数据包对象到out_box。
  1. 在循环的最后一步,将semaphore的key返回给它的bin。
  1. 保存和关闭Driver.sv。 

任务四:发展一个Receiver

  1. 用编辑器打开已经存在的Receiver.sv框架文件。

任务五:填写Receiver类的方法new()

  1. 在new()中,调用name和rtr_io为参数的super.new()。
  1. 在调用的super.new()后添加一个tracing语句。
  1. 将类的属性da的值通过port_id发出去。
  1. 将out_box 的属性通过参数列表传递出去。

任务六:填写reciever类中的方法start()

在start()方法中将会执行一个非阻塞的无限循环。每次循环中,都会从DUT中重新构建一个packet对象。一旦重新获取后,这个Packet对象将会通过发出邮箱(out mailbox)发送到scoreboard。

  1. 在start()方法的体中,添加一个trace语句。
  1. 在这个trace语句之后,创建一个非阻塞(non-blocking)的并行的线程(processthread)。
  1. 在fork-Join结构内部,创建一个单独的无限循环的代码块。
  1. 每一次循环中都会做以下的事情:
  1. 调用recv()来从DUT中重新获取packet对象。
  1. 将从DUT中获取的packet对象复制一份到out_box中去。
  1. 保存和关闭Receive.sv文件。 

任务七:检查generator类

已经准备好了Generater.sv文件,它已经封装了你之前完成的gen()程序和一个start()方法。

这个start()方法和前面的相比有两个重大的不同。一是:这个start()方法的循环由系统全局变量run_for_n_packets控制。如果run_for_n_packet<=0,则这个循环是无限的。如果它>0, 则这个循环在循环这么多次后会停止。二是:在调用方法gen()之后,会创建一个随机化packet对象(pkt2send)的拷贝,并且通过out_box邮箱发送到 Driver。

任务八:检查scoreboard的类

scoreboard.sv文件已经为你写好了,它主要封装了你之前已经写好的check()程序。这个类的新特征是可以实现Scoreboard,Driver和 Receiver之间的通信。Driver在发送数据包对象到DUT中时,也会将它发送到driver_mbox邮箱。一个Receiver在从 DUT接收到数据时,也会发送packet对象到receiver_mbox邮箱中去。

当scoreboard在receiver_mbox中发现一个packet对象时,它会把这个对象存储为pkt2cmp中去。接着,推动所有从driver_mbox中获得的 packet对象到refPkt[$] 序列中去。然后,在pkt2cmp对象中的输出端口地址(da)的基础上,将会查找在refPkt[$]序列的相应位置上的packet,并比较它们的内容。如果发现存在不匹配的 packet,就报告一个错误。

当packet对象的数量与全局变量run_for_n_packets相匹配的话,一个叫DONE的事件 flag会被触发。这个DONE flag将会让整个仿真在合适的时间结束。

任务九:修改test.sv来使用这些新类

你将会修改这个testbench,使它拥有一个Generator,一个Scoreboard,16个Driver和16个Receiver。

testbench整体结构:

  1. 用编辑器打开test.sv文件。
  1. 删除所有全局变量除了run_for_n_packets。
  1. 创建int型变量 TRACE_ON并且初始化为0(当想去debug时将其变为1,从而让子程序执行tracing)。
  1. 在两个全局变量后面,添加include语句来包括所有头文件和这些新的类文件(router_test.h,Driver.sv,Receiver.sv,Generator.sv, Scoreboard.sv)
  1. 在这些include语句后面,添加如下的程序全局变量:

  1. 删除所有initial块中的内容除了run_for_n_packets = 2000; 
  1. 构造所有声明在program块中的对象,确保这些邮箱都是正确连接的:

Generator到所有Driver(gen.out_box[i]);所有Driver到一个Scoreboardmailbox(sb.driver_mbox); 所有Receiver到一个Scoreboardmailbox(sb.receiver_mbox)。

  1. 在所有对象都被声明之后,调用reset()来重置DUT。
  1. 接着开始所有事务(transactor)。
  1. 最后,在program结束之前,设置scoreboard“DONE”的事件flag。 

任务十:混合检查

  1. 下面子程序应该从test.sv文件中删除

(确保reset()没有被删除)

 当完成以上步骤时,你的test.sv文件应该如下所示:

保存和关闭test.sv文件.

任务十一:编译和仿真

至此,Lab5已经完成啦。


实验六

Lab6的学习目标是:

  1. 实现功能覆盖率,从而可以决定仿真何时结束。


在lab5中我们还遗留了一个问题:究竟发送多少数据包才能测试到所有输入端口和输出端口的连接? 根据我们已有的随机激励代码并不能解决这个问题。你需要引入功能覆盖率(functional coverage)。

在这次的lab中,你将会给scoreboard类中添加功能覆盖组件。这个功能覆盖率可以来衡量你的testbench的测试进度,并且当你的testbench在完全检查到了所有输入端和输出端的连接后终止仿真。

任务一:在scoreboard类中创建一个covergroup

在SV中引入功能覆盖率要做的第一件事:就是定义coverage group。在coverage group中,coverage bins,update event,timing 和coverage goal都应该被定义。

应该给每一个输入端和输出端都创建coverage bins。然后,cross 所有输入端和输出端的coverage bins。

  1. 用编辑器打开Scoreboard.sv文件。
  1. 添加两个新的类属性。

bit[3:0] sa,da; //functional coverage properties

  1. 在这两个属性的声明之后,声明一个对cover group(router_cov)的定义。
  1. 在这个cover group内部
  1. 根据sa和da来创建coverpoint groups。
  1. 在两个取样group上来创建 cross bins(这个cross coverage是我们在寻找的real 覆盖率信息。)


任务二:修改new()来构建coverage对象

  1. 在new()的结构体中,构造router_cov。当完成后,covergroup的定义应跟以下一样。

任务三:为了覆盖率修改check()

  1. 在方法check()中,添加一个新的真实变量 coverage_result(这个数据类型一定是real,因为功能覆盖率的结果是作为真实意义(real values)返回的。)。将会在这个变量中存储跑出的功能覆盖率 (%)。
  1. 在pkt2send和pkt2Cmp的比较成功之后,设置类变量 sa 和da来衡量在 pkt2send 对象中的值。
  1. 接着,调用router_cov.sample()来触发功能覆盖率bin的更新。
  1. 调用$get_coverage()来重新获取更新后的功能覆盖率的值,并将他们存储在 coverage_result中去。
  1. 修改 $display()中的语句,使其可以打印覆盖率(%) 。
  1. 将if语句修改为:if 覆盖率到达了100%作为event flag来触发DONE的发生。
  1. 保存和关闭scoreboard.sv文件。

任务四:编译仿真并查看覆盖率报告

本节的任务是确保达到100%的覆盖率。 在必要的情况下增加包的数量, 调试发现的任何错误。

  1. 编译程序
  1. 运行

在仿真窗口(transcript)中,可以参考路桑的仿真命令:

vsim -i -classdebug -solvefaildebug -coverstore E:/sv_labs/lab6/data -sv_seed random work.router_test_top

其中-coverstore COVERAGE_STORAGE_PATH:这个命令是用来在仿真在最后结束时,生成覆盖率数据并且存储到COVERAGE_STORAGE_PATH。你可以自己制定COVERAGE_STORAGE_PATH,但需要注意路径名中不要包含中文字符

·        

接下来在仿真窗口敲入命令“run -all”,在仿真最后自动结束时会弹出仿真要求结束的对话框“Are you sure you want to finish?”,你可以点击NO,但一定要选择菜单栏”Simulate -> end simulation”来结束本次仿真。只有你结束了本次仿真,你才会得到覆盖率数据×××.data。

3.查看覆盖率

在本次仿真过程中或者结束时,你可以利用仿真器直接查看功能覆盖率。首先你需要选中View -> Coverage -> Covergroups。

在新添加的“Covergroups”窗口中查看本次仿真所收集到的功能覆盖率。

4.合并覆盖率

你可以参考上面的仿真步骤,多次运行,它们都会生成独一无二的数据库。接下来,你就可以将之前统一在COVERAGE_STORAGE_PATH下面生成的xxxx.data覆盖率数据做合并了。你可以在Questasim的仿真窗口中敲入命令

·         vcover merge -out merged_coverage.ucdb E:/sv_labs/lab6/data

这里标注黄色的部分依然代表COVERAGE_STORAGE_PATH。这个命令即是将你之前产生的若干个xxxx.data的覆盖率合并在一起,生成一个合并在一起的覆盖率文件。

接下来,你可以点击File -> Open来打开这个合并后的UCDB覆盖率数据库(注意选择文件类型UCDB就可以看到这个文件了)。当你打开这个数据库之后,你可以发现合并后的数据库要比之前单独提交的任何一个测试在仿真结束时的该次覆盖率都要高。你可以在covergroups窗口栏中查看功能覆盖率。

5.分析覆盖率

你可以依旧使用Questasim来打开UCDB利用工具来查看覆盖率,或者更直观的方式是在打开当前覆盖率数据库的同时,生成HTML报告。选择Tools -> Coverage Report -> HTML,按照下图所示进行勾选:

单击OK后,Questasim就会帮助你生成一份详尽的HTML覆盖率文档。

至此,所有Lab都已经完成啦。