| ||
正如我们之前在《跨平台的验证结构考量》中谈到的,SystemC/C/C++的用途广泛,在协助平台软件做驱动和固件开发时,可以用来模拟硬件的行为。而伴随着硬件的开发周期,这些virtual prototype也可以应用到:
验证环境中(作为参考模型)
设计环境中(暂时替代没有完善的硬件设计模块)
在近些年的功能验证报告中显示SV/UVM、SC、C/C++的使用率都在增长,上面谈到的混合仿真要求正是建立在这些语言应用都在增长的背景下的。它们都有着各自专场的领域:
C和C++模型是为了软件开发和untimed model(UT)。
SystemC是为了早期的硬件结构建模和软件开发。
TLM2通信是为了在独立的模型之间建立approximately timed(AT)和loosely timed(LT)事务通信。
Systemverilog在RTL级和门级仿真、随机约束激励、功能覆盖率和断言上发挥着重要作用。
UVM是为了开发模块化、可复用的、可剪裁的测试平台以及基于测试序列激励的验证方法学。
上面谈到的UT、AT和LT是针对SystemC建模的时间精准级别而言的,从精准级别来看,SystemC有能力可以完成从UT、AT、LT再到cycle-accurate,从宽松到严格时钟周期的行为。从这些不同语言的使用场景来看,那么使用混合或者多语言的SoC测试平台的需求越来越多也不是新鲜事了。而在2011年之后Open SystemC Initiative(OSCI),这个负责之前定义SystemC TLM1.0/2.0的组织跟Accellera(负责UVM的标准化推广)合并之后,在SystemC与UVM之间规范TLM通信就从更高的层面来统筹了。
这种统筹的合作方式,带来了“UVM Connect”package。这个包集成了已有的SystemC和UVM的TLM通信标准,继而使得SystemC与SystemVerilog之间的TLM模型可以实现通信。首先来看看UVM Connect通信包的一些特性吧:
开源的,用户可以在其基础上进一步开发。
便于移植,对于仿真器没有限制。
在最新的UVM标准基础上构建,不需要修改原有代码。
没有引入新的方法学,而是在不同的语言侧使用了转换接口。
上述的特性可以使得SystemC和SystemVerilog可以很方便地融合另外一端的TLM模型。
这些特性使得SC与SV的模型之间可以通过这个标准库完成快捷的通信,而且不需要修改原有的代码;同时它也将UVM Command API方法也输出到SystemC一侧,使得SystemC(C/C++)也可以方便地控制UVM仿真。 接下来我们就围绕着两个主要的特性,即UVMC连接和UVM指令API来简单介绍UVMC(UVM Connect)的特性。
UVMC连接
从上面的图中可以看到,在UVMC可以实现SV与SC之间的TLM1或者TLM2通信。考虑到基于UVMC的TLM1和TLM2连接方式类似,我们在下面的例码中只分析TLM2的连接。对于SV和SC,UVMC库提供类似的函数来完成相应的TLM port的注册:
上面的参数中,SV需要额外的transaction类型参数,默认情况下它的类型是uvm_tlm_gp;port_handle/port_ref即在SV/SC中port(port、export、imp或者socket)的句柄;lookup是用来注册该端口的字符串。在使用这些函数时,UVMC会在SV和SC两侧都注册端口的句柄和名字(lookup),而在后期的连接阶段中,只要有注册的端口名字匹配,那么UVMC就会将这两个端口连接起来,而并不关心它们是什么语言。也借助了这一优势,用户在连接时不需要关心它们是SV到SV的连接、SV到SC的连接、SC到SV的连接或者是SC到SC的连接,UVMC都可以协助完成连接和传输。
接下来我们就一个简单的例子,来理解SV与SC之间的混合仿真实现。下面的producer(SV)与consumer(SC)在仿真过程中是双顶层结构,即在混合仿真中,它们一开始并不会同步,例如SV触发SC一侧执行,或者SC触发SV一侧执行。在producer和consumer中,它们分别定义了initiator socket和producer socket。而在SV和SC的顶层例化中,它们通过UVMC提供的函数完成了SV到SC的连接。
SV部分的sv_main创建了producer的实例,而在执行UVM test之前(建立UVM环境之前),它先注册了producer的out initiator socket。而在SC的sc_main一侧,它也创建了consumer实例,并且在仿真开始前也利用UVMC对consumer的in target socket进行了注册。而在稍后的elaboration阶段(也可以理解为链接阶段)中,UVMC会将producer与consumer的socket连接起来。
在上面的连接中,SV与SC两侧的传输数据都是SV::uvm_tlm_generic_payload和SC::tlm_generic_payload,这两个类之间是“天然相通”的,用户并不需要再做额外的类型转化即可以完成语言两侧边界的通信。而在实际过程中,SV与SC两侧的TLM传输类在一开始如果没有一致的规划下,容易产生类型的不匹配,那么面对这种情况,我们首先需要做的是将SV与SC两侧不同的数据类内容进行合并,继而生成对于两侧均不会丢失信息的新的TLM传输数据类。所以,在统一了两侧原本不一致的TLM数据结构之后,两侧传递的数据不再是之前标准的数据类,而是自定义类。针对这种情况,我们需要完善创建相应的数据转化函数。例如,在SV于SC之间我们需要完成一个command、address和动态长度的数据队列,那么两侧的数据类定义应该保持匹配:
尽管SV与SC两侧定义的成员类型看起来一致,但这仍然无法直接完成数据传输,而依然需要完善相应的转化函数。无论是SV还是SC一侧,在完成从一端到另一端的传输之前,首先需要通过do_pack()函数来生成比特队列(bit queue),这么做的好处在于可以通过严格的比特长度检查来确认两边数据是否匹配,同时通过这样传输的方式对于内存的开销也是较为节省空间的(这种SV的unpacked和packed的数据存储方式比较类似,后者更节省空间)。而通过这种方式当由一端转化为另一端时,又会首先进行do_unpack(),以此来还原应有的数据结构,使得更便于操作运算。
由于SV/UVM一侧在使用`uvm_field宏时本身也会注册域成员(field member),考虑到宏的效率,我们推荐用户使用`uvm_pack/`uvm_unpack的方式在SV一侧实现自定义的do_pack()和do_unpack()回调函数。即略过默认的do_pack()函数,而要求使用自定义的do_pack()函数,这么做的另外一个好处是有更多的灵活性来调整最终输出比特队列的内容。需要注意的是,传送的数据应当继承于uvm_sequence_item,以便使用预定义的do_pack/do_unpack函数。
而在SC一侧,并没有类似于uvm_sequence_item支持数据预处理的类,因此读者需要将数据定义的类和数据转化的类分开。数据转化的类可以使用预定义好的模板类uvmc_converter<T>,该类允许用户通过参数化的形式指定要处理的数据类T,同时自定义do_pack/do_unpack方法。
在下面的例码中,除了传递数据类对象packet &t之外,也需要传递一个uvmc_packer &packer的实例,该实例的功能和UVM中的uvm_packer功能是一致的,都是将事务成员转化为比特位,或者与之相反。
通过上面在SV和SC两侧的数据转化就可以在极少的更新要求下,完成两侧的通信。也正是借助了UVMC这样一个开源库,使得SV和SC两端的TLM模型可以更好地融合,构成更丰富的VIP资源。UVMC的出现,降低了SV和SC的TLM模型复用的难度,并且有如下的优点:
原有的TLM模型不需要再继承其它的基类,而是通过了外部代理(external agent)的设计模式解决了这一问题。
SV/UVM一侧的transaction类并不要求在factory中注册,减少了对UVM factory的依赖性。
对于SV与SC两侧的数据转换,不需要额外的API函数。SV只需要用户实现已有的do_pack/do_unpack,而SC一侧用户不需要修改原有的TLM类代码,只需要在新添加的uvmc_converter类中实现。不同的transaction类可以有不同的转化方法。
SV与SC两侧的transaction类并不要求严格的一致性。从SV到SC或者从SC到SV的数据转换,都可以由各自的converter函数完成数据转换。这种方式也进一步提高了原有TLM模型的复用性。