4. RISC-V基础指令集:RV32IMAC指令集概览
各位同学,咱们今天聊聊RISC-V的指令集。说实话,我第一次接触RISC-V时,第一反应是——这玩意儿怎么这么简洁?后来做项目多了才明白,简洁才是王道。
GD32V系列用的是RV32IMAC指令集。这个命名看着复杂,其实拆开就懂了:
- RV32:32位RISC-V架构
- I:基础整数指令集(必选)
- M:整数乘除法扩展
- A:原子操作扩展
- C:压缩指令扩展
说白了,I是骨架,M、A、C是肌肉。没有I,其他都白搭。
核心要点:RV32I是所有RISC-V处理器的必选项。GD32V在I基础上加了M、A、C,覆盖了绝大多数嵌入式场景。
4.1 算术运算指令
算术指令,说白了就是加减乘除。RISC-V的算术指令设计得很干净——所有操作都在寄存器之间完成,不直接操作内存。
我记得刚用GD32V做电机控制项目时,需要频繁做乘加运算。一开始我习惯性地用MUL然后ADD,后来发现可以用MADD一条指令搞定,周期直接减半。嗯,这就是经验。
4.1.1 整数加减法
| 指令 | 格式 | 描述 |
|---|---|---|
| ADD | ADD rd, rs1, rs2 | rd = rs1 + rs2 |
| SUB | SUB rd, rs1, rs2 | rd = rs1 - rs2 |
| ADDI | ADDI rd, rs1, imm | rd = rs1 + 立即数 |
小技巧:ADDI的立即数是12位有符号数,范围-2048到2047。如果需要加更大的数,得先用LUI加载高20位,再用ADDI加低12位。我刚开始总忘记这个限制,调试时吃了不少亏。
4.1.2 乘除法(M扩展)
M扩展提供了整数乘除法指令。注意,RISC-V的乘法结果可以输出到两个寄存器——这一点和ARM不太一样。
| 指令 | 格式 | 描述 |
|---|---|---|
| MUL | MUL rd, rs1, rs2 | rd = (rs1 × rs2) 低32位 |
| MULH | MULH rd, rs1, rs2 | rd = (rs1 × rs2) 高32位(有符号) |
| MULHU | MULHU rd, rs1, rs2 | rd = (rs1 × rs2) 高32位(无符号) |
| DIV | DIV rd, rs1, rs2 | rd = rs1 ÷ rs2(有符号) |
| DIVU | DIVU rd, rs1, rs2 | rd = rs1 ÷ rs2(无符号) |
| REM | REM rd, rs1, rs2 | rd = rs1 % rs2(有符号余数) |
// 示例:计算 32位有符号乘法,获取完整64位结果
// 假设 a 在 x10,b 在 x11
MUL x12, x10, x11 // 低32位 → x12
MULH x13, x10, x11 // 高32位 → x13
// 结果:{x13, x12} 组成64位乘积
注意:除零操作不会触发异常,DIV指令在除零时返回-1,DIVU返回0xFFFFFFFF。余数指令REM在除零时返回被除数本身。这个行为我第一次遇到时还以为是bug,查了手册才发现是设计如此。
4.2 逻辑运算指令
逻辑指令在嵌入式里用得特别多——尤其是操作寄存器位的时候。你想想看,配置外设、屏蔽中断、设置GPIO,哪样离得开逻辑运算?
4.2.1 基本逻辑指令
| 指令 | 格式 | 描述 |
|---|---|---|
| AND | AND rd, rs1, rs2 | 按位与 |
| OR | OR rd, rs1, rs2 | 按位或 |
| XOR | XOR rd, rs1, rs2 | 按位异或 |
| ANDI | ANDI rd, rs1, imm | 与立即数 |
| ORI | ORI rd, rs1, imm | 或立即数 |
| XORI | XORI rd, rs1, imm | 异或立即数 |
4.2.2 移位指令
移位操作在嵌入式里也是家常便饭。比如你要从32位寄存器里提取某个字段,先右移再与掩码,两步搞定。
| 指令 | 格式 | 描述 |
|---|---|---|
| SLL | SLL rd, rs1, rs2 | 逻辑左移 |
| SRL | SRL rd, rs1, rs2 | 逻辑右移 |
| SRA | SRA rd, rs1, rs2 | 算术右移(符号位扩展) |
| SLLI | SLLI rd, rs1, shamt | 立即数左移 |
| SRLI | SRLI rd, rs1, shamt | 立即数逻辑右移 |
| SRAI | SRAI rd, rs1, shamt | 立即数算术右移 |
// 示例:从状态寄存器中提取位[15:8]
// 假设状态寄存器值在 x10
SRLI x11, x10, 8 // 右移8位,将目标字段移到低8位
ANDI x12, x11, 0xFF // 用掩码提取低8位
// x12 中就是位[15:8]的值
个人习惯:我写逻辑运算时,喜欢先用ANDI清零某些位,再用ORI置位。这样可读性比一次性写个复杂立即数好得多。另外,XOR指令用来翻转特定位非常方便——比如GPIO输出取反,一条XORI就搞定。
4.3 数据传送指令
数据传送指令,说白了就是搬数据。RISC-V的数据传送分两类:寄存器之间的搬移,以及寄存器和内存之间的搬移。
4.3.1 寄存器间传送
RISC-V没有专门的MOV指令。那怎么搬?用ADDI加0就行了。
// 寄存器间数据传送
ADDI x5, x6, 0 // x5 = x6 + 0,相当于 MOV x5, x6
为什么这么设计?说白了是为了简化硬件。少一条指令,解码器就少一个分支。RISC-V的设计哲学就是——能用已有指令组合实现的功能,就不加新指令。
4.3.2 加载与存储
这是嵌入式开发中最常用的指令类别。GD32V是32位处理器,但外设寄存器很多是8位或16位的,所以加载/存储指令要支持不同宽度。
| 指令 | 格式 | 描述 |
|---|---|---|
| LB | LB rd, offset(rs1) | 加载字节(有符号扩展) |
| LBU | LBU rd, offset(rs1) | 加载字节(无符号扩展) |
| LH | LH rd, offset(rs1) | 加载半字(有符号扩展) |
| LHU | LHU rd, offset(rs1) | 加载半字(无符号扩展) |
| LW | LW rd, offset(rs1) | 加载字 |
| SB | SB rs2, offset(rs1) | 存储字节 |
| SH | SH rs2, offset(rs1) | 存储半字 |
| SW | SW rs2, offset(rs1) | 存储字 |
// 示例:从内存地址0x20001000加载一个字
LUI x10, 0x20001 // 加载高20位
LW x11, 0x000(x10) // 加载字到 x11
// 示例:向GPIO输出寄存器写值
// 假设GPIO基址在 x12
LI x13, 0xAA // 要输出的值(伪指令,汇编器会展开)
SW x13, 0x00(x12) // 写入GPIO输出寄存器
对齐问题:RISC-V要求加载/存储地址必须对齐。LW要求地址4字节对齐,LH要求2字节对齐。如果地址不对齐,会触发异常。我曾经在调试一个串口驱动时,因为结构体成员没对齐,导致LH指令一直报错,查了半天才发现是__packed没加。
4.3.3 立即数加载
RISC-V的立即数加载有点特别。因为指令长度固定32位,没法一条指令加载完整的32位立即数。所以需要两条指令配合:
// 加载32位立即数 0x12345678
LUI x10, 0x12345 // 加载高20位:x10 = 0x12345000
ADDI x10, x10, 0x678 // 加低12位:x10 = 0x12345678
不过别担心,汇编器提供了LI伪指令,会自动帮你拆成两条。我平时写代码直接用LI,省心。
4.4 压缩指令扩展(C扩展)
C扩展是RISC-V的一大亮点。它把常用的指令压缩成16位,代码密度能降低25%-30%。对于Flash紧张的嵌入式系统来说,这简直是福音。
我记得有个项目,Flash只剩2KB空间,功能还得加。换成C扩展编译后,代码刚好塞下。那一刻,我对C扩展的好感度直接拉满。
| 16位指令 | 等效32位指令 | 节省空间 |
|---|---|---|
| C.ADDI | ADDI | 50% |
| C.LW | LW | 50% |
| C.SW | SW | 50% |
| C.J | JAL | 50% |
| C.BEQZ | BEQ + 零寄存器 | 50% |
使用建议:编译时加上 -march=rv32imac 选项,编译器会自动生成压缩指令。你不需要手动写16位指令——除非你在做汇编优化。我个人建议,除非对代码大小有极致要求,否则交给编译器就好。
4.5 本章小结
RV32IMAC指令集,说复杂也复杂,说简单也简单。核心就三点:
- 算术指令:加减乘除,都在寄存器里完成
- 逻辑指令:与或非移位,操作寄存器位的利器
- 数据传送:寄存器间用ADDI伪装MOV,内存访问注意对齐