最近学习 RISC-V 指令,参考书籍、博客:
RISC-V Reader Chinese v2p1
学习过程中使用 CH32V103 RV32 架构的单片机进行一些指令的使用复现,加强记忆!
以 32 位 RV 架构 CPU 为例子,从 CPU 级别看来,各种指令就是 32 位的一串数字,这 32 位的数字按照存储数据的位结构,具体可以分为 6 类:

关于 6 种指令的说明,参考博客内的总结:
R-typed
R-typed 指令是最常用的运算指令,具有三个寄存器地址,每个都用 5bit 的数表示。指令的操作由 7 位的 opcode、7 位的 funct7 以及 3 位的 funct3 共同决定的。R-typed 是不包含立即数的所有整数计算指令,一般表示寄存器-寄存器操作的指令。
I-typed
I-typed 具有两个寄存器地址和一个立即数,其中一个是源寄存器 rs1,一个是目的寄存器 rd,指令的高 12 位是立即数。指令的操作仅由 7 位的 opcode 和 3 位的funct3两者决定。值得注意的是,在执行运算时需要先把 12 位立即数扩展到 32 位之后再进行运算。I-typed 指令相当于将 R-typed 指令格式中的一个操作数改为立即数。一般表示短立即数和访存 load 操作的指令。
S-typed
S-typed 的指令功能由 7 位 opcode 和 3 位 funct3 决定,指令中包含两个源寄存器和指令的imm[31:25]和 imm[11:7]构成的一个12位的立即数,在执行指令运算时需要把12 位立即数扩展到 32 位,然后再进行运算,S-typed 一般表示访存 store 操作指令,如存储字(sw)、半字(sh)、字节(sb)等指令。
B-typed
B-typed 的指令操作由 7 位 opcode 和 3 位 funct3 决定,指令中具有两个源寄存器和一个 12 位的立即数,该立即数构成是指令的第32位是 imm[12]、第7位是imm[11]、25 到 30 是 imm[10:5]、8 到 11 位是 imm[4:1],同样的,在执行运算时需要把12 位立即数扩展到 32 位,然后再进行运算。B-typed 一般表示条件跳转操作指令,如相等(beq)、不相等(bne)、大于等于(bge)以及小于(blt)等跳转指令。
U-typed
U-typed 的指令操作仅由 7 位 opcode 决定,指令中包括一个目的寄存器 rd 和高20 位表示的 20 位立即数。U-typed 一般表示长立即数操作指令,例如 lui 指令,将立即数左移 12 位,并将低 12 位置零,结果写回目的寄存器中。
J-typed
J-typed 的指令操作由 7 位 opcode 决定,与 U-typed 一样只有一个目的寄存器 rd和一个 20 位的立即数,但是 20 位的立即数组成不同,即指令的 31 位是 imm[20]、 12 到 19 位是 imm[19:12]、20 位是 imm[11]、21 到 30 位是 imm[10:1],J-typed 一般表示无条件跳转指令,如 jal 指令。
了解了 RV32 的指令结构分类后,我们来看一下 RV32 架构的寄存器分配情况,RV32 有 32 个寄存器,这 32 个寄存器的定义如下:其中 ABI 是寄存器的二进制接口的名称,可以在汇编中使用。
| 寄存器编号 | 寄存器 ABI 名称 | 寄存器功能 |
|---|---|---|
| x0 | zero | 全0寄存器 |
| x1 | ra | 跳转返回指针 |
| x2 | sp | 栈指针 |
| x3 | gp | 全局指针 |
| x4 | tp | 线程指针 |
| x5 | t0 | 临时存储器 |
| x6 | t1 | 临时存储器 |
| x7 | t2 | 临时存储器 |
| x8 | s0/fp | 存储寄存器,框架指针 |
| x9 | s1 | 存储寄存器 |
| x10 | a0 | 函数参数寄存器(可用于返回值) |
| x11 | a1 | 函数参数寄存器(可用于返回值) |
| x12 | a2 | 函数参数寄存器 |
| x13 | a3 | 函数参数寄存器 |
| x14 | a4 | 函数参数寄存器 |
| x15 | a5 | 函数参数寄存器 |
| x16 | a6 | 函数参数寄存器 |
| x17 | a7 | 函数参数寄存器 |
| x18 | s2 | 存储寄存器 |
| x19 | s3 | 存储寄存器 |
| x20 | s4 | 存储寄存器 |
| x21 | s5 | 存储寄存器 |
| x22 | s6 | 存储寄存器 |
| x23 | s7 | 存储寄存器 |
| x24 | s8 | 存储寄存器 |
| x25 | s9 | 存储寄存器 |
| x26 | s10 | 存储寄存器 |
| x27 | s11 | 存储寄存器 |
| x28 | t3 | 临时存储器 |
| x29 | t4 | 临时存储器 |
| x30 | t5 | 临时存储器 |
| x31 | t6 | 临时存储器 |
除了上面的寄存器外,还有个 pc 指针指向程序运行地址!了解完指令结构和架构寄存器的分工后,我们了解一下指令的功能分类~
加载存储指令用于对寄存器进行数据的加载和保存操作,主要有以下几个
编程示例:
my_test:
li t0, 0x20000000
li t4, 0x12345678
sw t4, 0x0(t0)
lb t1, 0x0(t0)
lh t2, 0x0(t0)
lw t3, 0(t0)
sb t4, 4(t0)
sh t4, 8(t0)
sw t4, 12(t0)
j .
读取立即数 0x20000000 地址 (SRAM 地址 )到 t0,把 0x12345678 赋值给 t4 ,把 t4 的值保存到以 t0 值位地址的位置上,依次调用 lb、lh、lw 来读取,然后在调用 sb sh sw 将数据存放到 SRAM 上。
li 是立即数操作伪指令,因为立即数的操作是一些指令的合成,因为立即数操作使用频次较高,所以编译器将其缩减为 li,关于伪指令我在末尾会提到。
实验结果如下:
寄存器值:
Name : t0
Hex:0x20000000
Decimal:536870912
Octal:04000000000
Binary:100000000000000000000000000000
Default:536870912
Name : t1
Hex:0x78
Decimal:120
Octal:0170
Binary:1111000
Default:120
Name : t2
Hex:0x5678
Decimal:22136
Octal:053170
Binary:101011001111000
Default:22136
Name : t3
Hex:0x12345678
Decimal:305419896
Octal:02215053170
Binary:10010001101000101011001111000
Default:305419896
SRAM 存放值:

