向UVM-TLM通信发起决战

前言

开头想先说点体会,最早学习uvm时,TLM通信这一章,自己最开始并没有很重视。到了亲自搭验证环境时才发现TLM至关重要,没有TLM,产生的事务无法在各个验证组件之间流通。这就好比人空有一副骨架,但没有血液在流通。不掌握TLM机制,会导致很多代码看不明白。

个人总结的TLM的难点如下:

  • 端口类非常的多,且大量用到参数化的类这种写法
  • 组件之间的连接该用那个端口类比较模糊,没有一个清晰的架构图。

基于上述自己的短板,所以有了这篇文章,希望能对这部分有个全面的梳理和了解。

梳理的结果有三部分:

  1. 理论概念部分:port,export,imp的关系
  2. 浅析源代码部分:TLM端口类的类库地图,并根据源代码浅析他们之间的继承关系
  3. 实战部分:uvm验证环境端口连接关系图,这对于实际搭建uvm验证环境端口类的选取非常有用

一、理论概念部分

1.1 uvm1.2中,TLM1.0和TLM2.0的关系?

去看uvm_pkg.sv中的内容,会发现里面同时包括了tlm1和tlm2

去看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端口保持一致


端口阻塞方法(阻塞当前进程):

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原理

以put方法为例,TLM传输可以分为几个步骤:

  • 在connect phase中通过connect函数将端口连接
  • 主控方的端口发起操作,调用put任务
  • 在port端口的put任务中,调用了与其连接的export端口的put任务
  • 在export端口的put任务中,调用了与其连接的imp端口的put任务
  • 在imp端口的put任务中,调用了imp所在的component的put任务

上述的原理是基于概念图的抽象解释,于是引发了如下疑问:

  1. 这些方法在原型类中都是空的,最终的实现都是在imp所在的component即slaveB中完成的。那么slaveB中的put()任务究竟该怎么写?
  2. 源头的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中用到的所有类都集中在了这些文件中了,从上往下看可以分为四个部分

2.2 TLM1中的端口类

uvm_tlm_if_base#(T1,T2)  ★★★

这个类是tlm1中所有端口类的基类。注意,该类是最基础的基类,它没有从任何类扩展而来,注意端口类并不是从uvm_object或者uvm_component扩展而来的

这个类的主要作用是将端口的最基础的方法做了封装。该类中包含的方法有:

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)  ★★★

uvm_sqr_if_base也是一个基类,它不从任何类扩展而来,和uvm_tlm_if_base的地位是一样的。而且这个类是专门用于sequence,sequencer,driver之间的通信的。 该类的作用也是封装了一些方法:

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

这个类继承自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展开的。

这里的函数调用很有意思,这里定义的函数是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的端口类都是从此类扩展而来。

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事务类

port类型的端口类非常多,统计共有23个,看着眼花缭乱,其实有规律可循,可以分成如下几个部分。

图片来自于博客: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方法相关的类

这三个类之间的区别在于,封装的方法类型不一样。上面讲到了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)的端口类。

在文件中看其他的类,和上面所说的逻辑是完全一样的,这里不再赘述,通过一张图来进行整理

uvm_*_export(T)

在实际使用时,传入的参数T为该端口要处理的transaction事务类

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)相连

uvm_*_imp(T,IMP)

注意,imp类型的端口类传入的参数有两个,T是该端口要处理的transaction事务类,IMP是该imp端口所在的component类

imp类型的端口类从uvm_port_base继承而来,也有23个子类,和上述两类端口一一对应。

不同端口类型的连接规则

port可以向port连接,可以向export发起连接,可以向imp发起连接

export可以向export发起连接,可以向imp发起连接

imp不能向任何端口发起连接

层级的要求是个问题,需要探讨


2.3 TLM1中的FIFO类

注意FIFO类和端口类是完全不同的,FIFO类属于组件,是component。而端口类必须是在component类中例化的。

重视FIFO类在使用中的重要性

在实际验证环境中会遇到需要将进程1中的componentA连接到进程2中的componentB的情况,因为TLM组件需要在自己的进程中工作(解耦合)

之前的端口直连的过程是,只存在一个进程,port端口调用方法,会引发一系列的方法调用。

有些情况下,两个平级的组件的run_phase是并行执行的。它们在各自的线程中调用自己的方法。这时就需要有一个缓冲区。即componentA发出的事务先放在一个FIFO里存起来,componentB在执行到自己的线程时,再从FIFO里去取。


uvm_tlm_fifo_base#(T)

uvm_tlm_fifo_base#(T)继承于uvm_component,是一个virtual class,只能被继承,不能被实例化。fifo类可以看做是验证环境中的一个组件。在fifo类中定义了非常多的端口和方法。

包含的端口

在uvm_tlm_fifo_base#(T)中,只定义了三种类型的端口

  • uvm_put_imp#(T,IMP)
  • uvm_get_peek_imp#(T,IMP)
  • uvm_analysis_port#(T)

注意,在uvm_tlm_fifo_base#(T)中没有export类型端口。

虽然图上标注的都是export端口名,但是从源代码看出,这些都是imp类的句柄名。虽然起名叫export,但实际是imp类型。为什么要这样有意而为呢?

答案:

虽然有这么多的句柄,但是在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)

由于uvm_tlm_fifo_base#(T)是fifo类的基类,并且是virtual class。所以只定义了一些基础的公共变量和方法。不能直接拿来使用。

