一点前言
多周期 CPU 相比单周期 CPU 以及流水线的实现来说其实写起来要麻烦那么一些,但是相对于流水线以及单周期 CPU 而言,多周期 CPU 除了能提升主频之外似乎并没有什么卵用。不过我的课题是多周期 CPU 那么就开始吧。
多周期 CPU
不同于单周期 CPU,多周期 CPU 指的是将整个 CPU 的执行过程分成几个阶段,每个阶段用一个时钟去完 成,然后开始下一条指令的执行,而每种指令执行时所用的时钟数不尽相同,这就是所谓的多周期CPU。
CPU在处理指令时,一般需要经过以下几个阶段:
(1) 取指令(IF):根据程序计数器 PC 中的指令地址,从存储器中取出一条指令,同时,PC 根据指令字长度自动递增产生下一条指令所需要的指令地址,但遇到“地址转移”指令 时,则控制器把“转移地址”送入 PC,当然得到的“地址”需要做些变换才送入 PC。
(2) 指令译码(ID):对取指令操作中得到的指令进行分析并译码,确定这条指令需要完成的操作,从而产生相应的操作控制信号,用于驱动执行状态中的各种操作。
(3) 指令执行(EXE):根据指令译码得到的操作控制信号,具体地执行指令动作,然后转移到结果写回状态。
(4) 存储器访问(MEM):所有需要访问存储器的操作都将在这个步骤中执行,该步骤给出存储器的数据地址,把数据写入到存储器中数据地址所指定的存储单元或者从存储器中得 到数据地址单元中的数据。
(5) 结果写回(WB):指令执行的结果或者访问存储器中得到的数据写回相应的目的寄存器中。
这也就意味着一条 CPU 指令最长需要 5 个时钟周期才能执行完毕,至于具体需要多少周期则根据指令的不同而不同。
MIPS 指令集的设计为定长简单指令集,这为 CPU 的实现带来了极大的方便。
指令集
MIPS 指令分为三种:R、I 和 J,三种指令有不同的存储方式:
其中,
- op:操作码;
- rs:第1个源操作数寄存器,寄存器地址(编号)是00000~11111,00~1F;
- rt:第2个源操作数寄存器,或目的操作数寄存器,寄存器地址(同上);
- rd:目的操作数寄存器,寄存器地址(同上);
- sa:位移量(shift amt),移位指令用于指定移多少位;
- funct:功能码,在寄存器类型指令中(R类型)用来指定指令的功能;
- immediate:16位立即数,用作无符号的逻辑操作数、有符号的算术操作数、数据加载(Load)/数据保存(Store)指令的数据地址字节偏移量和分支指令中相对程序计数器(PC)的有符号偏移量;
- address:地址。
在执行指令的过程中,需要在不同的时钟周期之间进行状态转移:
本简易 CPU 姑且只实现以下指令:
控制单元
一个简易的多周期 CPU 的数据通路图如下:
三个 D 触发器用于保存当前状态,是时序逻辑电路,RST用于初始化状态“000“,另外两个部分都是组合逻辑电路,一个用于产生 下一个阶段的状态,另一个用于产生每个阶段的控制信号。从图上可看出,下个状态取决于 指令操作码和当前状态;而每个阶段的控制信号取决于指令操作码、当前状态和反映运算结果的状态 zero 标志和符号 sign标志。
其中指令和数据各存储在不同存储器中,即有指令存储器和数据存储器。访问存储器时,先给出内存地址,然后由读或写信号控制操作。对于寄存器组, 给出寄存器地址(编号),读操作时不需要时钟信号,输出端就直接输出相应数据;而在写操作时,在 WE使能信号为 1时,在时钟边沿触发将数据写入寄存器。
IR 指令寄存器目的是使指令代码保持稳定,PC 写使能控制信号PCWre,是确保PC 适时修改,原因都是和多周期工作的CPU有关。ADR、BDR、 ALUoutDR、DBDR四个寄存器不需要写使能信号,其作用是切分数据通路,将大组合逻辑切分为若干个小组合逻辑,大延迟变为多个分段小延迟。
各控制信号功能如下:
相关部件及引脚说明
Instruction Memory:指令存储器
- Iaddr,指令地址输入端口
- DataIn,存储器数据输入端口
- DataOut,存储器数据输出端口
- RW,指令存储器读写控制信号,为0 写,为 1读
Data Memory:数据存储器
- Daddr,数据地址输入端口
- DataIn,存储器数据输入端口
- DataOut,存储器数据输出端口
- /RD,数据存储器读控制信号,为 0 读
- /WR,数据存储器写控制信号,为0 写
Register File:寄存器组
- Read Reg1,rs 寄存器地址输入端口
- Read Reg2,rt 寄存器地址输入端口
- Write Reg,将数据写入的寄存器,其地址输入端口(rt、rd)
- Write Data,写入寄存器的数据输入端口
- Read Data1,rs 寄存器数据输出端口
- Read Data2,rt 寄存器数据输出端口
- WE,写使能信号,为1 时,在时钟边沿触发写入
IR: 指令寄存器,用于存放正在执行的指令代码
ALU: 算术逻辑单元
- result,ALU运算结果
- zero,运算结果标志,结果为 0,则 zero=1;否则 zero=0
- sign,运算结果标志,结果最高位为0,则 sign=0,正数;否则,sign=1,负数
ALU
ALU 为算术逻辑运算单元,功能如下:
模块设计
符号定义
为了更加明晰程序代码,并避免因二进制代码书写错误导致的问题,对状态码、操 作码等做出如下定义:
`define ALU_OP_ADD 3'b000
`define ALU_OP_SUB 3'b001
`define ALU_OP_SLL 3'b010
`define ALU_OP_OR 3'b011
`define ALU_OP_AND 3'b100
`define ALU_OP_LT 3'b101
`define ALU_OP_SLT 3'b110
`define ALU_OP_XOR 3'b111
`define OP_ADD 6'b000000
`define OP_SUB 6'b000001
`define OP_ADDIU 6'b000010
`define OP_AND 6'b010000
`define OP_ANDI 6'b010001
`define OP_ORI 6'b010010
`define OP_XORI 6'b010011
`define OP_SLL 6'b011000
`define OP_SLTI 6'b100110
`define OP_SLT 6'b100111
`define OP_SW 6'b110000
`define OP_LW 6'b110001
`define OP_BEQ 6'b110100
`define OP_BNE 6'b110101
`define OP_BLTZ 6'b110110
`define OP_J 6'b111000
`define OP_JR 6'b111001
`define OP_JAL 6'b111010
`define OP_HALT 6'b111111
`define PC_NEXT 2'b00
`define PC_REL_JUMP 2'b01
`define PC_REG_JUMP 2'b10
`define PC_ABS_JUMP 2'b11
`define STATE_IF 3'b000
`define STATE_ID 3'b001
`define STATE_EXE_AL 3'b110
`define STATE_EXE_BR 3'b101
`define STATE_EXE_LS 3'b010
`define STATE_MEM 3'b011
`define STATE_WB_AL 3'b111
`define STATE_WB_LD 3'b100
控制单元
状态转移
always @(posedge CLK or negedge RST) begin
if (!RST) State <= `STATE_IF;
else begin
case (State)
`STATE_IF: State <= `STATE_ID;
`STATE_ID: begin
case (OpCode)
`OP_ADD, `OP_SUB, `OP_ADDIU, `OP_AND, `OP_ANDI, `OP_ORI,
`OP_XORI, `OP_SLL, `OP_SLTI, `OP_SLT: State <= `STATE_EXE_AL;
`OP_BNE, `OP_BEQ, `OP_BLTZ: State <= `STATE_EXE_BR;
`OP_SW, `OP_LW: State <= `STATE_EXE_LS;
`OP_J, `OP_JAL, `OP_JR, `OP_HALT: State <= `STATE_IF;
default: State <= `STATE_EXE_AL;
endcase
end
`STATE_EXE_AL: State <= `STATE_WB_AL;
`STATE_EXE_BR: State <= `STATE_IF;
`STATE_EXE_LS: State <= `STATE_MEM;
`STATE_WB_AL: State <= `STATE_IF;
`STATE_MEM: begin
case (OpCode)
`OP_SW: State <= `STATE_IF;
`OP_LW: State <= `STATE_WB_LD;
endcase
end
`STATE_WB_LD: State <= `STATE_IF;
default: State <= `STATE_IF;
endcase
end
end
控制信号
不同控制信号根据不同的操作码得到,因此可以列出对于不同操作码的各控制信号的真值表:
控制信号不仅仅取决于操作码,还取决于当前的状态。各控制信号实现如下:
ALUSrcA:EXE 阶段 LS、SLL
ALUSrcA = ((State == `STATE_EXE_AL || State == `STATE_EXE_BR || State == `STATE_EXE_LS) && OpCode == `OP_SLL) ? 1 : 0;
ALUSrcB:EXE 阶段 ADDIU、ANDI、ORI、XORI、SLTI、LW、SW
ALUSrcB = ((State == `STATE_EXE_AL || State == `STATE_EXE_BR || State == `STATE_EXE_LS) && (OpCode == `OP_ADDIU || OpCode == `OP_ANDI || OpCode == `OP_ORI || OpCode == `OP_XORI || OpCode == `OP_SLTI || OpCode == `OP_LW || OpCode == `OP_SW)) ? 1 : 0;
RegWre:ID 阶段 JAL,或 WB 阶段 LD
RegWre = ((State == `STATE_ID && OpCode == `OP_JAL) || (State == `STATE_WB_AL || State == `STATE_WB_LD)) ? 1 : 0;
WrRegDSrc:ID 阶段 JAL
WrRegDSrc = (State == `STATE_ID && OpCode == `OP_JAL) ? 0 : 1;
mRD:MEM 或 WB 阶段 LW
mRD = ((State == `STATE_MEM || State == `STATE_WB_LD) && OpCode == `OP_LW) ? 1 : 0;
mWR:MEM 阶段 SW
mWR = (State == `STATE_MEM && OpCode == `OP_SW) ? 1 : 0;
IRWre:IF 阶段
IRWre = (State == `STATE_IF) ? 1 : 0;
ExtSel:EXE 阶段 ANDI、ORI、XORI
ExtSel = ((State == `STATE_EXE_AL || State == `STATE_EXE_BR || State == `STATE_EXE_LS) && (OpCode == `OP_ANDI || OpCode == `OP_ORI || OpCode == `OP_XORI)) ? 0 : 1;
PCSrc:IF 或 ID 阶段 JR 为 PC_REG_JUMP,IF 或 ID 阶段 J、JAL 为 PC_ABS_JUMP,EXE 阶段 BEQ、BNE、BLTZ 为 PC_REL_JUMP,否则均为 PC_NEXT
if ((State == `STATE_IF || State == `STATE_ID) && OpCode == `OP_JR) PCSrc = `PC_REG_JUMP;
else if ((State == `STATE_IF || State == `STATE_ID) && (OpCode == `OP_J || OpCode == `OP_JAL)) PCSrc = `PC_ABS_JUMP;
else if ((State == `STATE_EXE_AL || State == `STATE_EXE_BR || State == `STATE_EXE_LS) && (OpCode == `OP_BEQ && Zero) || (OpCode == `OP_BNE && !Zero) || (OpCode == `OP_BLTZ && Sign)) PCSrc = `PC_REL_JUMP;
else PCSrc = `PC_NEXT;
RegDst:ID 阶段 JAL 为 b00,WB 阶段 ADDIU、ANDI、ORI、XORI、SLTI、LW 为 b01,否则均为 b10
if (State == `STATE_ID && OpCode == `OP_JAL) RegDst = 2'b00;
else if ((State == `STATE_WB_AL || State == `STATE_WB_LD) && (OpCode == `OP_ADDIU || OpCode == `OP_ANDI || OpCode == `OP_ORI || OpCode == `OP_XORI || OpCode == `OP_SLTI || OpCode == `OP_LW)) RegDst = 2'b01;
else RegDst = 2'b10;
ALUOp:根据真值表即可得出
case (OpCode)
`OP_ADD, `OP_ADDIU, `OP_SW, `OP_LW: ALUOp = `ALU_OP_ADD;
`OP_SUB, `OP_BEQ, `OP_BNE, `OP_BLTZ: ALUOp = `ALU_OP_SUB;
`OP_SLL: ALUOp = `ALU_OP_SLL;
`OP_ORI: ALUOp = `ALU_OP_OR;
`OP_AND, `OP_ANDI: ALUOp = `ALU_OP_AND;
`OP_SLTI, `OP_SLT: ALUOp = `ALU_OP_SLT;
`OP_XORI: ALUOp = `ALU_OP_XOR;
endcase
PCWre:ID 阶段 J、JAL、JR,或 EXE 阶段 BEQ、BNE、BLTZ,或 MEM 阶段 SW,或 WB 阶段。另外,为保证在每条指令最初阶段的时钟上升沿 PC 发生改变,需要在上一条指令的最后一个下降沿将 PCWre 设置为 1,这样才能保证 PC 在每条指令最开始的时钟上升沿改变。
always @(negedge CLK) begin
case (State)
`STATE_ID: begin
if (OpCode == `OP_J || OpCode == `OP_JAL || OpCode == `OP_JR) PCWre <= 1;
end
`STATE_EXE_AL, `STATE_EXE_BR, `STATE_EXE_LS: begin
if (OpCode == `OP_BEQ || OpCode == `OP_BNE || OpCode == `OP_BLTZ) PCWre <= 1;
end
`STATE_MEM: begin
if (OpCode == `OP_SW) PCWre <= 1;
end
`STATE_WB_AL, `STATE_WB_LD: PCWre <= 1;
default: PCWre <= 0;
endcase
end
逻辑算术运算单元
该模块是一个32位的ALU单元,会根据控制信号对输入的操作数进行不同的运算,例如加、减、与、或等。
module ALU(
input [2:0] ALUOp,
input [31:0] A,
input [31:0] B,
output Sign,
output Zero,
output reg [31:0] Result
);
always @(*) begin
case (ALUOp)
`ALU_OP_ADD: Result = (A + B);
`ALU_OP_SUB: Result = (A - B);
`ALU_OP_SLL: Result = (B << A);
`ALU_OP_OR: Result = (A | B);
`ALU_OP_AND: Result = (A & B);
`ALU_OP_LT: Result = (A < B) ? 1 : 0;
`ALU_OP_SLT: Result = (((A < B) && (A[31] == B[31])) || ((A[31] && !B[31]))) ? 1 : 0;
`ALU_OP_XOR: Result = (A ^ B);
endcase
$display("[ALU] calculated result [%h] from a = [%h] aluOpCode = [%b] b = [%h]", Result, A, ALUOp, B);
end
assign Zero = (Result == 0) ? 1 : 0;
assign Sign = Result[31];
endmodule
寄存器组
该模块为一个32位而拥有32个寄存的寄存器组。寄存器组接受 InstructionMemory 的输入,输出对应寄存器的数据,从而实现读取寄存器里的数据的功能。
module RegisterFile(
input CLK,
input RST,
input WE,
input [4:0] ReadReg1,
input [4:0] ReadReg2,
input [4:0] WriteReg,
input [31:0] WriteData,
output [31:0] ReadData1,
output [31:0] ReadData2
);
reg [31:0] register[1:31];
integer i;
assign ReadData1 = ReadReg1 == 0 ? 0 : register[ReadReg1];
assign ReadData2 = ReadReg2 == 0 ? 0 : register[ReadReg2];
always @(negedge CLK or negedge RST) begin
if (!RST) begin
for (i = 1; i < 32; i = i + 1) begin
register[i] = 0;
end
end
else if (WE && WriteReg) begin
register[WriteReg] <= WriteData;
$display("[RegisterFile] wrote data [%h] into reg $[%d]", WriteData, WriteReg);
end
end
endmodule
符号扩展单元
该组件有两个功能:符号扩展和零扩展,输入的扩展方法和待扩展的数据,输出扩展后的数据。
module SignZeroExtend(
input ExtSel, // 0 - 0 extend, 1 - sign extend
input [15:0] Immediate,
output [31:0] DataOut
);
assign DataOut[15:0] = Immediate[15:0];
assign DataOut[31:16] = (ExtSel && Immediate[15]) ? 16'hFFFF : 16'h0000;
endmodule
指令存储器
把指令集以二进制的形式写成一个文件,然后在指令存储器中读进来,以读文件的方式把指令存储到内存中,实现指令的读取。
module InstructionMemory(
input RW,
input [31:0] IAddr,
output reg [31:0] DataOut
);
reg [7:0] memory[0:95];
initial begin
$readmemb(`MEMORY_FILE_PATH, memory);
end
always @(IAddr or RW) begin
if (RW) begin
DataOut[31:24] = memory[IAddr];
DataOut[23:16] = memory[IAddr + 1];
DataOut[15:8] = memory[IAddr + 2];
DataOut[7:0] = memory[IAddr + 3];
$display("[InstructionMemory] Loaded instruction [%h] from address [%h]", DataOut, IAddr);
end
end
endmodule
数据存储单元
数据存储单元负责存取数据,且由时钟下降沿出发写操作。实现为1字节8位的大端方式存储。
module DataMemory(
input CLK,
input mRD,
input mWR,
input [31:0] DAddr,
input [31:0] DataIn,
output [31:0] DataOut
);
reg [7:0] memory[0:127];
assign DataOut[7:0] = mRD ? memory[DAddr + 3] : 8'bz;
assign DataOut[15:8] = mRD ? memory[DAddr + 2] : 8'bz;
assign DataOut[23:16] = mRD ? memory[DAddr + 1] : 8'bz;
assign DataOut[31:24] = mRD ? memory[DAddr] : 8'bz;
always @(negedge CLK) begin
if (mWR) begin
memory[DAddr] <= DataIn[31:24];
memory[DAddr + 1] <= DataIn[23:16];
memory[DAddr + 2] <= DataIn[15:8];
memory[DAddr + 3] <= DataIn[7:0];
$display("[DataMemory] saved data [%h] into address [%h]", DataIn, DAddr);
end
end
endmodule
程序计数器
在时钟上升沿处给出下条指令的地址,或在重置信号下降沿处将PC归零。
PC的下一条指令可能是当前 PC+4,也可能是跳转指令地址,还有可能因为停机而不变。 因此还需要设计一个选择器来选择下一条指令地址的计算方式,为此创建了 JumpPCHelper用于计算 j 指令的下一条 PC 地址,和 NextPCHelper 用于根据指令选择不同的计算方式。
module PC(
input CLK,
input RST,
input PCWre,
input [31:0] PCAddr,
output reg [31:0] NextPCAddr
);
initial NextPCAddr = 0;
always @(posedge CLK or negedge RST) begin
if (!RST) NextPCAddr <= 0;
else if (PCWre || !PCAddr) NextPCAddr <= PCAddr;
end
endmodule
module JumpPCHelper(
input [31:0] PC,
input [25:0] NextPCAddr,
output reg [31:0] JumpPC);
wire [27:0] tmp;
assign tmp = NextPCAddr << 2; // address * 4
always @(*) begin
JumpPC[31:28] = PC[31:28];
JumpPC[27:2] = tmp[27:2];
JumpPC[1:0] = 0;
end
endmodule
module NextPCHelper(
input RST,
input [1:0] PCSrc,
input [31:0] PC,
input [31:0] Immediate,
input [31:0] RegPC,
input [31:0] JumpPC,
output reg [31:0] NextPC);
always @(RST or PCSrc or PC or Immediate or RegPC or JumpPC) begin
if (!RST) NextPC = PC + 4;
else begin
case (PCSrc)
`PC_NEXT: NextPC = PC + 4;
`PC_REL_JUMP: NextPC = PC + 4 + (Immediate << 2);
`PC_REG_JUMP: NextPC = RegPC;
`PC_ABS_JUMP: NextPC = JumpPC;
default: NextPC = PC + 4;
endcase
end
end
endmodule
选择器
数据选择,用于数据存储单元之后的选择,这里需要二选一和三选一数据选择器。
module Selector1In2#(
parameter WIDTH = 5
)(
input Sel,
input [WIDTH-1:0] A,
input [WIDTH-1:0] B,
output [WIDTH-1:0] Y);
assign Y = Sel ? B : A;
endmodule
module Selector1In3#(
parameter WIDTH = 5
)(
input [1:0] Sel,
input [WIDTH-1:0] A,
input [WIDTH-1:0] B,
input [WIDTH-1:0] C,
output reg [WIDTH-1:0] Y);
always @(Sel or A or B or C) begin
case (Sel)
2'b00: Y <= A;
2'b01: Y <= B;
2'b10: Y <= C;
default: Y <= 0;
endcase
end
endmodule
指令寄存器
用时钟信号 CLK 驱动,采用边缘触发写入指令二进制码。
module IR(
input CLK,
input IRWre,
input [31:0] DataIn,
output reg [31:0] DataOut
);
always @(posedge CLK) begin
if (IRWre) begin
DataOut <= DataIn;
end
end
endmodule
数据延迟处理
这部分模块用于切割数据通路。
module XDR(
input CLK,
input [31:0] DataIn,
output reg [31:0] DataOut
);
always @(negedge CLK) DataOut <= DataIn;
endmodule
CPU
有了以上各个模块,一个简单的 CPU 基本就完成了,最后再将他们串起来即可。
完结撒花。