オープンソース・アウトオブオーダCPU NaxRiscvを概観する (3. SpinalHDLの特殊記述について) - FPGA開発日記

FPGA開発日記

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

オープンソース・アウトオブオーダCPU NaxRiscvを概観する (3. SpinalHDLの特殊記述について)

msyksphinz.hatenablog.com

msyksphinz.hatenablog.com

NaxRiscvのVerilogコードだが、やっぱり何が書いてあるのかさっぱりわからない。

とりあえずSpinalHDLのチュートリアルを読んで、基本的なところを確認していこうと思う。以下のページがよさそうだ。

spinalhdl.github.io


抽象化 / HDL

NaxRiscvの実装では、SpinalHDL(Scalaのハードウェア記述ライブラリ)を使用する際に利用可能な多くのパラダイムを活用しています。

フレームワーク

NaxRiscvのトップレベルは、プラグインのリストをスケジューリングできるフレームワークを持つ空のコンポーネントであることがほとんどです。 フレームワーク自体はハードウェアを作成しません。

以下はNaxRiscvのトップレベルです:

class NaxRiscv(xlen : Int,
                           plugins : Seq[Plugin]) extends Component{
  NaxScope.create(xlen = xlen) //Will come back on that line later
  val framework = new Framework(plugins)
}

設計がどの程度プラグイン間で分担されているかを概観するために、1つの機能CPUに対するプラグインのリストを以下に示します:

val plugins = ArrayBuffer[Plugin]()
plugins += new DocPlugin()
plugins += new MmuPlugin(
  spec    = MmuSpec.sv32,
  ioRange = ioRange,
  fetchRange = fetchRange
)

//FETCH
plugins += new FetchPlugin()
plugins += new PcPlugin(resetVector)
plugins += new FetchCachePlugin(
  cacheSize = 4096*4,
  wayCount = 4,
  injectionAt = 2,
  fetchDataWidth = 64,
  memDataWidth = 64,
  reducedBankWidth = false,
  hitsWithTranslationWays = true,
  translationStorageParameter = MmuStorageParameter(
    levels   = List(
      MmuStorageLevel(
        id    = 0,
        ways  = 4,
        depth = 32
      ),
      MmuStorageLevel(
        id    = 1,
        ways  = 2,
        depth = 32
      )
    ),
    priority = 0
  ),
  translationPortParameter  = MmuPortParameter(
    readAt = 1,
    hitsAt = 1,
    ctrlAt = 1,
    rspAt  = 1
  )
)
plugins += new AlignerPlugin(
  decodeCount = 2,
  inputAt = 2
)
/* ... 以下略 ... */

これらのプラグインはそれぞれ、以下の選択肢がある:

  • 他のプラグインが使用するサービスの実装(例:ジャンプインターフェイスの提供、リスケジューリングインターフェイスの提供、パイプラインスケルトンの提供)
  • 他のプラグインの機能を使用する
  • ハードウェアの作成
  • 初期タスクの作成(プラグイン間のセットアップに使用)
  • 後期タスクの作成(必要なハードウェアを作成するために一般的に使われる)

プラグインタスク

ここでは、2つのタスク(セットアップ/ロジック)を作成するダミープラグインのインスタンスを示します:

class DummyPlugin extends Plugin {
  val setup = create early new Area {
    // ここで他のプラグインをセットアップすることができる
    //このコードは常に、すべてのlate taskの前で実行される
  }

  val logic = create late new Area {
    //ここで(例えば)ハードウェアを生成することができる
    //このコードは常に、すべてのearly taskの後に開始される
  }
}

create earlycreate late は、Frameworkクラスによってスケジューリングされた新しいスレッドでコードを実行することに注意してください。

service の定義

例えば、他のプラグインにハードウェアジャンプインターフェースを提供するJumpServiceは、次のように定義できます:

// エラボレーション時のソフトウェア・インタフェース
trait JumpService extends Service{
  def createJumpInterface(priority : Int) : Flow[JumpCmd]
}

// インタフェースのハードウェア・ペイロード
case class JumpCmd(pcWidth : Int) extends Bundle{
  val pc = UInt(pcWidth bits)
}

serviceの実装

先に示したJumpServiceを例にとると、PcPluginは次のように実装できます:

case class JumpSpec(interface :  Flow[JumpCmd], priority : Int)
class PcPlugin() extends Plugin with JumpService{
  val jumpsSpec = ArrayBuffer[JumpSpec]()

  override def createJumpInterface(priority : Int): Flow[JumpCmd] = {
    val spec = JumpSpec(Flow(JumpCmd(32)), priority)
    jumpsSpec += spec
    return spec.interface
  }

  val logic = create late new Area{
    // ここで、PCロジックを実装し、jumpsSpecインターフェイスを管理する。
        val pc = Reg(UInt(32 bits))
        val sortedJumps = jumpsSpec.sortBy(_.priority) //Lower priority first
        for(jump <- sortedJumps){
              when(jump.interface.valid){
                pc := jump.interface.pc
              }
        }
        ...
  }
}

サービスの使い方

別のプラグインは、以下のようにしてこのサービスを取得し、使用することができます:

class AnotherPlugin() extends Plugin {
  val setup = create early new Area {
    val jump = getService[JumpService].createJumpInterface(42)
  }