因为 CH32 是大端存储,高字节在低地址,所以每个 byte 存储的数据位置相反,关于大端小端可以参考我之前的文章:内存大小端
首先是寄存运算功能:加减乘除
编写代码测试指令:
li t1, 12
addi t2, t1, 4
给 t1 寄存器赋值 12,加上立即数 4 到 t2,编译运行查看结果

在将 t2 寄存器的值和 t1 寄存器相加,结果保存到 t2:
add t2, t2, t1
运行结果:

li t3, 4
sub t2, t2, t3
然后给 t3 赋值 4,使用 t2 寄存器的值减去 t3,结果保存到 t2,运行结果:

mul t1, t1, t3
将 t1 和 t3 的值相乘存储到 t1,运行现象:

div t4, t1, t3
再将 t1 除以 t3,结果保存到 t4,运行现象:

rem t4, t1, t3
将 t1 除以 t3 取余数,运行现象:

li t1, 0xF1
neg t2, t1
将 t1 的补码保存到 t2,运行结果:

sll rd,rs1,rs2:将寄存器rs1的值左移寄存器rs2的值这么多位,并写入寄存器rd
li t2, 0x000F0000
li t3, 4
sll t1, t2, t3
将 0x000F0000 逻辑左移四位:

slli t1, t2, 8
将 0x000F0000 逻辑左移8位:

srl t1, t2, t3
将 0x000F0000 逻辑右移4位:

srli t1, t2, 8
将 0x000F0000 逻辑右移8位:

算数偏移,往左相对于乘以 2,往右相对于除以 2,该方式下会将符号位带入计算
li t2, -128
sra t1, t2, t3
逻辑右移 4 位,相对于除以 16,符号位不变,得到 -8:

srai t1, t2, 8
逻辑右移 8 位,相对于除以 128,符号位不变,得到 -1:

li t1, 0b0A # 0000 1010
li t2, 0x06 # 0000 0110
and t3, t1, t2
执行结果:

andi t3, t1, 0x0F
执行结果:

or t3, t1, t2
执行结果:

ori t3, t1, 0x0F
执行结果:

xor t3, t1, t2
执行结果:

xori t3, t1, 0x0F
执行结果:

条件跳转是满足设置条件的情况下进行跳转:
beq rs1,rs2,lable:若rs1的值等于rs2的值,程序跳转到lable处继续执行
bne rs1,rs2,lable:若rs1的值不等于rs2的值,程序跳转到lable处继续执行
blt rs1,rs2,lable:若rs1的值小于rs2的值,程序跳转到lable处继续执行
bge rs1,rs2,lable:若rs1的值大于等于rs2的值,程序跳转到lable处继续执行
bltu rs1,rs2,lable:blt 无符号版
bgeu rs1,rs2,lable:bge 无符号版
写一段 c 语言方便理解:
if (t1 == t2) {
fun1();
} else if (t1 < t2) {
fun2();
} else if (t1 >= t2) {
fun3();
}
对应的汇编:
beq t1, t2, fun1
blt t1, t2, fun2
bge t1, t2, fun3
另一个版本
if (t1 != t2) {
fun1();
}
对应的:
bne t1, t2, fun1
无条件跳转没有设置条件,可直接进行跳转
jal 和 jalr 常用于函数跳转和返回
RISC - V 中有 8 个重要的 CSR 寄存器,寄存器如下:

这些寄存器控制着中断的使能,同时可以用于异常的捕获,当异常发生时:异常指令的 PC 被保存在 mepc 中,PC 被设置为 mtvec(对于同步异常,mepc 指向导致异常的指令;对于中断,它指向中断处理后应该恢复执行的位置。)根据异常来源设置 mcause,并将 mtval 设置为出错的地址或者其它适用于特定异常的信息字,在机器模式(M 模式,对硬件有 %100 的控制权限)下,异常信息字设置如下:

异常类型分为 5 种:
异常发生时异常指令的 PC 被保存在 mepc 中,PC 被设置为 mtvec(对于同步异常,mepc
指向导致异常的指令;对于中断,它指向中断处理后应该恢复执行的位置。)根据异常来源设置 mcause,并将 mtval 设置为出错的地址或者其它适用于特定异常的信息字。把控制状态寄存器 mstatus 中的 MIE 位置零以禁用中断,并把先前的 MIE 值保留到 MPIE 中。发生异常之前的权限模式保留在 mstatus 的 MPP 域中,再把权限模式更改为 M。
相关的寄存器操作使用如下指令完成,csr 就是上面相关寄存器: