2. RISC-V指令集架构基础

在把处理器从 RTL 一路带到 Linux 启动的过程中,指令集架构(ISA)是软硬件之间的第一份“契约”。RISC-V 的 ISA 采用模块化设计:基础整数指令集 RV32I 与 RV64I 定义了最核心的整数运算、访存、跳转和控制流;而特权架构则定义了机器态、监管态、用户态以及 CSR、异常和中断。理解这一层,才能看懂后续流水线译码、CSR 访问、trap 入口和 Linux 启动时的模式切换。

一、RV32I/RV64I 的指令格式

RV32I 的指令长度固定为 32 位,低 7 位是 opcode,用于区分指令大类。常见格式有 R、I、S、B、U、J 六种。它们并不是随意划分,而是围绕“源寄存器、目的寄存器、立即数、功能码”这几个字段做复用,从而在固定 32 位宽度内表达不同语义。

  • R 型:寄存器—寄存器运算,如 add x1, x2, x3,包含 funct7、rs2、rs1、funct3、rd、opcode。
  • I 型:立即数运算与加载,如 addi x1, x2, 10、lw x1, 0(x2),包含 12 位立即数。
  • S 型:存储指令,如 sw x1, 8(x2),立即数被拆成两段,分别放在指令的高位和 rs2 位置。
  • B 型:条件分支,如 beq x1, x2, label,立即数以 2 字节为单位编码,支持 ±4KB 范围。
  • U 型:高位立即数,如 lui、auipc,提供 20 位立即数并放到高 20 位。
  • J 型:跳转并链接,如 jal x1, label,立即数同样按 2 字节对齐编码。

RV64I 在 RV32I 基础上扩展了 64 位整数运算,例如 addw、sllw、ld、sd 等,寄存器宽度变为 64 位,地址空间和算术范围也随之扩大。但指令格式本身仍然保持 32 位定长,这为译码器设计带来很大便利。

RV32I 六种指令格式字段布局 固定 32 位宽度,opcode 位于低 7 位,不同格式复用字段表达不同语义 R 型 funct7 rs2 rs1 funct3 rd opcode I 型 imm[11:0] funct3 rd opcode S 型 imm[11:5] rs2 rs1 funct3 imm[4:0] opcode B 型 imm[12|10:5] rs2 rs1 funct3 imm[4:1|11] opcode U 型 imm[31:12] rd opcode J 型 imm[20|10:1|11|19:12] rd opcode

二、寄存器组织与 ABI 约定

RV32I/RV64I 定义了 32 个通用整数寄存器 x0~x31,其中 x0 恒为 0,写入被忽略。其余寄存器在汇编层面通常使用 ABI 名称,例如 ra、sp、a0、t0、s0 等。这些名称不是硬件强制,而是软件约定,决定了函数调用、参数传递和栈帧布局。

寄存器ABI 名用途调用约定
x0zero常量 0只读
x1ra返回地址调用者保存
x2sp栈指针被调用者保存
x5-x7t0-t2临时寄存器调用者保存
x10-x11a0-a1参数/返回值调用者保存
x8-x9, x18-x27s0-s11保存寄存器被调用者保存

此外还有程序计数器 pc,它不通过通用寄存器暴露,而是由硬件维护。RISC-V 没有延迟槽,分支和跳转的目标地址在指令执行时直接决定,这使流水线控制比某些传统架构更清晰。

三、特权模式与 CSR 寄存器

RISC-V 特权架构定义了三个层级:机器模式(M-mode)、监管模式(S-mode)和用户模式(U-mode)。M-mode 是最高权限,负责启动、时钟中断、异常入口和物理内存保护;S-mode 通常运行操作系统内核;U-mode 运行用户进程。Linux 启动时,固件先运行在 M-mode,随后通过 mret 切换到 S-mode,把控制权交给内核。

CSR(Control and Status Register)是特权模式的核心接口,通过 csrrw、csrrs、csrrc 等指令访问。常见 CSR 包括:

  • mstatus:全局中断使能、特权模式、浮点状态等。
  • mtvec:机器模式 trap 入口地址和模式。
  • mepc:异常发生时的返回地址。
  • mcause:异常或中断的原因编码。
  • satp:S-mode 地址翻译与分页配置。
  • sstatus、stvec、sepc、scause:S-mode 对应副本。

关键点:CSR 访问必须满足当前特权级。U-mode 访问 M-mode CSR 会触发非法指令异常;S-mode 访问某些 M-mode CSR 也会被拦截。这种分层是操作系统隔离和安全启动的基础。

四、异常与中断机制

异常是当前指令执行过程中产生的同步事件,例如非法指令、加载地址不对齐、断点、系统调用;中断是外部异步事件,例如定时器中断、外部中断。RISC-V 把它们统一称为 trap,并共用一套入口机制。

当 trap 发生时,硬件会做几件事:把当前 pc 保存到 mepc 或 sepc;把原因写入 mcause 或 scause;把 trap 类型和中断标志写入 mtval 或 stval;然后跳转到 mtvec 或 stvec 指定的入口。入口可以配置为直接模式或向量模式,向量模式下不同中断有不同偏移。

# 典型的 M-mode trap 入口伪代码
trap_entry:
    csrrw t0, mscratch, t0      # 交换临时寄存器
    sw    t1, 0(t0)             # 保存现场
    csrr  t1, mcause            # 读取异常原因
    csrr  t2, mepc              # 读取返回地址
    ...
    csrrw t0, mscratch, t0
    mret                        # 返回并恢复特权级

调试提示:如果 Linux 启动时卡在早期 trap,优先检查 mtvec 是否指向有效入口、mepc 是否保存了正确返回地址,以及 mcause 的低位编码是否与预期异常一致。

五、常见误区与复盘

  • 误区一:把 ABI 名当成硬件寄存器名。硬件只认 x0~x31,ABI 名是汇编器和调用约定层面的映射。
  • 误区二:忽略 x0 的只读特性。向 x0 写入不会改变其值,但可能影响指令编码和译码逻辑。
  • 误区三:认为所有 CSR 都能在任意模式访问。CSR 有特权级限制,越权访问会触发异常。
  • 误区四:把异常和中断混为一谈。异常是同步的,中断是异步的,但二者共用 trap 入口和 CSR 保存机制。

复盘时可以用一个最小场景验证:在 M-mode 下设置 mtvec,执行一条非法指令,观察 mcause 是否变为非法指令编码,mepc 是否指向该指令地址。这个实验能同时验证指令译码、CSR 访问和 trap 入口三条路径,是后续 RTL 调试和 Linux 启动分析的重要基础。