4. 开发环境搭建

各位同学,咱们今天聊聊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,覆盖了绝大多数嵌入式场景。

RV32IMAC指令集架构概览 RV32IMAC 指令集架构概览 RV32I(基础整数) M扩展(乘除法) A扩展(原子操作) C扩展(压缩指令) 指令集分层关系 RV32I 是基础,M/A/C 是可选扩展 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,内存访问注意对齐