跳转至

中断异常实现提示

理解中断和异常的处理逻辑

在实现中断异常相关支持之前,首先需要阅读监控程序文档和 RISC-V 文档,查阅相关要求,以及实现完成之后的效果。同时,阅读监控程序,理解其处理中断和异常的逻辑。其中,RISC-V 文档需要阅读以下几节的内容:

  • RISC-V Spec, Chapter 9 "Zicsr", Control and Status Register(CSR) Instructions
  • RISC-V Privileged, 2.2 CSR Listings (注意读写权限的处理)
  • RISC-V Privileged, 2.3 CSR Field Specifications (注意每个域读写非法值的处理)
  • RISC-V Privileged, 3.1.6 Machine Status Register (mstatus)
    • 只需要阅读完成 Overview 和 3.1.6.1 节即可,本节后面的部分在实验中不会涉及
  • RISC-V Privileged, 3.1.7 Machine Trap-Vector Base-Address Register (mtvec)
  • RISC-V Privileged, 3.1.9 Machine Interrupt Registers (mip and mie)
  • RISC-V Privileged, 3.1.13 Machine Scratch Register (mscratch)
  • RISC-V Privileged, 3.1.14 Machine Exception Program Counter (mepc)
  • RISC-V Privileged, 3.1.15 Machine Cause Register (mcause)
  • RISC-V Privileged, 3.2.1 Machine Timer Registers (mtime and mtimecmp)
  • RISC-V Privileged, 3.3.1 Environment Call and Breakpoint
  • RISC-V Privileged, 3.3.2 Trap-Return Instructions

在开始实现之前,请确认可以回答下面的问题,正确理解监控程序如何处理中断异常之后,再开始实现。

  1. 在支持中断异常的监控程序当中一共涉及几个内核态?监控程序工作在什么内核态上?什么时候会进入用户态(U 态)?
  2. 观察监控程序中 CSR 寄存器的变化。监控程序在执行 G 命令执行用户程序时,是如何切换内核态,并跳转到对应位置的?
  3. 用户程序执行 ecall 指令之后会发生什么?
  4. 考虑中断处理。监控程序的异常处理函数中严格过滤掉了在 M 态发生的时钟中断。如何在异常处理函数中判断当前处理的异常发生在什么态?来自 M 态的时钟中断会发生吗?
  5. 中断发生的具体条件是什么?监控程序中发生的中断类型是?
  6. 用户程序在执行完成后(执行 jr ra 指令回到监控程序后)会发生什么?
  7. 如何判断 CPU 当前运行在什么内核态?这个信息有暴露给软件吗?
  8. 如何读写 mtime 和 mtimecmp 寄存器?(尝试与串口的状态和数据寄存器进行类比)
  9. 监控程序在处理 mtvec 寄存器时使用了复杂的逻辑。结合 Privileged 2.3 节解释其原因,并理解什么是 WARL。

实现 CSR 寄存器和其读写指令

CSR 寄存器和通用寄存器虽然都是寄存器,但它们不能用相同的方式实现。

注意到 CSR 寄存器的地址空间有 12 位之多,我们并没有使用到所有 4096 个 CSR 寄存器。因此为了节省资源,需要将 CSR 寄存器以稀疏的方式实现。即不是每个 CSR 地址都对应着一个真实存在的寄存器,而是单独实现每个 CSR 寄存器。

考虑 CSR 的读和写发生在哪个阶段。如果参考通用寄存器,将读放在 ID,写放在 WB,则需要参考通用寄存器进行 CSR 寄存器的 RAW 冲突处理。

为了避免处理 RAW 冲突,我们可以将 CSR 寄存器的读和写放在同一段中。若全部放在 MEM 段,则需要注意 CSR 的读取指令获得数据的时间与 load 类指令相同,需要参考 load-relate 类数据冲突进行额外处理。

若全部放在 EX 段,则需要注意此时 EX 段会改变 CSR 寄存器的值,可能带来副作用,需要在异常处理中进行特殊处理。

也可以将 CSR 指令读写 CSR 寄存器的阶段放在其他位置上,需要参考上述分析其带来的额外冲突,并进行处理。当然,你总可以让自己的 CPU 保证在流水线中至多只有 1 条 CSR 读写指令来保证 CSR 寄存器读写的正确性。

