Migrated and updated lab1 and lab2 tutorials, etc

- update to two tutorials
- added environment and cmd tutorial
- updated lab1 CPU arch graph
This commit is contained in:
PurplePower
2025-07-19 20:58:44 +08:00
parent 18c1327051
commit fb2a030f07
36 changed files with 2084 additions and 1 deletions

View File

@@ -0,0 +1,178 @@
# 实验二 中断
[//]: # (完成流水线 CPU 实验后,你就已经对基于流水线 CPU 的原理和设计有初步认识了。但是这个简单的 CPU 只能按照预先的程序指令一直运行,无法中途打断。然而,我们生活的世界充满了不确定性,一个实用的 CPU 需要能够时刻准备好处理来自外部的事件,及时处理中断,并返回到原来的程序中继续执行。)
完成单周期 CPU 实验后,你获得了一个可以简单的按照预期指令执行的处理器。但是这个简单的 CPU 只能按照预先的程序指令一直运行,无法中途打断。然而,我们生活的世界充满了不确定性,一个实用的 CPU 需要能够时刻准备好处理来自外部的事件,及时处理中断,并返回到原来的程序中继续执行。
在本实验中,你将学习到:
- CSR 寄存器以及其操作命令
- 中断控制器的原理和设计
- 编写一个简单的定时中断发生器
下面的内容中,不管使用 IDE 还是执行命令,根目录是 `lab2` 文件夹。
## CSR指令支持
从预备知识[中断与异常](../../theory/interrupt-and-exception.md)我们已经学习到了 CSR 寄存器的基本概念。本实验里我们在单周期 CPU 的基础上增加对 CSR 寄存器的操作指令的支持。
CSR相关操作指令集的细节包括指令的语义编码等都可以通过阅读[非特权集手册](https://github.com/riscv/riscv-isa-manual/releases/download/Ratified-IMAFDQC/riscv-spec-20191213.pdf)的第九章获得。
中断相关的具体CSR寄存器的内容与对应含义请查阅[特权级手册](https://github.com/riscv/riscv-isa-manual/releases/download/Priv-v1.12/riscv-privileged-20211203.pdf)
有了实现单周期 CPU 的经验,我们可以把对 CSR 指令的支持分解为:
- **CSR 寄存器组**CSR 寄存器是一组类似于 RegisterFile 寄存器组的,地址空间大小为 4096 字节,独立编址的寄存器。
从指令手册可以看到对CSR寄存器的操作都是原子读写的CSR指令具体的语义请查阅手册。
CSR寄存器组需要根据ID模块译码后给出的控制信号和CSR寄存器地址来对内部寄存器进行寻址获取其内容并且修改。
- **ID 译码单元**ID 译码单元需要识别 CSR 指令,根据手册里描述的指令语义和编码规范,产生相应的传给其它模块的控制信号与数据。
- **EX 执行单元**CSR 指令都是原子读写的,即一条指令的执行结果中,既要把目标 CSR 寄存器原来的内容写入到目标通用寄存器中,还要按指令语义把从目标 CSR 寄存器读出来的内容修改之后再写回给该CSR 寄存器。此时 EX 里面的 ALU 单元是空闲的,要得到写入 CSR 寄存器的值,可以复用 ALU也可以不复用。
- **WB 写回单元**:支持 CSR 相关操作指令后,写回到目标通用寄存器的数据来源就多了一个从目标 CSR 寄存器读出来的修改前的值。
<!-- ----------------------------------------------------------------------- -->
## 中断控制器CLINT
CLINTCore-Local Interrupt是 RISC-V 架构中提供简单中断和定时器功能的中断处理器其在中断或异常发生且中断开启时将暂停CPU当前执行流设置好相关 CSR 寄存器信息后跳转到中断处理程序中执行中断处理程序。
关键就是该保存哪些信息到对应的CSR寄存器中答案就是 CPU 执行完当前指令后的下一个状态,比如当前指令是跳转指令,那么 `mepc` 保存的应该是当前跳转指令的跳转目标地址;以及一些关于中断发生原因的信息。
本实验中,我们希望实现一个支持基本功能的、简单的 CLINT对于一些特殊情况我们规定
1. 中断到来时,我们认为应该让当前指令执行完后,再跳转到中断处理程序。
2. `mstatus``mie`machine interrupt enable记录中断使能即是否响应中断。如果当前正在执行一条指令以关闭中断使能但此刻发生外部中断则规定这种情况不应响应中断。
3. 我们不考虑嵌套中断的情况,即中断处理过程中忽略到来的中断。
4. 仅考虑在 Machine 特权级下的情况。
<!-- 还有一些特殊情况。我们知道外部中断使能由 `mstatus` 内容决定,那么要思考如果当前指令如果是修改 `mstatus` 执行结果是关中断的指令执行时,外部中断到来了,那么下个周期是否应该响应中断?
为了统一起见,我们认为这种情况不应该响应中断,并且我们认为应该让当前指令执行完后,再跳转到中断处理程序。 -->
接下来,我们讨论 CLINT 要处理的情况,包括 进入中断处理 和 完成中断处理。
### 进入中断处理
#### 1. 响应硬件中断
此处将外部设备和定时器的中断归为一类讨论。响应硬件中断时CPU的工作具体分为两部分
保存现场记录CPU当前正在执行程序的下一条指令地址以便中断处理后恢复执行以及记录中断信息和设置相应权限。这对应了下面 CSR 上的操作:
- `mepc`保存的是中断或者异常处理完成后CPU返回并开始执行的地址。所以对于异常和中断`mepc` 的保存内容需要注意。
- `mcause`:保存的是导致中断或者异常的原因,具体代码请查阅特权级手册。
- `mstatus`:在响应中断时,需要将 `mstatus` 寄存器中的 `MPIE` 标志位设置为 `0`,禁用中断。
然后从 `mtvec` 获取中断处理程序的地址,跳转到该地址执行中断处理程序。
!!! info "扩展知识:操作系统与硬件配合处理中断"
事实上,中断的处理需要操作系统和硬件的紧密配合。在中断发生前,操作系统就要将中断处理程序写入内存并设置中断跳转向量到 `mtvec`
中断发生时CPU保存现场并设置中断信息操作系统还需保存进程上下文并进行资源调度等工作完成中断处理也类似。
后续操作系统课程中你将对此有更全面深入的了解。
#### 2. 响应软件中断
`ecall``ebreak` 指令是触发软件中断的两条 environment 指令。`ecall` 常用于系统调用,例如请求磁盘读写,`ebreak` 更多用于调试。
详见非特权级手册第2章的 Environment Call and Breakpoints 。
这两条指令都将触发中断CSR 的设置操作与响应硬件中断的相似。不同的是,`mcause` 的设置应参照 environment 相应的代码设置。
### 完成中断处理
不论任何原因进入到中断处理程序后,都需要使用 `mret` 指令以恢复 CPU 原先程序的执行。
这时候其实干的事与响应中断是大致相同的,只不过需要写入的寄存器只有 `mstatus`,跳转的目标地址则是从 `mepc` 获取。
为简单起见,对于 `mret``mstatus` 的要写入的值,就把 MIE 位置为 MPIE 位Machine Previous Interrupt Enable。若 MPIE 为 1 的话 `mret` 就会恢复中断,如果 MPIE 为 0 的话,`mret` 则不改变 `mstatus` 的值。
上述实现没有考虑嵌套中断,且以后实现特权级切换的时侯,`mstatus` 的改变更为复杂。中断的实现在手册中有明确的标准,想进一步了解中断机制的实现可以看特权级手册的 3.1.6.1 小节Privilege and Global Interrupt-Enable Stack in mstatus register 以及 9.6节Traps
而异常的现场保存和恢复与中断处理差不多,只不过保存和恢复的内容上有所不同而已,感兴趣的同学可以自行摸索。
### 关于 CLINT 的实现
CLINT 具体的实现方法很多,我们采用纯组合逻辑实现这个中断控制器。由于基于单周期 CPU 且 CLINT 是组合逻辑所以外部中断到来时CLINT 会马上响应。
目前 YatCPU 的主仓库的多周期CPU中的 CLINT 采用状态机来实现。
CLINT 需要一个周期就把多个寄存器的内容修改的功能,而正常的 CSR 指令只能对一个寄存器读-修改-写Read-Modify-Write, RMW。所以 CLINT 和 CSR 之间有独立的优先级更高的通路,用来快速更新 CSR 寄存器的值。
***
<!-- -------------------------------------------------------------- -->
## 简单的定时中断发生器
我们要实现一个 MMIO 的定时中断发生器——Timer。
MMIO Memory Mapped I/O简单来说就是该外设用来和 CPU 交互的寄存器是与内存一起编址的,所以 CPU 可以通过访存指令load/store来修改这些寄存器的值从而达到 CPU 和外设交互的目的。
目前在没有实现总线的情况下,使用多路选择器实现 MMIO 即可达到目的。原因主要是我们把取指令的操作和 load/store 访存操作分开了,让 Memory 有单独的一个通路进行取指令操作。
因此我们的模型还是一个 CPU 对多个外围设备,不会出现 CPU 的取指操作与访存操作冲突争抢外设的情况。
所以我们 CPU 发出的逻辑地址要发送到哪个设备,就由逻辑地址的高位作为外围设备的位选信号即可,低位则用于设备内部的寻址。
此外还有定时中断发生器的内部逻辑:两个控制寄存器 `enable` 寄存器和 `limit` 寄存器。
- `enable` 寄存器用来控制定时中断发生器的使能,为 `false` 则不产生中断, 映射到地址空间的逻辑地址为 0x80000008。
- `limit` 寄存器用来控制定时器的中断发生间隔,映射到地址空间的逻辑地址为 0x80000004。中断发生器内部有一个加一计数器当计数器的值到达 `limit` 为标准的界限时,定时器会发生一次中断信号(`enable` 使能情况下)。注:产生中断信号的时长没有太大关系,但是至少应该大于一个 CPU 时钟周期,确保 CPU 能够正确捕捉到该信号即可。
## 实验任务
!!! note "实验任务:支持中断处理"
1. 使 EX 单元支持 CSR 指令的运算。
2. 使 CSR 寄存器组支持 CLINT 和来自 CSR 指令的读写操作。
3. 使 CLINT 支持响应中断并且在中断结束后回到原来的执行流。
4. 使定时中断发生器可以正确产生中断信号,并且实现 Timer 寄存器的 MMIO。
请在 `// lab2(CLINTCSR)` 注释处完成上述支持,并通过 `CPUTest``ExecuteTest``CLINTCSRTest``TimerTest` 测试。
EX 执行单元的代码文件位于 `src/main/scala/riscv/core/Execute.scala`
CSR 寄存器组的代码文件位于 `src/main/scala/riscv/core/CSR.scala`
CLINT 的代码文件位于 `src/main/scala/riscv/core/CLINT.scala`
Timer 的代码位于 `src/main/scala/riscv/peripheral/Timer.scala`
!!! tip
IDEA 可以双击 shiftvscode 可以 shift+P 以打开文件快速搜索面板。
<!-- 在上面提到的 EX、CSR、CLINT、Timer 四个单元的相应文件里面,请在 `// lab2(CLINTCSR)` 注释处填入相应的代码,使其能够通过 `CPUTest``ExecuteTest``CLINTCSRTest``TimerTest` 测试。 -->
<!-- 如果能够正确完成本次实验,那么你的 CPU 就可以运行更加复杂的程序了,可以运行一下俄罗斯方块程序试试,如果想要上手玩的话,也许需要一个串口转接板,这样就可以通过电脑的键盘通过 UART 串口给程序输入字符了。 -->
***
## 实验报告
1. `CLINTCSRTest.scala` 中添加了 CLINT 处理硬件终端和软件中断的两个测试,请您选择至少一个,并:
1. 简述这个测试通过给部件输入什么信号,以测试 CLINT 的哪些功能?
2. 在测试波形图上,找到一次从开始处理中断到中断处理完成的波形图,并挑选其中关键的信号说明其过程。例如硬件中断的测试中,有在跳转指令和非跳转指令下的两次中断处理测试;软件测试则分别测试了 `ecall``ebreak` 两次中断,选择其中一次即可。
2. `CPUTest.scala` 中新增了 `SimpleTrapTest`,其执行 `csrc/simpletest.c` 的程序。请您:
1. 简述该测试程序如何测试 CPU 的中断处理正确性。
2. 在测试波形图上找出说明该程序成功执行的信号。
3. 说明您在完成实验的过程中,遇到的实验指导不足或改进建议。
## CPU架构图
![](images/single_cycle_CPU_with_interrupt_support.png)