打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
【转】第七篇 SV环境构建篇
SV环境构建篇
一、数据类型

从这一部分开始我们将进入SystemVerilog的语言学习和应用。

在进入SV(SystemVerilog)之前,如果读者已经学习过Verilog语言,那么对我们接下来的的从Verilog到SV过渡的部分会容易一些;如果读者之前也没有接触过Verilog语言,也不需要担心。我们对于SV的三个篇章将会带你在学习完这三章之后,懂得如何搭建测试平台、以及掌握SV的核心语法、产生测试场景和完成数据比对

之所以在Verilog的基础上扩展出新的语言SV,为的是构建一种专用的验证语言,而之前的硬件设计描述语言Verilog和VHDL并不具备如今SV在验证方面的语言特性优势,这些优势包括有:
  • 抽象的数据结构描述可方便于更高层面的验证需要。
  • 面向对象的软件编程方式提供了更好地模块性、封装性和复用性。
  • 将用于验证部分的语言属性用完全软件化的构建方式实现,使得验证一侧独立于设计一侧。
  • 约束化随机激励可提高递归测试的收益。
  • 功能覆盖率收集可量化功能验证点使得验证进度更易于反映。
  • 属性检查提供专用分支语言属性。

上述的优点我们也将会在接下来的SystemVerilog围绕着DUT——MCDF的验证实例展开:
  • SV的环境构建篇
  • SV的组件实现篇
  • SV的系统集成篇

在构思SV的语言介绍过程中,我们认真翻阅了现有的中英文资料和教材,所见到的书籍介绍语言的方式都主要将编程语言作为一门学科进行语法上面的分类。这种方式对于初学者的入门会有帮助,而在具体实践的环节,好多书籍又无法给读者一个全面的轮廓和一种循序渐进的直观感受。所以,本书选择了在SV的介绍部分一直围绕着验证环境建立和实现的具体问题,将核心语言结合着具体场景,让读者可以边学边掌握该语法、特性可以运用的场景。最后在读者学完SV部分之后,可以利用所学的语言知识自助构建出验证平台。

我们在介绍语言的过程中,语言的语法特点仅仅是一部分,更重要地在于使得读者可以懂得该语言特性被应用的场景,实际疑难点在什么地方。因为入门容易精通难,如果读者手头已经有一本趁手的SV学习入门资料,且将本书关于SV的部分作为SV的进阶应用资料。




相比于Verilog将寄存器(register))类型reg和线网(nets)类型例如wire区分地如此清楚,在SV中新引入了一个数据类型logic。它们的区分和联系在于:
  • Verilog作为硬件描述语言,倾向于设计人员自身懂得所描述的电路中哪些变量应该被实现为寄存器,而哪些变量应该被实现为线网类型。这不但有利于后端综合工具,也更便于阅读和理解。
  • SV作为侧重于验证的语言,并不十分关切logic对应的逻辑应该被综合为寄存器还是线网,因为logic被使用的场景如果是验证环境,那么它只会作为单纯的变量进行赋值操作,而这些变量也只属于软件环境构建。
  • logic被推出的另外一个原因也是为了方便验证人员驱动和连接硬件模块、而省去考虑究竟该使用reg还是wire的精力。这既节省了时间,也避免了出错的可能。

与logic相对应的是bit类型,它们均可以构建矢量类型(vector),而它们的区别在于:
  • logic为四值逻辑,即可以表示0、1、X、Z。
  • bit为二值逻辑,只可以表示0和1。

那么SV为什么在已经有了四值逻辑的基础上还要再引入二值逻辑呢?那就是SV在一开始设计的时候,就期望将硬件的世界与软件的世界分离开。在这里,硬件的世界值得就是硬件设计,所以四值逻辑属于那里,而软件的世界即验证环境,更多的是二值逻辑。所以,有了二值逻辑,验证环境在进行数据运算的时候不但会提高效率,而且还会省去其它不必要考虑的问题。

