FPGA開発日記

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

RISC-VのPLICのためのテストケースを作って、Spikeとの一致検証環境を構築する

msyksphinz.hatenablog.com

msyksphinz.hatenablog.com

msyksphinz.hatenablog.com

前回までで、Spikeを解析した結果PLICのメモリマップを確保する必要があること、そして、PLICの簡単なデバイスを作成してSpikeに登録してやる必要があることが分かった。

というわけで簡単に実装した。

// See LICENSE for license details.
#ifndef _RISCV_SIMPLEPLIC_H
#define _RISCV_SIMPLEPLIC_H

#include <riscv/mmio_plugin.h>

#include "abstract_device.h"
#include "mmu.h"

class simpleplic_t : public abstract_device_t {
 public:
  simpleplic_t(std::string name);
  bool load(reg_t addr, size_t len, uint8_t* bytes);
  bool store(reg_t addr, size_t len, const uint8_t* bytes);
 private:
};

/* ... 途中省略 ... */
#endif // _RISCV_SIMPLEPLIC_H

そして作成したデバイスライブラリをSpikeに登録する。

#ifndef SIM_MAIN
  argv[arg_max++] = "--extlib=../../../spike_dpi/libsimple_plic.so";
#else // SIM_MAIN
  argv[arg_max++] = "--extlib=./libsimple_plic.so";
#endif // SIM_MAIN
  argv[arg_max++] = "--device=simpleplic,201326592,plic";   // 201326592 = 0x0c00_0000

これにより、PLIC領域にアクセスするとちゃんと認識されるようになる。

argv[19] = (null)
serialdevice: uart loaded
simpleplic: plic loaded
simple PLIC_t::store called : addr = 0
simple PLIC_t::load called : addr = 8
simple PLIC_t::load called : addr = 0
simple PLIC_t::load called : addr = 0
simple PLIC_t::store called : addr = 4
simple PLIC_t::load called : addr = 4
simple PLIC_t::store called : addr = 8
simple PLIC_t::load called : addr = 10
simple PLIC_t::load called : addr = 8
simple PLIC_t::load called : addr = 8
simple PLIC_t::store called : addr = c
simple PLIC_t::load called : addr = c
simple PLIC_t::store called : addr = 10
simple PLIC_t::load called : addr = 18
simple PLIC_t::load called : addr = 10
simple PLIC_t::load called : addr = 10
simple PLIC_t::store called : addr = 14
simple PLIC_t::load called : addr = 14
simple PLIC_t::store called : addr = 18
simple PLIC_t::load called : addr = 20
simple PLIC_t::load called : addr = 18
simple PLIC_t::load called : addr = 18
simple PLIC_t::store called : addr = 1c
simple PLIC_t::load called : addr = 1c
simple PLIC_t::store called : addr = 1000
simple PLIC_t::load called : addr = 1008
simple PLIC_t::load called : addr = 1000
      1000 :        422 : IPC(recent) = 0.42, IPC(total) = 0.42
simple PLIC_t::load called : addr = 1000
      2000 :        439 : IPC(recent) = 0.02, IPC(total) = 0.22
      3000 :        439 : IPC(recent) = 0.00, IPC(total) = 0.15

問題はそのあとだな。デッドロックしているので要解析だ。

自作CPUにMatrix Schedulerを導入した場合の変更量について考える

