向UVM-TLM通信发起决战
前言开头想先说点体会,最早学习uvm时,TLM通信这一章,自己最开始并没有很重视。到了亲自搭验证环境时才发现TLM至关重要,没有TLM,产生的事务无法在各个验证组件之间流通。这就好比人空有一副骨架,但没有血液在流通。不掌握TLM机制,会导致很多代码看不明白。
个人总结的TLM的难点如下:
[*]端口类非常的多,且大量用到参数化的类这种写法
[*]组件之间的连接该用那个端口类比较模糊,没有一个清晰的架构图。
基于上述自己的短板,所以有了这篇文章,希望能对这部分有个全面的梳理和了解。
梳理的结果有三部分:
[*]理论概念部分:port,export,imp的关系
[*]浅析源代码部分:TLM端口类的类库地图,并根据源代码浅析他们之间的继承关系
[*]实战部分:uvm验证环境端口连接关系图,这对于实际搭建uvm验证环境端口类的选取非常有用
一、理论概念部分
1.1 uvm1.2中,TLM1.0和TLM2.0的关系?
去看uvm_pkg.sv中的内容,会发现里面同时包括了tlm1和tlm2
https://i-blog.csdnimg.cn/direct/348ff96dc66946738f7716ff3622e380.png
去看tlm1和tlm2中的内容,发现tlm2并不是对tlm1中的类进行了完善和优化,tlm1中出现的类,在tlm2中没有再出现,tlm2中定义了tlm1中完全没有的全新的类。所以tlm2.0是完全的新的东西。
而对于uvm的初级使用,基本上只会涉及到tlm1.0的内容。tlm2.0是更高阶的玩法。因此本篇只解析TLM1.0,仅TLM1.0就够我喝一壶的了。
接口与方法差异:
[*]TLM 1.0 定义了诸如 put()、get()、peek() 等基础的传输方法,组件之间的通信基于这些简单的方法实现。例如,一个发起方组件调用 put() 方法将事务发送给目标方组件。
[*]TLM 2.0 引入了新的接口和方法,其通信机制更为复杂和精确。它有 nb_transport_fw()、nb_transport_bw() 等方法,用于支持双向通信和精确的时序建模。这些新的接口和方法与 TLM 1.0 的接口在功能和使用方式上有很大不同,不能直接相互调用。
时序建模方式差异:
[*]TLM 1.0 对时序的支持非常有限,通常不考虑事务传输的精确时间,主要关注功能验证。
[*]TLM 2.0 强调精确的时序建模,通过时间注解来描述事务的传输时间和延迟。这种差异使得 TLM 1.0 组件和 TLM 2.0 组件在处理时序时难以直接协同工作。
1.2 TLM1端口类型及端口方法
首先,端口是不能单独存在的,它必须是被例化在某个component中。端口就相当于是component的门,而门不能是孤立的单独存在。 要想使用TLM机制,就必须要在对应的组件中创建相应的端口,TLM的功能是通过端口来实现的。
component就像房子,而端口就是房子的门和窗户。如果没有端口,component之间就是孤立的
在TLM机制中,拥有port类型端口的组件是发起操作的主动方(master),而拥有imp类型端口的组件是被动方(slave)
端口类型分为
[*]port,export,imp
[*]analysis_port,analysis_export,analysis_imp
总体分为两大类,带analysis的和不带analysis的。
[*]两类端口之间不互通。不能混用,即非analysis端口之间可以互相连接,analysis端口之间可以互相连接。但是非analysis端口和analysis端口之间不能互相连接
[*]不带analysis的端口,只能单个点对点连接。优先级port>export>imp。连接方式有:
port.connect(port)port.connect(export)port.connect(imp)export.connect(export)export.connect(imp)
[*]带有analysis类型的端口表示可以一对多连接。其他情况与非analysis端口保持一致
https://i-blog.csdnimg.cn/direct/83a84fd65c604533a2c37450e8b064e8.png
https://i-blog.csdnimg.cn/direct/c66423e643084cce8fda9449325b435d.png
端口阻塞方法(阻塞当前进程):
put(input T t)发起端把数据包发送给目标端。数据流向为发起段到目标端task类型,阻塞当前进程,直到trans发送成功get(output T t)发起段向目标端索要数据包。数据流向为目标端到发起端task类型,阻塞当前进程,直到trans获取成功peek(output T t)和get方法一样,区别是peek是获取数据包的复制包
task类型,阻塞当前进程,直到trans获取成功
端口非阻塞方法(不阻塞当前进程):
try_put(input T t)尝试发送trans,执行成功返回1,失败返回0
function类型,不会阻塞当前进程,传输可能成功也可能失败
can_put()检查接收端是否准备好了接收事务,执行成功返回1,失败返回0
function类型,不会阻塞当前进程
try_get(output T t)尝试获取trans,function类型,其他同上can_get()检查对方是否能够返回事务,function类型try_peek(output T t)function类型,其他同上can_peek()function类型,其他同上write()为analysis类型端口独有且仅有的方法,非阻塞 TODO几点结论:
[*]port,export,imp体现的是控制流而非数据流。地位优先级port>export>imp。put操作中,数据从port流向imp,get操作中,数据从imp流向port,这类似于master发起的写和读。操作都是由地位更高的master来发起的。analysis类型的同理。
[*]在两个端口的连接过程中,只有高优先级的才能调用connect函数,而相对低优先级的只能作为connect函数的参数。不能倒反天罡。
[*]connect函数调用要有始有终。开始一定是port发起,结束一定是imp端口。如果port和imp直接连接,那么总共调用一次connect函数port.connect(imp)。如果中间有export。这条连接线的结尾一定不能是export,export还需要连到最终的imp上。port.connect(export),export.connect(imp)。
1.3 TLM原理
https://i-blog.csdnimg.cn/direct/ffa57481de1e4ae298da3ded8a62f934.png
以put方法为例,TLM传输可以分为几个步骤:
[*]在connect phase中通过connect函数将端口连接
[*]主控方的端口发起操作,调用put任务
[*]在port端口的put任务中,调用了与其连接的export端口的put任务
[*]在export端口的put任务中,调用了与其连接的imp端口的put任务
[*]在imp端口的put任务中,调用了imp所在的component的put任务
上述的原理是基于概念图的抽象解释,于是引发了如下疑问:
[*]这些方法在原型类中都是空的,最终的实现都是在imp所在的component即slaveB中完成的。那么slaveB中的put()任务究竟该怎么写?
[*]源头的port端口调用put任务后,是如何引发后续端口的put任务的?
其实在port端口的put任务中,调用了与之相连的export的put任务。而在export的put任务中,又调用了与之相连的imp端口的put任务。imp的put任务中又调用了imp所在的component的put任务。
而port端口的put任务中,之所以能调用export的put任务。根本前提是通过connect函数将port和export连接成功之后,port端口类中拿到了指向export端口的句柄。其他同理。这部分的具体实现将在下文中源代码解析部分详细阐述。
到此为止,通过一些抽象的图初步理解了TLM通信的基本原理。但是如果真正去看一个uvm验证环境,会发现还是看不懂,会看到各种奇奇怪怪的类出现。只根据几个概念图去了解原理,而不去看真正的代码实现,根本不能叫做理解TLM机制!!!
uvm中,基于上面提到的各种端口类型都提供了一系列的类。实际验证环境中,各种端口类都是基于uvm提供的这些类来例化的。
因此,必须从源头出发,全面透彻的梳理TLM1中的类,清楚有哪些类,这些类可以分为几部分?这些类之间的继承关系是什么?在实际搭建环境时,该选取什么样的类?
二、走到代码中去
2.1 TLM1中用到的class全览
tlm1中用到的所有类都集中在了这些文件中了,从上往下看可以分为四个部分
https://i-blog.csdnimg.cn/direct/caa42ea5984e4b4eb3be193330b2393c.png
2.2 TLM1中的端口类
uvm_tlm_if_base#(T1,T2) ★★★
这个类是tlm1中所有端口类的基类。注意,该类是最基础的基类,它没有从任何类扩展而来,注意端口类并不是从uvm_object或者uvm_component扩展而来的
https://i-blog.csdnimg.cn/direct/3702c95ade4c4eb1b52c01549cc615e7.png
这个类的主要作用是将端口的最基础的方法做了封装。该类中包含的方法有:
https://i-blog.csdnimg.cn/direct/c156775dc8f84e44a70ec336eeb5a112.png
put(),get(),peek(),try_put(),can_put(),try_get(),can_get(),try_peek(),can_peek(), transport(),nb_transport(),write()
uvm_tlm_if_base是一个virtual class。不能被实例化,只是做了一些定义,其内部的虚方法也只是声明了名字,方法内部只有一句uvm_report_error。
这意味着从uvm_tlm_if_base扩展而来的子类,如果要调用这些方法,必须对这些虚方法进行重写。否则将会报错
uvm_sqr_if_base#(T1,T2) ★★★
https://i-blog.csdnimg.cn/direct/446e416350dd4c58b2f6827df1de73cd.png
uvm_sqr_if_base也是一个基类,它不从任何类扩展而来,和uvm_tlm_if_base的地位是一样的。而且这个类是专门用于sequence,sequencer,driver之间的通信的。 该类的作用也是封装了一些方法:
https://i-blog.csdnimg.cn/direct/d48dde7c4fcd4a02a165c68559c33926.png
get_next_item(),try_next_item(),item_done(),wait_for_sequences(), has_do_available(), get(),peek(),put(),put_response(),disable_auto_item_recording(),is_auto_item_recording_enabled()
同样,这些方法必须在子类中被重写,否则将会引发uvm_report_error
uvm_port_component_base
https://i-blog.csdnimg.cn/direct/3d3884cee28e41b9a1e265c2d7c9b534.png
这个类继承自uvm_component,是个抽象类,内部定义了一些纯虚方法:
get_connected_to(),get_provided_to(),is_port(),is_export(),is_imp
这个类是干什么用的?
uvm_port_component#(PORT)
继承自uvm_port_component_base,所以也是一个uvm_component类。会传入一个PORT参数,在类内部,PORT类会声明一个句柄m_port,uvm_port_component类在实例化时会传入一个PORT类的句柄port。 最终m_port指向的是传入的port。
这个类中所有的function和task都是围绕m_port展开的。
https://i-blog.csdnimg.cn/direct/c699b46a4cc14f8485b9a86b18571702.png
https://i-blog.csdnimg.cn/direct/84e26f1ecc244968854a5d5aa29442ee.png
这里的函数调用很有意思,这里定义的函数是is_port,是对父类uvm_port_component_base类中纯虚方法is_port的重写。函数内部调用的是m_port.is_port()。这里比较奇怪,如果PORT默认是uvm_object类型的话,是没有is_port()这个函数的。所以参数化的类,传入的参数,是根据实际传入的类型来看的
父类中如果有纯虚方法(pure virtual function),子类在继承时,必须对纯虚方法进行实现
uvm_port_base#(IF) ★★★★★
uvm_port_base是整个TLM1中所有类的核心。这个类极其极其重要,后面会发现所有的port,export,imp的端口类都是从此类扩展而来。
https://i-blog.csdnimg.cn/direct/95db3bfbcbd4417898bd6e7dec8e261c.png
uvm_port_base#(IF)类的声明很有意思,给该类传入一个参数IF,该参数作为uvm_port_base的父类。及uvm_port_base具有了IF类的全部特性。
uvm_port_base在实际使用时,传入的IF参数要么是uvm_tlm_if_base#(T,T),要么是uvm_seqr_if_base(T,T)。T为该端口实际要处理的transaction事务类
在该类中,还有一个写的巧妙的地方。在uvm_port_base类内部,typedef这个类为this_type,并将this_type作为uvm_port_component的PORT参数传入。 声明了一个uvm_port_component的句柄,m_comp
在该类中,定义了非常多的方法。列举一部分:
is_port(),is_export(),is_imp(),connect()
connect函数是整个的核心。port和export,port和imp,export和export,export和imp之间的连接都是通过connect函数实现的。整个的实现方法是非常复杂的,这里不深入研究。
uvm_*_port#(T)
在实际使用时,传入的参数类T为该端口要处理的transaction事务类
https://i-blog.csdnimg.cn/direct/ac1c5a52fc194d4f8f14a80b3349e5b4.png
port类型的端口类非常多,统计共有23个,看着眼花缭乱,其实有规律可循,可以分成如下几个部分。
https://i-blog.csdnimg.cn/direct/15573a83229a4929bc878a45bc50ea34.png
图片来自于博客:UVM Tutorial for Candy Lovers – 20. TLM 1 – ClueLogic
什么情况下该用什么样的port类?
上面的图把继承关系描述的非常清楚,最右侧是扩展而来的23个子类,这些子类之间的本质区别在于内部封装的方法不同。
这里以put_port为例,看上面的类图,从名字上会发现有如下规律:
uvm_blocking_put_port#(T),uvm_nonblocking_put_port#(T),uvm_put_port#(T),这三个都含有put,从源代码可以看出,它们是封装了*put方法相关的类
https://i-blog.csdnimg.cn/direct/a7d5949bc47348aa9f806c39c0993f6a.png
https://i-blog.csdnimg.cn/direct/0c6c78ba63204b979ee6341c5b6106a0.png
https://i-blog.csdnimg.cn/direct/a02d0bb7c95a4db1a79d42e7383e6657.png
这三个类之间的区别在于,封装的方法类型不一样。上面讲到了put()任务为阻塞类型,而try_put()方法和can_put()方法为非阻塞类型。
[*]从名字可以看出,uvm_blocking_put_port#(T)为阻塞类,所以其只能包含阻塞方法put()。
[*]uvm_nonblocking_put_port#(T)为非阻塞类,所以其只能包含非阻塞方法,try_put(),can_put()
[*]uvm_put_port#(T)既可以阻塞,也可以非阻塞,所以三种都包括
因此,一个component使用什么样的端口,取决于主控方想要发起的操作是什么。如果masterA的端口想要执行put()任务,那么只能使用uvm_blocking_put_port#(T)和uvm_put_port#(T)这两类端口。如果masterA的端口想要执行try_put()或者can_put(),那么只能使用uvm_nonblocking_put_port#(T)的端口类。
在文件中看其他的类,和上面所说的逻辑是完全一样的,这里不再赘述,通过一张图来进行整理
https://i-blog.csdnimg.cn/direct/f93b5c8362fb419e8a222626c9d5586e.png
uvm_*_export(T)
在实际使用时,传入的参数T为该端口要处理的transaction事务类
https://i-blog.csdnimg.cn/direct/2386476e912449f09dff8f10fc7fb4b3.png
export类型的端口类从uvm_port_base继承而来,也有23个子类,可以发现它们和port类型的端口一一对应。
数量一一对应是必须的,必须保证使用的端口类的前缀完全一致。因为在使用connect函数连接时,会先检查连接的端口类型的前缀是否一致。否则会报错
如uvm_nonblocking_get_port#(T)的前缀是uvm_nonblocking_get_。它只能和uvm_nonblocking_get_export#(T)或者uvm_nonblocking_get_imp#(T)相连
https://i-blog.csdnimg.cn/direct/448ae57738844ed7ac6bcf2711858d23.png
uvm_*_imp(T,IMP)
注意,imp类型的端口类传入的参数有两个,T是该端口要处理的transaction事务类,IMP是该imp端口所在的component类
https://i-blog.csdnimg.cn/direct/bba43d6322544cd78b499a08db57853a.png
imp类型的端口类从uvm_port_base继承而来,也有23个子类,和上述两类端口一一对应。
https://i-blog.csdnimg.cn/direct/8595c64d618d4f65b1cf98e8f37ef79c.png
不同端口类型的连接规则
port可以向port连接,可以向export发起连接,可以向imp发起连接
export可以向export发起连接,可以向imp发起连接
imp不能向任何端口发起连接
层级的要求是个问题,需要探讨
2.3 TLM1中的FIFO类
注意FIFO类和端口类是完全不同的,FIFO类属于组件,是component。而端口类必须是在component类中例化的。
重视FIFO类在使用中的重要性
在实际验证环境中会遇到需要将进程1中的componentA连接到进程2中的componentB的情况,因为TLM组件需要在自己的进程中工作(解耦合)。
https://i-blog.csdnimg.cn/direct/6f63b2fbaedd4713bd12609c5bb73a91.png
之前的端口直连的过程是,只存在一个进程,port端口调用方法,会引发一系列的方法调用。
有些情况下,两个平级的组件的run_phase是并行执行的。它们在各自的线程中调用自己的方法。这时就需要有一个缓冲区。即componentA发出的事务先放在一个FIFO里存起来,componentB在执行到自己的线程时,再从FIFO里去取。
uvm_tlm_fifo_base#(T)
https://i-blog.csdnimg.cn/direct/8a0cb3d8a51a4ea69df116a2f140d5c4.png
https://i-blog.csdnimg.cn/direct/5d7c5d31768846d0b7ee8838da7e47ec.png
uvm_tlm_fifo_base#(T)继承于uvm_component,是一个virtual class,只能被继承,不能被实例化。fifo类可以看做是验证环境中的一个组件。在fifo类中定义了非常多的端口和方法。
包含的端口
https://i-blog.csdnimg.cn/direct/1e435c69083140d4b6e28f10d4e738f5.png
在uvm_tlm_fifo_base#(T)中,只定义了三种类型的端口
[*]uvm_put_imp#(T,IMP)
[*]uvm_get_peek_imp#(T,IMP)
[*]uvm_analysis_port#(T)
https://i-blog.csdnimg.cn/direct/ee037b362f574d51bde99e6b4e00dc78.png
注意,在uvm_tlm_fifo_base#(T)中没有export类型端口。
虽然图上标注的都是export端口名,但是从源代码看出,这些都是imp类的句柄名。虽然起名叫export,但实际是imp类型。为什么要这样有意而为呢?
答案:
https://i-blog.csdnimg.cn/direct/14d2199302294e50a65d1a36014f96d1.png
虽然有这么多的句柄,但是在new函数中,只实例化了一个put_export,一个get_peek_export,一个put_ap,一个get_ap。 其余的句柄并没有单独实例化,而是指向了这四个实例。所以在图上,没有被实例化的句柄,用浅颜色标注。
声明的方法
build_phase(),flush(),size(),put(),get(),peek(),try_put(),try_get(),try_peek(),can_put(),can_get(),can_peek(),ok_to_put(),ok_to_get(),ok_to_peek(),is_empty(),is_full(),is_used()
这些方法都是空的,需要在子类中被重写,否则会报错。
uvm_tlm_fifo#(T)和uvm_tlm_analysis_fifo#(T)
https://i-blog.csdnimg.cn/direct/8a0cb3d8a51a4ea69df116a2f140d5c4.png
由于uvm_tlm_fifo_base#(T)是fifo类的基类,并且是virtual class。所以只定义了一些基础的公共变量和方法。不能直接拿来使用。
我们在实际搭建验证环境时,用到的fifo类都从该基类扩展而来。有两种,分别是uvm_tlm_fifo#(T)以及uvm_tlm_analysis_fifo#(T)。
https://i-blog.csdnimg.cn/direct/e34f850e82a447f888ae5803466946a7.png
图片来自于博客:UVM Tutorial for Candy Lovers – 20. TLM 1 – ClueLogic
[*]uvm_tlm_fifo#(T)继承了uvm_tlm_fifo_base#(T),并没有再额外声明端口,只是重写了uvm_tlm_fifo_base中的方法
[*]uvm_tlm_analysis_fifo#(T)继承自uvm_tlm_fifo,多了一个uvm_analysis_imp类型的端口,analysis_export
https://i-blog.csdnimg.cn/direct/c047d97c02154b6bae414e04e23d5216.png
一个关键问题:uvm_tlm_fifo是如何实现fifo的特性的
uvm_tlm_fifo是一个类,其具有fifo先入先出特性的核心是内部例化了一个mailbox类。mailbox本身就具有fifo的性质。
https://i-blog.csdnimg.cn/direct/df409809ce5c4a6c8c710253644b4440.png
https://i-blog.csdnimg.cn/direct/f75d6d7b08164dac82b717c2d2038de4.png
mailbox机制可以参考:
使用uvm_tlm_fifo的工作原理
https://i-blog.csdnimg.cn/direct/6f63b2fbaedd4713bd12609c5bb73a91.png
[*]connect:componentA.port.connect(uvm_tlm_fifo.put_export),componentB.connect(uvm_tlm_fifo.get_export)
[*]在componentA的run_phase中,调用componentA.put(),从而调用uvm_tlm_fifo.put_export.put(),进而调用uvm_tlm_fifo.put()。此时会把trans放入uvm_tlm_fifo的mailbox中
[*]在componentB的run_phase中,调用componentB.get(),从而调用uvm_tlm_fifo.get_export.get(),进而调用uvm_tlm_fifo.get()。此时trans会从mailbox中出来。
所以最终put()和get()方法的实现都是在imp所在的component类中实现的。uvm_tlm_fifo#(T)中的这些方法继承于uvm_tlm_fifo_base,并对这些方法进行了重写。
https://i-blog.csdnimg.cn/direct/8b0777918ddb420797038eb09229ee9f.png
这些方法都是对mailbox的操作,以及调用put_ap和get_ap的write()函数
这里put_ap和get_ap也没有和别的口连接,调用它们的write有什么用? 调用write函数,则put_ap和get_ap必须有所连接,否则应该会报错吧。但是什么时候会把put_ap和get_ap连接呢?和谁连接呢?必须要进行连接吗?
analysis_port和analysis_fifo的区别和联系
刚开始写代码时,会经常混淆analysis_port和analysis_fifo。如果把前面的梳理清楚,这里我们自然就清楚两者有着本质的区别。
注意analysis port和analysis fifo有着本质的不同,前者是端口类,而后者是FIFO类,FIFO可以认为是一个组件component,在FIFO中,例化了非常多的imp端口以及若干的analysis port端口
2.4 TLM1中的Channel类
https://i-blog.csdnimg.cn/direct/02a0937157cf4eb2bc7d73aa03204cdd.png
https://i-blog.csdnimg.cn/direct/3b605a47870c425eaef7802b2d49b658.png
暂未使用过,之后遇到再补充。
2.5 sequence机制中用到的端口类
sequence机制是事务产生和发送的核心机制。涉及到sequencer和driver之间的事务传输。用到的端口类也比较特殊。只有一种前缀对应的port,export,imp端口。这里把上述类库地图的最后一部分单独截图出来。
https://i-blog.csdnimg.cn/direct/8d1809971f0f4ff49556c646ffd50cbc.png
https://i-blog.csdnimg.cn/direct/a1fc260a47354672a2ae7bf9fba52272.png
https://i-blog.csdnimg.cn/direct/af9594b02f4a4df1842e7e150ba9dde9.png
这三种seq_port类是
[*]uvm_seq_item_pull_port#(REQ,RSP)
[*]uvm_seq_item_pull_export#(REQ,RSP)
[*]uvm_seq_item_pull_imp#(REQ,RSP,IMP)
它们也是继承自uvm_port_base#(IF),但是和其他端口类的区别是,继承的uvm_port_base#(IF)的IF参数是uvm_sqr_if_base#(REQ,RSP)。而不是uvm_tlm_if_base#(T,T)。两者的区别在2.2节中已经描述。
这三类端口的典型使用场景就是在sequence机制中。在uvm_driver中例化了uvm_seq_item_pull_port#(REQ,RSP),在uvm_sequencer中例化了uvm_seq_item_pull_imp#(REQ,RSP,IMP)。
https://i-blog.csdnimg.cn/direct/7cbb5dfd610a42ddaaa5927eb0d1c13a.png
https://i-blog.csdnimg.cn/direct/9eba1b0a88cf496499d779742158da1f.png
https://i-blog.csdnimg.cn/direct/580005f024d640bb9bbf9c9f1a1a5efa.png
sequence机制中的TLM传输
[*]connect:在agent中,连接driver和sequencer。my_drv.seq_item_port.connect(my_seqr.seq_item_export)
[*]在my_drv的run_phase中,调用seq_item_port.get_next_item(),进而调用了my_seqr.seq_item_export.get_next_item(),进而调用了my_seqr.get_next_item(),此时my_seqr收到my_drv的数据请求。会检查是否有sequence发送到my_seqr,如果没有则处于等待状态。
[*]seq调用seq.start(my_seqr),将产生的事务发送给my_seqr。此时事务通过my_seqr传递给my_drv。
[*]my_drv处理完事务之后,调用seq_item_port.item_done(),表示结束,最终调用的是my_seqr.item_done()
2.6 TLM机制的源代码解析:
在上面的传输过程中,多次描述到这样一个过程:
当调用port.put()方法时,会引发调用与之相连的imp.put()方法,进而会调用imp所在的component类的put()方法。
那么由此引发两个疑问:
[*]port.put()是如何引发后续方法的调用的?
[*]最终的put()方法是在imp所在的component类中实现的,具体要写成什么,才算实现了put()?
在"tlm1/uvm_ports.svh"文件中定义了各种各样的port类,定义写法都是一样的,这里找一个拆开解析
https://i-blog.csdnimg.cn/direct/18a90a759783420ca5c08396898d1143.png
首先,uvm_blocking_put_port#(T)继承自uvm_port_base#(IF),这里传入的IF参数是uvm_tlm_if_base#(T,T)类。所以uvm_blocking_put_port继承自uvm_port_base进而继承自uvm_tlm_if_base。 uvm_blocking_put_port传入的参数T,最终会传入到uvm_tlm_if_base中的参数(T,T)。 这里的参数T一般来说是流经端口的事务类。是uvm_sequence_item的子类。
在uvm_blocking_put_port中重写了uvm_tlm_if_base中的put任务,在put任务中调用了this.m_if.put()。this.m_if是从uvm_port_base中继承的,m_if是uvm_port_base类的一个句柄。
https://i-blog.csdnimg.cn/direct/d0f85953cf6e400889b79d6c03ea140e.png
这里的this.m_if指向的是与uvm_blocking_put_port相连的uvm_blocking_put_imp的实例。
这里有了一个关键性疑问,在put_port中的m_if,是在什么时候指向的put_imp实例?
很自然的会想到connect函数,因为只有调用port.connect(imp)才建立两个端口之间的联系。
走进connec()函数
https://i-blog.csdnimg.cn/direct/8f34726d7b28467892f16643d718938f.png
代码太长,这里就不贴了,总结connect函数主要干的事情。
connect()函数传入的参数为一个uvm_port_base类型的句柄,provider
[*]检查provider是否为空,是否是自己本身,检查provider的m_if_mask和自己的m_if_mask关系
[*]检查端口连接关系,不能是export连port,自己本身不能是imp等等
[*]检查relationship,更复杂的层级关系要满足
[*]以上都满足之后,说明端口连接是合法的,就把provider放入当前port端口类中的关联数组m_provided_by中。同时provider也把当前port类放入自己的关联数组m_provided_to中
会发现,connect()函数并没有对m_if的任何操作。
我想说的是接下来这部分,将是我看uvm以来最冲击我的一段代码。这段比较细节,即使不清楚也无所谓。
uvm_port_base中,唯一出现m_if赋值的地方是:
https://i-blog.csdnimg.cn/direct/422d7171a4aa4b448b991490bfb4e89a.png
寻找set_if(()被何时被调用
https://i-blog.csdnimg.cn/direct/f75e01a5c3f3401683429b77f12eec39.png
在uvm_port_phase中的resolve_bindings()函数中的最后一行,调用了set_if()函数。
resolve_bindings()函数做的事情是:
如果当前端口不是imp端口,那么会遍历m_provided_by这个关联数组,这个关联数组中是上述connect函数执行之后,存放的与当前port相连的端口。先调用了连接端口的resolve_bindings。 然后调用了m_add_list。 将m_provided_by中的值放到m_imp_list关联数组中。
最终调用了set_if()。在set_if中调用了get_if。最终m_if指向的是m_imp_list关联数组中的第一个imp实例。
那么resolve_bindings()是何时被调用的呢?
https://i-blog.csdnimg.cn/direct/e53a9e0112e84c28af31119dff50dcc5.png
不起眼的m_comp是关键。在uvm_port_base中,例化了一个uvm_component类型的类。
m_comp的作用一个是继承了来自uvm_component的一些report方法,便于打印一些信息。另外是m_comp在实例化之后,会根据phase机制,自动执行一些函数。
https://i-blog.csdnimg.cn/direct/68f83a570f4744cdbe9619807d3e2489.png
在m_comp中,有一个resolve_bindings函数,会调用m_port.resolve_bindings()函数。
而m_comp中的resolve_bindings函数是重写的uvm_component的resolve_bindings()函数。
https://i-blog.csdnimg.cn/direct/4afee325411a49b0bfd65ec439e39515.png
而uvm_component的resolve_bindings()函数是在end_of_elaboration phase开始之前自动执行的。
至此,整个的顺序就已经清晰了。
https://i-blog.csdnimg.cn/direct/c0122322f70d4882bdc23f4e14f013ee.png
回到最开始uvm_blocking_put_port#(T)中put()任务的描述:
https://i-blog.csdnimg.cn/direct/18a90a759783420ca5c08396898d1143.png
在put(T t)中,调用了this.m_if.put(t)。注意此时port端口处理的事务t,通过task的参数传递到了this.m_if.put(t)中。
再进到uvm_blocking_put_imp#(T)中,看其内部定义的put任务
https://i-blog.csdnimg.cn/direct/3125b38e4a434c65b827d14571668195.png
可以注意到imp类型的类,其参数多了一个IMP,这个IMP是端口所属的component类本身。一般在component中例化该imp端口时,会传入this。
在imp类中,声明IMP的一个句柄m_imp,imp类的put(T t)任务中,调用m_imp的put(t)任务。最终masterA中的trans,被slaveB拿到。
put()任务的参数t,在调用过程中传递,是整个TLM通信中事务传输的最终实现。对于get()任务,参数的传递方向改为output,与put()方向相反。
https://i-blog.csdnimg.cn/direct/e2c5dc0014244d51910b4eefbdbeb544.png
2.7 使用宏 `uvm_*_imp_decl
注意,该宏只针对imp类的端口
该宏出现解决的问题基于如下场景
https://i-blog.csdnimg.cn/direct/72d401e7d1da4f0c900addc8f1078af0.png
不同于之前的一对一连接,现在有componentA和componentC都要和componentB连接。将会面例如下问题:
当按照常规方法,做如下连接时,compA.portA.put(t)最终会调用compB.put(t),而compC.portC.put(t)最终也会调用compB.put(t)。如果A和C同时向B发起传输,将会同时调用put,并且无法解决各自的需求。这显然是不合理的。
class componentA extends from uvm_component; uvm_put_port#(A_trans) portA; // …… task run_phase(uvm_phase phase); portA.put(A_trans); endtask endclassclass componentC extends from uvm_component; uvm_put_port#(C_trans) portC; // …… task run_phase(uvm_phase phase); portC.put(C_trans); endtask endclass class componentB extends from uvm_component; uvm_put_imp#(A_trans,this) impAB; uvm_put_imp#(C_trans,this) impCB; task put(); // …… //这里将会引发矛盾和冲突 endtask endclass class test_env extends from uvm_env; componentAcompA; componentBcompB; componentCcompC; function void connect_phase(uvm_phase phase); compA.portA.connect(compB.impAB); compC.portC.connect(compB.impCB); endfunction endclass自然而然的会想到,在componentB中定义两个put方法,put_A()和put_C()方法。
[*]当发起compA.portA.put()时,最终调用compB.put_A()方法
[*]当发起compC.portC.put()时,最终调用comB.put_C()方法
以此来实现两路传输的独立性。但是依靠原有的框架,使用原有的类,是无法实现的。因为原有的imp类中,方法是固定写死的,不会有我们人为定义添加的put_A()和put_C()。
我们可以做的是自定义新的impAB类和impCB类,重新定义imp中的put()方法
[*]在impAB类中调用put()方法时,调用的是compB.put_A()方法
[*]在impCB类中调用put()方法时,调用的是compB.put_B()方法
而这所有的区别,仅在于后缀的不同。uvm中提供了宏帮我们来完成这个过程。
`uvm_*_imp_decl宏共有23种,这个宏展开后其实就是对比常规的23种imp类的重新定义。
uvm_blocking_put_imp_decl(SFX)
uvm_nonblocking_put_imp_decl(SFX)
uvm_put_imp_decl(SFX)
uvm_blocking_get_imp_decl
uvm_nonblocking_get_imp_decl(SFX)
uvm_get_imp_decl(SFX)
……
以uvm_blocking_put_imp_decl(SFX)为例,展开解析:
https://i-blog.csdnimg.cn/direct/b4b6a9428bd745848bbc488640c262bc.png
这个宏是带参数的宏,这个参数就是人为定义的后缀。使用该宏时,就会产生一个新class的声明。如`uvm_blocking_put_imp(_A)等价于声明了一个class,类名为 uvm_blocking_put_imp_A。
在类内部,又调用了两个宏,展开来看:
第一个宏就是在常规的imp类中用到的宏,声明了new函数,以及指定了type_name。没有什么特别之处。
https://i-blog.csdnimg.cn/direct/b7ab95252c944c419caffa0ca627525a.png
第二个宏,和常规imp类中的宏不同,这里重新定义了put()任务中的内容:
https://i-blog.csdnimg.cn/direct/1ccf5a4c479b43d98e4ea47fdeea9d01.png
https://i-blog.csdnimg.cn/direct/79d6bcbf5d064e3bba29deaeed61853e.png
至此,使用这个宏之后的代码如下:
`uvm_put_imp_decl(_A) `uvm_put_imp_decl(_C)// 两个宏的使用一定要在class之外,因为宏本身代表的就是class的声明。class componentA extends from uvm_component; uvm_put_port#(A_trans) portA; // …… task run_phase(uvm_phase phase); portA.put(A_trans); endtask endclassclass componentC extends from uvm_component; uvm_put_port#(C_trans) portC; // …… task run_phase(uvm_phase phase); portC.put(C_trans); endtask endclass class componentB extends from uvm_component; uvm_put_imp_A#(A_trans,this) impAB; //使用新定义的imp类 uvm_put_imp_C#(C_trans,this) impCB; //使用新定义的imp类 task put_A(); // task名字必须和后缀保持一致 // …… endtask task put_B(); // task名字必须和后缀保持一致 // …… endtask endclass class test_env extends from uvm_env; componentAcompA; componentBcompB; componentCcompC; function void connect_phase(uvm_phase phase); compA.portA.connect(compB.impAB); compC.portC.connect(compB.impCB); endfunction endclass至此,上述情景的传输过程如下:
https://i-blog.csdnimg.cn/direct/cfea2dc1622e4c6bb00ef31a00994267.png
注意:在新定义的imp类中,put任务是不带后缀的,依旧是叫put()。这里的名字必须和port中的保持一致。区别是调用的不再是componentB中的put(),而是componentB中的put_*()
三、实战部分
在这个网站中,提供了一些连接场景下的代码模板
UVM TLM Example
3.1 各component的连接结构图
需要画一张图,画一个大而全的图
3.2 代码部分
本文转自 https://blog.csdn.net/weixin_48157494/article/details/151726600,如有侵权,请联系删除。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页:
[1]