[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ofH8i8OK-1635320932444)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20211015201126595.png)]

Monitor

UVM组件之间的通信(方法原理)

要实现上述组件之间的通信目标,那么需要完成以下三点:
(1)数据的发送,它有三个步骤
第一步,在要发送数据的组件里,即monitor里声明uvm_analysis_port端口(简称ap端口),uvm_analysis_port是一个参数化的类,其参数就是这个ap端口需要传递的数据的类型,在本节中是command_transaction或result_transaction,即传送运算的指令和结果

第二步,声明了ap端口后,在需要发送数据的组件里,即在monitor的build_phase中将其实例化。

第三步,在需要发送数据的组件里,即在monitor的run_phase中收集完监测的transaction数据后,调用ap端口的write()方法将其写入ap端口。write()方法是uvm_analysis_port的一个内建函数。

(2)数据的接收,同样有三个步骤:

第一步,在要接收数据的组件里,即这里的scoreboard或coverage里声明uvm_blocking_get_port端口。同样也是一个参数化的类,其参数也是这个端口需要传递的数据类型,在本节中也是command_transaction或result_transaction。

第二步,声明了端口后,在需要接收数据的组件里,即scoreboard或coverage里的build_phase中将其实例化。

第三步,在需要接收数据的组件里,即在scoreboard或coverage里的run_phase中调用该端口的get()方法来获取发送组件的数据,即本例的monitor发送的transaction数据。

(3)利用fifo对数据发送和接收的端口进行连接,同样也有三个步骤:

在完成上面两点之后,通信的功能并没有实现,还需要在上一层组件,即本例的env环境中使用fifo将两个端口连接在一起。
第一步,在env中声明一个uvm_tlm_analysis_fifo。同样也是一个参数化的类,其参数也是这个端口需要传递的数据类型,在本节中也是command_transaction或result_transaction。

第二步,在env的build_phase中将其实例化。

第三步,在env的connect_phase里将fifo分别与monitor中的ap端口和scoreboard或coverage中的blocking_get_port端口进行连接。

question

为什么这里需要一个fifo呢?不能直接把monitor中的ap端口和分析组件里的blocking_get_port相连吗?
由于ap端口是非阻塞性质的,ap.write()函数调用完成后马上返回,不会等待数据被接收。假如当write函数调用时,blocking_get_port端口正在忙于其他事情,而没有准备好接收新的数据时,此时被write函数写入的transaction就需要一个暂存的位置,因此这里需要用到fifo。

Monitor代码实现

首先我们在BFM里声明本节的主角,即command_monitor和result_monitor

//文件路径:ch13/13.4/sim/testbench/tinyalu_bfm.sv
interface tinyalu_bfm;
import tinyalu_pkg::*;
command_monitor command_monitor_h;
result_monitor result_monitor_h;
...
endinterface : tinyalu_bfm

然后分别在这两个monitor的build_phase里将monitor自己传递给BFM,从而使得BFM可以调用monitor里的方法:

