前言
根据“自己动手写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) begin
if (rst == `RstEnable) begin
ce <= `ChipDisable; // 复位的时候指令存储器禁用
end
else begin
ce <= `ChipEnable; // 复位结束后指令存储器使能
end
end
always @ (posedge clk) begin
if (ce == `ChipDisable) begin
pc <= 32'h00000000;
end
else begin
pc <= pc + 4'h4; // pc+4指向下一条指令地址(一条指令32位对应4字节)
end
end
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) begin
if (rst == `RstEnable) begin
id_pc <= `ZeroWord;
id_inst <= `ZeroWord;
end
else begin // 其余时刻向下传递取指阶段的值
id_pc <= if_pc;
id_inst <= if_inst;
end
end
endmodule
1.2、译码阶段:对取到的指令进行译码 -> 给出要进行的运算类型,以及参与运算的操作数
- Regfile模块(regfile.v):实现32个32位通用整数寄存器,可以同时进行两个寄存器的读操作和一个寄存器的写操作
`include "../rtl/defines.v"
module regfile(
input wire clk,
input wire rst,
// write port
input wire we,
input wire[`RegAddrBus] waddr,
input wire[`RegBus] wdata,
// read port 1
input wire re1,
input wire[`RegAddrBus] raddr1,
output reg[`RegBus] rdata1,
// read port 2
input wire re2,
input wire[`RegAddrBus] raddr2,
output reg[`RegBus] rdata2
);
// 1. define 32's 32bits registers
reg[`RegBus] regs[0:`RegNum-1];
// 2. write operation
always @ (posedge clk) begin
if (rst == `RstDisable) begin
// we enable and write operation destination register != 0
if((we == `WriteEnable) && (waddr != `RegNumLog2'h0)) begin
regs[waddr] <= wdata;
end
end
end
// Notice: read operation is Combinatorial logic
// 3. read port1's read operation
always @ (*) begin
if(rst == `RstEnable) begin
rdata1 <= `ZeroWord;
end
else if(raddr1 == `RegNumLog2'h0) begin
rdata1 <= `ZeroWord;
end
// if read port1 want read register is same as the register to write
else if((raddr1 == waddr) && (we == `WriteEnable) && (re1 == `ReadEnable)) begin
rdata1 <= wdata;
end
else if(re1 == `ReadEnable) begin
rdata1 <= regs[raddr1];
end
// if read port1 can't be used
else begin
rdata1 <= `ZeroWord;
end
end
// 4. read port2's read operation
always @ (*) begin
if(rst == `RstEnable) begin
rdata2 <= `ZeroWord;
end
else if(raddr2 == `RegNumLog2'h0) begin
rdata2 <= `ZeroWord;
end
else if((raddr2 == waddr) && (we == `WriteEnable) && (re2 == `ReadEnable)) begin
rdata2 <= wdata;
end
else if(re2 == `ReadEnable) begin
rdata2 <= regs[raddr2];
end
else begin
rdata2 <= `ZeroWord;
end
end
endmodule
- 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 data
input wire[`RegBus] reg1_data_i,
input wire[`RegBus] reg2_data_i,
// ouput to Regfile's message
output reg reg1_read_o, // Regfile reg1's ReadEnable
output reg reg2_read_o, // Regfile reg2's ReadEnable
output reg[`RegAddrBus] reg1_addr_o,
output reg[`RegAddrBus] reg2_addr_o,
// sent Execution stage message
output 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 code
wire[5:0] op = inst_i[31:26]; // ori: judge 26-31bit can judge ori instruction
wire[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 need
reg[`RegBus] imm;
// instruction is valid or not
reg instvalid;
// 1. decode instructions
always @ (*) begin
if (rst == `RstEnable) begin
aluop_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 begin
aluop_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 address
reg2_addr_o <= inst_i[20:16]; // rt register's address
imm <= `ZeroWord;
case (op)
`EXE_ORI: begin // judge is ori ins
wreg_o <= `WriteEnable; // ori need write in destination register
aluop_o <= `EXE_OR_OP; // aiuop is or operation
alusel_o <= `EXE_RES_LOGIC; // logic operation
reg1_read_o <= 1'b1; // only need read rs
reg2_read_o <= 1'b0; // immediate num not need read from reg
imm <= {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: begin
end
endcase //case op
end //if
end //always
// 2. choose source num2
always @ (*) begin
if(rst == `RstEnable) begin
reg1_o <= `ZeroWord;
end
else if(reg1_read_o == 1'b1) begin
reg1_o <= reg1_data_i; // rs
end
else if(reg1_read_o == 1'b0) begin
reg1_o <= imm;
end
else begin
reg1_o <= `ZeroWord;
end
end
// 3. choose source num2
always @ (*) begin
if(rst == `RstEnable) begin
reg2_o <= `ZeroWord;
end
else if(reg2_read_o == 1'b1) begin
reg2_o <= reg2_data_i; // rt
end
else if(reg2_read_o == 1'b0) begin
reg2_o <= imm;
end
else begin
reg2_o <= `ZeroWord;
end
end
endmodule
- ID/EX模块(id_ex.v):将ID模块译码阶段取得的结果在下个时钟传递到流水线执行阶段
`include "../rtl/defines.v"
module id_ex(
input wire clk,
input wire rst,
// message from decodeStage
input 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 executeStage
output 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) begin
if (rst == `RstEnable) begin
ex_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;
end
end
endmodule
1.3、执行阶段:依据译码阶段的结果,对源操作数1、源操作数2、进行指定的运算
- EX模块(ex.v):从ID/EX模块得到运算的类型、子类型、源操作数1、源操作数2、要写入的目的寄存器地址,并依据这些数据进行计算
`include "../rtl/defines.v"
module ex(
input wire rst,
// message rom decodeStage
input 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 result
output reg[`RegAddrBus] wd_o,
output reg wreg_o,
output reg[`RegBus] wdata_o
);
// save logic op result
reg[`RegBus] logicout;
// 1. according aluop_i to operate
always @ (*) begin
if(rst == `RstEnable) begin
logicout <= `ZeroWord;
end
else begin
case (aluop_i)
`EXE_OR_OP: begin
logicout <= reg1_i | reg2_i;
end
default: begin
logicout <= `ZeroWord;
end
endcase
end
end
// 2. according alusel_i to choose an op result as last result
always @ (*) begin
wd_o <= wd_i; // detinationReg's address need write
wreg_o <= wreg_i; // write reg enable
case ( alusel_i )
`EXE_RES_LOGIC: begin
wdata_o <= logicout; // save op result
end
default: begin
wdata_o <= `ZeroWord;
end
endcase
end
endmodule
- EX/MEM模块(ex_mem.v):将执行阶段取得的运算结果在下个时钟传递到流水线访存阶段
`include "../rtl/defines.v"
module ex_mem(
input wire clk,
input wire rst,
// message from executeStage
input wire[`RegAddrBus] ex_wd,
input wire ex_wreg,
input wire[`RegBus] ex_wdata,
// message sent to accessStage
output reg[`RegAddrBus] mem_wd,
output reg mem_wreg,
output reg[`RegBus] mem_wdata
);
always @ (posedge clk) begin
if(rst == `RstEnable) begin
mem_wd <= `NOPRegAddr;
mem_wreg <= `WriteDisable;
mem_wdata <= `ZeroWord;
end
else begin
mem_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 executeStage
input wire[`RegAddrBus] wd_i,
input wire wreg_i,
input wire[`RegBus] wdata_i,
// result in accessStage
output reg[`RegAddrBus] wd_o,
output reg wreg_o,
output reg[`RegBus] wdata_o
);
always @ (*) begin
if(rst == `RstEnable) begin
wd_o <= `NOPRegAddr;
wreg_o <= `WriteDisable;
wdata_o <= `ZeroWord;
end else begin
wd_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 writeBackStage
output reg[`RegAddrBus] wb_wd,
output reg wb_wreg,
output reg[`RegBus] wb_wdata
);
always @ (posedge clk) begin
if(rst == `RstEnable) begin
wb_wd <= `NOPRegAddr;
wb_wreg <= `WriteDisable;
wb_wdata <= `ZeroWord;
end else begin
wb_wd <= mem_wd;
wb_wreg <= mem_wreg;
wb_wdata <= mem_wdata;
end //if
end //always
endmodule
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 instructionRomemary
output wire[`RegBus] rom_addr_o, // address ouput to instructionRomemary
output wire rom_ce_o // instructionRomemary enable
);
// PC -> IF/ID
wire[`InstAddrBus] pc;
// IF/ID -> ID
wire[`InstAddrBus] id_pc_i;
wire[`InstBus] id_inst_i;
// ID -> ID/EX
wire[`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 -> EX
wire[`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/MEM
wire ex_wreg_o;
wire[`RegAddrBus] ex_wd_o;
wire[`RegBus] ex_wdata_o;
// Ex/MEM -> MEM
wire 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 -> WB
wire wb_wreg_i;
wire[`RegAddrBus] wb_wd_i;
wire[`RegBus] wb_wdata_i;
// ID <-> Regfile
wire 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 <-> ROM
wire[`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.youkuaiyun.com/yvbycf/article/details/128359374
testbench文件
`include "../rtl/defines.v"
`timescale 1ns/1ps
module openmips_min_sopc_tb();
reg CLOCK_50;
reg rst;
initial begin
CLOCK_50 = 1'b0;
// cycle is 20ns: 50Mhz
forever #10 CLOCK_50 = ~CLOCK_50;
end
initial begin
rst = `RstEnable;
// min sopc start run
#195 rst= `RstDisable;
#1000 $stop;
end
openmips_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 mem
end
endmodule
注意:
- 要在vedi中显示波形需要使用
$fsdbDumpfile( )
函数指定波形文件名 - 要使用函数
$fsdbDumpvars( )
去dump tb和dut的端口加载波形 - 要加载寄存器数组的波形,需要使用
$fsdbDumpMDA( )
函数
我们使用Makefile文件控制VCS和verdi联合仿真
Makfile文件
all : vcs verdi
vcs :
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.youkuaiyun.com/yvbycf/article/details/128359374
如何编写Makefile
进行vcs+verdi联合仿真可参考本人的另一篇博客VCS + verdi + Makefile