Reindeer-RISCV学习笔记(4)RISCV指令集

这一篇文章好好学习一下RISCV指令集。

这里主要讲32位整数指令集,之后会提一下特权架构。这里不讲压缩指令,如果需要单独再讲。

写在前面

有谁可以告诉我为什么ARM对服务器支持不好,而Intel X86对移动端支持不好吗?下面是我查资料的结论,大家可以补充。

本质上ARM是精简指令集,X86是复杂指令集,前者注重功耗而性能不足,后者相反,并且昂贵。

1 RISCV体系结构

1.1基本内核RV64/RV32-IMAFD(G)

RISC-V ISA 被定义为一个基本的整数 ISA,必须在任何实现中存在,另外可以包含基于基本 ISA 的其他扩展。

基本整数 ISA 被命名为“I”(依据整数寄存器宽度不同,前缀 RV32或者 RV64),其中包含了整数计算指令、整数 load、整数 store 和控制流指令,并且在所有RISC-V 实现中,都是必须的。标准整数乘法和除法扩展被命名为“M”,其中增加了对保存在整数寄存器中的值进行乘法和除法的指令。标准原子指令扩展被命名为“A”,其中增加了对存储器进行原子的读、修改和写操作的指令,以支持处理器间的同步。标准单精度浮点扩展,被命名为“F”,增加了浮点寄存器、单精度计算指令、单精度 load 和 store 指令。标准双精度浮点扩展,被命名为“ D”,扩展了浮点寄存器,并增加了双精度计算指令、 load和 store 指令。一个基本整数内核加上这四个标准扩展(“IMAFD”),被缩写为“G”,它提供了一个通用的标量指令集。 RV32G 和 RV64G 现在是我们编译器工具链的缺省目标机器。

1.2 扩展性和稳定性

RISC-V尝试随着时间的推移,保持基本内核和每一个标准扩展不变,相反的,将新指令作为可选的扩展。例如,基本整数 ISA 将成为一个被持续支持的独立 ISA,而不管任何随后而来的扩展。

1.3 压缩指令扩展C

基本 RISC-V ISA 具有 32 位固定长度指令,并且必须在 32 位边界对齐。然而,标准 RISC-V编码模式被设计成支持变长指令的扩展C,在这个扩展中,每条指令长度可以是 16 位指令包裹(parcel)长度的整数倍,并且这些指令包裹必须在 16 位边界对齐。

每条短指令必须和一条标准的 32 位 RISC-V 指令一一对应。此外, 16 位指令只对汇编器和链接器可见,并且是否以短指令取代对应的宽指令由它们决定。编译器编写者和汇编语言程序员可以幸福地忽略 RV32C 指令及其格式,他们能感知到的则是最后的程序大小小于大多数其它 ISA 的程序。但是对于IC设计者,则必须能够识别并处理16位压缩指令。通常这种IC会混合16位压缩指令和32位指令,因此也包括了16位指令对齐到32位指令操作,然后一致使用32位指令解码执行。

1.4 特权架构,异常与自陷

RISCV 架构可以运行在三种特权级下,分别是机器模式、监管者模式、用户模式。前两种是特权架构,主要是用来处理异常中断以及系统调用,多核多线程之类的。而用户模式则是用户逻辑通常会使用到的指令(IMAFD和一些专用扩展)。

我们将术语异常(exception)认为是在运行时出现了一个与当前RISC-V线程中的一条指令相关的非正常的情况。我们将术语自陷(trap)认为是在一个RISC-V线程中出现了一个异常的情况,导致将控制同步传输到自陷处理函数。 自陷处理函数通常是在一个更高特权环境中执行的现了一个必须处理的中断,将会选择某条指令来接收中断异常, 然后顺序地产生一个自陷。

2 RISCV指令编码规则

2.1 指令长度编码约定

在这里插入图片描述

