V1.0:初始版本
时间:2021-10-20
最近看完了RISV-Reader,打算总结回顾以下。
可以在官方下电子版本。
网址:
http://riscvbook.com/chinese/RISC-V-Reader-Chinese-v2p1.pdf
特点或优点
- 开源
- 模块化
- 简介
- 易于编程连接(其实我觉得就是寄存器多)
寄存器
RV有32个通用寄存器,命名:x0-x31
-
x0:硬件连线为0
-
x1:ra 存返回地址
-
x2:sp 栈指针
-
x3、x4:分别为全局指针和线程指针
-
x5-x7:为临时寄存器、别名t0-t3
-
x8-x9:保留寄存器、别名s0-s1
-
x10-x17:参数寄存器、保存函数参数和返回值、别名a0-a7
-
x18-x27:保留寄存器、别名s2-s11
-
x28-x31:保留寄存器、别名t3-t6
函数调用规范
对于保留寄存器:子程序要保持运行前后的值不变。
x1-x4:用作特殊功能的寄存器不要改动
临时、参数寄存器随意使用;a0-a1用来保存返回值
标准函数入口出口
funx:
addi sp,sp,-framesize # 分配栈帧
sw ra,framesize-4(sp) # 保存返回地址
#其他寄存器按需保存
# ... 函数体
lw ra,framesize-4(sp) # 恢复 ra
addi sp,sp,framesize # 释放栈帧空间
ret
指令集
为什么说RISCV模块化呢,因为出了基础指令集、其余都是可选项,易于拓展和定制
书中介绍了以下的指令集:
- RV32I:基础整型指令集
- RV32M:乘除法指令集
- RV32F/D:单双浮点指令集
- RV32A:原子指令集
- RV32C:压缩指令集
- RV32V:向量指令集
- RV64G:64位通用指令集
RV32I
指令集图示如下、可以看到指令比较少:
RV32I的指令可以分成6种指令格式:
- R:寄存器与寄存器操作的指令
- I:短立即数和访存Load操作的指令
- S:访存Store指令
- B:分支指令
- U:长立即数指令
- J:无条件转跳指令
他们的编码方式如下:
源寄存器最多可以有2个,命名为rs1以及rs2。
目标寄存器最多有1个,命名为rd。
具体指令布局如下:
RV32M
指令集图示如下:
指令布局图示如下:
rem指令是求余数。
因为2个32位相乘可以得到一个64位的成绩;mulh、mulhu用于计算乘积的高32位
RV32FD
指令集图示如下:
基本上前面 I、M的指令对应的指令 就是 在前面加个f、然后后缀加 .s(单精度)或 .d(双精度)
指令布局图示如下:
浮点寄存器布局
浮点寄存器通用有32个
- f0-f7:浮点临时寄存器、别名ft0-ft7
- f8-f9:浮点保留寄存器、别名fs0-fs1
- f10-f17:浮点参数寄存器、别名fa0-fa7
- f18-f27:浮点保留寄存器、别名fs2-fs11
- f28-f31:浮点临时寄存器、别名ft8-ft11
浮点状态寄存器
状态控制寄存器。
frm是控制浮点的舍入模式的。
- b000:向最近偶数舍入
- b001:向0舍入
- b010:向下舍入
- b011:向上舍入
- b100:向最近最大值舍入
浮点和整型转换
看后缀就好了。
.x.y:to x from y
如:.s.w:to signal from word:从有符号整型转单浮点
.wu.d:to unsigned word from double:从双精度浮点转无符号整型
浮点和整型搬运
- x -> f寄存器:fmv.x.w
- f->x寄存器:fmv.w.x
RV32A
原子指令
看指令名就直到啥功能了、不过多解释了。
RV32C
压缩指令
主要是为了省内存、将一部分指令压缩成16bit
在压缩指令中、只能使用10个常用的寄存器(a0-a5、s0-s1、sp、ra)
note:
c.addi16sp:sp = sp + imm*16
c.addi4sp:sp = sp + imm*4
RV32V
RV32V有32个向量寄存器、以v开头。
但是每个寄存器宽度不确定、取决于设计者。
如:给向量寄存器分配的4096字节空间、那这32个寄存器就可以可以组成16个64位元素或32个32位元素或64个16位元素。
还可以禁止未使用的寄存器、如1024字节、仅使用2个64位的寄存器、则每个寄存器就有512字节(64个元素)
后缀:.vv、.vs、.sv、.vvv、.vvs等的后缀的意思
其主要表示:操作数的类型;
- .vv:表示第一第二个操作数都是向量
- .vs:表示第一个操作数是向量、第二个操作数是标量
以此类推。
标量意味着该数据来源于x或f寄存器
RV32V的指令未最终确定、所以没有指令布局图
寄存器
- mvl:最大向量长度寄存器(一次最大能运算的长度)
- vl:向量长度寄存器、控制向量的长度(当前运算的长度)
- vpi:8个谓词寄存器
- vsetdcfg:用来配置寄存器的类型,是16位的浮点还是8位的等…
vsetdcfg
例子:计算 y = a*x + y(x、y是向量)
li t0,2<<25
vsetdcfg t0 ;使能2个64bit的向量寄存器
loop:
;a0 是剩余未运算的长度 t0是当前已运算的长度(byte)
;t1是当前已运算的长度(double word)
setvl t0,ao ;vl = min(mvl,a0)
vld v0,a1 ;load x
vld v1,a2 ;load y
slli t1,t0,3 ;t1 = t0*8
vfmadd v1,v0,fa0,v1 ;v1 = v0*fa0 + v1(y = a*x + y)
sub a0,a0,t0 ;a0 -= t0(vl)
vset v1,a2 ;保存计算结果到 y
add a1,a1,t1 ;x往下移
add a2,a2,t1 ;y往下移
bnez a0,loop
ret
RV64
基本上是在RV32原有的指令上添加字和双字指令(也就是后缀.w和.d)
在上面RV32指令额外拓展的指令如下图所示:
RV特权架构
3种特权模式:
- 用户模式
- 监管者模式
- 机器模式
权限由高到低:机器>监管者>用户
异常
所有的中断和异常都在机器模式下处理。
但是机器模式可以委托中断或异常给监管者模式处理(因为Linux这种内核是S模式、需要接管一部分中断)
RISV-C支持2类中断:
- 局部中断:是指直接与hart相连的中中断、可以通过[m|s|u]cause直接获取中断类型;目前只有2种标准中断:计时中断和软件中断
- 全局中断:实际就是外部中断、与PLIC相连,经过仲裁后送入内核的中断控制器
CLINT模块
内核局部中断控制器(Core Local Interrupts Controller)
用于产生计时中断和软件中断,是一个地址映射的模。
占用64K内存空间(低27位固定、高位由具体SOC确定)。
下面是全志D1(C906)的CLINT寄存器视图
PLIC模块
类似于ARM的NVIC模块、专门管理外部中断。
机器模式寄存器
- mtvec:中断向量地址,定义的异常入口程序的基地
- mepc:发生异常的指令
- mcause:指示异常的种类,表明是什么事件造成异常
- mie:中断使能位、设置能响应的中断
- mip:中断标志位、指示当前正在处理的中断
- mtval:保存陷入(trap)附加信息;地址异常中出错的地址、非法指令异常中指令本身,其他异常为0
- msctratch:暂存寄存器
- mstatus:存放全局中断使能位、以及许多其他状态
- mideleg:机器中断委托寄存器,将响应的位置1就会将该中断委托给S模式的异常处理程序。sip和sie只有被委托的中断对应的位能读写,其他始终为0
S模式也有类似的SIE、SIP等寄存器
处理过程
进入异常\中断
- 更新mepc:如果是中断,则存的是下一条指令的地址;如果是异常,则是当前指令地址
- 更新mcause:根据异常的类型更新mcause
- 更新mtval:某些异常需要将信息写入到mtval
- 更新mstatus:
- 将异常发生前的MIE保存到MPIE;MIE设为0(失能中断)
- 将异常发生前的特权级保存到MPP;然后将当前模式设置为M
- 转跳到mtvec定义的入口地址执行代码
退出异常
当异常程序处理完后、最后会调用mret指令来退出异常程序。
MRET会执行如下操作:
- 恢复mepc到pc;
- 更新mstatus:将异常发生前的mstatus状态恢复
小结
从上面进入异常可以看到、是会关中断的,所以RV硬件上是不支持中断嵌套的,需要从软件上进行支持:
具体操作为:
- 在异常函数执行前,开中断
- 保存上下文(还得保存mstatus、因为可能会发生中断嵌套)
- 执行异常服务程序
- 关中断
- 恢复上下文
虚拟内存
S模式提供一个传统的虚拟内存系统。
访问未映射或权限不足的页都会导致页错误异常(pase fault exception)
页表项如下:
以下是不同位域的功能:
- V:该PTE是否有效,V=1有效
- R、W、X:是否可读、可写、可执行;如果都=0,表示该PTE指向下一级页表
- U:是否是用户页表;U=0,U模式不能访问这个页面
- G:表示这个映射是否对整个虚拟空间有效
- A:上次A被清除后、该页面是否被访问过
- D:上次D被清除后、该页面是否被弄脏(如被写入)
- RSW:留给操作系统使用
- PPN:物理页号;若该PTE是叶子节点,则PPN是物理地址的一部分,否则是下一级页表的地址
转换过程(Sv32为例):