SV组件实现篇之一:激励器的驱动(上)

从这一章开始,我们将进入验证各个组件的实现部分。对于各个组件介绍的顺序也将遵循于《验证的结构篇》,从激励器、到监测器再到比较器,而牵系到的知识点也会以各个验证师实现的方式来介绍,这么做的目的是为了告诉读者SV的特性出色在什么地方,以及如何思考从0到1的过程。《SV验证组件的实现篇》旨在于通过对比不同的实现方式,做出优劣或者应用场景的判别,从而为读者梳理SV语言的各种特性。

 

在组件实现的讨论中,我们会主要基于SV基本特性,深入浅出来介绍这些特性的应用。例如,对于类的介绍,我们不会陷入类本身的特种语法参考,而是比较实用类来封装组件同模块来实现组件的异同和优劣,通过这种思考来从根本上认识类。又比如,对于组件之间的通信,我们会介绍进程间主要通信的方式,这些通信特性的方法不在本书的重点,而这些通信的具体应用场景则会更细致的阐明。

 

通过对SV各种语言特性的思辨性探讨,我们为大家展开的不是一间“武器库”,而是一架“钢琴”。因为作者相信任何一门语言的特性不是与生俱来的,而是为了实现各种目的最终和谐奏鸣的,所以我们期待读者们最终可以谈好这架钢琴,而不是只从武器库里挑选一门新式的大炮用来打一只苍蝇。

 


stimulator(激励器)作为生成激励的源,究竟应该以一种什么方式来运作?试想一下,之前在Verilog又或者VHDL语言中的testbench,也可以产生出合适的激励,那么为什么另辟蹊径单独细说stimulator的实现呢?为了解释这个问题,我们首先看看老式stimulator的不足:

  • 如果激励接口复杂,那么各项激励之间的协调也较为困难,而激励本身也可能于设计时钟输入采样存在竞争的问题。

  • 激励时序难以调整,因为测试方式是平叙的方式写在进程中的。

  • 激励向量固定,因为是直接测试方式。

 

而我们从这一节开始则分别要解决上述问题,即通过合理的驱动、数据和功能的封装和激励自身的随机化来解决老式stimulator的不足。

 

在之前我们通过介绍interface来认识利用它来做验证组件与DUT之间的联系,而连接好以后,激励也会通过接口向DUT传递。同时给出符合接口协议的激励,我们也需要遵循接口的时序关系。对于verifier董 来说,他是这么安排他的stimulator和interface的。

 

在上一篇中verifier董 准备利用上面接口与对应的stimulator连接,我们来看看他是如何实现stimulator stm_ini以及同interface ini_if在testbench中的连线。

 

激励驱动的方法

与regs_ini_if相连接的stimulator ini_stim,verifier董将其声明为module。

 

由于stm_ini在内部声明了多个方法(methods),即op_wr、op_rd、op_idle和op_parse,且它们会对硬件信号做驱动。在深入这些方法之前,需要先声明几个变量v_cmd、v_cmd_addr、v_cmd_data_w,这是因为方法内部的非阻塞赋值只能引用logic类型或者reg类型,而无法直接对stm_ini的端口(wire类型)做赋值。

 

为了使得激励上的数据发送更加清晰,我们又对总线数据类型trans_t做了定义:

 

这么做的好处在于将每次发送激励的数据集结为一个数据体,无论从数据发送还是后面monitor的数据采集都有帮助。在对新的数据类型定义之后,我们需要声明一个数组并且为其做初始化。

 

上面声明了一个数组ts,其有三个成员,接下来的initial块将为其在仿真开始进行初始化。由于ts[0]是写操作,所以需要为其中三个成员赋初值,而ts[1]是读操作,只需要为两个成员赋初值,作为第三个成员ts[2]因为是空闲操作,因为只需要为其成员cmd赋初值。

在这里的操作命令,我们将其声明为了局部参数(localparam),如我们之前介绍过的,我们还可以通过宏定义(macro)、枚举(enum)或者常量(const)来做类似的声明。

接下来我们对各个对应的激励方法做出解释。

 

首先方法op_wr有一个参数trans t,如果没有标明传递方向,默认是为输入端(input trans t)。该方法从名字可以读出是为写命令的操作,即在时钟的上升沿会将变量t中的t.cmd、t.cmd_addr和t.cmd_data_w分别写入只硬件信号中,最终来触发一次写操作。

 

读方法也是在时钟上升沿将t中的t.cmd和t.cmd_addr写入至硬件信号中。

 

