计组-实验-P3
CPU设计
概述
本次我设计的logisim单周期CPU支持21条指令,采用多个模块进行设计。
所采用模块化设计为:
IFU:指令单元,接受指令地址PC来给出当前周期处理的指令GRF:寄存器模块,通过该模块统一进行32个寄存器的读写NPC:计算下一条指令的地址,接受jump,branch等指令来支持跳转操作ALU:进行运算操作,该ALU操作码为4为DM:内存组,通过内置的RAM实现ControlUnit:指令控制单元,通过接受op和func来发出指令,控制数据通路中的MUX

支持指令
支持21条指令,为:
- R指令:
add,addu,sub,or,sll,sllv,slt,sltu - I指令:
ori,lui,beq,sw,lw,sh,sb,lh,lb - J指令:
j,jr,jal,jalr
数据通路模块定义
IFU:指令单元
该模块内部包含PC(程序计数器)和IM(指令存储器)。PC通过logism内置的寄存器实现, IM通过Logisim内置的ROM实现(容量为24*32bit)。可以根据PC的值从IM取出对应的指令。
端口定义
| 信号名 | 方向 | 位宽 | 具体翻译 |
|---|---|---|---|
clk |
I | 1 | 时钟信号 |
reset |
I | 1 | 复位信号,为异步复位 |
PC_next |
I | 32 | 下一条执行指令的地址 |
inStr |
O | 32 | 当前正在执行的指令 |
PC |
O | 32 | 当前正在执行指令的地址 |
功能定义
通过指令地址PC取得指令。
该组件支持异步复位,复位后输出的是地址为0x00003000处的指令。
NPC:指令地址单元
端口定义
| 信号名 | 方向 | 位宽 | 具体翻译 |
|---|---|---|---|
PC |
I | 32 | 当前正在执行指令的地址 |
imm |
I | 26 | 对于J型指令时跳转的偏移量 |
offest |
I | 16 | 对于I型指令跳转时的偏移量 |
if_branch |
I | 1 | 是否进行I型指令的分支跳转 |
if_jump |
I | 1 | 是否进行J型指令的直接跳转 |
rs |
I | 32 | rs寄存器中的值 |
jumpSrc |
I | 1 | 跳转的地址选择是imm还是rs |
PC_next |
O | 32 | 输出计算后得到的下一个PC地址 |
PC+4 |
O | 32 | 输出当前输入PC值加4得到的PC值 |
功能定义
指令一共有三种情况:
- 不跳转,则
PC=PC+4 - 以
beq为代表的跳转,指令计算方式为PC和offest的计算,计算结果为PC_branch - J型指令跳转,指令计算有两种,通过
jumpSrc进行选择,计算结果为PC_jump- 以
j为代表的跳转,指令计算方式为立即数imm和PC的计算 - 以
jr为代表的跳转,指令计算方式为寄存器中储存的地址,即输入的rs寄存器中储存的值
- 以
同时,以为jal,jalr等指令会存储返回的地址,需要输出一个PC+4的地址
GRF:通用寄存器组
采用了P0课下实现的GRF。
端口定义
| 信号名 | 方向 | 位宽 | 具体翻译 |
|---|---|---|---|
clk |
I | 1 | 时钟信号 |
reset |
I | 1 | 复位信号,为异步复位 |
WE |
I | 1 | 写使能信号 |
A1 |
I | 5 | 读取编号1 |
A2 |
I | 5 | 读取编号2 |
A3 |
I | 5 | 写地址 |
WD |
I | 32 | 写数据 |
RD1 |
O | 32 | 输出读取编号1对应寄存器的值 |
RD2 |
O | 32 | 输出读取编号2对应寄存器的值 |
功能定义
实现了寄存器的读写功能,利用了MUX和DUX。
ALU:计算单元
端口定义
| 信号名 | 方向 | 位宽 | 具体翻译 |
|---|---|---|---|
inA |
I | 32 | 输入数据1 |
inB |
I | 32 | 输入数据2 |
op |
I | 4 | 计算类型 |
shift |
I | 5 | 偏移量 |
result |
I | 32 | 计算结果 |
zero |
O | 1 | 计算结果是否为0 |
功能定义
实现了如下操作加、建、与、或、异或、与或、左逻辑移位、右逻辑移位、右算数移位、大小判断操作。对应ALU操作码如下:
| 操作码 | 操作 |
|---|---|
| 0000 | A+B |
| 0001 | A-B |
| 0010 | A and B |
| 0011 | A or B |
| 0100 | A xor B |
| 0101 | ~ ( A OR B ) |
| 0110 | B << shift |
| 0111 | B >> shift |
| 1000 | B >>> shift |
| 1001 | B << 16 |
| 1010 | A < B |
DM:内存
利用内置的RAM模拟了内存,可以对内存中的数据进行读写。支持对字、半字、字节的读写操作。
端口定义
| 信号名 | 方向 | 位宽 | 具体翻译 |
|---|---|---|---|
address |
I | 32 | 访问的内存地址 |
WD |
I | 32 | 写入内存的数据 |
WE |
I | 1 | 写使能 |
clk |
I | 1 | 时钟信号 |
byte |
I | 1 | 是否对字节进行读取 |
half |
I | 1 | 是否对半字进行读写 |
RD |
O | 32 | 读出内存的数据 |
功能定义
使用logisim内置的RAM模拟了内存,支持对字、半字、字节的读写操作。
对于读取:
- RAM为按字索引,故将输入的地址右移两位,得到对应在RAM中的数据,进行读写
- 按字读取,则此时
if_half和if_byte均为低电平,直接输出结果 - 按字节读取,此时
if_byte为高电平,操作逻辑为:- 取输入地址低两位,向左偏移3位得到对应的数据偏移量
shift。即如果低两位为01,代表读写8-15为的数据,需要左移三位。 - 对于读,将从RAM中读出的数据右移
shift位,再截取低8位,得到结果并输出 - 对于写,按照位运算的逻辑,将该地址在RAM中以存储的值
dm和输入数据中对应位的数据进行操作,再存入RAM中。能够实现依赖于RAM的阻塞赋值特性
- 取输入地址低两位,向左偏移3位得到对应的数据偏移量
- 按半字读取,此时
if_half为高电平,实现逻辑与按字节读取类似
Control Unit:控制单元
个人认为是CPU最关键的部分,依靠指令的op和func部分生成控制逻辑,控制电路中的MUX单元,实现具体的运算。
端口定义
| 信号名 | 方向 | 位宽 | 具体翻译 |
|---|---|---|---|
op |
I | 6 | 指令中的op段 |
func |
I | 6 | 指令中的func段 |
regWrite |
O | 1 | GRF写使能信号 |
regDst |
O | 1 | GRF的写入地址是rt还是rd |
regSrc |
O | 1 | GRF的写入数据来源是ALU还是内存 |
regRa |
O | 1 | GRF的写入地址是regSrc还是31 |
aluSrc |
O | 1 | ALU的输入数据2来自rt还是指令中的offest字段 |
extendType |
O | 1 | 对指令中offest字段extand的方式是sign还是zero |
shiftSrc |
O | 1 | ALU的shift输入来自rs还是指令中的shamt字段 |
memWrite |
O | 1 | DM的写使能信号 |
branch |
O | 1 | NPC的if_branch信号 |
jump |
O | 1 | NPC的if_jump信号 |
jumpSrc |
O | 1 | NPC的jumpSrc输入来自指令中的imm字段还是rs |
aluControl |
O | 4 | ALU的操作码 |
功能定义
根据指令的op字段和func字段,生成CPU的控制信号。
具体逻辑不表,给出对应的信号表:
| 0-1 | enable | rt-rd | regDst-31 | alu-mem | regSrc-PC+4 | rt-imm/off | sign-zero | rs-shamt | enable | enable | enable | imm-rs | |||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| op | regWrite | regDst | regRa | regSrc | regPc | aluSrc | extendType | shiftSrc | memWrite | branch | jump | jumpSrc | aluControl | alu | aluOp | func | |
| add | 000000 | 1 | 1 | 0 | 0 | 0 | 0 | x | 0 | 0 | 0 | 0 | x | + | 0000 | 000 | 100000 |
| addu | 000000 | 1 | 1 | 0 | 0 | 0 | 0 | x | 0 | 0 | 0 | 0 | x | + | 0000 | 000 | 100001 |
| sub | 000000 | 1 | 1 | 0 | 0 | 0 | 0 | x | 0 | 0 | 0 | 0 | x | - | 0001 | 000 | 100010 |
| or | 000000 | 1 | 1 | 0 | 0 | 0 | 0 | x | 0 | 0 | 0 | 0 | x | or | 0011 | 000 | 100101 |
| sll | 000000 | 1 | 1 | 0 | 0 | 0 | 0 | x | 1 | 0 | 0 | 0 | x | << | 0110 | 000 | 000000 |
| sllv | 000000 | 1 | 1 | 0 | 0 | 0 | 0 | x | 0 | 0 | 0 | 0 | x | << | 0110 | 000 | 000100 |
| slt | 000000 | 1 | 1 | 0 | 0 | 0 | 0 | x | 0 | 0 | 0 | 0 | x | < | 1010 | 000 | 101010 |
| ori | 001101 | 1 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | x | or | 0011 | 011 | |
| lui | 001111 | 1 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | x | <-16 | 1001 | 100 | |
| sltu | 000000 | 1 | 1 | 0 | 0 | 0 | 0 | x | 0 | 0 | 0 | 0 | x | < | 1010 | 000 | 101011 |
| beq | 000100 | 0 | x | x | x | x | 0 | 0 | 0 | 0 | 1 | 0 | x | - | 0001 | 010 | |
| j | 000010 | 0 | x | 0 | x | 0 | x | x | x | 0 | 0 | 1 | 0 | x | x | x | |
| jr | 000000 | 0 | x | 0 | x | 0 | x | x | x | 0 | 0 | 1 | 1 | x | x | x | 001000 |
| jal | 000011 | 1 | 1 | 1 | 0 | 1 | x | x | x | 0 | 0 | 1 | 0 | x | x | x | |
| jalr | 000000 | 1 | 1 | 0 | x | 1 | x | x | x | 0 | 0 | 1 | 1 | x | x | x | 001001 |
| sw | 101011 | 0 | x | x | x | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | + | 0000 | 001 | |
| sh | 101001 | 0 | x | x | x | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | + | 0000 | 001 | |
| sb | 101000 | 0 | x | x | x | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | + | 0000 | 001 | |
| lw | 100011 | 1 | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | + | 0000 | 001 | |
| lh | 100001 | 1 | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | + | 0000 | 001 | |
| lb | 100000 | 1 | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | + | 0000 | 001 |
内部构成
使用了三个子模块:contorl1、control2、JumpControl。其中,各模块功能为:
-
control1:负责根据op字段生成和func无关的信号control1和control2之间通信的aluOp的含义:aluOp 含义 000 依赖 funct001 A + B 010 A - B 011 A or B 100 B << 16 -
control2:负责根据op和func字段生成其他信号,包括aluControl、shiftSrc -
JumpControl:负责生成和jump相关指令的控制信号,包括regRa、regPc、jumpSrc、jump
CPU测试
主要使用Mars生成测试码,之后手动导入logisim中进行测试,使用了助教提供的jar包测试工具。
自动测试工具
通过python程序将test.asm转化为16进制指令码1.txt,并通过正则表达式匹配得到ROM对应的代码区域,并形成新的电路文件cpu_test.circ。
1 | import os |
测试代码
忘记保存了,总的策略是对$1-$31为都测试一次,利用python进行批量生产数据。利用lui和ori的组合指令可以实现临界数据的生成。
思考题
-
上面我们介绍了通过 FSM 理解单周期 CPU 的基本方法。请大家指出单周期 CPU 所用到的模块中,哪些发挥状态存储功能,哪些发挥状态转移功能。
状态存储模块:IFU
状态转移模块:NPC
-
现在我们的模块中 IM 使用 ROM, DM 使用 RAM, GRF 使用 Register,这种做法合理吗? 请给出分析,若有改进意见也请一并给出。
不完全合理。
- RAM较大的容量满足了DM对内存的模拟,可以满足读写要求;
- GRF是通用寄存器堆,采用分开的寄存器实现可以满足其读写速度;
- 但是ROM的实现有些奇怪,这样的设计使得CPU称为了“一次性CPU”,只能执行特定的命令,不能在程序执行过程中增删指令。作为学习过程中的初级CPU,这样做是可以的,因为无论编写的mips程序还是测评都是单个程序的测试,但是也只能适用于教学,不是符合真实情景的IM设计。
- 至于改进方案,可以适用RAM实现,但在执行过程中对RAM进行指令读入,在logisim中难以实现,在verilog中可以实现。
-
在上述提示的模块之外,你是否在实际实现时设计了其他的模块?如果是的话,请给出介绍和设计的思路。
实现了,见上。
-
事实上,实现
nop空指令,我们并不需要将它加入控制信号真值表,为什么?由于我实现
sll指令,那么nop指令相当于sll $0,$0,0,一方面,左移0位不对原有寄存器造成印象,另一方面,不允许对0寄存器的写操作,故没有影响。即使CPU没有设置
sll指令,nop指令也不会对电路中任何元件进行操作,对电路没有任何影响。 -
阅读 Pre 的 “MIPS 指令集及汇编语言”一节中给出的测试样例,评价其强度(可从各个指令的覆盖情况,单一指令各种行为的覆盖情况等方面分析),并指出具体的不足之处。
- 测试数据过弱,没有很好地覆盖临界情况。
- 测试寄存器过少,没有测试绝大部分的寄存器,只测试了几个寄存器。
- 逻辑过于简单,没有实现
beq嵌套等操作。