在这里,我们将四值逻辑的类型和二值逻辑的类型分别摘列出来,请读者在使用时务必注意:
  • 四值逻辑类型:integer、reg、logic、reg、net-type(例如wire、tri)
  • 二值逻辑类型:byte、shortint、int、longint、bit

而通过logic和bit来声明的矢量均为无符号(unsigned)变量,例如:

可以从仿真器中得到的结果是:
如果按照有符号和无符号的类型进行划分,那么可以将常见的变量类型划分为:
  • 有符号类型:byte、shortint、int、longint、integer
  • 无符号类型:bit、logic、reg、net-type(例如wire、tri)

所以,读者在遇到常见的这些变量类型时,应该注意它们的逻辑类型和符号类型,因为在变量运算中,应该尽量避免两种不一致的变量进行操作,而导致意外的错误。
譬如从下面的例子里,我们可以看到有符号变量和无符号变量混用的运算结果会出乎意料:

仿真输出结果为:
我们这里来分析一下:
  1. 一开始 signed_vec 被赋值为 8'b1000_0000,表达为有符号十进制数值为-128。
  2. 在第一次赋值操作时 result_vec = signed_vec,右侧的有符号数值-128被赋值到左侧,并且需要从8位扩展为9位,且保证有符号数值不变的情况下,首先需要将8'h80扩展为9'h180(均为-128),进而在赋值到左侧。
  3. 在第二次赋值操作时,我们首先进行了类型转换操作 unsigned'(signed_vec),则转换结果应为十进制数值128,所以在赋值操作以后result_vec = unsigned'(signed_vec),result_vec同signed_vec就比特位的数值没有发生变化,但是实际表达的十进制数值则从-128被赋值为128。

所以,通过上面的例子我们可以发现,在编码时一定要注意操作符左右两侧的符号类型是否一致,如果不一致,应该首先将其转换为同一类型再进行运算。

对于转换方式,我们已经上面已经展示了一种转换方式——静态转换,即需要在转换的表达式前加上单引号即可,而该方式并不会对转换值做检查。如果发生转换失败,我们也无从得知,所以与之对应的动态转换$cast(tgt, src)也被经常运用到转换操作中。静态转换和动态转换均需要操作符号或者系统函数介入,我们统称为显式转换。动态转换的具体使用方法我们会在随后关于类和对象的章节着重介绍。

而不需要进行转换的一些操作,我们称之为隐式转换,例如下面的例子:


仿真结果为:
不难发现有两个问题:
  1. 被转换的变量为四值逻辑矢量,而被赋值的变量为二值逻辑矢量,且位宽不同。
  2. 在隐式转换中,x_vec[2:0]被保留下来,x_vec[3]则被丢弃,同时x_vec[0]的值'x'在转换过程中被转换为0值,即赋值的最终结果为b_vec = 'b110

从上面的示例中,我们在不同数据类型进行操作时应该注意变量的:
  1. 逻辑数值类型
  2. 符号类型
  3. 矢量位宽

二、模块定义与例化
在展开验证环境的构建之前,我们需要先了解模块的端口定义以及在SV环境下的例化。在这里, 我们以MCDF(multi-channel data formatter)中的寄存器模块ctrl_regs为例,来看看常见的模块定义方式有哪些。

模块定义
Verilog 模块定义1


Verilog 模块定义2

上面的两种定义方式是Verilog设计常见的做法,区别在于端口的方向可以在端口声明时定义,或者在端口声明完毕后再定义。我们再来看一看,如果用VHDL来定义ctrl_regs的接口会怎么来定义。

VHDL 模块定义1


VHDL 模块定义2


从上面两种VHDL端口定义的方式来看,第一种VHDL定义方式与之前的Verilog定义方式一致,而第二种定义方式需要作特别说明。

由于VHDL的数据类型中,有record类型,该类型作为硬件定义初衷是为了做硬件信号集束(signal collection)的。例如,上面首先定义了一个包mcdf_pkg,而在其中定义了两种数据类型:reg2fmt_t和reg2arb_t。随后,在ctrl_regs端口定义时,使用了这两种数据端口类型,进而使得ctrl_regs模块送给formatter模块的信号被集束在一个新定义的数据类型中。
对于稍后我们会提到的模块例化,如果MCDF的各个模块均为VHDL定义描述的,那么对于接口类型是record定义的模块在于其它相邻模块连接时,可以通过相同的record类型用来做信号对接,也可以通过信号分散赋值的形式进行连接。