上图展示了标准 RISC-V 指令长度编码约定。所有基本 ISA 中的 32 位指令的最低 2 位被设置为 11。可选的压缩 16 位指令集扩展中的指令,最低 2 位被设置为 00、 01 或者 10。超过 32 位的标准指令集扩展,在低位有额外的位被设置为 1, 48 位、 64 位长度约定如上图所示。指令长度在 80 位到 176 位之间的长度信息,被编码到一个 3 位的字段[14:12]中,给出了 16 位字的数量,加上最开始的 5× 16 位字。 位[14:12]编码为 111,保留给未来更长的指令编码。

此外,全0和全1都是非法指令。

这里注意一下,32位指令低两位都是1,如果一个支持标准 G 的 ISA 实现,只需要在指令缓存(instruction cache)保存指令的最高 30 位(带来 6.25%的节约)。

2.2 指令字节序

小端模式,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中
顺便说一下,例如0x12,高位为1,一边来说内存对字节存储是MSB在高地址,小端模式则是0x123456,地址从高到底保存为0x12,0x34,0x56.

基本RISC-V ISA具有一个小端存储器系统,但是非标准变种可以提供大端或者双端存储器系统。指令被保存在存储器中,每个16位包裹以实现的端字节顺序,被保存到一个存储器半字中。包含一条指令的包裹,被保存到递增的半字地址,其中最低寻址的包裹保存着指令规范中最低位的二进制值,也就是说,指令总是按照一系列包裹的小端顺序保存的,而不管储器的端字节顺序

我们必须将指令包裹在存储器中保存的顺序固定下来,而与存储器系统的
端字节顺序无关,来确保指令的长度编码位总是出现在半字地址的最前面。这就允许取指单元通过一次取指,读取第一个 16 位指令包裹的最低几位,就可以确定变长指令的长度。一旦我们确定了小端存储器系统和指令包裹顺序,自然导致我们将指令长度编码放到指令格式的 LSB 位置,以避免破坏操作码字段

正常情况下,RISCV存储器是小端模式的,不用考虑其他情况。

2.3 编码规则

Inst[1:0]参考【#指令长度编码约定】
在这里插入图片描述
表 9.1给出了RVG主要操作码的映射表。具有3个或者更多个最低位被置为1的主要操作码,保留给长度超过32位的指令。标记为reserved的操作码应当避免在定制的指令集扩展中使用,因为它们可能被未来的标准扩展使用。在标准32位指令格式中,标记为custom-0和custom-1的主要操作码会避免被未来的标准扩展使用,因此推荐给定制指令集扩展使用。标记为custom-2/rv128和custom-3/rv128的操作码,保留给未来的RV128使用,但是也会避免被标准扩展使用,因此也可以用于RV32和RV64的定制指令集扩展。

2.4 指令类型

在这里插入图片描述
如上图所示,RISCV共有6类指令类型,其中opcode、rs1、rs2、rd的位置是固定的。opcode=[6:0],rs1=[19:15],rs2=[24:20],rd=[11:7]。另外, 这些立即数都是符号扩展的,并且所有立即数的符号位总是在指令的第31位,以加速符号扩展电路。

注:为了加载一个 32 位立即数,需要两步: load 指令提供该立即数的高 20位[31:12],常规指令提供该立即数的低 12 位[11:0],最后拼接成一个 32 位立即数)。

2.5 立即数

六类指令类型,R只有寄存器,其他的ISBUJ都包括立即数。
在这里插入图片描述
在这里插入图片描述
S和SB格式唯一的区别在于,在SB格式中, 12位立即数字段用于编码2的倍数的分支偏移量。与通常在硬件中将编码在指令中的立即数所有位向左移动1位不同,此处中间位(imm[10:1])和符号位保持在固定的位置,而S格式中的最低位(inst[7])编码为SB格式中的高位(imm[11])。

类似的, U和UJ格式唯一的区别在于, 20位立即数被左移12位以生成U立即数,而被左移1位以生成J立即数。在U和UJ格式立即数,其在指令中的位置的选择,以最大化与其它指令的相互覆盖,以及最大化U和UJ格式立即数的相互覆盖。

