FPGA開発日記

FPGAというより、コンピュータアーキテクチャかもね! カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages

VerilogからChiselへ移行するためのTips(文法編)

f:id:msyksphinz:20190119013850p:plain

趣味でVerilogで書いたデザインをChiselに移植していたのだが、VerilogとChiselの文法的な違い、移植するにあたり注意すべき点が少しずつ分かってきた。

まだ文法的な部分しか見ていないが、Verilogで書いたデザインをChiselに移植するために気を付けなければならないところを、まずはSyntaxの面からみていこうと思う。

Bit ReassignmentはChiselでは不可能。

Bit Reassignmentとは、Verilogで言うところの変数のビットに対してassignを実行すること。 Chiselで書くと以下のようになる。ただしこれはエラーとなる。 outという変数の、とあるビット位置にだけ書き込むというのはできない。まあ、もともとがScalaというプログラミング言語なので、不可能なのは何となくわかる気がする。

class BitReassignError extends Module {
  val io = IO(new Bundle {
    val out = Output(UInt(2.W))
    val in = Input(UInt(2.W))
  })

  io.out(0) := io.in(1)
  io.out(1) := io.in(0)

}

エラーが出る。

[info] [0.003] Elaborating design...
[error] (run-main-0) chisel3.internal.ChiselException: Cannot reassign to read-only chisel3.core.Bool@11
[error] chisel3.internal.ChiselException: Cannot reassign to read-only chisel3.core.Bool@11

ビット書き込みをしたい場合は、Vec型に分解すること。 これをうまく活用するためには、最初からエレガントに設計することが求められる気がする。

class BitReassignError extends Module {
  val io = IO(new Bundle {
    val out = Output(Vec(2, UInt(1.W)))  // <-- ココ。1ビットの信号を2つのベクトルに分解。
    val in = Input(UInt(2.W))
  })

  io.out(0) := io.in(1)
  io.out(1) := io.in(0)

}

When条件が多重、抜け

これもやってしまいがち。例えば、以下の構文だとio.outはどのような条件でも代入されるように思えるが文法的にio.outに抜けが生じるのでエラー。.otherwiseをちゃんと使おう。

class MultiWhen extends Module {
  val io = IO(new Bundle {
    val out = Output(UInt(2.W))
    val inA = Input(UInt(2.W))
    val inB = Input(UInt(2.W))
    val en  = Input(Bool())
  })

  when (io.en) {
    io.out := io.inA
  } 
  when (~io.en) {
    io.out := io.inB
  }
}
[error] (run-main-0) firrtl.passes.CheckInitialization$RefNotInitializedException:  @[:@6.4] : [module MultiWhen]  Reference io is not fully initialized.
[error]    @[convert_verilog.scala 26:16:@8.4] : node _GEN_0 = mux(io.en, io.inA, VOID) @[convert_verilog.scala 26:16:@8.4]
[error]    @[convert_verilog.scala 29:17:@12.4] : node _GEN_1 = mux(_T_13, io.inB, _GEN_0) @[convert_verilog.scala 29:17:@12.4]
[error]    @[convert_verilog.scala 27:12:@9.6 convert_verilog.scala 30:12:@13.6] : io.out <= _GEN_1 @[convert_verilog.scala 27:12:@9.6 convert_verilog.scala 30:12:@13.6]
[error] firrtl.passes.CheckInitialization$RefNotInitializedException:  @[:@6.4] : [module MultiWhen]  Reference io is not fully initialized.
[error]    @[convert_verilog.scala 26:16:@8.4] : node _GEN_0 = mux(io.en, io.inA, VOID) @[convert_verilog.scala 26:16:@8.4]
[error]    @[convert_verilog.scala 29:17:@12.4] : node _GEN_1 = mux(_T_13, io.inB, _GEN_0) @[convert_verilog.scala 29:17:@12.4]
[error]    @[convert_verilog.scala 27:12:@9.6 convert_verilog.scala 30:12:@13.6] : io.out <= _GEN_1 @[convert_verilog.scala 27:12:@9.6 convert_verilog.scala 30:12:@13.6]

.otherwiseを使うと直る。

  when (io.en) {
    io.out := io.inA
  } .otherwise {
    io.out := io.inB
  }

使用していない(代入のない)変数

Verilog的には不定となって終わりだが、Chisel的には代入のない変数はNG。未初期化変数としてエラーになる。VerilogをChiselに変換するとこのエラーが大量に出てきて、結構使っていない変数が残っているんだなと絶望する。

class UnUsedVal extends Module {
  val io = IO(new Bundle {
    val outA = Output(UInt(2.W))
    val outB = Output(UInt(2.W))
  })

  io.outA := 0.U
  // io.outB := 1.U
}

io.outBを初期化していないのでエラー。

