RISC-V流水線CPU模擬器(c語言實現)


2020 年秋季學期計算機體系結構

Project 04——RISC-V流水線處理器

2020年11月27日

一、時序模擬和功能模擬分離

該RISC-V流水線處理器分為兩部分:功能模擬部分,時序模擬部分。

功能時序分離的優勢有兩點:

  1. 不同功能模塊化,減小耦合性,可以增強可擴展性。
  2. 有效降低流水線實現的復雜度和工作量。

具體實現上,功能模擬部分大體沿用之前編寫的單/多周期CPU,在其基礎上改進,加上了與時序模擬部分相互通信的接口,將進行時序模擬所需要的信息輸出到buffer文件中;而時序部分讀取buffer文件,通過功能模擬部分所提供的信息,計算流水線的時序信息,並統計輸出。

接下來是時序模擬的設計框架

二、各級流水線執行順序

雖然實際各級流水線是同時執行,但由於C語言的限制,所以需要選擇一個順序。

IF->WB

若直接執行會違反時序,如果需要實現則要將每個階段分成兩部分:取數據和執行,兩部分分階段執行。
流水線寄存器后的段先取指令,全部取到指令后再順次執行。
不足是會造成各階段的割裂,帶來一些不必要的麻煩。

WB->IF

符合時序要求,從后往前,在一個階段內連續完成從上一個流水線寄存器取值、執行、寫入下一個流水線寄存器。處理順序也是按照指令流入的先后順序進行,是相對理想的實現方案。

各級流水線大體執行框架如下:

void control()
{
    if(hazard_type == xxx){//發現沖突,且需要暫停處理
        WB();
        MEM();
        EX();
        ID();
        IF();
    }
    else{//無需暫停處理(無沖突或可數據定向)
        WB();
        MEM();
        EX();
        ID();
        IF();
    }
    return ;
}

三、各段完成的操作

取指IF

PC的更新

NPC的選擇:NPC來源有ID段的跳轉地址、PC+4、PC+2

獲取指令:根據此時的PC,從指令存儲器中讀入指令,統一讀入32位,冗余字段在后續過程中不會被使用,不會造成影響

指令長度確認:確定指令是否為壓縮指令,如果是則會將PC+4傳入多路選擇器,反之將PC+2傳入

框架如下:(僅表示IF的邏輯,實際實現中不會出現)

void IF()
{
    LL insn = read_mem_longlong(PC);
    if(JPC != -1){//JPC有效
        NPC = JPC;
    }
    else if(insn&3 == 3){//32位指令
        NPC = PC + 4;
    }
    else{//壓縮指令
        NPC = PC + 2;
    }
    //寫入流水線寄存器
    IFIDReg.insn = insn;
    IFIDReg.C = (insn&3!=3);
    return ;
}

譯碼ID

從指令中提取相應信息:提取出各個字段,生成出相應的立即數。采取的是固定字段譯碼,在實際電路中各數據通路是並行的,固定字段可以降低整體復雜度。

生成控制信號:根據指令類型生成相應的控制信號(如果需要的話),並寫入流水線寄存器

判斷分支轉移:測試分支條件寄存器,以盡快完成分支轉移是否成功的檢測

計算分支目標:為避免結構相關,使用一個新的加法器部件(而非ALU)進行分支目標地址的計算

在ID段處理分支指令可以減少流水線的暫停帶來的效率損失

框架如下:(僅表示ID的邏輯,實際實現中不會出現)

