第4章 RISC-V特权架构:特权级别、CSR寄存器与异常中断处理

好,咱们今天聊聊RISC-V的特权架构。说实话,这部分内容在初学者眼里往往有点「劝退」——又是特权级,又是CSR,又是异常中断的。但我想说,这部分恰恰是RISC-V最优雅的设计之一。你想想看,一个CPU如果没有特权级的保护,那用户程序岂不是可以随便改系统配置?那不乱套了。

4.1 特权级别:三个世界的划分

RISC-V定义了三个特权级别,从高到低分别是:机器模式(M-mode)监管模式(S-mode)用户模式(U-mode)。我个人习惯把它们比作三个「房间」:

  • M-mode:最高权限,相当于系统管理员。可以访问所有资源,负责启动、异常处理、物理内存保护等。这是所有RISC-V实现都必须有的模式。
  • S-mode:中等权限,相当于操作系统内核。负责虚拟内存管理、进程调度、中断处理等。运行Linux等操作系统时,内核就跑在这个模式下。
  • U-mode:最低权限,相当于普通用户。只能访问自己的那点东西,不能碰系统关键资源。用户程序就跑在这里。

为什么需要这么多级别?我在项目中遇到过这样一个坑:有个团队在嵌入式系统里把所有代码都跑在M-mode下,结果一个野指针直接写坏了系统配置寄存器,整个系统直接挂掉。嗯,这就是没有特权级隔离的后果。

核心要点:RISC-V规定,只有M-mode是必须实现的,S-mode和U-mode是可选的。但如果你想跑Linux,那S-mode是跑不掉的。

特权级之间的切换是通过异常中断触发的。比如用户程序执行了一条ecall指令,就会陷入到更高特权级的内核中去处理。这种机制保证了系统的安全性。

4.2 CSR寄存器:控制与状态的核心

CSR(Control and Status Register)是RISC-V特权架构的「控制面板」。每个CSR都有一个12位的地址编码,通过csrrwcsrrscsrrc等指令来读写。

常用的CSR寄存器有哪些?我列个表给你看:

CSR名称 地址 特权级 功能描述
mstatus 0x300 M 机器模式状态寄存器,控制全局中断使能、特权级切换等
mie 0x304 M 机器模式中断使能寄存器,控制各类中断的开关
mtvec 0x305 M 机器模式异常向量基址寄存器,指向异常处理程序的入口
mepc 0x341 M 机器模式异常PC寄存器,保存发生异常时的指令地址
mcause 0x342 M 机器模式异常原因寄存器,指示异常类型
stvec 0x105 S 监管模式异常向量基址寄存器
sepc 0x141 S 监管模式异常PC寄存器
scause 0x142 S 监管模式异常原因寄存器

这里我特别想提醒你注意mstatus寄存器。它里面有个MPP字段,记录了发生异常前的特权级。为什么重要?因为异常处理完后,你要通过mret指令返回,而mret会根据MPP字段决定返回到哪个特权级。我曾经见过有人忘了设置这个字段,结果异常返回后直接跑飞了。

小技巧:写CSR时,建议用csrrscsrrc这种原子操作,而不是先读再写。这样可以避免中断处理过程中出现竞态条件。

4.3 异常与中断处理:CPU的「紧急响应机制」

异常和中断,说白了就是CPU在执行过程中遇到了「意外情况」。异常是同步的,比如非法指令、缺页错误;中断是异步的,比如定时器到期、外部设备请求。

RISC-V的异常处理流程非常清晰,我总结为以下几步:

  1. 保存现场:CPU自动将当前PC保存到mepc(或sepc),将异常原因保存到mcause(或scause)。
  2. 切换特权级:CPU切换到M-mode(或S-mode),并关闭全局中断使能。
  3. 跳转到处理程序:根据mtvec(或stvec)指向的地址,跳转到异常处理程序。
  4. 处理异常:软件分析mcause,执行相应的处理逻辑。
  5. 恢复现场:执行mret(或sret)指令,恢复之前的PC和特权级。

