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