计算机组成实验 P7 回顾
前言
做完前六 P 之后,我们已经做出来一个相当厉害的流水线了。在 P7 中,我们的任务是增加外部中断响应和异常处理。
本篇博客中我将结合我做 P7 的方法,按照我的搭建过程逐个讲解 P7 中所要完成的工作。
注:本篇博客记录的是 21 级的 P7 搭建过程。计组课程组每年都可能会对细节有一些改动。因此建议自行甄别内容。最终实现细节还请以教程为准。
调整模块架构
在 P7 中,我们需要对流水线的架构做一些调整。我们需要添加系统桥和两个计时器,这三个组件和我们在 P6 中搭建的 CPU 在同一级别。也就是这样的:
系统桥
系统桥是 CPU 与外部组件进行通信的通道。我们的 CPU 与 DM、两个计时器、其他硬件之间的通信都需要经过系统桥进行。
在实现系统桥的过程中,主要有两个方向的信息流:CPU 到外部,以及外部到 CPU。
CPU 到外部
我们需要向外部传递的信息包括:
- 读写的 DM 地址与使能信号。
- 读写的计时器地址与使能信号。
- 对于外部中断的响应信号。
事实上这些信息的传播都是通过 store 类指令完成的。也就是说我们只将 P6 中 CPU 与 DM 通信的接口重新连接至系统桥内即可。
重点在于系统桥内部对于地址的选择。
对于常规读写 DM:不需要进行额外修改。
计时器
需要判断 CPU 中给出的存储地址是否在两个计时器的地址范围内。
1 | wire timer1_sel = (m_data_addr_in >= 32'h7f00) && (m_data_addr_in <= 32'h7f0b); |
若 store 的地址在这个范围内,说明我们要对计时器进行写入。将传给 timer 的信号进行相应的连接即可。
1 | assign timer1_WE = (&m_data_byteen_in == 1'b1) && (m_data_addr_in >= 32'h7f00) && (m_data_addr_in <= 32'h7f0b); |
注:前两行代码中 &m_data_byteen_in == 1'b1
是因为对计时器写入必须按字写入。
中断响应
课程组给出了中断响应时应写入的地址。注意,什么时候进行中断的响应是软件决定的事情,不需要硬件做决定。我们只需要在 写入 “中断响应对应的地址” 时将响应信号传出 CPU 即可。
在系统桥中的代码连接如下:
1 | wire Int_sel = (m_data_addr_in >= 32'h7f20) && (m_data_addr_in <= 32'h7f23) && (|m_data_byteen_in == 1'b1); |
外部到 CPU
外部至 CPU 的信号除了 load 类指令的相关信号外,还要增加 HWInt 信号来实现外部中断。
注意到 load 类指令可能从计时器中读取数据,因此我们在连接信号时要对来自计时器的信号做出相应的选择。
1 | assign m_data_rdata_out = timer1_sel == 1'b1 ? timer1_Dout : |
HWInt 共有六位,从低位开始依次是:timer1 的中断信号、timer2 的中断信号、外部中断信号与 3’b0。
即:
1 | assign HWInt = {3'b0, interrupt_in, timer2_IRQ, timer1_IRQ}; |
其中 interrupt_in 是来自外部的中断信号。
计时器
计时器这部分我们简单的阅读源码,理解后大致了解各个端口的连接方式即可。计时器的源码在教程中已经给出。
注意,由于教程未来可能会有版本更新,计时器的具体连接方式也可能有变化。笔者不保证我的连接方式一定是正确的。还请同学们依据实际情况自行定夺。
宏观 PC
由于我们要封装流水线,即让流水线看上去像一个单周期 CPU,所以我们使用了宏观 PC 这一概念。宏观 PC 表达的是 CPU 当前正在运转哪一条指令。
宏观 PC 要和协处理器 CP0 处于同一流水级中。有关协处理器的设计后文详谈。依据实现方式的不同,CP0 放在 E 级和 M 级均可。一般而言放在 M 级的同学居多,所以我也放在了 M 级,以便于和同学(大佬)们进行对拍。所以我宏观 PC 也就是 PC_M 的值。
在异常发生时,我们要保证产生异常的那条指令不会对 CPU 的状态造成影响。并将发生异常的指令作为 受害指令 存入协处理器的 EPC 中。这部分之后再详谈。
侦测异常
在 P7 中,最重要的任务是侦测程序运行过程中产生的 异常。
我们首先要侦测异常,然后根据异常的种类生成相应的 Exccode 码,并将 Exccode 码存入协处理器 CP0 中。
异常的种类
我们要实现的异常分为以下几类:
- AdEL(Exccode 码为 5’d4)
- 取 pc 的值时地址不对齐。
- lw 未字对齐与 lh 未半字对齐。
- 尝试使用 lh 和 lb 取 timer 的寄存器中的值。
- 计算取数地址的过程中出现加法溢出情况。
- 取数地址不在允许的范围中。
- AdES(Exccode 码为 5’d5)
- sw 未字对齐与 lh 未半字对齐。
- 尝试使用 sh 和 sb 向 timer 的寄存器中存值,或尝试向 timer 中的 Count 寄存器存值。
- 计算存数地址的过程中出现加法溢出情况。
- 存数地址不在允许的范围中。
- Syscall(Exccode 码为 5’d8)
- 由指令 syscall 产生的系统调用异常。
- RI(Exccode 码为 5’d10)
- 未知的指令码。
- Ov(Exccode 码为 5’d12)
- 加减指令的计算过程中出现溢出情况。
溢出判断
笔者使用 OverFlow 指示溢出的产生。对溢出的判读需要在 E 级的 ALU 中进行。其具体过程在指令集中已有说明。
1 | wire [32:0]tempAdd = {A[31], A} + {B[31], B}; |
其中 ALUOp 是我的实现方式中添加的信号。这个信号指示 ALU 要进行哪种运算。ALUOp 为 4’b0000 代表当前进行加法运算;ALUOp 为 4’b0001 代表当前进行减法运算。
A 和 B 分别是传入 ALU 的两个操作数。
OverFlow 在 E 级产生,需要流水到 M 级才能判断具体出现了哪种异常。
Exccode 信号的产生
我们可以创建一个模块来产生 Exccode 信号。这里我将其称为 ExccodeOccur。此模块最好和协处理器位于同一流水级。(设置在 M 级中)
在清楚了各个异常产生的条件后,依次产生各个异常即可。
AdEL
1 | wire AdEL = pcErrorM == 1'b1 ? 1'b1 : //pcError |
AdES
1 | wire AdES = (Sw == 1'b1 && (ALUAnsM[0] | ALUAnsM[1])) ? 1'b1 ://sw未字对齐 |
Syscall
直接识别指令是否是 syscall 就行。
1 | wire Syscall = (func == 6'b001100 && R_R == 1'b1) ? 1'b1 : 1'b0; |
RI
RI 信号在 Controller 中产生,然后流水到 ExccodeOccur 所在的流水级,参与 Exccode 的生成。
1 | //在Controller中产生,也就是在D级产生。 |
Ov
注意是由加减法运算指令导致的溢出才会产生 Ov 信号。
1 | wire Ov = ((Add | Addi | Sub) && OverFlowM == 1'b1) ? 1'b1 : 1'b0; |
Exccode
在得到所需的各个信号后,进行判断、生成即可。
1 | assign ExcCodeM = AdEL == 1'b1 ? 5'd4 : |
协处理器 CP0 设计
CP0 的设计是整个 P7 的关键所在。这里特别感谢 roief 佬及其博客。roief 的博客指路:P7 MIPS 微体系
CP0 的端口
我个人的 CP0 端口完全按照教程所推荐的设计。即:
1 | moudle CP0( |
对于各个输入端口的粗略解释:
CP0Add 和 CP0In 都是在实现 mtc0 和 mfc0 中要用到的。VPC 是受害指令的宏观 PC,此处即为 M 级 PC。
ExccodeIn 是我们在 ExccodeOccur 中产生的 Exccode。HWInt 是我们在系统桥中产生并传入 CPU 的,传入 CPU 后直接连接到 CP0 的端口上。
EXLClr 由指令 eret 产生。
BDIn 指示当前指令是不是延迟槽内部指令,这与 EPC 的产生有关。
CP0 中寄存器的实现
在 CP0 中我们要实现三个寄存器:SR、Cause、EPC。
为了便于 mtc0 和 mfc0 的实现以及不知道这两个指令会不会存取我们尚未实现的寄存器,我的 CP0 设计中添加了一个寄存器堆:reg [31:0]CP[0:31];
,并添加以下宏定义便于代码编写(注:下文的代码使用到的宏定义均位于此处):
1 |
各个域的功能:
- IE:是否允许中断。
- EXL:是否处于核心态(异常中断处理程序中)。
- IM:六个位分别指示是否允许发生对应的中断。
- BD:是否是延迟槽中指令
- IP:每周期更新,记录 HWInt 的值。
- ExcCode:发生异常时更新,记录异常码。
事后得知貌似是不会存取我们没有实现的寄存器的……不过这么写实现 mtc0 和 mfc0 都很方便,所以也是有其优点的。实际上协处理器 CP0 中就是包含有 32 个寄存器,因此这样实现可以说是毫无毛病(乐)。
BD 的产生
BD 这个信号是在外部产生并传入 CP0 中,指示当前指令是否处于延迟槽内。这里有一处要注意的地方是:在产生 BD 时,不管跳转指令是否跳转,都要将其视为处于延迟槽内。
我在 F 级产生这一信号,从而满足 BD 的产生晚于跳转指令一周期的条件。
1 | assign BDF = JalD | JrD | BeqD | BneD | 1'b0; |
CP0Out 和 EPCOut
很方便的,我的 CP0Out 的产生方式为:assign CP0Out = CP[CP0Add];
,一行搞定。
相应的,EPCOut 的产生方式为:assign EPCOut = `EPC;
。
EPC 的产生
对于 EPC 的产生,要注意:如果指令是延迟槽指令(即 BDIn == 1'b1
),那么我们记录的 EPC 应当是其所属的跳转指令(即受害指令的上一条指令,VPC - 32'd4
)。
Req 的产生
注:这部分参照了 roief 佬的写法。
1 | wire IntReq = (|(HWInt & `IM)) & !`EXL & `IE; // 允许当前中断 且 不在中断异常中 且 允许中断发生 |
Req 的产生就意味着我们要进入异常处理程序了。除了要在 CP0 中做出改变外,我们还需要在 CPU 中做出相应的操作。下文再说。
CP0 的时序逻辑
遵循设计即可。
结合注释观看效果更佳。
这里提一个要注意的点:当中断和异常同时发生时,要优先响应中断,即向 `ExcCode 存入的值应当是象征中断的 0。
1 | integer i; |
REQ 发生时
当 CP0 中的 Req 信号处于高位,即我们要进入异常处理程序时,我们要对流水线进行以下处理:
- 清空 D, E, M, W 级流水线寄存器。实现上可以直接将 Req 信号接入 reset 中。
- 将 PC 的值强制修改到异常处理程序的入口:
32'h00004180
。 - 保证异常发生时不会对 DM 等外部模块产生写信号(即
m_data_byteen == 4'h0
)。 - 正处于 E 级的指令不应修改乘除模块。
指令拓展
在 P7 中,我们要拓展以下几个指令:syscall, mfc0, mtc0, eret
其中 syscall 没什么好说的,产生相应的异常码就行。
mfc0 和 mtc0 的实现有些类似于 mfhi,mthi 。根据指令集进行拓展即可。此外根据个人实现的不同可能需要处理和 mfc0 与 mtc0 相关的转发(增加 W 级到 M 级的通路等),还请注意。
eret 要修改 PC 的值,返回 EPC 处继续执行指令。此处要有一个数据冲突问题需要注意:
当 mfc0 要修改 CP0 中的 14 号寄存器(即 `EPC),且后面跟着 eret 时,要进行相应的阻塞。
1 | wire stall_eret = EretD & ((Mtc0E & (instrE[15:11] == 5'd14)) | (Mtc0M & (instrM[15:11] == 5'd14))); |
此外,当 eret 流水至 M 级时要将 CP0 中的 `EXL 置 0。
修改 PC 的信号优先级
优先级为:reset > REQ > eret > 其他
自行体悟一下即可,不作过多解释。
阻塞时中断产生的问题
在 P6 的 CPU 中,当我们阻塞时插入 nop,nop 对应的 pc 值一般是 0。这个设计在 P7 中会招致问题:当处于阻塞时外部产生了中断信号,我们记录的 EPC 将会是 0。这会导致错误的产生。
正确的处理方法,是在阻塞时让 pc 和 BD 这两个信号依旧正常流水。
结语
P7 的工程量还是很大的。往年 P7 也被称为是最玄学的一 P。今年我在搭建时据说教程大更新,因此实际上并没有感觉有那么的 “玄学”。计组课程在不断地改进啊(喜)。
今年因为 P8 和烤漆完全撞上的缘故(考完概统的当天晚上进行 P8 上机),为了复习期末,不得已放弃了做 P8 的想法,不得不说是一个计组学习中的遗憾。
计组回忆的这一系列到这就结束了。虽然过程艰辛又曲折,但是回顾已有的成果,还是会有一种 “我都已经做出来这么多东西了啊!” 的感叹。
不知读者在学习计组的过程中是否体会到了乐趣呢?