目录
基本概念
TLM通信分类
单向通信
单向通信举例
单向通信代码
双向通信
多向通信
多向通信总结
通信管道
TLM FIFO
Analysis Port
Analysis TLM FIFO
芯片验证是在RTL模型初步建立后,通过验证语言和方法学例如SV/UVM来构建验证平台。该平台的特点是验证环境整体基于面向对象开发,组件之间的通信基于TLM,而在driver与硬件接口之间需要将TLM抽象事务降解到基于时钟的信号级别。
TLM是Transaction Level Modeling(事务级建模)的缩写,它起源于SystemC的一种通信标准。所谓transaction level是相对DUT中各个模块之间信号线级别的通信来说的。简单来说,一个transaction就是把具有某一特定功能的一组信息封装在一起而称为的一个类。仿真速度是TLM对项目进度的最大贡献,同时TLM传输中的 transaction 又可以保证足够大的信息量和准确性。
如果要提升系统模型的仿真性能,可以从两个方面出发:一个是建模本身的优化,另一个是模型之间的通信优化。通信优化可以通过降低通信频率,内容体积增大的方式来减少由于不同进程之间同步带来的资源损耗。TLM就是从通信优化角度提出的一种抽象通信方式。
TLM通信需要两个通信的对象,这两个对象分别称为 initiator 和 target 。区分它们的方法在于,谁先发起通信请求,谁就属于initiator;谁作为发起通信的响应方,谁就属于target ,但这个分类并不代表transaction一定是initiator发起的,transaction也可能是从target流向initiator。
按照transaction 的流向,我们可以将两个对象分为 producer 和 consumer 。数据从哪里产生,它就属于producer,数据流向了哪里,它就属于consumer。
譬如transaction从发起端到接收端,是发起端向接收端调用get函数,此时发起端是producer ,接收端是 consumer;transaction也可以从接收端到发起端,是发起端向接收端调用put函数,此时发起端是consumer,接收端是producer。
initiator 和 target 的关系同 producer 和 consumer的关系不是固定的
有两个参与通信的对象之后,用户需要将TLM通信方法在 target 一端中实现,以便于 initiator 将来作为发起方可以调用 target 的通信方法,实现数据传输;在 target 中实现了必要的通信方法后,需要对两个对象进行连接,在两个对象中创建TLM端口,继而在更高层次中将这两个对象进行连接。
TLM通信步骤可以分为:
① 分辨出component是属于initiator还是target,是producer还是consumer。
② 在target中实现TLM通信方法。(此为UVM的规定模式)
③ 在两个对象中创建TLM端口。(TLM端口不需要预设,只需要实例化即可)
④ 在更高层次中将两个对象的端口进行连接。
从数据流向来看,传输方向可以分为单向(unidirection)和双向(bidirection)
单向传输:由initiator发起request transaction。
双向传输:由initiator发起request transaction,传送至target;而target在接受了request transaction后,会发起response transaction,继而返回给initiator。
端口按照类型可以划分为三种:
※ port:经常作为initiator的发起端,initiator凭借port才可以访问target的TLM通信方法。
※ export:作为initiator和target中间层次的端口。
※ imp:只能作为target接收request的末端,它无法作为中间层次的端口,所以imp的连接无法再次延伸。
initiator需要有port,是起点,但是当initiator和target之间隔了很多层次的时候,那么中间这些层次的过渡就使用export,target上的就是imp,是终点。
如果将传输方向(单、双向)和端口类型(port、export、imp)加以组合,可以帮助理解TLM端口的继承树。TLM端口一共可以分为六类:
1. uvm_UNDIR_port #(trans_t) //单向
2. uvm_UNDIR_export #(trans_t)
3. uvm_UNDIR_imp #(trans_t, imp_parent_t)
4. uvm_BIDIR_port #(req_trans_t, rsp_trans_t) //双向
5. uvm_BIDIR_export #(req_trans_t, rsp_trans_t)
6. uvm_BIDIR_imp #(req_trans_t, rsp_trans_t, imp_parent_t)
要注意的是,端口是继承于uvm_void,端口既不是继承与object类型也不是继承与component类型,所以端口是不能使用type_id::create的。
单向通信(undirectional communication)指的是从initiator到target之间的数据流向是单一方向的,或者说initiator和target只能扮演producer和consumer中的一个角色。
按照UVM端口名的命名规则,它们指出了通信的两大要素:① 是否为阻塞的方式(是否可以等待延时);② 采用何种通信方法(put、get、peek)。其中单一端口函数的PORT可以为port、export和imp。
阻塞传输方式将blocking前缀作为函数名的一部分,而非阻塞方式则名为nonblocking。阻塞端口的方法类型为task,这保证可以实现事件等待和延时;非阻塞端口的方式类型为function,这保证方法调用可以立即返回。
blocking阻塞传输的方法包含:Put():initiator 先生成数据T t,同时将该数据传送至target。Get():initiator从target获取数据T t,而target中的该数据T t则应消耗。Peek():initiator从target获取数据T t,而target中的该数据T t还应保留。
nonblocking非阻塞方法分别是:try_put(); can_put(); try_get(); can_get(); try_peek(); can_peek()。非阻塞函数对应阻塞任务的区别在于它们必须立即返回值,执行成功返回1,执行失败返回0。
class itrans extends uvm_transaction;int id;int data;...
endclass
class otrans extends uvm_transaction;int id;int data;...
endclass
class comp1 extends uvm_component;uvm_blocking_put_port #(itrans) bp_port; //blocking putuvm_nonblocking_get_port #(otrans) nbg_port; //nonblocking get`uvm_component_utils(comp1)...task run_phase(uvm_phase phase)itrans itr;otrans otr;int trans_num = 2;forkbeginfor(int i=0; i
endclass
除了在component1和component2中定义任务和方法外,还要在env环境中对component1和component2做例化创建。以及接口的连接,这里要注意连接方向是initiator的port连接到target上面。
class env1 extends uvm_env;comp1 c1;comp2 c2;`uvm_component_utils(env1)...function void build_phase(uvm_phase phase);super.build_phase(phase);c1 = comp1::type_id::create("c1", this);c2 = comp2::type_id::create("c2", this);endfunction: build_phasefunction void connect_phase(uvm_phase phase);super.connect_phase(phase);c1.bp_port.connect(c2.bp_imp);c1.nbg_prt.connect(c2.nbg_imp);endfunction: connect_phase
endclass
首先comp1例化了两个port端口:
uvm_blocking_put_port #(itrans) bp_port; //blocking put
uvm_nonblocking_get_port #(otrans) nbg_port; //nonblocking get
comp2作为target例化了两个对应的imp端口:
uvm_blocking_put_imp #(itrans, comp2) bp_imp;
uvm_nonblocking_get_imp #(otrans, comp2) nbg_imp;
连接的方法是使用connect,其中connect的左侧是initiator,connect的右侧是target。
总结起来还是三步骤: ① 定义端口;② 实现对应方法;③ 在上层将端口进行连接。
与单向通信相同的是,双向通信(bidirectional communication)的两端也分为initiator和target,但数据流向在端对端之间是双向的。绝大多数的环境都是采用单向通信,双向通信应用的比较少。
双向通信中的两端同时扮演着producer和consumer的角色,而initiator作为request发起方,在发起request之后,还会等待response返回。
UVM双向端口不再采用get、put和peek,而是采用新的方式(transport、master和slave):
作为master的一端,在方法声明中,既有put()也有get()。
双向端口按照通信握手方式可以分为:① transport双向通信方式;② master和slave双向通信方式。transport端口通过transport()方法,可以在同一方法调用过程中完成REQ和RSP的发出和返回。master和slave的通信方式必须通过put、get和peek的调用,使用两个方法才可以完成一次握手通信。
多向通信(multi-directional communication)不是多个组件多个方向的通信,而是指initiator与target之间的相同TLM端口(同名,又相同类型)数目大于1时的处理办法。
产生问题的原因:
comp1有两个uvm_blocking_put_port,而comp2有两个uvm_blocking_put_imp端口,我们对于端口例化可以给不同的名字,连接也可以通过不同名字进行索引,但问题在于comp2中需要实现两个task put(itrans t),又因为不同端口之间要求在imp端口一侧实现专属方法,这就造成了方法命名冲突,即无法在comp2中定义两个同名的put任务。
解决方法:
UVM通过端口宏声明方式来解决这一问题,它解决问题的核心在于让不同端口对应不同名的任务,这样便不会造成方法名的冲突。
`uvm_blocking_put_imp_decl(_p1)
`uvm_blocking_put_imp_decl(_p2)//宏定义了两个独一无二的class comp1 extends uvm_component;uvm_blocking_put_port #(itrans) bp_port1;uvm_blocking_put_port #(itrans) bp_port2;`uvm_component_utils(comp1)...task run_phase(uvm_phase phase);itrans itr1, itr2;int trans_num = 2;forkfor(int i=0; i
endclass
这里涉及到很多知识点,在最开始的两行宏定义,就是解决多向通信问题的办法:
`uvm_blocking_put_imp_decl(_p1)
`uvm_blocking_put_imp_decl(_p2)
使用了如上的宏定义后,那么原来的uvm_blocking_put_imp包括里面的函数,都应该定义成加后缀“_p1”和后缀"_p2"的形式,所以在从comp2中定义target以及put函数后面都加了对应的后缀。而对于port端口而言,它只需要把itrans发出去就行了,而不必管itrans被发送到哪里,所以不需要对initiator组件内部的port进行宏定义。
不仅如此,为了防止互斥访问,还使用了旗语semaphore。通过key.get()和key_put()的方式来实现互斥访问,解决访问共同资源的数据冲突问题。
另外再提一点细节,因为我们调用的TLM端口时blocking类型的,所以可以有延时,因此我们使用的put函数是一个task类型,如果我们定义的TLM端口时non-blocking类型的,那么就要定义function try_get和can_put等,同样的后面也要加上对应的宏定义后缀。不仅如此,使用旗语semaphore也不能使用key.get,而是使用key.try_get(),如果get不到key要立即返回0。
根据三步骤,定义完端口和方法,最后一步就是在顶层进行连接,给出外部env1代码:
class env1 extends uvm_env;comp1 c1;comp2 c2;`uvm_component_utils(env1)...function void build_phase (uvm_phase phase);super.build_phase(phase);c1 = comp1::type_id::create("c1", this);c2 = comp2::type_id::create("c2", this);endfunction: build_phasefunction void connect_phase(uvm_phase phase);super.connect_phase(phase);c1.bp_port1.connect(c2.bt_imp_p1);c1.bp_port2.connect(c2.bt_imp_p2);endfunction: connect_phase
endclass
如果有多个相同类型的import,那么必须通过宏定义的方式,定义不同名称的import, 从而解决任务名冲突的问题。
对于组件1来讲,不需要对port进行区分,并且对于从comp1中调用的函数还是put函数,而不是put_p1或者put_p2,这就是宏定义方法的好处。对于initiator中的port来说,他不需要管连接的import是哪个,所定义的put函数被宏定义成什么名称,只要我们在import中进行了宏定义后,连接到port时,它会自动调用与put相对应的put_p1或者put_p2函数。
① 用户只需要在例化多个imp端口的组件中实现不同名称的方法,使其对应imp类型名保持一致。
② 而对于port端口一侧的组件,则不需要关心调用的方法名称,因为该方法名并不会发生改变。
③ 所以通过这种方式可以防止通信方法名的冲突,从而解决多向通信的问题。
经过上面的介绍,我们需要在target端定义get()和put()函数,每次定义target都要进行函数的声明,是否有方法不自己实现这些数据传输方法,同时可以使用TLM?以及数据传输过程中,如果出现一端到多端的情况,怎么处理?
几个TLM组件和端口可以帮助我们解决上面的问题:
※ TLM FIFO
※ analysis port
※ analysis TLM FIFO
※ request & response
在一般TLM传输过程中,无论是initiator给target发起一个transaction,还是initiator从target获取一个transaction,transaction最终都会流向consumer中。consumer在没有分析transaction时,我们希望先将对象存储到本地FIFO中供稍后使用。
UVM库中内置了一个uvm_tlm_fifo类,这个类是一个组件,它继承与uvm_component类,而且已经预先内置了多个端口以及实现了多个对应方法供用户使用。
对于uvm_tlm_fifo来讲,存放的数据类型是固定的,因为作为一个fifo里面存放的数据肯定类型是一致的。uvm_tlm_fifo还提供put、get以及peek对应的端口。
uvm_put_imp #(T, this_type) blocking_put_export;
uvm_put_imp #(T, this_type) nonblocking_put_export;
uvm_get_peek_imp #(T, this_type) blocking_get_export;
uvm_get_peek_imp #(T, this_type) nonblocking_get_export;
uvm_get_peek_imp #(T, this_type) get_export;
...
上面端口都是imp端口,虽然后面有get_export,但不是export端口,里面是有定义方法的。虽然看起来端口名是export,但真正的类型是imp。
除了端对端的传输,在一些情况下还有多个组件会对同一个数据进行运算处理,如果这个数据是从同一个源的TLM端口发出到达不同组件,这就要求该种端口可以满足从一端到多端的需求。
如果数据源端发生变化需要通知跟它关联的多个组件时,我们可以利用软件的设计模式之一观察者模式(observer pattern)来实现。
observer pattern的核心在于:① 这是从一个initiator端到多个target端的方式。② analysis port采取的是“push”模式,从initiator端调用多个target端的write()函数来实现数据传输。
按照传输方法和端口方向组合,可以将 analysis port 分为:uvm_analysis_port 、uvm_analysis_export 以及uvm_analysis_imp。target 一侧例化了 uvm_analysis_imp 后还需要实现write()函数。从initiator端调用write()函数时,它是通过轮询的方式将所有连接的target端内置的write()函数进行了调用。也因为是函数,所以无论一个initiator连接了多少target,initiator端调用的write()函数都是可以立即返回的。并且,特殊的是,采用analysis port,即使initiator没有和任何target相连都不会报错,这是和前面端对端的(port和imp成对出现)port有区别的地方。
uvm_tlm_analysis_fifo类继承于uvm_tlm_fifo,它具有单一TLM端口的数据特性,同时该类又有一个uvm_analysis_imp端口analysis_export并且实现了write()函数。
uvm_analysis_imp #(T, uvm_tlm_analysis_fifo #(T)) analysis_export;
整个数据流是initiator把数据push到fifo中,target需要的时候把数据从fifo中get出来,注意target定义的都是port端口,fifo的端口都是imp,这样的好处是uvm_tlm_analysis_fifo既可以实现一端到多端的目的端(已经内置各种数据处理函数)又可以实现数据缓存。并且由于initiator和target的端口类型都是port类型,所以不需要内置函数,只需要调用uvm_tlm_analysis_fifo中已经写好的函数即可,降低了整个系统的维护成本。