FPGA開発日記

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

RVC命令に対応したRISC-VアウトオブオーダコアBOOM v2.2リリース (1. ビルド & シミュレーション試行)

RISC-VのアウトオブオーダプロセッサであるBOOMのVersion 2.2がリリースされた。

リリース通知から、アップデート内容を確認してみると、

  • 全てのコンフィグレーションでRISC-V Compressed("RVC")命令をサポート
  • PMPレジスタのサポート
    • 確かにこれまでPMPレジスタにアクセスすると必ず例外が発生していた。
    • 多くのプロジェクトではPMPレジスタが使用されている(Keystoneなど)
  • ノンブロッキングL1キャッシュはRocket-Chipからフォークされた。
    • これはPMPサポートのために必要。しかしさらに強力なDCacheを搭載する予定。
  • マイナーなCSR命令の修正
  • ReadTheDocs documentation を作成 https://docs.boom-core.org/en/latest/
  • ウェブサイトを作り直し boom-core.org
  • BOOMのリポジトリはriscv-boomに移動しなおした。
    • これまではEsperantoTechのリポジトリとして管理されていたのだが、riscv-boomに戻ったのか。
  • BOOMによるSpectre攻撃のデモンストレーション https://github.com/riscv-boom/boom-attacks
  • CircleCIによるCI環境の構築
  • Chisel3の構文に書き換えを実施。

これはなかなかに面白そうだ。早速、使ってみよう。

github.com

ちなみに、今回はリビジョン 1e5bc7c を使って動作確認を行った。

ドキュメントは以下にすべて用意されている。といってもこれは内部パイプラインのドキュメントだ。これは便利。

docs.boom-core.org

ビルドの方法はこれまでの通り。資料を見ながら進める。

git clone https://github.com/riscv-boom/boom-template.git
cd boom-template
./scripts/init-submodules.sh

export RISCV=${HOME}/riscv_boomv22
export PATH=$RISCV/bin:$PATH

./scripts/build-tools.sh

これで、環境が整ったのでVerilatorでビルドし、シミュレーションを実行してみる。

cd verisim
make

早速Dhrystoneを実行したのだが、途中で失敗してしまった。

make output/dhrystone.riscv.out
  • output/dhrystone.riscv.out