注:为了加载一个 32 位立即数,需要两步: load 指令提供该立即数的高 20位[31:12],常规指令提供该立即数的低 12 位[11:0],最后拼接成一个 32 位立即数)。

3 RISCV-I

RV32I 可以仿真几乎所有其他的 ISA 扩展(除了 A 扩展,它需要额外的硬件以支持原子性)。

3.1 寄存器模型

有31个通用寄存器x1~x31,它们保存了整数数值。寄存器x0是硬件连线的常数0。对于RV32,其x寄存器是32位宽度的,对于RV64,它们是64位宽度的。使用术语XLEN来指明当前x寄存器的宽度(不是32就是64)。还有一个额外的用户可见寄存器:程序计数器pc保存了当前指令的地址。
在这里插入图片描述
下表描述了约定俗成的寄存器作用,下面的f系列是浮点寄存器,用于RISCV-FD扩展。
在这里插入图片描述
对于嵌入式情境下的E扩展,只有16个寄存器。

3.2 整数计算模型

整数计算指令要么使用I类格式编码为寄存器-立即数操作,要么使用R类格式编码为寄存器-寄存器操作。对于寄存器-立即数指令和寄存器-寄存器指令,其目标都是寄存器rd没有整数计算指令产生算术异常

在这里插入图片描述
此外,NOP指令并不改变任何用户可见的状态,除了使得pc向前推进。 NOP被编码为ADDI x0,x0,0

3.3 控制转移指令

RV32I提供了两类控制转移指令:无条件跳转和条件分支。 RV32I中的控制转移指令,并没有体系结构可见的分支延迟槽。
在这里插入图片描述
JAL指令和JALR指令会产生一个非对齐指令取指异常, 如果目标地址没有对齐到4字节边界。

3.4 访存指令

RV32I是一个load-store体系结构,也就是说,只有load和store指令可以访问存储器,而算术指令只在CPU寄存器上进行操作运算。 RV32I提供了一个32位用户地址空间,它是字节寻址并且是小端的。执行环境将定义这个地址空间的哪些部分是可以合法访问的。
在这里插入图片描述

为了获得最高的性能,所有load和store指令的有效地址,应该与该指令对应的数据类型相对齐(也就是说, 32位访问应该在4字节边界对齐, 16位访问应该在2字节边界对齐)。

3.5 CSR指令

统指令用于访问那些可能需要特权访问的系统功能,以I类指令格式编码。 这可以分为两类:一类是原子性读-修改-写控制和状态寄存器(CSR)的指令,另一类是其他特权指令。 CSR指令在本节描述。

系统指令被定义为,允许在简单的实现中,总是自陷到一个单一的软件自
陷处理函数(software trap handler)。更高级的实现,可以在硬件上执行一条或者多条系统指令

在这里插入图片描述

CSR主要是一组(按位)读写CSR寄存器的指令,RV32I提供了多个用户级只读的64位计数器, 它们被映射到一个12位的CSR地址空间中。对于这些 64 位计数器, 我们一次可以读取 32 位。这些计数器包括了系统时间, 时钟周期以及执行的指令数目。。

*3.5.1 RISCV标准CSR寄存器

下面转自:https://www.jianshu.com/p/2152708d75f1
这里列出了所有的CSR寄存器,大多数也不是必须要实现的,大家重点关注一下计数器和中断相关指令就可以了。

(1) 用户级别IS

在这里插入图片描述

(2) supervisor 级别ISA

在这里插入图片描述

(3) Hypervisor 级别ISA

在这里插入图片描述

(4) Machine级别ISA

在这里插入图片描述

3.5.2 定时器和计数器

在这里插入图片描述
cycle、cycleh是处理器周期计数器,由硬件时钟决定自增频率,用来统计自CPU复位以来共运行了多少个周期。可读可写。

instret、instreth是执行指令计数器,反映处理器成功执行了多少条指令。只要处理器每成功执行一条指令,此计数器都会自增一。可读可写。

