FPGA開発日記

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

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

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

./bin/llc -march=myriscvx -relocation-model=pic -filetype=asm ch3.riscv64.bc

llc: /home/msyksphinz/work/riscv/llvm-riscv-msyksphinz/lib/CodeGen/LLVMTargetMachine.cpp:60: void llvm::LLVMTargetMachine::initAsmInfo(): Assertion `TmpAsmInfo && "MCAsmInfo not initialized. " "Make sure you include the correct TargetSelect.h" "and that InitializeAllTargetMCs() is being invoked!"' failed.
Stack dump:
0.      Program arguments: ./bin/llc -march=myriscvx -relocation-model=pic -filetype=asm ch3.riscv64.bc
#0 0x00007f7786f6e6b3 llvm::sys::PrintStackTrace(llvm::raw_ostream&) /home/msyksphinz/work/riscv/llvm-riscv-msyksphinz/lib/Support/Unix/Signals.inc:490:0
#1 0x00007f7786f6e746 PrintStackTraceSignalHandler(void*) /home/msyksphinz/work/riscv/llvm-riscv-msyksphinz/lib/Support/Unix/Signals.inc:554:0

アサーションのFailを解決するためには、MCAsmInfoを追加する必要があるらしい。 資料を見ながら追加していく。

  • lib/Target/MYRISCVX/MCTargetDesc/MYRISCVXMCAsmInfo.h これは既存のクラスを継承しただけだ。
namespace llvm {
  class Triple;

  class MYRISCVXMCAsmInfo : public MCAsmInfoELF {
    void anchor() override;
  public:
    explicit MYRISCVXMCAsmInfo(const Triple &TheTriple);
  };

} // namespace llvm
  • lib/Target/MYRISCVX/MCTargetDesc/MYRISCVXMCAsmInfo.cpp さらに幾つかのRISC-V用のディレクティブを追加していく。
MYRISCVXMCAsmInfo::MYRISCVXMCAsmInfo(const Triple &TheTriple) {
  if ((TheTriple.getArch() == Triple::myriscvx))
    IsLittleEndian = false; // the default of IsLittleEndian is true

  AlignmentIsInBytes          = false;
  Data16bitsDirective         = "\t.2byte\t";
  Data32bitsDirective         = "\t.4byte\t";
  Data64bitsDirective         = "\t.8byte\t";
  PrivateGlobalPrefix         = "$";
// PrivateLabelPrefix: display $BB for the labels of basic block
  PrivateLabelPrefix          = "$";
  CommentString               = "#";
  ZeroDirective               = "\t.space\t";
  GPRel32Directive            = "\t.gpword\t";
  GPRel64Directive            = "\t.gpdword\t";
  WeakRefDirective            = "\t.weak\t";
  UseAssignmentForEHBegin = true;

  SupportsDebugInformation = true;
  ExceptionsType = ExceptionHandling::DwarfCFI;
  DwarfRegNumForCFI = true;
}

次に、InstPrinterを追加していく。 lib/Target/MYRISCVX/に、InstPrinterディレクトリを追加した。

InstPrinter
├── CMakeLists.txt
├── LLVMBuild.txt
├── MYRISCVXInstPrinter.cpp
└── MYRISCVXInstPrinter.h

InsntPrinterクラスで追加しているのは以下のメソッドになるが、基本的にその名の示す通り命令のプリントを担当しているクラスになる。

  • void printInstruction(const MCInst *MI, raw_ostream &O); : TLBGENで自動的に生成される。
  • static const char *getRegisterName(unsigned RegNo); : レジスタの名前を取得する。tdで定義された名前を返す。
  • void printRegName(raw_ostream &OS, unsigned RegNo) : レジスタ名をプリントする。
  • printInst(const MCInst *MI, raw_ostream &O, StringRef Annot,... 命令をプリントする。printInstructionを呼んでいる。
  • printAliasInstr(const MCInst *MI, raw_ostream &OS) : エイリアス付きの命令をプリントする?
  • printOperand(const MCInst *MI, unsigned OpNo, raw_ostream &O); オペランドのプリント。とりあえずレジスタ名の時と、即値の時で条件が分かれる。
  • void printUnsignedImm(const MCInst *MI, int opNum, raw_ostream &O); 即値のプリント
  • void printMemOperand(const MCInst *MI, int opNum, raw_ostream &O); メモリのオペランドのプリント。0x10(sp)みたいなやつ。
    • この関数は、MYRISCVXInstrInfo.tdで以下のように定義されているのでメモリ操作の時に起動される。
// Address operand
def mem : Operand<i32> {
  let PrintMethod = "printMemOperand";
  let MIOperandInfo = (ops CPURegs, simm16);
  let EncoderMethod = "getMemEncoding";
}
  • void printMemOperandEA(const MCInst *MI, int opNum, raw_ostream &O); メモリのオペランドの印刷。これはいつ使われる?

つぎに、MYRISCVXMCInstLowerを追加して、DAGを生成するクラスを作る。

  • lib/Target/MYRISCVX/MYRISCVXInstLower.h
...
    void Lower(const MachineInstr *MI, MCInst &OutMI) const;
    MCOperand LowerOperand(const MachineOperand& MO, unsigned offset = 0) const;
...

オペランドの種類によってオブジェクトを生成するらしい。

//@LowerOperand {
MCOperand MYRISCVXMCInstLower::LowerOperand(const MachineOperand& MO,
                                        unsigned offset) const {
  MachineOperandType MOTy = MO.getType();

  switch (MOTy) {
  //@2
  default: llvm_unreachable("unknown operand type");
  case MachineOperand::MO_Register:
    // Ignore all implicit register operands.
    if (MO.isImplicit()) break;
    return MCOperand::createReg(MO.getReg());
  case MachineOperand::MO_Immediate:
    return MCOperand::createImm(MO.getImm() + offset);
  case MachineOperand::MO_RegisterMask:
    break;
 }

  return MCOperand();
}

void MYRISCVXMCInstLower::Lower(const MachineInstr *MI, MCInst &OutMI) const {
  OutMI.setOpcode(MI->getOpcode());

  for (unsigned i = 0, e = MI->getNumOperands(); i != e; ++i) {
    const MachineOperand &MO = MI->getOperand(i);
    MCOperand MCOp = LowerOperand(MO);

    if (MCOp.isValid())
      OutMI.addOperand(MCOp);
  }
}

ビルド試行中だが、今日はここまでで時間切れ。。。

ちなみに、tlbgenでどのようなファイルが生成されるかは、この図である程度まとめてある。

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

RISC-V 64-bit LLVM Backendを試す (6. Subtargetの確認とllcの動作確認)

LLVMにはすでにRISC-Vのバックエンドサポートが追加されている。しかし、勉強のために独自のRISC-V実装をLLVMに追加している。 まだ現時点ではビルドにすら成功していないが、徐々に修正しながら必要なものを追加してビルドしている。

前回までのビルドしたバイナリで、動作を確認してみる。

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

llc: /home/msyksphinz/work/riscv/llvm-myriscvx64/tools/llc/llc.cpp:462: int compileModule(char**, llvm::LLVMContext&): Assertion `Target && "Could not allocate target machine!"' failed.
Stack dump:

