写在前面:在参考别人的博客自己做了一遍单周期cpu后,觉得不是很难,于是自己尝试了做一下多周期cpu,然后被各种bug糊脸。。。果然,自己尝试和有大佬指路还是有很大区别。。。
先把代码链接发上:多周期CPU代码
设计一个多周期CPU,该CPU至少能实现以下指令功能操作。需设计的指令与格式如下:(说明:操作码按照以下规定使用,都给每类指令预留扩展空间,后续实验相同。)
(1)add rd, rs, rt
000000 | rs(5位) | rt(5位) | rd(5位) | reserved |
---|
功能:rd<-rs + rt
(2)sub rd, rs, rt
000001 | rs(5位) | rt(5位) | rd(5位) | reserved |
---|
完成功能:rd<-rs - rt
(3)addi rt, rs, immediate
000010 rs(5位) rt(5位) immediate(16位)
功能:rt<-rs + (sign-extend)immediate
(4)or rd, rs, rt
010000 | rs(5位) | rt(5位) | rd(5位) | reserved |
---|
功能:rd<-rs | rt
(5)and rd, rs, rt
010001 | rs(5位) | rt(5位) | rd(5位) | reserved |
---|
功能:rd<-rs & rt
(6)ori rt, rs, immediate
010010 | rs(5位) | rt(5位) | immediate |
---|
功能:rt<-rs | (zero-extend)immediate
(7)sll rd, rs,sa
011000 | rs(5位) | 未用 | rd(5位) | sa | reserved |
---|
功能:rd<-rs<<(zero-extend)sa,左移sa位 ,(zero-extend)sa
(8)move rd, rs
100000 | rs(5位) | 00000 | rd(5位) | reserved |
---|
功能:rd<-rs + $0
(9) slt rd, rs, rt
100111 | rs(5位) | rt(5位) | rd(5位) | reserved |
---|
功能:如果(rs < rt),则rd=1; 否则 rd=0
(10)sw rt, immediate(rs)
110000 | rs(5位) | rt(5位) | immediate(16位) |
---|
功能:memory[rs+ (sign-extend)immediate]<-rt
(11)lw rt, immediate(rs)
110001 | rs(5位) | rt(5位) | immediate(16位) |
---|
功能:rt <- memory[rs + (sign-extend)immediate]
(12)beq rs,rt, immediate (说明:immediate是从pc+4开始和转移到的指令之间间隔条数)
110100 | rs(5位) | rt(5位) | immediate(16位) |
---|
功能:if(rs=rt) pc <-pc + 4 + (sign-extend)immediate <<2
(13)j addr
111000 | addr[27..2] |
---|
功能:pc <{pc[31..28],addr[27..2],0,0},转移
(14)jr rs
111001 | rs(5位) | 未用 | 未用 | reserved |
---|
功能:pc <- rs,转移
(15)jal addr
111010 | addr[27..2] |
---|
功能:调用子程序,pc <- {pc[31..28],addr[27..2],0,0};$31<-pc+4,返回地址设置;子程序返回,需用指令 jr $31。
(16)halt (停机指令)
111111 | 00000000000000000000000000(26位) |
---|
不改变pc的值,pc保持不变。
多周期CPU指的是将整个CPU的执行过程分成几个阶段,每个阶段用一个时钟去完成,然后开始下一条指令的执行,而每种指令执行时所用的时钟数不尽相同,这就是所谓的多周期CPU。CPU在处理指令时,一般需要经过以下几个阶段:
(1) 取指令(IF):根据程序计数器pc中的指令地址,从存储器中取出一条指令,同时,pc根据指令字长度自动递增产生下一条指令所需要的指令地址,但遇到“地址转移”指令时,则控制器把“转移地址”送入pc,当然得到的“地址”需要做些变换才送入pc。
(2) 指令译码(ID):对取指令操作中得到的指令进行分析并译码,确定这条指令需要完成的操作,从而产生相应的操作控制信号,用于驱动执行状态中的各种操作。
(3) 指令执行(EXE):根据指令译码得到的操作控制信号,具体地执行指令动作,然后转移到结果写回状态。
(4) 存储器访问(MEM):所有需要访问存储器的操作都将在这个步骤中执行,该步骤给出存储器的数据地址,把数据写入到存储器中数据地址所指定的存储单元或者从存储器中得到数据地址单元中的数据。
(5) 结果写回(WB):指令执行的结果或者访问存储器中得到的数据写回相应的目的寄存器中。
实验中就按照这五个阶段进行设计,这样一条指令的执行最长需要五个(小)时钟周期才能完成,但具体情况怎样?要根据该条指令的情况而定,有些指令不需要五个时钟周期的,这就是多周期的CPU。
图1 多周期CPU指令处理过程
MIPS32的指令的三种格式:
R类型:
31-26 | 25-21 | 20-16 | 15-11 | 10-6 | 5-0 |
---|---|---|---|---|---|
op | rs | rt | rd | sa | func |
6位 | 5位 | 5位 | 5位 | 5位 | 6位 |
I类型:
31-26 | 25-21 | 20-16 | 15-0 |
---|---|---|---|
op | rs | rt | immediate |
6位 | 5位 | 5位 | 16位 |
J类型:
31-26 | 25-0 |
---|---|
op | address |
6位 | 26位 |
其中,
op:为操作码;
rs:为第1个源操作数寄存器,寄存器地址(编号)是00000~11111,00~1F;
rt:为第2个源操作数寄存器,或目的操作数寄存器,寄存器地址(同上);
rd:为目的操作数寄存器,寄存器地址(同上);
sa:为位移量(shift amt),移位指令用于指定移多少位;
func:为功能码,在寄存器类型指令中(R类型)用来指定指令的功能;
immediate:为16位立即数,用作无符号的逻辑操作数、有符号的算术操作数、数据加载(Laod)/数据保存(Store)指令的数据地址字节偏移量和分支指令中相对程序计数器(PC)的有符号偏移量;
address:为地址。
图2 多周期CPU状态转移图
状态的转移有的是无条件的,例如从IF状态转移到ID 和 EXE状态就是无条件的;有些是有条件的,例如ID 或 EXE状态之后不止一个状态,到底转向哪个状态由该指令功能,即指令操作码决定。每个状态代表一个时钟周期。
图3 多周期CPU控制部件的原理结构图
图3是多周期CPU控制部件的电路结构,三个D触发器用于保存当前状态,是时序逻辑电路,RST用于初始化状态“000“,另外两个部分都是组合逻辑电路,一个用于产生下一个阶段的状态,另一个用于产生每个阶段的控制信号。从图上可看出,下个状态取决于指令操作码和当前状态;而每个阶段的控制信号取决于指令操作码、当前状态和反映运算结果的状态zero标志等。
图4是一个简单的基本上能够在单周期上完成所要求设计的指令功能的数据通路和必要的控制线路图。其中指令和数据各存储在不同存储器中,即有指令存储器和数据存储器。访问存储器时,先给出地址,然后由读/写信号控制(1-写,0-读。当然,也可以由时钟信号控制,但必须在图上画出来)。对于寄存器组,读操作时,给出寄存器地址(编号),输出端就直接输出相应数据;而在写操作时,在 WE使能信号为1时,在时钟边沿触发写入。图中控制信号功能如表1所示,表2是ALU运算功能表。
特别提示,图上增加IR指令寄存器,目的是使指令代码保持稳定,还有pc增加写使能控制信号pcWre,也是确保pc适时修改,原因都是和多周期工作的CPU有关。ADR、BDR、ALUout、ALUM2DR四个寄存器不需要写使能信号,其作用是切分数据通路,将大组合逻辑切分为若干个小组合逻辑,大延时变为多个分段小延时。
表1 控制信号作用
控制信号名 | 状态“0” | 状态“1” |
---|---|---|
PCWre | PC不更改,相关指令:halt | PC更改,相关指令:除指令halt外 |
ALUSrcB | 来自寄存器堆data2输出,相关指令:add、sub、addi、or、and、ori、move、beq、slt | 来自sign或zero扩展的立即数,相关指令:addi、ori、lw、sw、sll |
ALUM2Reg | 来自ALU运算结果的输出,相关指令:add、sub、addi、or、and、ori、slt、sll、move | 来自数据存储器(Data MEM)的输出,相关指令:lw |
RegWre | 无写寄存器组寄存器,相关指令:beq、j、sw、jr、halt | 寄存器组寄存器写使能,相关指令:add、sub、addi、or、and、ori、move、slt、sll、lw、jal |
WrRegData | 写入寄存器组寄存器的数据来自pc+4(pc4),相关指令:jal,写$31 | 写入寄存器组寄存器的数据来自存储器、寄存器组寄存器和ALU运算结果,相关指令:add、addi、sub、or、and、ori、slt、sll、move、lw |
InsMemRW | 读指令存储器(Ins. Data),初始化为0 | 写指令存储器 |
DataMemRW | 读数据存储器(Data MEM),相关指令:lw | 写数据存储器,相关指令:sw |
IRWre | IR(指令寄存器)不更改 | IR寄存器写使能。向指令存储器发出读指令代码后,这个信号也接着发出,在时钟上升沿,IR接收从指令存储器送来的指令代码。与每条指令都相关。 |
相关部件及引脚说明:
表2 ALU运算功能表
(PS:功能和单周期并不一样)
ALUOp[2..0] | 功能 | 描述 |
---|---|---|
000 | Y = A + B | 加 |
001 | Y = A – B | 减 |
010 | if (A < B)Y = 1; else Y = 0; | 比较A与B |
011 | Y = A >> B | A右移B位 |
100 | Y = A << B | A左移B位 |
101 | Y = A ∨ B | 或 |
110 | Y = A ∧ B | 与 |
111 | Y = A ⊕ B | 异或 |
值得注意的问题,设计时,用模块化的思想方法设计,关于Control Unit 设计、ALU设计、存储器设计、寄存器组设计等等,是必须认真考虑的问题。
首先,进行操作之前,需要先了解什么是多周期cpu:
多周期不是流水线!
多周期不是流水线!
多周期不是流水线!
重要的事情先说三次。
Q:什么是多周期?和单周期区别在哪?
A:单周期是一个大时钟周期(不妨叫指令周期),完成IF,ID,EXE,MEM,WB五个模块。多周期是把这个大的时钟周期,分成五个小的时钟周期,每个时钟周期只执行IF,或ID…..等其中一个小功能。
说这么多,不如来张图吧:
可以看出,其实多周期和单周期执行时间并没有什么区别,只是把一个大时钟周期拆开成了五个时钟周期而已。
所以问题又来了:
Q:单周期cpu改成多周期cpu会有什么提升呢?
A:单周期所有的指令都要按顺序执行IF->ID->EXE->MEM->WB(无论有没有用上)。但多周期可以不用,比如beq指令就只需要IF->ID->EXE,jal指令只需要IF->ID,就可以省很多不必要的时间。
举个例子,让单周期和多周期同时执行j和beq两条指令,结果如下:
时间短了一倍,很明显看得出多周期的优点了。
Q:怎么把一个指令周期变成多个小时钟周期?
A:首先我们得先明白一点:我们仿真模拟设置的时钟周期时间实际上比执行时间大太多,导致其实在时钟上升沿一刹那后,所有结果就计算出来了。所以,要把单周期分成多周期,我们就要强行延长周期,让IF结果在第一个时钟周期完成,让ID结果在第二个时钟周期完成,以此类推。关键的来了,用什么去实现呢?用上升沿触发的寄存器。
Q:寄存器怎么实现延迟时钟周期的效果?
A:读万卷书,不如行万里路。来实验下就知道了:
我先用一个小程序Test来模拟这个效果:(Test代码点我下载)
主模块(把四个上升沿触发的寄存器串联):
module Test(
input CLK,
input value
);
wire value1;
wire value2;
wire value3;
wire value4;
wire value5;
WireToReg r1(CLK, 1, value, value1);
WireToReg r2(CLK, 1, value1, value2);
WireToReg r3(CLK, 1, value2, value3);
WireToReg r4(CLK, 1, value3, value4);
WireToReg r5(CLK, 1, value4, value5);
endmodule
WireToReg(上升沿触发的寄存器):
module WireToReg(
input CLK,
input Enable,
input [31:0] in,
output reg[31:0] out
);
initial
begin
out &