RISCV Open Source Verification and Simulation Strategy

RISCV Core模擬驗證策略開源方案

RISC-V Core verification and simulation with OpenSource solution

最近看了比較多core,而自己也有為了測試跑了很多core的模擬,之前也因為實驗室需求搞了一些簡易的純模擬測試平台。所以把一些心得整理一下。

做CPU驗證流程當然應該是先確定邏輯正確,之後才是合成後timing與現實情況正確(上FPGA跑更複雜的模擬)。如果直接用FPGA跑模擬,要等合成,要搭建實驗平台,同時若要debug還要合ILA之類的debug module進去,每次時間會消耗很多,這樣對於前期處理器bug很多,還在建制的情況下十分沒有效率。因此有很多behavior model simulation方式,如用一些商用工具如:VCS或是Viviado進行模擬。這樣效率也很高,而且現在工具,如果testbench寫的好,tcl script寫的好,其實效率也是蠻高的。不過這些工具都是閉源,而且有些要錢(VCS),所以對於一般開發者,如我們學生來說難以取得這類工具支援。所以現在很多RISCV core提供Verilator模擬驗證方式。

Verilator簡介

Verilator是一項由Linux Foundation旗下的Chips Alliance維護的開源專案,自從轉入Linux Foundation旗下後,開發更加頻繁,社群也變得更大。Verilator跟一般的VCS或是Viviado不太一樣,她只能做cycle accurate behavioral model模擬。他的機制是將verilog或是SystemVerilog轉成C++或是SystmeC進行"cycle accurate"模擬,也就是所有delay時間等操作均無效。

cycle accurate模擬使她比一般商用模擬還要快(因為不用考慮timing),在此架構下,verilator可以讓執行檔pthread,進行多線程支援。但Verilator在verilog或是SystemVerilog上仍有一些支援與不支援的地方。基本上可以被合成電路的語法她都支援。在SystemVerilog下還部份支援C/C++與SystemVerilog溝通的Verilog Procedural Interface(VPI)語法與Direct Programming Interface (DPI)語法。而因為其會將Verilog編成C/C++,所以可以用一些C/C++的方法直接更改邏輯中的值,這些方法讓我們用C/C++寫testbench可以更加快速。

而Verilator只支援2-state(0和1),還有一點tristate,所以verilog中的’x’和’z’她會直接給入random值,這好處是會造成結果錯誤,可以更快debug,因為一般這些’x’和’z’訊號不該被拿來運算。

$ verilator -Wall --cc $(VERILOG_SRC) --exe sim_main.cpp # 產生obj_dir,裡面含有 *.cpp/*.h檔還有library檔
$ make -j -C obj_dir -f Vour.mk Vour #這裡的Vour.mk要看你的top module決定V{top module name}
$ ./obj_dir/Vour # 執行

RISCV core模擬方式

首先在模擬一個CPU我們需要模擬RAM,週邊裝置還有debug port(optional)。

添加一個虛擬的ram

