FPGA開発日記

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

Western DigitalのRISC-VコアSweRV-EH1 (4. テストベンチのコンパイル環境)

Western DigitalからオリジナルのRISC-VコアSweRVがリリースされ、テストベンチが動かせるようになった。

久しぶりに見てみると、アセンブリコンパイルしてシミュレーションに使用する環境がマージされている。

テストには

  • hello_world.s
  • hello_world2.s

が格納されていて、どちらもアセンブリ言語だ。C言語のシミュレーションはもう少し中身を解析しないといけないなあ。

使い方はこうだ。アセンブリファイルの場所を指定してmakeを実行すると、自動的にコンパイルされる。

make -f $RV_ROOT/tools/Makefile program.hex ASM_TEST=hello_world2 ASM_TEST_DIR=./testbench/asm/ verilator-run

最後に、以下のような結果が表示された。

Start of sim

------------------------------
Hello World from SweRV @WDC !!
------------------------------

Finished : minstret = 389, mcycle = 1658

うーん、アセンブリの中身を見てみても、単純にコンソールのメモリアドレスにデータをストアしているだけで、スタックポインタとかどこを使えば良いのかイマイチ謎なんだよね。

  • testbench/asm/hello_world.s
...
 addi  x0, x0, 0
 lui x11, 853376
 ori  x9, x0, 'H'
 sw x9, 0 (x11)
 ori  x9, x0, 'E'
 sw x9, 0 (x11)
 ori  x9, x0, 'L'
 sw x9, 0 (x11)
 sw x9, 0 (x11)
f:id:msyksphinz:20190205002042p:plain

オリジナルLLVM Backendを追加しよう (21. 剰余演算の最適化と論理・比較命令の追加)

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

資料としては Tutorial: Creating an LLVM Backend for the Cpu0 Architecture を使用している。やっとChapter-4だ。

DIV命令は演算のコストが大きくなってしまうので、LLVMはsrem命令を乗算に置き換えてしまう。

int b = 11;
b = (b + 1) % 12

上記の演算は、 以下のように変換される。正直この変換は意味が分からないが、等価なのであろう。。。

(b + 1) % 12 = 0xC * 0x2AAAAAAB = 0x2_00000004
A = 0x2 >> 1 = 1
B = 0x1 << 31 = 0
C = A + B = 1
D = C * 12 = 1 * 12 = 12
ANS = D - 12 = 0

以下のようなデータフローが生成される。

f:id:msyksphinz:20190217002141p:plain
最適化されたデータフロー。mulhsが使用されている。

ちなみに最適化前。通常のsremが使用されている。

f:id:msyksphinz:20190217002302p:plain
最適化される前のデータフロー。sremが使用されている。

一方でMipsの方式を使用すると、以下のようになるらしい。MIPSはHILOレジスタを使用するので、MUL命令とMFHIを使用すればよい。

        lui     $1, 20164
        addiu   $2, $4, 1
        ori     $1, $1, 60495
        mult    $2, $1
        mfhi    $1
        srl     $3, $1, 31
        sra     $1, $1, 2
        addu    $1, $1, $3
        sll     $3, $1, 1
        addu    $3, $3, $1
        sll     $1, $1, 4
        subu    $1, $3, $1
        jr      $ra
        addu    $2, $2, $1

一方で、volatileを使用すると最適化が抑制される。

int test_mod()
{
  int b = 11;
  volatile int a = 12;

  b = (b+1)%a;

  return b;
}
        addi    x10, x10, 1
        lw      x11, 0(x2)
        rem     x10, x10, x11
        sw      x10, 4(x2)
        lw      x10, 4(x2)
        addi    x2, x2, 8
        ret

ローテート命令の実装 以下をコンパイルすると、エラーが出てしまう。RISC-Vにはローテート命令は実装されていないので、これはどうするか問題だ。

$ ./bin/llc -march=myriscvx32 -relocation-model=pic -filetype=asm ch4_1_rotate.bc -o ch4_1_rotate.myriscvx.s
LLVM ERROR: Cannot select: t18: i32 = rotl t6, Constant:i32<30>
  t6: i32,ch = load<(dereferenceable load 4 from %ir.a)> t5, FrameIndex:i32<0>, undef:i32
    t2: i32 = FrameIndex<0>
    t4: i32 = undef
  t7: i32 = Constant<30>
In function: _Z16test_rotate_leftv

本物のRISC-Vサポートはどのような実装になっているのか調べてみた。

  • lib/Target/Mips/MipsISelLowering.cpp
  setOperationAction(ISD::ROTL,              MVT::i32,   Expand);
  setOperationAction(ISD::ROTL,              MVT::i64,   Expand);
...
  if (!Subtarget.hasMips32r2())
    setOperationAction(ISD::ROTR, MVT::i32,   Expand);

  if (!Subtarget.hasMips64r2())
    setOperationAction(ISD::ROTR, MVT::i64,   Expand);

setOperationAction()というのはどういうことだろう?とりあえず自分の実装にも加えてみる。

次に論理命令を追加する。これがないと最終的に上記のch4_1_rotateは生成できない。

-def ORI  : ArithLogicI<0b0010011, 0b110, "ori", or, uimm12, immZExt12, GPR>;
+def XORI : ArithLogicI<0b0010011, 0b110, "xori", xor, uimm12, immZExt12, GPR>;
+def ORI  : ArithLogicI<0b0010011, 0b110, "ori",  or,  uimm12, immZExt12, GPR>;
+def ANDI : ArithLogicI<0b0010011, 0b111, "andi", and, uimm12, immZExt12, GPR>;
+
 def LUI  : ArithLogicU<0b0110111, "lui", simm20, immSExt12>;
 def ADD  : ArithLogicR<0b0110011, 0b000, "add", add, GPR>;
 def SUB  : ArithLogicR<0b0110011, 0b000, "sub", sub, GPR>;

