在《类库地图》一节中,读者可以看到UVM世界中的类最初都是从一个uvm_void根类(root class)中得来的,而实际上这个类并没有一个成员变量和方法,它只是一个虚类(virtual class),还在等待将来继承与它的子类去开垦。在继承与uvm_void的子类中,有两类,一个类为uvm_object,另外一个类为uvm_port_base。
在后面的章节关于TLM传输的部分中我们会讲解uvm_port_base,而在本节中我们来重点分析uvm_object这一个类的核心功能和扮演的角色是什么。要知道,UVM世界的类库地图中除过事务接口(transaction interface)类继承与uvm_port_base,其它所有的类都是从uvm_object一步步继承而来的。
从uvm_object提供的方法和相关的宏操作来看,它的核心方法主要提供与数据操作的相关服务:
- Copy
- Clone
- Compare
- Print
- Pack/Unpack
对于一个将来可能含有多种成员变量的类而言,其对象的拷贝和克隆要复杂得多,更多的需要创建类的verifier自己去定义copy和clone等方法,这就为编码带来了不方便的地方。
在介绍uvm_object如何实现规模化的数据拷贝和对象克隆之前,需要再提醒一些初识面向对象编程的读者们,下面的这个操作并不是对象的拷贝:
module object_handle_copy;
import uvm_pkg::*;
`include "uvm_macros.svh"
typedef enum {RED, WHITE, BLACK} color_t;
class box extends uvm_object;
int volume = 120;
color_t color = WHITE;
string name = "box";
`uvm_object_utils(box)
function new(string name="box");
super.new(name);
this.name = name;
endfunction
endclass
box b1, b2;
initial begin
b1 = new("box1");
b2 = b1;
b2.name = "box2";
$display("b1 box name is %s", b1.name);
$display("b2 box name is %s", b2.name);
end
endmodule
输出结果:
b1 box name is box2
b2 box name is box2
在之前SV核心篇章中,读者已经见过类似的例子用来表明将b1赋值于b2,并不是将b1内的数据悉数拷贝给b2,而只是将b1的句柄拷贝给b2。由于b2与b1都指向了同一个对象,所以通过b2或者b1进行对象的数据操作都是对同一个对象成员变量的修改,最终的输出结果也反映了这一点。那么,如果要将b1的数据拷贝到b2,应该如何呢?在之前SV《激励器的封装》中关于对象的拷贝,读者需要自己编写copy()成员方法,来规定哪些成员数据需要拷贝。
可以想象的是,如果对于uvm_object类的成员数据的拷贝、打印操作如果都需要用户自己去定义的话,那么这会带来额外的编码负担,并且由于用户代码的不规范也容易出错。那么UVM是如何解决数据本身存放和相关操作的“基础建设”的呢?
域的自动化 (Field Automation)
UVM通过一些域的自动化,使得用户在注册UVM类的同时也可以声明今后会参与到对象拷贝、克隆、打印等等操作的成员变量。仍然是上面的例子,我们来看看通过UVM与域的自动化相关的宏,如何简化了对象的拷贝。
module object_copy;
import uvm_pkg::*;
`include "uvm_macros.svh"
typedef enum {RED, WHITE, BLACK} color_t;
class box extends uvm_object;
int volume = 120;
color_t color = WHITE;
string name = "box";
`uvm_object_utils_begin(box)
`uvm_field_int(volume, UVM_ALL_ON)
`uvm_field_enum(color_t, color, UVM_ALL_ON)
`uvm_field_string(name, UVM_ALL_ON)
`uvm_object_utils_end
function new(string name="box");
super.new(name);
this.name = name;
endfunction
endclass
box b1, b2;
initial begin
b1 = new("box1");
b1.volume = 80;
b1.color = BLACK;
b2 = new();
b2.copy(b1);
b2.name = "box2";
$display("%s", b1.sprint());
$display("%s", b2.sprint());
end
endmodule
输出结果:
-------------------------------
Name Type Size Value
-------------------------------
box1 box - @336
volume integral 32 'h50
color color_t 32 BLACK
name string 4 box1
-------------------------------
-------------------------------
Name Type Size Value
-------------------------------
box box - @337
volume integral 32 'h50
color color_t 32 BLACK
name string 4 box2
-------------------------------
从这个使用域的自动化宏的例子来看,在注册box的同时,也声明了将来会参与到uvm_object数据操作的成员变量。凡是声明了的成员变量,都将在数据操作时自动参与进来。那么,如果有一些数据,没有通过域的自动化来声明的话,它们也将不会自动参与到数据的拷贝、打印等操作,除非用户自己去定义。
这里,我们按照域的类型来将域的自动化时用来声明数据的宏进行分类:
域类型 | 域自动化的宏声明 | 成员类型 |
标量 | `uvm_field_int(ARG, FLAG) | 整形 |
`uvm_field_object(ARG, FLAG) | 继承与uvm_object的类型句柄 |
`uvm_field_string(ARG, FLAG) | 字符串类型 |
`uvm_field_enum(T, ARG, FLAG) | 枚举类型 |
`uvm_field_event(ARG, FLAG) | 事件类型 |
`uvm_field_real(ARG, FLAG) | 实数类型 |
静态数组 | `uvm_field_sarray_int(ARG, FLAG) | 包含整形的一维静态数组 |
`uvm_field_sarray_object(ARG, FLAG) | 包含uvm_object的一维静态数组 |
`uvm_field_sarray_string(ARG, FLAG) | 包含字符串的一维静态数组 |
`uvm_field_sarray_enum(T, ARG, FLAG) | 包含枚举类型的一维静态数组 |
动态数组 | `uvm_field_array_int(ARG, FLAG) | 包含整形的一维动态数组 |
`uvm_field_array_object(ARG, FLAG) | 包含uvm_object的一维动态数组 |
`uvm_field_array_string(ARG, FLAG) | 包含字符串的一维动态静态数组 |
`uvm_field_array_enum(T, ARG, FLAG) | 包含枚举类型的一维静态数组 |
队列 | `uvm_field_queue_int(ARG, FLAG) | 包含整形的队列 |
`uvm_field_queue_object(ARG, FLAG) | 包含uvm_object的队列 |
`uvm_field_queue_string(ARG, FLAG) | 包含字符串的队列 |
`uvm_field_queue_enum(T, ARG, FLAG) | 包含枚举类型的队列 |
关联数组 | 键类型: 字符串 | `uvm_field_aa_int_string(ARG, FLAG) | 包含整形的关联数组 |
`uvm_field_aa_object_string(ARG, FLAG) | 包含uvm_object的关联数组 |
`uvm_field_aa_string_string(ARG, FLAG) | 包含字符串的关联数组 |
键类型: 整形 | `uvm_field_aa_object_int(ARG, FLAG) | 由int索引的包含uvm_object的关联数组 |
`uvm_field_aa_int_int(ARG, FLAG) | 由int索引的包含int的关联数组 |
`uvm_field_aa_int_int_unsigned(ARG, FLAG) | 由int unsigned类型索引 |
`uvm_field_aa_int_integer(ARG, FLAG) | 由integer类型索引 |
`uvm_field_aa_int_integer_unsigned(ARG, FLAG) | 由integer unsigned类型索引 |
`uvm_field_aa_int_byte(ARG, FLAG) | 由byte类型索引 |
`uvm_field_aa_int_byte_unsigned(ARG, FLAG) | 由byte unsigned类型索引 |
`uvm_field_aa_int_shortint(ARG, FLAG) | 由shortint类型索引 |
`uvm_field_aa_int_shortint_unsigned(ARG, FLAG) | 由shortint unsigned类型索引 |
`uvm_field_aa_int_longint(ARG, FLAG) | 由longtin类型索引 |
`uvm_field_aa_int_longint_unsigned(ARG, FLAG) | 由longting unsigned类型索引 |
`uvm_field_aa_int_key(ARG, FLAG) | 由任何整形类型索引 |
`uvm_field_aa_int_enumkey(ARG, FLAG) | 由任何枚举类型索引 |
而上面的这些用于域的自动化的宏声明应当在uvm_object或者uvm_component注册时发生。即应该在`uvm_object_utils_begin和`uvm_object_utils_end之间,或者在`uvm_component_utils_begin和`uvm_component_utils_end之间签入要自动化的域。
在签入要自动化的域时,除过需要注意运用正确地宏来匹配域的成员类型(ARG),还应当声明这些域在将来会参与到的数据操作(FLAG),这一声明即由下面不同的枚举类型来确认。
数据操作方法 | 属性 | 描述 |
拷贝 | UVM_COPY | 该域会被自动拷贝(默认) |
UVM_NOCOPY | 该域不会被自动拷贝 |
UVM_DEEP | 对象域将被深拷贝,对象应当实现copy方法(默认) |
UVM_SHALLOW | 对象域将被浅拷贝 |
UVM_REFERENCE | 对象域只会拷贝句柄 |
比较 | UVM_COMPARE | 该域会被自动比较(默认) |
UVM_NOCOMPARE | 该域不会被自动比较 |
打印 | UVM_PRINT | 该域会被自动打印(默认) |
UVM_NOPRINT | 该域不会被自动打印 |
UVM_NODEFPRINT | 该域的值如果等于缺省值则不会被打印 |
UVM_BIN, UVM_DEC, UVM_UNSIGNED, UVM_OCT, UVM_HEX, UVM_STRING, UVM_TIME, UVM_ENUM | 规定打印该域时的字符串输出格式 |
记录 | UVM_RECORD | 该域会被自动记录(默认) |
UVM_NORECORD | 该域不会被自动记录 |
打包 | UVM_PACK | 该域会被自动打包(默认) |
UVM_NOPACK | 该域不会被自动打包 |
其它 | UVM_DEEP | 对象域进行深层次递归操作(拷贝/比较/打印/记录/打包) |
UVM_SHALLOW | 对象域进行浅层次递归操作(拷贝/比较/打印/记录/打包) |
UVM_REFERENCE | 对象域只对句柄操作(拷贝/比较/打印/记录/打包) |
UVM_READONLY | 对象域不会被自动配置 |
UVM_ALL_ON | 使能该域参与所有的数据操作 |
UVM_DEFAULT | 采取默认的域操作方法 |
UVM小白们在刚开始使用时,不妨先将FLAG写为UVM_ALL_ON或者UVM_DEFAULT。目前,UVM_ALL_ON和UVM_DEFAULT的功能是一致的,即将所有的数据操作方法打开,而UVM推荐的是UVM_DEFAULT,因为不排除日后添加一些其它的数据操作方法,而默认被关闭。
如果用户不写UVM_ALL_ON或者UVM_DEFAULT,那么只需要写出哪些操作是应当被关闭的。例如,FLAG设置为UVM_NOCOMPARE,那么该域会被排除出比较操作,但其它操作仍然是默认执行的。
此外,各种数据操作属性也可以进行多项指定,这里我们只推荐使用按位或操作符“|”。例如,"UVM_NOCOPY | UVM_NOCOMPARE",即将该域的拷贝操作和比较操作关闭,而保留其它操作。本书不推荐使用加法操作符"+"的原因在于,如果有多个相同属性参与到属性运算中,则容易导致无法预期的行为。
因此,通过上述的域自动化的方法,可以给读者省去一大笔编码的时间,同时uvm_object也具备了可以进行常用数据操作的方法。同时,通过上面的宏虽然节省了用户编码的时间,然而由于宏而引入的上百行额外的代码,这也会对仿真运算造成非常大量的隐形消耗。关于宏的这把双刃剑,我们也将在后面的文章《宏的优劣探讨》中详细分析。
接下来,我们可以分别深入这几种数据操作方法当中,基本掌握各个方法的用途。