SingleCycleCPU(单周期 CPU)
原理
单周期 CPU
单周期 CPU 指的是一条指令的执行在一个时钟周期内完成,然后开始下一条指令的执行,即一条指令用一个时钟周期完成。电平从低到高变化的瞬间称为时钟上升沿,两个相邻时钟上升沿之间的时间间隔称为一个时钟周期。时钟周期一般也称振荡周期(如果晶振的输出没有经过分频就直接作为CPU的工作时钟,则时钟周期就等于振荡周期。若振荡周期经二分频后形成时钟脉冲信号作为 CPU 的工作时钟,这样,时钟周期就是振荡周期的两倍)
CPU在处理指令时,一般需要经过以下几个步骤;
- 取指令(IF);根据程序计数器PC中的指令地址,从存储器中取出一条指令,同时,PC 根据指令字长度自动递增产生下一条指令所需要的指令地址,但遇到”地址转移”指令时,则控制器把”转移地址”送入 PC,当然得到的“地址”需要做些变换才送入 PC。
- 指令译码(ID);对取指令操作中得到的指令进行分析并译码,确定这条指令需要完成的操作,从而产生相应的操作控制信号,用于驱动执行状态中的各种操作。
- 指令执行(EXE);根据指令译码得到的操作控制信号,具体地执行指令动作,然后转移到结果写回状态。
- 存储器访问(MEM);所有需要访问存储器的操作都将在这个步骤中执行,该步骤给出存储器的数据地址,把数据写入到存储器中数据地址所指定的存储单元或者从存储器中得到数据地址单元中的数据。
- 结果写回(WB);指令执行的结果或者访问存储器中得到的数据写回相应的目的寄存器中。
单周期CPU,是在一个时钟周期内完成这五个阶段的处理。
MIPS 指令的三种格式
指令分解 | 含义 |
---|---|
op | 操作码 |
rs | 只读。第1个源操作数寄存器,寄存器地址(编号)是00000~11111,00~1F |
rt | 可读可写。为第2个源操作数寄存器,或目的操作数寄存器,寄存器地址同上 |
rd | 只写。目的操作数寄存器,寄存器地址同上 |
sa | 位移量(shift amt),移位指令用于指定移多少位 |
funct | 功能码,在寄存器类型指令中(R 类型)用来指定指令的功能与操作码配合使用 |
immediate | 16位立即数,用作无符号的逻辑操作数、有符号的算术操作数、数据加载(Load)/数据保存(Store)指令的数据地址字节偏移量和分支指令中相对程序计数器(PC)的有符号偏移量 |
address | 地址 |
数据通路图
图2是单周期CPU上的数据通路和控制线路图。其中指令和数据存储在不同存储器中,即有指令存储器和数据存储器。访问存储器时,先给出内存地址,然后由读或写信号控制操作。对于寄存器组,先给出寄存器地址,读操作时,输出端就直接输出相应数据;而在写操作时,在 WE 使能信号为1时,在时钟边沿触发将数据写入寄存器。指令执行的结果总是在时钟下降沿保存到寄存器和存储器中,PC 的改变是在时钟上升沿进行的,这样稳定性较好。图中控制信号作用如表1所示,表2是ALU运算功能表。
控制信号名 | 状态”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 MEMd)的输出,相关指令: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]
PCSrc | PC 表达式 | 相关指令 |
---|---|---|
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]
ALUOp | 输出结果逻辑表达式 | 功能描述 |
---|---|---|
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 | Y = (((rega < regb) and (rega[31] == regb[31] )) or ((rega[31] ==1 && regb[31] == 0))) ? 1 : 0 | 比较 A 与 B 带符号 |
111 | Y = A ^ B | 异或 |
相关部件及引脚说明:
- 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
设计
单周期 CPU 设计时,主要参考的流程图是数据通路和控制线路图。从图左边的 PC 模块开始,从左向右,从上到下进行各个模块的设计。
PC(程序计数器)
PC 输出下一指令地址;在时钟上升沿到来时,给出下一条指令的地址,或者在重置信号下降沿时,将下一条指令地址置零。需要注意的一个地方是 halt 指令需将 PC 值维持不变。
- Module PC:
输入:CLK,Reset,PCWre,NextPC
输出:IAddress
Reset | PCWre | PCSrc | NextPC |
---|---|---|---|
0 | X | x | 0 |
1 | 0 | x | 不变 |
1 | 1 | 00 | PC+4 |
1 | 1 | 01 | PC+4+(extend)immediate |
1 | 1 | 10 | {(PC+4) [31:28],address[27:2],0,0} |
1 | 1 | 11 | 不变 |
1 | module PC( |
- Module NextPC
PC 模块中的下一条指令地址,可能来自正常加四指令地址,分支跳转指令地址或 j 指令跳转地址,所以我们设计一个 NextPC 模块(可视为选择器),来辅助选择下一条指令地址。
输入:Reset,PCSrc,PC,Immediate,JPC
输出:NextPC
PCSrc | NextPC |
---|---|
00 | PC + 4 |
01 | PC + 4 + (Immediate << 2) |
10 | JPC |
其他 | PC + 4 |
1 | module NextPC( |
Module JPC
接下来,设计有关 j 指令的指令跳转地址计算。其中,计算公式如下:
PC <- {(PC+4) [31:28], addr[27:2], 2{0} }
输入:PC,IAddress
输出:JPC1
2
3
4
5
6
7
8
9
10
11
12
13
14module JPC(
input [31:0] PC, // PC 地址
input [25:0] IAddress, // 跳转地址
output reg [31:0] JPC // 跳转 PC
);
wire [27:0] temp;
assign temp = IAddress << 2;
always @(PC or IAddress) begin
JPC[31:28] = PC[31:28];
JPC[27:2] = temp[27:2];
JPC[1:0] = 0;
end
endmodule // Jump PC
InstructionMemory(指令存储器)
指令存储器存储着我们需要执行的指令及其对应地址。所以,需要存储指令的存储单元,并在指令读写选择信号的控制下进行指令(IDataIn)的写入或指令的读取。本实验只有读取指令操作。
输入:InsMemRW,IAddress,IDataIn
输出:IDataOut
1 | module InstructionMemory( |
Control Unit(控制单元)
控制单元,根据指令发出针对其余模块的控制信号,以使得其余模块按照指令正常工作。控制信号与指令的真值表如下:
OpCode | PCWre | ALUSrcA | ALUSrcB | DBDataSrc | RegWre | InsMemRW |
---|---|---|---|---|---|---|
000000 | 1 | 0 | 0 | 0 | 1 | 1 |
000001 | 1 | 0 | 1 | 0 | 1 | 1 |
000010 | 1 | 0 | 0 | 0 | 1 | 1 |
010000 | 1 | 0 | 1 | 0 | 1 | 1 |
010001 | 1 | 0 | 0 | 0 | 1 | 1 |
010010 | 1 | 0 | 0 | 0 | 1 | 1 |
011000 | 1 | 1 | 0 | 0 | 1 | 1 |
011011 | 1 | 0 | 1 | 0 | 1 | 1 |
100110 | 1 | 0 | 1 | x | 0 | 1 |
100111 | 1 | 0 | 1 | 1 | 1 | 1 |
110000 | 1 | 0 | 0 | x | 0 | 1 |
110001 | 1 | 0 | 0 | x | 0 | 1 |
111000 | 1 | x | x | x | 0 | 1 |
111111 | 0 | x | x | x | 0 | 1 |
OpCode | mRD | mWR | RegDst | ExtSel | PCSrc | ALUOp |
---|---|---|---|---|---|---|
000000 | 0 | 0 | 1 | x | 00 | 000 |
000001 | 0 | 0 | 0 | 1 | 00 | 000 |
000010 | 0 | 0 | 1 | x | 00 | 001 |
010000 | 0 | 0 | 0 | 0 | 00 | 011 |
010001 | 0 | 0 | 1 | x | 00 | 100 |
010010 | 0 | 0 | 1 | x | 00 | 011 |
011000 | 0 | 0 | 1 | x | 00 | 010 |
011011 | 0 | 0 | 0 | 1 | 00 | 110 |
100110 | 0 | 1 | x | 1 | 00 | 000 |
100111 | 1 | 0 | 0 | 1 | 00 | 000 |
110000 | 0 | 0 | x | 1 | 00(zero=0); 01(zero=1) | 001 |
110001 | 0 | 0 | x | 1 | 00(zero=1); 01(zero=0) | 001 |
111000 | 0 | 0 | x | x | 10 | 010 |
111111 | 0 | 0 | x | x | xx | xxx |
如果每次都输入指令的二进制码,太过繁琐,所以构造了一个 head.v 头文件,将所有二进制码定义为了其对应指令。(具体见代码文件下的 head.v 文件)
输入:OpCode,Zero,Sign
输出:PCWre,ALUSrcA,ALUSrcB,DBDataSrc,RegWre,InsMemRW,mRD,mWR,RegDst,ExtSel,PCSrc,ALUOp
1 | module ControlUnit( |
RegisterFile(寄存器组)
模拟 MIPS 中的32个寄存器,并注意在写入数据时 保护 0 号寄存器 。读取寄存器数据时,无需时钟信号;当写使能信号为 1 且时钟到达下降沿时,将数据写入寄存器。当重置信号为 0 时,重置所有寄存器数据。
输入:CLK,WE,Reset,ReadReg1,ReadReg2,WriteReg,WriteData
输出:ReadData1,ReadData2
1 | module RegisterFile( |
SignZeroExtend(符号位或零拓展)
根据拓展选择信号,选择拓展方式:符号位拓展或零拓展,并对 16 位立即数进行拓展,输出 32 位数。其中,拓展选择信号为 0 时,进行零拓展,为 1 时,进行符号位拓展。
1 | module SignZeroExtend( |
ALU(算逻运算单元)
算术逻辑运算单元,根据控制信号的不同,选择对输入操作数进行不同的算术或逻辑运算,得到结果。算术功能包括加,减和移位,输出包含结果与符号位;逻辑功能包括或,与,比较和异或,输出包含结果与标志位。具体功能与控制信号对应如下表:
ALUOp | 输出结果逻辑表达式 | 功能描述 |
---|---|---|
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 | Y = (((rega < regb) and (rega[31] == regb[31] )) or ((rega[31] ==1 && regb[31] == 0))) ? 1 : 0 | 比较 A 与 B 带符号 |
111 | Y = A ^ B | 异或 |
输入:ALUop,A,B
输出:Zero,Sign,Y
1 | module ALU( |
DataMemory(数据存储器)
数据存储器,采用 8 位一字节,大端模式模拟内存。当读使能信号为 1 时,读取数据;当写使能信号为 1 且时钟到达下降沿时,写入数据。
输入:CLK,mRD,mWR,DAddr,DataIn
输出:DataOut
1 | module DataMemory( |
仿真验证
前往 github 项目下 Documents 的 pdf 进行查看。
心得体会
开始设计 CPU 的时候,感觉无从下手,但在仔细数据通路图后,知晓了 CPU 的设计方法,类似与 面向对象 的设计:将 CPU 的各个模块进行分离,实现每个模块的时候,只关注这个模块本身的输入输出与逻辑功能。设计完成所有模块后,就是设计顶层文件,将模块整合在一起,完成整体 CPU 的设计。
在进行模块设计的时候,感触最深的就是控制单元的设计了。针对不同指令的输入给出真值表。开始的时候,直接在代码文件中使用指令二进制码进行真值选择,然后每次针对一个输出,就需要仔细观察二进制码,避免出错。这样书写实在是太不方便了,于是在网上查找了 Verilog 的文件引入与宏定义资料后,自行设计了 head.v 文件,使用宏将指令二进制码定义在文件中,之后在使用这些指令二进制码的文件中引入该文件并使用宏定义名替代,大大提高了书写代码的准确性,避免 硬编码问题 。
在寄存器组中,传入的是寄存器的地址,虽然代码上看来是数组下标,但应该区分这两者,所以在模块中设计变量名时,对于地址变量,大部分会写明 Address。我们采用的是 MIPS 的指令集,所以寄存器组中有 32 个寄存器。虽然无法做到真的如同 MIPS 中对应寄存器对应用途使用,但 针对 0 号寄存器,进行了保护 :写入数据时,会判断是否为 0 号寄存器,若是,则禁止写入。
第一次仿真的时候,输出全是高阻抗状态或者不确定值。但是指令地址与指令二进制码都与测试文件中相同。进行了模块的重新审视,但未发现问题。最终在顶层模块中发现问题所在:对于一个变量,如果有两个变量同时对其进行赋值,则其值会变为不确定。即,我的顶层模块中,出现了将一个变量作为某一模块的输出,同时将另一个变量赋值给了它。所以直接在顶层模块中,使用大写变量名作为它的输入输出,同时,使用小写变量名重新定义了所有的模块使用到的变量作为局部变量,让局部变量在内部运转,并将局部变量通过 assign 语句实时将值赋值给输出变量。这样之后的仿真输出大部分都正确了。
对于仿真输出确定之后,进行每条指令的 debug。在 j 跳转指令处出现了问题。j 跳转指令使用下面的赋值式:pc <-{(pc+4) [31..28],addr[27..2],2{0}}。从二进制数的方面考虑,25位的 addr 左移两位补零,然后我们截取的长度仍旧是 25 位,所以直接将 addr 赋值即可。但是这么做的话,仿真时跳转的地址就不对了,这个问题不知道在什么地方出错,并没有解决,改为左移后截取则跳转正常。
在烧板的时候,需要处理按键防抖动。但在设计时,开始忘记了处理这个问题,直接采取按键直接取反,但貌似时钟频率取值比较高且符合要求,这么处理的代码在进行验证的时候依旧可以运行。然后在重新查看烧板验证的 pdf 后发现了这个问题,于是通过查询资料得知,按键的防抖动可以采用在时钟上升沿时,对按键正信号或负信号进行取样,如果取样周期达到了预设周期,则输出正信号或负信号,这样可以避免按键信号的抖动问题,且输出的信号则一定是稳定状况下的按键信号。