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位,因此它能访问 2 5 =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版

猜你喜欢

转载自blog.csdn.net/Chuxin126/article/details/80941532
今日推荐