芯学长 | 掌握芯资讯,引领芯未来

您当前所在位置:首页 > IC行业资讯 > 行业热点

数字IC验证|systemVerilog验证环境

发布时间:2023-11-13来源:芯时代青年 0

如果一个初学者看到了这篇文章,我建议在学习systemVerilog绿皮书之后,UVM白皮书之前,试着跑一跑这个小工程,跑完之后你就明白验证环境到底是怎么转起来的了。

所以说如果你满足以下的条件可以跑一跑这个工程,否则到这里就可以省流退出了:

1.有意从事芯片验证方向的工作;

2.对芯片验证的了解局限于基本语法;

3.没有什么工具,只有modelsim可以跑跑编译和仿真;

4.看UVM白皮书看的云山雾罩,再多看一页就差不多要放弃了;

好的闲言少叙,开始正文。工程目录如还是在“阅读原文”中,行文过程中也会摆一部分的代码,完整的代码在modelsim_testbench_demo/source目录下。

RTL功能

想做验证平台总得有RTL可以验,所以规划了一个模块其接口如下:

image

模块的接口是典型的使能型包接口,这类接口在网络芯片中比较常见。接口时序一般是这样的:

image

vld用于表示数据有效,在数据传输的过程中vld可能连续也可能不连续(不过在这个项目里,我们令vld必须为连续传输);sop用于标记包头,eop用于标记包尾;data为传输的数据,包头一般为ID,包尾一般为校验位(在本项目我们不做区分)。

那么这个模块的功能就是进行包转发,在转发的过程中将每一个包数据与下一拍的数据做亦或称为新的数据发送出去,每个包的最后一个数据因为没有下一拍数据了所以直接输出。具体的代码实现请见工程目录下的flow_proc.v,这个代码不知道有没有bug哈。

验证环境框架

学了这么多语法了,终于该用来搭一个验证环境了,咱先把验证环境的结构理一理。验证环境用来干嘛的呢,我总结哈就三个功能:

1.打激励,就是给模块模拟外部的输入和真实的工作场景;

2.搞预测,就是把RTL干的事别管是用sv还是cmodel哪怕是verilog你环境里再写一遍;

3.做比对,就是把环境预测的结果和RTL真实输出的结果比一下,看看到底谁有错;

所以,一个最朴素的验证环境框架就出来了。

图片

红色的的gen和drv用于打激励,紫色的mon和rm用于搞预测,绿色的mon和chk用于做比对。interface用于验证环境和DUT之间桥接,其将接口按照功能汇总成若干组,便于环境驱动和采样。那么接着咱们来一步步从低向上实现这个验证环境的所有代码。

验证环境编写

pkt_if

验证环境的编写必然应该从时序层的interface和事物层的transaction开始,因此先写interface。通过观察dut可以发现他的输入输出接口信号是完全一样的,所以我们只需要做一组interface就可以了:

image

之前我习惯在interface之后直接声明一下virtual interface,virtual interface是为了解决环境中灵活调用而产生的语法,当然这个地方不声明也无所谓用法很灵活,可以先按照这个来写后面慢慢自己改进。

interface内必须针对drv和mon做两个clocking block以避免驱动和采样中存在的竞争和冒险,关于这个竞争与冒险建议入门者先不要去管他,之后出错了自然就会了。clocking block中针对信号分组,指定对于drv和mon的方向以及驱动和采样相对于时钟沿的偏移:

image

然后写这个环境的时候我还有个习惯,写了一个interface pack的文件作用是把输入和输出的inf合成一个文件,方便后面的传递,其本质还是个inf就是里面例化了输入和输出接口罢了(不想用的话就散着传也可以):

image

接口的代码搞定,接着就是从接口抽象到事务层,我们需要描述这个接口的属性。

pkt_data

pkt_data是一个transaction是一个事务描述符是一个对接口的属性进行抽象而创建的类。那么我们对pkt_if接口进行观察,仔细看啊:

图片

如果这是一个ID+payload+ecc型的包传输,那么显然它的属性包括以下几项:

1.包的长度,也就是从sop到eop要持续多少拍;

2.sop的ID值;

3.eop的校验值;

4.包之间的间隔;

5.每拍数据的值;

那么既然这个小项目不考虑头尾数据的特殊属性(当然一会在代码里会看到还是做了ID),那么2和3就不需要提炼为属性了。所以汇总来看,需要提取的属性无非就是包长、每拍的值(payload)和包间距。

image

pkt_dec是一个用于定义参数和全局常量的文件,将数值定义在其中以便所有的验证文件都能访问到共用的数值。这个名为pkt_data的transaction的大概结构就是这样了,可以发现所有提取出来的“属性”都用了rand前缀,这也正好体现了随机验证和定向用例的差别(如果对定向用例感兴趣,欢迎去用auto_verification哈)。

解释下细节,payload_q[$]队列自然就是要发送的事务中的数据信息,pkt_len是包的总长度,interval是两个包之间的间隔。send_over和data[$]是事务转时序接口时要借助的中间变量。那么之后就可以开始构建约束了:

image

对于pkt_data,以上三个约束实际已经够用了,不过实际上我还添加了一个约束:

image

这个约束是干嘛的呢,实际上是我把payload中的第一拍数作为ID来使用了。把payload的一部分作为ID有什么好处呢,想想看如果现在环境里发了100000个包,其中某个包对比出错了,从出错点反推输入是不是一个特别头大的过程。而如果在payload中有些什么标记能够一下子对应到入口的数据和时间点上去,那岂不是非常的快捷,效率就是这样一点点提升上来的。所以把一些不重要的随机数据放上一些关键信息用于加速定位,这也算是经验之谈吧。约束一共就这些,下面把几个函数挑重点看一下。

compare和psprintf就不提了,一个用来比对一个用来打印。copy需要说一下,sv中所有的等号赋值都是句柄拷贝,除非你就是想这么用否则记得用copy函数来进行示例的拷贝:

image

pack和unpack一般是成对出现的函数,这两个函数可以这么理解:pack是根据事务属性生成待发送的数据,这个数据只需要按数据打到接口上就是RTL的输入了;unpack是根据采样到的数据,反推出事务的属性。这俩函数一个是把事务打包为时序,一个是把时序拆解为事务,即使这里还没有体现出时序。

image

说明一下,pack里我还做了一个特殊操作,就是把数据拓展了3bit分别用来表示包头、包尾和包内数值,这高3bit不会作用在RTL接口上,但是验证环境自己可以看到。这就又体现了一个验证思想,就是理论上验证环境尤其是reference model应该看到和RTL完全一致的信息,但是实际操作上,是可以比RTL看到的更多更早的。比如说假设包尾是校验位而这个包是个校验无法通过的包,那么RTL只有在真正接收到包尾进行校验之后才会发现这是一个错包;而rm呢,这个错包就是环境产生的,通过一些位域偷偷的通知rm有什么问题吗?没有问题!不过作为新手的话,最好还是好好的遵守reference model和RTL看到的信息应当同时和一致这个大原则,避免玩出火来。

unpack这个函数写的有点问题,如果你发现了就改一下,不改也没事因为我在monitor里做了一些处理,不规范但是“又不是不能用”。

至此,interface和transaction已经写完了,那么下一步的目标是什么呢?就是产生transaction然后把他发送到interface上去作为RTL的激励,先吃个饭回来再写。

pkt_gen

产生transaction然后把他发送到interface上去作为RTL的激励这个事,咱们分两步走,先产生transaction也就是pkt_gen来做的事情。pkt_gen要怎么做这个事呢?

1.给pkt_gen配置,我说一个数pkt_gen就得产生这么多的pkt_data出来;

2.创建出一个pkt_data,给他随机一下;

3.传递给下游的drv去发到接口上;

4.重复2~3过程,直到说的这个数都发完了;