time、timeh是时钟计数器,统计自CPU复位以来共运行了多少时间,驱动time计数器是已知的固定频率的时钟,例如32768Hz的时钟。可读可写。

cycle vs time cycle统计的是CPU周期数,驱动cycle计数器的是CPU的核心时钟,核心时钟可能是动态调整的,例如繁忙状态下核心时钟调整到100MHz,空闲状态下核心时钟调整到10MHz,依据cycle是无法确定CPU运行了多少时间的,除非CPU的时钟是固定的。而驱动time计数器的一定是固定频率的时钟,所以可以用来确定CPU运行了多少时间。

cycle vs instret cycle统计的是周期数,instret统计的是指令数,有些指令需要多个周期才能完成,例如MUL指令,有些实现需要4个周期,那么执行MUL指令后,cycle增加4,而instret增加1。还有就是内存访问会引起等待周期,内存的等待周期会累计到cycle,但是不影响instret。

注意机器模式下提供了一个mtimecmp,可以和mtime进行比较然后触发中断。

3.5.3 中断

最主要的是mstatus寄存器,其次还有一些其他的:
在这里插入图片描述
在这里插入图片描述
先看一下mstatus寄存器:
在这里插入图片描述
MIE:全局中断使能
MPIE/MPP:保存进入异常前的模式

当处理器进入异常后:MPIE被更新为当前的MIE的值,MIE值更新为0(屏蔽全局中断),MPP被更新为异常发生前的模式。

mtvec寄存器用于配置异常的入口地址。

mepc寄存器用于保存进入异常之前指令的PC值(中断时指向下一条指令,因为当前指令已被成功执行;异常时指向当前指令),作为异常的返回地址。虽然这个寄存器会在中断时自动更新,但是也可以用户读写。

MIE寄存器用于控制不同中断类型的局部屏蔽。如外部中断,定时器中断,软件中断位。

MIP寄存器用于查询中断的等待状态。

MSCRATCH寄存器用于机器模式下的程序临时保存某些数据。

以上是中断跳转需要的寄存器,此外有一些寄存器用来记录异常来源,用于调试,包括mcause、mtval(mbadaddr)等。

(1) 中断实现流程

如何实现一个完备的中断系统较为复杂,本人也没有完全搞清楚,简单来说:

  1. 要有个中断控制器,然后配置CSR及中断控制器相关位使能相关的中断;
  2. 中断来临后屏蔽中断,保存现场,要记录返回PC值(mepc)以及保存相关寄存器的值;
  3. 要得到中断服务函数的地址并跳转。这个可能由两个办法,第一个办法是工具中断源的中断号,直接硬件计算出中断向量表的地址,然后取出中断函数入口地址更新到mtvec寄存器里面,然后直接使用mtvec的值跳转就行了。第二个办法就是统一使用一个中断服务函数,即mtvec的值是固定的,然后在这个中断服务函数里面读取中断控制器的中断源,并进行跳转。
  4. 执行完中断后恢复现场,并跳转到之前记录的PC值(mepc)继续执行。
(2) 中断优先级

一个方法中断都记录,然后是软件处理中断优先级;另一个方法是由mcause和中断控制器控制中断优先级。

(3) 中断嵌套

按照RISC v标准,进入中断后会屏蔽中断,所以不会有嵌套。

如果要中断嵌套的话,因为mstatus可读可写,所以进入中断后可以重新开启中断,但是在这个过程中要解决保存原来的中断上下文以及处理优先级的的问题。

*3.6 多核多线程下的存储器模型及FENCE指令

在基本RISC-V ISA中,每个RISC-V线程看到它自己的存储器操作,如同它们就是按照程序中的顺序执行一样。 RISC-V在线程间有一个放松的存储器模型(relaxed memory model),在不同的RISC-V线程之间的存储器操作,需要一条明确的FENCE指令来确保任何特定地顺序。
在这里插入图片描述

FENCE指令用于顺序化其他RISC-V线程、外部设备或者协处理器看到的设备I/O和存储器访问。FENCE就像一个栅栏, FENCE之前所有的存储器操作、 I/O操作必须完成后,在FENCE之后的指令才能看到结果。