空闲操作则无需参数,只需要将v_cmd置为IDLE,其余信号赋值为0。

 

上面的三种方法分别对应着写操作、读操作与空闲操作,那么在调用这些方法之前,我们需要对之前声明了的数组ts内每个成员类型做出解析,根据其成员的操作类型来判断应该调用的操作方法。这里,需要再声明一个方法op_parse,其根据参数的命令类型来判断应该最终调用哪种指令操作方法,如果是无效指令则会通过系统函数$error(MSG)来报告错误。

 

通过上面定义的方法,verifier董就可以对其定义的trans数组ts做出逐个解析,并且最终将数据转换为硬件信号加以驱动。在stm_ini最后又声明了一个initial块来产生最终的激励:

 

任务和函数

在上面stm_ini中定义了若干种方法:op_wr、op_rd、op_idle和op_parse,他们在定义时均声明为了task(任务),而不是function(函数)。相比较软件编程语言定义的方法均为函数类型,而非任务类型,我们有必要比较这两种方法类型的异同,使得用户清楚在何种场合定义和调用。

 

task与function的参数列表中均可以声明多个input(输入)、output(输出)、inout(输入输出)和ref(引用)类型。input即从外部拷贝传入的形式参数,output即由被调用方法产生拷贝传递给外部的形式参数,inout则表示分别在进入方法和退出方法是分别被拷贝了两次,而ref则类似于软件中的指针,在调用方式时不会有任何的拷贝行为,而是直接引用或者修改外部传入的数据对象。

 

inout和ref类型均可以使得形式参数在方法中内部被调用,并且将结果输出给外部。它们之间的不同是,inout只有在方法结束之后才会传递到外部,而ref可以在方法执行过程中直接修改数据对象无需等待方法执行结束。

 

在SV中,为了使用数据对象“指针”的便捷,并不会有像C语言中指针使用“*”寻址的方法。像下面的例子中,声明了拷贝函数op_copy,有两个参数,函数将把s中的数据直接拷贝给t。在这里,t被声明为ref类型,在实际使用中使用t = s则表明引用的数据被直接赋值,而用户不需要为何时该使用像C语言中的“*”来寻址,op_copy因此可以直接对外部传递进来的参数t本身操作。

 

这里需要注意的是,ref一般是对非句柄(类指针)类型的数据对象使用,而在后面介绍介绍了类和句柄之后,方法调用时则只需要传递句柄,无需ref类型声明。

 

输出结果:

 

除此之外,function还有以下的属性:

  • 默认的数据类型是为logic,例如 input [7:0] addr。

  • 数组可以作为形式参数传递。

  • function可以返回或者不返回结果,如果返回即需要用关键词return,如果不返回则应该在声明function时采用void function()

  • 只有数据变量可以在形式参数列表中被声明为ref类型,而线网类型则不能被声明为ref类型,这是为了防止通过ref的方式来修改线网信号值,也可以理解为可以通过输入形式参数采样线网数值,而不能通过ref来直接修改线网数值。具体修改线网信号的方法我们会在接下来stimulator通过interface输出激励的方式来介绍。

  • 在使用ref时,有时候为了保护参数使得只被读取不被写入时,可以通过const的方式来限定ref定义的参数。

  • 在声明参数时,可以给入默认数值,例如 input [7:0] addr = 0,同时在调用时如果省略该参数的传递,那么默认值即会被传递给function。

 

与function相比,task有以下几点不同:

  • task无法通过return返回结果,因此只能通过output、inout或者ref的参数来返回。

  • task内可以置入耗时语句,而function则不能。常见的耗时语句包括@event、wait event、# delay等。

 

通过上面的比较,我们对function和task建议的使用方式是:

  • 对于初学者,傻瓜式用法即全部采用task来定义方法,因为它可以内置常用到的耗时语句。

  • 对于有经验的使用者,请今后对这两种方法类型加以区别,在非耗时方法定义时使用function,在内置耗时语句时使用task,这么做的好处是在遇到了这两种方法定义时,就可以知道function可能运用于纯粹的数字或者逻辑运算,而task则会被运用于需要耗时的信号采样或者驱动场景。

  • 如果要调用function,则function和task均可在内部调用;而如果要调用task,则我们建议使用外部的task来调用,这是因为如果被调用的task内置有耗时语句,则外部调用它的方法类型必须为task。

posted on 2017-12-05 17:59  guolongnv  阅读(694)  评论(0)    收藏  举报