第2章 指令集速览
2026/8/6 9:36:29 网站建设 项目流程

本章核心问题: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) ; 写回

看起来很像,但有三处关键差异:

  1. 偏移写法:ARM 用[r0],RISC-V 必须显式写0(x10)
  2. 立即数加载:ARM 用LDR r0, =立即数,RISC-V 用li伪指令
  3. 无 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 对应
算术8add/sub/addiADD/SUB/ADDS
逻辑6and/or/xor/andi/ori/xoriAND/ORR/EOR
移位6sll/srl/sra/slli/srli/sraiLSL/LSR/ASR
加载/存储12lw/sw/lb/lbu/lh/lhu/sb/shLDR/STR系列
分支6beq/bne/blt/bltu/bge/bgeuBEQ/BNE
跳转4jal/jalrB/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 对应
x0zero恒为零(RISC-V 独有)
x1ra返回地址r14 (LR)
x2sp栈指针r13 (SP)
x3gp全局指针
x4tp线程指针
x5-x7t0-t2临时寄存器r12 (IP)
x8s0/fp保存/帧指针r4-r11
x9s1保存r4-r11
x10-x17a0-a7参数/返回值r0-r3
x18-x27s2-s11保存r4-r11
x28-x31t3-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)。这限制了单条指令的立即数范围:

ARMRISC-V说明
MOV r0, #1000addi x10, x0, 100012 位内直接
MOV r0, #0x12345lui x10, 0x2+addi x10, x10, 0x345大立即数需两条
MOV r0, #0x12345678li 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小于(有符号)bltuBLT/BLO
bge rs1, rs2, offset大于等于(有符号)bgeuBGE/BHS

关键差异:RISC-V 分支直接比较两个寄存器,不依赖标志位。

# ARM: 比较+分支(依赖 NZCV) CMP r0, r1 BEQ label # RISC-V: 直接比较 beq x10, x11, label

2.3.2 跳转指令

指令用途ARM 对应
jal rd, offset跳转 + 保存返回地址到 rdBL
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+lw2
LDR r0, [r1], #4(后索引)lw+addi2
LDR r0, [r1, #4]!(前索引)addi+lw2
LDMIA(多寄存器)多个lwN

2.4.2 数据宽度与符号扩展

RISC-V 显式区分零扩展符号扩展

指令宽度扩展
lb8 位符号扩展
lbu8 位零扩展
lh16 位符号扩展
lhu16 位零扩展
lw32 位

对比 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, -16

C 扩展的关键指令

压缩指令展开说明
c.addiaddi立即数加法
c.liaddi rd, x0, imm加载立即数
c.lw/c.swlw/sw栈内加载/存储
c.j/c.jrjal/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

双平台差异点总结

差异ARMRISC-V
压栈PUSH {r4-r7, lr}一条手动 5 条
比较+分支CMP+BLE两条blt一条
寄存器名r0-r15x0-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 六条要点

  1. RV32I 只有 37 条用户态指令——比 ARM 精简得多,3 天可上手
  2. 无标志位是最核心差异——比较直接进分支,不依赖 NZCV
  3. 无 PUSH/POP——手动管理栈,编译器自动处理
  4. 寻址模式唯一——只有"寄存器 + 偏移",多寄存器/前后索引需组合
  5. 立即数 12 位有符号——大立即数用lui+addili伪指令
  6. 扩展通过 Profile 组合——C 扩展省 20-30% 代码,Zb 提升位操作性能

2.7.2 常见坑清单

现象解决
subi不存在编译报错addi负立即数
忘记 x0 恒零写 x0 想清零addi x10, x0, 0
偏移必须写 0lw 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 手册。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询