TLM(transaction level modeling)是一个基于事务(transaction)的通信方式,通常在高抽象级的语言中被引用作为模块之间的通讯方式。我们通常使用tlm 来作为不同component之间的数据传递和通信。
1,端口数据的流向(发起人和响应人)
在讨论数据流向前,我们先来讨论一个问题,对于两个端口之间的方向,有下面几个点需要注意:
1,发起人:数据的请求方,也就是initiator
2,响应人:数据的向英方,也就是target
initiator:永远有主动权,不论时请求数据还是主动发送数据,永远是发起的那一方;
target:永远时被动的,只能被动的接收数据,或者被通知需要发送数据;
也就是说initiator永远都是主动的一方,就是甲方爸爸,甲方让你发数,乙方就必须给,也就是说target就必须得给initiator发送数据,而甲方爸爸向你输出数据,乙方必须接受,也就是说initiator向target发送数据,target必须接收。
通过上面的说明,我们能够看到,数据方向发生了变化,initiator可以直接向target要数据(get操作),也可以直接向target发数据(put操作),唯一不变的是发起人和响应人。至于initiator是谁,需要根据具体应用去设计。
2,端口的连接
通信端口分为3类:port,export,imp
1,port: 通信请求方initiator的发起端,initiator凭借port端口才可以访问target。
2,export:作为initiator和target中间层次的端口(一个起到中间连接作用的port)。
3,imp: 只能作为target接收请求的响应端,它无法作为中间层次的端口,imp为通信终点。
端口优先级:port > export > imp, 使用connect()建立连接关系时,只有优先级高的才能调用connect()做连接,即port可以连接port、export或者imp;export可以连接export或者imp;imp只能作为数据传送的重点,无法扩展连接。
实际的应用情况,我们下面再讲。
3,TML的通信方式,单向传输和双向传输
按通信传输的方向可以分为单向(unidirection)和双向(bidirection)。需要说明的是,不论是单向传输还是双向传输都有阻塞和非阻塞之分。下面以阻塞为例来说明。
3.1,单向传输
1,由initiator发起request transaction ,get操作(甲方爸爸问你要,你必须给)
2,由initiator发起直接发送transaction,put操作(甲方爸爸要给你,你不能不要)
3.2,双向传输
由initiator发起request transaction,传送至target;而target在消化了request transaction后,也会发起response transaction,继而返回给initiator。数据的流向是双向的,这种传递方式类似于一个握手的过程,先发请求,再发数据,秩序请求是谁发起的,与单项传输类似,只不过这次遇见了一个好说话的甲方,不论干啥事情之前,先询问你,你准备好了,再发数据。
3.3,带缓存的传输方式
uvm_tlm_fifo类是一个新的组件,它继承于uvm_component,且预先内置了多个端口、有多个对应方法供用户使用;功能类似mailbox #(T),该邮箱没有尺寸限制,用来存储数据类型T,而uvm_tlm_fifo的多个端口对应的方法都是利用该邮箱实现了数据读写。由于tlm_fifo是用来作为缓冲使用的,最好的办法是设计成imp对接下游端口,如下图所示:
为啥说最好设计成imp呢,因为对于上有来说,只需要向FIFO中丢数据即可,也就是这需要执行put操作,不论是不是阻塞的,因为你是FIFO所有有空间存储我无条件丢过去的数据,而对于下游get_ap来讲,我可以慢悠悠的从FIFO中拿数据,不用担心数据丢失的问题,因此从tlm_fifo这侧来看,你要我就给你数据,所以设计成imp的方式也是何理的。至于直接问FIFO要的方式,当然必须设计成imp了。
3.4,单向传输的实现 ---put()方法
put方法调用时要注意:
数据无条件的从component A 传递到 component B,不关心B除了接收外的事情。
如果为调用的是bloking相关的方法,必须放在task内,nonblocking 可以放在function中,实现的代码如下:
class A extends uvm_component;
`uvm_component_utils(A)
uvm_blocking_put_port#(my_transaction) A_port;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
extern function void build_phase(uvm_phase phase);
extern virtual task main_phase(uvm_phase phase);
endclass
function void A::build_phase(uvm_phase phase);
super.build_phase(phase);
A_port = new("A_port", this);
endfunction
task A::main_phase(uvm_phase phase);
my_transaction tr;
repeat(10) begin
#10;
tr = new("tr");
assert(tr.randomize());
A_port.put(tr);
end
endtask
class B extends uvm_component;
`uvm_component_utils(B)
uvm_blocking_put_imp#(my_transaction, B) B_imp;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
extern function void build_phase(uvm_phase phase);
extern function void connect_phase(uvm_phase phase);
extern function void put(my_transaction tr);
extern virtual task main_phase(uvm_phase phase);
endclass
function void B::build_phase(uvm_phase phase);
super.build_phase(phase);
B_imp = new("B_imp", this);
endfunction
function void B::connect_phase(uvm_phase phase);
super.connect_phase(phase);
endfunction
function void B::put(my_transaction tr);
`uvm_info("B", "receive a transaction", UVM_LOW)
tr.print();
endfunction
task B::main_phase(uvm_phase phase);
endtask
上述代码分别在component_A和component_B中声明并例化了两个端口实例:
uvm_blocking_put_port #(transaction) A_port;
uvm_blocking_put_imp #(transaction,consumer) B_imp;
最后在env中对两个组件之间的端口进行了连接,这使得producer(A)的main phase中可以通过自身的两个端口间接调用consumer(B)中的方法,如下代码所示:
class my_env extends uvm_env;
A A_inst;
B B_inst;
function new(string name = "my_env", uvm_component parent);
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
A_inst = A::type_id::create("A_inst", this);
B_inst = B::type_id::create("B_inst", this);
endfunction
extern virtual function void connect_phase(uvm_phase phase);
`uvm_component_utils(my_env)
endclass
function void my_env::connect_phase(uvm_phase phase);
super.connect_phase(phase);
A_inst.A_port.connect(B_inst.B_imp);
endfunction
对于get()方法:
与put()方法类似,get方法时通过component_B中将数据从component_A中,get出来,这种做法就是“强制性”的从A中拿走数据,与put的逻辑刚好相反。
总结:
tlm_port 和 imp相连时,数据的传递方法有很多,具体可以参考张强的《UVM实战》这本书,对于是get方法还是put方法,要具体根据应用场景的需要来选择,但是这会影响put/get方法实现的位置,具体情况是,谁是“乙方”在谁的component中实现:
具体来讲就是:
当使用put操作,数据是从cmp_a到 cmp_b,那么需要在comp_b中实现put函数;
当使用get操作,数据是从cmp_a到 cmp_b,那么需要在comp_a中实现get函数;
对于输出传递的方法,最好每种都在源码上看一遍,有助于理解,我这里就不写了,每种方法都有不同的应用场景,这里只是把最常用的put和get方法罗列出来了。
3.5,双向传输的实现 transport()方法
对于双向transport的使用,本人使用的比较少,这块先不介绍了。
3.6,带fifo的数据传递
整个连接过程分为两段,comp_a 的port与 tml_fifo的 expoert 相连(其实是个imp),另一端是comp_b的export/port与tlm_fifo的export相连(其实是个imp)。对于FIFO的深度,可以通过参数设置,这里不做介绍了,具体请看源码。
4,一对多的传输方式
UVM提供的analysis port,它在组件中是以广播(broadcast)的形式向外发送数据的,而不管存在几个imp或者没有imp。分析端口的根据端口类型的不同分为:
uvm_analysis_port、 uvm_analysis_export、 uvm_analysis_imp
并且只有一个操作:write();
由于是广播操作,理论上来讲,应该是非阻塞的,不然会影响其他模块获取数据。
代码实现如下:
class A extends uvm_component;
`uvm_component_utils(A)
uvm_analysis_port#(my_transaction) A_ap;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
extern function void build_phase(uvm_phase phase);
extern virtual task main_phase(uvm_phase phase);
endclass
function void A::build_phase(uvm_phase phase);
super.build_phase(phase);
A_ap = new("A_ap", this);
endfunction
task A::main_phase(uvm_phase phase);
my_transaction tr;
repeat(10) begin
#10;
tr = new("tr");
assert(tr.randomize());
A_ap.write(tr);
end
endtas
class B extends uvm_component;
`uvm_component_utils(B)
uvm_analysis_imp#(my_transaction, B) B_imp;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
extern function void build_phase(uvm_phase phase);
extern function void connect_phase(uvm_phase phase);
extern function void write(my_transaction tr);
extern virtual task main_phase(uvm_phase phase);
endclass
function void B::build_phase(uvm_phase phase);
super.build_phase(phase);
B_imp = new("B_imp", this);
endfunction
function void B::connect_phase(uvm_phase phase);
super.connect_phase(phase);
endfunction
function void B::write(my_transaction tr);
`uvm_info("B", "receive a transaction", UVM_LOW)
tr.print();
endfunction
task B::main_phase(uvm_phase phase);
endtask
class C extends uvm_component;
`uvm_component_utils(C)
uvm_analysis_imp#(my_transaction, C) C_imp;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
extern function void build_phase(uvm_phase phase);
extern function void connect_phase(uvm_phase phase);
extern function void write(my_transaction tr);
extern virtual task main_phase(uvm_phase phase);
endclass
function void C::build_phase(uvm_phase phase);
super.build_phase(phase);
C_imp = new("C_imp", this);
endfunction
function void C::connect_phase(uvm_phase phase);
super.connect_phase(phase);
endfunction
function void C::write(my_transaction tr);
`uvm_info("C", "receive a transaction", UVM_LOW)
tr.print();
endfunction
task C::main_phase(uvm_phase phase);
endtask
class my_env extends uvm_env;
A A_inst;
B B_inst;
C C_inst;
function new(string name = "my_env", uvm_component parent);
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
A_inst = A::type_id::create("A_inst", this);
B_inst = B::type_id::create("B_inst", this);
C_inst = C::type_id::create("C_inst", this);
endfunction
extern virtual function void connect_phase(uvm_phase phase);
`uvm_component_utils(my_env)
endclass
function void my_env::connect_phase(uvm_phase phase);
super.connect_phase(phase);
A_inst.A_ap.connect(B_inst.B_imp);
A_inst.A_ap.connect(C_inst.C_imp);
endfunction
上面这3段代码就实现了comp_a 向 comp_b 和 comp_c同时发送数据,所以write函数必然要在comp_b和comp_c上实现,这块没有多少隐藏在源码中的东西,按照这种规则实现就好,下一节我们在讨论tlm_port源码中的内容。
5,tlm port源码解析
这里讨论的源码是在 macros文件夹内叫做:uvm_tlm_defines.svh的源码,我们在这里举两个例子:
`define uvm_blocking_put_imp_decl(SFX)
// MACRO: `uvm_blocking_put_imp_decl
//
//| `uvm_blocking_put_imp_decl(SFX)
//
// Define the class uvm_blocking_put_impSFX for providing blocking put
// implementations. ~SFX~ is the suffix for the new class type.
`define uvm_blocking_put_imp_decl(SFX) \
class uvm_blocking_put_imp``SFX #(type T=int, type IMP=int) \
extends uvm_port_base #(uvm_tlm_if_base #(T,T)); \
`UVM_IMP_COMMON(`UVM_TLM_BLOCKING_PUT_MASK,`"uvm_blocking_put_imp``SFX`",IMP) \
`UVM_BLOCKING_PUT_IMP_SFX(SFX, m_imp, T, t) \
endclass
class uvm_blocking_put_imp (uvm_tlm_defines.svh)
class uvm_blocking_put_imp #(type T=int, type IMP=int)
extends uvm_port_base #(uvm_tlm_if_base #(T,T));
`UVM_IMP_COMMON(`UVM_TLM_BLOCKING_PUT_MASK,"uvm_blocking_put_imp",IMP)
`UVM_BLOCKING_PUT_IMP (m_imp, T, t)
endclass
我们看这两段代码其实都是在表达同样的内容,那为什么uvm还要再定义一套macros去表达类似的内容?咋一看确实没有这个必要,但是仔细去看的话,第一段代码其实是定义了一个后缀为SFX的uvm_blocking_put_imp,它内部定义的`UVM_BLOCKING_PUT_IMP_SFX这个宏去重新封装了一边put函数,代码如下:
`define UVM_BLOCKING_PUT_IMP_SFX(SFX, imp, TYPE, arg) \
task put( input TYPE arg); imp.put``SFX( arg); endtask
有没有想过,为什么要这么做呢?我可以直接使用第二段代码直接定义一个imp,为什么还要多此一举,通过宏定义的方式创建一个带SFX的imp ,并且从功能上说,两者本质上是一样的?
其实原因也很好说明,就拿put函数来讲,只要是同一个类型的tlm_port,不管是针对哪个imp,所有的put函数都是一样的,因此以scoreboard来讲,如果有多个imp,我怎么区分到底是哪个tlm_port上的put函数呢,这一下子把用户就搞晕了,也罢验证环境搞死了,所以uvm就采用了使用`define uvm_blocking_put_imp_decl(SFX) 这个宏的方式来区分不同的tlm_port,以及不同的put函数,只要SFX后缀不同,那么tlm_port实例化的class不同,调用的put函数也就不同了。这就是这个宏牛逼的地方!
`uvm_analysis_imp_decl(SFX)
// MACRO: `uvm_analysis_imp_decl
//
//| `uvm_analysis_imp_decl(SFX)
//
// Define the class uvm_analysis_impSFX for providing an analysis
// implementation. ~SFX~ is the suffix for the new class type. The analysis
// implemenation is the write function. The `uvm_analysis_imp_decl allows
// for a scoreboard (or other analysis component) to support input from many
// places. For example:
//
//| `uvm_analysis_imp_decl(_ingress)
//| `uvm_analysis_imp_decl(_egress)
//|
`define uvm_analysis_imp_decl(SFX) \
class uvm_analysis_imp``SFX #(type T=int, type IMP=int) \
extends uvm_port_base #(uvm_tlm_if_base #(T,T)); \
`UVM_IMP_COMMON(`UVM_TLM_ANALYSIS_MASK,`"uvm_analysis_imp``SFX`",IMP) \
function void write( input T t); \
m_imp.write``SFX( t); \
endfunction \
\
endclass
有了上面的例子,对于`uvm_analysis_imp_decl(SFX)来说,同样的也是为了区分不同的write函数,而定义的这个uvm_analysis_imp_decl宏,具体的用法我们这块不在展开了,可以学习张强的《UVM实战》来学习这部分的内容,并且后面我会专门再出一篇文章讲讲如何使用`uvm_analysis_imp_decl这个宏,对于tlm源码的解析,这里只是起到一个抛砖引玉的作用,还是那句话,请认真阅读源码,并结合实际情况去理解我们使用到的uvm源码,不要看别人这么写,我们就想都不想就这么用,通过学习源码的代码编写,不仅能看到前人的付出和智慧,也能加强我们的编码能力以及对验证环境的理解。