单周期CPU

 biubiu     2019-12-17   33181 words    & views

一. 实验目的

1、理解 MIPS 常用的指令系统并掌握单周期 CPU 的工作原理与逻辑功能实现。

2、通过对单周期CPU 的运行状况进行观察和分析,进一步加深理解。

二. 实验内容

利用 HDL 语言,基于 Xilinx FPGA basys3 实验平台,用 Verilog HDL 语言或 VHDL 语言来编写,实现单周期CPU 的设计,这个单周期 CPU 至少能够完成 20 条MIPS 指令,至少包含以下指令:

  • 支持基本的内存操作如 lw,sw 指令;
  • 支持基本的算术逻辑运算如 add,sub,and,ori,slt,addi 指令;
  • 支持基本的程序控制如 beq,j 指令;

并将其中的 alu 运算结果在开发板数码管上显示出来。

三. 实验原理

单周期CPU在每一个时钟周期会读取并执行一条指令,然后开始下一条指令的执行。时钟周期根据时钟边沿控制,两个时钟上升沿之间的时间间隔为一个时钟周期,在每一个时钟周期,CPU执行如下操作:

  1. 取指令IF:根据程序计数器PC中的指令地址,从指令存储器中取出一条指令,并且指令自增为PC+4,等待下一步操作;如果有跳转或者分支指令,PC指针也会随之修改。
  2. 译码ID:对上一步中得到的指令进行译码,确定这条指令需要完成的操作,从而产生相应的控制信号,并得到指令所访问的寄存器的位置。
  3. 执行EX:根据指令译码得到的控制信号,进行算术逻辑运算。
  4. 访问存储器MEN:根据数据存储器的数据地址,把数据写入到对应的存储单元或者从存储器中的存储单元中取出数据。
  5. 写回寄存器WB:将指令执行的结果或者数据存储器中得到的数据写回相应的目的寄存器中。

单周期CPU包含控制单元、程序计数器PC、指令存储器、数据存储器、ALU、寄存器堆、加法器与多个复用器,数据通路图如下:

具体设计时,我们使用模块化的方法,对控制单元、存储器、ALU、寄存器堆等各个部分作为各个模块分别进行设计,再利用一个顶层文件将其组合起来,模块化的结构便于各个部件的设计与检查。

我们设计的单周期CPU所执行的MIPS指令,是长度为32 bit的指令格式,分为R型、J型、I型三种类型:

R类型

31-26 25-21 20-16 15-11 10-6 5-0
op rs rt rd shamt func
操作码 第1个源操作数寄存器 第2个源操作数寄存器 目的操作数寄存器 位移量 功能码
6位 5位 5位 5位 5位 6位

I类型

31-26 25-21 20-16 15-0
op rs rt immediate
操作码 第1个源操作数寄存器 第2个源操作数寄存器 16位立即数
6位 5位 5位 16位

J类型

31-26 25-0
op address
操作码 26位地址
6位 26位

四. 实验器材

电脑一台,Xilinx Vivado 软件一套,Basys3板一块。

五. 实验过程与结果

1、组件构成

各个模块的构成如下:

模块名称 模块功能
top 顶层模块,连接各个模块组件,管理其输入输出信号,并进行程序计数器PC运算。
ctr 控制单元模块,通过解析指令op段得到控制信号,将其输出。
ALUctr ALU控制模块,通过解析指令funct段与控制信号aluOP,得出alu操作码。
ALU ALU模块,根据alu操作码进行算数逻辑运算,输出结果与零标志位。
Imem 指令存储器模块,根据输入的PC地址读取对应的指令并输出,传递给其他模块进行后续处理。
Dmem 数据存储器模块,使用256x8的模式模拟内存,用于内存读写指令的实现。
regFile 寄存器堆模块,用于存储32个寄存器,根据指令对寄存器进行读写操作。
Shifter 移位模块,用于处理移位指令,得到移位结果。
signext 符号扩展模块,用于立即数扩展。
display 数码管显示模块,用于将ALU结果/移位结果译码并显示到四位数码管上。

2、各模块实现

(1)ctr.v:

输入信号:
信号 功能
opCode 指令op段,第31-26位。
funct 指令func段,第5-0位。
控制信号表(输出信号):

控制单元模块主要得出以下控制信号,各控制信号的功能和相关指令如下表所示:

控制信号 功能 相关指令
regDst 写寄存器的地址,状态0时写寄存器为rt,状态1时写寄存器为rd。  
aluSrcA ALU的第二个输入,状态0时为寄存器rt,状态1时为扩展的立即数。  
memToReg 写回寄存器的值,状态0时为ALU运算结果,状态1时为从内存取出的值。  
regWrite 是否写回寄存器,状态0时写入寄存器无效,状态1时寄存器堆写使能。  
memRead 是否从内存读取数据,状态0时读取无效,状态1时内存读取有效。  
memWrite 是否将数据写入内存,状态0时写入无效,状态1时内存写入有效。  
imm_expand 立即数扩展方式,状态0时为0扩展,状态1时为符号扩展。  
branch 是否分支指令。  
aluop ALU控制信号,3位,用于与funct段结合得出ALU操作码。  
jmp 是否跳转指令。  
jal 是否跳转链接指令,  
jr 是否跳转寄存器指令。  
lui 是否立即数高位取指令。  
shift 是否移位指令  
bne 是否不等分支指令。  
bltz 是否小于0分支指令。  
bgtz 是否大于0分支指令。  
flag 内存操作字节数,2位,00-> byte,01->half word,1x->word  
代码实现:
module ctr(
    input [5:0] opCode,//
    input [5:0] funct,//
    output reg regDst,//
    output reg aluSrcA,//
    output reg [1:0] aluSrcB,//
    output reg memToReg,//
    output reg regWrite,//
    output reg memRead,//
    output reg memWrite,//
    output reg branch,//
    output reg [2:0] aluop,//
    output reg jmp,
    output reg jal,
    output reg jr,
    output reg lui,
    output reg shift,
    output reg bne,
    output reg bltz,
    output reg bgtz,
    output reg imm_expand,
    output reg [1:0] flag
);

always @(opCode,funct) 
begin
    case(opCode)
    
        6'b001001: 
        begin
            regDst = 0;    
            aluSrcA = 1'b1;//
            aluSrcB = 2'b01;//
            memToReg = 0;  //
            regWrite = 1;  //
            memRead = 0;   //
            memWrite = 0;  //
            branch = 0;    //
            aluop = 3'b000;//
            jmp = 0;
            jal=0;
            jr=0;
            lui=0; 
            bne=0;
            bltz=0;
            bgtz=0;   
            shift=0;
            imm_expand=0; 
            flag=2'b11;       
        end             
       /*
       以上为控制单元对某一条指令得出控制信号的代码示例,根据输入的opCode,funct得出各个控制信号并输出。
       */
end
endmodule

(2)aluctr.v:

引脚说明:
引脚信号 功能
ALUop 输入信号,ALU控制信号。
funct 输入信号,指令func段,第5-0位。
ALUctr 输出信号,ALU操作码,4位,用于确定不同类型的算术逻辑运算。

ALU一共会执行加、减、与、或、异或、或非、小于设置、lui(左移16位)、左移、逻辑右移、算术右移共11种操作,我们使用四位编码对不同的操作进行表示,ALUctr模块根据指令得到最终的ALU操作码,操作码将传入ALU模块中进行算术逻辑运算。

ALU功能表:

操作 ALU操作码 功能 相关指令
0010 A + B add、addi、addiu、lw、sw
0110 A - B sub、beq、bne、bgtz、bltz
0000 A & B and、andi
0001 A | B or、ori
异或 0100 A ^ B xor、xori
或非 0101 ~ ( A | B ) nor、nori
小于设置 0111 A < B slt、slti
左移16位 1100 A « 16 lui
左移 0011 A « B sllv
逻辑右移 1000 A » B srlv
算术右移 1111 (sign) A » B srav
代码实现:
module aluctr(
    input [2:0] ALUOp,
    input [5:0] funct,
    output reg [3:0] ALUCtr
);
always @(ALUOp or funct)  
    casex({ALUOp, funct}) 
        //非R型
        9'b000xxxxxx: ALUCtr = 4'b0010; // add
        9'b001xxxxxx: ALUCtr = 4'b0110; // sub
        9'b010xxxxxx: ALUCtr = 4'b0000; // and
        9'b011xxxxxx: ALUCtr = 4'b0001; // or
        9'b100xxxxxx: ALUCtr = 4'b0100; // xor
        9'b101xxxxxx: ALUCtr = 4'b0111; // slt
        9'b110xxxxxx: ALUCtr = 4'b1100; // lui
        //R型
        9'b111_100000: ALUCtr = 4'b0010; // add
        9'b111_100001: ALUCtr = 4'b0010; // addu
        9'b111_100010: ALUCtr = 4'b0110; // sub
        9'b111_100100: ALUCtr = 4'b0000; // and
        9'b111_100111: ALUCtr = 4'b0101; // nor
        9'b111_100101: ALUCtr = 4'b0001; // or
        9'b111_100110: ALUCtr = 4'b0100; // xor
        9'b111_101010: ALUCtr = 4'b0111; // slt  
                                          
        9'b111_000100: ALUCtr = 4'b0011; // sllv
        9'b111_000110: ALUCtr = 4'b1000; // srlv
        9'b111_000111: ALUCtr = 4'b1111; // srav  
        
        default:ALUCtr = 4'b0010;
    endcase
endmodule

(3)ALU.v:

引脚说明:
引脚信号 功能
input1 输入信号,第一个操作数。
input2 输入信号,第二个操作数。
ALUctr 输入信号,ALU操作码,用于确定不同类型的算术逻辑运算。
ALUres 输出信号,ALU运算结果。
Zero 输出信号,零标志位。

ALU模块用于完成算术逻辑运算,输出有运算结果与零标志,零标志主要用于beq、bne、bltz等指令的判断。

代码实现:
module alu( 
input [31:0] input1, 
input [31:0] input2, 
input [3:0] aluCtr, 
output reg[31:0] aluRes, 
output reg zero 
); 
always @(input1 or input2 or aluCtr)
begin 
case(aluCtr) 

4'b0110: // 减
begin 
  aluRes = input1 - input2; 
  if(aluRes == 0) 
  zero = 1; 
  else 
  zero = 0; 
end 
  
4'b1100: // lui
aluRes =input2 << 16;

4'b0010: // 加
aluRes = input1 + input2; 
  
4'b0100: // 异或
aluRes = input1 ^ input2; 
  
4'b0000: // 与
aluRes = input1 & input2; 
  
4'b0001: // 或
aluRes = input1 | input2; 
  
4'b0101: // 或非
aluRes = ~(input1 | input2); 
  
4'b0011: // 左移
aluRes = input1 << input2; 
  
4'b1000: // 逻辑右移
aluRes = input1 >> input2;
  
4'b1111: // 算术右移
aluRes = $signed(input1) >> input2; 
  
4'b0111: // 小于设置
begin 
  if(input1<input2) 
  aluRes = 1; 
  else
  aluRes = 0; 
end 
  
default: 
aluRes = 0; 
endcase 
end 
  
endmodule 

(4)regFile.v:

引脚说明:
引脚信号 功能
clk 输入信号,时钟信号。
reset 输入信号,重置信号。
regWriteData, 输入信号,写入写寄存器的值,32位。
regWriteAddr 输入信号,写寄存器的地址,5位。
regWriteEn 输入信号,写寄存器使能
RsAddr 输入信号,RS 寄存器地址,5位。
RtAddr 输入信号,RT 寄存器地址,5位。
RsData 输出信号,RS 段寄存器的值,32位。
RtData 输出信号,RT 段寄存器的值,32位。

寄存器堆模块,用于储存32个32位寄存器,并根据寄存器地址对寄存器组进行读写。

输入信号包括寄存器地址、数值与控制信号,其中控制信号regWriteEn控制是否写入寄存器。因为只有32个寄存器,则寄存器的地址只需要5位就可以表示。

同时,对寄存器堆的所有操作需要在时钟和复位信号控制下操作。

代码实现:
module regFile( 
	input clk, 
	input reset, 
  input [31:0] regWriteData, //写寄存器的值
  input [4:0] regWriteAddr, //写寄存器时寄存器的地址
  input regWriteEn,	//写寄存器使能
  input [4:0] RsAddr,	//RS寄存器地址
  input [4:0] RtAddr,		//RT寄存器地址
	output [31:0] RsData,	//RS寄存器的值
  output [31:0] RtData  //RT寄存器的值
);
  
reg[31:0]regs[0:31]; //寄存器组
  
//根据地址读出 Rs、Rt 寄存器数据
assign RsData = (RsAddr == 5'b0 ) ? 32'b0 : regs[RsAddr]; 
assign RtData = (RtAddr == 5'b0 ) ? 32'b0 : regs[RtAddr]; 
integer i;
  
always @( posedge clk ) //时钟上升沿操作
begin
    if(!reset) 
    begin
        if(regWriteEn == 1) //写使能信号为1时写操作
        begin
         regs[regWriteAddr] = regWriteData; //写入数据
        end
    end
    else 
    begin
         for(i = 0; i < 32; i = i + 1)
         regs[i] = 0; //重置时所有寄存器赋值为0,复位       
    end
end
endmodule

(5)Imem.xci:

引脚说明:
引脚信号 功能
clk 输入信号,时钟信号。
ena 输入信号,使能信号。
addra 输入信号,指令所在地址。
douta(inst) 输出信号,根据地址所取指令。
功能实现:

指令存储器通过IP核进行实现,设置为单通道ROM,数据宽度为32位,数据深度为32,即一共可以存储32条32位指令。

指令存储器中每一个地址为1个字节,所以每一条指令对应4个地址,一共4x8=32位;PC每次跳转到下一条指令时,都会将其地址+4;每次取出指令就是将这四个地址单元分别取出,取出后在其它模块中对指令进行分析、操作。

指令存储器的模块接口如下:

irom imem (
  .clka(clka),    // 时钟输入
  .ena(ena),      // 使能信号
  .addra(addra),  // 指令所在地址
  .douta(douta)  // 对应指令输出
);

(6)Dmem.v:

引脚说明:
引脚信号 功能
clk 输入信号,时钟信号。
reset 输入信号,重置信号。
flag 输入信号,选择内存操作的字节数。
addr 输入信号,内存地址。
we 输入信号,控制是否将数据写入内存。
re 输入信号,控制是否读取内存数据。
wd 输入信号,写入内存的数据。
memreaddata 输出信号,从内存读取的数据。

数据存储器模块, 用于模拟内存,MIPS指令中只有lw、sw指令会直接对内存进行操作,将数值存进相应地址的内存中或根据取值地址将内存数据取出。用255大小的8位寄存器数组模拟内存,地址采用小端模式。

输入信号包括内存地址、数值,和控制信号we、re分别控制写入、读取使能;而flag作为字节或字、半字操作的标志,所对应存取字大小如下:

flag 存取字大小
00 8位byte
01 16位half word
1x 32位word
代码实现:
module dram (
  input clk, 
  input we,
  input re,
  input reset,
  input [1:0] flag,//00-> byte 01->half word  1x->word
  input [7:0] addr, 
  input [31:0] wd,
  output [31:0] rd
);
			 	 
reg [7:0] RAM[255:0]; 

//read
assign rd=re && flag[1]? 
{ {RAM[a+3]},{RAM[a+2]},{RAM[a+1]},{RAM[a+0]}} : //字
( 
    re && flag[0]?
    { {16{RAM[a+1][7]}} ,{RAM[a+1]},{RAM[a]} }://半字
    { {24{RAM[a][7]}} ,RAM[a]} //字节
);

//write
integer i;
always @ (posedge clk,posedge reset)
begin
    if(reset)begin
        for(i = 0; i < 256; i = i + 1) 
            RAM[i]=0;       
    end	
    else if (we) begin
        if(flag==2'b00)//字节
        begin
            RAM[a]=wd[7:0];
        end
        else if(flag==2'b01 )//半字
        begin
            { {RAM[a+1]},{RAM[a]} }=wd[15:0];
        end
        else if(flag[1])//字
        begin
            { {RAM[a+3]},{RAM[a+2]},{RAM[a+1]},{RAM[a+0]}}=wd;
        end           
    end  
end
endmodule

(7)shifter.v:

引脚说明:
引脚信号 功能
SHIN 输入信号,操作数。
Sa 输入信号,位移量。
ShiDir 输入信号,移位方向。
Arith 输入信号,逻辑移位/算术移位。
SHOUT 输出信号,移位结果。

移位模块用于得到移位结果,实现方式较为简单。

移位指令有sll、srl、sra与sllv、srlv、srav两类,其中sllv、srlv、srav可以使用ALU实现。

但在sll、srl、sra类指令中,由于ALU的第一个操作数只设置为寄存器RS,而移位指令的两个操作数分别是RD与SHAMT,无法同时写入ALU,只能另外启用一个模块实现,并增加一个选择器,用以选择ALU运算结果或移位结果,将结果写回寄存器中。

代码实现:
module Shifter(
		input [31:0] SHIN,
		input [4:0]  Sa,//sa
		input        ShiDir,
		input        Arith,
		output reg [31:0] SHOUT
    );

	always @(*) begin
		if (!ShiDir)      SHOUT = SHIN << Sa ;//逻辑左移
		else if (!Arith) SHOUT = SHIN >> Sa;//逻辑右移
		else       SHOUT = $signed(SHIN) >>> Sa;//算数右移
	end	

endmodule

(8)signext.v:

引脚说明:
引脚信号 功能
inst 输入信号,立即数输入。
imm_expand 输入信号,立即数扩展方式。
data 输出信号,扩展后的立即数。

符号扩展模块,实现也很简单。只需根据符号与立即数扩展方式,将立即数的高位全部补1或者全部补0即可。

代码实现:
module signext( 
input [15:0] inst, // 输入16位
input imm_expand,
output [31:0] data // 输出32位
); 
// 根据符号与立即数扩展方式补充符号位
assign data = imm_expand && inst[15:15]?{16'hffff,inst}:{16'h0000,inst}; 
endmodule 

(9)display.v:

引脚说明:
引脚信号 功能
clk 输入信号,时钟信号。
data 输入信号,用于输出的数据信息。
sm_wei 输出信号,对应四位数码管的某一位。
sm_duan 输出信号,对应当前位数码管的七段码显示。

为了更好地观察实验结果,利用显示模块将运算结果显示到数码管上。将待显示的数据输入显示模块,Basys3板会根据指令显示ALU结果或移位结果。

同时,由于数码管是高频扫描显示,我们输入的时钟信号为高频的CPU时钟,而每条指令的时钟周期经过了分频,停留足够的时间让我们可以观察到,用于显示的时钟周期与CPU的时钟周期并不是相同的时钟信号

代码实现:
module display(clk,data,sm_wei,sm_duan); 

input clk;
input [15:0] data; 
output [3:0] sm_wei; 
output [6:0] sm_duan;
//分频 

integer clk_cnt;
reg clk_400Hz; 
always @(posedge clk)

if(clk_cnt==32'd100000)
begin 
    clk_cnt <= 1'b0; 
    clk_400Hz <= ~clk_400Hz;
end 
else 
clk_cnt <= clk_cnt + 1'b1; 

//位控制
 
reg [3:0]wei_ctrl=4'b1110; always @(posedge clk_400Hz)
wei_ctrl <= {wei_ctrl[2:0],wei_ctrl[3]}; 

//段控制 

reg [3:0]duan_ctrl;
always @(wei_ctrl)
case(wei_ctrl) 
4'b1110:duan_ctrl=data[3:0]; 
4'b1101:duan_ctrl=data[7:4]; 
4'b1011:duan_ctrl=data[11:8];
4'b0111:duan_ctrl=data[15:12];
default:duan_ctrl=4'hf;
 endcase
 
//解码模块 

reg [6:0]duan;
 
always @(duan_ctrl) 
case(duan_ctrl) 
4'h0:duan=7'b100_0000;//0 
4'h1:duan=7'b111_1001;//1 
4'h2:duan=7'b010_0100;//2 
4'h3:duan=7'b011_0000;//3 
4'h4:duan=7'b001_1001;//4 
4'h5:duan=7'b001_0010;//5 
4'h6:duan=7'b000_0010;//6 
4'h7:duan=7'b111_1000;//7 
4'h8:duan=7'b000_0000;//8 
4'h9:duan=7'b001_0000;//9 
4'ha:duan=7'b000_1000;//a 
4'hb:duan=7'b000_0011;//b 
4'hc:duan=7'b100_0110;//c 
4'hd:duan=7'b010_0001;//d 
4'he:duan=7'b000_0111;//e       
4'hf:duan=7'b000_1110;//f
default : 
duan = 7'b100_0000;//0
endcase

assign sm_wei = wei_ctrl; 
assign sm_duan = duan; 
endmodule

(10)top.v:

引脚说明:
引脚信号 功能
clk 输入信号,时钟信号。
reset 输入信号,复位信号。
seg 输出信号,对应当前位数码管的七段码显示。
sm_wei 输出信号,对应哪一个数码管。
   

顶层模块将整个CPU的各个部件连接起来,并实现了复用器和PC更新的功能。

实际写入板子时,使用了两个时钟信号,第一个时钟信号Clk为CPU系统时钟,频率高,使用这一时钟作为周期无法观察到结果。但Clk需要用来作为数码管显示的周期,因为四位数码管的显示为高频脉冲显示。

所以分频得到了一个时间约为1.5s的时钟信号clkin,作为单周期CPU的周期,这样一来,结果才能在数码管上成功显示。

复用器:

assign mux1 = reg_dst ? inst[15:11] : inst[20:16]; 
//得到写回寄存器rd/rt
assign mux2 = alu_srcA ? expand : RtData; 
//得到立即数/寄存器rd
assign mux6 = shift ? shiftRe : aluRes;
//得到移位结果/ALU结果
assign mux3 = memtoreg ? memreaddata : mux6; 
//得到写回寄存器的数值来自内存/来自计算结果
assign mux7 = jr ? {RsData, 2'b00} : jmpaddr;
//得到跳转地址来自寄存器/指令内26位地址
assign mux5 = jmp ? mux7 : mux4;
//得到跳转地址/分支地址
assign choose5 = bne ? ~zero : zero;
//得到bne的判断结果
assign choose4 = branch & choose5; 
//得到是否分支指令
assign mux4 = choose4 ? address : add4; 
//得到是否分支指令

PC功能:

assign jmpaddr = {add4[31:28], inst[25:0], 2'b00}; 
//指令内26位地址
assign expand2 = expand << 2; 
assign address = pc + expand2; 
//PC+4

代码实现:
module top( 
input Clk, 
input reset,
output [6:0] seg,
output [3:0] sm_wei
); 
// 指令寄存器pc 
reg[31:0] pc;
reg[31:0] add4; 
wire choose4, choose5; 

// 复用器信号线
wire[31:0] expand2, mux2, mux3, mux4, mux5,mux6,mux7,mux8,mux9, address, jmpaddr; 
wire[4:0] mux1; 
wire [31:0] inst;

// CPU控制信号线
wire reg_dst, jmp, branch, memread, memwrite, memtoreg; 
wire regwrite; 
wire[2:0] aluop; 
wire alu_srcA;

// ALU信号线
wire zero; 
wire[31:0] aluRes; 

// ALU控制信号线
wire[3:0] aluCtr;

// 内存信号线
wire[31:0] memreaddata; 

// 寄存器信号线
wire[31:0] RsData, RtData; 

// 扩展信号线
wire[31:0] expand; 

//拓展
wire jr;   
wire shift;
wire lui;
wire bne;
wire [31:0] shiftRe;
wire imm_expand;
wire [1:0] flag;

//分频得到clkin  
integer clk_cnt;
reg clkin;
always @(posedge Clk)
if(clk_cnt==32'd75_000_000) 
begin
clk_cnt <= 1'b0; 
clkin <= ~clkin;
end 
else
clk_cnt <= clk_cnt + 1'b1;

always @(negedge Clk) // 时钟下降沿操作
    begin 
    if(!reset) 
    begin
        pc = mux5; // 计算下一条pc,修改pc 
        add4 = pc + 4; 
    end 
else 
    begin
    pc = 32'b0; // 复位时pc写0 
    add4 = 32'h4; 
    end 
end 

// 实例化控制器模块
ctr mainctr( 
.opCode(inst[31:26]), 
.funct(inst[5:0]), 
.regDst(reg_dst), 
.aluSrcA(alu_srcA), 
.memToReg(memtoreg), 
.regWrite(regwrite), 
.memRead(memread), 
.memWrite(memwrite), 
.branch(branch), 
.aluop(aluop), 
.jmp(jmp),
.jal(jal),
.jr(jr),
.lui(lui),
.shift(shift),
.bne(bne),
.bltz(bltz),
.bgtz(bgtz),
.imm_expand(imm_expand),
.flag(flag)
); 

// 实例化ALU模块
alu alu(
.input1(RsData), 
.input2(mux2), 
.aluCtr(aluCtr), 
.zero(zero), 
.aluRes(aluRes));

// 实例化ALU控制模块
aluctr aluctr1( 
.ALUOp(aluop), 
.funct(inst[5:0]), 
.ALUCtr(aluCtr)
); 

// 实例化dmem模块
dram dmem( 
.clk(!clkin), 
.we(memwrite), 
.re(memread),
.reset(reset),
.flag(flag),
.a(aluRes[7:0]), 
.wd(RtData), 
.rd(memreaddata) 
); 

// 实例化imem模块
irom imem( 
.clka(!clkin),    // input wire clka
.ena(1'b1),      // input wire ena
.addra(pc[6:2]),  // input wire [4 : 0] addra
.douta(inst)  // output wire [31 : 0] douta
); 

// 实例化寄存器模块
regFile regfile( 
.RsAddr(inst[25:21]), 
.RtAddr(inst[20:16]), 
.clk(!clkin), 
.reset(reset), 
.regWriteAddr(mux1), 
.regWriteData(mux3), 
.regWriteEn(regwrite), 
.RsData(RsData), 
.RtData(RtData) 
); 

//实例化移位模块
Shifter shifte(
.SHIN(RtData),//rt
.Sa(inst[10:6]),//shamt
.ShiDir(inst[1:1]),
.Arith(inst[0:0]),
.SHOUT(shiftRe)//结果->rd
 );

// 实例化符号扩展模块
signext signext(
.inst(inst[15:0]), 
.imm_expand(imm_expand),
.data(expand)); 

//复用器
assign jmpaddr = {add4[31:28], inst[25:0], 2'b00}; 
assign mux1 = reg_dst ? inst[15:11] : inst[20:16]; 
assign mux2 = alu_srcA ? expand : RtData; 
assign mux6 = shift ? shiftRe : aluRes;; 
assign mux3 = memtoreg ? memreaddata : mux6; 
assign mux7 = jr ? {RsData, 2'b00} : jmpaddr;
assign mux5 = jmp ? mux7 : mux4;
assign choose5 = bne ? ~zero : zero;
assign choose4 = branch & choose5; 
assign mux4 = choose4 ? address : add4; 
assign expand2 = expand << 2; 
assign address = pc + expand2; 

//实例化数码管显示模块
smg_ip_model display (
.clk(Clk),
.sm_wei(sm_wei),
.data(mux6[15:0]),
.sm_duan(seg)
);
  
endmodule 

3、仿真检验

将指令转换成二进制代码,一共27类指令。为了测试方便,我们将代码分为三个表格,分别对应运算存取指令、分支指令、跳转指令,同时也为了保证测试时代码数量不会溢出(超过32条)。下面是三类代码对应的表格:

运算、存取测试:
指令 代码 op rs rt rd/imm
addiu $1,$0,-8 2401fff8 001001 00000 00001 11111 11111 111000
addi $1,$0,8 20010008 001000 00000 00001 00000 00000 001000
addu $2,$0,$1 00011021 000000 00000 00001 00010 00000 100001
add $2 $2 $1 00411020 000000 00010 00001 00010 00000 100000
sub $3,$2,$1 00411822 000000 00010 00001 00011 00000 100010
ori $4,$0,3 34040003 001101 00000 00100 00000 00000 000011
or $4,$4,$1 00812025 000000 00100 00001 00100 00000 100101
andi $5,$1,10 3025000a 001100 00001 00101 00000 00000 001010
and $5,$5,$1 00a12824 000000 00101 00001 00101 00000 100100
xori $6,$1,2 38260002 001110 00001 00110 00000 00000 000010
xor $6,$6,$1 00c13026 000000 00110 00001 00110 00000 100110
nor $7,$2,$1 00413827 000000 00010 00001 00111 00001 000000
lui $9,12 3c09000c 001111 00000 01001 00000 00000 001100
slt $10,$4,$3 0083502a 000000 00100 00011 01010 00000 101010
slti $11,$6,4 28cb0004 001010 00110 01011 00000 00000 000100
sll $8,$8,1 00084040 000000 00000 01000 01000 00001 000000
srl $8,$8,1 00084042 000000 00000 01000 01000 00001 000010
sra $8,$8,1 00084043 000000 00000 01000 01000 00001 000011
sllv $8,$1,$8 01014004 000000 01000 00001 01000 00000 000100
srlv $8,$1,$8 01014006 000000 01000 00001 01000 00000 000110
srav $8,$1,$8 01014007 000000 01000 00001 01000 00000 000111
sw $4,4($2) ac440004 101011 00010 00100 00000 00000 000100
lw $5,4($2) 8c450004 100011 00010 00101 00000 00000 000100
分支测试:
地址 指令 代码 op rs rt rd/imm
00000000 addi $2,$0,-1 2002ffff 001000 00000 00010 11111 11111 111111
00000004 nop 00000000 000000 00000 00000 00000 00000 000000
00000008 lab:nop 00000000 000000 00000 00000 00000 00000 000000
0000000c addi $2,$2,1 20420001 001000 00010 00010 00000 00000 000001
00000010 bne $2,$0,lab 1440fffb 000101 00010 00000 11111 11111 111011
00000014 beq $2,$0,lab 1040fffd 000100 00010 00000 11111 11111 111101
跳转测试:
地址 指令 代码 op rs rt rd/imm
00000000 addi $1,$0,4 20010004 001000 00000 00001 00000 00000 000100
00000004 jr $1 00200008 000000 00001 00000 00000 00000 001000
00000008 nop 00000000 000000 00000 00000 00000 00000 000000
0000000c lab:nop 00000000 000000 00000 00000 00000 00000 000000
00000010 nop 00000000 000000 00000 00000 00000 00000 000000
00000014 j lab 0c000003 000011 00000 00000 00000 00000 000011

将二进制代码写入coe文件,存入指令存储器中:

op.coe:

memory_initialization_radix=16;
memory_initialization_vector=
2401fff8 20010008 00011021 00411020 00411822 34040003 00812025 3025000a 00a12824 38260002 00c13026 00413827 3c09000c 
20080008 20010002 00084040 00084042 00084043 01014004 01014006 01014007 
ac440004 8c450004;

b.coe:

memory_initialization_radix=16;
memory_initialization_vector=2002ffff 00000000 00000000 20420001 1440fffb 1040fffd 00000000 0000000;

j.coe:

memory_initialization_radix=16;
memory_initialization_vector=20010004 00200008 00000000 00000000 0c000003;

仿真模块代码如下:

module topsim;
// Inputs 
reg clkin;
reg reset;
// output
wire [31:0] inst;
wire [31:0] pc;
wire [2:0] aluop;
wire alu_srcA;
wire [31:0] add4;
wire choose4;
wire reg_dst; 
wire jmp; 
wire branch; 
wire memread; 
wire memwrite;
wire memtoreg; 
wire regwrite;
wire zero;
wire [31:0] aluRes;
wire [3:0] aluCtr;
wire [31:0] memreaddata;
wire [31:0] RsData;
wire [31:0] RtData;
wire [31:0] expand2,expand;
wire[31:0] shiftRe;
wire[31:0]  mux2, mux3, mux4, mux5,mux6, address, jmpaddr; 
wire[4:0] mux1; 
wire imm_expand;
wire shift;
wire [1:0] flag;
// Instantiate the Unit Under Test (UUT) 

//调用top模块
top uut( 
.Clk(clkin),
.reset(reset),
.inst(inst),
.pc(pc),
.aluop(aluop),
.alu_srcA(alu_srcA),
.add4(add4),
.choose4(choose4),
.reg_dst(reg_dst),
.jmp(jmp),
.branch(branch), 
.memread(memread),
.memwrite(memwrite),
.memtoreg(memtoreg),
.regwrite(regwrite),
.zero(zero),
.shift(shift),
.aluRes(aluRes),
.aluCtr(aluCtr),
.shiftRe(shiftRe),
.memreaddata(memreaddata),
.RsData(RsData),
.RtData(RtData),
.expand(expand),
.expand2(expand2), 
.mux2(mux2),
.mux3(mux3), 
.mux4(mux4), 
.mux5(mux5), 
.mux6(mux6), 
.address(address), 
.jmpaddr(jmpaddr),
.mux1(mux1),
.imm_expand(imm_expand),
.flag(flag)
);

initial begin 
// Initialize Inputs 
clkin = 0; 
reset = 1; 
// Wait 100 ns for global reset to finish 
#100; 
reset = 0; 
end 
parameter PERIOD = 20; 
always begin 
clkin = 1'b0; 
#(PERIOD / 2) clkin = 1'b1; 
#(PERIOD / 2) ; 
end 
endmodule 

接下来依次分析各条指令的仿真执行情况:

运算指令:

指令 代码 op rs rt rd/imm
addiu $1,$0,-8 2401fff8 001001 00000 00001 11111 11111 111000
addi $1,$0,8 20010008 001000 00000 00001 00000 00000 001000
addu $2,$0,$1 00011021 000000 00000 00001 00010 00000 100001
add $2 $2 $1 00411020 000000 00010 00001 00010 00000 100000
sub $3,$2,$1 00411822 000000 00010 00001 00011 00000 100010
ori $4,$0,3 34040003 001101 00000 00100 00000 00000 000011
or $4,$4,$1 00812025 000000 00100 00001 00100 00000 100101
andi $5,$1,10 3025000a 001100 00001 00101 00000 00000 001010
and $5,$5,$1 00a12824 000000 00101 00001 00101 00000 100100
xori $6,$1,2 38260002 001110 00001 00110 00000 00000 000010
xor $6,$6,$1 00c13026 000000 00110 00001 00110 00000 100110
nor $7,$2,$1 00413827 000000 00010 00001 00111 00001 000000
lui $9,12 3c09000c 001111 00000 01001 00000 00000 001100
slt $10,$4,$3 0083502a 000000 00100 00011 01010 00000 101010
slti $11,$6,4 28cb0004 001010 00110 01011 00000 00000 000100

以上运算指令的仿真结果图如下:(以下结果均用16进制表示)

接下来对每条指令进行分析:

addiu:

addiu指令,将寄存器0与-8相加,得到无符号数aluRes=fffffff8;

控制信号aluSrcA=1选择立即数;

regwrite=1选择写回寄存器中;

regDst=0选择写回rt;

mux1为写回寄存器的地址,结果写回寄存器1。

addi:

addi指令,将寄存器0与8相加,得到有符号数aluRes=8,

控制信号aluSrcA=1选择立即数,

regwrite=1选择写回寄存器中,

regDst=0选择写回rt,

mux1为写回寄存器的地址,结果写回寄存器2。

addu:

addu指令,将寄存器0与寄存器1相加,得到无符号数aluRes=8,

控制信号aluSrcA=0选择寄存器rt,

regwrite=1选择写回寄存器中,

regDst=1选择写回rd,

mux1为写回寄存器的地址,结果写回寄存器2。

add:

add指令,将寄存器2与寄存器1相加,得到有符号数aluRes=10,

控制信号aluSrcA=0选择寄存器rt,

regwrite=1选择写回寄存器中,

regDst=1选择写回rd,

mux1为写回寄存器的地址,结果写回寄存器2。

sub:

sub指令,将寄存器2与寄存器1相减,得到有符号数aluRes=8,

控制信号aluSrcA=0选择寄存器rt,

regwrite=1选择写回寄存器中,

regDst=1选择写回rd,

mux1为写回寄存器的地址,结果写回寄存器3。

ori:

ori指令,将寄存器0与3相或,得到aluRes=3,

控制信号aluSrcA=1选择立即数,

regwrite=1选择写回寄存器中,

regDst=0选择写回rt,

mux1为写回寄存器的地址,结果写回寄存器4。

or:

or指令,将寄存器4与寄存器1相或,得到aluRes=b,

控制信号aluSrcA=0选择寄存器rt,

regwrite=1选择写回寄存器中,

regDst=1选择写回rd,

mux1为写回寄存器的地址,结果写回寄存器4。

andi:

andi指令,将寄存器1与10相与,得到aluRes=8,

控制信号aluSrcA=1选择立即数,

regwrite=1选择写回寄存器中,

regDst=0选择写回rt,

mux1为写回寄存器的地址,结果写回寄存器5。

and:

and指令,将寄存器5与寄存器1相与,得到aluRes=8,

控制信号aluSrcA=0选择寄存器rt,

regwrite=1选择写回寄存器中,

regDst=1选择写回rd,

mux1为写回寄存器的地址,结果写回寄存器5。

xori:

xori指令,将寄存器1与2相与,得到aluRes=a,

控制信号aluSrcA=1选择立即数,

regwrite=1选择写回寄存器中,

regDst=0选择写回rt,

mux1为写回寄存器的地址,结果写回寄存器6。

xor:

xor指令,将寄存器6与寄存器1异或,得到aluRes=2,

控制信号aluSrcA=0选择寄存器rt,

regwrite=1选择写回寄存器中,

regDst=1选择写回rd,

mux1为写回寄存器的地址,结果写回寄存器6。

nor:

nor指令,将寄存器7与寄存器1或非,得到aluRes=ffffffe7,

控制信号aluSrcA=0选择寄存器rt,

regwrite=1选择写回寄存器中,

regDst=1选择写回rd,

mux1为写回寄存器的地址,结果写回寄存器7。

lui:

lui指令,将数字c存到寄存器9的高16位中,得到aluRes=000c0000,

控制信号aluSrcA=1选择立即数,

regwrite=1选择写回寄存器中,

regDst=0选择写回rt,

mux1为写回寄存器的地址,结果写回寄存器9。

slt:

slt指令比较寄存器4<寄存器3是否成立,若成立,aluRes=1;

从上面的运算我们可以得到,寄存器4的值为b,寄存器3的值为8,b<8不成立,则aluRes=0;

控制信号aluSecA=0选择寄存器rt,

regDst=1选择写回寄存器rd,

mux1=a,结果写回寄存器10。

slti:

slti指令比较寄存器6<立即数4是否成立,若成立,aluRes=1;

从上面的运算我们可以得到,寄存器6的值为2,2<4成立,则aluRes=1;

控制信号aluSecA=1选择立即数,

regDst=0选择写回寄存器rt,

mux1=b,结果写回寄存器11。

移位指令:

指令 代码 op rs rt rd/imm
sll $8,$8,1 00084040 000000 00000 01000 01000 00001 000000
srl $8,$8,1 00084042 000000 00000 01000 01000 00001 000010
sra $8,$8,1 00084043 000000 00000 01000 01000 00001 000011
sllv $8,$1,$8 01014004 000000 01000 00001 01000 00000 000100
srlv $8,$1,$8 01014006 000000 01000 00001 01000 00000 000110
srav $8,$1,$8 01014007 000000 01000 00001 01000 00000 000111

以上六条移位指令的仿真结果图如下:

在初始化时,我们令$8=8、$1=2,移位指令分为两大类,分别使用移位模块、ALU模块执行运算,接下来分别看各条指令的结果。(以下结果均用16进制表示)

sll:

第一大类的移位指令通过移位模块计算,shift=1表示启用移位模块,shamt=1,将$8左移一位,结果显示在shiftRe中,结果10正确。

srl:

shift=1表示启用移位模块,shamt=1,将$8逻辑右移一位,结果显示在shiftRe中,结果8正确。

sra:

shift=1表示启用移位模块,shamt=1,将$8右移算数一位,结果显示在shiftRe中,由于8为正数,得到运算结果4,正确。

sllv:

第二大类的移位指令通过ALU模块计算,shift=0表示不启用移位模块,$1=2,将$8左移2位,结果显示在aluRes中,结果10正确。

srlv:

第二大类的移位指令通过ALU模块计算,shift=0表示不启用移位模块,$1=2,将$8逻辑右移2位,结果显示在aluRes中,结果4正确。

srav:

第二大类的移位指令通过ALU模块计算,shift=0表示不启用移位模块,$1=2,将$8算术右移2位,结果显示在aluRes中,结果1正确。

存取指令:

指令 代码 op rs rt rd/imm
sw $4,4($2) ac440004 101011 00010 00100 00000 00000 000100
lw $5,4($2) 8c450004 100011 00010 00101 00000 00000 000100

存取指令将寄存器4的值先存入内存地址为:寄存器2的值+4的位置,再将数据取出至寄存器5。

根据上面指令的运算结果,$4=b,$2=10,则内存地址应为14,存取的数据为b。接下来看指令的仿真结果图:

sw:

RsData=10,对应寄存器2的值;

RtData=b,对应寄存器4的值;

aluSrcA=1,选择立即数;

expand=4,立即数的值为4;

memwrite=1,写入内存使能;

regwrite=0,不写入寄存器;

aluRes=14,得到内存地址14;

将寄存器4的值b写入内存地址14中。

lw:

RsData=10,对应寄存器2的值;

aluSrcA=1,选择立即数;

expand=4,立即数的值为4;

memwrite=0,不写入内存;

menread=1,读取内存使能;

regwrite=1,写入寄存器;

mux1=5,写入寄存器地址为5;

aluRes=14,得到内存地址14;

将内存地址14中的值(b)写入寄存器5中。

分支指令:

地址 指令 代码 op rs rt rd/imm
00000000 addi $2,$0,-1 2002ffff 001000 00000 00010 11111 11111 111111
00000004 nop 00000000 000000 00000 00000 00000 00000 000000
00000008 lab:nop 00000000 000000 00000 00000 00000 00000 000000
0000000c addi $2,$2,1 20420001 001000 00010 00010 00000 00000 000001
00000010 bne $2,$0,lab 1440fffb 000101 00010 00000 11111 11111 111011
00000014 beq $2,$0,lab 1040fffd 000100 00010 00000 11111 11111 111101

由于分支测试比较难以判断,我们另外使用一个测试文件,专门用于测试分支指令。 指令如上图所示,在执行两道加法指令之后,寄存器2的值为0,则在bne指令中不会跳转,在beq指令中跳转。为了便于观察,我们添加了几条空指令nop。

测试结果如下:

接下来对指令依次分析:

bne:

add4=14为PC+4;

expand2 = ffff ffec为相对跳转地址,跳转到-5x4=-20处;

bne=1,表示执行bne指令;

zero=1,表示运算结果为0;

choose4=0,choose4判断最终是否执行跳转,为0不执行跳转;

branch=1,表示分支指令。

beq:

add4=18为PC+4;

expand2 = ffff fff4为相对跳转地址,跳转到-4x4=-10处;

zero=1,表示运算结果为0;

choose4=1,choose4判断最终是否执行跳转,为1执行跳转;

branch=1,表示分支指令。

跳转后的指令:

分支跳转之后,PC地址为18-10=8。

跳转指令:

地址 指令 代码 op rs rt rd/imm
00000000 addi $1,$0,4 20010004 001000 00000 00001 00000 00000 000100
00000004 jr $1 00200008 000000 00001 00000 00000 00000 001000
00000008 nop 00000000 000000 00000 00000 00000 00000 000000
0000000c lab:nop 00000000 000000 00000 00000 00000 00000 000000
00000010 nop 00000000 000000 00000 00000 00000 00000 000000
00000014 j lab 0c000003 000011 00000 00000 00000 00000 000011

为了验证跳转指令,我们另外使用一个测试文件,专门用于测试跳转指令。 指令如上图所示,在执行加法指令之后,寄存器1的值为4,在jr指令中会跳转到4«2=10的指令地址上;同样,j指令会跳转到c的指令地址上。为了便于观察,我们添加了几条空指令nop。

测试结果如下:

jr:

jmp=1,执行跳转指令;

jr=1,执行jr指令;

RsData=4,为寄存器1的值;

mux5=10,为4«2后得到的指令地址。

jr的下一条指令:

执行jr指令后,PC=10,成功跳转到对应地址。

j:

jmp=1,执行跳转指令;

mux5=c,为我们得到的的指令地址。

j的下一条指令:

执行j指令后,PC=c,成功跳转到对应地址。

4、Basys3板实现

引脚设置:

输入输出的引脚如下:

输入为时钟信号Clk、复位信号Reset,Clk连接系统CPU时钟,复位信号连接按钮开关U18。

输出信号为各个数码管显示所需的高低电平信号,分别为4个位选信号和7个段选信号。具体连接方式如下:

仿真文件:

set_property PACKAGE_PIN U7 [get_ports {seg[6]}]
set_property PACKAGE_PIN V5 [get_ports {seg[5]}]
set_property PACKAGE_PIN U5 [get_ports {seg[4]}]
set_property PACKAGE_PIN V8 [get_ports {seg[3]}]
set_property PACKAGE_PIN U8 [get_ports {seg[2]}]
set_property PACKAGE_PIN W6 [get_ports {seg[1]}]
set_property PACKAGE_PIN W7 [get_ports {seg[0]}]
set_property PACKAGE_PIN U2 [get_ports {sm_wei[0]}]
set_property PACKAGE_PIN U4 [get_ports {sm_wei[1]}]
set_property PACKAGE_PIN V4 [get_ports {sm_wei[2]}]
set_property PACKAGE_PIN W4 [get_ports {sm_wei[3]}]
set_property PACKAGE_PIN W5 [get_ports Clk]
set_property PACKAGE_PIN U18 [get_ports reset]

set_property IOSTANDARD LVCMOS33 [get_ports {seg[6]}]
set_property IOSTANDARD LVCMOS33 [get_ports {seg[5]}]
set_property IOSTANDARD LVCMOS33 [get_ports {seg[4]}]
set_property IOSTANDARD LVCMOS33 [get_ports {seg[3]}]
set_property IOSTANDARD LVCMOS33 [get_ports {seg[2]}]
set_property IOSTANDARD LVCMOS33 [get_ports {seg[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {seg[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {sm_wei[3]}]
set_property IOSTANDARD LVCMOS33 [get_ports {sm_wei[2]}]
set_property IOSTANDARD LVCMOS33 [get_ports {sm_wei[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {sm_wei[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports Clk]
set_property IOSTANDARD LVCMOS33 [get_ports reset]

端口映射:

运行结果:

数码管显示的结果为运算结果/PC地址,这里展示了不同类几条指令的显示结果,结果都与上述仿真结果一致:

add指令:运算结果为8。

sub指令:运算结果为8。

or指令:运算结果为b。

xor指令:运算结果为2。

nor指令:运算结果为ffe7。

lui指令:运算结果为0000。(高4位为000c无法显示)

sll指令:运算结果为10。

srl指令:运算结果为8。

sra指令:运算结果为4。

beq指令:当前地址为0014,分支跳转到0008。

j指令:当前地址为0014,分支跳转到000c。

六. 实验心得

本次实验是在之前实现各个小模块的基础上,将其整合起来,完成一个真正意义上的单周期CPU。这需要我们对CPU的工作原理有一个清晰的认识,而且对各个模块之间的联系、对每条指令控制信号的分析也需要清楚。

在实验里首先遇到的问题是语法问题,对我们来说,verilog语言是一个新的语言,虽然我们在之前的实验里,已经学会了看懂代码、理解其中的意思,但实际写代码的时候总会遇到各种各样的错误,有时候vivado的报错也让人花了很多时间。为了解决这些问题,我参考了网上的资料与以前实验的代码。

另外,对于各个模块的测试尽量先分别进行测试,在测试结果正确后再进行组合。而实验比较难的一部分是对各个指令的控制信号的测试。控制信号联系了各个模块,对控制信号进行分析时需要联系多个模块,并且对指令本身的功能需要完全理解。在根据控制信号进行测试花的时间是最长的,在测试遇到问题的时候,也需要仔细确认是哪一个模块出了问题。