目录
TLM1.0
验证平台内部的通信
如果要在两个 uvm_component 之间通信,例如一个 monitor 向一个 scoreboard 传递一个数据,有哪些方法呢?
个 monitor 要把数据传给一个 scoreboard,两者需要建立一条通信通路
最简单的方法就是使用全局变量,在 monitor 里对此全局变量进行赋值,在 scoreboard 里监测此全局变量值的改变。这种方法简单、直接,不过要避免使用全局变量,滥用全局变量只会造成灾难性的后果
稍微复杂一点的方法,在 scoreboard 中有一个变量,这个变量设置为外部可以直接访问的,即 public 类型的,在 monitor 中对此变量赋值(如图4-2所示)。要完成这个任务,那么要在 monitor 中有一个指向 scoreboard 的指针,否则虽然 scoreboard 把这个变量设置为非 local 类型的,但是 monitor 依然无法改变。这种方法的问题就在于,整个 scoreboard 里面的所有非 local 类型的变量都对 monitor 是可见的,而假如 monitor 的开发人员不小心改变了 scoreboard 中的一些变量,那么后果将可能会是致命的
scoreboard 把一个变量设为 public(外部可访问),monitor 持有指向 scoreboard 的指针并直接对该变量赋值来实现通信
由 config 机制的特性可以想出第三种方法来,即从 uvm_object 派生出一个参数类 config_object,在此类中有 monitor 要传给 scoreboard 的变量。在 base_test 中,实例化这个 config_object,并将其指针通过 config_db#(config_object)::set 传递给 scoreboard 和 monitor。当 monitor 要和 scoreboard 通信时,只要把此 config_object 中相应变量的值改变即可。scoreboard 中则监测变量值的改变,监测到之后做相应动作。这种方法比上面的两种方法都要好,但是仍然显得有些笨拙。一是要引入一个专门的 config_object 类,二是一定要有 base_test 这个第三方的参与。在大多数情况下,这个第三方是不会惹麻烦的,但是永远不能保证某一个从 base_test 派生而来的类会不会改变这个 config_object 类中某些变量的值。也就是说,依然存在一定的风险
上述问题只是最简单的一种情况,如果加入阻塞(blocking)和非阻塞(non-blocking)的概念,则会更加复杂。阻塞和非阻塞这两个术语对于有 Verilog 代码编写经验的人来说是比较熟悉的,因为 Verilog 中就有阻塞赋值和非阻塞赋值。当 monitor 向 scoreboard 传递数据时,scoreboard 可能并不一定有时间立刻接收这些数据。此时对于 monitor 来说有两种处理方法:一种方法是等在那里,一直等到 scoreboard 处理完事情,然后接收新的数据,另外一种方法是不等待,直接返回,至于后面是过一段时间继续发还是直接放弃不发了,则要看代码编写者的行为。前面一种想法相应的就是阻塞操作,而后一种方法就是非阻塞操作
除了阻塞及非阻塞外,还存在的一个问题是如果 scoreboard 主动要求向 monitor 请求数据,这样的行为方式如何实现?
些问题使用现行的 SystemVerilog 中的一些机制,如 Semaphore、Mailbox,再结合其他的一些技术等都能实现,但是这其中的问题在于这种通信显得非常复杂,用户需要浪费大量时间编写通信相关的代码。解决这些问题最好的办法就是在 monitor 和 scoreboard 之间专门建立一个通道,让信息只能在这个通道内流动,scoreboard 也只能从这个通道中接收信息,这样几乎就可以保证 scoreboard 中的信息只能从 monitor 中来,而不能从别的地方来;同时赋予这个通道阻塞或者非阻塞等特性。UVM 中的各种端口就可以实现这种功能
TLM的定义
TLM 是 Transaction Level Modeling(事务级建模)的缩写,它起源于 SystemC 的一种通信标准。所谓 transaction level 是相对 DUT 中各个模块之间信号线级别的通信来说的。简单来说,一个 transaction 就是把具有某一特定功能的一组信息封装在一起而成为的一个类。如 my_transaction 就是把一个 MAC 帧里的各个字段封装在了一起
UVM 中的 TLM 共有两个版本,分别是 TLM1.0 和 TLM2.0,后者在前者的基础上做了扩展。使用 TLM1.0 足以搭建起一个功能强大的验证平台,本章将只讲述 TLM1.0。
TLM 通信中有如下几个常用的术语:
1)put 操作,如图4-3所示,通信的发起者 A 把一个 transaction 发送给 B。在这个过程中,A 称为"发起者",而 B 称为"目标"。A 具有的端口(用方框表示)称为 PORT,而 B 的端口(用圆圈表示)称为 EXPORT。这个过程中,数据流是从 A 流向 B 的
A 作为发起者通过 PORT 把一个 transaction 发送给 B 的 EXPORT,数据流从 A 流向 B
2)get 操作,如图4-4所示,A 向 B 索取一个 transaction。在这个过程中,A 依然是"发起者",B 依然是"目标",A 上的端口依然是 PORT,而 B 上的端口依然是 EXPORT。这个过程中,数据流是从 B 流向 A 的。到这里,读者应该意识到,PORT 和 EXPORT 体现的是控制流而不是数据流。因为在 put 操作中,数据流是从 PORT 流向 EXPORT 的,而在 get 操作中,数据是从 EXPORT 流向 PORT 的。但是无论是 get 还是 put 操作,其发起者拥有的都是 PORT 端口,而不是 EXPORT。作为一个 EXPORT 来说,只能被动地接收 PORT 的命令
A 作为发起者通过 PORT 向 B 的 EXPORT 索取一个 transaction,数据流从 B 流向 A
3)transport 操作,如图4-5所示,transport 操作相当于一次 put 操作加一次 get 操作,这两次操作的"发起者"都是 A,目标都是 B。A 上的端口依然是 PORT,而 B 上的端口依然是 EXPORT。在这个过程中,数据流先从 A 流向 B,再从 B 流向 A。在现实世界中,相当于 A 向 B 提交了一个请求(request),而 B 返回给 A 一个应答(response)。所以这种 transport 操作也常常被称做 request-response 操作
A 通过 PORT 先向 B 的 EXPORT 做一次 put 再做一次 get(即 request-response),数据流先从 A 到 B 再从 B 到 A
put、get 和 transport 操作都有阻塞和非阻塞之分
UVM中的PORT与EXPORT
UVM 提供对 TLM 操作的支持,在其中实现了 PORT 与 EXPORT。对应于不同的操作,有不同的 PORT,UVM 中常用的 PORT 有:
代码4-1
```systemverilog
uvm_blocking_put_port#(T);
uvm_nonblocking_put_port#(T);
uvm_put_port#(T);
uvm_blocking_get_port#(T);
uvm_nonblocking_get_port#(T);
uvm_get_port#(T);
uvm_blocking_peek_port#(T);
uvm_nonblocking_peek_port#(T);
uvm_peek_port#(T);
uvm_blocking_get_peek_port#(T);
uvm_nonblocking_get_peek_port#(T);
uvm_get_peek_port#(T);
uvm_blocking_transport_port#(REQ, RSP);
uvm_nonblocking_transport_port#(REQ, RSP);
uvm_transport_port#(REQ, RSP);
```
三个 put 系列端口对应的是 TLM 中的 put 操作,三个 get 系列端口对应的是 get 操作,三个 transport 系列端口对应的则是 transport 操作(request-response 操作)。另外,上述端口中还有三个 peek 系列端口,它们与 get 系列端口类似,用于主动获取数据,它与 get 操作的区别将在 4.3.4 节中看到。除此之外,还有三个 get_peek 系列端口,它集合了 get 操作和 peek 操作两者的功能。这 15 个端口中前 12 个定义中的参数就是这个 PORT 中的数据流类型,而最后 3 个定义中的参数则表示 transport 操作中发起请求时传输的数据类型和返回的数据类型。这几种 PORT 对应 TLM 中的操作,同时以 blocking 和 nonblocking 关键字区分。对于名称中不含这两者的,则表示这个端口既可以用作是阻塞的,也可以用作是非阻塞的,否则只能用于阻塞的或者只能用于非阻塞的。由这种划分方法可以看出,UVM 把一个端口固定为只能执行某种操作,如对于 uvm_blocking_put_port#(T),它只能执行阻塞的 put 操作,想要执行非阻塞的 put 操作是不行的,想要执行 get 操作,也是不行的,更不用提执行 transport 操作了。所以在使用前用户一定要想清楚了,这个端口将会用于什么操作。如果想要其执行另外的操作,那么最好的方式是再另外使用一个端口
UVM 中常用的 EXPORT 有:
代码4-2
```systemverilog
uvm_blocking_put_export#(T);
uvm_nonblocking_put_export#(T);
uvm_put_export#(T);
uvm_blocking_get_export#(T);
uvm_nonblocking_get_export#(T);
uvm_get_export#(T);
uvm_blocking_peek_export#(T);
uvm_nonblocking_peek_export#(T);
uvm_peek_export#(T);
uvm_blocking_get_peek_export#(T);
uvm_nonblocking_get_peek_export#(T);
uvm_get_peek_export#(T);
uvm_blocking_transport_export#(REQ, RSP);
uvm_nonblocking_transport_export#(REQ, RSP);
uvm_transport_export#(REQ, RSP);
```
这 15 种 EXPORT 定义与前面的 15 种 PORT 一一对应。
PORT 和 EXPORT 体现的是一种控制流,在这种控制流中,PORT 具有高优先级,而 EXPORT 具有低优先级。只有高优先级的端口才能向低优先级的端口发起三种操作
UVM中各种端口的互连
PORT与EXPORT的连接
ABCD 四个端口,要在 A 和 B 之间、C 和 D 之间通信。为了实现这个目标,必须要在 A 和 B 之间、C 和 D 之间建立一种连接关系,否则的话,A 如何知道是和 B 通信而不是和 C 或者 D 通信呢?所以一定要在通信前建立连接关系
A、B、C、D 四个端口之间需要建立明确的连接关系(A-B、C-D)以指定通信对象
UVM 中使用 connect 函数来建立连接关系。如 A 要和 B 通信(A 是发起者),那么可以这么写:A.port.connect(B.export),但是不能写成 B.export.connect(A.port)。因为在通信的过程中,A 是发起者,B 是被动承担者。这种通信时的主次顺序也适用于连接时,只有发起者才能调用 connect 函数,而被动承担者则作为 connect 的参数
使用上述方式建立 A.PORT 和 B.EXPORT 之间的连接关系。A 的代码为:
代码4-3
```systemverilog
class A extends uvm_component;
`uvm_component_utils(A)
uvm_blocking_put_port#(my_transaction) A_port;
// ...
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);
endtask
```
其中 A_port 在实例化的时候比较奇怪,第一个参数是名字,而第二个参数则是一个 uvm_component 类型的父结点变量。事实上,一个 uvm_blocking_put_port 的 new 函数的原型如下:
代码4-4
```systemverilog
function new(string name,
uvm_component parent,
int min_size = 1;
int max_size = 1);
```
如果不看后两个参数,那么这个 new 函数其实就是一个 uvm_component 的 new 函数。new 函数中的 min_size 和 max_size 指的是必须连接到这个 PORT 的下级端口数量的最小值和最大值,也即这一个 PORT 应该调用的 connect 函数的最小值和最大值。如果采用默认值,即 min_size=max_size=1,则只能连接一个 EXPORT
B 的代码为:
代码4-5
```systemverilog
class B extends uvm_component;
`uvm_component_utils(B)
uvm_blocking_put_export#(my_transaction) B_export;
// ...
endclass
function void B::build_phase(uvm_phase phase);
super.build_phase(phase);
B_export = new("B_export", this);
endfunction
task B::main_phase(uvm_phase phase);
endtask
```
在 env 中建立两者之间的连接:
my_env.sv
代码4-6
```systemverilog
class my_env extends uvm_env;
A A_inst;
B B_inst;
// ...
virtual function void build_phase(uvm_phase phase);
// ...
A_inst = A::type_id::create("A_inst", this);
B_inst = B::type_id::create("B_inst", this);
endfunction
// ...
endclass
function void my_env::connect_phase(uvm_phase phase);
super.connect_phase(phase);
A_inst.A_port.connect(B_inst.B_export);
endfunction
```
运行上述代码,可以看到仿真器给出如下的错误提示:
```text
# UVM_ERROR @ 0: uvm_test_top.env.B_inst.B_export [Connection Error] connection count of 0 does not meet required minimum of 1
# UVM_ERROR @ 0: uvm_test_top.env.A_inst.A_port [Connection Error] connection count of 0 does not meet required minimum of 1
# UVM_FATAL @ 0: reporter [BUILDERR] stopping due to build errors
```
connect 函数的使用是没有什么问题的,A_port 与 B_export 的连接也是没有问题的,那么问题出在什么地方?
反思上述的 put 操作,A 通过其端口 A_port 把一个 transaction 传送给 B,这个 A_port 在 transaction 传输的过程中起了什么作用呢?PORT 恰如一道门,EXPORT 也如此。既然是一道门,那么它们也就只是一个通行的作用,它不可能把一笔 transaction 存储下来,因为它只是一道门,没有存储作用,除了转发操作之外不作其他操作。因此,这笔 transaction 一定要由 B_export 后续的某个组件进行处理。在 UVM 中,完成这种后续处理的也是一种端口:IMP
UVM中的IMP
除了 TLM 中定义的 PORT 与 EXPORT 外,UVM 中加入了第三种端口:IMP。IMP 才是 UVM 中的精髓,承担了 UVM 中 TLM 的绝大部分实现代码。UVM 中的 IMP 如下所示:
代码4-7
```systemverilog
uvm_blocking_put_imp#(T, IMP);
uvm_nonblocking_put_imp#(T, IMP);
uvm_put_imp#(T, IMP);
uvm_blocking_get_imp#(T, IMP);
uvm_nonblocking_get_imp#(T, IMP);
uvm_get_imp#(T, IMP);
uvm_blocking_peek_imp#(T, IMP);
uvm_nonblocking_peek_imp#(T, IMP);
uvm_peek_imp#(T, IMP);
uvm_blocking_get_peek_imp#(T, IMP);
uvm_nonblocking_get_peek_imp#(T, IMP);
uvm_get_peek_imp#(T, IMP);
uvm_blocking_transport_imp#(REQ, RSP, IMP);
uvm_nonblocking_transport_imp#(REQ, RSP, IMP);
uvm_transport_imp#(REQ, RSP, IMP);
```
这 15 种 IMP 与15 种 PORT 和 15 种 EXPORT 分别一一对应
IMP 定义中的 blocking、nonblocking、put、get、peek、get_peek、transport 等关键字的意思并不是它们发起做相应类型的操作,而只意味着它们可以和相应类型的 PORT 或者 EXPORT 进行通信,且通信时作为被动承担者。按照控制流的优先级排序,UVM 中三种端口顺序为:PORT、EXPORT、IMP。IMP 的优先级最低,一个 PORT 可以连接到一个 IMP,并发起三种操作,反之则不行
前六个 IMP 定义中的第一个参数 T 是这个 IMP 传输的数据类型。第二个参数 IMP,UVM 文档中把其解释为实现这个接口的一个 component。这句话怎么理解呢?以 blocking_put 端口为例,在图4-7中,A_port 被连接到 B_export,而 B_export 被连接到 B_imp。当写下 A.A_port.put(transaction) 时,此时 B.B_imp 会通知 B 有 transaction 过来了,这个过程是如何进行的呢?可以简单理解成 A.A_port.put(transaction) 这个任务会调用 B.B_export 的 put,B.B_export 的 put(transaction) 又会调用 B.B_imp 的 put(transaction),而 B_imp.put 最终又会调用 B 的相关任务,如 B.put(transaction)。所以关于 A_port 的操作最终会落到 B.put 这个任务上,这个任务是属于 B 的一个任务,与 A 无关,与 A 的 PORT 无关,也与 B 的 EXPORT 和 IMP 无关。也就是说,这些 put 操作最终还是要由 B 这个 component 来实现,即要由一个 component 来实现接口的操作。所以每一个 IMP 要和一个 component 相对应
A 的 PORT 经 B 的 EXPORT 最终连到 B 的 IMP;
A_port.put()的调用经 EXPORT 转发到 IMP,并最终落到 B 的put任务上
有了 IMP 之后,PORT 与 EXPORT 之间的连接就可以实现了。A 的代码为:
代码4-8
```systemverilog
class A extends uvm_component;
`uvm_component_utils(A)
uvm_blocking_put_port#(my_transaction) A_port;
// ...
endclass
// ...
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
```
B 的代码为:
代码4-9
```systemverilog
class B extends uvm_component;
`uvm_component_utils(B)
uvm_blocking_put_export#(my_transaction) B_export;
uvm_blocking_put_imp#(my_transaction, B) B_imp;
// ...
endclass
// ...
function void B::connect_phase(uvm_phase phase);
super.connect_phase(phase);
B_export.connect(B_imp);
endfunction
function void B::put(my_transaction tr);
`uvm_info("B", "receive a transaction", UVM_LOW)
tr.print();
endfunction
```
在 B 的代码中,关键是要实现一个 put 函数/任务。如果不实现,将会给出如下的错误提示:
```text
# ** Error: /home/landy/uvm/uvm-1.1d/src/tlm1/uvm_imps.svh(85): No field named 'put'.
# Region: /uvm_pkg::uvm_blocking_put_imp #(top_tb_sv_unit::my_transaction, top_tb_sv_unit::B)
```
env 的代码与 my_env 的代码相同
运行上述代码,可以见到 B 正确地收到了 A 发出的 transaction。在上述连接关系中,IMP 是作为连接的终点。在 UVM 中,只有 IMP 才能作为连接关系的终点。如果是 PORT 或者 EXPORT 作为终点,则会报错
PORT与IMP的连接
在 UVM 三种端口按控制流优先级排列中,PORT 优先级最高,IMP 的最低。理所当然的,一个 PORT 可以调用 connect 函数并把 IMP 作为函数调用时的参数。假如有三个 component:A、B 和 env,其中 env 是 A 和 B 的父结点,现在要把 A 中的 PORT 和 B 中的 IMP 连接起来实现通信,如图4-8所示
A 中的 PORT 直接与 B 中的 IMP 相连(env 作为二者父结点)实现通信
A 的代码与之前相同,B 的定义如下:
代码4-10
```systemverilog
class B extends uvm_component;
`uvm_component_utils(B)
uvm_blocking_put_imp#(my_transaction, B) B_imp;
// ...
endclass
// ...
function void B::put(my_transaction tr);
`uvm_info("B", "receive a transaction", UVM_LOW)
tr.print();
endfunction
```
由于 A 中采用了 blocking_put 类型的 PORT,所以在 B 中 IMP 相应的类型是 uvm_blocking_put_imp。同时,这个 IMP 有两个参数,第一个参数是将要传输的 transaction,第二个参数前面说过,就是实现接口的 uvm_component,在这里就是 B_imp 所在的 uvm_component B。IMP 的 new 函数与 PORT 的相似,第一个参数是名字,第二个参数是一个 uvm_component 的变量,一般填写 this 即可
B 中的关键是定义一个任务/函数 put。回顾一下,上节中在介绍 IMP 的时候,A_port 的 put 操作最终要落到 B 的 put 上。所以在 B 中要定义一个名字为 put 的任务/函数。这里有如下的规律:
- 当
A_port的类型是nonblocking_put(为了方便,省略了前缀uvm_和后缀_port,下同),B_imp的类型是nonblocking_put(为了方便,省略了前缀uvm_和后缀_imp,下同)时,那么就要在 B 中定义一个名字为try_put的函数和一个名为can_put的函数
- 当
A_port的类型是put,B_imp的类型是put时,那么就要在 B 中定义 3 个接口,一个是put任务/函数,一个是try_put函数,一个是can_put函数
- 当
A_port的类型是blocking_get,B_imp的类型是blocking_get时,那么就要在 B 中定义一个名字为get的任务/函数
- 当
A_port的类型是nonblocking_get,B_imp的类型是nonblocking_get时,那么就要在 B 中定义一个名字为try_get的函数和一个名为can_get的函数
- 当
A_port的类型是get,B_imp的类型是get时,那么就要在 B 中定义 3 个接口,一个是get任务/函数,一个是try_get函数,一个是can_get函数
- 当
A_port的类型是blocking_peek,B_imp的类型是blocking_peek时,那么就要在 B 中定义一个名字为peek的任务/函数
- 当
A_port的类型是nonblocking_peek,B_imp的类型是nonblocking_peek时,那么就要在 B 中定义一个名字为try_peek的函数和一个名为can_peek的函数
- 当
A_port的类型是peek,B_imp的类型是peek时,那么就要在 B 中定义 3 个接口,一个是peek任务/函数,一个是try_peek函数,一个是can_peek函数
- 当
A_port的类型是blocking_get_peek,B_imp的类型是blocking_get_peek时,那么就要在 B 中定义一个名字为get的任务/函数,一个名字为peek的任务/函数
- 当
A_port的类型是nonblocking_get_peek,B_imp的类型是nonblocking_get_peek时,那么就要在 B 中定义一个名字为try_get的函数,一个名为can_get的函数,一个名字为try_peek的函数和一个名为can_peek的函数
- 当
A_port的类型是get_peek,B_imp的类型是get_peek时,那么就要在 B 中定义 6 个接口,一个是get任务/函数,一个是try_get函数,一个是can_get函数,一个是peek任务/函数,一个是try_peek函数,一个是can_peek函数
- 当
A_port的类型是blocking_transport,B_imp的类型是blocking_transport时,那么就要在 B 中定义一个名字为transport的任务/函数
- 当
A_port的类型是nonblocking_transport,B_imp的类型是nonblocking_transport时,那么就要在 B 中定义一个名字为nb_transport的函数
- 当
A_port的类型是transport,B_imp的类型是transport时,那么就要在 B 中定义两个接口,一个是transport任务/函数,一个是nb_transport函数
在前述的这些规律中,对于所有 blocking 系列的端口来说,可以定义相应的任务或函数,如对于 blocking_put 端口来说,可以定义名字为 put 的任务,也可以定义名字为 put 的函数。这是因为 A 会调用 B 中名字为 put 的接口,而不管这个接口的类型。由于 A 中的 put 是个任务,所以 B 中的 put 可以是任务,也可以是函数。但是对于 nonblocking 系列端口来说,只能定义函数
回到前面的例子中来,当 B 中完成 B_imp 和 put 的定义后,在 env 的 connect_phase 就需要把 A_port 和 B_imp 连接在一起了:
my_env.sv
代码4-11
```systemverilog
function void my_env::connect_phase(uvm_phase phase);
super.connect_phase(phase);
A_inst.A_port.connect(B_inst.B_imp);
endfunction
```
connect 函数一定要在 connect_phase 调用。连接完成后,当在 A 中通过 put 向 A_port 写入一个 transaction 时,B 的 put 马上会被调用,并执行其中的代码。A 的代码与之前相同,在此段代码中,A 向 A_port 写入了 10 个 transaction,因此 B 的 put 会被调用 10 次
EXPORT与IMP的连接
PORT 可以与 IMP 相连接,同样的 EXPORT 也可以与 IMP 相连接,其连接方法与 PORT 和 IMP 的连接完全一样。上面已经看到了 EXPORT 与 IMP 的连接,不过在那个连接中 EXPORT 只是作为中间环节,这里把 EXPORT 作为连接的起点
要实现 A 中的 EXPORT 与 B 中的 IMP 连接,A 的代码为:
代码4-12
```systemverilog
class A extends uvm_component;
`uvm_component_utils(A)
uvm_blocking_put_export#(my_transaction) A_export;
// ...
endclass
// ...
task A::main_phase(uvm_phase phase);
my_transaction tr;
repeat(10) begin
#10;
tr = new("tr");
assert(tr.randomize());
A_export.put(tr);
end
endtask
```
B 的代码与 4-10 完全相同。my_env 中的连接关系为:
my_env.sv
代码4-13
```systemverilog
function void my_env::connect_phase(uvm_phase phase);
super.connect_phase(phase);
A_inst.A_export.connect(B_inst.B_imp);
endfunction
```
如上述代码所示,就可以实现一个 EXPORT 和一个 IMP 的连接。与上一小节中的例子对比可以发现,除了 A_port 变成 A_export 之外,其他没有任何改变。在 B 中也必须定义一个名字为 put 的任务。上一节中罗列的那些规律,对于 EXPORT 依然适用
PORT与PORT的连接
在前面的连接中,都是不同类型的端口之间连接(PORT 与 IMP、PORT 与 EXPORT、EXPORT 与 IMP),且不存在层次的关系。在 UVM 中,支持带层次的连接关系,如图4-9所示
C 的 PORT 连接到 A 的 PORT,再连到 B 的 IMP,体现带层次的端口级联连接
在上图中,A 与 C 中是 PORT,B 中是 IMP。UVM 支持 C 的 PORT 连接到 A 的 PORT,并最终连接到 B 的 IMP
C 的代码为:
代码4-14
```systemverilog
class C extends uvm_component;
`uvm_component_utils(C)
uvm_blocking_put_port#(my_transaction) C_port;
// ...
endclass
// ...
task C::main_phase(uvm_phase phase);
my_transaction tr;
repeat(10) begin
#10;
tr = new("tr");
assert(tr.randomize());
C_port.put(tr);
end
endtask
```
A的代码为:
代码4-15
```systemverilog
class A extends uvm_component;
`uvm_component_utils(A)
C C_inst;
uvm_blocking_put_port#(my_transaction) A_port;
// ...
endclass
function void A::build_phase(uvm_phase phase);
super.build_phase(phase);
A_port = new("A_port", this);
C_inst = C::type_id::create("C_inst", this);
endfunction
function void A::connect_phase(uvm_phase phase);
super.connect_phase(phase);
C_inst.C_port.connect(this.A_port);
endfunction
task A::main_phase(uvm_phase phase);
endtask
```
B 的代码与代码清单 4-10 完全相同,env 的代码与代码清单 4-11 完全相同
PORT 与 PORT 之间的连接不只局限于两层,可以有无限多层
EXPORT与EXPORT的连接
除了支持 PORT 与 PORT 之间的连接外,UVM 同样支持 EXPORT 与 EXPORT 之间的连接,如图4-10所示
C 的 EXPORT 连接到 B 的 EXPORT,再连到 B 中的 IMP,体现带层次的 EXPORT 级联
A 中是 PORT,B 与 C 中是 EXPORT,B 中还有一个 IMP。UVM 支持 C 的 EXPORT 连接到 B 的 EXPORT,并最终连接到 B 的 IMP
A 的代码与代码清单 4-8 相同,B 的代码与代码清单 4-9 相同。C 的代码为:
代码4-16
```systemverilog
class C extends uvm_component;
`uvm_component_utils(C)
B B_inst;
uvm_blocking_put_export#(my_transaction) C_export;
// ...
endclass
function void C::build_phase(uvm_phase phase);
super.build_phase(phase);
C_export = new("C_export", this);
B_inst = B::type_id::create("B_inst", this);
endfunction
function void C::connect_phase(uvm_phase phase);
super.connect_phase(phase);
this.C_export.connect(B_inst.B_export);
endfunction
task C::main_phase(uvm_phase phase);
endtask
```
env 中的连接关系为:
my_env.sv
代码4-17
```systemverilog
function void my_env::connect_phase(uvm_phase phase);
super.connect_phase(phase);
A_inst.A_port.connect(C_inst.C_export);
endfunction
```
同样的,EXPORT 与 EXPORT 之间的连接也不只局限于两层,也可以有无限多层
blocking_get端口的使用
get 系列端口与 put 系列端口在某些方面完全相反
要实现图4-7从 A 到 B 的通信,使用 blocking_get 系列端口的框图如图4-11所示
用 blocking_get 系列端口实现 A→B 的通信,此时数据流仍从 A 到 B,但动作发起者变为 B(B 主动 get,A 被动提供)
在这种连接关系中,数据流依然是从 A 到 B,但是 A 由动作发起者变成了动作接收者,而 B 由动作接收者变成了动作发起者
B_port 的类型为 uvm_blocking_get_port,A_export 的类型为 uvm_blocking_get_export,A_imp 的类型为 uvm_blocking_get_imp。与 uvm_blocking_put_imp 所在的 component 要实现一个 put 的函数/任务类似,uvm_blocking_get_imp 所在的 component 要实现一个名字为 get 的函数/任务。A 的代码为:
代码4-18
```systemverilog
class A extends uvm_component;
`uvm_component_utils(A)
uvm_blocking_get_export#(my_transaction) A_export;
uvm_blocking_get_imp#(my_transaction, A) A_imp;
my_transaction tr_q[$];
// ...
endclass
function void A::build_phase(uvm_phase phase);
super.build_phase(phase);
A_export = new("A_export", this);
A_imp = new("A_imp", this);
endfunction
function void A::connect_phase(uvm_phase phase);
super.connect_phase(phase);
A_export.connect(A_imp);
endfunction
task A::get(output my_transaction tr);
while(tr_q.size() == 0) #2;
tr = tr_q.pop_front();
endtask
task A::main_phase(uvm_phase phase);
my_transaction tr;
repeat(10) begin
#10;
tr = new("tr");
tr_q.push_back(tr);
end
endtask
```
在 A 的 get 任务中,每隔 2 个时间单位检查 tr_q 中是否有数据,如果有则发送出去。当 B 在其 main_phase 调用 get 任务时,会最终执行 A 的 get 任务。在 A 的 connect_phase,需要把 A_export 和 A_imp 连接起来
B 的代码为:
代码4-19
```systemverilog
class B extends uvm_component;
`uvm_component_utils(B)
uvm_blocking_get_port#(my_transaction) B_port;
// ...
endclass
function void B::build_phase(uvm_phase phase);
super.build_phase(phase);
B_port = new("B_port", this);
endfunction
task B::main_phase(uvm_phase phase);
my_transaction tr;
while(1) begin
B_port.get(tr);
`uvm_info("B", "get a transaction", UVM_LOW)
tr.print();
end
endtask
```
env 中的连接关系变为:
my_env.sv
代码4-20
```systemverilog
function void my_env::connect_phase(uvm_phase phase);
super.connect_phase(phase);
B_inst.B_port.connect(A_inst.A_export);
endfunction
```
上面介绍了 blocking_get_port 与 blocking_get_export 及 blocking_get_imp 的连接。与 blocking_put 系列端口类似,blocking_get_port 也可以直接连接到 blocking_get_imp,同时 blocking_get_port 也可以连接到 blocking_get_port,blocking_get_export 也可以连接到 blocking_get_export。在这些连接关系中,需要谨记的是连接的终点必须是一个 IMP、
blocking_transport端口的使用
transport 系列端口与 put 和 get 系列端口都不一样。在 put 和 get 系列端口中,所有的通信都是单向的,而在 transport 系列端口中,通信变成了双向的
A 的 blocking_transport_port 直接连到 B 的 blocking_transport_imp,实现双向的 request-response 通信
若要实现图4-12所示的连接关系,需要在 A 中定义一个 transport:
代码4-21
```systemverilog
class A extends uvm_component;
`uvm_component_utils(A)
uvm_blocking_transport_port#(my_transaction, my_transaction) A_transport;
// ...
endclass
// ...
task A::main_phase(uvm_phase phase);
my_transaction tr;
my_transaction rsp;
repeat(10) begin
#10;
tr = new("tr");
assert(tr.randomize());
A_transport.transport(tr, rsp);
`uvm_info("A", "received rsp", UVM_MEDIUM)
rsp.print();
end
endtask
```
B 中需要定义一个类型为 uvm_blocking_transport_imp 的 IMP:
代码4-22
```systemverilog
class B extends uvm_component;
`uvm_component_utils(B)
uvm_blocking_transport_imp#(my_transaction, my_transaction, B) B_imp;
// ...
endclass
// ...
task B::transport(my_transaction req, output my_transaction rsp);
`uvm_info("B", "receive a transaction", UVM_LOW)
req.print();
//do something according to req
#5;
rsp = new("rsp");
endtask
```
env 中的连接关系为:
my_env.sv
代码4-23
```systemverilog
function void my_env::connect_phase(uvm_phase phase);
super.connect_phase(phase);
A_inst.A_transport.connect(B_inst.B_imp);
endfunction
```
在 A 中调用 transport 任务,并把生成的 transaction 作为第一个参数。B 中的 transport 任务接收到这笔 transaction,根据这笔 transaction 做某些操作,并把操作的结果作为 transport 的第二个参数发送出去。A 根据接收到的 rsp 来决定后面的行为
在本例中,是 blocking_transport_port 直接连接到 blocking_transport_imp,前者还可以连接到 blocking_transport_export,这三者之间的连接关系与 blocking_put 系列端口类似
nonblocking端口的使用
onblocking 端口的所有操作都是非阻塞的,换言之,必须用函数实现,而不能用任务实现。本节以 nonblocking_put 端口为例介绍 nonblocking 端口的使用
以用 nonblocking 端口实现图4-8所示的连接关系为例,需要在 A 中定义一个 nonblocking_put_port 端口:
代码4-24
```systemverilog
class A extends uvm_component;
`uvm_component_utils(A)
uvm_nonblocking_put_port#(my_transaction) A_port;
// ...
endclass
// ...
task A::main_phase(uvm_phase phase);
my_transaction tr;
repeat(10) begin
tr = new("tr");
assert(tr.randomize());
while(!A_port.can_put()) #10;
void'(A_port.try_put(tr));
end
endtask
```
由于端口变为了非阻塞的,所以在送出 transaction 之前需要调用 can_put 函数来确认是否能够执行 put 操作。can_put 最终会调用 B 中的 can_put:
代码4-25
```systemverilog
class B extends uvm_component;
`uvm_component_utils(B)
uvm_nonblocking_put_imp#(my_transaction, B) B_imp;
my_transaction tr_q[$];
// ...
endclass
// ...
function bit B::can_put();
if(tr_q.size() > 0)
return 0;
else
return 1;
endfunction
function bit B::try_put(my_transaction tr);
`uvm_info("B", "receive a transaction", UVM_LOW)
if(tr_q.size() > 0)
return 0;
else begin
tr_q.push_back(tr);
return 1;
end
endfunction
task B::main_phase(uvm_phase phase);
my_transaction tr;
while(1) begin
if(tr_q.size() > 0)
tr = tr_q.pop_front();
else
#25;
end
endtask
```
在 A 中使用 can_put 来判断是否可以发送,其实这里还可以不用 can_put,而直接使用 try_put:
代码4-26
```systemverilog
task A::main_phase(uvm_phase phase);
my_transaction tr;
repeat(10) begin
tr = new("tr");
assert(tr.randomize());
while(!A_port.try_put(tr)) #10;
end
endtask
```
如果不使用 can_put,在 B 中依然需要定义一个名字为 can_put 的函数,这个函数里可以没有任何内容,纯粹是一个空函数
env 中的连接关系为:
my_env.sv
代码4-27
```systemverilog
function void my_env::connect_phase(uvm_phase phase);
super.connect_phase(phase);
A_inst.A_export.connect(B_inst.B_imp);
endfunction
```
nonblocking_get 系列端口和 nonblocking_transport 系列端口的使用与 nonblocking_put 类似
UVM中的通信方式
UVM中的analysis端口
除了这几种端口外,UVM 中还有两种特殊的端口:analysis_port 和 analysis_export。这两者其实与 put 和 get 系列端口类似,都用于传递 transaction。它们的区别是:
第一,默认情况下,一个 analysis_port(analysis_export)可以连接多个 IMP,也就是说,analysis_port(analysis_export)与 IMP 之间的通信是一对多的通信,而 put 和 get 系列端口与相应 IMP 的通信是一对一的通信(除非在实例化时指定可以连接的数量,参照A_port的new函数,代码4-4)
analysis_port(analysis_export)更像是一个广播
第二,put 与 get 系列端口都有阻塞和非阻塞的区分。但是对于 analysis_port 和 analysis_export 来说,没有阻塞和非阻塞的概念。因为它本身就是广播,不必等待与其相连的其他端口的响应,所以不存在阻塞和非阻塞
一个 analysis_port 可以和多个 IMP 相连接进行通信,但是 IMP 的类型必须是 uvm_analysis_imp,否则会报错
对于 put 系列端口,有 put、try_put、can_put 等操作,对于 get 系列端口,有 get、try_get 和 can_get 等操作。对于 analysis_port 和 analysis_export 来说,只有一种操作:write。在 analysis_imp 所在的 component,必须定义一个名字为 write 的函数
一个 analysis_port 同时连接多个 IMP(B、C),体现一对多的广播式通信
要实现图4-13中所示的连接关系,A 的代码为:
代码4-28
```systemverilog
class A extends uvm_component;
`uvm_component_utils(A)
uvm_analysis_port#(my_transaction) A_ap;
// ...
endclass
// ...
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
endtask
```
A 的代码很简单,只是简单地定义一个 analysis_port,并在 main_phase 中每隔 10 个时间单位写入一个 transaction
B 的代码为:
代码4-29
```systemverilog
class B extends uvm_component;
`uvm_component_utils(B)
uvm_analysis_imp#(my_transaction, B) B_imp;
// ...
endclass
// ...
function void B::write(my_transaction tr);
`uvm_info("B", "receive a transaction", UVM_LOW)
tr.print();
endfunction
```
如前所述,B 是 B_imp 所在的 component,因此要在 B 中定义一个名字为 write 的函数。在 B 的 main_phase 中不需要做任何操作
C 的代码与 B 完全相似,只要把相应的 B 替换为 C 即可
env 中的连接关系为:
my_env.sv
代码4-30
```systemverilog
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
```
在 env 中,可以看到 A_ap 分别与 B 和 C 中相应的 imp 连接到了一起。这种一对二的连接方式在 4.2 节中是没有出现过的
上面只是一个 analysis_port 与 IMP 相连的例子。analysis_export 和 IMP 也可以这样相连接,只需将上面例子中的 uvm_analysis_port 改为 uvm_analysis_export 就可以
与 put 系列端口的 PORT 和 EXPORT 直接相连会出错的情况一样,analysis_port 如果和一个 analysis_export 直接相连也会出错。只有在 analysis_export 后面再连接一级 uvm_analysis_imp,才不会出错
一个component内有多个IMP
o_agt 的 monitor 与 scoreboard 之间的通信,使用 analysis_port 实现。在 monitor 中:
代码4-31
```systemverilog
class monitor extends uvm_monitor;
uvm_analysis_port#(my_transaction) ap;
task main_phase(uvm_phase phase);
super.main_phase(phase);
my_transaction tr;
// ...
ap.write(tr);
// ...
endtask
endclass
```
在 scoreboard 中:
代码4-32
```systemverilog
class scoreboard extends uvm_scoreboard;
uvm_analysis_imp#(my_transaction, scoreboard) scb_imp;
task write(my_transaction tr);
//do something on tr
endtask
endclass
```
之后在 env 中可以使用 connect 连接。由于 monitor 与 scoreboard 在 UVM 树中并不是平等的兄妹关系,其中间还间隔了 o_agt,所以这里有三种连接方式,第一种是直接在 env 中跨层次引用 monitor 中的 ap:
代码4-33
```systemverilog
function void my_env::connect_phase(uvm_phase phase);
o_agt.mon.ap.connect(scb.scb_imp);
// ...
endfunction
```
第二种是在 agent 中声明一个 ap 并实例化它,在 connect_phase 将其与 monitor 的 ap 相连,并可以在 env 中把 agent 的 ap 直接连接到 scoreboard 的 imp:
代码4-34
```systemverilog
class my_agent extends uvm_agent ;
uvm_analysis_port #(my_transaction) ap;
// ...
function void build_phase(uvm_phase phase);
super.build_phase(phase);
ap = new("ap", this);
// ...
endfunction
function void my_agent::connect_phase(uvm_phase phase);
mon.ap.connect(this.ap);
// ...
endfunction
endclass
function void my_env::connect_phase(uvm_phase phase);
o_agt.ap.connect(scb.scb_imp);
// ...
endfunction
```
第三种是在 agent 中声明一个 ap,但是不实例化它,让其指向 monitor 中的 ap。在 env 中可以直接连接 agent 的 ap 到 scoreboard 的 imp:
代码4-35
```systemverilog
class my_agent extends uvm_agent ;
uvm_analysis_port #(my_transaction) ap;
// ...
function void my_agent::connect_phase(uvm_phase phase);
ap = mon.ap;
// ...
endfunction
endclass
function void my_env::connect_phase(uvm_phase phase);
o_agt.ap.connect(scb.scb_imp);
// ...
endfunction
```
如上所述的三种方式中,第一种最简单,但是其层次关系并不好,第二种稍显麻烦,第三种既具有明显的层次关系,同时其实现也较简单
上面的 monitor 和 scoreboard 之间的通信是通过采用一个 analysis_port 和一个 analysis_imp 相连的方式实现的。对于一个 analysis_imp 来说,必须在其实例化的 uvm_component 中定义一个 write 的函数。在上面的例子中,scoreboard 只接收一路数据,但在现实情况中,scoreboard 除了接收 monitor 的数据之外,还要接收 reference model 的数据。相应的 scoreboard 就要再添加一个 uvm_analysis_imp 的 IMP,如 model_imp。此时问题就出现了,由于接收到的两路数据应该做不同的处理,所以这个新的 IMP 也要有一个 write 任务与其对应。但是 write 只有一个,怎么办?
UVM 考虑到了这种情况,它定义了一个宏 uvm_analysis_imp_decl 来解决这个问题,其使用方式为:
my_scoreboard.sv
代码4-36
```systemverilog
`uvm_analysis_imp_decl(_monitor)
`uvm_analysis_imp_decl(_model)
class my_scoreboard extends uvm_scoreboard;
my_transaction expect_queue[$];
uvm_analysis_imp_monitor#(my_transaction, my_scoreboard) monitor_imp;
uvm_analysis_imp_model#(my_transaction, my_scoreboard) model_imp;
// ...
extern function void write_monitor(my_transaction tr);
extern function void write_model(my_transaction tr);
extern virtual task main_phase(uvm_phase phase);
endclass
```
上述代码通过宏 uvm_analysis_imp_decl 声明了两个后缀 _monitor 和 _model。UVM 会根据这两个后缀定义两个新的 IMP 类: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 后面添加上相应的后缀就可以正常工作了:
my_scoreboard.sv
代码4-37
```systemverilog
function void my_scoreboard::write_model(my_transaction tr);
expect_queue.push_back(tr);
endfunction
function void my_scoreboard::write_monitor(my_transaction tr);
my_transaction tmp_tran;
bit result;
if(expect_queue.size() > 0) begin
// ...
end
endfunction
```
使用FIFO通信
实现 monitor 和 scoreboard 之间的通信时先声明了两个后缀,然后再写相应的函数,这种方法看起来有些麻烦,而且对于初学者来说有些难以理解。那么有没有简单的方法呢?另外上节中 monitor 和 scoreboard 的通信,monitor 占据主动地位,而 scoreboard 只能被动地接收,那么有没有方法也让 scoreboard 实现主动的接收呢?这两个问题的答案都是肯定的,那就是使用第2章使用的方式:利用 FIFO 来实现 monitor 和 scoreboard 的通信

