第2章 RISC-V指令集架构基础:RV32I核心指令与编码格式

各位同学,欢迎来到第二章。这一章我们聊聊RISC-V的指令集架构基础。说白了,指令集就是CPU和软件之间的“合同”——软件按这个格式写,CPU按这个格式读。RV32I是RISC-V的基石,搞懂它,后面的特权架构、中断处理、外设驱动才能顺起来。

我个人习惯把指令集分成两个层面来理解:编码格式(二进制长什么样)和语义(CPU该怎么执行)。咱们先看编码格式。

2.1 指令编码格式:六种基本类型

RISC-V的指令长度固定为32位(RV32I)。但它的编码格式非常规整,一共只有6种:R、I、S、U、B、J。你想想看,x86那套变长指令动辄几十种格式,RISC-V用6种就覆盖了整数指令全集,这就是精简的魅力。

格式典型指令操作数特点
RADD, SUB, AND, ORrd, rs1, rs2三个寄存器操作数,无立即数
IADDI, LW, JALRrd, rs1, imm[11:0]一个12位有符号立即数
SSW, SBrs1, rs2, imm[11:0]存储指令,立即数分两段
ULUI, AUIPCrd, imm[31:12]20位高位立即数
BBEQ, BNE, BLTrs1, rs2, imm[12:1]条件分支,立即数编码最复杂
JJALrd, imm[20:1]无条件跳转,21位有符号立即数

关键点:所有指令的opcode(操作码)都在最低7位[6:0]。这意味着译码器只需要看最低7位就能确定指令类别。我在做FPGA实现时,第一级译码就用一个7-bit的case语句,干净利落。

这里有个细节——B格式和S格式的立即数编码是故意错位的。B格式的立即数位[12|10:5|4:1|11]被拆成4段,而S格式是[11:5|4:0]。为什么?因为B格式的立即数在硬件上需要左移1位(分支目标地址是偶数),这种编码方式让立即数位的位置和R格式的rs2/rs1字段对齐,简化了译码逻辑。嗯,设计者确实花了心思。

2.2 RV32I基础指令集详解

RV32I一共只有47条指令(算上伪指令也就50多条)。我当年从ARM转到RISC-V时,第一反应是“就这?”——但实际用下来发现,这些指令组合起来足够高效。

2.2.1 算术运算指令

  • ADD/SUB:加减法,R格式
  • ADDI:加立即数,I格式。注意:没有SUBI,减法用ADDI加负数实现
  • LUI:加载高位立即数,U格式。加载20位到寄存器[31:12]
  • AUIPC:PC加立即数,U格式。常用于位置无关代码

我的经验:在FPGA上实现LUI时,要注意它不修改低12位。如果你需要加载一个32位常数,通常用LUI+ADDI组合。但ADDI会符号扩展,所以当常数低12位的最高位为1时,需要额外加一条ADDI来修正。这个坑我踩过——当时仿真一直对不上,查了半天才发现是符号扩展的问题。

2.2.2 逻辑与移位指令

  • AND/OR/XOR:按位与/或/异或,R格式
  • ANDI/ORI/XORI:与/或/异或立即数,I格式
  • SLL/SRL/SRA:左移/逻辑右移/算术右移,R格式
  • SLLI/SRLI/SRAI:移位立即数,I格式(移位量用低5位)

注意:RV32I没有带进位的加减法(ADC/SBB),也没有乘除法指令(这些在M扩展里)。这是RISC-V的模块化设计思想——基础指令集只做最核心的事。

2.2.3 比较指令

  • SLT/SLTU:有符号/无符号小于则置位,R格式
  • SLTI/SLTIU:有符号/无符号小于立即数则置位,I格式

比较指令的结果是0或1,存在rd里。为什么没有大于、大于等于?因为这些都可以通过交换操作数或组合指令实现。指令集设计者追求的是最小完备集。

2.2.4 分支与跳转指令

  • BEQ/BNE/BLT/BGE/BLTU/BGEU:条件分支,B格式
  • JAL:跳转并链接,J格式。保存返回地址到rd
  • JALR:跳转并链接寄存器,I格式。用于函数返回和间接跳转