-// sra is IR node for ashr llvm IR instruction of .bc
-def SRL  : shift_rotate_reg<0b0110011, 0b0000000, 0b101, 0x0, "srl", srl, GPR>;
 def SLL  : shift_rotate_reg<0b0110011, 0b0000000, 0b001, 0x0, "sll", shl, GPR>;
+def AND  : ArithLogicR<0b0110011, 0b111, "and", and, GPR>;
+def SRL  : shift_rotate_reg<0b0110011, 0b0000000, 0b101, 0x0, "srl", srl, GPR>;
 def SRA  : shift_rotate_reg<0b0110011, 0b0100000, 0b101, 0x0, "sra", sra, GPR>;
+def OR   : ArithLogicR<0b0110011, 0b110, "or",  or,  GPR>;
+def XOR  : ArithLogicR<0b0110011, 0b100, "xor", xor, GPR>;

とりあえずこれで命令がローテート操作が命令出力できるようになった。一安心。

次に、比較命令の実装を行っていく。 比較命令のためには、Predicateのためのオペレーションとしてsltiとかsltuとかが使用できるが、通常のSDNodeではなく、PatFragという型で定義しなければならないらしい。 以下のフォーマットを作成した。

  • lib/Target/Mips/MipsISelLowering.cpp
// SetCC
class SetCC_R<bits<7> opcode, bits<3> funct3,
              string instr_asm, PatFrag cond_op,
              RegisterClass RC> :
  FI<opcode, funct3, (outs GPR:$ra), (ins RC:$rb, RC:$rc),
  !strconcat(instr_asm, "\t$ra, $rb, $rc"),
  [(set GPR:$ra, (cond_op RC:$rb, RC:$rc))], IIAlu> {
    let isReMaterializable = 1;
}


class SetCC_I<bits<7> opcode, bits<3> funct3,
              string instr_asm, PatFrag cond_op,
              Operand imm, PatLeaf imm_type, RegisterClass RC> :
  FI<opcode, funct3, (outs GPR:$ra), (ins RC:$rb, imm:$imm12),
  !strconcat(instr_asm, "\t$ra, $rb, $imm12"),
  [(set GPR:$ra, (cond_op RC:$rb, imm_type:$imm12))], IIAlu> {
    let isReMaterializable = 1;
}

これに基づいて比較命令を実装した。これで一応LLVMコンパイルできるようになった。比較処理はまだ命令生成できないけど。

def SLTI  : SetCC_I<0b0010011, 0b010, "slti",  setlt,  simm12, immSExt12, GPR>;
def SLTIU : SetCC_I<0b0010011, 0b011, "sltiu", setult, simm12, immSExt12, GPR>;
def SLT   : SetCC_R<0b0110011, 0b010, "slt",  setlt,  GPR>;
def SLTU  : SetCC_R<0b0110011, 0b011, "sltu", setult, GPR>;

オリジナルLLVM Backendを追加しよう (20. 算術演算命令の追加)

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

資料としては Tutorial: Creating an LLVM Backend for the Cpu0 Architecture を使用している。やっとChapter-4だ。

今回からは算術演算命令を追加していこう。

算術演算命令追加のためには MYRISCVXInstrInfo.td に命令を追加する。

commit d9a53af99b00ccba20747ef25eb3a2c2301bb4a4
Author: msyksphinz <msyksphinz_dev.gmail.com>
Date:   Thu Feb 14 16:11:05 2019 +0900

    Func: implement some operations

diff --git a/lib/Target/MYRISCVX/MYRISCVXInstrInfo.td b/lib/Target/MYRISCVX/MYRISCVXInstrInfo.td
index 0fda59cdc32..53627d494e8 100644
--- a/lib/Target/MYRISCVX/MYRISCVXInstrInfo.td
+++ b/lib/Target/MYRISCVX/MYRISCVXInstrInfo.td
@@ -256,9 +256,28 @@ def LUI  : ArithLogicU<0b0110111, "lui", simm20, immSExt12>;
 def ADD  : ArithLogicR<0b0110011, 0b000, "add", add, GPR>;
 def SUB  : ArithLogicR<0b0110011, 0b000, "sub", sub, GPR>;

+// sra is IR node for ashr llvm IR instruction of .bc
 def SRL  : shift_rotate_reg<0b0110011, 0b0000000, 0b101, 0x0, "srl", srl, GPR>;
-def SRLI : shift_rotate_imm32<0b0010011, 0b101, 0x00, "shli", shl>;
-
+def SLL  : shift_rotate_reg<0b0110011, 0b0000000, 0b001, 0x0, "sll", shl, GPR>;
+def SRA  : shift_rotate_reg<0b0110011, 0b0100000, 0b101, 0x0, "sra", sra, GPR>;
+
+def SRLI : shift_rotate_imm32<0b0010011, 0b101, 0x00, "shli", srl>;
+def SLLI : shift_rotate_imm32<0b0010011, 0b001, 0x00, "slli", shl>;
+def SRAI : shift_rotate_imm32<0b0010011, 0b101, 0x00, "srai", sra>;
+
+// Only op DAG can be disabled by ch4_1, data DAG cannot.
+def SDT_MYRISCVXDivRem : SDTypeProfile<0, 2,
+                                  [SDTCisInt<0>,
+                                  SDTCisSameAs<0, 1>]>;
+
+def MUL   : ArithLogicR<0b0110011, 0b000, "mul",    mul, GPR>;
+def MULH  : ArithLogicR<0b0110011, 0b001, "mulh",   mul, GPR>;
+def MULHSU: ArithLogicR<0b0110011, 0b010, "mulhsu", mul, GPR>;
+def MULHU : ArithLogicR<0b0110011, 0b011, "mulhu",  mul, GPR>;
+def DIV   : ArithLogicR<0b0110011, 0b100, "div",    sdiv, GPR>;
+def DIVU  : ArithLogicR<0b0110011, 0b101, "divu",   udiv, GPR>;
+def REM   : ArithLogicR<0b0110011, 0b110, "rem",    srem, GPR>;
+def REMU  : ArithLogicR<0b0110011, 0b111, "remu",   urem, GPR>;

 //===----------------------------------------------------------------------===//
 // Instruction aliases
Author: msyksphinz <msyksphinz_dev.gmail.com>
Date:   Thu Feb 14 20:49:50 2019 +0900

    Func: add arithmetic instructions

diff --git a/lib/Target/MYRISCVX/MYRISCVXInstrInfo.td b/lib/Target/MYRISCVX/MYRISCVXInstrInfo.td
index 53627d494e8..4582ef2e569 100644
--- a/lib/Target/MYRISCVX/MYRISCVXInstrInfo.td
+++ b/lib/Target/MYRISCVX/MYRISCVXInstrInfo.td
@@ -270,14 +270,14 @@ def SDT_MYRISCVXDivRem : SDTypeProfile<0, 2,
                                   [SDTCisInt<0>,
                                   SDTCisSameAs<0, 1>]>;

-def MUL   : ArithLogicR<0b0110011, 0b000, "mul",    mul, GPR>;
-def MULH  : ArithLogicR<0b0110011, 0b001, "mulh",   mul, GPR>;
-def MULHSU: ArithLogicR<0b0110011, 0b010, "mulhsu", mul, GPR>;
-def MULHU : ArithLogicR<0b0110011, 0b011, "mulhu",  mul, GPR>;
-def DIV   : ArithLogicR<0b0110011, 0b100, "div",    sdiv, GPR>;
-def DIVU  : ArithLogicR<0b0110011, 0b101, "divu",   udiv, GPR>;
-def REM   : ArithLogicR<0b0110011, 0b110, "rem",    srem, GPR>;
-def REMU  : ArithLogicR<0b0110011, 0b111, "remu",   urem, GPR>;
+def MUL   : ArithLogicR<0b0110011, 0b000, "mul",    mul,   GPR>;
+def MULH  : ArithLogicR<0b0110011, 0b001, "mulh",   mulhs, GPR>;
+def MULHSU: ArithLogicR<0b0110011, 0b010, "mulhsu", mulhs, GPR>;
+def MULHU : ArithLogicR<0b0110011, 0b011, "mulhu",  mulhu, GPR>;
+def DIV   : ArithLogicR<0b0110011, 0b100, "div",    sdiv,  GPR>;
+def DIVU  : ArithLogicR<0b0110011, 0b101, "divu",   udiv,  GPR>;
+def REM   : ArithLogicR<0b0110011, 0b110, "rem",    srem,  GPR>;
+def REMU  : ArithLogicR<0b0110011, 0b111, "remu",   urem,  GPR>;

 //===----------------------------------------------------------------------===//
 // Instruction aliases

RISC-Vには算術演算命令が複数定義されており、乗算命令、除算命令などはうまくフィットできそうだが、一つMULHSUはうまくフィットできない。これはどうすれば良いのだろう?

上記のPredicationのうち、mul, sdiv, udiv, srem, uremなどはLLVMに定義されているので、それを使用する。

llvm.org

Tutorialのうち、ch4_math.llと、ch4_1_math.cppを使用する。無事に命令生成に成功したようだ。

./bin/llc -march=myriscvx32 -relocation-model=pic -filetype=asm ../lbdex/input/ch4_math.ll -o -
        addi    x2, x2, -8
        .cfi_def_cfa_offset 8
        lw      x10, 0(x2)
        lw      x11, 4(x2)
        sub     x12, x11, x10
        add     x13, x11, x10
        add     x12, x13, x12
        mul     x13, x11, x10
        add     x12, x12, x13
        slli    x13, x11, 2
        add     x12, x12, x13
        srai    x13, x11, 2
        add     x12, x12, x13
        shli    x13, x11, 30
        add     x12, x12, x13
        addi    x13, x0, 1
        sll     x13, x13, x10
        add     x12, x12, x13
        sra     x11, x11, x10
        addi    x13, x0, 128
        srl     x10, x13, x10
        add     x10, x12, x10
        add     x10, x10, x11
        addi    x2, x2, 8
        ret
./bin/clang -target mips-unknown-linux-gnu -c ../lbdex/input/ch4_math.cpp -emit-llvm
./bin/llc -march=myriscvx32 -relocation-model=pic -filetype=asm ch4_1_math.bc -o -
        addi    x2, x2, -56
        addi    x10, x0, 5
        sw      x10, 52(x2)
        addi    x10, x0, 2
        sw      x10, 48(x2)
        addi    x10, x0, -5
        sw      x10, 44(x2)
        lw      x10, 52(x2)
        lw      x11, 48(x2)
        add     x10, x10, x11
        sw      x10, 40(x2)
        lw      x10, 52(x2)
        lw      x11, 48(x2)
...
        add     x10, x10, x11
        lw      x11, 16(x2)
        add     x10, x10, x11
        lw      x11, 0(x2)
        add     x10, x10, x11
        addi    x2, x2, 56
        ret
f:id:msyksphinz:20190215233346p:plain

Vivado-HLSを使って高位合成でCPUを作ってみる(3. CPUクラスへの実装まとめ上げとテストベンチ実行)

Vivado-HLSを使って簡単なRISC-V CPUを作ってみている。

まずは性能を度外視して動くものを作る。 次に、性能を向上させるための様々な試行を実施する。 まずは、標準的なテストパタンをPassさせるために命令を追加していこう。

