2. RISC-V指令集架构基础:RV32I基础指令集解析、指令编码格式、寄存器约定与ABI、特权架构简介
好,咱们正式开始啃指令集这块硬骨头。很多同学一听到“指令集架构”就觉得是理论课,想打瞌睡。我当年刚接触RISC-V时也这么想,直到在FPGA上跑第一个裸机程序,发现一条lui指令译码死活不对,才老老实实回头把指令编码格式翻了个底朝天。说白了,指令集就是你跟处理器核沟通的“合同”——你写什么二进制,它就得干什么事。
2.1 RV32I基础指令集解析
RV32I是RISC-V的“最小公倍数”子集。它只有不到50条指令,但足够跑一个完整的操作系统。我经常跟团队说:如果你能把RV32I在FPGA上验证透了,后面加乘除法、原子操作、压缩指令,都是锦上添花。
RV32I按功能分成以下几类:
| 类别 | 指令数 | 典型指令 | 我的验证关注点 |
|---|---|---|---|
| 整数运算 | 13 | ADD, SUB, AND, OR, XOR, SLL, SRL, SRA | 溢出标志?RISC-V没有标志位,全靠软件处理 |
| 逻辑运算 | 4 | ANDI, ORI, XORI | 立即数符号扩展是否正确 |
| 移位运算 | 6 | SLLI, SRLI, SRAI | 移位量只取低5位,别多移 |
| 加载/存储 | 8 | LB, LH, LW, LBU, LHU, SB, SH, SW | 非对齐访问要触发异常 |
| 分支跳转 | 8 | BEQ, BNE, BLT, BGE, BLTU, BGEU, JAL, JALR | 分支预测错误时的流水线冲刷 |
| 控制状态 | 6 | ECALL, EBREAK, FENCE, FENCE.I, CSRRW, CSRRS | CSR读写权限检查 |
关键认知:RV32I没有乘除法指令(那是M扩展的事),没有条件执行(ARM那套),没有标志位寄存器。所有比较结果都通过通用寄存器传递。这简化了硬件,但编译器要更聪明。
我记得第一次用Verilog写RISC-V译码器时,被SLTI和SLTIU的区别坑过——一个有符号比较,一个无符号比较,立即数都要先做符号扩展。如果你在FPGA上验证时发现比较结果反了,八成是符号扩展位没处理好。
2.2 指令编码格式:六种“模具”
RISC-V最优雅的设计之一,就是所有指令都只用六种编码格式。不像x86那种变长指令,译码器写到你怀疑人生。这六种格式分别是:R型、I型、S型、B型、U型、J型。
我习惯把它们想象成六套“模具”,每套模具上有固定的“槽位”(字段),不同的指令往对应的槽位里填不同的值。
实战技巧:在FPGA验证中,我建议你写一个“指令解码器”的独立测试模块。把六种格式的指令各挑几条,用$display打印出每个字段的值,跟RISC-V规范逐位比对。我曾经用这个方法半小时内抓到了三个译码bug。
2.3 寄存器约定与ABI
RISC-V有32个通用寄存器,每个都是32位宽(RV32)。但硬件并不限制你怎么用它们——寄存器就是寄存器。不过,为了让人写的代码和编译器生成的代码能愉快地合作,大家约定了一套调用约定(Calling Convention),也就是ABI(Application Binary Interface)。
这套约定不是硬性规定,但你不遵守的话,函数调用就会乱套。我见过一个项目,有人把sp(栈指针)当普通寄存器用,结果函数返回时栈指针指向了错误地址,程序直接跑飞。嗯,查这种bug真的很痛苦。
| 寄存器 | ABI名称 | 用途 | 谁负责保存 |
|---|---|---|---|
| x0 | zero | 硬连线为0,写入无效 | — |
| x1 | ra | 返回地址 | 调用者 |
| x2 | sp | 栈指针 | 被调用者 |
| x3 | gp | 全局指针 | — |
| x4 | tp | 线程指针 | — |
| x5–x7 | t0–t2 | 临时寄存器 | 调用者 |
| x8 | s0/fp | 保存寄存器/帧指针 | 被调用者 |
| x9 | s1 | 保存寄存器 | 被调用者 |
| x10–x17 | a0–a7 | 函数参数/返回值 | 调用者 |
| x18–x27 | s2–s11 | 保存寄存器 | 被调用者 |
| x28–x31 | t3–t6 | 临时寄存器 | 调用者 |
避坑指南:我曾经在验证一个RTOS上下文切换时,发现任务切换后ra寄存器值不对。查了两天才发现,是编译器把ra当临时寄存器用了,但ABI规定ra必须由调用者保存。解决方案:在任务切换的汇编代码中,手动保存和恢复所有“调用者保存”寄存器。
2.4 特权架构简介
RV32I本身只定义了用户模式(U-Mode)下的指令行为。但一个完整的处理器核,尤其是要跑Linux的,必须支持特权架构。RISC-V定义了三个特权级别:
- 机器模式(M-Mode):最高权限,必须实现。处理异常、中断、物理内存属性。Bootloader和固件跑在这里。
- 监管模式(S-Mode):可选,用于运行操作系统内核。支持页表、虚拟内存、异常委托。
- 用户模式(U-Mode):可选,运行应用程序。权限最低,不能直接访问CSR。
在FPGA原型验证中,我建议你从M-Mode开始验证。为什么呢?因为M-Mode是基础,所有异常处理、中断响应、CSR读写都在这里定义。我记得第一次在FPGA上验证mret指令时,从异常处理程序返回后PC值不对,原因是mepc寄存器没有正确更新——嗯,后来发现是译码器把mret当成普通指令处理了,没有做特殊控制。
验证重点:特权架构的验证核心是CSR(Control and Status Registers)。RV32I定义了至少6个必须实现的CSR:mstatus、mepc、mcause、mtvec、mip、mie。每个CSR的读写权限、位域定义、复位值,都要在验证计划中逐条列出。
特权架构还有一个重要概念叫异常委托(Exception Delegation)。简单说,就是M-Mode可以把某些异常类型“委托”给S-Mode处理,这样操作系统可以直接处理缺页异常、系统调用,而不需要每次都陷入M-Mode。我在验证这个功能时,写了一个测试用例:在U-Mode下执行ecall,然后检查medeleg寄存器是否把ecall-from-U-mode委托给了S-Mode。结果发现委托位设置正确,但S-Mode的异常向量表没初始化,导致程序跳转到0地址——又是一个经典的“忘记初始化”bug。