MIPS-单周期CPU设计
设计一个单周期CPU,该CPU至少能实现以下指令功能操作。需设计的指令与格式如下:
实验原理
单周期CPU指的是一条指令的执行在一个时钟周期内完成,然后开始下一条指令的执行,即一条指令用一个时钟周期完成。电平从低到高变化的瞬间称为时钟上升沿,两个相邻时钟上升沿之间的时间间隔称为一个时钟周期。时钟周期一般也称振荡周期(如果晶振的输出没有经过分频就直接作为CPU的工作时钟,则时钟周期就等于振荡周期。若振荡周期经二分频后形成时钟脉冲信号作为CPU的工作时钟,这样,时钟周期就是振荡周期的两倍。) CPU在处理指令时,一般需要经过以下几个步骤:
(1) 取指令(IF):根据程序计数器PC中的指令地址,从存储器中取出一条指令,同时,PC根据指令字长度自动递增产生下一条指令所需要的指令地址,但遇到“地址转移”指令时,则控制器把“转移地址”送入PC,当然得到的“地址”需要做些变换才送入PC。
(2) 指令译码(ID):对取指令操作中得到的指令进行分析并译码,确定这条指令需要完成的操作,从而产生相应的操作控制信号,用于驱动执行状态中的各种操作。
(3) 指令执行(EXE):根据指令译码得到的操作控制信号,具体地执行指令动作,然后转移到结果写回状态。
(4) 存储器访问(MEM):所有需要访问存储器的操作都将在这个步骤中执行,该步骤给出存储器的数据地址,把数据写入到存储器中数据地址所指定的存储单元或者从存储器中得到数据地址单元中的数据。
(5) 结果写回(WB):指令执行的结果或者访问存储器中得到的数据写回相应的目的寄存器中。
单周期CPU,是在一个时钟周期内完成这五个阶段的处理。
实验器材
电脑一台、Xilinx ISE 软件一套。
实验分析与设计
1、 根据数据通路图,将整个cpu设计分为六个模块。
分别为指令读取模块,控制单元,指令寄存器,扩展模块,运算模块和数据存储器。用一个顶层代码将cpucode.v将所有的模块统一管理,还有必要的wire线声明。
写完这几行代码后ctrl+s就会自动生成子模块,add source即可,记得命名一致。
//操作码给ControlUnit,zero用来bne和beq指令的判断跳转
ControlUnit controlunit (operation, zero, PCWre, ALUSrcB, ALUM2Reg, RegWre, InsMemRW, DataMemRW, ExtSel, PCSrc, RegOut,ALUOp);
RegisterFile registerfile (rs, rt, rd, write_data, RegWre, RegOut,clk,readData1,readData2);
Extend extend(immediate_16, ExtSel, immediate_32);
ALU alu(readData1, readData2, immediate_32, ALUSrcB, ALUOp, zero, result);
DataSaver datasaver(result, readData2, DataMemRW, ALUM2Reg, write_data);
除此之外,我将pc模块直接写在了主模块中,因为pc的操作涉及到初始化,所以分模块对于变量的修改会比较麻烦一些。
//执行下一条指令
always@(posedge clk) begin
if ( PCWre == 1)
PC <= (PCSrc == 0)? PC + 4 : PC + 4 + immediate_32 * 4;
else
PC <= PC;
end
initial begin
PC = 0;
clk = 0;
end
always #500
clk = ~clk;
2、 对于instructionSave模块,只需要申请一块mem空间,将指令读入并存储起来即可。并在PC改变的时候将新的指令赋值给指令线。
注意:由于mem是直接拿到一条指令的,所以PC加4不对,为了解决这个问题,将PC**右移两位**即可。
module InstructionSave(
input [31:0] PC,
output reg [31:0] instruction
);
reg [31:0] mem [0:64];
initial begin
$readmemb("my_test_rom.txt", mem);
end
always@(PC) begin
instruction <= mem[PC >> 2];
//$display("the instruction now is %d", instruction);
end
endmodule
3、 对于controlUnit模块,要负责产生信号。对各个不同的操作数要产生不同的信号值。
首先我们先列出各个信号各个值的作用。
接着列出各个指令下(操作码)应该有的值。
最初分析肯定是传入参数直接赋值。后来经过同学指点,发现有更简单的方法:如下
always@(operation) begin
//初始化这些变量
i_add = 0;
i_addi = 0;
i_sub = 0;
i_ori = 0;
i_and = 0;
i_or = 0;
i_move = 0;
i_sw = 0;
i_lw = 0;
i_beq = 0;
i_halt = 0;
//如果是对应操作码则将对应变量名置为1
case(operation)
ADD: i_add = 1;
ADDI: i_addi = 1;
SUB: i_sub = 1;
ORI: i_ori = 1;
AND: i_and = 1;
OR: i_or = 1;
MOVE: i_move = 1;
SW: i_sw = <