原文地址:http://bbs.eetop.cn/viewthread.php?tid=452518&extra=&authorid=828160&page=1
在新的项目中再次用到了UVM,距离上次项目自己写UVM代码已经快一年,所以很多UVM的细节东西自己也记不太清楚了,只能记得UVM各个点的主线,这次重拾UVM,又重点看到了factory部分,所以就在此mark一下,做个笔记。
在记录中肯定有不对和偏颇的地方,一个人的看法毕竟有死角,所以看到不会的地方请大家及时拍砖和轻喷。
主要有几个方面的体会:
①factory的需求来源是什么?或者说什么样的应用场景催生了这个东西。
②我心目中的UVM factory的演进。我坚信好的东西(如体系结构、设计模式、牛逼的算法等)都是从最丑陋的东西演进过来的,从可以用到好用到通用到标准化是需要一个不断优化的过程的。
③从使用方面来说,要到到同一个效果,UVM factory用多种形式可以达到,我想谈谈这些形式有哪些。
1.factory的需求来源
我们的测试平台一般都是通过component来组织成一个层次化的树形结构的,测试平台做什么事情取决于各个包含在其中的函数或者任务。总有一些时候,我们希望改变现有的处理机制或者流程。
举例来说,我有一个通用的CPU总线的测试平台,在上层看来(软件同事们),这个东西对他们开放的接口就是两个:read(address,length),write(address,data),他们不管下面是PCIe还是RapidIO。假设目前的底层接口形式是PCIe总线,也就是说这个CPU agent中的的driver是一个PCIe的driver;有一天CPU换了,底层接口形式也跟着换成了RapidIO总线,那之前的CPU总线的测试平台能不能除了换个RapidIO的driver,其它不变?如果测试平台做的够好,应该是这个结果。但是怎么个换法?
(1)把cpu agent.sv找到,改代码,至少有一个地方需要如下改改:pcie_driver drv;--->rapidio_driver drv;
(2)其他更好的方法?测试平台不用动,只需要在testcase中说明我现在希望用RapidIO driver来替换PCIe的driver。
我们当然喜欢第二种。
当然如果你的cpu env是一次性的,第一种方法也不错,如果是那样的话就与UVM思想相违背了,也会挨你继任同事的骂。
如果你的cpu env将会作为另一个更大level的一个agent组件,情况就变得有点糟糕了;
如果你的cpu env将会例化很多个,用在一个更大level的环境里,情况就变得更糟糕了;
如果你的pcie driver被作为一个component单独用在一个大环境的很多地方,且分布于树形结构的不同深度的话,那方法(1)简直是噩梦!
再假如,目前我有一个环境,有10个pcie driver,其中5个作为sub component用在5个cpu agent里面,另外5个pcie driver单独作为sub component用在top env(前面的5个cpu agent包含于top env中)里面。我现在想吧cpu agent里面的3个pcie driver换成rapidio driver,剩下的2个保持pcie driver不变;top level里面的5个pcie driver也有相似的需求,如果是用方法(1)改代码,那你的测试平台将会很糟糕,不具有任何可扩展性,容易出错等等.......
不同的需求回很多,因为需求永远是在变化的,我们需要一种方法来很好的适应这种变化,factory机制就可以做到。
2.我心目中的UVM factory的演进
2.1 使用继承来解决
2.1.1 前提
(1)有一个基类叫做cpu_bus_driver,所以的cpu总线的driver都从这个base class继承。
(2)cpu_bus_driver中定义一系列的总线操作,而且这写总线操作对它的任何继承者来说都是充分的:
get_transfer():得到此次cpu操作类型(read or write),地址,长度,写数据;
drive_bus():把读写操作转化成最后的signal level的信号;
get_response():返回读数据或者写是否成功的响应(如果需要的话);
当然这些函数都是virtual的。
(3)cpu_bus_agent中这样实例化了driver。
class cpu_bus_agent ;
.......
cpu_bus_driver driver ;
function build();
........
driver = new("driver");
.........
endfunction
endclass
(4)测试平台的层次结构是:
cpu_env.cpu_agent.cpu_driver;
cpu_env在cpu_base_test中实例化。
2.1.2 方法
step1:
定义新类:new_cpu_bus_driver
class new_cpu_bus_driver extends cpu_bus_driver ;
........//定义自己的get_tranfer, drive_bus, get_response函数
endclass
step2:
替换原有的类。
因为不能修改cpu_env及其一下各层次的代码,所以只能在testcase中进行替换:
class cpu_test1 extends cpu_base_test ;
new_cpu_bus_driver new_driver ;
......
new_driver = new("new_driver");
cpu_env.cpu_agent.cpu_driver = new_driver ;
.......
endclass
2.1.3 效果
没有修改任何cpu env及其一下层次的代码。
2.1.4 问题
(1)上面标示的蓝色粗体为关键代码,但是这个语句的执行时间点需要精确把控。
如果太早,则在运行cpu_agent.build()之后cpu_agent中的driver又会被重置为cpu_bus_driver而不是new_cpu_bus_driver;
如果太晚,则可能cpu agent可能已经用cpu_bus_driver运行了一段时间了,然后才会切换成new_cpu_bus_driver,这样回导致前面的错误操作。
最佳时间点显然是在所有的build phase之后,任何connection phase之前。
(2)开发testcase的人需要准确的知道cpu env的层次结构,也就是说需要知道xxx.xxx.xxx.xxx形式,很显然这是个缺点;
(3)如果有100个cpu driver需要替换,那么就需要100次
cpu_env0.xxx.xxx.old_driver = new_driver ;
....
cpu_env99.xxx.xxx.old_driver = new_driver ;
如果你觉得这个也不是问题,因为层次结构很规整嘛;那还有更麻烦的:
old_driver被单独用到了一些地方而不是只用在了cpu agent里面,比如 A_env.xxx.xxx.old_driver,B_env.xxx.old_driver,C_env.xxx.xxx.xxx.xxx.xxx.xxx.xxx.xxx.xxx.xxx.old_driver......
那你准备怎么办?
而且这种情况下缺点(2)的影响会更恶劣!
2.2 第一次改进
使用查找表来记录每个对象是否需要被替换。
2.2.1 缺点的本质
(1)需要准确的时间点的本质原因是testcase不知道driver时候被创建的确切时间!所以需要找个最保险的时间点,那就是在所有的组件都创建之后,但是又不能早于connection phase,幸好uvm提供了各个phase的管理,否则这个问题解决起来就特别棘手!
我们选择的时间点过于保守了,最合适的时间点就是在需要被创建的时候就做替换!也就是说每个对象的new函数调用处就是最佳的替换时间点。
(2)需要知道层次结构的本质是在testcase层次以下没有任何东西来记录每个对象的路径!如果有这么一个地方,它记录了所有存在于环境中的对象的路劲,那么我只需要对它说,把你的记录里面每个driver对象都换成new driver。
我只想对这个记录中心提供如下信息:
1)我需要替换所有的driver,无论它分布于何处,它的层次路劲如何
2)把driver替换成new driver.
其余的交给记录中心来完成,我不想知道怎么做。
2.2.2 缺点的改进
(1)怎么样才能在每个对象new的时候就能够做替换呢?
首先,我们需要知道的一条重要的限制就是new函数返回的对象永远都是它声明的类类型的对象!
也就是说 :A a_obj ;=》任何时候我们调用a_obj = new;返回的对象永远是A类类型的,不可能是其他类,连A的继承类也不可能!就是注意点彻底的告诉我们,需要一种方法(假设叫create方法)来代替new函数,当我调用它的时候,它不是100%返回A类型,而是视情况而定,什么情况?那就是:当我不像替换A的时候它返回A类类型的对象;当我想用A的派生类A1替换A的时候它能返回A1类类型的对象;当我想用A的派生类A2替换A的时候它能返回A2类类型的对象……
我们不应该在这里用new,而是用create!
(2)很明显,我们需要数据库来记录每个对象的基本信息
第一个数据库的每个条目可能如下:
层次路径 对象类型
“a.b.c.obj1” A
“a.b.obj2” B
“a.b.obj3” A
第二个数据库的每个条目可能如下:
原对象的类型 希望被替换成的类型
A A1
B B2
C C(也就是不替换)
这种数据结构用关联数据再合适不过!
2.2.3 实现
对于缺点1的改进,按照我们的分析,应该做如下改变:
a) 在cpu agent中创建driver的时候不用new函数,用另外一个叫做create的函数,用这个函数取代new函数。问题来了,这个create函数应该放在哪里?具有神马性质?
首先,这个create函数可以放在cpu driver类及其子类中;或者外面。我们假设放在cpu driver类及其子类中。
其次,既然能够取代new函数,那么必须不能针对于某一个具体对象,不然不能够调用;所以create应该是静态的。
再则,我们的主要目的是需要这个函数视是否需要被替换的情况而来产生出对应的对象。达到这个目的可有很多种方法,最直观的一种是:在create之前我去中心数据库查一下先,如上述的第二个数据库,如果目前查到这样一条条目:/“原对象类型 cpu_bus_driver”,"需要替换",“替换成的对象类型 new_cpu_driver”/,那么我就可以知道虽然实在调用cpu_bus_driver的create函数,但是实际上是想让我生成cpu_bus_driver的继承类new_cpu_bus_driver!
对应缺点2的改进需求,我们可以建立一个数据库中心,取名叫做factory,它的核心内容就是若干个map,每个map中的条目都是为create函数来服务的。
好了,基于上述的分析,我们写出如下的伪代码:
class cpu_bus_driver ;
.......
virtual function cpu_bus_driver create_object();
cpu_bus_driver me ;
me = new ;
return me ;
endfunction
static virtual function cpu_bus_driver create();
cpu_bus_driver key;
key = new ;
new_driver = factory.create_object(factory.lookup(key));
return new_driver ;
endfunction
endclass class new_cpu_bus_driver extends cpu_bus_driver;
.......
virtual function cpu_bus_driver create_object();
new_cpu_bus_driver me ;
me = new ;
return me ;
endfunction
static virtual function cpu_bus_driver create();
new_cpu_bus_driver key;
key = new ;//用key作为键字去factory的数据库中查出替换条目,得知想不需要替换factory利用查得的结果,生成一个new_cpu_driver对象给我,叫做 new_driver
return new_driver ;
endfunction
endclass class factory ;
cpu_bus_driver override_map [cpu_bus_driver] ;
function cpu_bus_driver lookup(cpu_bus_driver key);
return overrride_map[key];
endfunction
function cpu_bus_driver create_object(cpu_bus_driver override_obj);
return override_obj.create_object();
endfunction
endclass
好了,举例来说明一下具体的工作过程:
a)声明一个 cpu_bus_driver driver;
b)调用 driver = cpu_bus_driver::create();
b1)key = new ;////key是cpu_bus_driver类的;
b2)factory.lookup(key);////返回一个new_cpu_bus_driver信息;
b3)factory.create_oject(new_cpu_bus_driver);
b4)调用new_cpu_bus_driver::create();
b5)new_cpu_bus_driver::create()调用new_cpu_bus_driver的new函数生成一个new_cpu_bus_drive类型的对象赋给driver。
经过上诉调用后driver就不是它声明是的cpu_bus_driver类型的一个对象了,而是一个地地道道的new_cpu_bus_drive类型的对象了,唯一一点的遗憾就是这个对象是用父类的句柄hold住的。
2.3 第二次改进
细心的朋友可能已经注意到上述实现的一个问题就是,factory的override map的键字是一个cpu_bus_driver的对象,而我的cpu_bus_driver类型的对象可能很多,举例来说
如果override map中的cpu_bus_driver键字是一个叫做 “张三”的,很明显,如果我随意创建一个新的cpu_bus_driver类型的对象“李四”,虽然他们都是cpu_bus_driver类型的,但是用“李四”去查factory的override map时,就会查不到任何条目!
这个就是2.2的实现中的一个问题!
如何改进?很自然我们想到一种解决方案:
用一个全局的众所周知的cpu_bus_driver对象去做键字。这个对象是给factory的override map专用的,不做他用,假设“张三”就是这样一个对象。这样一来问题就解决了,任何时候只要是想查询cpu_bus_driver类型的override条目,我只需要用 factory.override_map[“张三”]来查询即可!
这个解决方案看起来好多了,也解决问题了,但是记得有一个前提就是:
记得在进行任何查找override map[“张三”]之前,确保这个条目已经存在!
看到这里,大家可能已经马上就想到了UVM中的factory register,没错!就是要向factory中注册!
注册越早越好!
2.4 第三次改进
在第二次改进中,我们提议用一个众所周知的全局对象来向factory注册,然后用这个注册的全局对象来作为override map的查询的key来得出是否需要替换成别的子类的结论。这没有任何问题,但是不优美!
原因有如下几点:
(1)比如我有一个类叫做tcp_pkt,我希望我可以创建一个叫任何名字的tcp_pkt的对象,而不是叫“张三”的名字我不能取,因为这个“张三”被factory专用!
在一个大型的环境中,会有很多个用户定义的类类型,比如除了tcp_pkt,还有ip_pkt,udp_pkt,还有其它方面的如ethernet_driver,ethernet_monitor.........假设设计中有100个类类型,那我岂不是要记着100个“张三”,“王三”,“李三”.......这些factory专用对象!
(2)性能和效率太差!为什么这么说呢?
如果我定义的类很复杂,那么这对象势必需要专用很大的内存,而每个类都需要一个factory专用对象,这些对象一直存在着[因为在override map随时都有可能需要lookup],所以内存不释放!
既然问题分析了,那就要找出对策了!
问题在哪里?想一个问题:factory的override map里需要的最根本的信息是什么?没错,那就是类型而非一个具体对象!factory只想知道也只需要知道在我的override map里面有一个条目的含义是类型A需要不需要替换成A的子类,如果有,是A1还是A2还是其它.....我不像知道A里面是什么,有什么,做什么!
很显然,我们想factory的override map里面注册的东西过于饱含信息量了!叫好比说,我在你家看到你的电视机我很喜欢,我也想去买一个,我其实只想让你告诉[注册]我电视机的型号[类型],但是你却给我这个电视机[对象]让我抱着它去商场里比对这个实物电视去买!不错,我这样是能买到这个牌子的电视机,但是这样做是不是最好的办法呢,显然不是的。
但是systemverilog有一个关键字叫做type,如果我声明以下一条语句:
type a_type;
那么我希望能够这样来操作这个a_type变量:
if(a_type == int)......
if(a_type == uvm_component).....
a_type = uvm_object; .......
它就是type operator,也就是type操作符!
我可以使用诸如 var type(uvm_component) my_component这样的声明语句,也可以使用如下负责的判断语句:
bit [:] A_bus, B_bus;
parameter type bus_t = type(A_bus);
case (type(bus_t))
type(bit[:]): addfixed_int #(bus_t) (A_bus,B_bus);
type(real): add_float #(type(A_bus)) (A_bus,B_bus);
endcase
那就是我们的facotyr中的lookup函数必然会有这样的结构所表达的含义:
case(type(lookup key))
type(cpu_bus_driver): .......
type(tcp_pkt):.......
type(A):.......
........
endcase
注意,我指的是用这case语句结构表达的含义的本质,而不是说用case语句来表达!
所以回到那两点问题上来,这两个问题的本质就是:
(1)注册对象的全局性,换句话说有唯一性的需求;
(2)信息的必要性,换句话说不要信息过量;
所以,有一个最普通不过的方法来做就行了,怎么做呢?
(1)tcp_pkt的“张三”对象你不希望我留着专用,那好,我给tcp_pkt变身一下,再定义一个新的类型叫做tcp_pkt_wrapper,而这个tcp_pkt_wrapper类型不会被任何的用户代码所用到,那么只要我生成一个tcp_pkt_wrapper的singleton不就行了,那么即不会影响用户使用tcp_pkt[你现在可以创建一个对象叫做“张三”等其它任何名字了],也满足唯一性的要求了!
(2)那么透过tcp_pkt_wrapper能不能也把信息不要过量也一并满足了呢?这个wrapper再合适不过了,为什么?因为sv可以定义参数化的类类型,我把这些tcp_pkt,cpu_bus_driver,A等等作为模版参数传给tcp_pkt_wrapper,cpu_bus_driver_wrapper,A_wrapper.....岂不是很好!
所以通过sv的type操作符,我们达到了不要信息过量的目的。
整合上述两点,以tcp_pkt来说,我们只需要定义这样一个wrapper就达到目的了:
class tcp_pkt_wrapper(type T=tcp_pkt);
.......//实现singleton......
.......
enclass
3 UVM/OVM中的factory机制
上诉的描述已经基本上把一些本质的东西都提及了,太过细节的东西没有必要在分析下去了;那我们就结合UVM中的factory来看看UVM的factory是如何实现的。
(1)任何user defined 的class都是从uvm_object来
(2)定义公共的wrapper类,uvm_obect_wrapper,此类的存在是为了提供一个基类,以便作为factoryoverride map的lookup key和 lookup value;根本原因是因为每个参数化类都是一个不同的类型,如果没有一个公共基类,则无法用一个统一类型的句柄来作为override map的lookup key。
(3)在(2)的基础上定了模版类 uvm_component_registry和uvm_object_registry,模版参数就是user defined的类。
(4)每一个uvm_component_registry#(T)或uvm_object_registry#(T)都会有一个singleton用来作为user definded类型T的轻量级代理和factory的注册。
(5)factory有多种override map,用来进行不同override方式的支持,如override by name、override by type、override inst、override type等。
(6)当我们使用uvm_object_utils/uvm_component_utils的时候,这些宏的重要作用之一就是利用上述机制像uvm_factory中注册,定义变量type_id,定义函数get_type()等。
看到这里,我最想说的是上述这些就是我对uvm class reference文档里下面这段话的理解,原文是:
”the uvm_object_wrapper provides an abstract interface for creating object and component proxies. Instances of these lightweight proxies, representing every uvm_object-based and uvm_component-based object available in the test environment, are requested with the uvm_factory. When the factory is called upon to create and object or component, it finds and delegates the request to the appropriate proxy.“
4 factory的本质
4.1 软件工程设计模式中的factory的定义
对“*”的《设计模式》书中工厂模式的定义,大意是:工厂模式定义了一个接口,这个接口是为创建对象而存在,而且它可以让对象的子类来决定对象的创建。工厂方法把对象的实例化推迟到自己的子类中。
如果不结合实例,“*”的书总是难以理解。就我个人而言,这本书看过几遍,但是看不懂,对书中的理解总是在碰到实际的例子的时候再去看看是否和书中的某个模式能对上号。我觉得如果软件工程方面的实践过少、没有软件架构设计的经历,这本书大部分东西完全看不懂;好在其中有几个最常用的模式相比较而言还是容易一点,factory模式就是其中之一。
以下是我能想到factory的实例。前提如下:
1)假设有一个餐馆提供快餐服务,大家可以通过订餐函数来得到一份盒饭
2)目前餐馆能提供鸡肉套餐、鸭肉套餐和牛肉套餐
3)每种套餐都由“取材”,“清洗”,“烹饪”,“起锅打包”四部组成,只有“取材”步骤依赖于套餐名;且每种材料都会定义“清洗”,“烹饪”,“起锅打包” 这三个步骤
4)取材的结果是“鸡肉”对象,“鸭肉”对象,“牛肉”对象,它们都是以类型“材料”为基类
为了服务客户,餐馆类最开始可能是这个样子的:
class 餐馆 ; //类型名字为餐馆
套餐 function 订餐(string 套餐名);//函数返回值:盒饭,函数名:订餐,函数入参:套餐名
材料 base ; //定义一个材料句柄,用于hold取材步骤的结果
////step 1:取材
case(套餐名)
“鸡肉”:base = new 鸡肉 ;
“鸭肉” base = new 鸭肉 ;
“牛肉” base = new 牛肉 ;
endcase
////step 2:清洗
base.清洗();
////step 3:烹饪
base.烹饪();
////step 4:起锅打包
base.打包();
return base ;///经过上述处理就变成套餐了
endfunction
endclass
客户订一份鸡肉餐过程如下:
1)找个一个餐馆,也就是get a inst of “餐馆”:餐馆_inst
2)餐馆_inst.订餐(“鸡肉”)
上述的实例将会产生factory模式的需求。
可以想想,用 设计模式 的一些术语来列一下上述这种实现有如下一些缺点:
1)在对实现编程,而不是接口编程
因为取材过程需要调用new,而且使用new返回的特定对象,这就意味着在对实现在编程
2)对扩展封闭,对修改开放
如果餐馆新增一个鱼套餐或者因为牛肉太贵不提供牛肉套餐了,你是不是要修改case语句?
对策:使用面向对象的基本对策,即找到变化的部分,将其封装,留下固定部分。
上述例子的变化部分不言而喻,就是那个取材部分的case语句!事实上,在使用具有面向对象特性的语言进行设计的时候,case语句往往意味着你这里的东西是会带来扩展性、维护性问题的。
改进将是直接的,把取材部分拧出来,然后封装成一个class,我们这个类叫做工厂,你没有看错,工厂的前身就是如此简单和直接。
第一次改进后的效果:
class 餐馆 ; ///类型名字为 餐馆
factory factory_inst; /////假设有一个专门的取材工厂
套餐 function 订餐(string 套餐名);////函数返回值:盒饭,函数名:订餐,函数入参:套餐名
材料 base ; /////定义一个材料句柄,用于hold取材步骤的结果
////step 1:取材
base = factory_inst.create(套餐名);
////step 2:清洗
base.清洗();
////step 3:烹饪
base.烹饪();
////step 4:起锅打包
base.打包();
return base ;///经过上述处理就变成套餐了
endfunction
endclass class factory ;////新增的取材工厂
材料 function create(套餐名);
材料 base ;
case(套餐名)
“鸡肉”:base = new 鸡肉 ;
“鸭肉” base = new 鸭肉 ;
“牛肉” base = new 牛肉 ;
endcase
return base ;
endfunction
endclass
这种改变看似仅仅挪动了case语句,但是它带来的好处就是把修改点集中在factory中,让不会改变的固定部分变得简洁。
可能你会说,这也叫工厂模式?如此而已?我想说,这个目前来说只能叫一个好的面向对象编程习惯,还不能称之为一个模式,但正是这个习惯造就了以下更加完美的factory。
显然,问题依然存在,那就是class factory中的case语句。针对接口编程的规则意味着我们应该如下改变factory的create函数:
class factory ;////鸡肉取材工厂
材料 function create(套餐名);
//根据套餐名来自动生成一个所需的对象 ;
endfunction
endclass
这样的话,case语句没有了,但是create函数中“根据套餐名来自动生成一个所需的对象”该如何做才能达到case语句的效果呢?
换个角度,类自己知道自己会create成什么样子。什么意思?意思是鸡肉工厂一定知道自己create成鸡肉,而不是牛肉或鸭肉!话到这里,思路清晰了,没错那就是用继承机制来替换case语句的效果。
第二次改进的效果 :factory模式的第一种变体
class 餐馆 ; ///类型名字为 餐馆
factory factory_inst; /////假设有一个专门的取材工厂
套餐 function 订餐();////函数返回值:盒饭,函数名:订餐
材料 base ; /////定义一个材料句柄,用于hold取材步骤的结果
////step 1:取材
base = factory_inst.create();
////step 2:清洗
base.清洗();
////step 3:烹饪
base.烹饪();
////step 4:起锅打包
base.打包();
return base ;///经过上述处理就变成套餐了
endfunction
endclass virtual class factory ;////定义取材工厂的接口
pure 材料 function create();
endclass class 鸡肉factory extends factory;
材料 function create();
return 鸡肉;
endfunction
endclass class 鸭肉factory extends factory;
材料 function create();
return 鸭肉;
endfunction
endclass class 牛肉factory extends factory;
材料 function create();
return 牛肉;
endfunction
endclass
如此一来,客户的订餐流程变成了
1)找个餐馆,告诉你想要鸡肉套餐
2)餐馆工作人员去取材工厂如菜市场取来鸡肉
3)餐馆完成套餐递给你
persudo code就是:
step1:餐馆_inst = new 餐馆 ;
step2:chicken_factory = new 鸡肉factory ;
step3:餐馆_inst.factory_inst = chicken_factory ;
step4: 餐馆_inst.订餐();
来看看现在是改进点:
1)餐馆的订餐函数不用再改了,是在对factory的create接口编程
2)增加一个鱼套餐;ok没问题:
class 鱼factory extends factory ;
材料 function create();
return 鱼;
endfunction
endclass
persudo code就是:
step1:餐馆_inst = new 餐馆 ;
step2:chicken_factory = new 鱼factory ;
step3:餐馆_inst.factory_inst = chicken_factory ;
step4: 餐馆_inst.订餐();
减少菜品更没问题,不用就是了。
上述订餐的4个step中,关键一步就是第step2:
persudo code:
step1:餐馆_inst = new 餐馆 ;
step2:chicken_factory = new 鱼factory或者鸭肉factory或者鸡肉factory或者牛肉factory ;
step3:餐馆_inst.factory_inst = chicken_factory ;
step4: 餐馆_inst.订餐();
就是这一步把工厂模式的定义中的“把对象的实例化推迟到自己的子类中”这句话体现的淋漓尽致!
顺便提一句,在我们做ASIC verification时,上述的这4步订餐步骤往往对应着testcase中的configure的某些部分。
还有没有别的方法可以达到同样的效果,且看factory模式的第二种变体:
我们想想,除了用class可以封装东西,还有什么可以?不要忘记我们在学习C语言时的一个好的编程习惯:用函数封装功能点或者变化点!很容易在学习了面向对象语言之后,什么东西都想着用继承来搞定,不要忘记了函数也是一个利器!
那我们就尝试着用函数来封装变化点吧。
class 餐馆 ; ///类型名字为 餐馆
套餐 function 订餐(套餐名);////函数返回值:盒饭,函数名:订餐,函数入参:套餐名
材料 base ; /////定义一个材料句柄,用于hold取材步骤的结果
////step 1:取材
base = create(套餐名);///这次用自己的一个函数来封装自己的变化点
////step 2:清洗
base.清洗();
////step 3:烹饪
base.烹饪();
////step 4:起锅打包
base.打包();
return base ;///经过上述处理就变成套餐了
endfunction 材料 function create(套餐名);
材料 base ;
case(套餐名)
“鸡肉”:base = new 鸡肉 ;
“鸭肉” base = new 鸭肉 ;
“牛肉” base = new 牛肉 ;
endcase
return base ;
endfunction endclass
显然,我们遇到了需要去除case语言影响的同样难题!解决方案很简单,既然是同样的难题那就用同样的方法:继承!
用继承改进后的factory模式的第二种变体:
virtual class 餐馆 ;
套餐 function 订餐();////函数返回值:盒饭,函数名:订餐
材料 base ; /////定义一个材料句柄,用于hold取材步骤的结果
////step 1:取材
base = create();///这次用自己的一个函数来封装自己的变化点
////step 2:清洗
base.清洗();
////step 3:烹饪
base.烹饪();
////step 4:起锅打包
base.打包();
return base ;///经过上述处理就变成套餐了
endfunction pure 材料 function create(套餐名);
endclass class 鸡肉餐馆 extends 餐馆;
材料 function create();
return “鸡肉” ;
endfunction
endclass class 鸭肉餐馆 extends 餐馆;
材料 function create();
return “鸭肉” ;
endfunction
endclass class 牛肉餐馆 extends 餐馆;
材料 function create();
return “牛肉” ;
endfunction
endclass
对应的订餐perdudo code就是:
step1:餐馆_inst = new 鸡肉餐馆 ;
step2: 餐馆_inst.订餐();
对于上述两种变体,第一种变体就好比说餐馆只有一家,有很多种食材市场,你需要挑一个食材市场得到食材,餐馆只负责帮你加工;而第二种变体就好比说有很多家不同种类的餐馆,你主动挑一家。
用一个UVM中的例子来说明其对应的factory中的知识点:
cpu_bus_driver = pcie_bus_driver::type_id::create();
1)create就是暴露给用户的编程接口,相等于取材create函数;注意,不是用new函数了,这是根本所在,一用new函数那必定就是生成一个确确实实的cpu_bus_driver对象而不是pcie_bus_driver对象
2)pcie_bus_driver就相当于鸡肉餐馆等,而cpu_bus_driver就相当于餐馆基类
显然,UVM所用的factory模式和第二种变体较为接近。
type_id的引入是对factory模式的再一次提升!为何如此说?
再次以前面的那个餐馆实例来说:
假设现在的餐馆提供的鸡肉套餐是土鸡肉做的,突然间土鸡肉出问题,找个替代品吧,用乌骨鸡肉代替。
那么按照变体二的实现方式,需要新定义一个土鸡肉餐馆,我们画出一个继承树形结构来就是如下形式:
餐馆
|
————————— |——————————
| | | |
土鸡肉餐馆 乌骨鸡肉餐馆 鸭肉餐馆 牛肉餐馆
我们知道土鸡和乌骨鸡都是鸡,所以我们希望树形结构如下可能更好:
餐馆
|
|—————— |————————
| | |
鸡肉餐馆 鸭肉餐馆 牛肉餐馆
|
|——————|
| |
土鸡肉餐馆 乌骨鸡肉餐馆
从第一个树形图演变到第二个树形结构就是所谓的UVM中的override机制;举例来说,如果对应的factory中的override map的一个entry是:
原类型 override类型
“鸡肉餐馆” “乌骨鸡肉餐馆”
那么餐馆_handle = 鸡肉餐馆::type_id::create() 实际上相比前面的factory第二种变体形式多了一个步骤,那就是lookup override map这一步。
而在UVM执行type_id::create()的时候,包含了以下几步的调用:
1)调用type_id的create函数,也就是uvm_registry#(鸡肉餐馆)的create函数
2)查询override map的“鸡肉餐馆”entry,得到“乌骨鸡餐馆”
2)调用factory中的create_by_type(type = 乌骨鸡餐馆)
3)调用乌骨鸡餐馆的create函数,这就是为什么每个uvm_object都要实现create函数的根本原因,这个uvm_object类及其派生类中的create函数的调用过程也很好的诠释了“把对象的实例化推迟到自己的子类中”
由此可见,UVM中的factory机制比通常的factory机制更近一步,更加完美!
顺便说一句,每个类都需要像uvm factory进行注册,而这个注册的思想多少借鉴了另一个设计模式:observer模式,这个模式也相对比较容易理解,就不在此记录了。
至此,我个人心中的uvm factory的内部机制就是如此,欢迎大家贡献自己的看法,谢谢!