//文件路径:ch13/13.4/sim/testbench/tb_classes/command_monitor.svh
function void build_phase(uvm_phase phase);
if(!uvm_config_db #(virtual tinyalu_bfm)::get(null, "*","bfm", bfm))
`uvm_fatal("COMMAND MONITOR", "Failed to get BFM")
bfm.command_monitor_h = this;//或者 bfm.result_monitor_h = this;
...
endfunction : build_phase

这里首先从UVM 配置数据库里得到 BFM 的句柄 , 然后把自己的句柄 this 拷贝到BFM 的句柄变量里 ,那么BFM 就可以通过command_monitor_h或result_monitor句柄向验证平台传送数据了 。

然后我们来看BFM里是如何通过这俩个句柄变量来向验证平台传送数据的。

//文件路径:ch13/13.4/sim/testbench/tinyalu_bfm.sv
interface tinyalu_bfm;
...
always @(posedge clk) begin : cmd_monitor
bit new_command;if (!start)
new_command = 1;
else
if (new_command) begin
command_monitor_h.write_to_monitor(A, B, op);
new_command = 0;
end
end : cmd_monitor
always @(negedge reset_n) begin : rst_monitor
if (command_monitor_h != null) //guard against VCS time 0 negedge
command_monitor_h.write_to_monitor(A, B, rst_op);
end : rst_monitor
always @(posedge clk) begin : rslt_monitor
if (done)
result_monitor_h.write_to_monitor(result);
end : rslt_monitor
endinterface : tinyalu_bfm

可以看到它是通过always块来实现对DUT的指令和结果,即输入和输出进行监测的。

对于指令,即输入的监测过程是,监测时钟上升沿,判断是否为新的指令,由于前面我们介绍DUT时序功能时讲过,start在运算的整个过程
中保持为高,因此当start被拉低时,new_command被置为1,然后当指令开始运算时,start被拉高,此时new_command刚好为1,此时command_monitor将总线上操作数A和B以及指令op采样并调用command_monitor的**write_to_monitor()**方法将采样监测的数据发送给对应的command_monitor,数据发送完成后,new_command被置0,按照DUT的波形图,此时done运算完成信号被拉高一个周期,即指令运算完成,start和done都被拉低,然后总线继续监测下一个新的指令并发送给monitor。

对于输出的监测中,监测时钟上升沿,判断运算完成信号done是否为高,如果是,则采样运算结果result并调用result_monitor的**write_to_monitor()**方法将采样监测的数据发送给对应的result_monitor。

最后我们利用之前介绍的UVM组件之间的通信的方法从而将转换后的transaction发送给分析组件coverage和scoreboard。

Scoreboard

Scoreboard代码实现

第一步,声明uvm_blocking_get_port端口。由于scoreboard需要比较运算的期望值和DUT实际运算出的值,因此这里需要定了两个端口,其中一个端口用来接收输入的指令,用来在scoreboard里调用预测器(参考模型)来计算出期望值,另一个端口用来接收DUT实际运算出来的值。

uvm_blocking_get_port #(command_transaction) cmd_port;
uvm_blocking_get_port #(result_transaction) result_port;

第二步,声明了端口后,在build_phase中将其实例化。

virtual function void build_phase(uvm_phase phase);
...
cmd_port = new("cmd_port", this);
result_port = new("result_port", this);
endfunction

第三步,在run_phase中调用该端口的get()方法来获取发送数据的组件,即本例的monitor发送的transaction数据。

fork
forever begin
cmd_port.get(cmd);
...
end
forever begin
result_port.get(act_result);
...
end
join

在这里的run_phase中我们建立了两个线程,其中一个线程用来接收指令cmd,并调用predict_result()方法计算出期望值,然后调用push_pack()方法将期望值写入队列exp_queue,另一个线程用来接收DUT实际运算出来的结果。然后将队列exp_queue中的期望值取出,利用之前在transaction中定义的字符串转换函数covert2string()将指令,期望结果和实际结果转换成字符串,最后调用transaction定义的compare()方法对期望值和实际的结果进行比较并打印比较的结果

Coverage

类似于scoreboard

env

env代码实现1

我们利用fifo对数据发送和接收的端口进行连接:

第一步,声明三个uvm_tlm_analysis_fifo。前两个fifo分别用于连接command_monitor和coverage以及command_monitor和scoreboard的通信端口。第三个fifo用于连接result_monitor和scoreboard。

uvm_tlm_analysis_fifo #(command_transaction) command_mon_cov_fifo;
uvm_tlm_analysis_fifo #(command_transaction) command_mon_scb_fifo;
uvm_tlm_analysis_fifo #(result_transaction) result_mon_scb_fifo;

第二步,在build_phase中将其实例化。

virtual function void build_phase(uvm_phase phase);
...
command_mon_cov_fifo = new("command_mon_cov_fifo",this);
command_mon_scb_fifo = new("command_mon_scb_fifo",this);
result_mon_scb_fifo = new("result_mon_scb_fifo",this);
endfunction

第三步,在connect_phase中将fifo分别与monitor中的analysis_port和scoreboard或coverage中的blocking_get_port进行连接。

function void connect_phase(uvm_phase phase);
command_monitor_h.ap.connect(command_mon_cov_fifo.analysis_export);
coverage_h.cmd_port.connect(command_mon_cov_fifo.blocking_get_export);
command_monitor_h.ap.connect(command_mon_scb_fifo.analysis_export);
scoreboard_h.cmd_port.connect(command_mon_scb_fifo.blocking_get_export);
result_monitor_h.ap.connect(result_mon_scb_fifo.analysis_export);
scoreboard_h.result_port.connect(result_mon_scb_fifo.blocking_get_export);
endfunction : connect_phase

env代码实现2

加入sequence后需要做:

(1)例化sequencer和driver。
(2)连接sequencer和driver的两个端口,从而建立起前面说的“通道”连接,用于传送sequence_item。

function void connect_phase(uvm_phase phase);
//connect driver & sequencer
driver_h.seq_item_port.connect(sequencer_h.seq_item_export);····

env代码实现3

加入agent之后

//文件路径:ch15/15.2/sim/testbench/tb_classes/component/env.svh
class env extends uvm_env;
`uvm_component_utils(env)
agent agent_h;
coverage coverage_h;
scoreboard scoreboard_h;
uvm_tlm_analysis_fifo #(sequence_item) command_mon_cov_fifo;
uvm_tlm_analysis_fifo #(sequence_item) command_mon_scb_fifo;
uvm_tlm_analysis_fifo #(result_transaction) result_mon_scb_fifo;
function void build_phase(uvm_phase phase);
agent_h = agent::type_id::create ("agent_h",this);
agent_h.is_active = UVM_ACTIVE;
//analysis
coverage_h = coverage::type_id::create ("coverage_h",this);
scoreboard_h = scoreboard::type_id::create("scoreboard_h",this);
//fifos
command_mon_cov_fifo = new("command_mon_cov_fifo",this);
command_mon_scb_fifo = new("command_mon_scb_fifo",this);
result_mon_scb_fifo = new("result_mon_scb_fifo",this);
endfunction : build_phase
function void connect_phase(uvm_phase phase);
//connect fifos
agent_h.cmd_ap.connect(command_mon_cov_fifo.analysis_export);
coverage_h.cmd_port.connect(command_mon_cov_fifo.blocking_get_export);
agent_h.cmd_ap.connect(command_mon_scb_fifo.analysis_export);
scoreboard_h.cmd_port.connect(command_mon_scb_fifo.blocking_get_export);
agent_h.result_ap.connect(result_mon_scb_fifo.analysis_export);
scoreboard_h.result_port.connect(result_mon_scb_fifo.blocking_get_export);
endfunction : connect_phase
function void end_of_elaboration_phase(uvm_phase phase);
scoreboard_h.set_report_verbosity_level_hier(UVM_MEDIUM);
agent_h.set_report_verbosity_level_hier(UVM_MEDIUM);
endfunction
function new (string name, uvm_component parent);
super.new(name,parent);
endfunction : new
endclass

env代码实现4

加入adapter后

//文件路径:ch24/24.7/sim/testbench/tb_classes/component/env.svh
class env extends uvm_env;
`uvm_component_utils(env)agent agent_h;
coverage coverage_h;
scoreboard scoreboard_h;
bus_agent bus_agent_h;
reg_model reg_model_h;
adapter adapter_h;
uvm_tlm_analysis_fifo #(sequence_item) command_mon_cov_fifo;
uvm_tlm_analysis_fifo #(sequence_item) command_mon_scb_fifo;
uvm_tlm_analysis_fifo #(result_transaction) result_mon_scb_fifo;
function void build_phase(uvm_phase phase);
...
reg_model_h = reg_model::type_id::create ("reg_model_h");
reg_model_h.configure();
reg_model_h.build();
reg_model_h.lock_model();
reg_model_h.reset();
adapter_h = adapter::type_id::create ("adapter_h");
endfunction : build_phase
function void connect_phase(uvm_phase phase);
...
reg_model_h.default_map.set_sequencer(bus_agent_h.sequencer_h, adapter_h);
reg_model_h.default_map.set_auto_predict(1);
endfunction : connect_phase
...
endclass

Sequence机制

Sequence 执行过程

首先squence 调用start_item()开启sequence_item的传送,然后driver调用get_next_item()来发起对sequence_item的获取,接着sequencer对sequence进行仲裁选择,此时sequence调用finish_item来等待driver调用 item_done ()。在等待的过程中drier已经获取到了sequence_item,这个时候driver会将事务级的transaction数据转换成信号级数据以符合BFM总线接口的要求,然后BFM将此激励发送给DUT,完成之后**driver调用item_done()**结束,此时sequence可以再次开启sequence_item的传送,如此循环直到将sequence里的
suquence_item都传送完。

Sequence代码实现

先加入sequence_item 其他的item可以继承sequence_item 。

一般uvm_object对象有生命周期。因为sequence_item和sequence本质上都是uvm_object,因此两者都有生命周期。只是sequence的生命周期比sequence_item要更长一些,其内的sequence_item全部发送完毕后,它的生命周期也就结束了。这就好比一个弹夹,其里面的子弹用完后就没有任何意义了。而uvm_component组件没有生命周期,只要仿真还没结束,那么将会一直存在,因为组件是构建UVM验证平台的基石,没有了组件,验证平台也就不存在了。

//文件路径:ch14/14.3/sim/testbench/tb_classes/sequence/random_sequence.svh
class random_sequence extends uvm_sequence #(sequence_item);
`uvm_object_utils(random_sequence)
function new(string name = "random_sequence");
super.new(name);
endfunction : new
task body();
sequence_item command;
repeat (1000) begin : random_loop
command = sequence_item::type_id::create("command");
start_item(command);
assert(command.randomize());
finish_item(command);
`uvm_info("RANDOM SEQ", $sformatf("random command: %s", command.convert2string), UVM_MEDIUM)
end : random_loop
endtask : body
endclass : random_sequence

sequence派生于uvm_sequence,是一个参数化的类,这里其参数是sequence_item,即此sequence要操作的transaction的类型。每一个sequence都有一个body任务,当一个sequence启动之后,会自动执行body中的代码。首先使用宏uvm_object_utils()将random_sequence注册到factory中,然后声明sequence_item类型的变量command。接着在body()任务里构造名为command的sequence_item,并调用 start_item() 来开启对command指令的传送,然后对该command做一些约束调整,这里只是做了随机化操作,然后调用finish_item() 方法**来等待 driver 调用 item_done (),**否则将一直阻塞在那里,等待完成后将command转换为字符串并打印报告,如此循环1000次。

总结:

第一步,构造声明要发送的sequence_item;
第二步,调用start_item()开启;
第三步,对sequence_item进行约束控制和调整;
第四步,调用finish_item()等待完成。

还可以用uvm_do宏来实现以上4个步骤

//文件路径:ch14/14.3/sim/testbench/tb_classes/sequence/random_sequence.svh
class random_sequence extends uvm_sequence #(sequence_item);
`uvm_object_utils(random_sequence)
function new(string name = "random_sequence");
super.new(name);
endfunction : new
task body();
sequence_item command;
repeat (1000) begin : random_loop
`uvm_do(command)
`uvm_info("RANDOM SEQ", $sformatf("random command: %s", command.convert2string), UVM_MEDIUM)
end : random_loop
endtask : body
endclass : random_sequence

类似地,也可以使用uvm_do_with宏来简化,它是uvm_do系列宏中的一个,用于在随机化时对指定字段做约束。

`uvm_do_with(command,{command.op==add_op;})

sequencer代码实现

加下来加入sequencer

sequencer从sequence里取出 sequence_item 然后传送给driver。
这里sequencer的定义非常简单,派生自uvm_sequencer,是UVM的组件之一,利用之前我们讲过的“四条家规”进行创建。但要注意uvm_sequencer是一个参数化的类,其参数是sequence_item,即此sequencer操作的transaction的类型。

//文件路径:ch14/14.3/sim/testbench/tb_classes/component/sequencer.svh
class sequencer extends uvm_sequencer #(sequence_item);
`uvm_component_utils(sequencer)
function new(string name,uvm_component parent);
super.new(name,parent);
endfunction
endclass

driver代码实现

driver派生自uvm_driver,是UVM的组件之一,同样利用之前我们讲过的“四条家规”进行创建。但要注意uvm_driver是一个参数化的类,其参数是sequence_item,即在定义driver时需要指明此driver要驱动的transaction的类型。这样定义的好处是可以直接使用uvm_driver中的某些预先定义好的成员变量,如uvm_driver中有成员变量req,它的类型就是传递给uvm_driver的参数,在这里就是要发送给DUT的激励sequence_item,可以直接使用req代替

那么sequencer是如何将sequence_item传送给driver的呢?
这个传送是通过一个“通道”来实现的。
首先在uvm_driver中有UVM预先定义好的成员变量seq_item_port,可以理解为发出请求的端口,即通过调用该端口的get_next_item()方法向sequencer请求sequence_item。
相对应的,在uvm_sequencer中则有UVM预先定义好的成员变量seq_item_export,可以理解为响应请求的端口,即过该端口来响应driver发出的请求,将driver想要获取的sequence_item传送过去。*
综上,这两者之间通过这两个端口建立了一个“通道”,通道中传递的transaction类型就是定义sequencer和driver时指定的transaction类型,即sequence_item。driver通过调用get_next_item()来获取一个新的sequence_item,然后通过BFM发送给(驱动到)DUT,驱动完成后调用item_done()通知sequencer刚刚发送过来的sequence_item已经被接收并驱动完成。

class driver extends uvm_driver #(sequence_item);`uvm_component_utils(driver)virtual tinyalu_bfm bfm;function void build_phase(uvm_phase phase);if(!uvm_config_db #(virtual tinyalu_bfm)::get(null, "*","bfm", bfm))`uvm_fatal("DRIVER", "Failed to get BFM")endfunction : build_phasetask run_phase(uvm_phase phase);forever begin : cmd_loopshortint unsigned result;seq_item_port.get_next_item(req);bfm.send_op(req.A, req.B, req.op, result);req.result = result;seq_item_port.item_done();end : cmd_loopendtask : run_phasefunction new (string name, uvm_component parent);super.new(name, parent);endfunction : new
endclass : driver

在driver的代码中使用了forever循环,因为driver只负责驱动transaction,而不负责产生,只要有transaction就驱动,所以必须做成一个无限循环的形式。
run_phase里调用了 seq_item_port 的 get_next_item() 方法,这个方法是阻塞性的 , 它从seq_item_port里取得 sequencer传送的req , 然后调用 BFM 中的 send_op 并返回结果然后把结果存回req对象里 , 从而把结果返回给之前的sequence,最后调用seq_item_port 对象的item_done() 方法来指示 sequencer 可以传送下一个sequence_item了。
其实除了get_next_item()方法之外,还可以使用try_next_item()。get_next_item是阻塞的,它会一直等到有新的transaction才会返回,而try_next_item()是非阻塞的,顾名思义,它会尝试着询问sequencer是否有新的transaction,如果有,则获取此transaction,否则就直接
返回。

hierarchical sequence代码实现

在之前我们讲sequence的代码实现的时候大家已经知道了以下这两点:
UVM在启动sequence之后会自动调用这个body() 任务,因此我们需要在该任务里对sequence_item进行约束控制。
sequence使用start_item()和finish_item() 这两个方法来完成向sequencer传送sequence_item。
现在补充一点,即UVM预先在sequence定义好了 m_sequencer 这个数据成员,其保存着sequence所对应的sequencer的句柄,我们可以在一个顶层的sequence里来使用它,从而完成多线程混合sequence的仿真,这样一来就可以实现千变万化的测试用例,这个顶层的sequence就被叫做“hierarchical sequence“。

//文件路径:ch14/14.3/sim/testbench/tb_classes/sequence/parallel_sequence.svh
class parallel_sequence extends uvm_sequence #(uvm_sequence_item);
`uvm_object_utils(parallel_sequence)
reset_sequence reset_seq;
random_sequence random_seq;
fibonacci_sequence fibonacci_seq;
function new(string name = "parallel_sequence");
super.new(name);
reset_seq = reset_sequence::type_id::create("reset_seq");
fibonacci_seq = fibonacci_sequence::type_id::create("fibonacci_seq");
random_seq = random_sequence::type_id::create("random_seq");
endfunction : new
task body();
reset_seq.start(m_sequencer);
fork
fibonacci_seq.start(m_sequencer);
random_seq.start(m_sequencer);
join
endtask : body

首先在这个顶层的sequence里例化声明reset_sequence、random_sequence和fibonacci_sequence,然后在**body()**任务里先启动reset_sequence,然后利用 fork…join 并行启动剩下的两个sequence,此时sequencer会进行仲裁,按照一定顺序发送激励给sequencer,即最终要么是fibonacci_sequence要么是random_sequence里面的sequence_item发送给DUT进行运算。这里使用到了m_sequencer,它指向的是该顶层sequence,即parallel_sequence所对应的sequencer,我们在其对应的test测试用例中完成m_sequencer的赋值。

test

test代码实现

一般情况下,会有一个基本的base_test用于对env和其他共同的component组件的例化。然后其他的test则继承该base_test并有针对性地配置env或者选择不同的sequence来实现不同的测试

//文件路径:ch14/14.3/sim/testbench/tb_classes/test/base_test.svh
class base_test extends uvm_test;
`uvm_component_utils(base_test)
env env_h;
sequencer sequencer_h;
function void build_phase(uvm_phase phase);
env_h = env::type_id::create("env_h",this);
endfunction : build_phase
function void end_of_elaboration_phase(uvm_phase phase);
sequencer_h = env_h.sequencer_h;
endfunction : end_of_elaboration_phase
function void report_phase(uvm_phase phase);
`uvm_info("RANDOM TEST",$sformatf("Coverage op_cov is %f ,zeros_or_ones_on_ops is
%f",env_h.coverage_h.op_cov.get_coverage(),env_h.coverage_h.zeros_or_ones_on_ops.get_coverage()),UVM_LO
W);
endfunction
function new (string name, uvm_component parent);
super.new(name,parent);
endfunction : new
endclass

例化了env环境和sequencer,然后在验证平台建立和连接完成之后,在end_of_elaboration_phase里对验证平
台做一些调整,其实这里只是将env里的sequencer句柄传递到了base_test,那么所有base_test的子类都可以使用sequencer的句柄来完成sequence的启动,其实也就是仿真的开始,因为仿真的开始首先需要产生测试激励 。

agent加入后注意修改sequencer指针

function void end_of_elaboration_phase(uvm_phase phase);
if(env_h.agent_h.is_active==UVM_ACTIVE)
sequencer_h = env_h.agent_h.sequencer_h;
uvm_top.print_topology();
endfunction : end_of_elaboration_phase
//文件路径:ch14/14.3/sim/testbench/tb_classes/test/fibonacci_test.svh
class fibonacci_test extends base_test;
`uvm_component_utils(fibonacci_test)
task main_phase(uvm_phase phase);
fibonacci_sequence fibonacci;
fibonacci = new("fibonacci");
phase.raise_objection(this);
fibonacci.start(sequencer_h);
phase.drop_objection(this);
endtask
function new(string name, uvm_component parent);
super.new(name,parent);
endfunction : new
endclass

首先创建一个fibonacci_sequence的实例fibonacci,之后调用start()方法来指明sequence对应的sequencer,如果不指明sequencer,那么sequence就不知道将产生的transaction交给哪个sequencer(如果有好几个sequencer的话,当前本例中只有一个)。
这里需要注意objection机制的使用,在UVM中,objection的提起和撤销一般伴随着sequence,通常只在sequence出现的地方才会提起和撤销objection。如前面所说,sequence是弹夹,当弹夹里面的子弹用光之后,即sequence_item都发送完了之后,就可以结束仿真了。

agent

而UVM是通过agent来实现对协议的封装的,换句话说不同的agent就代表了不同的协议。

一般来说,一个agent会包括一个sequencer,一个driver和两个monitor,其中两个monitor,一个用于监测DUT的输入端口,一个用于监测DUT的输出端口,原因我们之前课程的内容就讲过了,记不得的记得回去看一下。agent本身不具备额外的功能,只是将协议做一个封装。而且agent有两种工作模式,我们可以通过修改其数据成员变量 is_active 的值来配置其工作模式,分别是:
(1)active模式(配置为UVM_ACTIVE):包括sequencer、driver和monitor,一般用于产生DUT的激励同时监测DUT的输入和输出端口。
(2)passive模式(配置为UVM_PASSIVE):将只有monitor,一般只用于监测DUT的输入和输出端口,因为不需要产生激励,即不需要驱动任何信号,因此不需要例化产生并驱动激励的sequencer和driver

**需要在build_phase调用的一开始或者在这之前就配置好agent的工作模式,然后根据配置的工作模式才可以对sequencer、driver和monitor进行创建。这里的“一开始”,指的是agent的build_phase里的第一条语句就配置is_active的值,这里的“在此之前”则是在agent的更顶层组件,如env环境的build_phase里对agent的is_active进行配置,因为build_phase是自顶向下进行构建的,所以更顶层组件的build_phase会更早被调用。 **

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ECRDmyN2-1635320932446)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20211015221328704.png)]

