學習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。也符合設計意圖。
自家電腦上最好把 sudo加上,不然會有很多問題,或者自己把GEM5所有檔案的許可權全部改一下也可以。下面記錄一下這次過程中主要碰到的一些問題。
第一個就是我最開始不想用他這個裡面的字尾名.hh和.cc,但中間有些生成檔案又是這樣的,反正還是有點麻煩,算了,用他的平臺還是按他的規矩來吧,都用 .hh和.cc做檔案字尾吧。
第二個是各種include檔案,這個我現在還沒有研究清楚,因為用了平臺很多現成的類,需要包含進來,我就直接參考他裡面的例子了,不然又各種錯。
第三個就是引數名,一定記住是你的設計的類的名字和Params的組合,平臺能自動識別,比如我這個就是mcccParams,區分大小寫注意。gem5的所有程式碼設計風格都是大小寫組合的,我個人不太喜歡這種命名風格,切換大小寫煩死,程式碼的搜尋也不好搜。我還是喜歡小寫加下劃線這種的。惱火。。
第四個就是在建構函式中一定要把事件加進去,這是規定。
第五個還是引數的問題,之前GEM5平臺是要求設計者把引數的create函數寫出來,但現在可以不用寫了,但條件是宣告引數的格式一定要按他的來,就是這樣的mccc(const mcccParams &p)。const要加上,如果不加平臺識別不了會報錯,只有這種格式才可以識別並給你自動加上。。不然就只能手動加了。
其實這個小設計我還是想來類比verilog的模擬,有了這個設計,其實如果自己寫一個新的功能的話,相當於只需要把process換成新的設計主函數就可以,其他的部分相當於是驗證平臺了。並且可以設定時脈頻率和模擬時長。作為數位電路的模型用途好像也夠了,當然這是最基本的。實際中還會有很多複雜的,比如迴歸,組態檔等等。