为什么RISC-V ISA里有个必须要实现的指令FENCE呢?这就涉及RISC-V的存储模型了。RISC-V采用的是 RISC-V Weak Memory Ordering (RVWMO)模型,对存储操作的执行顺序限制较少,为了保证一致性需要特殊的指令来规范存储操作的执行顺序。顾名思义,FENCE指令犹如一道屏障,把前面的存储操作和后面的存储操作隔离开来,前面的决不能到后面再执行,后面的决不能先于FENCE前的指令执行。FENCE指令带参数标志隔开前后何种类型的存储操作。这里的说法不够严谨,仅仅提供了FENCE指令感性上的认知。
对于简单的单hart理器来说,FENCE指令可以当做NOP来处理。如果想以简单的硬件实现FENCE的功能可以将其实现为一个trap,把工作量转嫁到软件上去。Rocket对于FENCE的实现是在decode stage停下来,知道cache通知它可以继续。
来自:https://zhuanlan.zhihu.com/p/139797515

详细内容大家可以直接查资料,这里就不多说了。

*3.7 ecall和ebreak

在 RISC-V 指令集中, ecall 指令用于向运行时环境发出请求,例如系统调用。调试器使用 ebreak 指令将控制转移到调试环境。

这个暂时我也没搞懂,就先不讲了。

4 RISCV-MAFD

4.1 整数乘除法指令扩展

整数乘除法指令扩展包含针对两个整数寄存器中的数值进行乘法或者除法的指令。

4.1.1 乘法

因为两个32位数相乘结果是64位,所以为了正确地得到一个有符号或无符号的 64 位积, RISC-V 中带有四个乘法指令。要得到整数 32 位乘积(64 位中的低 32 位)就用 mul 指令。要得到高 32 位,如果操作数都是有符号数,就用 mulh 指令;如果操作数都是无符号数,就用 mulhu 指令;如果一个有符号一个无符号,可以用 mulhsu 指令。

如果同时需要乘法结果的高位和低位,那么建议的代码顺序为: MULH[[S]U] rdh,rs1,rs2; MUL rdl,rs1,rs2(源寄存器区分符必须按照同样的顺序,并且rdh不能是rs1或者rs2)。

mulh 和 mulhu 可以检查乘法的溢出 如果 mulhu 的结果为零,则在使用 mul 进行无符号乘法时不会溢出。类似地,如果 mulh结果中的所有位与 mul 结果的符号位匹配(即当 mul 结果为正时 mulh 结果为 0, mul 结果为负时 mulh 结果为十六进制的 ffffffff),则使用 mul 进行有符号乘法时不会溢出

4.1.2 除法

DIV和DIVU指令分别执行有符号、无符号的XLEN位整数除以XLEN位整数除法操作。REM、REMU给出了相应除法的余数。如果同时需要商和余数, 那么建议的代码顺序为: DIV[U] rdq,rs1,rs2; REM[U] rdr,rs1,rs2(rdq不能是rs1或者rs2)。

在这里插入图片描述

检查是否除零 要测试除数是否为零,只需要在除法操作之前加入一条用于测试的 beqz 指令。 RV32I 不会因为除零操作而 trap,因为极少数程序需要这种行为,而且在那些软件中可以很容易地检查是否除零。当然,除以其它常数永远不需要检查。

4.2 浮点数扩展FD

ARM 浮点运算,软浮点,硬浮点_Jalen_king-CSDN博客

(1)硬浮点(hard-float)

编译器将代码直接编译成硬件浮点协处理器(浮点运算单元FPU)能识别的指令,这些指令在执行的时候ARM核直接把它转给协处理器执行。FPU 通常有一套额外的寄存器来完成浮点参数传递和运算。使用实际的硬件浮点运算单元(FPU)会带来性能的提升。

(2)软浮点(soft-float)

