FPGA開発日記

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

RISC-V Linuxビルド用GCCの構築

f:id:msyksphinz:20170325154038p:plain

RISC-V用のLinuxは、以下のリポジトリで公開されている。

github.com

f:id:msyksphinz:20170325153123p:plain

Compile and install RISC-V cross-compiler · lowRISC

RISC-V用のLinuxには、riscv64-unknown-linux-gnu-gcc が必要だ。このビルドには、riscv-gnu-toolchainフォルダでのリビルドが必要になる。

# set up the RISCV environment variables
export RISCV=/home/msyksphinz/riscv

cd ~/work/riscv-tools/riscv-gnu-toolchain
# ignore if build already exist
mkdir build
cd build
../configure --prefix=$RISCV
make -j linux

一度buildディレクトリが生成され、通常用のriscv64-unknown-elf-gccが生成されている場合は一度ファイルをすべて削除しておくこと。

cd build
rm -rf *

これで、$RISCVディレクトリにriscv64-unknown-linux-gnu-gccが生成される。これでRISC-V Linuxの構築が行える。

Zephyr-OSがRISC-Vをサポート(QEMUでの試行)

少し前に、RISC-VのMLにてZephyr-OSがRISC-Vをサポートしたというアナウンスがあった。

Zephyr-OSは、Linux Foundationが発表した、モバイル用途向けの小規模のオペレーティングシステムだ。

Home | Zephyr Project

Zephyr (operating system) - Wikipedia

f:id:msyksphinz:20170324003639p:plain

Wikipediaによると、Zephyr-OSは以下のような特徴があるということだ。

RISC-V QEMUでZephyr-OS を動作させる

ZephyrのRISC-V移植版は以下で入手できる。

github.com

手順としてはReadmeに書いてある通りで動作させることができる。ただし、事前にriscv32のツールチェインと、riscv-qemuをビルドしておくこと。

  • Compiling zephyr-riscv for the qemu_riscv32 board

github.com

以下を実行することで、とりあえずQEMU中でZephyrを動作させることはできた。

cd zephyr-riscv/samples/philosophers
make BOARD=qemu_riscv32 run

f:id:msyksphinz:20170324004627p:plain

なんか動き始めた。哲学者の食事問題を動作させているのかな?

詳細については、今後勉強していこう。

HiFive1のパフォーマンスカウンタについて

前回Coremarkの測定を行ったし、その前は各命令のレイテンシを測定した。 このとき、実際にプログラムのサイクル数を測定しているわけだが、これはどのようにして実現しているのだろう。

msyksphinz.hatenablog.com

msyksphinz.hatenablog.com

まず各命令のレイテンシ、スループット測定には、以下のようなサイクル取得ルーチンを使用している。

#define rdmcycle(x)  {                                 \
    uint32_t lo, hi, hi2;                              \
    __asm__ __volatile__ ("1:\n\t"                     \
                          "csrr %0, mcycleh\n\t"       \
                          "csrr %1, mcycle\n\t"        \
                          "csrr %2, mcycleh\n\t"       \
                          "bne  %0, %2, 1b\n\t"                 \
                          : "=r" (hi), "=r" (lo), "=r" (hi2)) ; \
    *(x) = lo | ((uint64_t) hi << 32);                          \
  }

これはつまり、

  1. 現在のmcyclehの値を取得し、hiに代入する。
  2. 現在のmcycleの値を取得し、loに代入する。
  3. 現在のmcyclehの値を取得し、hi2に代入する。
  4. hiとhi2を比較し、不一致ならば1.から繰り返す。

これはmcyclehがオーバフローした場合を考慮したルーチンとなっている。

例えば、

1.を実行した時点で{mcycleh,mcycle}=0x0000_0010_ffff_ffff 2.を実行した時点で{mcycleh,mcycle}=0x0000_0011_0000_0000

となった場合、最後の計算で{mcycleh,mcycle}=0x0000_0011_ffff_ffff となってしまい、大幅にサイクル数が狂ってしまう。 これを防ぐために、mcyclehを2回ロードし、変化していないことを確認してからサイクル数を計算するという訳だ。

32bit版RISC-V (HiFive1) プロセッサでのサイクルカウント方法

計算方法としては難しいけれども、32bit版のRISC-Vでは上記ではそのまま計測できない。そこで、サイクルカウンタのHiレジスタとLoレジスタを別々に出力することにした。

#define rdmcycle(hi_cycle, lo_cycle)  {                            \
    uint32_t lo, hi, hi2;                              \
    __asm__ __volatile__ ("1:\n\t"                     \
                          "csrr %0, mcycleh\n\t"       \
                          "csrr %1, mcycle\n\t"        \
                          "csrr %2, mcycleh\n\t"       \
                          "bne  %0, %2, 1b\n\t"                 \
                          : "=r" (hi), "=r" (lo), "=r" (hi2)) ; \
   hi_cycle = hi; lo_cycle = lo;                               \
  }

以下のようにして出力する。差分の計算とかは難しいけど。とりあえずprintf()くらいは表示できるようになった。

  uint32_t start_cycle[2], stop_cycle[2];
  rdmcycle(start_cycle[1], start_cycle[0]);
...

  printf ("Correct = %d\n", correct);
  printf ("Time = %08x%08x - %08x%08x\n", stop_cycle[1], stop_cycle[0], start_cycle[1], start_cycle[0]);

HiFive1でベンチマークプログラム測定(Coremarkを動作させる)

HiFive1でベンチマークプログラムや一般的なプログラムを動作させるのはそこまで難しい話ではなくて、SiFiveもプログラム開発用のツールセットを公開している。

freedom-e-sdkというリポジトリは、RISC-V向けプログラムのコンパイル環境を提供しており、Coremarkをコンパイルして動作させるまでのフローも公開してある。これをなぞってみよう。

github.com

Freedom-e-sdkを使ってCoremarkをコンパイルする

Coremarkのコンパイル方法は、githubリポジトリに書いてある通り、Coremark本体をダウンロードして当該ディレクトリに配置し、コンパイルするだけだ。

github.com

f:id:msyksphinz:20170322011916p:plain

この際、freedom-e-sdkに付属しているRISC-V向けGCCを再度コンパイルするのが面倒だったので、パスを差し替えて既存のGCCを指すように変更した。

diff --git a/bsp/env/common.mk b/bsp/env/common.mk
index 74f5582..ecb194d 100644
--- a/bsp/env/common.mk
+++ b/bsp/env/common.mk
@@ -23,7 +23,8 @@ INCLUDES += -I$(BSP_BASE)/drivers/
 INCLUDES += -I$(ENV_DIR)
 INCLUDES += -I$(PLATFORM_DIR)

-TOOL_DIR = $(BSP_BASE)/../toolchain/bin
+# TOOL_DIR = $(BSP_BASE)/../toolchain/bin
+TOOL_DIR = /home/msyksphinz/riscv32/bin

 CC := $(TOOL_DIR)/riscv32-unknown-elf-gcc
 AR := $(TOOL_DIR)/riscv32-unknown-elf-ar

あと、OpenOCDが当該パスに必要なので、それだけcloneしてビルドしておく。

git submodule init
git submodule update openocd --recursive
make openocd

あとは、指示通りにビルドしてプログラムを書き込むだけだ。UARTの速度は115200に設定しておく。

  • シリアル受信側
screen /dev/ttyUSB1 115200
  • ビルド&アップロード側

以下のコマンドを実行する。

make software PROGRAM=coremark
make upload PROGRAM=coremark

以下のような出力が得られた。

core freq at 274192793 Hz
2K performance run parameters for coremark.
CoreMark Size    : 666
Total ticks      : 798820
Total time (secs): 24.375000
Iterations/Sec   : 410.256410
Iterations       : 10000
Compiler version : GCC6.1.0
Compiler flags   : -O2 -fno-common -funroll-loops -finline-functions --param max-inline-insns-auto=20 -falign-functions=4 -falign-jumps=4 -falign-loops=4
Memory location  : STACK
seedcrc          : 0xe9f5
[0]crclist       : 0xe714
[0]crcmatrix     : 0x1fd7
[0]crcstate      : 0x8e3a
[0]crcfinal      : 0x988c
Correct operation validated. See readme.txt for run and reporting rules.
CoreMark 1.0 : 410.256410 / GCC6.1.0 -O2 -fno-common -funroll-loops -finline-functions --param max-inline-insns-auto=20 -falign-functions=4 -falign-jumps=4 -falign-loops=4 / STACK

Progam has exited with code:0x00000000

Coremark/MHzを測定すると、{ 410.256410 / 274.192793 = 1.496 } となる。

f:id:msyksphinz:20170322014023p:plain

HiFive1のCPUコア性能を測定(ベンチマークプログラム作成)

前回、HiFive1のプログラムをC/C++で開発するための環境を構築した。

次に、HiFive1のCPUコアの基本性能を測定してみよう。まずは、通常のプログラムを動作させる前に、同一の命令を何度も実行して、命令のスループットおよびレイテンシを測定してみる。

msyksphinz.hatenablog.com

命令のスループットおよびレイテンシを測定するベンチマークプログラム

github.com

命令のスループットは、同一の命令を何度も実行して、消費サイクル数を測定する。命令間の依存関係は存在しないように記述しておく。 これにより、プログラムの依存関係のストールを無視して、命令発行能力を測定することができる。

  for(int i=0; i < 65536; i++) {
    func (&dst, src0, src1);
    func (&dst, src0, src1);
    func (&dst, src0, src1);
    func (&dst, src0, src1);
    func (&dst, src0, src1);
    func (&dst, src0, src1);
    func (&dst, src0, src1);
    func (&dst, src0, src1);
  }

一方の命令レイテンシは、同一の命令を何度も実行するが、各命令には依存関係が存在する。 これにより、ある命令を実行してから、次の同一命令を実行するのに必要なサイクル数を測定できる。

  for(int i=0; i < 65536; i++) {
    func (&dst, src0, src1);
    func (&dst, dst,  src1);
    func (&dst, dst,  src1);
    func (&dst, dst,  src1);
    func (&dst, dst,  src1);
    func (&dst, dst,  src1);
    func (&dst, dst,  src1);
    func (&dst, dst,  src1);
  }

これを、各命令について実装してみる。まずは、add, sub, mul, divについて実装してみた。

以下のような共通関数を用意し、測定対象を関数ポインタとして渡す。以下は、1ループにつき8命令、

uint32_t inst_latency(void (*func)(int*,int,int), int init_src0, int init_src1)
{
  uint32_t start_time, end_time, wrap_time;
  int dst, src0 = init_src0, src1 = init_src1;
  rdmcycle (&start_time);
  for(int i=0; i < 65535; i++) {
    func (&dst, src0, src1);
    func (&dst, dst,  src1);
    func (&dst, dst,  src1);
    func (&dst, dst,  src1);
    func (&dst, dst,  src1);
    func (&dst, dst,  src1);
    func (&dst, dst,  src1);
    func (&dst, dst,  src1);
  }
  rdmcycle (&end_time);
  // printint(0, start_time, 10, 1); uart_print("\r\n");
  // printint(0, end_time, 10, 1);   uart_print("\r\n");
  // printint(0, wrap_time, 10, 1);  uart_print("\r\n");
  return end_time - start_time;
}

測定対象の関数は以下のようになる。

inline void add_inst(int *dst, int src0, int src1) {
  asm volatile ("add %0,%1,%2" :"=r"(*dst) :"r"(src0),"r"(src1) : );
}
inline void sub_inst(int *dst, int src0, int src1) {
  asm volatile ("sub %0,%1,%2" :"=r"(*dst) :"r"(src0),"r"(src1) : );
}
inline void mult_inst(int *dst, int src0, int src1) {
  asm volatile ("mul %0,%1,%2"  :"=r"(*dst) :"r"(src0),"r"(src1) : );
}
inline void div_inst(int *dst, int src0, int src1) {
  asm volatile ("div %0,%1,%2"  :"=r"(*dst) :"r"(src0),"r"(src1) : );
}

以下のように、関数をコールしては測定結果を出力した。