如图4-14b 所示,在 agent 和 scoreboard 之间添加一个 uvm_analysis_fifo。FIFO 的本质是一块缓存加两个 IMP。在 monitor 与 FIFO 的连接关系中,monitor 中依然是 analysis_port,FIFO 中是 uvm_analysis_imp,数据流和控制流的方向相同。在 scoreboard 与 FIFO 的连接关系中,scoreboard 中使用 blocking_get_port 端口:
my_scoreboard.sv
代码4-38
```systemverilog
class my_scoreboard extends uvm_scoreboard;
my_transaction expect_queue[$];
uvm_blocking_get_port #(my_transaction) exp_port;
uvm_blocking_get_port #(my_transaction) act_port;
// ...
endclass
// ...
task my_scoreboard::main_phase(uvm_phase phase);
// ...
fork
while (1) begin
exp_port.get(get_expect);
expect_queue.push_back(get_expect);
end
while (1) begin
act_port.get(get_actual);
// ...
end
join
endtask
```
而 FIFO 中使用的是一个 get 端口的 IMP。在这种连接关系中,控制流是从 scoreboard 到 FIFO,而数据流是从 FIFO 到 scoreboard
对比 (a) 直接用 analysis_port/IMP 相连 与 (b) 在 agent 与 scoreboard 之间插入 uvm_analysis_fifo 两种通信结构
在 env 里面以如下方式连接:
my_env.sv
代码4-39
```systemverilog
class my_env extends uvm_env;
my_agent i_agt;
my_agent o_agt;
my_model mdl;
my_scoreboard scb;
uvm_tlm_analysis_fifo #(my_transaction) agt_scb_fifo;
uvm_tlm_analysis_fifo #(my_transaction) agt_mdl_fifo;
uvm_tlm_analysis_fifo #(my_transaction) mdl_scb_fifo;
// ...
endclass
function void my_env::connect_phase(uvm_phase phase);
super.connect_phase(phase);
i_agt.ap.connect(agt_mdl_fifo.analysis_export);
mdl.port.connect(agt_mdl_fifo.blocking_get_export);
mdl.ap.connect(mdl_scb_fifo.analysis_export);
scb.exp_port.connect(mdl_scb_fifo.blocking_get_export);
o_agt.ap.connect(agt_scb_fifo.analysis_export);
scb.act_port.connect(agt_scb_fifo.blocking_get_export);
endfunction
```
如图4-14b 所示,FIFO 中有两个 IMP,但是在上面的连接关系中,FIFO 中却是 EXPORT,这是为什么呢?实际上,FIFO 中的 analysis_export 和 blocking_get_export 虽然名字中有关键字 export,但是其类型却是 IMP。UVM 为了掩饰 IMP 的存在,在它们的命名中加入了 export 关键字。如 analysis_export 的原型如下:
代码4-40
```systemverilog
uvm_analysis_imp #(T, uvm_tlm_analysis_fifo #(T)) analysis_export;
```
使用 FIFO 连接之后,第一个好处是不必在 scoreboard 中再写一个名字为 write 的函数。scoreboard 可以按照自己的节奏工作,而不必跟着 monitor 的节奏。第二个好处是 FIFO 的存在隐藏了 IMP,这对于初学者来说比较容易理解。第三个好处是可以轻易解决上一节讲到的当 reference model 和 monitor 同时连接到 scoreboard 应如何处理的问题。事实上,FIFO 的存在自然而然地解决了它,这根本就不是一个问题了
FIFO上的端口及调试
上面介绍了 uvm_tlm_analysis_fifo,并介绍了它的两个端口:blocking_get_export 和 analysis_export。事实上,FIFO 上的端口并不局限于上述两个,一个 FIFO 中有众多的端口,如图4-15所示
一个 FIFO 内部众多的端口(12 种 IMP 性质的 EXPORT、analysis_port 等),说明其可分别与相应 PORT/EXPORT 连接的拓扑
上图中所有以圆圈表示的 EXPORT 虽然名字中有 export,但是本质上都是 IMP。这里面包含了代码清单 4-7 中除 transport 系列外的 12 种 IMP,用于分别和相应的 PORT 及 EXPORT 连接。前文已经介绍了 put 和 get 系列端口,这里简要地说明一下 peek 系列端口。peek 端口与 get 相似,其数据流、控制流都相似,唯一的区别在于当 get 任务被调用时,FIFO 内部缓存中会少一个 transaction,而 peek 被调用时,FIFO 会把 transaction 复制一份发送出去,其内部缓存中的 transaction 数量并不会减少
除了这 12 个 IMP 外,上图中还有两个 analysis_port:put_ap 和 get_ap。当 FIFO 上的 blocking_put_export 或者 put_export 被连接到一个 blocking_put_port 或者 put_port 上时,FIFO 内部被定义的 put 任务被调用,这个 put 任务把传递过来的 transaction 放在 FIFO 内部的缓存里,同时,把这个 transaction 通过 put_ap 使用 write 函数发送出去。FIFO 的 put 任务定义如下:
代码4-41
```systemverilog
virtual task put( input T t );
m.put( t );
put_ap.write( t );
endtask
```
上述代码中的 m 即是 FIFO 内部的缓存,使用 SystemVerilog 中的 mailbox 来实现。
与 put_ap 相似,当 FIFO 的 get 任务被调用时,同样会有一个 transaction 从 get_ap 上发出:
代码4-42
```systemverilog
virtual task get( output T t );
m_pending_blocked_gets++;
m.get( t );
m_pending_blocked_gets--;
get_ap.write( t );
endtask
```
什么时候会触发 FIFO 中的这个 get 任务呢?在上一节中,一个 blocking_get_port 连接到了 FIFO 上,当它调用 get 任务获取 transaction 时就会调用 FIFO 的 get 任务。除此之外,FIFO 的 get_export、get_peek_export 和 blocking_get_peek_export 被相应的 PORT 或者 EXPORT 连接时,也能会调用 FIFO 的 get 任务
FIFO 的类型有两种,一种是上节介绍的 uvm_tlm_analysis_fifo,另外一种是 uvm_tlm_fifo。这两者的唯一差别在于前者有一个 analysis_export 端口,并且有一个 write 函数,而后者没有。除此之外,本节上面介绍的所有端口同时适用于这两者
FIFO 中的众多端口方便了用户的使用,同样的,UVM 也提供了几个函数用于 FIFO 的调试
used 函数用于查询 FIFO 缓存中有多少 transaction。is_empty 函数用于判断当前 FIFO 缓存是否为空。与 is_empty 对应的是 is_full,用于判断当前 FIFO 缓存是否已经满了。作为一个缓存来说,其能存储的 transaction 是有限的。那么这个最大值是在哪里定义的呢?FIFO 的 new 函数原型如下:
代码4-43
```systemverilog
function new(string name, uvm_component parent = null, int size = 1);
```
FIFO 在本质上是一个 component,所以其前两个参数是 uvm_component 的 new 函数中的两个参数。第三个参数是 size,用于设定 FIFO 缓存的上限,在默认的情况下为 1。若要把缓存设置为无限大小,将传入的 size 参数设置为 0 即可。通过 size 函数可以返回这个上限值。
除了上述的函数外,FIFO 中还有一个 flush 函数,其原型为:
代码4-44
```systemverilog
virtual function void flush();
```
这个函数用于清空 FIFO 缓存中的所有数据,它一般用于复位等操作
用FIFO还是用IMP
用 FIFO 还是直接用 IMP 来实现通信呢?
每个人对于这个问题都有各自不同的答案。在用 FIFO 通信的方法中,完全隐藏了 IMP 这个 UVM 中特有、而 TLM 中根本就没有的东西。用户可以完全不关心 IMP。因此,对于用户来说,只需要知道 analysis_port、blocking_get_port 即可。这大大简化了初学者的工作量。尤其是在 scoreboard 面临多个 IMP,且需要为 IMP 声明一个后缀时,这种优势更加明显
FIFO 连接的方式增加了 env 中代码的复杂度,满满的看上去似乎都是与 FIFO 相关的代码。尤其是当要连接的端口数量众多时,这个缺点更加明显
不过对于使用端口数组的情况,FIFO 要优于 IMP。假如参考模型中有 16 个类似端口要和 scoreboard 中相应的端口相互通信,如此多数量的端口,在参考模型中可以使用端口数组来实现:
my_model.sv
代码4-45
```systemverilog
class my_model extends uvm_component;
uvm_blocking_get_port #(my_transaction) port;
uvm_analysis_port #(my_transaction) ap[16];
// ...
endclass
// ...
function void my_model::build_phase(uvm_phase phase);
super.build_phase(phase);
port = new("port", this);
for(int i = 0; i < 16; i++)
ap[i] = new($sformatf("ap_%0d", i), this);
endfunction
```
如果连接关系使用 IMP 加后缀的方式,那么在 scoreboard 中的代码如下:
my_scoreboard.sv
代码4-46
```systemverilog
`uvm_analysis_imp_decl(_model0)
// ...
`uvm_analysis_imp_decl(_modelf)
`uvm_analysis_imp_decl(_monitor)
class my_scoreboard extends uvm_scoreboard;
my_transaction expect_queue[$];
uvm_analysis_imp_monitor#(my_transaction, my_scoreboard) monitor_imp;
uvm_analysis_imp_model0#(my_transaction, my_scoreboard) model0_imp;
// ...
uvm_analysis_imp_modelf#(my_transaction, my_scoreboard) modelf_imp;
`uvm_component_utils(my_scoreboard)
extern function new(string name, uvm_component parent = null);
extern virtual function void build_phase(uvm_phase phase);
extern virtual task main_phase(uvm_phase phase);
extern function void write_monitor(my_transaction tr);
extern function void write_model0(my_transaction tr);
// ...
extern function void write_modelf(my_transaction tr);
endclass
// ...
function void my_scoreboard::build_phase(uvm_phase phase);
super.build_phase(phase);
monitor_imp = new("monitor_imp", this);
model0_imp = new("model0_imp", this);
// ...
modelf_imp = new("modelf_imp", this);
endfunction
function void my_scoreboard::write_model0(my_transaction tr);
expect_queue.push_back(tr);
endfunction
// ...
function void my_scoreboard::write_modelf(my_transaction tr);
expect_queue.push_back(tr);
endfunction
function void my_scoreboard::write_monitor(my_transaction tr);
// ...
endfunction
```
并且在 env 中,需要:
my_env.sv
代码4-47
```systemverilog
function void my_env::connect_phase(uvm_phase phase);
super.connect_phase(phase);
i_agt.ap.connect(agt_mdl_fifo.analysis_export);
mdl.port.connect(agt_mdl_fifo.blocking_get_export);
o_agt.ap.connect(scb.monitor_imp);
mdl.ap[0].connect(scb.model0_imp);
mdl.ap[1].connect(scb.model1_imp);
// ...
mdl.ap[14].connect(scb.modele_imp);
mdl.ap[15].connect(scb.modelf_imp);
endfunction
```
在如上列出的代码中使用了很多省略号,但是即使这样,相信读者也能感受到其中代码的冗余到了多么严重的程度。这一切都是因为 ap 与 imp 直接相连而不能使用 for 循环引起的。
假如使用 FIFO 连接,那么在 scoreboard 中可以:
代码4-48
```systemverilog
class my_scoreboard extends uvm_scoreboard;
my_transaction expect_queue[$];
uvm_blocking_get_port #(my_transaction) exp_port[16];
uvm_blocking_get_port #(my_transaction) act_port;
// ...
endclass
// ...
function void my_scoreboard::build_phase(uvm_phase phase);
super.build_phase(phase);
for(int i = 0; i < 16; i++)
exp_port[i] = new($sformatf("exp_port_%0d", i), this);
act_port = new("act_port", this);
endfunction
task my_scoreboard::main_phase(uvm_phase phase);
// ...
for(int i = 0; i < 16; i++)
fork
automatic int k = i;
while (1) begin
exp_port[k].get(get_expect);
expect_queue.push_back(get_expect);
end
join_none
while (1) begin
act_port.get(get_actual);
// ...
end
endtask
```
在 env 中也可以使用 for 循环:
代码4-49
```systemverilog
class my_env extends uvm_env;
// ...
uvm_tlm_analysis_fifo #(my_transaction) agt_scb_fifo;
uvm_tlm_analysis_fifo #(my_transaction) agt_mdl_fifo;
uvm_tlm_analysis_fifo #(my_transaction) mdl_scb_fifo[16];
// ...
virtual function void build_phase(uvm_phase phase);
// ...
agt_scb_fifo = new("agt_scb_fifo", this);
agt_mdl_fifo = new("agt_mdl_fifo", this);
for(int i = 0; i < 16; i++)
mdl_scb_fifo[i] = new($sformatf("mdl_scb_fifo_%0d", i), this);
endfunction
// ...
endclass
function void my_env::connect_phase(uvm_phase phase);
super.connect_phase(phase);
i_agt.ap.connect(agt_mdl_fifo.analysis_export);
mdl.port.connect(agt_mdl_fifo.blocking_get_export);
for(int i = 0; i < 16; i++) begin
mdl.ap[i].connect(mdl_scb_fifo[i].analysis_export);
scb.exp_port[i].connect(mdl_scb_fifo[i].blocking_get_export);
end
o_agt.ap.connect(agt_scb_fifo.analysis_export);
scb.act_port.connect(agt_scb_fifo.blocking_get_export);
endfunction
```
无论使用 FIFO 还是使用 IMP,都能实现同样的目标,两者各有其优势与劣势
UVM 中的 TLM1.0 通信机制:
- 通信抽象 :UVM 用 PORT / EXPORT / IMP 三种端口在 component 之间建立受控的 transaction 通道,取代了全局变量、
public变量、config_object等笨拙且易出错的方案,并天然支持阻塞 / 非阻塞语义
- 操作类型 :TLM 定义了
put、get、transport三类操作,分别对应数据流 A→B、B→A、双向 request-response;PORT/EXPORT 体现的是"控制流"而非"数据流",发起者永远是 PORT
- 端口互连规则 :按控制流优先级 PORT > EXPORT > IMP,只有 IMP 能作为连接的终点 。
uvm_analysis_imp、uvm_analysis_export等虽名为 IMP/EXPORT,但本质是 IMP。连接既支持一对一,也支持带层次的 PORT↔PORT、EXPORT↔EXPORT 级联
- analysis 端口 :
analysis_port/analysis_export是广播式的一对多通信,只有write一种操作,无阻塞/非阻塞概念,对应 IMP 必须是uvm_analysis_imp
- 多 IMP 场景 :一个 component 内有多个
analysis_imp时,用宏uvm_analysis_imp_decl声明不同后缀(如write_monitor/write_model)来区分不同来源的数据处理函数
- FIFO 通信 :
uvm_tlm_analysis_fifo/uvm_tlm_fifo本质是"缓存 + 两个 IMP",让scoreboard能主动get、按自己节奏工作,并隐藏了 IMP 细节;其analysis_export、blocking_get_export等名字带export实为 IMP。FIFO 还提供used/is_empty/is_full/size/flush等调试接口,以及put_ap/get_ap两个内建analysis_port
- FIFO vs IMP :少量连接时用 IMP 更直接;当存在端口数组(如 16 路)时,FIFO +
for循环能大幅消除 IMP 方式的代码冗余,二者可按习惯取舍














