前一文章讲述了译码逻辑的实现方式,代码没有具体描述任何一条指令。通过构建转换架构,底层与硬件逻辑交互使用的是基于Masked描述的映射关系,使用输入指令与数组内的Masked进行比较匹配,再索引相关的译码输出信号,具体的硬件逻辑通过调用object Symplify实现。
该代码架构的上层,则是提供了基于add、addDefault函数方法的接口,可添加指令和相关译码信号的默认值。本文通过几个例子,讲讲VexRiscv是如何实现指令添加的,添加之后,又是如何基于译码信号进行执行的,最后看看是如何添加自定义指令的。
涉及到的代码如下:
vexriscv/plugin/IntAluPlugin.scala
vexriscv/plugin/RegFilePlugin.scala
vexriscv/plugin/ShiftPlugin.scala
vexriscv/plugin/SrcPlugin.scala
vexriscv/plugin/CustomInstruction.scala
该代码实现了一些非常简单的指令,包括加减法、比较、逻辑运算等操作。如下为添加指令的代码,调用了DecoderSimplePlugin提供的add函数方法。由于需同时添加多个指令,使用了List列表。
在列表内,每一行表示一条指令,第一个'->'符号左侧为待添加指令,如ADD/SUB,实际上是定义为MaskedLiteral类型的变量(使用函数定义的);如前文所述,'->'符号右侧为译码输出信号列表及其对应的值,该列表内仅需添加该指令需关注的值,无需将所有译码信号囊括在内。再关注图中的nonImmediateActions、immediateActions信号,实际上是前面统一定义的信号列表,同样是译码信号及其对应的译码输出值,如下所示。
至此,已经为译码模块添加了多条指令,如下以ADD指令为例,看一下加入了哪些译码输出信号。实际上,译码信号可基于指令做一些自定义,并没有统一的译码输出信号规则,只需要基于Stageable定义信号。
- SRC1_CTRL/SRC2_CTRL信号描述指令操作数的源头,其信号值是以枚举类型Enum描述的,RS表示来自寄存器RegFile,也即ADD指令内rs1和rs2域段。
- REGFILE_WRITE_VALID表示指令结果需写回寄存器,指令的rd域段是有效的。- BYPASSABLE_xxx,在指定的流水线内,该指令结果已经是有效的,用于处理Hazard相关的逻辑。
- RS1_USE/RS2_USE,描述是否需使用rs1/rs2域段的值。
- ALU_CTRL,描述ALU执行的操作类型,同样是使用Enum枚举类型描述,ADD_SUB指示当前指令需实现加减法操作。
- SRC_USE_SUB_LESS,指示当前操作需实现减法还是加法,如下为False,指示当前需实现加法。
完成指令添加后,就是如何实现指令的动作,也即是EXCUTE,相关代码如下所示。excute plug表示在excute阶段插入逻辑代码;然后是生成AND/OR/XOR的运算结果bitwise;最后是生成REGFILE_WRITE_DATA信号,此处使用了srcPlugin的计算结果,基于input获得其信号值。该位置使用了其余plugin的信号,如操作数、比较结果和加减结果,看起来非常简洁,但实现的指令数还挺多的。这里使用到了stage的input,output和insert方法。其中insert方法表示首次添加该信号,并对其进行赋值;input方法为当时流水线需使用该信号,该信号可能是其余流水线添加的,或者是当前流水线添加的;output方法是对insert之后的信号进行修改,因此必须附带when条件。
注意到逻辑运算的操作数SRC1和SRC2,比较指令的结果和加减法的结果,均是基于input方法获得的,其实这几个信号均是在SrcPlugin加入的。
该代码基于操作数实现了一些简单运算,如比较、加减法等等,还有操作数的生成,用于其余指令的执行阶段。如下代码用于生成SRC1和SRC2操作数,有译码信号SRC1_CTRL、SRC2_CTRL控制生成操作数。
然后是基于SRC1和SRC2两个操作数,做加减法运算和比较运算,代码都比较简单,使用insert方法将结果加入流水线。
这两个位置就可以体现一些配置化的特性了,关注上两个图中的第一行代码,分别基于executeInsertion和decodeAddSub定义insertionStage和addSubStage,也就是说仅仅基于这两个参数,就可以轻易地将‘{}’内的代码挪到另外一个流水线内。如果在excute阶段的时序比较紧张,而decode阶段时间还有余量,可以简单修改配置,将excute阶段的部分逻辑挪至decode阶段。设想一下,如果通过Verilog实现,该修改多少代码?是否还能够做到如此简单的配置?
在SrcPlugin代码内,操作数的生成,使用了RS1和RS2两个信号,实际上是基于指令内的rs1和rs2读取寄存器获得的,相关代码在RegFilePlugin。该Plugin实现了对寄存器的读取和写回,同样可以做一些配置。在这里可以关注一下寄存器写回的代码,如下所示,核心代码就是如下标注的3行,基于译码输出信号生成写入信号,读取指令的rd域段作为写入地址,写入数据为REGFILE_WRITE_DATA,是否看起来非常简洁?后续几行代码都是对x0寄存器的特殊处理。
再来看一下移位指令的实现,完全相同的套路,定义译码输出信号列表,使用add方法添加指令。
然后是excute和生成写回数据,其中生成写回数据可基于配置做一些调整。将操作数的高低位进行反转,左移操作可变为右移操作进行处理。
最后看一下如何实现自定义指令,demo目录提供了示例代码。添加的指令为SIMD_ADD,32bit操作数分为4个8bit的数据,分别进行加法运算。
执行指令内容,基于Stage的input/output两个方法,指令执行相关的代码变得非常简单。
该自定义指令比较简单,但基本说明清楚该如何添加自定义指令。如有兴趣,可关注plugin目录下的AesPlugin.scala,提供了用于加速AES加解密的自定义指令,可在21个cycles完成一次AES round。