P7_L0_document

设计草稿

任务清单

新增加指令:

eret:COP0== 010000、func ==011000,PC <- CP0[EPC]

mfc0:COP0== 010000、rs(原来rs的地方)== 00000,GRF[rt] <- CP0[rd]

mtc0:COP0 == 010000、rs(原来rs的地方) == 00100,CP0[rd] <- GRF[rt]

syscall:op == 000000、func == 001100

任务 解释
计时器 课程组提供代码,理解应用即可
系统桥 为CPU提供统一的访问外设的接口,需按规格自行实现
协处理器CP0 设置CPU的异常处理功能,反馈CPU的异常信息,需按规格自行实现
内部异常检测与流水 CPU需可检测内部指令执行错误的能力
外部中断响应 CPU需有初步相应外部中断信号的能力
异常处理指令 异常处理程序中需实现一些特殊的指令
单周期CPU的封装 让CPU从外部看上去是一个单周期CPU
异常处理程序 利用MARS编写的异常处理程序用于测试

P7整体图

上图中Timer、中断发生器和IM、DM都为外设即外部设备,是同CPU地位平等的一组设备。

其中有关异常的处理,采用异常处理流的方式,即发生一些“事件”(内部异常(由于指令执行错误导致,如加法溢出,除法除零等)和外部中断(计时器信号,键盘输入等)),改变程序的原有流向,PC跳转到特定的地址。如下图:

异常处理

这里的异常处理程序,是软件(在CO中表现为一端汇编代码),不属于MIPS微系统内。课下需要自行编写handler(异常处理程序)进行本地测试。

采用“高内聚,低耦合”的接口设计理念,通过设计系统桥实现CPU对于不同外设的接入。系统桥连接CPU和外设的功能设备,提供给CPU接口。P7中,CPU对于DM、Timer和中断发生器的访问都是通过系统桥的。

外设的实现

计时器

一种外设设备,根据设定的时间来定时产生中断信号

模块端口定义:

信号名 方向 描述
clk I 时钟信号
reset I 同步复位信号
Addr[31:2] I Timer写入地址
WE I Timer写入使能
Din[31:0] I Timer写入数据
Dout[31:0] O Timer读取数据
IRQ O 中断请求
中断发生器

抽象简化现实中外设后得到的,在不确定时产生一个中断信号,持续置高,直到微系统响应,才复位。响应是通过系统桥实现,通过store类指令访问地址0x7F20达到响应中断的目的。只需确保微系统可以1.通过外部端口接受外部中断信号(计时器部分已经实现)。2.通过访问地址0x7F20store类指令,改变对应微系统输出信号(m_int_addrm_int_byteen,即系统桥实现正确

系统桥

采用划分地址空间的方式使外设与CPU进行沟通,通过一个CPU视图下的内存地址,读写相应数据即可实现与外设沟通的目的。所谓的内存在外设中只是若干寄存器。系统桥传入对地址的访问请求后,通过系统桥内部的转换代码,将请求转变为相应寄存器的读写操作

规定的地址空间设计:

条目 地址 备注
数据寄存器 0x0000_0000~0x0000_2FFF
指令寄存器 0x0000_3000~0X0000_6FFF
PC初始值 0x0000_3000
异常处理程序入口地址 0x0000_4180
计时器0寄存器地址 0x0000_7F00∼0x0000_7F0B 计时器0的3个寄存器
计时器1寄存器地址 0x0000_7F10∼0x0000_7F1B 计时器1的3个寄存器
中断发生器响应地址 0x0000_7F20∼0x0000_7F23

桥结构

CPU中store类指令需要储存的数据经BE处理后通过m_data_addrm_data_byteenm_data_wdata三个信号输出到桥中,桥根据写使能m_data_addr和地址m_data_addr判断写的是内存还是外设,并且给出正确的写使能。

load类指令则是全部将地址传递给每个外设和DM,桥根据地址选择从应该反馈给CPU从哪里读出来的数据,在DE中处理读出的数据,并反馈正确的结果。

端口定义:

名称 方向 描述
DM_byteen[3:0] O 写入DM的使能信号
DM_rdata[31:0] I 从DM读取的值
TC0_WE O Timer0的写使能信号
TC0_Dout[31:0] I Timer0的读取值
TC1_WE O Timer1的写使能信号
TC1_Dout[31:0] I Timer1的读取值
m_data_addr[31:0] I 从CPU读取的需要进行处理的外设的地址值
m_data_wdata[31:0] I 从CPU读取的需要写入外设的数据
m_data_byteen[3:0] I 从CPU读取的写使能信号
m_data_rdata[31:0] O 从外设读取来的数据
Bridge_out_addr[31:0] O 写入外设的地址
Bridge_out_wdata[31:0] O 写入外设的数据

异常处理流的实现

CP0寄存器

主要完成对异常进行配置和记录异常的信息两个功能。相应需要实现的寄存器如下:

寄存器 编号 功能
SR[31:0] 12 配置异常的功能,**[15:10]为IM域,分别对应6个外部中断,相应位置1表允许中断,置0表禁止。只能通过mtco指令修改,通过修改可以屏蔽一些中断。 位宽1位处为EXL域,任何异常发生时置位,强制进入核心态(即进入异常处理程序)并禁止中断。 位宽0位**处为IE域,全局中断使能,置1表允许中断,置0表禁止中断。
Cause[31:0] 13 记录异常发生的原因和情况。 位宽31位处为BD域,表示发生中断时的指令是否为延迟槽内的指令,置1,则写入EPC的指令应该是跳转那条指令的地址即延迟槽指令地址-4。反之为当前指令的地址。 [15:10]为IP域,6位待决的中断位,对应6个外部中断,相应位置1表有中断,置0表无中断,每个周期会被修改一次,内容来自计时器和外部中断。 **[6:2]**为ExcCode域,异常编码,记录当前发生的异常类型。
EPC[31:0] 14 记录异常处理结束后需要返回的PC值

发生异常时,CPU自动将异常写入CP0相应寄存器(CauseEPC)。异常处理程序会访问相应寄存器,了解异常的信息进行异常处理。

将CP0寄存器模块放置在M级便于及时对由于内部指令产生的中断异常进行处理。

模块端口定义:

端口名称 方向 描述
clk I 时钟信号
reset I 同步复位信号
WE_CP0 I 写使能信号
CP0Addr[4:0] I 寄存器地址
CP0In[31:0] I CP0写入数据
CP0Out[31:0] O CP0读出数据
VPC[31:0] I 受害PC值
BDIn I 是否是延迟槽内的指令
ExcCode[4:0] I 异常类型
HWInt[5:0] I 输入中断信号
EXLClr I 用来复位EXL
EPCOut[31:0] O EPC的值
Req O 进入处理程序请求

异常码(ExcCode)定义:

异常与中断码 助记符与名称 指令与指令类型 描述
0 Int (外部中断) 所有指令 中断请求,来源于计时器与外部中断。
4 AdEL (取指异常) 所有指令 PC 地址未字对齐。
4 AdEL (取指异常) 所有指令 PC 地址超过 0x3000 ~ 0x6ffc
4 AdEL (取数异常) lw 取数地址未与 4 字节对齐。
4 AdEL (取数异常) lh 取数地址未与 2 字节对齐。
4 AdEL (取数异常) lh, lb 取 Timer 寄存器的值。
4 AdEL (取数异常) load 型指令 计算地址时加法溢出。
4 AdEL (取数异常) load 型指令 取数地址超出 DM、Timer0、Timer1、中断发生器的范围。
5 AdES (存数异常) sw 存数地址未 4 字节对齐。
5 AdES (存数异常) sh 存数地址未2字节对齐
5 AdES (存数异常) sh,sb 存Timer寄存器的值
5 AdES (存数异常) store 型指令 计算地址加法溢出。
5 AdES (存数异常) store 型指令 向计时器的 Count 寄存器存值。
5 AdES (存数异常) store 型指令 存数地址超出 DM、Timer0、Timer1、中断发生器的范围。
8 Syscall (系统调用) syscall 系统调用。
10 RI(未知指令) - 未知的指令码。
12 Ov(溢出异常) add, addi, sub 算术溢出。

因此内部的异常总共可能来自F级的取地址异常D级的未知指令和syscall指令E级的算数溢出M级的存取数异常。因此需要在D级流水线寄存器堆、E级流水线寄存器堆、M级流水线寄存器堆都新增ExcCode信号和BDIn信号的流水。

而外部异常中断随时可能发生,因此每一条指令的地址都要是有效地址,即使是由于阻塞产生的空泡nop指令,它的地址值应该是阻塞冲突产生的提供数据的那条指令地址

中断检测时需要判断是否允许中断,并且还需要考虑全局中断使能(IE)和禁止中断进入核心态使能(EXL)。

1
assign IntReg = |(HWInt & IM) & IE & !EXL;

响应中断需要保存PC/跳转/关中断:

1.保存:将PC和ExcCode保存在EPC和Cause中,若当前指令不为延迟槽中指令,则直接写入当前指令的地址,否则写入PC-4。

2.跳转:PC寄存器写入0x0000_4180

3.关中断:EXL置位,防止再次进入响应中断的状态而不跳转到异常处理程序地址。

在异常处理程序处理完成后,通过eret指令,跳转回原来需要执行指令的地址。需要注意的是:**eret并没有延迟槽,即测试数据中可能出现 eret 指令后紧跟另一条非 nop 指令的情况。你的设计应该保证 eret 的后续指令不被执行。则需要在eret指令执行后手动清空延迟槽,产生nop指令的地址应该是EPC中的地址,谨防外部中断的随时到来**。

恢复PC:将EPC中的值写入PC寄存器

开中断:清除EXL,允许中断再次发生。

在异常中断产生并且被允许执行时,跳转地址到0x4180,存入此时的受害EPC,并且将所有流水线寄存器堆的值都清空置0除了PC寄存器(置0x4180

异常中断优先级的问题:

当本级有异常时,传递本级的异常编码。外部中断优先级高于内部异常。

封装成单周期CPU

宏观PC

CP0所在流水级的地址。

流水线寄存器接受的控制信号优先级

复位信号、阻塞信号、刷新信号、请求信号。

优先级:

信号 优先级
reset 同步复位信号,最高级,复位高于一切
Req 次高,中断请求比内部阻塞重要
flush/stall 最低,流水线信号

其中reset正常复位,Req请求信号产生时需要将流水线寄存器堆的PC寄存器置为0x0000_4180来反映宏观PC便于评测。而flush/stall信号不需要改变流水线寄存器堆中的PC寄存器的值和是否是延迟槽指令的判断信号。

测试方案

大致就是分部分进行测试,分为内部异常和外部中断(来不及搓了)的测试

取指异常

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
.text

lui $28, 0
lui $29, 0

# jr PC mod 4 not 0
lui $1, 0
ori $1, $1, 0x3024
lui $2, 0
ori $2, $2, 0x3024
addi $1, $1, 1
jr $1
nop

# jr PC < 0x3000
ori $1, $0, 0x2996
ori $2, $0, 0x3038
jr $1
nop

# jr PC > 0x4ffc
ori $1, $0, 0x4fff
ori $2, $0, 0x3044
jr $1
nop

end:
beq $0, $0, end
nop

.ktext 0x4180 #实际程序中为nop填充
mfc0 $12, $12
mfc0 $13, $13
mfc0 $14, $14
mtc0 $2, $14
eret
ori $1, $0, 0

存取地址异常

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
.text 
ori $28, $0, 0x0000
ori $29, $0, 0x0f00
mtc0 $0, $12

lui $8, 0x7fff
ori $8, $8, 0xffff

lui $9, 0x8000
ori $9, $9, 0x0000

lw $10, 1($8) # 测试对 lw 地址上界溢出的处理
lh $10, 1($8) # 测试对 lh 地址上界溢出的处理
lb $10, 1($8) # 测试对 lb 地址上界溢出的处理
lw $10,-1($9) # 测试对 lw 地址下界溢出的处理
lh $10,-1($9) # 测试对 lh 地址下界溢出的处理
lb $10,-1($9) # 测试对 lb 地址下界溢出的处理

sw $10, 1($8) # 测试对 sw 地址上界溢出的处理
sh $10, 1($8) # 测试对 sh 地址上界溢出的处理
sb $10, 1($8) # 测试对 sb 地址上界溢出的处理
sw $10,-1($9) # 测试对 sw 地址下界溢出的处理
sh $10,-1($9) # 测试对 sh 地址下界溢出的处理
sb $10,-1($9) # 测试对 sb 地址下界溢出的处理
end:
beq $0, $0, end
nop

.ktext 0x4180 #实际用nop填充
mfc0 $12, $12
mfc0 $13, $13
mfc0 $14, $14
addi $14, $14, 4
mtc0 $14, $14
eret
andi $1, $0, 0

计算溢出

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
.text 
ori $28, $0, 0x0000
ori $29, $0, 0x0f00
mtc0 $0, $12

lui $8, 0x7fff
ori $8, $8, 0xffff

lui $9, 0x8000
ori $9, $9, 0x0000

ori $10, 0x0001
lui $11, 0xffff
ori $11, $11, 0xffff

add $12, $10, $8 # 测试 add 上界溢出的情况
add $12, $11, $9 # 测试 add 下界溢出的情况
addi $12, $8, 1 # 测试 addi 上界溢出的情况
addi $12, $9, -1 # 测试 addi 下界溢出的情况
sub $12, $8, $11 # 测试 sub 上界溢出的情况
sub $12, $9, $10 # 测试 sub 下界溢出的情况

end:
beq $0, $0, end
nop

.ktext 0x4180
mfc0 $k0, $14
addi $k0, $k0, 4
mtc0 $k0, $14
eret #跳过该指令
lui $k0, 0

计时器(通过与同伴进行安全对拍)

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
.text
ori $12, $0, 0x0c01
mtc0 $12, $12

ori $1, $0, 500
ori $2, $0, 9

sw $1, 0x7f04($0)
sw $2, 0x7f00($0)
ori $1, $0, 1000
sw $1, 0x7f14($0)
sw $2, 0x7f10($0)

lw $1, 0x7f00($0)
lw $1, 0x7f04($0)
lw $1, 0x7f10($0)
lw $1, 0x7f14($0)

lui $1, 0
lui $2, 0

lw $1, 0x7f00($0)
lw $1, 0x7f04($0)
lw $1, 0x7f10($0)
lw $1, 0x7f14($0)

end:
beq $0, $0, end
nop

.ktext 0x4180 #实际上是用nop填充
mfc0 $13, $13
lui $15, 0x7fff
ori $15, $15, 0xffff
and $13, $13, $15
ori $14, $0, 1024
beq $13, $14, timer0
nop
ori $14, $0, 2048
beq $13, $14, timer1
nop
eret

timer0:
ori $1, $0, 1
sw $0, 0x7f00($0)
eret

timer1:
ori $2, $0, 2
sw $0, 0x7f10($0)
eret

未知指令与系统调用(与同伴进行安全对拍)

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
.text
lui $s0,0x8000
lui $s1,0x7fff
ori $s1,$s1,0xffff
syscall
add $10,$s0,$s0
sub $10,$s0,$s1
addi $10,$s1,10
sw $10,0x1002($0)
sh $10,0x1001($0)
mult $10,$10
lw $10,0x1002($0)
lh $10,0x1001($0)
mult $10,$10
lh $10,0x1001($0)
sub $10,$s0,$s1
addi $10,$s1,10
sw $10,0x1002($0)
sh $10,0x1001($0)
mult $10,$10
sw $10,0x1002($0)
sh $10,0x1001($0)
lw $10,0x1002($0)
lh $10,0x1001($0)
lhu $10,0x1001($0) # 未知指令
mult $10,$10
sh $10,0x1001($0)
add $10,$s0,$s0
sub $10,$s0,$s1
mult $10,$10
add $10,$s0,$s0
sub $10,$s0,$s1
add $10,$s0,$s0
sub $10,$s0,$s1

end:
beq $0, $0, end
nop

.ktext 0x4180
mfc0 $k0, $14
addi $k0, $k0, 4
mtc0 $k0, $14
eret #跳过该指令
lui $k0, 0

延迟槽中指令发生异常

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
.text
ori $28, $0, 0x0000
ori $29, $0, 0x0f00
mtc0 $0, $12

lui $8, 0x7fff
ori $8, $8, 0xffff

lui $9, 0x8000
ori $9, $9, 0x0000

beq $0, $0, lab0
lw $10, 1($8) # 测试处于延迟槽中指令发生计算地址异常
nop
nop

lab0:
nop
nop
bne $0, $8, lab1
sw $10, 1($8)

nop
lab1:
beq $0, $0, lab2
lh $10, 1($8)
lab2:
nop
beq $0, $0, lab3
nop

lab3:
jal lab4
sh $10, 1($8)
nop
lab4:
beq $0, $0, lab4
lb $10, 1($8)
nop
beq $0, $0, lab5
nop
lab6:
jr $ra
sb $10, 1($8)
beq $0, $0, lab7

lab5:
jal lab6
nop

lab7:


beq $0, $0, lab8
lw $10, -1($9)
nop
nop

lab8:
nop
nop
bne $0, $8, lab9
sw $10, -1($9)

nop
lab9:
beq $0, $0, lab10
lh $10, -1($9)
lab10:
nop
beq $0, $0, lab11
nop

lab11:
jal lab12
sh $10, -1($9)
nop

lab12:
beq $0, $0, lab12
lb $10, -1($9)
nop
beq $0, $0, lab13
nop
lab14:
jr $ra
sb $10, -1($9)
beq $0, $0, lab15

lab13:
jal lab14
nop

lab15:

end:
beq $0, $0, end
nop

.ktext 0x4180
mfc0 $k0, $14
addi $k0, $k0, 8
mtc0 $k0, $14
eret #跳过该指令
lui $k0, 0

思考题

1.请查阅相关资料,说明鼠标和键盘的输入信号是如何被 CPU 知晓的?

键盘和鼠标是通过中断请求的方式进行I/O操作的,例如当键盘上按下一个按键时,键盘会发出一个中断信号,该信号经过中断控制器传到CPU,CPU根据不同的中断信号执行不同的中断响应程序,然后进行相应的I/O操作。

2.请思考为什么我们的 CPU 处理中断异常必须是已经指定好的地址?如果你的 CPU 支持用户自定义入口地址,即处理中断异常的程序由用户提供,其还能提供我们所希望的功能吗?如果可以,请说明这样可能会出现什么问题?否则举例说明。(假设用户提供的中断处理程序合法)

1.指定好的地址方便CPU统一划分出固定的区域处理中断异常,进而使CPU的地址空间更加规范,避免了一些不必要的错误的产生。

2.仍然可以实现,只不过需要更改CPU出现异常中断时要跳转到的异常处理程序的地址,再由用户提供的程序对中断异常进行处理。

3.可能会出现由于跳转地址的来回变动带来的CPU普适性降低,因为换个用户需要重新修改相应的异常处理地址。

3.为何与外设通信需要 Bridge?

外设的种类是无穷无尽的,而 CPU 的指令集却是有限的,并不能总是因为新加入了一个外设,就专门为这个外设增加新的 CPU 指令,希望尽管外设多种多样,但是 CPU 可以用统一的方法访问它们。为了实现这个目标,需要系统桥。同时,系统桥使外设同CPU的沟通变得更加便捷,因为CPU并不需要知道具体的数值,只需要知道相应的地址即可。

4.请阅读官方提供的定时器源代码,阐述两种中断模式的异同,并分别针对每一种模式绘制状态移图。

相同之处:允许进行计数时,两种模式都是从初值寄存器获取值进行倒计数的,这两种模式都受到控制寄存器的控制。

不同之处:模式0下的倒计数结束时产生的中断信号将持续有效直至控制寄存器中的中断屏蔽位被设置为0即ctrl[3]被置0时或者Enable位发生改变;而模式1下计数器每次计数循环中仅产生一周期的中断信号,且计数器计数位为0时,计数器会再次被赋初值,计数器继续倒计数,直到ctrl寄存器中的IM即3位和Enable即0位发生改变的时候才终止。

模式0的状态转移图:

1
2
3
4
5
6
7
8
9
10
11
12
origin --> IDLE

IDLE --> (reset) ? IDLE :
(`ctrl[0]) ? LOAD(IRQ <= 1) : IDLE

LOAD --> (reset) ? IDLE : CNT(`count <= `present)

CNT --> (reset || (!reset && !`ctrl[0])) ? IDLE :
(`ctrl[0] && `count > 1) ? CNT(`count <= `count - 1) : INT(`count <= 0 && IRQ <= 1)

INT --> IDLE(`ctrl[0] <= 0)
//状态后紧跟的括号内表示的是该状态转移进行的同时进行的操作

模式1的状态转移图:

1
2
3
4
5
6
7
8
9
10
11
origin --> IDLE

IDLE --> (reset) ? IDLE :
(`ctrl[0]) ? LOAD(IRQ <= 1) : IDLE

LOAD --> (reset) ? IDLE : CNT(`count <= `present)

CNT --> (reset || (!reset && !`ctrl[0])) ? IDLE :
(`ctrl[0] && `count > 1) ? CNT(`count <= `count - 1) : INT(`count <= 0 && IRQ <= 1)

INT --> IDLE(IRQ <= 0)

5.倘若中断信号流入的时候,在检测宏观 PC 的一级如果是一条空泡(你的 CPU 该级所有信息均为空)指令,此时会发生什么问题?在此例基础上请思考:在 P7 中,清空流水线产生的空泡指令应该保留原指令的哪些信息?

1.会发生写入EPC地址不是发生异常时的受害指令的地址,导致在处理完异常后跳转回原指令出错。

2.因此在P7中,清空流水线产生的空泡指令应该保留原指令的地址和是否是延迟槽指令信号的信息。谨防随时产生的中断信号给EPC寄存器写入错误的地址,或者原来是延迟槽的指令发生中断却被误认为不是导致的异常处理完跳转回延迟槽中的情况发生。

6.为什么 jalr 指令为什么不能写成 jalr $31, $31

如果jalr指令的延迟槽内指令发生异常中断,在处理完异常中断回跳时,$31寄存器的值已经变为了PC+4,发生了改变,无法回到原来需要回跳的地址了。

7. 请详细描述你的测试方案及测试数据构造策略。

测试方案大致是分不同模块进行,主要是进行内部异常和外部中断的测试。

其中对于内部异常又分为取指异常、溢出异常、未定义指令异常等多种,再根据上述异常种类进行有针对性且覆盖率较高的手搓测试数据,再通过同Mars对拍或者同同伴安全对拍(虽然也存在一起错的风险)来定位以及修复bug。

对于外部中断,通过改造tb代码并且在异常处理程序中进行相对应的中断异常处理来模拟CPU对于中断的响应,其中中断发生器发生的中断信号,通过tb代码中宏观PC的变化来产生相对应的中断信号,并通过sb $0, 0x7f20($0)指令来响应。而Timer的中断信号则通过执行代码内部的死循环来达到触发中断的方式,通过ori $1, $0, 9sw $1, 0x7f00($0)sw $1, 0x7f10($0)来分别响应Timer0和Timer1发生的中断信号。再通过同同伴安全对拍的方式来确保相应响应中断的方式是否有误(不过建议多找几个同伴对拍,不然大有出现一起错的情况)。