UVM实战卷I学习笔记10——UVM中的寄存器模型(3)
⽬录
后门访问与前门访问
*UVM中前门访问的实现
前门访问:通过寄存器配置总线(如APB协议、OCP协议、I2C协议等)对DUT进⾏操作。任何总线协议中前门访问操作只有两种:读操作和写操作。前门访问操作是⽐较正统的⽤法。对实际焊接在电路板上正常⼯作的芯⽚来说,此时若要访问其中某些寄存器,前门访问操作是唯⼀的⽅法。
前⾯提到对于参考模型来说,最⼤的问题是如何在其中启动⼀个sequence,列举了全局变量和config_db的两种⽅式。除这两种⽅式之外,如果能在参考模型中得到sequencer的指针,也可以在此sequencer上启动sequence。这通常⽐较容易实现,只要在其中设置⼀个
p_sqr的变量,并在env中将sequencer的指针赋值给此变量即可。
接下来的关键是分别写⼀个读写的sequence:
class reg_access_sequence extends uvm_sequence#(bus_transaction);
string tID =get_type_name();
bit[15:0] addr;奥迪中国
bit[15:0] rdata;
bit[15:0] wdata;
bit is_wr;
virtual task body();
bus_transaction tr;
tr =new("tr");
tr.addr = this.addr;
tr.wr_data = this.wdata;
tr.bus_op =(is_wr ? BUS_WR : BUS_RD);
`uvm_info(tID, $sformatf("begin to access register: is_wr = %0d, addr = %0h",
is_wr, addr), UVM_MEDIUM)
`uvm_send(tr)
`uvm_info(tID,"successfull access register", UVM_MEDIUM)
this.rdata = tr.rd_data;
endtask
endclass
之后,在参考模型中使⽤如下⽅式来进⾏读操作:
task my_model::main_phase(uvm_phase phase);
reg_access_sequence reg_seq;
super.main_phase(phase);
reg_seq =new("reg_seq");
reg_seq.addr =16'h9;
reg_seq.is_wr =0;
reg_seq.start(p_sqr);
while(1) begin
if(reg_seq.rdata)
invert_tr(new_tr);
ap.write(new_tr);
end
endtask
sequence是⾃动执⾏的,但在其执⾏完毕后(body及post_body调⽤完成),为此sequence分配的内存依然是有效的,所以可以使⽤reg_seq继续引⽤此sequence。上述读操作正是⽤到了这⼀点。
对UVM,其在寄存器模型中使⽤的⽅式也与此类似。上述操作⽅式的关键是在参考模型中有⼀个sequencer的指针,在寄存器模型中也有这样的指针,就是在前⾯base_test的connect_phase为default map设置的sequencer指针。
UVM是⼀种通⽤的验证⽅法学,所以要能够处理各种transaction类型。这些要处理的transaction都⾮常相似,综合它们的特征后,UVM 内建了⼀种transaction:uvm_reg_item。通过adapter的bus2reg及reg2bus,可以实现uvm_reg_item与⽬标transaction的转换。以读操作为例,其完整的流程为:
参考模型调⽤寄存器模型的读任务
寄存器模型产⽣sequence,并产⽣uvm_reg_item:rw
产⽣driver能够接受的transaction:bus_2bus(rw)
把bus_req交给bus_sequencer
driver得到bus_req后驱动它得到读取的值,并将读取值放⼊bus_req中,调⽤item_done
寄存器模型调⽤adapter.bus2reg(bus_req, rw)将bus_req中的读取值传递给rw
将rw中的读数据返回参考模型
如果driver⼀直发送应答⽽sequence不收集应答,那么将会导致sequencer的应答队列溢出。UVM考虑到这种情况,在adapter中设置了provide_responses选项:
virtual class uvm_reg_adapter extends uvm_object;
bit provides_responses;
endclass
设置此选项后,寄存器模型在调⽤bus2reg将⽬标transaction转换成uvm_reg_item时,传⼊的参数是rsp⽽不是req。使⽤应答机制的操作流程为:
参考模型调⽤寄存器模型的读任务
寄存器模型产⽣sequence,并产⽣uvm_reg_item:rw
产⽣driver能够接受的transaction:bus_2bus(rw)
将bus_req交给bus_sequencer
driver得到bus_req后驱动它得到读取的值,并将读取值放⼊rsp中,调⽤item_done
寄存器模型调⽤adapter.bus2reg(rsp, rw)将rsp中的读取值传递给rw
将rw中的读数据返回参考模型
后门访问操作的定义
为了讲述后门访问操作,从本节开始将引⼊⼀个新的DUT,这个DUT中加⼊了寄存器counter。它的功能就是统计rx_dv为⾼电平的时钟数。
在通信系统中有⼤量计数器⽤于统计各种包裹的数量,如长包、中包、短包等。这些计数器的共同特点是它们是只读的,DUT的总线接⼝⽆法通过前门访问操作对其进⾏写操作。这些寄存器的位宽⼀般都⽐较宽,如32位或64位等,超过了设计中对加法器宽度的上限限制。计数器在计数过程中需要使⽤加法器,对于加法器来说在同等⼯艺下位宽越宽则其时序越差,因此设计时⼀般会规定加法器的最⼤位宽。上述DUT的加法器位宽被限制在16位。实现32位counter的加法操作,需要使⽤两个叠加的16位加法器。
为counter分配16‘h5和16’h6的地址,采⽤⼤端格式将⾼位数据存放在低地址。此计数器是可读的,另外可以对其进⾏写1清0操作。如果对其写⼊其他数值则不会起作⽤。
后门访问是与前门访问相对的操作,⼴义上,所有不通过DUT的总线⽽对DUT内部的寄存器或者存储器进⾏存取的操作都是后门访问操作。如在top_tb中可以使⽤如下⽅式对counter赋初值:
initial begin
@(posedge rst_n);
unter =32'hFFFD;
end
所有后门访问操作都是不消耗仿真时间(即$time打印的时间)⽽只消耗运⾏时间,这是后门访问操作的最⼤优势。它存在的意义在于:
后门访问操作能够更好地完成前门访问操作所做的事情。后门访问不消耗仿真时间,它消耗的运⾏时间要远⼩于前门访问操作的运⾏时间。⼤型芯⽚的验证中,在其正常⼯作前需配置众多的寄存器,配置时间可能要达到⼀个或⼏个⼩时,如果使⽤后门访问操作,则时间可能缩短为原来的1/100。
后门访问操作能够完成前门访问操作不能完成的事情。如在⽹络通信系统中计数器通常都是只读的(有⼀些会附加清零功能),⽆法对其指定⼀个⾮零的初值。⼤部分计数器是多个加法器的叠加,需测试它们的进位操作。本节DUT的counter使⽤两个叠加的16位加法器,需测试当计数到32’hFFFF时能否顺利进位为32’h1_0000,可通过延长仿真时间使其计数到32’hFFFF,这在本节DUT中是可以的,因为计数器每个时钟都加1。但在实际中可能要⼏万个或更多的时钟才会加1,需要⼤量运⾏时间。如果是更⼤位数的计数器,情况则会更坏。这种情况下后门访问操作能够完成前门访问操作完成的事情,给只读的寄存器⼀个初值。
当然,后门访问操作也有其劣势。如所有前门访问操作都可以在波形⽂件中到总线信号变化的波形及所有操作记录。但后门访问⽆法在波形⽂件中到操作痕迹。其操作记录只能仰仗验证平台编写者在进⾏后门访问操作时输出的打印信息,这样增加了调试的难度。
*使⽤interface进⾏后门访问操作
上节提到过在top_tb中使⽤绝对路径对寄存器进⾏后门访问操作,这需要更改top_tb.sv⽂件,但这个⽂件⼀般是固定的,不会因测试⽤例不同⽽变化,所以这种⽅式的可操作性不强。在driver等组件中也可使⽤这种绝对路径的⽅式进⾏后门访问操作,但强烈建议不要在driver 等验证平台的组件中使⽤绝对路径。这种⽅式的可移植性不强。
如果想在driver或monitor中使⽤后门访问,⼀种⽅法是使⽤接⼝。可以新建⼀个后门interface:
interface backdoor_if(input clk, input rst_n);
function void poke_counter(input bit[31:0] value);
_unter = value;
endfunction
function void peek_counter(output bit[31:0] value);
value = _unter;
endfunction
endinterface
poke_counter为后门写,peek_counter为后门读。在测试⽤例中(或者drvier、scoreboard), 若要对寄存器赋初值可以直接调⽤此函数:
task my_case0::configure_phase(uvm_phase phase);
phase.raise_objection(this);
@(posedge vif.rst_n);
vif.poke_counter(32'hFFFD);
phase.drop_objection(this);
endtask
如果有n个寄存器,那么需要写n个poke函数;同时如果有读取要求的话,还要写n个peek函数, 这限制了其使⽤,且此⽂件完全没有任何移植性。
这种⽅式在实际中是有⽤的,它适⽤于不想使⽤寄存器模型提供的后门访问或者根本不想建⽴寄存器模型,同时⼜必须要对DUT中的⼀个寄存器或⼀块存储器(memory)进⾏后门访问操作的情况。
UVM中后门访问操作的实现:DPI+VPI
前⾯提供两种⼴义的后门访问⽅式,它们的共同点都是在SV中实现的。但在实际的验证平台中,还有在C/C++代码中对DUT中的寄存器进⾏读写的需求。Verilog提供VPI接⼝,可以将DUT的层次结构开放给外部的C/C++代码。
常⽤的VPI接⼝有如下两个:
vpi_get_value(obj, p_value);
vpi_put_value(obj, p_value, p_time, flags);
vpi_get_value⽤于从RTL中得到⼀个寄存器的值。
vpi_put_value⽤于将RTL中的寄存器设置为某个值。
但如果单纯使⽤VPI进⾏后门访问操作,在SV与C/C++之间传递参数时将⾮常⿇烦。VPI是Verilog提供
老宝来论坛的接⼝,为了调⽤C/C++中的函数,提供更好的⽤户体验,SV提供了⼀种更好的接⼝:DPI。如果使⽤DPI,以读操作为例,在C/C++中定义如下函数: int
uvm_hdl_read(char *path, p_vpi_vecval value); 在这个函数中通过最终调⽤vpi_get_value得到寄存器的值。
在SV中⾸先需要使⽤如下⽅式将在C/C++中定义的函数导⼊:import “DPI-C” context function int uvm_hdl_read(string path, output uvm_hdl_data_t value);
以后就可以在SV中像普通函数⼀样调⽤uvm_hdl_read函数了。这种⽅式⽐单纯地使⽤VPI的⽅式简练许多。它可以直接将参数传递给
C/C++中的相应函数,省去了单纯使⽤VPI时繁杂的注册系统函数的步骤。
整个过程如下图所⽰:
51二手汽车网
这种DPI+VPI的⽅式,要操作的寄存器路径被抽像成⼀个字符串,⽽不再是⼀个绝对路径:uvm_hdl_read(“_unter”, value);
与前⾯使⽤interface进⾏后门访问操作的代码相⽐,可以发现这种⽅式的优势:路径被抽像成字符串,从⽽以参数的形式传递并可以存储,为建⽴寄存器模型提供了可能。单纯的Verilog路径如_unter,它不能被传递的,也⽆法存储。
UVM中使⽤DPI+VPI的⽅式进⾏后门访问操作,它⼤体的流程是:
1. 在建⽴寄存器模型时将路径参数设置好。
封闭式电动车2. 进⾏后门访问的写操作时,寄存器模型调⽤uvm_hdl_deposit函数:import “DPI-C” context function int
uvm_hdl_deposit(string path, uvm_hdl_data_t value); 在C/C++侧,此函数内部会调⽤vpi_put_value函数来对DUT中的寄存器进⾏写操作。
3. 进⾏后门访问的读操作时,调⽤uvm_hdl_read函数,在C/C++侧,此函数内部会调⽤vpi_get_value函数对DUT中的寄存器进⾏读
操作,并将读取值返回。
*UVM中后门访问操作接⼝
掌握UVM后门访问操作的原理后,就可以使⽤寄存器模型的后门访问功能。使⽤这个功能需要做如下准备:
在reg_block中调⽤uvm_reg的configure函数时,设置好第三个路径参数:
class reg_model extends uvm_reg_block;
rand reg_invert invert;
rand reg_counter_high counter_high;
rand reg_counter_l ow counter_low;
virtual function void build();
figure(this, null,"counter[31:16]");
figure(this, null,"counter[15:0]");
endfunction
endclass
由于counter是32bit,占据两个地址,因此在寄存器模型中它是作为两个寄存器存在的。
当上述⼯作完成后,在将寄存器模型集成到验证平台时,需要设置好根路径hdl_root:
function void base_test::build_phase(uvm_phase phase);
rm = reg_model::type_id::create("rm", this);
rm.build();
rm.lock_model();
rm.set_hdl_path_root("_dut");
endfunction
UVM提供两类后门访问的函数:⼀是UVM_BACKDOOR形式的read和write,⼆是peek和poke。 这两类函数的区别是:第⼀类会在进⾏操作时模仿DUT的⾏为,第⼆类则完全不管DUT的⾏为。如对⼀个只读的寄存器进⾏写操作,那么第⼀类由于要模拟DUT的只读⾏为,所以是写不进去的,但是使⽤第⼆类可以写进去。
poke函数⽤于第⼆类写操作,其原型为:
菱智1.6l商务车task uvm_reg::poke(output uvm_status_e status,
input uvm_reg_data_t value,
input string kind ="",
input uvm_sequence_base parent = null,
input uvm_object extension = null,
input string fname ="",
input int lineno =0);
peek函数⽤于第⼆类的读操作,其原型为:
task uvm_reg::peek(output uvm_status_e status,
output uvm_reg_data_t value,
input string kind ="",
input uvm_sequence_base parent = null,无证驾驶怎么处罚
input uvm_object extension = null,
input string fname ="",
input int lineno =0);
两个函数常⽤的参数都是前两个。各⾃第⼀个参数表⽰操作是否成功,第⼆个参数表⽰读写的数据。
在sequence中可使⽤如下⽅式调⽤这两个任务: