发布于 

「计算机组成」P7 支持异常的流水线 CPU

P7 中最重要的概念就是 异常,本文的全部内容都将围绕 异常 一词进行叙述

而系统桥等内容在本文中不会提及,请结合教程自行学习

什么是异常

在 MIPS 体系结构中,中断、自陷、系统调用以及其他打断程序正常执行流的事件统称为异常

See MIPS Run Linux

本文中的 异常See MIPS Run Linux 中的定义一致。 需要注意到异常是一个中性词

为何要有异常

异常的存在,能让程序专注当前的逻辑,而将所有的异常情况交给异常处理程序处理。 这样,某些游离于正常逻辑之外的情况比如键盘触发,访存错误等就不需要程序使用大量的周期去检测, 而是统一包装成异常等待确认和处理。

什么时候发生一个异常

本次课设中给出了 CPU 需要捕捉的异常以及对应的情况, 主要分为两种:内部指令异常和外部中断异常,详情见下:

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

如何捕捉这些异常

对于内部指令异常,通常会有一个发生的关键时间点。 比方说 load 指令的运算溢出可在 EALU 处完成判断

那么,假设有多条指令,在不同的流水级发生异常该如何处理呢? 显然,由于指令之间的依赖关系和因果逻辑,我们应该优先响应异常指令中时间上最靠前的指令(而不是最先出现异常的指令)。

为了解决这个问题,可以令异常也一起流水,到某个特定的流水级进行捕捉,以此保证异常之间的时序关系

显然,捕捉异常的元件必须放置在可能出现异常的流水级之后。 经计算该元件只能放在 EM 或者 W 级。 我们称这个元件为 CP0,后文中所有异常相关信息的储存都将放置在这个模块中

对于外部指令,产生异常时由 CP0 直接捕捉

如何打断当前工作流

一般来说,我们将 CP0 捕捉到异常的对应指令称之为 受害指令,对应的 PC 称之为 VPC

假设我们想要能够从异常返回并且不受破坏接着执行被打断的执行流,流水线中的每条指令要么执行完毕,要么就像我们根本没见过一样

See MIPS Run Linux

自然的,令受害指令之前的指令为需要执行完毕的指令,而受害指令及其之后的指令为应该还未执行的指令,就如同在受害指令之前断开了一般。

满足如上要求的 CPU 称之为支持 精确异常

在单周期 CPU 中,这样的结构很容易实现。 但是在多周期流水线 CPU 中,多条指令处在不同的阶段并行运行。 当指令触发异常时,受害指令本身和后面的指令可能已经结束了某些阶段的任务。 如何解决这一问题?

注意到指令流经流水线过时,可能留下影响的操作只有修改 PC ,写入流水线寄存器,写入乘除模块,写入 DM 和写入 GRF。 由此方案如下:

  • 由于后续需要跳转到异常处理程序, PC 无需处理
  • 流水线寄存器储存的是指令中间信息,直接清空
  • 在我们的课设中,不考虑写入乘除模块的影响
  • 只需要还原 GRFDM 的写入,分别在 MW
    • 一个简单的方法是直接将 CP0 放在 M 级及之前
    • 当异常发生时,受害指令和之后的指令都未写入DMGRF,也就不必完成复杂的回退操作

而对于受害指令之前的指令,继续流水即可完成完成。

如何进入异常处理程序?

打断当前工作流后,直接让修改 PC 跳转到异常处理程序入口,课设要求为 0x4180

如何进行异常处理?

See MIPS Run Linux 对应章节

如何从异常处理程序返回?

按照定义,自异常处理程序返回后,应从受害指令开始重新执行,以接驳正常工作流。

那么在异常处理之前,CP0 需要保存受害指令的 PCVPC,从而保证程序的正确返回。 我们将储存程序返回位置的寄存器称之为 EPC,其中 EPC = VPC

若受害指令是一条延迟槽指令,仅仅重新执行延迟槽指令会导致对应跳转指令失效。 在该情况下,需要将对应跳转指令一并重新执行,也就是说 EPC 中保存的值应该为跳转指令的 PC 也就是 VPC - 4

当然,异常处理程序也可能修改 EPC 的值以返回不同的位置,此处不表

最后异常处理程序将会使用命令 eret 跳转到 EPC 对应地址,即从异常处理程序返回

细节

由于阻塞的存在,受害指令可能有两种情况:

  1. 一条平平无奇的普通指令
  2. 由于阻塞产生的空泡指令

注意到在情况 2 真正的受害指令应该是产生空泡的对应指令, 所以在产生空泡时需要继承原指令的 PC 和延迟槽标记

注意异常发生时若 CP0 不接受则正常运行

注意 AdELRI 出现时指令视作 nop(无乘除指令则不需要考虑)

注意 eretmtc0 的冲突(ERETCP0 同级则不需要考虑)

注意 Execode 中断和其他异常的优先级

总结

流程

  • 产生内部异常
  • 产生外部异常或内部异常流水到 CP0
    • CP0 捕捉异常,给出全局异常信号
    • CP0 保存异常相关信息
  • 清空受害指令及之后的全部指令信息
  • 跳转到异常处理程序入口
  • 完成异常处理程序
  • eret 指令结束异常处理程序,跳转回到正常工作流

为了支持异常,我们需要依次完成如下操作:

  • 让流水线内部能检测内/外部异常并进行异常流水,延迟槽标记流水
  • 设计 CP0 模块
    • CP0 模块应放置在 E 级及之后,M 级及之前:E 级或 M
    • CP0 模块应保存异常处理的相关信息:异常类型 ExcCode,返回位置 EPC,延迟槽标记 BD
  • 完成 mfc0, mtc0 指令支持
  • 完成 systemcalleret 指令支持

一些想说的话

P7 整体而言包含的内容较多,需要援引学习的资料更是多上加多。 为了每个部分都有所提及,又要控制整体篇幅,很容易导致教程整体结构较散。

关于各类实现的细节比如说 CP0 相应外部中断异常的条件, CP0 中究竟需要保存什么信息等需要大家自行阅读 See MIPS Run Linux 等资料

愿大家武运昌隆,顺利过关