アサーションで落ちてしまった。ターゲットマシンは登録してあるはずだから、この動作は少し想定外だ。

GDBで動作を確認しながらおかしいところを追いかけていく。

./tools/llc/llc.cppアサーションが落ちていた。

  • ./tools/llc/llc.cpp
  std::unique_ptr<TargetMachine> Target(TheTarget->createTargetMachine(
      TheTriple.getTriple(), CPUStr, FeaturesStr, Options, getRelocModel(),
      getCodeModel(), OLvl));

  assert(Target && "Could not allocate target machine!");

ここの動作を追いかけていくと、TargetMachineを登録するルーチンにたどり着いた。

  • ./include/llvm/Support/TargetRegistry.h
  TargetMachine *createTargetMachine(StringRef TT, StringRef CPU,
                                     StringRef Features,
                                     const TargetOptions &Options,
                                     Optional<Reloc::Model> RM,
                                     Optional<CodeModel::Model> CM = None,
                                     CodeGenOpt::Level OL = CodeGenOpt::Default,
                                     bool JIT = false) const {
    if (!TargetMachineCtorFn)
      return nullptr;
    return TargetMachineCtorFn(*this, Triple(TT), CPU, Features, Options, RM,
                               CM, OL, JIT);
  }

ここでMYRISCVXの実装を確認してみると、よく見たらBigEndianのアーキテクチャのみを有効にして、LittleEndianの実装を作っていなかった。BigEndianの実装は登録してもいないので、Little Endiianの実装を登録した。

diff --git a/lib/Target/MYRISCVX/MCTargetDesc/MYRISCVXMCTargetDesc.h b/lib/Target/MYRISCVX/MCTargetDesc/MYRISCVXMCTargetDesc.h
index 24f8dbea..c89e6030 100644
--- a/lib/Target/MYRISCVX/MCTargetDesc/MYRISCVXMCTargetDesc.h
+++ b/lib/Target/MYRISCVX/MCTargetDesc/MYRISCVXMCTargetDesc.h
@@ -20,7 +20,6 @@ namespace llvm {
 class Target;
 class Triple;

-extern Target TheMYRISCVXTarget;
 extern Target TheMYRISCVXelTarget;

 } // End llvm namespace
diff --git a/lib/Target/MYRISCVX/TargetInfo/MYRISCVXTargetInfo.cpp b/lib/Target/MYRISCVX/TargetInfo/MYRISCVXTargetInfo.cpp
index b6efdf2a..4505588e 100644
--- a/lib/Target/MYRISCVX/TargetInfo/MYRISCVXTargetInfo.cpp
+++ b/lib/Target/MYRISCVX/TargetInfo/MYRISCVXTargetInfo.cpp
@@ -12,9 +12,9 @@
 #include "llvm/Support/TargetRegistry.h"
 using namespace llvm;

-Target llvm::TheMYRISCVXTarget, llvm::TheMYRISCVXelTarget;
+Target llvm::TheMYRISCVXelTarget;

 extern "C" void LLVMInitializeMYRISCVXTargetInfo() {
   RegisterTarget<Triple::myriscvx,
-                 /*HasJIT=*/true> Y(TheMYRISCVXTarget, "myriscvx", "MYRISCVX", "MYRISCVX");
+                 /*HasJIT=*/true> Y(TheMYRISCVXelTarget, "myriscvx", "MYRISCVX", "MYRISCVX");
 }

これで再度ビルドして確認してみる。