编译器把浮点运算转成浮点运算的函数调用和库函数调用,没有FPU的指令调用,也没有浮点寄存器的参数传递。浮点参数的传递也是通过ARM寄存器或者堆栈完成。现在的Linux系统默认编译选择使用hard-float,如果系统没有任何浮点处理器单元,这就会产生非法指令和异常。因而一般的系统镜像都采用软浮点以兼容没有VFP的处理器。

用一句话总结,软浮点是通过浮点库去实现浮点运算的,效率低;硬浮点是通过浮点运算单元(FPU)来完成的,效率高。

4.2.1 F扩展

F扩展加入了32个浮点寄存器, f0-f31,每个是32位宽度,一个浮点控制和状态寄存器fcsr,
它包含了操作模式和浮点单元的异常状态。

浮点控制和状态寄存器fcsr是一个RISC-V控制和状态寄存器(CSR)。 它是一个32位的读/
写寄存器,用于为浮点算术操作选择动态舍入模式,并保存产生的异常标志,如图 7.2所示。

在这里插入图片描述

4.2.2 D扩展

D扩展加宽了32个浮点寄存器, f0-f31,到64位。

4.3 原子指令扩展

标准原子性指令扩展被称为“A”扩展,包含了对存储器执行原子性读-写-修改的指令,以支持运行在同一个存储器空间中的多个RISC-V线程之间的同步操作。有两种原子性指令,一种是load-reserved/store-conditional指令,另一种是原子性fetch-and-op存储器指令。
在这里插入图片描述

AMO 指令对内存中的操作数执行一个原子操作,并将目标寄存器设置为操作前的内存值。原子表示内存读写之间的过程不会被打断,内存值也不会被其它处理器修改。

针对单个存储器字的复杂原子性操作,是由load-reserved(LR)指令和store-conditional(SC)指令来完成的。 load-reserved/store-conditional指令主要适用于实现常见的原子的比较-交换(compare-and-swap)操作:比较一个寄存器中的值和另一个寄存器中的内存地址指向的值,如果它们相等,将第三个寄存器中的值和内存中的值进行交换。这是一条通用的同步原语,其它的同步操作可以以它为基础来完成[Herlihy 1991]。建议先看一下这个:https://blog.csdn.net/qq_45467083/article/details/121512569

用两条指令来实现比较交换操作,主要是为了RISCV指令格式的一致性,都使用两个操作数。因为使用两条指令,因此需要额外的硬件监测加载数据的变化情况。简单地说, LR指令从存储器读一个数值,同时处理器会监视这个存储器地址,看它是否会被其他处理器修改; SC指令发现在此期间没有其他处理器修改这个值,则将新值写入该地址。如果保存失败,那么需要重新开始整个序列。

在这里插入图片描述

原子性扩展还有很多细节内容,这部分我也不是非常清楚,大家感兴趣可以专门研究一下。

5 嵌入式RISCV-E

RV32E基本整数指令集是RV32I为嵌入式系统而设计的简化版本。主要的变动在于将整数寄存器的数目减少到16个,去掉了RV32I里面强制必须的计数器。RV32E 使用与 RV32I 相同的指令集编码,但是在一条指令中使用 x16-x31 寄存器区分符将导致产生一个非法指令异常。

在这里插入图片描述

RV32E不建议支持硬件浮点数和完整的Unix类操作系统,但是可以设计支持。

6 自定义指令集扩展

指令集扩展和外设的主要区别是什么呢?

可能你会说是运行速率不同,外设一般使用低俗总线通信,外设运行过程与处理器异步,运行结束后触发中断通知处理器处理。而CPU指令集则是工作在流水线上的。

虽然上面的回答是正确的,但是个人感觉对于设计自定义指令集扩展(接口)没有太多帮助,因为扩展指令集也可以完成外设的工作,并且没有指令延迟(但是可能会阻塞流水线,或者你用中断也行)。

所以我认为指令集扩展和外设的主要区别在于(应该说是基于RISC-V的,其他的架构不确定),指令集扩展局限在对CPU寄存器组进行操作,而不涉及存储器模型,从而可以嵌入在流水线的结构中,保持小面积电路的同时,实现高速运算的结果。而外设一般是存储器模型的扩展,和内存共享地址空间。