我们需要额外注意的是,如果遇到了Verilog模块与VHDL模块的连接,或者Verilog模块中例化VHDL的时候,需要对VHDL record类型进行特别处理。

所以,我们接下来进入模块的例化部分。这里,我们将ctrl_regs的testbench称之为ctrl_regs_tb,首先我们需要对ctrl_regs进行例化。

模块例化
对verilog ctrl_reg2的例化


对于VHDL的 ctrl_reg3的例化也同上面对ctrl_regs2的例化,而需要注意的是,如果SV或者Verilog作为顶层,来例化含有record类型接口的时候,我们建议通用的方法是,验证人员需要首先新建立一个VHDL wrapper来作为一个盒子用来将ctrl_regs4的两种record类型接口reg2fmt_oreg2arb_o进一步转化为通用的std_logic_vector类型,即ctrl_reg3的接口形式。否则,如果在SV或者Verilog内部直接例化含有record类型接口,经常会有接口类型无法匹配的编译问题。

对于VHDL record接口类型在Verilog中例化的问题,一些工具厂商如MentorGraphics的仿真工具QuestaSim提出了Verilog/VHDL/SV数据定义包混用的编译选项-mixedsvvh,用来做工具一侧的支持,而有些工具商则没有提出类似方案。所以就测试平台的移植性来看,我们依然建议通过首先改造VHDL record接口,再者进行验证平台内DUT实例化的方式来进行。

参数使用
在IP设计中,经常会遇到参数化的模块。例如,我们可以将ctrl_regs模块的地址宽度和数据宽度参数化,从而得到这样的端口定义:

参数化端口定义


对此,我们可以在模块例化时再决定端口的宽度,例如:


上面的例子在模块例化时,将地址宽度addr_width修改为16,而保持了数据宽度data_width的默认值32。

参数修改
对设计和验证环境引入参数的优点在于,通过参数可以更方便地调整结构和数据类型,而不需要修改对象内部的定义。

除了我们上面提到的模块参数可以在例化时修改以外,我们还可以在什么时候对参数加以修改呢?在讨论之前,我们首先需要阐述关于目前主流HDL仿真器编译仿真代码的过程。通过对比不同仿真器(simulator),我们可以统一将代码编译运行的过程分为三个部分:
  • 编译阶段(compilation):工具通过阅读目标代码,进行语法和语义分析分析,将每个模块分别编入库中(library)。
  • 建模阶段(elaboration):工具将各个模块按照设计集成关系最终组成顶层模块。这一过程包括了各个模块(module)的例化、接口(interface)例化、程序(program)例化、层次集成、计算参数、解决层次信号引用、建立模块连接等。这一过程发生在了编译阶段之后,仿真阶段之前,类似于软件编译的link阶段。
  • 仿真阶段(simulation):通过读取建模阶段的对象文件,建立硬件RTL模型和验证环境,通过周期驱动(cycle-driven)或者事件驱动(event-driven)的方式来进行仿真。

所以从数据修改的手段上来看,参数修改可以在编译阶段修改即通过模块例化时的参数传入的方式进行,也可以在建模阶段通过工具提供的参数修改的elaboration命令选项来修改。参数无法在仿真阶段进行修改。

不同仿真器对于这三个阶段的执行方式是不相同的,例如QuestaSim会首先执行compilation将各个模块编译到库中,而接下来的仿真阶段实际上会首先进行内置的elaboration环节,所以参数可以在仿真阶段通过命令行修改。而VCS则将上述三个阶段独立开来,使得compilation与elaboration可以通过仿真前的命令行单独执行,而simualtion阶段则直接运行已经建立好的模型。

