difftest在測試集中可以起到十分重要的作用,可以快速找到發生問題的指令和pc暫存器地址。在nemu作為dut,參考其他模擬器(比如spike)的功能中,大部分程式碼已經完成,我們只需要完成暫存器的比對即可。但在RTL中重新實現這一功能或者類似功能時,我們需要完成更多函式,但大體的框架已經完成,我們可以參考nemu中已有的difftest功能去補全函式。
講義中已經提示我們需要完成哪些函式:
// 在DUT host memory的`buf`和REF guest memory的`addr`之間複製`n`位元組, // `direction`指定複製的方向, `DIFFTEST_TO_DUT`表示往DUT複製, `DIFFTEST_TO_REF`表示往REF複製 void difftest_memcpy(paddr_t addr, void *buf, size_t n, bool direction); // `direction`為`DIFFTEST_TO_DUT`時, 獲取REF的暫存器狀態到`dut`; // `direction`為`DIFFTEST_TO_REF`時, 設定REF的暫存器狀態為`dut`; void difftest_regcpy(void *dut, bool direction); // 讓REF執行`n`條指令 void difftest_exec(uint64_t n); // 初始化REF的DiffTest功能 void difftest_init();
我們要完成的ref.c的函式。我們可以以spike作為參考,spike的程式碼位於/nemu/tools/spike-diff/difftest.cc 。但是這只是一部分,difftest.cc只是提供了工具函式,主要的邏輯則位於dut.c中。除過ref.c,我們還需要修改npc的函式。其實這二者是對應的:
difftest.cc |
ref.c |
dut.c | main.cpp |
ref_difftest_memcpy(RESET_VECTOR, guest_to_host(RESET_VECTOR), img_size, DIFFTEST_TO_REF);
ref_difftest_regcpy(&cpu, DIFFTEST_TO_REF);
隨後,在正常情況下,dut每執行一條指令,就暫停下來讓ref也執行一條指令,然後對比二者的暫存器值是否一一對應。樣例dut.c中多了很多邏輯,因為有的指令可能需要跳過,我們的npc與nemu對比時暫無此情況,不需要進行處理。
------------------------------
在清楚思路以後,我們可以仿照difftest.cc,完成ref.c的三個函式:
首先需要注意,在nemu為dut時,DIFFTEST_TO_REF是從nemu到spike,我們實現時,DIFFTEST_TO_REF就是從npc到nemu了。反之也是同理。
regcpy的基本功能就是根據方向,決定是把nemu的暫存器值給npc,還是把npc的值給nemu。內部交換值是使用memcpy()還是直接cpu.gpr=xxx均可。我自己是選擇的memcpy。
difftest_memcpy同樣。根據方向,使用paddr_write或者paddr_read()即可。注意:傳入的dut是void*,我自己是轉換word_t *後用下標賦值的,這裡隱含了小端序。如果你的傳值不一樣,最好小心這一點,可以在傳值後原地檢查一次nemu的記憶體,一方面可以看賦值是否成功,一方面可以檢查值是否正確。比如0x0000 0413這樣一條指令,其實0x00位是13,不是00,從左到右記憶體地址遞減。小端序的記憶體最低位元組在最低位。不過c語言內部會處理,我們自己做RTL時選擇什麼記憶體模型會有影響。
--------------------------------
npc的main裡面除了呼叫這幾個函式,還需要自己實現兩個函式:get_regs,負責獲取當前的通用暫存器和pc值。check_regs,負責對比暫存器值。
關於如何獲取npc裡通用暫存器的值,有兩種主要的方法:一種是在reg元件裡利用DPI-C機制宣告函式給C++呼叫,一種是根據編譯出的標頭檔案直接查詢暫存器的名字呼叫。宣告DPI-C函式的程式碼如下:
export "DPI-C" function get_reg_value; function int get_reg_value(input int index); begin if (index < 2**ADDR_WIDTH) begin get_reg_value = rf[index]; end else begin $display("wrong reg index, %0d",index); get_reg_value = -1; end end endfunction
同時npc的C++部分也要先宣告再使用。此外有很多注意事項:
1. 宣告時需要extern "C";
2.verilog的function引數不能出現output,並且用export宣告函式時不要寫引數。
3.verilog的function和c++側的引數型別宣告要對應:
Verilog byte 對應 C char Verilog int 對應 C int Verilog shortint 對應 C short Verilog longint 對應 C long long Verilog real 對應 C double Verilog bit 和 logic 對應 C int(通常用於單個位)
有時候有/無符號和logic/bit等的對應會造成額外麻煩。需要注意的是,Verilog中的byte
是一個8位的有符號整數,與C語言中的char
在大多數平臺上是等價的(也是8位有符號整數)
我最終沒有選擇這種方法,因為使用時編譯器反覆報錯,不管是否使用return ,怎麼修改函式宣告,或者是修改內部函式,都在出問題。最終選擇了方法二。
-------------------------------------------
方法二隻需要根據編譯出的標頭檔案尋找元件名:在我們編譯出的obj檔案裡,會有Vtop.h Vtop__dpi.h和Vtop___xxxroot.h。第一個標頭檔案表示頂層元件的一些功能性函式介面,比如eval() 以及DPI-C函式。 第二個是dpi-c機制的介面。第三個root.h就是從頂層元件開始所有連線元件內變數的標記,reg型和wire型都有:
如圖,變數名中間的DOT就相當於verilog語言裡的"." ,透過這個我們就可以透過頂層元件訪問到連線的元件,一層層向內。我的通用暫存器是頂層元件->執行器->暫存器,所以我的暫存器變數名就叫 "top__DOT__exec_unit__DOT__RegFile__DOT__rf',訪問方法也很簡單,像陣列一樣直接下標就可以訪問。但是,在C++元件裡要訪問,首先需要引用標頭檔案root.h,其次需要用top訪問,講義裡也提到,對於我而言,想要訪問這個變數,就需要寫成:
top->rootp->top__DOT__exec_unit__DOT__RegFile__DOT__rf
這個暫存器變數不能直接用top->訪問,中間還需要rootp。只要查閱了兩個標頭檔案就可以看明白。
--------------------
在搞清楚怎麼獲取暫存器的值以後,剩下的事就很簡單了。在開始前初始化,把npc的程式碼img和暫存器狀態給nemu。在每個時鐘週期後:
獲取npc暫存器值
獲取nemu暫存器值
對比
不過還有一點需要注意:如果你的暫存器使用了時序邏輯+非阻塞賦值,那麼暫存器賦值會晚一個時鐘週期,在對比時需要注意。在這裡我沒有寫出讓nemu執行一條指令這個函式應該在哪裡,因為個人情況可能不同。