菱形代表发送端口uvm_analysis_port,圆形代表接收端口uvm_blocking_get_port,端口之间通过uvm_tlm_analysis_fifo进行连接 。

agent代码实现

//文件路径:ch15/15.2/sim/testbench/tb_classes/component/agent.svh
class agent extends uvm_agent;
`uvm_component_utils(agent)
sequencer sequencer_h;
driver driver_h;
command_monitor command_monitor_h;
result_monitor result_monitor_h;
uvm_analysis_port #(sequence_item) cmd_ap;
uvm_analysis_port #(result_transaction) result_ap;
function new (string name, uvm_component parent);
super.new(name,parent);
endfunction : new
function void build_phase(uvm_phase phase);
//stimulus
if (is_active == UVM_ACTIVE) begin
sequencer_h = sequencer::type_id::create("sequencer_h",this);
driver_h = driver::type_id::create("driver_h",this);
end
//monitors
command_monitor_h = command_monitor::type_id::create("command_monitor_h",this);
result_monitor_h = result_monitor::type_id::create("result_monitor_h",this);
endfunction : build_phase
function void connect_phase(uvm_phase phase);
if (is_active == UVM_ACTIVE) begin
//connect driver & sequencerdriver_h.seq_item_port.connect(sequencer_h.seq_item_export);
end
cmd_ap=command_monitor_h.ap;
result_ap=result_monitor_h.ap;
endfunction : connect_phase
endclass : agent

首先根据其父类uvm_agent预先定义好的成员变量is_active的值来进行选择性例化sequencer、driver和监测DUT输入输出的monitor,因为这里我们agent是active工作模式,因此需要例化sequencer和driver并connect_phase里对两者进行连接 。

注意我们声明了两个uvm_analysis_port端口,但我们不需要对它们进行实例化,而只需要在connect_phase中将monitor对应的端口值赋给它,换句话说,这相当于是一个指向两个monitor的ap的指针。

其实我们也就相当于将agent里的两个monitor的菱形端口拉到了其上一层即agent上,即图中红框的部分

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4Vz9RLHS-1635320932448)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20211015222317840.png)]

然后我们需要做两件事:
(1)在把sequencer、driver和monitor封装成agent后,在env中只需要实例化agent就可以了,不需要直接实例化agent包含的组件了。
(2)另外“需要在build_phase调用的一开始或者在这之前就配置好agent的工作模式,然后根据配置的工作模式才可以对sequencer、driver和monitor进行创建”。因此我们在其上一层UVM组件env环境中对agent进行配置,即对is_active成员变量进行赋值。

Phase机制

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NkaG3dLg-1635320932450)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20211015224222552.png)]

比如上面5步原先都在run_phase里去完成,现在可以分散到如下的phase去完成:
\1. 上电→pre_reset_phase
\2. 复位→reset_phase
\3. 配置寄存器→configure_phase
\4. 跑测试激励完成目标测试内容→main_phase
\5. 等待DUT完成测试→shutdown_phase

在parallel_sequence里,我们只是将原先的reset_sequence删去,因为已经在parallel_test的reset_phase里完成了复位。

建议:只在顶层测试用例test里使用reset/configure/main/shutdown phases细分的Run phases,而在其他组件里只使用run_phase。

消息报告系统

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j3j3LXgo-1635320932451)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20211016135511066.png)]

`uvm_fatal(“message_id”, “message_string”)
`uvm_error(“message_id”, “message_string”)
`uvm_warning(“message_id”, “message_string”)
`uvm_info(“message_id”, “message_string”, uvm_verbosity)

**message_id:**用于给每个消息添加id标签,方便打印出来进行统计分类;
message_string:具体要打印的字符串信息;
uvm_verbosity:这是一个枚举类型的参数,这个参数只有UVM_INFO类型的消息有,其他的没有,用来设置信息冗余的阈值,从而
完成对打印消息的过滤,即只打印显示我们感兴趣的消息,防止重要信息被过多的消息淹没。总共有五个级别,建议按照如下进行设
置:
UVM_NONE :要求必须要打印的信息,比如版本信息、设计或验证平台的配置信息。
UVM_LOW:一般情况下都会被打印的信息,但可以被UVM_NONE给过滤掉,比如一些重要的 打印信息或者断言。
UVM_MEDIUM:phase的开始和结束信息,默认的打印阈值。
UVM_HIGH :详细的事务数据信息。
UVM_FULL :具体的debug过程信息。

有以下两种设置显示阈值verbosity的方式

  1. UVM全局设置verbosity
    可以通过仿真命令行设置全局显示阈值,+UVM_VERBOSITY=UMV_HIGH。

  2. 在UVM 层级中设置 verbosity

    可以在build_phase()之后,run_phase()之前的end_of_elaboration_phase()方法里来设置。通过 set_report_verbosity_level_hier 来完成设置

    也可以通过 set_report_id_verbosity 来对同一个组件的不同id标签的信息设置不同的显示阈值

消息重载

set_report_severity_override

set_report_severity_id_override

消息动作

set_report_severity_action

set_report_severity_id_action

消息动作uvm_log使用:

常用的设置输出日志文件的方法如下:
set_report_default_file(UVM_FILE file)
设置默认输出文件,用来将UVM输出的四种消息类型UVM_INFO、UVM_WARNING、UVM_ERROR、UVM_FATAL都输出到指定文件。
set_report_id_file(string id, UVM_FILE file)
用于将指定ID标签的信息输出到指定文件。
set_report_severity_file(uvm_severity severity, UVM_FILE file)
用于将指定消息类型的信息输出到指定文件。
set_report_severity_id_file(uvm_severity severity, string id, UVM_FILE file)
用于将指定的消息类型和ID标签信息的组合输出到指定文件。

objection机制

Domain机制

domain把两个时钟域分开,那么两个时钟域内的消耗仿真时间的Run-time的细分phase就可以异步地运行,彼此之间相互独立。但注意,这里domain只能隔离Run-time 的细分phase

class new_domain_component extends uvm_component;
`uvm_component_utils(new_domain_component)
uvm_domain new_domain;
function new(string name,uvm_component parent);
super.new(name,parent);
new_domain = new("new_domain");
endfunction
function void connect_phase(uvm_phase phase);
set_domain(new_domain);
endfunction······
class new_domain_component extends uvm_component;
`uvm_component_utils(new_domain_component)
uvm_domain new_domain;
function new(string name,uvm_component parent);
super.new(name,parent);
new_domain = new("new_domain");
endfunction
function void connect_phase(uvm_phase phase);
set_domain(new_domain);
endfunction······

注意别忘了在env里对新创建的组件进行例化。

set_domain函数的原型是:

function void uvm_component::set_domain(uvm_domain domain, int hier=1);

可以像下面这样使用**get_domain_name()**方法来查看

//parallel_test、sequencer、new_domain_component
task main_phase(uvm_phase phase);
`uvm_info("DOMAIN TEST",$sformatf("DOMAIN is %s",phase.get_domain_name()),UVM_LOW);
endtask

