验证平台用到的参数有两大类,一类是验证环境与DUT中都要用到的参数,这些参数通常都对应DUT中的寄存器,前面已经将这些参数组织成一个参数类;另一类是验证环境独有的,比如driver中要发送的preamble数量的上限和下限。本节讲述如何组织这类参数。
大项目要配置的参数可能有千百个。如果全部使用config_db写法,那么就会出现下面这种情况:
classs base_test extends uvm_test;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
uvm_config_db#(int)::set(this, "path1", "var1", 7);
…
uvm_config_db#(int)::set(this, "path1000", "var1000", 999);
endfunction
endclass
可以想像写这1000句set函数将是多么麻烦的一件事情。比较好的方法是将这1000个变量放在一个专门的类里面来实现:
class my_config extends uvm_object;
rand int var1;
…
rand int var1000;
constraint default_cons{
var1 = 7;
…
var1000 = 999;
}
`uvm_object_utils_begin(my_config)
`uvm_field_int(var1, UVM_ALL_ON)
…
`uvm_field_int(var1000, UVM_ALL_ON)
`uvm_object_utils_end
endclass
经过上述定义之后,可以在base_test中这样写:
classs base_test extends uvm_test;
my_config cfg;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
cfg = my_config::type_id::create("cfg");
uvm_config_db#(my_config)::set(this, "env.i_agt.drv", "cfg", cfg);
uvm_config_db#(my_config)::set(this, "env.i_agt.mon", "cfg", cfg);
…
endfunction
endclass
这样省略了绝大多数的set语句。在driver中以如下方式使用这个聚合参数类:
class my_driver extends uvm_driver#(my_transaction);
my_config cfg;
`uvm_component_utils_begin(my_driver)
`uvm_field_object(cfg, UVM_ALL_ON | UVM_REFERENCE)
`uvm_component_utils_end
extern task main_phase(uvm_phase phase);
endclass
task my_driver::main_phase(uvm_phase phase);
while(1) begin
seq_item_port.get_next_item(req);
pre_num = $urand_range(cfg.pre_num_min, cfg.pre_num_max);
…//drive this pkt, and the number of preamble is pre_num
seq_item_port.item_done();
end
endtask
如果在某个测试用例中想要改变某个变量的值,可以这样做:
class case100 extends base_test;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
cfg.pre_num_max = 100;
cfg.pre_num_min = 8;
…
endfunction
endclass
使用聚合参数后,可以将此参数类的指针放在virtual sequencer中:
class my_vsqr extends uvm_sequencer;
my_config cfg;
…
endclass
classs base_test extends uvm_test;
my_config cfg;
my_vsqr vsqr;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
cfg = my_config::type_id::create("cfg");
vsqr = my_vsqr::type_id::create("vsqr", this);
vsqr.cfg = this.cfg;
…
endfunction
endclass
这样,当sequence要动态地改变某个验证平台中的变量值时,可使用如下方式:
class vseq extends uvm_sequence;
`uvm_object_utils(vseq)
`uvm_declare_p_sequencer(vsequencer)
task body();
…//send some transaction
p_sequencer.cfg.pre_num_max = 99;
…//send other transaction
endtask
endclass
聚合参数方便了在sequence中改变验证平台参数。某些情况下甚至可以将interface也放入此聚合参数类中:
class my_config extends uvm_object;
`uvm_object_utils(my_config)
virtual my_if vif;
function new(string name = "my_config");
super.new(name);
$display("%s", get_full_name());
if(!uvm_config_db#(virtual my_if)::get(null, get_full_name(), "vif", vif))
`uvm_fatal("my_config", "please set interface")
endfunction
endclass
这样无论在driver还是monitor,都可直接使用cfg.vif,而不必使用config_db得到interface:
task my_driver::main_phase(uvm_phase phase);
cfg.vif.data <= 8'b0;
cfg.vif.valid <= 1'b0;
while(!cfg.vif.rst_n)
@(posedge cfg.vif.clk);
…
endtask
同样,如果将该cfg的指针赋值给普通sequencer,那么在前面心跳sequence的实现中,sequencer也不必再使用uvm_config_db::get得到接口。
上面my_config中使用config_db的形式得到vif。这里出现了uvm_config_db::get(),由于my_config是一个object而不是component,所以get_full_name得到的结果是其实例化时指定的名字。所以base_test中实例化cfg的名字要与top_tb中config_db::set的路径参数一致。如:
function void base_test::build_phase(uvm_phase phase);
…
cfg = new("cfg");
endfunction
module top_tb;
…
initial begin
uvm_config_db#(virtual my_if)::set(null, "cfg", "vif", input_if);
end
endmodule
或者:
function void base_test::build_phase(uvm_phase phase);
…
cfg = new({get_full_name(), ".cfg"});
endfunction
module top_tb;
…
initial begin
uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.cfg", "vif", input_if);
end
endmodule
最方便还是用直接赋值的形式。在top_tb中将interface通过config_db::set方式传递给base_test,在base_test中实例化cfg后就可以直接赋值:
function void base_test::build_phase(uvm_phase phase);
…
cfg = new("cfg");
cfg.vif = this.vif.
endfunction
将所有参数聚合的做法方便了验证平台的搭建。将这个聚合类的指针赋值给任意component,这些component再也不必用config_db::get函数来获取参数。当验证平台某个组件要增加参数时,只需在这个聚合类中加入此参数,在测试用例中直接为其赋值,然后在验证平台中就可直接使用:
class my_config extends uvm_object;
rand int new_var;
…
endclass
function void my_case0::build_phase(uvm_phase phase);
…
cfg.new_var = 1;
endfunction
task my_driver::main_phase(uvm_phase phase);
if(cfg.new_var)
…
endtask
假如不使用聚合类而使用config_db,那么需要在测试用例中进行设置:
function void my_case0::build_phase(uvm_phase phase);
…
uvm_config_db#(int)::set(this, "uvm_test_top.env.i_agt.drv", "new_var", 1);
endfunction
在driver中增加一个变量并使用get语句获取它:
function void my_driver::build_phase(uvm_phase phase);
…
void'(uvm_config_db#(int)::get(this, "", "new_var", new_var);
endfunction
task my_driver::main_phase(uvm_phase phase);
if(new_var)
…
endtask
可以看出使用聚合类减少了config_db::set的使用,也大大降低出错的概率。
但聚合参数也不是完美的。聚合参数的本质是将一些属于某个uvm_component的变量变成对所有uvm_component可见,从而使得这些变量错误地被其他uvm_component修改。
另外,聚合参数整合了整个验证平台的参数,一定程度上降低了验证平台的可重用性。 之前讲述了env级别的重用,聚合参数类对于这种重用没有任何问题。但在实际中能够做到env级别重用的IC公司并不多。很多公司使用的是基于agent的重用。
假如某个agent中需要的参数只占聚合参数类10%的参数,现在这个agent被其他项目重用,那么在新项目中也需实例化这个聚合参数类。但在新项目中这个聚合参数类可能90%的参数没有用。解决这个问题的方式是缩小聚合参数的粒度,将一个聚合参数类分成多个小的聚合参数类,如将agent所有参数定义为一个聚合参数类,在大的聚合参数类中实例化这个小的聚合参数类。只是这样可能每个聚合参数类中只有一两个参数。与直接使用config_db相比并没有方便多少。
前面介绍使用config_db几乎都是在build_phase中。由于其config_db::set的第二个参数是字符串而经常出错。要想避免config_db::set第二个参数引起的问题,一种可行的想法是把这个参数使用get_full_name()。
如在测试用例中对driver中某个参数进行设置:
uvm_config_db#(int)::set(null, env.i_agt.drv.get_full_name(), "pre_num", 100);
若要对sequence的某个参数设置,可以:
uvm_config_db#(int)::set(null, {env.i_agt.sqr.get_full_name(),“.*”}, "pre_num", 100);
但在build_phase时,整棵UVM树还没有形成,使用env.i_agt.drv的形式进行引用会引起空指针的错误。所以要想这么使用有两种方法,一种是所有的实例化工作都在各自的new函数中完成:
function base_test::new(string name, uvm_component parent);
super.new(name, parent);
env = my_env::type_id::create("env", this);
endfunction
function my_env::new(string name, uvm_component parent);
super.new(name, parent);
i_agt = my_agent::type_id::create("i_agt", this);
o_agt = my_agent::type_id::create("o_agt", this);
…
endfunction
…
这种情况下当整个验证平台运行到build_phase时UVM树已实例化完毕,在uvm_confg_db::set中使用get_full_name没有任何问题。
第二种方式是将uvm_config_db::set移到connect_phase中去。由于connect_phase是由下向上执行,base_test(或者测试用例)的connect_phase几乎是最后执行的,因此应该在end_of_elaboration_phase或start_of_simulation_phase调用uvm_config_db::get。
function void my_case0::connect_phase(uvm_phase phase);
uvm_config_db#(int)::set(null, env.i_agt.drv.get_full_name();, "pre_num", 100);
endfunction
function void my_driver::end_of_elaboration_phase(uvm_phase phase);
void'(uvm_config_db#(int)::get(this, "", "pre_num", pre_num));
endfunction
两种方式对top_tb的config_db::set无效,因为在top_tb中很难用env.i_agt.sqr.get_full_name()的类似方式获得一个路径值。幸运的是top_tb中的config_db::set语句不多,且它们相对比较固定,通常不会出问题。
已经见识到config_db::set函数的第二个参数带来的不便,因此要尽量减少config_db的使用。那么有没有可能完全不使用config_db?
完全可以。config_db设置的参数有两种,一种是结构性的参数,如控制driver是否实例化的参数is_active:
function void my_agent::build_phase(uvm_phase phase);
super.build_phase(phase);
if (is_active == UVM_ACTIVE) begin
sqr = my_sequencer::type_id::create("sqr", this);
drv = my_driver::type_id::create("drv", this);
end
mon = my_monitor::type_id::create("mon", this);
endfunction
对于这种参数,可以在实例化agent时同时指明其is_active的值:
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
if(!in_chip) begin
i_agt = my_agent::type_id::create("i_agt", this);
i_agt.is_active = UVM_ACTIVE;
end
…
endfunction
这是本书一直使用的方式。对于在build_phase中设置的一些非结构性的参数,如向某个driver传递某个参数:
uvm_config_db#(int)::set(this, "env.i_agt.drv", "pre_num", 100);
可以完全在build_phase之后的任意phase中使用绝对路径引用进行设置:
function void my_case0::connect_phase(uvm_phase phase);
env.i_agt.drv.pre_num = 100;
endfunction
对于向sequence传递参数,可在virtual sequence中启动sequence并通过赋值的方式传递。但这样的前提是virtual sequence已经启动。那么如何启动virtual sequence呢?大部分例子都通过default_sequence启动:
function void my_case0::build_phase(uvm_phase phase);
super.build_phase(phase);
uvm_config_db#(uvm_object_wrapper)::set(this, "v_sqr.main_phase",
"default_sequence",
case0_vseq::type_id::get());
endfunction
但其实可以在测试用例的main_phase中手工启动此sequence:
task my_case0::main_phase(uvm_phase phase);
case0_vseq vseq;
super.main_phase(phase);
vseq = new("vseq");
vseq.starting_phase = phase;
vseq.start(vsqr);
endtask
这样可以不用再在build_phase中设置default_sequence。
如何为sequence中的set语句寻找替代者呢?可通过uvm_root::get()得到UVM树真正的根uvm_top,从uvm_top的孩子中找到base_test(大多数情况下uvm_top只有一个名字为uvm_test_top的孩子,不过也不能排除有多个孩子的情况)的实例,并通过绝对路径引用赋值:
class case0_vseq extends uvm_sequence;
…
virtual task body();
my_transaction tr;
drv0_seq seq0;
drv1_seq seq1;
base_test test_top;
uvm_component children[$];
uvm_top.get_children(children);//notice
foreach(children[i]) begin
if($cast(test_top, children[i])) ;
end
if(test_top == null)
`uvm_fatal("case0_vseq", "can't find base_test 's instance")
fork
`uvm_do_on(seq0, p_sequencer.p_sqr0);
`uvm_do_on(seq1, p_sequencer.p_sqr1);
begin
#10000;
//uvm_config_db#(bit)::set(uvm_root::get(), "uvm_test_top.env0.scb", "cmp_en", 0);
test_top.env0.scb.cmp_en = 0;
#10000;
//uvm_config_db#(bit)::set(uvm_root::get(), "uvm_test_top.env0.scb", "cmp_en", 1);
test_top.env0.scb.cmp_en = 1;
end
join
#100;
endtask
endclass
至于在top_tb中使用config_db对interface进行的传递,可使用绝对路径的方式:
function void base_test::connect_phase(uvm_phase phase);
env0.i_agt.drv.vif = testbench.input_if0;
…
endfunction
这里用到了绝对路径。如果不使用绝对路径,可通过静态变量实现。新建一个将此验证平台中所有可能用到的interface包含的类中作为成员变量:
class if_object extends uvm_object;
…
static if_object me;
static function if_object get();
if(me == null) begin
me = new("me");
end
return me;
endfunction
virtual my_if input_vif0;
virtual my_if output_vif0;
virtual my_if input_vif1;
virtual my_if output_vif1;
endclass
在top_tb中为这个类的interface赋值:
module top_tb;
…
initial begin
if_object if_obj;
if_obj = if_object::get();
if_obj.input_vif0 = input_if0;
if_obj.input_vif1 = input_if1;
if_obj.output_vif0 = output_if0;
if_obj.output_vif1 = output_if1;
end
endmodule
get函数是if_object的一个静态函数,通过它可得到if_object的一个实例,并对此实例中的interface进行赋值。
在base_test的connect_phase(或build_phase之后的任一phase)对所有的interface进行赋值:
function void base_test::connect_phase(uvm_phase phase);
if_object if_obj;
if_obj = if_object::get();
v_sqr.p_sqr0 = env0.i_agt.sqr;
v_sqr.p_sqr1 = env1.i_agt.sqr;
env0.i_agt.drv.vif = if_obj.input_vif0;
env0.i_agt.mon.vif = if_obj.input_vif0;
env0.o_agt.mon.vif = if_obj.output_vif0;
env1.i_agt.drv.vif = if_obj.input_vif1;
env1.i_agt.mon.vif = if_obj.input_vif1;
env1.o_agt.mon.vif = if_obj.output_vif1;
endfunction
使用上述方式,可以在验证平台中完全避免config_db的使用。
无论如何,config_db机制是UVM一项重要的机制,完全不用config_db是走向另一个极端。config_db机制最大问题在于不对set函数的第二个参数进行检查。本节介绍一个函数,可在一定程度上(并不能检查所有!)实现对第二个参数有效性的检查。可将这个函数加入到自己的验证平台中。
函数的代码如下:
function void check_all_config();
check_config::check_all();
endfunction
这个全局函数会调用check_config的静态函数check_all:
static function void check_all();
uvm_component c;
uvm_resource_pool rp;
uvm_resource_types::rsrc_q_t rq;
uvm_resource_types::rsrc_q_t q;
uvm_resource_base r;
uvm_resource_types::access_t a;
uvm_line_printer printer;
c = uvm_root::get();
if(!is_inited)
init_uvm_nodes(c);
rp = uvm_resource_pool::get();
q = new;
printer=new();
foreach(rp.rtab[name]) begin
rq = rp.rtab[name];
for(int i = 0; i < rq.size(); ++i) begin
r = rq.get(i);
//$display("r.scope = %s", r.get_scope());
if(!path_reachable(r.get_scope)) begin
`uvm_error("check_config", "the following config_db::set's path is not reachable in
r.print(printer);
r.print_accessors();
end
end
end
endfunction
该函数先根据is_inited值来调用init_nodes函数,将uvm_nodes联合数组初始化。is_inited和uvm_nodes是check_config的两个静态成员变量:
class check_config extends uvm_object;
static uvm_component uvm_nodes[string];
static bit is_inited = 0;
在init_nodes函数中用递归的方式遍历整棵UVM树并将树上所有结点加入到uvm_nodes中。uvm_nodes的索引是相应结点的get_full_name值,而存放的值就是相应结点的指针:
static function void init_uvm_nodes(uvm_component c);
uvm_component children[$];
string cname;
uvm_component cn;
uvm_sequencer_base sqr;
is_inited = 1;
if(c != uvm_root::get()) begin
cname = c.get_full_name();
uvm_nodes[cname] = c;
if($cast(sqr, c)) begin
string tmp;
$sformat(tmp, "%s.pre_reset_phase", cname);
uvm_nodes[tmp] = c;
…
$sformat(tmp, "%s.main_phase", cname);
uvm_nodes[tmp] = c;
…
$sformat(tmp, "%s.post_shutdown_phase", cname);
uvm_nodes[tmp] = c;
end
end
c.get_children(children);
while(children.size() > 0) begin
cn = children.pop_front();
init_uvm_nodes(cn);
end
endfunction
初始化工作只进行一次。下次调用此函数将不会进行初始化。初始化完成后check_all函数会遍历config_db库中所有记录。对任意记录检查其路径参数,并将该参数与uvm_nodes中所有路径参数对比,如果能够匹配,说明这条路径在验证平台中是可达的。这里调用了path_reachable函数:
static function bit path_reachable(string scope);
bit err;
int match_num;
match_num = 0;
foreach(uvm_nodes[i]) begin
err = uvm_re_match(scope, i);
if(err) begin
//$display("not_match: name is %s, scope is %s", i, scope);
end
else begin
//$display("match: name is %s, scope is %s", i, scope);
match_num++;
end
end
return (match_num > 0);
endfunction
config_db::set的第二个参数支持通配符,所以path_reachable通过调用uvm_re_match函数检查路径是否匹配。uvm_re_match是UVM实现的一个函数,检查两条路径是否一样。uvm_nodes遍历完成后如果匹配的数量为0,说明路径根本不可达,此时将给出一个UVM_ERROR的提示。
在UVM中使用很多的是default_sequence的设置:
uvm_config_db#(uvm_object_wrapper)::set(this,
"env.i_agt.sqr.main_phase",
"default_sequence",
case0_sequence::type_id::get());
在这个设置的第二个参数中出现了main_phase。如果只是将sequencer的get_full_name的结果与这个路径相比,那么path_reachable函数认为不匹配。所以init_nodes函数对sequencer在其中加入了对各phase的支持。
由于要遍历整棵UVM树的结点,所以check_all_config函数只能在build_phase之后才能被调用,如connect_phase等。不匹配时它会给出一条UVM_ERROR的提示信息,如前面在i_agt后插入一个空格,它将会给出如下错误提示:
UVM_ERROR check_config.sv(101) @ 0: reporter [check_config] the following config_db::set's path is
not reachable in your verification environment, please check
default_sequence [/^uvm_test_top\.env\.i_agt \.sqr\.main_phase$/] : (class uvm_pkg::uvm_object_wrapper) ?
default_sequence: (<unknown>@478) @478
--------
uvm_test_top reads: 0 @ 0 writes: 1 @ 0
这个函数可以在很多地方调用。如在sequence中使用config_db::set函数后就可以立即调用该函数检查有效性。
需要说明的是这个函数有一些局限,其中之一就是不支持config_db::set向object传递的参数,如前面向my_config传递virtual interface出现错误就不能通过该函数检查出来。幸运的是这种传递参数的方式并不多见,出现错误的概率也比较低。