./bin/llc -march=myriscvx -relocation-model=pic -filetype=asm ch3.riscv64.bc

llc: /home/msyksphinz/work/riscv/llvm-riscv-msyksphinz/lib/CodeGen/LLVMTargetMachine.cpp:60: void llvm::LLVMTargetMachine::initAsmInfo(): Assertion `TmpAsmInfo && "MCAsmInfo not initialized. " "Make sure you include the correct TargetSelect.h" "and that InitializeAllTargetMCs() is being invoked!"' failed.
Stack dump:
0.      Program arguments: ./bin/llc -march=myriscvx -relocation-model=pic -filetype=asm ch3.riscv64.bc
#0 0x00007f7786f6e6b3 llvm::sys::PrintStackTrace(llvm::raw_ostream&) /home/msyksphinz/work/riscv/llvm-riscv-msyksphinz/lib/Support/Unix/Signals.inc:490:0
#1 0x00007f7786f6e746 PrintStackTraceSignalHandler(void*) /home/msyksphinz/work/riscv/llvm-riscv-msyksphinz/lib/Support/Unix/Signals.inc:554:0

エラーが出なくなった。AsmInfoはまだ実装していないので、この動作は想定通りだ。さて、次に進むぞ。

一応、-mcpu=helpで動作を確認しておく。

$ ./bin/llc -march=myriscvx -mcpu=help
Available CPUs for this target:

  myriscvx64_impl - Select the myriscvx64_impl processor.

Available features for this target:

  myriscvx64      - MYRISCVX64 ISA Support.
  myriscvx64_impl - MYRISCVX64 ISA Support.

ちゃんと登録したアーキテクチャが表示されている。どうやらうまくいったようだ。 この"Available CPUs for this target"と"Available features for this target"で"myriscvx64_impl"が被っているのは、"Available features for this target"の方が範囲が広いからだと思う。

ここで追加されたアーキテクチャは、lib/Target/MYRISCVX.tdで追加していたものだ。SubtargetFeatureとしてプロセッサモデルはmyriscvx64_impl, アーキテクチャモデルとしてはmyriscvxを追加している。

def FeatureMYRISCVX64 : SubtargetFeature<"myriscvx64", "HasArchX64", "true", "MYRISCVX64 ISA Support">;

def FeatureMYRISCVX64_CPU : SubtargetFeature<"myriscvx64_impl", "MYRISCVXArchVersion",
                                "MYRISCVX64", "MYRISCVX64 ISA Support",
                                [FeatureMYRISCVX64]>;

def : ProcessorModel<"myriscvx64_impl", NoSchedModel, [FeatureMYRISCVX64_CPU]>;

f:id:msyksphinz:20190109233324p:plain
llcのオプションとして独自アーキテクチャ実装myriscvxが追加された。

Chiselで設計したHWのコンパイルオプションで生成されるモジュールを切り替える方法

Chiselという言語はVerilogとは似て非なる。 ハードウェアを記述する言語ではあるが、Verilogと同じように考えているとどのようにハードウェアを作ればよいのかわからなくなったり、Chiselの本来のうま味を引き出せなくなる。

前回は、Chiselの記述中にVerilogモデルを埋め込んだり、Verilogファイルを参照してモジュールとして埋め込む方法について調査した。

この方法を調査した目的は、「シミュレーションは全てChiselを使う(Verilogを使わない分変換コストが削減される)」と「合成用のデザインにはVerilogを埋め込む」という、外部のスイッチにより、生成されるVerilogを切り替えたいためだ。 これを達成するためには、

を切り替える方法について考える。

まず問題となるのが、ブラックボックス側は基底クラスがBlackBox()で、Chisel側はModule()クラスが基底クラスになっていることだ。 BlackBox()クラスはベースクラスがBaseModuleであり、一方でModuleクラスもBaseModuleであるため、どうにかして接続できるはずだ。

https://chisel.eecs.berkeley.edu/api/latest/chisel3/core/BaseBlackBox.html

まずは、モジュールの切り替え方だが、パラメータconf.debugにより接続するモジュールを切り替える。

class CpuMemory [Conf <: RVConfig](conf: Conf) extends Module {
  val io = IO(new Bundle {
    val mem = new MemoryIo(conf)
  })

  val memory = conf.debug match {
    case true => {
      val mem_model = Module(new MemoryModel(conf))
      mem_model
    }
    case _ => {
      val mem_model = Module(new MemoryInlineBoxCore(conf))
      mem_model.io.clock <> clock
      mem_model
    }
  }

  memory.io.mem <> io.mem
}

conf.debugがTrueならば、Chiselで記述されたシミュレーション用のMemoryModelクラスが使用され、そうでなければMemoryInlineBox()が使用される。

しかし、MemoryInlineBox()クラスがBlackBoxを基底クラスとしており、どうも接続時に怒られる。

[error] /home/msyksphinz/work/riscv/chisel/minicpu/src/main/scala/cpu/memory.scala:34:10: value io is not a member of chisel3.core.BaseModule
[error]   memory.io.mem <> io.mem
[error]          ^

そこで、ブラックボックスをラップするモジュールをさらに一つ作成し、切り替えの境界はModuleクラスで統一する。

  val memory = conf.debug match {
    case true => {
      val mem_model = Module(new MemoryModel(conf))
      mem_model
    }
    case _ => {
      val mem_model = Module(new MemoryInlineBox(conf))
      mem_model
    }
  }
  memory.io.mem <> io.mem

ここで注意しなければならないのが、Chiselのモジュールクラスは明示的なクロックとリセットが存在しない。一方でBlackBoxモジュールはクロックとリセットがそもそも存在しないため、明示的に接続する必要がある。

調べてみると、Moduleクラスはclockという変数が暗黙的にクロックとして使用されているらしい。そこで、BlackBoxクラスのIOとしてclockポートを定義する。

class MemoryInlineBox [Conf <: RVConfig](conf: Conf) extends Module {
  val io = IO(new Bundle {
    val mem = new MemoryIo(conf)
  })
  val mem_black_box_core = Module(new MemoryInlineBoxCore(conf))

  mem_black_box_core.io.clock <> clock  // 明示的にクロックを接続する。
  mem_black_box_core.io.mem <> io.mem
}
class MemoryInlineBoxCore [Conf <: RVConfig](conf: Conf) extends BlackBox with HasBlackBoxInline {
  val io = IO(new Bundle {
    val clock = Input(Clock())
    val mem = new MemoryIo(conf)
  })

  setInline("MemoryBlackBox.v",
    s"""...

これでVerilogを生成すると、生成されたVerilogで接続されていることが分かる。

module CpuMemory( // @[:@35.2]
  input         clock, // @[:@36.4]
  input         io_mem_inst_bus_req, // @[:@38.4]
  input  [15:0] io_mem_inst_bus_addr, // @[:@38.4]
...
  MemoryInlineBox memory ( // @[memory.scala 28:29:@40.4]
    .clock(memory_clock),
    .io_mem_inst_bus_req(memory_io_mem_inst_bus_req),
    .io_mem_inst_bus_addr(memory_io_mem_inst_bus_addr),
...
  assign memory_clock = clock; // @[:@41.4]

これで、Chisel内で、スイッチを使用したモジュールの切り替えと、Verilogブラックボックスとの切り替えが出来るようになった。

f:id:msyksphinz:20190108001048p:plain
CpuCoreと3種類のChiselモジュールの切り替え

2019/01/31追記。結局、BlackBox入りのモジュールを切り替えるために何個もクラスを経由させなければならないのをどうにかできないかと思ったが、よく考えたらBlackBoxを継承したクラスはModuleクラスの中に作ってしまえば良いんだ。これなら楽。

class ModuleWrapper extends Module {
  private clas ModuleBlkBoxWrapper extends Module {
    val u_mem = Module(new ModuleBlackBox())
    u_mem.io <> io
  }
  val u_mem = Conf.Debug match {
    true => {
      val u_mem = Module(new ModuleReal ())
      u_mem
    }
    _ => {
      val u_mem = Module(new ModuleBlkBoxWrapper())
      u_mem
    }
  }
}

class ModuleBlackBox extends BlackBox with HasBlackBoxInline {
  setInline("MemoryBlackBox.v",
    s"""...
}

RISC-V 64-bit LLVM Backendを試す (5. 基本的なファイルの追加とビルド)

LLVMにはすでにRISC-Vのバックエンドサポートが追加されている。しかし、勉強のために独自のRISC-V実装をLLVMに追加している。 まだ現時点ではビルドにすら成功していないが、徐々に修正しながら必要なものを追加してビルドしている。

ビルドを通すためには、基本的なファイルを追加しする必要がある。少しずつファイルを追加しながらビルドを進めていく。

  • 命令追加のためのファイル群:
    • MYRISCVXInstrFormats.td
    • MYRISCVXInstrInfo.cpp
    • MYRISCVXInstrInfo.h
    • MYRISCVXInstrInfo.td
    • MYRISCVXSEInstrInfo.cpp
    • MYRISCVXSEInstrInfo.h
  • レジスタ追加のためのファイル群:
    • MYRISCVXRegisterInfo.cpp
    • MYRISCVXRegisterInfo.h
    • MYRISCVXRegisterInfo.td
    • MYRISCVXSERegisterInfo.cpp
    • MYRISCVXSERegisterInfo.h
  • 関数フレームのためのファイル群:
    • MYRISCVXFrameLowering.cpp
    • MYRISCVXFrameLowering.h
    • MYRISCVXSEFrameLowering.cpp
    • MYRISCVXSEFrameLowering.h
  • 命令選択のためのファイル群:
    • MYRISCVXISelLowering.cpp
    • MYRISCVXISelLowering.h
    • MYRISCVXSEISelLowering.cpp
    • MYRISCVXSEISelLowering.h

大量にMYRISCVXSExxxというファイルを作ったのだが、どうやらSEというのは標準の32bitアーキテクチャのことを示すらしい。 あれ、64bitを前提に作ってしまったが間違っていたかな。例えば、MIPSだとMips16, MipsSE(=32-bitクラス), Mips64のクラスが生成されるそうだ。

あと、td(Target Description)ファイルを作ることによって結構なファイルが自動生成される。とりあえず今のところ分かるのはこんな感じかな。 ビルドすると以下の図のようなイメージでファイルが生成される。

f:id:msyksphinz:20190107012713p:plain
tdファイルとそれに基づいて自動生成されるファイル群(.incファイル)

ABIの追加

RISC-VのABIについては資料を参考にしながら進めていく。RISC-Vには関数の引数として使えるレジスタがなんと8個も存在する。

f:id:msyksphinz:20190107004435p:plain
RISC-Vには、x10-x17までの8個のレジスタが引数として使用できる。
  • lib/Target/MYRISCVX/MYRISCVXCallingConv.td
def RetCC_MYRISCVXEABI : CallingConv<[
  // i32 are returned in registers A0, A1, A2, A3, A4, A5, A6, A7
  CCIfType<[i32], CCAssignToReg<[A0, A1, A2, A3, A4, A5, A6, A7]>>
]>;

ABIについてはCpu0のものを参考にした。Cpu0の資料には以下のように書いてある。とりあえずO32をベースにして改造することにした。

Function call — Tutorial: Creating an LLVM Backend for the Cpu0 Architecture

Mentioned in last section, option llc -cpu0-s32-calls=true uses S32 calling convention which passes all arguements at registers while option llc -cpu0-s32-calls=false uses O32 pass first two arguments at registers and other arguments at stack. The result as follows, S32は全ての引数をスタック経由で渡すが、O32は最初の2つのレジスタレジスタで渡しての残りはスタック経由で渡す。

  • ADJCALLSTACKDOWN / ADJCALLSTACKUPの追加。これはどのアーキテクチャでも追加してあるらしい。

Function call — Tutorial: Creating an LLVM Backend for the Cpu0 Architecture

Pseudo hook instruction ADJCALLSTACKDOWN and ADJCALLSTACKUP DAG.getCALLSEQ_START() and DAG.getCALLSEQ_END() are set before and after the “for loop”, respectively, they insert CALLSEQ_START, CALLSEQ_END, and translate them into pseudo machine instructions !ADJCALLSTACKDOWN, !ADJCALLSTACKUP later according Cpu0InstrInfo.td definition as follows.

  • lib/Target/MYRISCVX/MYRISCVXInstrInfo.td
let Defs = [SP], Uses = [SP] in {
def ADJCALLSTACKDOWN : MYRISCVXPseudo<(outs), (ins i32imm:$amt1, i32imm:$amt2),
                                  "!ADJCALLSTACKDOWN $amt1",
                                  [(callseq_start timm:$amt1, timm:$amt2)]>;
def ADJCALLSTACKUP   : MYRISCVXPseudo<(outs), (ins i32imm:$amt1, i32imm:$amt2),
                                  "!ADJCALLSTACKUP $amt1",
                                  [(callseq_end timm:$amt1, timm:$amt2)]>;
} // Defs = [SP], Uses = [SP]

callseq_startとcallseq_endは以下のようになっている。これも、Cpu0から取ってきた。

  • lib/Target/MYRISCVX/MYRISCVXInstrInfo.td
def SDT_MYRISCVXCallSeqStart : SDCallSeqStart<[SDTCisVT<0, i32>]>;
def SDT_MYRISCVXCallSeqEnd   : SDCallSeqEnd<[SDTCisVT<0, i32>, SDTCisVT<1, i32>]>;
...
// These are target-independent nodes, but have target-specific formats.
def callseq_start : SDNode<"ISD::CALLSEQ_START", SDT_MYRISCVXCallSeqStart,
                           [SDNPHasChain, SDNPOutGlue]>;
def callseq_end   : SDNode<"ISD::CALLSEQ_END", SDT_MYRISCVXCallSeqEnd,
                           [SDNPHasChain, SDNPOptInGlue, SDNPOutGlue]>;
  • MYRISCVXISelLowering.cpp / MYRISCVXSEISelLowering.cpp を追加した。まだほとんど空である。
    • このファイルはMYRISCVXにおけるLLVMのコードをDAGに変換するための関数群らしい。
  • MYRISCVXSERegisterInfo.h を追加した。まだほとんど空である。

    • MYRISCVXのレジスタ情報を含んでいる?
    • getCalleeSavedRegs()
    • getCallPreservedMask()
    • getReservedRegs()
    • eliminateFrameIndex()
    • requiresRegisterScavenging()
    • trackLivenessAfterRegAlloc()
    • getFrameRegister()
  • MYRISCVXInstrInfo.cpp / MYRISCVXSEInstrInfo.cpp

    • MYRISCVXの命令情報を含んでいる?
    • create()
    • getRegisterInfo()
    • GetInstSizeInBytes()
    • createMYRISCVXSEInstrInfo()
  • MYRISCVSEInstrInfo.h

  class MYRISCVXSEInstrInfo : public MYRISCVXInstrInfo {
    const MYRISCVXSERegisterInfo RI;

 public:
    explicit MYRISCVXSEInstrInfo(const MYRISCVXSubtarget &STI);

    const MYRISCVXRegisterInfo &getRegisterInfo() const override;


  };
  • MYRISCVSERegisterInfo.h
  class MYRISCVXSERegisterInfo : public MYRISCVXRegisterInfo {
 public:
    MYRISCVXSERegisterInfo(const MYRISCVXSubtarget &Subtarget);

    const TargetRegisterClass *intRegClass(unsigned Size) const override;
  };
  • MYRISCVXFrameLowering.cpp / MYRISCVXSEFrameLowering.cpp を追加した。まだ殆ど空である。
    • これは関数のフレームを処理するためのファイルかな?
    • create()
    • hasFP()
    • eliminateCallFramePseudoInstr()
    • createMYRISCVXSEFrameLowering()

と、とりあえず見よう見まねで最低限のファイルだけを追加してなんとかビルドが通るまでになった。 あとは、動きを確認しながら機能を追加していこう。

RISC-V 64-bit LLVM Backendを試す (4. SubtargetとProcessor Model)

LLVMにはすでにRISC-Vのバックエンドサポートが追加されている。しかし、勉強のために独自のRISC-V実装をLLVMに追加している。 まだ現時点ではビルドにすら成功していないが、徐々に修正しながら必要なものを追加してビルドしている。

まず、ターゲットアーキテクチャについては、さすがにRISCV_msyksphinzは長すぎて実装している自分が嫌になってきたので、名前を変更した。 MYRISCVXという名前に変更して再スタートだ。

ビルド試行をしていると、サブターゲットがないと怒られてしまった。サブターゲットとはなんだか良く分からなかったのだが、どうやらメインとなるターゲット以外にそのターゲットに準ずる流派や、細かい実装の違い、ISAの機能の分離などが可能らしい。

情報が無くて苦労しているのだが、例えば、RISC-Vの本物の実装には以下のようなサブターゲットが使用されていた。

この例では、RISC-VのISAのモジュールの部分であるM(乗除算命令のサポート)と、A(アトミック命令)のサポートをサブターゲットとして定義している(もちろんそれ以外にも、FやDなどのモジュールもサブターゲットとして定義されている。このサブターゲットを最低1つでも定義しておかないと、LLVMのターゲット生成時にエラー(というか、実際にはクラスの中身が空)で怒られてしまう。

  • lib/Target/RISCV/RISCV.td
//===----------------------------------------------------------------------===//
// RISC-V subtarget features and instruction predicates.
//===----------------------------------------------------------------------===//
def FeatureStdExtM
    : SubtargetFeature<"m", "HasStdExtM", "true",
                       "'M' (Integer Multiplication and Division)">;
def FeatureStdExtA
    : SubtargetFeature<"a", "HasStdExtA", "true",
                       "'A' (Atomic Instructions)">;

これ以外に、プロセッサのモデルについても定義することができる。このプロセッサモデルについても最低1個は定義しておく必要がある。

  • lib/Target/RISCV/RISCV.td
//===----------------------------------------------------------------------===//
// RISC-V processors supported.
//===----------------------------------------------------------------------===//
def : ProcessorModel<"generic-rv32", NoSchedModel, []>;
def : ProcessorModel<"generic-rv64", NoSchedModel, [Feature64Bit]>;

さらにCpu0の実装をパクッて、以下のようなMYRISCVXのサブターゲットを定義する。 つまり、ここではFeatureMYRISCVX64というサブターゲット(64bitのRISC-V ISAをサポートするという意味(自分で勝手に付ける))を追加し、さらに、MYRISCVXのプロセッサモデルとしてmyriscv64_implを定義した。 このプロセッサはFeatureMYRISCVX64のISAサブターゲットをサポートしており、またCPUのパイプラインモデルは含まないという意味だ。

  • lib/Target/MYRISCV/MYRISCV.td
def FeatureMYRISCVX64 : SubtargetFeature<"x64", "HasArchX64", "true", "MYRISCVX64 ISA Support">;
def : ProcessorModel<"myriscv64_impl", NoSchedModel, [FeatureMYRISCVX64]>;

これでLLVMのビルドを行うと、${ビルドルートディレクトリ}/lib/Target/MYRISCVX/MYRISCVXGenSubtargetInfo.incにこの情報が追加された。

  • ${ビルドルートディレクトリ}/lib/Target/MYRISCVX/MYRISCVXGenSubtargetInfo.inc
namespace llvm {
// Sorted (by key) array of values for CPU features.
extern const llvm::SubtargetFeatureKV MYRISCVXFeatureKV[] = {
  { "x64", "MYRISCVX64 ISA Support", { MYRISCVX::FeatureMYRISCVX64 }, { } },
};

// Sorted (by key) array of values for CPU subtype.
extern const llvm::SubtargetFeatureKV MYRISCVXSubTypeKV[] = {
  { "myriscv64_impl", "Select the myriscv64_impl processor", { MYRISCVX::FeatureMYRISCVX64 }, { } },
};

この辺のクラスの関係は、Cpu0の資料を見ながら進める。

https://jonathan2251.github.io/lbd/_images/1.png
https://jonathan2251.github.io/lbd/_images/2.png

サブターゲットの識別は、見よう見まねで以下のように実装している。

  • MYRISCVSubtarget.cpp
MYRISCVXSubtarget &
MYRISCVXSubtarget::initializeSubtargetDependencies(StringRef CPU, StringRef FS,
                                               const TargetMachine &TM) {
  if (TargetTriple.getArch() == Triple::myriscvx) {
    if (CPU.empty() || CPU == "generic") {
      CPU = "myriscvx";
    }
    else if (CPU == "help") {
      CPU = "";
      return *this;
    }
    else if (CPU != "myriscvx") {
      CPU = "myriscvx";
    }
  }
  else {
    errs() << "!!!Error, TargetTriple.getArch() = " << TargetTriple.getArch()
           <<  "CPU = " << CPU << "\n";
    exit(0);
  }
  • 追加したファイル群

MYRISCVXのビルドを通すために、さらに以下のファイルを追加した。

  • MYRISCVXFrameLowering.cpp
  • MYRISCVXFrameLowering.h
  • MYRISCVXMachineFunction.cpp
  • MYRISCVXMachineFunction.h
  • MYRISCVXRegisterInfo.cpp
  • MYRISCVXRegisterInfo.h

MYRISCVXRegisterInfo.cppをいじっていた。保存レジスタを設定する。 本当はABIでCalling Conventionを定義していたらそれを設定すればよいのだが、まだ設定していないのでまだ何も実装していない。

getReservedRegs()では、RISC-Vのzero, gp, tp, sp, ra, s0(つまりfp)を設定してみた。

// pure virtual method
//@getReservedRegs {
BitVector MYRISCVXRegisterInfo::
getReservedRegs(const MachineFunction &MF) const {
//@getReservedRegs body {
  static const uint16_t ReservedCPURegs[] = {
    MYRISCVX::ZERO, MYRISCVX::GP, MYRISCVX::TP, MYRISCVX::SP, MYRISCVX::RA, MYRISCVX::S0
  };
  BitVector Reserved(getNumRegs());

  for (unsigned I = 0; I < array_lengthof(ReservedCPURegs); ++I)
    Reserved.set(ReservedCPURegs[I]);

  return Reserved;
}

Chiselの中にVerilogを埋め込む方法

ChiselはScalaをベースにしたハードウェア記述言語で、より高位な記法を使ってハードウェアを設計しようという考え方を持っている。

しかし、どうしてもVerilogを使いたい場合、あるいはある部品は既存のVerilogで記述されたものを使いたいと思うときがある。 例えば、Chiselを使えばメモリも記述することができるが、実際にはSRAMに置き換えたい場合であったり、FPGAで推論しやすくするためにVerilogで書いたモジュールに置き換えたい場合がある。このような場合に、どのようにChiselを記述するのかについて調査した。

ChiselにVerilogを埋め込むためのBlackBoxモジュール

ChiselにVerilogを埋め込むための方法は2つ存在する。

f:id:msyksphinz:20190105142514p:plain
HasBlackBoxInlineとHasBlackBoxResourceの違い

どちらの方法を使うかは、その状況下で使い分ければいいと思う。 例えば、今回は以下のようなメモリモジュールをVerilogで置き換えるためにはどのようにしたらよいのか考えた。

class Memory [Conf <: RVConfig](conf: Conf) extends Module {
  val io = new MemoryIo(conf)
...

インラインでVerilogを記述する方法

Memoryモジュールを置き換えるために、内部にインラインでVerilogを記述するためにはBlackBoxモジュールを継承し、HasBlackBoxInlineトレイトを追加する。

class MemoryBlackBox [Conf <: RVConfig](conf: Conf) extends BlackBox with HasBlackBoxInline {
  val io = IO(new Bundle {
    val clock = Input(Clock())
    val mem = new MemoryIo(conf)
  })

ここで、I/Oにクロックを追加している。理由は、このブラックボックスのモジュールは明示的にクロックとリセットを追加しないと自動的に挿入されないためだ。 つまり、明示的にポートを追加しないと内部でクロックを使用できない。

BlackBoxes · freechipsproject/chisel3 Wiki · GitHub

Unlike Module, BlackBox has no implicit clock and reset. Ports declared in the IO Bundle will be generated with the requested name (ie. no preceding io_).

そして、内部にVerilogコードをそのまま埋め込んでいく。これにはsetInlineを使用する。

  setInline("MemoryBlackBox.v",
    s"""
|module MemoryBlackBox
|  (
|   input logic         clock,
|   input logic         mem_inst_bus_req,
...
|
|endmodule // MemoryBlackBox
    """.stripMargin)
}

これで完了だ。クロックポートを追加したので、メモリのインタフェースはmemという変数でラップされてしまった。オリジナル(Chiselで記述した)Memoryモジュールとのポート互換性を合わせたければ、Memoryモジュール側も同じようにI/Oポートをラップさせるのが良いと思う。

class Memory [Conf <: RVConfig](conf: Conf) extends Module {
  val io = IO(new Bundle {
    val mem = new MemoryIo(conf)
  })

これでVerilogを生成すると、CpuTop.vというCPU本体のモジュールと、MemoryBlackBox.vという別にファイルが生成される。 さらに、ブラックボックスのファイル一覧を示すblack_box_verilog_files.fというファイルも生成される。

ls -lt
MemoryBlackBox.v
black_box_verilog_files.f
CpuTop.v
  • MemoryBlackBox.v
module MemoryBlackBox
  (
   input logic         clock,
   input logic         mem_inst_bus_req,
   input logic [15:0]  mem_inst_bus_addr,
   output logic        mem_inst_bus_ack,
   output logic [31:0] mem_inst_bus_rddata,
   input logic         mem_data_bus_req,
   input logic [1:0]   mem_data_bus_cmd,
   input logic [15:0]  mem_data_bus_addr,
...

外部のVerilogファイルを指定する方法

こちらの方がリソースを再利用する方法としては優れていると思う。

インラインする方法とは異なるトレイト(HasBlackBoxResource)を使用する。Verilogファイルは、setResourceで場所を指定する。 こちらもクロックは明示的に追加しないとポートとして現れないので、IOバンドルとしてラップする。

このsetResourceで指定されるVerilogファイルは、プロジェクトのルートディレクトリからsrc/{main,test}/resources/に配置されている必要がある。 今回は、src/main/resources/memory_real.vVerilogファイルを配置した。

class MemoryResourceBox [Conf <: RVConfig](conf: Conf) extends BlackBox with HasBlackBoxResource {
  val io = IO(new Bundle {
    val clock = Input(Clock())
    val mem = new MemoryIo(conf)
  })
  setResource ("/memory_real.v")
}
  • `src/main/resources/memory_real.v``