void add_bench ()
{
  uint32_t ret;
  ret = inst_latency    (add_inst, 1, 1); printint(0, ret, 10, 1); uart_print("\r\n");
  ret = inst_throughput (add_inst, 1, 1); printint(0, ret, 10, 1); uart_print("\r\n");

  ret = inst_latency    (sub_inst, 1, 1); printint(0, ret, 10, 1); uart_print("\r\n");
  ret = inst_throughput (sub_inst, 1, 1); printint(0, ret, 10, 1); uart_print("\r\n");

  ret = inst_latency    (mult_inst, 1, 1); printint(0, ret, 10, 1); uart_print("\r\n");
  ret = inst_throughput (mult_inst, 1, 1); printint(0, ret, 10, 1); uart_print("\r\n");

  ret = inst_latency    (div_inst, 0x7fffffff, 1); printint(0, ret, 10, 1); uart_print("\r\n");
  ret = inst_throughput (div_inst, 0x7fffffff, 1); printint(0, ret, 10, 1); uart_print("\r\n");
}

測定結果は以下のようになった。単純な整数演算系は、1サイクル1命令実行することができる。 一方で、乗算命令は実行に7サイクル程度必要であり、除算命令は36命令程度必要である。 ただし、除算命令はオペランドの値に従ってレイテンシが変わるようなので、これよりも小さな場合もあるし、大きな場合もあるかもしれない。 とりあえずここでは、被除数=0x7fffffff, 除数=0x01で測定している。

Latency(Cycle) Throughput(Cycle) Latency(CPI) Throughput(IPC)
add 568291 560092 1.08 0.93
sub 560094 560094 1.06 0.93
mul 3632046 3689406 6.93 0.142
div 18836174 18893504 35.92 0.027

HiFive1向けのC/C++言語プログラムを開発するフロー構築

HiFive1は基本的にArduino経由でプログラムを開発するようになっているが、実際に動作しているのはriscv32-gccであり、隣家スクリプトコンパイルオプションさえ揃えれば普通にC/C++でプログラムを開発できる。

今回、そのプログラム開発フローを構築したので、githubにアップロードした。

ちなみに、Arduinoに付属しているRISC-V GCCを利用しているので、Arduino IDEのインストールと、RISC-V向けツールチェインの導入は必要だ。

github.com

main()内に好きなプログラムを書けば、それを実行できる。 以下はuart_print()関数を定義し、シリアルに対して出力を行った結果。

  • main.cpp
/*
 * \brief Main entry point of Arduino application
 */
int main( void )
{
  //   init();
  // Compute F_CPU inverse, used for millis and micros functions.
  calc_inv(F_CPU/1000, &f_cpu_1000_inv);
  calc_inv(F_CPU/1000000, &f_cpu_1000000_inv);
  freedom_e300_specific_initialization();
  setup();

  uart_print ("Hello World\r\n");

  do {
    loop();
...
  • serial.c
void uart_print (uint8_t *str)
{
  while (*str != '\0') {
    uart_putchar(*str);
    str++;
  }

  return;
}


void uart_putchar (uint8_t a)
{
  const int blocking = 1;

  volatile uint32_t *val = UART_REGP(UART_REG_TXFIFO);
  uint32_t busy = (*val) & 0x80000000;
  if (blocking) {
    while (*val & 0x80000000);
  } else if (busy) {
    return;
  }
  UART_REG(UART_REG_TXFIFO) = a;

  return;
}

9600baudでシリアル受信状態にしておけば、受信できる。

f:id:msyksphinz:20170320235540p:plain

コーヒーメーカー「バリスタアイ」購入

コーヒーメーカーとして、初代バリスタを使用していたのだがしばらく前に壊れてしまった。

買ってから結構時間がたっているし、保証も切れているので、せっかくなので新しいものを購入することにした。 同じメーカーの新機種のバリスタアイだ。本日ヨドバシのエクストリーム配送で届いた。

f:id:msyksphinz:20170319133016p:plain

色は木目調のウッディブラウンとした。初代のバリスタは白色にしていたのだが、ウッディブラウンは結構雰囲気出てよい感じだと思う。

f:id:msyksphinz:20170319133330p:plain

初代バリスタと比較すると、少しだけ背が低い。 これ、普通のコーヒーカップを使っている場合は非常に良いと思っており、背が高いとコーヒーカップと天井が空いており、カプチーノを作る場合にお湯が跳ねてしまい汚くなってしまっていた。 背が低いとお湯がはねる領域が狭くなり汚くなりにくいと考えている。

f:id:msyksphinz:20170319133304p:plain

iPhoneともBluetoothで接続できるので便利。ただし、コップを置いていないのに間違えて抽出しないようにしなきゃ。