这里有个细节:mtvec寄存器可以配置两种模式:直接模式向量模式。直接模式下,所有异常都跳转到同一个入口;向量模式下,不同异常跳转到不同的偏移地址。我个人习惯用向量模式,因为可以减少异常分发的时间开销。

避坑指南:我曾经在移植FreeRTOS到RISC-V平台时,发现中断响应特别慢。排查了半天,原来是mtvec配置成了直接模式,每次中断都要软件去查mcause做分发。改成向量模式后,性能提升了一倍。

4.4 中断嵌套与优先级

中断嵌套是个老生常谈的话题。RISC-V的中断优先级其实很简单:M-mode的中断优先级高于S-mode,S-mode高于U-mode。同特权级内的中断优先级,由软件自己决定。

为什么RISC-V不搞硬件优先级?我觉得这是RISC-V设计哲学的一个体现——「保持简单」。把优先级决策交给软件,虽然增加了软件复杂度,但给了开发者更大的灵活性。你想想看,在实时系统中,你可能需要动态调整中断优先级,硬件固定的优先级反而成了束缚。

实现中断嵌套的关键在于:在进入中断处理程序后,手动打开全局中断使能。但要注意,这需要你保存好现场,否则嵌套中断回来时,寄存器已经被破坏了。

# 中断嵌套示例(汇编伪代码)
interrupt_handler:
    # 保存上下文
    csrrw sp, mscratch, sp    # 交换sp和mscratch
    # 保存通用寄存器到栈上
    ...
    
    # 打开全局中断使能,允许嵌套
    csrrs zero, mstatus, (1 << 3)   # 设置MIE位
    
    # 处理中断
    ...
    
    # 关闭中断使能
    csrrc zero, mstatus, (1 << 3)
    
    # 恢复上下文
    ...
    csrrw sp, mscratch, sp
    mret

重要提醒:中断嵌套时,栈空间要预留足够。我曾经在资源受限的MCU上,因为栈溢出导致系统崩溃,查了三天才找到原因。建议每个中断处理程序至少预留256字节的栈空间。

4.5 实战经验:从零搭建异常处理框架

好了,理论说完了,咱们来点实际的。假设你要在RISC-V上写一个简单的RTOS,异常处理框架该怎么搭?

第一步,设置mtvec。我建议用向量模式:

# 设置异常向量表基址
la t0, vector_table
csrw mtvec, t0
# 设置向量模式(最低位为1)
ori t0, t0, 1
csrw mtvec, t0

第二步,编写异常向量表。每个异常入口占4字节,存放跳转指令:

.section .vectors, "ax"
vector_table:
    j handle_reset        # 复位异常
    j handle_illegal_insn # 非法指令异常
    j handle_syscall      # 系统调用异常
    j handle_timer_int    # 定时器中断
    j handle_ext_int      # 外部中断
    ...

第三步,实现具体的处理函数。以系统调用为例:

handle_syscall:
    # 保存上下文
    addi sp, sp, -128
    sw ra, 0(sp)
    sw a0, 4(sp)
    ...
    
    # 根据a7寄存器(系统调用号)分发
    # 执行系统调用服务
    ...
    
    # 恢复上下文
    lw ra, 0(sp)
    lw a0, 4(sp)
    ...
    addi sp, sp, 128
    mret

嗯,这个框架虽然简单,但已经能覆盖大部分场景了。实际项目中,你还需要考虑嵌套中断、中断优先级、上下文切换等更复杂的问题。不过别急,后面章节我们会逐一深入。

最后说一句:特权架构是RISC-V的「灵魂」所在。理解了它,你就理解了为什么RISC-V既能跑简单的嵌入式系统,又能跑复杂的Linux。这部分内容值得你花时间好好消化。