1. callback机制是什么?
以最高效的方式完成芯片验证,一直以来都是验证人员的首要目标,那么最直接的方式就是环境的移植和重用,一个优秀的验证工程师,在开发环境的过程中,一定会考虑环境的继承和重用。
继承(extend)这个我们在实际工作中经常使用,主要是为了在原有功能的基础上扩展一些新的功能,比如我们的testcase一般都是从一个base_test继承而来的,如果继续将testcase继续细分,又会产生一些中间的function_base_test,主要目的是为了将一些公共的功能属性抽取出来提炼成公共的变量或方法供所有相关的testcase继承使用,这便是“继承”在环境复用时的主要用途。
除了继承,UVM还提供了一个callback机制,他的主要应用场景在于环境开发者预知部分功能在将来会存在多种使用场景,需要在环境中预留一个可扩展的hook,相当于针对部分功能函数或任务进行override,这不同于继承,虽然继承的时候也可以override其中的virtual方法,但是比起“大动干戈”对整个类继续继承扩展,如果有那么一种机制能够只是对局部的方法进行精准扩展,那么代码的可读性和灵活度将大大提升,callback机制就是用来完成这种精准扩展的。
callback机制比起上面的继承覆盖来说,区别主要在于环境开发者为环境使用者提前预留好了hook供使用者根据使用的需要进行扩展,这就要求环境开发者对环境的使用场景进行提前布局,需要有一些提前预见性。比起继承覆盖来说,callback机制更像是对简单粗暴的继承覆盖一刀切所进行的一次微创手术,环境开发者前期需要多付出一些effort,后期就可以一劳永逸了。
可以举几个例子说明下,什么场景下适合用callback机制,什么场景下只能用继承覆盖进行扩展。
先说一个典型的callback机制的应用场景。
我们在写driver的时候的run_phase流程一般是这样的:
virtual task run_phase(uvm_phase phase);
...
while(1) begin
...
seq_item_port.get_next_item(tr);
...
drive_tr(tr);
...
seq_item_port.item_done();
end
...
endtask
环境开发者在开发这么个driver的时候应该考虑的问题就是当收到包之后到发包之前,是不是要预留给使用者这么一个功能,就是对包进行一些预处理,例如注错功能,这样后期一旦需要进行注错时只需要把这个预处理的注错功能进行定制化的扩展就可以了,否者的话就只能从driver继承一个新的driver对run_phase进行override了,所以环境开发者可以在driver_tr(tr)调用之前添加一个pre_drive()的函数对包进行预处理,这个pre_drive()函数体的具体实现留给环境使用者去完成,这便是callback机制的典型的应用场景。
再看一个只能用继承覆盖而不能用callback机制的典型场景,还是以driver为例,例如我们需要开发一个DDR2的driver,开发完成了我们可能还不知道未来的DDR3、DDR4…具体长什么样,所以我们无法提前预留好hook给未来新的driver功能做扩展,但是通常我们知道的是一般这种产品的迭代一般都是向下兼容的,也就是说ddr3一定是兼容ddr2的全部功能,所以说在ddr2上能跑pass的case在ddr3上一定也是可以跑pass的,未来需要在原有ddr2的环境上升级到ddr3时,我们就需要将ddr2_driver继承产生ddr3_driver,然后重写其中的run_phase()乃至driver_tr()一些列的方法,使用factory机制将ddr3_driver去override ddr2_driver再把所有ddr2环境中的case全部跑一遍。对于这种使用场景就只能用继承覆盖,而不能用callback机制,callback机制最大的特点就是他的预见性,即对类中的局部方法的精准覆盖。
callback的概念有点类似于c语言的接口,如果想要实现某个功能,如果c语言有接口函数,那么直接调用接口,改写接口函数而实现用户需求的功能。而uvm的callback也是类似的思想,环境的开发者在一些组件中预留好接口,而在开发的过程中将这些接口函数嵌入到环境开发里,这样如果有用户想要实现满足自身需求的功能,只需要改写这个接口函数,即可达到自己的验证目的。
2. callback机制如何实现?
真如上文提到的,callback机制是环境开发者给环境使用者预留的hook,方便环境使用者对环境进行定制化的扩展。这就决定了callback机制的实现需要环境开发者和使用者共同换成,我们在讨论callback机制的实现步骤的时候就需要明确分工。
使用callback的简单步骤如下:
- 定义一个uvm_callback空壳类,并定义相关的callback方法(声明为virtual以便扩展覆盖,后期也可以根据实际需要扩充更多的callback方法);环境开发者
- 在UVM组件中注册上面定义的callback空壳类,并在组件代码恰当的位置内嵌callback方法;环境开发者
- 从UVM callback空壳类扩展用户自定义的uvm_callback类,并根据实际需求重写其中的callback方法;环境使用者
- 在验证环境中创建并添加uvm_callback实例。环境使用者
3. callback机制示例
还是以driver为例,先看看环境开发者需要做什么。图1是driver文件中的代码示例。
图1 driver文件代码示例
代码中的3-6行定义的pkt_process_callback类就是上文所述的环境开发者定义的空壳callback类,预留给环境使用者扩展的方法是pkt_pre_trans()任务,目的是让环境使用者根据自己的需要在发包之前对包做一些定制化的处理。这样便完成了步骤1的工作。
my_driver类中callback相关的代码是15行和33行,完成了callback类的注册以及callback方法的调用,这两个宏我们在后面的源代码分析时会具体介绍都做了什么,这里先理解成完成了步骤2的callback空壳类的注册以及callback方法的内嵌。
如此一来,callback机制环境开发者的相关工作就完成了,下面看环境使用者所需完成工作的代码示例,如图2所示。
图2 testcase文件示例
代码4-11行完成了从环境开发者定义的空壳callback类继承而来的环境开发者自定义的callback类,其中对方法pkt_process_callback()进行了重写,这里有个小的知识点,那就是扩展类中的方法pkt_process_callback并没有使用virtual进行修饰,这是因为SystemVerilog的语法规定了virtual方法在扩展类中被自动继承virtual属性,所以在扩展类里原有的virtual修饰的方法可以省去virtual修饰符。如此便完成了步骤3的工作。
代码的第25行完成了callback类的创建,这里创建必须使用factory机制的type_id::create()方法进行创建,方便后续对callback类的扩展override。31行将callback类添加到了相应的callback池子里了,后续源代码分析会对add()函数完成的具体工作和callback实现机制进行具体的分析。这样便完成了步骤4的工作。
经过上述4个步骤,我们便实现了对my_driver功能的扩展,在my_tc里发包的时候就会对包进行预处理,将包的payload强行改为32’h555aaa并添加打印信息,实现了最简单的一个callback机制的应用。
4. callback机制源代码分析
我们先看下UVM源代码中uvm_callback的定义,如图3所示。
图3 src/base/uvm_callback.svh中的的关于类uvm_callback的代码截图
uvm_callback本身作为用户自定义的callback类的基类,直接例化uvm_callback没有任何意义。类中除了定义了打开和关闭callback相关的功能(m_enabled),并没有其他具体的功能实现,甚至连m_enabled是如何控制callback的打开和关闭都没有提及。我们接着往下看。
回到一开始我们讲述环境开发者所做的步骤1和2,步骤1只是从uvm_callback继承产生了新的callback类,并添加了一个callback方法,并没有什么可讲的。
步骤2中的`uvm_register_cb宏定义如图4所示。
图4 src/macros/uvm_callback_defines.svh中宏uvm_register_cb相关的代码截图
相当于把图1中15行的代码转化为下面的代码。
static local bit m_register_cb_pkt_process_callback = uvm_callbacks#(my_driver, pkt_process_callback)::m_register_pair("my_driver", "pkt_process_callback");
由图2中38-40行的介绍可知要想使用uvm_callback源代码中的相关方法,首先必须对type-callback对进行注册,这个配对关系包含了uvm_callback类以及其callback方法所被使用的类(以下简称容器类),例如本例中的pkt_process_callback和my_driver,uvm_register_cb宏实现了将uvm_callback类(pkt_process_callback)注册到容器类(my_driver)中。
`uvm_register_cb宏等价于声明了一个静态的本地bit类型的变量 m_register_cb_pkt_process_callback,并且调用参数化的uvm_callbacks类中的静态方法m_register_pair进行了初始化,初始化即对上面提到的type-callback对进行注册。而这个uvm_callbacks类我们在第4步uvm_callback类创建的时候正是用的其中的静态方法add,下面我们来正式揭开这个uvm_callbacks类的庐山真面目,如图5所示。
图5 src/base/uvm_callback.svh中的关于类uvm_callbacks的代码截图
uvm_callbacks继承链条上的相关类如图6、图7所示。
图6 src/base/uvm_callback.svh中的关于类uvm_typed_callbacks的代码截图
图7 src/base/uvm_callback.svh中的关于类uvm_callbacks_base的代码截图
从上面UVM源代码中关于uvm_callbacks类以及uvm_register_cb宏相关的描述中我们可以得出如下结论:
- 参数化的类uvm_callbacks和uvm_register_cb宏对应的两个参数T和CB是配对关系,T表示用户自定义的callback类中callback方法是在哪个类中被调用的,缺省值为uvm_object类型(虽然绝大多数情况下我们都是用的它的子类uvm_component类型的扩展类),CB表示与用户自定义的uvm_callback类,缺省值为基类型uvm_callback。
- 参数化的uvm_typed_callback类只包含了容器类型,而与具体的uvm_callback类型无关,这就决定了该类可以匹配任何类型的uvm_callback类,他只跟容器类型T相关,其中定义的uvm_queue用来存放注册在容器类T中的所有uvm_callback对象,并且提供了诸多接口方法供uvm_callbacks类继承使用。
- uvm_callbacks_base作为一个单例模式类,它比uvm_typed_callback类更为抽象,uvm_typed_callback类使用uvm_queue类型存放了参数化的容器类型T所包含的所有uvm_callback类型,而uvm_callbacks_base则使用uvm_pool类型存放了容器类和uvm_queue所组成的键值对,它可以容纳所有uvm_callback类的type-uvm_callback配对关系。
- uvm_callbacks类继承了uvm_typed_callback类和uvm_callbacks_base类的所有属性和方法,并且绝大多数属性和方法都是静态类型的,加上uvm_callbacks_base类的单例属性,这样我们便可以使用uvm_callbacks类直接引用其中的静态属性和方法了,这其中比较重要的几个方法如图8所示。
图8 src/base/uvm_callback.svh中的几个常用方法的代码截图
下面依次对图8中的这几个函数做介绍。
uvm_callbacks类中定义的相关变量如图9所示,484-485行定义了两个类型super_type和this_type方便后面简化代码,而其中的m_inst就是this_type类型,它也使用了单例模式用于type-callback对的全局共享。
图9 src/base/uvm_callback.svh中uvm_callbacks类中定义的变量的代码截图
首先看m_register_pair函数,其中涉及的两个变量m_typeid和m_cb_typeid都是uvm_typeid_base类型,uvm_typeid_base的定义如图10所示,它实现了一个简化版的类似于factory机制的类映射表,其中包含了3个static类型的变量,分别是字符串类型typename以及两个key-value互换的关联数组。而m_typeid和m_cb_typeid的初始化则是通过544行调用get()函数间接调用uvm_typeid_base参数化的子类uvm_typeid中的get()函数获取的,而m_typeid和m_cb_typeid在使用uvm_typeid创建时的参数则分别对应着uvm_callbacks的两个参数T和CB,get()函数实现如图11所示。
图10 src/base/uvm_callback.svh中uvm_typeid_base类及其子类的代码截图
图11 src/base/uvm_callback.svh中get()函数实现的代码截图
这样通过uvm_register_cb宏调用m_register_pair函数便实现了uvm_callback类CB和容器类T的配对关系,如果没有指定CB类型(使用缺省值uvm_callback),映射关系则保存在uvm_typeid_base类的静态变量联合数组typeid_map和type_map中,如果指定了CB类型,则将type-callback对信息保存在uvm_callbacks_base中定义的m_this_type队列中。注册完成后会将m_regitsered变量置为1,方便后续查阅type-callback对的注册情况。
简而言之就是uvm_register_cb宏实现了将uvm_callback类与容器类绑定,并将绑定信息储存在uvm_callbacks类中的相关变量中。
下面继续看看add和delete这两个静态函数,字面意思就是添加和删除。add函数实现将用户自定义的callback对象cb添加到obj对象中,并且obj允许是一个null类型(调用该callback对象中的callback方法时第一个参数也用null即可,uvm_do_callbacks(null, CB, METHOD))。同一个obj对象可以注册多个uvm_callback对象,多个uvm_callback对象执行顺序由uvm_apprepend类型的参数ordering指定,枚举类型uvm_apprepend有两个值UVM_APPEND和UVM_PREPEND,UVM_APPEND作为缺省值,表示当前添加的uvm_callback对象在前面添加的uvm_callback对象后面执行,UVM_PREPEND反之。这里就不对add/delete函数的细节做具体介绍了,感兴趣的可以自行研究。
如此一来,关于UVM源代码中uvm_callback实现的4个步骤我们只剩`uvm_do_callbacks这个宏没有介绍了,这涉及到了callback方法是如何被调用执行的,如图12所示。
图12 src/macros/uvm_callback_defines.svh中宏uvm_do_callbacks相关的代码截图
由宏描述不难得出以下结论:
- uvm_do_callbacks 本质上调用了uvm_do_obj_callbacks,将其中的参数OBJ使用了缺省值this,区别在于this这个参数仅仅对uvm_component适用,而对于像sequence这种uvm_object类型中如果想调用uvm_callback方法的话则不能使用 uvm_do_callbacks 宏,而必须使用uvm_do_obj_callbacks,对应的OBJ参数可以使用null或者指定对应的注册了该uvm_callback类的uvm_component,这个有点像在sequence中使用uvm_config_db#(T)::get(OBJ, )的第一个参数,OBJ也是不能用this的,可以用null或者p_sequencer/m_sequencer。这里可以看看UVM源代码中还有个小错误就是133行的示例用法中宏函数的第1、2两个参数用反了,第一个参数应该是component,第二个参数是uvm_callback。uvm_do_obj_callbacks实现了将一个与uvm_object类型关联的uvm_callback类在某个类型T中执行起来。
- uvm_do_callbacks和uvm_do_obj_callbacks宏都会迭代的调用其中注册的所有uvm_callback对象中的指定方法METHOD,调用顺序由add时候指定的顺序决定。关于迭代器uvm_callback_iter类也定义在文件uvm_callback.svh中,这里就不展开讲解了,感兴趣的同学可以自行查阅。
回调函数的执行(步骤2)本质上都是依赖于步骤1、3、4上对callback相关类的定义、注册、创建、添加后在某个特定的时间点被调用执行的。 - 关于回调函数的执行有时候还会添加相关的控制变量来控制回调函数的开关,例如synopsis家的VIP中包含了很多供用户扩展使用的callback接口,相关的callback函数一般都会在configure中有个对应的enable变量控制其打开和关闭,这样callback的实现就包含了对应configure的enable以及callback类的继承实现两部分共同完成了。
如此一来,上文提到的UVM callback实现的4个步骤在UVM源代码中的具体实现我们便全部介绍完毕了。
5. 总结
本文通过一个具体的例子引出对UVM源代码中callback的实现机制进行了粗浅的分析,虽说不能让大家看完后对UVM中的callback机制有醍醐灌顶的感觉,但相信同学们看完在后面使用uvm_callback的时候应该会有一个更为立体的理解。
6.附录
大家在网上经常看到的关于UVM callback机制的应用基本上都是在driver或者monitor等uvm_component的扩展类中实现的,而在uvm_object的扩展类中使用则很少见到,既然上面在介绍宏uvm_do_obj_callbacks已经提到了相关应用,这里就在最后附上一段代码示例,介绍下如何在sequence中实现UVM的callback机制,相信大家看完之后应该会有更为深刻的理解,重要代码片段用红色框标出。
图13 将空uvm_callback类注册到vseq_0中,并在需要的位置通过宏uvm_do_obj_callbacks调用回调方法
图14 创建用户自定义的callback类并重写回调方法,将新的callback类添加到对应的type-callback对中
图15 仿真结果显示新的回调方法在sequence中被自动调用
这里需要注意的是不要混淆宏uvm_do_obj_callbacks的参数T和OBJ,T表示的是callback注册所在的类,而OBJ则是T的一个关联实例,相应的在uvm_callbacks#(T, CB)::add(cb, obj)添加type-callback对的时候也需要将obj与宏uvm_do_obj_callbacks中的OBJ参数匹配上。
在前面driver中使用callback的示例中,T用my_driver表示,OBJ则是用默认的driver的实例env.i_agt.drv,那么在my_driver中调用对应的回调方法是第三个参数就必须也是env.i_agt.drv,示例中使用的是宏uvm_do_callbacks缺省了OBJ参数,使用的是值this指代的就是当前类my_driver的对象。
同理,在sequence中使用宏uvm_do_obj_callbacks的OBJ参数也需要与uvm_callbacks#(T, CB)::add()的第二个参数保持一致,我们在宏uvm_do_obj_callbacks传递的OBJ参数值为null,那么在uvm_callbacks#(T, CB)::add()的第二个参数也为null。当然我们还可以有下面一种选择,就是把callback注册在sequence对应的sequencer中,所有的T都换成virtual_sequencer,对应的则使用virtual_sequencer的实例,方法类似于上述my_driver的用例,达到的效果相同,如图16、17所示,两种用法的差别部分用红色框标出。
图16 sequence中将uvm_callback注册到对应sequencer中代码1
图17 sequence中将uvm_callback注册到对应sequencer中代码2