7、UVM机制(二)
7、UVM机制(二)
7.1 sequence机制
控制和产生一系列的事务,并将事务发送给driver的一套机制。
sequence机制的作用
控制事务:控制何时产生事务、何时发送事务。
产生事务。
将事务发送给driver。
sequence机制是会消耗仿真时间的,所以需要在task phase中使用,而不能在function phase中使用。
sequence机制工作过程
- 由driver向sequencer发送事务请求
- sequencer将请求转发给关联的sequence
- sequence将产生的transaction发送到sequencer
- sequencer发送给driver
- driver收到transaction并完成数据处理(例如用transaction驱动DUT,或者显示等)之后,产生完成的响应标志,发送给sequencer
- sequencer将完成标志转发给sequence
- sequence产生下一个事务,并等待下一个事务请求。
UVM使用sequence机制的原因
- 将事务的产生和驱动分离开来,提高验证平台的可重用性。例如,driver可以不再关注事务的产生,而专注于驱动DUT,修改驱动数据不需要直接修改driver,提高了driver的重用性。
- 通过挂载不同的sequence,平台就可以实现各种不同的验证数据的使用。
UVM sequence机制的原理
原理基本上和工作过程一致,不过有几点要说明:
- driver和sequencer之间有一个fifo,这个fifo的输入从sequencer来,放入的是sequence产生的transaction。
- sequencer得到driver的完成标志后,需要传给sequence表示完成。
- 无论如何,driver都不会直接和sequence通信,必须经过sequencer。
- test中,需要配置default_sequence项目。如果没有定义default_sequence,UVM会去检查其他组件是否手动实例化了sequence并调用了它的start方法,如果没有,就说phase中没有sequence执行。
UVM sequence机制实例
driver中的相关代码
class my_driver extends uvm_driver#(my_transaction); `uvm_component_utils(my_driver) function new(string name="", uvm_component parent); super.new(name, parent); endfunction // driver的主要任务都是run_phase中完成的 virtual task run_phase(uvm_phase phase); logic [7:0] temp; repeat(15)@(m_vif.driver_cb); // 重点在这里,这条语句就是向sequencer发送事务请求,这是一条阻塞语句。 seq_item_port.get_next_item(req); // 完成后,调用这个语句表示给出事务已经处理完成 seq_item_port.item_done(); endtask endclass
sequencer
sequencer的功能不简单,但它的处理方式非常重复,所以UVM已经把它封装在内部,不需要管里面的内容。FIFO等东西都有。下面这段就是之前的sequencer代码:
typedef uvm_sequencer#(my_transaction) my_seqr
sequence
sequence的主要功能在body方法中完成。
class my_sequence extends uvm_sequence#(my_transaction);
`uvm_object_utils(my_sequence)
function new(string name="");
super.new(name);
endfunction
virtual task body();
repeat(15) begin
// 用这个语句实现sequence机制,它包括产生事务、等待返回完成标志的动作。
`uvm_do(req)
end
endtask
endclass
分解uvm_do宏
uvm_do宏不止`uvm_do一个,它是一个系列。整个系列能完成不同的功能。
`uvm_do(SEQ_OR_ITEM) // 这句不止可以产生transaction,也能产生sequence
`uvm_do_with(SEQ_OR_ITEM, CONSTRAINTS)// 后面的constraint是约束,用于在transaction之外指定其随机化的范围。
`uvm_do_on(SEQ_OR_ITEM, SEQR)// 为transaction或sequence指定其关联的sequencer
`uvm_do_on_with(SEQ_OR_ITEM, SEQR, CONSTRAINTS)// 是以上两个宏的集合体
首先,根据传入类型决定要创建的是sequence还是transaction,然后为对象做设置,例如它对应的sequencer。然后需要考虑:
- 如果传入的是transaction(即sequence_item类型)就要调用对象的start_item()语句,并随机化item。如果同时传入的还有约束,需要一起考虑。最后调用finish_item()。
- 如果传入的是sequence,则调用start()启动传入的sequence。
绕开uvm_do宏产生sequence_item
在sequence代码中,不使用uvm_do宏,而是自己生成transaction也是可行的。只要根据上面的描述去定制产生过程就行。在sequence代码中的body任务中:
// 只列出关键代码
virtual task body();
// 需要声明创建对象的句柄
my_transaction tr;
// 不再使用uvm_do宏
// `uvm_do(req)
// 手动创建transaction对象,使用factory机制
tr=my_transaction::type_id::create("tr");
// 调用start_item方法并随机化,注意,start_item()是需要参数的,参数为产生对象的句柄
start_item(tr);
tr.randomize();
// 结束产生
finish_item(tr);
endtask
尽管可以自己去创建sequence_item对象,也就是transaction,但通常情况下直接根据需要直接用uvm_do宏完成就行。
启动sequence的方法
在sequencer中自动启用
如果用uvm_do宏,那么只要配置sequencer中的default_sequence,sequencer就会自动调用sequence的start()方法。
在test中有这样一段语句:
uvm_config_db#(uvm_object_wrapper)::set(
this, "*.m_seqr.run_phase", "default_sequence", my_sequence::get_type()
);
这就是在test中指定default_sequence。
用其他平台组件手动启动sequence
例如这里选择在test中启动sequence,这里忽略和以前相同的代码。
class my_test extends uvm_test;
// 在run_phase中手动启动sequence
virtual task run_phase(uvm_phase phase);
// 声明句柄并用factory机制实例化sequence,注意不是sequencer。sequence是一个object,所以不需要传入父对象
my_sequence m_seq;
m_seq = my_sequencer::type_id::create("m_seq");
// 因为这里只展示了一个run_phase,所以加上这句
phase.raise_objection(this);
// 启动时,需要为这个sequence绑定关联的sequencer。
m_seq.start(m_env.m_agent.m_seqr);
phase.drop_objection(this);
endtask
endclass
第二种方法能对sequence进行更精确的控制,是工程中更常用的方法。
sequence的嵌套
一个sequence是可以包含多个子sequence的,其实从uvm_do_on这个宏可以传入的参数中就可以看出来。
假设现在有三个sequence需要启动,包括:
- 用于重置的reset_sequence,其句柄为t_seq
- 用于写的write_sequence,句柄为w_seq
- 用于读的read_sequence,句柄为r_seq
那么,创建一个顶层sequence类,命名为top_sequence,使用uvm_do启动这三个子sequence
// 在top_sequence中先声明句柄
reset_sequence t_seq;
write_sequence w_seq;
read_sequence r_seq;
// 在top_sequence的body中
virtual task body();
`uvm_do(t_seq)
`uvm_do(w_seq)
`uvm_do(r_seq)
endtask
sequence仲裁
一个sequencer同一时刻只能接收一个sequence发送的transaction。如果同时启动了多个sequence,则需要仲裁机制。
UVM提供了一些宏,为每一个sequence指定优先级。
`uvm_do_pri(SEQ_OR_ITEM, PRIORITY)
`uvm_do_pri_with(SEQ_OR_ITEM, PRIORITY, CONSTRAINTS)
`uvm_do_on_pri(SEQ_OR_ITEM, SEQR, PRIORITY)
如何同时启动多个sequence?只要在top_sequence的body里,用fork join语句包围需要同时启动的uvm_do就行。但在这之前,一定要先指定仲裁算法:
// top_sequence的body
virtual task body();
// 这里的仲裁算法是权重算法,权重高的sequence优先执行。
//如果权重相同,则按代码顺序执行。如果不写权重,权重默认是100。
// 仲裁算法是对关联的sequencer的设置。
m_sequencer.set_arbitration(SEQ_ARB_STRICT_FIFO)
fork
`uvm_do_pri(t_seq, 1000)
`uvm_do_pri(w_seq, 50)
`uvm_do_pri(r_seq) // 不写默认是100
join
endtask
除了这里的SEQ_ARB_STRICT_FIFO之外还有些其他的算法:
// 默认仲裁算法,优先执行先获得的sequence,忽略权重
SEQ_ARB_FIFO
// 优先执行高权重的
SEQ_ARB_WEIGHTED
// 随机执行sequence
SEQ_ARB_RANDOM
// 先按照权重值分类,优先执行高权重的sequence,也就是上面代码用的算法
SEQ_ARB_STRICT_FIFO
// 先按照权重值分类,具有高优先级的sequence随机执行
SEQ_ARB_STRICT_RANDOM
// 自定义仲裁算法。这需要用户扩展uvm_sequencer并重载user_priority_arbitration()方法。
SEQ_ARB_USER
实际应用里,需要并行执行sequence的情况不多。
如果每个sequence各对应一个sequencer,而不是用一个sequencer管理多个sequence的时候,就不需要仲裁。
sequence获取响应
如果打算把driver的返回响应保存到本地或是其他操作,可以用sequence响应方法获取。
driver处理完事务后,不会立即给sequencer发送完成标志,而是先发送一个响应,这个响应会先被转发给sequence。注意,完成标志和响应是两种不同的东西。
sequence中的代码增加
在调用完finish_item(tr);后,只需要添加上get_response(rsp);
就可以获取响应了。
通过rsp句柄引用返回的响应,打印一下看看
virtual task body();
get_response(rsp);
`uvm_info("SEQ", {"\n", "Sequence get the response:\n", rsp.sprint(), }, UVM_MEDIUM)
endtask
driver的行为改变
在完成驱动或其他什么事情之后,给sequencer发送完成标志之前,需要产生响应,所以代码就放在两者之间。
在driver的主要行为块run_phase中:
virtual task run_phase(uvm_phase phase);
// 声明响应的句柄
my_transaction rsp;
//其他代码
// 获取事务句柄
seq_item_port.get_next_item(req);
// 用factory例化,并产生响应
rsp = my_transaction::type_id::create("rsp");
$cast(rsp, req.clone());
// 将响应与对应事务相关联。响应和事务应该一一对应。
rsp.set_id_info(req);
// 发送响应
seq_item_port.put_response(rsp);
// 返回完成标志
seq_item_port.item_done();
endtask
7.2 TLM 1.0机制
什么是TLM机制
Transaction Level Modeling,缩写为TLM,这是事务级模型的英文全称。这里再展示一次组件之间的连接图,可以看到有analysis port和port等端口,这些组件之间的端口就是TLM端口。
这里需要说明的是,driver和sequencer之间的通信虽然没有表明,但也是用TLM实现的。
为什么要使用TLM机制
建立专门的通信信道,避免通信出现混乱。各个通信信道之间是相互独立的。
有些语言需要通过全局变量实现模块间的通信,这很容易出问题。
SystemVerilog自带的通信方法有mailbox(信箱)、semaphore(旗语)、event(事件)这几种。但它们的数量多起来,或者出现冲突的时候,需要有效地管理才不会乱。
UVM TLM机制原理
1. 端对端
这种情况下, 平台组件分为producer和consumer。
producer需要创建一个port类型的端口,consumer需要创建一个import类型的端口,数据将会从port端口传向import端口。
在TLM中,port总是通信的主动发起方,import总是接收方。
put模式/发送模式:把port端口定义在producer那边
由带有port的producer主动发起发送行为。
// producer中需要发送,tx表示需要发送的数据的句柄,一般是transaction的对象。
// 但其实这个put任务是在consumer中定义的,而不是producer中。
port.put(tx);
// consumer中需要定义put任务,这是个用户定义的任务。
task put(tx);
xxx
endtask
producer中调用put时,调用的其实是consumer中的put任务。
get模式/获取模式:把port定义在consumer一方
由带有port的consumer主动发起发送。这种情况下, producer产生事务后,不会主动发送数据给consumer,而是等待consumer发起请求。
和put模式相反,consumer中的get任务是在producer中定义的。
// consumer中
port.get(tx);
// producer中
task get(tx);
xxx
endtask
consumer中调用get时,调用的其实是producer中的get任务。
这两种通信模式都有一个特点:port的所属方其实不知道对方的工作情况:
在put模式下,producer向consumer发送事务时,并不知道consumer是否正在请求数据。
在get模式下,consumer向producer请求事务时,并不知道producer是否已经产生了数据。
FIFO模式:在producer和consumer中间加入FIFO作为缓冲
这样,producer和consumer都成了主动方,也能解决get模式和put模式互不知情的问题。而FIFO则为被动方。
在producer中使用的put任务和在consumer中使用的get任务都在FIFO里定义就行。
2. 一对多(write模式)
由一个producer产生事务,发送给多个consumer。这种情况下,只能由producer主动发起事务发送,发送端口要使用analysis_port,而不是简单的port;在接收端,需要analysis_import。
发送时,由producer调用write任务,把事务发送给各个consumer。而这个write任务是定义在各个consumer中的。这个write任务需要用户自己定义。
// 在producer中
analysis_port.write(tx)
// 在consumer中
task write(tx);
xxx
endtask
常用的TLM端口
常用的TLM端口有三种:port、import、export
port
port也分为几种:uvm_put_port、uvm_blocking_put_port、uvm_nonblocking_put_port、uvm_get_port、uvm_blocking_get_port、uvm_nonblocking_get_port。
- uvm_put_port需要用到的语句:
uvm_put_port #(T); // 需要传入port传输的的transaction类型
与它相关联的任务/函数有三个:
// put是一个任务,它会阻塞并等待事务传输成功。
put()
// try_put是一个函数,它不阻塞进程,不论传输成功与否都会直接返回
try_put()
// can_put是一个函数,不用于数据传输,而是用来查看接收方是否准备好接收事务
can_put()
- uvm_blocking_put_port的语句:
uvm_blocking_put_port #(T);
和uvm_put_port不同,uvm_blocking_put_port只支持put任务,就是会阻塞等待的那个任务。
- uvm_nonblocking_put_port的语句:
uvm_nonblocking_put_port #(T);
和uvm_blocking_put_port正好相反,只支持不会阻塞的两个函数,即try_put和can_put
- uvm_get_port语句
uvm_get_port #(T)
与之关联的有三个语句,和put是非常类似的,功能也对应。
// get是一个任务,调用时会阻塞,直到传输成功
get()
// try_get是一个函数,不阻塞进程,不论传输是否成功都会立即返回
try_get()
// can_get是一个函数,仅用于确认是否可以传输
can_get()
- uvm_blocking_get_port语句
uvm_blocking_get_port#(T);
仅能使用get()这个阻塞语句。
- uvm_nonblocking_get_port语句
uvm_nonblocking_get_port#(T);
仅能使用try_get()和can_get()这些非阻塞语句。
六个任务或函数都需要用户重载后才能使用,否则会出错。
put();
try_put();
can_put();
get();
try_get();
can_get();
import
要完成TLM之间的通信,port必须与对应的import相连,下表是port和import的对应关系。另外需要说明:每一个port必须对应一个import。
port | import |
---|---|
uvm_put_port#(T); | uvm_put_imp#(T, IMP); |
uvm_blocking_put_port#(T); | uvm_blocking_put_imp#(T, IMP); |
uvm_nonblocking_put_port#(T); | uvm_nonblocking_put_imp#(T, IMP); |
uvm_get_port#(T); | uvm_get_imp#(T, IMP); |
uvm_blocking_get_port#(T); | uvm_blocking_get_imp#(T, IMP); |
uvm_nonblocking_get_port#(T); | uvm_nonblocking_get_imp#(T, IMP); |
相比port而言,import多了一个名为IMP的参数。这个参数是指import所在组件的指针。而put、try_put、can_put等任务、函数也需要在这个组件中重载。这些方法虽然在port中调用,但定义是在import中实现的。
task put、try_put等方法的源码:
virtual task put(input T t);
`uvm_report_error("put", `UVM_TASK_ERROR, UVM_NONE)
endtask
// 返回一个1位的值来表示是否传输成功
virtual function bit try_put(input T t);
`uvm_report_error("try_put", `UVM_TASK_ERROR, UVM_NONE);
return 0;
endfunction
// 无参数,不能传递信息,返回一个1位的值,表示目标组件是否可以接收数据
virtual function bit can_put();
`uvm_report_error("can_put", `UVM_TASK_ERROR, UVM_NONE);
return 0;
endfunction
virtual task get(output T t);
`uvm_report_error("get", `UVM_TASK_ERROR, UVM_NONE)
endtask
virtual function bit try_get(output T t);
`uvm_report_error("try_get", `UVM_TASK_ERROR, UVM_NONE);
return 0;
endfunction
virtual function bit can_get();
`uvm_report_error("can_get", `UVM_TASK_ERROR, UVM_NONE);
return 0;
endfunction
如果不重载直接调用,不论调用哪个,显然会直接报错,而且只会报错。
export
和port的功能、用法其实是一样的。
uvm_put_export#(T);
uvm_blocking_put_export#(T);
uvm_nonblocking_put_export#(T);
uvm_get_export#(T);
uvm_blocking_get_export#(T);
uvm_nonblocking_get_export#(T);
一般用于中间组件。
组件连接
组件A和组件B的直接连接:
组件A中有port端口,组件B为import端口,连接起两个端口只需要语句:
port.connect(imp);
组件A、组件B中间有一个中间组件,即组件C的情况:
A组件有port端口、组件B有import端口,组件C有export端口。
要将A端口的port与export连接起来:
port.connect(export);
将B端口的import与export连接:
export.connect(imp);
在sequencer的源码中就有这样一个TLM通信端口,名字正是seq_item_export。在之前的driver代码中可以看到,driver调用了seq_item_port端口的方法get_next_item,以及item_done()方法。前面也说到,driver和seqencer之间正是由TLM通信连接的。在agent的connect_phase中,需要把它们连接起来,当时用的代码如下:
virtual function void connect_phase(uvm_phase phase);
if(UVM_IS_ACTIVE)
// 如果是active模式,就要这条连接
m_driv.seq_item_port.connect(m_seqr.seq_item_export);
endfunction
几种TLM端口的使用
put模式
根据put、get和fifo模式去决定要用哪种端口比较合适。
这里先说put模式。如上文所说,put模式下,由producer主动发起一次数据传输,交给consumer。这里以master agent中的monitor发送数据给reference model为例。因为需要跨过一个master agent,所以不论如何都要在master agent中定义一个export,作为中转端口。当然,也可以用接口传递的方式,monitor里的端口发送给agent,然后连接到reference model上,而不是连接到agent上,再通过agent发送出去。这里采用前者。
// reference model和其他组件不同,需要直接从uvm_component扩展。
class my_reference_model extends uvm_component;
`uvm_component_utils(my_reference_model)
// 定义一个端口,指定传输事务的类型和import所在的类
uvm_blocking_put_imp #(my_transaction, my_reference_model) i_m2r_imp;
// 在构造函数中,实例化端口
function new(string name="", uvm_component parent);
super.new(name, parent);
// 第二个参数是该接口所在的组件,这里肯定是写this
this.i_m2r_imp = new("i_m2r_imp", this);
endfunction
// 重载put任务,put模式需要在consumer中重载,这里重载为调用put后只是打印一下transaction。
// 根据这个task的参数,可以用tr为句柄,引用发送过来的事务。
// 这个put会在producer中被调用,也就是Monitor。
// 注意,它并不是一个虚方法。
task put(my_transaction tr);
`uvm_info("REF_REPORT", {"\n", "master agent have been sent a transaction: \n", tr.sprint()}, UVM_MEDIUM)
endtask
endclass
class my_monitor extends uvm_monitor;
`uvm_component_utils(my_monitor)
// 定义一个端口
uvm_blocking_put_port #(my_transaction) m2r_port;
function new(string name="", uvm_component parent);
super.new(name, parent);
// 在这里初始化
m2r_port = new("m2r_port", this);
endfunction
virtual task run_phase(uvm_phase phase);
// 前面一段代码需要先获取DUT端口上的数据,打包成tr对象,这里先省略。
`uvm_info("Monitor", "Now monitor send the transaction to the reference model!", UVM_MEDIUM)
// put在monitor里调用,把打包好的tr发送给reference model。
this.m2r_port.put(tr);
endfunction
endclass
// monitor和reference中间还隔着一层agent,所以需要定义一个export作为中转。当然,也可以不定义,用其他方法。这里采用定义export的方法。
class master_agent extends uvm_agent;
`uvm_component_utils(master_agent)
// export端口定义
uvm_blocking_put_export #(my_transaction) m_a2r_export;
function new(string name="", uvm_component parent);
super.new(name, parent);
a2r_export = new("m_a2r_export", this);
endfunction
// 在connect里连接
virtual function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
// 必须由port来发起这次连接。此处是agent,所以找到本实例中的monitor对象就可以发起连接。
m_moni.m2r_port.connect(this.m_a2r_export);
endfunction
endclass
在env中,需要实例化reference model,同样是用factory机制。
并且,需要在env中完成export和import的连接。
class my_env extends uvm_env;
`uvm_component_utils(my_env)
// 声明reference句柄
my_reference_model ref_model;
function new(string name="", uvm_component parent);
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
ref_model = my_reference_model::type_id::create("ref_model", this);
endfunction
// 同样要在connect_phase里完成连接
virtual function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
// import是被动接收方,所以需要由export来主动发起连接请求
m_agent.m_a2r_export.connect(ref_model.i_m2r_imp);
endfunction
endclass
get模式
和put模式不同,get模式由consumer主动发起数据请求。而producer不知道何时会发来请求,所以需要在producer中创建一个数组作为FIFO,暂存数据。这个FIFO不能算作是FIFO模式的标志。
get模式与put模式比较重复,所以不再演示,但后文将会演示FIFO模式。
class my_reference_model extends uvm_component;
`uvm_component_utils(my_reference_model)
// 声明port端口
uvm_blocking_get_port #(my_transaction) i_m2r_port;
function new(string name="", uvm_component parent);
super.new(name, parent);
// 在构造函数里例化端口。
// 提问:这里为什么要强调this?
this.i_m2r_port = new("i_m2r_port", this);
endfunction
// 作为主动发起方,reference需要不断调用get任务,用forever循环实现
virtual function void run_phase(uvm_phase phase);
`uvm_info("REF_MODEL_RUN", "Reference model running!", UVM_MEDIUM)
// forever循环
forever begin
i_m2r_port.get(item);
`uvm_info("REF_REPORT", {"\n", "master agent have been sent a transaction: \n", item.sprint()}, UVM_MEDIUM)
end
endfunction
endclass
class my_monitor extends uvm_monitor;
`uvm_component_utils(my_monitor)
// 声明import端口,import端口需要声明所在的组件
uvm_blocking_get_imp #(my_transaction, my_monitor) m2r_imp;
// 需要声明一个队列作为FIFO,储存生成后未被使用的事务,类型就是my_transaction
my_transaction tr_fifo[$];
function new(string name="", uvm_component parent);
super.name(name, parent);
m2r_imp = new("m2r_imp", this);
endfunction
virtual task run_phase(uvm_phase phase);
// 前面加入其他的任务
// 从DUT读出事务后,把数据打包,然后放入fifo中
tr_fifo.push_back(tr);
endtask
// import所在的模块需要重载get方法,它会在port所在的组件中被调用。
// 注意,它并不是一个虚方法。
task get(output my_transaction s_tr);
// 如果FIFO为空,等待一个时钟周期,反复等待直到FIFO中存在事务
while(tr_fifo.size()==0) @(m_vif.imonitor_cb);
// 把FIFO最靠前的事务取出
s_tr = tr_fifo.pop_front();
`uvm_info("Monitor", {"\n", "Now monitor send the transaction to the reference model: \n", s_tr.sprint()}, UVM_MEDIUM)
endtask
endclass
class master_agent extends uvm_agent;
`uvm_component_utils(master_agent)
// 声明中转的export端口,这个export在env中和reference连接,
// 和monitor的连接就在本类中进行。
uvm_blocking_get_export #(my_transaction) m_a2r_export;
function new(string name="", uvm_component parent);
super.new(name, parent);
m_a2r_export = new("m_a2r_export", this);
endfunction
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
ref_model = my_reference_model::type_id::create("ref_model", this);
endfunction
virtual function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
this.m_a2r_export.connect(m_moni.m2r_imp);
endfunction
endclass
class my_env extends uvm_env;
`uvm_component_utils(my_env)
my_reference_model ref_model;
my_agent m_agent;
function new(string name="", uvm_component parent);
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
ref_model = my_reference_model::type_id::create("ref_model", this);
m_agent = my_agent::type_id::create("ref_model", this);
endfunction
virtual function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
ref_model.i_m2r_port.connect(m_agent.m_a2r_export);
endfunction
endclass
FIFO模式
FIFO模式会让monitor和reference model都变成主动发起数据传输方,而把FIFO本身作为被动方。所以,在monitor和reference model上都是用port。不同的是,monitor上用的是put port,reference model上用的是get port。
这里用到的FIFO已经在UVM中定义好了,不需要自己定义。类似于sequencer已经在UVM中定义好了那样。只需要传入数据类型作为参数就可以。
同时,FIFO中也已经定义了多种端口,只需要直接连接就行。这里用到的是FIFO上的blocking put export和blocking get export。
至于get和put方法,也是已经定义好的。使用FIFO模式就有这些好处,很多东西不需要自己写,开箱即用。
class reference_model extends uvm_component;
`uvm_component_utils(reference_model)
// reference model需要get port
uvm_blocking_get_port #(my_transaction) i_m2r_port;
// 用item句柄储存发送来的事务
my_transaction item;
function new(string name="", uvm_component parent);
super.new(name, parent);
this.i_m2r_port = new("i_m2r_port", this);
endfunction
virtual task run_phase(uvm_phase phase);
`uvm_info("REF_REPORT", {"\n", "master agent have been sent a transaction: \n", item.sprint()}, UVM_MEDIUM)
// 不断调用get任务
forever begin
i_m2r_port.get(item);
`uvm_info("REF_REPORT", {"\n", "master agent have been sent a transaction: \n", item.sprint()}, UVM_MEDIUM)
end
endtask
endclass
env中,需要添加FIFO。这个FIFO和agent、reference model平级。
class my_env extends uvm_env;
`uvm_component_utils(my_env)
my_reference_model ref_model;
master_agent m_agent;
// 声明FIFO句柄,并指定存储数据的类型
uvm_tlm_analysis_fifo #(my_transaction) magt2ref_fifo;
function new(string name="", uvm_component parent);
super.new(name, parent);
// FIFO需要在构造函数中实例化,而不是build phase。
// FIFO本身是一个component,而不是object,所以需要传入父对象的类,这里写this就可以
magt2ref_fifo = new("magt2ref_fifo", this);
endfunction
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
ref_model = my_reference_model::type_id::create("ref_model", this);
m_agent = master_agent::type_id::create("m_agent", this);
endfunction
virtual function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
// 在connect phase中连接agent和FIFO、reference和FIFO。FIFO所拥有的端口类型是已经写好的。
`super.connect_phase(phase);
// monitor还是需要通过agent传出事务来,所以保留agent中的export,将它连接到FIFO的export上。
`uvm_info("ENV", "Connect the agent to FIFO", UVM_MEDIUM)
m_agent.m_a2r_export.connect(this.magt2ref.blocking_put_export);
`uvm_info("ENV", "Connect the reference model to FIFO")
ref_model.i_m2r_port.connect(this.magt2ref.blocking_get_export);
endfunction
endclass
常用的特殊UVM TLM端口
示例代码
持续更新中