事先清楚不同仿真器对于上述阶段的处理,就明白了针对不同的仿真器何时可以修改参数。QuestaSim需要在仿真阶段修改,而VCS需要在独立的elaboration阶段修改。

宏定义
除了parameter的方式,我们还可以通过宏定义的方式进行参数化设计。例如上面的例子ctrl_regs5可以修改为宏定义的方式:

针对宏定义的形式,用户如果要修改上面端口的宽度,必须在compilation阶段完成。这使得通常,针对宏定义的形式,我们会将公共使用的宏存放在公共的空间作为头文件(header file),在编译之前通过修改宏的定义,或者调用不同的头文件来决定端口宽度,或者别的设计和环境参数。

通过认识如何进行模块的例化,我们已经可以在首先将DUT置入到测试平台中,下一节课我们将对SV的重要特性——接口(interface)进行讨论,从而掌握如何通过接口让硬件部分DUT与随后的验证环境相连接。
三、接口
在认识了DUT ctrl_regs的接口定义以及如何进行例化以后,我们接下来就需要考虑如何在testbench中给ctrl_regs添加需要的激励。从下面这张图可以看到,ctrl_regs在MCDF的集成中需要与多个模块进行连接,这些模块包括有:
  • 外部的时钟复位模块(Clock & Reset)
  • 外部的控制模块(Controller)
  • 内部的Slaves、FIFOs、Arbiter以及Formatter


如果要完成充分的验证,我们就需要对上述的连接关系给予激励。在发生激励之前,我们采用接口(interface)进行stimulator与DUT的连接。

interface的基本作用是对各个模块做清晰有序地连接,因此interface可以看做一捆智能的集束线(collector)。如果将interface作为一个路由器(router),那么意味着只要同interface的接口类型一致,多个模块都可以使用这个interface,将其与interface进行连接也不需要担心信号的驱动方向、连接性问题。可见,interface不但可以方便多个模块之间的连线,也能将DUT同testbench的连接隔离开来。对于DUT同testbench隔离的好处,我们将会在后续部分关于stimulator、monitor的实现中进一步介绍。

那么有了interface,我们应该如何声明这样的接口,而且应该需要几个接口呢?下面是常见的两种实现方式:

interface连接方式1

该连接方式只定义了一个interface: regs_if,而且将所有需要同ctrl_regs相连接的信号均定义在regs_if中且与ctrl_regs在testbench中进行连接。

interface连接方式2

该方式则定义了三个interface: regs_cf_if、regs_ini_if和regs_rsp_if。该三个interface的功能划分更加明确:
  • regs_cr_if:时钟和复位的接口,用来供给ctrl_regs和其它两个接口。
  • regs_ini_if:ctrl_regs读写接口,也由于该接口模拟了集成以后外部控制器的作用,我们将其称之为initiator(发起端)接口,即该接口主动发起读写请求。
  • regs_rsp_if:ctrl_regs配置和状态反馈接口,该接口时为了能够及时响应ctrl_regs在相应寄存器经过读写以后对所对应模块的功能配置或者状态返回。我们也将其称之为responsor(响应端)接口。

那么这两种方式从实现方式来看,方式2看起来需要做更多接口的定义,而且似乎将一件连接环境的事情变得更复杂了,是这样的吗?从实现的工作量来看,方式2确实会引入更多的工作,但从日后ctrl_regs的testbench在更高层级的集成复用来看,现在按照ctrl_regs边界信号进行有效的划分会对日后的工作提供更多的方便,这一点也会在以后的篇章关于《SV的系统集成篇》中得到体现。

那么,下面是关于上述三个interface的定义:


在上述interface定义中我们需要注意以下几点:
  • interface可以定义input/output/inout端口,或者如果这些接口对不同连接模块的方向不相同时,可以将这些端口定义在interface实体中,这样的信号定义本身没有方向。
  • 我们建议将interface上信号数据类型定义为logic,而非wire或者reg。这么做是因为SV的logic类型本身扩展了传统的reg类型,使得其也可以像wire进行连线。值得注意的一点是,唯一不能使用logic变量的地方是含有多驱动(multi-drive)的场景,这时候你必须使用连线类型,例如wire。
  • 此外,我们需要再次强调,interface中的信号如果用来同DUT相连接,那么应该必须是四值逻辑即logic/reg/wire等,而不应该是二值逻辑bit/int/byte等。这么做的考虑是确保在今后的stimulator到DUT的驱动场景或者DUT到monitor的数据采集场景中,硬件部分的'X'或者'Z'信号不会被缺省转换以致丢失
  • interface也可以定义parameter来方便扩展复用。

