【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱:feixiaoxing @163.com】
很多同学可能不了解mips处理器,如果个人想补充一点mips cpu的知识,可以找些书籍资料来读一下,比如《See Mips Run》。
上一期说到了,在cpu设计的过程当中,使用了数据预取的技术。它解决的问题就是,因为流水线的关系,后续数据在计算的过程当中,如果仅仅从寄存器获取数据,就有可能发生数据读取错误的情况。因为这个时候,相关寄存器的数据可能在执行、访存、写回的阶段就已经被改写了,只不过数据暂时还没有写到寄存器里面而已。所以,这个时候就需要从执行、访存、写回模块把数据提前读回来,这样就不会发生数据读取错误的情况。
1、添加更多mips 命令
有了数据预取的操作,这个时候cpu就可以添加更多汇编指令来执行了。比如说,简单的逻辑指令and、andi、or、ori、xor、xori、nor、lui,简单的移位指令sll、sllv、sra、srav、srl、srlv等等。完成这些指令的解析之后,可以准备一段汇编代码,
-
- .org 0x0
- .global _start
- .set noat
- _start:
- lui $1,0x0101
- ori $1,$1,0x0101
- ori $2,$1,0x1100
- or $1,$1,$2
- andi $3,$1,0x00fe
- and $1,$3,$1
- xori $4,$1,0xff00
- xor $1,$4,$1
- nor $1,$4,$1
生成对应的二进制代码inst_rom.data,
- 3c010101
- 34210101
- 34221100
- 00220825
- 302300fe
- 00610824
- 3824ff00
- 00810826
- 00810827
有了这些二进制代码,加上在译码、执行阶段添加必要的verilog代码。这样就可以通过波形图来验证我们的设计是否正确了。
因为对应的汇编代码比较简单,所以可以直接通过一些寄存器的阅读就可以判断汇编代码是否执行正确。被引入的波形主要是三部分,第一部分是pc_reg0;第二部分是mem_wb0;第三部分是regfile1。pc_reg0主要看pc地址有没有层层递进;mem_wb0则查看exe-wb阶段要写回的寄存器地址、数值是什么,对不对;regfile1则确认一下写回的寄存器数值对不对。
比如说从wb_wd寄存器地址为1开始,也就是汇编第一条指令的访存结束阶段,寄存器地址为1,数值为0x01010000,可以结合汇编指令看下是否正确。下一条写回的寄存器地址还是1,数值为0x01010101,继续结合汇编看下对不对。以此类推,不断检测要写的寄存器地址、寄存器数值,就可以判断之前添加的译码、执行、数据预取有没有做正确。这就是怎么读波形的一个方法。
需要注意的是,regfile1会比mem_wb0晚一个上升沿,这个留意一下即可。
2、hi和lo寄存器
hi和lo属于协处理器,不在通用寄存器的范围内。这两个寄存器主要是在用来处理乘法和除法。以乘法作为示例,如果两个整数相乘,那么乘法的结果低位保存在lo寄存器,高位保存在hi寄存器。当然,这两个寄存器也可以独立进行读取和写入。读的时候,使用mfhi、mflo;写入的时候,用mthi、mtlo。
和通用寄存器不同,mfhi、mflo是在执行阶段才开始从hi、lo寄存器获取数值的。写入则和通用寄存器一样,也是在写回的时候完成的。
也许这个时候,就有同学发现了一个问题。如果mfhi、mflo发现在访存和写回阶段,hi和lo的数值已经发生了变更怎么办?其实方法很简单,就是继续使用数据预取的方法即可。在数值还没有写回到hi和lo的时候,就提前把这些数据读回来。
- `include "defines.v"
-
- module hilo_reg(
-
- input wire clk,
- input wire rst,
-
-
- input wire we,
- input wire[`RegBus] hi_i,
- input wire[`RegBus] lo_i,
-
-
- output reg[`RegBus] hi_o,
- output reg[`RegBus] lo_o
-
- );
-
- always @ (posedge clk) begin
- if (rst == `RstEnable) begin
- hi_o <= `ZeroWord;
- lo_o <= `ZeroWord;
- end else if((we == `WriteEnable)) begin
- hi_o <= hi_i;
- lo_o <= lo_i;
- end
- end
-
- endmodule
这是hi、lo寄存器的访问文件hilo_reg.v。内容还是比较简单的。复位的时候,直接置为0;如果有写入的操作,则对hi_o、lo_o进行赋值操作。
解决了hi、lo的写入、读取问题,下面就要在ex.v中解决数据依赖的问题,
- always @ (*) begin
- if(rst == `RstEnable) begin
- {HI,LO} <= {`ZeroWord,`ZeroWord};
- end else if(mem_whilo_i == `WriteEnable) begin
- {HI,LO} <= {mem_hi_i,mem_lo_i};
- end else if(wb_whilo_i == `WriteEnable) begin
- {HI,LO} <= {wb_hi_i,wb_lo_i};
- end else begin
- {HI,LO} <= {hi_i,lo_i};
- end
- end
这段代码是组合逻辑。如果复位,则HI、LO为0;如果访存做了修改,那么直接从访存获取数值;如果写回做了修改,那么直接从写回获取数值;否则就是正常从hilo_reg.v那里获取数值。有了这段代码,那么对访存阶段的hi、lo输出就有了基本的依据,
- always @ (*) begin
- if(rst == `RstEnable) begin
- whilo_o <= `WriteDisable;
- hi_o <= `ZeroWord;
- lo_o <= `ZeroWord;
- end else if(aluop_i == `EXE_MTHI_OP) begin
- whilo_o <= `WriteEnable;
- hi_o <= reg1_i;
- lo_o <= LO;
- end else if(aluop_i == `EXE_MTLO_OP) begin
- whilo_o <= `WriteEnable;
- hi_o <= HI;
- lo_o <= reg1_i;
- end else begin
- whilo_o <= `WriteDisable;
- hi_o <= `ZeroWord;
- lo_o <= `ZeroWord;
- end
- end
注意,这段代码同样是一段组合逻辑。复位之后,没有操作;如果是mthi,那么将reg1_i和LO进行输出,这里的LO来自刚刚介绍的组合逻辑;如果是mtlo,那么将HI和reg1_i进行输出,同样HI也是来自之前的组合逻辑;当然如果上面的情况都不对,直接返回0。
上面只是说了hi、lo操作的方法和原理。真正要实施起来,还需要对访存阶段的代码修改一下。至少需要把hi、lo必要的寄存器透传下去。
- `include "defines.v"
-
- module ex_mem(
-
- input wire clk,
- input wire rst,
-
-
-
- input wire[`RegAddrBus] ex_wd,
- input wire ex_wreg,
- input wire[`RegBus] ex_wdata,
- input wire[`RegBus] ex_hi,
- input wire[`RegBus] ex_lo,
- input wire ex_whilo,
-
-
- output reg[`RegAddrBus] mem_wd,
- output reg mem_wreg,
- output reg[`RegBus] mem_wdata,
- output reg[`RegBus] mem_hi,
- output reg[`RegBus] mem_lo,
- output reg mem_whilo
-
-
- );
-
-
- always @ (posedge clk) begin
- if(rst == `RstEnable) begin
- mem_wd <= `NOPRegAddr;
- mem_wreg <= `WriteDisable;
- mem_wdata <= `ZeroWord;
- mem_hi <= `ZeroWord;
- mem_lo <= `ZeroWord;
- mem_whilo <= `WriteDisable;
- end else begin
- mem_wd <= ex_wd;
- mem_wreg <= ex_wreg;
- mem_wdata <= ex_wdata;
- mem_hi <= ex_hi;
- mem_lo <= ex_lo;
- mem_whilo <= ex_whilo;
- end
- end
-
-
- endmodule
接下来就可以准备一段汇编代码测试下,
- .org 0x0
- .set noat
- .global _start
- _start:
- lui $1,0x0000
- lui $2,0xffff
- lui $3,0x0505
- lui $4,0x0000
-
- movz $4,$2,$1
- movn $4,$3,$1
- movn $4,$3,$2
- movz $4,$2,$3
-
- mthi $0
- mthi $2
- mthi $3
- mfhi $4
-
- mtlo $3
- mtlo $2
- mtlo $1
- mflo $4
-
转成必要的指令数据,16行有效汇编代码对应16条32位数据,
- 3c010000
- 3c02ffff
- 3c030505
- 3c040000
- 0041200a
- 0061200b
- 0062200b
- 0043200a
- 00000011
- 00400011
- 00600011
- 00002010
- 00600013
- 00400013
- 00200013
- 00002012
做好了这些准备之后,就可以开始进行仿真测试了。同样,这里使用的都是《自己动手写cpu》里面的参考代码,是Chapter6里面的代码。借鉴使用别人的代码问题不大,关键是能够看懂、学会,这样就可以在自己的项目中不断实践了。当然,之前说过,需要对代码进行修改一下。
简单分析下,先找到pc第一次取指操作,然后找到第一次wb数据。从图形上看,第一次写回数据的时候是290ns。写回的地址是0x1,数值是0x0,这个时候就可以结合汇编第一条代码确认下是不是这样的。没有问题的话,可以再确认下一个周期里面,是不是regs1真的发生了改变。这就是第一条汇编指令的分析过程。
继续分析,直到450ns的时候,开始执行第一个协处理器指令mthi。
这里分析下mthi是否正确,可以直接把两组信号拖出来。一组信号来自于mem_wb0;另一组信号来自于hilo_reg0。从450ns开始,第一次写入的数据hi是0x0、lo是0x0;第二次写入的数据hi是0xffff0000、lo是0x0,以此类推。这两个数据写的对不对,可以结合汇编代码一起来看下。对应的汇编代码应该是mthi $0,mthi $2这样的。
从530ns开始,执行的命令变成了mtlo,分析方法和刚才mthi还是一样的。
从上面可以看出,cpu的实现主要还是要静下心来慢慢做,耐得烦。要相信自己,问题总是可以解决的。