前言

做完前六 P 之后,我们已经做出来一个相当厉害的流水线了。在 P7 中,我们的任务是增加外部中断响应和异常处理。

本篇博客中我将结合我做 P7 的方法,按照我的搭建过程逐个讲解 P7 中所要完成的工作。

注:本篇博客记录的是 21 级的 P7 搭建过程。计组课程组每年都可能会对细节有一些改动。因此建议自行甄别内容。最终实现细节还请以教程为准。

调整模块架构

在 P7 中,我们需要对流水线的架构做一些调整。我们需要添加系统桥和两个计时器,这三个组件和我们在 P6 中搭建的 CPU 在同一级别。也就是这样的:

系统桥

系统桥是 CPU 与外部组件进行通信的通道。我们的 CPU 与 DM、两个计时器、其他硬件之间的通信都需要经过系统桥进行。

在实现系统桥的过程中,主要有两个方向的信息流:CPU 到外部,以及外部到 CPU。

CPU 到外部

我们需要向外部传递的信息包括:

  • 读写的 DM 地址与使能信号。
  • 读写的计时器地址与使能信号。
  • 对于外部中断的响应信号。

事实上这些信息的传播都是通过 store 类指令完成的。也就是说我们只将 P6 中 CPU 与 DM 通信的接口重新连接至系统桥内即可。

重点在于系统桥内部对于地址的选择。

对于常规读写 DM:不需要进行额外修改。

计时器

需要判断 CPU 中给出的存储地址是否在两个计时器的地址范围内。

1
2
3
wire timer1_sel = (m_data_addr_in >= 32'h7f00) && (m_data_addr_in <= 32'h7f0b);
wire timer2_sel = (m_data_addr_in >= 32'h7f10) && (m_data_addr_in <= 32'h7f1b);
// m_data_addr_in 是从CPU处拿到的存储地址信号

若 store 的地址在这个范围内,说明我们要对计时器进行写入。将传给 timer 的信号进行相应的连接即可。

1
2
3
4
5
6
assign timer1_WE = (&m_data_byteen_in == 1'b1) && (m_data_addr_in >= 32'h7f00) && (m_data_addr_in <= 32'h7f0b);
assign timer2_WE = (&m_data_byteen_in == 1'b1) && (m_data_addr_in >= 32'h7f10) && (m_data_addr_in <= 32'h7f1b);
assign timer1_addr = m_data_addr_in[31:2];
assign timer2_addr = m_data_addr_in[31:2];
assign timer1_Din = m_data_wdata_in;
assign timer2_Din = m_data_wdata_in;

注:前两行代码中 &m_data_byteen_in == 1'b1 是因为对计时器写入必须按字写入。

中断响应

课程组给出了中断响应时应写入的地址。注意,什么时候进行中断的响应是软件决定的事情,不需要硬件做决定。我们只需要在 写入 “中断响应对应的地址” 时将响应信号传出 CPU 即可。

在系统桥中的代码连接如下:

1
2
3
wire Int_sel = (m_data_addr_in >= 32'h7f20) && (m_data_addr_in <= 32'h7f23) && (|m_data_byteen_in == 1'b1);
assign m_int_addr = Int_sel == 1'b1 ? m_data_addr_in : 32'h0;
assign m_int_byteen = Int_sel == 1'b1 ? m_data_byteen_in : 4'b0;

外部到 CPU

外部至 CPU 的信号除了 load 类指令的相关信号外,还要增加 HWInt 信号来实现外部中断。

注意到 load 类指令可能从计时器中读取数据,因此我们在连接信号时要对来自计时器的信号做出相应的选择。

1
2
3
assign m_data_rdata_out = timer1_sel == 1'b1 ? timer1_Dout :
timer2_sel == 1'b1 ? timer2_Dout :
m_data_rdata_in;

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
2
3
4
wire [32:0]tempAdd = {A[31], A} + {B[31], B};
wire [32:0]tempSub = {A[31], A} - {B[31], B};
assign OverFlowE = (ALUOp == 4'b0000 && tempAdd[32] ^ tempAdd[31] == 1'b1) ? 1'b1 :
(ALUOp == 4'b0001 && tempSub[32] ^ tempSub[31] == 1'b1) ? 1'b1 : 1'b0;

其中 ALUOp 是我的实现方式中添加的信号。这个信号指示 ALU 要进行哪种运算。ALUOp 为 4’b0000 代表当前进行加法运算;ALUOp 为 4’b0001 代表当前进行减法运算。

A 和 B 分别是传入 ALU 的两个操作数。

OverFlow 在 E 级产生,需要流水到 M 级才能判断具体出现了哪种异常。

Exccode 信号的产生

我们可以创建一个模块来产生 Exccode 信号。这里我将其称为 ExccodeOccur。此模块最好和协处理器位于同一流水级。(设置在 M 级中)

在清楚了各个异常产生的条件后,依次产生各个异常即可。

AdEL

1
2
3
4
5
6
7
8
9
10
wire AdEL = pcErrorM == 1'b1 ? 1'b1 : //pcError
(Lw == 1'b1 && (ALUAnsM[0] | ALUAnsM[1])) ? 1'b1 : //lw未字对齐
(Lh == 1'b1 && ALUAnsM[0]) ? 1'b1 : //lh未半字对齐
((Lh | Lb) && (ALUAnsM >= 32'h00007f00 && ALUAnsM <= 32'h00007f1b)) ? 1'b1 : //取Timer寄存器的值
((Lw | Lh | Lb) && OverFlowM == 1'b1) ? 1'b1 ://计算地址时加法溢出
((Lw | Lh | Lb) && (ALUAnsM > 32'h00007f23)) ? 1'b1 :
((Lw | Lh | Lb) && (ALUAnsM > 32'h00007f0b && ALUAnsM < 32'h00007f10)) ? 1'b1 :
((Lw | Lh | Lb) && (ALUAnsM > 32'h00007f1b && ALUAnsM < 32'h00007f20)) ? 1'b1 :
((Lw | Lh | Lb) && (ALUAnsM > 32'h00002fff && ALUAnsM < 32'h00007f00)) ? 1'b1 :1'b0; //取数地址超出上限
// 注:其实可以简化但是因为写完后感觉工工整整的看上去很帅气所以就没有改

AdES

1
2
3
4
5
6
7
8
9
wire AdES = (Sw == 1'b1 && (ALUAnsM[0] | ALUAnsM[1])) ? 1'b1 ://sw未字对齐
(Sh == 1'b1 && ALUAnsM[0]) ? 1'b1 ://sh未半字对齐
((Sh | Sb) && (ALUAnsM >= 32'h00007f00 && ALUAnsM <= 32'h00007f1b)) ? 1'b1 ://存Timer寄存器的值
((Sw | Sh | Sb) && OverFlowM == 1'b1) ? 1'b1 ://计算地址时加法溢出
((Sw | Sh | Sb) && (ALUAnsM == 32'h00007f08 || ALUAnsM == 32'h00007f18)) ? 1'b1 ://向计时器的Count存值
((Sw | Sh | Sb) && (ALUAnsM > 32'h00007f23)) ? 1'b1 :
((Sw | Sh | Sb) && (ALUAnsM > 32'h00007f0b && ALUAnsM < 32'h00007f10)) ? 1'b1 :
((Sw | Sh | Sb) && (ALUAnsM > 32'h00007f1b && ALUAnsM < 32'h00007f20)) ? 1'b1 :
((Sw | Sh | Sb) && (ALUAnsM > 32'h00002fff && ALUAnsM < 32'h00007f00)) ? 1'b1 :1'b0;//存数地址超出上限

Syscall

直接识别指令是否是 syscall 就行。

1
wire Syscall = (func == 6'b001100 && R_R == 1'b1) ? 1'b1 : 1'b0;

RI

RI 信号在 Controller 中产生,然后流水到 ExccodeOccur 所在的流水级,参与 Exccode 的生成。

1
2
3
4
5
6
//在Controller中产生,也就是在D级产生。
assign RID = ~(Beq | Bgez| Bne | Addi| Andi| Ori | Lui | J |
Jal | Sb | Sh | Sw | Lb | Lh | Lw | Add |
Sub | And | Or | Sll | Srl | Jr | Slt | Sltu|
Mult|Multu| Div | Divu| Mfhi| Mflo| Mthi| Mtlo|
Syscall| Mfc0| Mtc0| Eret);

Ov

注意是由加减法运算指令导致的溢出才会产生 Ov 信号。

1
wire Ov = ((Add | Addi | Sub) && OverFlowM == 1'b1) ? 1'b1 : 1'b0;

Exccode

在得到所需的各个信号后,进行判断、生成即可。

1
2
3
4
5
assign ExcCodeM = AdEL == 1'b1 ? 5'd4 :
AdES == 1'b1 ? 5'd5 :
Syscall == 1'b1 ? 5'd8 :
RIM == 1'b1 ? 5'd10 :
Ov == 1'b1 ? 5'd12 : 5'd0;

协处理器 CP0 设计

CP0 的设计是整个 P7 的关键所在。这里特别感谢 roief 佬及其博客。roief 的博客指路:P7 MIPS 微体系

CP0 的端口

我个人的 CP0 端口完全按照教程所推荐的设计。即:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
moudle CP0(
input clk,
input reset,
input en,
input [4:0] CP0Add,
input [31:0] CP0In,
output [31:0] CP0Out,
input [31:0] VPC,
input BDIn,
input [4:0] ExcCodeIn,
input [5:0] HWInt,
input EXLClr,
output [31:0] EPCOut,
output Req
);
...

对于各个输入端口的粗略解释:

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
2
3
4
5
6
7
8
9
10
11
`define SR CP[12]
`define Cause CP[13]
`define EPC CP[14]

`define IE CP[12][0]
`define EXL CP[12][1]
`define IM CP[12][15:10]

`define BD CP[13][31]
`define IP CP[13][15:10]
`define ExcCode CP[13][6:2]

各个域的功能:

  • 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
2
3
wire IntReq = (|(HWInt & `IM)) & !`EXL & `IE; // 允许当前中断 且 不在中断异常中 且 允许中断发生
wire ExcReq = (|ExcCodeIn) & !`EXL; // 存在异常 且 不在中断中
assign Req = IntReq | ExcReq;

Req 的产生就意味着我们要进入异常处理程序了。除了要在 CP0 中做出改变外,我们还需要在 CPU 中做出相应的操作。下文再说。

CP0 的时序逻辑

遵循设计即可。

结合注释观看效果更佳。

这里提一个要注意的点:当中断和异常同时发生时,要优先响应中断,即向 `ExcCode 存入的值应当是象征中断的 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
integer i;
always@(posedge clk) begin
if(reset == 1'b1) begin
i = 0;
for(i = 0; i < 32; i = i + 1) begin
CP[i] = 32'b0;
end
end
else begin
if(en == 1'b1) begin // 使能信号为真时将数据写入对应的寄存器
CP[CP0Add] = CP0In;
end
if(Req == 1'b1) begin // 当异常或中断发生时
`EXL <= 1'b1;
`ExcCode <= IntReq == 1'b1 ? 0 : ExcCodeIn; // 中断的优先级高于异常
`EPC <= BDIn == 1'b1 ? VPC - 32'd4 : VPC; // 结合BDIn生成EPC
`BD <= BDIn;
end
if(EXLClr == 1'b1) begin
`EXL <= 1'b0;
end
if((en == 1'b1 && CP0Add == 13)) begin
// 这里是为了防止mtc0的目标寄存器是Cause时会对IP域写入两次
end
else begin
`IP <= HWInt; //每周期更新IP的值
end
end
end

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 的想法,不得不说是一个计组学习中的遗憾。

计组回忆的这一系列到这就结束了。虽然过程艰辛又曲折,但是回顾已有的成果,还是会有一种 “我都已经做出来这么多东西了啊!” 的感叹。

不知读者在学习计组的过程中是否体会到了乐趣呢?