单周期 CPU 设计与实现
单周期CPU设计与实现
实验内容:学校资料
设计一个单周期CPU,该CPU至少能实现以下指令功能操作。指令与格式如下:
==> 算术运算指令
1. add rd , rs, rt (说明:以助记符表示,是汇编指令;以代码表示,是机器指令)
000000 | rs(5位) | rt(5位) | rd(5位) | reserved |
---|
功能:rd←rs + rt。reserved为预留部分,即未用,一般填“0”。
2. addi rt , rs ,immediate
000001 | rs(5位) | rt(5位) | immediate(16位) |
---|
功能:rt←rs + (sign-extend)immediate;immediate符号扩展再参加“加”运算。
3. sub rd , rs , rt
000000 | rs(5位) | rt(5位) | rd(5位) | reserved |
---|
功能:rd←rs - rt
==> 逻辑运算指令
4. ori rt , rs ,immediate
010000 | rs(5位) | rt(5位) | immediate(16位) |
---|
功能:rt←rs | (zero-extend)immediate;immediate做“0”扩展再参加“或”运算。
5. and rd , rs , rt
010001 | rs(5位) | rt(5位) | rd(5位) | reserved |
---|
功能:rd←rs & rt;逻辑与运算。
6. or rd , rs , rt
010010 | rs(5位) | rt(5位) | rd(5位) | reserved |
---|
功能:rd←rs | rt;逻辑或运算。
==>移位指令
7. sll rd, rt,sa
011000 | 未用 | rt(5位) | rd(5位) | sa | reserved |
---|
功能:rd<-rt<<(zero-extend)sa,左移sa位 ,(zero-extend)sa
==>比较指令
8. slti rt, rs,immediate 带符号
011011 | rs(5位) | rt(5位) | immediate(16位) |
---|
功能:if (rs <(sign-extend)immediate) rt =1 else rt=0, 具体请看表2 ALU运算功能表,带符号
==> 存储器读/写指令
9. sw rt ,immediate(rs) 写存储器
100110 | rs(5位) | rt(5位) | immediate(16位) |
---|
功能:memory[rs+ (sign-extend)immediate]←rt;immediate符号扩展再相加。即将rt寄存器的内容保存到rs寄存器内容和立即数符号扩展后的数相加作为地址的内存单元中。
10. lw rt , immediate(rs) 读存储器
100111 | rs(5位) | rt(5位) | immediate(16位) |
---|
功能:rt ← memory[rs + (sign-extend)immediate];immediate符号扩展再相加。
即读取rs寄存器内容和立即数符号扩展后的数相加作为地址的内存单元中的数,然后保存到rt寄存器中。
==> 分支指令
11. beq rs,rt,immediate
110000 | rs(5位) | rt(5位) | immediate(16位) |
---|
功能: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
110001 | rs(5位) | rt(5位) | immediate(16位) |
---|
功能:if(rs!=rt) pc←pc + 4 + (sign-extend)immediate <<2 else pc ←pc + 4
特别说明:与beq不同点是,不等时转移,相等时顺序执行。
==>跳转指令
13. j addr
111000 | addr[27…2] |
---|
功能:pc <-{(pc+4)[31..28],addr[27..2],2{0}},无条件跳转。
说明:由于MIPS32的指令代码长度占4个字节,所以指令地址二进制数最低2位均为0,将指令地址放进指令代码中时,可省掉!这样,除了最高6位操作码外,还有26位可用于存放地址,事实上,可存放28位地址了,剩下最高4位由pc+4最高4位拼接上。
==> 停机指令
14. halt
111111 | 00000000000000000000000000(26位) |
---|
功能:停机;不改变PC的值,PC保持不变。
实验原理:
单周期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:为地址。
单周期CPU数据通路和控制线路图:
控制信号名 | 状态“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数据通路图进行划分,主要依据数据通路图进行划分太冗余,因此将一些数据选择器合并进了部分功能模块中,实现简化。模块划分结果如图三所示。
pcAdd
模块功能:根据控制信号PCSrc,计算获得下一个pc以及控制信号Reset重置。
实现思路:首先先决定何时引起触发,决定敏感变量,该模块选择将时钟的下降沿以及控制信号Reset的下降沿作为敏感变量,主要是为了能够确保下一条pc能够正确得到。
- 主要实现代码:
1 | `timescale 1ns / 1ps |
PC
模块功能:根据控制信号PCWre,判断pc是否改变以及根据Reset信号判断是否重置
实现思路:将时钟信号的上升沿和控制信号Reset作为敏感变量,使得pc在上升沿的时候发生改变或被重置。
主要实现代码:
1 | `timescale 1ns / 1ps |
InsMEM
模块功能:依据当前pc,读取指令寄存器中,相对应地址的指令
实现思路:将pc的输入作为敏感变量,当pc发生改变的时候,则进行指令的读取,根据相关的地址,输出指令寄存器中相对应的指令
- 主要实现代码:
1 | `timescale 1ns / 1ps |
InstructionCut
模块功能:对指令进行分割,获得相对应的指令信息
实现思路:根据各种类型的指令结构,将指令分割,得到相对应的信息
- 主要实现代码:
1 | `timescale 1ns / 1ps |
ControlUnit
模块功能:控制单元,依据指令的操作码(op)以及标记符(ZERO),输出PCWre、ALUSrcB等控制信号,各控制信号的作用见实验原理的控制信号作用表(表3),从而达到控制各指令的目的.
- 主要实现代码:
1 | `timescale 1ns / 1ps |
RegisterFile
模块功能:寄存器组,通过控制单元输出的控制信号,进行相对应的读或写操作
- 主要实现代码:
1 | `timescale 1ns / 1ps |
ALU
模块功能:算术逻辑单元,对两个输入依据ALUOp进行相对应的运算
实现思路:依据实验原理中的ALU运算功能表(表2)完成操作码对应的操作
- 主要实现代码:
1 | `timescale 1ns / 1ps |
DataMEM
模块功能:数据存储器,通过控制信号,对数据寄存器进行读或者写操作,并且此处模块额外合并了输出DB的数据选择器,此模块同时输出写回寄存器组的数据DB。
- 主要实现代码:
1 | `timescale 1ns / 1ps |
SignZeroExtend
模块功能:根据指令相关的控制信号ExtSel,对立即数进行扩展。
实现思路:根据控制信号ExtSel判断是0扩展还是符号扩展,然后进行相对应的扩展
- 主要实现代码:
1 | `timescale 1ns / 1ps |
顶层模块:SingleCycleCPU
- 实现思路:在顶层模块中将各个已实现的底层模块进行实列,并且用verilog语言将各个模块用线连接起来
- 代码
1 | `timescale 1ns / 1ps |
CPU正确性的验证
仿真程序:
1 | `timescale 1ns / 1ps |
程序代码测试
地址 | 汇编程序 | 指令代码 | |||
---|---|---|---|---|---|
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语言,之前学的懵懵懂懂的,最重要的是学会模块化,将一项工作分成多个模块进行完成,先简化成小部分,然后再将其组合起来。