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()
  }
}

オープンソース・アウトオブオーダCPU NaxRiscvを概観する (12. PcPluginの実装解析)

PcPluginの実装の解析

github.com

とりあえずまずはChatGPTにこのソースコードを解説してもらおうと思う。

- ジャンプロジックの設定:

ジャンプロジックでは、登録されたジャンプ要求を優先度に応じてソートし、優先度が高いものから順に評価します。
ジャンプ命令が有効であれば、それに応じてプログラムカウンタが更新されます。これは、異なるジャンプグループを適切に扱うことで複数のジャンプ要求をまとめて処理します。

- プログラムカウンタの初期化とインクリメントロジック:

初期化ロジックでは、システムが完全に起動するまでのサイクル数を計算し、その間はプログラムカウンタの更新を遅延させます。
PCの計算では、ジャンプ命令による更新がない場合は通常のインクリメントを行います。ジャンプ命令が発行された場合は、プログラムカウンタをジャンプ先のアドレスに設定し直します。

最初のジャンプロジックの設定は以下のソースコードの部分だと思われる:

    val jump = new Area {
      val sortedByStage = jumpsSpec.sortWith(_.priority > _.priority)
      val valids = sortedByStage.map(_.interface.valid)
      val cmds = sortedByStage.map(_.interface.payload)
      val oh = OHMasking.firstV2(valids.asBits)

      val grouped = sortedByStage.groupByLinked(_.aggregationPriority).toList.sortBy(_._1).map(_._2)
      var target = PC()
      for(group <- grouped){
        val indexes = group.map(e => sortedByStage.indexOf(e))
        val goh = indexes.map(i => oh(i))
        val mux = OhMux.or(goh, group.map(_.interface.pc))
        if(group == grouped.head) target := mux else when(goh.orR){
          KeepAttribute(target)
          target \= mux
        }
      }

      val pcLoad = Flow(JumpCmd(pcWidth = widthOf(PC)))
      pcLoad.valid := jumpsSpec.map(_.interface.valid).orR
      pcLoad.pc    := target
    }

これが以下のようにVerilogに変換されている。もう何が何だか分からない。

  reg        [39:0]   PcPlugin_logic_jump_target_1;
  wire       [5:0]    _zz_PcPlugin_logic_jump_oh;
  wire                _zz_PcPlugin_logic_jump_oh_1;
  wire                _zz_PcPlugin_logic_jump_oh_2;
  wire                _zz_PcPlugin_logic_jump_oh_3;
  wire                _zz_PcPlugin_logic_jump_oh_4;
  wire                _zz_PcPlugin_logic_jump_oh_5;
  reg        [5:0]    _zz_PcPlugin_logic_jump_oh_6;
  wire       [5:0]    PcPlugin_logic_jump_oh;
  (* keep , syn_keep *) wire       [39:0]   PcPlugin_logic_jump_target /* synthesis syn_keep = 1 */ ;
  wire                _zz_PcPlugin_logic_jump_target_1;
  wire                PcPlugin_logic_jump_pcLoad_valid;
  wire       [39:0]   PcPlugin_logic_jump_pcLoad_payload_pc;
    PcPlugin_logic_jump_target_1 = PcPlugin_logic_jump_target;
      PcPlugin_logic_jump_target_1 = (_zz_PcPlugin_logic_jump_target_1 ? BtbPlugin_setup_btbJump_payload_pc : 40'h0000000000);
  assign _zz_PcPlugin_logic_jump_oh = {BtbPlugin_setup_btbJump_valid,{FetchCachePlugin_setup_redoJump_valid,{AlignerPlugin_setup_sequenceJump_valid,{DecoderPredictionPlugin_setup_decodeJump_valid,{CommitPlugin_setup_jump_valid,PrivilegedPlugin_setup_jump_valid}}}}};
  assign _zz_PcPlugin_logic_jump_oh_1 = _zz_PcPlugin_logic_jump_oh[0];
  assign _zz_PcPlugin_logic_jump_oh_2 = _zz_PcPlugin_logic_jump_oh[1];
  assign _zz_PcPlugin_logic_jump_oh_3 = _zz_PcPlugin_logic_jump_oh[2];
  assign _zz_PcPlugin_logic_jump_oh_4 = _zz_PcPlugin_logic_jump_oh[3];
  assign _zz_PcPlugin_logic_jump_oh_5 = _zz_PcPlugin_logic_jump_oh[4];
    _zz_PcPlugin_logic_jump_oh_6[0] = (_zz_PcPlugin_logic_jump_oh_1 && (! 1'b0));
    _zz_PcPlugin_logic_jump_oh_6[1] = (_zz_PcPlugin_logic_jump_oh_2 && (! _zz_PcPlugin_logic_jump_oh_1));
    _zz_PcPlugin_logic_jump_oh_6[2] = (_zz_PcPlugin_logic_jump_oh_3 && (! (|{_zz_PcPlugin_logic_jump_oh_2,_zz_PcPlugin_logic_jump_oh_1})));
    _zz_PcPlugin_logic_jump_oh_6[3] = (_zz_PcPlugin_logic_jump_oh_4 && (! (|{_zz_PcPlugin_logic_jump_oh_3,{_zz_PcPlugin_logic_jump_oh_2,_zz_PcPlugin_logic_jump_oh_1}})));
    _zz_PcPlugin_logic_jump_oh_6[4] = (_zz_PcPlugin_logic_jump_oh_5 && (! (|{_zz_PcPlugin_logic_jump_oh_4,{_zz_PcPlugin_logic_jump_oh_3,{_zz_PcPlugin_logic_jump_oh_2,_zz_PcPlugin_logic_jump_oh_1}}})));
    _zz_PcPlugin_logic_jump_oh_6[5] = (_zz_PcPlugin_logic_jump_oh[5] && (! (|{_zz_PcPlugin_logic_jump_oh_5,{_zz_PcPlugin_logic_jump_oh_4,{_zz_PcPlugin_logic_jump_oh_3,{_zz_PcPlugin_logic_jump_oh_2,_zz_PcPlugin_logic_jump_oh_1}}}})));
  assign PcPlugin_logic_jump_oh = _zz_PcPlugin_logic_jump_oh_6;
  assign PcPlugin_logic_jump_target = ((((PcPlugin_logic_jump_oh[0] ? PrivilegedPlugin_setup_jump_payload_pc : 40'h0000000000) | (PcPlugin_logic_jump_oh[1] ? CommitPlugin_setup_jump_payload_pc : 40'h0000000000)) | ((PcPlugin_logic_jump_oh[2] ? DecoderPredictionPlugin_setup_decodeJump_payload_pc : 40'h0000000000) | (PcPlugin_logic_jump_oh[3] ? AlignerPlugin_setup_sequenceJump_payload_pc : 40'h0000000000))) | (PcPlugin_logic_jump_oh[4] ? FetchCachePlugin_setup_redoJump_payload_pc : 40'h0000000000));
  assign _zz_PcPlugin_logic_jump_target_1 = PcPlugin_logic_jump_oh[5];
  assign when_PcPlugin_l55 = (|_zz_PcPlugin_logic_jump_target_1);
  assign PcPlugin_logic_jump_pcLoad_valid = (|{PrivilegedPlugin_setup_jump_valid,{CommitPlugin_setup_jump_valid,{BtbPlugin_setup_btbJump_valid,{DecoderPredictionPlugin_setup_decodeJump_valid,{AlignerPlugin_setup_sequenceJump_valid,FetchCachePlugin_setup_redoJump_valid}}}}});
  assign PcPlugin_logic_jump_pcLoad_payload_pc = PcPlugin_logic_jump_target_1;
    if(PcPlugin_logic_jump_pcLoad_valid) begin
    if(PcPlugin_logic_jump_pcLoad_valid) begin
      PcPlugin_logic_fetchPc_pc = PcPlugin_logic_jump_pcLoad_payload_pc;
    if(PcPlugin_logic_jump_pcLoad_valid) begin

例えば jump_oh の実装は以下の感じでめっちゃややこしくなっている。

  assign _zz_PcPlugin_logic_jump_oh = {BtbPlugin_setup_btbJump_valid,{FetchCachePlugin_setup_redoJump_valid,{AlignerPlugin_setup_sequenceJump_valid,{DecoderPredictionPlugin_setup_decodeJump_valid,{CommitPlugin_setup_jump_valid,PrivilegedPlugin_setup_jump_valid}}}}};
  assign _zz_PcPlugin_logic_jump_oh_1 = _zz_PcPlugin_logic_jump_oh[0];
  assign _zz_PcPlugin_logic_jump_oh_2 = _zz_PcPlugin_logic_jump_oh[1];
  assign _zz_PcPlugin_logic_jump_oh_3 = _zz_PcPlugin_logic_jump_oh[2];
  assign _zz_PcPlugin_logic_jump_oh_4 = _zz_PcPlugin_logic_jump_oh[3];
  assign _zz_PcPlugin_logic_jump_oh_5 = _zz_PcPlugin_logic_jump_oh[4];
    _zz_PcPlugin_logic_jump_oh_6[0] = (_zz_PcPlugin_logic_jump_oh_1 && (! 1'b0));
    _zz_PcPlugin_logic_jump_oh_6[1] = (_zz_PcPlugin_logic_jump_oh_2 && (! _zz_PcPlugin_logic_jump_oh_1));
    _zz_PcPlugin_logic_jump_oh_6[2] = (_zz_PcPlugin_logic_jump_oh_3 && (! (|{_zz_PcPlugin_logic_jump_oh_2,_zz_PcPlugin_logic_jump_oh_1})));
    _zz_PcPlugin_logic_jump_oh_6[3] = (_zz_PcPlugin_logic_jump_oh_4 && (! (|{_zz_PcPlugin_logic_jump_oh_3,{_zz_PcPlugin_logic_jump_oh_2,_zz_PcPlugin_logic_jump_oh_1}})));
    _zz_PcPlugin_logic_jump_oh_6[4] = (_zz_PcPlugin_logic_jump_oh_5 && (! (|{_zz_PcPlugin_logic_jump_oh_4,{_zz_PcPlugin_logic_jump_oh_3,{_zz_PcPlugin_logic_jump_oh_2,_zz_PcPlugin_logic_jump_oh_1}}})));
    _zz_PcPlugin_logic_jump_oh_6[5] = (_zz_PcPlugin_logic_jump_oh[5] && (! (|{_zz_PcPlugin_logic_jump_oh_5,{_zz_PcPlugin_logic_jump_oh_4,{_zz_PcPlugin_logic_jump_oh_3,{_zz_PcPlugin_logic_jump_oh_2,_zz_PcPlugin_logic_jump_oh_1}}}})));
  assign PcPlugin_logic_jump_oh = _zz_PcPlugin_logic_jump_oh_6;
  assign PcPlugin_logic_jump_target = ((((PcPlugin_logic_jump_oh[0] ? PrivilegedPlugin_setup_jump_payload_pc : 40'h0000000000) | (PcPlugin_logic_jump_oh[1] ? CommitPlugin_setup_jump_payload_pc : 40'h0000000000)) | ((PcPlugin_logic_jump_oh[2] ? DecoderPredictionPlugin_setup_decodeJump_payload_pc : 40'h0000000000) | (PcPlugin_logic_jump_oh[3] ? AlignerPlugin_setup_sequenceJump_payload_pc : 40'h0000000000))) | (PcPlugin_logic_jump_oh[4] ? FetchCachePlugin_setup_redoJump_payload_pc : 40'h0000000000));

次のfetchPCがPCのインクリメントの部分だと思われる。

    val fetchPc = new Area{
      //PC calculation without Jump
      val output = Stream(PC)
      val pcReg = Reg(PC) init(resetVector) addAttribute(Verilator.public)
      val correction = False
      val correctionReg = RegInit(False) setWhen(correction) clearWhen(output.fire)
      val corrected = correction || correctionReg
      val pcRegPropagate = False
      val inc = RegInit(False) clearWhen(correction || pcRegPropagate) setWhen(output.fire) clearWhen(!output.valid && output.ready)
      val pc = pcReg + (U(inc) << sliceRange.high+1)


      val flushed = False

      when(inc) {
        pc(sliceRange) := 0
      }

      when(jump.pcLoad.valid) {
        correction := True
        pc := jump.pcLoad.pc
        flushed := True
      }

      when(init.booted && (output.ready || correction || pcRegPropagate)){
        pcReg := pc
      }

      pc(0) := False
      if(!RVC) pc(1) := False

      val fetcherHalt = False
      output.valid := !fetcherHalt && init.booted
      output.payload := pc
    }

これも以下のようにVerilogに変換されていた。

  assign _zz_PcPlugin_logic_fetchPc_pc_1 = ({3'd0,PcPlugin_logic_fetchPc_inc} <<< 2'd3);
  assign _zz_PcPlugin_logic_fetchPc_pc = {36'd0, _zz_PcPlugin_logic_fetchPc_pc_1};
    PcPlugin_logic_fetchPc_correction = 1'b0;
      PcPlugin_logic_fetchPc_correction = 1'b1;
  assign PcPlugin_logic_fetchPc_output_fire = (PcPlugin_logic_fetchPc_output_valid && PcPlugin_logic_fetchPc_output_ready);
  assign PcPlugin_logic_fetchPc_corrected = (PcPlugin_logic_fetchPc_correction || PcPlugin_logic_fetchPc_correctionReg);
  assign PcPlugin_logic_fetchPc_pcRegPropagate = 1'b0;
  assign when_PcPlugin_l82 = (PcPlugin_logic_fetchPc_correction || PcPlugin_logic_fetchPc_pcRegPropagate);
  assign when_PcPlugin_l82_1 = ((! PcPlugin_logic_fetchPc_output_valid) && PcPlugin_logic_fetchPc_output_ready);
    PcPlugin_logic_fetchPc_pc = (PcPlugin_logic_fetchPc_pcReg + _zz_PcPlugin_logic_fetchPc_pc);
    if(PcPlugin_logic_fetchPc_inc) begin
      PcPlugin_logic_fetchPc_pc[2 : 2] = 1'b0;
      PcPlugin_logic_fetchPc_pc = PcPlugin_logic_jump_pcLoad_payload_pc;
    PcPlugin_logic_fetchPc_pc[0] = 1'b0;
    PcPlugin_logic_fetchPc_pc[1] = 1'b0;
    PcPlugin_logic_fetchPc_flushed = 1'b0;
      PcPlugin_logic_fetchPc_flushed = 1'b1;
  assign when_PcPlugin_l98 = (PcPlugin_logic_init_booted && ((PcPlugin_logic_fetchPc_output_ready || PcPlugin_logic_fetchPc_correction) || PcPlugin_logic_fetchPc_pcRegPropagate));
  assign PcPlugin_logic_fetchPc_fetcherHalt = 1'b0;
  assign PcPlugin_logic_fetchPc_output_valid = ((! PcPlugin_logic_fetchPc_fetcherHalt) && PcPlugin_logic_init_booted);
  assign PcPlugin_logic_fetchPc_output_payload = PcPlugin_logic_fetchPc_pc;
  assign PcPlugin_logic_fetchPc_output_ready = FetchPlugin_stages_0_ready;
  assign FetchPlugin_stages_0_valid = PcPlugin_logic_fetchPc_output_valid;
  assign FetchPlugin_stages_0_Fetch_FETCH_PC = PcPlugin_logic_fetchPc_output_payload;
      PcPlugin_logic_fetchPc_pcReg <= 40'h0080000000;
      PcPlugin_logic_fetchPc_correctionReg <= 1'b0;
      PcPlugin_logic_fetchPc_inc <= 1'b0;
      if(PcPlugin_logic_fetchPc_correction) begin
        PcPlugin_logic_fetchPc_correctionReg <= 1'b1;
      if(PcPlugin_logic_fetchPc_output_fire) begin
        PcPlugin_logic_fetchPc_correctionReg <= 1'b0;
        PcPlugin_logic_fetchPc_inc <= 1'b0;
      if(PcPlugin_logic_fetchPc_output_fire) begin
        PcPlugin_logic_fetchPc_inc <= 1'b1;
        PcPlugin_logic_fetchPc_inc <= 1'b0;
        PcPlugin_logic_fetchPc_pcReg <= PcPlugin_logic_fetchPc_pc;

オープンソース・アウトオブオーダCPU NaxRiscvのドキュメントを読んでいく (2. )

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

spinalhdl.github.io

msyksphinz.hatenablog.com


Frontend

デコーダ

特になし。

物理レジスタ割り当て

物理レジスタ割り当ては、割り当てられていない物理レジスタのインデックスを含む循環バッファを実装することで行われる。 これは、分散RAMを搭載したFPGAにうまく適合する。

アーキテクチャ・レジスタから物理レジスタ

アーキテクチャ・レジスタファイルから物理レジスタへの変換は、以下の3つのテーブルを実装することで行われる。

  • 投機的マッピング:アーキテクチャ・インデックスから物理インデックスへの変換。命令デコード後に更新され、分散RAMで実装される
  • コミット後マッピング : アーキテクチャ・インデックスから物理インデックスへの変換。命令のコミット後に更新され、分散RAMで実装される
  • Location : アーキテクチャ・インデックスから、使用すべきマッピング・テーブル(投機的マッピングテーブル またはコミット後マッピングテーブル)への変換。レジスタとして実装される(分岐の予測ミスが発生した場合はクリアする必要がある)。

これにより、パイプラインが分岐を誤って予測した場合に、変換の状態を即座に元に戻すことができる。

物理インデックスからROB ID

依存関係の物理レジスタファイルが計算されると、それらは依存するROB IDに変換される。これは次の2つの方法で行われる。

  • ROBマッピング:物理インデックスからROB IDに変換する分散RAM
  • ビジー:指定されたROB IDがまだ実行中であるかどうかを指定する。これは命令がディスパッチされたときに設定され、命令が完了したときにクリアされる。

ディスパッチ/命令発行

現在の実装に関する具体的なポイントをいくつか挙げてみる。

  • ユニファイドディスパッチ・命令発行:主に領域を節約するため、また各エントリーの使用頻度を最大限に高めるため
  • 2Dキュー:エントリーはC=decodeCount列、L=slotCount/decodeCount行に配置される
  • 行プッシュ : キューに何かがプッシュされると、その行全体が「消費」される。たとえその行が完全に使用されていない場合でも、行全体を消費する。
  • 圧縮なし : 空行には圧縮が適用されない。プッシュが行われるごとに、ホワイトキューが1行シフトする。これにより、行列FFのより優れた推論と、より小さく高速なROB IDウェイクロジックが可能になる。
  • マトリックスベース : どの命令を格納するかは、ハーフマトリックスとして実行される内容によって決まる
  • 古い順 : 複数の命令が特定の実行ユニットで同時にディスパッチされる場合、古い命令が選択される
  • ROB IDによるウェイクアップ : ダイナミックウェイクアップの場合、ROB IDが識別子として使用される(物理レジスタファイルIDではない)

そのため、全体的には、タイミングを維持するために超えないようにするには、32スロットのキューが限界のようだ。また、現在の設計では、キューの占有面積はCPU全体と比較してそれほど大きな問題ではないようだ。

以下にいくつかの図を示す:

オープンソース・アウトオブオーダCPU NaxRiscvのドキュメントを読んでいく (1. イントロダクション)

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

spinalhdl.github.io


イントロダクション

NaxRiscv

NaxRiscv は現在、以下の特徴を持つコアである。

  • レジスタ・リネーミング付きアウト・オブ・オーダ実行
  • スーパースカラ(例:2デコード、3実行ユニット、2リタイア)
  • (RV32/RV64) IMAFDCSU(Linux / Buildroot / Debian
  • ポータブルHDLだが、分散RAM付きのターゲットFPGA(これまで使用された参照はXilinxシリーズ7)
  • (比較的) 低い面積使用率と高い fmax をターゲットとする (最高の IPC ではない)
  • 分散型ハードウェア演算 (プラグインでパラメータ設定可能な空のトップレベル)
  • カスタマイズを容易にするパイプラインフレームワークをベースとしたフロントエンドの実現
  • 複数のリフィルおよびライトバックスロットを備えたノンブロッキングデータキャッシュ
  • BTB + GSHARE + RAS ブランチ予測
  • ハードウェアリフィル MMU (SV32、SV39)
  • 投機的キャッシュヒット予測により、レイテンシ3サイクルでロードを実行
  • verilatorシミュレーションとKonata(gem5ファイルフォーマット)によるパイプラインの視覚化
  • RISCV外部デバッグサポートv. 0.13.2の実装によるJTAG / OpenOCD / GDBサポート
  • Tilelinkによるメモリコヒーレンシのサポート
  • オプションのコヒーレントL2キャッシュ

プロジェクトの開発と現状

  • このプロジェクトはフリーかつオープンソースである
  • ハードウェア(ArtyA7 / Litex)上でDebian/Buildroot/Linuxを上流で実行できる
  • 2021年10月に開始し、その後NLnetから資金援助を受けた(https://nlnet.nl/project/NaxRiscv/#ack)

第三者によるドキュメントもこちらで入手可能(CEA-Letiより): https://github.com/SpinalHDL/naxriscv_doc

なぜFPGAをターゲットにしたOoOコアなのか

理由はいくつかある

  • シングルスレッドのパフォーマンスを向上させるため。

VexRiscvでLinuxを実行したテストでは、マルチコアが役立つとしても、「ほとんど」のアプリケーションはそれを活用するように作られていないことが明らかになった。 - メモリレイテンシを隠蔽するため(FPGAに大きなL2キャッシュを搭載できるほどのメモリはない)。 - より高度なハードウェア記述パラダイム(Scala/SpinalHDL)を試すため。 - 個人的な興味から。

また、世の中にはOoOのオープンソース・ソフトコアがあまり存在していなかった(Marocchino、RSD、OPAなど)。 いくつかの指標において、より良い結果を出すことができるという賭けだった。 そして、より良いパフォーマンス(面積の犠牲を払って)を提供することで、1命令発行/インオーダ・コアのソフトコアを置き換えるのに十分なほど良い結果を出すことができるだろう。

RISC-Vのメモリプロテクション機構のおさらい(PMP: Physical Memory Protection)

結構昔にRISC-VのPMP機構(Physical Memory Protection)について調査したのだった。

msyksphinz.hatenablog.com

改めて復習しているが、PMPのNAPOTについてあまり明確に理解していなかった。 PMPのアドレス範囲選択については、3つのモードがある:

  • pmpcfg[*].A == 0 : 無効
  • pmpcfg[*].A == 1 : TOR(Top of Range)
  • pmpcfg[*].A == 2 : NA4(Naturally aligned four-byte region)
  • pmpcfg[*].A == 3 : NAPOT(Naturally aligned power-of-two region, >=8)

TORについては、2つのpmp領域を利用してマッチングアドレスの上下を設定する。

NA4とNAPOTは似ていて、NAPOTはアドレスの範囲の指定領域が2の累乗バイトに限定されるアドレス範囲の指定方法である。

アドレス範囲はpmpaddr[*]を使って指定するのだが、ここで注意するのはpmpaddr[*]のアドレスは4バイトアラインであり、下位の2ビットは右に2ビットシフトされて節約されている。

`pmpaddr[*]`レジスタの定義。addressの下位2ビットは削られていることに注意。

ここで、NAPOTのアドレス指定方式について見ていく。仕様書の中では G という変数が使われているが、これはおそらくハードウェアで指定されたアドレス領域の粒度(Granularity)の意味だと思う。ここでは、例えば8バイト粒度だと G=log2(8/4)=1として取り扱うことにする(/4としているのはNAPOTが4バイト粒度以下をサポートしていないから)。

まず、pmpcfgを書き込んでNAPOTモードにすると、当該pmpaddrレジスタの G-2 ビットの位置までがすべて1に設定される(G=1のときはどこも1に設定されない)。これはNAPOTモードを解除すると解放される。

これはどういう意味なんだろうととちょっと考えていたのだが、普通に考えるとマッチングのアドレス領域を簡単に指定しようとしているのだと気が付く。

例えば、上記の図で16-byteの粒度の場合、NAPOTのアドレスマッチング範囲はyyyy...yy00 から yyyy...yy01までとなる(実際には下位にビットが省略されているので、 yyyy...yy0000 から yyyy...yy0111までとなる。

TORと同じ機構でアドレスの比較ができるため、このような仕組みになっているのか、と思っている。

ただ、上記の例だと0x0~0x7までしかチェックできないため1ビット分足りないな?どういう仕組みになっているんだろう。

Binary Hacks Rebootedを購入しました

結構昔のBinary Hacksがまだ本棚に残っているかどうか不明だけど、Rebootedということで買いました。 自分はハードウェア屋さんなので、もうBinary Hacksの中身も忘れてしまいましたね。

内容は面白そうです。実作業にすぐに役に立つかというとそうでもないけど、こういう本はパズル的な感覚で楽しく読むといいかもしれない。

まだ時間が無くて中身を読み込んだわけではないけど、前回のBinary Hackと同様にみたいに各サブセクションで話がつながらず、各サブセクションを著者がバラバラに書いているんだと思うので、短編小説的な感じで進めていけばいいのかな。

ちょっとずつ流し読みしながら「ホーンなるほど」と思えればいいと思う。気軽に読んでいくことにしよう。

Rebootedといえば、私が書いたLLVMの本も、LLVMのバージョンアップがあるとすぐに情報が古くなるので、改版したり情報アップデートに興味がある方は手伝ってくれるととてもありがたいです。 もう一回同じレベルの本を書くかというと分からないけど。とても時間がかかったので、今はそんな時間が取れそうもない。

オープンソース・アウトオブオーダCPU NaxRiscvを概観する (11. NaxRiscvで生成されるデザインの比較)

NaxRiscvの様々なコンフィグレーションでのVivado論理合成結果を見て比較してみる。 次はRV64の構成で試してみた。

Slice LUT Slice Registers WNS (ns)
rv32ima 13478 7899 3.408
rv32imaf 18071 10655 3.150
rv32imafd 17669 9786 2.708
rv32imac 13702 8189 3.394
rv32imafc 18543 10948 3.052
rv32imafdc 21159 12934 3.022
rv64ima 17669 9786 2.708
rv64imaf 22956 12808 2.555
rv64imafd 25383 14431 2.836
rv64imac 17930 10137 3.018
rv64imafc 23078 13047 2.849
rv64imafdc 25870 14805 2.846

RV64の場合は面積は順当に変化している気がする。周波数に関してはRV32と同様に、ちょっと相関がないかな。

msyksphinz.hatenablog.com

msyksphinz.hatenablog.com

msyksphinz.hatenablog.com

msyksphinz.hatenablog.com

msyksphinz.hatenablog.com

msyksphinz.hatenablog.com

msyksphinz.hatenablog.com

msyksphinz.hatenablog.com