5.这个时候置起一个状态,能够让环境知道你这块工作完成了可以下班了;

所以,pkt_gen的壳子就出来了:

image

env_cfg这个东西是干嘛的呢,严格说他不应该出现在这里,他是环境的一致整体配置,但是这环境不严格所以就出现在这了。pkt_gen在把随机的data发出去之后,通过给env_cfg的gen_idle赋值为1告诉环境data生成完了。mailbox这东西就是UVM里那个fifo的本质,VMM里那个channel的本质,就是在组件之间传递数据的。send_num就是配置的发包数量,而pkt呢就是要发出去的那个数据我有点想用工厂模式那个思路,反正看看就好啦。

pkt_gen里面的核心方法就是这个run(),上层如果想要pkt_gen工作起来也是要调用这个run(),方法内做的事就是上文所说的生成(在new里做了)-随机-发送-生成-随机-发送。

image

pkt_drv

pkt_drv实现的功能就是把事务层的信息转换为时序层,也就是RTL能够处理的接口信息。而对应的pkt_mon功能则是把时序层的信息转换为事务层的信息,也就是环境能处理的信息。

所以pkt_drv做事情的流程是啥呢?

1.看看mailbox有没有pkt_gen发过来的事务;

2.有的话,把这个事务转为时序信息驱动到interface上去;

3.驱动完了再看看有没有下一个事务,重复1~2过程;

4.都处理完了(实际上,drv很难知道都处理完了,除非环境告诉他了),或者drv很久没有驱动总线了,那就认定drv可以下班了;

因此pkt_drv的结构大抵就是这样的:

image

env_cfg出现在这里的原因跟之前一样,不多说了。mailbox就是为了跟pkt_gen相连接用的。vdrv呢就是在interface里定义的那个虚接口:

image

虚接口嘛,就是外面给连啥就是啥(interface本质上跟module是一样的,直接使用的话都是静态的且必须是真实例化的,通过virtual就可以声明一个虚的interface句柄,等待指向外面真实例化的interface)。然后看一下几个主要的方法,最核心的就是my_run()他执行的就是从mailbox里取pkt,调用pkt_send把这个pkt驱动到总线去,然后再去取pkt:

task pkt_drv::my_run();
   pkt_data send_pkt;

//$display("At %0t, [DRV NOTE]: pkt_drv run start!", $time);
   rst_sig();
//$display("At %0t, [DRV NOTE]: after rst_n", $time);
   while(1) begin
       gen2drv_chan.peek(send_pkt);
       //$display("At %0t, [DRV NOTE]: get no.%0d pkt from gen", $time, this.get_num++);  
       send_pkt.pack();
       pkt_send(send_pkt);
       gen2drv_chan.get(send_pkt);//这里实际上是把发完的pkt丢掉
       rst_sig();
   end
endtask:my_run

set_idle()这个方法呢实际就是在数有多少拍没有驱动过总线,如果时间超过配置的阈值了,那么就认为已经没有pkt需要处理了,声明一下在环境中使用@(negedge top.clk)是一种非常不好的代码习惯,请禁止:

task pkt_drv::set_idle(); while(1)begin @(negedge top.clk); if(this.dif.vld == 1) idle_cnt = 0; else idle_cnt++; if(idle_cnt > this.cfg.drv_wait_pkt_time) this.cfg.drv_idle = 1; else this.cfg.drv_idle = 0; endendtask:set_idle

好了,到目前为止,数据已经驱动到总线上去了,DUT会接收到数据然后进行处理并输出处理结果。环境需要对DUT输入的数据和输出的数据进行采样,这就需要用到pkt_mon了。鉴于写了这么久了可能要忘了,把结构图放在这再看一下:

图片

pkt_mon

前面提过了,pkt_mon功能则是把时序层的信息转换为事务层的信息,也就是环境能处理的信息。转换完的这个信息要发到哪去呢?通常而言有两个目的地:

