本博客内容基于《自己动手写CPU》这本书
上一篇文章介绍了一下流水线思想、设计流程等,下面我们可以实操一下实现第一条ori指令。
其实实现一条ori指令不难,我目前对这一条指令的理解简单来说就是,先看图对应每一个接口连线。
ori指令说明
ori是进行逻辑“或”运算的指令,指令格式如下图所示
指令用法为:ori rs,rt,immediate,将指令中的16位立即数immediate进行无符号拓展至32位,然后与地址为rs的通用寄存器进行逻辑“或”运算,将运算结果保存至地址为rt的通用寄存器中。
原始OpenMIPS五级流水线结构
首先看图,在上一篇文章介绍了五级流水线,这里就不在赘述了。下图是实现一条ori指令的五级流水线结构,在后续的学习中我们会在每个阶段增加一点,到最后形成一个OpenMIPS32五级流水线。
下图是原始OpenMIPS五级流水线系统结构图
然后就是将你现在看到的线通过各个模块连接起来。(我把宏定义放在最后了)
取指阶段
用于给出指令地址,其 I/O 端口如下
PC模块代码:
`include "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; //指令存储器禁用时,PC为0
end
else begin
pc <= pc + 4'h4; //指令存储器使能时,PC的值每时钟周期加4
end
end
endmodule
取指/译码模块
用于暂时保存取指得到的指令,以及地址,在下一个时钟周期送给译码阶段,其 I/O 端口如下
if_id 模块代码:
`include "defines.v"
module if_id(
input wire rst,
input wire clk,
//来自取值阶段的信号,InstBus表示指令宽度为32
input wire [`InstBus] if_inst,
//对应的译码阶段的信号
output reg [`InstBus] id_inst
);
always @(posedge clk) begin
if(rst == `RstEnable) begin
id_inst <= `ZeroWord;
end
else begin
id_inst <= if_inst;
end
end
endmodule
译码模块
将得到的指令1.给出运算类型 2.给出参与运算的操作数
Regfile模块
实现了32个32位通用整数寄存器,可以同时进行两个寄存器的读操作和一个寄存器的写操作,其 I/O 端口如下
模块代码:
`include "defines.v"
module regfile(
input wire clk,
input wire rst,
//写端口
input wire we,
input wire[`RegAddrBus] waddr,
input wire[`RegBus] wdata,
//读端口1
input wire re1,
input wire[`RegAddrBus] raddr1,
output reg[`RegBus] rdata1,
//读端口2
input wire re2,
input wire[`RegAddrBus] raddr2,
output reg[`RegBus] rdata2
);
//定义32个位宽为32的通用寄存器
reg [`RegBus] regs[0:`RegNum-1];
//写操作
always @(posedge clk) begin
if(rst == `RstDisable) begin
if((we == `WriteEnable) && (waddr != `RegNumLog2'h0)) begin //寄存器0不可写
regs[waddr] <= wdata;
end
end
end
//MIPS32架构规定$0的值只能为0
//读操作1,组合逻辑
always @(*) begin
if(rst == `RstEnable) begin
rdata1 <= `ZeroWord;
end
else if(raddr1 == `RegNumLog2'h0) begin
rdata1 <= `ZeroWord;
end
else if((re1 == `ReadEnable) && (we == `WriteEnable) && (waddr == raddr1)) begin
rdata1 <= wdata;
end
else if(re1 == `ReadEnable) begin
rdata1 <= regs[raddr1];
end
else begin
rdata1 <= `ZeroWord;
end
end
//读操作2
always @(*) begin
if(rst == `RstEnable) begin
rdata2 <= `ZeroWord;
end
else if(raddr2 == `RegNumLog2'h0) begin
rdata2 <= `ZeroWord;
end
else if((re2 == `ReadEnable) && (we == `WriteEnable) && (waddr == raddr2)) begin
rdata2 <= wdata;
end
else if(re2 == `ReadEnable) begin
rdata2 <= regs[raddr2];
end
else begin
rdata2 <= `ZeroWord;
end
end
endmodule
ID模块
对指令进行译码,得到最终的运算类型、子类型、源操作数1 2、要写入的目的寄存器等,其 I/O 端口如下
模块代码:
`include "defines.v"
module id(
input wire rst,
input wire [`InstBus] inst_i,
//读取的Regfile的值
input wire[`RegBus] reg1_data_i,
input wire[`RegBus] reg2_data_i,
//输出到Regfile的信息
output reg reg1_read_o,
output reg reg2_read_o,
output reg [`RegAddrBus] reg1_addr_o,
output reg [`RegAddrBus] reg2_addr_o,
//送到执行阶段的信息
output reg[`AluSelBus] alusel_o,
output reg[`AluOpBus] aluop_o,
output reg[`RegBus] reg1_o,
output reg[`RegBus] reg2_o,
output reg[`RegAddrBus] wd_o, //译码阶段的指令要写入的目的寄存器地址
output reg wreg_o //译码阶段的指令是否有要写入的目的寄存器
);
//取得指令的指令码与功能码
wire [5:0] op = inst_i[31:26];
wire [4:0] op2 = inst_i[10:6];
wire [5:0] op3 = inst_i[5:0];
wire [4:0] op4 = inst_i[20:16];
//保存指令执行需要的立即数
reg[`RegBus] imm;
//指示指令是否有效
reg instvalid;
//第一段:对指令进行译码
always @(*) begin
if(rst == `RstEnable) begin
aluop_o = `EXE_NOP_OP;
alusel_o = `EXE_RES_NOP;
wd_o = `NOPRegAddr;
wreg_o = `WriteDisable;
instvalid = `InstInvalid;
reg1_read_o = `ReadDisable;
reg2_read_o = `ReadDisable;
reg1_addr_o = `NOPRegAddr;
reg2_addr_o = `NOPRegAddr;
imm = `ZeroWord;
end
else begin
case(op)
`EXE_ORI:begin
aluop_o = `EXE_OR_OP;
alusel_o = `EXE_RES_LOGIC;
wd_o = inst_i[20:16];
wreg_o = `WriteEnable;
instvalid = `InstValid;
reg1_read_o = `ReadEnable;
reg2_read_o = `ReadDisable;
reg1_addr_o = inst_i[25:21];
reg2_addr_o = inst_i[20:16];
imm = {16'h0,inst_i[15:0]};
end
default: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 = `ReadDisable;
reg2_read_o = `ReadDisable;
reg1_addr_o = inst_i[25:21];
reg2_addr_o = inst_i[20:16];
imm = `ZeroWord;
end
endcase
end
end
//确定进行运算的操作数1
always @(*) begin
if(rst == `RstEnable) begin
reg1_o = `ZeroWord;
end
else if(reg1_read_o == 1'b1) begin
reg1_o = reg1_data_i;
end
else if(reg1_read_o == 1'b0) begin
reg1_o = imm;
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
reg2_o = reg2_data_i;
end
else if(reg2_read_o == 1'b0) begin
reg2_o = imm;
end
else begin
reg2_o = `ZeroWord;
end
end
endmodule
译码/执行模块
将译码阶段信息在下一个时钟周期给执行阶段,其 I/O 端口如下
模块代码:
`include "defines.v"
module id_ex(
input wire clk,
input wire rst,
//从译码阶段传递过来的信息
input wire[`AluSelBus] id_alusel,
input wire[`AluOpBus] id_aluop,
input wire[`RegBus] id_reg1,
input wire[`RegBus] id_reg2,
input wire[`RegAddrBus] id_wd, //执行阶段的指令要写入的目的寄存器的地址
input wire id_wreg,//执行阶段的指令是否有要写入的目的寄存器
//传递到执行阶段的信息
output reg[`AluSelBus] ex_alusel,
output reg[`AluOpBus] ex_aluop,
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_alusel <= `EXE_RES_NOP;
ex_aluop <= `EXE_NOP_OP;
ex_reg1 <= `ZeroWord;
ex_reg2 <= `ZeroWord;
ex_wd <= `NOPRegAddr;
ex_wreg <= `WriteDisable;
end
else begin
ex_alusel <= id_alusel;
ex_aluop <= id_aluop;
ex_reg1 <= id_reg1;
ex_reg2 <= id_reg2;
ex_wd <= id_wd;
ex_wreg <= id_wreg;
end
end
endmodule
执行模块
根据 ID/EX 得到的数据进行运算,其 I/O 端口如下
模块代码:
`include "defines.v"
module ex(
input wire rst,
//译码阶段送到执行阶段的信息
input wire[`AluSelBus] alusel_i,
input wire[`AluOpBus] aluop_i,
input wire[`RegBus] reg1_i,
input wire[`RegBus] reg2_i,
input wire[`RegAddrBus] wd_i,
input wire wreg_i,
//执行的结果
output reg[`RegAddrBus] wd_o, //执行阶段的结果最终要写入的目的寄存器的地址
output reg wreg_o,
output reg[`RegBus] wdata_o //执行阶段最终要写入目的寄存器的值
);
//保存逻辑运算的结果
reg [`RegBus] logicout;
//根据aluop指示的运算子类型进行运算,此处只有逻辑或运算
always @(*) begin
if(rst == `RstEnable) begin
logicout = `ZeroWord;
end
else begin
case(aluop_i)
`EXE_OR_OP: logicout = reg1_i | reg2_i;
default: logicout = `ZeroWord;
endcase
end
end
//根据alusel指示的运算类型,选择一个运算结果作为最终结果
//此时只有逻辑运算结果
always @(*) begin
wd_o = wd_i;
wreg_o = wreg_i;
case(alusel_i)
`EXE_RES_LOGIC: wdata_o = logicout;
default: wdata_o = `ZeroWord;
endcase
end
endmodule
执行/访存模块
将执行阶段得到的结果在下一个时钟周期传到访存阶段,其 I/O 端口如下
模块代码:
`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,
//送到访存阶段的信息
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
访存模块
由于ori指令在访存阶段不需要访问数据寄存器,所以在访存阶段不做任何事,只是简单传递数据到回写阶段,其 I/O 端口如下
模块代码:
`include "defines.v"
module mem(
input wire rst,
//来自执行阶段的信息
input wire[`RegAddrBus] wd_i,
input wire wreg_i,
input wire[`RegBus] wdata_i,
//访存阶段的结果
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
访存/回写阶段
将访存阶段的结果在下一时钟周期传到回写阶段,其 I/O 端口如下
模块代码:
`include "defines.v"
module mem_wb(
input wire clk,
input wire rst,
//访存阶段的结果
input wire[`RegAddrBus] mem_wd,
input wire mem_wreg,
input wire[`RegBus] mem_wdata,
//送到回写阶段的信息
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
end
endmodule
回写阶段
参考上面五级流水线,此时这个阶段实际上是在Regfile模块中实现的,将上一模块得到的值传到Regfile。
顶层模块
每个模块都完成后,我们现在需要将各个模块进行例化、连接,连接的关系还是看上面的图。
例化其实也就相当于面向对象语言中实例化一个对象出来,然后将各个对象连接起来就可以得到我们的顶层模块
下面我们看看顶层模块的端口
当然,我们现在只是实现一条简单的ori指令,所以顶层模块会比较简单,等到后期指令增加、增加协处理器、中断处理的时候顶层模块就会复杂很多,不过还是像上面说的一样--看图,连线
模块代码:
`include "defines.v"
module openmips(
input wire clk,
input wire rst,
input wire[`InstBus] rom_data_i,
output wire[`InstAddrBus] rom_addr_o,
output wire rom_ce_o
);
//连接IF/ID模块与译码阶段ID模块的变量
wire[`InstBus] id_inst_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;
//连接译码阶段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 [`RegAddrBus] ex_wd_i;
wire ex_wreg_i;
//连接执行阶段EX模块输出与EX/MEM模块的输入的变量
wire [`RegBus] ex_wdata_o;
wire [`RegAddrBus] ex_wd_o;
wire ex_wreg_o;
//连接EX/MEM模块输出与访存MEM模块输入的变量
wire [`RegBus] mem_wdata_i;
wire [`RegAddrBus] mem_wd_i;
wire mem_wreg_i;
//连接MEM模块输出与MEM/WB模块输入的变量
wire [`RegBus] mem_wdata_o;
wire [`RegAddrBus] mem_wd_o;
wire mem_wreg_o;
//连接MEM/WB模块输出与寄存器堆模块输入的变量
wire [`RegBus] wb_wdata_o;
wire [`RegAddrBus] wb_wd_o;
wire wb_wreg_o;
//pc_reg例化
pc_reg pc_reg0(
.clk(clk), .rst(rst), .pc(rom_addr_o), .ce(rom_ce_o)
);
//IF/ID模块例化
if_id if_id0(
.rst(rst), .clk(clk), .if_inst(rom_data_i), .id_inst(id_inst_i)
);
//译码阶段ID模块例化
id id0(
.rst(rst), .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),
.alusel_o(id_alusel_o), .aluop_o(id_aluop_o),
.reg1_o(id_reg1_o), .reg2_o(id_reg2_o),
.wd_o(id_wd_o), .wreg_o(id_wreg_o)
);
//通用寄存器Regfile模块例化
regfile regfile0(
.clk(clk), .rst(rst),
.we(wb_wreg_o), .waddr(wb_wd_o), .wdata(wb_wdata_o),
.re1(reg1_read), .raddr1(reg1_addr), .rdata1(reg1_data),
.re2(reg2_read), .raddr2(reg2_addr), .rdata2(reg2_data)
);
//ID/EX模块例化
id_ex id_ex0(
.clk(clk), .rst(rst),
.id_alusel(id_alusel_o), .id_aluop(id_aluop_o),
.id_reg1(id_reg1_o), .id_reg2(id_reg2_o),
.id_wd(id_wd_o), .id_wreg(id_wreg_o),
.ex_alusel(ex_alusel_i), .ex_aluop(ex_aluop_i),
.ex_reg1(ex_reg1_i), .ex_reg2(ex_reg2_i),
.ex_wd(ex_wd_i), .ex_wreg(ex_wreg_i)
);
//EX模块例化
ex ex0(
.rst(rst),
.alusel_i(ex_alusel_i), .aluop_i(ex_aluop_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_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模块例化
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_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_o), .wb_wreg(wb_wreg_o), .wb_wdata(wb_wdata_o)
);
endmodule
代码看着比较长,但实际其本质都是一样的,懂了一块其他模块都懂了(不要放弃,加油!)
指令存储器ROM的实现
顾名思义,只读存储器,与openMIPS相连再构成最小SOPC
模块代码:
`include "defines.v"
module inst_rom(
input wire ce,
input wire[`InstAddrBus] addr,
output reg[`InstBus] inst
);
//定义一个数组,大小是InstMemNum,元素宽度是InstBus
reg [`InstBus] inst_mem[0:`InstMemNum-1];
//使用文件inst_rom.data初始化寄存器,依据自己的路径给data文件改成绝对路径
initial $readmemh ("D:/inst_rom.data",inst_mem);
//当复位信号无效时,依据输入的地址,给出指令存储器ROM中对应的元素
always @(*) begin
if(ce == `ChipDisable) begin
inst <= `ZeroWord;
end
else begin
//指令寄存器的每个地址是32bit的字,所以要将OpenMips给出的指令地址除以4再使用
inst <= inst_mem[addr[`InstMemNumLog2+1:2]];
end
end
endmodule
⚠️注意注意!
这个读取文件的路径一定要用绝对路径!!!!
最小SOPC的实现
上面说过,将openMIPS与inst_rom连接成为一个最小SOPC
建立Test Bench文件
在上一篇文章讲过Test Bench文件
将各个模块设计完成后,通过test Bench文件,test Bench文件为测试或仿真一个Verilog HDL程序搭建了一个平台,我们被测试的模块施加激励信号,通过观察被测试模块的输出响应,从而判断逻辑是否正确。
模块代码:
//时间单位是1ns,精度是1ps
`timescale 1ns/1ps
`include "defines.v"
module openmips_min_sopc_tb();
reg CLOCK_50;
reg rst;
//每隔10ns,CLOCK_50信号翻转一次,所以一个周期是20ns,对应50MHz
initial begin
CLOCK_50 = 1'b0;
forever #10 CLOCK_50 = ~CLOCK_50;
end
//最初时刻,复位信号有效,在第195ns,复位信号无效,最小SOPC开始运行
//运行1000ns后,暂停仿真
initial begin
rst = `RstEnable;
#195 rst = `RstDisable;
#1000 $stop;
end
//例化最小SOPC
openmips_min_sopc openmips_min_sopc0(
.clk(CLOCK_50),
.rst(rst)
);
endmodule
之后将所有文件放在一起仿真就可以得到结果啦!
总结
·看图将各个模块根据图的描述写出来,再将各个模块例化连接起来(顶层模块),将顶层模块与存储器连接后建立最小SOPC,最后给SOPC激励信号进行仿真。
·交叉编译环境在这里还没有提到,其实就是通过GNU工具链对你的源代码(汇编代码)进行编译、链接、得到bin文件 最后将格式转化就可以得到我们需要的存储器文件inst_rom.data
宏定义代码
//全局宏定义
`define RstEnable 1'b1 //复位信号有效
`define RstDisable 1'b0 //复位信号无效
`define ChipEnable 1'b1 //芯片使能
`define ChipDisable 1'b0 //芯片禁止
`define ZeroWord 32'h00000000 //32位的数值0
`define WriteEnable 1'b1 //使能写
`define WriteDisable 1'b0 //禁止写
`define ReadEnable 1'b1 //使能读
`define ReadDisable 1'b0 //禁止读
`define AluSelBus 2:0 //译码阶段的输出alusel_o的宽度
`define AluOpBus 7:0 //译码阶段的输出aluop_o的宽度
`define InstValid 1'b1 //指令有效
`define InstInvalid 1'b0 //指令无效
//与指令存储器相关的定义
`define InstAddrBus 31:0 //ROM的地址总线宽度
`define InstBus 31:0 //ROM的数据总线宽度
`define InstMemNum 131071 //ROM的实际大小为128KB
`define InstMemNumLog2 17 //ROM实际使用的地址线宽度
//与通用寄存器Regfile有关的宏定义
`define RegAddrBus 4:0 //Regfile模块的地址线宽度
`define RegBus 31:0 //Regfile模块的数据线宽度
`define RegNum 32 //通用寄存器的数量
`define RegNumLog2 5 //寻址通用寄存器所用的地址位数
`define NOPRegAddr 5'b00000
//与具体指令相关的宏定义
`define EXE_ORI 6'b001101 //指令ori的指令码
`define EXE_NOP 6'b000000
//AluOp
`define EXE_OR_OP 8'b00100101
`define EXE_NOP_OP 8'b00000000
//AluSel
`define EXE_RES_LOGIC 3'b001
`define EXE_RES_NOP 3'b000