注意 CSR 寄存器的读写权限处理。

在实现完本小节之后,可以尝试在监控程序中使用 CSR 读写指令,检查实现的正确性。

PMP CSR 寄存器

注意,为了和 QEMU 兼容, 监控程序在启动时会写入 pmpcfg0pmpaddr0 这两个 CSR 寄存器,将其配置为 0x00000000 ~ 0xFFFFFFFF RWX,即全地址空间可读、可写、可执行。因此,你可以忽略对 PMP CSR 的任何操作,但请不要在访问这些 CSR 地址时出现例外的情况(如卡死、异常等)。

基本的异常处理

在实现完 CSR 寄存器和基本读写指令之后,继续来实现基本的异常处理。

尽管内核态的信息没有暴露给程序,但我们在实现 CPU 时仍然需要进行内核态的维护。因此首先创建一个两位的寄存器来指示当前 CPU 所处的内核态。

由于 RISC-V 要求精确异常,因此需要保证在异常发生之后的指令不会改变 CPU 的“状态”而带来副作用。下面我们来分析一下哪些流水线段可能会改变 CPU 的状态。假设我们将 CSR 指令读写放在 MEM 段上。

流水线段 可能的副作用
IF 仅读取内存,无副作用
ID 仅读取寄存器,无副作用
EX 跳转,修改 PC
MEM 写内存,写 CSR
WB 写通用寄存器

参考这张表,为了保证异常处理器 CPU 状态的正确性,一种方式是在 EX 段直接进行异常处理。但这样的方式有一个潜在的坏处—— MEM 段可能会发生缺页异常,需要在 EX 段提前查询页表,探测可能的缺页异常进行处理。

另一种方式是在 MEM 段进行异常处理,并且当 EX 段需要跳转时,先等待 MEM 段宣布没有异常之后,再跳转,这样也可以保证 CPU 正确性。

在实现时可以参考上述两种方案,或者设计自己的异常处理方案(如增加额外的流水线段),并保证异常处理的正确性。

在探测到异常之后,修改对应的 CSR 寄存器,同时根据 mtvec 进行跳转。注意,为了保证异常处理的原子性,上述这些操作需要在同一个周期发生。

这之后,则需要实现对应的流水线寄存和流水线段屏蔽逻辑。如在 IF 段发生异常之后,给指令添加异常标记,并以流水线的方式传递到对应的异常处理段进行处理。当一个不进行异常处理的流水线段遇到了带有异常标记的指令,则需要跳过该指令在本流水线段的执行。同时为了维护其他 CSR 寄存器的信息,可能需要在流水线上寄存更多异常相关信息供异常处理使用。

在完成本小节功能的实现之后,可以尝试在监控程序的用户程序中参考监控程序文档,使用 ecall 指令访问串口输出信息。

mtime 和 mtimecmp 寄存器

这两个 CSR 寄存器与其他的 CSR 寄存器不同,不能通过 CSR 读写指令读写,而是通过 load / store 的方式进行处理。

因此有两种可能的处理方案:

  1. 参考串口,给 mtime 和 mtimecmp 实现单独的 wishbone slave
  2. 在 MEM 段访问 wishbone 总线之前进行特判,重定向对这两个寄存器的请求

在完成本小节功能的实现之后,可以尝试访问这两个 CSR 寄存器的地址,进行读写,验证实现的正确性。

中断处理

中断的处理不需要在发生中断的第一个周期就立即进行处理,可以等待。

因此,我们可以选择以下面的方式实现中断的处理:

  • IF 段取指令时 / ID 段译码时,检查当前是否满足中断发生的条件,并在发生中断时给指令带上异常标记。
  • 进入 EX 阶段后,跳过该指令在 EX 段的执行。
  • 在异常处理阶段接受中断,修改对应的 CSR 寄存器。

在实现完本小节的功能之后,可以尝试在监控程序的用户程序中编写死循环,检查其是否会被正确打断。

到此,我们已经完成了可以支持第二版本监控程序的 CPU 实现。


最后更新: 2024年11月14日
作者:Jiajie Chen (0.94%), cuibst (99.06%)