2. RISC-V异常类型详解:同步异常与精确异常
好,咱们接着聊。上一章我们把异常和中断的基本概念理清了,这一章我打算深入讲讲RISC-V里的异常类型。说白了,异常就是CPU在执行指令时遇到的“意外情况”。但意外也分很多种,有的能恢复,有的直接崩了;有的能精确定位,有的让你抓瞎。
我个人习惯把异常分成两大类:同步异常和异步异常。中断属于异步,这个我们后面专门讲。今天重点说同步异常——它是由指令执行本身触发的,跟外部信号无关。
2.1 同步异常的分类
同步异常,你想想看,就是CPU执行某条指令时,发现不对劲了。比如指令本身非法,或者访问了不该访问的内存。RISC-V规范里把同步异常分成了几个子类,我挑几个最常用的说说。
2.1.1 指令异常
指令异常,顾名思义,是取指令阶段出的问题。常见的有这么几种:
- 指令地址未对齐:PC指向了一个非对齐的地址。比如你想从0x1003取一条32位指令,但地址没对齐到4字节边界。RISC-V不允许这样。
- 指令访问错误:取指令时发生了物理内存错误,比如访问了不存在的内存区域。
- 非法指令:指令编码不在RISC-V定义的范围内。我遇到过有人手写机器码时写错了opcode,结果CPU直接跳异常。
- 断点异常:执行了
ebreak指令。调试时常用。
重点:指令异常发生在指令执行之前。也就是说,这条指令根本没机会改变任何寄存器或内存状态。这对精确异常来说很重要,后面会讲。
2.1.2 数据异常
数据异常是访存指令(load/store)触发的。我在项目中调试过最头疼的就是这类异常,因为往往要追很久才能找到是哪个指针写飞了。
- load/store地址未对齐:比如你想从0x1001读一个32位整数,但地址没对齐到4字节。RISC-V默认不支持非对齐访问(除非你开了C扩展或者手动处理)。
- load/store访问错误:访问了物理上不存在的内存区域,或者权限不够。
- 页错误:开启了虚拟内存后,页表里找不到对应的物理页。这个在操作系统里很常见。
我的经验:在FPGA上调试时,数据异常往往是因为地址总线连错了,或者内存控制器没初始化好。我曾经花了一整天,最后发现是地址偏移算错了4个字节……嗯,从那以后我写地址计算时都会反复核对。
2.1.3 环境调用异常
这个比较特殊。它是由ecall指令主动触发的。说白了,就是用户程序想请求操作系统帮忙,比如读写文件、分配内存。RISC-V里ecall会根据当前的特权级触发不同的异常:
- U模式下执行
ecall→ 触发环境调用来自U模式异常 - S模式下执行
ecall→ 触发环境调用来自S模式异常 - M模式下执行
ecall→ 触发环境调用来自M模式异常
你可能会问:M模式还能调用谁?其实M模式是最高权限,它调用ecall通常是为了触发一个异常,让更底层的固件处理。不过实际中很少这么用。
2.2 精确异常 vs 不精确异常
好,接下来这部分我觉得是很多工程师容易搞混的——精确异常和不精确异常。这个概念在RISC-V里尤其重要,因为它直接决定了你写异常处理程序时的复杂度。
2.2.1 什么是精确异常?
精确异常的意思是:当异常发生时,处理器能精确地告诉你——是哪条指令触发了异常,以及这条指令之前的所有指令都已经执行完毕,之后的指令一条都没执行。
说白了,异常发生时的处理器状态是“干净”的。你可以安全地保存现场、处理异常、然后恢复执行。RISC-V的同步异常都是精确异常。这是规范强制要求的。
为什么RISC-V要强制精确异常? 因为RISC-V的设计哲学是简洁、确定。精确异常让软件处理起来非常直观——你只需要查mepc寄存器,就知道异常发生在哪条指令上。
2.2.2 不精确异常长什么样?
不精确异常就麻烦多了。它通常出现在高性能处理器里,比如超标量、乱序执行的CPU。当异常发生时,可能已经有多条指令在执行中,有的完成了,有的没完成。处理器无法精确告诉你“哪条指令是罪魁祸首”。
举个例子:
指令1: ld x1, 0(x2) // 加载数据
指令2: add x3, x1, x4 // 使用加载结果
指令3: sd x3, 0(x5) // 存储结果
如果指令1发生了页错误,但指令2和指令3已经乱序执行完了……那处理器就尴尬了。它没法回滚指令2和指令3的效果。这就是不精确异常。
注意:RISC-V规范允许实现不精确异常,但仅限于非精确的浮点异常。其他所有同步异常都必须是精确的。如果你在FPGA上实现RISC-V核,我建议你一开始就只做精确异常,省得后面调试到怀疑人生。
2.2.3 精确异常在FPGA中的实现要点
在FPGA里实现精确异常,核心就是保证异常发生时,流水线里的状态是可控的。我一般这么做:
- 每个流水级都保存
pc值,异常发生时逐级回溯 - 在写回级(Write Back)之前,所有指令都不允许修改架构状态(寄存器、内存)
- 异常信号一旦产生,立即冻结流水线,防止后续指令继续执行
嗯,这里要注意:如果你用了乱序执行或者多发射,精确异常的实现会复杂很多。但对于大多数FPGA上的RISC-V实现(比如单发射、顺序执行),精确异常是天然成立的。
2.3 异常处理流程速览
最后,我画了一张图,帮你把异常处理的整个流程串起来。从异常触发到返回,一共就几步:
这张图里,从异常触发到mret返回,一共五个步骤。你在FPGA里实现时,重点要保证mepc和mcause在异常发生时被正确锁存。我见过有人把mepc锁存到了下一条指令的PC,结果返回时直接死循环了……
2.4 小结
这一章我们聊了RISC-V的同步异常分类,包括指令异常、数据异常和环境调用。重点强调了精确异常的概念——它是RISC-V设计里一个非常优雅的特性,让异常处理变得简单可靠。在FPGA上实现时,只要保证流水线状态可控,精确异常是很容易做到的。
下一章我们会深入异常处理的具体实现,包括寄存器配置和优先级仲裁。到时候我会拿一个实际的FPGA项目代码来拆解,敬请期待。
公众号:蓝海资料掘金营,微信deep3321