2、RISC-V架构基础:RISC-V指令集架构概述、模块化设计理念、基础整数指令集(RV32I)介绍

好,我们正式开始聊RISC-V的架构基础。说实话,我第一次接触RISC-V时,第一反应是——这玩意儿怎么这么「干净」?跟ARM和x86比起来,它简直像一张白纸。但正是这种简洁,让我在后续的FPGA项目中省了不少力气。

2.1 RISC-V指令集架构概述

RISC-V是一个基于精简指令集计算(RISC)原则设计的开源指令集架构(ISA)。它诞生于2010年,由加州大学伯克利分校的团队开发。你可能会问:市面上已经有ARM、x86、MIPS了,为什么还要搞一个新的?

我个人理解是:因为现有的ISA要么太复杂(x86),要么授权太贵(ARM),要么已经过时(MIPS)。RISC-V的出现,说白了就是给芯片设计行业提供了一个「自由的选择」。

它的几个核心特点:

  • 完全开源:不需要支付授权费,任何人都可以自由使用
  • 简洁优雅:基础指令集只有几十条指令,学起来很快
  • 可扩展性强:支持自定义指令扩展,适合特定领域加速
  • 模块化设计:你可以只取你需要的部分,不需要全盘接受

我在项目中遇到过这样一个场景:客户需要一个低功耗的IoT控制器,要求面积小、功耗低。如果用ARM Cortex-M系列,授权费加上IP成本,小公司根本扛不住。最后我们选了RISC-V,自己裁剪了不需要的指令扩展,把核心面积压缩到了极致。嗯,效果相当不错。

2.2 模块化设计理念

RISC-V最让我欣赏的一点,就是它的模块化设计。它不像ARM那样给你一个「大礼包」,而是让你像搭积木一样,按需选择。

整个RISC-V指令集分为两部分:

  • 基础整数指令集(I):这是必须的,是所有RISC-V处理器的核心
  • 标准扩展(M/A/F/D/C等):可选的,按需添加

打个比方:基础整数指令集就像一辆车的底盘和发动机,没有它车动不了。而扩展指令集就像天窗、导航、座椅加热——有当然好,没有也能开。

常见的扩展包括:

扩展名 全称 功能
M 整数乘除法扩展 提供乘法、除法、取模指令
A 原子操作扩展 支持多核同步的原子指令
F 单精度浮点扩展 32位浮点运算
D 双精度浮点扩展 64位浮点运算
C 压缩指令扩展 16位短指令,减小代码体积

你想想看,如果你的嵌入式系统只需要做简单的控制逻辑,根本用不上浮点运算。那你就只取RV32I就够了,省下来的芯片面积可以放更多功能模块。这就是模块化的魅力。

核心思想:RISC-V的模块化设计让你「按需付费」——不是按钱,而是按芯片面积和功耗。这在FPGA设计中尤其重要,因为FPGA的逻辑资源是有限的。

2.3 基础整数指令集(RV32I)介绍

RV32I是RISC-V的基石。无论你后面加什么扩展,RV32I都是必须实现的。它包含47条指令(嗯,你没看错,就47条)。相比之下,ARMv7-A有几百条指令。这就是为什么我说它「干净」。

RV32I的指令可以分为以下几类:

2.3.1 算术运算指令

包括加法、减法、移位、逻辑运算等。比如:

ADD   rd, rs1, rs2    // rd = rs1 + rs2
SUB   rd, rs1, rs2    // rd = rs1 - rs2
AND   rd, rs1, rs2    // rd = rs1 & rs2
OR    rd, rs1, rs2    // rd = rs1 | rs2
XOR   rd, rs1, rs2    // rd = rs1 ^ rs2
SLL   rd, rs1, rs2    // rd = rs1 << rs2 (逻辑左移)
SRL   rd, rs1, rs2    // rd = rs1 >> rs2 (逻辑右移)
SRA   rd, rs1, rs2    // rd = rs1 >> rs2 (算术右移)

这里有个细节:RISC-V没有专门的减法指令?其实有,SUB就是。但为什么没有ADDI(立即数加法)对应的SUBI?因为立即数减法可以用ADDI加负数来实现。你看,这就是RISC-V的设计哲学——能省则省。

避坑指南:我曾经在写汇编时,想当然地写了个SUBI指令,结果汇编器报错。后来才意识到,RISC-V确实没有SUBI。正确的做法是用ADDI加上一个负的立即数。嗯,这个坑我替你们踩过了。

2.3.2 逻辑运算指令

