北航计组-P3课下
CPU设计部分
概述
本次笔者用logisim
搭建了一个单周期CPU
,能够处理9条指令,分别为add
sub
ori
lw
sw
beq
lui
nop
,采用模块化和层次化设计,让结构更加清晰明了。所设计的模块如下:
IFU
取指令单元,通过PC
的值来确定当前要执行的指令GRF
通用寄存器组,通过该模块实现对32个通用寄存器的读和写ALU
算术逻辑运算单元DM
数据存储器,通过RAM
实现NPC
计算Next_PCController
控制器,解析funct
和op
从而将相应的控制信号置0、1- 最终效果如图:
数据通路部件
IFU:指令单元
该模块包含PC
和 IM
两部分,分别为程序计数器和指令存储器。
- PC用寄存器直接实现,应具有异步复位功能,复位值为起始地址,地址范围为0x00003000 ~ 0x00006FFF
- IM用
ROM
实现,容量为 4096 * 32 bit.ROM
的0位置对应PC
为 0x00003000的指令 - 不难发现 ROM 实际地址宽度仅需 12 位,这是因为每一条程序占32位,即4byte,每次地址改变是以4byte为最小单位进行的
信号名 | 方向 | 位宽 | 说明 |
---|---|---|---|
clk | I | 1 | 时钟信号 |
reset | I | 1 | 异步复位信号 |
Next_PC | I | 32 | 下一条指令的地址 |
Instr | O | 32 | 正在执行的指令的编码 |
PC | O | 32 | 当前执行指令的地址 |
GRF:通用寄存器堆
该模块P1课下已经实现了,直接用即可,copy copy !(DMX记得开三态噢!😅)
信号名 | 方向 | 位宽 | 说明 |
---|---|---|---|
RA1 | I | 5 | 1_读取的寄存器编号 |
RA2 | I | 5 | 2_读取的寄存器编号 |
WE | I | 1 | 寄存器写入使能端 |
WA | I | 5 | 写入的寄存器编号 |
WD | I | 32 | 写入寄存器的数据 |
reset | I | 1 | 异步复位 |
clk | I | 1 | 时钟信号 |
RD1 | O | 32 | 1_读取的寄存器数据 |
RD2 | O | 32 | 2_读取的寄存器数据 |
ALU:算术逻辑运算单元
端口定义
信号名 | 方向 | 位宽 | 说明 |
---|---|---|---|
inA | I | 32 | 操作数1 |
inB | I | 32 | 操作数2 |
AluCtrol | I | 4 | 运算类型 |
result | O | 32 | 运算结果 |
zero | O | 1 | 结果是否为0 |
运算编码
编码 | 功能 |
---|---|
0000 | A + B |
0001 | A - B |
0010 | A & B |
0011 | A or B |
0100 | 加载立即数至高位 |
DM:数据存储器
- 使用
RAM
实现,容量为 3072 * 32 bit - 使用异步复位功能,复位值为0
RAM
应使用双端口模式
信号名 | 方向 | 位宽 | 说明 |
---|---|---|---|
clk | I | 1 | 时钟信号 |
reset | I | 1 | 异步复位信号 |
address | I | 32 | 内存地址 |
WD | I | 32 | 写入的数据 |
WE | I | 1 | 写入使能 |
RD | O | 32 | 读出的数据 |
EXT:扩展单元
直接使用logisim
自带的extender
即可,但是注意既有符号扩展又有0扩展(such as ori)
信号名 | 方向 | 位宽 | 说明 |
---|---|---|---|
in | I | 16 | 待扩展的信号 |
ExtOp | I | 1 | 扩展方式选择信号(为0则0扩展) |
Out | O | 32 | 扩展的结果 |
NPC:指令地址单元
Next_PC
一共有 3 种情况,分别为 PC + 4、PC + 4 + offest
<< 2 (beq
指令) 、Imm
26 -> Imm
32(j 指令),端口定义如下
信号名 | 方向 | 位宽 | 说明 |
---|---|---|---|
PC | I | 32 | 当前指令地址 |
offest | I | 16 | beq 指令的相对偏移量 |
Imm | I | 16 | j指令的26位跳转地址 |
branch | I | 1 | 是否分支(beq ) |
if_j | I | 1 | 是否j跳转 |
Next_PC | O | 32 | 下一条指令的地址 |
控制器设计
与逻辑模块
先根据指令的opcode
和 funct
来确定具体的指令类型,见下表R指令类型
R型指令 | Opcode | Rs | Rt | Rd | Shamt | Funct |
---|---|---|---|---|---|---|
add rd rs rt | 000000 | rs | rt | rd | XXXXX | 100000 |
sub rd rs rt | 000000 | rs | rt | rd | XXXXX | 100010 |
再来看看I型指令:
I型指令 | Opcode | Rs | Rt | 16 bits offest |
---|---|---|---|---|
ori rt rs Imm | 001101 | rs | rt | Imm16 |
lui rt Imm | 001111 | rs | rt | Imm16 |
lw rt Imm(rs) | 100011 | rs | rt | Imm16 |
sw rt Imm(rs) | 101011 | rs | rt | Imm16 |
beq rs rt Imm | 000100 | rs | rt | Imm16 |
J指令:
J型指令 | Opcode | address |
---|---|---|
J target | 000010 | target_26bit |
Jr $rs | 000000(funct: 001000) | $rs |
Jal target | 000011 | target_26bit & $ra = PC+4 |
当然,也可以加入其他的指令来扩展功能,有了上表,我们就可以来设计与逻辑模块了……
或逻辑模块
在这部分中,我们要根据与逻辑输出的具体指令来指定各个控制信号的高低电平,以此来实现对整个电路的控制。与此同时,这部分要求我们将各条指令的执行过程谙熟于心。
总控信号说明
信号 | 说明 |
---|---|
Reg_Write | 寄存器文件写入使能端 |
DM_Write | 内存区域写入使能端 |
Reg_Dst | 决定被写入寄存器在指令中的位置,0为I型,1为R型 |
MemtoReg | 决定写入寄存器的是Alu结果还是DM取出的数据(if 1 :DM) |
Alu_Op | 决定Alu运算方式,见Alu表 |
Alu_Src | 决定Alu 第二个运算数为 寄存器还是立即数,0为寄存器 |
Ext_Ctrl | 决定扩展方式,0表示0扩展,1为符号扩展 |
Branch | 是否分支,1 as 分支 |
If_Jump | 是否跳转,1 as 为跳转 |
指令对应的控制信号
指令 | Reg_Write | DM_Write | Reg_Dst | MemtoReg | Alu_Op | Alu_Src | Ext_Ctrl | Branch | If_Jump |
---|---|---|---|---|---|---|---|---|---|
add | 1 | 0 | 1 | 0 | 0000 | 0 | 0 | 0 | 0 |
sub | 1 | 0 | 1 | 0 | 0001 | 0 | 0 | 0 | 0 |
ori | 1 | 0 | 0 | 0 | 0011 | 1 | 0 | 0 | 0 |
lui | 1 | 0 | 0 | 0 | 0100 | 1 | 0 | 0 | 0 |
lw | 1 | 0 | 0 | 1 | 0000 | 1 | 1 | 0 | 0 |
sw | 0 | 1 | 0 | 0 | 0000 | 1 | 1 | 0 | 0 |
beq | 0 | 0 | 0 | 0 | 0001 | 0 | 1 | 1 | 0 |
J | 0 | 0 | 0 | 0 | 0000 | x | 0 | 0 | 1 |
Jr | 0 | 0 | 0 | 0 | 0101 | 0 | 0 | 0 | 1 |
Jal | 1 | 0 | 0 | 0 | 0000 | 1 | 0 | 0 | 1 |
思考题
上面我们介绍了通过 FSM 理解单周期 CPU 的基本方法。请大家指出单周期 CPU 所用到的模块中,哪些发挥状态存储功能,哪些发挥状态转移功能。
- 状态存储:IFU、DM、GRF
- 状态转移:NPC、Controller
现在我们的模块中 IM 使用 ROM, DM 使用 RAM, GRF 使用 Register,这种做法合理吗? 请给出分析,若有改进意见也请一并给出。
- 合理,因为IM中的指令是只需要被读取解析的,使用ROM只有读取功能;而DM是数据存储器,需要读出和写入,因此需要使用RAM,同时不能用寄存器,因为我们需要的内存量非常的大,有限的Register无法满足;而GRF用寄存器堆实现则可以实现高速的读写,同时与ALU的输入相连,送操作数或者存储中间变量,使用寄存器快速读写较合理。
在上述提示的模块之外,你是否在实际实现时设计了其他的模块?如果是的话,请给出介绍和设计的思路。
- 我只增加了一个NPC模块,因为自行增加了J指令,PC的计算方式有 PC + 4, PC + 4 + offset<<2 , label,甚至如果加入jr指令的话,还会直接赋值为寄存器的值,考虑到后续扩展问题,引入NPC模块
事实上,实现
nop
空指令,我们并不需要将它加入控制信号真值表,为什么?- 因为
nop
指令相当于sll $0, $0, 0
,对应的操作码为0x0000_0000,因为0移动0位还是0,,并且0号寄存器不能写入,所以无需加入控制信号表。同时该指令有时被用于空循环,有时被编译器用于与体系结构相关的编译优化。
- 因为
阅读 Pre 的 “MIPS 指令集及汇编语言” 一节中给出的测试样例,评价其强度(可从各个指令的覆盖情况,单一指令各种行为的覆盖情况等方面分析),并指出具体的不足之处。
- 数据强度不够,没有涉及int边界附近的较大的数据的测试,没有覆盖边界情况
- 只测试了少数寄存器,并未对所有寄存器的读写进行测试,虽然有些寄存器实际上并不需要读写,如1号寄存器
- 跳转范围不够,没有测试向前跳转、本行跳转,也没有测试嵌套跳转、循环跳转等特殊情况
测试
必看
每年都有奆佬发布评测机,大家多多关注
这里提供几个工具,对于懒得折腾以及没有科学上网工具的同学,直接点击下面的链接下载即可,简单的配置方法我会讲
工具
说明
一个是魔改MARS,能直接输出寄存器和内存的写入情况,与$display
一致,来自Toby-Shi
学长,链接在这里
另一个是logisim的debug工具
,一个是探针可以直接根据指令码显示当前的指令语言,另一个寄存器堆可以直接查看各个寄存器的值,也可以直接修改。
配置方法
魔改MARS
注意在Settings下勾选Ignore Arithmetic Overflow
以及Output Log level 1
,并在Memory Configuration
下选择large text
,这样可以一次运行4000多行mips
代码(如果用不到这么大的测试数据也可以选第二个)
logisim debug辅助工具
以下内容来源于讨论区助教帖子
工具整合 && 主题帖作者:徐俊豪助教
下载工具包,然后在 Logisim 中点击 Project - Load Library - JAR Library,选择刚刚下载的 jar 包,如图
然后在左侧可以看到这两个元件,其中寄存器堆 RegisterFile
效果如下,可以直接看到寄存器的值,也可以直接对寄存器的值进行编辑,同学们在调试的时候可以用它替换自己的寄存器堆来方便地观察寄存器状态
端口定义如下:
- RA1: 5 位读寄存器编号 1
- RA2: 5 位读寄存器编号 2
- WA: 5 位写寄存器编号
- WD: 32 位写入值
- WE: 1 位写使能
- RD1: 32 位读出值 1
- RD2: 32 位读出值 2
- clk(图中未标出,三角形那个):1 位时钟信号
Mips Probe 效果如下,可以直接将 32 位 MIPS 机器码翻译为汇编指令,支持本课程课下用到的全部指令及一些其它指令,同学们可以在调试时使用来查看机器码对应的汇编指令
注意事项
- 该工具的导入会导致测评错误!!!因此强烈建议大家将电路复制一份来导入上述库进行调试
- 课上不会提供该工具