FPGA開発日記

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

RoCCを使ったRocket Core拡張方法の調査 (5. 波形デバッグ)

前回から一生懸命RoCCインタフェースに接続したアクセラレータのデバッグをしているのだが、どうも上手く行かない。 RoCCインタフェースからコマンドを受け取って、L1キャッシュに対してアクセスをしてデータを取り出し、そのデータに対して加工を行いたいのだが、途中でステートマシンがハングしてしまう。 これをデバッグするために、シミュレーション実行時にvcdを出力して、wlfで変換しQuestaSimで見てみることにした。

f:id:msyksphinz:20170901023111p:plain

RocketCoreでシミュレーション実行結果でVCDを取得する方法

デバッグ用のシミュレーションバイナリを生成して、vcdを出力する。

cd emulator
make debug CONFIG=RoccExampleConfig
./emulator-rocketchip-RoccExampleConfig-debug +verbose -vrocc.vcd ~/riscv64/riscv64-unknown-elf/riscv64-unknown-elf/bin/pk ../../rocket-rocc-examples/build/test-accumulator 2> rocc_debug.log

さらに、vcdをwlfに変換して、QuestaSimで波形デバッグする。

vcd2wlf rocc.vcd rocc.wlf
vsim -gui rocc.wlf

昨日もちょっと愚痴ったが、この一連の流れを実施するのに、おおよそイタレーションとしては2時間程度かかってしまう。Chiselの状態でシミュレーションさせてくれ。。。 何のためのVerilogラッパーだよ。。。

RoCCインタフェースの解析

L1 Dキャッシュに対して複数メモリアクセスを実施しなければならないのだが、途中でステートマシンが止まっているのはL1 Dキャッシュへアクセスするインタフェースの、Ready信号がずっと落ちてしまっておりステートマシンが動作しない。

f:id:msyksphinz:20170901022813p:plain

f:id:msyksphinz:20170901023345p:plain

このあたり、資料が無くて本当にやりづらい。。。

2017/09/02追記。Request Validを、Request Readyが上がるまで待つっていうステートマシンに変えたら本当にRequest Readyが上がらなくて泣きそうである。

f:id:msyksphinz:20170902025017p:plain

The RISC-V Book

なんだいこれは?真ん中にあるやつ。モナリザが異様な雰囲気を醸し出している。

f:id:msyksphinz:20170831011010p:plain

riscv.org

  • 著者: David Patterson & Andrew Waterman
  • 発売日: September 1, 2017
  • 179ページ
  • 価格 : $10

やっす!でもAmazonにまだ出ていない。モナリザの顔で、179ページも何を語るのか。

A slim introduction and reference to RISC-V for students and embedded systems programmers. It has one chapter per RV instruction extension, 50 pages of instruction definitions, a reference (“green”) card, and code comparisons to ARM, MIPS, & x86.

学生の勉強用みたいね。

Chiselでのprintfデバッグの方法

f:id:msyksphinz:20170830023047p:plain

Chisel、コンセプトとしてはいいのかもしれないが、マジで書きにくいなあ。 一度Scalaでシミュレーションすればよいのかもしれないが、Rocket CoreのScalaシミュレーションとかどうやるんだろう? RTLシミュレーションするのだとしても一度Verilogに変換してからシミュレーションするのですごく遅いし、今現在ではフリーのツールではverilatorでvcdしか出力できないので、すごく遅い。

今現在でできることと言えば、念入りにprintfデバッグをすることだ。Chiselの開発環境においてprintfデバッグとはどうやって行うのか、調べた。

まず、本家の説明はこのようになっている。

Printing in Chisel · freechipsproject/chisel3 Wiki · GitHub

こちらの説明では、Chiselで普通にprintfを埋め込むことができるようになっている。実際。自作のアクセラレータ上で、printfを埋め込むことができた。

上記のWikiの説明では、以下のようになっているが、

val myUInt = 33.U
printf(p"myUInt = $myUInt") // myUInt = 33

どちらかというとこっちの方がしっくりくるなあ。

    printf("MemTotalExample: Command Received. %x, %x\n", io.cmd.bits.rs1, io.cmd.bits.rs2)

という訳で、ガンガンprintf()を入れて自作アクセラレータのデバッグを行っているわけだが、まだ時間がかかりそうだ。。。

ちなみに、Verilogに変換されると、このprintf()は以下のようにVerilogのシミュレーション用に埋め込まれ、Verilatorの実行中にも出力することができる。

RocketCoreの構成はRoccExampleConfig、シミュレーションは以下のようにして実行している。

make CONFIG=RoccExampleConfig
./emulator-rocketchip-RoccExampleConfig +verbose  ~/riscv64/riscv64-unknown-elf/riscv64-unknown-elf/bin/pk ../../rocket-rocc-examples/build/test-accumulator 2> rocc.log
  • emulator/generated-src/rocketchip.RoccExampleConfig.v
...
        end
      end
    end
    `ifndef SYNTHESIS
    `ifdef PRINTF_COND
      if (`PRINTF_COND) begin
    `endif
        if (_T_1526 & _T_1528) begin
          $fwrite(32'h80000002,"MemTotalExample: Command Received. %h, %h\n",io_cmd_bits_rs1,io_cmd_bits_rs2);
        end
    `ifdef PRINTF_COND
      end
    `endif
    `endif
    `ifndef SYNTHESIS
    `ifdef PRINTF_COND
      if (`PRINTF_COND) begin
    `endif
        if (io_mem_resp_valid & _T_1528) begin
          $fwrite(32'h80000002,"MemTotalExample: IO.MEM Received %h %h\n",io_mem_resp_bits_data,r_state);
        end
    `ifdef PRINTF_COND
      end
    `endif

すると、rocc.logにアクセラレータが呼び出されると以下のデバッグ分が表示される。

C                   0:    4493955 [1] pc=[000001012c] W[r12=0000000000000064][1] R[r 0=0000000000000000] R[r 4=0000000000000003] inst=[06400613] DASM(06400613)
C                   0:    4493956 [1] pc=[0000010130] W[r10=000000008ffffac0][1] R[r11=000000008ffffac0] R[r12=0000000000000064] inst=[00c5f55b] DASM(00c5f55b)
MemTotalExample: Command Received. 000000008ffffac0, 0000000000000064
C                   0:    4493957 [0] pc=[0000010130] W[r 0=000000008ffffac0][0] R[r11=0000000000000064] R[r12=000000008ffffac0] inst=[00c5f55b] DASM(00c5f55b)
C                   0:    4493958 [0] pc=[0000010130] W[r 0=000000008ffffac0][0] R[r11=000000008ffffac0] R[r12=000000008ffffac0] inst=[00c5f55b] DASM(00c5f55b)
C                   0:    4493959 [0] pc=[0000010130] W[r 0=000000008ffffac0][0] R[r11=000000008ffffac0] R[r12=000000008ffffac0] inst=[00c5f55b] DASM(00c5f55b)
...

C                   0:    4493991 [0] pc=[0000010130] W[r 0=000000008ffffac0][0] R[r11=000000008ffffac0] R[r12=000000008ffffac0] inst=[00c5f55b] DASM(00c5f55b)
C                   0:    4493992 [0] pc=[0000010130] W[r 0=000000008ffffac0][0] R[r11=000000008ffffac0] R[r12=000000008ffffac0] inst=[00c5f55b] DASM(00c5f55b)
MemTotalExample: IO.MEM Received 0000000000000000 1
C                   0:    4493993 [0] pc=[0000010130] W[r 0=000000008ffffac0][0] R[r11=000000008ffffac0] R[r12=000000008ffffac0] inst=[00c5f55b] DASM(00c5f55b)
MemTotalExample: IO.MEM Received 0000000000000000 1
C                   0:    4493994 [0] pc=[0000010130] W[r 0=000000008ffffac0][0] R[r11=000000008ffffac0] R[r12=000000008ffffac0] inst=[00c5f55b] DASM(00c5f55b)
C                   0:    4493995 [0] pc=[0000010130] W[r 0=000000008ffffac0][0] R[r11=000000008ffffac0] R[r12=000000008ffffac0] inst=[00c5f55b] DASM(00c5f55b)
...

まあ便利と言えば便利だけど、Rocketの場合は特に、一回のシミュレーションで時間がかかりすぎる。 もうちょっと何とかならないかなあ。

CentOS7にriscv-toolsをインストールするための手順

f:id:msyksphinz:20170830020919p:plain

完全に自分のメモな訳だが。。。

ちょっとした事情でCentOSRISC-Vの環境をインストールする必要が生じたので、一応メモしておく。 通常の手順はUbuntu上での手順が基本なのだが、CentOSの場合はパッケージのインストール方法が異なるだけだ。

Ubuntuの場合のインストール方法

これはriscv-toolsのREADME通りにパッケージをインストールすればよい。

$ sudo dnf install autoconf automake @development-tools curl dtc libmpc-devel mpfr-devel gmp-devel gawk build-essential bison flex texinfo gperf libtool patchutils bc zlib-devel

CentOSの場合のインストール方法

ターゲットとしたのは、CentOSの現在の最新バージョン CentOS 7.3 1611 だ。

Index of /Linux/centos/7.3.1611/isos/x86_64

yumパッケージのインストール

パッケージがいくつか異なり、yumを使ってインストールする必要がある。

sudo yum install -y git automake gcc libevent-devel ncurses-devel libtool libusbx-devel gcc-c++ bison flex swig python-devel texinfo bzip2 zlib-devel

device-tree-compilerのインストール

CentOSでDevice Tree Compilerのインストール方法が見つからなかったので、ソースコードをダウンロードしてビルドした。

git clone git://git.kernel.org/pub/scm/utils/dtc/dtc.git
cd dtc
make && sudo make install
cd -

riscv-toolsのインストール

ここまで来れば大丈夫。通常のフローでインストール可能だ。

export RISCV=/home/msyksphinz/riscv
git clone https://github.com/freechipsproject/rocket-chip.git --recurse-submodules   # 今回はRocketCoreごとダウンロード
cd rocket-chip/riscv-tools
./build.sh
...

RISC-V Toolchain installation completed!

上手く行った。

RoCCを使ったRocket Core拡張方法の調査(4. 独自Acceleratorの作成)

RoCCへの接続方法について、少しずつ分かってきたので、独自Acceleratorを作って、接続してみたい。願わくば、FPGAで動作確認できるところまで行ってみたいな。

RoCC Interfaceについて

RoCCには、いくつかインタフェースが入っているが、大きく分けて使用するのは、

  • io.cmd: Rocket Coreからの発行命令情報など。
  • io.mem: L1キャッシュへのアクセスインタフェース

この2つが制御できれば問題ないと思われる。ここでは、オペランド値1で指定されたアドレスから、オペランド値2で指定されたサイズのデータをL1キャッシュから読み込み、値をすべて加算して返すという回路を作ってみたい。

最終的には、行列積を実行できる回路を作ってみたいが、これはまずは前哨戦。

MemTotal Acceleratorの概要

今回作ってみたいのは、L1キャッシュからデータを読み込んでそれを加算し、結果をRocket Coreに戻す命令。 用途的には全く意味が無いけれども、まずは簡単なものから。

f:id:msyksphinz:20170828225011p:plain

ステートマシンの記述と、L1へのアクセスはサンプルデザインを元に作成した。

これを使ってテストを実行しているのだが、どうも上手く行かない。っていうか、コンパイルとシミュレーションは、しっかりとVerilogでシミュレーションするくらいに時間がかかる。 これならばVerilogを使った方がデバッグも楽だし分かりやすいんじゃ。。。とか思ったら負けなので、継続して試行していく予定。

RoCCを使ったRocket Coreの拡張方法の調査 (3. サンプルデザインの解析)

RoCC (Rocket Custom Coprocessor) のチュートリアルって意外と少ないので、調査するのに苦労する。

前回のサンプルプログラムは、一応意図通りに動作したのだが、いったいどのような仕組みになっているのか調査してみる。

test_accumulator.c の解析

test_accumulator.c は、大きく2つのセクションに分かれている。accumulatorのテストと、 translatorのテストだ。

f:id:msyksphinz:20170826134021p:plain

accumulatorの動作

accumulatorの制御は、簡略化すると以下のようになっている。

  uint64_t data [] = {0xdead, 0xbeef, 0x0bad, 0xf00d}, y;

  uint16_t addr = 1;

  doWrite(y, addr, data[0]);    // 
  doRead(y, addr);
  uint64_t data_accum = -data[0] + data[1];
  doAccum(y, addr, data_accum);
  doRead(y, addr);
  1. doWrite により、Accumulator内のレジスタ(addr=1)に0xdeadを書き込む。
  2. doReadにより、Accumulator内のレジスタ(addr=1)を読み込む。(0x0000deadが読み込まれる)。
  3. -data[0]+dead[1]を実行する。答えは、-0x0000dead+0x0000beef=0xffffe042
  4. Accumulator内レジスタに対して0xffffe042を加算する。 0xffffe042+0x0000dead=0x0000beef
  5. Accumulator内レジスタを読み込む。0x0000beefが読み込まれる。

という仕組みになっている。doWrite(), doRead(), doAccum()はどのように実装されているかというと、custom0命令によって実現されている。

  • src/main/c/accumulator.h
#define k_DO_WRITE 0
#define k_DO_READ 1
#define k_DO_LOAD 2
#define k_DO_ACCUM 3

#define XCUSTOM_ACC 0

#define doWrite(y, rocc_rd, data)                                       \
  ROCC_INSTRUCTION(XCUSTOM_ACC, y, data, rocc_rd, k_DO_WRITE);
#define doRead(y, rocc_rd)                                              \
  ROCC_INSTRUCTION(XCUSTOM_ACC, y, 0, rocc_rd, k_DO_READ);
#define doLoad(y, rocc_rd, mem_addr)                                    \
  ROCC_INSTRUCTION(XCUSTOM_ACC, y, mem_addr, rocc_rd, k_DO_LOAD);
#define doAccum(y, rocc_rd, data) \
  ROCC_INSTRUCTION(XCUSTOM_ACC, y, data, rocc_rd, k_DO_ACCUM);
  • src/main/c/rocc.h
#define ROCC_INSTRUCTION(X, rd, rs1, rs2, funct)                \
  ROCC_INSTRUCTION_R_R_R(X, rd, rs1, rs2, funct, 10, 11, 12)

// rd, rs1, and rs2 are data
// rd_n, rs_1, and rs2_n are the register numbers to use
#define ROCC_INSTRUCTION_R_R_R(X, rd, rs1, rs2, funct, rd_n, rs1_n, rs2_n) { \
    register uint64_t rd_  asm ("x" # rd_n);                            \
    register uint64_t rs1_ asm ("x" # rs1_n) = (uint64_t) rs1;          \
    register uint64_t rs2_ asm ("x" # rs2_n) = (uint64_t) rs2;          \
    asm volatile (                                                      \
        ".word " STR(CUSTOMX(X, rd_n, rs1_n, rs2_n, funct)) "\n\t"      \
        : "=r" (rd_)                                                    \
        : [_rs1] "r" (rs1_), [_rs2] "r" (rs2_));                        \
    rd = rd_;                                                           \
  }

translatorの動作

translatorは、仮想アドレスを物理アドレスに変換宇するためのハードウェアを制御している。このために、ptwを制御するためのフローを用意している。

  private val ptw = io.ptw(0)

  when (ptw.req.fire()) { state := s_ptw_resp }

  when (state === s_ptw_resp && ptw.resp.valid) {
    pte := ptw.resp.bits.pte
    state := s_resp
  }

CharacterCountの動作

これだけサンプルが無かったのでいろいろ探すしかないのだが、io.autlという説明のない場所にリクエストが渡っており、正直良く分からん。。。

以下のようなプログラムを作って動作を見てみたが、自分の考える答えと一致しないなあ。。。調査中。

  uint64_t data_addr;
  char str[] = "hello world";
  doTranslate (data_addr, str);
  printf ("[DoTranslate] virt: %p, phys: %p\n", str, (void *)data_addr);
  uint64_t count;
  countChar (count, data_addr);
  printf ("[countChar] 0x%lx\n", count);
  data_addr = (uint64_t)str;
  countChar (count, data_addr);
  printf ("[countChar] 0x%lx\n", count);

シミュレーション結果。

$ ./emulator-rocketchip-RoccExampleConfig ~/riscv64/riscv64-unknown-elf/riscv64-unknown-elf/bin/pk ../../rocket-rocc-examples/build/test-accumulator
[DoTranslate] virt: 0xfeefb00, phys: 0x8ffffb00
[countChar] 0x0
[countChar] 0x1

RoCCを使ったRocket Coreの拡張方法の調査 (2. サンプルデザインの実行)

f:id:msyksphinz:20170825021220p:plain

RoCC (Rocket Custom Coprocessor) のチュートリアルって意外と少ないので、調査するのに苦労する。

githubチュートリアルっぽいものがあったので、試行してみようと思った。

github.com

これは、RocketCoreのカスタムデザインの中でアキュムレータを内蔵しているデザインがあるので、これをソフトウェアで叩くアプリケーションを作ってみる、という話になっているようだ。

RocketCoreを生成する際に、CONFIGオプションとしてRoccExampleConfigを指定することで、RoCCにアクセラレータが接続されたデザインを生成することができる。

ちなみに、RoccExampleConfigを指定することで、以下のConfig.scalaの記述の構成が有効になる。

class RoccExampleConfig extends Config(new WithRoccExample ++ new BaseConfig)

うーん、難しいねえ。WithRoccExampleというのは何だ?

class WithRoccExample extends Config(
  (pname, site, here) => pname match {
    case BuildRoCC => Seq(
      RoccParameters(
        opcodes = OpcodeSet.custom0,
        generator = (p: Parameters) => Module(new AccumulatorExample()(p))),
      RoccParameters(
        opcodes = OpcodeSet.custom1,
        generator = (p: Parameters) => Module(new TranslatorExample()(p)),
        nPTWPorts = 1),
      RoccParameters(
        opcodes = OpcodeSet.custom2,
        generator = (p: Parameters) => Module(new CharacterCountExample()(p))))

    case RoccMaxTaggedMemXacts => 1
    case _ => throw new CDEMatchError
  })

なるほど、custom0, custom1, custom2 のカスタム命令に対して、それぞれ AccumulatorExample, TranslatorExample, CharacterCountExampleのデザインを接続しているように見える。それぞれ、

  • AccumulatorExample (src/main/scala/rocket/Rocc.scala)
  • TranslatorExample (src/main/scala/rocket/Rocc.scala)
  • CharacterCountExample (src/main/scala/rocket/Rocc.scala)
class AccumulatorExample(n: Int = 4)(implicit p: Parameters) extends RoCC()(p) {
  val regfile = Mem(n, UInt(width = xLen))
  val busy = Reg(init = Vec.fill(n){Bool(false)})

...
  // datapath
  val addend = cmd.bits.rs1
  val accum = regfile(addr)
  val wdata = Mux(doWrite, addend, accum + addend)

  when (cmd.fire() && (doWrite || doAccum)) {
    regfile(addr) := wdata
  }

本質は↑のような気がしていて、レジスタファイルがアキュムレータとして動作しており、cmd.bits.rs1で指定した数を加算しているように見える。 メモリアクセスを実行すると同じようにレジスタファイルにデータをロードしているようにも見えるね。詳細仕様がないので正直良く分からない。

このRoccExampleConfigを有効にしてRocketCoreをビルドするためには、RocketChipのリポジトリ内でemulatorディレクトリに移動し、以下のようにタイプする。

make CONFIG=RoccExampleConfig

これでビルドが完了する。次に、Accumulatorのテストプログラムを動作させるためにrocket-rocc-exampleリポジトリに格納されているパッチを当てた。

git clone https://github.com/seldridge/rocket-rocc-examples.git
cd ../rocket-chip/riscv-tools/riscv-pk
git apply ../../../rocket-rocc-examples/patches/riscv-pk.patch
mkdir build
cd build
../configure --prefix=$RISCV/riscv64-unknown-elf --host=riscv64-unknown-elf
make
make install

で、環境変数RISCVで指定されているツールセットのディレクトリで、pk(proxy kernel)が更新される。 ちなみに、パッチの内容だが、query-mem()の設定をすべて取り払っており、メモリサイズを固定している。なんだこりゃ?

diff --git a/machine/configstring.c b/machine/configstring.c
index fb2fed7..f3f21d2 100644
--- a/machine/configstring.c
+++ b/machine/configstring.c
@@ -6,12 +6,7 @@

 static void query_mem(const char* config_string)
 {
-  query_result res = query_config_string(config_string, "ram{0{addr");
-  assert(res.start);
-  uintptr_t base = get_uint(res);
-  assert(base == DRAM_BASE);
-  res = query_config_string(config_string, "ram{0{size");
-  mem_size = get_uint(res);
+  mem_size = 0x10000000;
 }

 static void query_rtc(const char* config_string)
diff --git a/machine/minit.c b/machine/minit.c
index b3f2c86..54ff88e 100644
--- a/machine/minit.c
+++ b/machine/minit.c
@@ -19,6 +19,7 @@ static void mstatus_init()
   uintptr_t ms = 0;
   ms = INSERT_FIELD(ms, MSTATUS_VM, VM_CHOICE);
   ms = INSERT_FIELD(ms, MSTATUS_FS, 1);
+  ms = INSERT_FIELD(ms, MSTATUS_XS, 1);
   write_csr(mstatus, ms);

   // Make sure the hart actually supports the VM mode we want

次にテストプログラムのビルドを行った。テストプログラムはrocket-rocc-exampleリポジトリに入っているので、そのリポジトリに戻ってmakeを実行するだけでよい。

cd ../../../rocket-rocc-exsamples
make

これで、rocket-rocc-examples/build/test-accumulatorにテストプログラムが作られた。

このテストプログラム、上手く動作するようならば、以下のようにAccumulatorを動かしてログが出力されるはずである。

[INFO] Write R[1] = 0xdead
[INFO] Read R[1]
[INFO]   Received 0xdead
[INFO] Accum R[1] with 0xffffffffffffe042
[INFO] Read R[1]
[INFO]   Received 0xbeef
[INFO] Load 0xbad (virt: 0x0xfeefac0, phys: 0x0x8ffffac0) via L1 data cache
[INFO] Read R[1]
[INFO]   Received 0xbad
Completed after 5614110 cycles

ところが、今回同様にテストプログラムを走らせると、アサーションエラーとなった。

$ ./emulator-rocketchip-RoccExampleConfig ~/riscv64/riscv64-unknown-elf/riscv64-unknown-elf/bin/pk ../../rocket-rocc-examples/build/test-accumulator
../pk/elf.c:46: assertion failed: !(eh.e_flags & EF_RISCV_RVC)

これは、pk/elf.cの以下の記述が問題らしい。RVCというのはCompressed Instructionのことで、RISC-Vの16bit命令セットのことだ。これで落ちたということは、__riscv_compressedが指定されていなかったにもかかわらず、EF_RISCV_RVCフラグが有効になっていたということかしら。 とりあえず、ここの記述をコメントアウトしてみるとどうだろう。

#ifndef __riscv_compressed
  assert(!(eh.e_flags & EF_RISCV_RVC));
#endif
mkdir build
../configure --prefix=$RISCV/riscv64-unknown-elf --host=riscv64-unknown-elf && make && make install

これで再度RocketChipを実行してみる。

$ ./emulator-rocketchip-RoccExampleConfig ~/riscv64/riscv64-unknown-elf/riscv64-unknown-elf/bin/pk ../../rocket-rocc-examples/build/test-accumulator
[INFO] Write R[1] = 0xdead
[INFO] Read R[1]
[INFO]   Received 0xdead
[INFO] Accum R[1] with 0xffffffffffffe042
[INFO] Read R[1]
[INFO]   Received 0xbeef
[INFO] Load 0xbad (virt: 0x0xfeefae0, phys: 0x0x8ffffae0) via L1 data cache
[INFO] Read R[1]
[INFO]   Received 0xbad

おお、所望通りに動作した。次に、このカスタムデザインの中身を読み解いていこう。