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
endmodule3、 对于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 = 1;
LW: i_lw = 1;
BEQ: i_beq = 1;
HALT: i_halt = 1;
endcase
end
//有点类似数字电路中学到的真值表。将各个信号直接用操作码的变量表示。
assign PCWre = !i_halt;
assign ALUSrcB = i_addi || i_ori || i_sw || i_lw;
assign ALUM2Reg = i_lw;
assign RegWre = !(i_sw || i_beq);
assign InsMemRW = 0;
assign DataMemRW = i_sw;
assign ExtSel = !i_ori; //除了ori是0扩展,其他的也可以为符号扩展
assign PCSrc = (i_beq && zero); //beq且相减之后值为0
assign RegOut = !(i_addi || i_ori || i_lw);
assign ALUOp = {i_and, i_ori || i_or, i_sub || i_ori || i_or || i_beq};即用或操作进行管理,减少了许多重复的代码量。
4、 对于registerFile模块,根据老师讲课给的寄存器行为代码稍加修改即可。
实现取数,存数的功能
module RegisterFile(
input [4:0] rs,rt,rd,
input [31:0] write_data,
input RegWre,RegOut,clk,
output [31:0] readData1,readData2
);
// integer i;
wire [4:0] rin;
assign rin = (RegOut == 0) ? rt : rd;
//声明31个寄存器,不需要0号寄存器因为后面会判断
reg [31:0] register [1:31]; // r1 - r31
integer i;
initial begin
for (i = 0; i < 32; i = i + 1)
register[i] = 0;
end
//0号寄存器值固定为0
assign readData1 = (rs == 0)? 0 : register[rs]; // 取数
assign readData2 = (rt == 0)? 0 : register[rt]; // 取数
//当时钟上升沿到来,将计算得到的值或者拿到的值存入指定寄存器
always @(posedge clk) begin
if ((rin != 0) && (RegWre == 1)) begin // RegWre==1时写入
register[rin] <= write_data;
end
end
endmodule5、 对于extend模块,较为简单,甚至可以一句话说清。三种情况,ExtSel == 0 零扩展, == 1,符号扩展
assign immediate_32 = (ExtSel)? {{16{immediate_16[15]}}, immediate_16[15:0]} : {{16{1'b0}}, immediate_16[15:0]};
6、 对于alu模块
要根据输入的ALUop进行计算,初次之外,如果计算结果为0,要将zero变量置1,因为要考虑beq的影响。Beq是相当于减法,如果zero等于1且操作码为beq时会进行跳转。
wire [31:0] alub;
//ALUSrcB == 1 要计算立即数
assign alub = (ALUSrcB == 0) ? readData2 : immediate_32;
always@( readData1 or alub or ALUOp) begin
$display("here");
case (ALUOp)
3'b000: result <= readData1 + alub;
3'b001: result <= readData1 - alub;
3'b010: result <= alub - readData1;
3'b011: result <= readData1 | alub;
3'b100: result <= readData1 & alub;
3'b101: result <= ~readData1 & alub;
3'b110: result <= (~readData1 & alub) | (readData1 & ~alub);
3'b111: result <= (readData1 & alub) | (~readData1 & ~alub);
endcase
end
assign zero = (result == 0) ? 1 : 0;7、对于DataSave数据存储模块,有存储数据,获得数据,以及选择是运算输出还是lw输出几个功能。
always@(result or DataMemRW) begin
if (DataMemRW == 0) //lw
DataOut = DataMem[result];
else //sw
DataMem[result] = readData2;
end
// == 0 是alu运算输出, == 1是数据存储输出 数据选择器
assign write_data = (ALUM2Reg == 0) ? result : DataOut;至此,模块分析完毕,下面是数据测试。
编写简单的测试程序段:
my_rom_test.txt 如下:
0000000000000000000000000000000
00000100000000010000000000000100
00000100000000100000000000001000
10011000010000100000000000000000
00000000010000010001100000000000
00001000011000010001100000000000
11000000010000111111111111111110
01000000001000010000000000000001
01001000010000010001100000000000
10000000010000000001100000000000
01000100011000100000100000000000
10011100010001000000000000000000
11111100000000000000000000000000
下面对每条指令进行分析:选中cpucode.v,右键仿真运行,Memory可查看寄存器的值
实验心得
问题一、不理解wire reg 这些的具体用法,经常出现语法错误:
解决:wire是线,在某一模块定义后,可传入其子模块。当变量改变时能够互相传递。wire在该定义模块不能作为表达式的左值。reg 则可以作为左值。在需要修改变量的时候要用reg声明。如将wire传到子模块后,需要修改值可以用output reg …
问题二、<= 和 = 的区别?
解决:<= 是非阻塞式赋值, = 是阻塞式赋值。当 = 这个赋值语句执行的时候是不允许有其它语句执行的,这就是阻塞的原因。而非阻塞赋值,例如a<=b;当这个赋值语句执行的时候是不阻碍其它语句执行的。
问题三、主模块将immediate_32这个变量声明拼写错误后,传入拼写正确,但是不报错,能运行。
解决:这绝对是我debug最久的一个问题。第一个addi语句一直没法成功执行。Immediate_32在主模块的值是zzzzzzzz高阻态。而子模块extend却能够得出正确的结果。Alu拿不到值,后面的运算没法进行,整个程序都动不了。找了很久才发现变量声明错误。我只想知道为什么verilog不支持报这种语法错误。
心得:
不管怎么说花了好长时间写出第一个cpu还是相当兴奋的,虽然简单,但是通过整个过程能够理解到cpu工作的一些基本原理,再和理论课的知识结合起来,印象就十分深刻了。测试过程中一步一步地改善自己的代码,一步一步调试自己的错误。希望自己以后还是要多细心点吧,不要再犯低级的拼写错误了。verilog和之前学的语言不一样,整个并发式执行开始的时候有点难理解,整个思维都要跟着变化,现在一个简单的mips cpu下来,感觉也挺好玩的吧。受益良多。
完整代码
cpucode.v
`timescale 1ns / 1ps
`timescale 1ns / 1ps
module CPU_CPU_sch_tb();
//此模块中需要赋值的变量
wire [5:0] operation;
wire [4:0] rs;
wire [4:0] rt;
wire [4:0] rd;
wire [15:0] immediate_16;
reg clk;
wire [31:0] result;
wire [31:0] write_data;
//controlunit产生的控制信号线
wire PCWre;
wire ALUSrcB;
wire ALUM2Reg;
wire RegWre;
wire InsMemRW;
wire DataMemRW;
wire ExtSel;
wire PCSrc;
wire RegOut;
wire [2:0] ALUOp;
//其他的模块相互传递的线
wire [31:0] instruction;
reg [31:0] PC;
wire [31:0] immediate_32;
wire [31:0] readData1;
wire [31:0] readData2;
wire zero;
initial begin
PC = 0;
clk = 0;
end
always #500
clk = ~clk;
//输入PC地址,需要InsMemRW的值来确定是读指令还是写指令,得到的值存在instruction
InstructionSave instructionsave(PC, instruction);
//分解拿到的指令instruction各个组块
assign operation[5:0] = instruction[31:26];
assign rs = instruction[25:21];
assign rt = instruction[20:16];
assign rd = instruction[15:11];
assign immediate_16 = instruction[15:0];
//操作码给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);
//执行下一条指令
always@(posedge clk) begin
if ( PCWre == 1)
PC <= (PCSrc == 0)? PC + 4 : PC + 4 + immediate_32 * 4;
else
PC <= PC;
end
endmodule
instructionSave.v
`timescale 1ns / 1ps
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
controlUnit.v
`timescale 1ns / 1ps
module ControlUnit(
input [5:0] operation,
input zero,
output PCWre,
output ALUSrcB,
output ALUM2Reg,
output RegWre,
output InsMemRW,
output DataMemRW,
output ExtSel,
output PCSrc,
output RegOut,
output [2:0] ALUOp
);
parameter ADD = 6'b000000, ADDI = 6'b000001, SUB = 6'b000010, ORI = 6'b010000,
AND = 6'b010001, OR = 6'b010010, MOVE = 6'b100000, SW = 6'b100110,
LW = 6'b100111, BEQ = 6'b110000, HALT = 6'b111111;
reg i_add, i_addi, i_sub, i_ori, i_and, i_or, i_move, i_sw, i_lw, i_beq, i_halt;
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 = 1;
LW: i_lw = 1;
BEQ: i_beq = 1;
HALT: i_halt = 1;
endcase
end
//有点类似数字电路中学到的真值表。将各个信号直接用操作码的变量表示。
assign PCWre = !i_halt;
assign ALUSrcB = i_addi || i_ori || i_sw || i_lw;
assign ALUM2Reg = i_lw;
assign RegWre = !(i_sw || i_beq);
assign InsMemRW = 0;
assign DataMemRW = i_sw;
assign ExtSel = !i_ori; //除了ori是0扩展,其他的也可以为符号扩展
assign PCSrc = (i_beq && zero); //beq且相减之后值为0
assign RegOut = !(i_addi || i_ori || i_lw);
assign ALUOp = {i_and, i_ori || i_or, i_sub || i_ori || i_or || i_beq};
endmodule
registerFile.v
`timescale 1ns / 1ps
module RegisterFile(
input [4:0] rs,rt,rd,
input [31:0] write_data,
input RegWre,RegOut,clk,
output [31:0] readData1,readData2
);
// integer i;
wire [4:0] rin;
assign rin = (RegOut == 0) ? rt : rd;
//声明31个寄存器,不需要0号寄存器因为后面会判断
reg [31:0] register [1:31]; // r1 - r31
integer i;
initial begin
for (i = 0; i < 32; i = i + 1)
register[i] = 0;
end
//0号寄存器值固定为0
assign readData1 = (rs == 0)? 0 : register[rs]; // 取数
assign readData2 = (rt == 0)? 0 : register[rt]; // 取数
//当时钟上升沿到来,将计算得到的值或者拿到的值存入指定寄存器
always @(posedge clk) begin
if ((rin != 0) && (RegWre == 1)) begin // RegWre==1时写入
register[rin] <= write_data;
end
end
endmodule
extend.v
`timescale 1ns / 1ps
module Extend(
input [15:0] immediate_16,
input ExtSel,
output [31:0]immediate_32
);
assign immediate_32 = (ExtSel)? {{16{immediate_16[15]}}, immediate_16[15:0]} : {{16{1'b0}}, immediate_16[15:0]};
endmodule
alu.v
module ALU(
input [31:0] readData1,
input [31:0] readData2,
input [31:0] immediate_32,
input ALUSrcB,
input [2:0] ALUOp,
output wire zero,
output reg [31:0] result
);
wire [31:0] alub;
//ALUSrcB == 1 要计算立即数
assign alub = (ALUSrcB == 0) ? readData2 : immediate_32;
always@( readData1 or alub or ALUOp) begin
$display("here");
case (ALUOp)
3'b000: result <= readData1 + alub;
3'b001: result <= readData1 - alub;
3'b010: result <= alub - readData1;
3'b011: result <= readData1 | alub;
3'b100: result <= readData1 & alub;
3'b101: result <= ~readData1 & alub;
3'b110: result <= (~readData1 & alub) | (readData1 & ~alub);
3'b111: result <= (readData1 & alub) | (~readData1 & ~alub);
endcase
end
assign zero = (result == 0) ? 1 : 0;
endmoduledatasaver.v
module DataSaver(
input [31:0] result,
input [31:0] readData2,
input DataMemRW,
input ALUM2Reg,
output [31:0] write_data
);
reg [31:0] DataMem [0:63];
reg [31:0] DataOut;
//初始化
integer i;
initial begin
for (i = 0; i < 64; i = i + 1)
DataMem[i] = 0;
end
//通过判断DataMemRW的值来判断要进行读操作还是写操作
always@(result or DataMemRW) begin
if (DataMemRW == 0) //lw
DataOut = DataMem[result];
else //sw
DataMem[result] = readData2;
end
// == 0 是alu运算输出, == 1是数据存储输出 数据选择器
assign write_data = (ALUM2Reg == 0) ? result : DataOut;
endmodule