P3课下

设计草稿

设计要求

处理器为32位单周期处理器,支持:add(000000 rs(5) rt(5) rd(5) 00000 100000), sub(000000 rs rt rd 00000 100010), ori(001101 rs rt imm), lw(100011 base(5) rt offset), sw(101011 base rt offset), beq(000100 rs rt offset(16)), lui(001111 00000 rt immediate)(将立即数加载至高位), nop(0x00000000)

对于每一条指令的执行,应该可以拆分成取指令、译码、执行、访存和回写5个主要的步骤。

模块规格

IFU(取指令单元)

模块端口:

信号名 方向 描述
clk I 时钟信号
reset I 异步复位信号,将PC寄存器的值复位为起始地址0x00003000(即将PC置0:其中1:复位;0:无效。)
NPC I 32位,代表下一条指令的地址,读入后用PC寄存器存储起来
PC O 32位,代表当前指令的地址,输出给NPC模块来生成下一条指令的地址
Data O 32位,当前指令的机器码

模块功能定义:

序号 功能名称 描述
1 复位 reset有效时,将PC寄存器复位为起始地址
2 取指令 通过当前PC值读取并输出当前指令

IFU模块

GRF(寄存器单元)

模块端口:

信号名 方向 描述
clk I 时钟信号
reset I 复位信号,将32个寄存器中的值全都清零,其中,1:复位;0:无效
WE I 写使能信号,1:可向GRF中写入数据;0:不可写入
A1 I 5为地址输入信号,指定32个寄存器中一个,将其中存储的数据读出到RD1
A2 I 5为地址输入信号,指定32个寄存器中一个,将其中存储的数据读出到RD2
A3 I 5为地址输入信号,指定32个寄存器中一个作为写入的目标寄存器
WD I 32位数据输入信号
RD1 O 输出A1指定的寄存器中的32位数据
RD2 O 输出A2指定的寄存器中的32位数据

模块功能定义:

序号 功能名称 描述
1 复位 reset信号有效时,所有寄存器存储的数据清零。
2 读数据 读出A1,A2地址对应寄存器中所存储的数据到RD1,RD2中。
3 写数据 当WE有效且时钟上升沿来临时,将WD写入A3对应寄存器中。

GRF模块

NPC(指令转移单元)

模块端口:

信号名 方向 描述
PC I 32位,当前指令的地址
ifBeq I 1位,表示当前指令是否为beq
zero I 1位,表示是否满足相等关系
imm I 16位,16位立即数
NPC O 32位,下一条指令的地址

模块功能定义:

序号 功能名称 描述
1 非跳转指令的PC自增 对于非跳转指令,需要通过PC自加4完成向下一条指令的转移
2 跳转指令的PC跳转 针对类如beq等跳转指令的在PC自增功能的基础下的PC地址偏移即将再加上立即数左移两位后符号扩展成32位的结果,进而实现PC的跳转

NPC模块

DM(数据寄存器)

模块端口:

信号名 方向 描述
WD I 32位,写入数据
A I 32位,地址选择信号
RE I 1位,是否读出信号
WE I 1位,是否写入信号
clk I 时钟信号
reset I 复位信号
RD O 输出信号

模块功能定义:

序号 功能名称 描述
1 读数据 RE有效时,地址选择信号A选择的存储字放在RD输出总线上输出
2 写数据 WE有效且时钟信号上升沿到来时,WD上数据被写入地址A选择的存储单元中
3 复位 reset有效时,将RAM地址复位为起始地址0x00000000

DM模块

Controller

模块端口定义:

信号名 方向 描述
op I 6位,区分非R型指令的标识
func I 6位,区分R型指令的标识
RegWrite O 1位,表示是否要将结果写回寄存器中
MemWrite O 1位,表示是否要将结果写入DM中
ifSignExt O 1位,表示立即数是否进行符号扩展
ALUctr O 2位,表示ALU将进行什么类型的运算操作
ifBeq O 1位,表示当前指令是否为beq
MemToReg O 1位,表示在写使能端有效前提下判断是否从DM读取数据写入寄存器中,其余情况均是从ALU运算得到结果写入寄存器中
RegDst O 1位,表示回写的寄存器是否为rd
ALUsec O 1位,表示ALU进行操作的第二个操作数是否是立即数

输入与输出之间的关系表:

func 100000 100010 n/a n/a n/a n/a n/a 000000
op 000000 000000 001101 001111 100011 101011 000100 000000
variate add sub ori lui lw sw beq nop
RegDst 1 1 0 0 0 0 0 0
ALUsec 0 0 1 1 1 1 0 0
MemToReg 0 0 0 0 1 0 0 0
RegWrite 1 1 1 1 1 0 0 0
MemWrite 0 0 0 0 0 1 0 0
ifBeq 0 0 0 0 0 0 1 0
ifSignExt 0 0 0 0 1 1 0 0
ALUctr[0] (add(00))0 (sub(01))1 (or(10))0 (lui(11))1 (add(00))0 (add(00))0 (sub(01))1 0
ALUctr[1] 0 0 1 1 0 0 0 0

Controller

ALU

模块端口定义:

信号名 方向 描述
ALUctr I 2位,进行I1和I2的相关运算
I1 I 32位,操作数一
I2 I 32位,操作数二
Out O 32位,ALU计算结果
zero O 1位,判断减操作的结果是否为0

模块功能定义:

序号 功能名称 描述
1 加操作 ALUctr为00时进行I1和I2的加操作
2 减操作 ALUctr为01时进行I1-I2的操作
3 或操作 ALUctr为10时进行I1|I2的操作
4 加载到高位的操作 ALUctr为11时进行将imm加载到高位

ALU模块

测试方案

首先先根据所给样例和Pre提供的测试样例进行相应的测试(采用内存对照和实时查看指令名称及对应寄存器变化的方式),再加强Pre样例的覆盖面并通过上述方法再次进行测试。并且还参考了讨论区中大佬提供的评测机加上随机生成的一系列测试数据对拍完成了相应的测试。

加强版Pre样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
.text
ori $a0, $0, 123 #$a0 = 123
ori $a1, $a0, 456
ori $0, $a0, 456
ori $a0, $0, 1
ori $a0, $a0, 2
ori $a0, $0, 65534
ori $a0, $a0, 42528
lui $a2, 123
lui $a3, 0xffff
lui $0, 13
lui $a2, 2
lui $a2, 65535
lui $a2, 25779
ori $a0, $0, 1
ori $a1, $0, 2
loop:
sub $a1, $a1, $a0
beq $a1, $a0, loop
ori $a3, $a3, 0xffff
add $s0, $a0, $a2
add $s1, $a0, $a3
add $s2, $a3, $a3
add $a3, $a3, $a3
add $a3, $a3, $a3
sub $s0, $a0, $a2
sub $s1, $a0, $a3
sub $s2, $a3, $a3
sub $a3, $a3, $a3
sub $a3, $a3, $a3
ori $t0, $0, 0x0000
sw $a0, 0($t0)
sw $a1, 4($t0)
sw $a2, 8($t0)
sw $a3, 12($t0)
ori $a0, $0, 8
add $t0, $t0, $a0
sw $a3, -4($t0)
lw $a2, -8($t0)
sub $t0, $t0, $t0
sub $t0, $0, $a0
sw $a3, 8($t0)
lw $a2, 8($t0)
ori $t0, $0, 0
sw $s0, 16($t0)
sw $s1, 20($t0)
sw $s2, 24($t0)
lw $a0, 0($t0)
lw $a1, 12($t0)
sw $a0, 28($t0)
sw $a1, 32($t0)
sw $t0, 32($a3)
lw $0, 0($t0)
lw $0, 4($t0)
loop4:
ori $a0, $0, 1
ori $a1, $0, 2
ori $a2, $0, 1
loop3:
beq $a0, $a1, loop3
beq $a0, $a1, loop
beq $a0, $a1, loop1
beq $a0, $a2, loop2
loop1:sw $a0, 36($t0)
loop2:sw $a1, 40($t0)

随机生成数据代码(改编自向巨):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import random #导入随机生成库
import time #导入时间模块

if __name__ == '__main__': #判断当前python模块是否是独立的脚本
for fi in range(10):
random.seed(time.time()) #设置固定随机数种子为当前时间的时间戳
lenth = 300 #单个文件的最多指令数
labelCount = 0 #label的个数
file = open(f"C:\\Users\\yumo\\Desktop\\logisim_tester\\tools\\result{fi}.asm","w") #打开相应文件,并将首指针传给file
file.write(".text\n") #首行写入.text标志着代码段的开始
for i in range(lenth): #循环300次生成300条指令
tmp = random.randint(0,8) #随机生成一个选择8条指令中其中一条的标识数
num0 = random.randint(0,31)
while (num0 == 1):
num0 = random.randint(0,31)
#虽然有概率死循环,但是这样的num0可以避免写入寄存器$1中,防止对$at寄存器的修改,但是目前的实现好像还不用管这些?(未经)

match (tmp):
case 0:
file.write(f"add ${num0},${random.randint(0,31)},${random.randint(0,31)}\n") #生成add指令
case 1:
file.write(f"sub ${num0},${random.randint(0,31)},${random.randint(0,31)}\n") #生成sub指令
case 2:
file.write(f"ori ${num0},${random.randint(0,31)},{random.randint(0,65535)}\n") #生成ori指令
case 3:
file.write(f"lw ${num0},{random.randint(0,3071)<<2}($0)\n") #生成lw指令,lw指令原本应为:lw $rt, offset($rs),这里为了避免出现地址偏移出现超出给定范围的情况,采用在给定地址范围上随机产生offset,$rs赋为$0,则本质上地址偏移量是在给定地址范围上,符合规定,只是缺少了offset为负数,$rs为正负数的情况的指令判断。
case 4:
file.write(f"sw ${random.randint(0,31)},{random.randint(0,3071)<<2}($0)\n") #原理和生成依据与lw指令类似
case 5:
if(labelCount >= 1):
num1 = random.randint(0,31)
file.write(f"ori ${num0},${num1},{random.randint(0,65535)}\n")
file.write(f"beq ${num0},$0,label{random.randint(0,labelCount-1)}\n") #在跳转指令前跟上ori指令改变相应判断的情况,避免死循环的出现
case 6:
file.write(f"lui ${num0},{random.randint(0,65535)}\n") #写入lui指令
case 7:
file.write(f"nop\n") #写入nop
case 8:
file.write(f"label{labelCount}:\n")
labelCount += 1 #写label


file.close()

思考题

1.上面我们介绍了通过 FSM 理解单周期 CPU 的基本方法。请大家指出单周期 CPU 所用到的模块中,哪些发挥状态存储功能,哪些发挥状态转移功能。

在”取指令”的环节,即依据当前地址值取出指令并计算出下一条指令的地址的过程中,用到了NPC模块和IFU模块,这两者组成一个Moore型有限状态机,而其中IFU模块中的PC寄存器起到存储并稳定输出当前指令的地址值的状态存储功能,NPC模块通过当前PC寄存器的值生成下一条指令的地址,起到状态转移功能。

在译码、执行、访存和回写的环节,用到了Controller、ALU、GRF、DM等模块,这四者根据ALU的输出或者DM的读出数据形成了Mealy型有限状态机。其中Controller模块完成了译码的功能,得到当前具体的指令以及操作;GRF用于寄存器的状态存储、改变和输出功能;ALU起到了运算的功能,得到了当前指令操作的运算值;DM起到了RAM的状态存储、改变和输出功能。因此其中,Controller、GRF和ALU共同发挥了状态转移的功能,而GRF和DM分别发挥了状态存储功能。

2.现在我们的模块中 IM 使用 ROM,DM 使用 RAM,GRF 使用 Register,这种做法合理吗? 请给出分析,若有改进意见也请一并给出。

合理。因为IM模块的功能是依据当前指令的PC地址值,取出当前指令;DM模块的功能是依据给定的地址值将给定寄存器中的数据写入或者输出数据写入给定寄存器;GRF的功能是进行寄存器的状态存储、改变和输出功能。其中对IM存储的数据的要求是一旦写入就不再改变,因为一个程序的全部指令是在运行之前就全部写入的了,不会出现运行过程中还能添加指令的情况,因此使用只能读取的ROM是十分合理的,观察到Logisim内置的RAM在Data Interface状态为One asynchronous load/store portsel端口输入始终为高电平时,应该同样可以完成依据地址取相应的指令,不过不知道评测机写入时是否能够成功写入?;对于DM中的数据应该支持多地址存储、写入和输出的功能,而使用只能ROM无法实现写入功能,Register无法实现多地址存储功能,因此使用RAM是最合理的;对于GRF中的数据有单地址存储、单地址写入和双地址输出的需求,因此使用32个Register可以很好的满足需求,而ROM并不支持写入的功能,但是原本“感觉”使用两个Data Interface状态为Separate load and store ports的RAM,利用RAM的32个32位地址作为相对应寄存器,写入数据时两个RAM同时写入,写出时两个RAM分别输出也可以达到同样的效果,对于复位功能采用以复位信号作为选择端将0同写入数据作为输入端进行多路选择来达到复位功能,实践证明两个RAM并不能实现读入的同时也输出两个RAM 因此RAM也不能很好的实现GRF的需求,因此使用Register构造GRF是很合理的。

3.在上述提示的模块之外,你是否在实际实现时设计了其他的模块?如果是的话,请给出介绍和设计的思路。

还设计了NPC(指令转换的模块),就如同设计草稿中的对于NPC模块的定义,主要实现的是正常的指令地址转移(PC值自增4)和跳转指令的转移(在PC值自增4的基础上进行地址偏移的计算与叠加)。

4.事实上,实现 nop 空指令,我们并不需要将它加入控制信号真值表,为什么?

因为nop指令的机器码为0x00000000,且nop指令下CPU并不进行任何操作,那么对于nop空指令来说,加入控制信号表后,控制部件的输出全为0,而不加入nop指令时,控制部件的输出也全为0,这样一来加入不加入并不影响最终的输出,因此考虑到资源的节省以及电路的构造简约性,并不需要将其加入控制信号真值表。

5.阅读 Pre 的“MIPS 指令集及汇编语言”一节中给出的测试样例,评价其强度(可从各个指令的覆盖情况,单一指令各种行为的覆盖情况等方面分析),并指出具体的不足之处。

测试样例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
ori $a0, $0, 123
ori $a1, $a0, 456
lui $a2, 123 # 符号位为 0
lui $a3, 0xffff # 符号位为 1
ori $a3, $a3, 0xffff # $a3 = -1
add $s0, $a0, $a2 # 正正
add $s1, $a0, $a3 # 正负
add $s2, $a3, $a3 # 负负
ori $t0, $0, 0x0000
sw $a0, 0($t0) #$a0为正
sw $a1, 4($t0) #$a1为正
sw $a2, 8($t0) #$a2为正
sw $a3, 12($t0) #$a3为负
sw $s0, 16($t0) #$s0为正
sw $s1, 20($t0) #$s1为正
sw $s2, 24($t0) #$s2为负
lw $a0, 0($t0)
lw $a1, 12($t0)
sw $a0, 28($t0)
sw $a1, 32($t0)
ori $a0, $0, 1
ori $a1, $0, 2
ori $a2, $0, 1
beq $a0, $a1, loop1 # 不相等
beq $a0, $a2, loop2 # 相等
loop1:sw $a0, 36($t0)
loop2:sw $a1, 40($t0)

测试样例是针对支持oriluiaddswlwbeq这些指令的CPU设计的。

其中第1、2、5、9、21、22和23是针对ori指令设计的,测试了一些正常范围内的立即数以及大于0附近的立即数的或运算,没有测试一些边界数据,如小于0附近的数据、16位无符号数边界附近的数,且没有考虑到目标寄存器是$0的情况。

第3、4行测试了lui指令,同样未考虑到一些边界数据,覆盖效果较差。

第6、7、8行测试了add指令,计算了正正相加、正负相加和负负相加的情况,但是同样并没有考虑一些边界数据的测试。且没有针对sub指令的测试,指令覆盖不够全面。

第10、11、12、13、14、19和20行是针对sw的测试,其中从寄存器写入内存的值覆盖了正负值,但没有考虑0,以及一些边界数据,且都是顺序写入,既没有考虑复写,也没有考虑偏移为负数或0的情况,$base寄存器中的值全为0,没有覆盖正负数的情况。

第17、18行是针对lw指令的测试,同样offset只考虑了0和整数的情况,没考虑负数;$base只考虑了0的情况,没考虑正负数;且目标寄存器没有考虑$0这个永远为0的情况。

第24、25、26、27行是针对beq指令的测试,只考虑了不跳转且目标在跳转指令之后和跳转且目标在跳转指令之后的情况,并未考虑跳转,且目标在此跳转指令之前;跳转,且目标是此跳转指令;不跳转,且目标在此跳转指令之前;不跳转,且目标是此跳转指令这四种情况。

P3课上

第一道题(增加cwp指令)

仔细认真增加相应控制信号并完成相应连线即可。

第二道题(增加bgc指令)

将GPR[rs]的从0~31的每二位转换为格雷码后计算其中1的个数再通过同rt的零扩展比较完成相应的跳转:

主要困难的地方在于格雷码转换并得到1的个数:

2位格雷码转换先封装一个模块单独处理:

1.把转换后的每一位扩展成32位后相加:

1)整31个加法器(如果头脑混乱的话,这就是考场最佳做法

2)整两个2位相加、两个4位相加等模块减少单一模块的多次复制

2.使用Bit Adder可以直接得出输入的二进制形式中的1的个数

第三道题(增加lwso指令)

文字过于绕直接上伪代码:

#lwso rs, rt, offset
h_type <- GPR[rt]3…0

reg_right <- (GPR[rt] >> h_type)4…0

reg_left <- GPR[rt] << h_type)4…0

memdata <- (GPRrs] + sign_ext(offset))

if memdata0

GPR[reg_left] <- memdata

else

GPR[reg_right] <- memdata`

需要注意:控制信号要基本上跟lw一致(因为本质上lwsolw都是将内存中的数load进入寄存器中),再增加一个ifLWSO指令来判断当前指令是否为lwso作为多加的写入判断的多路选择器的选择端。其中就顺其自然,连线不要连错,完全按RTL要求来即可。

注意

增加指令一定要充分考虑原有指令的适配性,例如增加R型指令一定要注意R型指令的op是000000,func代表对应指令

此外,如果要更改电路外观一定要注意连线与布局的相对应(不然大有可能被硬控一节课)。