目录
CPU的一般性设计方法
cpu设计就是设计数据通路+控制逻辑。因为cpu就是一个数字逻辑电路,包含组合逻辑和时序逻辑。数据通路就是输入,运算,存储的数据在输出的数据都在组合逻辑电路和时序逻辑电路上流转。同时,因为数据通路中会有多路选择器、时序逻辑器件,所以还要有相应的控制信号,产生这些控制信号的逻辑称为控制逻辑。所以,从宏观的视角来看,设计一个CPU就是设计它的“数据通路+控制逻辑"。
市面上不同的书籍介绍的实现方法不同,如最开始看的CPU设计实战,在介绍实现方法时一次考虑所有的指令、所有的情况,然后给出代码。在后来代码实现时有些吃力故又找来下面这本书。
自己动手写CPU这本书中,笔者借鉴了软件开发中的“增量模型”概念,也称为迭代思想:先考虑最简单的情况,给出代码,然后考虑稍微多一 点的情况,修改、补充代码,随着考虑情况的增多,不停地修改、补充代码,最终,使代码实现需求。适合初学者跟着实现一遍。
五级流水线的过程
- 取指阶段:从指令存储器读出指令,同时确定下一条指令地址。
- 译码阶段:对指令进行译码,从通用寄存器中读出要使用的寄存器的值,如果指令中含有立即数,那么还要将立即数进行符号扩展或无符号扩展。如果是转移指令,并且满足转移条件,那么给出转移目标,作为新的指令地址。
- 执行阶段:按照译码阶段给出的操作数、运算类型,进行运算,给出运算结果。如果是Load/Store指令,那么还会计算Load/Store的目标地址。
- 访存阶段: 如果是Load/Store指令,那么在此阶段会访问数据存储器,反之,只是将执行阶段的结果向下传递到回写阶段。同时,在此阶段还要判断是否有异常需要处理,如果有,那么会清除流水线,然后转移到异常处理例程入口地址处继续执行。
- 回写阶段:将运算结果保存到目标寄存器。
defines头文件的定义
defines.v此文件定义了下面模块中所用到一些宏定义,如常用的数据宽度,数据长度以及指令码的定义。
使用头文件只需在模块最前面加上`include"defines.v"
//全局
`define RstEnable 1'b1
`define RstDisable 1'b0
`define ZeroWord 32'h00000000
`define WriteEnable 1'b1
`define WriteDisable 1'b0
`define ReadEnable 1'b1
`define ReadDisable 1'b0
`define AluOpBus 7:0
`define AluSelBus 2:0
`define InstValid 1'b0
`define InstInvalid 1'b1
`define Stop 1'b1
`define NoStop 1'b0
`define InDelaySlot 1'b1
`define NotInDelaySlot 1'b0
`define Branch 1'b1
`define NotBranch 1'b0
`define InterruptAssert 1'b1
`define InterruptNotAssert 1'b0
`define TrapAssert 1'b1
`define TrapNotAssert 1'b0
`define True_v 1'b1
`define False_v 1'b0
`define ChipEnable 1'b1
`define ChipDisable 1'b0
//指令
`define EXE_ORI 6'b001101
`define EXE_NOP 6'b000000
//AluOp
`define EXE_OR_OP 8'b00100101
`define EXE_ORI_OP 8'b01011010
`define EXE_NOP_OP 8'b00000000
//AluSel
`define EXE_RES_LOGIC 3'b001
`define EXE_RES_NOP 3'b000
//指令存储器inst_rom
`define InstAddrBus 31:0
`define InstBus 31:0
`define InstMemNum 131071
`define InstMemNumLog2 17
//通用寄存器regfile
`define RegAddrBus 4:0
`define RegBus 31:0
`define RegWidth 32
`define DoubleRegWidth 64
`define DoubleRegBus 63:0
`define RegNum 32
`define RegNumLog2 5
`define NOPRegAddr 5'b00000
一、取指阶段的实现
取指阶段整体的作用:取出指令存储器中的指令,PC值递增,准备取下一条指令。
1.1PC模块
首先思考接口部分的实现,需要了解以下两点:
1.PC模块的实现目标:
给出指令地址就是32位的pc
2.与前后模块的关系:
之前没有模块就是基本的时钟clk和复位信号rst;
之后的模块是指令存储器(因为要去这里根据指令地址把指令码取出来)因此在取指的时候需要把指令存储器的使能端口打开这里记为ce。
接下来思考内部的功能实现,这里我们肯定要了解PC模块实现的功能,这里可以根据输出信号来设计:
PC:每一个时钟周期+4,要注意只有在存储器打开的时候才行否则一直为0;
CE:根据复位信号对指令存储器进行使能,复位的时候存储器不可用
module pc_reg(
input wire clk,
input wire rst,
output reg [31:0] pc,
output reg ce,
);
//复位时使能无效
always @(posedge clk) begin
if (rst) begin
ce = 0;
end else begin
ce = 1;
end
end
//使能无效时为0,有效+4
always @(posedge clk ) begin
if (ce == 0) begin //先写特殊情况
pc <= 32'h0000_0000;
end
else begin //再写更多的那种情况
pc <= pc + 4'h4;
end
end
endmodule
1.2 IF/ID模块即取指和译码中间阶段的模块
首先思考接口部分的实现,需要了解以下两点:
1.IF/ID模块的实现目标:
暂时保存取指阶段的指令和指令地址,在下一个时钟周期转递给译码阶段。加上这个模块是为了确保流水线的正常运行,因为流水线是按照时钟周期划分的如下图。
2.与前后模块的关系:
之前的模块就是PC模块和指令存储器模块,接收这两个模块中输出的指令地址if_pc和指令码if_inst;
之后的模块就是译码模块,我们需要将保存的指令和指令地址输出给译码模块进行译码,输出指令地址id_pc,输出指令为id_inst。
接下来思考内部的功能实现,已经了解IF/ID模块实现的功能就是暂时保存并在下一个上升周期输出给译码阶段,所以只需要一个触发器,不过要考虑复位时要清零。
module if_id(
input clk,
input rst,
input [31:0] if_pc,
input [31:0] if_inst,
output reg [31:0] id_pc,
output reg [31:0] id_inst
);
//复位时输出都为0,其他直接赋值
always @(posedge clk ) begin
if (rst) begin
id_pc <= 0;
id_inst <= 0;
end else begin
id_pc <= if_pc;
id_inst <= if_inst;
end
end
endmodule
二、译码阶段
我们需要先回顾下ori指令的实现:
指令用法为: ori rs, rt, immediate,作用是将指令中的16位立即数immediate进行无符号
扩展至32位,然后与索引为rs的通用寄存器的值进行逻辑“或”运算,运算结果保存到索引
为rt的通用寄存器中。
2.1 ID模块
在分析ID模块的接口时首先需要了解ID模块实现了哪些功能?
ID模块的作用是对指令进行译码,得到最终运算的类型、子类型、源操作数1、源操作
数2、要写入的目的寄存器地址等信息,其中运算类型指的是逻辑运算、移位运算、算术运算
等,子类型指的是更加详细的运算类型,比如:当运算类型是逻辑运算时,运算子类型可以是,逻辑“或"运算、逻辑“与”运算、逻辑“异或”运算等。
根据上述功能,输入肯定有pc_i和inst_i;
因为要对寄存器中源操作数进行操作,所以要根据寄存器里的数据地址去寄存器堆里读写读数据。故输出有reg_addr和reg_data。又因为不同的指令操作,需要读写寄存器的数量不同,ori指令只需要读一个寄存器数据即可,根据下图一共有三种类型的指令,最多对两个寄存器进行读操作,所以这里的reg_addr和reg_data各有两个。此外每个读操作还具有一个读使能来判断是否进行读写即reg1_read_o和reg2_read_o。
因为最终要得到最终运算的类型(aluop_o)、子类型(alusel_o)、源操作数1(reg1)、源操作数2(re2)、要写入的目的寄存器地址(wd_0)等信息,这些信息是为了送到执行阶段进行运算。而又根据上图可以看到J类型的指令不需要wd_0,所以需要一个使能信号wreg_0判断是否写入目的寄存器,不过写回操作是流水线的最后一个操作,所以要一直到跟着流水到写回操作。
据此ID模块的输入输出信号分析完毕,可以看到对输入输出信号的分析需要结合模块功能以及上下模块的关系来综合考虑。输入输出信号如下图所示:
输出信号 | 具体实现 | 判断条件的来源 |
alu_op | 根据不同指令得到 | op |
alusel_o | 同上 | op |
reg1_o | 根据读使能判断等于reg1_data_i或imm | reg1_read_o |
reg2_o | 根据读使能判断等于reg2_data_i或imm | reg2_read_o |
wd_o | 根据指令判断输出目的地址 | op |
wreg_o | 根据指令判断使能 | op |
reg1_addr_o | 根据输入inst_i指令码得到 | 无 |
reg2_addr_o | 根据输入inst_i指令码得到 | 无 |
reg1_read_o | 根据指令判断使能 | op |
reg2_read_o | 根据指令判断使能 | op |
根据每个输出信号的判断条件(这里的判断条件就是指让输出信号变化的来源,比如对于aluop_o它只随着指令的变化而改变,所以判断条件op),可以分为两段来写:
1.以op信号为判断条件的输出信号赋值
//取得指令码功能码操作地址等
wire [5:0] op = inst_i[31:26];
reg [31:0] imm;//为什么是reg?
//根据不同指令进行译码,输出操作类型以及使能信号
//回答:是否进行写,写地址;要从寄存器读几个数据;需要立即数吗;
//其实都是需要输出到下一阶段使用的信号
always @(*) begin
//根据指令的特点对使能等信号进行赋值
case (op)
`EXE_ORI:begin
aluop_o = `EXE_OR_OP;
alusel_o = `EXE_RES_LOGIC;
reg1_read_o = 1;
reg2_read_o = 0;
wreg_o = 1;
imm = {16'b0,inst_i[15:0]};
wd_o = inst_i[20:16];
end
default: begin
end
endcase
end
2.以读使能信号为判断条件的输出信号
//根据寄存器堆返回的数据进行输出,需要考虑的情况有:复位,读使能有效,读使能无效即输出立即数
always @(*) begin
if (rst) begin
reg1_o = 0;
end
else if (!reg1_read_o) begin
reg1_o = imm;
end
else if (reg1_read_o) begin
reg1_o = reg1_data_i;
end
else begin
reg1_o = 0;
end
end
always @(*) begin
if (rst) begin
reg2_o = 0;
end
else if (!reg2_read_o) begin
reg2_o = imm;
end
else if (reg2_read_o) begin
reg2_o = reg2_data_i;
end
else begin
reg2_o = 0;
end
end
3.最后还要对每个输出信号赋予初始值以确保每个状态都有输出避免xz的产生,复位时全为0即可,其他时候只需要给出两个源操作数和目的寄存器的最常用地址即可。
always @(*) begin
//对所有输出信号进行初始化操作,确保每个信号在任何状态下都有输出
if (rst) begin
aluop_o = `EXE_NOP_OP;
alusel_o = `EXE_RES_NOP;
//reg1_o = 0;//为什么不对输出信号全部初始化?
//reg2_o = 0;
wd_o = `NOPRegAddr;
wreg_o = 0;
reg1_addr_o = `NOPRegAddr;
reg2_addr_o = `NOPRegAddr;
reg1_read_o = 0;
reg2_read_o = 0;
imm = 0;
end
else begin
aluop_o = `EXE_NOP_OP;
alusel_o = `EXE_RES_NOP;
//reg1_o = 0;
//reg2_o = 0;
wd_o = inst_i[15:11];//最常用的rd,先这样解释?
wreg_o = 0;
reg1_addr_o = inst_i[25:21];
reg2_addr_o = inst_i[20:16];
reg1_read_o = 0;
reg2_read_o = 0;
imm = 0;
end
//根据指令的特点对使能等信号进行赋值
case (op)
`EXE_ORI:begin
aluop_o = `EXE_OR_OP;
alusel_o = `EXE_RES_LOGIC;
reg1_read_o = 1;
reg2_read_o = 0;
wreg_o = 1;
imm = {16'b0,inst_i[15:0]};
wd_o = inst_i[20:16];
end
default: begin
end
endcase
end
2.2 regfile寄存器堆模块具体实现
在分析ID模块的功能时我们发现需要去寄存器堆进行读数据操作,所以还需要一个寄存器堆Regfile模块,具体实现在之前有讲过CPU设计实战-verilog实现MIPS下CPU中的寄存器堆
不过在具体实现中还是稍有不同,因为参考的书目不同。
首先每个寄存器端口都加了一个读使能信号,这个在上述讲过,因为不是所有指令都要读两个寄存器;然后对写入功能也加了写使能信号,原因同理,不是所有指令都要写回目标寄存器。
另外在写操作的具体实现中,需要注意0寄存器不能写入,因为规定恒为0;
在读操作中需要注意复位和使能信号:
1.复位和读地址为0时输出数据为0
2.当读写地址一样时,直接把写入的数据给输出信号
3.写操作是时序逻辑操作,与电平同步,读操作是组合逻辑操作,确保随时能读到
代码如下:
module regfile(
input clk,
input rst,
input [4:0] raddr1,
output reg[31:0]rdata1,
input re1,
input [4:0] raddr2,
output reg[31:0]rdata2,
input re2,
input [4:0] waddr,
input [31:0]wdata,
input we
);
//32个32位寄存器堆
reg [31:0] regs [0:31];
//写操作
always @(posedge clk ) begin
if (!rst && we && (waddr != 0)) begin
regs[waddr] <= wdata;
end
end
//读操作
always @(*) begin
if (!rst && re1) begin
rdata1 = regs[raddr1];
end
else if (!rst && re1 && (raddr1 == 0)) begin//0号寄存器恒为0
rdata1 = 0;
end
//读写同一个地址,把写地址直接赋值,保证写优先,这样才能读到最新的数据,这也是为什么先写写操作的代码
else if (!rst && re1 && we && (raddr1 == waddr)) begin
rdata1 = wdata;
end
else begin
rdata1 = 0;
end
end
always @(*) begin
if (!rst && re2) begin
rdata2 = regs[raddr2];
end
else if (!rst && re2 && (raddr2 == 0)) begin//0号寄存器恒为0
rdata2 = 0;
end
else if (!rst && re2 && we && (raddr2 == waddr)) begin
rdata2 = wdata;
end
else begin
rdata2 = 0;
end
end
end
endmodule
2.3 ID/EX模块的实现
作用与之前的IF/ID模块一样都是暂时存储数据,在下一个高电平时传递给下一阶段EXE执行模块。那么输入输出接口也比较简单,都是一样的。
具体实现仅需考虑复位时的额外情况,其他时候直接输入对应输出即可。
代码如下:
module id_ex(
input clk,
input rst,
input [7:0] id_aluop,
input [2:0] id_alusel,
input [31:0]id_reg1,
input [31:0]id_reg2,
input [4:0] id_wd,
input id_wreg,
output reg [7:0] ex_aluop,
output reg [2:0] ex_alusel,
output reg [31:0] ex_reg1,
output reg [31:0] ex_reg2,
output reg [4:0] ex_wd,
output reg ex_wreg
);
always @(posedge clk ) begin
if (rst) begin
ex_aluop <= 0;
ex_alusel <= 0;
ex_reg1 <= 0;
ex_reg2 <= 0;
ex_wd <= 0;
ex_wreg <= 0;
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
三、执行阶段的实现
3.1 EX模块
其实之前的译码阶段的输出为什么要分为最终运算的类型、子类型、源操作数1、源操作数2主要是根据执行这个阶段来考虑的,因为需要根据不同指令不同的运算逻辑去选择执行的运算。
执行阶段的作用就是把源操作数进行运算的结果写入目的寄存器中
就ori这个运算来说,执行阶段实现的逻辑运算很简单就是根据源操作数进行或运算,最终输出计算好的写入目的寄存器的数据wdata_o和地址wd_o,i因为不是所有指令操作都有写入目的寄存器的值,所以需要加一个使能信号wreg_o是否有要写入的目的寄存器,,这个之前也说过。
具体实现做的事情:
1.依据aluop_i指示的运算子类型进行运算,此处只有逻辑“或”运算
2.依据alusel_ i指示的运算类型,选择一个运算结果作为最终结果,此处只有逻辑运算结果
对输出信号进行分析
wdata_0 | 根据op和sel对操作数进行运算 |
wd_o | 根据输入wd_i |
wreg_o | 根据输入wreg_i |
`include "defines.v"
module ex(
//input clk,全是组合逻辑运算
input rst,
input [7:0] aluop_i,
input [2:0] alusel_i,
input [31:0]reg1_i,
input [31:0]reg2_i,
input [4:0] wd_i,
input wreg_i ,
output reg [31:0] wdata_o,
output reg [4:0] wd_o,
output reg wreg_o
);
reg [31:0] logicout;
always @(*) begin
if (rst) begin
logicout = 0;
end
else begin
case (aluop_i)
`EXE_OR_OP:begin
logicout = reg1_i | reg2_i;
end
default:begin
logicout = 0;
end
endcase
end
end
always @(*) begin
wd_o = wd_i;
wreg_o = wreg_i;
case (alusel_i)
`EXE_RES_LOGIC: begin
wdata_o = logicout;
end
default: begin
wdata_o = 0;
end
endcase
end
endmodule
3.2 EX/MEM模块
EX模块的输出连接到EX/MEM模块,后者的作用是将执行阶段取得的运算结果,在下一个时钟传递到流水线访存阶段,其接口描述如下图:
module ex_mem(
input clk,
input rst,
input [31:0] ex_wdata,
input [4:0] ex_wd,
input ex_wreg,
output reg [31:0] mem_wdata,
output reg [4:0] mem_wd,
output reg mem_wreg
);
always @(posedge clk ) begin
if (rst) begin
mem_wdata <= 0;
mem_wd <= 0;
mem_wreg <= 0;
end
else begin
mem_wdata <= ex_wdata;
mem_wd <= ex_wd;
mem_wreg <= ex_wreg;
end
end
endmodule
四、访存阶段的实现
4.1 MEM模块
现在,ori 指令进入访存阶、段了,但是由于ori 指令不需要访问数据存储器,所以在访存.阶段,不做任何事,只是简单地将执行阶段的结果向回写阶段传递即可。
module mem(
input rst,
input [31:0] wdata_i,
input [4:0] wd_i,
input wreg_i,
output reg [31:0]wdata_o,
output reg [4:0] wd_o,
output reg wreg_o
);
always @(*) begin
if (rst) begin
wdata_o = 0;
wd_o = 0;
wreg_o = 0;
end
begin
wdata_o = wdata_i;
wd_o = wd_i;
wreg_o = wreg_i;
end
end
endmodule
4.2 MEM/WB模块
MEM/WB模块的作用是将访存阶段的运算结果,在下一个时钟传递到回写阶段,其接口描述如下图所示。
MEM/WB的代码与MEM模块的代码十分相似,都是将输入信号传递到对应的输出端口,但是MEM/WB模块中的是时序逻辑电路,即在时钟上升沿才发生信号传递,而MEM模块中是组合逻辑电路。
module mem_wb(
input clk,
input rst,
input [31:0] mem_wdata,
input [4:0] mem_wd,
input mem_wreg,
output reg [31:0] wb_wdata,
output reg [4:0] wb_wd,
output reg wb_reg
);
always @(posedge clk ) begin
if (rst) begin
wb_wdata <= 0;
wb_wd <= 0 ;
wb_reg <= 0 ;
end
else begin
wb_wdata <= mem_wdata;
wb_wd <= mem_wd ;
wb_reg <= mem_wreg ;
end
end
endmodule
五、回写阶段的实现
回写阶段其实就是把访存阶段的输出:目的寄存器地址以及要写入目的寄存器的数据传回寄存器堆,寄存器堆已经实现,将相关信号连接即可。
至此,一个最简单的实现一个指令的五级流水线模块已经写好,整体模块架构如下:
至此,第一条简单指令ORI在五级流水线的实现过程全部完成,下一章将对其进行验证,在验证之前需要对以上模块设计一个顶层模块my_mips用于给各个模块之间的连线。