我们在main_phase里对其所在的domain查看,因为在进入Run time Phase之前已经完成了domain的设置。
可以在上面的test.log日志文件里看到new_domain_component所在的domain名称叫做new_domain,而其余的component组件,比如parallel_test和sequencer所在domain的名称都是uvm 。

Factroy机制

(1)new构造函数的参数必须按顺序包含string类型的name和uvm_component类型的parent

(2)new构造函数里面的第一条语句必须是 super.new(name, parent)

(3)使用宏 `uvm_component_utils() 将派生的子类注册到Factory

(4)根据需要重载UVM的Phase

使用以下这个方法进行重载

set_type_override_by_type (
uvm_object_wrapper original_type,
uvm_object_wrapper override_type,
bit replace = 1
)

另一个方法进行重载

set_inst_override_by_type(
string relative_inst_path,
uvm_object_wrapper original_type,
uvm_object_wrapper override_type
)

第一个参数是替换的路径,第二个参数是被重载的类的类型,第三个参数是重载的类的类型,一般使用::get_type()方法获取 。

通信TLM机制

看书

使用analysis系列端口实现一对多通信

使用订阅者模式实现一对多通信

UVM中的FIFO通信

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pRN2Jt3U-1635320932452)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20211016143828282.png)]

从图中可以看到,组件A和FIFO通过put系列端口进行连接。组件A调用put系列端口相关的接口方法(put、try_put、can_put)将transaction数据发送给FIFO,然后FIFO利用其内部实现的put系列接口方法来接收组件A发送的数据并缓存到FIFO里,同时利用put_ap端口将接收到的数据向外广播 。

从图中可以看到,组件B和FIFO通过get或peek系列端口进行连接。组件B调用get或peek系列方法来从FIFO中获取transaction数据,然后FIFO利用其内部实现的get或peek系列接口方法来讲缓存中的数据取出给组件B,同时利用get_ap端口将从缓存中取出的数据向外广播。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rSAUaU2n-1635320932453)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20211016144926518.png)]

Sequence机制详细

sequence可以分类为

  1. 扁平类(flat sequence)
  2. 层次类(hierarchical sequence)
  3. 虚拟类(virtual sequence)这一类则是最终控制整个测试场景的方式,由于整个环境中往往存在不同的通信协议,也即往往存在多个agent,也即存在不同种类的sequencer和其对应的sequence 需要一个virtual sequence来协调顶层的测试场景 之所称这个方式为虚拟的sequence,是因为该sequence本身并不固定挂载于某一sequencer类型上,而是它会将其内部的各种不同类型的sequence最终挂载到不同的目标sequencer上。

Sequencer与driver的通信机制

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wKPGksl4-1635320932454)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20211016151758592.png)]

sequencer和driver之间的通信是通过TLM双向端口seq_item_port和seq_item_export来完成的。通过这两个端口可以完成激励请求数据REQ sequence_item以及反馈数据RSP sequence_item的通信传输

而实现sequencer和driver这两个组件之间的双向通信端口的连接是通过在上一层组件agent的connect_phase里将seq_item_port和seq_item_export进行连接实现的

//agent
driver_h.seq_item_port.connect(sequencer_h.seq_item_export);

sequencer和driver是一对一连接进行通信的

Sequence获取反馈

Sequence机制提供了一种sequence→sequencer→driver的单向数据传输机制。但是在复杂的验证平台中,sequence需要根据driver的反馈来决定接下来要发送的sequence_item。换言之,sequence需要得到driver的一个反馈 。

  1. 首先我们需要在原先的sequence_item里增加反馈数据成员,比如这里int类型的response

  2. 然后我们在sequence中使用get_response任务来获取driver的response:

这里sequence调用get_response后会发生阻塞,直到driver调用put_response放入sequence_item。sequence在获取到来自driver的反馈之后进行处理。

  1. 接下来在driver中,使用put_response任务来放入response:

UVM通过在每个sequence_item中加入了id域来处理这个问题,id域用来标识sequence_item和所对应的sequence,这里的关键是设置set_id_info()函数,它将req的id域信息复制到rsp中,那么sequencer就知道将response返回给哪个sequence了。

sequence Library的使用

步,首先从uvm_sequence_library类进行派生并设置sequence_item类型参数,参数的设置需要和sequence的设置保持一致。
步,然后使用宏uvm_object_utils和uvm_sequence_library_utils来注册该sequence library。
步,接下来将需要包含的sequence注册到sequence_library中。
步,别忘记在new构造函数里调用init_sequence_library()方法来进行注册的最终确认。
步,类似其他sequence一样调用start()方法启动。

//文件路径:ch23/23.8/sim/testbench/tb_classes/sequence/sequence_lib.svh
class sequence_lib extends uvm_sequence_library #(sequence_item);
`uvm_object_utils(sequence_lib)
`uvm_sequence_library_utils(sequence_lib)
function new(string name = "sequence_lib");
super.new(name);
init_sequence_library();
endfunction : new
endclass : sequence_lib

可以看到,这里第一、二和四步都已经完成了,下面来完成第三和第五步,即将需要包含的sequence注册到sequence_library中,这里主
要有两种注册方式:
(1)在各个sequence中使用宏`uvm_add_to_seq_lib(<sequence_type>,<seq_library_type>)将自己注册到sequence library中去。