在实现了interface的端口定义之后,我们进一步深入interface的其它应用。首先,我们提到了regs_cr_if的功能是提供时钟和复位信号的,这里我们可以在regs_cr_if中产生时钟和复位信号。


从上面的例子可以看到,interface也可以包含过程语句(always和initial)以及连续赋值语句,这对更高层级的建模和testbench应用都有好处。从上面的regs_cr_if中可以看到通过两个简单的initial过程语句可以在interface内部产生出时钟和复位信号。

接下来我们看三个interface与DUT的实际连接关系:
interface本身既需要同DUT连接, 也需要同stimulator和monitor连接,那么对于不同对象来讲,连接的信号方向也是不相同的。考虑到interface中声明了的信号自身没有方向,这就使得同一个信号可能会连错或者反向驱动的问题。

interface为了限制不同对象对其信号的访问权限和方向,通过modport来做进一步的声明来确立信号应该连接的方向。这里,我们以regs_ini_if为例:

在regs_ini_if实体中,进一步定义了3个modport:dut,stim和mon。它们分别用来作为DUT,stimulator和mon的“插座”,这样一方面澄清了各个对象可以连接的interface信号,也通过限制的方向避免了反向驱动的问题。如果形象地来理解modport的作用,可以将interface作为一个插排,而各个modport作为针对不同对象的插槽,这使得针对更复杂的连线问题可以得到简化。


如果regs_ini_if通过modport来进一步约束信号的连接,那么上面的代码实例对于DUT的连接就转换为下面的部分:


可以看到,除了时钟和复位信号意外,其它信号与ini_if和rsp_if内部信号连接时需要再进一步通过modport dut来连接。与之前的点对点连接方式比较,modport可以使所有相关的信号集中在同一个地方描述,减少出错的概率,但也需要额外地对这些不同的信号进行分类集合。

在介绍完interface主要的使用情况以后,我们下一节将介绍testbench的“外包装”——program(程序块),来看一看program与module的比较,以及它们之间可能存在的竞争问题。

四、程序和模块
module(模块)作为SV从Verilog继承过来的概念,也自然保持了它的特点,除了作为RTL模型的外壳包装和实现硬件行为,在更高层的集成上面,模块之间也需要通信和同步。从硬件实现的角度来看,Verilog通过always,initial过程语句块、信号数据连接来实现进程间通信。为此,我们可以将不同的module作为独立的程序块,他们之间的同步通过信号的变化(event触发)或者通过等待特定事件(时钟周期)或者时间(固定延时)来完成



如果按照软件的思维理解硬件仿真,那么上面的三个模块首先是独立运行的线程(thread)在仿真伊始并行执行,除了每个线程会依照自身内部产生的事件触发进程模块之外,也同时依靠相邻模块间的信号变化。

Verilog设计竞争问题
为了避免在RTL仿真行为中发生的信号竞争问题,我们建议通过非阻塞赋值或者特定的信号延迟来解决同步的问题。例如,从下面的例子可以看到,采用阻塞赋值可能引起的竞争问题:

从仿真结果来看,寄存器a在每个时钟计数以后的值会被当前时钟内的b采集到,所以,每个时钟周期内a和b的数值是一致的。但是,这种仿真行为并不符合真实的硬件行为,因为b如果要对a的数值采样,那么必须有一个周期的延迟,而从软件角度理解,b不应该在当前时刻收集a变化后的数值,而应该是变化前的数值。

所以,我们可以通过非阻塞赋值的形式来避免这种情况。
输出结果:

