工欲善其事,必先利其器。在芯片设计流程中,某种类型的芯片是否有模拟器,对该类芯片的架构设计至关重要。这其中,最具代表性的就是CPU的模拟器如开源的gem5等。模拟器的核心就是用C/C++等软件语言去描述芯片的工作流程,这里需要解决的首要问题便是如何采用“串行”执行的软件语言去描述“并行”执行的芯片中各个模块的行为。同时,如何做到在保证效率的前提下还能最大程度的仿真芯片的真实行为,是其中最核心的问题。如果描述的精度是时间触发的级别(如时钟),则跟用VHDL和Verilog写出来的没有太大区别,复杂且仿真效率低下;但如果采用事件触发的思想,则常常又过于粗糙无法真实的反应硬件的运行机制。CPU的模拟器、网络仿真工具opnet和NS2等是如何设计出来的,其核心思想是什么,能否自己开发一种自己设计的芯片的一种模拟器?本文将为你一一解答。
长期以来,在设计芯片时经常遇到这样的困惑,采用传统流程设计某种类型的芯片时周期很长,某些模块的特点至少等到进行FPGA验证阶段才能分析其性能,如果不合适,还需要推翻原来的架构重新设计,给设计流程和设计复杂度带来很大的困扰。为了在芯片真正开始写代码设计之前就把上述问题解决掉,芯片模拟器的思想应运而生了。
gem5与计算机架构仿真器
GEM5是一款模块化的离散事件驱动全系统模拟器,它结合了M5(多处理器模拟器)和GEMS(存储层次模拟器)中最优秀的部分,是一款高度可配置、集成多种ISA和多种CPU模型的体系结构模拟器。GEM5已经能够支持多种商用ISA,包括X86、ARM、ALPHA、MIPS、Power、SPARC等,并且能够在X86、ARM、ALPHA上加载LINUX操作系统。是一种名副其实的全系统计算机架构仿真工具。
笔者所在课题组也曾经研究过一段时间gem5,在上面跑起来了linux操作系统。只是速度比真实芯片上跑起来的有点慢而已。 网上有很多相关的学习笔记,比如:一个从刚入大学就励志做CPU设计到毕业后如愿以偿的故事!
让我们看一下gem5介绍框图。
事实上,计算机架构仿真器有很多种,有些不是完整的系统仿真器。全系统仿真器包括 Simics, Simflex, GEM5, Bochs, MARSSX86, PTLsim。 (QEMU, GEMS以及其它项目)。其中最具代表性的有Simflex, GEM5, Bochs, MARSSX86等。
更加全面系统介绍计算机架构仿真器的页面请访问:
http://pages.cs.wisc.edu/~arc...。
这里有各种仿真器官网的链接及相应的工具下载链接。可惜很少看到有中国人开发的仿真器。
Tensilica公司与ASIP
个人认为,Tensilica公司是一家真正将计算机架构仿真器商用化的公司。或许很多同学没有听说过这个公司。其实,这家公司主要做定制指令集处理器(ASIP)的一家公司。ASIP的理念是:专业的事情应该由专业的指令集来完成!举个例子,通用ARM处理器功能几乎无所不能,可以播放视频,可以处理网络数据包等业务,但什么都能做的反面就意味着可能什么都做的不是最好的!Tensilica的思路是,分析通用处理器在执行某些具体任务如播放视频的指令执行情况,从其中选出最经常被用到的几十条或者几百条通用指令集的组合,将这些最经常被用到的指令集组合实现的功能采用专门的一条新指令来替换掉!当然,这条新的指令就意味着处理器本身必须在硬件上去支持这一条指令去完成的所有功能。有了新的指令,当然处理器在进行视频播放时效率就会有非常显著的提升。这个新的被优化过的指令集就是播放视频而定制的专用指令集,这就是ASIP的思想。
在这个理念下,DSP就是一种被优化了数值计算功能的ASIP,GPU也是一种被优化了视频或图像处理的ASIP,而寒武纪的AI处理器也是一种被优化了机器学习功能的ASIP,而所有的网络处理器也是被优化了网络数据包处理功能的ASIP......
笔者曾在研究生阶段申请了Tensilica公司大学计划的LICENSE,也曾尝试过在这个新的架构仿真器平台下对JPEG等编解码功能的指令集优化,据介绍,可以在这个平台下一个小时之内就能定制一款ASIP的处理器。并且,在新的指令集下可以直接运行嵌入式的各种操作系统,综合验证软硬件是否能协同工作。这种理念在十几年前曾引起了不小的轰动,被认为是一种能击倒ARM处理器的一种先进的SoC设计理念,可惜,当时的Xtensa等处理器由于价格昂贵等因素,没有能迅速的推广开。以至于到2013年的时候,被Cadence公司以3.8亿美金收购。
有人认为,被收购的原因是大势所趋。因为一组数据的对比,大约2003年,网络电信专用设备硬件和软件的投资比80:20的样子,而2008年基本就已经逆转成20:80的样子。添加指令,是牺牲软件的兼容性来增加硬件性能。因此当软硬件投资比例发生逆转的时候,过去的一些好技术,就不再有市场。这解释了intel干倒了一堆高性能的CPU,也解释了ARM的崛起,也解释了Tesillica不能独立运营,只能被收购。
还有人认为被收购的原因是太过于对工具的依赖。针对某一类应用设计一套指令集,相对应的是一种处理器体系结构。但是要想真正从某一类应用出发设计出一个处理器,那困难大了去了,你先得抽象出一般共性的指令集,然后选择合适的体系结构,根据指令集去调整优化这个结构,最后指令集和体系结构逐步收敛。期间还要考虑一整套处理器工具链的设计和实现。就像国内龙芯和最近呼声很高的RISCV处理器一样,最后发现,最重要的不是芯片的设计,而是针对芯片的编译器的设计。从事CPU芯片设计的同学们都知道那本经典的《计算机体系架构:量化分析方法》书籍,此书可以说是计算机界的奇书,没有之一,三十年来出了四版。两位作者都是大仙,一个是斯坦福大学校长,另一个是是RISC的发明人,看这本书,让人清楚的感受到处理器设计的门槛之高,涉及技术领域之广。而Tensilica公司的Xtensa处理器出现后,一下子把处理器的设计门槛降低了!你可以针对你的目标应用,对这个处理器原型进行修修补补,尤其是他们提供了一个强大的软件工具,你只需要用一种高级语言描述你的处理器,就会自动产生处理器相关的工具链和最终的RTL代码/网表。整个过程比重新开始一个新指令集/体系结构要容易的多。也有人认为这里面蕴含着一种设计哲学:要想从用户需求侧设计一种复杂的系统,相对简单的思路是选择一种熟悉的、通用的系统原型,进行修修补补,迭代收敛完成最终的设计。
有关Tensilica的更详细的技术细节,可以参考chris rowen博士的《复杂SoC设计》一书。
离散事件模型
言归正传。接下来介绍一种上面牛叉的各种芯片模拟器平台的原始设计思想:离散事件模型!
几年前,因为笔者所在的团队常做各种定制的网络与交换的FPGA和芯片,所以就想着,是否能够像设计CPU那样开发出一套网络与交换专用的模拟器。当然,网络分析的模拟器也有很多常用的工具,比如OPNET、NS3等工具,但这些太偏系统,无法模拟出一款专用的交换芯片内部模块的功能,最重要的是,这是别人做的工具,用起来实在不顺手。大学的使命就是做研究,研究这些模拟器的核心思想到底是什么,我们能否开发出自己的芯片模拟器?这才是我们作为一个高校科研工作者的使命和担当。于是才有了上面那些资料的查找和理解过程。
我们的目标是,通过模拟器可以对各种交换功能进行裁剪和定制,满足各种接口个数和接口速率(从百兆千兆万兆到25G、40G到100G和400G)的需求,并且,通过模拟器还可以判断逻辑资源和存储资源的需求,从而确定FPGA的选型,还可以给出数据包的时延、抖动以及在各种网络数据源模型下的性能分析......
硬件上实现一个复杂的交换单元的设计方案,所需要的开发成本十分高昂,实现周期长。而且硬件实现的结果如果不符合交换单元的需求,那么又需要修改硬件实现方案,又需要经过漫长的时间,这样大大增加了交换单元的开发时间。如果能够在硬件实现之前,能够确保设计方案的性能满足需求,则能减少硬件修改的次数,从而缩短开发周期。而软件仿真能够衡量设计方案的性能好坏,在发现设计方案问题的时候可以及时对设计方案进行修改,然后继续进行仿真测试性能,这样为硬件实现提供了良好的方案,避免重复繁琐的硬件修改过程。
仿真平台能够把一些比较成熟的仿真实现方法接口化和模块化,让软件仿真开发者可以直接调用已有的,减少了不必要的重复繁琐的仿真设计工作,这样有助于缩短软件仿真开发周期,从而降低了实现一款交换单元的时间成本。这样的仿真平台能让平台的开发者更关注于平台底层功能的开发,而平台使用者即仿真开发者更注重具体交换单元的仿真方案的设计和实现。
交换单元软件仿真平台的开发人员主要设计和实现仿真平台底层,并且设计各种API函数和模块供使用者调用。平台的使用者则针对具体交换单元的设计方案,设计仿真方案,然后调用平台接口实现自己的功能逻辑,而平台底层负责运行逻辑。
好了,不啰嗦了。下面看一个图。
上图就是一个最典型的离散时间模型的实现方式,二维事件链表。用事件队列来表示所发生各种事件的先后顺序,以及在不同的事件发生后会触发新的事件的发生,通过入队和出队操作来模拟事件的发生和结束。整个模型采用事件触发的机制。当然,描述芯片内部行为,还需要维护一个时间轴,所有的事件都有专属的执行时间的概念。在这个简单模型下,一个数据帧的到达可以用数个事件来描述(帧长、耗费的时间等)。其实,大家应该都做过类似的程序设计,比如经典的银行窗口服务系统等就是最简单的一个离散事件。
其实,离散事件模型不仅仅可以用来搭建芯片的仿真器(变时钟驱动为事件驱动),还可以做各种通信协议的仿真平台,甚至是其它所有你可以抽象为离散事件的场景。
事件驱动仿真器关键技术
以下内容是课题组学生总结的一些体会和心得,通过这些总结就能够看出初次写一个模拟器需要注意的内容。
1、什么是事件
事件是什么,事件是一种软件层面的抽象,一定要理解软件中的抽象。事件是对将要执行的动作的一种抽象描述,一个事件最主要的就是啥事、啥时候、谁啊、干啥啊。在实际的硬件中,大多数的动作都是中断触发的,包括定时器中断,我们都可以说有个事件要执行。在MFC框架中,也有事件的概念,一条消息、什么时候的消息、给谁的消息、怎么处理这个消息。总之当我们将上述元素组合在一起时,将能够精确地描述一个实际操作。例如,我要发送数据包,我在***时刻发送,我这么这么发送。再例如,我要规划信道,我在***时刻规划,我这么这么规划。原来上面说的一大堆模块都可以抽象成为事件的概念,在不同的时刻执行不同的例如,生成数据、发送数据、接收数据的操作。我不敢说所有软件都是基于事件的,但我们的仿真框架就是基于事件的。
2、需要调度器吗
事件有了,然后呢?怎么安排这些事件在不同的时刻去执行呢?我的第一个思路,把这些事件写成数组,一个挨着一个执行,因为我们的协议是TDD的嘛,有时间轴。这个方法貌似可行。。。不对有问题,同一时刻有多个事件怎么办?这个数组不够大怎么办?随着事件的执行,很多空间变得不再需要怎么办?我可能要添加新的事件怎么办?我脑残添加错了想删除事件怎么办?好了好了别问了,我用链表不就行了。。。(实现的可能千千万)
经过努力,现在所有的事件以一种合理的方式存储着,而且你可以随时添加新的事件,在发现自己2B以后可以删除事件。我们貌似有了一张动态的表格,随着时间的推移已经执行的事件在表格中擦除了,但是同时也会有新的需要执行的事件写入了这张表格。
而调度器的工作就是每次提取第一个将要执行的事件并执行相关处理(FIFO,最简单的调度器,也最符合仿真场景)。Windows怎么做的?GetMessage、TranslateMessage、DispatchMessage大循环。NS2怎么做的deque_event、dispatch_event大循环。
这里由于windows是实际系统,所以他不知道有什么message,这个message是由于用户交互操作产生的,是不可知的,而且系统的时钟是连续流逝的。NS2中,在一定程度上事件是提前可知的,而且系统的时钟可以不连续的流逝。下面我们就说说时间轴和离散的问题,来完善这个调度器的说明。
3、时间轴和离散事件驱动
我们在测量协议性能的时候,有一个性能指标叫做时延,数据包从到达系统,然后离开系统所经过的时间。实际硬件中,系统应该会维护一个时钟,这样对每个包在出队入队时分别读取这个时间就可以知道包的时延。那么在仿真中如何做到?
没错我们也需要一个时间轴,在数据包入队事件中为每个包打上标记,在数据包发送并接收事件处理时读取时间轴时间,以获得包的时延。问题是我们怎么样提供一个时间轴?
Windows下可以获取系统时间,能拿这个当做我们仿真的时间轴对吗?You are wrong!记得我们说过的仿真可以屏蔽硬件特性吗?如果我们这样做会有什么样的结果呢?我有一台i7-4770k处理器的台式机,一个数据包从入队到出队历时2.5ms;你有一台奔腾III处理器的台式机,同样的操作历时5ms。哇,你又引入了硬件特性,更何况windows能提供的时间精度是什么样的我们都不知道。
我们该怎么办?别给我说windows都做不到,臣妾也做不到啊~面包会有,办法也会有的。办法就是我们自己提供一个时间轴,时间的流逝由我们自己控制,流逝的精度任意设定,这样就完全屏蔽了不同硬件带来的不同结果。
我觉得我又说了个废话,方法具体是啥呢?第一个思路,设计时间流逝精度是1us,现在时刻now_time,我查看事件表格里的第一个事件的时刻是不是等于now_time,若是则执行;若不是则now_time+1,时间流逝1us。这里为了屏蔽硬件特性,在这个操作没有执行结束时时间轴是不会向前移动的,因为实际硬件中高速率cpu可能0.5us就执行完毕,而低速率cpu可能需要2us。这里无论这个操作在实际硬件中需要多久,我们都假设他在1us内执行结束就屏蔽了硬件。
然后,我又发现了问题,这样做确实可行,但是我需要轮训查询,轮训效率是相对较低的。而且,若1us有个事件,1000us有个事件,我需要查询1000次,而且全是无用功,怎么办?离散事件驱动来帮助你。。。
每次取出一个事件,事件中标记此事件的发生时间,我们只需要将事件的发生时刻赋值给now_time,就可以模拟时间流逝到这个事件的发生时刻。还是上述例子,now_time为1,取出一个事件,发现其发生时刻为1000,则设定now_time为1000,并执行相关操作。没有了查询,而且跳过了没有任何事件的1000个时间单位,效率大大提升。这里时间轴没有一个一个时间单元的流逝,而是根据事件的发生时刻,从1跳到1000,再跳到1002.。。。。时间点是离散的,事件也没有在时间轴上均匀分布,这就叫离散事件驱动,基于此原理的调度器就是离散事件调度器,也就是我们仿真框架使用的模型。
4、事件单元
至此,我们应该对框架有个大概的认识:事件就是协议中的各种操作,他们被事先安排在一个合理的结构中,调度器每次从这个结构中取出第一个待执行的事件执行,完毕后重复上述步骤,完成deque_event、dispatch_event循环。这里,大循环不是问题,问题是第一事件应该有哪些元素,第二这个所谓存储事件的结构是啥样的,我们一个个回答。
事件应由哪些元素组成呢?回想一下windows程序中的消息,它包括消息所属的窗口(谁的消息),消息标识符(消息的名字或者类型),消息投递时间(消失的产生时间),消息的附加信息。回头想想之前说的事件中的元素,现在已经拥有了谁啊(消息所属)、啥事儿(消息标识符)、啥时候(消息投递时间),但是没有看到干啥啊,windows里的结构不完整啊,这是个bug吧。再翻开孙鑫的书,每个消息有指定所属的窗口,每个窗口结构有一个函数指针,指向此窗口对所有消息的处理函数,函数中根据消息的类型不同,利用switch进行语句分支,达到处理不同消息的目的。
貌似问题解决了,我们可以为每个模块设定类似上述结构(事件类型、事件执行者、事件发生时间),然后每个模块根据判定事件类型的不同选择不同的程序分支进行处理。具体些,调度器调出一个事件,首先判定事件执行者,程序调转到相关执行者代码段,再判定事件类型,程序跳转至相应代码段,所有操作执行结束,返回调度器,重复上述。思路没有大的问题,程序会有两个switch,一个判定执行者,一个判定事件类型。但是我们可以想象随着模块的增多、事件类型的增多,这两个switch会不断增大,以致后来我们自己徜徉在我们的代码里却找不到北。
巨硬搞错了吧,设计这么糟糕的框架。别忘了巨硬还有个框架叫MFC,它里面使用了另外一套机制来解决代码越来越长的问题---消息映射机制,光从这个名字就知道这是啥技术。一条消息对应一个消息处理函数,而不像原来所有消息对应到一个入口函数,在内部实现消息的不同处理。这样第一我们能迅速的找到处理此消息的代码,第二我们程序猿不用为了满足产品狗不断的需求,而让那唯一的函数变得越来越大、越来越长~(这里微软是怎么映射的我们就不探讨了,那是具体实现的问题了)
说了这么一大堆windows的东西,和我们有何关系呢?如前所述,我们的事件就类似windows中的消息,有差不多相同的元素,再加上事件到事件处理函数的映射关系,我们的框架貌似就要完成了。不过先别急,我们再看看NS2的代码。
NS2更懒,消息映射了我还要查消息映射表,老子决定不查了,老子要纹身,纹上我要怎么处理。然后NS2中每个事件都有一个元素,叫做事件处理函数指针的东西。
好吧,齐活。事件包括事件类型和名称(名称也许只有调试的时候有用),事件执行者ID(协议仿真中总要分不同的结点),事件发生时间(别忘了我们还有张事件表格,事件在表格中是有先后顺序的),事件执行函数指针(瞬间我就找到了处理我的代码,真是极好的)。
哎呀师兄,这个函数指针我理解不了。要触类旁通。再看看孙鑫的书,看看windows窗口程序的函数指针,它有四个参数,前两个参数--窗口句柄、消息类型,为了之前所说的两个switch用的,看来我们的结构是用不上了,因为我们不需要映射。后两个参数--消息带来的两个参数,这是干嘛的。废话啊,程序是完成交互处理的,当然要有参数了,不然函数不就不能完成多样性的输出了嘛。
对啊,我们的事件执行函数指针总要有个类型吧,指明这类函数要怎么样的输入参数,输出怎么样的值。说得对,但问题是执行函数千奇百怪,参数个数不尽相同,怎么可能设计出适合所有函数的指针呢?
哎呀呀,要你这么说linux中的驱动程序那么高级的抽象是怎么做出来的。办法有,而且很简单,使用void*指针作为函数的输入参数(void fun(void* pdata))。当我们需要一个输入参数时,一个viod*指针就够了;两个参数怎么办,传输形参前先将他们封装到一个结构体内,在函数内部再取出各个域。这个办法,你有一百个参数都能传进去。
方法很好,不过我们能不能简化一下处理呢?一个函数完成一个功能,需要处理输入得到输出,那么这个函数类型可以简化为void fun(void *src_data,void *des_data),这样如果函数的参数不是很多的话,我们就不用设计那些封装参数的结构体了。送佛送到西,再来一个void fun(void *src_data,void *des_data, void *add_data),我们的函数都采用这样的形式吧,源参数、目的参数和附加参数,源参数主要承载函数获取数据的来源,目的参数负责指明结果写到哪里,附加参数你就发挥想象吧,什么都可以。
哇,问题似乎是得到了解决,事件包括上述的那些元素,其中包括一个这样一个函数指针---void (*pfun)(void*src_data, void *des_data, void *add_data)。事件处理循环中,取出事件,然后执行这个指针指向的函数。
代码写着写着发现问题了,执行这个指针指向的函数,他需要参数啊,而且虽说所有事件执行函数的参数个数一致,但是参数的名字不一致啊,怎么可能写出一条语句去执行所有的事件执行函数呢?看看NS2咋搞的,很遗憾NS2没有利用我们设计的这套机制,难道我们要推倒重建?Nononono。。。。
再看一下NS2的事件执行循环函数:
注意红色划线部分,先取出一个事件p,然后将p传递给dispatch函数,然后在dispatch内部:
调用事件p的执行函数指针,并将事件p本身作为参数传递给事件执行函数。
原来是这样啊,我们可以将所有的参数封装进事件,作为事件元素的一部分,然后对于事件执行函数我们设计void (*pfun)(Event *e)的指针,每次将事件本身传递给执行函数,并在函数内部解析出需要的各种参数。
真是个浩大的工程,用了三分之一的篇幅终于将事件的所有重要元素搞出来了。事件包括:事件的发生时间、事件的类型、事件的所属对象、事件的执行函数指针、事件执行函数的源参数指针、事件执行函数的目的参数指针、事件执行函数的附加参数指针(针对不同事件,不是所有的域都要使用)。
那么当我们实例化一个事件时,这个结构就叫做一个事件单元,代表着一个将要执行的动作的所有要素。
5、事件存储单元
有了事件单元后,存进一个怎么样的结构就是个问题。这里仅仅讲解存储的思路,实现不做具体分析。我们有两个问题,第一同一时间点的事件如何放置,第二不同时间点的事件如何放置。
第一个问题,最简单的方法就是队列,同一时刻的事件按照事件的先后顺序(也有可能事件间没有先后顺序,但总要有个队列)。
第二个问题,不同的时刻也要能够很简单的索引到,因为当插入新事件时,一定是向当前时刻之后的某个时刻插入。这里我们可以再次使用链表,或者hash表,这都是实现的问题了。
至此,我们设计出了一个事件单元的结构,以及为了方便索引至任意时刻的任意事件单元的存储结构。而且,通过每次提取这个存储结构中的第一个事件并执行,我们的框架中最主要的部分就完成了,剩下的工作就是设计不同的事件及其相关操作函数。
EDA相关与芯片仿真器
长期以来,国内的EDA相关工具都严重依赖于国外。正是因为国内从事EDA工具开发的公司在Synopsys、Cadence、Mentor面前实力过于悬殊,国内IC设计公司几乎100%采用国外EDA工具。而且在相当长的一段时间里,看不到缩小和Synopsys、Cadence、Mentor技术差距的可能性。但笔者今天所说的内容,不属于常见的芯片设计EDA工具范畴,但却关乎国产芯片能否做大做强的关键。今年9月,一家国产eda公司获得国家集成电路产业投资基金投资,从2017年底至今,该公司已获得累计数亿元投资,这将有助于该公司提升自身技术水平,填补中国EDA工具上的空白。不过,由于中国在EDA工具上与国外三大厂差距过大,追赶之路任重道远。
我们也希望国内也能够出现类似于Cadence和Synopsys 这样的巨无霸,不是只提供芯片设计专用的EDA工具,也能够出售各种成熟的IP和仿真器平台。
全文完。
作者:网络交换FPGA
原文链接:网络交换FPGA
推荐阅读
更多IC设计技术干货请关注IC设计技术专栏。