一个工厂小demo实现UVM的run_test

run_test是UVM的一大卖点. run_test根据运行时的参数+UVM_TESTNAME=[test_name]来例化[test_name]对应的继承自UVM_TEST的子类对象并运行. 把所有的用例一起编译之后, 就可以实现”一次编译, 多次运行”.

说到UVM的Factory工厂, 大家的第一反应可能是Factory的create例化:

1
2
usr_obj = usr_ojbect_class::type_id::create("obj_name");
usr_com = usr_com_class::type_id::create("com_name", this);

仔细看的话, 这种例化方式, 已经把对象的类型都写明了, 仍然是Hardcode硬编码的形式. 对比之下, run_test才是真正符合工厂模式内涵的动态行为.

这里用一个轻量级的demo来展示UVM的工厂模式实现run_test这种行为的机制.

核心部分代码

这套轻量级run_test系统的核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
virtual class TestPrototype;
pure virtual task test ();
endclass: TestPrototype
class TestFactory;
static local TestPrototype tests[string];
static function void register (TestPrototype obj, string test_name);
assert(obj != null)
else begin
$display("Can't register null object in factory!!");
return; end
if (tests.exists(test_name)) begin
$display("Override obj registered with '%s'!!", test_name); end
tests[test_name] = obj;
endfunction: register
static task run_test (string test_name);
if (!tests.exists(test_name)) begin
$display("Can't find %s in factory!!", test_name);
return; end
tests[test_name].test();
endfunction: run_test
endclass
`define __register(T) \
static local T reg_obj = get(); \
static local function T get(); \
if (reg_obj == null) begin \
reg_obj = new(); \
TestFactory::register(reg_obj, `"T`"); \
end \
return reg_obj; \
endfunction

短短三十多行, 不过还是分成三个部分来分析一下.

注册类原型

首先是test的原型父类TestPrototype, 这个类只需要一个纯虚task test, 做个对照的话, 这个task相当于uvm_test这个父类里的所有phase函数.

1
2
3
virtual class TestPrototype;
pure virtual task test ();
endclass: TestPrototype

原型父类用抽象类来实现, 其实用interface class接口类的话是一种更好的实现, 这样可以在不改变原继承关系的情况下为任意的类实现接口. 只不过现在Cadence的Incisive(竟然)还不支持interface class, 为了兼容性只能退而求其次了.

工厂

接下来是工厂TestFactory. 总的说来, 框架里的工厂都是全局的单例. 简便起见, 这里用静态成员和静态方法实现工厂类.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class TestFactory;
static local TestPrototype tests[string];
static function void register (TestPrototype obj, string test_name);
assert(obj != null)
else begin
$display("Can't register null object in factory!!");
return; end
if (tests.exists(test_name)) begin
$display("Override obj registered with '%s'!!", test_name); end
tests[test_name] = obj;
endfunction: register
static task run_test (string test_name);
if (!tests.exists(test_name)) begin
$display("Can't find %s in factory!!", test_name);
return; end
tests[test_name].test();
endfunction: run_test
endclass

成员tests是个联合数组(哈希表), 存储类型是TestPrototype, 索引类型是字符串, 用于保存工厂注册的对象. TestPrototype是抽象类, 是不能例化, 不过父类句柄是可以
携带子类对象, 因此实际上联合数组里存储的都是TestPrototype的子类对象. testslocal修饰以防外部的篡改.

方法register用于注册, 将输入参数TestPrototype对象(同样的, 这里输入的都是TestPrototype的子类对象)与字符串test_name作为一组value-key存入联合数组. 最前面是空句柄检查, 这里选择检查到空句柄的时候提供告警log并直接退出, 以防空句柄之后调用的时候引起运行错误. 之后是字符串重复注册的处理, 这里的选择是提供告警使用新的对象覆盖.

方法run_test就是这个系统里使用的实际接口, run_test根据输入的字符串在联合数组tests中查找TestPrototype对象, 查找失败就告警返回, 查找成功就调用该对象的test()方法. 这里使用了多态特性, 由于tests句柄中携带的都是子类对象, 因此调用的都是子类实现的实际使用的test().

自动注册宏

为了更好的模拟UVM, 这里提供了一个__register宏来提供自动注册的功能.

对于用例对象来讲, 需要在run_test之前完成注册, 而UVM中的run_test是在0时刻运行的, 因此自动注册是非常重要的, __register宏也是里面真正的核心.

1
2
3
4
5
6
7
8
9
`define __register(T) \
static local T reg_obj = get(); \
static local function T get(); \
if (reg_obj == null) begin \
reg_obj = new(); \
TestFactory::register(reg_obj, `"T`"); \
end \
return reg_obj; \
endfunction

__register宏的用法跟UVM的uvm_*_util宏一致, 在类定义里面调用宏传入类名参数.

__register生成该类名下的私有静态成员变量, 并直接用get方法初始化. get方法是私有静态方法, 用来创建单例对象reg_obj并进行注册.

这种方式跟饿汉式单例模式是一致的, 保证了用例对象的单例在类载入也就是所有线程启动之前就完成了自动的注册. UVM也使用的这种方法, 不过隐藏的更深一点, 直接从uvm_*_util宏里面是看不出来的:)

注册的时候使用

1
`"T`"

将类名转换为同名的字符串, 这是只有宏才能实现的黑科技.

工厂使用与测试示例

到了这里, 已经可以归纳一下如何使用工厂来添加用例的流程了:

  1. TestPrototype基类继承;
  2. 使用__register宏注册类型;
  3. 实现test()接口task, 在test()里放入需要运行的代码.

下面通过一个示例来演示这个工厂demo的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
`define __add_test(TN) \
class TN extends TestPrototype; \
`__register(TN) \
function new(); \
$display("New in %s", `"TN`"); \
endfunction \
task test (); \
$display("Test in %s", `"TN`"); \
endtask \
endclass
`__add_test(foo)
`__add_test(bar)
`__add_test(baz)

首先用宏__add_test来定义一个简单的用例类原型, 用例类从TestPrototype继承, 使用__register注册, 提供了newtest()方法. 简单起见这两个方法只是提供了打印信息.

之后使用这个宏添加了三个用例类foo, bar, baz.

接下来是demo_test这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
task demo_test ();
foo fh;
TestFactory::register(fh, "fh");
fh = new();
TestFactory::register(fh, "fh");
TestFactory::run_test("foo");
TestFactory::run_test("bar");
TestFactory::run_test("luis");
TestFactory::run_test("baz");
TestFactory::run_test("fh");
endtask: demo_test

这个task里面显式的调用了工厂的接口, 并对空指针检查, key检查进行测试.

在top module的initial块里面调用这个task, 就可以得到如下的log:

1
2
3
4
5
6
7
8
9
10
# New in foo
# New in bar
# New in baz
# Can't register null object in factory!!
# New in foo
# Test in foo
# Test in bar
# Can't find luis in factory!!
# Test in baz
# Test in foo

这里提一句, 这里的为了测试工厂的API将TestPrototype子类的new方法公开. 如果能在使用的时候将new方法也设置为私有, 用例对象变成彻底的只在线程启动前例化的单例, 这样设计感更强, 是更好的实践方案.

命令行参数接口

接下来是最后一步了, 用$value$plusargs实现运行参数接口:

1
2
3
4
5
6
7
8
9
task factory_run_test ();
string tn;
if($value$plusargs("TEST=%s", tn)) begin
TestFactory::run_test(tn);
end
else begin
$display("Please offer a +TEST=<test_name> in simulation arguments");
end
endtask: factory_run_test

定义的参数形式是+TEST=<test_name>, 读取<test_name>传入工厂的run_test(). 这个task放在top module的initial block里, 整个run_test()系统就完工了.

结语

本文模拟UVM实现了一套轻量级run_test系统, 其中的主要机制(动态绑定, 对象的载入时例化和自定义运行参数)都是UVM代码中实际使用的技术.

这套代码只有短短的几十行代码, 但是麻雀虽小五脏俱全, 已经应用到我的个人项目当中了.

最后再附上这个demo的完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
virtual class TestPrototype;
pure virtual task test ();
endclass: TestPrototype
class TestFactory;
static local TestPrototype tests[string];
static function void register (TestPrototype obj, string test_name);
assert(obj != null)
else begin
$display("Can't register null object in factory!!");
return; end
if (tests.exists(test_name)) begin
$display("Override obj registered with '%s'!!", test_name); end
tests[test_name] = obj;
endfunction: register
static task run_test (string test_name);
if (!tests.exists(test_name)) begin
$display("Can't find %s in factory!!", test_name);
return; end
tests[test_name].test();
endfunction: run_test
endclass
`define __register(T) \
static local T reg_obj = get(); \
static local function T get(); \
if (reg_obj == null) begin \
reg_obj = new(); \
TestFactory::register(reg_obj, `"T`"); \
end \
return reg_obj; \
endfunction
task factory_run_test ();
string tn;
if($value$plusargs("TEST=%s", tn)) begin
TestFactory::run_test(tn);
end
else begin
$display("Please offer a +TEST=<test_name> in simulation arguments");
end
endtask: factory_run_test