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

好,我们正式开始啃 RISC-V 最核心的 RV32I 基础指令集。这部分是 RISC-V 的基石,一共 40 多条指令,但别被数字吓到——它们分类清晰,每条指令都有明确用途。我个人习惯把这部分分成五类来学:整数运算、逻辑移位、比较分支、跳转和 Load/Store。今天我们就逐个击破。

整数运算指令:加减与立即数

整数运算指令是 CPU 最基础的操作。RV32I 里只有加法(ADD)和减法(SUB),没有乘除——乘除在 M 扩展里。为什么这么设计?说白了,RISC-V 追求极简,把复杂操作留给软件组合或可选扩展。

核心指令速览:

  • ADD rd, rs1, rs2 —— rd = rs1 + rs2
  • SUB rd, rs1, rs2 —— rd = rs1 - rs2
  • ADDI rd, rs1, imm12 —— rd = rs1 + 符号扩展的 12 位立即数

这里有个细节:ADDI 的立即数是符号扩展的。这意味着你可以用 ADDI 加负数,变相实现减立即数。我在项目中遇到过有人写 ADDI x5, x5, -1 来实现自减,完全没问题。

另外,RISC-V 没有 SUBI 指令。为什么?因为 ADDI 加负数已经覆盖了。你想想看,少一条指令,硬件就少一个解码分支,面积和功耗都能省一点。

逻辑指令:与或异或

逻辑指令包括 AND、OR、XOR 以及它们的立即数版本 ANDI、ORI、XORI。这些指令按位操作,常用于掩码、置位、清零和校验。

指令 功能 典型用途
AND rd, rs1, rs2 按位与 提取位域、清零特定位
OR rd, rs1, rs2 按位或 置位特定位
XOR rd, rs1, rs2 按位异或 翻转位、校验
ANDI rd, rs1, imm12 立即数与 快速掩码
ORI rd, rs1, imm12 立即数或 快速置位
XORI rd, rs1, imm12 立即数异或 快速翻转

避坑指南:我曾经在调试一个 SPI 控制器时,发现数据总是不对。查了半天,原来是掩码写错了——ANDI 的立即数只有 12 位,符号扩展后高位全是 1,把不该清的数据也清了。记住:立即数只有 12 位,高位会符号扩展!

移位指令:逻辑左移、逻辑右移、算术右移

移位指令有三条:SLL(逻辑左移)、SRL(逻辑右移)、SRA(算术右移)。它们都有立即数版本:SLLI、SRLI、SRAI。

逻辑右移和算术右移的区别,嗯,这里要注意:逻辑右移高位补 0,算术右移高位补符号位(即原数的最高位)。

// 假设 x5 = 0x80000001(最高位为 1)
SRLI x6, x5, 1   // x6 = 0x40000000(高位补 0)
SRAI x7, x5, 1   // x7 = 0xC0000000(高位补 1,保持符号)

为什么需要算术右移?因为对有符号数做除法时,算术右移相当于除以 2 并向下取整。我在做定点数运算时经常用到这个特性。

比较指令:SLT 与 SLTU

比较指令只有两条:SLT(有符号小于)和 SLTU(无符号小于)。它们把比较结果(0 或 1)写入目标寄存器。

  • SLT rd, rs1, rs2 —— 若 rs1 < rs2(有符号),则 rd = 1,否则 rd = 0
  • SLTU rd, rs1, rs2 —— 若 rs1 < rs2(无符号),则 rd = 1,否则 rd = 0
  • SLTI rd, rs1, imm12 —— 立即数版本的有符号比较
  • SLTIU rd, rs1, imm12 —— 立即数版本的无符号比较

常见陷阱:有符号和无符号比较的结果可能完全不同。比如 0xFFFFFFFF 和 0x00000001:有符号下 0xFFFFFFFF 是 -1,小于 1;无符号下 0xFFFFFFFF 是 4294967295,远大于 1。选错比较指令,条件判断就全错了。

分支跳转指令:控制流的灵魂

分支指令改变程序执行流。RV32I 有 6 条条件分支指令和 2 条无条件跳转指令。

条件分支(BEQ、BNE、BLT、BGE、BLTU、BGEU):

  • BEQ:相等则跳转
  • BNE:不等则跳转
  • BLT:有符号小于则跳转
  • BGE:有符号大于等于则跳转
  • BLTU:无符号小于则跳转
  • BGEU:无符号大于等于则跳转

无条件跳转(JAL、JALR):

  • JAL:跳转并链接(保存返回地址到 rd)
  • JALR:通过寄存器间接跳转并链接

这里有个设计巧思:JAL 的 rd 如果设为 x0(零寄存器),就相当于普通跳转,不保存返回地址。RISC-V 用这种方式减少指令数量,一条 JAL 同时覆盖了函数调用和普通跳转。

分支指令的编码格式:分支指令的立即数编码比较特殊,不是连续的 12 位,而是分散在指令的不同位置。这是为了硬件实现时能更快地计算目标地址。嗯,设计者做了取舍——软件编译时多花点功夫解码,硬件执行时就能快一点。

Load/Store 指令:与内存打交道

Load/Store 指令是 CPU 访问内存的唯一途径。RISC-V 是典型的 Load/Store 架构——只有这两类指令能访问内存,运算指令只能在寄存器之间操作。

Load 指令:

  • LB:加载字节(有符号扩展)
  • LH:加载半字(有符号扩展)
  • LW:加载字
  • LBU:加载字节(无符号扩展)
  • LHU:加载半字(无符号扩展)

Store 指令:

  • SB:存储字节
  • SH:存储半字
  • SW:存储字

注意 Load 有符号扩展和无符号扩展的区别。LB 把 8 位数据符号扩展到 32 位,LBU 则高位补 0。选错的话,数据值可能完全不对。

// 假设内存地址 0x1000 处存放 0x80
LB x5, 0(x0)    // x5 = 0xFFFFFF80(符号扩展)
LBU x6, 0(x0)   // x6 = 0x00000080(无符号扩展)

我的经验:在 SoC 集成调试时,Load/Store 指令是最容易出问题的。我曾经遇到一个 bug:CPU 读外设寄存器时总是读到 0,查了两天才发现是地址对齐问题——外设要求 4 字节对齐访问,而我用了 LB 读一个 32 位寄存器的低字节。记住:外设寄存器通常要求按字访问,别用字节指令去读。

知识体系总览

下面这张图把 RV32I 基础指令集的结构梳理清楚了。你可以把它当作速查地图。

RV32I 基础指令集知识体系 RV32I 基础指令集知识体系 整数运算 逻辑指令 移位指令 比较分支 Load/Store ADD / SUB ADDI AND/OR/XOR ANDI/ORI/XORI SLL/SRL/SRA SLLI/SRLI/SRAI SLT/SLTU BEQ/BNE/BLT/BGE JAL/JALR LB/LH/LW/LBU/LHU SB/SH/SW 核心设计哲学 极简指令集:每条指令都有明确用途,无冗余 Load/Store 架构:只有 Load/Store 指令访问内存,运算在寄存器间完成 RV32I 共约 40 条指令 整数运算 3 条 | 逻辑 6 条 | 移位 6 条 | 比较 4 条 | 分支 8 条 | Load/Store 8 条 所有指令均为 32 位定长编码,简化硬件实现

RV32I 这 40 条指令,每条我都亲手写过对应的汇编代码,也在 FPGA 上跑过。说实话,刚开始觉得它们太简单,后来才发现——正是这种简单,让 RISC-V 的硬件实现异常干净,也让调试变得可控。你想想看,如果每条指令都像 x86 那样变长编码、隐含副作用,光解码器就能把人逼疯。