如何在RISC-V中添加一个指令
RISC-V支持指令的扩展,本文介绍在从添加一个完整的指令并且使用需要作出那些改动。(用户态指令)
步骤
- 确定这一指令的功能:计算加速/安全检查等等
- 确定指令的需求:需要读取几个寄存器,是否有立即数参与,最终确定指令类型(R、I、S、U)。参见指令手册2.2章Base Instruction Formats。
- 确定指令的编码空间:是否兼容与已有指令,是否是一个新的扩展,找到一个编码空间。
- 硬件实现
- 在idecode.scala中添加指令编码,控制线(如果需要)
- 选择如何实现这一指令:RoCC指令添加,Rocket核心中添加或者自己增加一个单独的核心。
- 汇编器实现
- 在gnu工具链中添加汇编器的识别
- 在gnu工具链中添加反汇编识别
- 在编译器中实现
- 如何在编译器中使用这个新的指令
确定指令功能、需求和实现方式
确定指令需求是添加指令的第一步,如果指令设计有问题,之后的实现肯定遭遇各种问题。首先要思考以下问题:
- 为什么要添加新的指令?
- 用于加速:能够有多少加速效果?
- 用RoCC指令本身就会因为无法使用旁路等机制减慢,要综合考虑加速的得失。
- 用原有指令来组成这一功能需要多少条指令?加速的效果明显么?
- 是否需要访存,是否需要从Cache中单独建立通路?
- 用于添加功能:
- 添加的功能是什么?
- 这个功能需要哪些操作数,是否和CPU原有流水和功能有冲突?
- 一个指令只有64bit,除去opcode的部分用于指定读写寄存器和立即数,是否够用?(等价于选用什么指令类型,如果不是规范的指令类型会在指定寄存器和立即数上更加复杂)
- 每个寄存器需要5bit
- 在核心中每条指令仅有两个读寄存器和一个写寄存器,超过这个数量会变得复杂。
- RoCC不能够进行控制转移(修改PC)等修改,在核心中更改的代价是否过高?
- 用于加速:能够有多少加速效果?
- 硬件上如何处理这个指令?Rocket Core对于一般的指令自己处理,乘除法交给div模块处理,浮点交给fpu处理,定制指令交过RoCC模块处理。
- RoCC是标准的扩展指令接口,经典的例子是用于sha算法加密等,这种方法优点是实现简单,完全自主,但效率比较低,不能应用旁路等。适合如下的指令:
- 输入方法简单
- 运算本身复杂
- 与其他指令相关性小
- 不产生控制流转移
- 直接在Rocket Core中添加功能是可以效率很高的,但问题是比较复杂,你需要考虑五级流水中所有的问题。如果你的期望指令有如下的特性,那更适合直接在Rocket Core中修改:
- 运算简单
- 经常与前后指令产生旁路
- 产生控制转移
- 产生中断/异常
- 当然,也可以像fpu一样,自己定义接口自己写,但这样一般更加适合直接用RoCC,包括定制的RoCC接口。
- RoCC是标准的扩展指令接口,经典的例子是用于sha算法加密等,这种方法优点是实现简单,完全自主,但效率比较低,不能应用旁路等。适合如下的指令:
确定指令的编码
你需要考虑如下问题:
- 是否要建立一个新的Extension?参见指令手册21章Extending RISC-V
- 指令的格式是什么样的,包括几个读写寄存器和立即数?指令手册2.2章Base Instruction Formats
- 确定编码后,在
rocket-chip/src/main/scala/rocket/instructions.scala
中写入你自己的指令。其中能够确定的决定指令的部分用二进制表示,表示寄存器/立即数的非确定部分用"?"表示。这样硬件上就可以给你的指令分配了一个指令空间。 - 确定编码后,在
$RISCV/riscv-gnu-toolchain/riscv-binutils-gdb/opcodes/riscv-opc.c
中添加你的指令和格式,统一指令的不同格式(省略等)必须相邻。 - 在
$RISCV/riscv-gnu-toolchain/riscv-binutils-gdb/include/opcode/riscv-opc.h
中写入你的指令mask和经过mask后的match,以及格式。如果你看不懂mask和match,你应该考虑一下你是否适合干这行。 - 完成上述两条,你在汇编器中就给你的新指令占据了一个指令空间。分别编译硬件部分和软件部分确定你的编码空间是不冲突的。
确定控制线
一个指令在Rocke中被如何处理多数取决于decode阶段如何对这一指令解码,解码的结果是若干个控制线,这些解码表在rocket-chip/src/main/scala/rocket/idecode.scala
中。这一文件中先定义了所有的控制线,然后定义了每个指令对应的控制线的解码结果。其中部分是X
表示这个控制线可以是任意值。在Rocket Core中,id_ctrl/ex_ctrl/mem_ctrl/wb_ctrl就是控制线的集合,可以方便的调用任意阶段的指令解码后的控制线。
使用RoCC时的控制线设计
如果使用RoCC来处理新的指令,那么控制线基本不需要改动,因为控制线中的rocc位为真的话,Rocket Core直接将这个指令转发给RoCC处理。你只需要将读写寄存器相关的控制线确定好即可。
使用Rocket Core时的控制线设计
在Rocket Core中处理新指令时,最简单分辨你的新指令和旧指令的方法就是添加新的控制线。添加的控制线在所有旧指令中为假,在你的新指令中为真(亦或相反)。在各级流水处理时可以直接调用这条控制线来设计逻辑。
其余的控制线你仍然需要了解其意义,因为这决定了在Rocket Core中,如何读取和写入寄存器,是否调用加法器和乘法器等等,确保你认识每一条控制线和它的功能,再决定你的指令如何解码。
解码的逻辑是自动生成的,你无需关心。
硬件实现
RoCC的硬件实现
RoCC的硬件实现相对比较规范,你可以先看懂教程(忘了链接)。RoCC中有一个router,RoCC指令首先被发往这个router,router决定这一指令由哪个核心来进行处理。
简单的说,你需要实现一个自己的核心,专门处理自己的指令,并在RoCC router中识别并转发给你的核心。当计算完成后,结果返回给Rocket Core。
在这一实现中,你的实现方法是任意的,周期数也没有明确的要求,你可以随心所欲地设计自己的逻辑,尽快地得出结果并返回。
Rocket Core的硬件实现
如果你想在Rocket Core中实现,那么必须看懂rocket-chip/src/main/scala/rocket/rocket.scala
。如何实现你所需的逻辑,需要明确的设计,和推敲。必须不影响其他指令的正常执行。这一方法比较麻烦,没办法直接在文章中说清。
汇编和反汇编实现
在决定编码的阶段,我们说过了需要改动$RISCV/riscv-gnu-toolchain/riscv-binutils-gdb/opcodes/riscv-opc.c
和$RISCV/riscv-gnu-toolchain/riscv-binutils-gdb/include/opcode/riscv-opc.h
两个文件。改动正确之后,汇编器可以正确的识别新的指令并编码为二进制。幸运的是,objdump也直接可以识别新的指令。
反汇编器其实还有一个,就是spike-disas
,也就是在硬件模拟的output中负责处理反汇编的小程序。这个反汇编器如何改,不知道。
编译器如何使用这一指令
至此,你的新的指令已经可以被汇编器汇编,被硬件处理了,剩下的只剩如何在编译器中使用你的指令了。
然而这可能是所有步骤中最复杂的。
gcc中
gcc中可能反而比较简单,因为gcc中经常直接使用字符串作为编译器的汇编结果输出。
LLVM中
LLVM中相对麻烦一些,你需要知道如何在后端中加入这个新指令,并在MIR中设置好何时调用这一指令。
总之,在编译器中如何使用这个指令是十分复杂的。