前言
根据“自己动手写CPU”这本书学习,自己动手实现一个MIPS指令集的CPU。
本文章实现了一个ori
指令即“或”操作的五级流水线,后续会持续添加其他指令完善此CPU。
文章作为学习笔记持续更新,源代码也在github上持续更新
项目源码https://github.com/yizhixiaohuihui/OpenMIPS.git
一、设计实现ori
指令的OpenMIPS处理器
实现ori
指令的OpenMIPS五级流水线结构图
说明:
1.1、取指阶段:取出指令存储器中的指令,同时PC值递增,准备取下一条指令
- PC模块(pc_reg.v):给出指令地址
`include "../rtl/defines.v"module pc_reg(input wire clk, input wire rst,output reg[`InstAddrBus] pc, // 要读取的指令地址output reg ce // ָ指令存储器使能信号
);always @ (posedge clk) beginif (rst == `RstEnable) begince <= `ChipDisable; // 复位的时候指令存储器禁用end else begince <= `ChipEnable; // 复位结束后指令存储器使能endendalways @ (posedge clk) beginif (ce == `ChipDisable) beginpc <= 32'h00000000;end else beginpc <= pc + 4'h4; // pc+4指向下一条指令地址(一条指令32位对应4字节)endend
endmodule
- IF/ID模块(if_id.v):暂时保存取指阶段取得的指令
`include "../rtl/defines.v"module if_id(input wire clk,input wire rst,input wire[`InstAddrBus] if_pc, // 取指阶段取得的指令对应的地址input wire[`InstBus] if_inst, // 取指阶段取得的指令output reg[`InstAddrBus] id_pc, // 译码阶段的指令对应的地址output reg[`InstBus] id_inst // 译码阶段的指令
);always @ (posedge clk) beginif (rst == `RstEnable) beginid_pc <= `ZeroWord;id_inst <= `ZeroWord;end else begin // 其余时刻向下传递取指阶段的值id_pc <= if_pc;id_inst <= if_inst;endend
endmodule
1.2、译码阶段:对取到的指令进行译码 -> 给出要进行的运算类型,以及参与运算的操作数
- Regfile模块(regfile.v):实现32个32位通用整数寄存器,可以同时进行两个寄存器的读操作和一个寄存器的写操作
`include "../rtl/defines.v"module regfile(input wire clk,input wire rst,// write portinput wire we,input wire[`RegAddrBus] waddr,input wire[`RegBus] wdata,// read port 1input wire re1,input wire[`RegAddrBus] raddr1,output reg[`RegBus] rdata1,// read port 2input wire re2,input wire[`RegAddrBus] raddr2,output reg[`RegBus] rdata2);// 1. define 32's 32bits registersreg[`RegBus] regs[0:`RegNum-1];// 2. write operationalways @ (posedge clk) beginif (rst == `RstDisable) begin// we enable and write operation destination register != 0if((we == `WriteEnable) && (waddr != `RegNumLog2'h0)) beginregs[waddr] <= wdata;endendend// Notice: read operation is Combinatorial logic// 3. read port1's read operationalways @ (*) beginif(rst == `RstEnable) beginrdata1 <= `ZeroWord;end else if(raddr1 == `RegNumLog2'h0) beginrdata1 <= `ZeroWord;end // if read port1 want read register is same as the register to writeelse if((raddr1 == waddr) && (we == `WriteEnable) && (re1 == `ReadEnable)) beginrdata1 <= wdata;end else if(re1 == `ReadEnable) beginrdata1 <= regs[raddr1];end // if read port1 can't be usedelse beginrdata1 <= `ZeroWord;endend// 4. read port2's read operationalways @ (*) beginif(rst == `RstEnable) beginrdata2 <= `ZeroWord;end else if(raddr2 == `RegNumLog2'h0) beginrdata2 <= `ZeroWord;end else if((raddr2 == waddr) && (we == `WriteEnable) && (re2 == `ReadEnable)) beginrdata2 <= wdata;end else if(re2 == `ReadEnable) beginrdata2 <= regs[raddr2];end else beginrdata2 <= `ZeroWord;endendendmodule
- ID模块(id.v):对指令进行译码,得到最终运算的类型、子类型、源操作数1、源操作数2、要写入的目的寄存器地址等信息
`include "../rtl/defines.v"module id(input wire rst,input wire[`InstAddrBus] pc_i,input wire[`InstBus] inst_i,// readed regfile's datainput wire[`RegBus] reg1_data_i,input wire[`RegBus] reg2_data_i,// ouput to Regfile's messageoutput reg reg1_read_o, // Regfile reg1's ReadEnableoutput reg reg2_read_o, // Regfile reg2's ReadEnableoutput reg[`RegAddrBus] reg1_addr_o,output reg[`RegAddrBus] reg2_addr_o, // sent Execution stage messageoutput reg[`AluOpBus] aluop_o,output reg[`AluSelBus] alusel_o,output reg[`RegBus] reg1_o,output reg[`RegBus] reg2_o,output reg[`RegAddrBus] wd_o,output reg wreg_o
);// instructions codewire[5:0] op = inst_i[31:26]; // ori: judge 26-31bit can judge ori instructionwire[4:0] op2 = inst_i[10:6];wire[5:0] op3 = inst_i[5:0];wire[4:0] op4 = inst_i[20:16];// store immediate num that execute instructions needreg[`RegBus] imm;// instruction is valid or notreg instvalid;// 1. decode instructionsalways @ (*) begin if (rst == `RstEnable) beginaluop_o <= `EXE_NOP_OP;alusel_o <= `EXE_RES_NOP;wd_o <= `NOPRegAddr;wreg_o <= `WriteDisable;instvalid <= `InstValid;reg1_read_o <= 1'b0;reg2_read_o <= 1'b0;reg1_addr_o <= `NOPRegAddr;reg2_addr_o <= `NOPRegAddr;imm <= 32'h0; end else beginaluop_o <= `EXE_NOP_OP;alusel_o <= `EXE_RES_NOP;wd_o <= inst_i[15:11];wreg_o <= `WriteDisable;instvalid <= `InstInvalid; reg1_read_o <= 1'b0;reg2_read_o <= 1'b0;reg1_addr_o <= inst_i[25:21]; // rs register's addressreg2_addr_o <= inst_i[20:16]; // rt register's addressimm <= `ZeroWord; case (op)`EXE_ORI: begin // judge is ori ins wreg_o <= `WriteEnable; // ori need write in destination registeraluop_o <= `EXE_OR_OP; // aiuop is or operationalusel_o <= `EXE_RES_LOGIC; // logic operationreg1_read_o <= 1'b1; // only need read rsreg2_read_o <= 1'b0; // immediate num not need read from regimm <= {16'h0, inst_i[15:0]}; // extend immediate num to unsinged 32 bits wd_o <= inst_i[20:16]; // rt: destination register's address to write instvalid <= `InstValid; end default: beginendendcase //case op end //ifend //always// 2. choose source num2always @ (*) beginif(rst == `RstEnable) beginreg1_o <= `ZeroWord;end else if(reg1_read_o == 1'b1) beginreg1_o <= reg1_data_i; // rsend else if(reg1_read_o == 1'b0) beginreg1_o <= imm;end else beginreg1_o <= `ZeroWord;endend// 3. choose source num2 always @ (*) beginif(rst == `RstEnable) beginreg2_o <= `ZeroWord;end else if(reg2_read_o == 1'b1) beginreg2_o <= reg2_data_i; // rtend else if(reg2_read_o == 1'b0) beginreg2_o <= imm;end else beginreg2_o <= `ZeroWord;endendendmodule
- ID/EX模块(id_ex.v):将ID模块译码阶段取得的结果在下个时钟传递到流水线执行阶段
`include "../rtl/defines.v"module id_ex(input wire clk,input wire rst,// message from decodeStageinput wire[`AluOpBus] id_aluop,input wire[`AluSelBus] id_alusel,input wire[`RegBus] id_reg1,input wire[`RegBus] id_reg2,input wire[`RegAddrBus] id_wd,input wire id_wreg, // message sent to executeStageoutput reg[`AluOpBus] ex_aluop,output reg[`AluSelBus] ex_alusel,output reg[`RegBus] ex_reg1,output reg[`RegBus] ex_reg2,output reg[`RegAddrBus] ex_wd,output reg ex_wreg);always @ (posedge clk) beginif (rst == `RstEnable) beginex_aluop <= `EXE_NOP_OP;ex_alusel <= `EXE_RES_NOP;ex_reg1 <= `ZeroWord;ex_reg2 <= `ZeroWord;ex_wd <= `NOPRegAddr;ex_wreg <= `WriteDisable;end else begin ex_aluop <= id_aluop;ex_alusel <= id_alusel;ex_reg1 <= id_reg1;ex_reg2 <= id_reg2;ex_wd <= id_wd;ex_wreg <= id_wreg; endendendmodule
1.3、执行阶段:依据译码阶段的结果,对源操作数1、源操作数2、进行指定的运算
- EX模块(ex.v):从ID/EX模块得到运算的类型、子类型、源操作数1、源操作数2、要写入的目的寄存器地址,并依据这些数据进行计算
`include "../rtl/defines.v"module ex(input wire rst,// message rom decodeStageinput wire[`AluOpBus] aluop_i,input wire[`AluSelBus] alusel_i,input wire[`RegBus] reg1_i,input wire[`RegBus] reg2_i,input wire[`RegAddrBus] wd_i,input wire wreg_i,// execute resultoutput reg[`RegAddrBus] wd_o,output reg wreg_o,output reg[`RegBus] wdata_o);// save logic op resultreg[`RegBus] logicout;// 1. according aluop_i to operatealways @ (*) beginif(rst == `RstEnable) beginlogicout <= `ZeroWord;end else begincase (aluop_i)`EXE_OR_OP: beginlogicout <= reg1_i | reg2_i;enddefault: beginlogicout <= `ZeroWord;endendcaseend end // 2. according alusel_i to choose an op result as last resultalways @ (*) beginwd_o <= wd_i; // detinationReg's address need write wreg_o <= wreg_i; // write reg enablecase ( alusel_i ) `EXE_RES_LOGIC: beginwdata_o <= logicout; // save op resultenddefault: beginwdata_o <= `ZeroWord;endendcaseend endmodule
- EX/MEM模块(ex_mem.v):将执行阶段取得的运算结果在下个时钟传递到流水线访存阶段
`include "../rtl/defines.v"module ex_mem(input wire clk,input wire rst,// message from executeStageinput wire[`RegAddrBus] ex_wd,input wire ex_wreg,input wire[`RegBus] ex_wdata, // message sent to accessStageoutput reg[`RegAddrBus] mem_wd,output reg mem_wreg,output reg[`RegBus] mem_wdata);always @ (posedge clk) beginif(rst == `RstEnable) beginmem_wd <= `NOPRegAddr;mem_wreg <= `WriteDisable;mem_wdata <= `ZeroWord; end else beginmem_wd <= ex_wd;mem_wreg <= ex_wreg;mem_wdata <= ex_wdata; end end endmodule
1.4、访存阶段:由于ori指令不需要访问数据存储器,所以在访存阶段不做任何事,只是简单地将执行阶段的结果向写回阶段传递
- MEM模块(mem.v):将输入的执行阶段的结果直接作为输出
`include "../rtl/defines.v"module mem(input wire rst,// message from executeStageinput wire[`RegAddrBus] wd_i,input wire wreg_i,input wire[`RegBus] wdata_i,// result in accessStageoutput reg[`RegAddrBus] wd_o,output reg wreg_o,output reg[`RegBus] wdata_o);always @ (*) beginif(rst == `RstEnable) beginwd_o <= `NOPRegAddr;wreg_o <= `WriteDisable;wdata_o <= `ZeroWord;end else beginwd_o <= wd_i;wreg_o <= wreg_i;wdata_o <= wdata_i;end end endmodule
- MEM/WB模块(mem_wb.v):将访存阶段的运算结果,在下一个时钟传递到回写阶段
`include "../rtl/defines.v"module mem_wb(input wire clk,input wire rst,// accessStage result input wire[`RegAddrBus] mem_wd,input wire mem_wreg,input wire[`RegBus] mem_wdata,// message sent to writeBackStageoutput reg[`RegAddrBus] wb_wd,output reg wb_wreg,output reg[`RegBus] wb_wdata );always @ (posedge clk) beginif(rst == `RstEnable) beginwb_wd <= `NOPRegAddr;wb_wreg <= `WriteDisable;wb_wdata <= `ZeroWord; end else beginwb_wd <= mem_wd;wb_wreg <= mem_wreg;wb_wdata <= mem_wdata;end //ifend //alwaysendmodule
1.5、回写阶段:将指令的运算结果写入目的寄存器
- Regfile模块(regfile.v):将指令的运算结果写入目的寄存器
ori
指令格式
地址为rs的寄存器的值 = 立即数 or 地址为rt的寄存器的值
目的操作数 = 源操作数1 || 源操作数2
1.6、顶层模块
`include "../rtl/defines.v"module openmips(input wire clk,input wire rst,input wire[`RegBus] rom_data_i, // instruction get from instructionRomemaryoutput wire[`RegBus] rom_addr_o, // address ouput to instructionRomemaryoutput wire rom_ce_o // instructionRomemary enable);// PC -> IF/IDwire[`InstAddrBus] pc;// IF/ID -> IDwire[`InstAddrBus] id_pc_i;wire[`InstBus] id_inst_i;// ID -> ID/EXwire[`AluOpBus] id_aluop_o;wire[`AluSelBus] id_alusel_o;wire[`RegBus] id_reg1_o;wire[`RegBus] id_reg2_o;wire id_wreg_o;wire[`RegAddrBus] id_wd_o;// ID/EX -> EXwire[`AluOpBus] ex_aluop_i;wire[`AluSelBus] ex_alusel_i;wire[`RegBus] ex_reg1_i;wire[`RegBus] ex_reg2_i;wire ex_wreg_i;wire[`RegAddrBus] ex_wd_i;// EX -> EX/MEMwire ex_wreg_o;wire[`RegAddrBus] ex_wd_o;wire[`RegBus] ex_wdata_o;// Ex/MEM -> MEMwire mem_wreg_i;wire[`RegAddrBus] mem_wd_i;wire[`RegBus] mem_wdata_i;// MEM -> MEM/WB wire mem_wreg_o;wire[`RegAddrBus] mem_wd_o;wire[`RegBus] mem_wdata_o;// MEM/WB -> WBwire wb_wreg_i;wire[`RegAddrBus] wb_wd_i;wire[`RegBus] wb_wdata_i;// ID <-> Regfilewire reg1_read;wire reg2_read;wire[`RegBus] reg1_data;wire[`RegBus] reg2_data;wire[`RegAddrBus] reg1_addr;wire[`RegAddrBus] reg2_addr;pc_reg pc_reg0(.clk(clk),.rst(rst),.pc(pc),.ce(rom_ce_o) );assign rom_addr_o = pc;if_id if_id0(.clk(clk),.rst(rst),.if_pc(pc),.if_inst(rom_data_i),.id_pc(id_pc_i),.id_inst(id_inst_i) );id id0(.rst(rst),.pc_i(id_pc_i),.inst_i(id_inst_i),.reg1_data_i(reg1_data),.reg2_data_i(reg2_data),.reg1_read_o(reg1_read),.reg2_read_o(reg2_read), .reg1_addr_o(reg1_addr),.reg2_addr_o(reg2_addr), .aluop_o(id_aluop_o),.alusel_o(id_alusel_o),.reg1_o(id_reg1_o),.reg2_o(id_reg2_o),.wd_o(id_wd_o),.wreg_o(id_wreg_o));regfile regfile1(.clk (clk),.rst (rst),.we (wb_wreg_i),.waddr (wb_wd_i),.wdata (wb_wdata_i),.re1 (reg1_read),.raddr1 (reg1_addr),.rdata1 (reg1_data),.re2 (reg2_read),.raddr2 (reg2_addr),.rdata2 (reg2_data));id_ex id_ex0(.clk(clk),.rst(rst),.id_aluop(id_aluop_o),.id_alusel(id_alusel_o),.id_reg1(id_reg1_o),.id_reg2(id_reg2_o),.id_wd(id_wd_o),.id_wreg(id_wreg_o),.ex_aluop(ex_aluop_i),.ex_alusel(ex_alusel_i),.ex_reg1(ex_reg1_i),.ex_reg2(ex_reg2_i),.ex_wd(ex_wd_i),.ex_wreg(ex_wreg_i)); ex ex0(.rst(rst),.aluop_i(ex_aluop_i),.alusel_i(ex_alusel_i),.reg1_i(ex_reg1_i),.reg2_i(ex_reg2_i),.wd_i(ex_wd_i),.wreg_i(ex_wreg_i),.wd_o(ex_wd_o),.wreg_o(ex_wreg_o),.wdata_o(ex_wdata_o));ex_mem ex_mem0(.clk(clk),.rst(rst),.ex_wd(ex_wd_o),.ex_wreg(ex_wreg_o),.ex_wdata(ex_wdata_o),.mem_wd(mem_wd_i),.mem_wreg(mem_wreg_i),.mem_wdata(mem_wdata_i));mem mem0(.rst(rst),.wd_i(mem_wd_i),.wreg_i(mem_wreg_i),.wdata_i(mem_wdata_i),.wd_o(mem_wd_o),.wreg_o(mem_wreg_o),.wdata_o(mem_wdata_o));mem_wb mem_wb0(.clk(clk),.rst(rst),.mem_wd(mem_wd_o),.mem_wreg(mem_wreg_o),.mem_wdata(mem_wdata_o),.wb_wd(wb_wd_i),.wb_wreg(wb_wreg_i),.wb_wdata(wb_wdata_i));endmodule
二、验证设计正确性
2.1 指令存储器ROM
- 在初始化指令存储器时使用了
initial
过程语句,不能被综合工具支持,若想被综合修改初始化指令存储器的方法 $readmemh
读取数据的系统函数,表示从inst_rom.data文件中读取数据以初始化inst_mem- OpenMIPS是按字节寻址的,而此处定义的指令存储器的每个地址是一个32bit的字,所以要将OpenMIPS给出的地址除以4再使用
2.2最小SOPC
为了验证建立一个SOPC:OpenMIPS从指令存储器读取指令,指令进入OpenMIPS开始执行
`include "../rtl/defines.v"module openmips_min_sopc(input wire clk,input wire rst);//openmips <-> ROMwire[`InstAddrBus] inst_addr;wire[`InstBus] inst;wire rom_ce;openmips openmips0(.clk(clk),.rst(rst),.rom_addr_o(inst_addr),.rom_data_i(inst),.rom_ce_o(rom_ce));inst_rom inst_rom0(.addr(inst_addr),.inst(inst),.ce(rom_ce) );endmodule
2.3使用VCS和verdi联合仿真
“自己动手写CPU”这本书是在windows环境下用modelsim
软件进行仿真验证,而众所周知工业界都是在Linux环境下使用VCS
和verdi
进行仿真验证,因此本文使用Linux环境进行验证并可以学习一些VCS
和verdi
软件的使用。
如何得到inst_rom.data文件参考文章https://blog.csdn.net/yvbycf/article/details/128359374
testbench文件
`include "../rtl/defines.v"
`timescale 1ns/1psmodule openmips_min_sopc_tb();reg CLOCK_50;reg rst;initial beginCLOCK_50 = 1'b0;// cycle is 20ns: 50Mhzforever #10 CLOCK_50 = ~CLOCK_50;endinitial beginrst = `RstEnable;// min sopc start run#195 rst= `RstDisable;#1000 $stop;endopenmips_min_sopc openmips_min_sopc0(.clk(CLOCK_50),.rst(rst) );initial begin$fsdbDumpfile("tb.fsdb"); // generate "tb.fsdb"// $fsdbDumpvars(0, openmips_min_sopc_tb, "+mda"); // dump tb and dut ports, "+mda"->dump mem$fsdbDumpvars(0, openmips_min_sopc_tb); // dump tb and dut ports$fsdbDumpMDA( ); // dump memendendmodule
注意:
- 要在vedi中显示波形需要使用
$fsdbDumpfile( )
函数指定波形文件名 - 要使用函数
$fsdbDumpvars( )
去dump tb和dut的端口加载波形 - 要加载寄存器数组的波形,需要使用
$fsdbDumpMDA( )
函数
我们使用Makefile文件控制VCS和verdi联合仿真
Makfile文件
all : vcs verdivcs :vcs \-f filelist.f \-timescale=1ns/1ps \-debug_acc+dmptf -debug_region+cell+encrypt -full64 -R +vc +v2k -sverilog -debug_all \-P ${LD_LIBRARY_PATH}/novas.tab ${LD_LIBRARY_PATH}/pli.a \| tee vcs.log verdi :verdi -f filelist.f -ssf tb.fsdb clean :rm -rf *~ core csrc simv* vc_hdrs.h ucli.key urg* *.log novas.* *.fsdb* verdiLog 64* DVEfiles *.vpd
在文件夹sim/下使用命令
make vcs
make verdi
即可仿真及打开verdi查看波形
如何编写Makefile
进行vcs+verdi联合仿真可参考本人的另一篇博客VCS + verdi + Makefile
仿真波形图
- if_inst是取到的指令,从仿真可知,依次取出inst_rom.data中的指令
- 观察regs[1]、regs[2]、regs[3]、regs[4]的最终值,可知OpenMIPS正确执行了程序
三、链接汇总
项目源码https://github.com/yizhixiaohuihui/OpenMIPS.git
如何得到inst_rom.data文件参考文章https://blog.csdn.net/yvbycf/article/details/128359374
如何编写Makefile
进行vcs+verdi联合仿真可参考本人的另一篇博客VCS + verdi + Makefile