在线咨询
eetop公众号 创芯大讲堂 创芯人才网
切换到宽版

EETOP 创芯网论坛 (原名:电子顶级开发网)

手机号码,快捷登录

手机号码,快捷登录

找回密码

  登录   注册  

快捷导航
搜帖子
查看: 53957|回复: 99

[原创] UVM/OVM中的factory---------个人总结

[复制链接]
发表于 2014-7-9 19:56:31 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?注册

x
本帖最后由 asic_wang 于 2014-7-14 21:08 编辑

在熬夜看球之前,再记录一点自己的UVM体会;在新的项目中再次用到了UVM,距离上次项目自己写UVM代码已经快一年,所以很多UVM的细节东西自己也记不太清楚了,只能记得UVM各个点的主线,这次重拾UVM,又重点看到了factory部分,所以就在此mark一下,做个笔记。
      在记录中肯定有不对和偏颇的地方,一个人的看法毕竟有死角,所以看到不会的地方请大家及时拍砖和轻喷。
      主要有几个方面的体会:
      1. factory的需求来源是什么?或者说什么样的应用场景催生了这个东西。
      2. 我心目中的UVM factory的演进。我坚信好的东西(如体系结构、设计模式、牛逼的算法等)都是从最丑陋的东西演进过来的,从可以用到好用到通用到标准化是需要一个不断优化的过程的。
      3. 从使用方面来说,要到到同一个效果,UVM factory用多种形式可以达到,我想谈谈这些形式有哪些。
 楼主| 发表于 2014-7-9 21:22:29 | 显示全部楼层
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机制就可以做到。
 楼主| 发表于 2014-7-9 22:21:28 | 显示全部楼层
本帖最后由 asic_wang 于 2014-7-9 22:31 编辑

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
    (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)的影响会更恶劣!
 楼主| 发表于 2014-7-9 22:57:03 | 显示全部楼层
2.2 第一次改进
      使用查找表来记录每个对象是否需要被替换。
2.2.1 缺点的本质:
    (1)需要准确的时间点的本质原因是testcase不知道driver时候被创建的确切时间!所以需要找个最保险的时间点,那就是在所有的组件都创建之后,但是又不能早于connection phase,幸好uvm提供了各个phase的管理,否则这个问题解决起来就特别棘手!
           我们选择的时间点过于保守了,最合适的时间点就是在需要被创建的时候就做替换!也就是说每个对象的new函数调用处就是最佳的替换时间点。
    (2)需要知道层次结构的本质是在testcase层次以下没有任何东西来记录每个对象的路径!如果有这么一个地方,它记录了所有存在于环境中的对象的路劲,那么我只需要对它说,把你的记录里面每个driver对象都换成new driver。
           我只想对这个记录中心提供如下信息:
           1)我需要替换所有的driver,无论它分布于何处,它的层次路劲如何
           2)把driver替换成new driver.
           其余的交给记录中心来完成,我不想知道怎么做。
 楼主| 发表于 2014-7-9 23:10:52 | 显示全部楼层
本帖最后由 asic_wang 于 2014-7-9 23:19 编辑

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(也就是不替换)
         
           这种数据结构用关联数据再合适不过!
 楼主| 发表于 2014-7-10 00:19:20 | 显示全部楼层
未完待续....
 楼主| 发表于 2014-7-10 20:23:48 | 显示全部楼层
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住的。
 楼主| 发表于 2014-7-10 20:40:36 | 显示全部楼层
本帖最后由 asic_wang 于 2014-7-10 20:41 编辑

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中注册!
      注册越早越好!
发表于 2014-7-11 15:02:03 | 显示全部楼层
很不错!楼主继续!
 楼主| 发表于 2014-7-13 10:49:42 | 显示全部楼层
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],所以内存不释放!
您需要登录后才可以回帖 登录 | 注册

本版积分规则

关闭

站长推荐 上一条 /1 下一条

×

小黑屋| 关于我们| 联系我们| 在线咨询| 隐私声明| EETOP 创芯网
( 京ICP备:10050787号 京公网安备:11010502037710 )

GMT+8, 2024-3-29 13:28 , Processed in 0.036057 second(s), 8 queries , Gzip On, Redis On.

eetop公众号 创芯大讲堂 创芯人才网
快速回复 返回顶部 返回列表