まあいつかの段階で自作CPUのスケジューラを変更しようと思っているのだが、これをどのようにしようかいろいろ思いを巡らせている。 アウトオブオーダのフロントエンドはあまり得意ではないので、どうやって作ればいいのか悩んでいる。

  • 現在の実装

    • Issue Unitが各モジュール (ALU / LSU / FPUなど)に分散している
    • Matrix Schedulerを導入したいが、これをどのように変更しようか?
  • まず、Matrix SchedulerはIssue Windowの大きさのみを管理するということなので、仮にその大きさを16として(Reloadedの実装を含まずにNaiveな方法で)で導入することを仮定しよう

    • Matrix Schedulerはレジスタの依存関係ではなく命令の依存関係を解析するので、命令間の直接的な依存関係を見る必要がある
      • これをリネーム時に検出する必要がある?
      • つまり、命令Aが読もうとしているレジスタx/yがどの命令によって生成される(Producer)なのかをチェックする必要があるということか。
        • これは論理レジスタでやってよい?
        • その情報に基づきMatrixを作る必要があるのか?
  • 依存関係を解決の検出は、これまでに使用しているSchedulerの通知方法を使えばいいはずだが、

    • これまでは、物理レジスタへの書き込みの2サイクル前にEarly Notificationを出力して、先行して発行させていた。
      • LSUによるSpeculative Missが起きない限りはこの方法がいける
    • なので、Matrix SchedulerのWakeupの実際は、Wakeup Logicが動いたタイミングというよりは、実際のパイプラインがResultを生成する直前にアップデートしなければならないのか?
      • そうでないと、長いパイプラインの命令でA-->Bの依存関係があり、Aを発行した後すぐにBを発行するとBのデータが間に合わない
  • Speculative Missが発生した場合

    • これまでは、分散したSchedulerにおいて、各命令の発行エントリがステートマシンを持っており、Speculative Missを検出するとそこで制御していた
    • 例えばLSUの場合、EX1でEarly WakeupでResultの依存する命令をWakeupするが、次のサイクルのEX2でSpeculative Missが生成されると、止めることができる
      • 命令Bは命令Aに起こされるが、次のサイクルでSpeculative Missを検出するともう一度待機状態に戻ることができる。
A --> ISS --> EX0 --> EX1 --> EX2 --> EX3
                      Early Wakeup    Result
                              [Speculative Miss]
                              
B                     "Detect Wakeup"
                               Stop

これは2サイクル遅れでもなんとかなるはずで、Bが発行されてからEarly Wakeupまでの余裕があるためBに依存するさらに先の命令がWakeupすることはない。

A --> ISS --> EX0 --> EX1 --> EX2 --> EX3
                      "Early Wakeup"  "Result"
                                      [Speculative Miss]
                                      
B                     "Detect Wakeup"
                                ISS --> Stop
  • で、これをMatrix Schedulerでやるとどうなるかという話だが、行毎にBackupが取れるようにしておき、投機的なWakeupが発生した場合はBackupを取るということになるのだろうか?

RISC-VのPLICのためのテストケースを作って、Spikeの挙動を解析する

msyksphinz.hatenablog.com

msyksphinz.hatenablog.com

前回まででPLICについては結構仕様を勉強して、じゃあ次にRTLを作ってみて、テストしてみようということになったのだが、 とりあえず簡単なテストを動かしてみたい。PLICのメモリマップは0xc00_0000なので、ここにアクセスする簡単なテストパタンを作ってみる。

#define PLIC_BASE_ADDR         0xc000000
#define PLIC_SOURCE_BASE_ADDR  (PLIC_BASE_ADDR)
#define PLIC_PENDING_BASE_ADDR (PLIC_BASE_ADDR + 0x1000)
#define PLIC_ENABLE_BASE_ADDR  (PLIC_BASE_ADDR + 0x2000)
#define PLIC_THRESHOLD_ADDR    (PLIC_BASE_ADDR + 0x200000)

int plic_reg_rw_test()
{
  *((volatile unsigned int *)(PLIC_SOURCE_BASE_ADDR)) = 0xdeadbeef;
  *(volatile unsigned int *)(PLIC_SOURCE_BASE_ADDR);

  *((volatile unsigned int *)(PLIC_PENDING_BASE_ADDR)) = 0xdeadbeef;
  *(volatile unsigned int *)(PLIC_PENDING_BASE_ADDR);

  return 0;
}

が、Spikeの挙動が異なっており、Spikeは当該メモリ領域へのメモリアクセスは例外処理になっていた。

