FPGA開発日記

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

Rocket Chipの足回りを理解する (6. デバッグモジュールによるRocket Chipの内部レジスタアクセス)

RISC-VのフラグシップモデルであるRocket Chip Generatorは、RISC-Vのほぼ全ての仕様を網羅している非常に優れているデザインだが、最初はなかなか取っつきにくい部分がある。

それはいくつかあるのだが、

  • ChiselというScala拡張の言語で記述されており、Verilogに慣れている人にとって見れば読みにくい。
  • DPI-Cが多用されており、プログラムのロードやデバッグ機構がどのように実現してあるのかが読みにくい。

例によって私も苦戦しているのだが、特にVerilatorのオプションで指定したelfファイルが、どのようにしてRocket Chipにロードされ、実行されるのかを理解するのはなかなか難しい。 現在、RISC-VのC言語の実装にprintf()を挿入しながら(やり方がダサいが...)、Rocket Chipがどのようにしてプログラムをロードして実行されているのかを解析している。

解析を進めていくにつれて、プログラムのロードや制御について理解するためにはRISC-Vのデバッグ仕様について理解することが不可欠だということが分かってきた。 すなわち、これまで正直殆どノーマークであった、デバッグ仕様について理解する必要がある。

  • RISC-V External Debug SupportVersion 0.13

https://static.dev.sifive.com/riscv-debug-spec-0.13.b4f1f43.pdf

Rocket Chipの制御機構とDPI-C

Rocket Chipの制御機構は、全体をまとめるTestDriverというモジュールに対しTestHarnessというモジュールが含まれている。 TestHarness にはRocket Chipの実体である ExampleRocketSystemと、メモリであるSRAMモジュール、さらにデバッグと制御を司る SimDTM がインスタンスされている。

このSimDTMが今回の主役なのだが、SimDTM.vというモジュールがインスタンスされており、この中身は debug_ticks()というC/C++で記述されたDPI関数が毎サイクル呼ばれている。これによりプログラムの制御や、デバッグモジュールの動作、プログラムのロード処理などが行われている。

