UVM 的factory 机制

factory 机制本质上就是对重载的支持, SystemVerilog 天然支持重载

任务与函数的重载

SystemVerilog是一种面向对象的语言。 面向对象语言都有一大特征: 重载。 当在父类中定义一个函数/任务时, 如果将其设置
为virtual类型, 那么就可以在子类中重载这个函数/任务:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
文件: src/ch8/section8.1/8.1.1/my_case0.sv
class bird extends uvm_object;
    virtual function void hungry();
        $display("I am a bird, I am hungry");
    endfunction
    function void hungry2();
        $display("I am a bird, I am hungry2");
    endfunction
    
endclass
class parrot extends bird;
    virtual function void hungry();
        $display("I am a parrot, I am hungry");
    endfunction
    function void hungry2();
        $display("I am a parrot, I am hungry2");
    endfunction
    
endclass

上述代码中的hungry就是虚函数, 它可以被重载。 但是hungry2不是虚函数, 不能被重载。 重载的最大优势是使得一个子类的
指针以父类的类型传递时, 其表现出的行为依然是子类的行为

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
文件: src/ch8/section8.1/8.1.1/my_case0.sv
function void my_case0::print_hungry(bird b_ptr);
    b_ptr.hungry();
    b_ptr.hungry2();
endfunction
function void my_case0::build_phase(uvm_phase phase);
    bird bird_inst;
    parrot parrot_inst;
    super.build_phase(phase);
    bird_inst = bird::type_id::create("bird_inst");
    parrot_inst = parrot::type_id::create("parrot_inst");
    print_hungry(bird_inst);
    print_hungry(parrot_inst);
endfunction

输出结果:

1
2
3
4
5
"I am a bird, I am hungry"
"I am a bird, I am hungry2"

"I am a parrot, I am hungry"
"I am a bird, I am hungry2"

只有虚函数能被重载
这种函数/任务重载的功能在UVM中得到了大量的应用。
其实最典型的莫过于各个phase。 当各个phase被调用时, 以 build_phase为例, 实际上系统是使用如下的方式调用:

1
c_ptr.build_phase();

其中c_ptr是uvm_component类型的, 而不是其他类型, 如my_driver( 但是c_ptr指向的实例却是my_driver类型的) 。 在一个验
证平台中, UVM树上的结点是各个类型的, UVM不必理会它们具体是什么类型, 统一将它们当作uvm_component来对待, 这极大
方便了管理。

约束的重载

使用场景

在测试一个接收MAC功能的DUT时, 有多种异常情况需要测试, 如preamble错误、 sfd错误、 CRC错误等。
针对这些错误, 在 transaction中分别加入标志位:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
文件: src/ch8/section8.1/8.1.2/rand_mode/my_transaction.sv
class my_transaction extends uvm_sequence_item;
    rand bit[47:0] dmac;
    rand bit [47:0] smac;
    rand bit [15:0] ether_type;
    rand byte       pload[];
    rand bit [31:0] crc;
    rand bit        crc_err;
    rand bit        sfd_err;
    rand bit        pre_err;
    
        `uvm_object_utils_begin(my_transaction)
            `uvm_field_int(dmac, UVM_ALL_ON)
            `uvm_field_int(smac, UVM_ALL_ON)
            `uvm_field_int(ether_type, UVM_ALL_ON)
            `uvm_field_array_int(pload, UVM_ALL_ON)
            `uvm_field_int(crc, UVM_ALL_ON)
            `uvm_field_int(crc_err, UVM_ALL_ON | UVM_NOPACK)
            `uvm_field_int(sfd_err, UVM_ALL_ON | UVM_NOPACK)
            `uvm_field_int(pre_err, UVM_ALL_ON | UVM_NOPACK)
        `uvm_object_utils_end
    
endclass

这些错误都是异常的情况, 在大部分测试用例中, 它们的值都应该为0。 如果在每次产生transaction时进行约束会非常麻烦:

1
uvm_do_with(tr, {tr.crc_err == 0; sfd_err == 0; pre_err == 0;})

由于它们出现的概率非常低, 因此结合SystemVerilog中的dist, 在定义transaction时指定如下的约束:

1
2
3
4
5
constraint default_cons{
crc_err dist{0 := 999_999_999, 1 := 1};
pre_err dist{0 := 999_999_999, 1 := 1};
sfd_err dist{0 := 999_999_999, 1 := 1};
}

上述语句的意思是, 在随机化时, crc_err、 pre_err和sfd_err只有1/1_000_000_000的可能性取值会为1, 其余均为0。 这看似非
常令人满意, 但是其中最大的问题是其何时取1、 何时取0是无法控制的。 如果某个测试用例用于测试正常的功能, 里面则不能有
错误产生, 换句话说, crc_err、 pre_err和sfd_err的值要一定为0。 上面的constraint明显不能满足这种要求, 因为虽然只有
1/1_000_000_000的可能性, 但是这种可能性依然存在。 在运行特别长的测试用例时, 如发送了1_000_000_000个包, 那么这其中
有非常大的可能性会产生一个crc_err、 pre_err或sfd_err值为1的包。
要解决上述问题, 有两种解决方案。