core   0: 3 0x00000000800025f0 (0x4581) x11 0x0000000000000000
core   0: 3 0x00000000800025f2 (0x4501) x10 0x0000000000000000
core   0: 3 0x00000000800025f4 (0x142000ef) x 1 0x00000000800025f8
core   0: 3 0x0000000080002736 (0xdeadc7b7) x15 0xffffffffdeadc000
core   0: 3 0x000000008000273a (0xeef78793) x15 0xffffffffdeadbeef
core   0: 3 0x000000008000273e (0x0c0006b7) x13 0x000000000c000000
// 0xc00_0000へのメモリストアを試行した
// ここで例外が発生している
core   0: 3 0x0000000080000090 (0x716d) x 2 0x0000000080022780
core   0: 3 0x0000000080000092 (0xe406) mem 0x0000000080022788 0x00000000800025f8
core   0: 3 0x0000000080000094 (0xe80a) mem 0x0000000080022790 0x0000000080022780
core   0: 3 0x0000000080000096 (0xec0e) mem 0x0000000080022798 0x0000000080002f70
core   0: 3 0x0000000080000098 (0xf012) mem 0x00000000800227a0 0x0000000080002940
core   0: 3 0x000000008000009a (0xf416) mem 0x00000000800227a8 0x0000000080000090
core   0: 3 0x000000008000009c (0xf81a) mem 0x00000000800227b0 0x0000000000000000
core   0: 3 0x000000008000009e (0xfc1e) mem 0x00000000800227b8 0x0000000000000000
core   0: 3 0x00000000800000a0 (0xe0a2) mem 0x00000000800227c0 0x0000000000000000

これをどうやって直せばいいのかすっかり忘れていたのだが、とりあえずDTSを改造してみても駄目なようだ。 これはDTSを追加するだけではなく、ちゃんとデバイスを追加しないと駄目なようだ。CLINTがその例である。

   void *fdt = (void *)dtb.c_str();
   //handle clic
   clint.reset(new clint_t(procs, CPU_HZ / INSNS_PER_RTC_TICK, real_time_clint));
   reg_t clint_base;
   if (fdt_parse_clint(fdt, &clint_base, "riscv,clint0")) {
     bus.add_device(CLINT_BASE, clint.get());
   } else {
     bus.add_device(clint_base, clint.get());
   }

したがって、同じようにPLICの領域に仮想的なデバイスを作って追加する必要があるようだ。Spikeの挙動をやっと理解できるようになってきた。

DIGITAL ELECTRONICS NOTESをやってみる (3. グレイコード)

以下のデジタル回路の問題をやってみる。続き。

lancamentomerlo.my.canva.site

やり方としては、まずMSBの1を保持し、上位ビットから順番に次のビットとのXORを取っていく。

4 3 2 1 0  // bit-position
1 1 0 0 1  // オリジナルグレイコード
1 0 0 0 1  // bit[4]^bit[3] = 0 
1 0 0 0 1  // bit[3]^bit[2] = 0
1 0 0 0 1  // bit[2]^bit[1] = 0
1 0 0 0 1  // bit[1]^bit[1] = 1

というように変換する。これが成立する理由としては、グレイコード自体が、ビットの変化がたかだか1ビットしか変更しないというルールを適用する必要があるためだ。

逆に、2進数からグレイコードに変換するときは、

ja.wikipedia.org

通常の二進表現をグレイコードに変換するには、「対象の二進表現」と、「それを1ビット右シフトし、先頭に0をつけたもの」との排他的論理和をとる。

これは、隣のビットとの変化を見つけていることに相当する。うまく理論的に説明できないが、これでうまく動作するのはなんとなく感覚で分かる。

2進数
0 0 0 0 0 : 00000 ^ 00000 = 00000
0 0 0 0 1 : 00001 ^ 00000 = 00001
0 0 0 1 0 : 00010 ^ 00001 = 00011
0 0 0 1 1 : 00011 ^ 00001 = 00010
0 0 1 0 0 : 00100 ^ 00010 = 00110
0 0 1 0 1 : 00101 ^ 00010 = 00111

DIGITAL ELECTRONICS NOTESをやってみる (2. 符号に関するクイズ)

以下のデジタル回路の問題をやってみる。続き。

lancamentomerlo.my.canva.site

パリティビットは、2進コードの文字列の末尾に付加されるビットで、文字列中の値「1」を持つビットの数が偶数か奇数かを示すものである。 従って、パリティコードには偶数パリティと奇数パリティの2種類がある。

パリティビットの計算には、2進コード中の "1 "の値を持つビットの総数を数える。 "1"の数が奇数で、偶数パリティを使用する場合、パリティビットを1に設定し、パリティビットを含む「1」の総数が偶数になるようにする。 "1"の数が奇数で、奇数パリティを使用する場合、パリティビットは0に設定され、パリティビットを含む1の総数は奇数になるようにする。

