学习GEM5其实是因为工作需要,主要是用来做数字电路的模型仿真的,之前用过 systemC,现在公司用的 gem5,其实本质上都是 C++只是套个不同的壳然后拿去仿真而已,SC本身就提供了时钟可以仿真,gem5用的是事件触发,对我来说都差不多,反正能跑起来就行。只是GEM5的资源要多一些,SC实在是感觉不太行,应该不大起得来,号称的系统级设计其实GEM5一样的可搞定。之前一直在关注我们的工作设计本身,平台都是前人搭好了的,我们直接用,不用太关心,现在有些空,所以对平台本身还是有点兴趣,研究了一下,感觉其实还挺好玩的,这次我自己写了个最简单的程序用这个平台跑起来,一方面试一下可行性,另一方面其实是我很久没有碰这个东西了,都忘完了,正好回忆一下。
这个小代码主要目的就是想搞清楚他这个事件到底是怎么弄的,verilog是有时钟的,C++没有时钟,都是顺序执行,所以他搞了个事件(event)的概念,每个事件相当于一个周期的时钟,我目测只要设计写好了,我写个for语句把程序调个几百万次好像也可以实现这么多个时钟的效果一样。但他这个平台提供了很多现成的直接用的东西感觉可能是要方便点吧。
这次就写了一个最简单的计数器,函数里面只有一个加1的语句,每执行一次值才增1,就像数字电路的计数器给他几个时钟他才加几次,就是想来研究他是不是执行一次事件他才加1,最终值跟事件执行的次数相关。下面是第一个代码,是一个.h文件,只定义了类和函数名。
第10行就是类的名字,所有的类都要继承SimObject,这是gem5的规定,这个类本身还有参数,所以也会有params在构造函数里面。
第13行是构造函数,mcccParams是这个平台自己会识别和生成的参数类型,就是类的名字mccc和Params的组合。
第14行是一个C++的智能指针,可以不用释放,方便一些。这里我为了方便后面处理可以用[0]这种下标就用了数组的形式,其实普通指针对数组操作更友好点,智能指针稍烦一点,这个指针其实就是声明了一个int数组的智能指针。
第15行是事件的声明,EventFunctionWrapper是gem5定义好了的一个事件类型,我们声明一个这个类型的变量event,后面再加入具体的要处理的事情。
第16行是一个整型变量,主要是向计数器传入初值,计数器不一定都要从0开始计数,也可以我们想从哪开始就从哪开始,所以有一个初始值的变量,以供我们配置。
第19,20行是仿照数字电路仿真时需要的时钟频率和我们要仿真的时长。这两个参数也是可以我们后面传入的。
第22行就是相当于是我们设计的顶层入口,相当于verilog中的设计的计数器顶层,第24行相当于是tb了,用来启动仿真的。这些都只是类比 verilog而已,正常的解释应该是计数器的实现和开始仿真和控制仿真的一些功能。
第23行是平台里面的一个函数,在正式启动仿真前会自动调用一次这个函数,也是从这时开始算时间,此时为0 TICK。这个函数的作用其实就是启动第一次事件,后面的事件就要靠里面的其实函数根据需要情况来启动了。
1 #include <memory>
2
3 //#include "learning_gem5/part5/mccc.h"
4 #include "params/mccc.hh"
5 #include "sim/sim_object.hh"
6
7 namespace gem5
8 {
9
10 class mccc : public SimObject
11 {
12 public:
13 mccc(const mcccParams &p);
14 std::unique_ptr <int []> counter;
15 EventFunctionWrapper event;
16 int firstnumber;
17 // int * counter;
18
19 int clkfreq;//mhz
20 int runtime;//us
21
22 void process();
23 void startup();
24 void startsim();
25 };
26
27 }
下面是.cc文件,这个gem5里面的文件为了区别和C++自己搞了个后辍名,头文件叫.hh,主体文件叫.cc。我这里还是用以前的叫.h。
第8行就是构造函数,当然有个params参数。9到13行就是初始化一些变量,9行就是把params传给SimObject,10行是构造具体的事件,按照官方文档的说法,event的参数有两个,第一个应该是一个函数,主要是我们真正想执行的程序的入口,第二个就是一个名字而已,一般我们就用当前类的名字加上一个.event来表示,实际代码中第一个参数是一个隐匿函数,里面有一个我们真正想在事件中执行的函数startsim(),每次执行事件时就会执行这个函数,这个函数就相当于我们整个设计的顶层入口,我的这个代码简单就一两个函数。第二个名字部分实际应该是mccc.event,代码中用name()这个函数来表示,这个函数的返回值就是当前类的名字,再加上.event一起构成了完整的名字。11到13行就是通过传参里面的一些参数来初始化赋值给变量,包括计数器初始值,时钟频率,仿真时间。
第15行是构造函数的函数体,主要就是干了一件事就是初始化counter指针,智能指针初始化稍和普通指针有一点不同,这里我用的reset来初始化的,new一个整型变量并且把fistnumber作为初始值给这个变量,这个变量的指针就给了counter。16行是一个打印,主要是把构造函数完成后打印出来可以直接看,方便看点。
第19行就是startsim的函数体了,21行是计算要执行的事件的总数,其实就相当于是总的时钟周期数,用频率乘以时间就是总的周期数,前面说了每一次事件执行相当于是一个时钟周期。23行是执行process()函数,这个函数就是我扪的计数器函数,每执行一次计数器加1。
第24行和29行都是为了方便看效果加的打印,可以直接看出当前的时钟数和计数器的值。curTick()这个函数的返回值就是当前的tick数,也就是周期数可以理解为。
第25行是判断当前的时间是不是已经完成所有的周期数,如果还没有完成就是继续执行下一次事件,如果完成了就不执行,gem5有个机制是如果一直不执行事件的话到一定的时间后就会自动退出仿真。
第27行就是执行事件,schedule这个单词就有安排计划的意思,这里也比较形象,就是安排去执行事件了,里面两个参数,第一个就是我们的event,第二个参数就是时间,一般是用当前时间加上一个值去表示从当前开始数,到了加的这个值的时间就去执行这个事件,比如我下面写的curTick()+1,就是在下一个周期时执行第一个参数的事件。这里一般不要写固定值,因为这个时间不能是过去的时间,所以都是curTick()再加一个固定值,才表示从当前开始算,只一种情况可以写固定值,就是startup里面,因为这里就是Tick开始计数的时间我们知道就是0,所以写个固定值没问题,并且这个startup只执行一次,也不会影响后面的执行。
第31行就是startup函数,可以看到里面就是安排了一次事件,因为他是整个仿真的第一步,需要他来启动第一次安排,后面的按排都在startsim里面根据时间自己安排了。这里可以写100,不要curTick(),因为此时cutTick()也为0。
第36行是process函数,就是我们设计的目的所在,是一个计数器,可以看到里面就是一个计数器加1而已,没啥特别的,这里我用的数组的形式,指针可以用这种,很多人在实际过程中也喜欢用指针来代替数组,三级指针就可以代替三维数组。counter[0]就跟*counter是一样效果这里。
1 #include "learning_gem5/part5/mccc.h"
2 #include "base/logging.hh"
3 #include "base/trace.hh"
4 #include "sim/sim_exit.hh"
5
6 namespace gem5
7 {
8 mccc::mccc(const mcccParams ¶ms) :
9 SimObject (params),
10 event ([this]{startsim();}, name()+".event"),
11 firstnumber (params.firstnumber),
12 clkfreq (params.clkfreq),
13 runtime (params.runtime)
14 {
15 counter.reset(new int (firstnumber));
16 std::cout<<"construct finished"<<std::endl;
17 }
18
19 void mccc::startsim()
20 {
21 int alltick = runtime * clkfreq;
22
23 process();
24 std::cout<<"before: "<<curTick()<<std::endl;
25 if(curTick() < alltick)
26 {
27 schedule(event, curTick() + 1);
28 }
29 std::cout<<counter[0]<<std::endl;
30 }
31 void mccc::startup()
32 {
33 schedule(event, curTick() + 100);
34 }
35
36 void mccc::process()
37 {
38 counter[0] = counter[0] + 1;
39 }
40
41 }
下面是本次代码对应的py文件,每个C++需要一个对应的python文件,第4行是类的名字mccc,也是要继承SimObject。
第5到7行算是固定写法吧,是对这个类的说明相当于,按照模版写就行。
第9到11行就是我们之前代码里说要传入的参数,这些参数在这里声明,后面再传入到C++里面去,这里的参数都是声明的Param里面的类型,Param是平台自带的一个参数类,里面把常见的类型封装了一下,比如int在这里就是Int,封一下之后用的时候可以传东西进去,可以传一个参数或两个参数,如果只传一个参数那就是对这个参数的说明,比如第9行就是对这个参数的说明而已,方便后人理解。如果传两个参数的话,第一个参数就是默认值,第二个才是参数说明。如果在仿真文件里面不传入新的值进来,则就会把这个默认值传入C++里面。我这里没有设置默认值。
1 from m5.params import *
2 from m5.SimObject import SimObject
3
4 class mccc(SimObject):
5 type = 'mccc'
6 cxx_header = "learning_gem5/part5/mccc.h"
7 cxx_class = 'gem5::mccc'
8
9 firstnumber = Param.Int("fist number of counter")
10 clkfreq = Param.Int("clk freq MHz")
11 runtime = Param.Int("run time us")
下面的代码就是本次仿真的入口文件,一般是放在config里面的一个pyhton文件,相当于整个设计加上验证一起的顶层。第4行也算是固定写法吧,每个设计里面都要有一个最高的例化,就是ROOT,里面的是选择是不是全系统仿真,我们这里当然不是。
第6行就是例化我们的设计模块,就是在这里传入我之前设定的参数。第8行和11行就是初始化仿真器和执行仿真,10行12行是一些打印方便看。12行会有个退出的时间和原因打印,之前说过我扪的事件完成后,一段时间没有安排的话就会自动退出。这里的原因最后打来基本上都是事件等待超时之类的。
1 import m5
2 from m5.objects import *
3
4 root = Root(full_system = False)
5
6 root.mccc = mccc(firstnumber = 5, clkfreq = 1000, runtime = 2)
7
8 m5.instantiate()
9
10 print("Beginning simulation!")
11 exit_event = m5.simulate()
12 print('Exiting @ tick %i because %s' % (m5.curTick(), exit_event.getCause()))
下面是最后一步,把我们的设计加到整个gem5中去,让他在编译的时候带上,也就是每个设计代码文件夹里面都会有的SConscript文件,我的设计代码是新建了个文件夹part5,我的这个文件代码如下:
1 Import('*')
2
3 SimObject('mccc.py')
4
5 Source('mccc.cpp')
这个就是参考其他的这个文件格式来的,还是挺简单。把我们写的放进去就可以。
这里我们的代码就写完了,可以编译和跑起来了。命令如下:
1 sudo python3 `which scons` build/NULL/gem5.opt -j8 //compile
2
3 sudo build/NULL/gem5.opt configs/learning_gem5/part5/mccc.py //run
最后结果如下:
1 before: 1990
2 1896
3 before: 1991
4 1897
5 before: 1992
6 1898
7 before: 1993
8 1899
9 before: 1994
10 1900
11 before: 1995
12 1901
13 before: 1996
14 1902
15 before: 1997
16 1903
17 before: 1998
18 1904
19 before: 1999
20 1905
21 before: 2000
22 1906
23 Exiting @ tick 18446744073709551615 because simulate() limit reached
这里我设的时间长2000个周期,所以计数器比较大,显示不完,我取了最后一点。可以看出时间是跑了2000次,符合设计,计数器值最后是1906,因为我的仿真起始位置是从100开始的所以2000对应的计数器就应该是1900,但又因为我的计数器的初始值是从5开始的,所以最终结果就是1906。也符合设计意图。
另外我试了一下这个TICK到底是怎么计数的,我把事件的安排的时间参数只写了curTick(),后面没有加时间,试了下,结果打出来全是0,也就是在0TICK时把所有的程序就执行完了,我理解这个TICK其实就是创造出来的一个概念,为了体现时间先后,其实也就是对应了RTL代码的时钟,当安排事件时写了在后面的第几个TICK执行他才去让TICK动起来,不然他好像不动也可以完成任务,只是在最后需要统计的时候没有TICK作为参考不晓得哪些在哪里执行了,也算不出来我们想要的东西。如果在这个代码里面直接把后面的数字去掉,则跑起来后就停不下来了,因为TICK没动, startsim里面的if 就不可能为假。
自家电脑上最好把 sudo加上,不然会有很多问题,或者自己把GEM5所有文件的权限全部改一下也可以。下面记录一下这次过程中主要碰到的一些问题。
第一个就是我最开始不想用他这个里面的后缀名.hh和.cc,但中间有些生成文件又是这样的,反正还是有点麻烦,算了,用他的平台还是按他的规矩来吧,都用 .hh和.cc做文件后缀吧。
第二个是各种include文件,这个我现在还没有研究清楚,因为用了平台很多现成的类,需要包含进来,我就直接参考他里面的例子了,不然又各种错。
第三个就是参数名,一定记住是你的设计的类的名字和Params的组合,平台能自动识别,比如我这个就是mcccParams,区分大小写注意。gem5的所有代码设计风格都是大小写组合的,我个人不太喜欢这种命名风格,切换大小写烦死,代码的搜索也不好搜。我还是喜欢小写加下划线这种的。恼火。。
第四个就是在构造函数中一定要把事件加进去,这是规定。
第五个还是参数的问题,之前GEM5平台是要求设计者把参数的create函数写出来,但现在可以不用写了,但条件是声明参数的格式一定要按他的来,就是这样的mccc(const mcccParams &p)。const要加上,如果不加平台识别不了会报错,只有这种格式才可以识别并给你自动加上。。不然就只能手动加了。
其实这个小设计我还是想来类比verilog的仿真,有了这个设计,其实如果自己写一个新的功能的话,相当于只需要把process换成新的设计主函数就可以,其他的部分相当于是验证平台了。并且可以配置时钟频率和仿真时长。作为数字电路的模型用途好像也够了,当然这是最基本的。实际中还会有很多复杂的,比如回归,配置文件等等。