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 异常处理流程速览

最后,我画了一张图,帮你把异常处理的整个流程串起来。从异常触发到返回,一共就几步:

RISC-V同步异常处理流程 指令执行触发异常 保存PC到mepc 设置mcause和mtval 跳转到mtvec处理 mret返回恢复执行 关键寄存器 mepc: 异常返回地址 mcause: 异常原因编码 mtval: 异常附加信息 mtvec: 异常入口基址 mstatus: 全局中断使能 注意:所有同步异常 处理时,mepc指向 触发异常的指令 (而非下一条)

这张图里,从异常触发到mret返回,一共五个步骤。你在FPGA里实现时,重点要保证mepcmcause在异常发生时被正确锁存。我见过有人把mepc锁存到了下一条指令的PC,结果返回时直接死循环了……

2.4 小结

这一章我们聊了RISC-V的同步异常分类,包括指令异常、数据异常和环境调用。重点强调了精确异常的概念——它是RISC-V设计里一个非常优雅的特性,让异常处理变得简单可靠。在FPGA上实现时,只要保证流水线状态可控,精确异常是很容易做到的。

下一章我们会深入异常处理的具体实现,包括寄存器配置和优先级仲裁。到时候我会拿一个实际的FPGA项目代码来拆解,敬请期待。


公众号:蓝海资料掘金营,微信deep3321