void ID()
{
    //取值
    ULL insn = IFIDReg.insn;
    bool C = IFIDReg.C;
    //譯碼並寫入流水寄存器
    insn = (int)insn;
    IDEXReg.OPCODE   =   insn & 0x7f;
    IDEXReg.RD       =   (insn >> 7) & 0x1f;
    IDEXReg.RS1      =   (insn >> 15) & 0x1f;
    IDEXReg.RS2      =   (insn >> 20) & 0x1f;
    IDEXReg.IMM_I    =   ((int)insn) >> 20;
    IDEXReg.UIMM_I   =   (unsigned int)(insn) >> 20;

    IDEXReg.FUNC3    =   (insn >> 12) & 0x7;
    IDEXReg.FUNC7    =   (insn >> 25) & 0x7f;
    IDEXReg.SHAMT    =   (insn >> 20) & 0x3f;

    IDEXReg.IMM_J    =   ( (insn >> 11) & 0xfff00000 ) | ( insn & 0xff000 ) | ( (insn >> 9) & 0x800 ) | ( ((insn >> 21) & 0x3ff) << 1 );
    IDEXReg.UIMM_J   =   ( (insn >> 11) & 0x100000 ) | ( insn & 0xff000 ) | ( (insn >> 9) & 0x800 ) | ( ((insn >> 21) & 0x3ff) << 1 );
    IDEXReg.IMM_U    =   (insn & 0xfffff000) >> 12;
    IDEXReg.IMM_B    =   ( ((insn >> 8) & 0xf) << 1 ) | ( ((insn >> 25) & 0x3f) << 5 ) | ( ((insn >> 7) & 0x1 ) << 11) | ((insn >> 20) & 0xfffff000);
    IDEXReg.UIMM_B   =   ( ((insn >> 8) & 0xf) << 1 ) | ( ((insn >> 25) & 0x3f) << 5 ) | ( ((insn >> 7) & 0x1 ) << 11) | ((insn >> 20) & 0x1000);
    IDEXReg.IMM_S    =   ((insn >> 7) & 0x1f) | ((insn >> 25) << 5);
    IDEXReg.UIMM_S   =   (((insn >> 7) & 0x1f) | ((insn >> 25) << 5))&0x00000fff;

    IDEXReg.IMM12    =   (insn >> 20) & 0xfff;
    IDEXReg.IMM5L    =   (insn >> 7) & 0x1f;
    IDEXReg.IMM7     =   (insn >> 25) & 0x7f;
    IDEXReg.IMM5H    =   (insn >> 27) & 0x1f;
    IDEXReg.FMT      =   (insn >> 25) & 0x3;
    IDEXReg.RM       =   (insn >> 12) & 0x7;
    IDEXReg.RS3      =   IMM5H;
    IDEXReg.WIDTH    =   RM;

    IDEXReg.OP_16    =   insn & 0x3;
    IDEXReg.RS2_16   =   (insn >> 2) & 0x1f;
    IDEXReg.RS1_16   =   (insn >> 7) & 0x1f;
    IDEXReg.RD_16    =   RS1_16;

    IDEXReg.FUNC6_16 =   (insn >> 10) & 0x3f;
    IDEXReg.FUNC4_16 =   (insn >> 12) & 0xf;
    IDEXReg.FUNC3_16 =   (insn >> 13) & 0x7;
    IDEXReg.FUNC2_16 =   (insn >> 5) & 0x3;

    IDEXReg.OFFSETL_16 = (insn >> 2) & 0x1f;
    IDEXReg.OFFSETH_16 = (insn >> 10) & 0x7;

    IDEXReg.IMML_CI  =   RS2_16;
    IDEXReg.IMMH_CI  =   (insn >> 12) & 0x1;
    IDEXReg.IMM_CSS  =   (insn >> 7) & 0x3f;
    IDEXReg.IMM_CIW  =   (insn >> 5) & 0xff;
    IDEXReg.IMML_CLS =   FUNC2_16;
    IDEXReg.IMMH_CLS =   OFFSETH_16;

    IDEXReg.RDLA_16  =   (insn >> 2) & 0x7;
    IDEXReg.RS2A_16  =   RDLA_16;
    IDEXReg.RDHA_16  =   (insn >> 7) & 0x7;
    IDEXReg.RS1A_16  =   RDHA_16;

    IDEXReg.TARGET_16 =  (insn >> 2) & 0x7ff;

    IDEXReg.CSR      =   (((insn>>20)<<20)>>20);
    IDEXReg.ZIMM     =   ((insn>>15)&0x1f) & 0xffffffff;

    //提前處理分支指令
    LL RS1 = get_longlong(IDEXReg.RS1);
    LL RS2 = get_longlong(IDEXReg.RS2);
    ULL uRS1 = get_ulonglong(IDEXReg.RS1);
    ULL uRS2 = get_ulonglong(IDEXReg.RS2);
    JPC = -1;

    if(C){
        //略
    }
    else{
        switch (IDEXReg.OPCODE)
        {
        case 111://jal
            JPC = PC + IDEXReg.IMM_J;
            break;
        case 103://jalr
            JPC = (RS1 + IDEXReg.IMM_I) & (~1);
            break;
        case 99://branch
            switch (IDEXReg.FUNC3)
            {
            case 0://beq
                if(RS1 == RS2)  JPC = PC + IDEXReg.IMM_B;
                break;
            case 1://bne
                if(RS1 != RS2)  JPC = PC + IDEXReg.IMM_B;
                break;
            case 4://blt
                if(RS1 < RS2)  JPC = PC + IDEXReg.IMM_B;
                break;
            case 5://bge
                if(RS1 >= RS2)  JPC = PC + IDEXReg.IMM_B;
                break;
            case 6://bltu
                if(uRS1 < uRS2)  JPC = PC + IDEXReg.IMM_B;
                break;
            case 7://bgeu
                if(uRS1 >= uRS2)  JPC = PC + IDEXReg.IMM_B;
                break;
            default:
                printf("ERROR: No such instruction!\n");
                break;
            }
            break;
        default:
            printf("ERROR: No such instruction!\n");
            break;
        }
    }
    
    //判斷訪存行為類型,略——這里直接賦值true
    IDEXReg.W_R = true;
    
    return ;
}