module MemoryResourceBox
  (
   input logic         clock,
   input logic         mem_inst_bus_req,
   input logic [15:0]  mem_inst_bus_addr,
...

同様にVerilogファイルを生成すると、CpuTop.vというCPU本体のモジュールと一緒に、プロジェクトのルートディレクトリにもmemory_real.vが生成され、さらにブラックボックスのファイルリストを含んでいるblack_box_verilog_files.fも生成された。

RISC-V 64-bit LLVM Backendを試す (3. TargetMachineとTargetInfoを追加する)

LLVMについて理解するために、RISC-Vをネタにしていろいろ勉強してみたい。 Release 7.0では一応RISC-Vの32-bitと64-bit版がサポートされているようなので、ビルドに追加して様子を見てみる。

f:id:msyksphinz:20190104013152p:plain

次はRISCV_msyksphinzのターゲットアーキテクチャについて定義していく。 Cpu0をベースにして、少しずつコードを追加していこう。

computeDataLayout(const Triple &TT, StringRef CPU, const TargetOptions &Options, bool isLittle)

アーキテクチャのデータレイアウトを定義する関数。データレイアウトについてはさっぱり知らなかったので、資料を参考にしながら読み解いていく。

LLVM Language Reference Manual — LLVM 8 documentation

static std::string computeDataLayout(const Triple &TT, StringRef CPU,
                                     const TargetOptions &Options,
                                     bool isLittle) {

データレイアウトがリトルエンディアンならば"e", ビッグエンディアンならば "E"とする。 RISC-Vにはリトルエンディアンしか存在しないので"e"で良いと思う。

  if (isLittle)
    Ret += "e";
  else
    Ret += "E";

C++などでも使用されるマングリング?を指定する。ここで"m"となっているので、MIPS形式のマングリングが適用されるのだと思う。

  Ret += "-m:m";

ポインタと変数のアラインメントについて。Cpu0では、以下のように定義されていた。 ポインタについては32ビット長で、32bitアライメントを前提とする。その次の行は、8bit、16bitはそれぞれそのビット長でアライメントしてよいが、実際には32bitでアライメントしてほしいことを意味している?

  // Pointers are 32 bit on some ABIs.
  Ret += "-p:32:32";

  // 8 and 16 bit integers only need to have natural alignment, but try to
  // align them to 32 bits. 64 bit integers have natural alignment.
  Ret += "-i8:8:32-i16:16:32-i64:64";
  // 32 bit registers are always available and the stack is at least 64 bit
  // aligned.
  Ret += "-n32-S64";

RISCVの実装を参考にすると、以下のようになっているので、

  if (TT.isArch64Bit()) {
    return "e-m:e-p:64:64-i64:64-i128:128-n64-S128";
  } else {
    assert(TT.isArch32Bit() && "only RV32 and RV64 are currently supported");
    return "e-m:e-p:32:32-i64:64-n32-S128";
  }
  • RV64のときは64bit長でポインタをアライメント、変数は64bitでアライメントすべき。128bitの変数もアライメントを定義する。
  • RV32のときは32bit長でポインタをアライメント、64bitと128bitの変数もアライメントを定義する。

RISCV_msyksphinzアーキテクチャインスタンス

そして、このComptueDataLayoutを使用してアーキテクチャを定義する関数は2つ用意されている。

RISCV_msyksphinzebTargetMachine::RISCV_msyksphinzebTargetMachine(const Target &T, const Triple &TT,
                                         StringRef CPU, StringRef FS,
                                         const TargetOptions &Options,
                                         Optional<Reloc::Model> RM,
                                         CodeModel::Model CM,
                                         CodeGenOpt::Level OL)
    : RISCV_msyksphinzTargetMachine(T, TT, CPU, FS, Options, RM, CM, OL, false) {}

void RISCV_msyksphinzelTargetMachine::anchor() { }

RISCV_msyksphinzelTargetMachine::RISCV_msyksphinzelTargetMachine(const Target &T, const Triple &TT,
                                         StringRef CPU, StringRef FS,
                                         const TargetOptions &Options,
                                         Optional<Reloc::Model> RM,
                                         CodeModel::Model CM,
                                         CodeGenOpt::Level OL)
    : RISCV_msyksphinzTargetMachine(T, TT, CPU, FS, Options, RM, CM, OL, true) {}

ここで、ビッグエンディアンは必要ないので削除してしまおう。RISCV_msyksphinzebTargetMachineの方だ。ebとelが微妙に異なる。

そして、LLVMのターゲットを作成するのはLLVMInitializeRISCV_msyksphinzTarget()である。これもビッグエンディアン用が存在したのだが削除した。

extern "C" void LLVMInitializeRISCV_msyksphinzTarget() {
  // Register the target.
  //- Little endian Target Machine
  RegisterTargetMachine<RISCV_msyksphinzelTargetMachine> Y(TheRISCV_msyksphinzelTarget);
}