UVM实战指南第3部分.docx
《UVM实战指南第3部分.docx》由会员分享,可在线阅读,更多相关《UVM实战指南第3部分.docx(11页珍藏版)》请在冰豆网上搜索。
![UVM实战指南第3部分.docx](https://file1.bdocx.com/fileroot1/2023-1/9/d8c24a92-cee6-4614-9578-7f604cf9f015/d8c24a92-cee6-4614-9578-7f604cf9f0151.gif)
UVM实战指南第3部分
(*)题外话:
TLM可能是UVM中最重要的概念,掌握了TLM,就可以开始尝试编写一些小程序了。
翻译这篇文章,也是为了巩固加强对TLM的理解。
(*)几个名词:
transaction翻译为事务或者交易;packet翻译为封包,packet属于transaction;monitor翻译为监视器;driver翻译为驱动器;scoreboard翻译为记分牌;有些词汇直接被运用到UVM源代码上,所以有时候用英文更容易描述清楚。
(*)语言的目的是为了交流,翻译不是为了纯粹的语言转换,而是为了传递思想。
4.6UVM中事务级建模(TLM)
20多年前,设计者从门级转向RTL级。
这次转换来自于标准Verilog/VHDL的RTL编码风格,以及RTL综合实现工具的推出。
使用RTL最大的好处是让设计者更多的专注于时序行为的设计以及功能的正确性,而很少考虑门级相关设计。
TLM(事务级建模)同样在抽象级别上更进了一步,在设计和验证领域都有出现。
通过TLM,中心放在系统级别的各种事务流的建模,而更少关心时钟级别的行为。
TLM在测试向量中已经使用多年。
通常,在产生激励和覆盖率检查的时候使用事务而不是用时钟级别建模,这种方式就是TLM.为了验证RTL级别的DUT(需要测试的模块),测试向量使用事务发生器(transactor)(有时也称为总线功能模型(BFM)),将RTL级和事务级进行转换。
在UVM中,此事务发生器也被叫做驱动(driver)或者收集器(collector)。
TLM中,事务通过方法调用和类对象来建模。
使用事务级而不是信号级别来建模有几个显著的好处:
∙TLM比RTL更简洁,仿真速度快。
∙TLM模型的抽象级别更高,更加契合验证工程师或设计工程师对内部功能的考虑,从而使得建模更简单,并且更容易被其他工程师理解。
∙TLM模型将不符合复用的部分移到模型之外,因此TLM很适合复用。
并且,TLM使用面向对象的技术,比如继承、实现和接口分离的技术。
TLM的采纳依赖于标准的TLM建模技术的出现,就像RTL综合流程的采纳归功于标准RTL编码风格的实现。
幸运的是,近些年来,几个重要的标准TLM应用程序接口(API)得到定义。
在EDA和ESL领域,两个最重要的标准是开放SystemC计划(OSCI)的TLM1.0以及TLM2.0标准。
OSCITLM1.0标准是一个简单通用的TLMAPI,用来建模消息传递。
在消息传递时,对象(事务)在组件之间传递的方式和封包在网络之间传递的方式类似。
在发送封包的消息传递中,发送端和接收端之间没有共享的状态,他们之间的通讯讯息仅仅包含在消息中。
TheOSCITLM2.0标准能够用来开发SystemC中的高速虚拟平台模型。
TLM2.0标准特别被用作片上存储映射的总线系统,包含许多能够进行片上总线互联的整合复用模块.
OSCITLM1.0和TLM2.0是互相独立的标准,满足不同的需要。
有人可能通过其命名方式认为TLM2.0优于TLM1.0,但是实际上并不是这样。
UVM提供的TLM类和API是基于TLM1.0标准的。
这是因为TLM通用消息传递语法很好的满足了多种验证组件的事务级建模。
TLM1.0也适合多种语言之间的通信建模,比如SystemVerilog,SystemC以及e语言之间的建模。
UVM中TLM1.0接口甚至可以用来和SystemC中的TLM2.0模型进行通讯。
这一章节阐述了UVM中TLM的几个重要概念,让读者理解如何使用TLM来构造可复用的验证组件。
关于TLM各种类的更详细说明请参阅UVM参考手册。
4.6.1UVM中TLM的关键概念
4.6.1.1对事务建模
在UVM中,从uvm_sequence_item继承而来的任何类都是事务。
用户根据需要定义事务类的字段和方法,用来在验证环境中不同组件之间进行信息交换。
例如,一个简单的包如下所示:
1.class simple_packetextendsuvm_sequence_item;
2.randintsrc_addr;
3.randintdst_addr;
4.randbyteunsigneddata[];
5.constraintaddr_constraint{src_addr!
=dst_addr;}
6....
7.endclass
事务通常包含足够多的数据字段让驱动器(driver)或者事务产生器能够产生事务的真实信号级别的动作表示。
事务也可以包含更多的数据字段,来控制数据的随机产生,或者是验证环境中的其他目的。
可以通过继承方式来增加更多的数据成员,方法以及约束。
后续章节将会说明,如何通过继承事务,从而花费最小的代价来完成特定的验证任务。
4.6.1.2TLM调用端口(Ports)和实现端口(Exports)
UVM中的TLM使用一系列特殊的方法调用来进行模型之间的事务通讯。
在UVM中,一个port对象定义了一系列可以被调用的方法,而export对象提供了对这些方法的实现。
在构建验证环境的时候,port和export通过connect()函数进行连接,之后,调用port端的TLM方法将会执行export中对此TLM方法的实现。
实例4.7:
使用put方法将事务从生产者传递给消费者
在UVM的TLM中,put接口能够被用来将transaction从生产者发送给消费者。
一个简单的生产者示例如下:
class producerextendsuvm_component;
uvm_blocking_put_port #(simple_packet)put_port;
function new(stringname,uvm_componentparent);
put_port=new("put_port",this);
endfunction
virtual task run();
simple_packetp=new();
..
put_port.put(p);
endtask
endclass
之前有提到,putport通过调用connect()函数连接到putexport.对上面的put方法的实现将由消费者组件来完成,如下:
classconsumerextendsuvm_component;
uvm_blocking_put_imp#(simple_packet,consumer)put_export;
task put(simple_packetp);
//consumethepacket
endtask
endclass
将port连接到export之后,调用生产者的put方法将会触发消费者的put方法实现得到执行,从而使得simple_packet对象从生产者传递到了消费者。
TLM也引入了标准的图形化示意来描述不同类型的通讯。
put通讯流程的模块图如下:
图4-2:
简单的生产者/消费者的put通讯
TLM接口定义了一些生产者和消费者都必须遵循的简单规则,在这个示例中,对于put接口,规则如下:
∙put方法的实现在执行时有可能阻塞,因此对put方法调用的对象必须负责确保在put方法阻塞的时候能够正常工作。
∙生产者负责创建封包,而消费者不能修改封包(如果需要修改,必须先拷贝一份新的)
满足了上述规则,能够很容易的将生产者或者消费者替换成其他的模型,只要这些模型满足相同的TLM接口即可。
TLMAPI提供了一个简单的能够互相操作的接口协议,类似硬件世界中的USB,以太网标准一样。
由于能够容易的替换模型,UVM的TLM在满足模型复用和验证目标上发挥了关键性的作用,我们可以在后续章节进一步了解。
上述示例,在生产者中存在单独一个进程,当调用put方法时,控制流转到消费者中的put方法中。
put方法将事务沿着方法调用控制流相同的方向进行传送。
在某些情况,由于消费者中包含一个需要事务数据的进程,希望将事务沿着TLM方法调用控制流相反的方向传送。
在这种情形下,生产者/消费者将使用get接口来实现,示例如下:
1.classproducer_2extendsuvm_component;
2. uvm_blocking_get_imp #(simple_packet,producer_2)get_export;
3. task get(outputsimple_packetp);
4. simple_packetp_temp=new();
5. ...
6. p=p_temp;
7. endtask
8.endclass
9.classconsumer_2extendsuvm_component;
10. uvm_blocking_get_port #(simple_packet)get_port;
11. functionnew(stringname,uvm_componentparent);
12. get_port=new("get_port",this);
13. endfunction
14. virtualtaskrun();
15. simple_packetp;
16. ...
17. get_port.get(p);
18. endtask
19.endclass
在上面的put接口示例中,UVM对使用put接口的生产者和消费者设定了如下规则:
∙get方法实现可能被阻塞。
因此调用方必须确保当此方法阻塞的时候也能够正确工作。
∙get方法的实现必须创建并返回一个事务对象给get的调用方。
get接口通讯的图形化示意如下:
图4-3:
消费者调用生产者中的get方法
4.6.1.3连接port和export
上面例子中,port对export的连接是通过调用connect方法完成的。
用户需要在消费者/生产者的父组件中的connect回调函数[仿真阶段函数connect())中调用此connect方法:
classparent_compextendsuvm_component;
producerproducer_inst;
consumerconsumer_inst;
...
virtualfunctionvoidconnect();
producer_inst.put_port.connect(consumer_inst.put_export);
endfunction
endclass
连接port和export的通用准则是:
子组件中port的connect方法以子组件export作为参数进行调用.
4.6.1.4port和port的连接以及export和export的连接
VerilogRTL中,模块的端口(port)代表信号级别的界面。
VerilogRTL模块的内部也可以包含子模块,子模块也有各自的信号端口。
然而,只有父模块的端口代表整个模块的接口,子模块的接口被当作实现细节而被隐藏。
同样的,UVM的TLM中,组件的port和export代表了组件的TLM的对外接口。
其子组件以及子组件的port和export被看作是实现细节而被隐藏。
此种隐藏内部结构的方式加强了整个验证环境的模块化,能够更加容易的复用以及被替换。
但是,如果当需要子组件的port/export能够被外部看到的时候,该如何处理呢?
这种情况下,需要通过将子组件的port连接到父组件的port上,将子组件的export连接到父组件的export上。
实例4-8:
连接子组件的port到父组件的port
classparent_producerextendsuvm_component;
uvm_blocking_put_port#(simple_packet)put_port;
producerchild_producer_inst;
functionnew(stringname,uvm_componentparent);
put_port=new("put_port",this);
child_producer_inst=new("child_producer_inst",this);
endfunction
virtualfunctionvoidconnect();
child_producer_inst.put_port.connect(put_port);
endfunction
endclass
通用的规则是:
当连接子组件的port到父组件的时候,子组件port的connect函数被调用,其调用参数是父组件的port.
实例4-9:
连接子组件的export到父组件的export
classparent_consumerextendsuvm_component;
uvm_blocking_put_export#(simple_packet)put_export;
consumerchild_consumer_inst;
functionnew(stringname,uvm_componentparent);
put_export=new("put_export",this);
child_consumer_inst=new("child_consumer_inst",this);
endfunction
virtualfunctionvoidconnect();
put_export.connect(child_consumer_inst.put_export);
endfunction
endclass
通用的规则是:
当连接子组件的export到父组件的export时,父组件export的connect函数被调用,其调用参数是子组件的export.请注意此方式和上述的子组件port和父组件port的连接方式不同。
4.6.1.5使用uvm_tlm_fifo
在最前面的生产者/消费者示例1中,在生产者中有一个进程,而消费者中没有任何进程。
消费者中的put方法在生产者的put方法被调用的时候执行。
接下来的生产者/消费者的示例2中,消费者中有一个进程,消费者有一个getport用来获得封包。
(生产者中没有进程)
我们有可能会遇到这种情况:
需要将示例1中的生产者组件和示例2中的消费者组件相连接。
如何做到这两个组件相连接呢?
一个非常常用的方法是使用UVM的uvm_tlm_fifo来完成。
uvm_tlm_fifo是一个参数化的FIFO(先进先出队列),此FIFO同时拥有putexport和getexport.uvm_tlm_fifo实例化的参数就是需要在此FIFO中存储的数据对象类型。
其构造函数的参数代表了此FIFO的最大深度(缺省值是1).
实例4-10:
uvm_tlm_fifo的使用
classproducer_consumer_2extendsuvm_component;
producerproducer_inst;
consumer_2consumer2_inst;
uvm_tlm_fifo #(simple_packet)fifo_inst; //fifostoressimple_packets
functionnew(stringname,uvm_componentparent);
producer_inst=new("producer_inst",this);
consumer2_inst=new("consumer2_inst",this);
fifo_inst=new("fifo_inst",this,16); //setfifodepthto16
endfunction
virtualfunctionvoidconnect();
producer_inst.put_port.connect(fifo_inst.put_export);
consumer2_inst.get_port.connect(fifo_inst.get_export);
endfunction
endclass
运行此模块时,生产者组件中的进程创建封包,同时将封包放入FIFO,消费者组件在调用get方法的时候取出封包。
由于FIFO的使用,这两个进程的同步耦合关系被分开。
每个进程的执行都可以任意延时,使用FIFO以及阻塞性put/get调用可以确保没有任何封包丢失。
许多验证环境建模时,对延时不敏感的特性以及确保事务(封包)能够完好不丢失传送的特性是非常需要的。
UVM的TLM使得对此类系统建模非常容易。
FIFO连接的图形化示意如下:
图4-4:
使用uvm_tlm_fifo
4.6.1.6分析port和分析export(analysisport/analysisexport)
到目前为止,put/getport要求在仿真开始之前必须有且只有一个export与之相连接。
如果port没有连接,UVM会报告错误信息,要求你将之相连。
有时候,我们需要构建类似监控器(monitor)的组件,需要将一个port要么不连接,要么连接到一个或者多个组件上。
这是因为监控器一般在整个验证环境中是被动组件,他们不会影响仿真激励的产生,也不会影响DUT(被测试模块)的同步关系。
监控器只是被动的收集事务数据,并将之发送给其他已经注册的需要此数据的组件。
分析port因此应运而生。
分析port和其他TLMport类似,但是允许将其空接,也允许连接任意多个分析export。
图4-5:
分析端口通讯
对那些熟悉回调(callback)的人来说,分析port本质上就是结构化的回调函数(使用port连接的回调)
每个分析port有一个void类型的write()函数,其参数是一个transaction(事务)。
每个分析port将维护一个与之相连接的分析export列表。
当write方法以某个transaction作为参数被调用的时候,分析port将使用相同的transaction参数调用每个与之相连的分析port中的write函数。
因为write方法是一个函数(function),所以分析port的write函数将不会被阻塞,直接返回。
另外,由于此write方法是一个void类型,在write函数返回后将不会传递任何状态给组件。
对包含分析port的组件的整体影响来说,可以不必去知道和关心与此分析port相连接的任何组件。
实例4-11:
使用分析port的监控器(Monitor)
classpacket_monitorextendsuvm_component;
uvm_analysis_port #(simple_packet)analysis_port;
functionnew(stringname,uvm_componentparent);
analysis_port=new("analysis_port",this);
endfunction
virtualtaskrun();
simple_packetp=new();
..//reassemblepacketherefromlowerlevelprotocol
analysis_port.write(p);//writethecollectedpackettotheanalysisport
endtask
endclass
实例4-12:
使用分析export的组件
classpacket_checkerextendsuvm_component;
uvm_analysis_imp #(simple_packet,packet_checker)analysis_export;
functionnew(stringname,uvm_componentparent);
analysis_export=new("analysis_export",this);
endfunction
functionvoid write (simple_packetp);
//checkthepackethere
endfunction
endclass
这两个组件可以在父组件中被创建,然后使用UVM通用的TLM连接规则将分析port和分析exort相连接。
上面提到,既然分析port允许多个分析export与之相连,可以例化多个具有分析export的组件,将他们连接到packet_monitor组件的分析port上。
有时候,通过分析port传送的交易不能够马上被与之相连的下游组件处理,这些交易需要存储一段时间之后才能够被消耗处理掉。
比如,当记分牌组件需要将DUT产生的真实封包和参考模型产生的封包进行比较的情形。
在这种情况下,由于DUT需要延时,从参考模型产生的封包需要存储下来。
uvm_tlm_fifo能够在需要的时候存储封包,似乎可以很好的解决上述问题。
然而,uvm_tlm_fifo并没有一个分析export,所以不能直接将它连接到分析port上去。
一个重要的原因是:
在分析export中对write方法的实现要求传递transaction之后马上返回,但是如果FIFO是有限的固定深度,就不是总能够满足这一点。
UVM使用uvm_tlm_analysis_fifo来解决此问题。
uvm_tlm_analysis_port拥有一个分析export,因此可以直接和分析port相连,并且它的FIFO具有无限深度,所以write方法调用可以立即成功返回。
4.6.1.7`uvm_*_imp_decl宏
有些情形,组件需要对相同的接口拥有多种实现。
例如一个记分牌类需要对多个接口(比如两个input和一个output)进行监测。
在这种情况下,必须提供处理多个接口的方法。
有3种潜在的解决方案:
∙为每个特定的接口创建一个组件来实现.
∙如果每个接口的交易类型相同,可以使用一个实现;这个需要交易对象提供一个可以区分其来源的机制.
∙对每个port创建_imp类型,每个_imp类型调用不同的功能实现函数.
在UVM中,由于使用了`uvm_*_imp_decl宏,使得第3种方式最简单。
这些宏用来创建新的实现类型,用来转到不同的实现函数。
比如,使用`uvm_analysis_imp_decl(_1),将会得到一个uvm_analysis_imp_1#(typeT)的类实现,此类中实现了对函数write_1的实现。
小技巧:
`uvm_*_imp_decl宏的使用准则