FPGA開発日記

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

オープンソース・アウトオブオーダCPU NaxRiscvを概観する (6. SpinalHDLで記述されたRVC Decompressor)

msyksphinz.hatenablog.com

msyksphinz.hatenablog.com

もうちょっとなんとかSpinal HDLのわけわからん記述が読めるようになりたい: とりあえずRVC Alignerの部分で何とか理解できる部分はないだろうか。

Aligner Pluginをもうちょっと腰を据えて解析してみたいと思う。

RVC付きのXLEN=64でNaxRiscvを生成し直そうと思う。

  • NaxRiscv/src/main/scala/naxriscv/Gen.scala
// ramstyle = "MLAB, no_rw_check"
object Gen64 extends App{
  LutInputs.set(6)
  def plugins = {
    val l = Config.plugins(
      sideChannels = false, //WARNING, FOR TEST PURPOSES ONLY, turn to false for real stuff <3
      xlen = 64,
      withRdTime = false,
      aluCount    = 2,
      decodeCount = 2,
      withRvc = true,
      withDebug = false,
      withEmbeddedJtagTap = false,
      debugTriggers = 4,
      withFloat = false,
      withDouble = false,
      lqSize = 16,
      sqSize = 16,
      asic = false
    )
    l.foreach{
      case p : EmbeddedJtagPlugin => p.debugCd.load(ClockDomain.current.copy(reset = Bool().setName("debug_reset")))
      case _ =>
    }
//    Tweek.euWritebackAt(l, "ALU0", 1)
//    Tweek.euWritebackAt(l, "ALU1", 1)
    l
  }

キャッシュブロックが128ビットの場合、16ビットのRVCを検出すべき単位は以下の図のように8個に分割される。

NaxRiscvの実装ではキャッシュ・ブロックが64ビットなので、4つのブロック分割されるはずだ。 キャッシュ・ブロックのビット幅/16ビットなので、NaxRiscvではSLICE_COUNT=4となるはずだ。

このときのslicesの作り方がまた良く分からないのだが、dataWORDbuffer.data の concatenate、そしてこれをsubdivideInによりSLICE_WIDTHビットごとに切り分ける。 つまり、dataSLICE_WIDTH毎のSLICE_COUNT*2個のデータ要素に分けられる。