非阻塞赋值和信号输出的延迟赋值都可以有效避免设计层面的竞争问题。那么当testbench与DUT连接时,这种方式还是否行之有效呢?在讨论之前,我们首先从SV的仿真调度入手。

SV的仿真调度
SV的仿真调度完全支持Verilog的仿真调度,同时又扩展出来支持新的SV的结构体例如program(程序)和断言(assertion)。充分理解SV的不同结构体在仿真中执行的先后顺序,有利于理解testbench中对DUT的驱动和采样的顺序,避免不合理的驱动采样方式。
由于仿真器一般基于event驱动方式来执行,所以如果对各个类型的event发生做出合理安排就可以保证设计与验证环境之间有清晰的发生顺序,进而避免这两者之间的竞争问题


这里time-slot(时间片)是仿真时间中的一个抽象单位Ts,在该单位内所有的线程(always,initial,assertion等)和数据对象的赋值(阻塞赋值和非阻塞赋值)会被赋予相应的优先级,依次被执行。这种优先级即上图显示的scheduling regions(调度区域)。除了observed和reactive区域,其它区域均是继承于Verilog调度区域。preponed区域是从上一个Ts进入本时间单位的入口,而postponed区域则是所有event触发完毕所有数据也被赋值结束以后该Ts的出口。

从上述不同调度区域的功能来看,可以将其划分为:

接下来我们对这些调度区域做简单的介绍。
active 区域:在从preponed region进入active region之后,所有处于该调度阶段的线程(例如always、assign、initial等)将会执行。其中,跟非阻塞赋值有关的操作执行完毕后,对应的线程会进入NBA,而带有"#0"延时操作的线程会直接进入inactive区域。

inactive 区域:所有被进行零延时操作的线程会在inactive区域被激活,同时在被执行之前迁往active区域。所以,零延时的操作会延缓线程的执行时间。

NBA 区域:该区域是在所有的active区域和inactive区域均没有其它线程之后所到达的调度区,到达了该区域之后,之前在active区域的非阻塞赋值会生效,而这些非阻塞如果触发了别的线程,那么这些被触发的线程又要被迁移到active区域。

observed 区域:当之前active/inactive/NBA区域均全部执行完毕之后,也即表示了设计部分的线程执行完毕,接下来的区域是SV为验证一侧准备的。在进入到observed区域之后,这一区域是为了属性断言(property assertion)准备的,由于属性中需要监测设计中的变量,且必须等到所有每一个数据对象被赋予最终的数值,所以该区域处在了设计区域之后。这样做的好处可以避免采集到不稳定的变量从而导致错误的属性检查。同时,该区域也同样适用于interface和program中的采样操作,使得采集到的数据是处于该Ts的最终值。

reactive 区域:在经历了数据采样之后,断言语句需要进行属性判断,而同时,该区域如果再次对设计区域中的线网和变量赋值则又会使得被激活的线程被再次迁移到active区域。经历了信号采样之后,处于testbench区域中的线程也会被在该区域执行。

postpone 区域:在分别经历了跟设计与testbench有关的区域之后,当前Ts进入了postponed区域。该区域内的值保持稳定,且应同下一个Ts中preponed的值一致。同时,该区域也作为SV PLI/DPI的回调函数(callback)点,使得在SV外部的调用语言例如C在使用SV变量时仍然可以使用到最新的数值,无论是设计部分还是验证部分。

在对上面各区域做了介绍以后,我们结合着之前阻塞赋值和非阻塞赋值的例子进行分析。在上面阻塞赋值时:


再来看看非阻塞赋值的仿真调度安排

所以非阻塞赋值可以用来避免一些设计中的竞争情况,而这种方式也针对于组合和同步时序逻辑的设计场景。但这种设计技巧在验证领域中仍然受到了不少的挑战:
  • 验证人员在testbench实现中更多地采用软件编程方式,即连续性赋值(continous assignment)而不是阻塞/非阻塞赋值的形式
  • 验证人员首先并不关心设计行为中可能出现的竞争场景,对他们而言首要的是采集到正确的稳定数值
  • SV中的属性/断言需要在一个特定的仿真调度区域采集数据和执行属性检查

