一个基本的 uvm 验证环境结构如下图所示,包含两个 agent,其中 in_agent 用于驱动 DUT ,同时将驱动数据同时传递给 reference model, out_agent 用于按照协议采集 DUT 的输出数据,并将数据传递给 scoreboard,在 scoreboard 收集到 reference model 的运算结果后,再进行比对验证。
要想实现 monitor 和 reference model 进行通信,大致可以有以下几种方式,第一种,通过全局变量;第二种,通过 定义一个参数类来进行信息传递;第三种:通过前面刚谈到的 uvm_config 来进行传递信息。似乎这些东西都可以实现我们定义的任务,但是有个很重要的点还没有提到,如果我们需要一种 阻塞的场景,即有数据时就取数据,没有数据时就等待。这在验证中是一种比较常见的场景。为了解决这个问题 uvm 提炼出了一种具有阻塞和非阻塞功能的类型,取名为TML。
通俗意义上讲,TLM就是一些带阻塞和非阻塞 port 端口, 为了满足缓存需要,其中也有一些能缓存能力的 port。TLM 端口的使用有点像我们看电视时用插头插插座,插看好后电网的电量就源源不断的通过接口输入进电视机,结合我们的验证环境来说 driver 和 reference model 中包含一对匹配的结构,一个用于发送数据,一个用于接收数据。
1.端口连接
在 UVM 中 port 可以分为三种:
a.port : 优先级最高,可以作为数据链路的端口的起点或者端口中间节点 ;
b.export :优先级最中,可以作为数据链路的或者起点端口中间节点 ;
c.import :优先级最低,只能作为数据链路的重点 。
连接形式可以为
a.port -> import; port -> export -> import; port -> port -> import; port -> export -> export -> import ;
b.export -> import; export -> export -> import;
注意 -> 表示连接关系,A port -> B import,表示为 A.connect(B) ,其中 A 的优先级需要比 B 的优先级高。
2.端口操作
如下图所示,在 UVM 中 port 常见的两种操作就是:
a. put 操作 :通信的发起者 A 把一个 transaction 发送给接受者 B ;
b. get 操作 :通信的发起者 A 向 接受者 B 请求一个 transaction ;
c. transport操作,transport操作相当于一次 put 操作加一次 get 操作 。
uvm 包含了很多类型的 port/export/import,如 uvm_blocking_put_port/export/import、uvm_blocking_peek_port/export/import 和 uvm_blocking_transport_port/export/import 等,但除了这些,uvm 还包含了两种特殊的端口 analysis_port 和 analysis_export, 这两种端口和 put/peek/get 等系列端口类似,但是包含一些重要区别:
- 默认情况下,一个 analysis_port(analysis_export)可以连接多个 analysis_import,即 analysis_port(analysis_export)与 analysis_import 之间是一对多通信;而 put/peek/get 等系列端口只能进行一对一通信。analysis_port(analysis_export)的通信方式像一个广播。
- put/peek/get 等系列端口都具有阻塞和非阻塞之分,但对于 analysis_port(analysis_export)来说,没有阻塞和非阻塞的概念。
- 不同于put/peek/get 等系列端口,对于analysis_port(analysis_export)两种端口仅包含 write 操作,与之相连的 analysis_imp 所在的 component 必须定义一个名为 write 的函数。
3.端口实例1 - uvm_analysis_port
在 uvm 环境中,常用 TML 通信的组件有 monitor、scoreboard和reference model 三者之间的通信,下面以 monitor 和 scoreboard 通信为例,monitor端 uvm_analysis_port的代码为:
my_monitor
class my_monitor extends uvm_monitor; `uvm_component_utils(my_monitor) uvm_analysis_port(my_transaction) A_port; ... endclass ... virtual function void my_monitor::build_phase(uvm_phase phase); super.build_phase(phase); A_port=new("A_port",this); ... endfuction
virtual function void my_monitor::main_phase(uvm_phase phase);
my_transaction tr;
forever begin
assert(tr.randomize());
A_port.write(tr);
end
endfuction
scoreboard 端 uvm_analysis_port 的代码为:
scoreboard
class my_scoreboard extends uvm_scoreboard; `uvm_component_utils(my_scoreboard) uvm_analysis_import(my_transaction,my_scoreboard) A_import; ... endclass ... virtual function void my_scoreboard::build_phase(uvm_phase phase); super.build_phase(phase); A_import=new("A_import",this); ... endfuction
virtual function void my_scoreboard::write(my_transaction tr);
deal_with(tr);
endfuction
顶层 env 连接代码:
my_env
class my_env extends uvm_env;
...
endclass
...
virtual function void my_scoreboard::connect_phase(uvm_phase phase);
super.connect_phase(phase);
agt.monitor.A_port.connect(scb.A_import)
...
endfuction
实例中,在 monitor 中定义了 analysis_port;socreboard 中定义了 analysis_import;在 env 的 connect_phase 中对 analysis_port 和 analysis_import 进行连接。数据在 monitor 产生后,数据的流通过程如下图所示, 数据首先通过 analysis_port 的系统函数 write 调用 analysis_import 的系统函数 write,再由 analysis_import 的系统函数 write 触发 my_scoreboard 中用户自定义的 write 函数,这也为什么在定义 scoreboard 的 impport时,需要通过 uvm_analysis_port(my_transaction,my_scoreboard) A_import; 将 my_scoreboard 和 A_import 绑定的原因,就是为了 能顺利触发 自定义的 write 函数。而且这种触发是没有延迟的触发机制,可以通过这种 write 方式来满足一些特定时序要求。
在了解了上述内容后,在来看看 uvm_analysis_port 的最重要的特性,一对多通信是怎么实现的,现在考虑一种场景,scoreboard 需要收集 monitor 和 reference model 两个组件传输过来的数据,但是我们定义write 函数的时候只有一个名字,那么 monitor 和 reference model 中的 analysis_import 的系统函数 write 怎么触发 my_scoreboard 中用户自定义的 write 函数呢?
为了解决上述问题 uvm 定了一宏 uvm_analysis_imp_decl 来解决上述问题,现在我们来看看这个宏怎么用,更新后的 my_scoreboard 代码如下:
scoreboard
`uvm_analysis_imp_decl(_monitor) `uvm_analysis_imp_decl(_model) class my_scoreboard extends uvm_scoreboard; my_transaction inpect_queue[$] my_transaction expect_queue[$] uvm_analysis_imp_monitor(my_transaction,my_scoreboard) mon_import; uvm_analysis_imp_model(my_transaction,my_scoreboard) model_import; ... extern function void write_monitor(my_transaction tr); extern function void write_model(my_transaction tr); ... endclass ...
virtual function void my_scoreboard::write_monitor(my_transaction tr);
inpect_queue.push_back(tr);
...
endfuction
virtual function void my_scoreboard::write_model(my_transaction tr);
expect_queue.push_back(tr);
...
endfuction
上述代码通过宏 uvm_analysis_imp_decl 声明了两个后缀 _monitor 和 _model 。UVM 会根据这两个后缀定义两个新的 import 类:uvm_analysis_imp_monitor 和 uvm_analysis_imp_model ,并在 my_scoreboard 中分别实例化这两个类:monitor_imp 和 model_imp。当与 monitor_imp 相连接的 analysis_port 执行 write 函数时,会自动调用 write_monitor 函数,而与 model_imp 相连接的 analysis_port 执行 write 函数时,会自动调用 write_model 函数。所以,只要完成后缀的声明,并在write后面添加上相应的后缀就可以正常工作了。
3.端口实例2 - uvm_tlm_analysis_fifo
另外一种带缓存能力的 analysis port ,叫做 uvm_tlm_analysis_fifo,其本质为 一块 FIFO 加一组端口,如下图所示,共有 3 类,12 import 为一类,包含 一族 *_put_export 、一族 *_get_export 和一族 *_peek_export ,似乎上述都是 export,其实这只是取名为 export 而已,它们本质上还是 import 。
*_put_export 族:调用时,导致 uvm_tlm_analysis_fifo 中的缓存单元(**本质为 mailbox ),元素个数加 1;
*_get_export 族:调用时,导致 uvm_tlm_analysis_fifo 中的缓存单元(本质为 mailbox ),元素个数减 1;
*_peek_export 族:调用时,导致 uvm_tlm_analysis_fifo 中的缓存单元(本质为 mailbox **),元素个数不变,同时把 transaction 复制一份发送出去。
现在来说说图中的另外两个端口 - put_ap 和 get_ap, 这两组的 端口的源码如下
scoreboard
virtual task void put(int T t);
m.put(t);
put_ap.write(t);
endtask
virtual task void get(int T t);
m_pending_blocked_gets++;
m.get( t );
m_pending_blocked_gets--;
get_ap.write( t );
endtask
当 uvm_tlm_analysis_fifo 上 的 _put_export 族端口被连接到 一个 put 端口时,且调用 put 函数时,put 函数会把传递过来的 transactoin 放在 uvm_tlm_analysis_fifo 的缓存单元 m (mailbox)中,同时把这个 transaction 通过 put_ap 端口的 write 函数发送给与之相连的端口;当 uvm_tlm_analysis_fifo 上 的_get_export 族 被连接到一个 get 端口,且调用 get 函数时,过程类似。我们常用的是 analysis_export 端口。带 FIFO 缓存的端口类型除了 uvm_tlm_analysis_fifo,还有一种是 uvm_tlm_fifo, 区别在于前者有一个 analysis_export 端口和一个 write 函数。
现在以 monitor 与 reference model 的通信为例,其 monitor 端代码如下
monitor
class my_monitor extends uvm_monitor; `uvm_component_utils(my_monitor) uvm_analysis_port(my_transaction) A_port; ... endclass ... virtual function void my_monitor::build_phase(uvm_phase phase); super.build_phase(phase); A_port=new("A_port",this); ... endfuction
virtual function void my_monitor::main_phase(uvm_phase phase);
my_transaction tr;
forever begin
assert(tr.randomize());
A_port.write(tr);
end
endfuction
其 reference model 端代码如下
reference model
class my_reference extends uvm_component; `uvm_component_utils(my_reference ) uvm_tlm_analysis_fifo(my_transaction) A_fifo_port; ... endclass ... virtual function void my_reference ::build_phase(uvm_phase phase); super.build_phase(phase); A_fifo_port=new("A_fifo_port",this); ... endfuction
virtual function void my_reference ::main_phase(uvm_phase phase);
my_transaction tr = null;
forever begin
A_fifo_port.get(tr);
...
end
endfuction
顶层 env 连接代码:
my_env
class my_env extends uvm_env;
...
endclass
...
virtual function void my_scoreboard::connect_phase(uvm_phase phase);
super.connect_phase(phase);
agt.monitor.A_port.connect(ref.A_fifo_port.analysis_export)
...
endfuction
其中 monitor 通过 uvm_analysis_port A_port 调用 write 函数将 my_transaction 存入 uvm_tlm_analysis_fifo 的缓存单元中,在 reference model 中通过 get 函数将 tranansction 取出进行后续处理。
3.端口特例
这组特例 port 就是 uvm_seq_item_pull_port 和 uvm_seq_item_pull_imp ,通过名字可以知道,这组端口通常是在 sequencer 和 driver 中使用的,用于它们之间的数据传递。uvm library 中 原型代码如下所示:
uvm_seq_item_pull_port
class uvm_driver #(type REQ=uvm_sequence_item,type RSP=REQ) extends uvm_component;
uvm_seq_item_pull_port #(REQ,RSP) seq_item_port;
...
endclass
class uvm_sequencer #(type REQ=uvm_sequence_item,type RSP=REQ) extends uvm_sequencer_para_base #(REQ,RSP);
uvm_seq_item_pull_imp #(REQ,RSP) seq_item_export;
...
endclass
在使用时,因为 sequencer 中的操作被隐藏,所以使用时很简单,实例如所示:
uvm_seq_item_pull_port 实例
class my_sequence extends uvm_sequence #(my_transaction);
`uvm_object_utils(my_sequence);
virtual task body(uvm_phase phase);
`uvm_do(req);
get_response(rsp);
endtask
endclass
class my_sequencer extends uvm_sequencer #(my_transaction);
`uvm_component_utils(my_sequencer);
function new(string name, uvm_component phase);
super.new(name,parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
endfunction
endclass
class my_driver extends uvm_driver #(my_transaction);
`uvm_component_utils(my_driver );
...
virtual task run_phase(uvm_phase phase);
my_transaction tr;
while(1)begin
seq_item_port.get_next_item(req);
send(tr);
se_item_port.item_done();
seq_item_port.put_response(rsp);
end
endtask
endclass