本章核心问题:ARM 工程师如何在 3 天内上手 RISC-V 指令集?哪些指令可以直接迁移,哪些必须换思路?
阅读建议:配合附录 A 的"ARM→RISC-V 指令映射表"使用。本章讲原理和差异,附录 A 是可查的速查表。
2.0 引子:用 ARM 汇编写的第一个 RISC-V
假设你要实现一个功能:把内存地址
0x2000_0000的 4 字节读出来,加 1,再写回。
ARM 版本(Cortex-M 风格):
LDR r0, =0x20000000 ; 地址 LDR r1, [r0] ; 读值 ADD r1, r1, #1 ; 加 1 STR r1, [r0] ; 写回RISC-V 版本:
li x10, 0x20000000 ; 地址(伪指令,展开为 lui+addi) lw x11, 0(x10) ; 读值 addi x11, x11, 1 ; 加 1 sw x11, 0(x10) ; 写回看起来很像,但有三处关键差异:
- 偏移写法:ARM 用
[r0],RISC-V 必须显式写0(x10) - 立即数加载:ARM 用
LDR r0, =立即数,RISC-V 用li伪指令 - 无 SUBI:加 1 用
addi,减 1 也是addi x11, x11, -1
如果你的第一反应是"这也没多难嘛"——恭喜,你已经有了正确的直觉。RISC-V 指令集确实简洁,难点不在指令本身,而在"没有的东西"(无 PUSH、无标志位、无条件执行)。
2.1 RV32I 基础指令
2.1.1 什么是 RV32I
RV32I 是 RISC-V 的基础整数指令集,32 位,所有 RISC-V 实现必须支持。地位相当于:
- ARM 的 Cortex-M 核心指令集
- x86 的基础指令集
| 特性 | 说明 |
|---|---|
| 名称 | RV32I(32 位)/ RV64I(64 位) |
| 状态 | v2.2 冻结,永久稳定 |
| 指令数 | 约 47 条(含 CSR),纯用户态约 37 条 |
| 哲学 | 极简、正交、可教学 |
2.1.2 六大类指令速览
| 类别 | 指令数 | 代表指令 | ARM 对应 |
|---|---|---|---|
| 算术 | 8 | add/sub/addi | ADD/SUB/ADDS |
| 逻辑 | 6 | and/or/xor/andi/ori/xori | AND/ORR/EOR |
| 移位 | 6 | sll/srl/sra/slli/srli/srai | LSL/LSR/ASR |
| 加载/存储 | 12 | lw/sw/lb/lbu/lh/lhu/sb/sh | LDR/STR系列 |
| 分支 | 6 | beq/bne/blt/bltu/bge/bgeu | BEQ/BNE等 |
| 跳转 | 4 | jal/jalr | B/BL/BX/BLX |
2.1.3 与 ARM 的核心差异:无标志位
这是 ARM 工程师最大的认知转变。RISC-V 没有 CPSR/NZCV 标志位。
| 功能 | ARM 做法 | RISC-V 做法 |
|---|---|---|
| 比较 | CMP r0, r1(改 NZCV) | sub x0, x10, x11(结果丢弃到 x0) |
| 条件分支 | BEQ依赖上次比较 | beq直接比较两个寄存器 |
| 进位 | ADC/SBC依赖 C 标志 | 无进位标志,需手动处理 |
| 溢出 | V标志 | 需mulh/比较判断 |
无标志位的影响:
- 指令更简洁(不需要区分
ADD/ADDS) - 分支必须直接比较(
beq x10, x11而非cmp; beq) - 多精度运算(64 位在 32 位机上)更复杂
💡工程视角:无标志位让指令译码更简单、乱序执行更容易。这是 RISC-V 面向高性能设计的哲学——把复杂度从硬件转移到编译器。
2.2 寄存器与立即数
2.2.1 32 个通用寄存器
| 寄存器 | ABI 名 | 用途 | ARM 对应 |
|---|---|---|---|
| x0 | zero | 恒为零(RISC-V 独有) | — |
| x1 | ra | 返回地址 | r14 (LR) |
| x2 | sp | 栈指针 | r13 (SP) |
| x3 | gp | 全局指针 | — |
| x4 | tp | 线程指针 | — |
| x5-x7 | t0-t2 | 临时寄存器 | r12 (IP) |
| x8 | s0/fp | 保存/帧指针 | r4-r11 |
| x9 | s1 | 保存 | r4-r11 |
| x10-x17 | a0-a7 | 参数/返回值 | r0-r3 |
| x18-x27 | s2-s11 | 保存 | r4-r11 |
| x28-x31 | t3-t6 | 临时 | — |
2.2.2 x0 恒零的妙用
x0 永远为 0,写入被忽略。这个设计看似简单,但非常巧妙:
# 生成 0 addi x10, x0, 0 # x10 = 0 # 比较(结果丢弃) sub x0, x10, x11 # 比较 x10 和 x11 # 跳转(无链接,相当于 B) jal x0, label # 返回地址写 x0(丢弃),等同无条件跳转2.2.3 立即数编码(12 位有符号)
RISC-V 的立即数操作是12 位有符号(-2048 到 2047)。这限制了单条指令的立即数范围:
| ARM | RISC-V | 说明 |
|---|---|---|
MOV r0, #1000 | addi x10, x0, 1000 | 12 位内直接 |
MOV r0, #0x12345 | lui x10, 0x2+addi x10, x10, 0x345 | 大立即数需两条 |
MOV r0, #0x12345678 | li x10, 0x12345678(伪指令) | 编译器展开 |
⚠️重要提醒:
li是伪指令,编译器会自动展开为lui+addi(必要时加额外指令处理符号位)。不要在性能关键代码里频繁使用大立即数。
2.3 分支与跳转
2.3.1 六种条件分支
| 指令 | 条件 | 无符号版 | ARM 对应 |
|---|---|---|---|
beq rs1, rs2, offset | 相等 | — | BEQ |
bne rs1, rs2, offset | 不等 | — | BNE |
blt rs1, rs2, offset | 小于(有符号) | bltu | BLT/BLO |
bge rs1, rs2, offset | 大于等于(有符号) | bgeu | BGE/BHS |
关键差异:RISC-V 分支直接比较两个寄存器,不依赖标志位。
# ARM: 比较+分支(依赖 NZCV) CMP r0, r1 BEQ label # RISC-V: 直接比较 beq x10, x11, label2.3.2 跳转指令
| 指令 | 用途 | ARM 对应 |
|---|---|---|
jal rd, offset | 跳转 + 保存返回地址到 rd | BL |
jalr rd, offset(rs1) | 间接跳转 + 保存返回地址 | BLX/BX |
jal x0, label | 无条件跳转(返回地址丢弃) | B |
jalr x0, 0(x1) | 函数返回(=ret伪指令) | BX lr |
函数调用的标准模式:
# 调用函数(返回地址存 ra = x1) jal ra, my_function # 函数返回 ret # = jalr x0, 0(x1)2.3.3 PC 相对寻址
RISC-V没有 PC 寄存器(不像 ARM 的 r15)。要访问 PC 相对数据,用auipc:
# 加载当前 PC 附近的字符串 auipc x10, 0 # x10 = PC + 0(PC 高位) addi x10, x10, offset # 加低位偏移2.4 加载存储寻址
2.4.1 唯一的寻址模式
RISC-V 的加载/存储只有一种寻址模式:寄存器 + 立即数偏移。
| ARM 寻址 | RISC-V 处理 | 指令数 |
|---|---|---|
LDR r0, [r1] | lw x10, 0(x11) | 1 |
LDR r0, [r1, #4] | lw x10, 4(x11) | 1 |
LDR r0, [r1, r2] | add+lw | 2 |
LDR r0, [r1], #4(后索引) | lw+addi | 2 |
LDR r0, [r1, #4]!(前索引) | addi+lw | 2 |
LDMIA(多寄存器) | 多个lw | N |
2.4.2 数据宽度与符号扩展
RISC-V 显式区分零扩展和符号扩展:
| 指令 | 宽度 | 扩展 |
|---|---|---|
lb | 8 位 | 符号扩展 |
lbu | 8 位 | 零扩展 |
lh | 16 位 | 符号扩展 |
lhu | 16 位 | 零扩展 |
lw | 32 位 | — |
对比 ARM:ARM 用LDRB(零扩展)/LDRSB(符号扩展)区分,概念类似,但 RISC-V 命名更直观。
2.4.3 压栈:没有 PUSH/POP 怎么办
ARM 有PUSH/POP,RISC-V没有。需要手动管理栈指针:
# ARM: 保存 4 个寄存器 PUSH {r4-r7, lr} # RISC-V: 手动压栈 addi sp, sp, -20 # 分配 20 字节(5 个寄存器 × 4 字节) sw x1, 16(sp) # 保存 ra sw x8, 12(sp) # 保存 s0 sw x9, 8(sp) # 保存 s1 sw x10, 4(sp) # 保存 a0 sw x11, 0(sp) # 保存 a1# ARM: 恢复 POP {r4-r7, pc} # RISC-V: 手动弹栈 lw x1, 16(sp) # 恢复 ra lw x8, 12(sp) # 恢复 s0 lw x9, 8(sp) # 恢复 s1 lw x10, 4(sp) # 恢复 a0 lw x11, 0(sp) # 恢复 a1 addi sp, sp, 20 # 释放栈帧 ret💡工程视角:编译器(GCC/LLVM)会自动处理压栈弹栈,你几乎不需要手写。但理解这个过程对调试、阅读反汇编、手写汇编至关重要。
2.5 扩展指令集
2.5.1 标准扩展一览
RISC-V 通过"基础 + 扩展"模式灵活组合。核心扩展:
| 扩展 | 名称 | 功能 | 冻结状态 |
|---|---|---|---|
| M | 乘法除法 | mul/div/rem | ✅ |
| A | 原子操作 | lr/sc/amoswap | ✅ |
| F | 单精度浮点 | 浮点运算 | ✅ |
| D | 双精度浮点 | 双精度浮点 | ✅ |
| C | 压缩指令 | 16 位指令(代码密度) | ✅ |
| Zb | 位操作 | andn/clz/rori | ✅ 1.0 |
| V | 向量 | 向量运算(AI/DSP) | ✅ 1.0 |
| K | 标量密码 | 密码学加速 | ✅ 1.0 |
| H | 虚拟化 | Hypervisor | ✅ 稳定 |
2.5.2 M 扩展:乘除指令
| 指令 | 功能 | ARM 对应 |
|---|---|---|
mul | 低 32 位乘积 | MUL |
mulh | 有符号高 32 位 | SMULL(部分) |
mulhu | 无符号高 32 位 | UMULL |
mulhsu | 混合符号高 32 位 | — |
div | 有符号除法 | SDIV |
divu | 无符号除法 | UDIV |
rem | 有符号余数 | — |
remu | 无符号余数 | — |
2.5.3 C 扩展:压缩指令(重要)
C 扩展是代码密度的关键,相当于 ARM 的 Thumb。典型用法:
# 标准指令(4 字节) addi sp, sp, -16 # 压缩指令(2 字节) c.addi sp, -16C 扩展的关键指令:
| 压缩指令 | 展开 | 说明 |
|---|---|---|
c.addi | addi | 立即数加法 |
c.li | addi rd, x0, imm | 加载立即数 |
c.lw/c.sw | lw/sw | 栈内加载/存储 |
c.j/c.jr | jal/jalr | 跳转 |
c.beqz/c.bnez | 比较 x0 | 零分支 |
💡工程视角:启用 C 扩展通常可减少20-30% 代码体积,对 Flash 有限的 IoT 芯片至关重要。平头哥玄铁、Telink 等嵌入式核都支持 C 扩展。
2.5.4 Zb 位操作扩展(新)
| 指令 | 功能 | 性能提升 |
|---|---|---|
andn/orn/xnor | 取反操作 | ~2x |
clz/ctz/cpop | 位计数 | ~5x |
rol/ror | 循环移位 | ~2x |
min/max | 极值 | ~2x |
sext.b/sext.h | 符号扩展 | 省 1 条 |
⚠️重要提醒:Zb 扩展虽好,但不是所有核都支持。使用前必须确认目标核的扩展集,否则代码无法编译/运行。
2.6 汇编实战:双平台对照
2.6.1 冒泡排序(经典教学示例)
ARM 版本:
@ 冒泡排序 r0 = 数组基址, r1 = 长度 bubble_sort: PUSH {r4-r7, lr} MOV r4, r0 @ 数组基址 MOV r5, r1 @ 长度 SUB r6, r5, #1 @ 外层循环次数 = n-1 outer: MOV r7, r4 @ 内层起点 MOV r2, #0 @ 交换标志 inner: LDR r3, [r7] @ arr[i] LDR r12, [r7, #4] @ arr[i+1] CMP r3, r12 @ 比较 BLE no_swap @ 若有序跳过 STR r12, [r7] @ 交换 STR r3, [r7, #4] MOV r2, #1 @ 有交换 no_swap: ADD r7, r7, #4 @ 下一元素 CMP r7, r6, LSL #2 @ 是否到末尾 BNE inner CMP r2, #0 @ 无交换则提前结束 BEQ done SUBS r6, r6, #1 BNE outer done: POP {r4-r7, pc}RISC-V 版本:
# 冒泡排序 x10 = 数组基址, x11 = 长度 bubble_sort: addi sp, sp, -24 sw x1, 20(sp) # 保存 ra sw x8, 16(sp) # 保存 s0 sw x9, 12(sp) # 保存 s1 sw x10, 8(sp) # 保存 a0 sw x11, 4(sp) # 保存 a1 addi x8, x10, 0 # s0 = 数组基址 addi x9, x11, 0 # s1 = 长度 addi x28, x9, -1 # t3 = n-1(外层次数) outer: addi x29, x8, 0 # t4 = 内层起点 li x30, 0 # t5 = 交换标志 inner: lw x5, 0(x29) # arr[i] lw x6, 4(x29) # arr[i+1] blt x6, x5, swap # arr[i+1] < arr[i] 则交换 j no_swap swap: sw x6, 0(x29) # 交换 sw x5, 4(x29) li x30, 1 # 有交换 no_swap: addi x29, x29, 4 # 下一元素 bltu x29, x28, inner # 未到末尾继续(用 bltu 与基址+偏移比较) beqz x30, done # 无交换提前结束 addi x28, x28, -4 # 外层递减 bgez x28, outer done: lw x1, 20(sp) # 恢复 ra lw x8, 16(sp) # 恢复 s0 lw x9, 12(sp) # 恢复 s1 lw x10, 8(sp) # 恢复 a0 lw x11, 4(sp) # 恢复 a1 addi sp, sp, 24 ret双平台差异点总结:
| 差异 | ARM | RISC-V |
|---|---|---|
| 压栈 | PUSH {r4-r7, lr}一条 | 手动 5 条 |
| 比较+分支 | CMP+BLE两条 | blt一条 |
| 寄存器名 | r0-r15 | x0-x31 + ABI 名 |
| 循环控制 | SUBS带标志位 | addi+bgez(无标志位) |
| 返回 | POP {..., pc} | ret |
2.6.2 双平台 FIFO 环形缓冲区(C 代码 + 内联汇编)
核心逻辑(C):
// 环形缓冲区读(返回 0=空,1=成功)intfifo_pop(volatileuint32_t*buf,uint32_t*head,uint32_t*tail,uint32_tsize,uint32_t*out){if(*head==*tail)return0;// 空*out=buf[*tail];*tail=(*tail+1)%size;return1;}ARM 内联汇编(关键路径):
staticinlineuint32_tread_fifo_arm(uint32_t*buf,uint32_ttail){uint32_tval;__asmvolatile("ldr %0, [%1, %2]":"=r"(val):"r"(buf),"r"(tail));returnval;}RISC-V 内联汇编:
staticinlineuint32_tread_fifo_riscv(uint32_t*buf,uint32_ttail){uint32_tval;__asmvolatile("lw %0, 0(%1)":"=r"(val):"r"(buf+tail));returnval;}💡工程视角:现代编译器对 RISC-V 支持已经非常成熟(GCC/LLVM 都是 upstream)。99% 的代码用 C 写,内联汇编只在最热路径使用。手写汇编的必要性远低于十年前。
2.7 本章小结
2.7.1 六条要点
- RV32I 只有 37 条用户态指令——比 ARM 精简得多,3 天可上手
- 无标志位是最核心差异——比较直接进分支,不依赖 NZCV
- 无 PUSH/POP——手动管理栈,编译器自动处理
- 寻址模式唯一——只有"寄存器 + 偏移",多寄存器/前后索引需组合
- 立即数 12 位有符号——大立即数用
lui+addi或li伪指令 - 扩展通过 Profile 组合——C 扩展省 20-30% 代码,Zb 提升位操作性能
2.7.2 常见坑清单
| 坑 | 现象 | 解决 |
|---|---|---|
subi不存在 | 编译报错 | 用addi负立即数 |
| 忘记 x0 恒零 | 写 x0 想清零 | addi x10, x0, 0 |
| 偏移必须写 0 | lw x10, (x11)报错 | lw x10, 0(x11) |
| 大立即数溢出 | 编译错误 | 用li伪指令 |
| 忘记保存 ra | 函数返回崩溃 | 调用前jal会覆盖 ra |
| Zb 指令不支持 | 编译/运行错误 | 确认目标核扩展集 |
2.7.3 下章预告
第 3 章进入寄存器与调用约定:为什么 ARM 程序搬到 RISC-V 直接崩溃?AAPCS 和 RISC-V ABI 到底差在哪?x0 恒零、无 PC 寄存器、8 个参数寄存器——这些设计如何影响你的 C 代码和汇编?
参考引用
📌规范引用
- RISC-V Unprivileged Spec v2.2(20191213)— RV32I 基础指令
- RISC-V 标准扩展规范(M/A/F/D/C)
- RISC-V Zb 位操作扩展规范 1.0
- RISC-V V 向量扩展规范 1.0
📌参考资料
- 《RISC-V 架构与嵌入式开发快速入门》(胡振波)
- RISC-V Reader(中文版)
- 平头哥玄铁系列汇编手册
⚠️重要提醒:不同核的扩展支持差异大(如 Zb 非全支持),使用扩展指令前必须查阅目标核的 ISA 手册。