//文件路径:ch23/23.8/sim/testbench/tb_classes/sequence/A.svh
class A extends uvm_sequence #(sequence_item);
`uvm_object_utils(A)
`uvm_add_to_seq_lib(A, sequence_lib)
//`uvm_add_to_seq_lib(A, sequence_lib2)
...
endclass : A

一个sequence可以注册到多个不同的sequence library中。
**(2)在顶层serial_sequence里通过sequence_library句柄调用add_typewide_sequences()**或者add_sequence()来注册。

//文件路径:ch23/23.8/sim/testbench/tb_classes/sequence/serial_sequence.svh
task body();
reset_seq.start(m_sequencer);
A_seq.start(m_sequencer);
B_seq.start(m_sequencer);
C_seq.start(m_sequencer);
D_seq.start(m_sequencer);
`uvm_info("SEQ LIB TEST","The following sequence is from sequence_lib",UVM_MEDIUM)
//一次注册一个sequence
sequence_lib_seq.add_typewide_sequence(B::get_type());
//一次注册多个sequence
sequence_lib_seq.add_typewide_sequences({
C::get_type(),
D::get_type()sequence_lib_seq = sequence_lib::type_id::create("sequence_lib_seq");
sequence_lib_seq.start(m_sequencer);

第二种必须在构造sequence_lib之前进行注册添加,因为在此之后会调用init_sequence_library()完成最终的注册确认

启动的序列总数是可以控制的,默认值为10。这是由sequence library内部的两个变量控制的:

int unsigned min_random_count=10;
int unsigned max_random_count=10;

这里也需要注意两点:
(1)必须在构造sequence_lib之后对最大最小random_count进行设置,否则报错。
(2)必须调用randomize()方法从而获得需要随机发送执行的sequence的数量,否则设置不起作用。

Sequencer的仲裁机制

UVM_SEQ_ARB_FIFO :默认模式。sequencer将sequence按照FIFO先进先出,即先来后到的方式依次发送给driver,和sequence的优先级无关。
UVM_SEQ_ARB_WEIGHTED:sequencer将sequence按照它们的优先级来随机发送其产生的sequence_item给driver。
UVM_SEQ_ARB_RANDOM :sequencer随机发送sequence产生的sequence_item给driver,跟先来后到和优先级都无关。
UVM_SEQ_ARB_STRICT_FIFO:首先考虑优先级,当遇到优先级相同的情况时,再考虑先来后到的顺序。
UVM_SEQ_ARB_STRICT_RANDOM:首先考虑优先级,当遇到优先级相同的情况时,再随机从中进行选择。

UVM_SEQ_ARB_USER:使用用户自定义的方法进行仲裁。

//文件路径:ch23/23.11_2/sim/testbench/tb_classes/sequence/serial_sequence.svh
class parallel_sequence extends uvm_sequence #(uvm_sequence_item);
...
task body();
m_sequencer.set_arbitration(UVM_SEQ_ARB_WEIGHTED);
//m_sequencer.set_arbitration(UVM_SEQ_ARB_RANDOM);
//m_sequencer.set_arbitration(UVM_SEQ_ARB_STRICT_FIFO);
//m_sequencer.set_arbitration(UVM_SEQ_ARB_USER);
`uvm_info("SEQ ARB TEST",$sformatf("arbitration mode is
%s",m_sequencer.get_arbitration().name()),UVM_MEDIUM)
reset_seq.start(m_sequencer);
fork
A_seq.start(m_sequencer,this,100);
B_seq.start(m_sequencer,this,200);
C_seq.start(m_sequencer,this,200);
D_seq.start(m_sequencer,this,300);
join
endtask : body
endclass : parallel_sequence

仲裁模式就使用m_sequencer句柄调用set_arbitration()方法即可。
而sequence的优先级,则在start()方法里设置优先级参数

这里的第一个参数是sequencer。
第二个参数是parent sequence,因为这里是hierarchical sequence,即parallel_sequence,因此可以设置成this,否则在其他组件
里,设置成对应的sequencer的路径即可。
第三个参数是优先级,如果不指定则此值为-1,它同样不能设置为一个小于-1的数字。

start()方法的原型是

virtual task start( uvm_sequencer_base sequencer, uvm_sequence_base parent_sequence = null, int
this_priority = -1, bit call_pre_post = 1);

UVM_SEQ_ARB_USER

使用用户自定义的方法进行仲裁。
为了做到这一点,uvm_sequencer必须被重载user_priority_arbitration()方法,换句话说,我们需要在sequencer里实现该方法。
该方法的原型是:

virtual function integer user_priority_arbitration( integer avail_sequences[$]);

该方法中的参数用于接收来自sequencer中包含的sequence_items队列,即该队列是sequencer即将要发送给driver的sequence_item的集合,用户实现**user_priority_arbitration()**方法返回一个整数来从该队列中选择一个sequence_items以完成仲裁

下面我们在sequencer里重载user_priority_arbitration()方法以实现总是选择最后一sequence_item,相当与原先仲裁机制的结果相反,等会我们可以来仿真比较一下,我们先来看实现的代码:

//文件路径:ch23/23.11_2/sim/testbench/tb_classes/component/sequencer.svh
class sequencer extends uvm_sequencer #(sequence_item);
`uvm_component_utils(sequencer)
function new(string name,uvm_component parent);
super.new(name,parent);
endfunction
function integer user_priority_arbitration(integer avail_sequences[$]);
int end_index;
end_index = avail_sequences.size() - 1;
return (avail_sequences[end_index]);
endfunction
endclass

Sequencer的独占操作

有些情况下,sequence需要独占sequencer来访问driver,一种典型的场景是中断操作。为了满足这种应用场景,uvm_sequencer提供了一种lock机制,通过调用**lock()和grab()**来实现。lock()用来对优先级中断进行建模,而grab()则用来对不可屏蔽中断进行建模。除此之外,还需要使用unlock()和ungrab()来释放独占访问,否则sequencer将产生死锁。

简单说就是,与lock操作一样,grab操作也用于暂时拥有对sequencer的独占权限,只是grab操作比lock操作优先级更高。lock请求是被插入sequencer仲裁队列的最后面,等到它时,它前面的仲裁请求都已经结束了。grab请求则被放入sequencer仲裁队列的最前面,即立刻产生独占影响,即立刻拥有对sequencer的独占权限。

//文件路径:ch23/23.12/sim/testbench/tb_classes/sequence/A.svh
task body();···m_sequencer.lock(this);m_sequencer.unlock(this);

Virtual Sequence的使用

virtual sequence使用多个sequencer来控制激励的产生和发送,它是所有sequence的顶层。即它将其包含的所有的子sequence与挂载到不同的sequencer上,从而使用这些不同的sequencer来发送不同的激励。

整个验证平台使用这样一个虚拟的sequence来协调各种不同的BFM通信接口并完成他们之间的连接交互。

不同于平常的sequence,virtual sequence本身不发送sequence_item,而是控制产生其他sequences并在不同的目标协议代理agent上执行这些sequences,因此它起到的是统一调度的作用。

第一种

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rvwMqLBM-1635320932455)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20211016160759141.png)]

第二种

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cWguKiqO-1635320932455)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20211016161114716.png)]

UVM的寄存器模型

寄存器bus总线上的信号

**bus_valid:**为1时总线数据有效,为0时无效。该有效信号只持续一个时钟,DUT应该在其为1的期间对总线上的数据进行采样。如果是写操作,DUT应该在下一个时钟检测到总线数据有效后,采样总线上的数据并写入到其内部寄存器。如果是读操作,DUT应该在下一个时钟检测到总线数据有效后,将寄存器数据读到数据总线上。
**bus_op:**总线读写操作。为1时向总线上写数据,为0时从总线上读数据。
**bus_addr:**表示地址总线上的地址,其位宽为16位。
**bus_wr_data:**表示写入数据总线的16位宽的数据。
**bus_rd_data:**表示从数据总线上读取的16位宽的数据。

在Sequence里对寄存器进行读写

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5XYr6oyU-1635320932456)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20211016190256319.png)]

即通过使用bus_driver向总线上发送读指令的sequence,并给出要读的寄存器地址来查看一个寄存器的值。或者发送写指令的sequence,并给出要写的寄存器地址和要写入的寄存器值从而最终实现对寄存器的读写。

步骤

  1. 我们需要创建一个bus_transaction
  2. 创建驱动该transanction的bus_driver
  3. 创建驱动该transanction的bus_driver
  4. 创建负责传送bus_transaction给bus_driver的bus_sequencer
  5. 再创建一个bus_monitor用于监测bus总线上的数据
  6. 创建bus_agent
  7. 接着在env中对bus_agent进行实例化
  8. 然后创建bus_sequence
  9. 需要使用virtual sequence来启动不同的sequencer(sequencer和bus_sequencer)
  10. 在virtual_sequence_test测试用例中实现
  11. 在virtual_sequence_test测试用例中实现

在UVM组件里对寄存器进行读写

1.对寄存器的读取
需要在scoreboard里得到bus_sequencer的指针,然后在此sequencer上启动一个sequence以读取寄存器,该sequence将发送一个bus_transaction给bus_driver,然后将sequence读取的寄存器的值传递给scoreboard。
2.对寄存器的写入
同样的原理,启动一个seqeunce并由bus_driver来驱动并写入到DUT寄存器即可。

步骤

  1. 关于在scoreboard里得到bus_sequencer的指针,只要在scoreboard里设置一个bus_sequencer的变量,并在env中将sequencer的指针赋值给此变量即可。
  2. 然后创建用于寄存器读写的sequence
  3. 然后修改scoreboard

寄存器模型实现

UVM的寄存器模型给我们提供了非常便捷的实现方式:

reg_model.ctrl_reg.read(status, value, UVM_FRONTDOOR);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8XLZCnXC-1635320932457)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20211016193759106.png)]

UVM预先定义好了一种sequence_item,叫做uvm_reg_item。然后通过适配器adapter的bus2reg()及reg2bus()方法实现uvm_reg_item与目标总线协议的bus_item的转换(即这里需要根据总线通信协议将寄存器读写操作转换为符合目标总线协议的sequence_item)。最后由sequencer和driver驱动给DUT,从而最终完成对目标寄存器的读写。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rgI2lkEE-1635320932457)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20211016193919552.png)]

读寄存器访问流程大致为:
(1)比如,在UVM组件scoreboard中调用寄存器模型中寄存器的读访问方法,并更新寄存器模型相应寄存器的值
(2)寄存器模型产生sequence,并产生uvm_reg_item
(3)调用adapter的reg2bus()方法将uvm_reg_item转换成bus_item
(4)把bus_item交给bus_sequencer,bus_sequencer再传送给driver,driver得到bus_item后驱动它,得到读取的值,并将读取值放回
bus_item中,调用item_done
(5)调用adapter的bus2reg()方法将bus_item中读取的值传递给uvm_reg_item
(6)将uvm_reg_item中的读数据返回给adapter,即返回给了寄存器模型
(7)在scoreboard中使用寄存器模型句柄来获取读到的寄存器的值。
写寄存器访问流程大致为:
(1)调用寄存器模型中寄存器的写访问方法,并更新寄存器模型相应寄存器的值

(2)寄存器模型产生sequence,并产生uvm_reg_item
(3)调用adapter的reg2bus()方法将uvm_reg_item转换成bus_item
(4)把bus_item交给bus_sequencer,bus_sequencer再传送给driver,driver得到bus_item后驱动它,从而完成对DUT寄存器的写入,
调用item_done。

寄存器模型的分层结构

UVM的寄存器模型主要由5个部分组成,分别是:

Register field:寄存器field。寄存器里具体每一位(field)的功能,其有对应的宽度(width)和偏移(offset),以及可读写(read/write)、只读(read only)、只写(write only)属性。
Register:寄存器。包含一个或多个registr field。
Register block:寄存器块。对应一个具体的硬件,可以理解为一个容器,这个容器包含一个或多个register以及一个或多个的registermap。我们说的寄存器模型实际上就是指的一个register block的实例。
Memory:存储。由uvm_mem建模,包括大小(size),和地址范围(range),其是register block的一部分,其偏移(offset)取决于register map。同样也有可读写(read/write)、只读(read only)、只写(write only)属性。但每次读写一个地址时,读写的是该地址整个的数据,而不是针对某些位进行的操作,因此其不具有类似寄存器的register field的概念。
**Register map :寄存器地址映射表。**用来定义对于在总线上的其父模块来说内部所包含的寄存器和存储地址空间的偏移。每个寄存器在加入寄存器模型时都有其地址,uvm_reg_map就是存储这些地址,并将其转换成可以访问的物理地址(因为加入寄存器模型中的寄存器地址一般都是偏移地址,而不是绝对地址)。当寄存器模型使用前门访问方式来实现读或写操作时(具体什么是前门访问以及后门访问,我们后面再做详细地介绍),uvm_reg_map就会将地址转换成绝对地址,启动一个读或写sequence,并将读或写的结果返回。在每个reg_block内部,至少有一个(通常也只有一个)uvm_reg_map。

加入寄存器模型代码实现

//文件路径:ch24/24.7/sim/testbench/tb_classes/register/ctrl_reg.svh
class ctrl_reg extends uvm_reg;
`uvm_object_utils(ctrl_reg)
rand uvm_reg_field invert_field;
rand uvm_reg_field border_field;
uvm_reg_field reserved_field;
function new(string name="ctrl_reg");
super.new(name, 16, UVM_NO_COVERAGE);
endfunction
function void build();
invert_field = uvm_reg_field::type_id::create("invert_field");
invert_field.configure(this, 1, 0, "RW", 0, 1'b0, 1, 1, 0);
border_field = uvm_reg_field::type_id::create("border_field");
border_field.configure(this, 1, 1, "RW", 0, 1'b0, 1, 1, 0);
reserved_field = uvm_reg_field::type_id::create("reserved_field");
reserved_field.configure(this, 14, 2, "RW", 0, 14'h0000, 1, 0, 0);
endfunction
endclass

其中uvm_reg的new构造原型如下

function new (string name="", // 寄存器名称
int unsigned n_bits, // 寄存器的宽度,即总共bits位数
int has_coverage); // 是否被覆盖率模型所统计支持

首先调用 uvm_reg_field::type_id::create(“reg field name”) 来对reg_field进行例化。然后调用configure()方法来配置该regfield,其原型如下:

function void configure(
uvm_reg parent, //归属的寄存器,一般为this,即当前寄存器
int unsigned size, // 该reg_field的位宽
int unsigned lsb_pos, //该reg_field的最低位在该寄存器内的偏移offset
string access, //访问属性,包括"RW", "RO"等,具体介绍见下面
bit volatile, // 是否具有易失性,一般不使用
uvm_reg_data_t reset, // 复位后的初始值
bit has_reset, // 是否可以被复位
bit is_rand, // 是否可以被随机化,主要用于对寄存器进行随机写的测试,这也是为什么一开始在reg_ field前加关键字rand
的原因
bit individually_accessible //是否可单独存取
);

创建寄存器模型

//文件路径:ch24/24.7/sim/testbench/tb_classes/register/reg_model.svh
class reg_model extends uvm_reg_block;
`uvm_object_utils(reg_model)
rand ctrl_reg ctrl_reg_h;
rand status_reg status_reg_h;
function new(string name="reg_model");
super.new(name, UVM_NO_COVERAGE);
endfunction
function void build();
default_map = create_map("default_map", 'h0, 2, UVM_LITTLE_ENDIAN);
ctrl_reg_h = ctrl_reg::type_id::create("ctrl_reg_h");
ctrl_reg_h.configure(this);
ctrl_reg_h.build();
default_map.add_reg(ctrl_reg_h, 16'h8, "RW");
status_reg_h = status_reg::type_id::create("status_reg_h");
status_reg_h.configure(this);
status_reg_h.build();
default_map.add_reg(status_reg_h, 16'h9, "RO");
endfunction
endclass

其构造函数原型 :

function new(
string name = "",
int has_coverage = UVM_NO_COVERAGE
)

一个uvm_reg_block中一定要对应一个uvm_reg_map,即寄存器映射表,用于指明block内的寄存器对应的地址,查看其原型发现在uvm_reg_block里已经预先声明好了default_map,其在内部帮我们写好了:

uvm_reg_map default_map;

我们只需要在build中使用create_map()方法的方式将其实例化即可。该方法原型如下:

function uvm_reg_map create_map(
string name, // 寄存器映射表名称,这里名称default_map
uvm_reg_addr_t base_addr, // 基地址,默认64位宽,这里为'h0
int unsigned n_bytes, // 总线宽度(以字节为单位),这里为16位宽
uvm_endianness_e endian, // 大小端数据格式,这里为小端格式UVM_LITTLE_ENDIAN
bit byte_addressing=1 // 默认支持字节寻址
);

这里的总线宽度uvm_reg_addr_t类似之前的uvm_reg_data_t,默认为64位宽。同样,如果想要扩展地址总线的位宽,可以通过重新定义UVM_REG_ADDR_WIDTH宏来扩展 。

随后实例化寄存器ctrl_reg和status_reg并调用configure配置函数。这个函数的主要功能是指定寄存器进行后门访问操作时的路径。其原型如下

function void configure (
uvm_reg_block blk_parent, //所在的block,一般填this
uvm_reg_file regfile_parent = null, //reg_file的指针,后面再做介绍,这里暂且填null
string hdl_path = ""); //该寄存器的后门访问路径,后面再做介绍,这里暂且为空

最后一步则是将这两个寄存器加入寄存器映射表default_map中。由于default_map用于指明block内的寄存器对应的地址,因此必须将实例化的寄存器加入default_map中,否则无法进行前门访问操作。
可以使用add_reg()方法将寄存器加入到寄存器映射表,该方法原型是:

function void add_reg (
uvm_reg rg, // 要加入的寄存器名称
uvm_reg_addr_t offset, // 地址偏移,默认64位宽
string rights = "RW", // 访问属性,默认“RW”可读可写
bit unmapped=0, // 关闭映射,相当于本条语句失效,即该寄存器不会被加入到寄存器映射表,一般默认为0
uvm_reg_frontdoor frontdoor=null // 寄存器前门访问的句柄,一般默认为null
);

加入adapter

(1)reg2bus()方法

简单说,即uvm_reg_bus_op → bus_transaction,即实现了uvm_reg_item → bus_item。

pure virtual function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);

(2)bus2reg()方法

简单说,即bus_transaction → uvm_reg_bus_op,即实现了bus_item → uvm_reg_item。

pure virtual function void bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);

寄存器模型的数据变量为uvm_reg_bus_op,其包含的数据成员有:

  1. addr数据类型为uvm_reg_addr_t。寄存器访问地址,默认位宽64位。
  2. data数据类型为uvm_reg_data_t。寄存器读写数据,默认位宽64位。
  3. kind数据类型为uvm_access_e。寄存器访问操作,读或写,即UVM_READ或UVM_WRITE。
  4. n_bits数据类型为unsigned int。传输位数。
  5. byte_en数据类型为uvm_reg_byte_en_t。字节使能。
  6. status数据类型为uvm_status_e。操作返回结果。UVM_IS_OK,UVM_IS_X,UVM_NOT_OK。
class adapter extends uvm_reg_adapter;
`uvm_object_utils(adapter)
function new(string name = "bus_adapter");
super.new(name);
supports_byte_enable = 0;
provides_responses = 0;
endfunction
function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
bus_transaction bus_trans;
bus_trans = bus_transaction::type_id::create("bus_trans");
bus_trans.addr = rw.addr;
bus_trans.bus_op = (rw.kind == UVM_READ)? bus_rd: bus_wr;
if (bus_trans.bus_op == bus_wr)
bus_trans.wr_data = rw.data;
return bus_trans;
endfunction
function void bus2reg(uvm_sequence_item bus_item,ref uvm_reg_bus_op rw);
bus_transaction bus_trans;
if (!$cast(bus_trans, bus_item)) begin
`uvm_fatal("NOT_BUS_TYPE","Provided bus_item is not of the correct type")
return;
end
rw.kind = (bus_trans.bus_op == bus_rd)? UVM_READ : UVM_WRITE;
rw.addr = bus_trans.addr;
rw.data = (bus_trans.bus_op == bus_rd)? bus_trans.rd_data : bus_trans.wr_data;
rw.status = UVM_IS_OK;
endfunction
endclass

provides_responses如果被设置为1,那么寄存器模型将会等待来自driver的反馈,如果一直没有等到反馈的话,那么将不再产生激励,系统将会产生死锁。supports_byte_enable用于设定总线协议是否支持字节使能。

然后修改env

要将一个寄存器模型集成到env中,首先需要在env中定义两个成员变量,一个是reg_model,另一个是adapter。将所有用到的类在build_phase中实例化。
在实例化后reg_model还要做五件事:
(1)调用configure()方法
其原型是:

function void configure(uvm_reg_block parent = null, string hdl_path = "")

其中第一个参数是父parent block,由于是最顶层的reg_block,因此默认为null,第二个参数是后门访问路径,这里暂且为空。
(2)调用build()方法
将所有的寄存器实例化。
(3)调用lock_model()方法
调用此方法后,地址映射就最终确定了,即确保reg_model不会被其他用户更改,reg_model也中就不能再加入新的寄存器了。
(4)调用reset()方法
如果不调用此方法,那么reg_model中所有寄存器的值都是0,调用此方法后,所有寄存器的值都将变为设置的复位值。
(5)调用default_map的set_sequencer()方法
寄存器模型的前门访问操作最终都将由uvm_reg_map完成,因此在connect_phase中,需要将adapter和bus_sequencer通过set_sequencer()方法告知reg_model的default_map,并将default_map设置为自动预测状态。关于“自动预测状态”后面再做详细讲解。

在Sequence和component中使用寄存器模型

有如下两种常见的使用场景:
在sequence中使用寄存器模型
主要用于配置DUT工作的状态,并通过不同的输入操作数,从而更全面的测试到DUT的功能特性。
在component中使用寄存器模型
主要在分析组件里来使用,以实现对寄存器的读写,当然主要是读操作,然后与DUT实际的寄存器值进行比较分析,从而帮助判断DUT寄存器相关功能的正确性。

对于寄存器的读写访问,寄存器模型提供了两个基本的方法:read和write(当然不止这俩个,后面再对其他的访问方法做详细介绍)。
若要读取寄存器,可以使用**read()**方法,其原型如下:

extern virtual task read(output uvm_status_e status,
output uvm_reg_data_t value,
input uvm_path_e path = UVM_DEFAULT_PATH,
input uvm_reg_map map = null,
input uvm_sequence_base parent = null,
input int prior = -1,
input uvm_object extension = null,
input string fname = "",
input int lineno = 0);

在sequence中使用寄存器模型,首先需要在bus_sequencer中有一个寄存器模型的指针。

reg_model reg_model_h;

在env中实例化了reg_model,因此只需要在virtual_sequence_test中将该寄存器模型传递给bus_sequencer即可:

//文件路径:ch24/24.7/sim/testbench/tb_classes/test/virtual_sequence_test.svh
function void connect_phase(uvm_phase phase);
...
env_h.bus_agent_h.sequencer_h.reg_model_h = env_h.reg_model_h;
endfunction

**write()**方法原型如下 :

extern virtual task write(output uvm_status_e status,
input uvm_reg_data_t value,
input uvm_path_e path = UVM_DEFAULT_PATH,
input uvm_reg_map map = null,
input uvm_sequence_base parent = null,
input int prior = -1,
input uvm_object extension = null,
input string fname = "",
input int lineno = 0);
//文件路径:ch24/24.7/sim/testbench/tb_classes/sequence/bus_sequence.svh
class bus_sequence extends uvm_sequence #(bus_transaction);
`uvm_object_utils(bus_sequence)
`uvm_declare_p_sequencer(bus_sequencer)
function new(string name = "bus_sequence");
super.new(name);
endfunction : new
task body();
uvm_status_e status;
uvm_reg_data_t value;
p_sequencer.reg_model_h.ctrl_reg_h.write(status, 16'h1, UVM_FRONTDOOR);
p_sequencer.reg_model_h.ctrl_reg_h.read(status, value, UVM_FRONTDOOR);
`uvm_info("BUS SEQ", $sformatf("ctrl_reg value is %4h", value),UVM_MEDIUM)
endtask : body
endclass : bus_sequence
//文件路径:ch24/24.7/sim/testbench/tb_classes/test/virtual_sequence_test.svh
class virtual_sequence_test extends base_test;
`uvm_component_utils(virtual_sequence_test)
virtual_sequence virtual_sequence_h;
function new(string name, uvm_component parent);
super.new(name,parent);virtual_sequence_h = virtual_sequence::type_id::create("virtual_sequence_h");
endfunction : new
function void connect_phase(uvm_phase phase);
virtual_sequence_h.sequencer_h = env_h.agent_h.sequencer_h;
virtual_sequence_h.bus_sequencer_h = env_h.bus_agent_h.sequencer_h;
env_h.bus_agent_h.sequencer_h.reg_model_h = env_h.reg_model_h;
env_h.scoreboard_h.reg_model_h = env_h.reg_model_h;
endfunction
task main_phase(uvm_phase phase);
phase.raise_objection(this);
virtual_sequence_h.start(null);
phase.drop_objection(this);
endtask
endclass
//文件路径:ch24/24.7/sim/testbench/tb_classes/sequence/virtual_sequence.svh
class virtual_sequence extends uvm_sequence #(uvm_sequence_item);
`uvm_object_utils(virtual_sequence)
reset_sequence reset_seq;
random_sequence random_seq;
bus_sequence bus_seq;
sequencer sequencer_h;
bus_sequencer bus_sequencer_h;
function new(string name = "virtual_sequence");
super.new(name);
reset_seq = reset_sequence::type_id::create("reset_seq");
random_seq = random_sequence::type_id::create("random_seq");
bus_seq = bus_sequence::type_id::create("bus_seq");
endfunction : newtask body();
reset_seq.start(sequencer_h);
bus_seq.start(bus_sequencer_h);
random_seq.start(sequencer_h);
endtask : body
endclass : virtual_sequence

以在scoreboard中使用为例,需要在scoreboard中有一个寄存器模型的指针

reg_model reg_model_h;

在env中实例化了reg_model,因此只需要在virtual_sequence_test中将该寄存器模型传递给scoreboard即可:

//文件路径:ch24/24.12/sim/testbench/tb_classes/test/virtual_sequence_test.svh
function void connect_phase(uvm_phase phase);
...
env_h.scoreboard_h.reg_model_h = env_h.reg_model_h;
endfunction

类似地,若要在scoreboard中读写寄存器,也可以使用**read()和write()**方法:

代码见别

此时,不再在virtual_sequence里包含bus_sequence来对寄存器进行读写了,而是直接在上面的scoreboard中对寄存器进行读写,因此只需要把bus_seq.start(bus_sequencer_h)注释即可。

后门访问的传统实现方式

一种传统的寄存器模型后门访问的实现方式,即直接通过使用绝对路径的“点点”的方式找到DUT中寄存器的位置,简单粗暴地读写即可,可以通过定义一个interface接口来实现:

//文件路径:ch24/24.15/sim/testbench/backdoor_bfm.sv
interface backdoor_bfm;
function void poke_ctrl_reg(input bit[15:0] value);
top.DUT.ctrl_reg = value;
endfunction
function void peek_ctrl_reg(output bit[15:0] value);
value = top.DUT.ctrl_reg;
endfunction
endinterface : backdoor_bfm

poke_ctrl_reg为后门写,而peek_ctrl_reg为后门读。

然后在顶层top模块中通过配置数据库向验证平台传递该interface

然后在scoreboard中获取该interface,然后可以直接调用此方法来完成对寄存器ctrl_reg的后门读写操作:

后门访问的UVM接口实现方式

详解寄存器概念中三种值的类型

镜像值 期望值

寄存器模型的访问方法

寄存器模型提供了很多对寄存器的读写访问方法,这些方法的本质是将寄存器类型的transaction转换成目标总线能够接受的transaction,然后最终由目标总线上的agent完成对目标寄存器的读写。

看pdf吧。。

rt(sequencer_h);
endtask : body
endclass : virtual_sequence


以在scoreboard中使用为例,需要在scoreboard中有一个寄存器模型的指针  ```verilog
reg_model reg_model_h;

在env中实例化了reg_model,因此只需要在virtual_sequence_test中将该寄存器模型传递给scoreboard即可:

//文件路径:ch24/24.12/sim/testbench/tb_classes/test/virtual_sequence_test.svh
function void connect_phase(uvm_phase phase);
...
env_h.scoreboard_h.reg_model_h = env_h.reg_model_h;
endfunction

类似地,若要在scoreboard中读写寄存器,也可以使用**read()和write()**方法:

代码见别

此时,不再在virtual_sequence里包含bus_sequence来对寄存器进行读写了,而是直接在上面的scoreboard中对寄存器进行读写,因此只需要把bus_seq.start(bus_sequencer_h)注释即可。

后门访问的传统实现方式

一种传统的寄存器模型后门访问的实现方式,即直接通过使用绝对路径的“点点”的方式找到DUT中寄存器的位置,简单粗暴地读写即可,可以通过定义一个interface接口来实现:

//文件路径:ch24/24.15/sim/testbench/backdoor_bfm.sv
interface backdoor_bfm;
function void poke_ctrl_reg(input bit[15:0] value);
top.DUT.ctrl_reg = value;
endfunction
function void peek_ctrl_reg(output bit[15:0] value);
value = top.DUT.ctrl_reg;
endfunction
endinterface : backdoor_bfm

poke_ctrl_reg为后门写,而peek_ctrl_reg为后门读。

然后在顶层top模块中通过配置数据库向验证平台传递该interface

然后在scoreboard中获取该interface,然后可以直接调用此方法来完成对寄存器ctrl_reg的后门读写操作:

后门访问的UVM接口实现方式

详解寄存器概念中三种值的类型

镜像值 期望值

寄存器模型的访问方法

寄存器模型提供了很多对寄存器的读写访问方法,这些方法的本质是将寄存器类型的transaction转换成目标总线能够接受的transaction,然后最终由目标总线上的agent完成对目标寄存器的读写。

看pdf吧。。

uvm学习笔记----适合新手快速学习相关推荐

  1. [学习笔记]韩顺平-快速学习github -github教程 github视频 github入门

    Github实战:创建repository发布到公网,所有人能访问 效果:网站https://tnhsp2.github.io/ 就会看到 好大的一只鲸鱼 步骤1:按照规范创建一个repositroy ...

  2. slua 是c语言开发的吗,初学者必备文档:LUA新手快速学习笔记

    LUA程序设计语言 是一个简洁.轻量.可扩展的脚本语言.LUA读作/'lua/(噜啊),是葡萄牙语中"Luna"(月亮)的意思. LUA的目标是成为一个很容易嵌入其它语言中使用的语 ...

  3. Winform设计-小百货 涵盖基础插件学习(适合新手)

    Winform设计-小百货 涵盖基础插件学习(适合新手) 第一次写winform,主要是为了加快对  事件的 理解. 代码如下: private void Form1_Load(object send ...

  4. Bootstrap学习笔记01【快速入门、栅格布局】

    Java后端 学习路线 笔记汇总表[黑马程序员] Bootstrap学习笔记01[快速入门.栅格布局][day01] Bootstrap学习笔记02[全局CSS样式.组件和插件.案例_黑马旅游网][d ...

  5. 深度学习笔记:01快速构建一个手写数字识别系统以及张量的概念

    深度学习笔记:01快速构建一个手写数字识别系统 神经网络代码最好运行在GPU中,但是对于初学者来说运行在GPU上成本太高了,所以先运行在CPU中,就是慢一些. 一.安装keras框架 使用管理员模式打 ...

  6. 适合新手小白学习的web前端学习路线图

    Web前端是一个入行门槛较低的开发技术,但更是近几年热门的职业,web前端不仅薪资高发展前景好,是很多年轻人向往的一个职业,想学习web前端,那么你得找到好的学习方法,以下就给大家分享一份适合新手小白 ...

  7. Hadoop学习笔记—15.HBase框架学习(基础知识篇)

    Hadoop学习笔记-15.HBase框架学习(基础知识篇) HBase是Apache Hadoop的数据库,能够对大型数据提供随机.实时的读写访问.HBase的目标是存储并处理大型的数据.HBase ...

  8. Hadoop学习笔记—16.Pig框架学习

    Hadoop学习笔记-16.Pig框架学习 一.关于Pig:别以为猪不能干活 1.1 Pig的简介 Pig是一个基于Hadoop的大规模数据分析平台,它提供的SQL-LIKE语言叫Pig Latin, ...

  9. 学习笔记之——基于深度学习的图像超分辨率重建

    最近开展图像超分辨率( Image Super Resolution)方面的研究,做了一些列的调研,并结合本人的理解总结成本博文~(本博文仅用于本人的学习笔记,不做商业用途) 本博文涉及的paper已 ...

最新文章

  1. 事务处理不当,线上接口又双叒内存泄漏了!(附图解问题全过程)
  2. github上传文件夹教程
  3. FFmpeg使用基础
  4. [问题解决]基于注解配置dubbo遇到ConnectionLoss for /dubbo/xxx问题解决
  5. 1.4 为什么深度学习会兴起?(Why is Deep Learning taking off?)
  6. 手机modem开发(1)---MTK modem开发
  7. linux 函数自动补全,Shell脚本中实现自动补全功能
  8. VGG11、VGG13、VGG16、VGG19网络结构图
  9. 在Android上实现汉字笔顺动画效果
  10. jquery html5自动播放,jquery html5 视频播放控制代码
  11. 计算机配置路径,计算机基础~Windows工作路径、相对路径、绝对路径
  12. cad动态块制作翻转_cad动态块拉伸制作方法,单开门动态块制作教程具体分析
  13. Duplicate Symbols for Architecture解决办法
  14. OpenCV3学习笔记(1)- OpenCV安装配置、基本架构、使用CMake编译OpenCV源代码
  15. 2023年全国最新工会考试精选真题及答案10
  16. 最新版的PDF转图片软件
  17. 【2020.06】国税总局发票查验平台验证码最新获取方法
  18. 建立适当的索引(ZZ)
  19. 第82届奥斯卡获奖名单
  20. 绝地求生服务器维护9月19日,绝地求生9月19日维护到几点_绝地求生9月19日维护内容介绍_快吧单机游戏...

热门文章

  1. Angular报注入错误,以及解决方式..$injector:unpr]
  2. VS2015使用opencv
  3. (六)Python查询12306余票:升级版-----图形化界面代码
  4. Latex学习笔记 (Ex4)希腊字母、罗马字母一览表
  5. 小米路由器3无线网连接到服务器,小米路由器3的设置方法步骤
  6. python画抛物线_在python中利用最小二乘拟合二次抛物线函数的方法
  7. 我国天宫空间站以及各个仓位介绍
  8. win10管理员权限启动cmd
  9. solidworks显示无法连接到服务器,SOLIDWORKS Electrical解决方法:无法连接协同服务器...
  10. Java蚂蚁金服面试