构建五级流水线——lab4
本文最后更新于 2026年5月9日 15:59:55
一、引言
通过前三个 lab,我们设计的 CPU 已经具备基本的计算、跳转、内存操作功能,具有图灵完备性。并没有,实际上乘除法是bonus内容,我还没实现
1 | |
也就是理论上,可以完成任何有限的计算。
然而,在真实计算机系统中,CPU 必须具备与外部世界交互、自我监控以及处理突发事件的能力。为了应对这些复杂的系统级需求,RISC-V 指令集引入了控制与状态寄存器(CSR, Control and Status Register)以及相应的特权架构。在 Lab4 中,我们会从上游接收不同的指令,并转换成对应的操作。
二、Lab4 内容介绍
1. 指令
本次 Lab4 涉及如下指令:
CSRRW: Control and Status Register Read and Write(读出并写入)CSRRS: Control and Status Register Read and Set(读出并置位。将rs1作为掩码,将rs1上是1的对应位置强制设为1)CSRRC: Control and Status Register Read and Clear(读出并清零。将rs1作为掩码,将rs1上是1的对应位置强制设为0)
及三个对应的带有 -I 后缀的指令,意味着它们不从通用寄存器读取数据,而是直接使用机器码中自带的一段 5 位立即数来操作 CSR。
| [31:20] (12位) | [19:15] (5位) | [14:12] (3位) | [11:7] (5位) | [6:0] (7位) |
|---|---|---|---|---|
csr (寄存器地址) |
rs1 或 zimm (源操作数) |
funct3 (操作类型) |
rd (目标寄存器) |
opcode (操作码) |
2. 寄存器
bonus:参考英文指令集手册,简述一下此次 lab 中各个 csr 寄存器的作用
本次 Lab4 涉及如下寄存器:
计数器与硬件信息
mcycle(Machine Cycle Counter):保存了CPU已经运行的时钟周期数,需要将其设置为每周期加一。如果溢出则直接从0重新开始;如果有写入操作,则用写入值覆盖。mhartid(Machine Hardware Thread ID Register):保存当前CPU核的编号。因为目前实验只有一个核心,一直设为0即可,且不需要考虑写入。
异常与中断处理 (Trap)
当 CPU 遇到外部中断(如时钟、外设信号)或内部异常(如非法指令、缺页错误)时,会统一称为 Trap。以下寄存器专门用于处理这些情况:
mepc(Machine Exception Program Counter):处理完异常时,需要为CPU提供一个返回地址,让CPU从Trap发生的地方继续执行。mepc正是起到记录返回地点的作用。它可以是故障指令,也可以是被中断的指令。mcause(Machine Cause Register):用于记录触发Trap的原因码。其最高位用于区分硬件中断与软件异常,其余位存放具体的异常号或中断号。mtval(Machine Trap Value Register):作为mcause的补充,用于保存Trap发生时的附加信息(关联值)。例如在发生访存越界或缺页时,它会捕获具体的故障访存地址;在遇到非法指令时,它会记录该指令的机器码。mtvec(Machine Trap-Vector Base-Address Register):CPU在遇到Trap时需要有地方处理,这个地址就是mtvec。其中高30位是基地址,低2位对应四种不同模式。mie(Machine Interrupt Enable Register):用于设置CPU需要对什么类型的外界中断作出反应,每一位对应一种具体的Trap。为1则代表CPU会处理这种异常,为0则完全不管。mip(Machine Interrupt Pending Register):与mie完全一一对应,接收外界发来的中断信号,反映当前是否收到了中断请求。在实验中,它的某些位由外界直接控制,禁止写入的;有些读取时恒为0或1。
状态与控制
mstatus(Machine Status Register):包含一系列状态和控制位。例如其中一位控制CPU是否响应mie的变化,另一位控制能不能处理新的中断。sstatus(Supervisor Status Register):是mstatus寄存器中某些位的抽象子集。物理上并不需要单独保存它,而是与mstatus绑定在一起(按掩码写)。
内存管理
satp(Supervisor Address Translation and Protection Register):用于支持页式内存管理和内存隔离。它负责告诉CPU现在要不要进行地址翻译(即是否开启分页),以及页表的根地址在哪里。
其他
mscratch(Machine Scratch Register):文档要求在Lab4中实现此寄存器。在RISC-V规范中,它通常用于供机器模式的代码暂存一个关键的数据指针,帮助快速保存上下文。
涉及到这些寄存器比较少见(因为这意味着出现了中断、异常等),并不像通用寄存器那样要承担高频次的读写,因此不与有限数量的通用寄存器竞争(部分甚至不对应真实的物理内存)。然而,一旦其中的某些寄存器的值发生变化,意味着错误的指令已经进入了流水线中,需要刷新流水线,以避免错误操作继续。
RISC-V 架构定义了不同的特权级别(Privilege Levels),在这些模式下,软件所能执行的指令和能访问的 CSR 寄存器是严格受限的。
机器模式(Machine Mode,简称
M-mode)- 适用者: 底层固件(Firmware)、裸机程序(Bare-metal,如
Lab3/4)、Bootloader(如OpenSBI)。 - 权限:最高权限(绝对控制权)。可以无条件执行所有指令,直接访问所有物理内存和外设,读写所有级别的
CSR寄存器。所有RISC-V处理器都必须实现该模式。
- 适用者: 底层固件(Firmware)、裸机程序(Bare-metal,如
监管者模式(Supervisor Mode,简称
S-mode)- 适用者: 操作系统内核(OS Kernel,如
Linux、Windows)。 - 权限:中等权限(系统管理权)。核心权限是管理虚拟内存(分页机制)。可以读写
s级别的CSR,但禁止越权访问m级别的CSR。遇到无法处理的底层硬件异常时,需陷入(Trap)M-mode请求帮助。
- 适用者: 操作系统内核(OS Kernel,如
用户模式(User Mode,简称
U-mode)- 适用者: 普通应用程序(Applications,如你写的常规
C/C++程序、浏览器等)。 - 权限:最低权限(受限沙盒)。只能在操作系统分配的独立虚拟内存空间内运行。严禁执行任何特权指令或修改关键
CSR。如果需要读写文件或申请内存,必须通过ecall指令发起系统调用,主动陷入S-mode请求操作系统代为执行。
- 适用者: 普通应用程序(Applications,如你写的常规
三、具体实施
了解这些背景之后,针对五级流水线,本次 Lab4 相当于就是对新的 CSR 机器码进行解释,作出运算之后再将结果写回寄存器。因此采用类似 Lab1 的实施方式,即 decoder 解码、ex 运算、wb 写回。
- 解码
仍然交给 decoder:
本次 Lab4 中所有指令都会涉及到 CSR 寄存器的读写,专门排除了是系统指令但不是读写指令的 ecall/ebreak。也就是说,is_csr 表示当前指令是 CSR 读写类指令;它一定读取 CSR 旧值,但是否写 CSR 由 csr_should_write 决定。
接着完成 CSR 指令的相关内容:
- 提取
CSR寄存器地址csr_rid(csr register id),普通寄存器地址仍然通过rd读取。 - 提取立即数
zimm(zero-extended immediate,相对于普通符号扩展的imm)和rs1_id,后续会根据is_csr_imm的值选择其中一种。 - 判断
CSR类型csr_op。
接着,在 core 里面实例化。
- 完成
csr_file
这一部分对应 regfile,针对 CSR 指令提供单独处理写回操作的辅助模块。
首先注意两个特殊寄存器:mhartid 在本次 Lab4 中恒为 0,sstatus 是 mstatus 的子集,不需要新的寄存器来存储它的值。剩下的 10 个寄存器都在 csr_file 中分配 64 位空间用于存放它们的值,相对地为外界访问开出 12 个端口。之后,这个模块可以按照以下几步实施:
将端口连接到寄存器
组合逻辑:如果需要读取寄存器,就将对应寄存器的值传给
read_data(mhartid直接传0,sstatus根据它的mask从mstatus提取)时序逻辑:
- 如果
reset,就清空所有寄存器。 - 否则,如果写寄存器,根据写入地址和
mask从写入数据中抠出对应的数据(mhartid不用管,sstatus的操作相对较为复杂):
1
2CSR_SSTATUS: mstatus_reg <= (mstatus_reg & ~(SSTATUS_MASK & MSTATUS_MASK)) |
(write_data & SSTATUS_MASK & MSTATUS_MASK);这句命令先用
~(SSTATUS_MASK & MSTATUS_MASK)保留旧有mstatus的状态,再用“或”操作将新写入的值加进来。- 当不写入
mcycle的时候才更新它。
- 如果
注意,这里和 regfile 一样,存在着 Difftest 读取问题:真正的写入会在下一拍进行,但 Difftest 希望在这一拍就看到结果。csr_file 的组合输出采用 write-first 旁路:当本拍提交 CSR 写入时,输出端和读端直接反映写入后的可见值;真实 *_reg 在时钟沿更新
- 将
csr_file在core中实例化,并完成id_ex、ex_mem、mem_wb的信号传递
这里在 id_ex 寄存器中传递完整的 CSR 相关数据(is_csr、csr_use_imm、csr_op、csr_rid、csr_zimm、csr_old_value)。尤其是额外多传递一个 csr_old_value,因为部分 CSR 指令会用到旧的寄存器值。在 bubble 时,直接让这个值清零是否会导致数据丢失?并不会,因为这个值是从 csr_file 中的 CSR 寄存器中读取出来的,bubble 并不会影响真正的寄存器值,所以数据不会丢失。
经过 ex 计算之后,仍然需要从 decoder 传下来的信号就只剩 is_csr 和 csr_rid,其他的算完了就可以扔了。只需要把计算结果 csr_write_data 和 csr_should_write 一路传到 wb 阶段,用于写回时进行判断。其中,is_csr_out 这条信号用于判断当前指令是否是 CSR 指令,因此在 bubble 时需要严格控制,只在 valid 有效时传递。这个布尔值是 CSR 指令的命脉,只需要单独将它清零就行了。
- 完成
csr模块的搭建
这一步真正落实 CSR 相关的计算。csr_rd_value 是写回 GPR 的旧 CSR 值;csr_write_data 是未来要写入 CSR 的新值;csr_should_write 表示这条 CSR 指令语义上到底要不要改 CSR。
首先,根据 use_imm 来决定使用寄存器值还是 zimm 进行计算,寄存器值还是要用到 Lab2 的老朋友 forward。
接着,完成三类 CSR 操作的计算。
最后,CSR_RW 一定涉及写入,根据 is_csr 直接决定是否真的要写;CSR_RS 和 CSR_RC 根据 operand 的值是否全为 0 来判断是否真的要写入:
1 | |
这是因为 CSRRS/CSRRC 的规范语义并不只是“写回一个数值不变的新值”,而是规定了某些情况下根本不发生 CSR 写入:普通形式在 rs1 = x0 时只读不写,立即数形式在 zimm = 0 时只读不写。这样可以用 csrrs rd, csr, x0 这类形式只读取 CSR 旧值并写入 rd,而不对 CSR 本身产生写副作用。虽然本实验涉及的 CSR 副作用较少,不判断在很多情况下数值结果也可能一样,但为了符合指令语义,仍然需要生成 csr_should_write。
最后在 ex 模块中,根据 ex_result_real 来决定写回的数据来源是 alu 还是 csr:
1 | |
- 完成
csr_file的写入部分
写入 CSR 寄存器的内容被 csr 模块算出,因此可以写入了:
1 | |
即只有在允许写入(commit_fire = 1)、且是 CSR 指令、且确实应该写入的时候,才进行后续的写入操作。
- 完成
CSR导致的跳转
bonus:思考为什么一定要刷新流水线?
这是因为 CSR 寄存器的状态会影响整个 CPU 的“运行环境”。比如 CSR 会更新页表翻译机制(satp),或者影响中断机制(mie),或者切换运行模式从而改变一条指令是否合法。因此,一旦 CSR 的值改变,流水线中处在 IF 和 ID 阶段的指令有可能就是根据旧规则得到的错误指令,必须强制刷新。
解决办法就是在 ex 阶段额外判断当前指令(从 id_ex 寄存器中读出)是否是 CSR 指令。如果是,一律跳转到 CSR 指令对应的地址 +4 字节的位置(也就是 CSR 指令的下一条指令)。
- 完成
Difftest的接线
DifftestInstrCommit、DifftestArchIntRegState、DifftestTrapEvent、DifftestCSRState:修改mhartid的值(这里其实全是0)。
1 | |
DifftestCSRState额外将mstatus、sstatus、mepc、mtval、mtvec、mcause、satp、mip、mie、mscratch的值接入,也就是csr_file模块维护的寄存器状态。
四、bonus
bonus:实现 csr.sv 给定的所有寄存器,包括那些以 s 开头的寄存器(如 stvec 等等)
这些寄存器大概分成三类:
1. S-mode 相关寄存器
涉及寄存器:stvec、sscratch、sepc、scause、stval、sie、sip。
这些寄存器大多已经有对应的 M-mode 实现,在原理上大差不差。对于 stvec、sscratch、sepc、scause、stval,只需要在 csr_file 中新建对应端口和寄存器,做同样的读写与 mask 操作即可。其中只有 stvec 涉及 mask,可以参考 mtvec 的处理方式。最后在 core 中完成实例化和连线。
监管者模式下的中断逻辑对应第 1、5、9 位,因此定义 mask = 64'h222,用于让 sie 和 sip 分别从 mie 和 mip 中提取信息。Difftest 不看 sie/sip,因此不需要在 core 中提供新的输出端口或额外连线。
相对地,sstatus 虽然也是 mstatus 的子集,但是 Difftest 会检查这个量,所以需要提供一个端口。
2. 委托寄存器
涉及寄存器:medeleg、mideleg。
这两个寄存器分别告诉 CPU 哪些异常或中断可以交给监管者模式处理,从而避免底层机器模式频繁插手。在 csr_file 中增加对应寄存器,然后用 mask 过滤即可。
由于 Difftest 中有 medeleg/mideleg 端口,所以这两个寄存器也需要在 core 中实例化并完成连线。
3. PMP 寄存器
涉及寄存器:pmpcfg0、pmpaddr0。
这两个寄存器属于 RISC-V 的 PMP(Physical Memory Protection,物理内存保护)机制。在经过 satp 和页表将虚拟地址翻译成真实地址后,CPU 可以进一步根据这两个寄存器判断当前模式是否真的有权限访问对应的物理内存。
这两个寄存器没有额外的 mask,直接在 csr_file 里新增即可。Difftest 不看 pmpcfg0/pmpaddr0,所以不需要实际的输入输出端口。
五、最终输出
六、吐槽
这次实施我让 agent 帮我写了一份计划,但感觉功能还是不太明确。我大概是希望每一步单独完成一件事情,然后所有的连线和实例化单独完成,比如可以 1 完成功能,1.5 完成连线之类的。功能模块和连线放在一起感觉不是很明确。而且,很多计算相关的内容根本就没落实,你怎么连线。。。到最后还得一个个删掉或者修改,特别花时间。
同时,它还会引入不少的冗余逻辑。就算是看起来很机械的连线操作,也不是可以闭着眼睛交给 agent 的。有些信号完全没必要传递,它还非得为了“完整性”进行传递;还有就是命名,命名总是不太像人,名字还是需要自己选一个合适的。
同时,Gemini 的解释有时候会为了简单和形象而抛弃严谨。。。