1.如果是对RTL的入口进行采样,转换后的信息一般发往reference model,rm根据输入信息对输出进行预期;

2.如果是对RTL的出口进行采样,转换后的信息一般发往checker,chk将RTL的处理结果和rm的预期结果进行比对以判定是否有bug;

当然了一般而言入口和出口的mon不是针对一种数据类型的,但是咱们这个凑巧了输入输出长一个样,所以用一个pkt_mon就够了。看一下pkt_mon长什么样:

class pkt_mon;
   env_cfg cfg;
   vmon mif;
   mailbox mon2chk_chan;
   bit inout_type;//only for display
   bit rec_status;//0:wait sop, 1:wait eop
   int wait_pkt_time = 1000;

   extern function new(env_cfg cfg, vmon mif, mailbox mon2chk_chan, bit inout_type);
   extern virtual task run();
   extern virtual task send_chk(pkt_data pkt);
endclass

pkt_mon比pkt_drv要更难实现一些,难在哪里呢?总线上可能会有错误。比如说RTL处理后的结果,一个包没有eop了这怎么办,pkt_mon都得考虑到这些事情。所以核心的run()里面是while套一个状态机,基本的思路就是等sop - 收数 - 等eop - 把收好的pkt通过mailbox发出去 - 等下一个包的sop - 收数...这么个过程,大家在工程里自己看下就好啦。

pkt_mon把接口时序转为事务,发送给rm和chk,那么接下来就看rm和chk怎么处理了。

pkt_rm

rm相当于就是用sv或者c或者systemc语言实现的一个“RTL”。rm的思路都是比较一致的,就是接收RTL入口经mon采样得到的信息,对信息进行处理预期输出结果,然后将预期结果送到chk去,所以rm的样子基本都是这样的:

image

针对my_run()单独看一下就可以,其内部就是刚刚所述接收-预期-发送三个过程,预期行为通过调用data_trans来完成:

image

起始看到这里,也应该能够发现我前后代码风格有些变化哈,比如rm里我就已经使用in_chan和out_chan而不是显性的叫这个mailbox实际功能的名字了(前面都是叫mon2chk_chan啥的)。rm中根据输入数据做出了预期,送到了out_chan,自然chk要接收这些数据去跟RTL的输出进行比对了。

pkt_chk

有些地方把checker和scoreboard分开写,起始我认为大可不必,这个的本质就是从rm拿输出从RTL拿输出,然后放到一起来比对的组件,非要再弄个计分板出来起始意义不大。chk对二者进行比对的策略有很多,比如顺序比对、乱序比对、优先级比对、分ID比对等等,具体到咱们这个小工程里,因为RTL的输入输出是有明确顺序可以预期的,所以我们实现顺序比对的chk就可以了。

所以呢chk里需要两个队列,一个用来放rm的输出,一个用来放RTL的输出。然后当RTL的队列不空时,就要从rm输出的队列中把最早的一个数找出来,跟RTL输出队列的最早一个数比对。综上,chk的整体结构就是下面这样了:

class pkt_chk;
   env_cfg cfg;
   pkt_data expect_q[$];
   int in_expect, in_actual;
   int match, not_match;
   mailbox gen2chk_chan;
   mailbox mon2chk_chan;

   //for finish simu
   int idle_cnt;

   extern function new(env_cfg cfg, mailbox gen2chk_chan, mailbox mon2chk_chan);
   extern virtual task run();
   extern virtual task expect_gain();
   extern virtual task actual_gain();
   extern virtual task set_idle();
   extern virtual function report();
endclass:pkt_chk

比对的行为在actual_gain()中实现,可以重点关注下。需要报错的情况有两个:一是RTL有输出时,rm队列中是空的;二是RTL输出的数和rm输出的数比对不上。而环境如果遇到报错了,也不能立刻停止,一般会累计一些报错后才会退出仿真。

至此,所有的组件都准备好了,可以开始集成env!再看一次结构图嗷:

图片

environment