  val logic = create late new Area {
    setup.jump.valid := ???
    setup.jump.pc := ???
  }
}

サービスパイプラインの定義

プラグインによっては、パイプラインのスケルトンを作成し、それを他のプラグインが入力することもできます。 例えば:

class FetchPlugin() extends Plugin with LockedImpl {
  val pipeline = create early new Pipeline{
    val stagesCount = 2
    val stages = Array.fill(stagesCount)(newStage())

    import spinal.lib.pipeline.Connection._
    //Connect every stage together
    for((m, s) <- (stages.dropRight(1), stages.tail).zipped){
      connect(m, s)(M2S())
    }
  }

  val logic = create late new Area{
    lock.await() // 他のプラグインが、パイプラインの段階で望むものをすべて指定するまで、このブロッキングを行うことを許可する。
    pipeline.build()
  }
}

サービス・パイプラインの使用法

例えば、PcPluginはPC値をフェッチパイプラインに導入したいと思うだろう:

object PcPlugin extends AreaObject{
  val FETCH_PC = Stageable(UInt(32 bits))  // FETCH_PC信号がパイプラインを通して使用可能であるという概念を定義する。
}

class PcPlugin() extends Plugin with ...{

  val setup = create early new Area{
    getService[FetchPlugin].retain() // 関連するアクセスをすべて作成するまで、FetchPluginロジックのタスクを保持する必要がある。
  }

  val logic = create late new Area{
    val fetch = getService[FetchPlugin]
    val firstStage = fetch.pipeline.stages(0)

    firstStage(PcPlugin.FETCH_PC) := ???   // パイプラインのfirstStageでFETCH_PCの値を代入する。他のプラグインがダウンストリームでアクセスするかもしれない。
    fetch.release()
  }
}

実行ユニット

実行ユニットの実装は、このコンセプトのもう一つの実用的な使い方です。 一意の実行ユニット識別子を持つ新しいExecutionUnitBaseを作成することで、実行ユニットを生成することができます:

plugins += new ExecutionUnitBase("EU0")

次に、同じ識別子を持つ新しいExecutionUnitElementSimpleを追加することで、その実行ユニットを生成することができます:

plugins += new SrcPlugin("EU0")
plugins += new IntAluPlugin("EU0")
plugins += new ShiftPlugin("EU0")

以下は、以下の演算を操作するプラグインです:

  • mul/div
  • jump/branches
  • load/store
  • CSR accesses
  • ebreak/ecall/mret/wfi
plugins += new ExecutionUnitBase("EU1", writebackCountMax = 1)
plugins += new SrcPlugin("EU1")
plugins += new MulPlugin("EU1", writebackAt = 2, staticLatency = false)
plugins += new DivPlugin("EU1", writebackAt = 2)
plugins += new BranchPlugin("EU1", writebackAt = 2, staticLatency = false)
plugins += new LoadPlugin("EU1")
plugins += new StorePlugin("EU1")
plugins += new CsrAccessPlugin("EU1")(
  decodeAt = 0,
  readAt = 1,
  writeAt = 2,
  writebackAt = 2,
  staticLatency = false
)
plugins += new EnvCallPlugin("EU1")(rescheduleAt = 2)

ShiftPlugin

以下は、ExecutionUnitElementSimpleプラグインの例としてのShiftPluginです:

object ShiftPlugin extends AreaObject {
  val SIGNED = Stageable(Bool())
  val LEFT = Stageable(Bool())
}

class ShiftPlugin(euId : String, staticLatency : Boolean = true, aluStage : Int = 0) extends ExecutionUnitElementSimple(euId, staticLatency) {
  import ShiftPlugin._

  override def euWritebackAt = aluStage

  override val setup = create early new Setup{
    import SrcKeys._

    add(Rvi.SLL , List(SRC1.RF, SRC2.RF), DecodeList(LEFT -> True,  SIGNED -> False))
    add(Rvi.SRL , List(SRC1.RF, SRC2.RF), DecodeList(LEFT -> False, SIGNED -> False))
    add(Rvi.SRA , List(SRC1.RF, SRC2.RF), DecodeList(LEFT -> False, SIGNED -> True))
    add(Rvi.SLLI, List(SRC1.RF, SRC2.I ), DecodeList(LEFT -> True , SIGNED -> False))
    add(Rvi.SRLI, List(SRC1.RF, SRC2.I ), DecodeList(LEFT -> False, SIGNED -> False))
    add(Rvi.SRAI, List(SRC1.RF, SRC2.I ), DecodeList(LEFT -> False, SIGNED -> True))
  }

  override val logic = create late new Logic{
    val process = new ExecuteArea(aluStage) {
      import stage._
      val ss = SrcStageables

      assert(Global.XLEN.get == 32)
      val amplitude  = ss.SRC2(4 downto 0).asUInt
      val reversed   = Mux[SInt](LEFT, ss.SRC1.reversed, ss.SRC1)
      val shifted = (S((SIGNED & ss.SRC1.msb) ## reversed) >> amplitude).resize(Global.XLEN bits)
      val patched = LEFT ? shifted.reversed | shifted

      wb.payload := B(patched)
    }
  }
}