电子说
接下来介绍行为型设计模式在UVM中的应用。
模板模式
Template method patttern: 在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。
模板模式的使用很简单,就是 继承 + 重写 的配合使用。在父类中定义一函数XXX(), 里面封装了函数 pre_XXX()和函数 post_XXX(), 当执行函数XXX()时,会自动执行 pre_XXX(), post_XXX(), 相当于在父类中固定了这种执行流程,pre_XXX() -> XXX() -> post_XXX()。pre_XXX(), post_XXX()扩展了XXX()的功能。子类继承父类,根据需求重写。pre_XXX(), post_XXX()相当于hook, 也可以广义的称为回调函数,但是和回调函数仍有区别,在下一节具体阐述。
UVM中常见:
squence中的start()前后插入pre_start(),post_start(), body()前后的pre_body(), post_body();
uvm_object中copy()被调用会自动执行do_copy(), compare()中的do_compare(), print()中的do_print(), pack()中的do_pack(), unpack()中的do_unpack()。
phase中的pre_xxx_phase, post_xxx_phase
SV调用randomize()自动调用pre_randomize(), post_randomize()。
这里就不在一一列举和代码展示了,
策略模式
Strategy Pattern: 定义一种算法类,将每个算法分别封装起来,让它们可以相互替换。算法调用者只需包含抽象算法的类然后调用算法,算法的具体实现被独立出来,保持主体的结构稳定。
策略模式让具体算法独立于算法的调用者,这里的算法指一个具体的行为,具备多态特性,可以实现重写,算法的调用可以简单理解为一个函数被调用。策略模式其实就是 组合 + 多态 的配合使用。
在第一篇中,提到了composition这个单词,也就是组合,合成的意思。相对于继承(Inheritance),合成使用了“有”(has-a)的关系,继承使用了“是”(is-a)的关系。对于抽象算法的实现,可以联系到第一篇中提到的interface class, 一种面向接口编程的特性。
下面将 composition 和 interface class两种结合起来,使用Systemverilog编写一个简单的策略模式的示例:
1. car 两个子类,sedan轿车和truck卡车,都需要请求加油。
2. 抽象类add_oil_behavior_interface, 两个子类 add_gasoline add_diesel 是一种 add_oil “算法” 不同的实现,一个加汽油,一个加柴油。
3. 在testbench中,通过set_oil_type将实例对象传入。类的多态,调用add_oil, 根据对象调用实际“算法”。
上面的例子通过策略模式构建,似乎有些繁琐,通过if else判断是卡车还是轿车选择加什么油,更简单明了。但是这里仅仅使用了一种车加油的“特征”,如果有更多种类的车,更多特征,比如载人数量,车身颜色,组件型号等,使用if else对各种类型的车做归类将使代码难以维护。如果卡车有一天加了汽油,只需要set_oil_type(add_gasoline_h)就可以完成,通过if else编写的代码显然hardcoding了。
上述代码还不够make sense,无路是汽车还是卡车,当被创建时,应该有一个default的默认加油方式,所以可以改写如下:
在汽车或者卡车被创建时,就指定一种加油方式。line20将卡车切换成加汽油,line24再切换成加柴油时,报错。因为变量被 protected 修饰,不可以在类外部调用。
UML图:
所以策略模式的 “策略” 体现在对算法的调用和算法的实现的解耦,我们把“算法”(具体函数处理)封装到一个抽线类中,在调用类中声明这个抽象类(这便是composition,调用类可以使用抽象类的方法),对抽象类实现或者重写成多个子类,调用类调用算法时,根据内部抽象类句柄指向的具体的子类对象,调用子类对象的”算法“(多态)。
策略模式很实用,在纯软件编程中非常常见。在UVM源码中也应用到了策略模式,比如default_sequence和uvm_callback。UVM中使用的策略模式和纯软件编程中所总结的策略模式稍有不同,但核心是一样的,通过 “ 组合 + 多态” 的方式,实现对算法的调用和算法的实现的解耦。
default_sequence
构建用例创建的sequence继承于uvm_sequence, 继承关系如图:
sequence不像component一样,没有被UVM赋予phase机制,sequence的运行需要“挂载”在sequencer上,一般有三种方式(UVM设计模式 (六)命令模式、三种sequence启动方式、start_item/finish_item、中介模式、virtual sequence):
1:采用default_sequence的方式启动
2. 直接调用sequence的start()函数启动
3. 在virtual sequence中调用uvm_do宏启动sequence
下面梳理下default_sequence中策略模式的应用:
1. 将case0_sequence“挂载”到sequencer上。
2. uvm_sequence_base中的虚函数start()调用了pre/post_start(), pre/post_body(),以及body()函数,这些函数都是虚函数,且没有定义任何操作。case0_sequence重写body()函数。
3. 当执行到main_phase()时,会执行uvm_sequencer_base中的start_phase_sequence()函数,uvm_config_db#() get::()获得case0_sequence的type_id, 然后调用factory的create_object_by_type创建case0_sequence的实例。(参考上一节工厂模式) $cast中的seq是uvm_sequence_base类型,多态。
4. 调用seq.start(), 执行body()函数的代码。
5. 只有使用default_sequence的方式启动,case0_sequence中starting_phase才不等于 null.
(只摘取了与策略模式有关的 code) + :
default_sequence的方式启动,会调用seq.start()函数来运行body()函数的代码, 不同用例body()函数的实现不同,这里的body()就相当于策略模式中的“算法",将body()函数的实现放在子类sequence中重写,实现解耦。
不同之处是,UVM中通过 confid_db和facotry结合创建sequence,更灵活。抽象类使用的是virtual class而不是interface class,区别在第一篇中有阐述。UVM设计模式(一)
callback
在学习uvm_callback之前,先看一下如何用Systemverilog写一个简单的callback。
1. 抽象函数Driver_cbs定义了抽象函数pre_tx(), post_tx(),然后子类Driver_cbs_drop重写了pre_tx()函数,实现了每100个事务随机丢弃1个事务的功能。在class Driver中声明了这个抽象类的队列 cbs[$] 。
2. 在test中创建Driver_cbs_drop的实例对象,然后放入到cbs[$]这个队列中。可以放进去的原因是类的多态,父类句柄可以指向子类对象。
3. 遍历cbs[$]中放入的callback类,执行子类对象的pre_tx()函数。
源代码:《SystemVerilog 验证-测试平台编写指南》 8.7 :
上述的callback和策略模式的实现方式一样(组合 + 多态),在Driver类声明抽象类,然后调用抽象类中的函数,根据类的多态,实际调用的是子类重写过的函数。解耦pre_tx()函数的实现与调用,保持代码结构稳定,提高扩展性。
不同点在于callback定义的 pre_tx() 更像一个hook钩子,callback 常在VIP中使用,为了满足不同使用者的需求(在 driver中实现注错或者异常 ;在 monitor中收集功能覆盖率;或者实现控制 objection 的 raise/drop功能 ),设计VIP的人员需要留出这个hook供使用者根据业务需求自行定义,使用者不需要了解VIP driver的具体实现,只关心这个hook的实现。从这点来看,callback和上一节中的模板模式更接近,只不过实现方式不同,模板模式利用 继承+重写 实现。callbcak提供的hook相比模板模式提供的hook,扩展性和复用性更好,但是实现更复杂些。实际工作根据业务需求选择合适方式预留hook。
而纯软件中的策略模式是为了将一类算法归一抽象,然后分别实现。侧重于每一种算法的相互替代,使算法的变化独立于使用它们的客户端(这里的客户端指使用算法的代码)。通过解耦控制模块代码的复杂度和代码量,解决大量使用if-else分支判断逻辑。除此之外,策略模式还能满足开闭原则呢,添加新策略的时候,最小化、集中化的代码改动,减少引入bug的风险。
但是遵循第一篇中提到的KISS(Keep It Stupid Simple)原则,怎么简单怎么来,就是最好的设计,非得用策略模式,搞出n多类,反倒是一种过度设计。设计模式之美
uvm_callback
uvm_callback的大体结构与上一节提到的callbcak类似,具体实现细节不在列举(可参考《UVM1.1 应用指南及源码分析》- 19 callback机制源码分析 ),分析侧重日常调用以及策略模式的相关内容:
下图红框由VIP开发者或者平台搭建者完成,黄框为callback调用者完成。
1. 设计一个class A包含虚函数pre_tran()作为hook,供driver调用。typedef 将 A_pool 定义为 uvm_callbacks#(my_dirver, A) 类型的参数化的类。(class A相当于上节的 class Driver_cbs)
2. class my_callback 继承 class A, 重写 task pre_tran()。(class my_callback相当于上节的 class Driver_cbs_drop)
3. 在tc的connect_phase()中创建my_callback的对象,调用静态函数 A_pool::add(),完成对uvm_callbacks_base类中静态关联数组m_pool的赋值。(uvm_pool看成关联数组, uvm_queue看成队列, m_pool索引为object(drv), 值为存放uvm_callback(my_cb)的uvm_queue )( 此处的 uvm_queue相当于上节的 cbs[$] 队列,调用函数 add()相当于上节 cbs.push_back(dcd) 。)
(不同之处,UVM引入了 m_pool 这个关联数组作为 “池子“,可以存放 A类型的 callback, 也可以存放 B类型的 callback, 所以 A类型对应一个队列,B类型对应一个队列。此例中只用到了m_pool 。class uvm_typde_callbacks # (type T = uvm_object) 中还有一个静态变量 m_tw_cb_q ,当 A_pool :: add (null, my_cb),第一个参数为 null,则 my_cb放入m_tw_cb_q队列中。null则不指定具体 instance,表明该 callback对这个 type实例的对象都有效 。m_pool全局只有一个,而m_tw_cb_q则每个 type对应一个)
4. `uvm_register_cb宏展开,调用静态函数m_register_pair(), 完成callback与object的配对。试想,如果平台设计者设计了callback A 给driver使用 `uvm_register_cb(my_driver,A),callback B给monitor使用 `uvm_register_cb(my_monitor,B),以及其他很多callback。而使用者在调用callback时却在A_pool::add() 中误加入了类型B的callback, 编译仿真都正常进行,但是实际hook并没有被调用。幸运的是,会打印一个warning供使用者debug,帮助他及时发现前面的错误。
5. `uvm_do_callbacks(T,CB,THIS,METHOD)展开后,创建uvm_callback_iter的实例,这个类提供的是iterator迭代器的功能,因为所有callback都放在了容器 m_pool 或 m_tw_cb_q 中,在此处调用的callback需要满足 uvm_object=my_driver, uvm_callback = A 的条件,需要对容器检索遍历获得。如上节单独的foreach循环无法满足要求,所以UVM提供了一个迭代器类专门负责此事。该示例中重复的过程如下:(此处对 uvm_queue的遍历相当于上节通过 foreach()对 cbs[$] 的遍历过程)
通过 m_get_q() 函数在 m_pool 中找到 和driver相对应的 callback uvm_queue(ref类型,赋值给get_first()函数中的q变量),然后在q队列中找到A类型的callback, 调用callback中的函数。然后重复执行上述步骤,直到 cb == null 结束。
uvm_queue中可能会放不同类型的callback,通过 $cast筛选出符合的类型。
如果使用 A_pool :: add (null, my_cb), 则是对 m_tw_cb_q 队列的遍历,这里没有列出。
更多内容见:UVM设计模式 ( 五 ) 迭代器模式、Python/SV中的迭代器、uvm_callback_iter、scoreboard中的迭代器
+ (源代码UVM实战 9.1.4):
根据上述红色斜体的内容描述,UVM中的callback和SV中的callback使用思路一致,也是 组合 + 多态 的实现方式,和策略模式一样。通过UVM工厂模式的重写功能也可以实现callback的效果,选择哪种方式要根据实际场景。
扩展使用
在实际工作中,可以采用策略模式进行解耦,将那些经常变化的“内容”抽象出来,在外部分别实现。
下面列举DVCon上的两篇策略模式实际应用的文章:
2016 DVCon US : Design Patterns by Example for SystemVerilog Verification Environments Enabled by SystemVerilog 1800-2012
将PackBehavior和CheckBehaviro这两个"行为”从base_packet中拎出来,外部实现重写interface class, 构建了 v1_pack, v2_pack, v3_pack, Parity, Crc这几个类。
根据不同版本V1,V2,V3继承base_packet,创建v1_packet, v2_packet, v3_packet类,调用setPackBehaviro(), setCheckBehaviro()赋予不同的"行为“。
2019 DVCon INDIA : Using Software Design Patterns in Test Bench Development for a Multi-Layer Protocol
和上一篇类似,也是对packet中的pack_behaviro和check_behavior的操作。业务是DSI中的PHY layer。
审核编辑:汤梓红
全部0条评论
快来发表一下你的评论吧 !