FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

オープンソース・アウトオブオーダCPU NaxRiscvのドキュメントを読んでいく (3. 実行ユニットについて)

NaxRiscvのドキュメントを、改めて位置から読んでみることにしたいと思う。

spinalhdl.github.io

msyksphinz.hatenablog.com

msyksphinz.hatenablog.com


実行ユニット

実行ユニットの主な特徴は、それに依存する命令をウェイクアップする方法である。

  • 静的ウェイクアップ : 固定レイテンシ(ストールなし)のEUの場合、発行キューは静的ウェイクアップが可能である。これにより、complete-to-useの遅延を2サイクル短縮できる。
  • 動的ウェイクアップ : 可変レイテンシ(ストールあり)のEUの場合、EUは発行キューにウェイクアップ・コマンドを送信する責任がある。

以下は、2つのEU(静的ウェイクアップと動的ウェイクアップのそれぞれ1つ)が、依存する命令をウェイクアップさせるために、発行キューとやりとりする様子を示した図解である。

図は本論文より引用

カスタム命令

NaxRiscvにカスタム命令を追加するにはいくつかの方法がある。本節ではいくつかのデモを示す。

SIMD add命令

整数レジスタファイル上で動作するSIMD加算(4x8ビット加算器)を実装するプラグインを定義しよう。

このプラグインは、ALUプラグインの実装をよりシンプルにするExecutionUnitElementSimpleをベースとする。このようなプラグインは、指定の実行ユニット(ExecutionUnitBaseがホスト)を構成するために使用できる。

例えば、プラグイン構成は次のようになる。

plugins += new ExecutionUnitBase("ALU0")
plugins += new IntFormatPlugin("ALU0")
plugins += new SrcPlugin("ALU0", earlySrc = true)
plugins += new IntAluPlugin("ALU0", aluStage = 0)
plugins += new ShiftPlugin("ALU0" , aluStage = 0)
plugins += new ShiftPlugin("ALU0" , aluStage = 0)
plugins += new SimdAddPlugin("ALU0") // <- このプラグインを実装することになる

プラグインの実装

以下はこのプラグインをどのように実装するかの例である: (https://github.com/SpinalHDL/NaxRiscv/blob/d44ac3a3a3a4328cf2c654f9a46171511a798fae/src/main/scala/naxriscv/execute/SimdAddPlugin.scala#L36)

package naxriscv.execute

import spinal.core._
import spinal.lib._
import naxriscv._
import naxriscv.riscv._
import naxriscv.riscv.IntRegFile
import naxriscv.interfaces.{RS1, RS2}
import naxriscv.utilities.Plugin

    //このプラグインの例では SIMD_ADD と呼ばれる以下の処理を行う新しい命令を実装する
//
//RD : Regfile Destination, RS : Regfile Source
//RD( 7 downto  0) = RS1( 7 downto  0) + RS2( 7 downto  0)
//RD(16 downto  8) = RS1(16 downto  8) + RS2(16 downto  8)
//RD(23 downto 16) = RS1(23 downto 16) + RS2(23 downto 16)
//RD(31 downto 24) = RS1(31 downto 24) + RS2(31 downto 24)
//
//Instruction encoding :
//0000000----------000-----0001011   <- Custom0 func3=0 func7=0
//       |RS2||RS1|   |RD |
//
    //Note :  RS1, RS2, RD のビット位置はRISC-Vの仕様に準拠しており、このISAのすべての命令において共通である。


object SimdAddPlugin{
      //命令タイプとエンコーディングを定義する
  val ADD4 = IntRegFile.TypeR(M"0000000----------000-----0001011")
}

    //ExecutionUnitElementSimpleは、同じeuIdを持つExecutionUnitBaseによって提供されるパイプラインに結合されるベースクラスである。
    // これは、カスタム命令の実装を容易にするための多数のユーティリティを提供する。
    // ここでは、SIMD加算をレジスタファイルに追加するプラグインを実装する。
    // staticLatency=true は、このプラグインがパイプラインを停止させることは決してないことを指定し、その結果に依存する命令を静的に発行キューで実行できるようにする。
class SimdAddPlugin(val euId : String) extends ExecutionUnitElementSimple(euId, staticLatency = true) {
      //このプラグインが完全に組み合わせ回路で構成されると仮定する
  override def euWritebackAt = 0

      // セットアップコードは、プラグインが互いのことを指定するもので、手遅れになる前に設定する。
  override val setup = create early new Setup{
    //Let's assume we only support RV32 for now
    assert(Global.XLEN.get == 32)

            //現在のプラグインがADD4命令を実装することを実行ユニットベースに指定する。
    add(SimdAddPlugin.ADD4)
  }

  override val logic = create late new Logic{
    val process = new ExecuteArea(stageId = 0) {
      //Get the RISC-V RS1/RS2 values from the register file
      val rs1 = stage(eu(IntRegFile, RS1)).asUInt
      val rs2 = stage(eu(IntRegFile, RS2)).asUInt

      //Do some computation
      val rd = UInt(32 bits)
      rd( 7 downto  0) := rs1( 7 downto  0) + rs2( 7 downto  0)
      rd(16 downto  8) := rs1(16 downto  8) + rs2(16 downto  8)
      rd(23 downto 16) := rs1(23 downto 16) + rs2(23 downto 16)
      rd(31 downto 24) := rs1(31 downto 24) + rs2(31 downto 24)

      //Provide the computation value for the writeback
      wb.payload := rd.asBits
    }
  }
}

NaxRiscv の生成

次に、新しいプラグイン付きのNaxRiscvを生成するために、以下のAppを実行することができる : (https://github.com/SpinalHDL/NaxRiscv/blob/d44ac3a3a3a4328cf2c654f9a46171511a798fae/src/main/scala/naxriscv/execute/SimdAddPlugin.scala#L71)

object SimdAddNaxGen extends App{
  import naxriscv.compatibility._
  import naxriscv.utilities._

  def plugins = {
    //Get a default list of plugins
    val l = Config.plugins(
      withRdTime = false,
      aluCount    = 2,
      decodeCount = 2
    )
    //Add our plugin to the two ALUs
    l += new SimdAddPlugin("ALU0")
    l += new SimdAddPlugin("ALU1")
    l
  }

  //Create a SpinalHDL configuration that will be used to generate the hardware
  val spinalConfig = SpinalConfig(inlineRom = true)
  spinalConfig.addTransformationPhase(new MemReadDuringWriteHazardPhase)
  spinalConfig.addTransformationPhase(new MultiPortWritesSymplifier)

  //Generate the NaxRiscv verilog file
  val report = spinalConfig.generateVerilog(new NaxRiscv(xlen = 32, plugins))

  //Generate some C header files used by the verilator testbench to connect to the DUT
  report.toplevel.framework.getService[DocPlugin].genC()
}

このAppを実行するために、NaxRiscvのディレクトリで以下を実行する:

sbt "runMain naxriscv.execute.SimdAddNaxGen"

ソフトウェアテスト

次に、アセンブリテストコードを書こう : (https://github.com/SpinalHDL/NaxSoftware/tree/849679c70b238ceee021bdfd18eb2e9809e7bdd0/baremetal/simdAdd)

.globl _start
_start:

#include "../../driver/riscv_asm.h"
#include "../../driver/sim_asm.h"
#include "../../driver/custom_asm.h"

    //Test 1
    li x1, 0x01234567
    li x2, 0x01FF01FF
    opcode_R(CUSTOM0, 0x0, 0x00, x3, x1, x2) //x3 = ADD4(x1, x2)

    //Print result value
    li x4, PUT_HEX
    sw x3, 0(x4)

    //Check result
    li x5, 0x02224666
    bne x3, x5, fail

    j pass

pass:
    j pass
fail:
    j fail

以下のようにしてコンパイルする

make clean rv32im

シミュレーション

src/test/cpp/naxriscv のシミュレーションを実行する (最初にreadmeに書いてあるセットアップを行う必要がある)

make clean compile
./obj_dir/VNaxRiscv --load-elf ../../../../ext/NaxSoftware/baremetal/simdAdd/build/rv32im/simdAdd.elf --spike-disable --pass-symbol pass --fail-symbol fail --trace

シェルに2224666と表示されれば成功である :D

Conclusion

したがって、この例では、追加のデコードの指定方法や、マルチサイクルALUの定義方法については紹介していない。(TODO)。 しかし、IntAluPlugin、ShiftPlugin、DivPlugin、MulPlugin、BranchPluginでは、同じExecutionUnitElementSimpleベースクラスを使用して、それらの処理を行っている。

また、ExecutionUnitElementSimpleベースクラスを使用する必要はなく、LoadPlugin、StorePlugin、EnvCallPluginのように、より基本的なアクセスも可能である。

ハードコアな方法

同じ命令の例だが、ExecutionUnitElementSimpleの機能を使用せずに実装した例を以下に示す: (https://github.com/SpinalHDL/NaxRiscv/blob/72b80e3345ecc3a25ca913f2b741e919a3f4c970/src/main/scala/naxriscv/execute/SimdAddPlugin.scala#L100)

object SimdAddRawPlugin{
  val SEL = Stageable(Bool()) //Will be used to identify when we are executing a ADD4
  val ADD4 = IntRegFile.TypeR(M"0000000----------000-----0001011")
}

class SimdAddRawPlugin(euId : String) extends Plugin {
  import SimdAddRawPlugin._
  val setup = create early new Area{
    val eu = findService[ExecutionUnitBase](_.euId == euId)
    eu.retain() //We don't want the EU to generate itself before we are done with it

    //Specify all the ADD4 requirements
    eu.addMicroOp(ADD4)
    eu.setCompletion(ADD4, stageId = 0)
    eu.setStaticWake(ADD4, stageId = 0)
    eu.setDecodingDefault(SEL, False)
    eu.addDecoding(ADD4, SEL, True)

    //IntFormatPlugin provide a shared point to write into the register file with some optional carry extensions
    val intFormat = findService[IntFormatPlugin](_.euId == euId)
    val writeback = intFormat.access(stageId = 0, writeLatency = 0)
  }

  val logic = create late new Area{
    val eu = setup.eu
    val writeback = setup.writeback
    val stage = eu.getExecute(stageId = 0)

    //Get the RISC-V RS1/RS2 values from the register file
    val rs1 = stage(eu(IntRegFile, RS1)).asUInt
    val rs2 = stage(eu(IntRegFile, RS2)).asUInt

    //Do some computation
    val rd = UInt(32 bits)
    rd( 7 downto  0) := rs1( 7 downto  0) + rs2( 7 downto  0)
    rd(16 downto  8) := rs1(16 downto  8) + rs2(16 downto  8)
    rd(23 downto 16) := rs1(23 downto 16) + rs2(23 downto 16)
    rd(31 downto 24) := rs1(31 downto 24) + rs2(31 downto 24)

    //Provide the computation value for the writeback
    writeback.valid   := stage(SEL)
    writeback.payload := rd.asBits

    //Now the EU has every requirements set for the generation (from this plugin perspective)
    eu.release()
  }
}