執行EX

ALU單元:從流水線寄存器讀取控制信號,並根據控制信號選擇相應的操作數、立即數並進行處理,得到結果ALUoutput

傳遞控制信號:為訪存段確定訪存行為(讀取 or 寫入),將控制信號寫入流水線寄存器

大體框架如下:(僅表示ID的邏輯,實際實現中不會出現)

void EX()
{
    //讀取流水線寄存器IDEXReg,與ID段的寫入類似,重復且過於冗長,此處省略
    ULL insn = IDEXReg.insn;
    bool C = IDEXReg.C;
    bool W_R = IDEXReg.W_R;

    LL aluoutput;
    
    if(c){
        //.......
    }
    else{
        switch(OPCODE){
            case R_type:	break;
            case B_type:	break;
            //......
            default:
                printf("ERROR: No such instruction!\n");
                break;
        }
    }
	
    //寫入流水線寄存器EXMEMReg
    
}

訪存MEM

確定訪存地址:由流水線寄存器讀取

確定訪存行為:根據流水線寄存器中讀取的控制信號來確定

執行訪存動作:根據地址和行為,具體執行相應操作。如果是讀取內存的行為,結果放入ReadData,並向后傳遞。

邏輯框架較簡單,不在此贅述。

寫回WB

選擇數據和寄存器:根據控制信號選擇正確的數據,確定目的寄存器

寫回寄存器:將所選數據寫回相應寄存器

邏輯框架較簡單,不在此贅述。

四、流水線暫停

檢測機制

沖突控制單元:將各種冒險的檢測集中在沖突控制單元處理。沖突控制單元是一個處理和傳遞全局控制信息的部件,從各流水線寄存器中讀取數據,進行分析,若發現存在冒險,則生成全局控制信號,控制各部件進行相應操作,以解決冒險。

控制信號:控制信號的更新不按照固定時鍾周期更新,而是依據各級流水線運行情況動態執行。需要在各級流水線均完成自己的處理任務,並將數據寫入下一級流水線寄存器后,才能開始新一次的處理,處理過程包括從各級流水線寄存器讀取最新數據,然后更新控制信號,並即時傳遞給各部件,然后等待流水線下一次的流動。

暫停

暫停:部分沖突可以通過數據重定向來解決,但有些沖突則必須進行暫停處理。暫停的控制是由沖突控制單元通過傳遞控制信號來完成,當某一級流水線接收到暫停的信號后,就不從上一個流水線寄存器取值。當然,當某一級流水線被暫停時,它前面的各級流水線也會被暫停,這一點依然是由沖突控制單元來保證。

插入氣泡:被暫停的連續幾級流水線的最后一級,它需要向下一個流水線寄存器寫入NOP指令(亦可采用其他執行空操作的信號機制),否則下一級流水線將重復操作,在執行某些指令的情況下會造成錯誤。這個過程就是插入氣泡。

實現

沖突檢測的實現分為兩部分,分別處理數據沖突和控制沖突。