これまでは、CPUのクラスは存在していたが、実装がまとまっていなかったので、CPUクラスにまとめ上げていく。

CPUの実行は以下のループにまとめ上げることにした。

  do {
    u_rv32_cpu.fetch_inst();
    u_rv32_cpu.decode_inst();
    u_rv32_cpu.execute_inst();
  } while (!u_rv32_cpu.is_finish_cpu());

基本的に上記の3段にまとめた。 まず、fetch_inst()は命令をフェッチする。

void rv32_cpu::fetch_inst ()
{
  m_inst = m_data_mem[m_pc >> 2];
}

次に、decode_inst()は命令をデコードする。

void rv32_cpu::decode_inst ()
{
  switch(m_inst & 0x7f) {
    case 0x0f : {
      switch ((m_inst >> 12) & 0x07) {
        case 0b000 : m_dec_inst = FENCE;   break;
        case 0b001 : m_dec_inst = FENCE_I; break;
        default    : m_dec_inst = NOP;     break;
...

最後に、execute_inst()は命令実行のステージだ。

void rv32_cpu::execute_inst()
{
...
    case AUIPC : {
      XLEN_t imm = ExtendSign (ExtractBitField (m_inst, 31, 12), 19);
      imm = SExtXlen(imm << 12) + m_pc & 0x0fff;
      write_reg(m_rd, imm);
      break;
    }
    case LW  : {
      uint32_t addr = read_reg(m_rs1) + ((m_inst >> 20) & 0xfff);
      XLEN_t reg_data = mem_access(LOAD, read_reg(m_rs1), addr);
      write_reg(m_rd, reg_data);
      break;
...

RTL実装だと5段のパイプラインステージなどになるのだろうが、今回はVivado-HLSの高位合成なので適当に実装していく。あとでパイプラインを切って行けば良いか。

CPUのクラスには以下のようなメソッドを追加して、機能を1つのクラスにまとめ上げた。

  XLEN_t mem_access (memtype_t op, uint32_t data, uint32_t addr);
  RegAddr_t get_rs1_addr (Inst_t inst) ;
  RegAddr_t get_rs2_addr (Inst_t inst) ;
  RegAddr_t get_rd_addr (Inst_t inst) ;
  XLEN_t read_reg(RegAddr_t addr);
  void write_reg(RegAddr_t addr, XLEN_t data);
  XLEN_t csrrw (uint16_t addr, XLEN_t data);
  XLEN_t csrrs (uint16_t addr, XLEN_t data);
  XLEN_t csrrc (uint16_t addr, XLEN_t data);
  // Utilitity for decoder
  uint32_t ExtendSign (uint32_t data, uint32_t msb);
  uint32_t ExtractBitField (uint32_t hex, uint32_t left, uint32_t right);
  uint32_t ExtractUJField (uint32_t hex);
  uint32_t ExtractIField (uint32_t hex);
  uint32_t ExtractSBField (uint32_t hex);
  uint32_t ExtractSHAMTField (uint32_t hex);
  inline XLEN_t  SExtXlen (uint32_t  hex) { return (hex << 32) >> 32; }
  inline uint32_t UExtXlen (uint32_t hex) { return (hex << 32) >> 32; }

  void fetch_inst  ();
  void decode_inst ();
  void execute_inst();

  bool is_finish_cpu() { return m_dec_inst == WFI; }

これでテストを実行した。とりあえず命令が進むようになったが、Cシミュレーション時にSegmentation Faultで落ちてしまう。 テストをすべて通すためには、もう少し解析が必要だ。

$ spike-dasm < cpu.log
[00000000] : 04c0006f j       pc + 0x4c
[0000004c] : f1402573 csrr    a0, mhartid
x10 <= 00000000
[00000050] : 00051063 bnez    a0, pc + 0
[00000054] : 00000297 auipc   t0, 0x0
x05 <= 00000054
[00000058] : 01028293 addi    t0, t0, 16
x05 <= 00000064
[0000005c] : 30529073 csrw    mtvec, t0
[00000060] : 18005073 csrwi   satp, 0
[00000064] : 00000297 auipc   t0, 0x0
x05 <= 00000064
[00000068] : 02028293 addi    t0, t0, 32
x05 <= 00000084
[0000006c] : 30529073 csrw    mtvec, t0
[00000070] : 800002b7 lui     t0, 0x80000
...
f:id:msyksphinz:20190215011547p:plain

Vivado-HLSを使って高位合成でCPUを作ってみる(2. 標準的な命令を実装する)

Vivado-HLSを使って簡単なRISC-V CPUを作ってみている。

まずは性能を度外視して動くものを作る。 次に、性能を向上させるための様々な試行を実施する。 まずは、標準的なテストパタンをPassさせるために命令を追加していこう。

一般的なCPUと同じで、まずは命令のデコード、デコードに応じて命令の動作の記述、そしてプログラムカウンタの更新と、これらの処理をすべてC言語で記述し、Vivado-HLSで高位合成させてみる。

まず、テストベンチを作成した。これはrv32ui-p-simpleテストパタンを逆アセンブリし、32bit単位でhexを並べてメモリに格納しておく。 これで、テストパタン完成だ。

riscv64-unknown-elf-objcopy -O binary rv32ui-p-simple rv32ui-p-simple.bin
hexdump -v -e ' 1/4 "%08x " "\n"' rv32ui-p-simple.bin > rv32ui-p-simple.hex
  • rv32ui-p-simple.hex
04c0006f
34202f73
00800f93
03ff0a63
00900f93
03ff0663
00b00f93
03ff0263
...

これを高位合成CPUのテスト環境で動作させるために、test_cpu_hls.cpp(cpu_hlsのテストパタン環境)でファイルとしてロードする。このあたり、C言語の関数がそのまま使えるので便利だ。

  • test_cpu_hls.cpp
int main ()
{
  uint32_t memory[2048];

  FILE *fp;
  if ((fp = fopen ("test.hex", "r")) == NULL) {
    perror ("test.hex");
    exit (1);
  }

  int idx = 0;
  while (fscanf(fp, "%08x", &memory[idx]) != EOF) {
    fprintf (stdout, "memory[%d] <= %08x\n", idx, memory[idx]);
    idx ++;
  }
...

そして、これをデコードして命令を実行してく。 cpu_hls内に、以下の命令のデコーダとデータパスを実装した。

  • rv32_cpu.hpp
typedef enum {
  CSRRW , CSRRS , CSRRC ,
  CSRRWI, CSRRSI, CSRRCI,
  LUI, AUIPC,
  ADDI, SLTI, SLTIU, XORI, ORI, ANDI, SLLI, SRLI, SRAI,
  ADD, SUB, SLL ,SLT, SLTU, XOR, SRL, SRA, OR, AND,
  LW,  SW,
  JAL, JALR,
  BEQ, BNE, BLT, BGE, BLTU, BGEU,
  NOP,
  WFI
} inst_rv32_t;

これらに対して、以下のように命令のデータパスを追加していく。

...
      case ORI : {
        reg_data = u_rv32_cpu.read_reg(rs1) | u_rv32_cpu.ExtractIField (inst);
        u_rv32_cpu.write_reg(rd, reg_data);
        break;
      }
      case ANDI : {
        reg_data = u_rv32_cpu.read_reg(rs1) & u_rv32_cpu.ExtractIField (inst);
        u_rv32_cpu.write_reg(rd, reg_data);
        break;
      }

      case ADD : {
        reg_data = u_rv32_cpu.read_reg(rs1) + u_rv32_cpu.read_reg(rs2);
        u_rv32_cpu.write_reg(rd, reg_data);
        break;
      }
      case SUB : {
        reg_data = u_rv32_cpu.read_reg(rs1) - u_rv32_cpu.read_reg(rs2);
        u_rv32_cpu.write_reg(rd, reg_data);
        break;
      }
...

かなり冗長な記述をしているが、これで回路規模がどのようになるのかは検証していない。 もう少し最適化の余地がありそうな気がする。

そして、さらに迷ったのが、高位合成デザイン内でfprintf()などのデバッガを仕込んでいる場合、これを合成時に除去するためのdefineは、__SYNTHESIS__というものを使うらしい。 FPGAの部屋を調べると書いてあった。ありがたい。

marsee101.blog19.fc2.com

  • cpu_hls.cpp
#ifndef __SYNTHESIS__
  FILE *cpu_log;
  if ((cpu_log = fopen("cpu.log", "w")) == NULL) {
    perror ("cpu.log");
  }
#endif  // __SYNTHESIS__

#ifndef __SYNTHESIS__
    fprintf(cpu_log, "[%08x] : %08x DASM(%08x)\n", pc, inst, inst);
#endif // __SYNTHESIS__

fprintf()でログを出力し、その内容を解析した。

spike-dasm < cpu.log

結果は以下のようになった。fenceはまだ実装していないのでそこで停止してしまったが、概ね動作しているような気がする。ちゃんとレジスタ書き込み値も出力した方が良いな。

[00000000] : 04c0006f j       pc + 0x4c
[0000004c] : f1402573 csrr    a0, mhartid
[00000050] : 00051063 bnez    a0, pc + 0
[00000054] : 00000297 auipc   t0, 0x0
[00000058] : 01028293 addi    t0, t0, 16
[0000005c] : 30529073 csrw    mtvec, t0
[00000060] : 18005073 csrwi   satp, 0
[00000064] : 00000297 auipc   t0, 0x0
[00000068] : 02028293 addi    t0, t0, 32
[0000006c] : 30529073 csrw    mtvec, t0
[00000070] : 800002b7 lui     t0, 0x80000
[00000074] : fff28293 addi    t0, t0, -1
[00000078] : 3b029073 csrw    pmpaddr0, t0
[0000007c] : 01f00293 li      t0, 31
[00000080] : 3a029073 csrw    pmpcfg0, t0
[00000084] : 00000297 auipc   t0, 0x0
[00000088] : 01828293 addi    t0, t0, 24
[0000008c] : 30529073 csrw    mtvec, t0
[00000090] : 30205073 csrwi   medeleg, 0
[00000094] : 30305073 csrwi   mideleg, 0
[00000098] : 30405073 csrwi   mie, 0
[0000009c] : 00000193 li      gp, 0
[000000a0] : 00000297 auipc   t0, 0x0
[000000a4] : f6428293 addi    t0, t0, -156
[000000a8] : 30529073 csrw    mtvec, t0
[000000ac] : 00100513 li      a0, 1
[000000b0] : 01f51513 slli    a0, a0, 31
[000000b4] : 00054863 bltz    a0, pc + 16
[000000b8] : 0ff0000f fence
f:id:msyksphinz:20190215011547p:plain
Vivado-HLSのC言語モードでCPU_HLSをシミュレーションした。

オリジナルLLVM Backendを追加しよう (19. 大きな値の生成)

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

f:id:msyksphinz:20190117013715p:plain

Cpu0チュートリアルの、ch3_largeframe.bcをうまくアセンブリ生成することができない。 大きな値を作成するために、MYRISCVXAnalyzeImmediate.cppの実装がうまく行っていないようで、これを解析している。

int test_largegframe() {
  int a[469753856];

  return 0;
}

このときにスタックをずらすため、大きな値を生成するルーチンが必要になる。このために、MYRISCVXAnalyzeImmediate.cppというルーチンを追加している。これは、MIPSのものを参考にした。

実装は進んでいるのだが、なぜか命令を生成するときにハングしてしまう。 試しに、MYRISCVXAnalyzeImmediate::Analyzeの命令生成過程をダンプしてみた。

-march=mipsだと以下のようになった。

Analyze : Imm = 1879015432, Size = 32
GetInstSeqLsADDI : Imm = 1879015432, RemSize = 32
GetInstSeqLsORI : Imm = 1879015432, RemSize = 32
Analyze : Imm = 1879015432, Size = 32
GetInstSeqLsADDI : Imm = 1879015432, RemSize = 32
GetInstSeqLsORI : Imm = 1879015432, RemSize = 32
Analyze : Imm = 1879015428, Size = 32
GetInstSeqLsADDI : Imm = 1879015428, RemSize = 32
Analyze : Imm = 1879015428, Size = 32
GetInstSeqLsADDI : Imm = 1879015428, RemSize = 32

一方でMYRISCVXだと以下のようになる。符号が完全に逆だ。

Analyze : Imm = 18446744071830536192, Size = 32
GetInstSeqLsADDI : Imm = 18446744071830536192, RemSize = 32
GetInstSeqLsORI : Imm = 18446744071830536192, RemSize = 32
Invalid opcode! 43668112, 136

Mipsの実装を参考に、スタックが伸びる方向がマイナスの場合はSUB命令を使うように変更した。

diff --git a/lib/Target/MYRISCVX/MYRISCVXSEInstrInfo.cpp b/lib/Target/MYRISCVX/MYRISCVXSEInstrInfo.cpp
index fbfc76e5810..8b0cb9f57eb 100644
--- a/lib/Target/MYRISCVX/MYRISCVXSEInstrInfo.cpp
+++ b/lib/Target/MYRISCVX/MYRISCVXSEInstrInfo.cpp
@@ -65,16 +65,21 @@ void MYRISCVXSEInstrInfo::adjustStackPtr(unsigned SP, int64_t Amount,
                                          MachineBasicBlock &MBB,
                                          MachineBasicBlock::iterator I) const {
   DebugLoc DL = I != MBB.end() ? I->getDebugLoc() : DebugLoc();
-  unsigned ADD  = MYRISCVX::ADD;
-  unsigned ADDI = MYRISCVX::ADDI;

-  if (isInt<16>(Amount)) {
+  if (isInt<12>(Amount)) {
     // addiu sp, sp, amount
-    BuildMI(MBB, I, DL, get(ADDI), SP).addReg(SP).addImm(Amount);
+    BuildMI(MBB, I, DL, get(MYRISCVX::ADDI), SP).addReg(SP).addImm(Amount);
   }
   else { // Expand immediate that doesn't fit in 16-bit.
+    // For numbers which are not 16bit integers we synthesize Amount inline
+    // then add or subtract it from sp.
+    unsigned Opc = MYRISCVX::ADD;
+    if (Amount < 0) {
+      Opc = MYRISCVX::SUB;
+      Amount = -Amount;
+    }
     unsigned Reg = loadImmediate(Amount, MBB, I, DL, nullptr);
-    BuildMI(MBB, I, DL, get(ADD), SP).addReg(SP).addReg(Reg, RegState::Kill);
+    BuildMI(MBB, I, DL, get(Opc), SP).addReg(SP).addReg(Reg, RegState::Kill);
   }
 }

と思ったらベースになるメンバ変数にOpcodeをアサインしていなかった。しょうもないミスをしてしまった。 っていうか直接MYRISCVX::命令名を書くのじゃダメなのか...?

diff --git a/lib/Target/MYRISCVX/MYRISCVXAnalyzeImmediate.h b/lib/Target/MYRISCVX/MYRISCVXAnalyzeImmediate.h
index 2548555cc0b..f81c3f792e4 100644
--- a/lib/Target/MYRISCVX/MYRISCVXAnalyzeImmediate.h
+++ b/lib/Target/MYRISCVX/MYRISCVXAnalyzeImmediate.h
@@ -54,7 +54,6 @@ namespace llvm {
     /// return it in Insts.
     void GetShortestSeq(InstSeqLs &SeqLs, InstSeq &Insts);
     unsigned Size;
-    unsigned ADDI, ORI, SHL, LUi;
     InstSeq Insts;
   };
 }
diff --git a/lib/Target/MYRISCVX/MYRISCVXAnalyzeImmediate.cpp b/lib/Target/MYRISCVX/MYRISCVXAnalyzeImmediate.cpp
index 19a0b629207..834e2c75d19 100644
--- a/lib/Target/MYRISCVX/MYRISCVXAnalyzeImmediate.cpp
+++ b/lib/Target/MYRISCVX/MYRISCVXAnalyzeImmediate.cpp
@@ -29,14 +29,14 @@ void MYRISCVXAnalyzeImmediate::ADDInstr(InstSeqLs &SeqLs, const Inst &I) {
 void MYRISCVXAnalyzeImmediate::GetInstSeqLsADDI(uint64_t Imm, unsigned RemSize,
                                                 InstSeqLs &SeqLs) {
   GetInstSeqLs((Imm + 0x8000ULL) & 0xffffffffffff0000ULL, RemSize, SeqLs);
-  ADDInstr(SeqLs, Inst(ADDI, Imm & 0xffffULL));
+  ADDInstr(SeqLs, Inst(MYRISCVX::ADDI, Imm & 0xffffULL));
 }

これでllcを生成する。再度ch3_largeframe.bcを食わせてみると、今度はRegのレジスタアドレスがおかしいと言われた。

        .globl  _Z16test_largegframev   # -- Begin function _Z16test_largegframev
        .p2align        2
        .type   _Z16test_largegframev,@function
        .ent    _Z16test_largegframev   # @_Z16test_largegframev
_Z16test_largegframev:
        .frame  $x8,1879015424,$x1
        .mask   0x00000000,0
        .set    noreorder
        .set    nomacro
# %bb.0:                                # %entry
        lui     llc: /home/masayuki/others/riscv/llvm/build-myriscvx/lib/Target/MYRISCVX/MYRISCVXGenAsmWriter.inc:238: static const char *llvm::MYRISCVXInstPrinter::getRegisterName(unsigned int): Assertion `RegNo && RegNo < 33 && "Invali
d register number!"' failed.
Stack dump:

うーん、このレジスタCreateVirtualRegister関数を使って生成しているレジスタだ。つまり、CreateVirtualRegisterで作ったレジスタを正しく割り当てできていないことになる。これは実装の途中ではまだ未対応なのか?よく分からない。

仕方がないので、とりあえずS0レジスタに決め打ちでレジスタを割り付けた。これなら最後まで生成できる。

diff --git a/lib/Target/MYRISCVX/MYRISCVXSEInstrInfo.cpp b/lib/Target/MYRISCVX/MYRISCVXSEInstrInfo.cpp
index a6cbc2bd79d..b358cdb3c01 100644
--- a/lib/Target/MYRISCVX/MYRISCVXSEInstrInfo.cpp
+++ b/lib/Target/MYRISCVX/MYRISCVXSEInstrInfo.cpp
@@ -98,11 +98,15 @@ MYRISCVXSEInstrInfo::loadImmediate(int64_t Imm, MachineBasicBlock &MBB,
   bool LastInstrIsADDiu = NewImm;

   MachineRegisterInfo &RegInfo = MBB.getParent()->getRegInfo();
+
   // The first instruction can be a LUi, which is different from other
   // instructions (ADDiu, ORI and SLL) in that it does not have a register
   // operand.
   const TargetRegisterClass *RC = &MYRISCVX::GPRRegClass;
-  unsigned Reg = RegInfo.createVirtualRegister(RC);
+
+  // MachineRegisterInfo &MRI = MBB.getParent()->getRegInfo();
+  // xxx: It's very temporal implementation
+  unsigned Reg = MYRISCVX::S0;

   const MYRISCVXAnalyzeImmediate::InstSeq &Seq =
       AnalyzeImm.Analyze(Imm, Size, LastInstrIsADDiu);

これは最終的には修正してCreateVirtualRegister()に対応させなければならない。とりあえずこのまま進んで大丈夫かな。。。

大きな整数を生成するルーチン

例えば、469753856 x 4 =1879015424 (= 0x6FFF8000) のような値を生成する場合、AnalyzeImmediate関数は以下のような処理を行う。

&MYRISCVXAnalyzeImmediate::Analyze(uint64_t Imm, unsigned Size,
                                   bool LastInstrIsADDI) {
  // Get the list of instruction sequences.
  if (LastInstrIsADDI | !Imm)
    GetInstSeqLsADDI(Imm, Size, SeqLs);
  else
    GetInstSeqLs(Imm, Size, SeqLs);
  GetShortestSeq(SeqLs, Insts);
  return Insts;
}

Immに0x6FFF8000が入っている。GetInstSeqLs(Imm, Size, SeqLs)が呼ばれる。

void MYRISCVXAnalyzeImmediate::GetInstSeqLs(uint64_t Imm, unsigned RemSize,
                                            InstSeqLs &SeqLs) {
  uint64_t MaskedImm = Imm & (0xffffffffffffffffULL >> (64 - Size));
  // Do nothing if Imm is 0.
  if (!MaskedImm)
    return;
  // A single ADDI will do if RemSize <= 16.
  if (RemSize <= 16) {
    ADDInstr(SeqLs, Inst(MYRISCVX::ADDI, MaskedImm));
    return;
  }
  // Shift if the lower 16-bit is cleared.
  if (!(Imm & 0xffff)) {
    GetInstSeqLsSHL(Imm, RemSize, SeqLs);
    return;

  }

この部分は条件に入らないので除去される。下位16ビットが0の場合はGetInstSeqLsSHL(Imm, RemSize, SeqLs)が呼ばれ、単純な16ビットシフトによる命令生成が行われる。 そうでない場合、

  GetInstSeqLsADDI(Imm, RemSize, SeqLs);

ADDIを使った整数命令処理が行われる。これは、まず上位の16ビットを作って、それからADDI命令を使って足し算をして、大きな整数を作るという処理になる。

void MYRISCVXAnalyzeImmediate::GetInstSeqLsADDI(uint64_t Imm, unsigned RemSize,
                                                InstSeqLs &SeqLs) {
  GetInstSeqLs((Imm + 0x8000ULL) & 0xffffffffffff0000ULL, RemSize, SeqLs);
  ADDInstr(SeqLs, Inst(MYRISCVX::ADDI, Imm & 0xffffULL));
}

というわけでもう一度GetInstSeqLs((Imm + 0x8000ULL) & 0xffffffffffff0000ULL, RemSize, SeqLs)が呼ばれる。この時、下位の16ビットは全部0になっているので、先ほどのルーチンでGetInstSeqLsSHL()が呼ばれる。

void MYRISCVXAnalyzeImmediate::GetInstSeqLsSHL(uint64_t Imm, unsigned RemSize,
                                               InstSeqLs &SeqLs) {
  unsigned Shamt = countTrailingZeros(Imm);
  GetInstSeqLs(Imm >> Shamt, RemSize - Shamt, SeqLs);
  ADDInstr(SeqLs, Inst(MYRISCVX::SRL, Shamt));
}

上記のルーチンは、下位の0になっている部分をシフトして除去し、まずその値をGetInstSeqLs(Imm >> Shamt, RemSize - Shamt, SeqLs)で生成する。 次にMYRISCVX::SRLで、必要な分だけシフトして値を生成する。

これらの処理を繰り返して、まずは

  • ADDIによる上位16ビットの値を下位ビットに生成する。
  • SRLによる論理シフトで上位ビット生成
  • ADDIで下位の値を作る。

という一連の命令が生成される。

これを最適化する。具体的にはGetShortestSeq()関数、さらにReplaceADDISHLWithLUi()で処理される。

つまり、ADDI、SHLの処理がLUI命令に置き換えることができるならば置き換える。

void MYRISCVXAnalyzeImmediate::ReplaceADDISHLWithLUi(InstSeq &Seq) {
  // Check if the first two instructions are ADDI and SHL and the shift amount
  // is at least 16.
  if ((Seq.size() < 2) || (Seq[0].Opc != MYRISCVX::ADDI) ||
      (Seq[1].Opc != MYRISCVX::SRL) || (Seq[1].ImmOpnd < 16))
    return;
  // Sign-extend and shift operand of ADDI and see if it still fits in 16-bit.
  int64_t Imm = SignExtend64<16>(Seq[0].ImmOpnd);
  int64_t ShiftedImm = (uint64_t)Imm << (Seq[1].ImmOpnd - 16);
  if (!isInt<16>(ShiftedImm))
    return;
  // Replace the first instruction and erase the second.
  Seq[0].Opc = MYRISCVX::LUI;
  Seq[0].ImmOpnd = (unsigned)(ShiftedImm & 0xffff);
  Seq.erase(Seq.begin() + 1);

というところまでは理解できた。ただし、これはRISC-V のADDI、つまり即値が12ビットの時に対応できていないので、今度はそれを考える。

Vivado-HLSを使って高位合成でCPUを作ってみる(1. メモリのRead/Writeのモデルを作成)

Vivado-HLSで高位合成からCPUを作ってみたい。 高位合成でCPUを作るというのはネタとしてはイマイチだけれども、高位合成がどういうものか、目標をもって試してみるには良い題材だと思うので試してみている。

前回はとりあえずメモリにアクセスしてデータをフェッチしたり書き込んだりするモデルを作った。 次は、本格的にCPUっぽいものを作っていく。

目標としてはRISC-VのISSに近い、C++の実装からVivado-HLSを通じて高位合成でハードウェアを生成することだ。 C++ISSを作った場合、プロセッサのモデルをクラス化し、そこからいろんなメソッドを追加するような構成になる(と思う)。

そこで、まずは一般的なCPUのクラスを作成し、そこにレジスタ、メモリアクセスのメソッド、システムレジスタアクセス用のサブクラスを追加していく。

f:id:msyksphinz:20190213005116p:plain
高位合成で使用するCPUのファイル群の構成
class rv32_cpu {
  XLEN_t m_reg32[32];
  rv32_csr m_rv32_csr;
  uint32_t *m_data_mem;

public:
  rv32_cpu (uint32_t *data_mem);
...

m_rv32_csrCSRレジスタをまとめたクラスだ。m_reg32[32]汎用レジスタ群となる。

まず、命令をフェッチしてデコードする。

  do {
    inst = inst_mem[addr];
    dec_inst = u_rv32_cpu.decode_inst(inst);

デコード論理はこんな感じ。非常にしょぼい。

inst_rv32_t rv32_cpu::decode_inst (uint32_t inst)
{
  switch(inst & 0x3f) {
    case 0x33 : return ADD;
    case 0x03 : return LW;
    case 0x23 : return SW;
    case 0xff : return NOP;   //
    case 0x73 :
      switch ((inst >> 12) & 0x07) {
        case 0b001 : return CSRRW  ;
        case 0b010 : return CSRRS  ;
        case 0b011 : return CSRRC  ;

次に、オペランドを取り出す。

    RegAddr_t rs1 = u_rv32_cpu.get_rs1_addr (inst);
    RegAddr_t rs2 = u_rv32_cpu.get_rs2_addr (inst);
    RegAddr_t rd  = u_rv32_cpu.get_rd_addr  (inst);
    uint16_t  csr_addr = (inst >> 16) & 0x0ffff;

次にデコード結果に基づき実行を行う。メモリアクセスについては、mem_access関数に集約し、data_mem配列にアクセスすることで実現している。

    switch (dec_inst) {
      case CSRRW  : {
        reg_data = u_rv32_cpu.csrrw (csr_addr, rs1);
        u_rv32_cpu.write_reg(rd, reg_data);
        break;
      }
      case CSRRS  : {
        reg_data = u_rv32_cpu.csrrs (csr_addr, rs1);
...
      case LW  : {
        uint32_t addr = rs1 + ((inst >> 20) & 0xfff);
        reg_data = u_rv32_cpu.mem_access(LOAD, rs1, addr, data_mem);
        u_rv32_cpu.write_reg(rd, reg_data);
        break;
      }
      case ADD : {
        reg_data = u_rv32_cpu.read_reg(rs1) + u_rv32_cpu.read_reg(rs2);
        u_rv32_cpu.write_reg(rd, reg_data);
...

という訳で、これらのCPUのモデルをクラス化した。

github.com

これでVivado-HLSを実行してみると、なるほど、ちゃんと合成が終了したようだ。 C++のクラスも扱えるし、ポインタも行ける。 Vivado-HLSはすごいなあ。

module cpu_hls (
        ap_clk,
        ap_rst_n,
        s_axi_slv0_AWVALID,
        s_axi_slv0_AWREADY,
        s_axi_slv0_AWADDR,
        s_axi_slv0_WVALID,
        s_axi_slv0_WREADY,
...
f:id:msyksphinz:20190213005435p:plain