对此,我们可以从下面这个的两个例子观察提到的testbench部分数据采样和执行的部分:
module数据采样示例1


仿真结果:


可以看到DUT与TB的采样均发生在clk1的上升沿,并且均采样到了dut.cnt变化前的数值。用仿真调度时序图来表达,则如下图所示。

    
DUT和TB对dut.cnt的采样均在active区域发生,所以都采样到了dut.cnt变化前的数值。

module数据采样示例2


在DUT和testbench的数据采样结果不一致:


发生数据采集不一致的原因在于DUT和TB用于采样的时钟不同,即DUT使用了clk1,而TB使用了clk2。粗看起来,clk1与clk2没有延迟,但因为非阻塞赋值使得clk2较clk1有从active区到NBA区的延迟,简单而言,clk2的沿变化要比clk1晚,由此带来的变化造成了数据采样的竞争问题,用时序图描述可以表达为下图的形式:

由于dut与tb使用的时钟存在一个active到NBA的延迟周期,这使得testbench在使用clk2对dut进行采样时,已经采集到了该Ts中DUT在第一个NBA已经生效的非阻塞赋值。

通过上面的两个采样示例可以看到,TB中的数据采样如果在module内部执行会有可能造成不同的采样结果。在这里我们需要强调的不是是否应该采样的当前Ts中dut.cnt变化前或者变化后的数值,而是应该保证的是,采样的结果是按照预期执行的。也就是说,如果通过一些方法可以使得采样的数据按照预期发生在dut.cnt变化前或者变化后,都是可以接受的。

我们之前已经介绍了可以通过SV的property中的sequence采样特性、interface采样以及program采样三种方法。在这一节,我们先介绍program的采样方式。通过对上面的例子进行简单的改造,我们可以使得program内部发生的采样是预期的结果。

program数据采样示例:


仿真结果:


可以看到仿真结果同“module数据采样示例2”保持一致,而且通过program内部进行数据采样的结果是可以预期的。我们再通过仿真进度时序图来理解这种采样方式:

从通用角度来解释program内部执行的情况,上面的示例可以遵循下面的进度安排原则:
  • 在program执行之前,会先进行设计代码相关的仿真调度区域即active、inactive和NBA。
  • 待设计调度区域执行完后,会通过observed区域,最后至reactive区域。而program会在reactive区域执行。所以program会采用之前已经被阻塞/非阻塞赋值后的稳定值进行计算。
  • 在program执行过程中,如果有内部变量发生变化且又影响到该Ts中设计调度区相关的变量,则对应设计的调度区会被再次迁移到active区域,而该program会被挂起,直到整个调度阶段再次进入reactive区域。

由此看来,SV介绍program的一个重要部分就是为了将设计和验证的调度区域通过显式的方式来安排,例如设计部分被建议放置在module中,而测试采样部分被建议放置在program中。下面是一些关于program实现的要求和建议:
  • 读者可以将program看做是软件的“领地”,所以program中不可以出现和硬件行为相关的过程语句和实例,例如always、module、interface,也不应该出现其它program例化语句。
  • 为了使得program进行类软件方式的顺序执行方式,可以在program内部定义变量,以及发起多个initial块
  • program内部定义的变量赋值的方式应该采用阻塞赋值(软件方式)。
  • program内部在驱动外部的硬件信号时应该使用非阻塞赋值(硬件方式)。
  • program中的initial块(类软件的执行方式)会在reactive区域被执行,而program之外的initial块(module内部)则会在active区域被执行,这一点值得注意。

所以SV通过program可以将DUT与TB的领地做清晰的划分,根本上从调度区域的不同执行顺序来解决的。在下一篇的《SV的组件实现篇》中我们会介绍如何通过interface clocking来给出第二种解决时序采样和驱动信号的方式

在清楚了硬件信号采样可能出现的竞争问题以及如何通过program来解决之后,我们便可以通过合适的连接和采样方式将验证组件和DUT连接,而连接之后一旦有了激励,如何结束仿真,结束仿真的方式有哪些,我们将在下一节的《测试的始终》为大家介绍。