麻烦的解决方案

在定义transaction时, 使用如下的方式定义constraint:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
文件: src/ch8/section8.1/8.1.2/rand_mode/my_transaction.sv
class my_transaction extends uvm_sequence_item;
    
    constraint crc_err_cons{
        crc_err == 1'b0;
    }
    constraint sfd_err_cons{
        sfd_err == 1'b0;
    }
    constraint pre_err_cons{
        pre_err == 1'b0;
    }
    
endclass

在正常的测试用例中, 可以使用如下方式随机化:

1
2
my_transaction tr;
`uvm_do(tr)

在异常的测试用例中, 可以使用如下方式随机化:

1
2
3
4
5
6
7
8
9
文件: src/ch8/section8.1/8.1.2/rand_mode/my_case0.sv
virtual task body();
    
    m_trans = new();
    `uvm_info("sequence", "turn off constraint", UVM_MEDIUM)
    m_trans.crc_err_cons.constraint_mode(0);
    `uvm_rand_send_with(m_trans, {crc_err dist {0 := 2, 1 := 1};})
    
endtask

能够使用这种方式的前提是m_trans已经实例化。 如果不实例化, 直接使用uvm_do宏:

1
2
3
my_transaction m_trans;
m_trans.crc_err_cons.constraint_mode(0);
`uvm_do(m_trans)

这样会报空指针的错误。
sfd_err与pre_err的情况也可以使用类似的方式实现。 上述语句中只是单独地关闭了某一个约束, 也可以使用如下的语句关闭所有的约束:

1
m_trans.constraint_mode(0);

在这种情况下, 随机化时就需要分别对crc_err、 pre_err及sfd_err进行约束。

推荐的解决方案

SystemVerilog中一个非常有用的特性是支持约束的重载。 因此, 依然使用第一种方式中my_transaction的定义,
在其基础上派生一个新的transaction:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
文件: src/ch8/section8.1/8.1.2/override/my_case0.sv
class new_transaction extends my_transaction;
    `uvm_object_utils(new_transaction)
    function new(string name= "new_transaction");
        super.new(name);
    endfunction
    constraint crc_err_cons{
        crc_err dist {0 := 2, 1 := 1};    // 2/3 是0 , 1/3 是 1
    }
endclass

在这个新的transaction中将crc_err_cons重载了。 因此, 在异常的测试用例中, 可以使用如下的方式随机化:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
文件: src/ch8/section8.1/8.1.2/override/my_case0.sv
virtual task body();
    new_transaction ntr;
    
        repeat (10) begin
            `uvm_do(ntr)
            ntr.print();
        end
    
endtask

使用factory机制进行重载

factory 机制式的重载

factory机制最伟大的地方在于其具有重载功能。 重载并不是factory机制的发明, 前面已经介绍过的所有面向对象的语言都支
持函数/任务重载, 另外, SystemVerilog还额外支持对约束的重载。 只是factory机制的重载与这些重载都不一样.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
文件: src/ch8/section8.2/8.2.1/my_case0.sv
class bird extends uvm_object;
    virtual function void hungry();
        $display("I am a bird, I am hungry");
    endfunction
    function void hungry2();
        $display("I am a bird, I am hungry2");
    endfunction
    
endclass
class parrot extends bird;
    virtual function void hungry();
        $display("I am a parrot, I am hungry");
    endfunction
    function void hungry2();
        $display("I am a parrot, I am hungry2");
    endfunction
    
endclass
1
2
3
4
5
6
7
8
9
文件: src/ch8/section8.2/8.2.1/correct/my_case0.sv
function void my_case0::build_phase(uvm_phase phase);
    
    set_type_override_by_type(bird::get_type(), parrot::get_type());
    bird_inst = bird::type_id::create("bird_inst");
    parrot_inst = parrot::type_id::create("parrot_inst");
    print_hungry(bird_inst);
    print_hungry(parrot_inst);
endfunction

结果:

1
2
3
4
"I am a parrot, I am hungry"
"I am a bird, I am hungry2"
"I am a parrot, I am hungry"
"I am a bird, I am hungry2"

虽然print_hungry接收的是bird类型的参数, 但是从运行结果可以推测出来, 无论是第一次还是第二次调用print_hungry, 传递
的都是类型为bird但是指向parrot的指针。 对于第二次调用, 可以很好理解, 但第一次却使人很难接受。 这就是factory机制的重载
功能, 其原理如下图所示:

虽然bird_inst在实例化以及传递给hungry的过程中, 没有过与parrot的任何接触, 但是它最终指向了一个parrot的实例。 这是因
为bird_inst使用了UVM的factory机制式的实例化方式:

1
bird_inst = bird::type_id::create("bird_inst");

在实例化时, UVM会通过factory机制在自己内部的一张表格中查看是否有相关的重载记录。 set_type_override_by_type语句相当于在factory机制的表格中加入了一条记录。
当查到有重载记录时, 会使用新的类型来替代旧的类型。 所以虽然在build_phase中写明创建bird的实例, 但是最终却创建了parrot的实例

使用factory机制的重载是有前提

第一, 无论是重载的类(parrot) 还是被重载的类(bird) , 都要在定义时注册到factory机制中。

第二, 被重载的类(bird) 在实例化时, 要使用factory机制式的实例化方式, 而不能使用传统的new方式

第三, 最重要的是, 重载的类(parrot) 要与被重载的类(bird) 有派生关系。 重载的类必须派生自被重载的类, 被重载的类必须是重载类的父类

在有多个重载时, 最终重载的类要与最初被重载的类有派生关系。 最终重载的类必须派生自最初被重载的类, 最初被重载的
类必须是最终重载类的父类。

第四, component与object之间互相不能重载。 虽然uvm_component是派生自uvm_object, 但是这两者的血缘关系太远了, 远到根本不能重载。 从两者的new参数的函数就可以看出来, 二者互相重载时, 多出来的一个parent参数会使factory机制无所适从

重载的方式及种类

set_type_override_by_type

set_type_override_by_type函数可以实现两种不同类型之间的重载。 这个函数位于uvm_component中, 其原型是:

1
extern static function void set_type_override_by_type (uvm_object_wrapper original_type, uvm_object_wrapper override_type, bit replace=1);
  • 第一个参数是被重载的类型,
  • 第二个参数是重载的类型。
  • 第三个参数是replace

set_type_override_by_type 会把平台中所有的类型重载

set_inst_override_by_type

但是有时候可能并不是希望把验证平台中的A类型全部替换成B类型, 而只是替换其中的某一部分,
这种情况就要用到 set_inst_override_by_type函数。 这个函数的原型如下:

1
extern function void set_inst_override_by_type(string relative_inst_path, uvm_object_wrapper original_type, uvm_object_wrapper override_type);
  • 第一个参数是相对路径,
  • 第二个参数是被重载的类型,
  • 第三个参数是要重载的类型。

假设有如下的monitor定义:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
文件: src/ch8/section8.2/8.2.2/my_case0.sv
class new_monitor extends my_monitor;
    `uvm_component_utils(new_monitor)
    
    virtual task main_phase(uvm_phase phase);
        fork
            super.main_phase(phase);
        join_none
        `uvm_info("new_monitor", "I am new monitor", UVM_MEDIUM)
    endtask
endclass


如上图要将env.o_agt.mon替换成new_monitor:

1
set_inst_override_by_type("env.o_agt.mon", my_monitor::get_type(), new_monitor::get_type());

经过上述替换后, 当运行到main_phase时, 会输出下列语句:

1
I am new_monitor

无论是set_type_override_by_type还是set_inst_override_by_type, 它们的参数都是一个uvm_object_wrapper型的类型参数,
这种参数通过xxx::get_type() 的形式获得。

set_type_override

UVM还提供了另外一种简单的方法来替换这种晦涩的写法: 字符串。
与set_type_override_by_type相对的是set_type_override, 它的原型是:

1
extern static function void set_type_override(string original_type_name, string override_type_name, bit replace=1);

要使用parrot替换bird, 只需要添加如下语句

1
set_type_override("bird", "parrot")

set_inst_override

与set_inst_override_by_type相对的是set_inst_override, 它的原型是:

1
extern function void set_inst_override(string relative_inst_path, string original_type_name, string override_type_name);

对于上面使用new_monitor重载my_monitor的例子, 可以使用如下语句:

1
set_inst_override("env.o_agt.mon", "my_driver", "new_monitor");

在无法使用 component 的地方使用以下四个函数

上述的所有函数都是uvm_component的函数, 但是如果在一个无法使用component的地方, 如在top_tb的initial语句里, 就无法使用。
UVM提供了另外四个函数来替换上述的四个函数, 这四个函数的原型是:

1
2
3
4
extern function void set_type_override_by_type (uvm_object_wrapper original_type, uvm_object_wrapper override_type, bit replace=1);
extern function void set_inst_override_by_type (uvm_object_wrapper original_type, uvm_object_wrapper override_type, string full_inst_path);
extern function void set_type_override_by_name (string original_type_name, string override_type_name, bit replace=1);
extern function void set_inst_override_by_name (string original_type_name, string override_type_name, string full_inst_path);

这四个函数都位于uvm_factory类中, 其中

  • 第一个函数与uvm_component中的同名函数类似, 传递的参数相同。
  • 第二个对应 uvm_component中的同名函数, 只是其输入参数变了, 这里需要输入一个字符串类型的full_inst_path。
    这个full_inst_path就是要替换的实例中使用get_full_name() 得到的路径值。
  • 第三个与uvm_component中的set_type_override类似, 传递的参数相同。
  • 第四个函数对应uvm_component中的set_inst_override, 也需要一个full_inst_path。

如何使用这四个函数呢? 系统中存在一个uvm_factory类型的全局变量factory。 可以在initial语句里使用如下的方式调用这四个函数

1
2
3
initial begin
factory.set_type_override_by_type(bird::get_type(), parrot::get_type());
end

在一个component内也完全可以直接调用factory机制的重载函数:

1
factory.set_type_override_by_type(bird::get_type(), parrot::get_type());

事实上, uvm_component的四个重载函数直接调用了factory的相应函数。
除了可以在代码中进行重载外, 还可以在命令行中进行重载。 对于实例重载和类型重载, 分别有各自的命令行参数:

1
2
<sim command> +uvm_set_inst_override=<req_type>,<override_type>,<full_inst_path>
<sim command> +uvm_set_type_override=<req_type>,<override_type>[,<replace>]

这两个命令行参数分别对应于 set_inst_override_by_name 和 set_type_override_by_name
对于实例重载:

1
<sim command> +uvm_set_inst_override="my_monitor,new_monitor,uvm_test_top.env.o_agt.mon"

对于类型重载:

1
<sim command> +uvm_set_type_override="my_monitor,new_monitor"

类型重载的命令行参数中有三个选项, 其中最后一个replace表示是否可以被后面的重载覆盖。
它的含义与代码清单8-20中的 replace一样, 将会在下节讲述

复杂的重载

UVM支持连续的重载。 依然以bird与parrot的例子讲述, 现在从parrot又派生出了一个新的类big_parrot:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
文件: src/ch8/section8.2/8.2.3/consecutive/my_case0.sv
class big_parrot extends parrot;
    virtual function void hungry();
        $display("I am a big_parrot, I am hungry");
    endfunction
    function void hungry2();
        $display("I am a big_parrot, I am hungry2");
    endfunction
    `uvm_object_utils(big_parrot)
    function new(string name = "big_parrot");
        super.new(name);
    endfunction
endclass

在build_phase中设置如下的连续重载, 并调用print_hungry函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
文件: src/ch8/section8.2/8.2.3/consecutive/my_case0.sv
function void my_case0::build_phase(uvm_phase phase);
    bird bird_inst;
    parrot parrot_inst;
    super.build_phase(phase);
    set_type_override_by_type(bird::get_type(), parrot::get_type());
    set_type_override_by_type(parrot::get_type(), big_parrot::get_type());
    bird_inst = bird::type_id::create("bird_inst");
    parrot_inst = parrot::type_id::create("parrot_inst");
    print_hungry(bird_inst);
    print_hungry(parrot_inst);
endfunction

最终输出的都是:

1
2
# I am a big_parrot, I am hungry
# I am a bird, I am hungry2

除了这种连续的重载外, 还有一种是替换式的重载。 假如从bird派生出了新的鸟sparrow:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
文件: src/ch8/section8.2/8.2.3/replace/my_case0.sv
class sparrow extends bird;
    virtual function void hungry();
        $display("I am a sparrow, I am hungry");
    endfunction
    function void hungry2();
        $display("I am a sparrow, I am hungry2");
    endfunction
    `uvm_object_utils(sparrow)
    function new(string name = "sparrow");
        super.new(name);
    endfunction
endclass

在build_phase中设置如下重载:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
文件: src/ch8/section8.2/8.2.3/replace/my_case0.sv
function void my_case0::build_phase(uvm_phase phase);
    bird bird_inst;
    parrot parrot_inst;
    super.build_phase(phase);
    set_type_override_by_type(bird::get_type(), parrot::get_type());
    set_type_override_by_type(bird::get_type(), sparrow::get_type());
    bird_inst = bird::type_id::create("bird_inst");
    parrot_inst = parrot::type_id::create("parrot_inst");
    print_hungry(bird_inst);
    print_hungry(parrot_inst);
endfunction

那么最终的输出结果是:

1
2
3
4
# I am a sparrow, I am hungry
# I am a bird, I am hungry2
# I am a parrot, I am hungry
# I am a bird, I am hungry2

这种替换式重载的前提是调用set_type_override_by_type时, 其第三个replace参数被设置为1( 默认情况下即为1) 。 如果为0,
那么最终得到的结果将会是:

1
2
3
4
# I am a parrot, I am hungry
# I am a bird, I am hungry2
# I am a parrot, I am hungry
# I am a bird, I am hungry2

在创建bird的实例时, factory机制查询到两条相关的记录, 它并不会在看完第一条记录后即直接创建一个parrot的实例, 而是
最终看完第二条记录后才会创建sparrow的实例。 由于是在读取完最后的语句后才可以创建实例, 所以其实下列的重载方式也是允
许的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
文件: src/ch8/section8.2/8.2.3/strange/my_case0.sv
function void my_case0::build_phase(uvm_phase phase);
    bird bird_inst;
    super.build_phase(phase);
    set_type_override_by_type(bird::get_type(), parrot::get_type());
    set_type_override_by_type(parrot::get_type(), sparrow::get_type(), 0);
    // parrot_inst = parrot::type_id::create("parrot_inst");    // 这句不删会报错
    bird_inst = bird::type_id::create("bird_inst");
    print_hungry(bird_inst);
endfunction

最终输出的结果是:

1
2
# I am a sparrow, I am hungry
# I am a bird, I am hungry2

sparrow并没有派生自parrot, 但是依然可以重载parrot。 但是这样使用依然是有条件的, 最终创建出的实例是sparrow类型的,
而最初是bird类型的, 这两者之间依然有派生关系。 所以重载四前提的第三条应该改为:
在有多个重载时, 最终重载的类要与最初被重载的类有派生关系。 最终重载的类必须派生自最初被重载的类, 最初被重载的
类必须是最终重载类的父类。

factory机制的调试

factory机制的重载功能很强大, UVM提供了print_override_info函数来输出所有的打印信息,
以上节中的new_monitor重载my_monitor为例

1
set_inst_override_by_type("env.o_agt.mon", my_monitor::get_type(), new_monito r::get_type());

验证平台中仅仅有这一句重载语句, 那么调用print_override_info函数打印的方式为:

1
2
3
4
5
文件: src/ch8/section8.2/8.2.4/my_case0.sv
function void my_case0::connect_phase(uvm_phase phase);
    super.connect_phase(phase);
    env.o_agt.mon.print_override_info("my_monitor");
endfunction

最终输出的信息为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Given a request for an object of type 'my_monitor' with an instance
# path of 'uvm_test_top.env.o_agt.mon', the factory encountered
# the following relevant overrides. An 'x' next to a match indicates a
# match that was ignored.
##
Original Type Instance Path Override Type
# ------------- -------------------------- -------------
# my_monitor uvm_test_top.env.o_agt.mon new_monitor
##
Result:
##
The factory will produce an object of type 'new_monitor'

这里会明确地列出原始类型和新类型。 在调用print_override_info时, 其输入的类型应该是原始的类型, 而不是新的类型。
print_override_info是一个uvm_component的成员函数, 它实质上是调用uvm_factory的 debug_create_by_name

debug_create_by_type

print_override_info除了这个函数外, uvm_factory还有debug_create_by_type, 其原型为:

1
extern function void debug_create_by_type (uvm_object_wrapper requested_type, string parent_inst_path="", string name="");

使用它对new_monitor进行调试的代码为:

1
factory.debug_create_by_type(my_monitor::get_type(), "uvm_test_top.env.o_agt.mon");

其输出与使用print_override_info相同

print

除了上述两个函数外, uvm_factory还提供print函数:

1
extern function void print (int all_types=1);

这个函数只有一个参数, 其取值可能为0、 1或2。

  • 当为0时, 仅仅打印被重载的实例和类型, 其打印出的信息大体如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
        #### Factory Configuration (*)
        ##
        Instance Overrides:
        ##
        Requested Type Override Path Override Type
        # -------------- -------------------------- -------------
        # my_monitor uvm_test_top.env.o_agt.mon new_monitor
        #
        # No type overrides are registered with this factory
    
  • 当为1时, 打印参数为0时的信息, 以及所有用户创建的、 注册到factory的类的名称。

  • 当为2时
    打印参数为1时的信息, 以及系统创建的、 所有注册到factory的类的名称( 如uvm_reg_item)。

除了上述这些函数外, 还有另外一个重要的工具可以显示出整棵UVM树的拓扑结构, 这个工具就是uvm_root的print_topology 函数。
UVM树在build_phase执行完成后才完全建立完成, 因此, 这个函数应该在build_phase之后调用:

1
uvm_top.print_topology();

最终显示的结果( 部分) 为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
--------------------------------------------------------------------
Name Type Size Value
--------------------------------------------------------------------
<unnamed> uvm_root - @158
uvm_test_top my_case0 - @455
env my_env - @469

    i_agt my_agent - @481

    mon my_monitor - @822

    o_agt my_agent - @489
    mon new_monitor - @865

从这个拓扑结构中可以清晰地看出, env.o_agt.mon被重载成了new_monitor类型。 print_topology这个函数非常有用, 即使在不
进行factory机制调试的情况下, 也可以通过调用它来显示整个验证平台的拓扑结构是否与自己预期的一致。 因此可以把其放在所
有测试用例的基类base_test中。

常用的重载

重载transaction

在有了factory机制的重载功能后, 构建CRC错误的测试用例就多了一种选择。 假设有如下的正常sequence, 此sequence被作为
某个测试用例的default_sequence:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
文件: src/ch8/section8.3/8.3.1/my_case0.sv
class normal_sequence extends uvm_sequence #(my_transaction);
    
    virtual task body();
        repeat (10) begin
            `uvm_do(m_trans)
        end
        #100;
    endtask
    `uvm_object_utils(normal_sequence)
endclass

现在要构建一个新的测试用例, 这是一个异常的测试用例, 要测试 CRC错误的情况。 可以从这个transaction派生一个新的transaction:

1
2
3
4
5
6
7
文件: src/ch8/section8.3/8.3.1/my_case0.sv
class crc_err_tr extends my_transaction;
    
    constraint crc_err_cons{
        crc_err == 1;
    }
endclass

有了factory机制的重载功能后, 可以不用重新写一个abnormal_sequence, 而继续使用normal_sequence作为新的测试用例
的default_sequence, 只是需要将my_transaction使用crc_err_tr重载:

1
2
3
4
5
6
文件: src/ch8/section8.3/8.3.1/my_case0.sv
function void my_case0::build_phase(uvm_phase phase);
    super.build_phase(phase);
    factory.set_type_override_by_type(my_transaction::get_type(), crc_err_tr::get_type());
    uvm_config_db#(uvm_object_wrapper)::set(this, "env.i_agt.sqr.main_phase", "default_sequence", normal_sequence::type_id::get());
endfunction

经过这样的重载后, normal_sequence产生的transaction就是CRC错误的transaction。 这比新建一个CRC错误的sequence的方式简练了很多

重载sequence

transaction可以重载, 同样的, sequence也可以重载。 上节使用的transaction重载能工作的前提是约束也可以重载。 但是很多人
可能并不习惯于这种用法, 而习惯于最原始的如8.1.2节中代码清单8-9的方法。
在其他测试用例中已经定义了如下的两个sequence:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
文件: src/ch8/section8.3/8.3.2/my_case0.sv
class normal_sequence extends uvm_sequence #(my_transaction);
    
    virtual task body();
        `uvm_do(m_trans)
        m_trans.print();
    endtask
    `uvm_object_utils(normal_sequence)
endclass
class case_sequence extends uvm_sequence #(my_transaction);
    
    virtual task body();
        normal_sequence nseq;
        repeat(10) begin
            `uvm_do(nseq)
        end
    endtask
endclass

这里使用了嵌套的sequence。 case_sequence被作为default_sequence。 现在新建一个测试用例时, 可以依然将case_sequence作为
default_sequence, 只需要从normal_sequence派生一个异常的sequence:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
文件: src/ch8/section8.3/8.3.2/my_case0.sv
class abnormal_sequence extends normal_sequence;
    
    virtual task body();
        m_trans = new("m_trans");
        m_trans.crc_err_cons.constraint_mode(0);
        `uvm_rand_send_with(m_trans, {crc_err == 1;})
        m_trans.print();
    endtask
endclass

并且在build_phase中将normal_sequence使用abnormal_sequence重载掉:

1
2
3
4
5
6
文件: src/ch8/section8.3/8.3.2/my_case0.sv
function void my_case0::build_phase(uvm_phase phase);
    
    factory.set_type_override_by_type(normal_sequence::get_type(), abnorma l_sequence::get_type());
    uvm_config_db#(uvm_object_wrapper)::set(this, "env.i_agt.sqr.main_phase", "default_sequence", case_sequence::type_id::get());
endfunction

重载component

除了使用重载transaction和重载sequence的方式产生异常的测试用例。 其实, 还可以使用重载driver的方式产生。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
文件: src/ch8/section8.3/8.3.3/my_case0.sv
class crc_driver extends my_driver;
    
    virtual function void inject_crc_err(my_transaction tr);
        tr.crc = $urandom_range(10000000, 0);
    endfunction
    virtual task main_phase(uvm_phase phase);
        vif.data <= 8'b0;
        vif.valid <= 1'b0;
        while(!vif.rst_n)
            @(posedge vif.clk);
        while(1) begin
            seq_item_port.get_next_item(req);
            inject_crc_err(req);
            drive_one_pkt(req);
            seq_item_port.item_done();
        end
    endtask
endclass

然后在build phase中将my_driver使用crc_driver重载:

1
2
3
4
5
6
文件: src/ch8/section8.3/8.3.3/my_case0.sv
function void my_case0::build_phase(uvm_phase phase);
    
    factory.set_type_override_by_type(my_driver::get_type(), crc_driver::get_type());
    uvm_config_db#(uvm_object_wrapper)::set(this, "env.i_agt.sqr.main_phase", "default_sequence", normal_sequence::type_id::get());
endfunction

在本节所举的例子中看不出重载driver的优势, 因为CRC错误是一个非常普通的异常测试用例。 对于那些特别异常的测试用
例, 异常到使用sequence实现起来非常麻烦的情况, 重载driver就会显示出其优势。
除了transaction, component, sequence, driver可以重载外, scoreboard与参考模型等都可以重载。 尤其对于参考模型来说, 处理异常的激励源是相当耗时的一件
事情。 可能对于一个DUT来说, 其80%的代码都是用于处理异常情况, 作为模拟DUT的参考模型来说, 更是如此。 如果将所有的
异常情况都用一个参考模型实现, 那么这个参考模型代码量将会非常大。 但是如果将其分散为数十个参考模型, 每一个处理一种
异常情况, 当建立相应异常的测试用例时, 将正常的参考模型由它替换掉。 这样, 可使代码清晰, 并增加了可读性。

重载driver以实现所有的测试用例

重载driver使得一些在sequence中比较难实现的测试用例轻易地在driver中实现。 那么如果放弃sequence, 只使用factory机制实
现测试用例可能吗? 答案确实是可能的。 当不用sequence时, 那么要在driver中控制发送包的种类、 数量, 对于objection的控制又
要从sequence中回到driver中, 恰如2.2.3节那样, 似乎一切都回到了起点。
但是不推荐这么做:

  • 引入sequence的原因是将数据流产生的功能从driver中独立出来。 取消sequence相当于一种倒退, 会使得driver的职能不明

确, 与现代编程中模块化、 功能化的趋势不合。

  • 虽然用driver实现某些测试用例比sequence更加方便, 但是对于另外一些测试用例, 在sequence里做起来会比driver中更加方

便。

  • sequence的强大之处在于, 它可以在一个sequence中启动另外的sequence, 从而可以最大程度地实现不同测试用例之间

sequence的重用。 但是对于driver来说, 要实现这样的功能, 只能将一些基本的产生激励的函数写在基类driver中。 用户会发现到最
后这个driver的代码量非常恐怖。

  • 使用virtual sequence可以协调、 同步不同激励的产生。 当放弃sequence时, 在不同的driver之间完成这样的同步则比较难。

基于以上原因, 请不要将所有的测试用例都使用driver重载实现。 只有将driver的重载与sequence相结合, 才与UVM的最初设
计初衷相符合, 也才能构建起可重用性高的验证平台。 完成同样的事情有很多种方式, 应综合考虑选择最合理的方式

factory机制的实现

run_test

一个run_test语句会创建一个my_driver的实例, 并且会自动调用my_driver的main_phase
即 run_test 可以代替下面代码:

1
2
3
4
5
6
initial begin
    my_driver drv;
    drv = new("drv", null);
    drv.main_phase(null);
    $finish();
end

run_test 不带参数, 就必须在命令行中指定实例名字, 不然uvm 不知道要创建哪个实例

top_tb.sv 中

1
2
3
initial begin
   run_test();
end

命令行中

1
+UVM_TESTNAME="my_case0"

如果命令行中不指定测试用例的名字的话会报如下错误:

1
UVM_FATAL @ 0: reporter [NOCOMP] No components instantiated. You must either instantiate at least one component before calling run_test or use run_test to do so. To run a test using run_test, use +UVM_TESTNAME or supply the test name in the argument to run_test(). Exiting simulation.

run_test 带参数

top_tb.sv 中

1
2
3
initial begin
   run_test("my_case0");
end

创建一个类的实例的方法

UVM根据run_test的参数my_driver创建了一个my_driver的实例, 这是factory机制的一大功能。
一个run_test语句会创建一个my_driver的实例, 并且会自动调用my_driver的main_phase

在一般的面向对象的编程语言中, 要创建一个类的实例有两种方法, 一种是在类的可见的作用范围之内直接创建:

1
2
3
4
5
6
7
8
9
class A
    
endclass
class B;
    A a;
    function new();
        a = new();
    endfunction
endclass

另外一种是使用参数化的类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class parameterized_class # (type T)
    T t;
    function new();
        t = new();
    endfunction
endclass
class A;
    
endclass
class B;
    parameterized_classs#(A) pa;
    function new();
        pa = new();
    endfunction
endclass

这样pa实例化的时候, 其内部就创建了一个属于A类型的实例t。 但是, 如何通过一个字符串来创建一个类? 当然了,
这里的前提是这个字符串代表一个类的名字。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
        class A;
            
            endclass
        class B;
            string type_string;
            function new();
                type_string = "A";
                //how to create an instance of A according to type_string
            endfunction
        endclass