    val slices = new Area {
      val data = (WORD ## buffer.data).subdivideIn(SLICE_WIDTH bits)
      var carry = (isInputValid ? input(MASK_FRONT) | B(0)) ## buffer.mask //Which slice have valid data
      var remains = CombInit(carry)
      var used = B(0, SLICE_COUNT*2 bits)
    }
  wire       [127:0]  _zz_AlignerPlugin_logic_slices_data_0;
  wire       [15:0]   AlignerPlugin_logic_slices_data_0;
  wire       [15:0]   AlignerPlugin_logic_slices_data_1;
  wire       [15:0]   AlignerPlugin_logic_slices_data_2;
  wire       [15:0]   AlignerPlugin_logic_slices_data_3;
  wire       [15:0]   AlignerPlugin_logic_slices_data_4;
  wire       [15:0]   AlignerPlugin_logic_slices_data_5;
  wire       [15:0]   AlignerPlugin_logic_slices_data_6;
  wire       [15:0]   AlignerPlugin_logic_slices_data_7;

  assign _zz_AlignerPlugin_logic_slices_data_0 = {AlignerPlugin_setup_s2m_Fetch_WORD,AlignerPlugin_logic_buffer_data};
  assign AlignerPlugin_logic_slices_data_0 = _zz_AlignerPlugin_logic_slices_data_0[15 : 0];
  assign AlignerPlugin_logic_slices_data_1 = _zz_AlignerPlugin_logic_slices_data_0[31 : 16];
  assign AlignerPlugin_logic_slices_data_2 = _zz_AlignerPlugin_logic_slices_data_0[47 : 32];
  assign AlignerPlugin_logic_slices_data_3 = _zz_AlignerPlugin_logic_slices_data_0[63 : 48];
  assign AlignerPlugin_logic_slices_data_4 = _zz_AlignerPlugin_logic_slices_data_0[79 : 64];
  assign AlignerPlugin_logic_slices_data_5 = _zz_AlignerPlugin_logic_slices_data_0[95 : 80];
  assign AlignerPlugin_logic_slices_data_6 = _zz_AlignerPlugin_logic_slices_data_0[111 : 96];
  assign AlignerPlugin_logic_slices_data_7 = _zz_AlignerPlugin_logic_slices_data_0[127 : 112];

これらをデコーダを用いて展開していく。

    val decoders = for (i <- 0 until SLICE_COUNT * 2) yield new Area {
      val rvc = RVC.get generate slices.data(i)(1 downto 0) =/= 3

      val usage = if(RVC) {
        def mask16 = B(1 << i, SLICE_COUNT*2 bits)
        def mask32 = B(3 << i, SLICE_COUNT*2 bits)
        if(i == SLICE_COUNT*2 - 1) mask16 else rvc ? mask16 | mask32

RVCが有効で、当該16ビットがrvcブロックとして認識されればmask16でマスクし、そうでなければ32ビット命令なので2ブロックぶんとしてマスクする。

  assign AlignerPlugin_logic_decoders_0_usage = (AlignerPlugin_logic_decoders_0_rvc ? 8'h01 : 8'h03);
  assign AlignerPlugin_logic_decoders_1_usage = (AlignerPlugin_logic_decoders_1_rvc ? 8'h02 : 8'h06);
  assign AlignerPlugin_logic_decoders_2_usage = (AlignerPlugin_logic_decoders_2_rvc ? 8'h04 : 8'h0c);
  assign AlignerPlugin_logic_decoders_3_usage = (AlignerPlugin_logic_decoders_3_rvc ? 8'h08 : 8'h18);
  assign AlignerPlugin_logic_decoders_4_usage = (AlignerPlugin_logic_decoders_4_rvc ? 8'h10 : 8'h30);
  assign AlignerPlugin_logic_decoders_5_usage = (AlignerPlugin_logic_decoders_5_rvc ? 8'h20 : 8'h60);
  assign AlignerPlugin_logic_decoders_6_usage = (AlignerPlugin_logic_decoders_6_rvc ? 8'h40 : 8'hc0);
  assign AlignerPlugin_logic_decoders_7_usage = 8'h80;

notEnoughDataは、2キャッシュライン分のデータにおいて、

  • 最上位がRVI(=32ビット)場合は、キャッシュラインを跨ぐのでデータが足りない。
  • 真ん中のブロックの場合は、RVI(=32ビット)かつ上位キャッシュラインが無効

というときに成立する、というものだと思う。

      val notEnoughData = RVC.get match {
        case true =>
          if(i == SLICE_COUNT * 2 - 1)
            !rvc
          else if(i == SLICE_COUNT - 1)
            !rvc && (!MASK_FRONT.lsb || !isInputValid)
          else
            False
        case false => False
      }

次に pastPredictionは2キャッシュラインのうち上位のキャッシュラインにおいて、WORD_BRANCH_SLICEよりも大きさブロック位置であれば有効となる。これはどういうことだ?多分分岐命令の位置を予測しているのかな?

      val pastPrediction = if(i <= SLICE_COUNT) False else WORD_BRANCH_VALID && U(i - SLICE_COUNT) > WORD_BRANCH_SLICE //TODO may use MASK_BACK instead for timings ?
      val usable = !notEnoughData && !pastPrediction
    }
  assign FetchPlugin_stages_1_BtbPlugin_logic_ENTRY_slice = _zz_FetchPlugin_stages_1_BtbPlugin_logic_ENTRY_hash[17 : 16];

  assign FetchPlugin_stages_1_Prediction_WORD_BRANCH_SLICE = FetchPlugin_stages_1_BtbPlugin_logic_ENTRY_slice;