f:id:msyksphinz:20171003015225p:plain

  • vsrc/SimDTM.v
  always @(posedge clk)
  begin
    r_reset <= reset;
    if (reset || r_reset)
    begin
      __debug_req_valid = 0;
      __debug_resp_ready = 0;
      __exit = 0;
    end
    else
    begin
      __exit = debug_tick(
        __debug_req_valid,
        __debug_req_ready,
        __debug_req_bits_addr,
...

debug_tick()の中身は実際には csrc/SimDTM.ccに記述されており、これも中身を読んでみるとC/C++で書かれた制御ルーチンと、SimDTM経由で接続されているデバッグ専用モジュールを接続しているという構成になる。

さて、ここで具体的にどのようにしてVerilatorから指定したelfファイルをRocket Chipに渡しているかということだが、まずはRISC-Vのフロントエンドサーバ Fesvrについて理解する必要がある。

riscv-fesvrはRokect Chipと外界を接続するためのインタフェースなのだが、Rocket ChipではDPI-Cインタフェースを通じてインスタンスされており、riscv-fesvr/fesvr/dtm.cc内で、 htif_t型のオブジェクトとしてインスタンスされている。 htif_tはコンストラクタによって、load_program()というそれっぽい関数が呼ばれているので、これを追いかけていくことにする。load_program()htif.cc内で定義されている。

void htif_t::load_program()
{

...
  std::map<std::string, uint64_t> symbols = load_elf(path.c_str(), &mem, &entry);
...

load_elf()elfloader.cc内で定義されているのだが、さあここからが大変だ。Rocket Chipのデバッグモジュールの仕様について理解していく必要がある。

  • riscv-fesvr/fesvr/elfloader.cc
std::map<std::string, uint64_t> load_elf(const char* fn, memif_t* memif, reg_t* entry)
{
...
  #define LOAD_ELF(ehdr_t, phdr_t, shdr_t, sym_t) do { \
    ehdr_t* eh = (ehdr_t*)buf; \
    phdr_t* ph = (phdr_t*)(buf + eh->e_phoff); \
    *entry = eh->e_entry; \
    assert(size >= eh->e_phoff + eh->e_phnum*sizeof(*ph)); \
    for (unsigned i = 0; i < eh->e_phnum; i++) { \
      if(ph[i].p_type == PT_LOAD && ph[i].p_memsz) { \
        if (ph[i].p_filesz) { \
          assert(size >= ph[i].p_offset + ph[i].p_filesz); \
          memif->write(ph[i].p_paddr, ph[i].p_filesz, (uint8_t*)buf + ph[i].p_offset); \
        } \
        zeros.resize(ph[i].p_memsz - ph[i].p_filesz); \
...

memif->write()という関数が呼ばれた。これがRocket Chipのメモリインタフェースとのやり取りをするための関数だが、これはデバッグプロトコルを用いてメモリとのやり取りが行われている。 さらに潜ってみよう。このmemif->write()memif.ccで定義されている。いくつか条件があるが、ここでは 0x8000_0000から0x19cバイトのプログラムを書き込み(addr= 0x80000000, len=0x01c9)するとする。

  • riscv-fesvr/fesvr/memif.cc
void memif_t::write(addr_t addr, size_t len, const void* bytes)
{

  size_t align = htif->chunk_align();

...
  if (len & (align-1))
  {
    size_t this_len = len & (align-1);
    size_t start = len - this_len;
    uint8_t chunk[align];

    htif->read_chunk(addr + start, align, chunk);
    memcpy(chunk, (char*)bytes + start, this_len);
    htif->write_chunk(addr + start, align, chunk);

    len -= this_len;
  }

chunk_align()はデータを転送する場合のアラインを指定するらしい。ここでは値を取得してみると8バイトであった。これはデータ転送量の0x19cのサイズと一致していないため、最後の0x198から0x1a0までのデータを先に取得してマージし、先に一番後ろのデータから書き込むという謎の動作からスタートする。

これで残りのデータ全てはアライン書き込みすることが出来るようになったため、一気にプログラムを書き込む。

  • riscv-fesvr/fesvr/memif.cc
    size_t max_chunk = htif->chunk_max_size();
    for (size_t pos = 0; pos < len; pos += max_chunk)
      htif->write_chunk(addr + pos, std::min(max_chunk, len - pos), (char*)bytes + pos);
  }

やっと本題に戻ってwrite_chunk()の中身だが、これはdtm.ccに戻ってくる。

  • riscv-fesvr/fesvr/dtm.cc
void dtm_t::write_chunk(uint64_t taddr, size_t len, const void* src)
{
  uint32_t prog[ram_words];
  uint32_t data[data_words];

  const uint8_t * curr = (const uint8_t*) src;

  halt(current_hart);

  uint64_t s0 = save_reg(S0);
  uint64_t s1 = save_reg(S1);
...

まずはsave_reg(S0), save_reg(S1)を見てみよう。これはRocket Chip内のs0, s1レジスタを取得して一時的に保存してくための命令だ。実体は、

uint64_t dtm_t::save_reg(unsigned regno)
{
  uint32_t data[xlen/(8*4)];
  uint32_t command = AC_ACCESS_REGISTER_TRANSFER | AC_AR_SIZE(xlen) | AC_AR_REGNO(regno);
  RUN_AC_OR_DIE(command, 0, 0, data, xlen / (8*4));

  uint64_t result = data[0];
  if (xlen > 32) {
    result |= ((uint64_t)data[1]) << 32;
  }
  return result;
}

AC_ACCESS_REGISTER_TRANSFERriscv-fesvr/fesvr/debug_defines.h で定義されている。

  • riscv-fesvr/fesvr/debug_defines.h
#define AC_ACCESS_REGISTER_TRANSFER_OFFSET  17
#define AC_ACCESS_REGISTER_TRANSFER_LENGTH  1
#define AC_ACCESS_REGISTER_TRANSFER         (0x1 << AC_ACCESS_REGISTER_TRANSFER_OFFSET)

ここでRISC-Vのデバッグレジスタの仕様を見てみる。RISC-Vにおいて、デバッグモジュール(DMI)へのコマンド転送は以下のプロトコルが定義されている。

f:id:msyksphinz:20171003022322p:plain

17bit目が、TRANSFERコマンド、つまりWriteコマンドを示す。さらにデータサイズ AC_AR_SIZE, AC_AR_REGNO により対象のレジスタの値を転送する。

さて、やっとデバッグモジュールを通じてデータの読み書きができるところまで来た。次は、データの書き込みについて調査していく。

RocketChipの足回りを理解する(5. Load_Elf 解析中)

前回の続き。Rocket Chipがどのようにして制御を行っているのか、またプログラムのロードや、Start,Stopの制御はどのように行っているのかなどを調査している。

前回まではload_program()関数が呼ばれるところまでを見てきた。

msyksphinz.hatenablog.com

実際には内部のload_program()elfloader.ccload_elf()を呼び出しており、これが実質的なプログラムのロードルーティンになっている。

  • riscv-tools/riscv-fesvr/fesvr/elfloader.cc
std::map<std::string, uint64_t> load_elf(const char* fn, memif_t* memif, reg_t* entry)
{
...
  std::map<std::string, uint64_t> symbols;

  #define LOAD_ELF(ehdr_t, phdr_t, shdr_t, sym_t) do { \
    ehdr_t* eh = (ehdr_t*)buf; \
    phdr_t* ph = (phdr_t*)(buf + eh->e_phoff); \
    *entry = eh->e_entry; \
    assert(size >= eh->e_phoff + eh->e_phnum*sizeof(*ph)); \
    for (unsigned i = 0; i < eh->e_phnum; i++) { \
      if(ph[i].p_type == PT_LOAD && ph[i].p_memsz) { \
        if (ph[i].p_filesz) { \
          assert(size >= ph[i].p_offset + ph[i].p_filesz); \
          memif->write(ph[i].p_paddr, ph[i].p_filesz, (uint8_t*)buf + ph[i].p_offset); \
        } \
...

実際にプログラムの書き込みを発生させているのは、

        memif->write(ph[i].p_paddr + ph[i].p_filesz, ph[i].p_memsz - ph[i].p_filesz, &zeros[0]); \

の部分だとは思われるが、memif_t *mem_ifの定義は memif.ccで定義されており、ここでwrite()を呼び出すということは、

  • riscv-tools/riscv-fesvr/fesvr/memif.cc
  size_t align = htif->chunk_align();
  if (len && (addr & (align-1)))
  {
    size_t this_len = std::min(len, align - size_t(addr & (align-1)));
    uint8_t chunk[align];

    htif->read_chunk(addr & ~(align-1), align, chunk);
    memcpy(chunk + (addr & (align-1)), bytes, this_len);
    htif->write_chunk(addr & ~(align-1), align, chunk);
...

の部分だと思われる。さらにこのread_chunk(), write_chunk()というのがhtif_t *htifにて定義されており、実体はおそらくこれがdtm.ccで定義されているものになる。

  • riscv-tools/riscv-fesvr/fesvr/dtm.cc
uint32_t dtm_t::do_command(dtm_t::req r)
{
  req_buf = r;
  target->switch_to();
  assert(resp_buf.resp == 0);
  return resp_buf.data;
}

uint32_t dtm_t::read(uint32_t addr)
{
  return do_command((req){addr, 1, 0});
}

uint32_t dtm_t::write(uint32_t addr, uint32_t data)
{
  return do_command((req){addr, 2, data});
}

この中で実際にはreq_bufにコマンドの情報を挿入することでメモリアクセスなどを実現しているものと思われるが、正直ここから先は良く分からなくなった。 なんでこれでメモリアクセスが出来るんだ?まだ解析は続く。

f:id:msyksphinz:20170712013910p:plain

Facets with Jupyter Notebook on Ubuntu on VirtualBox のインストール手順

Googleの公開した機械学習向けデータビジュアライゼーションソフトウェア Facets を Jupyter Notebook にインストールした。

普段の作業はUbuntu Linux on Virtual Boxでやっているので、いろいろ迷ったのだがVirtual Box上のJupyter NotebookにFacetsをインストールし、それをWindows上のWebブラウザで参照するのが良い使い方だと気が付いたので、その方法をメモしておく。

Facetsのインストール

sudo pip3 install jupyter
sudo pip3 install pandas-datareader

Bazelのインストール

Facetsのインストールには、Bazel(Google謹製のビルドツール)をインストールする必要がある。

sudo apt-get install openjdk-8-jdk
echo "deb [arch=amd64] http://storage.googleapis.com/bazel-apt stable jdk1.8" | sudo tee /etc/apt/sources.list.d/bazel.list
curl https://bazel.build/bazel-release.pub.gpg | sudo apt-key add -
sudo apt-get update && sudo apt-get install bazel

facetsをインストールする。githubからダウンロードしてくる。

git clone https://github.com/PAIR-code/facets
cd facets
bazel build facets:facets_jupyter

以上でFacetsのビルドができた。さらに、Virtual Boxのポートをフォワードするためには Jupyter Notebookを以下のオプションで起動する。

jupyter notebook --ip=0.0.0.0 &

[I 23:31:34.704 NotebookApp] The port 8889 is already in use, trying another port.
[I 23:31:34.705 NotebookApp] The port 8890 is already in use, trying another port.
[I 23:31:34.788 NotebookApp] Serving notebooks from local directory: /home/msyksphinz/work/software/facets/rocket_chip
[I 23:31:34.788 NotebookApp] 0 active kernels
[I 23:31:34.788 NotebookApp] The Jupyter Notebook is running at:
[I 23:31:34.788 NotebookApp] http://0.0.0.0:8891/

本当はポート8889あたりで割り当てられるのかな、とりあえず8891でポート割り当てしたぞとメッセージが表示されたので、VirtualBoxでポートフォワーディングを設定すると、WindowsのWebブラウザ上からJupyter Notebookにアクセスできるようになる。

今回は8889ポートを 18890 にフォワーディングした。

f:id:msyksphinz:20171010233455p:plain

f:id:msyksphinz:20171010233645p:plain

Webブラウザからlocalhost:18890にアクセスすると、無事にJupyter Notebookが立ち上がり、Facetsのサンプルプログラムも動作した。

RISC-V実装 BOOM v2をコンパイルしてRTLシミュレーションを実行する

f:id:msyksphinz:20170927231702p:plain

RISC-Vのアウトオブオーダ実装であるBOOMv2は、githubで既に公開されている。

github.com

BOOMv2の実装とRocket Chipの実装は分離されており、内部コアをBOOMv2、外部のインタフェース部分をRocket Chipで実装する。

上記のBOOM v2.0.1は riscv-boom リポジトリの e71168e で実装している。この時にRocket Chipのリポジトリのリビジョンも調整し、 21cce2c とする。

git clone https://github.com/freechipsproject/rocket-chip.git
cd rocket-chip
git checkout boom  # Revision 21cce2c に移動する。
git submodule update --init --recursive chisel3 firrtl hardfloat torture boom
cd boom
git checkout v2.0.1
cd -

この状態でVerilatorでシミュレーション準備をする。

cd emulator
make CONFIG=BOOMConfig

ただし、ツールチェインのリビルドをサボっていたからか、ベンチマークプログラムのシミュレーションを実行するとアサーションフェイルしてしまった。 これはプロキシカーネルなどをリビルドして修正していきたい。

$ make CONFIG=BOOMConfig output/qsort.riscv.out
$ ln -fs /home/msyksphinz/riscv64/riscv64-unknown-elf/share/riscv-tests/benchmarks/qsort.riscv output/qsort.riscv
./emulator-rocketchip-BOOMConfig +max-cycles=100000000 +verbose output/qsort.riscv 3>&1 1>&2 2>&3 | /home/msyksphinz/riscv64/bin//spike-dasm  > output/qsort.riscv.out && [ $PIPESTATUS -eq 0 ]
$ less output/qsort.riscv.out
using random seed 1506619177
emulator-rocketchip-BOOMConfig: ../fesvr/dtm.cc:556: void dtm_t::producer_thread(): Assertion `get_field(hartinfo, DMI_HARTINFO_NSCRATCH) > 0' failed.

修正した。再度RISC-V BOOMv1とBOOMv2でDhrystoneの実行を行った。

確認に使ったリビジョンも一応書いておこう:

  • BOOMv2
  • RocketChip : 21cce2c [boom] bump with bug fixes.
  • boom : e71168e [chisel3] Add IO wrapper.
Microseconds for one run through Dhrystone: 294
Dhrystones per Second:                      3392
mcycle = 147995
minstret = 200153
  • BOOMv1
  • RocketChip : 6751f8e delete unnecessary instrumentation
  • boom : 30d8b3c delete l2 hacks
Microseconds for one run through Dhrystone: 270
Dhrystones per Second:                      3698
mcycle = 135723
minstret = 200153

BOOMv2で、約9%のサイクル数増だ。まあ周波数向上のためのv2なので、特に驚くに当たらない。 パイプラインも伸びてるし。

ついでにBOOMv1と BOOMv2で各種ベンチマークプログラムの実行サイクルを測定してみた。BOOMv2はBOOMv1よりも最大で20%程度サイクル数が増えるという結果になっている。/span>

f:id:msyksphinz:20171008213729p:plain

Instruction BOOMv1 BOOMv2 BOOMv2/BOOMv1
dhrystone.riscv 200153 137573 147995 1.08
median.riscv 4259 7805 9400 1.20
mm.riscv 25287 17295 17079 0.99
mt-matmul - 15992 16298 1.02
mt-vvadd.riscv - 9073 6870 0.76
mt-vvadd.riscv - 7438 6084 0.82
multiply.riscv 21003 18243 20110 1.10
qsort.riscv 124233 206542 252488 1.22
rsort.riscv 171284 112013 100222 0.89
spmv.riscv 32535 27714 28843 1.04
towers.riscv 6623 4605 5395 1.17
vvadd.riscv 2519 1981 2014 1.02

RISC-Vのアウトオブオーダ実装 BOOM v2の内部構成

少し前に、RISC-VのRocket Coreのアウトオブオーダ版、BOOM(Berkeley Out-of-Order Machine) のVersion2がリリースされたことがアナウンスされたが、CARRV (RISC-Vの学術ワークショップ) に向けてその詳細が公開されたので読んでみた。

BOOM v2: an open-source out-of-order RISC-V core | EECS at UC Berkeley

詳細を読み進めていくと、BOOM v2は、BOOM v1で問題となったクリティカルパスの問題を解決すべく設計されたプロセッサのようだ。 一方で、性能(IPC)は約20%低下している。これらの設計情報や性能情報の定量的なものは不明だが、構造的なものについてザックリと説明してある。

まず、BOOM v1を解析した結果、クリティカルパスは以下の4つに存在するとしている。

  • Issue select
  • Register Rename Busy Table Read
  • Conditional Branch Predictor Redirect
  • Register File Read

まず、大まかな構成として、

  • フロントエンド (命令フェッチから分岐予測) : 3ステージ
  • デコードからリネーミング : 2ステージ
  • レジスタリードと命令発行 : 2ステージ

と、BOOMv1からパイプラインステージは伸びている。また、これまでほ整数レジスタファイルと浮動小数レジスタファイルが同じSRAMとして実装されていたが、これを分離することでFanOutを減少させている。

f:id:msyksphinz:20170927231702p:plain

次に分岐予測だが、BPD(Conditional Branch Predictor)がクリティカルパスになっていたものを、予測のために1サイクル使えるように変更した。図によると、BPDのインデックスを参照するために最初にPCからインデックスを生成するのだが、これを丸々1サイクルかけて実行し、次のステージでBPDのテーブルを参照している。 BPDの結果が分かるまでに余計に1サイクルかかるため、分岐予測の実質の性能は落ちるものと思われるが、それよりも動作周波数を選択した結果だということが出来る。

f:id:msyksphinz:20170927231329p:plain

命令発行のためのエントリは、BOOM v1では20エントリだったものを、BOOM v2では整数、浮動小数点、メモリアクセスでそれぞれ16エントリ持たせることで、全体で48エントリと倍以上にしている。

最後にレジスタファイルのクリティカルパス解消だが、まずは上記にも示した通り、命令発行ステージとレジスタリードのステージを分離することでクリティカルパスを削減している。

さらに問題になるのが、70個もの物理レジスタファイルをどのようにして配置配線するかという問題だ。BOOM v2は整数レジスタで6Read, 3Writeができる構成となっており、配線が非常に混雑する。 この問題を解消するために、3-stateバッファを用いたレジスタ制御の方式が利用されている (これをどのようにしてChiselに落とし込んでいるのかはちゃんと見ていないが)。

f:id:msyksphinz:20170927232611p:plain

64bitレジスタならば、上記のブロックが横方向に64bit分ならび、縦方向に70エントリ分並ぶことになる。アドレスデコーダは自分のブロックのアドレスが指定されているかどうかを検査する。検査の結果、このブロックの指定するアドレスが有効の場合、有効信号をアサートする。これを6ポート分用意する。 アドレスが有効な場合、上から流れてくる信号をトライステートバッファに取り込み、下方向に流す。アドレス指定によりどのブロックを使うかは一意に決まるため、横方向のデコードと縦方向のデータの流れで綺麗にレジスタファイルを作ることが出来るというわけだ。

以上これらの方式を用いて、BOOM v2では動作周波数の向上に取り組んだ。詳細な定量的なデータは、CARRVにて発表されるものと思われるので、要注目。

Raspberry-Pi3でOpenCVを動作させて顔検出するチュートリアル

ちょっと久しぶりにRaspberry-Pi3を立ち上げた。やりたいことは組み込み機器でOpenCVとか、OpenVXをどのようにして動かせばよいのかの調査だ。 まずは、Raspberry-Pi3を使ってOpenCVを動作させたい。 これは既に先人によって手法が確立されていることで、Qiitaにも記事が上がっている。

qiita.com

これと同じ手法で、OpenCVの環境を構築することが出来た。当該記事ではOpenCVのバージョンが3.2.0で、調べてみると今は3.3.0だったけど、一応3.2.0でやってみた。

ちなみに、ビルド中にJava関連でビルドエラーが出た場合は、JAVA_HOMEを設定してやるとうまくビルドできるようだ。

export JAVA_HOME=/usr/lib/jvm/jdk-8-oracle-arm32-vfp-hflt/

顔検出プログラムを動作させる。

個人的には、OpenCVの環境としてはC++ではなく、Pythonが主流になっていることに驚いてしまった。時代は進んでるなあ。

下記も上記参考リンクのほぼそのまま。

import cv2
import numpy as np

faceCascade = cv2.CascadeClassifier('../opencv/opencv/data/haarcascades/haarcascade_frontalface_default.xml')

img = cv2.imread('<imagefile.jpg>', cv2.IMREAD_COLOR)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
face = faceCascade.detectMultiScale(gray, 1.1, 3)

if len(face) > 0:
    for rect in face:
        cv2.rectangle(img, tuple(rect[0:2]), tuple(rect[0:2]+rect[2:4]), (0, 0,255), thickness=2)
else:
    print ("no face")

cv2.imwrite('detected.jpg', img)

画像まで一緒だと芸が無いので、大好きなスターウォーズのポスターでやってみる。この画像自体は http://starwars.disney.co.jp/movie/force.html より拝借。

f:id:msyksphinz:20170926225407p:plain

なんか変なの認識してる。でもまあ主人公級はみんな認識できたので良しとしよう。あとドロイドまで顔として認識しちゃった。

Rocket ChipのChiselを使ってアクセラレータを作る(3. Dot Productアクセラレータのデバッグ)

前回Dot Productアクセラレータを作ったのだが、正しく動作していなかったのでデバッグしていた。

よく考えたら、行列積A\times B を求めるにあたり、メモリのフェッチアドレスのとび幅を決めるのに、行列Bの列サイズも必要だったので、それを設定するファンクションも追加して再測定。

今回は、A(17,23)\times B(23,27) の行列積を実行して、シミュレーション上でデバッグした。

f:id:msyksphinz:20170926011255p:plain

結果は正しく動作したようだ。次にFPGAに同じデザインをインプリメントして実機での動作を確認する。実機では、さらに大きな行列を使用してみようと思う。 まず、A(50, 203)\times B(203, 172) のサイズの行列を用意してみたが、正しく動作してくれなかった。具体的には数値がソフトウェアでの結果とずれている。

そこで再度A(17,23)\times B(23,27)で実行してみたのだが、先頭の要素だけずれている。これは何でだろう?まだデバッグが必要だ。

f:id:msyksphinz:20170926015308p:plain