单周期CPU设计与实现

实验内容:​​学校资料​

设计一个单周期CPU,该CPU至少能实现以下指令功能操作。指令与格式如下:

==> 算术运算指令

1. add rd , rs, rt (说明:以助记符表示,是汇编指令;以代码表示,是机器指令)




功能:rd←rs + rt。reserved为预留部分,即未用,一般填“0”。

2. addi rt , rs ,immediate




功能:rt←rs + (sign-extend)immediate;immediate符号扩展再参加“加”运算。

3. sub rd , rs , rt




功能:rd←rs - rt

==> 逻辑运算指令

4. ori rt , rs ,immediate




功能:rt←rs | (zero-extend)immediate;immediate做“0”扩展再参加“或”运算。

5. and rd , rs , rt




功能:rd←rs & rt;逻辑与运算。

6. or rd , rs , rt




功能:rd←rs | rt;逻辑或运算。

==>移位指令

7. sll rd, rt,sa




功能:rd<-rt<<(zero-extend)sa,左移sa位 ,(zero-extend)sa

==>比较指令

8. slti rt, rs,immediate 带符号




功能:if (rs <(sign-extend)immediate) rt =1 else rt=0, 具体请看表2 ALU运算功能表,带符号

==> 存储器读/写指令

9. sw rt ,immediate(rs) 写存储器




功能:memory[rs+ (sign-extend)immediate]←rt;immediate符号扩展再相加。即将rt寄存器的内容保存到rs寄存器内容和立即数符号扩展后的数相加作为地址的内存单元中。

10. lw rt , immediate(rs) 读存储器




功能:rt ← memory[rs + (sign-extend)immediate];immediate符号扩展再相加。

即读取rs寄存器内容和立即数符号扩展后的数相加作为地址的内存单元中的数,然后保存到rt寄存器中。

==> 分支指令

11. beq rs,rt,immediate




功能:if(rs=rt) pc←pc + 4 + (sign-extend)immediate <<2 else pc ←pc + 4

特别说明:immediate是从PC+4地址开始和转移到的指令之间指令条数。immediate符号扩展之后左移2位再相加。为什么要左移2位?由于跳转到的指令地址肯定是4的倍数(每条指令占4个字节),最低两位是“00”,因此将immediate放进指令码中的时候,是右移了2位的,也就是以上说的“指令之间指令条数”。

12. bne rs,rt,immediate




功能:if(rs!=rt) pc←pc + 4 + (sign-extend)immediate <<2 else pc ←pc + 4

特别说明:与beq不同点是,不等时转移,相等时顺序执行。

==>跳转指令

13. j addr




功能:pc <-{(pc+4)[31..28],addr[27..2],2{0}},无条件跳转。

说明:由于MIPS32的指令代码长度占4个字节,所以指令地址二进制数最低2位均为0,将指令地址放进指令代码中时,可省掉!这样,除了最高6位操作码外,还有26位可用于存放地址,事实上,可存放28位地址了,剩下最高4位由pc+4最高4位拼接上。

==> 停机指令

14. halt




功能:停机;不改变PC的值,PC保持不变。

实验原理:

单周期CPU指的是一条指令的执行在一个时钟周期内完成,然后开始下一条指令的执行,即一条指令用一个时钟周期完成。电平从低到高变化的瞬间称为时钟上升沿,两个相邻时钟上升沿之间的时间间隔称为一个时钟周期。时钟周期一般也称振荡周期(如果晶振的输出没有经过分频就直接作为CPU的工作时钟,则时钟周期就等于振荡周期。若振荡周期经二分频后形成时钟脉冲信号作为CPU的工作时钟,这样,时钟周期就是振荡周期的两倍。)

CPU在处理指令时,一般需要经过以下几个步骤:

  • 取指令(IF):根据程序计数器PC中的指令地址,从存储器中取出一条指令,同时,PC根据指令字长度自动递增产生下一条指令所需要的指令地址,但遇到“地址转移”指令时,则控制器把“转移地址”送入PC,当然得到的“地址”需要做些变换才送入PC。
  • 指令译码(ID):对取指令操作中得到的指令进行分析并译码,确定这条指令需要完成的操作,从而产生相应的操作控制信号,用于驱动执行状态中的各种操作。
  • 指令执行(EXE):根据指令译码得到的操作控制信号,具体地执行指令动作,然后转移到结果写回状态。
  • 存储器访问(MEM):所有需要访问存储器的操作都将在这个步骤中执行,该步骤给出存储器的数据地址,把数据写入到存储器中数据地址所指定的存储单元或者从存储器中得到数据地址单元中的数据。
  • 结果写回(WB):指令执行的结果或者访问存储器中得到的数据写回相应的目的寄存器中。

单周期CPU,是在一个时钟周期内完成这五个阶段的处理。

MIPS指令的三种格式:

你还不会设计单周期CPU?_底层应用开发

其中,

  • op:为操作码;
  • rs:只读。为第1个源操作数寄存器,寄存器地址(编号)是00000~11111,00~1F;
  • rt:可读可写。为第2个源操作数寄存器,或目的操作数寄存器,寄存器地址(同上);
  • rd:只写。为目的操作数寄存器,寄存器地址(同上);
  • sa:为位移量(shift amt),移位指令用于指定移多少位;
  • funct:为功能码,在寄存器类型指令中(R类型)用来指定指令的功能与操作码配合使用;
  • immediate:为16位立即数,用作无符号的逻辑操作数、有符号的算术操作数、数据加载(Load)/数据保存(Store)指令的数据地址字节偏移量和分支指令中相对程序计数器(PC)的有符号偏移量;
  • address:为地址。

单周期CPU数据通路和控制线路图:

你还不会设计单周期CPU?_硬件_02



控制信号名

状态“0”

状态“1”

Reset

初始化PC为0

PC接收新地址

PCWre

PC不更改,相关指令:halt

PC更改,相关指令:除指令halt外

ALUSrcA

来自寄存器堆data1输出,相关指令:add、sub、addi、or、and、ori、beq、bne、slti、sw、lw

来自移位数sa,同时,进行(zero-extend)sa,即 {

{27{0}},sa},相关指令:sll

ALUSrcB

来自寄存器堆data2输出,相关指令:add、sub、or、and、sll、beq、bne

来自sign或zero扩展的立即数,相关指令:addi、ori、slti、sw、lw

DBDataSrc

来自ALU运算结果的输出,相关指令:add、addi、sub、ori、or、and、slti、sll

来自数据存储器(Data MEM)的输出,相关指令:lw

RegWre

无写寄存器组寄存器,相关指令:beq、bne、sw、halt、j

寄存器组写使能,相关指令:add、addi、sub、ori、or、and、slti、sll、lw

InsMemRW

写指令存储器

读指令存储器(Ins. Data)

mRD

输出高阻态

读数据存储器,相关指令:lw

mWR

无操作

写数据存储器,相关指令:sw

RegDst

写寄存器组寄存器的地址,来自rt字段,相关指令:addi、ori、lw、slti

写寄存器组寄存器的地址,来自rd字段,相关指令:add、sub、and、or、sll

ExtSel

(zero-extend)immediate(0扩展),相关指令:ori

(sign-extend)immediate(符号扩展),相关指令:addi、slti、sw、lw、beq、bne

PCSrc[1..0]

00:pc<-pc+4,相关指令:add、addi、sub、or、ori、and、slti、sll、sw、lw、beq(zero=0)、bne(zero=1);01:pc<-pc+4+(sign-extend)immediate,相关指令:beq(zero=1)、bne(zero=0);10:pc<-{(pc+4)[31:28],addr[27:2],2{0}},相关指令:j;11:未用

-

ALUOp[2..0]

ALU 8种运算功能选择(000-111),看功能表


​相关部件及引脚说明:​
  • Instruction Memory:指令存储器,
  • Iaddr,指令存储器地址输入端口
  • IDataIn,指令存储器数据输入端口(指令代码输入端口)
  • IDataOut,指令存储器数据输出端口(指令代码输出端口)
  • 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时,在时钟边沿触发写入
  • ALU: 算术逻辑单元
  • result,ALU运算结果
  • zero,运算结果标志,结果为0,则zero=1;否则zero=0


表2 ALU运算功能表




ALUOp[2:0]

功能

描述

000

Y = A + B

001

Y = A – B

010

Y = B << A

B左移A位

011

Y = A ∨ B

100

Y = A ∧ B

101

Y =(A < B)? 1: 0

比较A与B 不带符号

110

比较A与B 带符号

111

Y = A ⊕ B

异或


实验过程与结果


设计思路以及流程:


完成控制信号与相对应指令之间相互关系的表格


表3是依据表1控制信号的作用以及表2 ALU运算功能表完成的,某些指令无需用到部分模块,则相对应模块的使能控制信号与其无关。例如,对于跳转指令而言,其无需对数据寄存器进行读写操作,则数据寄存器相关的控制信号mRD,mWR设为0,防止修改里面的数据。部分指令执行不需要所有的模块都参与,故有些模块的控制信号与其没有直接关系,为了防止出现一些不必要的错误,统一将指令相对应的无关的使能控制信号默认设置为低电平(0),无需ALU运算的(例如跳转指令)默认将其操作变成(000)。**


表3 控制信号与相对应指令之间的相互关系



指 令

控制信号量

PCWre

ExtSel

InsMemRW

RegDst

RegWre

ALUSrcA

ALUSrcB

PCSrc(zero:0/1)

ALUOp

mRD

mWR

DBDataSrc

addi

1

1

1

0

1

0

1

00

000

0

0

0

ori

1

1

1

0

1

0

1

00

011

0

0

0

add

1

0

1

1

1

0

0

00

000

0

0

0

sub

1

1

1

1

1

0

0

00

001

0

0

0

and

1

0

1

1

1

0

0

00

100

0

0

0

or

1

0

1

1

1

0

0

00

011

0

0

0

sll

1

0

1

1

1

1

0

00

010

0

0

0

bne

1

1

1

X

0

0

0

01/ 00

001

0

0

0

slti

1

1

1

0

1

0

1

00

101

0

0

0

beq

1

1

1

X

0

0

0

00 / 01

001

0

0

0

sw

1

1

1

X

0

0

1

00

000

0

1

0

lw

1

1

1

0

1

0

1

00

000

1

0

1

j

1

0

1

X

0

X

X

10

000

0

0

0

halt

0

0

0

X

0

X

X

00

000

0

0

0


​完成控制信号与相对应指令之间的关系以后该表后,对于如何实现单周期依旧感到很模糊,不知道相对应的信号量具体的控制意义,因此尝试结合实验原理中的图2单周期CPU数据通路和控制线路图,思考三种类型的指令,R型、I型、J型指令的CPU处理过程。对于R型指令而言,主要是一些算术运算指令和逻辑运算,主要为取指令,解析指令,执行指令,将运算结果写回寄存器组,其不需要访问数据寄存器,下一条指令顺序下一条,即pc←pc+4,其中的一些运算则由控制单元得到指令的操作码以后,设置控制信号,控制各个模块执行不同操作或者数据选择器选择相对应的输入作为输出;对于I型指令,其包含指令种类比较多,存储器指令,需要对存储器进行读或写的操作,对于pc没有别的特别影响,而分支指令则下一个pc可能不是pc+4,需要依据其运算结果做相对应的跳转操作或者顺序执行操作;对于J型指令,其是跳转指令,跳转到指令中相对应的地址中,主要对pc进行操作。不同类型的指令,其进行的过程并非完成相同的,不同类型指令所使用的模块并不是一样的,所有的指令也不是都需要完整的五个处理阶段。结合CPU数据通路图以及指令相对应的控制信号后,对于每种指令的数据通路有了一个比较清晰的了解,对于每个控制信号与相对应的功能模块更加熟悉和了解,理清了如何设计单周期CPU,即将其模块化,并且在控制单元中依据指令的操作码,对各个模块的控制信号进行一定的设定,执行指令相对应的操作。​

CPU模块划分与实现

依据图2 单周期CPU数据通路和控制线路图,将CPU划分为9个模块,没有完全依据单周期CPU数据通路图进行划分,主要依据数据通路图进行划分太冗余,因此将一些数据选择器合并进了部分功能模块中,实现简化。模块划分结果如图三所示。

你还不会设计单周期CPU?_硬件_03

pcAdd

  • 模块功能:根据控制信号PCSrc,计算获得下一个pc以及控制信号Reset重置。
  • 实现思路:首先先决定何时引起触发,决定敏感变量,该模块选择将时钟的下降沿以及控制信号Reset的下降沿作为敏感变量,主要是为了能够确保下一条pc能够正确得到。
  • 主要实现代码:
`timescale 1ns / 1ps
module pcAdd(
input Reset,
input CLK, //时钟
input [1:0] PCSrc, //数据选择器输入
input [31:0] immediate, //偏移量
input [25:0] addr,
input [31:0] curPC,
output reg[31:0] nextPC //新指令地址
);

initial begin
nextPC <= 0;
end

reg [31:0] pc;

always@(negedge CLK or negedge Reset)
begin
if(!Reset) begin
nextPC <= 0;
end
else begin
pc <= curPC + 4;
case(PCSrc)
2'b00: nextPC <= curPC + 4;
2'b01: nextPC <= curPC + 4 + immediate * 4;
2'b10: nextPC <= {pc[31:28],addr,2'b00};
2'b11: nextPC <= nextPC;
endcase
end
end
endmodule

PC

  • 模块功能:根据控制信号PCWre,判断pc是否改变以及根据Reset信号判断是否重置
  • 实现思路:将时钟信号的上升沿和控制信号Reset作为敏感变量,使得pc在上升沿的时候发生改变或被重置。
  • 主要实现代码:
`timescale 1ns / 1ps
module PC(
input CLK, //时钟
input Reset, //是否重置地址。0-初始化PC,否则接受新地址
input PCWre, //是否接受新的地址。0-不更改;1-可以更改
input [1:0] PCSrc, //数据选择器输入
input [31:0] nextPC, //新指令地址
output reg[31:0] curPC //当前指令的地址
);

initial begin
curPC <= 0;
end

always@(posedge CLK or negedge Reset)
begin
if(!Reset) // Reset == 0, PC = 0
begin
curPC <= 0;
end
else
begin
if(PCWre) // PCWre == 1
begin
curPC <= nextPC;
end
else // PCWre == 0, halt
begin
curPC <= curPC;
end
end
end
endmodule

InsMEM

  • 模块功能:依据当前pc,读取指令寄存器中,相对应地址的指令
  • 实现思路:将pc的输入作为敏感变量,当pc发生改变的时候,则进行指令的读取,根据相关的地址,输出指令寄存器中相对应的指令
  • 主要实现代码:
`timescale 1ns / 1ps
//ROM
//instruction memory 指令寄存器
module InsMEM(
input [31:0] IAddr,
input InsMemRW, //状态为'0',写指令寄存器,否则为读指令寄存器
output reg[31:0] IDataOut
);

reg [7:0] rom[128:0]; // 存储器定义必须用reg类型,存储器存储单元8位长度,共128个存储单元,可以存32条指令

// 加载数据到存储器rom。注意:必须使用绝对路径
initial
begin
$readmemb("地址\\rom.txt", rom);
end

//大端模式
always@(IAddr or InsMemRW)
begin
//取指令
if(InsMemRW)
begin
IDataOut[7:0] = rom[IAddr + 3];
IDataOut[15:8] = rom[IAddr + 2];
IDataOut[23:16] = rom[IAddr + 1];
IDataOut[31:24] = rom[IAddr];
end
//$display("iaddr: %d insmemrw: %d inst; %d",IAddr, InsMemRW, IDataOut);
end

endmodule

InstructionCut

  • 模块功能:对指令进行分割,获得相对应的指令信息
  • 实现思路:根据各种类型的指令结构,将指令分割,得到相对应的信息
  • 主要实现代码:
`timescale 1ns / 1ps
//指令分割
module InstructionCut(
input [31:0] instruction,
output reg[5:0] op,
output reg[4:0] rs,
output reg[4:0] rt,
output reg[4:0] rd,
output reg[4:0] sa,
output reg[15:0] immediate,
output reg[25:0] addr
);

initial begin
op = 5'b00000;
rs = 5'b00000;
rt = 5'b00000;
rd = 5'b00000;
end

always@(instruction)
begin
op = instruction[31:26];
rs = instruction[25:21];
rt = instruction[20:16];
rd = instruction[15:11];
sa = instruction[10:6];
immediate = instruction[15:0];
addr = instruction[25:0];
end
endmodule

ControlUnit

  • 模块功能:控制单元,依据指令的操作码(op)以及标记符(ZERO),输出PCWre、ALUSrcB等控制信号,各控制信号的作用见实验原理的控制信号作用表(表3),从而达到控制各指令的目的.
  • 主要实现代码:
`timescale 1ns / 1ps
//Control Unit
module ControlUnit(
input zero, //ALU运算结果是否为0,为0时候为1
input [5:0] op, //指令的操作码
output reg PCWre, //PC是否更改的信号量,为0时候不更改,否则可以更改
output reg ExtSel, //立即数扩展的信号量,为0时候为0扩展,否则为符号扩展
output reg InsMemRW, //指令寄存器的状态操作符,为0的时候写指令寄存器,否则为读指令寄存器
output reg RegDst, //写寄存器组寄存器的地址,为0的时候地址来自rt,为1的时候地址来自rd
output reg RegWre, //寄存器组写使能,为1的时候可写
output reg ALUSrcA, //控制ALU数据A的选择端的输入,为0的时候,来自寄存器堆data1输出,为1的时候来自移位数sa
output reg ALUSrcB, //控制ALU数据B的选择端的输入,为0的时候,来自寄存器堆data2输出,为1时候来自扩展过的立即数
output reg [1:0]PCSrc, //获取下一个pc的地址的数据选择器的选择端输入
output reg [2:0]ALUOp, //ALU 8种运算功能选择(000-111)
output reg mRD, //数据存储器读控制信号,为0读
output reg mWR, //数据存储器写控制信号,为0写
output reg DBDataSrc //数据保存的选择端,为0来自ALU运算结果的输出,为1来自数据寄存器(Data MEM)的输出
);

initial begin
InsMemRW = 1;
PCWre = 1;
mRD = 0;
mWR = 0;
DBDataSrc = 0;
end

always@(op or zero)
begin
PCWre = (op == 6'b111111) ? 0 : 1; //halt
InsMemRW = (op == 6'b111111) ? 0 : 1;
mWR = (op == 6'b100110) ? 1 : 0; //sw
mRD = (op == 6'b100111) ? 1 : 0; //lw
DBDataSrc = (op == 6'b100111) ? 1 : 0;

case(op)
//addi
6'b000001:
begin
ExtSel = 1;
RegDst = 0;
RegWre = 1;
ALUSrcA = 0;
ALUSrcB = 1;
PCSrc = 2'b00;
ALUOp = 3'b000;
end
//ori
6'b010000:
begin
ExtSel = 1;
RegDst = 0;
RegWre = 1;
ALUSrcA = 0;
ALUSrcB = 1;
PCSrc = 2'b00;
ALUOp = 3'b011;
end
//add
6'b000000:
begin
ExtSel = 0;
RegDst = 1;
RegWre = 1;
ALUSrcA = 0;
ALUSrcB = 0;
PCSrc = 2'b00;
ALUOp = 3'b000;
end
//sub
6'b000010:
begin
ExtSel = 1;
RegDst = 1;
RegWre = 1;
ALUSrcA = 0;
ALUSrcB = 0;
PCSrc = 2'b00;
ALUOp = 3'b001;
end
//and
6'b010001:
begin
ExtSel = 0;
RegDst = 1;
RegWre = 1;
ALUSrcA = 0;
ALUSrcB = 0;
PCSrc = 2'b00;
ALUOp = 3'b100;
end
//or
6'b010010:
begin
ExtSel = 0;
RegDst = 1;
RegWre = 1;
ALUSrcA = 0;
ALUSrcB = 0;
PCSrc = 2'b00;
ALUOp = 3'b011;
end
//sll
6'b011000:
begin
ExtSel = 0;
RegDst = 1;
RegWre = 1;
ALUSrcA = 1;
ALUSrcB = 0;
PCSrc = 2'b00;
ALUOp = 3'b010;
end
//bne
6'b110001:
begin
ExtSel = 1;
RegDst = 0;
RegWre = 0;
ALUSrcA = 0;
ALUSrcB = 0;
PCSrc = zero ? 2'b00 : 2'b01;
ALUOp = 3'b001;
end
//slti
6'b011011:
begin
ExtSel = 1;
RegDst = 0;
RegWre = 1;
ALUSrcA = 0;
ALUSrcB = 1;
PCSrc = 2'b00;
ALUOp = 3'b101;
end
//beq
6'b110000:
begin
ExtSel = 1;
RegDst = 0;
RegWre = 0;
ALUSrcA = 0;
ALUSrcB = 0;
PCSrc = zero ? 2'b01 : 2'b00;
ALUOp = 3'b001;
end
//sw
6'b100110:
begin
ExtSel = 1;
RegDst = 0;
RegWre = 0;
ALUSrcA = 0;
ALUSrcB = 1;
PCSrc = 2'b00;
ALUOp = 3'b000;
end
//lw
6'b100111:
begin
ExtSel = 1;
RegDst = 0;
RegWre = 1;
ALUSrcA = 0;
ALUSrcB = 1;
PCSrc = 2'b00;
ALUOp = 3'b000;
end
//j
6'b111000:
begin
ExtSel = 0;
RegDst = 0;
RegWre = 0;
ALUSrcA = 0;
ALUSrcB = 0;
PCSrc = 2'b10;
ALUOp = 3'b000;
end
//halt
6'b111111:
begin
ExtSel = 0;
RegDst = 0;
RegWre = 0;
ALUSrcA = 0;
ALUSrcB = 0;
PCSrc = 2'b11;
ALUOp = 3'b000;
end
endcase
end
endmodule

RegisterFile

  • 模块功能:寄存器组,通过控制单元输出的控制信号,进行相对应的读或写操作
  • 主要实现代码:
`timescale 1ns / 1ps
//寄存器组
module RegisterFile(
input CLK, //时钟
input [4:0] ReadReg1, //rs寄存器地址输入端口
input [4:0] ReadReg2, //rt寄存器地址输入端口
input [31:0] WriteData, //写入寄存器的数据输入端口
input [4:0] WriteReg, //将数据写入的寄存器端口,其地址来源rt或rd字段
input RegWre, //WE,写使能信号,为1时,在时钟边沿触发写入
output reg[31:0] ReadData1, //rs寄存器数据输出端口
output reg[31:0] ReadData2 //rt寄存器数据输出端口
);

initial begin
ReadData1 <= 0;
ReadData2 <= 0;
end


reg [31:0] regFile[0:31]; // 寄存器定义必须用reg类型
integer i;
initial begin
for (i = 0; i < 32; i = i+ 1) regFile[i] <= 0;
end

always@(ReadReg1 or ReadReg2)
begin
ReadData1 = regFile[ReadReg1];
ReadData2 = regFile[ReadReg2];
//$display("regfile %d %d\n", ReadReg1, ReadReg2);
end

always@(negedge CLK)
begin
//$0恒为0,所以写入寄存器的地址不能为0
if(RegWre && WriteReg)
begin
regFile[WriteReg] <= WriteData;
end
end
endmodule

ALU

  • 模块功能:算术逻辑单元,对两个输入依据ALUOp进行相对应的运算
  • 实现思路:依据实验原理中的ALU运算功能表(表2)完成操作码对应的操作
  • 主要实现代码:
`timescale 1ns / 1ps
module ALU(
input ALUSrcA,
input ALUSrcB,
input [31:0] ReadData1,
input [31:0] ReadData2,
input [4:0] sa,
input [31:0] extend,
input [2:0] ALUOp,
output reg zero,
output reg[31:0] result
);

reg [31:0] A;
reg [31:0] B;

always@(ReadData1 or ReadData2 or ALUSrcA or ALUSrcB or ALUOp)
begin
//定义两个输入端口
A = (ALUSrcA == 0) ? ReadData1 : sa;
B = (ALUSrcB == 0) ? ReadData2 : extend;
case(ALUOp)
3'b000: result = A + B;
3'b001: result = A - B;
3'b010: result = B << A;
3'b011: result = A | B;
3'b100: result = A & B;
3'b101: result = (A < B) ? 1 : 0;
3'b110: result = (((ReadData1 < ReadData2) && (ReadData1[31] == ReadData2[31] )) ||( ( ReadData1[31] ==1 && ReadData2[31] == 0))) ? 1:0;
3'b111: result = A ^ B;
endcase
zero = (result == 0) ? 1 : 0;
end
endmodule

DataMEM

  • 模块功能:数据存储器,通过控制信号,对数据寄存器进行读或者写操作,并且此处模块额外合并了输出DB的数据选择器,此模块同时输出写回寄存器组的数据DB。
  • 主要实现代码:
`timescale 1ns / 1ps
//RAM
//data memory 数据存储器
module DataMEM(
/*
Daddr,数据存储器地址输入端口
DataIn,数据存储器数据输入端口
DataOut,数据存储器数据输出端口
mRD,数据存储器读控制信号,为0读
mWR,数据存储器写控制信号,为0写
*/
input mRD,
input mWR,
input CLK,
input DBDataSrc,
input [31:0] DAddr,
input [31:0] DataIn,
output reg[31:0] DataOut,
output reg[31:0] DB
);

initial begin
DB <= 16'b0;
end

reg [7:0] ram [0:31]; // 存储器定义必须用reg类型

always@(mRD or DAddr or DBDataSrc)
begin
//读
DataOut[7:0] = mRD ? ram[DAddr + 3] : 8'bz; // z 为高阻态
DataOut[15:8] = mRD ? ram[DAddr + 2] : 8'bz;
DataOut[23:16] = mRD ? ram[DAddr + 1] : 8'bz;
DataOut[31:24] = mRD ? ram[DAddr] : 8'bz;

DB = (DBDataSrc == 0) ? DAddr : DataOut;
end

always@(negedge CLK)
begin
//写
if(mWR)
begin
ram[DAddr] = DataIn[31:24];
ram[DAddr + 1] = DataIn[23:16];
ram[DAddr + 2] = DataIn[15:8];
ram[DAddr + 3] = DataIn[7:0];
end
//$display("mwr: %d $12 %d %d %d %d", mWR, ram[12], ram[13], ram[14], ram[15]);
end

endmodule

SignZeroExtend

  • 模块功能:根据指令相关的控制信号ExtSel,对立即数进行扩展。
  • 实现思路:根据控制信号ExtSel判断是0扩展还是符号扩展,然后进行相对应的扩展
  • 主要实现代码:
`timescale 1ns / 1ps
module SignZeroExtend(
input wire [15:0] immediate, //立即数
input ExtSel, //状态'0',0扩展,否则符号位扩展
output [31:0] extendImmediate
);

always@(extendImmediate)
begin
$display("%d", extendImmediate[31]);
end

assign extendImmediate[15:0] = immediate;
assign extendImmediate[31:16] = ExtSel ? (immediate[15] ? 16'hffff : 16'h0000) : 16'h0000;
endmodule

顶层模块:SingleCycleCPU

  • 实现思路:在顶层模块中将各个已实现的底层模块进行实列,并且用verilog语言将各个模块用线连接起来
  • 代码:
`timescale 1ns / 1ps

module SingleCycleCPU(
input CLK,
input Reset,
output [31:0] curPC,
output [31:0] nextPC,
output [31:0] instruction,
output [5:0] op,
output [4:0] rs,
output [4:0] rt,
output [4:0] rd,
output [31:0] DB,
output [31:0] A,
output [31:0] B,
output [31:0] result,
output [1:0] PCSrc,
output zero,
output PCWre, //PC是否更改的信号量,为0时候不更改,否则可以更改
output ExtSel, //立即数扩展的信号量,为0时候为0扩展,否则为符号扩展
output InsMemRW, //指令寄存器的状态操作符,为0的时候写指令寄存器,否则为读指令寄存器
output RegDst, //写寄存器组寄存器的地址,为0的时候地址来自rt,为1的时候地址来自rd
output RegWre, //寄存器组写使能,为1的时候可写
output ALUSrcA, //控制ALU数据A的选择端的输入,为0的时候,来自寄存器堆data1输出,为1的时候来自移位数sa
output ALUSrcB, //控制ALU数据B的选择端的输入,为0的时候,来自寄存器堆data2输出,为1时候来自扩展过的立即数
output [2:0]ALUOp, //ALU 8种运算功能选择(000-111)
output mRD, //数据存储器读控制信号,为0读
output mWR, //数据存储器写控制信号,为0写
output DBDataSrc //数据保存的选择端,为0来自ALU运算结果的输出,为1来自数据寄存器(Data MEM)的输出
);

wire [31:0] extend;
wire [31:0] DataOut;
wire[4:0] sa;
wire[15:0] immediate;
wire[25:0] addr;

pcAdd pcAdd(.Reset(Reset),
.CLK(CLK),
.PCSrc(PCSrc),
.immediate(extend),
.addr(addr),
.curPC(curPC),
.nextPC(nextPC));

PC pc(.CLK(CLK),
.Reset(Reset),
.PCWre(PCWre),
.PCSrc(PCSrc),
.nextPC(nextPC),
.curPC(curPC));

InsMEM InsMEM(.IAddr(curPC),
.InsMemRW(InsMemRW),
.IDataOut(instruction));

InstructionCut InstructionCut(.instruction(instruction),
.op(op),
.rs(rs),
.rt(rt),
.rd(rd),
.sa(sa),
.immediate(immediate),
.addr(addr));

ControlUnit ControlUnit(.zero(zero),
.op(op),
.PCWre(PCWre),
.ExtSel(ExtSel),
.InsMemRW(InsMemRW),
.RegDst(RegDst),
.RegWre(RegWre),
.ALUSrcA(ALUSrcA),
.ALUSrcB(ALUSrcB),
.PCSrc(PCSrc),
.ALUOp(ALUOp),
.mRD(mRD),
.mWR(mWR),
.DBDataSrc(DBDataSrc));

RegisterFile RegisterFile(.CLK(CLK),
.ReadReg1(rs),
.ReadReg2(rt),
.WriteData(DB),
.WriteReg(RegDst ? rd : rt),
.RegWre(RegWre),
.ReadData1(A),
.ReadData2(B));

ALU alu(.ALUSrcA(ALUSrcA),
.ALUSrcB(ALUSrcB),
.ReadData1(A),
.ReadData2(B),
.sa(sa),
.extend(extend),
.ALUOp(ALUOp),
.zero(zero),
.result(result));

DataMEM DataMEM(.mRD(mRD),
.mWR(mWR),
.CLK(CLK),
.DBDataSrc(DBDataSrc),
.DAddr(result),
.DataIn(B),
.DataOut(DataOut),
.DB(DB));

SignZeroExtend SignZeroExtend(.immediate(immediate),
.ExtSel(ExtSel),
.extendImmediate(extend));

endmodule

CPU正确性的验证

仿真程序:

`timescale 1ns / 1ps

module TestSingleCycleCpu();
// Inputs
reg CLK;
reg Reset;

// Outputs
wire [1:0] PCSrc;
wire [5:0] op;
wire [4:0] rs;
wire [4:0] rt;
wire [4:0] rd;
wire [31:0] DB;
wire [31:0] result;
wire [31:0] curPC;
wire [31:0] nextPC;
wire [31:0] instruction;
wire [31:0] A;
wire [31:0] B;
wire zero;
wire PCWre; //PC是否更改的信号量,为0时候不更改,否则可以更改
wire ExtSel; //立即数扩展的信号量,为0时候为0扩展,否则为符号扩展
wire InsMemRW; //指令寄存器的状态操作符,为0的时候写指令寄存器,否则为读指令寄存器
wire RegDst; //写寄存器组寄存器的地址,为0的时候地址来自rt,为1的时候地址来自rd
wire RegWre; //寄存器组写使能,为1的时候可写
wire ALUSrcA; //控制ALU数据A的选择端的输入,为0的时候,来自寄存器堆data1输出,为1的时候来自移位数sa
wire ALUSrcB; //控制ALU数据B的选择端的输入,为0的时候,来自寄存器堆data2输出,为1时候来自扩展过的立即数
wire [2:0]ALUOp; //ALU 8种运算功能选择(000-111)
wire mRD; //数据存储器读控制信号,为0读
wire mWR; //数据存储器写控制信号,为0写
wire DBDataSrc; //数据保存的选择端,为0来自ALU运算结果的输出,为1来自数据寄存器(Data MEM)的输出
// Instantiate the Unit Under Test (UUT)
SingleCycleCPU uut (
.CLK(CLK),
.Reset(Reset),
.curPC(curPC),
.nextPC(nextPC),
.instruction(instruction),
.op(op),
.rs(rs),
.rt(rt),
.rd(rd),
.DB(DB),
.A(A),
.B(B),
.result(result),
.PCSrc(PCSrc),
.zero(zero),
.PCWre(PCWre),
.ExtSel(ExtSel),
.InsMemRW(InsMemRW),
.RegDst(RegDst),
.RegWre(RegWre),
.ALUSrcA(ALUSrcA),
.ALUSrcB(ALUSrcB),
.ALUOp(ALUOp),
.mRD(mRD),
.mWR(mWR),
.DBDataSrc(DBDataSrc)
);

initial begin
// Initialize Inputs
CLK = 1;
Reset = 0;

CLK = !CLK; // 下降沿,使PC先清零
Reset = 1; // 清除保持信号
forever #5
begin // 产生时钟信号,周期为50s
CLK = !CLK;
end
end
endmodule
程序代码测试



地址

汇编程序

指令代码

op(6)

rs(5)

rt(5)

rd(5)/immediate (16)

0x00000000

addi $1,$0,8

000001

00000

00001

0000 0000 0000 1000

0x00000004

ori $2,$0,2

010000

00000

00010

0000 0000 0000 0010

0x00000008

add $3,$2,$1

000000

00010

00001

0001 1000 0000 0000

0x0000000C

sub $5,$3,$2

000010

00011

00010

0010 1000 0000 0000

0x00000010

and $4,$5,$2

010001

00101

00010

0010 0000 0000 0000

0x00000014

or $8,$4,$2

010010

00100

00010

0100 0000 0000 0000

0x00000018

sll $8,$8,1

011000

00000

01000

0100 0000 0100 0000

0x0000001C

bne $8,$1,-2 (≠,转18)

110001

01000

00001

1111 1111 1111 1110

0x00000020

slti $6,$2,8

011011

00010

00110

0000 0000 0000 1000

0x00000024

slti $7,$6,0

011011

00110

00111

0000 0000 0000 0000

0x00000028

addi $7,$7,8

000001

00111

00111

0000 0000 0000 1000

0x0000002C

beq $7,$1,-2 (=,转28)

110000

00111

00001

1111 1111 1111 1110

0x00000030

sw $2,4($1)

100110

00001

00010

0000 0000 0000 0100

0x00000034

lw $9,4($1)

100111

00001

01001

0000 0000 0000 0100

0x00000038

j 0x00000040

111000

00000

00000

0000 0000 0001 0000

0x0000003C

addi $10,$0,10

000001

00000

01010

0000 0000 0000 1010

0x00000040

Halt

111111

00000

00000

0000 0000 0000 0000

0x00000044

0x00000048

0x0000004C


​使用上面程序段进行测试CPU正确性,将其中的指令写入一个romData.txt文件中。
在模块InsMEM中进行读入(使用的路径为绝对路径)​

(​​源码和实验报告​​)

总结

本次实验中遇到的问题比较多。首先是关于CPU的设计,其次就是verilog语言。一开始不知道如何实现,感觉无从下手。主要通过分析实验原理中的图2 单周期CPU数据通路和控制线路图,分析各种指令的处理过程,学会将CPU内各个部分模块化,各个模块分别实现一定的功能,然后通过相对应的控制信号连接起来,这样就实现cpu设计。完成模块的划分以后,按照先前对每个模块功能预设进行完成,但是每个模块的敏感信号的选择还是很重要的,有些模块程序要在时钟信号上升沿触发,而有些模块要在时钟信号的下降沿触发,有些则将电平信号作为敏感信号,每个模块里面的敏感信号的选择都十分的重要,一开始没有太过注意导致出现了很多的问题,后面重新仔细的想指令的处理过程,重新规定了各个模块always@里面的敏感信号。

其次就是verilog里面的wire和reg两种变量类型,感觉这是比较大的坑。一开始不了解两者的区别,导致后面一堆报错。现在大致的清楚了二者的区别,wire主要起信号间连接的作用,例如顶层模块中,需要将各个模块连接起来,这时候只能用wire连接,不能使用reg,wire不保存状态,它的值的随时可以改变,不受时钟信号的影响,而reg则是寄存器的抽象表达,可以用于存储数值,例如指令寄存器和寄存器组以及数据寄存器里面的存储器必须为reg类型,用于保留数据。其次wire类型只能通过assign进行赋值,而reg类型只能在always里面被赋值,而涉及到always又有阻塞赋值和非阻塞赋值这个大坑,一开始也不知道怎么弄,就混用了,后面也是出现乱七八糟的问题,后面仔细学习了一下,敏感信号为电平信号的时候,采用阻塞赋值(=),而敏感信号为时序信号的时候,采用非阻塞赋值(<=)。

再者就是烧板的时候的消抖问题。一开始没有进行消抖,然后总是按一下运行了几条指令,后面上网学习了一下如何消抖,顺利的解决了该问题。

还有比较疑惑的问题就是使用vivado进行Implemention的时候,有时候进行Running place_design这一部分的时候就一直在此处运行,没有任何进度了,网上也没有合理的解释,然后新新建个项目,将里面的代码复制进去又可以正常的运行了,这个问题目前尚未解决。

本次单周期CPU设计实验,将计组理论课上所讲的指令处理过程自己重复并实现了单周期CPU的设计,加深了CPU处理指令过程理解,之前由于计组理论学的不是特别清楚,本次实验加深了印象,也更加了解每条指令的处理过程以及单周期CPU是如何工作的,同时本次实验也更加了解verilog语言,之前学的懵懵懂懂的,最重要的是学会模块化,将一项工作分成多个模块进行完成,先简化成小部分,然后再将其组合起来。 版权声明:本文为CSDN博主「liuyt49」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处​​链接​​及本声明。