數據沖突:因為一條指令在ID段可得到所有譯碼信息,包括源寄存器。而數據沖突的產生,正是后面指令是源寄存器與前面指令的目的寄存器相同,造成的若不處理會帶來錯誤的相關。所以所有數據沖突最早可以在ID段確定下來,故數據沖突的檢測,是在ID段出現新指令后,將其信息與前面幾條指令相匹配,檢測是否存在沖突,且標記沖突類型。沖突類型標記用於后面的沖突處理。在出現多個沖突同時出現時,將判斷沖突是否可合並,可合並是指ID段源寄存器和EX,MEM,WB段中的多個同時出現了沖突,且沖突的寄存器相同,這樣實際只需要將最靠近ID段的作為沖突即可,因為ID段需要的是最新的信息。而不可合並的沖突,都會計入統計中。

控制沖突:該方案尚未采用分支預測機制,在每一個分支跳轉處都暫停一個周期,作為延遲槽。具體實現是在單/多周期流水線中,在向buffer中輸出信息時,若當前指令是分支跳轉指令,則在后面再輸出一個特定的nop指令,這樣在TimingSimulator讀取buffer時,相當於已經對控制沖突進行了處理,只需正常處理nop指令即可。

五、計時和計數

時間驅動

優點:可以確定每一個時鍾周期整個CPU的狀態信息。

缺點:可能過於陷入細節,各級流水線執行速度的差異將增加整體的復雜性。

事件驅動

優點:可以將每一級流水線內部的操作視為原子操作,降低復雜度,隱藏很多不必要的細節。

缺點:難以任意地跟蹤查看周期級CPU的狀態信息。

此處選擇事件驅動的方式。

實現:維護一個全局的事件隊列,即此時流水線中正在運行的指令,每一條指令的執行視為一個事件。在隊列中的每個事件有一些屬性:該指令的名稱、源寄存器、目的寄存器、指令執行開始時間、指令執行結束時間,在每一級流水線處理所需要的時間。其中,指令在各階段結束時間的更新,需要由隊列根據所有事件來統一確定,否則會因為各指令的執行周期數差異造成混亂。

事件隊列結構大致如下:

struct event{
    int name;
    int rs1;
    int rs2;
    int rd;
    int csrd;
    int time_start;
    int time_cost[5];
    int time_end;
};
typedef struct event event;

event queue[5];

還有一些配套的函數方法(具體見代碼)。

計數

計數分為兩部分。一是在事件隊列每次正常出隊列時查看被彈出事件的信息,並進行持續地統計。統計各類型指令數量、執行周期數等。二是與沖突控制單元數據交互,每次出現數據冒險和控制冒險就進行記錄。還有一些功能模擬時的必要信息,通過buffer進行傳遞。

統計信息

統計信息暫時分為4部分:

  1. 所執行的所有指令時序信息,每一條包括:指令名稱,開始時間,結束時間,總耗時。
  2. 各種指令的執行數量,並從高到低進行了排序,便於查看哪些指令出現頻率高。
  3. 沖突計數,統計了各類型數據沖突的數量,以及控制沖突的數量。
  4. 計算了CPI。

注意,這里的數據沖突是流水線事件隊列每一次更新后進行檢測,且均以ID段為中心,有些需要暫停的沖突,在暫停之后,會變成僅需要數據定向的沖突,此時會統計為兩次沖突。如果需要更換統計模式,僅記為一次沖突的話,對統計結果進行簡單的減法即可(因為這樣的多次統計必然是一一對應的)。

六、時序模擬部分總框架

逐級向下進行部分關鍵函數的展示(為簡略,省去了細節,具體實現請閱讀代碼)

int main()
{
    init();
    TimingSimulator();
    print();
    return 0;
}
void init(){
    memset(count, 0, sizeof(count));
    memset(hazard_cnt, 0, sizeof(hazard_cnt));
    for(int i=0; i<5; i++){
        queue[i].name = nop;
        queue[i].rs1  = -1;
        queue[i].rs2  = -1;
        queue[i].rd   = -1;
        queue[i].csrd = -1;
        queue[i].time_start = queue[i].time_end = 0;
        for(int j=0; j<5; j++)
            queue[i].time_cost[j] = 0;
    }
}
void TimingSimulator()
{
    freopen("buffer.txt", "r", stdin);
    freopen("Timing.txt", "w", stdout);
    while(1){
        hazard();
        out = control();
		input();
       	output();
    }
    cycles = out.time_end;
}

具體代碼會在該課程結束后上傳......


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM