Mips32位CPU20條基本指令設計及下板測試

整體框架

框架圖

這裏寫圖片描述
主要分爲PC、ID、EX、WB、REGFILE、Instruction ROM、MIOC、IO、DataMem RAM九個模塊。

模塊簡介

  1. PC
    程序計數器PC,取指令時使用PC作爲存儲器地址。

  2. ID
    負責指令的譯碼,確定源操作數,目的操作數,目的存儲地址以及是否指令跳轉。

  3. EX
    本質上是ALU,負責運算的電路。ALU需實現以下的運算:
    ADD(加)、SUB (減)、AND(與)、OR (或)、XOR (異或)、LUI (置高位立即數)、SLL (邏輯左移)、SRL (邏輯右移)和SRA (算術右移)。

  4. WB
    寫數據模塊,負責把給定的數據寫到給定地址的Dataram中

  5. REGFILE
    32個32位寄存器模塊,MIPS指令中的寄存器號(rs、 rt和rd)有5位,因此它能訪問25 =32個寄存器

  6. Instruction ROM
    指令存儲器,可以使用系統函數進行初始化,也可以在initial塊內自己對每個存儲器單元進行賦值

  7. MIOC
    通過從WB傳過來的信號對I/O還是Dataram的讀寫操作進行置位。使用case語句進行操作。

  8. IO
    負責控制外部設備,此程序中用到了開關、按鍵、LED燈、數碼管四個設備

  9. DataMem
    數據存儲器,使用4個8位存儲器組合成一個32位存儲器,sell信號用於片選

實現的指令格式及功能

MIPS指令格式簡介

這裏寫圖片描述
其中,rs 和rt是兩個源操作數的寄存器號,rd 是目的寄存器號。注意它們在彙編指令及指令格式中的位置。-一般我們用分號表示註釋部分,但MIPS彙編語言使用#。

指令基本功能簡介

  s11/srl/sra    rd, rt, sa  # rd <--rt   shift sa
  • 這是3條移位指令(Shift Left/ Right Logical/Arithmetic),5位的sa (Shift Amount)指定移位的位數。
lui rt, imm  # rt <--  imm << 16
  • lui (Load Upper Immediate)指令把16位立即數imm左移16位,
    存入rt寄存器。它與ori指令合作,可以爲一個32位的寄存器賦任意值: lui 賦高16位,ori 賦低16位。
addi  rt, rs, imm # rt <-- rs+ imm(符號擴展)
  • addi (Add Immediate)是立即數的加法指令。注意目的寄存器號是t,立即數要符號擴展到32位。因爲是符號擴展,因此MIPS指令系統中沒有類似於subi這樣的指令。
andi/ori/xori  rt,rs, imm # rt<-- rs op imm(零擴展)
  • 這3條是邏輯操作指令(And/Or/Xor Immediate),
    因此立即數要零擴展。
lW  rt, offset (rs) # rt <-- memory[rs + offset ]
  • lw (Load Word)是- -條取存儲器字的指令。寄存器rs的內容與符號擴展的offset相加,得到存儲器地址。從存儲器取來的數據存人rt寄存器。注意,offset 就是前面講的立即數。
SW rt, offset (rs) # memory[rs + offset ]  <-- rt
  • SW(Store Word)是一條存字的指令,與lw方向相反,把rt寄存器的內容放入存儲器。存儲器地址的計算與lw相同。
beq rs, rt, label # if (rs==rt)  PC <-- label
  • beq (Branch on Equal)是一-條條件轉移指令。當寄存器rs的內容與寄存器rt的內容相等時,轉移到label。如果程序計數器PC是beq指令的地址,則label= PC+4 +offset << 2。offset 左移兩位導致PC的最低兩位永遠是0,這是因爲PC是字節地址而一條指令要佔4個字節。offset 是要符號擴展的,因此beq能實現向前和向後兩種轉移。
bne rs,rt,label # if(rs != rt)PC<-- label
  • 與beq類似,但bne (Branch on Not Equal)是在兩個寄存器的內容不相等時轉移。
  j target # PC <-- target
  • j(Jump)是一條跳轉指令。target是跳轉的目標地址,32位,由3部分組成:最高4位來自於PC+4的高4位,中間26位是指令中的address,最低兩位爲0。這條指令在生成目標地址時不需要任何電路進行計算,只需把3部分地址拼接起來就行。以下的兩條指令也不需要計算。
  jal  target #  r31<-,一  PC + 8; PC <-- target
  • jal (Jump and Link)指令與j類似, 但要把返回地址保存在r31中。即jal是子程序調用指令。jal 下一條指令的地址是PC+4, 爲什麼返回地址是PC+ 8?這是因爲MIPS指令系統實現流水線的延遲轉移功能,詳見第8章。注意,寄存器號31是約.定好的,該號碼並不出現在指令中。因此在設計電路時,應當由硬件爲jal指令產生這個號碼。
  jr rs #PC<-- rs
  • jr (Jump Register)也是一-條跳轉指令,它把rs寄存器的內容寫入PC。如果指定rs爲31,則jr是從子程序返回的指令。

到此,基本指令格式與功能介紹完畢

具體模塊實現

PC

module pc_reg(
    input wire                  clk,
    input wire                  rst,
    input wire                  PCWre, //halt指令需要除能PC
    input wire                  PCSrc,
    input wire[`InstAddrBus]    branch_addr,

    output reg[`InstAddrBus]    pc,
    output reg                  ce
);