我们在实际搭建验证环境时,用到的fifo类都从该基类扩展而来。有两种,分别是uvm_tlm_fifo#(T)以及uvm_tlm_analysis_fifo#(T)。

图片来自于博客: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


一个关键问题:uvm_tlm_fifo是如何实现fifo的特性的

uvm_tlm_fifo是一个类,其具有fifo先入先出特性的核心是内部例化了一个mailbox类。mailbox本身就具有fifo的性质。

mailbox机制可以参考:


使用uvm_tlm_fifo的工作原理

  • 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,并对这些方法进行了重写。

这些方法都是对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类

暂未使用过,之后遇到再补充。


2.5 sequence机制中用到的端口类

sequence机制是事务产生和发送的核心机制。涉及到sequencer和driver之间的事务传输。用到的端口类也比较特殊。只有一种前缀对应的port,export,imp端口。这里把上述类库地图的最后一部分单独截图出来。

这三种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)。

sequence机制中的TLM传输
  1. connect:在agent中,连接driver和sequencer。my_drv.seq_item_port.connect(my_seqr.seq_item_export)
  2. 在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,如果没有则处于等待状态。
  3. seq调用seq.start(my_seqr),将产生的事务发送给my_seqr。此时事务通过my_seqr传递给my_drv。
  4. my_drv处理完事务之后,调用seq_item_port.item_done(),表示结束,最终调用的是my_seqr.item_done()

2.6 TLM机制的源代码解析:

在上面的传输过程中,多次描述到这样一个过程:

当调用port.put()方法时,会引发调用与之相连的imp.put()方法,进而会调用imp所在的component类的put()方法。

那么由此引发两个疑问:

  1. port.put()是如何引发后续方法的调用的?
  2. 最终的put()方法是在imp所在的component类中实现的,具体要写成什么,才算实现了put()?

在"tlm1/uvm_ports.svh"文件中定义了各种各样的port类,定义写法都是一样的,这里找一个拆开解析

首先,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类的一个句柄。

这里的this.m_if指向的是与uvm_blocking_put_port相连的uvm_blocking_put_imp的实例。

这里有了一个关键性疑问,在put_port中的m_if,是在什么时候指向的put_imp实例?

很自然的会想到connect函数,因为只有调用port.connect(imp)才建立两个端口之间的联系。

走进connec()函数

代码太长,这里就不贴了,总结connect函数主要干的事情。

connect()函数传入的参数为一个uvm_port_base类型的句柄,provider

  1. 检查provider是否为空,是否是自己本身,检查provider的m_if_mask和自己的m_if_mask关系
  2. 检查端口连接关系,不能是export连port,自己本身不能是imp等等
  3. 检查relationship,更复杂的层级关系要满足
  4. 以上都满足之后,说明端口连接是合法的,就把provider放入当前port端口类中的关联数组m_provided_by中。同时provider也把当前port类放入自己的关联数组m_provided_to中

会发现,connect()函数并没有对m_if的任何操作。

我想说的是接下来这部分,将是我看uvm以来最冲击我的一段代码。这段比较细节,即使不清楚也无所谓。

uvm_port_base中,唯一出现m_if赋值的地方是:

寻找set_if(()被何时被调用

在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()是何时被调用的呢?

不起眼的m_comp是关键。在uvm_port_base中,例化了一个uvm_component类型的类。

m_comp的作用一个是继承了来自uvm_component的一些report方法,便于打印一些信息。另外是m_comp在实例化之后,会根据phase机制,自动执行一些函数。

在m_comp中,有一个resolve_bindings函数,会调用m_port.resolve_bindings()函数。

而m_comp中的resolve_bindings函数是重写的uvm_component的resolve_bindings()函数。

而uvm_component的resolve_bindings()函数是在end_of_elaboration phase开始之前自动执行的。

至此,整个的顺序就已经清晰了。


回到最开始uvm_blocking_put_port#(T)中put()任务的描述:

在put(T t)中,调用了this.m_if.put(t)。注意此时port端口处理的事务t,通过task的参数传递到了this.m_if.put(t)中。

再进到uvm_blocking_put_imp#(T)中,看其内部定义的put任务

可以注意到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()方向相反。

2.7 使用宏 `uvm_*_imp_decl

注意,该宏只针对imp类的端口

该宏出现解决的问题基于如下场景

不同于之前的一对一连接,现在有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 endclass  class 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;     componentA  compA;     componentB  compB;     componentC  compC;          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)为例,展开解析:

这个宏是带参数的宏,这个参数就是人为定义的后缀。使用该宏时,就会产生一个新class的声明。如`uvm_blocking_put_imp(_A)等价于声明了一个class,类名为 uvm_blocking_put_imp_A。

在类内部,又调用了两个宏,展开来看:

第一个宏就是在常规的imp类中用到的宏,声明了new函数,以及指定了type_name。没有什么特别之处。

第二个宏,和常规imp类中的宏不同,这里重新定义了put()任务中的内容:

至此,使用这个宏之后的代码如下:

`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 endclass  class 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;     componentA  compA;     componentB  compB;     componentC  compC;          function void connect_phase(uvm_phase phase);         compA.portA.connect(compB.impAB);         compC.portC.connect(compB.impCB);    endfunction endclass

至此,上述情景的传输过程如下:

注意:在新定义的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,如有侵权,请联系删除。

posted @ 2025-09-24 17:15  LeslieQ  阅读(15)  评论(0)    收藏  举报