using random seed 1548641781
This emulator compiled with JTAG Remote Bitbang client. To enable, use +jtag_rbb_enable=1.
Listening on port 61599
  first_pc: 0x0080001140 last_npc: 0x0080001142  Assertion failed: [fetchmonitor] A non-cfi instruction is followed by the wrong instruction.
    at fetchmonitor.scala:146 assert (first_pc === last_npc,

そこで、COMMITログをONにして様子を見てみることにした。

diff --git a/src/main/scala/common/consts.scala b/src/main/scala/common/consts.scala
index 3abc41d..78c7ca8 100755
--- a/src/main/scala/common/consts.scala
+++ b/src/main/scala/common/consts.scala
@@ -23,7 +23,7 @@ import freechips.rocketchip.util.Str
 trait BOOMDebugConstants
 {
    val DEBUG_PRINTF        = false // use the Chisel printf functionality
-   val COMMIT_LOG_PRINTF   = false // dump commit state, for comparision against ISA sim
+   val COMMIT_LOG_PRINTF   = true  // dump commit state, for comparision against ISA sim
    val O3PIPEVIEW_PRINTF   = false // dump trace for O3PipeView from gem5
    val O3_CYCLE_TIME       = (1000)// "cycle" time expected by o3pipeview.py

Dhrystoneの実行結果のログを確認すると、以下の命令を実行して停止している。

3 0x0000000080001b32 (0x84818d13) x26 0x00000000800028f0
3 0x0000000080001b36 (0x00001997) x19 0x0000000080002b36
3 0x0000000080001b3a (0xda298993) x19 0x00000000800028d8
3 0x0000000080001b3e (0x490a1497) x 9 0x00000000c90a2b3e
  first_pc: 0x0080001b40 last_npc: 0x0080001b42  Assertion failed: [fetchmonitor] A non-cfi instruction is followed by the wrong instruction.
    at fetchmonitor.scala:146 assert (first_pc === last_npc,

うーん、0x1b3eの次は0x1b42で、その周辺も命令も問題なさそうだが、、、

riscv64-unknown-elf-objdump -D dhrystone.riscv | less
...
    80001b32:   84818d13                addi    s10,gp,-1976 # 800028f0 <Ptr_Glob>
    80001b36:   00001997                auipc   s3,0x1
    80001b3a:   da298993                addi    s3,s3,-606 # 800028d8 <Ch_2_Glob>
    80001b3e:   00001497                auipc   s1,0x1
    80001b42:   baa48493                addi    s1,s1,-1110 # 800026e8 <main+0xcfc>
    80001b46:   f3043903                ld      s2,-208(s0)
    80001b4a:   00000517                auipc   a0,0x0
    80001b4e:   67e50513                addi    a0,a0,1662 # 800021c8 <main+0x7dc>
    80001b52:   85ca                    mv      a1,s2
    80001b54:   e24ff0ef                jal     ra,80001178 <debug_printf>
    80001b58:   4505                    li      a0,1

GitHubのページにはrv64ui-p-addの結果のみ書いてあるのだが、まだ調整が必要なのかな。

Western DigitalのRISC-VコアSweRV-EH1 (1. SweRVの環境構築とVivado論理合成)

Western DigitalからオリジナルのRISC-VコアSweRVがリリースされた。

2019/06/20追記。Swerv-EH1 CoreはCHIPS Allianceプロジェクトのサブプロジェクトとして正式にGitHubリポジトリが移された。Swerv-EH 1.1が公開されている。

github.com

https://github.com/westerndigitalcorporation/swerv_eh1

基本的なスペックは以下の通り。これはSweRVの資料から引っ張ってきた。

https://github.com/westerndigitalcorporation/swerv_eh1/tree/master/docs

  • サポートアーキテクチャ : RV32IMCZifencei_Zicsr
    • そんな名称聞いたことがないが、32-bit, 整数命令、乗除算命令、命令フェッチフェンス、CSRサポート の意味らしい。
      ECC対応のITCM, DTCMをオプションでサポート
  • 4wayセットアソシアティブ命令キャッシュ(パリティECCをサポート)をオプションでサポート
  • 255個の外部割込みをサポートするプログラマブル割り込みコントローラをオプションでサポート
  • システムバスインタフェースは4種類 : 命令フェッチ、データアクセス、デバッグアクセス、TCM向けの外部DMA
    • 64-bit幅のAXIか、AHB-Liteをサポート
  • RISC-Vデバッグ仕様に基づいたコアデバッグユニットをサポート
  • 28nmプロセスで1GHz動作をターゲットとする。

仕様を読む限りはハイパフォーマンス向けでは無く、コントローラ用途のマイコンに思える。仕様的にも結構手を加えている印象。

以下の図は同資料から引用。内部のコンポーネントとインタフェースについて。

f:id:msyksphinz:20190128144153p:plain
SweRVの構成 (RISC-V_SweRV_EH1_PRM.pdf より抜粋)

ビルド方法

cd ${HOME}/work/riscv/
git clone https://github.com/westerndigitalcorporation/swerv_eh1.git
export RV_ROOT=${HOME}/work/riscv/swerv_eh1

このRV_ROOTという変数は必ず必要らしい。ここからはコンフィグレーションを設定する。

$ ${RV_ROOT}/configs/swerv.config -h

Main configuration database for SWERV

This script documents, and generates the {`#} define/include files for verilog/assembly/backend flows

User options:
     -target = { default}
        use default settings for one of the targets

     -set=var=value
        set arbitrary variable to a value
     -unset=var
        unset any definitions for var
     -snapshot=name
        name the configuration (only if no -target specified)

Additional direct options for the following variables:
...

ちなみに、DCCMはData Closely Coupled Memory(=DTCM)のことらしい。TCMのことか。 ICCMはInstruction Closely Coupled Memory(=ICTM)でもある。

とりあえずデフォルトで構成した。

${RV_ROOT}/configs/swerv.config
swerv: Using target "default"
swerv: target                    = default
swerv: ret_stack_size            = 4
swerv: btb_size                  = 32
swerv: bht_size                  = 128
swerv: dccm_enable               = 1
swerv: dccm_region               = 0xf
swerv: dccm_offset               = 0x80000
swerv: dccm_size                 = 32
swerv: dccm_num_banks            = 8
swerv: iccm_enable               = 0
swerv: iccm_region               = 0xe
swerv: iccm_offset               = 0xe000000
swerv: iccm_size                 = 512
swerv: iccm_num_banks            = 8
swerv: icache_enable             = 1
swerv: icache_ecc                = 0
swerv: icache_size               = 16
swerv: pic_2cycle                = 0
swerv: pic_region                = 0xf
swerv: pic_offset                = 0x100000
swerv: pic_size                  = 32
swerv: pic_total_int             = 8
swerv: lsu_stbuf_depth           = 8
swerv: lsu_wrbuf_depth           = 4
swerv: dma_buf_depth             = 2
swerv: lsu_num_nbload            = 4
swerv: dec_instbuf_depth         = 4
swerv: opensource                = 1
swerv: no_secondary_alu          = 0
swerv: Writing /home/msyksphinz/work/riscv/swerv_eh1/configs/snapshots/default/common_defines.vh
swerv: Writing /home/msyksphinz/work/riscv/swerv_eh1/configs/snapshots/default/defines.h
swerv: Writing /home/msyksphinz/work/riscv/swerv_eh1/configs/snapshots/default/pd_defines.vh
swerv: Writing /home/msyksphinz/work/riscv/swerv_eh1/configs/snapshots/default/whisper.json
swerv: Writing /home/msyksphinz/work/riscv/swerv_eh1/configs/snapshots/default/perl_configs.pl

これで、コンフィグレーションを決めるための以下のファイルが生成されるようだ。それぞれ、RTL用、C言語プログラミング用、ポストレイアウト用などのファイルが生成されている。

$RV_ROOT/configs/snapshots/default
├── common_defines.vh                       # `defines for testbench or design
├── defines.h                               # #defines for C/assembly headers
├── pd_defines.vh                           # `defines for physical design
├── perl_configs.pl                         # Perl %configs hash for scripting
├── pic_ctrl_verilator_unroll.sv            # Unrolled verilog based on PIC size
├── pic_map_auto.h                          # PIC memory map based on configure size
└── whisper.json                            # JSON file for swerv-iss

この後Verilatorでコンパイルできるのだが、テストベンチとテストプログラム類も同梱されていないのでどうも先に進めない。 自分で用意しろってことか。 どうもアドレスマップもRocketとはずいぶん異なるようで、Rocketのテストベンチのバイナリをそのまま動かすことはできないような気がする。

ソースコードの中身を見てみたが、SystemVerilogと言いつつ随分と前時代的で、なんだタダのVerilogじゃん、という気がしなくもない。 まあ広く一般に公開するんだから、SystemVerilogの難しい構文を使われるとツールによってはコンパイルできないのでありがたい、と考えることもできる。

Vivadoで論理合成をしてみよう

リグレッション環境が存在しないので少し触るのに時間がかかりそうだが、せっかくなのでVivadoを使って論理合成を行ってみたい。

論理合成の環境も用意されていない。自分で作ることにした。 Vivadoに読み込ませるべきファイルリストは、designディレクトリに存在するflistを参考に作成した。

  • $RV_ROOT/syn/filelist.tcl
read_verilog -sv ../design/swerv_wrapper.sv
read_verilog -sv ../design/mem.sv
read_verilog -sv ../design/pic_ctrl.sv
read_verilog -sv ../design/swerv.sv
...

どうにかして論理合成が終わった。動作周波数は50MHzくらいかな? Zynq

オリジナルLLVM Backendを追加しよう (16. SelDAGtoDAGの実装)

LLVMにはすでにRISC-Vのバックエンドサポートが追加されている。しかし、勉強のために独自のRISC-V実装をLLVMに追加している。

SelectionDAGを追加している。きつねさんLLVM本曰く、SelDAGtoDAGというのは、

LLVM IR から SelectionDAG を経由して MI Layerに落とすパス (SelectionDAGIsel) の定義. ということなので、LLVM IRのコードをどのようにしてRISC-Vの実装に落とし込むかというところになる。

利用するのはTableGenで生成されるMYRISCVXGenDAGISel.incである。

用意したファイルは MYRISCVXISelDAGToDAG.cppMYRISCVXSEISelDAGToDAG.cppなのだが、SEがついているのは32bit用、それ以外は64bit用という理解である。

createMYRISCVXSEISelDagが呼ばれると、以下のような流れでSelectionDAGが生成されていくという理解である。

  • lib/Target/MYRISCVX/MYRISCVXSEISelDAGToDAG.cpp
FunctionPass *llvm::createMYRISCVXSEISelDag(MYRISCVXTargetMachine &TM,
                                            CodeGenOpt::Level OptLevel) {
  return new MYRISCVXSEDAGToDAGISel(TM, OptLevel);
}
  • lib/Target/MYRISCVX/MYRISCVXSEISelDAGToDAG.h
  class MYRISCVXSEDAGToDAGISel : public MYRISCVXDAGToDAGISel {
 public:
    explicit MYRISCVXSEDAGToDAGISel(MYRISCVXTargetMachine &TM, CodeGenOpt::Level OL)
        : MYRISCVXDAGToDAGISel(TM, OL) {}
  • lib/Target/MYRISCVX/MYRISCVXISelDAGToDAG.h
  class MYRISCVXDAGToDAGISel : public SelectionDAGISel {
 public:
    explicit MYRISCVXDAGToDAGISel(MYRISCVXTargetMachine &TM, CodeGenOpt::Level OL)
        : SelectionDAGISel(TM, OL), Subtarget(nullptr) {}

アドレスの生成はSelectAddrを使用するらしい。

  • lib/Target/MYRISCVX/MYRISCVXInstrInfo.td
// MYRISCVX Address Mode! SDNode frameindex could possibily be a match
// since load and store instructions from stack used it.
def addr :
  ComplexPattern<iPTR, 2, "SelectAddr", [frameindex], [SDNPWantParent]>;
  • lib/Target/MYRISCVX/MYRISCVXISelDAGToDAG.cpp
//@SelectAddr {
/// ComplexPattern used on MYRISCVXInstrInfo
/// Used on MYRISCVX Load/Store instructions
bool MYRISCVXDAGToDAGISel::
SelectAddr(SDNode *Parent, SDValue Addr, SDValue &Base, SDValue &Offset) {
...
}

これでコードを生成してみる。

$ ./bin/llc -march=myriscvx64 -relocation-model=pic -filetype=asm ch3.bc -o -

MYRISCVXISD::Retが生成できずにエラーとなった。これは、Return値の設定において特定のレジスタを称しなければならないのだが、それに対応できていない。

'+myriscv?' is not a recognized feature for this target (ignoring feature)
        .text
        .section .mdebug.abiO32
        .previous
        .file   "ch3.cpp"
Selecting: t7: ch = MYRISCVXISD::Ret t4, Register:i32 $ra

LLVM ERROR: Cannot select: t7: ch = MYRISCVXISD::Ret t4, Register:i32 $ra
  t6: i32 = Register $ra
In function: main

MIPSでは、サブルーチンコールとして「jal」→「jr $ra」で戻るのが基本だが、処理を高速化するためにjr命令は別のレジスタを使って戻ることができる。このようなコードを生成することも可能だ。

オリジナルLLVM Backendを追加しよう (15. RISC-Vオリジナルターゲットの作り直し)

LLVMにはすでにRISC-Vのバックエンドサポートが追加されている。しかし、勉強のために独自のRISC-V実装をLLVMに追加している。

Chapter-3のサイズの大きなスタックフレームの処理に関して、どうにも解決できずに悩んでいる。 とりあえず最初から全体的に見直すために、いろいろ見直す場所もあるので最初から作り直している。

ディレクトリ構成

lib/Target/MYRISCVX
├── CMakeLists.txt
├── LLVMBuild.txt
├── MCTargetDesc
│?? ├── CMakeLists.txt
│?? ├── LLVMBuild.txt
│?? ├── MYRISCVXABIInfo.cpp
│?? ├── MYRISCVXABIInfo.h
│?? ├── MYRISCVXMCTargetDesc.cpp
│?? └── MYRISCVXMCTargetDesc.h
├── MYRISCVX.h
├── MYRISCVX.td
├── MYRISCVXCallingConv.td
├── MYRISCVXFrameLowering.cpp
├── MYRISCVXFrameLowering.h
├── MYRISCVXISelLowering.cpp
├── MYRISCVXISelLowering.h
├── MYRISCVXInstrFormats.td
├── MYRISCVXInstrInfo.cpp
├── MYRISCVXInstrInfo.h
├── MYRISCVXInstrInfo.td
├── MYRISCVXMachineFunction.cpp
├── MYRISCVXMachineFunction.h
├── MYRISCVXOther.td
├── MYRISCVXRegisterInfo.cpp
├── MYRISCVXRegisterInfo.h
├── MYRISCVXRegisterInfo.td
├── MYRISCVXSEFrameLowering.cpp
├── MYRISCVXSEFrameLowering.h
├── MYRISCVXSEISelLowering.cpp
├── MYRISCVXSEISelLowering.h
├── MYRISCVXSEInstrInfo.cpp
├── MYRISCVXSEInstrInfo.h
├── MYRISCVXSERegisterInfo.cpp
├── MYRISCVXSERegisterInfo.h
├── MYRISCVXSchedule.td
├── MYRISCVXSubtarget.cpp
├── MYRISCVXSubtarget.h
├── MYRISCVXTargetMachine.cpp
├── MYRISCVXTargetMachine.h
├── MYRISCVXTargetObjectFile.cpp
├── MYRISCVXTargetObjectFile.h
└── TargetInfo
    ├── CMakeLists.txt
    ├── LLVMBuild.txt
    └── MYRISCVXTargetInfo.cpp

TargetInfo/MYRISCVXTargetInfo.cpp

Targetを32bitと64bitの2つ登録する。どちらともリトルエンディアン。

-  extern Target TheMYRISCVXTarget;
-  extern Target TheMYRISCVXelTarget;
+  extern Target TheMYRISCVX32Target;
+  extern Target TheMYRISCVX64Target;

-Target llvm::TheMYRISCVX32Target, llvm::TheMYRISCVX64Target;
+namespace llvm {
+Target TheMYRISCVX32Target;
+Target TheMYRISCVX64Target;
+}

MYRISCVXTargetObjectFile.{h, cpp}

SmallDataSectionとBSSDataSectionを定義している。あとMYRISCVXTargetMachineオブジェクト。

SmallDataSectionのスレッショルド値は8と設定されている?

SSThreshold("MYRISCVX-ssection-threshold", cl::Hidden,
            cl::desc("Small data and bss section threshold size (default=8)"),
            cl::init(8));

Initializeにおいて、

  1. ELFの初期化 (InitializeELF())
  2. SmallSection の初期化 (.sdata) ELF::SHT_PROGBITS, ELF::SHF_WRITE | ELF::SHF_ALLOC
  3. SmallBSSSectionの初期化 (.sbss) ELF::SHT_NOBITS, ELF::SHF_WRITE | ELF::SHF_ALLOC

MYRISCVXTargetMachine.{h, cpp}

isLittleという変数により、BigEndianとLittleEndianを判定する。

LLVMInitializeMYRISCVXTargetが定義され、BigEndian版とLittleEndian版が定義されるようだが、LittleEndianしか必要ない?というか、isLittleも必要ないのでMYRISCVXTargetMachineのみ定義することにした。

  MYRISCVXTargetMachine(const Target &T, const Triple &TT, StringRef CPU,
                    StringRef FS, const TargetOptions &Options,
                    Optional<Reloc::Model> RM, CodeModel::Model CM,
                    CodeGenOpt::Level OL);

MYRISCVX{SE}FrameLowering.{h,cpp}

MYRISCVXのフレームを定義する。SEは32bit用という通例となっている。

とりあえず形だけ。

MYRISCVXSelILowering.{h,cpp}

LLVM IRをSelection DAGに変換する。

f:id:msyksphinz:20190131012633p:plain
MYRISCVXターゲットアーキテクチャLLVMディレクトリ構成

オリジナルLLVM Backendを追加しよう (14. RISC-V命令フィールド登録の見直し)

LLVMにはすでにRISC-Vのバックエンドサポートが追加されている。しかし、勉強のために独自のRISC-V実装をLLVMに追加している。

Chapter-3のサイズの大きなスタックフレームの処理に関して、どうにも解決できずに悩んでいる。 とりあえず最初から全体的に見直すために、いろいろ見直す場所もあるので最初から作り直している。

せっかくなのでMYRISCVX(LLVMに追加している独自RISC-V実装)の命令フィールドの定義も作り直すことにした。

ディレクトリ構成

$ tree lib/Target/MYRISCVX
lib/Target/MYRISCVX
├── CMakeLists.txt
├── LLVMBuild.txt
├── MCTargetDesc
│   ├── CMakeLists.txt
│   ├── LLVMBuild.txt
│   └── MYRISCVXMCTargetDesc.cpp
├── MYRISCVX.h
├── MYRISCVXInstrInfo.td
├── MYRISCVXOther.td
├── MYRISCVXRegisterInfo.td
├── MYRISCVXTargetMachine.cpp
└── TargetInfo
    ├── CMakeLists.txt
    ├── LLVMBuild.txt
    └── MYRISCVXTargetInfo.cpp

MYRISCVXInstrInfo.tdの見直し

MYRISCXVはRISC-V命令セットと全く同一で作るつもりなので、別に変な加工をする必要はない。なるべく自力で作ってみるというだけである。

まず、RISC-V命令セットの命令タイプは大きく分けて以下の4種類に分けられる。

f:id:msyksphinz:20190129235828p:plain
RISC-Vの4種類の命令タイプ

R, I, S, Uの4種類が定義されており、それぞれにクラスを定義すれば良さそうだ。

def Pseudo : Format<0>;
def FrmR   : Format<1>;
def FrmI   : Format<2>;
def FrmS   : Format<3>;
def FrmU   : Format<4>; // Instruction w/ a custom format

まずはベースとなるMYRISCVXInstクラスを定義する。

// Generic MYRISCVX Format
class MYRISCVXInst<dag outs, dag ins, string asmstr, list<dag> pattern,
                   InstrItinClass itin, Format f>: Instruction
{
  // Inst and Size: for tablegen(... -gen-emitter) and
  // tablegen(... -gen-disassembler) in CMakeLists.txt
  field bits<32> Inst;
  Format Form = f;

  let Namespace = "MYRISCVX";

  let Size = 4;

  bits<7> Opcode = 0;

  // Bottom 7 bits are the 'opcode' field
  let Inst{6-0} = Opcode;

...
}

次に、R型の命令を定義する。Opcodeの部分はすでにフィールドに割り付け済みなので、それ以外を作っていく。

class FR<bits<7> opcode, bits<7> funct7, bits<3> funct3,
         dag outs, dag ins, string asmstr,
         list<dag> pattern, InstrItinClass itin>:
      MYRISCVXInst<outs, ins, asmstr, pattern, itin, FrmR>
{
  bits<5>  rs2;
  bits<5>  rs1;
  bits<5>  rd;

  let Opcode = opcode;

  let Inst{31-25} = funct7;
  let Inst{24-20} = rs2;
  let Inst{19-15} = rs1;
  let Inst{14-12} = funct3;
  let Inst{11-7}  = rd;
}

こんな感じでいいのだろうか。次にI型、S型を作る。

class FI<bits<7> opcode, bits<3> funct3,
         dag outs, dag ins, string asmstr, list<dag> pattern,
         InstrItinClass itin>: MYRISCVXInst<outs, ins, asmstr, pattern, itin, FrmI>
{
  bits<5>  rs1;
  bits<5>  rd;
  bits<12> imm12;

  let Opcode = opcode;

  let Inst{31-20} = imm12;
  let Inst{19-15} = rs1;
  let Inst{14-12} = funct3;
  let Inst{11-7}  = rd;
}
class FS<bits<7> opcode, bits<3> funct3,
         dag outs, dag ins, string asmstr, list<dag> pattern,
         InstrItinClass itin>: MYRISCVXInst<outs, ins, asmstr, pattern, itin, FrmS>
{
  bits<5>  rs1;
  bits<5>  rs2;
  bits<12> imm12;

  let Opcode = opcode;

  let Inst{31-25} = imm12{11-5};
  let Inst{19-15} = rs1;
  let Inst{24-20} = rs2;
  let Inst{14-12} = funct3;
  let Inst{11-7}  = imm12{4-0};
}
f:id:msyksphinz:20190129235914p:plain
MYRISCVXの命令フィールドクラスの構造

ここに、各種命令を登録していった。

class LoadM<bits<7> opcode, bits<3> funct3, string instr_asm, PatFrag OpNode, RegisterClass RC,
            Operand MemOpnd, bit Pseudo>:
  FI<opcode, funct3, (outs RC:$ra), (ins MemOpnd:$addr),
     !strconcat(instr_asm, "\t$ra, $addr"),
     [(set RC:$ra, (OpNode addr:$addr))], IILoad> {
  let isPseudo = Pseudo;
}


class StoreM<bits<7> opcode, bits<3> funct3, string instr_asm, PatFrag OpNode, RegisterClass RC,
             Operand MemOpnd, bit Pseudo>:
  FI<opcode, funct3, (outs), (ins RC:$ra, MemOpnd:$addr),
     !strconcat(instr_asm, "\t$ra, $addr"),
     [(OpNode RC:$ra, addr:$addr)], IIStore> {
  let isPseudo = Pseudo;
}


//@ 32-bit load.
multiclass LoadM32<bits<7> opcode, bits<3> funct3, string instr_asm, PatFrag OpNode,
                   bit Pseudo = 0> {
  def #NAME# : LoadM<opcode, funct3, instr_asm, OpNode, GPR, mem, Pseudo>;
}

// 32-bit store.
multiclass StoreM32<bits<7> opcode, bits<3> funct3, string instr_asm, PatFrag OpNode,
                    bit Pseudo = 0> {
  def #NAME# : StoreM<opcode, funct3, instr_asm, OpNode, GPR, mem, Pseudo>;
}


//@JumpFR {
let isBranch=1, isTerminator=1, isBarrier=1, imm12=0, hasDelaySlot = 0,
isIndirectBranch = 1 in
class JumpFR<bits<7> opcode, bits<3> funct3, string instr_asm, RegisterClass RC>:
  FI<opcode, funct3, (outs), (ins RC:$ra),
     !strconcat(instr_asm, "\t$ra"), [(brind RC:$ra)], IIBranch> {
  let rs1   = 1;  // RA
  let imm12 = 0;
}
//@JumpFR }

defm LW : LoadM32 <0b0000011, 0b010, "lw", load_a >;
defm SW : StoreM32<0b0100011, 0b010, "sw", store_a>;

/// Arithmetic Instructions (ALU Immediate)
// IR "add" defined in include/llvm/Target/TargetSelectionDAG.td, line 315 (def add).
def ADDI : ArithLogicI<0b0010011, 0b000, "addi", add, simm12, immSExt12, GPR>;

/// Arithmetic Instructions (3-Operand, R-Type)
def JALR : JumpFR<0b1100111, 0b000, "jalr", GPR>;
def : InstAlias<"jr $rs1",      (JALR ZERO, GPR:$rs1, 0)>;
def RET : RetBase<GPR>;

/// No operation
def : InstAlias<"nop",  (ADDI ZERO, ZERO, 0)>;

Chiselで非同期クロック設計をしよう

Chisel3.0からは異種クロックのサポート、つまり複数のクロックを使ったデザインがサポートされるようになった。

Chisel3でクロックを2つ用意するためには、CrossingIOを使用する。

class AsyncQueue[T <: Data](gen: T, params: AsyncQueueParams = AsyncQueueParams()) extends Crossing[T] {
  val io = IO(new CrossingIO(gen))

CrossingIOのBundleには、以下が含まれている。2つのクロック、2つのリセット、そしてデータを示すenq, deqだ。

  • asyncqueue/src/main/scala/Crossinrg.scala
class CrossingIO[T <: Data](gen: T) extends Bundle {
  // Enqueue clock domain
  val enq_clock = Clock(INPUT)
  val enq_reset = Bool(INPUT) // synchronously deasserted wrt. enq_clock
  val enq = Decoupled(gen).flip
  // Dequeue clock domain
  val deq_clock = Clock(INPUT)
  val deq_reset = Bool(INPUT) // synchronously deasserted wrt. deq_clock
  val deq = Decoupled(gen)
}

例えばAsyncQueueでは、以下のようにSourceにenq_clock, enq_reset, Sinkの方にdeq_clock, deq_resetを使用する。

  source.clock := io.enq_clock
  source.reset := io.enq_reset
  sink.clock := io.deq_clock
  sink.reset := io.deq_reset

AsyncQueueの構成

f:id:msyksphinz:20190121233846p:plain
ChiselでのAsyncQueueの構成

AsyncQueueは、大きく分けてsourceとsinkの2種類のモジュールで構成されている。

sourceは入力側、ASyncQueueSourceモジュールで構成されている。sinkは出し側、AsyncQueueSinkモジュールで構成されている。

sourceとsinkはasyncという信号で接続されている。このBundleは以下の信号で構成されている。

class AsyncBundle[T <: Data](private val gen: T, val params: AsyncQueueParams = AsyncQueueParams()) extends Bundle {
  // Data-path synchronization
  val mem   = Output(Vec(params.wires, gen))
  val ridx  = Input (UInt((params.bits+1).W))
  val widx  = Output(UInt((params.bits+1).W))
  val index = params.narrow.option(Input(UInt(params.bits.W)))

  // Signals used to self-stabilize a safe AsyncQueue
  val safe = params.safe.option(new AsyncBundleSafety)
}
f:id:msyksphinz:20190120235419p:plain
AsyncBundleの構成

memは非同期を行うためのFIFOのエントリ数だと思われる。ridxは読み出し側(つまりQueueの出し側)、widxは書き込み側(つまりQueueの挿入側)の受け渡し用のメモリのインデックスを示しているものと思われる。 indexsafeはどのように使われているのかはよく分からない。ちなみにVerilogを生成してみるとasync_indexは生成されていなかった。オプションだろうと思われる。

  output [31:0] io_async_mem_0, // @[:@921.4]
  output [31:0] io_async_mem_1, // @[:@921.4]
  output [31:0] io_async_mem_2, // @[:@921.4]
  output [31:0] io_async_mem_3, // @[:@921.4]
  output [31:0] io_async_mem_4, // @[:@921.4]
  output [31:0] io_async_mem_5, // @[:@921.4]
  output [31:0] io_async_mem_6, // @[:@921.4]
  output [31:0] io_async_mem_7, // @[:@921.4]
  input  [3:0]  io_async_ridx, // @[:@921.4]
  output [3:0]  io_async_widx, // @[:@921.4]

AsyncQueueSourceの内部は、まずはSourceとSinkを接続するためのメモリがインスタンスされている。

  val mem = Reg(Vec(params.depth, gen)) // This does NOT need to be reset at all.

このメモリに書き込むためのインデックスとして、Write側はwidx, Read側はridxが用意されている。 widxはGrayCodeカウンタ、ridxもSink側から接続されるGraycodeカウンタだろうが、Synchronizerを経由して、Sink側のクロックからSource側のクロックに置き換えているものと思われる。

メモリへの書き込み側のインデックスは以下のように生成する。

  val index = if (bits == 0) 0.U else io.async.widx(bits-1, 0) ^ (io.async.widx(bits, bits) << (bits-1))
  val widx_reg = AsyncResetReg(widx, "widx_gray")
  io.async.widx := widx_reg

FIFOにデータが埋まっているか(つまり入力側に対するReady信号)は以下のようにして生成する。 グレイコードが一致するだけではFIFOのFullが判定できないので、以下のような実装となっている。

  val ready = sink_ready && widx =/= (ridx ^ (params.depth | params.depth >> 1).U)

参考文献として以下を見つけた。 ここで、ライト側でFIFOがフル(full)である条件:wadr[WA:0]=={~radr[WA],radr[WA-1:0]}という記述とやりたいことは一緒だと思う。

メモリからのデータ読み出しは、io.asyncバンドルを通じてデータをすべてSink側に見せ、データを取得する。

  io.async.index match {
    case Some(index) => io.async.mem(0) := mem(index)
    case None => io.async.mem := mem
  }

読み出し側では、FIFO内にデータが入っているかどうかを以下の論理で生成する。 ridxはSink側のクロックを使ってGrayCounterを生成し、widxはSource側のクロックで動いているので、AsyncResetSynchronizerShiftRegを使ってクロックを入れ替えている。

  val ridx = GrayCounter(bits+1, io.deq.fire(), !source_ready, "ridx_bin")
  val widx = AsyncResetSynchronizerShiftReg(io.async.widx, params.sync, Some("widx_gray"))
  val valid = source_ready && ridx =/= widx

RISC-V 64-bit LLVM Backendを試す (13. Return文生成の解析)

LLVMにはすでにRISC-Vのバックエンドサポートが追加されている。しかし、勉強のために独自のRISC-V実装をLLVMに追加している。

前回までで、簡単なReturn命令は生成されるようになったが、まだ-O2オプションを付加しないとエラーが発生する。 EmitPrologueとEmitEpilogueを実装したのだが、まだ途中でスタックダンプしてしまい、正しく動作していない様だ。

とりあえず、bin/llcを実行した際に-march=riscv64-march=myriscvxでどのように挙動が変わるのかを観測した。

Legalizing node: t7: i32 = Register $a0
Ignoring node results
Legally typed node: t7: i32 = Register $a0

Legalizing node: t6: i32 = Constant<4>
Analyzing result type: i32
Legal result type
Legally typed node: t6: i32 = Constant<4>

Legalizing node: t4: i64 = undef
Analyzing result type: i64
Expand integer result: t4: i64 = undef

Creating new node: t10: i32 = undef
Legalizing node: t10: i32 = undef
Analyzing result type: i32
Legal result type
Legally typed node: t10: i32 = undef

Legalizing node: t2: i64 = FrameIndex<0>
Analyzing result type: i64
Expand integer result: t2: i64 = FrameIndex<0>

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!

-O2でコンパイルしたものはllvm-disで逆アセンブルすると以下のようになる。

; Function Attrs: norecurse nounwind readnone
define dso_local signext i32 @main() local_unnamed_addr #0 {
entry:
  ret i32 0
}

一方で、-O2を付加しないものはllvm-disで逆アセンブルすると以下のようになる。

; Function Attrs: noinline norecurse nounwind optnone
define dso_local signext i32 @main() #0 {
entry:
  %retval = alloca i32, align 4
  store i32 0, i32* %retval, align 4
  ret i32 0
}

FrameIndexというNodeを変換するときに失敗する。FrameIndexについていろいろ調べてみると、どうも関数フレームを処理するためのポインタの処理を行っているらしい。しかもExpandIntegerResultということは値の拡張をしようとしているので、どうもそこで引っかかっている気がする。

まさかと思って、ポインタのサイズの定義を64bitから32bitに変えてみた。

diff --git a/lib/Target/MYRISCVX/MYRISCVXTargetMachine.cpp b/lib/Target/MYRISCVX/MYRISCVXTargetMachine.cpp
index 1053f2428d8..0cadaed5cc5 100644
--- a/lib/Target/MYRISCVX/MYRISCVXTargetMachine.cpp
+++ b/lib/Target/MYRISCVX/MYRISCVXTargetMachine.cpp
@@ -36,7 +36,7 @@ static std::string computeDataLayout(const Triple &TT, StringRef CPU,
   std::string Ret = "";
   Ret += "e";
   Ret += "-m:m";
-  Ret += "-p:64:64-i64:64-i128:128-n64-S128";
+  Ret += "-p:32:32-i64:64-i128:128-n64-S128";
   return Ret;
 }

これでビルドを行った。すると問題となっていた部分は通過した。やはり、32bitから64bitへの拡張ができていなかったのか。 とりあえず、32bitのアドレッシングで進めることにしよう。あとで修正する。

ただ、llcの途中でまだ固まってしまう。

1: nullptr
2: %bb.0
Found roots: %bb.0
        discovered a new reachable node nullptr
        discovered a new reachable node %bb.0
Skipping pass 'Shrink Wrapping analysis' on function main
alloc FI(0) at SP[-4]

フレームについてもう少し実装を進めてみる。

  • lib/Target/MYRISCVX/MYRISCVXRegisterInfo.cpp
// FrameIndex represent objects inside a abstract stack.
// We must replace FrameIndex with an stack/frame pointer
// direct reference.
void MYRISCVXRegisterInfo::
eliminateFrameIndex(MachineBasicBlock::iterator II, int SPAdj,
                    unsigned FIOperandNum, RegScavenger *RS) const {
...
  // The following stack frame objects are always referenced relative to $sp:
  //  1. Outgoing arguments.
  //  2. Pointer to dynamically allocated stack space.
  //  3. Locations for callee-saved registers.
  // Everything else is referenced relative to whatever register
  // getFrameRegister() returns.
  unsigned FrameReg;

  FrameReg = MYRISCVX::SP;

  // Calculate final offset.
  // - There is no need to change the offset if the frame object is one of the
  //   following: an outgoing argument, pointer to a dynamically allocated
  //   stack space or a $gp restore location,
  // - If the frame object is any of the following, its offset must be adjusted
  //   by adding the size of the stack:
  //   incoming argument, callee-saved register location or local variable.
  int64_t Offset;
  Offset = spOffset + (int64_t)stackSize;

  Offset    += MI.getOperand(i+1).getImm();

  dbgs() << "Offset     : " << Offset << "\n" << "<--------->\n";

  // If MI is not a debug value, make sure Offset fits in the 16-bit immediate
  // field.
  if (!MI.isDebugValue() && !isInt<16>(Offset)) {
    assert("(!MI.isDebugValue() && !isInt<16>(Offset))");
  }

  MI.getOperand(i).ChangeToRegister(FrameReg, false);
  MI.getOperand(i+1).ChangeToImmediate(Offset);

これで無事に命令を生成することができた。

./bin/llc -march=myriscvx -relocation-model=pic --debug -filetype=asm ch3.bc -o -
********** COMPUTING STACKMAP LIVENESS: main **********
        .globl  main                    # -- Begin function main
        .type   main,@function
        .ent    main                    # @main
main:
        .frame  $x8,8,$x1
        .mask   0x00000000,0
        .set    noreorder
        .set    nomacro
        discovered a new reachable node %bb.0
# %bb.0:                                # %entry
        addi    $x2, $x2, -8
        addi    $x10, $x0, 0
        st      $x10, 4($x2)
        addi    $x10, $x0, 4
        addi    $x2, $x2, 8
        ret     $x1
        .set    macro
        .set    reorder
        .end    main
$func_end0:
        .size   main, ($func_end0)-main
                                        # -- End function

        .ident  "clang version 7.0.1 (https://git.llvm.org/git/clang.git/ 65f84326edb6105fb0263f0b023719b491f8cf1a) (https://github.com/msyksphinz/llvm.git 232d729230027419ef0c460c2289d5f1147e61a3)"
        .section        ".note.GNU-stack","",@progbits
f:id:msyksphinz:20190120194335p:plain