除了上面的AND/OR/XOR,还有它们的立即数版本:

ADDI  rd, rs1, imm    // rd = rs1 + 符号扩展的立即数
ANDI  rd, rs1, imm    // rd = rs1 & 立即数
ORI   rd, rs1, imm    // rd = rs1 | 立即数
XORI  rd, rs1, imm    // rd = rs1 ^ 立即数

注意:立即数只有12位,符号扩展到32位。这意味着你只能加/减一个范围在[-2048, 2047]之间的数。如果要加载更大的常数,需要用LUI(加载高位立即数)配合ADDI来实现。

2.3.3 内存访问指令

RV32I只支持加载和存储两种内存操作:

LW    rd, offset(rs1)  // 加载字 (32位)
LH    rd, offset(rs1)  // 加载半字 (16位)
LB    rd, offset(rs1)  // 加载字节 (8位)
SW    rs2, offset(rs1) // 存储字
SH    rs2, offset(rs1) // 存储半字
SB    rs2, offset(rs1) // 存储字节

这里有个重要的设计选择:RISC-V的内存访问必须是对齐的。也就是说,加载一个字(4字节)时,地址必须是4的倍数。如果不对齐,会触发异常。

注意:我在调试一个FPGA上的RISC-V软核时,遇到过因为未对齐访问导致程序跑飞的情况。当时查了半天,最后发现是编译器优化后生成了未对齐的加载指令。解决方案是在编译器选项中强制对齐访问,或者在硬件上实现未对齐访问的异常处理。

2.3.4 分支与跳转指令

控制流指令是任何CPU的核心。RV32I提供了条件分支和无条件跳转:

BEQ   rs1, rs2, label  // if(rs1 == rs2) 跳转
BNE   rs1, rs2, label  // if(rs1 != rs2) 跳转
BLT   rs1, rs2, label  // if(rs1 < rs2) 跳转 (有符号)
BGE   rs1, rs2, label  // if(rs1 >= rs2) 跳转 (有符号)
BLTU  rs1, rs2, label  // if(rs1 < rs2) 跳转 (无符号)
BGEU  rs1, rs2, label  // if(rs1 >= rs2) 跳转 (无符号)
JAL   rd, label        // 跳转并链接 (用于函数调用)
JALR  rd, rs1, offset  // 间接跳转并链接

你可能会注意到:没有BGT(大于跳转)和BLE(小于等于跳转)?是的,RISC-V用BLT和BGE的组合来实现这些。比如BGT可以通过交换操作数然后用BLT来实现。这又是RISC-V精简设计的一个体现。

2.3.5 立即数加载指令

前面提到,要加载一个32位常数,需要两条指令配合:

LUI   rd, imm20       // 加载高20位到rd,低12位填0
ADDI  rd, rd, imm12   // 加上低12位

比如加载0x12345678:

LUI   x5, 0x12345     // x5 = 0x12345000
ADDI  x5, x5, 0x678   // x5 = 0x12345678

说白了,这就是RISC-V的「拼图」式常数加载。虽然多了一条指令,但简化了硬件设计——指令编码中不需要留出32位来放立即数。

2.4 寄存器与调用约定

RV32I有32个通用寄存器,每个32位宽。其中x0是硬连线的0,写入x0的数据会被丢弃。这个设计非常巧妙——很多地方需要0值,有了x0就不用专门生成0了。

寄存器的使用有约定(ABI名称):

寄存器 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 临时寄存器

这个调用约定不是硬件强制要求的,但软件必须遵守才能互相兼容。我在写RISC-V的启动代码时,就严格按照这个约定来分配寄存器,否则链接器会报一些莫名其妙的错误。

2.5 小结

RV32I虽然只有47条指令,但它是图灵完备的——理论上可以完成任何计算任务。它的设计哲学是:把简单留给硬件,把复杂交给软件。这种思路在FPGA上实现时特别舒服,因为硬件逻辑越简单,占用的LUT和FF就越少,时序也更容易收敛。

我个人觉得,RISC-V的模块化设计是它最大的亮点。你可以在一个FPGA项目里只实现RV32I,然后在另一个项目里加上M扩展和C扩展,代码和工具链基本不用改。这种灵活性,是传统ISA很难做到的。

好了,RV32I的基础就聊到这里。下一节我们会深入指令编码格式,看看这些指令在二进制层面是怎么表示的。到时候我会分享一些我在写RISC-V汇编器时踩过的坑,保证让你少走弯路。


公众号:蓝海资料掘金营,微信deep3321