env中完成以下的内容:

1.声明并创建所有的组件;

2.通过mailbox将所有组件互联,并将inf配置给drv和mon;

3.做一个run()启动函数,把所有组件的run()函数集成在一起,一同启动;

4.仿真结束之后,打印相关的信息;

因此就可以把env壳子做出来:

image

里面的四个函数就对应了四个阶段:创建组件,连接组件,执行操作,打印报告。注意在run()中,所有组件的run方法必须通过fork-join_none的方式来并行执行:

image

而run()方法中另外一部分就是如何结束仿真的事情。随机验证环境中,如何结束仿真是一门很大的学问,UVM方法学中通过objection的投票机制来完成这一行为。这个验证环境中,通过等待各个组件的结束状态和RTL的接口状态来等待仿真结束。

image

env完成之后,就该封装验证环境的顶层test了。传统的验证顶层通过program封装的,VMM中也是这样做的,而在UVM中是通过module来封装顶层的,本项目里采用比较传统的program封装顶层。

test

test就是验证顶层,看一下那个结构图,他只有一个输入就是interface,而在其内部需要例化env并启动env(也就相当于启动了所有组件)。此外,在test中还可以进行各种各样的配置:

program automatic test(pkt_if_pack bus);
   timeunit 10ns;
   timeprecision 1ns;
   environment env;

   initial begin
       $printtimescale;
       $timeformat(-9, 3, "..ns..", 6);
   end

   initial begin
       env = new(bus);
       env.build();
       env.gen.send_num = 10;
       env.run();
       $display("At %0t, [TESt NOTE]: simulation finish~~~~~~~~~~~~~~~~~~", $time);
       $finish;
   end

endprogram

你看这里面使用initial启动的,跟testbench已经没啥区别了,也就是说例化之后环境的组件就会自己转起来了,就是这么神奇。而且咱们平时写的testcase,起始本质就是“改写”这个test。test作为验证顶层完成封装后,下一步就是和RTL一起集成到真正的顶层top中去。

top

top和平时经常写的testbench很相似,一般分这个几个区域:

1.创建时钟和复位;

2.例化RTL;

3.例化interface;

4.例化test验证顶层;

5.互联RTL - interface - test;

然后仿真开始后,所有的initial块启动,在时间片的推动下仿真环境开始运转,代码我就不放了。

至此,所有的设计和验证代码准备完成。准备建工程跑波形!

建立工程

打开modelsim,新建project,选择work目录后完成工程创建。点击添加已有文件,把刚刚的这些文件都加进去,就按这个顺序就没问题(编译顺序这个问题,我通过在文件中加了各种include来规避的):


图片

然后点击上方的编译全部文件会发现编译都通过了:

图片

然后点击右边的simulate跑仿真,注意选择顶层为top,设置里把apply full...也选上啊:

图片

点击ok后就进入了仿真界面:

图片

在flow_proc上右键add wave,再改一下上面的跑波形时间,改成10ms:

图片

然后点击“run”,肯定会跳出来一个对话框问你跑完了要不要退出,千万别点退出嗷:

图片

然后就看到波形啦,在波形上按一下f就会缩小到全景:

图片

想看log怎么办呢,点击view中的TypeScript就可以了:

图片

如果大家需要数字IC验证相关资料,可以点击留言,免费领取数字IC验证,项目资料,面试题,书籍等。

相关推荐:

学习数字IC验证|你需要的可能是一个规划!

器件转行数字IC验证,秋招offer25-50w上岸!

开出50w+最受欢迎的IC公司合集及面试经历

【免责声明】:本站部分文章为转载或网友发布,目的在于传递和分享信息,并不代表本网赞同其观点和对其真实性负责;文章版权归原作者及原出处所有,如涉及作品内容、版权和其它问题,我们将根据著作权人的要求,第一时间更正或删除。

文章评价

-   全部 0 条 我要点评

有疑惑?
在线客服帮您
029-81122100

立即咨询 >