进一步讲,制约CPU速度的实际上数据访存速率,寄存器最快,cache次之,内存和外设则更慢。而流水线上的运算控制指令为了保证高速,数据访存全部局限在寄存器组上,访存指令则使用cache进行加速。换言之,扩展指令集接口也应该提供一组与寄存器访问速率差不多的寄存器组用于数据访存,对于慢速接口访存则应该提供cache。而设计一个自定义指令接口时,最简单的办法还是将扩展指令局限在标准寄存器组中,而利用load/store指令获取其他慢速数据访存。但是为了实现多个数据的并行加速(向量),有必要提供多个数据的load/store指令扩展。

在使用FPGA实现协处理器时,我觉得可以考虑计算单元池的发送,然后软硬件协同对功能模块加速,这样可以实现非标准向量(例如链表)的并行计算。其步骤包括计算单元池剩余量检测,任务分配,以及返回结果和池回收。

6.1 标准 vs. 非标准扩展

任何RISC-V处理器实现都必须支持一个基本整数ISA(RV32I或者RV64I)。另外,一个实现可以支持一个或者多个扩展。我们将扩展分为两大类: 标准的、 非标准的。

  • 一个标准扩展是一个通用的扩展,并不与其它任何标准扩展冲突。当前,“MAFDQLCBTPV”扩展是已经完成或者正在计划的标准扩展。
  • 一个非标准扩展是一个高度特殊化的扩展,可能与其它标准或者非标准扩展冲突。

6.2 绿地扩展 vs. 棕地扩展

我们使用术语“绿地扩展(greenfield extension)”来描述一个扩展开始使用一个新的指令编码空间,因此仅可能在前缀级别产生编码冲突。我们使用术语“棕地扩展(brownfieldextension)”来描述一个扩展嵌入到一个前面已经定义指令编码空间。例如,基本ISA是一个30位指令空间的绿地编码(前缀是11),而FDQ浮点扩展(前缀也是11),都是基本ISA 30位编码空间下的棕地扩展。

注意到我们认为标准A扩展是一个绿地扩展,因为它在整个32位基本指令空间中,定义了一个新的、以前空白的、最左边25位的编码空间,即使它的标准前缀落在基本ISA的30位编码空间中。只改变它的单个7位前缀,就可以将A扩展移动到一个不同的30位编码空间,而仅仅需要考虑在前缀级别的冲突,而不需要考虑编码空间内部的冲突。
所以我也没搞懂他这个是怎么划分的,大概就是编码空间要是一个闭集,指令空间的位数不超过这个闭集的范围。

6.3 标准兼容的全局编码

一个真正的RISC-V实现的ISA完整或者全局编码(complete or global encoding),必须对其包含的每一个指令编码空间分配一个唯一的、不冲突的前缀。基本内核和每一个标准扩展都有一个已经分配好的标准前缀, 可以确保它们可以在一个全局编码中共存。

一个标准兼容的全局编码,是一个其基本内核和每一个包括在内的标准扩展都是标准前缀的全局编码。一个标准兼容的全局编码可以包含非标准扩展,它们和所包含的标准扩展不冲突。一个标准兼容的全局编码可以为非标准扩展使用标准前缀,如果相对应的标准扩展并没有包括在这个全局编码内的话。换句话说,在一个标准兼容的全局编码中,一个标准扩展必须使用它的标准前缀,但如果没有包含某个标准扩展,那么这个标准扩展的标准前缀,是可以挪作他用的。这些约束,可以使得一个通用工具链可以以任何RISC-V标准兼容全局编码的子集的实现作为目标机。

6.4 在固定 32 位指令格式内的扩展

6.4.1 可用的 30 位指令编码空间

在标准编码中, 3个可用的30位指令编码空间(以00、 01、 10为前缀)可用于可选的压缩指令扩展。然而,如果不需要压缩指令集扩展,那么这3个30位编码空间就可以自由使用。这将在32位格式中扩大了4倍的可用编码空间。

6.4.2 可用的 25 位指令编码空间

在这里插入图片描述
custom-0,custom-1用于32位指令扩展。此外一些浮点扩展之类的,如果自己设计的CPU没有用,也是可以扩展的。

如果一个实现不需要浮点,则保留给标准浮点扩展的7个主要操作码(LOAD-FP、 STORE-FP、MADD、 MSUB、 NMSUB、 NMADD、 OP-FP) 可以被非标准扩展重用。类似的, AMO主要操作码在不需要标准原子性扩展时,也可以被重用。

6.4.3 可用的 22 位指令编码空间

在基本和标准扩展编码中,一个22位指令编码空间对应于一个funct3次要操作码。好几个主要操作码拥有没被完全占用的funct3次要操作码,留下了几个可用的22位编码空间。通常一个主要操作码使用这种格式在指令剩余的位中编码操作数,理想情况下,一个扩展应当遵守这种主要操作码的操作数格式,以简化硬件译码。

6.4.4 其他空间

在某些主要操作码下还有更小的空间可用,并且并不是所有的次要操作码都被完全使用了的。

6.5 非标准扩展命名

非标准子集被命名为使用一个“X”,后面跟在一个以字母开始的字符串和可选的版本号。例如,“Xhwacha”命名了Hwacha向量取指ISA扩展;“Xhwacha2”和“Xhwacha2p0”命名了同样机器的2.0版。

非标准扩展必须使用一个下划线与其它多字母扩展相分隔。例如,一个具有非标准扩展Argle和Bargle的ISA,可以命名为“RV64GXargle_Xbargle”。

6.6 蜂鸟E203扩展指令接口

https://doc.nucleisys.com/hbirdv2/core/core.html#nice
蜂鸟E203处理器核使用CUSTOM指令扩展。该处理器接口借鉴了Rocket Core的协处理器接口RoCC,命名为EAI。

6.6.1 指令编码

在这里插入图片描述
在这里插入图片描述
可以看出,funct7用于用户指令编码,7为编码有128条指令,因为可以使用custom-0到3,所以总共可以扩展512条用户指令。

另外可以看到,为了实现通用性,固定死了三个寄存器,同时增加了三位使能位原来实现非3寄存器指令,浪费了一定的资源。因为不支持立即数操作,所以在调用用户指令前,首先要对寄存器赋值,然后才能使用用户指令。但是总的来说,可以实现一切对寄存器的操作指令。

对外设操作使用load/store指令。

6.6.2 流水线接口

在这里插入图片描述
执行过程:

  1. Decode单元在EXU级对指令的Opcode译码,判断是否是Custom组指令;
  2. 如果是Custom指令,则主处理器在EXU级将指令的编码信息,源操作数的值,和指令序号通过请求通道发送给协处理器;协处理器可以通过ready信号控制接收多拍指令或者一拍指令。
  3. 协处理器在执行完成后,通过反馈通道将结果反馈给主处理器。如果协处理器处理了多条指令,则返回结果应当按序返回。
  4. 主处理器收到协处理器结果后,写回CPU的寄存器组。在协处理器执行过程中,主处理器按照流水线进行,如果存在数据相关性,则阻塞流水线。

在协处理器执行的过程中,协处理器可以向主存发出读写请求,通过存储器接口。

6.6.2 指令接口总线

在这里插入图片描述

6.6.3 存储器接口总线

在这里插入图片描述
注意一下,存储器访问请求是由协处理器发出的,并且要求协处理器拉高独占存储器信号,避免出现死锁之类的问题。此外,协处理器一次只能读写一个数据。

因为协处理器和主处理器各自访问主存的,所以存在竞争问题,需要相应的硬件进行处理访问源选择。

6.6.4 参考示例

详细参考《手把手教你设计CPU——RISC-V处理器篇》
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

参考资料

RISC-V精简到何种程度?能省的都省了!https://xueqiu.com/4463035516/126923203

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

朽木白露

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值