[error] (run-main-0) firrtl.passes.CheckInitialization$RefNotInitializedException:  @[:@6.4] : [module UnUsedVal]  Reference io is not fully initialized.
[error]    : io.outB <= VOID
[error] firrtl.passes.CheckInitialization$RefNotInitializedException:  @[:@6.4] : [module UnUsedVal]  Reference io is not fully initialized.
[error]    : io.outB <= VOID

整数iとハードウェア整数i.Uを正しく使い分けよう

これは最初にハマる。Chiselではハードウェアとして使われる整数は全て.Uを末尾に付ける。 これはScalaで使用される整数(.Uのつかないもの)と明確に区別される。

0.U
1.U
2.U(31, 0)  // ビットフィールドを正しく使い分けよう。Scalaの整数2をChiselのハードウェア整数に変換する。

じゃあScalaの変数はどうするかというと、Verilogのgenerate forのような感じで使うとよろしい。

for (i <- 0 until 8) {  // このiはScalaの変数
    io.out(i) := function(io.inA, i.U)  // i.Uはハードウェアで取り扱われる整数。
}

所々でCatは使えない。

例えばswitch内の条件でCat(ビット連結)を使うと失敗する。

io.out := io.default   // switchのデフォルト値はここに書く。これがないとエラー。
switch(io.idx) {
  is (Cat(1.U(2, 0), 0.U(1, 0)) {  // つまりビット値で言うと0b001_00
    io.out := io.in
  }
}

上記はswitch文のis要素でCatを使ったのでエラー。

[error] (run-main-0) java.lang.IllegalArgumentException: requirement failed: is conditions must be literals!
[error] java.lang.IllegalArgumentException: requirement failed: is conditions must be literals!

文法的にPassさせるためには、以下のように書こう。

  io.out := io.in_default
  switch(io.idx) {
    // is (Cat(1.U(2, 0), 0.U(1, 0))) {
    is (((1 << 2) | 0).U) {  // ビットシフトで肩代わり。
      io.out := io.in
    }
  }

RISC-V 64-bit LLVM Backendを試す (11. 簡単なReturn命令を生成する)

LLVMにはすでにRISC-Vのバックエンドサポートが追加されている。しかし、勉強のために独自のRISC-V実装をLLVMに追加している。 簡単なReturn IRが変換できるように実装を進めていこう。

実装するのに見ている関数群は以下のようなものだが、まず関数の意味が分からない、というところから始めないといけない。

  • LowerFormalArguments(): 入力引数を処理するためのフック。
  • LowerReturn() : return値を処理するためのフック。値をreturn用レジスタ(RISC-Vの場合はA0)に挿入する。

CCValAssign : 戻り値が戻り値用のレジスタに格納されたことを表現する。

SmallVector<CCValAssign, 16> RVLocs;

analyzeReturnにより戻り値の解析を行う、らしい。

  // Analyze return values.
  MYRISCVXCCInfo.analyzeReturn(Outs, Subtarget.abiUsesSoftFloat(),
                               MF.getFunction().getReturnType());

実体はこちら。

template<typename Ty>
void MYRISCVXTargetLowering::MYRISCVXCC::
analyzeReturn(const SmallVectorImpl<Ty> &RetVals, bool IsSoftFloat,
              const SDNode *CallNode, const Type *RetTy) const {
  CCAssignFn *Fn;

  Fn = RetCC_MYRISCVX;

  for (unsigned I = 0, E = RetVals.size(); I < E; ++I) {
    MVT VT = RetVals[I].VT;
    ISD::ArgFlagsTy Flags = RetVals[I].Flags;
    MVT RegVT = this->getRegVT(VT, RetTy, CallNode, IsSoftFloat);

    if (Fn(I, VT, RegVT, CCValAssign::Full, Flags, this->CCInfo)) {
#ifndef NDEBUG
      dbgs() << "Call result #" << I << " has unhandled type "
             << EVT(VT).getEVTString() << '\n';
#endif
      llvm_unreachable(nullptr);
    }
  }
}

もう単語の意味から型の意味まで全く分からないのだが、

  • MVT = Machine Value Type : 型の種類のようなもの。
    if (Fn(I, VT, RegVT, CCValAssign::Full, Flags, this->CCInfo)) {

の意味しているところは、つまるところRetCC_MYRISCVX(I, VT, RegVT, CCValAssign::Full, Flags, this->CCInfo)を呼び出して、 その型にマッチしていなければFalseを返すということだ。これでReturnする値の型を判定している?

  // Copy the result values into the output registers.
  for (unsigned i = 0; i != RVLocs.size(); ++i) {
    SDValue Val = OutVals[i];
    CCValAssign &VA = RVLocs[i];
    assert(VA.isRegLoc() && "Can only return in registers!");

    if (RVLocs[i].getValVT() != RVLocs[i].getLocVT())
      Val = DAG.getNode(ISD::BITCAST, DL, RVLocs[i].getLocVT(), Val);

    Chain = DAG.getCopyToReg(Chain, DL, VA.getLocReg(), Val, Flag);

    // Guarantee that all emitted copies are stuck together with flags.
    Flag = Chain.getValue(1);
    RetOps.push_back(DAG.getRegister(VA.getLocReg(), VA.getLocVT()));
  }

SmallVectorのサイズがなぜ16なのかは分からない。おそらく16個以上の戻り値を返そうとすると(C言語では不可能だがほかの言語だと可能かもしれない)、 エラーを吐いてしまうのではなかろうか。その辺りの実装はどのようになっているのかはよく分からない。 forループの中にprintf()を入れて確認してみるとこのループは1度しか実行されていないようだった。

チュートリアル通り、-O2で最適化したIRで生成すると、A0に戻り値を渡してからRetが実行されているのが分かる。 -O2無しで生成したIRでは、まだクラッシュしてしまう。

./bin/clang -O2 -c -target mips-unknown-linux-gnu ../lbdex/input/ch3.cpp -emit-llvm -o ch3.myriscvx_o2.bc
./bin/llc -march=myriscvx -relocation-model=pic -filetype=asm ch3.myriscvx_o2.bc -o -

...

# %bb.0:                                # %entry
        addiu   $x10, $x0, 0
        ret     $x1

Small Immediateの挿入

MYRISCVXInstrInfo.tdに以下のパターンを追加しており、これで"return val(val は即値)" のA0レジスタへの代入処理が実行できるようになっている。

//===----------------------------------------------------------------------===//
//  Arbitrary patterns that map to one or more instructions
//===----------------------------------------------------------------------===//

// Small immediates
def : Pat<(i32 immSExt16:$in),
          (ADDiu ZERO, imm:$in)>;
f:id:msyksphinz:20190117013715p:plain
return命令の生成結果

Chiselを使ったハードウェアデザインでトレイトを活用するためには(2. MultiIOModuleを使ったI/Oの追加)

f:id:msyksphinz:20190113171015p:plain
Chiselでのトレイトの活用

Chiselでトレイトを使って機能を拡張する話、関数の追加やポート幅の拡張などの方法は分かったが、ポートを増やすためにはどのようにすれば良いのだろうか。

いろいろ調べていると、MultiIOModuleという機能が使えそうであった。

github.com

MultiIOModuleのサンプルを見ると、モジュールに対してトレイトでI/Oを追加したり、機能を追加することができるように見える。 この手法について調査した。

ベースとなるモジュール

ベースとなるモジュールは以下だ。派生元がModuleクラスではなく、MultiIOModuleであることに注意。 in_Ain_Bから受け取った値を、ハッシュ値を計算してhashに出力する単純な機能だ。

abstract class MultiIOTest extends MultiIOModule {
  def Width: Int = 32
  def HashWidth: Int = 32
  val in_A = IO(Input(SInt(Width.W)))
  val in_B = IO(Input(SInt(Width.W)))
  val hash = IO(Output(UInt(HashWidth.W)))

  hash := (in_A.asUInt ^ in_B.asUInt)(HashWidth-1, 0)
}

ここで、幾つかのトレイトを定義する。ここでは、64bitのポートと16bitのポートを追加するトレイトだ。ポート名はval_outと言う。

trait AddMultiIOTrait extends MultiIOModule {
  def OutWidth: Int = 64
  val val_out = IO(Output(UInt(OutWidth.W)))

  val_out := 0.U
}


trait SubMultiIOTrait extends MultiIOModule {
  def OutWidth: Int = 16
  val val_out = IO(Output(UInt(OutWidth.W)))

  val_out := 1.U
}

では、このトレイトを接続する鵜。ベースとなるMultiIOTestモジュールに対して、トレイトとしてAddMultiIOTraitSubMultiIOTraitを追加するわけだ。

class AddMultiIOTest extends MultiIOTest with AddMultiIOTrait
class SubMultiIOTest extends MultiIOTest with SubMultiIOTrait

一方で、このトレイトで追加したI/Oに、ベースとなるモジュールからI/Oを接続して拡張する場合にはどうするか。 このためにはトレイトに機能を追加する方法と、モジュールに機能を追加する方法がある。

FuncMultiIOTraitは、ポートの宣言だけを行っている。一方でFunc2MultiIOTraitはポートの宣言と、機能の追加(in_A - in_B)の実装を行っている。

trait FuncMultiIOTrait extends MultiIOModule {
  def OutWidth: Int = 32
  val sub_out = IO(Output(SInt(OutWidth.W)))
}


trait Func2MultiIOTrait extends MultiIOTest {
  def OutWidth: Int = 32
  val sub_out = IO(Output(SInt(OutWidth.W)))

  sub_out := in_A - in_B
}

これらは、どちらもインスタンス化できる。

class FuncMultiIOTest extends MultiIOTest with FuncMultiIOTrait {
  sub_out := in_A - in_B
}
class Func2MultiIOTest extends MultiIOTest with Func2MultiIOTrait

結果として、Func2MultiIOTest.vを見てみよう。

object MultiIOTest extends App {
  chisel3.Driver.execute(args, () => new AddMultiIOTest())
  chisel3.Driver.execute(args, () => new SubMultiIOTest())
  chisel3.Driver.execute(args, () => new FuncMultiIOTest())
  chisel3.Driver.execute(args, () => new Func2MultiIOTest())
}
module Func2MultiIOTest( // @[:@3.2]
  input         clock, // @[:@4.4]
  input         reset, // @[:@5.4]
  input  [31:0] in_A, // @[:@6.4]
  input  [31:0] in_B, // @[:@7.4]
  output [31:0] hash, // @[:@8.4]
  output [31:0] sub_out // @[:@9.4]
);
  wire [31:0] _T_10; // @[multiio_test.scala 15:17:@11.4]
  wire [31:0] _T_11; // @[multiio_test.scala 15:31:@12.4]
  wire [32:0] _T_16; // @[multiio_test.scala 43:19:@16.4]
  wire [31:0] _T_17; // @[multiio_test.scala 43:19:@17.4]
  assign _T_10 = $unsigned(in_A); // @[multiio_test.scala 15:17:@11.4]
  assign _T_11 = $unsigned(in_B); // @[multiio_test.scala 15:31:@12.4]
  assign _T_16 = $signed(in_A) - $signed(in_B); // @[multiio_test.scala 43:19:@16.4]
  assign _T_17 = $signed(in_A) - $signed(in_B); // @[multiio_test.scala 43:19:@17.4]
  assign hash = _T_10 ^ _T_11; // @[multiio_test.scala 15:8:@15.4]
  assign sub_out = $signed(_T_17); // @[multiio_test.scala 43:11:@19.4]
endmodule

Verilogが正しく生成できている!

RISC-V 64-bit LLVM Backendを試す (10. サブターゲットのエラーを解析する)

f:id:msyksphinz:20190109233324p:plain

LLVMにはすでにRISC-Vのバックエンドサポートが追加されている。しかし、勉強のために独自のRISC-V実装をLLVMに追加している。 少しずつ進んでいるが、まだターゲットのプログラムを正しくバイナリに変換することができない。

SubtargetFeature.cppを確認してみると、このアサーションで落ちている。 確かに、Featureの文字列を設定した場所がどこにも確認できない。

  • lib/MC/SubtargetFeature.cpp
static inline bool hasFlag(StringRef Feature) {
  assert(!Feature.empty() && "Empty string");
  // Get first character
  char Ch = Feature[0];
  // Check if first character is '+' or '-' flag
  return Ch == '+' || Ch =='-';
}

MIPSの実装では、以下で設定がされているようだった。

  • lib/Target/Mips/MipsTargetMachine.cpp
MipsTargetMachine::MipsTargetMachine(const Target &T, const Triple &TT,
                                     StringRef CPU, StringRef FS,
...
      NoMips16Subtarget(TT, CPU, FS.empty() ? "-mips16" : FS.str() + ",-mips16",
                        isLittle, *this, Options.StackAlignmentOverride),
      Mips16Subtarget(TT, CPU, FS.empty() ? "+mips16" : FS.str() + ",+mips16",
                      isLittle, *this, Options.StackAlignmentOverride) {
...
}

同じような実装で、RISCV(本家の実装)の場合は設定がされていない?これはなぜだろう。RISC-Vもターゲットを追加して再度ビルドして様子を見た。

RISCVTargetMachine::RISCVTargetMachine(const Target &T, const Triple &TT, StringRef CPU, StringRef FS, const TargetOptions &Options, ... TLOF(make_unique()), Subtarget(TT, CPU, FS, *this) { initAsmInfo(); ...

ターゲットをRISC-Vとしてllcを起動すると、`+64bit`と`-64bit`が使われている。これはどこで登録されているんだろう?

./bin/llc -march=riscv32 -relocation-model=pic -filetype=asm ch3.riscv64.bc -o - Feature : -64bit Feature : -64bit Feature : +64bit Feature : +64bit ...

`RISCV.td`に登録されている以下が気になる。

- `lib/Target/RISCV/RISCV.td`

def RV64 : HwMode<"+64bit">; def RV32 : HwMode<"-64bit">;




$ ./bin/llc -march=myriscvx -relocation-model=pic -filetype=asm ch3.riscv64.bc -o - Feature : +myriscvx64 Feature : +myriscvx64 Feature : +myriscvx64 Feature : +myriscvx64 .text .section .mdebug.abilp64 .previous .file "ch3.cpp" Feature : +myriscvx64 Feature : +myriscvx64 Feature : +myriscvx64 Feature : +myriscvx64 ExpandIntegerResult #0: t2: i64 = FrameIndex<0>

Do not know how to expand the result of this operator! UNREACHABLE executed at /home/msyksphinz/work/riscv/llvm-myriscvx64/lib/CodeGen/SelectionDAG/LegalizeIntegerTypes.cpp:1369! Stack dump:

いろいろ調べていって、結局FeatureStringを登録するためにの方法が分かってきた。
一つは、MipsのようにSubtargetを登録する際に追加すること。

- `lib/Target/Mips/MipsTargetMachine.cpp`

MipsTargetMachine::MipsTargetMachine(const Target &T, const Triple &TT, StringRef CPU, StringRef FS,

  NoMips16Subtarget(TT, CPU, FS.empty() ? "-mips16" : FS.str() + ",-mips16",
                    isLittle, *this, Options.StackAlignmentOverride),
  Mips16Subtarget(TT, CPU, FS.empty() ? "+mips16" : FS.str() + ",+mips16",
                  isLittle, *this, Options.StackAlignmentOverride) {

...

const MipsSubtarget * MipsTargetMachine::getSubtargetImpl(const Function &F) const { if (hasMips16Attr) FS += FS.empty() ? "+mips16" : ",+mips16"; else if (hasNoMips16Attr) FS += FS.empty() ? "-mips16" : ",-mips16"; if (HasMicroMipsAttr) FS += FS.empty() ? "+micromips" : ",+micromips"; else if (HasNoMicroMipsAttr) FS += FS.empty() ? "-micromips" : ",-micromips"; if (softFloat) FS += FS.empty() ? "+soft-float" : ",+soft-float"; ``

もう一つは、RISC-Vのように生成された関数を使用することだ。

  • lib/Target/RISCV/RISCVSubtarget.cpp
RISCVSubtarget &RISCVSubtarget::initializeSubtargetDependencies(StringRef CPU,
                                                                StringRef FS,
                                                                bool Is64Bit) {
...
  ParseSubtargetFeatures(CPUName, FS);
...
  • (ビルドディレクトリ側)lib/Target/RISCV/RISCVGenSubtargetInfo.inc
// ParseSubtargetFeatures - Parses features string setting specified
// subtarget options.
void llvm::RISCVSubtarget::ParseSubtargetFeatures(StringRef CPU, StringRef FS) {
  LLVM_DEBUG(dbgs() << "\nFeatures:" << FS);
  LLVM_DEBUG(dbgs() << "\nCPU:" << CPU << "\n\n");
  InitMCProcessorInfo(CPU, FS);
...
  • lib/MC/MCSubtargetInfo.cpp
static FeatureBitset getFeatures(StringRef CPU, StringRef FS,
                                 ArrayRef<SubtargetFeatureKV> ProcDesc,
                                 ArrayRef<SubtargetFeatureKV> ProcFeatures) {
  SubtargetFeatures Features(FS);
  return Features.getFeatureBits(CPU, ProcDesc, ProcFeatures);
}

void MCSubtargetInfo::InitMCProcessorInfo(StringRef CPU, StringRef FS) {
  FeatureBits = getFeatures(CPU, FS, ProcDesc, ProcFeatures);
  if (!CPU.empty())
    CPUSchedModel = &getSchedModelForCPU(CPU);
  else
    CPUSchedModel = &MCSchedModel::GetDefaultSchedModel();
}
...

まずは手動でFSの文字列を追加してみた。

diff --git a/lib/Target/MYRISCVX/MYRISCVXTargetMachine.cpp b/lib/Target/MYRISCVX/MYRISCVXTargetMachine.cpp
index 2f60ab55..91466345 100644
--- a/lib/Target/MYRISCVX/MYRISCVXTargetMachine.cpp
+++ b/lib/Target/MYRISCVX/MYRISCVXTargetMachine.cpp
@@ -97,7 +97,7 @@ MYRISCVXTargetMachine::getSubtargetImpl(const Function &F) const {
                        ? FSAttr.getValueAsString().str()
                        : TargetFS;

-  // FS += "+myriscvx64";
+  FS += "+myriscvx64";

   auto &I = SubtargetMap[CPU + FS];
   if (!I) {

これで再度ビルドすると、Assertionのエラーは発生しなくなった。

$ ./bin/llc -march=myriscvx -relocation-model=pic -filetype=asm ch3.riscv64.bc -o -
        .text
        .section .mdebug.abilp64
        .previous
        .file   "ch3.cpp"
Feature : +myriscvx64
Feature : +myriscvx64
Feature : +myriscvx64
Feature : +myriscvx64
ExpandIntegerResult #0: t2: i64 = FrameIndex<0>

Do not know how to expand the result of this operator!
UNREACHABLE executed at /home/msyksphinz/work/riscv/llvm-myriscvx64/lib/CodeGen/SelectionDAG/LegalizeIntegerTypes.cpp:1369!
Stack dump:
...

ただし、今度はDAGの選択に失敗する。これは単純に追加しているDAGが少ないからかな? 解析を進めていこう。

Chiselを使ったハードウェアデザインでトレイトを活用するためには

f:id:msyksphinz:20190113171015p:plain
Chiselでのトレイトの活用

Chiselはハードウェア記述言語だが、ベースの言語はScalaなので様々な高級言語の機能を活用することができる。 ScalaやRustではトレイトという機能を使うことができる。 トレイトというのはあまり私も詳しくないのだが、

ja.wikipedia.org

  • 振る舞いを実装するメソッド群を提供する。
  • 提供された振る舞いをパラメーター化するメソッド群を必要とする。
  • 状態変数を必要としない。
    • 状態変数を直接には読み書きしない

ということで、ハードウェアのモジュール(Chiselではクラス)に対して分離したメソッドを定義することができるという理解だ。 これにより、ベースのクラスから、必要な機能に従ってトレイトを追加することで、コードの再利用を行うことができるようになる。

トレイトを使ってChiselのハードウェアを実装するために、どのようにすればよいのかいろいろ試行してみた。

ベースとなるクラス

ベースクラスとして、以下のようなモジュールを作ってみる。 入出力として32bitのin_A, in_Bを取り、出力としてhashというポートにハッシュ値を32bitのサイズで出力する。 ここで、out_Cは実質的に使用されていない。別の場所でfunc()を定義しなければならない。

abstract class TraitTest extends Module {
  def Width: Int = 32
  def HashWidth: Int = 32
  def func(valA: SInt, valB: SInt): SInt;
  val io = IO(new Bundle {
    val in_A = Input(SInt(Width.W))
    val in_B = Input(SInt(Width.W))
    val hash = Output(UInt(HashWidth.W))
    val out_C = Output(SInt(Width.W))
  })

  io.out_C := func(io.in_A, io.in_B)
  io.hash  := (io.in_A.asUInt ^ io.in_B.asUInt)(HashWidth-1, 0)

ここで、out_Cに計算させる関数を定義するために、2種類のトレイトを作ってみる。funcを定義し、加算と減算を定義した。 さらに注目なのは、このトレイト内でビット幅を示す変数Widthを変更していること。これで、モジュールごとに出力ポートのサイズを調整できる。

コード修正しました。トレイト内でWidthをオーバライドするためには、TraitTestから継承させる必要があります。

trait AddModuleTrait extends TraitTest {
  override def Width: Int = 64
  def func(valA: SInt, valB: SInt): SInt = {
    return valA + valB
  }
}

trait SubModuleTrait extends TraitTest {
  override def Width: Int = 96
  def func(valA: SInt, valB: SInt): SInt = {
    return valA - valB
  }
}

つまり、AddModuleTraitは加算をするためのモジュール、SubModuleTraitは減算をするためのモジュールである。

このトレイトを使用したモジュールを2種類作ってみる。TraitTestAddTraitTestモジュールをベースにしており、AddModuleTraitをトレイトとして接続している。 一方で、TraitTestを同様にベースモジュールとしているTraitTestSubは、SubModuleTraitをトレイトとして接続している。

class TraitTestAdd extends TraitTest with AddModuleTrait {
  override def Width: Int = 64
  io.out_C := func(io.in_A, io.in_B)
}

class TraitTestSub extends TraitTest with SubModuleTrait {
  io.out_C := func(io.in_A, io.in_B)
}

上記の2種類のモジュールをVerilogとして生成させてみよう。

object TraitTest extends App {
  chisel3.Driver.execute(args, () => new TraitTestAdd())
  chisel3.Driver.execute(args, () => new TraitTestSub())
}

それぞれ、生成されたVerilogは以下のようになり、ベースモジュールに対して機能を切り替えることができるようになった。

それでも、

  assign _T_17 = $signed(io_in_A) + $signed(io_in_B); // @[trait_test.scala 23:17:@13.4]
  assign _T_18 = $signed(io_in_A) + $signed(io_in_B); // @[trait_test.scala 23:17:@14.4]

とか、

  assign _T_17 = $signed(io_in_A) - $signed(io_in_B); // @[trait_test.scala 29:17:@13.4]
  assign _T_18 = $signed(io_in_A) - $signed(io_in_B); // @[trait_test.scala 29:17:@14.4]

と、2回もインスタンスされているのはなんでだろう(論理合成で消えるだろうから別に良いのだけれども)。

  • TraitTestAdd.v
module TraitTestAdd( // @[:@3.2]
  input         clock, // @[:@4.4]
  input         reset, // @[:@5.4]
  input  [63:0] io_in_A, // @[:@6.4]
  input  [63:0] io_in_B, // @[:@6.4]
  output [31:0] io_hash, // @[:@6.4]
  output [63:0] io_out_C // @[:@6.4]
);
  wire [63:0] _T_13; // @[trait_test.scala 17:23:@8.4]
  wire [63:0] _T_14; // @[trait_test.scala 17:40:@9.4]
  wire [63:0] _T_15; // @[trait_test.scala 17:30:@10.4]
  wire [64:0] _T_17; // @[trait_test.scala 23:17:@13.4]
  wire [63:0] _T_18; // @[trait_test.scala 23:17:@14.4]
  assign _T_13 = $unsigned(io_in_A); // @[trait_test.scala 17:23:@8.4]
  assign _T_14 = $unsigned(io_in_B); // @[trait_test.scala 17:40:@9.4]
  assign _T_15 = _T_13 ^ _T_14; // @[trait_test.scala 17:30:@10.4]
  assign _T_17 = $signed(io_in_A) + $signed(io_in_B); // @[trait_test.scala 23:17:@13.4]
  assign _T_18 = $signed(io_in_A) + $signed(io_in_B); // @[trait_test.scala 23:17:@14.4]
  assign io_hash = _T_15[31:0]; // @[trait_test.scala 17:11:@12.4]
  assign io_out_C = $signed(_T_18); // @[trait_test.scala 35:12:@16.4]
endmodule
  • TraitTestSub.v
module TraitTestSub( // @[:@3.2]
  input         clock, // @[:@4.4]
  input         reset, // @[:@5.4]
  input  [95:0] io_in_A, // @[:@6.4]
  input  [95:0] io_in_B, // @[:@6.4]
  output [31:0] io_hash, // @[:@6.4]
  output [95:0] io_out_C // @[:@6.4]
);
  wire [95:0] _T_13; // @[trait_test.scala 17:23:@8.4]
  wire [95:0] _T_14; // @[trait_test.scala 17:40:@9.4]
  wire [95:0] _T_15; // @[trait_test.scala 17:30:@10.4]
  wire [96:0] _T_17; // @[trait_test.scala 29:17:@13.4]
  wire [95:0] _T_18; // @[trait_test.scala 29:17:@14.4]
  assign _T_13 = $unsigned(io_in_A); // @[trait_test.scala 17:23:@8.4]
  assign _T_14 = $unsigned(io_in_B); // @[trait_test.scala 17:40:@9.4]
  assign _T_15 = _T_13 ^ _T_14; // @[trait_test.scala 17:30:@10.4]
  assign _T_17 = $signed(io_in_A) - $signed(io_in_B); // @[trait_test.scala 29:17:@13.4]
  assign _T_18 = $signed(io_in_A) - $signed(io_in_B); // @[trait_test.scala 29:17:@14.4]
  assign io_hash = _T_15[31:0]; // @[trait_test.scala 17:11:@12.4]
  assign io_out_C = $signed(_T_18); // @[trait_test.scala 40:12:@16.4]
endmodule

I/Oの追加はどのようにするのか?

モジュールの機能を切り替える方法については何となく理解できるようになったが、 ついでならばトレイトを使ってI/Oを追加したり削除したりしたい。 これはどのようにすれば実現できるのだろう?要調査だ。

Chiselで生成したVerilogをVivadoで論理合成試行する

ChiselでRISC-Vプロセッサを設計するプロジェクト、しばらく手を付けていなかったのだが、実際にFPGAで動かすことを想定してVivadoで論理合成を実施した。

github.com

Vivadoの論理合成スクリプトは、まだCPUのIP単体だけなので単純に合成をするだけで配置配線までは行わない。

さらに、メモリの部分をBlackBoxでブロックRAMに置き換えるために、Verilogを生成している。

module MemoryResourceBox
  (
   input logic         clock,
   input logic         io_mem_inst_bus_req,
   input logic [15:0]  io_mem_inst_bus_addr,
   output logic        io_mem_inst_bus_ack,
...
memory_core
  #(.DATAW(64), .ADDRW(16))
u_mem
  (
   .clock (clock),

   .addra(io_mem_data_bus_addr),
...
module memory_core
#(
  parameter DATAW = 64,
  parameter ADDRW = 16
  )
(
 input logic               clock,
 input logic [ADDRW-1: 0]  addra,
...

無事に合成を完了することができた。

合成結果、BlockRAMからデータを読みだしたものをBlockRAMに書き出すパスが存在する?クリティカルパスとして現れてしまった。 動作周波数としては30MHz程度になってしまった、これは改善しないとなあ。

f:id:msyksphinz:20190113000442p:plain
RISC-V CPUを合成した結果をVivadoでオープンした。

RISC-V 64-bit LLVM Backendを試す (9. IDAGtoDAGの実装)

LLVMにはすでにRISC-Vのバックエンドサポートが追加されている。しかし、勉強のために独自のRISC-V実装をLLVMに追加している。 前回までで独自アーキテクチャのオプション(MYRISCVX)をllcに表示できるようになった。

実行してみると、アサーションで落ちる問題がある。

$ ./bin/llc -march=myriscvx -relocation-model=pic -filetype=asm ch3.riscv64.bcllc: /home/msyksphinz/work/riscv/llvm-myriscvx64/lib/MC/SubtargetFeature.cpp:179: static void llvm::SubtargetFeatures::ApplyFeatureFlag(llvm::FeatureBitset&, llvm::StringRef, llvm::ArrayRef<llvm::SubtargetFeatureKV>): Assertion `hasFlag(Feature)' failed.
Stack dump:
0.      Program arguments: ./bin/llc -march=myriscvx -relocation-model=pic -filetype=asm ch3.riscv64.bc
#0 0x00007f4634d7373d llvm::sys::PrintStackTrace(llvm::raw_ostream&) /home/msyksphinz/work/riscv/llvm-myriscvx64/lib/Support/Unix/Signals.inc:490:0
#1 0x00007f4634d737d0 PrintStackTraceSignalHandler(void*) /home/msyksphinz/work/riscv/llvm-myriscvx64/lib/Support/Unix/Signals.inc:554:0
#2 0x00007f4634d7150c llvm::sys::RunSignalHandlers() /home/msyksphinz/work/riscv/llvm-myriscvx64/lib/Support/Signals.cpp:67:0

原因を調べていると、どうもFeatureの実装が問題らしい。MYRISCVXArchiFeatureには先頭に-+を付けなければらならないらしい。

commit 9f7fd922439614e40d4c5efc13c44539f50ac1f0 (HEAD -> riscv_msyksphinz)
Author: msyksphinz <msyksphinz_dev@gmail.com>
Date:   Sat Jan 12 13:36:02 2019 +0900

    Bug: fix flag description

diff --git a/lib/Target/MYRISCVX/MCTargetDesc/MYRISCVXMCTargetDesc.cpp b/lib/Target/MYRISCVX/MCTargetDesc/MYRISCVXMCTargetDesc.cpp
index 13459c3b..b0befc56 100644
--- a/lib/Target/MYRISCVX/MCTargetDesc/MYRISCVXMCTargetDesc.cpp
+++ b/lib/Target/MYRISCVX/MCTargetDesc/MYRISCVXMCTargetDesc.cpp
@@ -46,7 +46,7 @@ static StringRef selectMYRISCVXArchFeature(const Triple &TT, StringRef CPU) {
   if (CPU.empty() || CPU == "generic") {
     if (TT.getArch() == Triple::myriscvx) {
       if (CPU.empty() || CPU == "myriscvx_impl") {
-        MYRISCVXArchFeature = "myriscvx64";
+        MYRISCVXArchFeature = "+myriscvx64";
       }
     }
   }

さらに、DAGToDAGのパスを追加する。これは、LLVM IRからターゲットのIRに変換するための処理らしい。

以下の資料を参考に実装する。

Backend structure — Tutorial: Creating an LLVM Backend for the Cpu0 Architecture

  • lib/Target/MYRISCVX/MYRISCVXInstrInfo.td
let isReturn=1, isTerminator=1, hasDelaySlot=0, isBarrier=1, hasCtrlDep=1 in
def RetLR : MYRISCVXPseudo<(outs), (ins), "", [(MYRISCVXRet)]>;

def RET     : RetBase<GPROut>;

GPROutというのは、汎用レジスタとして使用できるレジスタ群のことで、これを以下のように定義する。Cpu0ではSWを除去しているのだが、MYRISCVXでは汎用レジスタ群はすべて使用できる。

  • lib/Target/MYRISCVX/MYRISCVXGPROutForOther.td
//===----------------------------------------------------------------------===//
// Register Classes
//===----------------------------------------------------------------------===//

def GPROut : RegisterClass<"MYRISCVX", [i32], 32, (add CPURegs)>;
  • lib/Target/MYRISCVX/MYRISCXRegisterInfo.td
//===----------------------------------------------------------------------===//
//@Register Classes
//===----------------------------------------------------------------------===//

def CPURegs : RegisterClass<"MYRISCVX", [i32], 32, (add
  // Reserved
  ZERO,
  // Return Values and Arguments
  A0, A1, A2, A3, A4, A5, A6, A7,
  // Not preserved across procedure calls
  T0, T1, T2, T3, T4, T5, T6,
  // Callee save
  S0, S1, S2, S3, S4, S5, S6, S7, S8, S9, S10, S11,
  // Reserved
  RA, SP, GP, TP
  )>;

これでビルドを実行してプログラムを実行してみる。

% ./bin/llc -march=myriscvx -relocation-model=pic -filetype=asm ch3.riscv64.bc
Feature = +myriscvx64
Feature = +myriscvx64
ExpandIntegerResult #0: t2: i64 = FrameIndex<0>

Do not know how to expand the result of this operator!
UNREACHABLE executed at /home/masayuki/others/llvm/llvm-myriscvx/lib/CodeGen/SelectionDAG/LegalizeIntegerTypes.cpp:1369!
Stack dump:
0.      Program arguments: ./bin/llc -march=myriscvx -relocation-model=pic -filetype=asm ch3.riscv64.bc
1.      Running pass 'Function Pass Manager' on module 'ch3.riscv64.bc'.
2.      Running pass 'MYRISCVX DAG->DAG Pattern Instruction Selection' on function '@main'

Assertionは落ちなくなったが、まだPassが足りないらしい。これから追加していこう。

https://jonathan2251.github.io/lbd/_images/dyn_reg.png