FPGA開発日記

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

RISC-V Vector命令をサポートした自作命令セットシミュレータの実装検討 (7. 単純な算術演算の実装)

自作RISC-V命令セットシミュレータ、Vector Extensionのサポート続き。メモリアクセスが一応実装できたので、次は基本的な四則演算を実装して行きたい。 基本的な四則演算として、整数の四則演算を見ていく。対象としては、

  • VADD.VV : 加算
  • VSUB.VV : 減算
  • VMINU.VV : 符号なし最小値選択
  • VMIN.VV : 符号あり最小値選択
  • VMAXU.VV : 符号なし最大値選択
  • VMAX.VV : 符号あり最大値選択
  • VAND.VV : 論理積
  • VOR.VV : 論理和
  • VXOR.VV : 排他的論理和

さらにややこしいことに、これらの命令は演算単位が命令自体で明確に定義されておらず、SEW CSRレジスタの値によって決定されるということである。 つまりこのVADD.VV命令の中に、8ビット、16ビット、32ビット、64ビットなどの実行モードが含まれており、これらはCSRの値によって決定される。

まず、演算毎の制御を統一するためにラムダ関数を引数として取れる共通関数VecExecInt2Op()を定義しておく。Funcによって動作を変えることのできる共通関数である。

template <typename Dst_t, typename Src_t, typename Func>
void RiscvPeThread::VecExecInt2Op(bool vm,
                                  RegAddr_t vs1_addr, RegAddr_t vs2_addr, RegAddr_t vd_addr,
                                  Func func)
{
  const int DWIDTH = sizeof(DWord_t) * 8;
  Word_t vl;
  CSRRead (static_cast<Addr_t>(SYSREG_ADDR_VL), &vl);
  Word_t vstart; CSRRead (static_cast<Addr_t>(SYSREG_ADDR_VSTART), &vstart);
  for (int i = vstart; i < vl; i++) {
    if (vm == 0) {
      const int midx = i / DWIDTH;
      const int mpos = i % DWIDTH;
      bool skip = ((ReadVReg<DWord_t>(0, midx) >> mpos) & 0x1) == 0;
      if (skip) {
        continue;
      }
    }
    Src_t vs1_val = ReadVReg<Src_t> (vs1_addr, i);
    Src_t vs2_val = ReadVReg<Src_t> (vs2_addr, i);

    Dst_t res = func(vs1_val, vs2_val);   // ここがラムダ関数によって渡される。

    WriteVReg<Dst_t> (vd_addr, i, res);
  }
}

そして、引数FuncのところをADD/SUB/MAX/MINなどで切り替えればよいわけだ。

     // VADD.VVの場合
      m_pe_thread->VecExecInt2Op<DWord_t, DWord_t> (vm, vs1_addr, vs2_addr, \
                                                    vd_addr, \
                                                    [](DWord_t op1, DWord_t op2) { return op1 + op2; }); \

     // VSUB.VVの場合
      m_pe_thread->VecExecInt2Op<DWord_t, DWord_t> (vm, vs1_addr, vs2_addr, \
                                                    vd_addr, \
                                                    [](DWord_t op1, DWord_t op2) { return op1 - op2; }); \

ここでもう一つ、SEWの値によってデータ幅を切り替えなければならないので、さらにマクロで包み込んで一般化する。 下記のVI_VV_LOOP()では、CSRの値によって8ビット幅の演算から64ビット幅の演算まで切り替えることができるようになっている。

#define VI_VV_LOOP(op)                          \
  REQUIRE_VEC; \
\
  RegAddr_t vs1_addr = ExtractR1Field (inst_hex); \
  RegAddr_t vs2_addr = ExtractR2Field (inst_hex); \
  RegAddr_t vd_addr  = ExtractRDField (inst_hex); \
                                                  \
  bool vm = ExtractBitField(inst_hex, 25, 25);    \
                                                  \
  int vtype;                                                            \
  m_pe_thread->CSRRead (static_cast<Addr_t>(SYSREG_ADDR_VTYPE), &vtype); \
  RvvSEW sew = static_cast<RvvSEW>((vtype >> 2) & 0x7);                 \
                                                                        \
  switch(sew) {                                                         \
    case RvvSEW::SEW_8BIT:                                              \
      m_pe_thread->VecExecInt2Op<Byte_t, Byte_t> (vm, vs1_addr, vs2_addr, \
                                                  vd_addr,                \
                                                  [](Byte_t op1, Byte_t op2) { return op; }); \
      break; \
    case RvvSEW::SEW_16BIT: \
      m_pe_thread->VecExecInt2Op<HWord_t, HWord_t> (vm, vs1_addr, vs2_addr, \
                                                    vd_addr, \
                                                    [](HWord_t op1, HWord_t op2) { return op; }); \
      break; \
    case RvvSEW::SEW_32BIT: \
      m_pe_thread->VecExecInt2Op<Word_t, Word_t> (vm, vs1_addr, vs2_addr, \
                                                  vd_addr, \
                                                  [](Word_t op1, Word_t op2) { return op; }); \
      break; \
    case RvvSEW::SEW_64BIT: \
      m_pe_thread->VecExecInt2Op<DWord_t, DWord_t> (vm, vs1_addr, vs2_addr, \
                                                    vd_addr, \
                                                    [](DWord_t op1, DWord_t op2) { return op; }); \
      break; \
    default: \
      m_pe_thread->DebugPrint("Unsupported SEW."); \
      m_pe_thread->GenerateException (ExceptCode::Except_IllegalInst, 0); \
      return; \
  } \
                                                \
  return;

最終的に、各命令は以下のような定義まで単純化される。

void InstEnv::RISCV_INST_VADD_VV(InstWord_t inst_hex)  { VI_VV_LOOP(op1 + op2); }
void InstEnv::RISCV_INST_VSUB_VV(InstWord_t inst_hex)  { VI_VV_LOOP(op1 - op2); }
void InstEnv::RISCV_INST_VMINU_VV(InstWord_t inst_hex) { VI_VV_LOOP_U(op1 < op2 ? op1 : op2); }
void InstEnv::RISCV_INST_VMIN_VV(InstWord_t inst_hex)  { VI_VV_LOOP(op1 < op2 ? op1 : op2); }
void InstEnv::RISCV_INST_VMAXU_VV(InstWord_t inst_hex) { VI_VV_LOOP_U(op1 > op2 ? op1 : op2); }
void InstEnv::RISCV_INST_VMAX_VV(InstWord_t inst_hex)  { VI_VV_LOOP(op1 > op2 ? op1 : op2); }
void InstEnv::RISCV_INST_VAND_VV(InstWord_t inst_hex)  { VI_VV_LOOP(op1 & op2); }
void InstEnv::RISCV_INST_VOR_VV(InstWord_t inst_hex)   { VI_VV_LOOP(op1 | op2); }
void InstEnv::RISCV_INST_VXOR_VV(InstWord_t inst_hex)  { VI_VV_LOOP(op1 ^ op2); }

各命令でテストを書いて、一応問題なく動作することを確認した。とりあえず基本的な動作は問題ない。