在建構RAM來模擬時我們需要考量:

  1. RAM的interface 如你的core是透過AXI來從RAM抓取資料的,還是透過其他protocol抓取資料的,一般的CPU不會直接從core透過AXI接到主記憶體,中間可能會經過一些L1 cache,L2 cache,或是一些用來存取主記憶體的ip,而這些內部的protocol通常不像AXI那樣複雜,如果你的目標不是測試這些interface或是比較外圍的module,你可以考慮讓你的core直接透過這些比較簡單的protocl接到你的 “假RAM”。這邊pulp-platform/ariane是一個很好的範例: https://github.com/pulp-platform/ariane/blob/master/tb/common/dp_ram.sv ,你可以發現他的dp_ram寫的十分簡單。

  2. RAM的大小

    verilog array或是vector在verilator中無法支援太大的大小,verilator有array width上限,不能太大,不可能合個4GB ram什麼的,而且如果只是要跑測試也不太需要這麼大的空間。

    但是如果是32bit core,可能會存取高位,或是特定區段的RAM,解決方式,可以就設定一個小一點ram(幾MB)這樣,接下來在"假ram"中進行位址轉換,例如:把0x80000000 map到0x0這類操作。如果:要存取比較分散的ram位址,可以用類似方法map多段ram。

  3. C++ testbench RAM存取小技巧

    雖然Verilator可以存取module裡的reg或是wire(前提是該module是有標記為"/*verilator public_module*/",或是該變數被標為 /*verilator public*/),但是我們也可以透過藉由task 或是 function來存取verilator model(https://www.embecosm.com/appnotes/ean6/html/ch06s02s03.html),範例如下:

    當我們在"假ram"中宣告以下function

  task writeByte;
    /*verilator public*/
    input integer byte_addr;
    input [7:0] val;
    begin
      reg [31:0] tmp_addr;
      tmp_addr = byte_addr - MEM_OFFSET;
      if (byte_addr >= MEM_OFFSET &&  byte_addr < MEM_OFFSET+MEM_SIZE) begin
        mem[tmp_addr[PART_ADDR_WIDTH-1:0]] = val;
      end
    end
  endtask

verilator合成完的dp_ram會產生獨立的.cpp和.h檔,裡面你可以發現類似的class method宣告,你會發現裡面出現writeByte這個method:


//----------

VL_MODULE(Vtestharness_dp_ram) {
  public:
    
    // PORTS
    VL_IN8(__PVT__clk,0,0);
    //...
    VL_OUTW(__PVT__rdata_dcache_o,255,0,8);
    
    // LOCAL SIGNALS
    CData/*1:0*/ __PVT__icache_cur_state;
    //...
    CData/*7:0*/ mem[2097152];
    
    // LOCAL VARIABLES
    CData/*5:0*/ __Vtableidx1;
    static CData/*1:0*/ __Vtable1___PVT__dcache_next_state[64];
    
    // INTERNAL VARIABLES
  private:
    Vtestharness__Syms* __VlSymsp;  // Symbol table
  public:
    
    // PARAMETERS
    enum _IDataMEM_SIZE { MEM_SIZE = 0x200000U};
    enum _IDataMEM_OFFSET { MEM_OFFSET = 0x80000000U};
    
    // CONSTRUCTORS
  private:
    VL_UNCOPYABLE(Vtestharness_dp_ram);  ///< Copying not allowed
  public:
    Vtestharness_dp_ram(const char* name = "TOP");
    ~Vtestharness_dp_ram();
    
    // INTERNAL METHODS
    void __Vconfigure(Vtestharness__Syms* symsp, bool first);
  private:
    void _ctor_var_reset() VL_ATTR_COLD;
  public:
    
    uint32_t readByte(uint32_t byte_addr);
    uint32_t readWord(uint32_t byte_addr);
    void writeByte(uint32_t byte_addr, uint32_t val);
    void writeWord(uint32_t byte_addr, uint32_t val);
    static void traceInit(VerilatedVcd* vcdp, void* userthis, uint32_t code);
    static void traceFull(VerilatedVcd* vcdp, void* userthis, uint32_t code);
    static void traceChg(VerilatedVcd* vcdp, void* userthis, uint32_t code);
} VL_ATTR_ALIGNED(VL_CACHE_LINE_BYTES);

之後你就可以在testbench 的c++中呼叫這些函式。如下範例,header請依照你verilator synthesis出來的為準:

#include "verilated_vcd_c.h"
#include "Vtestharness.h"
#include "Vtestharness__Syms.h"
#include "verilated.h"
void sim_mem_write(Vtestharness_dp_ram* ram,uint32_t addr, size_t length, const void* bytes)
{
  for (int i = 0 ; i < length ; i +=4 ) {
    ram->writeByte(addr+i,*((unsigned char*)bytes+i+3));
    ram->writeByte(addr+i+1,*((unsigned char*)bytes+i+2));
    ram->writeByte(addr+i+2,*((unsigned char*)bytes+i+1));
    ram->writeByte(addr+i+3,*((unsigned char*)bytes+i));
  }
}

添加一個虛擬的UART (週邊)

有時候為了方便debug,需要支援printf等透過UART輸出的界面(riscv-tests不需要)。所以需要一個假的uart來透過memory map IO輸出資料到console,通常不需要支援input。由於是透過memory map IO,因此也可以跟上面提到的protocol選擇一樣選一個比較簡單好實做的部份。

實做只要透過$write() function來輸出資料到console。

Debug module部份我合到底下來談。

RISCV core 模擬驗證策略

有兩種測試方案,第一種是與debug module合併,透過CPU本身的Debug module與host的RISCV frontend server(fesvr)進行互動(上傳binary到CPU的RAM中,監控,提供gdb support等)。令一個是針對沒有debug module支援的core,透過Verilator直接改寫RAM來載入程式,以及提供監控功能。

簡單的Verilator 程式載入RISCV core驗證策略

這個方式你沒辦法讓你的Verilator model與gdb等工具互動,因此你需要建立一個你自己的elf載入機制。關於riscv elf可以在https://github.com/riscv/riscv-isa-sim/blob/master/fesvr/elfloader.cc 找到,這個code可以幫助我們解析elf並得到physical memory address,加上上面提到的"假RAM"讀寫機制,我們可以合併達成程式載入器。另一個方法是透過SystemVerilog的readmemh方式載入預先處理好的memory file到"假RAM"中,這兩種方式都有開源專案使用。以下列出兩個代表專案:

由於沒有Jtag或是Debug module支援,這類處理器通常會透過Verilator的model存取功能,寫自己的core tracer,來追蹤CPU內部重要訊號的動作。

riscv-tests 使用

應該沒有人寫riscv core不跑過一遍riscv-tests的吧。由於riscv-tests預設你的core不一定有UART等界面,所以她透過tohostfromhost這兩個CSR(其實是memory)進行溝通,所以當你的core在透過"假ram"方式進行模擬時,你必須要讓你的verilator testbench可以去得到tohost的修改事件(監控tohost的記憶體存取),這時會牽扯到你的core的cache行為,如果你core有DCache且行為不是write through時,由於riscv-test的結果會透過一個無窮迴圈一直寫入tohost,當你的cache是用write-back時,並不會觸發write back到main memory的機制(因為該cache line根本就不會被取代)。這時你需要其他監控tohost的值的方法,以下有一些解決選項:

  1. tohost的所在記憶體區段用軟體設為non cacheable,前提是你的core支援這項操作
  2. 建構一個假的peripheral device,就像前面提到的uart一樣監控IO device行為

Verilator RISCV core + fesvr策略

對於開發完成度很高的core,尤其是提供JTAG等Debug Transport module的CPU通常會使用這種策略,而這種策略最有名都是UC Berkeley的專案。

fesvr

fesvr是riscv-isa-sim(spike)下的一個子系統riscv-isa-sim ,全名是RISC-V frontend server,用來作為host 與測試Core溝通的橋樑。有用來提供Remote Bit-Bang(rbb) client與host常用的openocd相連,提供類似實體晶片常用的jtag的功能。

Host Target Interface(HTIF)

HTIF(software)是一個Host跟測試hardware溝通的手段,主要是透過存取tohostfromhost這兩個CSR (雖說是CSR,其實是記憶體位址,在riscv-test中存取方式是透過sw/lw,https://github.com/riscv/riscv-test-env/blob/master/p/riscv_test.h#L259)來溝通,fesvr載入elf程式時會特別找到tohost和fromhost label(https://github.com/riscv/riscv-isa-sim/blob/master/fesvr/htif.cc#L126),然後執行過程中監看與讀寫這兩個CSR,這其實是一個非官方的手段,不過在riscv的ecosystem中很常使用這手段進行溝通,例如riscv-pk,也使用這方式與host溝通(https://github.com/riscv/riscv-pk/blob/master/machine/htif.c)。

Debug Transport Module(DTM)

riscv-debug-release.pdf

Debug Transport Module是在platform介於Debug Transport Hardware和Debug module之間的部份,由於Debug Transport Hardware有多種實做方式,例如:最廣泛的JTAG或是USB等傳輸方式來與Debug Host(例如:PC等)溝通。Debug Transport Module(DTM)在整個Debug的架構圖如上。 Debug Transport Module現在在RISCV debug spec中有詳細的定義。

連接verilator model與fesvr

為了連接Design Under Test(DUT,這裡是core),verilator model必須透過DPI來與Core的JTAG和DTM相連,這邊可以參考rocket-chip的作法( https://github.com/chipsalliance/rocket-chip/tree/master/src/main/resources),在這資料夾的vsrc下有SimJTAG.v和SimDTM.v,在csrc下有SimJTAG.cc和 SimDTM.cc,進去看原始碼就會看到DPI的寫法。為了使用JTAG必須使用軟體實做bit-bang(csrc下的remote_bitbang.cc),好讓HTIF和fesvr thread可以與其相連。最後就可以透過開啟remote bit-bang,接下來讓host的openocd連上verilator model,再開啟讓gdb連接完成完整的debug環境。

代表專案

Reference

comments powered by Disqus