五、测试的始终
在上一节《程序和模块》中我们提到了各个设计自身可以作为一个大的线程,内部又包含多个并行的线程,而模块之间即线程的通信主要依靠信号的变化。可以想象,对于一个设计,如果在仿真开始没有任何的激励,譬如时钟和复位信号,那么仿真并未开始,也可以认为已经结束,因为对于设计内部并没有产生任何新的事件,也不会由这些事件进一步触发组合逻辑和时序逻辑。

那么,如果我们在仿真开始后提供时钟和复位信号,这对于验证而言是必要的步骤,但是它本身不会对设计的功能产生实质的功能影响。从设计的角度来看,复位信号只是为了让设计进入确定的状态,而时钟信号是如同血管的供血功能一般保证设计可以正常地“跳动”。

在Verilog的测试方式中,即便我们只给设计提供复位和时钟信号,整个仿真也会一直持续下去,并不会主动结束。即使DUT的输入激励已经执行完毕,仿真也会一直进行下去,这就需要通过Verilog系统函数主动结束仿真。

结束方式之一:系统函数
在Verilog测试中,需要通过Verilog提供的系统函数来结束仿真。下面的例子即在仿真500ns时通过系统函数$finish结束了仿真,而用户也看可以考虑使用$stop来暂停仿真。这两者的区别在于$finish会使得仿真退出,将控制权交回给操作系统,仿真无法再次继续;$stop会使得仿真暂停,用户还有机会让仿真继续运行。

module tb;
bit clk;
 initial begin
   forever #5ns clk <= !clk;
 end
 counter dut(clk);
 initial begin
   #500ns;
   $finish();
 end
endmodule


结束方式之二:program隐式结束
在SV推出program将验证部分与设计部分进行有效隔离以后,SV也会将每一个program块作为一个独立的测试,如果testbench中只有一个program,则会在执行完该program中最后一个initial后自动结束仿真。如果testbench中有多个program,那么需要等待所有program中最后一个initial才能结束仿真。



执行结果:
从上面这个例子可以看到, 仿真会在p1.proc2最后执行完毕后自动结束。

结束方式之三:program显式结束
从上面的第二种结束方式来看,要求仿真自动结束的前提是所有program的initial块都应该在一定时间内完成,而实际上有的program内的initial语句块会一直运行下去,这就使得仿真无法等到所有的program均执行完毕,也就无法自动结束。这时候,我们可以在目标program内置入系统函数$exit来要求该program强行结束,待该program结束之后,仿真器仍然会等待其它program执行完毕后再结束仿真。



执行结果:

从上面的例子可以看到p2由于有forever loop proc2,本身无法正常结束,所以仿真是无法自动结束的。这时,我们可以在p2内的proc1(任何一个initial块)置入一个系统函数$exit()。该系统函数的作用是可以强制结束它所在的program,这使得在proc1在700ns结束之后p2就结束了,而仿真器仍然会 统览其它的program2:p1,发现所有的program均执行完毕,于是就自动结束仿真了。

通过上面三种在SV中结束仿真的方式,我们可以更便利地控制仿真的结束,也能在更深的层次掌握仿真结束的机制。

至此,我们本篇《SV环境构建篇》就介绍完毕了,通过本篇的学习,读者可以在开始搭建“测试房子”之前懂得如何与设计做恰当的连接、模块的例化、验证与设计部分的隔离和结束测试的方式

我们下一篇《SV组件实现篇》将对之前做模块验证的几位验证者做一线跟拍,看看他们是如何实现它们的验证组件和环境的
本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
从Verilog到VHDL(下):Verification using SV
【精品博文】吴明系列博文 FPGA 何去何从(三)
SoC功能仿真验证技术分享
UVM系统验证基础知识1(基于UVM的verilog验证 )
各种波形文件的区别、生成方法wlf/vcd/fsdb/shm/vpd
Testbench文件编写纪要(Verilog)
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服