always @(posedge clk)
  begin
    if(rst==`RstEnable) begin
        ce <=`ChipDisable;      //復位時指令存儲器禁用
        pc<=32'h00000000;       //指令存儲器禁用時,pc爲0
    end
    else if(PCWre) begin
        ce<=`ChipEnable;        //復位結束使能指令存儲器
        if (PCSrc) pc <= branch_addr;
        else       pc <= pc + 4'h4;
    end
    else  begin
        pc <= pc;  //halt指令,停機,pc保持不變
    end
  end


endmodule
  • branch_addr保存的是要跳轉的地址,該地址由ID模塊進行計算;
  • PCSrc爲跳轉標誌位,若置位,則跳轉,即把branch_addr的地址給pc;
  • PCWre爲HALT(停機)指令標誌位

ID

ID代碼較長,在這裏我只說明部分代碼,完整代碼見文末地址

//提取指令各個部分
wire[4:0]  rs           = instruction[25:21];
wire[5:0]  rt           = instruction[20:16];
wire[4:0]  rd           = instruction[15:11];
wire[15:0] immediate_16 = instruction[15:0];
wire[5:0]  func         = instruction[5:0]; //後6位
wire [5:0] op           = instruction[31:26];
/*對每條指令設置一個標誌位,其他指令類似,便於後面譯碼,本來自己
想寫的更加精簡,但水平有限,最後還是寫成了***,但最起碼實現了功能*/
i_add  = 0;
i_and  = 0;
i_sub  = 0;
i_or   = 0;
i_xor  = 0; 
//對指令進行case,並設置相應的alu_op
case(op)   
        `EXE_ORI:  begin i_ori  = 1; aluop_o = `EXE_OR_OP;  end  
        `EXE_J  :  begin i_j    = 1; branch_addr = {pc_i[31:28],instruction[25:0],2'b00}; end
        `EXE_SW:   begin i_sw    = 1; aluop_o = `EXE_ADD_OP; end
        `EXE_BEQ:  begin i_beq   = 1; aluop_o = `EXE_SUB_OP; 
                         branch_addr = pc_i + 4'h4 + {immediate_16,2'b00};end

        `EXE_HALT: begin i_halt  = 1;                        end

        6'b000000: begin
         case(func)

               `EXE_ADD: begin i_add   = 1; aluop_o = `EXE_ADD_OP; end
               `EXE_SLL: begin i_sll   = 1; aluop_o = `EXE_SLL_OP; end
               `EXE_JR:   begin i_jr   = 1;                        end
           endcase
         end             
//置相應的信號
    reg1_read_o = !(i_sll || i_srl || i_sra || i_jal || i_lui);  //除了這仨個不需要,其他都需要reg1data
    reg2_read_o = (i_add || i_sub || i_and || i_beq || i_bne || i_or || i_xor || i_sll || i_srl ||     i_sra || i_sw);
    reg1_addr_o = rs;
    reg2_addr_o = rt;
    PCWre = !i_halt;
    ALUM2Reg  = i_lw;   //lw指令標誌位,需要傳給WB模塊
    wreg_o = i_add || i_sub || i_and || i_or || i_xor || i_sll || i_srl || i_sra || i_addi || 
                         i_andi || i_ori || i_xori || i_lw || i_lui || i_jal;  //是否寫寄存器
    DataMemRW = i_sw;  //sw指令標誌位,需要傳給WB模塊
    PCSrc = (i_beq && zero) || i_j || (i_bne && !zero) || i_jal || i_jr; 
               //目標寄存器,如果是addi,ori,lw指令,寫目標寄存器爲rt,其餘爲rd
    waddr_o = (i_addi || i_andi || i_ori || i_sw || i_xori || i_lw) ? rt : rd;
    waddr_o = (op == `EXE_JAL) ?  5'b11111 : waddr_o;
    //wreg_o = !(i_j || i_beq || i_bne || i_sw);  // 除了這些指令其他指令都要寫寄存器
    branch_addr = (func == `EXE_JR) ? reg1_data_i : branch_addr; //jar_address

//確定運算源操作數1
always @ (*) begin
    if(rst == `RstEnable) begin
        reg1_o <= `ZeroWord;
    end else if(reg1_read_o == 1'b1) begin
        reg1_o <= reg1_data_i;  //Regfile讀端口1的輸出值
    end else if(reg1_read_o == 1'b0) begin
        if(op == `EXE_JAL)
            reg1_o <= pc_i + 4'h4;
            //此處應該把LUI的操作讓ALU去做,後續再改吧
        else if(op == `EXE_LUI)
            reg1_o <= {immediate_16,16'b0000000000000000};
        else
            reg1_o <= immediate_16;          //立即數
    end else begin
        reg1_o <= `ZeroWord;
    end
end

//確定運算源操作數2
always @ (*) begin
    if(rst == `RstEnable) begin
        reg2_o <= `ZeroWord;
    end else if(reg2_read_o == 1'b1) begin
        if (op == `EXE_SW)
          begin
           reg2_oo <= reg2_data_i;  //Regfile讀端口1的輸出值
           reg2_o  <= immediate_16;
          end
        else
          reg2_o <= reg2_data_i; 
    end else if(reg2_read_o == 1'b0) begin
        if(op == `EXE_JAL)
           reg2_o <= 0;
        else
           reg2_o <= immediate_16;       //立即數
    end else begin
        reg2_o <= `ZeroWord;
    end
end

EX

接口定義
module ex(
    input wire                      rst,
    //譯碼模塊傳來的信息
    input wire[`AluOpBus]           aluop_i,
    input wire[`AluSelBus]          alusel_i,
    input wire[`RegBus]             reg1_i,
    input wire[`RegBus]             reg2_i,
    input wire[`RegAddrBus]         waddr_i,
    input wire                      wreg_i,
    input wire[`RegBus]             reg2_ii,

    //運算完畢後的結果
    output reg[`RegAddrBus]         waddr_o,
    output reg                      wreg_o,
    output reg[`RegBus]             wdata_o,
    output reg[`RegBus]             reg2_o,
      //送到id
    output wire                     zero
);
//值得一提的是這個零標誌位,其他部分見完整代碼
assign zero = (logicout == 0) ? 1 : 0;  //運算0標誌位

WB

if (ALUM2Reg == 1) //lw
  begin         
      io_r     = (((ex_wdata & 32'hFFFF_F000) == 32'hFFFF_F000) ? 1'b1:1'b0);
        mr       = !io_r;
        m_iaddr  = ex_wdata;
        wb_wdata = rm_idata;  //送往regfile的數據,來自ram
    end
else if(DataMemRW == 1)  //sw
    begin
        io_w      = (((ex_wdata & 32'hFFFF_F000) == 32'hFFFF_F000) ? 1'b1:1'b0);
        mw        = !io_w;
        m_iaddr   = ex_wdata;
        wm_idata  = reg2_data_i;   //來自rt的數據
   end

LW/SW指令與其他指令不同的地方在於EX運算的結果是一個地址還是要存儲的數。

測試代碼詳解

/*流水燈每次進行判斷開關數據是否改變,如果改變則重新流水,否則繼續當前流水
       ,並將當前開關數據每四位顯示到一個數碼管上,由於共有16個開關,但有8個數碼管,
       此處我將數碼管分爲兩組,兩組數碼管顯示的數據是一樣的*/
      inst_mem[0] = 32'h34210001;//前三條指令爲了後續程序擴展暫時留用
      inst_mem[1] = 32'h34210001;
      inst_mem[2] = 32'h34220002;
      inst_mem[3] = 32'h3484ffff;  //ORI指令,4號寄存器|0x0000ffff
      inst_mem[4] = 32'h00042c00;  //左移指令,4號寄存器數左移16位存到5號寄存器
      inst_mem[5] = 32'h8ca6f008;   //把開關的數讀到6號寄存器
      inst_mem[6] = 32'h20c70000;   //把6號寄存器數送到7號寄存器便於後面比較   
      inst_mem[7] = 32'haca7f000;   //把7號寄存器的數顯示到數碼管
      inst_mem[8] = 32'haca6f004;   //把六號寄存器數送到LED
      inst_mem[9] = 32'h00064840;   // 把六號寄存器數左移一位給9號寄存器 
      //0000-00 00-000 0-0110 0100-1 000-01 00-00s00  00064840
      //0010-00 01-001 0-0110 0000 0000 0000 0000   21260000
      inst_mem[10] = 32'h21260000;//9號寄存器 送回6號寄存器;//9號寄存器 送回6號寄存器
      inst_mem[11] = 32'h8ca8f008;   //把開關的數讀到8號寄存器
      inst_mem[12] = 32'h11070001;   //比較8號寄存器和7號寄存器,相等即跳轉,執行第14條指令
      //0001-00 01-000 0-0111- 0000 0000 0000 0001
      inst_mem[13] = 32'h15070001;   //比較8號寄存器和7號寄存器,不等即跳轉,執行第15條指令
      inst_mem[14] = 32'h08000007;
      inst_mem[15] = 32'h08000005;

總結

遇到的問題

  1. 在整個設計中主要遇到的問題就是關於阻塞與非阻塞賦值的問題,由於自己在之前的Verilog課程中設計程序中基本沒有用過非阻塞,在本次設計開始時自己想當然的認爲使用非阻塞會提高程序運行效率,結果在有關於部分變量依賴於其他變量的賦值語句塊中出現了問題;請教老師說使用阻塞賦值不會影響效率,但自己對於其原因還不是很清楚;
  2. 模塊之間引腳的定義,輸出引腳的類型都要注意;

  3. 設計之前一定要把各個模塊實現的功能以及需要的信號和引腳搞清楚,在本次設計中我自己是邊設計邊加(刪)引腳,導致做了很多無用功,浪費了很多時間。文章開頭那個框架圖是我在設計完整個程序後完善的。

其他的話

  1. 搞清楚整個設計對於理解最基本的工作原理還是很有幫助的
  2. 不要自己瞎寫,雖然可以執行,但整個程序比較糟糕,容易出現問題(自己參考資料一半,瞎寫一半)
  3. 其餘的後續想到再補充吧

參考資料

參考博客
參考書籍:計算機原理與設計:Verilog HDL版

完整代碼地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章