中断异常实现提示
理解中断和异常的处理逻辑
在实现中断异常相关支持之前,首先需要阅读监控程序文档和 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
在开始实现之前,请确认可以回答下面的问题,正确理解监控程序如何处理中断异常之后,再开始实现。
- 在支持中断异常的监控程序当中一共涉及几个内核态?监控程序工作在什么内核态上?什么时候会进入用户态(U 态)?
- 观察监控程序中 CSR 寄存器的变化。监控程序在执行 G 命令执行用户程序时,是如何切换内核态,并跳转到对应位置的?
- 用户程序执行 ecall 指令之后会发生什么?
- 考虑中断处理。监控程序的异常处理函数中严格过滤掉了在 M 态发生的时钟中断。如何在异常处理函数中判断当前处理的异常发生在什么态?来自 M 态的时钟中断会发生吗?
- 中断发生的具体条件是什么?监控程序中发生的中断类型是?
- 用户程序在执行完成后(执行
jr ra
指令回到监控程序后)会发生什么? - 如何判断 CPU 当前运行在什么内核态?这个信息有暴露给软件吗?
- 如何读写 mtime 和 mtimecmp 寄存器?(尝试与串口的状态和数据寄存器进行类比)
- 监控程序在处理 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 兼容, 监控程序在启动时会写入 pmpcfg0
和 pmpaddr0
这两个 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 的方式进行处理。
因此有两种可能的处理方案:
- 参考串口,给 mtime 和 mtimecmp 实现单独的 wishbone slave
- 在 MEM 段访问 wishbone 总线之前进行特判,重定向对这两个寄存器的请求
在完成本小节功能的实现之后,可以尝试访问这两个 CSR 寄存器的地址,进行读写,验证实现的正确性。
中断处理
中断的处理不需要在发生中断的第一个周期就立即进行处理,可以等待。
因此,我们可以选择以下面的方式实现中断的处理:
- IF 段取指令时 / ID 段译码时,检查当前是否满足中断发生的条件,并在发生中断时给指令带上异常标记。
- 进入 EX 阶段后,跳过该指令在 EX 段的执行。
- 在异常处理阶段接受中断,修改对应的 CSR 寄存器。
在实现完本小节的功能之后,可以尝试在监控程序的用户程序中编写死循环,检查其是否会被正确打断。
到此,我们已经完成了可以支持第二版本监控程序的 CPU 实现。