RISC-Vの実装sodorを見てみる
はじめに
chiselの勉強がてらに何か実装してみようと考えたところ、当然のごとくRISC-Vが引っ掛かったので色々調べてみた。
RISC-VのISAなどは眺めてみたもののCPUを1から実装したことがなく、全体像が見えなかったためすでにある実装を参考にさせてもらった。
rocket chip など有名な実装は規模の大きいため、まずは簡潔な実装をしているSodorを参考に実装しながら勉強することにした。
この記事ではそのSodorについて調べて分かったことをまとめていく
Sodorについて
Sodorは教育用に考案されたRISC-VのCPUの一種(subset?)
パイプラインが1段~3段の実装があり、1段の実装は処理内容が2つのファイルにほとんど収まっているのでまずはこれを参考にした。
動作環境を作る
Sodor自体はmakeコマンドでテストケースを走らせることができるが、
どんなことをしているか調べるため1段パイプラインのテストケースを個々に実行するためにソースコード等を抜き出して調査環境を整える。
で、調べてみた結果テストケースを走らせるために必要なコマンドは以下の流れになっていた。
RISCV_ISA=$HOME/workspace/risc-practive/riscv-sodor/riscv-isa-sim
RISCV_LIB=$HOME/workspace/risc-practive/tmp/tests1/emulator/fesvr/libfesvr.a
ELF_FILE=$RISCV/target/share/riscv-tests/isa/rv32ui-p-add
sbt "run -td generated-src"
g++ -O1 -std=c++11 -g -I$PWD/emulator/common -I$RISCV_ISA -I$VERILATOR_ROOT/include -c -o emulator/common/SimDTM.o emulator/common/SimDTM.cc
(cd emulator/fesvr; ../../riscv-isa-sim/configure --enable-sodor )
make -C emulator/fesvr libfesvr.a &&
verilator --cc --exe --top-module Top +define+PRINTF_COND=1 --assert --output-split 20000 --x-assign unique \
-I$PWD/vsrc -O3 \
-CFLAGS "-O1 -std=c++11 -g -I$PWD/emulator/common -I$RISCV_ISA -DVERILATOR -include $PWD/emulator/common/verilator.h" \
-LDFLAGS "$RISCV_LIB -lpthread" \
--trace -o $PWD/emulator/emulator-debug \
$PWD/generated-src/Top.v \
$PWD/vsrc/SimDTM.v \
$PWD/emulator/common/emulator.cpp \
$PWD/emulator/common/SimDTM.o
make -C obj_dir -f VTop.mk
set -x
./emulator/emulator-debug -voutput/rv32ui-p-add.vcd +max-cycles=30000 $ELF_FILE | tee result1.log
set +x
前提としてSodorではchisel環境ではRTLの生成のみを行い、
シミュレーションは生成したRTLを改めてVerilatoでコンパイルして実行していた。
コマンドの流れとしてはまずsbtでVerilogを生成。
次にSimDTMのオブジェクトファイルを生成する。これはDPI-Cと呼ばれる機能(フレームワーク?)を使ってSystemVerilogから呼び出すC++の関数をコンパイルしている。
次にriscv-isa-simにあるRISC-Vデバッグ用のサーバライブラリをビルドしている。内容については後述するがsodorではriscv-isa-simの特定バージョンのみに含まれる--enable-sodor
オプションを使ってsodor用にビルドしている。
最後に生成したRTLやライブラリをVerilatorに食わせて実行ファイルを生成後、テストプログラムとしてriscv-testsに含まれるrv32ui-p-addを実行している。
シミュレーションの構成
次に全体の動作を見てみる。
シミュレーション実行時の構成は以下の図のようになっている。
main関数ではchiselからVerilogに変換されたCPUモジュールにクロックを供給し、Topモジュールのsuccess信号がONになるかタイムアウトするまでクロックを回す。
dtm_tはメソッド自体はDPI-Cを介して呼び出される。
主な処理内容はテストプログラムの書き込みで今回はRISC-Vの命令セットテスト用のプログラムを下記のriscv-testsからビルドして実行させた。
dtm_tからのメモリ書き込みはRISC-Vのデバッグしように基づいて行われている。
がSodorのデバッグモジュールは完全とは言えないようなのでsodor用のオプションなしでlibsvr.aをビルドした場合、書き込みがうまくいかないようだ。
Debug Specification - RISC-V International
CPUコアの構成について
次に上のSodor全体図Coreの構成についても大まかにまとめる
Tileはcore, memory, debugで構成されておりCPUとしての処理はcore, memoryで実行する。
その一方で外部からはdebugを介してcoreのレジスタやmemoryのデータにアクセスできるようになっている。
memoryはdebug用と命令フェッチ用とデータR/W用の3つのポートの構成になっている。
memory自体の記述としてはポートの数をパラメータで容易に増減させられて、メモリのどのポートも遅延なしでリードできる記述となっている。
Coreはcpathとdpathに分かれている。
CPUとしての主な処理はdpathで行われており、cpathは入力された命令をパースした結果をdpathに通知している。
まとめ
今回はRISC-Vを実装してみる前に既存の実装としてSodorの実装がどうなっているかを見ていった。とりあえず全体の構成をみれて、シミュレーションの方法も見当がついたので実装をしながらRISC-Vの理解を深めていきたいと思う。
3段にした場合やオリジナルの命令を組み込んだ場合の構成についても後々踏み込んでいきたいと思う。
Cyclone V Soc でLチカ
前回の記事ではDE10-nanoの起動用SDカードを作成した
今回の記事ではFPGAをコンフィグしてFPGA側のLEDをC言語のプログラムで操作する
(何番煎じだというかんじですが)
設定手順
今回は下記の手順で設定を行う
1.FPGAデータの合成
FPGAへの書き込みに使用するデータのプロジェクトには今回もTerasicが出している
DE10_NANO_SoC_GHRDを利用する
makeファイルがあるため合成にはコマンドラインでmake rbf
を実行することで、
FPGAの書き込みに使うoutput_files/DE10_NANO_SoC_GHRD.rbf
が生成される
ただし、Quartus 19.xで合成しようとするとQsysコンポーネントで使用しているPIOのIPが見つからないためエラーになってしまう。なので、合成にはQuartus 18.1で合成した。
2.u-bootからのFPGAコンフィグ
作成したRBFファイルをFPGAに書き込む。
FPGAのコンフィグはLinuxから行う方法とLinuxのブート前にu-bootのスクリプトから行う方法があるが、今回はu-bootからのコンフィグを行う。
まずは、RBFファイルをu-bootから読めるようにFATパーティション(zImageとかを入れたパーティション)に入れる。
次にu-bootのコンソールからFPGAのコンフィグスクリプトを登録する
u-bootのコンソールに入るには起動時にシリアルコンソールで下記のメッセージが表示された際にキー入力を行う。
In: serial
Out: serial
Err: serial
Model: Altera SOCFPGA Cyclone V SoC Development Kit
Net: eth0: ethernet@ff702000
Hit any key to stop autoboot:
FPGAをコンフィグするには1度メモリ上にデータを展開して、FPGAにそのデータを流す
そのためのコマンドは下記のようになる
fatload mmc 0:1 ${loadaddr} DE10_NANO_SoC_GHRD.rbf
fpga load 0 ${loadaddr} 2082088;
fatloadではDE10_NANO_SoC_GHRD.rbfを${loadaddr}
のアドレスに展開している
この${loadaddr}
は後のLinuxの展開に使われるアドレスだがその前に借りる形になる
fpga loadでは展開したデータをFPGAに書き込んでいる。2082088はFPGAデータのサイズで本来ならfatloadしたサイズを取得するべきだが、今回は固定値にしている。
(FPGAが変わらなければ固定値でも問題ないはず)
このコマンドがうまくいっていればLEDが点灯して一番端のLEDが点滅する
次にこれらのコマンドを自動で実行するように環境変数を追記する
通常、ブートの際にキー入力が行われなければmmc_bootのスクリプトからzImageのロード等が行われLinuxが立ち上がる
FPGAのコンフィグをこのブートの流れに組み込むために、setenv, editenvコマンドを使って下記の通り変数を追記、編集する
callscript=if fatload mmc 0:1 ${loadaddr} soc_system.rbf; then fpga load 0 ${loadaddr} 2082088; bridge enable; echo done config FPGA; else echo not found DE10_NANO_SoC_GHRD.rbf;fi;
mmc_boot=run callscript; if mmc dev ${devnum}; then devtype=mmc; run scan_dev_for_boot_part; fi
saveenv
この変更でmmc_bootでboot前にcallscriptが実行される
callscriptではFPGAデータのロードに成功すればFPGAのコンフィグを行う
また、bridge enableというコマンドを実行しているが、これはFPGA-CPU間の通信のためのポートに関して、レジスタ設定を行っている
(このあたりのu-bootのコマンド一覧についてはこちらで調べられている)
最後にsaveenvすることで再起動しても今回の変更が反映される
3.Lチカプログラムの作成実行
最後にFPGAのレジスタを操作してLEDを点灯(消灯)させる
LEDを制御するモジュールはQsysの設定でlw_hps2fpgaの0x0000_3000番地に接続されている。lw_hps2fpgaのベースアドレスは0xff20_0000(ここでCyclone Vのアドレスマップが確認できる)
なので、0xff20_3000番地に値をかきこむことでLEDのON/OFFが切り替えられる。
C言語でこの物理アドレスにアクセスするプログラムは下記のようになる
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <errno.h>
int main()
{
volatile unsigned int *address;
unsigned int lw_h2f_led = 0xff203000U;
unsigned int map_size = 0x1000U; //map_sizeは適当(小さすぎるとページングの関係でマッピングに失敗するため)
int fd;
/* メモリアクセス用のデバイスファイルを開く */
/* キャッシュを無効化するためO_SYNCフラグをたてる*/
if ((fd = open("/dev/mem", O_RDWR | O_SYNC)) < 0) {
perror("open");
return -1;
}
/* lw_h2f_ledをマッピングした仮想アドレスを取得する*/
address = (int*)mmap(NULL, 0x1000U, PROT_READ | PROT_WRITE, MAP_SHARED, fd, lw_h2f_led);
if (address == MAP_FAILED) {
perror("mmap");
close(fd);
return -1;
}
printf("led status check.\n");
printf("led status : 0x%08x.\n", address[0x00]); /* FPGAのレジスタからデータを取得*/
address[0x0] = 0x00; /* LEDは初期状態では点灯しているので消灯 */
printf("led status : 0x%08x.\n", address[0x00]); /* 変更が反映されていることを確認*/
munmap((void*)address, map_size);
close(fd);
return 0;
}
このプログラムをarmのクロスコンパイラでコンパイルした後、生成されたa.outをDE10-nanoで実行するとLEDが消灯する(0を書き込んだので)
これで自分の書いたHDLにCPUからアクセスするための環境ができた(AvalonとかAXIとかのInterfaceの話は残っているが)ので、今後はHDLを書いたり、それ用のLinuxドライバも作っていきたいと思う。
間違っているところ等あれば教えていただけると嬉しいです。
DE10-Nanoブート用のイメージの作成手順について
DE10-nanoで色々試す(遊ぶ)ためにLinuxカーネルやブートローダ、ディストリビューションのビルドを行ったのでその手順を備忘録としてまとめる
背景
今回、DE10-nanoで遊んでみようと思ったのはHDLを色々かいて勉強する際にFPGAで実際に動かしてみようと思ったのがきっかけ
Cyclone V SoCを選んだのは、SoC FPGAでデバイスドライバとかの足回りについても調べてみようかなと思ったのと、Cyclone Vなら数年前に少し触ったのが理由
(時間とお金があればZynq, Zynq MPとかにも挑戦したい)
参考文献
今回、カーネル等のビルドの手順を調べてみたところmacnicaのサイトにビルド手順がまとめられていたのでこれらを参考にした。
2.インテル® SoC FPGA 向け Linux ビルド方法 - 半導体事業 - マクニカ
ビルド手順
これから行うビルドの手順は以下の通り
2.u-bootのビルド
3.Angstromのビルド
4.SDカードへの書きこみ
AngstromのビルドでLinuxカーネルやu-bootも生成されるが今回はデバイスドライバのクロスコンパイルも考えているので別にビルドを行う
また、ビルドにかかる時間を短縮するためにAWSのt2.xlargeインスタンス(ubuntu16.04)でビルドを行った
1.Linuxカーネルのビルド
まずはビルドに使うコンパイラをダウンロードし、環境変数に登録する
参考にしたサイトではバージョン2015.06のコンパイラであったが、最新のものを試してみる
$ wget https://releases.linaro.org/components/toolchain/binaries/7.5-2019.12/arm-linux-gnueabihf/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz
$ tar xf gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz
$ export PATH=`pwd`/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin/:$PATH
$ export ARCH=arm
$ export CROSS_COMPILE=`pwd`/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf-
$ git clone https://github.com/altera-opensource/linux-socfpga
$ cd linux-socfpga
$ git checkout -t -b test origin/socfpga-4.14.130-ltsi
$ make socfpga_defconfig
$ make -j 24 zImage dtbs
linux-socfpga/arch/arm/boot/zImage
(圧縮イメージ)linux-socfpga/arch/arm/boot/dts/socfpga_cyclone5_socdk.dtb
(デバイスツリー)の2つのファイルを今回利用する
2.u-bootのビルド
u-bootのビルドでは別のコンパイラを使うのでダウンロードして、環境変数を設定しなおす。こちらでも最新のコンパイラを使ってみる
$ wget https://releases.linaro.org/components/toolchain/binaries/7.5-2019.12/arm-eabi/gcc-linaro-7.5.0-2019.12-x86_64_arm-eabi.tar.xz
$ tar xf gcc-linaro-7.5.0-2019.12-x86_64_arm-eabi.tar.xz
$ export PATH=`pwd`/gcc-linaro-7.5.0-2019.12-x86_64_arm-eabi/bin:$PATH
$ export ARCH=arm
$ export CROSS_COMPILE=`pwd`/gcc-linaro-7.5.0-2019.12-x86_64_arm-eabi/bin/arm-eabi-
$ ~/workspace/opt/intelFPGA/19.1/embedded/embedded_command_shell.sh
$ cd DE10_NANO_SoC_GHRD
$ rm -rf software
$ mkdir -p software/bootloader
$ bsp-create-settings --type spl --bsp-dir software/bootloader --preloader-settings-dir "hps_isw_handoff/soc_system_hps_0" --settings software/bootloader/settings.bsp
$ cd software/bootloader
$ git clone https://github.com/altera-opensource/u-boot-socfpga
$ cd u-boot-socfpga
$ git checkout -t -b test origin/socfpga_v2019.04
$ ./arch/arm/mach-socfpga/qts-filter.sh cyclone5 ../../../ ../ ./board/altera/cyclone5-socdk/qts/
$ make socfpga_cyclone5_defconfig
$ make -j 24
これでu-bootがビルドされる。今回使うのはSPLとu-bootが一体化したu-boot-with-spl.sfpファイル
3.Angstromディストリビューションのビルド
今回はRocketboard.orgのリポジトリがあるAngstromのディストリビューションを使う
バージョンとしてはAngstom-v2019.06-warrior
をビルドしていく
とりあえずおまじないとして以下のコマンドを実行する
$ mkdir angstrom-build-v2019.06
$ cd angstrom-build-v2019.06
$ wget http://commondatastorage.googleapis.com/git-repo-downloads/repo
$ chmod 777 repo
$ ./repo init -u git://github.com/Angstrom-distribution/angstrom-manifest -b angstrom-v2019.06-warrior
$ wget http://releases.rocketboards.org/release/2019.10/src/altera.xml
$ mkdir -p .repo/local_manifests
$ mv altera.xml .repo/local_manifests/
$ ./repo sync
$
layers/meta-altera-refdes/conf/layer.conf
の12行目にあるLAYERSERIES_COMPAT_meta-altera-refdes = "zeus"
をコメントアウトするmeta-altera-refdes
の最新版はAngstrom-v2019.12-zeus
を指定しているのに対し、meta-altera
の最新版はAngstrom-v2019.06-warrior
用になっているのでこのままではビルドできない。meta-altera-refdes
のバージョン指定をコメントアウトすることでとりあえずビルドできるようになる(設定ファイルの変更内容を入れるのは冗長な気がするがこれもまとめとして入れておく)
$ MACHINE=cyclone5 . ./setup-environment
$ sed -i '/meta-altera/a \ \ ${TOPDIR}\/layers\/meta-altera-refdes \\' conf/bblayers.conf
$ sed -i '/meta-atmel/d' conf/bblayers.conf
$ sed -i '/meta-freescale/d' conf/bblayers.conf
$ echo "DISTRO_FEATURES_remove = \" wayland \"" >> conf/local.conf
$ echo "DISTRO_FEATURES_remove = \" alsa \"" >> conf/local.conf
$ export KERNEL_PROVIDER=linux-altera-ltsi
$ export KERNEL_TAG=refs/tags/ACDS19.3_REL_GSRD_PR
$ export KBRANCH=socfpga-4.14.130-ltsi
$ export BB_ENV_EXTRAWHITE="$BB_ENV_EXTRAWHITE KBRANCH KERNEL_TAG UBOOT_TAG KERNEL_PROVID
$ bitbake gsrd-console-image
deploy/glibc/images/cyclone5/gsrd-console-image-cyclone5.tar.gz
4.SDカードへの書きこみ
zImage
socfpga_cyclone5_socdk.dtb
u-boot-with-spb.sfp
gsrd-console-image-cyclone5.tar.gz
作成するのは
$ sudo fdisk /dev/sdb
コマンド (m でヘルプ): o
選択 (既定値 p): p
パーティション番号 (1-4, 既定値 1): 3
最初のセクタ (2048-124735487, 既定値 2048):
最終セクタ, +セクタ番号 または +サイズ{K,M,G,T,P} : +16MB
コマンド (m でヘルプ): t
16 進数コード (L で利用可能なコードを一覧表示します): a2
コマンド (m でヘルプ): n
選択 (既定値 p): p
パーティション番号 (1,2,4, 既定値 1): 1
最初のセクタ (32768-124735487, 既定値 32768):
最終セクタ, +セクタ番号 または +サイズ{K,M,G,T,P} : +128M
コマンド (m でヘルプ): t
16 進数コード (L で利用可能なコードを一覧表示します): b
コマンド (m でヘルプ): n
選択 (既定値 p): p
パーティション番号 (2,4, 既定値 2): 2
最初のセクタ (294912-124735487, 既定値 294912):
最終セクタ, +セクタ番号 または +サイズ{K,M,G,T,P} :
コマンド (m でヘルプ): w
$ sudo mkdir /mnt/sdcard1
$ sudo mount /dev/sdb1 /mnt/sdcard1
$ sudo cp zImage /mnt/sdcard1/.
$ sudo cp socfpga_cyclone5_socdk.dtb /mnt/sdcard1/.
$ sudo sh -c "echo 'LABEL Linux Defaul' > /mnt/sdcard1/extlinux/extlinux.conf"
$ sudo sh -c "echo ' KERNEL ../zImage' >> /mnt/sdcard1/extlinux/extlinux.conf"
$ sudo sh -c "echo ' FDT ../socfpga_cyclone5_socdk.dtb' >> /mnt/sdcard1/extlinux/extlinux.conf"
$ sudo sh -c "echo ' APPEND root=/dev/mmcblk0p2 rw rootwait earlyprintk console=ttyS0,115200n8' >> /mnt/sdcard1/extlinux/extlinux.conf"
$ sudo umount /mnt/sdcard1
gsrd-console-imae-cyclone5.tar.gz
を展開する
$ sudo mkfs.ext3 /dev/sdb2
$ sudo mkdir /mnt/sdcard2
$ sudo mount /dev/sdb2 /mnt/sdcard2
$ sudo tar xf gsrd-console-image-cyclone5.tar.gz -C /mnt/sdcard2/
$ sudo umount /mnt/sdcard2
u-boot-with-spl.sfp
を書きこむ$ sudo dd if=u-boot-with-spl.sfp of=/dev/sdb3
非同期回路の2段FFについて
FPGAで非同期入力に対してはFFを2段挿入することは知っていたが
その理由をよくわかっていなかったので調べて分かったことを
(投稿の練習も含めて)備忘録として書いておく
主に参考にしたのは以下の2つのサイトです
1 . 非同期クロック と 検証手法−1 - 半導体事業 - マクニカ
2. 電気回路/HDL/非同期信号を扱うための危ういVerilogライブラリ - 武内@筑波大
まず、非同期の入力をFPGAの内部クロックでラッチしようとすると
FPGAのセットアップホールドに違反するタイミングで入力される場合がある。
違反した入力を受けたレジスタはメタステーブルと呼ばれる不安定な出力状態になる。
このメタステーブルへの対策としてFFを2段(以上)挿入する。
肝心のFFを2段挿入する理由は、1段目のFFがメタステーブル状態になっても次のクロックまでには安定した状態に戻るため、2段目のFFは安定した状態をラッチすることができる。
もし、1段目のFFと2段目のFFの間に組み合わせ回路があったりして遅延時間があると
2段目のFFは不安定な入力をラッチしていしまい誤動作のもとになる。
とりあえず、FFを2段挿入する理由は上記のイメージで理解することができたが
参考にしたマクニカのサイトによると3段以上挿入する場合も増えてきたらしい。
3段にするとメタステーブル状態をより抑えられる理由が
・単純に1段目と2段目の遅延が短く仕切れないから
なのか
・複数クロックにわたるメタステーブルを抑制するため
なのか分かれば調べてみたいなと思った。
また、Quartusで段数をどのくらいにすればいいかを調べられるみたいなのでそれも使えるようになってみたいなとも思った。
以上、間違っているところなどあれば教えてくださるとうれしいです。