パリティビットは、2進数の文字列のすべてのビットのXORを取ることによって計算される。 パリティビットは、最も単純な誤り検出符号として使用される。

    1. 与えられた2進文字列 111001に対して、適切な奇数パリティビットを計算してください。

与えられた2進文字列 111001は、4つの "1"を持っています。 奇数パリティを使用する場合、パリティビットを含むバイナリ文字列の「1」の総数は奇数である必要がある。 したがって、この文字列の奇数パリティビットは1である。

    1. 1の補数、2の補数とは何ですか?また、どこで使われているのですか?

2進数のすべてのビットを反転させ、1を0に、0を1にしたものを1の補数と呼ぶ。 例えば 2進数110010の1の補数は001101となる。

2進数の2の補数は、1進数の補数に1を足して得られる。 例えば 2進数110010の2の補数は、001101+1 = 001110となる。

2の補数は符号付き2進数の表現に使用される。また、2進数の引き算にも使用される。 1の補数は、2の補数を得るための中間ステップである。

    1. BCDコードとは何ですか、2進コードとどう違うのですか?10進数27のBCDコードと2進数コードはどうなりますか?

BCDとはBinary coded decimalの略で、10進数(0〜9)の任意の桁を表現できる4ビットの2進数コードである。 2進コードは10進数を2進数で表現したもので、2進コードに必要なビット数は10進数に依存することになる。 0から9までの10進数の場合、BCDも2進数コードも同じになる。

27という数字は、2と7を4ビットで表現することによりBCDで表現できる。 したがって、27のBCDは0010 0111となる。

    1. ウェイトコードとは何ですか?例を挙げてください。

ウェイトコードは、各ポジションに固定されたウェイトを持つことになる。 例えば、通常の2進法では、位置の値に位置の重みを掛けて加算することで10進数に相当する値を得ることができる。

    1. Excess-3コードの特長は何ですか?

自己補数型です。Excess-3の9の補数は、ビットを反転するだけで得られる。

    1. 非重み付け符号の例を挙げてください。

重みのある符号と異なり、重みを持たない符号である。 例えば、Excess-3符号やGray符号など。そのため、重みのない符号で表現された数値は、そのまま10進数に変換することはできない。

「プログラマーのためのCPU入門」を買いました

面白そうなので買ってみました。物理本は送料が意外と高かったので電子書籍版を買いました。

プログラマーのためのCPU入門 ― CPUは如何にしてソフトウェアを高速に実行するかwww.lambdanote.com

ざっくりと眺めましたが、タイトルに偽りなし、ソフトウェアエンジニアにとって、ハードウェアをどのように理解すればよいか、ということに重きが置かれています。これでハードウェアが書けるようになるというわけではないので、そこは勘違いしないようにしたい。

実際問題、ソフトウェアエンジニアの人たちは、サービスの速度向上を図りたいとき、どのようなアプローチをとっているのだろう?というのは興味があるところです。まさかフロントエンドエンジニアが「このサブルーチンはこういう命令に変換されるから...」ということを考えてプログラムを組んでいるわけがない。 そういう時はせいぜいデータ構造を考え直すか、システムコールを呼び出す回数を数えてみるとか、そういう話なのかな、と勝手に想像しています。

自分はCPUの中の人なので、そういうのはどうしても気になってしまうが、ソフトウェアの人たちは例えば命令依存とか気にするわけがない。 自分と気持ちとしては、あまりソフトウェアさんがハードウェアの中身とかを気にせずに良いものを作りたいし、そうであるべきだと思う。 コンピュータの基本はブラックボックスで、上位のレイヤは下位のレイヤを完全に隠蔽することができ、そのインタフェースだけを操ればよい。これがコンピュータの基本原理だ。

だからそんなことは気にしなくてもいいのだ。ソフトウェアを書く人たちは、CPUの周波数の向上と、メモリの増加、性能の向上におんぶにだっこで良い。 思う存分性能を無駄遣いしてほしい。その分、いいソフトウェアを早く作ってくれ。

ただ、ひとたびレイヤを潜るとそこは楽しいし、ソフトウェアエンジニアが気にしない「要点」を自分たちは改善していくつもりだ。 一人のエンジニアとして考えると、自分よりも一つ違うレイヤを学ぶことはいいことだ。だけど、本当はそんなことしなくてもいいのだ。自分の世界で思いっきり戦ってほしい。

ところで、BOOMの章に自分のブログが入っていてちょっとうれしかったです。

サイクル精度シミュレータSniperでRegion of Interest(ROI)を設定するための手法の調査

サイクル精度シミュレータSniperでは、計測範囲を指定する用語としてROI(Region of Interest)というものがある。 通常、サイクル数などはこの範囲を計測するのだが、これを取得する方法はいくつかあって、Sniperは基本的にC言語用のマクロを用意している。

github.com

ここにはRISC-V用のROI関数が定義されていないので、無理やり自分で定義する。x0への書き込み命令(ADDI x0)をROI用の関数として定義してしまう。

#if defined(__riscv)

#define SimMagic0(cmd) ({                       \
   asm volatile (           \
   "addi x0, x0, %0\n"   \
   :: "i"(cmd)             \
   );                       \
})

#define SimRoiStart()             SimMagic0(SIM_CMD_ROI_START)
#define SimRoiEnd()               SimMagic0(SIM_CMD_ROI_END)

そしてこれらをテストベンチマーク上に埋め込むようにする。

  SimRoiStart();
  spmv(R, val, idx, x, ptr, y);
  SimRoiEnd();

SniperはSIFTファイル生成用にSpikeを使用するので、Spike側を改造してMagicを送信するようにしてみる。

  • riscv/execute.cc
  } else {
    p->get_state()->log_writer->Instruction(addr, size, num_addresses, addresses, is_branch, taken, 0 /*is_predicate*/, 1 /*executed*/);
    if ((MASK_ADDI & sift_executed_insn) == MATCH_ADDI) {
      if ((INSN_FIELD_IMM12 & sift_executed_insn) == 0x00100000) {
        p->get_state()->log_writer->Magic (1, 0, 0);
      }
      if ((INSN_FIELD_IMM12 & sift_executed_insn) == 0x00200000) {
        p->get_state()->log_writer->Magic (2, 0, 0);
      }
    }
  }

ここまでは良かったが、SniperのSIFT Writer/SIFT ReaderはResponseを想定しているらしく、これがよく分からない。Magicを送信するだけで、なんでそんなことがいるんだ?

とりあえず余計な部分をコメントアウトして試行してみる。

diff --git a/sift/sift_reader.cc b/sift/sift_reader.cc
index 300371c..2cf2c0c 100644
--- a/sift/sift_reader.cc
+++ b/sift/sift_reader.cc
@@ -752,21 +752,21 @@ void Sift::Reader::sendSimpleResponse(RecOtherType type, void *data, uint32_t si
    std::cerr << "[DEBUG:" << m_id << "] Write SimpleResponse type=" << type << std::endl;
    #endif

-   if (!initResponse())
-   {
-      std::cerr << "[SIFT:" << m_id << "] Error: initResponse failed\n";
-   }
-
-   Record rec;
-   rec.Other.zero = 0;
-   rec.Other.type = type;
-   rec.Other.size = size;
-   response->write(reinterpret_cast<char*>(&rec), sizeof(rec.Other));
-   if (size > 0)
-   {
-      response->write(reinterpret_cast<char*>(data), size);
-   }
-   response->flush();
+   // if (!initResponse())
+   // {
+   //    std::cerr << "[SIFT:" << m_id << "] Error: initResponse failed\n";
+   // }
+   //
+   // Record rec;
+   // rec.Other.zero = 0;
+   // rec.Other.type = type;
+   // rec.Other.size = size;
+   // response->write(reinterpret_cast<char*>(&rec), sizeof(rec.Other));
+   // if (size > 0)
+   // {
+   //    response->write(reinterpret_cast<char*>(data), size);
+   // }
+   // response->flush();
 }

 uint64_t Sift::Reader::getPosition()
diff --git a/sift/sift_writer.cc b/sift/sift_writer.cc
index 387afd0..20feeaf 100644
--- a/sift/sift_writer.cc
+++ b/sift/sift_writer.cc
@@ -757,6 +757,8 @@ uint64_t Sift::Writer::Magic(uint64_t a, uint64_t b, uint64_t c)
    output->write(reinterpret_cast<char*>(&c), sizeof(uint64_t));
    output->flush();

+   return 0;
+
    initResponse();

    // wait for reply