在芯片开发流程中,有两个地方对项目的助推起到了关键作用:系统原型、芯片验证
TLM是一种基于事务(transaction)的通信方式,通常在高抽象级语言例如SystemC或者SV/UVM中作为模块之间的通讯方式。
TLM成功地将模块内的计算和模块之间的通信从时间跨度方面剥离开了。
在抽象语言建模体系中,各个模块通过一系列并行的进程实现,并通过通信和计算来模拟出正确的行为。
如果要提高系统模型的仿真性能,需要考虑两个方面
TLM是一种基于事务(transaction)的通信方式,通常在高抽象级语言例如SystemC或者SV/UVM中作为模块之间的通讯方式。
TLM通信需要两个通信的对象,这两个对象分别称为initiator和target。区分它们的方法在于,谁先发起通信请求,谁就属于initiator,而谁作为发起通信的响应方,谁就属于target。
在初学过程中读者们还应该注意,通信发起方并不代表了transaction的流向起点,即不一定数据是从initiator流向target,也可能是从target流向了initiator。
因此按照transaction的流向,我们又可以将两个对象分为producer和consumer。区分它们的方法是,数据从哪里产生,它就属于producer,而数据流向了哪里,它就属于consumer。
initiator与target的关系同producer与consumer的关系不是固定的。有了两个参与通信的对象之后,用户需要将TLM通信方法在target一端中实现,以便于initiator将来作为发起方可以调用target的通信方法,实现数据传输。
在target实现了必要的通信方法之后,最后一步我们需要将两个对象进行连接,这需要在两个对象中创建TLM端口,继而在更高层次中将这两个对象进行连接。
比如monitor和scoreboard,monitor是发起端;driver和generator,driver是发起端,get到,消化后,put rsp回去。
我们可以将TLM通信步骤分为:
从数据流向来看,传输方向可以分为单向(unidirection)和双向(bidirection) 。
端口的按照类型可以划分为三种:
如果将传输方向和端口类型加以组合,可帮助理解TLM端口的继承树。TLM端口一共可以分为六类:(既不是object也不是component,无法使用工厂create)
uvm_UNDIR_port #(trans_t)
uvm_UNDIR_export#(trans_t)
uvm_UNDIR_imp # (trans_t, imp_parent_t) 单向
uvm_BIDIR_port # (req_trans_t, rsp_trans_t) 双向
uvm_BIDIR_export # (req_trans_t, rsp_trans_t)
uvm_BIDIR_imp # (req_trans_t, rsp_trans_t, imp_parent_t)
就单向端口而言,声明port和export作为request发起方,需要指定transaction类型参数,而声明imp作为request接收方,不但需要指定transaction类型,也需要指定它所在的component类型。
就声明双向端口而言,指定参数需要考虑双向传输的因素,将传输类型transaction拆分为request transaction类型和response transaction类型。
从对应连接关系得出TLM端口连接的—般做法:
从示例中可以得出关于建立TLM通信的常规步骤:
单向通信(unidirectional communication)指的是从initiator到target之间的数据流向是单一方向的,或者说initiator和target只能扮演producer和consumer中的一个角色。
在UVM中,单—数据流向的TLM端口有很多类型:(PORT分别表示为port,export,import)
按照UVM端口名的命名规则,它们指出了通信的两个要素:
阻塞传输方式将blocking前缀作为函数名的一部分,而非阻塞方式则名为nonblocking。阻塞端口的方法类型为task,这保证了可以实现事件等待和延时;非阻塞端口的方式类型为function,这确保了方法调用可以立即返回。
我们从方法名也可以发现,例如uvm_blocking _put_PORT提供的方法task put()会在数据传送完后返回,uvm_nonblocking_put_PORT对应的两个函数try_put()和can_put()是立刻返回的。
uvm_put_PORT则分别提供了blocking和nonblocking的方法,这为通讯方式提供了更多选择。blocking阻塞传输的方法包含:
与上述三种任务对应的nonblocking非阻塞方法分别是:. try_put()
这六个非阻塞函数与对应阻塞任务的区别在于,它们必须立即返回,如果try_xxx函数可以发送或者获取数据,那么函数应该返回1,如果执行失败则应返回0。或者通过can_xxx函数先试探target是否可以接收数据,如果可以,再通过try_xxx函数发送,提高数据发送的成功率。
(这些方法不是port的,port不实现任何方法,方法是在target里的)
component1:
uvm_blocking_put_port #(itrans) bp_port; //注意参数化的类,参数为定义的transaction类型
uvm_nonblockINg_get_port #(otrans) ngb_port;
this.bp_port.put(itr); //组件发送tr时,是不知道发送到哪里的(哪个组件),实现了组件之间更好的
//隔离。put方法取决于接下来连接的组件是否实现put方法。
this.nbg_port.try_get(otr);
component2:
class comp2 extends uvm_component;
umv_blocking_put_imp #(itrans,comp2) bp_imp; //多传递一个类型,连接可以找到import端// //口,要想通过import找到组件里的方法,就将组件的句柄一同作为参数传入。 第一步通过connect找到import;第二步通过参数关系找到组件的put方法
uvm_nonblocking_get_imp #(otrans,comp2) ngb_imp;
itrans itr_q[$];
……
task put(itrans t);
itr_q.push_back(t); //target实际作为buffer,存储数据。从而使用put、get等方法。
endtask
……
endclass
首先compl例化了两个port端口:
comp2作为target则相应例化了两个对应的imp端口:.
env1环境将comp1与comp2连接之前,需要在comp2中实现两个端口对应的方法:
与单向通信相同的是,双向通信(bidirectional communication)的两端也分为initiator和target,但是数据流向在端对端之间是双向的。
双向通信中的两端同时扮演着producer和consumer的角色,而initiator作为request发起方,在发起request之后,还会等待response返回。
UVM双向端口分为以下类型:(transport既有req又有rsp。调用mater或slave时则只有一个,需要调用两次才行)
双向端口按照通信握手方式可以分为:
transport端口通过transport()方法,可以在同一方法调用过程中完成REQ和RSP的发出和返回。
master和slave的通信方式必须分别通过put、get和peek的调用,使用两个方法才可以完成一次握手通信。
master端口的slave端口的区别在于,当initiator作为master时,它会发起REQ送至target端,而后再从target端获取RSP;当initiator使用slave端口时,它会先从target端获取REQ,而后将RSP送至target端。
对于master端口或者slave端口的实现方式,类似于之前介绍的单向通信方式,只是imp端口所在的组件需要实现的方法更多了。
comp1:
uvm_blocking_transport_port #(itrans,otrans) bt_port; //两个tr类型
this.bt_port.transport(itr,otr); //两个tr句柄
comp2:
uvm_blocking_transport_imp #(itrans,otrans) bt_im;
task transport(itrans req,output rsp);
……
c1.bt_port.connect(c2.bt_imp)
多向通信(multi-directional communication)这个概念听起来容易让读者产生歧义,因为这种方式服务的仍然是两个组件之间的通信,而不是多个组件之间的通信,毕竟多个组件的通信仍然可以由基础的两个组件的通信方式来构建。
多向通信指的是,如果initiator与target之间的相同TLM端口数目超过一个时的处理解决办法。
compl有两个uvm_blocking put_port,而comp2有两个uvm_blocking_put_imp端口,我们对于端口例化可以给不同名字,连接也可以通过不同名字来索引,但问题在于comp2中需要实现两个task put(itrans t),又因为不同端口之间要求在imp端口一侧实现专属方法,这就造成了方法命名冲突,即无法在comp2中定义两个同名的put任务。
UVM通过端口宏声明方式来解决这一问题,它解决问题的核心在于让不同端口对应不同名的任务,这样便不会造成方法名的冲突。UVM为解决多向通信问题的宏按照端口名的命名方式分为:
(decl:表示声明;SFX表示后缀名称)
不同名import,定义不同方法
`uvm_blocking_put_imp_decl(_p1)
`uvm_blocking_put_imp_decl(_p2)
comp1:
uvm_blocking_put_port #(itrans) bp_port1;
uvm_blocking_put_port #(itrans) bp_port2;
this.bp_port1.put(itr1); //调用方法都是put,取决于connect。若调用put1,编译错误。
this.bp_port2.put(itr2); //注意此时不知道组件连接到同一个组件还是两个组件
comp2
uvm_blocking_put_imp_p1 #(itrans,comp2) bt_imp_p1; //参数化的类也加了后缀,帮助找到p1任务
uvm_blocking_put_imp_p2 #(itrans,comp2) bt_imp_p2;
`uvm_component_utils(comp2)
itrans itr_q[$];
semaphore key;
task put_p1(itrans t); //方法也加后缀,同端口一致
task put_p2(itrans t);
key.get();
itr_q.push_back(t);
key.put; //资源共享,用semaphore进行互斥保护。比如put_p1的时候,另外一个拿不到
c1.bp_port1.connect(c2.bt_imp_p1);
c1.np_port2.connect(c2.bt_imp_p2);
当一个组件的两个端口通过相向方法(譬如task put())向另外一个组件传输数据时,就需要使用上述的宏,分别声明两个不同的imp类型,完整的实现步骤包括:
TLM通信的实现方式,这些通信有一个共同的地方即都是端对端的,同时在target—端需要实现传输方法,例如put()或者get()。
这种方式在实际使用也可能会给用户带来一些烦恼,如何可以不自己实现这些传输方法,同时可以享受到TLM的好处?(是否先可以先例化buffer,再实现方法,形成固定形式,不需要自己实现方法)
对于monitor、coverage collector等组件在传输数据时,会存在—端到多端的传输,如何解决这一问题?
几个TLM组件和端口可以帮助用户免除这些烦恼:
在一般TLM传输过程中,无论是initiator给target发起一个transaction,还是initiator从target获取一个transaction,transaction最终都会流向consumer中(initiator和target都可以是consumer) 。consumer在没有分析transaction时,我们希望将该对象先存储到本地FIFO中供稍后使用。用户需要分别在两个组件中例化端口,同时在target中实现相应的传输方法。多数情况下,需要实现的传输方法都是相似的,方法的主要内容即是为了实现一个数据缓存功能。
TLM FIFO uvm_tlm_fifo类是一个新组件,它继承于uvm_component类,而且已经预先内置了多个端口以及实现了多个对应方法供用户使用。
uvm_tlm_fifo的功能类似于mailbox,不同的地方在于uvm_tlm_fifo提供了各种端口供用户使用。我们推荐在initiator端例化put_port或者get_peek_port,来匹配uvm_tlm_fifo的端口类型。 当然,如果用户例化了其它类型的端口,uvm_tlm_fifo还提供put、get以及peek对应的端口:
buffer只有一个,存放的数据类型是固定的。端口都是import类型,即使名称为export。
除了端对端的传输,在一些情况下还有多个组件会对同一个数据进行运算处理。
如果这个数据是从同一个源的TLM端口发出到达不同组件,这就要求该种端口可以满足从—端到多端的需求。
如果数据源端发生变化需要通知跟它关联的多个组件时,我们可以利用软件的设计模式之一观察者模式(observer pattern)(广播模式)来实现。(端到端必须有两个,这种广播涉及的听众可能没有,也可能多个)observer pattern的核心在于:
initiator.ap.connect(target1.aimp);
initiator.ap.connect(target2.aimp);
initiator.ap.connect(target3.aimp);
一个典型的analysis port类型端口的连接方式,类似于其它TLM端口的是,按照传输方法和端口方向组合可以将analysis port分为uvm_analysis_port、uvm_analysis_export以及
uvm_analysis_imp。
target一侧例化了uvm_analysis_imp后还需要实现write()函数。
在顶层可以将initiator端的uvm_analysis_port同多个target端的uvm_analysis_imp进行连接。
在initiator端调用write()函数时,实际上它是通过循环的方式将所有连接的target端内置的write()函数进行了调用。
由于函数立即返回的特点,无论连接多少个target端,initiator端调用write()函数总是可以立即返回的。不同于之前单一端口函数调用的是,即使没有target与之相连,调用write()函数时也不会发生错误。(循环时发现没有target相连就不调用write)
由于analysis端口提出实现了一端到多端的TLM数据传输,而一个新的数据缓存组件类uvm_tlm_analysis_fifo为用户们提供了可以搭配uvm_analysis_port端口uvm_analysis_imp端口和write()函数。
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到多个target的连接方式,用户如果想轻松实现一端到多端的数据传输,可以插入多个uvm_tlm_analysis_fifo,(相比maibox,支持各种形式端口)我们这里给出连接方式:
initiator.ap.connect(tlm_analysis_fifo1.analysis_export);
target1.get_port.connect(tlm_analysis_fifo1.get_export) (target是针对initiator而言)
initiator.ap.connect(t1m_analysis_fifo2.analysis_export) ;
target2.get _port.connect(tlm_analysis_fifo2.get_export); //注意这里是从target开始
initiator.ap.connect(tlm_analysis_fifo3.analysis_export);
target3.get_port.connect(tlm_analysis_fifo3.get_export) ;
initiator和target不用实现方法,可以直接调用put和get,这些方法在analysis_fifo里都有。(这也是为什么taget调用get方法,连接时在前面吗?import变为port)
双向通信端口transport,即通过在target端实现transport()方法可以在一次传输中既发送request又可以接收response。
UVM提供了两种简便的通信管道,它们作为数据缓存区域,既有TLM端口从外侧接收request和response,同时也有TLM端口供外侧获取request和response。这两种TLM通信管道分别是:
对于uvm_tlm_req_rsp_channel而言,它提供的端口首先是单一方向的,为了让端口列表清爽一些,我们只列出该类例化的端口:
有了这么多丰富的端口,用户可以在使用成对的端口进行数据的存储和访问。需要注意的是,uvm_tlm_req_rsp_channel内部例化了两个mailbox分别用来存储request和response:
例如initiator端可以连接channel的put_request_export,target连接channel的get_peek_request_export,同时target连接channel的put_response_export,initiator连接channel的get_peek_response_export端口。
通过这种对应的方式,使得initiator与target可以利用uvm_tlm_req_rsp_channel进行request与response的数据交换。
也可以利用另外—种连接方式:(相比前面借助两个端口,发送两次(put、get)。这里只要一个端口,还是调用两次)
通过所述的这些方式,我们可以实现initiator与target之间自由的request和response传输,而这两种连接方式仍然需要分别调用两次方法才可以完成request和response的传输。
在uvm_tlm_req_rsp_channel的基础上,UVM又添加了具备transport端口的管道组件uvm_tlm_transport_channel类。它继承于uvm_tlm_req_rsp_channel,并且新例化了transport端口:
uvm_transport_imp #(REQ,RSP,this_type) transport_export;
左侧变为transport_port,右侧target依然是slave_port
新添加的这个TLM FIFO组件类型是针对于一些无法流水化处理的request和response传输,例如initiator—端要求每次发送完request,必须等到response接收到以后才可以发送下一个request,这时transport()方法就可以满足这一需求。(前面只要单侧get、put就结束?)
如果将上面的传输方式进行修改,需要变化的是initiator端到req_rsp_channel的连接,应该修改为︰
initiator.transport_port.connect(transport_channel.transport_export)
至于transport_channel和target之间的连接,则可以仍然保留之前的单向传输连接方式。