没有任何语言会内建一种如上的机制: 即通过一个字符串来创建此字符串所代表的类的一个实例。
如果要实现这种功能, 需要自己做, factory机制正是用于实现上述功能

根据字符串来创建一个类

factory机制根据字符串创建类的实例是如此强大, 那么它是如何实现的呢? 要实现这个功能, 需要用到参数化的类。 假设有如下的类:

1
2
3
4
class registry#(type T=uvm_object, string Tname="");
    T inst;
    string name = Tname;
endclass

在定义一个类( 如my_driver) 时, 同时声明一个相应的registry类及其成员变量:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class my_driver
    typedef registry#(my_driver, "my_driver") this_type;
    local static this_type me = get();
    static function this_type get();
        if(me != null) begin
            me = new();
            global_tab[me.name] = me;
        end
        return me;
    endfunction

向这个registry类传递了新定义类的类型及类的名称, 并创建了这个registry类的一个实例。
在创建实例时, 把实例的指针和“my_driver”的名字放在一个联合数组global_tab中。
上述的操作基本就是uvm_*utils宏所实现的功能, 只是uvm*_utils宏做得更多、 更好。

当要根据类名“my_driver”创建一个my_driver的实例时, 先从global_tab中找到“my_driver”索引对应的 registry#( my_driver, “my_driver”) 实例的指针me_ptr,
然后调用me_ptr.inst=new( ) 函数, 最终返回me_ptr.inst。 整个过程如下:

1
2
3
4
5
6
function uvm_component create_component_by_name(string name)
    registry#(uvm_object, "") me_ptr;
    me_ptr = global_tab[name];
    me_ptr.inst = new("uvm_test_top", null);
    return me_ptr.inst;
endfunction

基本上使用factory机制根据类名创建一个类的实例的方式就是这样。 真正的factory机制实现起来会复杂很多, 这里只是为了
说明而将它们简化到了极致

用factory机制创建实例的接口

factory机制提供了一系列接口来创建实例。

create_object_by_name

create_object_by_name, 用于根据类名字创建一个object, 其原型为:

1
function uvm_object uvm_factory::create_object_by_name (string requested_type_name, string parent_inst_path="", string name="");

一般只使用第一个参数:

1
2
my_transaction tr;
void'($cast(tr, factory.create_object_by_name("my_transaction")));

create_object_by_type, 根据类型创建一个object, 其原型为:

1
function uvm_object uvm_factory::create_object_by_type (uvm_object_wrapper requested_type, string parent_inst_path="", string name="");

一般也只使用第一个参数

1
2
my_transaction tr;
void'($cast(tr, factory.create_object_by_type(my_transaction::get_type())));

create_component_by_name, 根据类名创建一个component, 其原型为:

1
function uvm_component uvm_factory::create_component_by_name (string requested_type_name, string parent_inst_path="", string name, uvm_component parent);

有四个参数, 第一个参数是字符串类型的类名, 第二个参数是父结点的全名, 第三个参数是为这个新的component起的名字,
第四个参数是父结点的指针。 在调用这个函数时, 这四个参数都要使用

1
2
my_scoreboard scb;
void' ($cast(scb, factory.create_component_by_name("my_transaction", get_full_name(), "scb", this)));

这个函数一般只在一个component的new或者build_phase中使用。 如果是在一个object中被调用, 则很难确认parent参数; 如果
是在connect_phase之后调用, 由于UVM要求component在build_phase及之前实例化完毕, 所以会调用失败。
uvm_component内部有一个函数是create_component, 就是调用的这个函数:

1
function uvm_component uvm_component::create_component (string requested_type_name, string name);

只有两个参数, factory.create_component_by_name中剩余的两个参数分别就是this和this.get_full_name( ) 。
create_component_by_type, 根据类型创建一个component, 其原型为:

1
function uvm_component uvm_factory::create_component_by_type (uvm_object_wrap per requested_type, string parent_inst_path="", string name, uvm_component parent);

其参数与create_component_by_name类似, 也需要四个参数齐全:

1
2
3
my_scoreboard scb;
void' ($cast(scb, factory.create_component_by_type(my_transaction::get_type(),
get_full_name(), "scb", this)));

factory机制的本质

使用factory机制除了使用new函数外, 还可以根据类名创建这个类的一个实例; 另外, 还可以在创建类的实例时
根据是否有重载记录来决定是创建原始的类, 还是创建重载的类的实例。
所以, 从本质上来看, factory机制其实是对SystemVerilog中new函数的重载。 因为这个原始的new函数实在是太简单了, 功能
太少了。 经过factory机制的改良之后, 进行实例化的方法多了很多。 这也体现了UVM编写的一个原则, 一个好的库应该提供更多
方便实用的接口, 这种接口一方面是库自己写出来并开放给用户的, 另外一方面就是改良语言原始的接口, 使得更加方便用户的
使用。