RISC-V 指令学习笔记(基于CH32V103)
Posted Top嵌入式
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了RISC-V 指令学习笔记(基于CH32V103)相关的知识,希望对你有一定的参考价值。
文章目录
RISC-V 指令学习笔记(基于CH32V103)
最近学习 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 指针指向程序运行地址!了解完指令结构和架构寄存器的分工后,我们了解一下指令的功能分类~
三、加载存储指令
加载存储指令用于对寄存器进行数据的加载和保存操作,主要有以下几个
- lb rd,offset(rs1):从地址为寄存器rs1的值加offset的主存中读一个字节,符号扩展后存入rd
- lh rd,offset(rs1):从地址为寄存器rs1的值加offset的主存中读半个字,符号扩展后存入rd
- lw rd,offset(rs1):从地址为寄存器rs1的值加offset的主存中读一个字,符号扩展后存入rd
- lbu rd,offset(rs1):从地址为寄存器rs1的值加offset的主存中读一个无符号的字节,零扩展后存入rd
- lhu rd,offset(rs1):从地址为寄存器rs1的值加offset的主存中读半个无符号的字,零扩展后存入rd
- lwu rd,offset(rs1):从地址为寄存器rs1的值加offset的主存中读一个无符号的字,零扩展后存入rd
- sb rs1,offset(rs2):把寄存器rs1的值存入地址为寄存器rs2的值加offset的主存中,保留最右端的8位
- sh rs1,offset(rs2):把寄存器rs1的值存入地址为寄存器rs2的值加offset的主存中,保留最右端的16位
- sw rs1,offset(rs2):把寄存器rs1的值存入地址为寄存器rs2的值加offset的主存中,保留最右端的32位
编程示例:
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 存储的数据位置相反,关于大端小端可以参考我之前的文章:内存大小端
四、算数运算指令
首先是寄存运算功能:加减乘除
- add rd,rs1,rs2:将寄存器rs1与rs2的值相加并写入寄存器rd
编写代码测试指令:
li t1, 12
addi t2, t1, 4
给 t1 寄存器赋值 12,加上立即数 4 到 t2,编译运行查看结果
- addi rd,rs1,imm:将寄存器rs1的值与立即数imm相加并存入寄存器rd
在将 t2 寄存器的值和 t1 寄存器相加,结果保存到 t2:
add t2, t2, t1
运行结果:
- sub rd,rs1,rs2:将寄存器rs1与rs2的值相减并写入寄存器rd
li t3, 4
sub t2, t2, t3
然后给 t3 赋值 4,使用 t2 寄存器的值减去 t3,结果保存到 t2,运行结果:
- mul rd,rs1,rs2:将寄存器rs1与rs2的值相乘并写入寄存器rd
mul t1, t1, t3
将 t1 和 t3 的值相乘存储到 t1,运行现象:
- div rd,rs1,rs2:将寄存器rs1除以寄存器rs2的值,向零舍入并写入寄存器rd
div t4, t1, t3
再将 t1 除以 t3,结果保存到 t4,运行现象:
- rem rd, rs1, rs2:寄存器 rs1 除以寄存器 rs2 的值,向 0 舍入,都视为 2 的补码,余数写入 rd 寄存器
rem t4, t1, t3
将 t1 除以 t3 取余数,运行现象:
- neg rd, rs2:把寄存器 rs2 的值的二进制补码写入 rd 寄存器
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 rd,rs1,imm:将寄存器rs1的值左移立即数imm的值这么多位,并写入寄存器rd
slli t1, t2, 8
将 0x000F0000 逻辑左移8位:
- srl rd,rs1,rs2:将寄存器rs1的值逻辑右移寄存器rs2的值这么多位,并写入寄存器rd
srl t1, t2, t3
将 0x000F0000 逻辑右移4位:
- srli rd,rs1,imm:将寄存器rs1的值逻辑右移立即数imm的值这么多位,并写入寄存器rd
srli t1, t2, 8
将 0x000F0000 逻辑右移8位:
- sra rd,rs1,rs2:将寄存器rs1的值算数右移寄存器rs2的值这么多位,并写入寄存器rd
算数偏移,往左相对于乘以 2,往右相对于除以 2,该方式下会将符号位带入计算
li t2, -128
sra t1, t2, t3
逻辑右移 4 位,相对于除以 16,符号位不变,得到 -8:
- srai rd,rs1,imm:将寄存器rs1的值算数右移立即数imm的值这么多位,并写入寄存器rd
srai t1, t2, 8
逻辑右移 8 位,相对于除以 128,符号位不变,得到 -1:
六、逻辑操作指令
- and rd,rs1,rs2:将寄存器rs1与rs2的值按位与并写入寄存器rd
li t1, 0b0A # 0000 1010
li t2, 0x06 # 0000 0110
and t3, t1, t2
执行结果:
- andi rd,rs1,imm:将寄存器rs1的值与立即数imm的值按位与并写入寄存器rd
andi t3, t1, 0x0F
执行结果:
- or rd,rs1,rs2:将寄存器rs1与rs2的值按位或并写入寄存器rd
or t3, t1, t2
执行结果:
- ori rd,rs1,imm:将寄存器rs1的值与立即数imm的值按位或并写入寄存器rd
ori t3, t1, 0x0F
执行结果:
- xor rd,rs1,rs2:将寄存器rs1与rs2的值按位异或(相异为1,相同为0)并写入寄存器rd
xor t3, t1, t2
执行结果:
- xori rd,rs1,imm:将寄存器rs1的值与立即数imm的值按位异或并写入寄存器rd
xori t3, t1, 0x0F
执行结果:
七、跳转指令
7.1 条件跳转
条件跳转是满足设置条件的情况下进行跳转:
-
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
7.2 无条件跳转
无条件跳转没有设置条件,可直接进行跳转
- j label:程序直接跳转到lable处继续执行
- jal rd,label:把下一条指令的地址保存在rd中(通常用x1),然后跳转到label处继续执行
- jalr rd,offset(rs):把下一条指令的地址存到rd中,然后跳转到rs+offset地址处的指令继续执行。若 rd 为全 0 寄存器,则相当于 j
jal 和 jalr 常用于函数跳转和返回
八、比较判断
- slt rd,rs1,rs2:若rs1的值小于rs2,rd置为1,否则置为0
- slti rd,rs1,imm:若rs1的值小于立即数imm,rd置为1,否则置为0
- sltu rd,rs1,rs2:若rs1的值小于rs1的值,rd置为1,否则置为0
- sltiu rd,rs1,imm:若rs1的值小于立即数imm,rd置为1,否则置为0
九、CSR 操作指令
RISC - V 中有 8 个重要的 CSR 寄存器,寄存器如下:
- mtvec(Machine Trap Vector)它保存发生异常时处理器需要跳转到的地址
- mepc(Machine Exception PC)它指向发生异常的指令
- mcause(Machine Exception Cause)它指示发生异常的种类
- mie(Machine Interrupt Enable)它指出处理器目前能处理和必须忽略的中断
- mip(Machine Interrupt Pending)它列出目前正准备处理的中断
- mtval(Machine Trap Value)它保存了陷入(trap)的附加信息:地址例外中出错
的地址、发生非法指令例外的指令本身,对于其他异常,它的值为 0 - mscratch(Machine Scratch)它暂时存放一个字大小的数据
- mstatus(Machine Status)它保存全局中断使能,以及许多其他的状态,如图所示
这些寄存器控制着中断的使能,同时可以用于异常的捕获,当异常发生时:异常指令的 PC 被保存在 mepc 中,PC 被设置为 mtvec(对于同步异常,mepc 指向导致异常的指令;对于中断,它指向中断处理后应该恢复执行的位置。)根据异常来源设置 mcause,并将 mtval 设置为出错的地址或者其它适用于特定异常的信息字,在机器模式(M 模式,对硬件有 %100 的控制权限)下,异常信息字设置如下:
异常类型分为 5 种:
- 访问错误异常 当物理内存的地址不支持访问类型时发生(例如尝试写入 ROM)
- 断点异常 在执行 ebreak 指令,或者地址或数据与调试触发器匹配时发生
- 环境调用异常 在执行 ecall 指令时发生
- 非法指令异常 在译码阶段发现无效操作码时发生
- 非对齐地址异常 在有效地址不能被访问大小整除时发生
异常发生时异常指令的 PC 被保存在 mepc 中,PC 被设置为 mtvec(对于同步异常,mepc
指向导致异常的指令;对于中断,它指向中断处理后应该恢复执行的位置。)根据异常来源设置 mcause,并将 mtval 设置为出错的地址或者其它适用于特定异常的信息字。把控制状态寄存器 mstatus 中的 MIE 位置零以禁用中断,并把先前的 MIE 值保留到 MPIE 中。发生异常之前的权限模式保留在 mstatus 的 MPP 域中,再把权限模式更改为 M。
相关的寄存器操作使用如下指令完成,csr 就是上面相关寄存器:
- csrrw rd, csr, rs:是读后写控制状态寄存器,先将 csr 的值记为 t,把 rs 寄存器的值写入 csr,再将 t 写入 rd 中;
- csrrwi rd, csr, zimm:是立即数读后写控制状态寄存器,将 csr 的值写入 rd 中,再将立即数写入 csr 中;
- csrrs rd, csr, rs1:是读后 置位 控制状态寄存器,先将 csr 的值记为 t,让 t 和 rs1 取或并写入 csr,再将 t 写入 rd 中;
- csrrsi rd, csr, zimm:是立即数读后 置位 控制状态寄存器,先将 csr 的值记为 t,把 t 和立即数 zimm 取或并写入 csr,再将 t 写入 rd 中;
- csrrc rd, csr, rs1:是读后 清除位 控制状态寄存器,先将 csr 的值记 为 t,把 t 和 rs1 位与并写入 csr,再将 t 写入 rd 中;
- csrrci rd, csr, zimm:是立即数读后 清除位 控制状态寄存器,csr 的值记为 t,把 t 和立即数 zimm 位与并写入 csr,再将 t 写入 rd 中。
以上是关于RISC-V 指令学习笔记(基于CH32V103)的主要内容,如果未能解决你的问题,请参考以下文章