- 一、UVM factory机制基础及用法
1.1 factory机制基本概念
uvm的基本思想在于建立组件的uvm树形结构,而建立树形结构的原理为需要将组件加入uvm的factory机制中,即如果是uvm_component组件,需要调用`uvm_component_utils宏加入factory机制,如果是uvm_object类型,则需要调用`uvm_object_utils宏加入factory机制。
factory机制为基于uvm的验证环境提供了一套自主可控的环境build和连接能力,加入到factory机制中的组件可以享受到uvm factory带来的红利,比如基于factory机制的重载,以及自顶向下的建立uvm树形结构,自底向上的连接uvm树。factory机制的核心逻辑实际上是丰富了systemverilog在类的构造方面的功能,传统一个类在声明过后,实例化的过程中需要调用new函数,而new函数本身的功能过于单调,即只分配一块内存空间给这个类的实例。而factory机制赋予了类的构造更强大的功能,主要体现在两个方面:一是建立和其他组件的关系,也就是维持uvm的树形结构,也就是为什么需要在组件的构造函数中传入父节点指针,因为factory的底层也是在调用类的new函数;二是赋予了组件的重载特性,能够助力于验证环境的重用,以及一些异常用例的开发。
以component为例,加入factory机制需要用到`uvm_component_utils宏,这个宏的原型为:
这个宏中包括了两个宏:
这个宏的核心代码是定义了一个uvm_component_registry#(T, T) type_id类型的变量,然后定义了几个函数,用于返回type_id的类型,那么通常情况下,注册到factory机制中的组件,在实例化时要使用如下方式:
A = A::type_id::create(“A”, this)
也就是说,实例化的时候,正式调用了注册到factory机制的宏type_id中的create函数进行实例化。那么create函数在uvm_component_registry类中,原型为:
这个函数内部实例化了uvm_factory类,而实例化组件调用的正是uvm_factory中的create_component_by_type函数,即基于factory机制进行组件的实例化,函数的原型为:
create_component_by_type函数会先检查有没有重载,即调用find_override_by_type,最后返回uvm_object_wrapper的create_component函数,实际上并不是调用的uvm_object_wrapper中的create_component函数,因为在基类uvm_object_wrapper中,这个函数返回空指针:
这个问题的原因其实很简单,因为向create_component_by_type函数传递的参数,第一个是一个get方法:
这个方法在uvm_component_registry类中,而这个类正是继承自uvm_object_wrapper:
get函数返回一个uvm_component_registry类型的实例,然后传递给create_component_by_type函数,函数返回uvm_component_registry类中的create_componet方法:
这个函数会声明注册到factory的组件类名,并调用其new函数,返回这个实例。因此,可以得到factory机制的一个底层逻辑:实际上factory最终会调用组件的new函数进行实例化。
回顾整个过程,总结起来就是:factory机制进行实例化的过程,会先对component进行注册,即调用f.registory,然后查询是否有重载记录,即调用find_override_by_type,如果有重载记录,则返回重载后的类,最后调用registory中的create_component函数,这个函数最终会调用重载后的类的构造函数new,如果没有重载记录,则调用当前实例化组件的构造函数(new)。
本文重点关注factory机制的重载的用法
1.2 factory机制的重载函数用法
factory最终要的功能,就是支持组件的重载,而重载的底层逻辑,实际上是运用了类的继承和多态特性。
想象一个场景,B项目需要在A项目的基础上做增量开发,而相应的验证也要继承A项目,但是其中一个子模块的输入接口发生变化,因此需要改写A项目的driver,通常情况下,不希望直接改掉A项目的代码,因为往往在一些公司中,A项目的代码只有一套,且需要按时回归,做长期的随机测试。那么就需要运用到UVM factory机制的重载特性,开发一个新的driver,在验证的过程中将A项目环境的driver重载掉。
new driver代码:
1. class new_driver extends my_driver;
2. ......
3. `uvm_component_utils(new_driver)
4. `uvm_set_super_type(new_driver, my_driver);
5.
6. function void new(string name="new_driver", uvm_component parent);
7. super.new(name, parent);
8. endfunction
9.
10. virtual task void run_phase(uvm_phase phase);
11. super.run_phase(phase);
12. while(1) begin
13. seq_item_port.get_next_item(req);
14. if(!$cast(tr, req))
15. `uvm_fatal(get_type_name(), "error!");
16. `uvm_do_callback(my_driver, pkt_process_callback, pkt_pre_trans(this, tr));
17. new_drv_pkt();
18. seq_item_port.item_done();
19. end
20. endtask:run_phase
21.
22. virtual task new_drv_pkt();
23. ......
24. endtask:drv_pkt
25. endclass
那么在TC中,直接将my_driver重载为new_driver:
1. class tc_new_drv extends base_test;
2. ......
3. `uvm_component_utils(tc_new_drv)
4. ......
5. function void new(string name="tc_new_drv", uvm_component parent);
6. super.new(name, parent);
7. endfunction
8. virtual function void build_phase(uvm_phase phase);
9. super.build_phase(phase);
10. set_type_override_by_type(my_driver::get_type(), new_driver::get_type());
11. ......
12. endfunction:build_phase
13. ......
14. endclass
注意在实例化driver之前,要调用set_type_override_by_type方法,这个方法实际上是uvm_component内置的方法,最后会调用factory的set_type_override_by_type方法。这个函数有三个参数,第一个为被重载的类,第二个为重载的类,第三个为是否可以被后面的重载覆盖。重载过后,环境会按照new_driver进行激励的驱动。
uvm factory机制提供了多种重载函数,见下表:
函数原型 | 参数 | 说明 |
set_type_override_by_type(ori_cls, new_cls, replace) | 被重载类:uvm_object_wrapper类型,通过get_type获取; 重载类:uvm_object_wrapper类型,通过get_type获取; replace:是否被后续的重载替换 | 函数调用在uvm_component中,函数原型会调用uvm_factory机制中的:set_type_override_by_type方法;调用该方法会将环境中所有的ori_cls重载掉 |
set_inst_override_by_type(path, ori_cls, new_cls) | path:重载类的实例化路径 被重载类:uvm_object_wrapper类型,通过get_type获取; 重载类:uvm_object_wrapper类型,通过get_type获取; | 函数调用在uvm_component中,函数原型会调用uvm_factory机制中的:set_inst_override_by_type方法;调用该方法只会重载参数path路径下的组件 |
set_type_override(ori_type_name, new_type_name, replace) | 被重载类名:通过类名的传递; 重载类名:通过类名传递; replace:是否被后续类重载覆盖 | 函数调用在uvm_component中,函数原型会调用uvm_factory机制中的:set_type_override_by_name方法;调用该方法重载所有的组件,可以通过类名字符串传递 |
set_inst_override(path, ori_type_name, new_type_name) | path:需要重载类的组件的路径 被重载类名:通过类名的传递; 重载类名:通过类名传递; | 函数调用在uvm_component中,函数原型会调用uvm_factory机制中的:set_type_override_by_name方法;调用该方法重载所有的组件,可以通过类名字符串传递 |
上表中函数的原型均位于uvm_component基类中,实际上在基类中,调用的是uvm_factory中的函数:
所以,在tc_base中,同样可以调用factory的相应函数实现重载,那么tc_new_drv的代码第10行可以改写为:
factory.set_type_override_by_type(my_driver::get_type(), new_driver:;get_type())
使用这种方法做重载的优势在于,uvm_factory是独立与uvm树形结构之外的全局类,因此可以在任何组件中调用,最实用的实在testbench top层调用进行重载:
1. initial begin
2. factory.set_inst_override_by_type(my_driver::get_type(), new_driver::get_type(), "uvm_test_top.env.i_agt.drv");
3. end
依旧总结一下:
函数原型 | 参数 | 说明 |
set_type_override_by_type(ori_cls, new_cls, replace) | 被重载类:uvm_object_wrapper类型,通过get_type获取; 重载类:uvm_object_wrapper类型,通过get_type获取; replace:是否被后续的重载替换 | 函数需要通过factory的作用域使用,可在testbench顶层调用 |
set_inst_override_by_type(ori_cls, new_cls, path) | 被重载类:uvm_object_wrapper类型,通过get_type获取; 重载类:uvm_object_wrapper类型,通过get_type获取; path:重载类的实例化路径 | 函数需要通过factory的作用域使用,可在testbench顶层调用 |
set_type_override_by_name(ori_type_name, new_type_name, replace) | 被重载类名:通过类名的传递; 重载类名:通过类名传递; replace:是否被后续类重载覆盖 | 函数需要通过factory的作用域使用,可在testbench顶层调用 |
set_inst_override_by_name(ori_type_name, new_type_name, path) | 被重载类名:通过类名的传递; 重载类名:通过类名传递; path:需要重载的组件路径 | 函数需要通过factory的作用域使用,可在testbench顶层调用 |
另外,uvm还可以通过调用仿真命令进行组件的重载:
<vcs command> +uvm_set_type_override=<req type>, <ovr type>, <replace>
<vcs command> +uvm_set_inst_override=<req type>, <ovr type>, <full_path>
上述实例可以使用为:
<vcs command> +uvm_set_type_override=”my_driver, new_driver”
或:<vcs command> +uvm_set_inst_override=”my_driver, new_driver, uvm_test_top.env.i_agt.drv”
1.3 factory机制的复杂重载
实际上,重载是有前提的,要求重载的类必须是被重载类的子类,才可以进行重载,并且,uvm factory机制支持连续重载,本位以《UVM实战》书籍中的例子进行说明,假设有bird类和parrot类,parrot类继承自bird,且都注册到uvm factory机制中。且新增一个子类big_parrot继承自parrot。
在tc的build_phase中进行重载:
可以看到最终的输出结果,bird_inst和parrot调用create后,实际上是调用的big_parrot的create,因此最终打印为big_parrot的函数。
factory机制还支持替换式的重载,假设有一个新的子类继承自bird:
在tc的build phase中:
最终输出结果为bird被重载为最终的子类sparrow,而parrot还是本身。
这种替换式的重载方法,需要将replace参数设定为1(默认为1),如果设定为0,bird只会重载第一个替换的子类,也就是parrot:
实际上还可以这么用:
输出结果为:
这个时候,需要去掉parrot的实例化,如果实例化出来,会报错,因为在86行的写法下,会将parrot进行实例化。
factory机制的重载原理,过程可以按如下方式说明:
如果是实例化parrot,因为parrot没有被后续的类重载:
如果实例化bird,因为bird被重载过:
总结一下:uvm factory机制在实例化类的过程中,会查询factory的重载记录,如果这个类被重载过,那么最终会调用重载类的create方法,即最终会调用到重载类的new构造函数。
这个机制的实现是在find_override_by_type函数中实现,总体思想是在factory机制中会维护一张protect修饰的重载表,如果类被set_type_override_by_type函数调用过,那么会在表中增加一条重载记录,在create的过程中会查询这张表,调用返回最后被重载的表的实例,并最终调用该实例的构造函数new。
整套算法实现这套机制的代码还是相对复杂,而且重载component和重载object有些许差异,再加上是否连续重载replace功能上,就更加复杂,读者可以通过研究uvm源码的角度再进行深刻理解。
- 二、factory机制的调试方法
2.1 组件中对factory的调试
在一个多继承的环境中,如果基线环境大量运用了这种重载,那么对于新接手的工程师来讲,对于环境的调试机制就显得非常重要,factory机制定义了多种调试和打印能力。如果用户想查看某个组件有没有被重载过,可以在该组件中调用:(以driver为例,在my_driver的build_phase之后的phase中调用)
this.print_override_info(“my_driver”);
也可以在tc中调用,但是要注意路径:
env.i_agt.drv.print_override_info(“my_driver”);
这个打印函数需要注意的点在于:
- 必须在build_phase之后的phase调用,因为如果在build_phase调用,有可能会出现当前组件并没有被实例化,而报类空指针错。
- 函数的参数必须是原始的基类,如果是重载类,会查看重载类有没有被重载,所以一定要是原始基类的类名。
2.2 top层的调试
除了查看某个特定的组件是否被实例化之外,factory机制还提供了调试手段,debug_create_by_type和debug_create_by_name方法,这种调试手段可以不依赖组件,在testbench top中进行调用:
debug_create_by_name方法有三个参数,第一个为需要打印重载的组件字符串名,第二个参数为组件的实例化路径。第三个参数为实例名字,可以为空
debug_create_by_type方法有三个参数,第一个为需要打印重载的组件类名,需要用get_type获取,第二个参数为组件的实例化路径。第三个参数为实例名字,可以为空。
可以在testbench顶层调用:
1. initial begin
2. factory.debug_create_by_name("my_driver", "uvm_test_top.env.i_agt.drv");
3. //factory.debug_create_by_type(my_driver::get_type(), "uvm_test_top.env.i_agt.drv");
4. end
或者在tc的build_phase之后的phase调用也可以。
另外,factory还提供了print函数,用于打印重载信息,注册到factory机制中的组件信息,以及是否包含系统创建的注册到factory机制的组件信息:
这个函数包含一个参数:all_types,取值范围只有0,1,2
0:只打印重载的类,以及被重载类的信息;
1:参数为0的打印,以及所有用户创建的并注册到factory机制中的类的信息
2:参数为1的打印,以及所有系统创建的并注册到factory机制中的类的信息
最常用的参数为1,也就是默认参数,这个函数可以在testbench顶层通过factory.print()的方式调用,也可以在组件中调用(build_phase之后)。
最后一种方法:通过uvm_top.print_topology(),打印整个uvm树形结构,打印信息中包含了重载的组件。这个方法可以在testbench中调用,也可以在组件中调用,同样需要在组件的build_phase之后调用。
一般的经验是,在base_test中的end_of_elaboration_phase中调用这些打印信息,uvm_top.print_topology()使用最为常见,可以和factory.print()配合使用。
print_topology()调用的效果:
可以看到mon被重载为new_monitor。
总结一下:
- factory机制调试有四种方法:1、通过组件调用print_override_info(name_string)查看当前组件的重载状态,只能在组件中调用;2、通过factory调用debug_create_by_name和debug_create_by_type的方式,需要传递组件的路径,第一个函数第一个参数传递为查看组件的字符串,第二个函数参数为组件的类型(uvm_object_wrapper类型,需要通过get_type获取);3、通过factory的print方法,参数有0/1/2,不同参数打印范围不同;4. uvm_top.print_topology(),打印整个验证环境的uvm拓朴结构。
- 通过factory的调试和通过uvm_top的调试,可以在验证环境的任意位置进行打印,包括testbench顶层,如果在组件中调用,需要在build_phase之后的phase调用
- 一般的环境构造,是在tc_base的end_of_elaboration_phase中调用factory.print,和uvm_top.print_topology()方法进行调试,这也是环境打印的必要信息。
- 三、factory在环境重用性方面的应用
3.1 重载transaction
transaction中包含了环境所需的所有数据类型,以及环境数据类型的约束,事实上,system_verilog支持约束的重载。而重载transaction在构造异常场景的测试下具有很大的用处。比如希望构造一个crc_err的类,来产生crc_err场景的驱动:
my_transaction和crc_transaction:
1. class my_transaction extends uvm_sequence_item;
2. rand logic [31:0] data[];
3. rand logic [1:0] data_type;
4. rand bit crc_error;
5. .........
6. constraint crc_cons { crc_error == 0;}
7. constraint data_size {data.size >= 500; data_size<=1000;}
8. `uvm_object_utils_begin(my_transaction)
9. `uvm_field_array_int(data, UVM_ALL_ON)
10. `uvm_field_int(data_type, UVM_ALL_ON)
11. ......
12. `uvm_field_int(crc_error, UVM_ALL_ON)
13. `uvm_object_utils_end
14. endclass
15.
16. class crc_transaction extends my_transaction;
17. ......
18. constraint crc_cons { crc_error == 1; }
19. ......
20. endclass
那么在tc的build_phase可以对transaction进行重载:
1. class tc_crc_err extends base_test;
2. ......
3. `uvm_component_utils(tc_crc_err)
4. ......
5. function void new(string name="tc_crc_err", uvm_component parent);
6. super.new(name, parent);
7. endfunction
8. virtual function void build_phase(uvm_phase phase);
9. super.build_phase(phase);
10. set_type_override_by_type(my_transaction::get_type(), crc_transaction::get_type());
11. ......
12. endfunction:build_phase
13. ......
14. endclass
实际上,大多数工程是并不会用这个重载机制实现crc_error的测试,而是通过约束的控制,将crc_error的约束关掉,在sequence中:
1. class crc_seq extends uvm_sequence#(uvm_sequence_item);
2. my_transaction my_tr;
3. `uvm_object_utils(crc_seq)
4. ......
5. virtual task body();
6. my_tr = new("my_tr");
7. my_tr.crc_cons.constraint_mode(0);
8. `uvm_rand_send_with(my_tr, {my_tr.crc_error==1;})
9. ......
10. endclass
通过cons_ins.constraint_mode(0)关闭掉某个约束,使用`uvm_rand_send_with进行发送.
为什么不能用uvm_do系列宏?
因为constraint_mode的调用需要先实例化,而uvm_do系列宏不需要进行实例化,不实例化直接传递,就无法关闭约束,所以这种方法需要用到uvm_send_with系列宏,并和`uvm_create宏配合使用。如果要使用uvm_do系列宏,就要重写sequence和建立新的crc_transaction,方法有很多,这里就不一一实现。
另外,通过tr.constraint_mode(0),可以关闭掉tr中所有的约束。
3.2 重载sequence
实际上,重载transaction的例子,还可以通过重载sequence来实现,可以新建一个crc_seq,专门用于产生crc错误的数据类型,假设存在一个base_sequence,在base_sequence中调用了用于正常传输的norm_seq,用于传输正常的数据格式,即不带crc_error。
1. class norm_seq extends uvm_sequence#(uvm_sequence_item);
2. my_transaction my_tr;
3. `uvm_object_utils(norm_seq)
4. `uvm_declare_p_sequencer(my_sequencer)
5. ......
6. virtual task body();
7. `uvm_do_on(my_tr, p_sequencer.my_sqr);
8. my_tr.print()
9. endtask:body
10. ......
11. endclass
12.
13. class base_seq extends uvm_sequence#(uvm_sequence_item);
14. norm_seq nseq;
15. `uvm_object_utils(base_seq)
16. `uvm_declare_p_sequencer(my_sequencer)
17. ......
18. virtual task body();
19. repeat(10) begin
20. `uvm_do_on(nseq, p_sequencer.my_sqr)
21. nseq.print();
22. end
23. endtask
24. .....
25. endclass
然后再开发一个crc_error的seq,继承自norm_seq:
1. class crc_seq extends norm_seq;
2. ......
3. `uvm_object_utils(crc_seq)
4. `uvm_declare_p_sequencer(my_sequencer)
5. ......
6. virtual task body();
7. my_transaction my_tr;
8. `uvm_create(my_tr);
9. my_tr.crc_cons.constraint_mode(0);
10. `uvm_rand_send_with(my_tr, {my_tr.crc_error==1;})
11. endtask:body
12. ......
13. endclass
在tc中将其重载掉:
1. class tc_crc_drv extends base_test;
2. ......
3. `uvm_component_utils(tc_crc_drv)
4. ......
5. function void new(string name="tc_crc_drv", uvm_component parent);
6. super.new(name, parent);
7. endfunction
8. virtual function void build_phase(uvm_phase phase);
9. super.build_phase(phase);
10. set_type_override_by_type(norm_seq::get_type(), crc_seq::get_type());
11. uvm_config_db#(uvm_object_wrap)::set(this, "env.i_agt.sqr.main_phase", "default_sequence", base_seq::type_id::get());
12. ......
13. endfunction:build_phase
14. ......
15. endclass
重载过后,default_sequence的设置,依旧是base_seq,此处运用了sequence的嵌套技术。
3.3 重载component
重载组件的例子在本文已经介绍很多遍了,除了重载driver,还可以重载monitor,以及参考模型,scoreboard等组件,使用方法就是运用1.2节提到的函数或者方法。这里不过多介绍。
重载参考模型和重载scoreboard具有很重要的应用,在dut的异常测试中,因为参考模型通常是基于正常用例开发,对于异常来说,参考模型通常需要重构,或者关掉环境的比对,或只做部分比对,这样,通过factory的重载机制,可以轻松实现异常测试的参考模型和scoreboard与正常模式下的区分,两者独立处理,环境的组件结构更为清晰,代码也较易维护。
3.4 基于factory的重载机制实现所有用例
正式因为factory机制的强大功能,提供了可以重载组件的机制,事实上,可以通过重载的方法实现所有的测试用例,也就是通过开发不同测试场景下的driver,利用重载机制实现完备性验证,但并不推荐这样做,因为uvm_sequence机制的引入,就是希望让driver的功能更为独立,就是用于驱动数据转化为激励的时序,而不用去处理数据。
另外,uvm_sequence机制的virtual sequence可以用于协调不同的激励和完成不同数据间的同步,如果不用这个功能,driver结合factory重载机制实现该功能会比较困难。
一般情况下,组件的重载配合sequence机制,以及sequence的重载结合起来,才能构建出重用度高,功能完备,可以进行完备性验证的环境。
3.5 factory机制创建环境实例方法
factory机制还提供了两个函数,用于生成组件,对于生成component组件的函数:
create_component_by_name和create_component_by_type,create_component:
这两个函数位于uvm_factory基类中
这个函数位于uvm_component_registry基类中。
创建object的factory实例化方法:create_object_by_type和create_object_by_name,create_object方法:
这两个函数位于uvm_factory基类中:
该函数位于uvm_object_registry基类中。
这些函数总结起来就是既可以通过类名创建,需要调用type_id::get,或者get_type返回一个uvm_object_wrapper类型的变量,传递给函数的第一个参数(因为第一个参数类型是uvm_object_wrapper,该函数与组件或者object不具备继承关系);还可以通过组件或者object的字符串名字直接创建。
使用方法:
object:
component:
注意create_component_by_name/type有4个参数都要用到,第一个参数是字符串类型的类名,第二个参数是父结点的全名,第三个参数是为这个新的component起的名字,第四个参数是父结点的指针。
个人感觉,这几个函数在实际工程中用处不大,一般的建立方法都是直接调用create,因此这些应用在工程上用的比较少,有不同见解的朋友可以一起讨论。
参考资料《UVM实战》