避坑指南:我曾经在实现B格式指令时,把立即数拼接顺序搞反了。B格式的立即数位顺序是[12|10:5|4:1|11],不是简单的[12:1]。如果你用Verilog做拼接,一定要对照ISA手册的图2.3来写。我当时仿真分支跳转总是不对,最后用$display打印出立即数值才发现问题——低1位被错误地置0了。

2.2.5 加载与存储指令

  • LB/LH/LW/LBU/LHU:加载字节/半字/字(有符号/无符号),I格式
  • SB/SH/SW:存储字节/半字/字,S格式

加载指令的地址由rs1+imm[11:0]计算得到。注意:RISC-V不要求地址对齐——你可以从任意地址加载一个字。但不对齐的访问在硬件上可能需要两次内存访问,性能会下降。我在项目中通常还是按对齐来写代码,除非有特殊需求。

2.2.6 特殊指令

  • FENCE:内存屏障,用于多核同步
  • FENCE.I:指令屏障,用于自修改代码
  • ECALL:环境调用,触发异常,进入机器模式
  • EBREAK:调试断点
  • CSRRW/CSRRS/CSRRC等:CSR(控制状态寄存器)访问指令

CSR指令在特权架构中非常重要。咱们后面会专门讲,这里先记住它们属于I格式的变体。

2.3 寄存器约定与ABI

RISC-V有32个通用寄存器x0~x31。x0是硬连线的0,写入无效。其他寄存器在ABI(应用程序二进制接口)中有约定用途。

寄存器ABI名称用途是否被调用者保存
x0zero硬连线0—
x1ra返回地址否
x2sp栈指针是
x3gp全局指针—
x4tp线程指针—
x5~x7t0~t2临时寄存器否
x8s0/fp保存寄存器/帧指针是
x9s1保存寄存器是
x10~x17a0~a7函数参数/返回值否
x18~x27s2~s11保存寄存器是
x28~x31t3~t6临时寄存器否

ABI的核心规则:被调用者保存(Callee-saved)的寄存器,如果函数要用,必须先保存到栈上,返回前恢复。调用者保存(Caller-saved)的寄存器,调用函数前如果还有用,调用者自己保存。这个约定保证了函数调用的正确性。

我个人习惯在写汇编时,把频繁使用的变量放在s0~s11里,临时计算用t0~t6。参数传递用a0~a7。这样代码可读性好,也符合编译器预期。

2.4 特权架构简介

RISC-V的特权架构定义了三种模式:机器模式(M-mode)、监管模式(S-mode)、用户模式(U-mode)。咱们做FPGA软核,最常用的是M-mode(裸机程序)和M-mode+S-mode(运行Linux)。

  • M-mode:最高权限,可以访问所有CSR,处理所有异常和中断。复位后CPU默认在M-mode。
  • S-mode:操作系统内核运行的模式,可以管理页表、处理系统调用。
  • U-mode:用户程序运行的模式,权限最低,不能直接访问硬件。

异常和中断的处理流程:CPU检测到异常→跳转到M-mode的异常向量表基地址(mtvec)→保存异常原因(mcause)和返回地址(mepc)→执行异常处理程序→执行mret返回。

我的建议:刚开始做FPGA RISC-V验证时,先只跑M-mode的裸机程序。等M-mode下的指令集验证通过、中断处理稳定了,再引入S-mode和页表。一步一个脚印,别贪多。

2.5 本章知识体系图

下面这张SVG图把本章的核心知识点串起来了。你可以看到指令编码格式、指令分类、寄存器ABI、特权模式之间的关系。

RV32I指令集架构知识体系 RV32I指令集架构知识体系 RV32I 基础指令集 6种指令编码格式 6类指令功能 寄存器ABI与特权架构 R/I/S/U/B/J opcode固定 立即数布局 算术/逻辑 分支/跳转 加载/存储 32个寄存器 ABI约定 M/S/U模式 核心:RV32I用6种编码格式、47条指令、32个寄存器,覆盖了嵌入式系统90%以上的需求 特权架构的M/S/U模式为操作系统提供了硬件隔离基础

2.6 小结