Moiz's journal

プログラミングやFPGAなどの技術系の趣味に関するブログです

ラズベリーパイ4にCLionをインストール

CLionとラズベリーパイ4

はじめに

先日まで知らなかったのですが、CLionやPyCharmはラズベリーパイで動作するようです。

とくにしかけは必要ありません。ただ配布されているパッケージをインストールするだけです。 どうやらCLionやPyCharm含めたIntelliJ系のIDEはほとんどの部分Javaで動作しているようです。

ただし、オフィシャルにサポートされているわけではないようなので、試される方は自己責任で実行ください。

また、当ブログの著者はこのブログで紹介した内容を実行した結果について、いかなる責任も負いかねます。

CLionをラズベリーパイ4にインストール

CLionをラズベリーパイ4にインストールしてみます。使用した機種はRaspberry Pi 4B 4GB版で、OSはRaspbian OSの32bit版(Buster)です。

Raspberry Pi OSの64bit版でもインストールできましたが、手順に一部細かな違いがありますので、そういった点も紹介します。

まずはLinux版のパッケージをダウンロードします。

f:id:uzusayuu:20200608054748p:plain
CLionのダウンロードページ

ダウンロードしたら解凍して、インストールしたいフォルダに移動します。 今回は/home/pi/bin/にインストールしました。

# ダウンロードしたフォルダで実行
# ファイル名はダウンロードした実際のファイルに一致させる
tar -xvf CLion-2020.1.2.tar.gz

# インストールしたいフォルダを指定。今回は~/bin/にインストールする。
mkdir -p /home/pi/bin
mv clion-2020.1.2 /home/pi/bin

Java VMがインストールされていなければ、インストールします。 自分が試したところ、64bit版のRaspberry Pi OSではデフォルトではインストールされていませんでした。

sudo apt update
sudo apt install default-jdk

あとはbinフォルダ以下のclion.shを実行します。

/home/pi/bin/clion-2020.1.2/bin/clion.sh

うまく動作すれば、プライバシーライセンスなどの確認画面が表示されますので、レビューして問題なければ許諾して進みます。

初回起動時はスクリプトの設定やプラグインの設定画面がでます。今回はデフォルトのままで進みます。

また、途中でライセンスの確認画面が出るので、ライセンスを持っている場合は入力する必要があります。なければ評価版として進めて行きます

CLionの実行環境の設定

初回の実行時、自分の環境ではこのような設定画面が表示されました。

f:id:uzusayuu:20200608054901p:plain
CLion設定、変更前

どうもバンドルされているcmakeが実行できないのが原因のようです。おそらくx86用のものしか付属しないのでしょう。

解決するためにcmakeをインストールして、CLionに設定します。

sudo apt install cmake

cmakeとgdbのパスを設定すると、のこりは自動で設定されます。

f:id:uzusayuu:20200608054945p:plain
CLion設定、変更後

これでCLionが起動できます。

f:id:uzusayuu:20200608055032p:plain
CLion起動画面

CLionの動作確認

この節は単なる動作確認です。 通常のLinuxでの動作と同じなので、CLionをご存知の方は読み飛ばしても問題ありません。

さっそくNew Projectを選んでみましょう。C++ Executableを選択して、LOCATIONをhelloにします。

f:id:uzusayuu:20200608055111p:plain
New Project

Createをクリックして次にすすみます。

すでに"Hello, world!"と表示するデフォルトのmain.cppができています。

f:id:uzusayuu:20200608055133p:plain
hello world code

右上の三角形をクリックして実行します。

f:id:uzusayuu:20200608055154p:plain
Hello world execution

Hello, world!と表示されました。動作確認成功です。

追加の設定

2回めの起動以降、外部のファイルをモニターするfsnotifierが動作しないという警告が表示されました。 これは、fsnotifierをコンパイルしてパスの設定をすることで回避できます。くわしくはこちらを参照ください。

Compiling File Watcher - IntelliJ IDEA - Confluence

上記の記事にしたがって、必要な実行ファイルを作成してCLionに設定してみます。

まず適当なフォルダを作って、その中で以下のコマンド実行して必要なファイルをダウンロードします。 これは一つ一つブラウザなどからダウンロードしても構いません

wget https://raw.githubusercontent.com/JetBrains/intellij-community/master/native/fsNotifier/linux/fsnotifier.h
wget https://raw.githubusercontent.com/JetBrains/intellij-community/master/native/fsNotifier/linux/inotify.c
wget https://raw.githubusercontent.com/JetBrains/intellij-community/master/native/fsNotifier/linux/main.c
wget https://raw.githubusercontent.com/JetBrains/intellij-community/master/native/fsNotifier/linux/make.sh
wget https://raw.githubusercontent.com/JetBrains/intellij-community/master/native/fsNotifier/linux/util.c

次にmake.shを実行します。

chmod u+x make.sh
./make.sh

32bit版だと、fsnotifier-armv7lというファイルができています。(64bit版ではfsnotifier-aarch64)

これをpycharmのbinフォルダにコピーします。 以下は先程のインストールパスの場合の例です。違うフォルダにインストールした場合は、そのフォルダにコピーしてください。

cp fsnotifier-armv7l ~/bin/clion-2020.1.2/bin/

CLionに戻って、Help -> Edit Custom Propertiesを選択します。ファイルを作るか聞かれたらYesを押して作成します。

以下の行を入力して、セーブします。

32bit版の場合

idea.filewatcher.executable.path = fsnotifier-armv7l

64bit版の場合

idea.filewatcher.executable.path = fsnotifier-aarch64

f:id:uzusayuu:20200608055221p:plain

これでfsnotifierに関する警告はでなくなると思います。

その他のIntelliJ製品

Raspbian上にPyCharm CEをインストールしてみたところ、こちらもインストールができました。

f:id:uzusayuu:20200608031309p:plain

まとめ

ラズベリーパイ4にCLionをインストールしてみました。 最低限必要な事前準備ははJava VMがインストールされている事と、cmakeなどのC++開発自体に必要な環境が構築されていることを確認することだけです。

自分自身インストールしたばかりでどんな使い勝手かまだわかりませんし、どんな不具合があるかも予想できませんが、いろいろ試してみようと思います。

Nexdock 2 を買いました

Nextdock 2 を買いました

去年注文したNexdock 2が一月ほど前に届きました。

f:id:uzusayuu:20200607144519j:plain
NexDock 2 とラズベリーパイ4

最初は癖があって使いにくいと思ったのですが、使いこなし方のコツを覚えたらだいぶ実用的になりました。

NextDock って?

NexDockというのは、FullHDモニターとキーボード、バッテリーが一体化した製品です。

nexdock.com

ノートPCとの最大の違いは、プロセッサやストレージが載っていない点です。PCや外部ディスプレイ対応のスマートフォンを接続して使うのが前提です。 私はラズベリーパイ4を接続してつかっています。

私が使っているのはNexDock 2ですが、こちらはもう注文を受け付けていないようです。 今はタッチ機能付きのNexDock Touchの注文を受け付けているようです。

使い勝手の方ですが、とっつきは良くないものの、うまく使いこなせれば悪くないと思います。 ただ、自分もなれるまで苦労したので、そういう試行錯誤や不安定さが許容できない人(例えば仕事で本格的に使いたいなど)は、事前に調査してから購入された方が良いと思います。

ラズベリーパイとの接続

ラズベリーパイとの接続には、USBケーブルと、HDMIケーブルを使います。 癖があるというのは主にこの部分に関するものです。

マニュアル通りに接続すると、NexDock2 とラズベリーパイの間の接続は*1

  • Raspberry Pi Micro HDMI 出力 -> Micro HDMI to HDMI 変換器 -> HDMIケーブル -> NexDock2 HDMI
  • Raspberry Pi USB Type-A -> 専用ケーブルのUSB Type-A端子 -> Nex Doc 2 USB Type-C
  • Raspberry Pi USB Type-C 電源 -> Type-C to Micro USB 変換器 -> 専用ケーブルのMicro USB端子 -> NexDock 2 USB Type-C

となります。(なお、ケーブルや変換器はすべて製品パッケージに含まれています。) どうもこの接続が厄介で、

  • 変換器に力がかかりやすい
  • 専用ケーブルが二股な上、硬くて扱いにくい
  • USB接続が不安定な気がする
  • 専用ケーブルがさせるNexDock 2 USB Type-Cポートは特定の一つだけで間違えやすい

といった問題がありました。とくに不安定なのは問題で、ボタンを押していないのに特定のキー入力が続いたりすることまでありました。 ただこのあたりは

  • 使いやすいMicro HDMI to HDMIケーブルにとりかえ
  • 専用ケーブルをやめ、USB Type-C ケーブルを使う

という方法で改善できました。

特に後者はマニュアルには記載がなかったのですが、NexDock2のサポートページに方法がのっていました ラズベリーパイのconfig.txtに以下の行を付け加えると、ラズベリーパイ4の電源用USB Type-C端子が、USB接続にも使えるようになるそうです。

# USB Type-C Host
dtoverlay=dwc2,dr_mode=host

ラズベリーパイの電源端子は電源専用だと思っていたので驚きました。

これで接続に必要なケーブルが2本に減り、しかも専用ケーブルを使わずにすむのでだいぶスッキリしました。 ケーブルの取り回しが楽になったので、ラズベリーパイ本体をモニター裏に貼り付ける、なんてこともやりやすくなりました。

f:id:uzusayuu:20200607150846j:plain

その他のTip

その他の問題と、改善方法を紹介します

タッチパッドが敏感すぎる

タッチパッドが大きい上に敏感なので、キー入力中についつい触れてしまうことがありました。 こちらはフォーラムで「Fn + ESCでタッチパッドが一時的にオフにできる」という投稿を見つけて解決しました。

今は画像のようにマウスと併用していますが、持ち運んでいるときなどはタッチパッドに切り替えて使うこともできます。

画像がブランクになると電源が切れる

NexDock2の電源はHDMI入力に連動しているので、ラズベリーパイの画像出力が止まると電源が切れます。 通常の接続では、ラズベリーパイの電源はNexDock2から供給されるので、ラズベリーパイの電源も切れます。 しかもよりによって、ラズベリーパイのスクリーンセーバーの初期設定は、画像出力をオフにする事になっているようです。 この組み合わせのせいで、ラズベリーパイをしばらく放置していると電源が切れている事がよくありました。

これは画像の出力がオフにならないようにすることで解決できます。 自分はxscreensaverをインストールしてスクリーンセーバーをオフにしました。

その他

あいにく自分の使っているスマートフォンは外部モニタに対応していないのですが、HDMI出力のあるものなら大概接続できるはずです。 USBで電源供給できるものなら、モバイル化するのも容易です。

たとえば、メガドラミニをつなげば、どこでもメガドラミニが!

f:id:uzusayuu:20200607152605j:plain

まとめ

癖があるので万人におすすめするものではありませんが、NexDock 2はなかなか面白いデバイスです。

特にラズベリーパイ4と接続する際は

  • config.txtの設定で、接続に必要なUSBケーブルの本数を一本にする
  • Micro HDMI to HDMIのケーブルにかえる
  • とりまわしのしやすいUSB Type-C ケーブルにかえる
  • xscreensaverを導入するなどして、画像出力がブランクにならないようにする
  • Fn+ESCを使って、不要なときはタッチパッドをオフにする

といった方法で、使い勝手を向上させる事ができました。

*1:ちなみに対応スマートフォンとの接続にはUSB Type-Cケーブル一本ですみます。

RISCVエミュレータ-ELFファイルの実行

はじめに

前回前々回のエントリーの続きです。

ゆっくりとRISCVエミュレータを作っています。

命令の追加

RV32Iの命令の殆ど(SRETとWFI以外)を処理できるようになりました。 ただし、システムレジスタ系の命令(CSRR*)は、読み書きはできるものの内部状態は実装されていません。 同じ理由でMRETも、権限の変更は行いません。

システムコールエミュレーション

ECALL命令ではシステムコールのエミュレーションを実行します。今実行できるのは以下の2つです。

システムコール 番号 処理
exit 93 終了
write 64 ファイルへの書き出し

fstat等の実装がまだなので、writeが出力するのは標準出力だけです。

ELFファイルの読み込み

簡単なELFファイルを読みこんで実行できるようになりました。 これで普通のC言語でRISCVプログラムを書いて、GCCでクロスコンパイルして、RISCVエミュレータで実行する事ができます。

RISCV用のクロスコンパイル環境はriscv-gnu-toolchainを使用しました。

github.com

実行できる命令が限られているので、このエミュレータ用のコンフィギュレーションにするには、toolのビルドのときに、

./configure --prefix=/opt/riscv --with-arch=rv32i --with-abi=ilp32

を指定して、I命令とILP32のABIを指定する必要があります。

これで、例えばこんなCプログラムが実行できます。

#include <unistd.h>

int main()
{
  write(1, "Hello, RISCV\n", 13);
  return 0;
}

コンパイルするときはriscv用コンパイラを使って、RV32IとILP32を指定して、ライブラリを静的にリンクする必要があります。 例えばこうなります。(gccにパスが通っている前提です。)

$ riscv32-unknown-elf-gcc hello.c -o hello -Wall -march=rv32i -mabi=ilp32 -static

これでhelloという名前のELF形式実行ファイルが作られます。

ちなみに-Sオプションをつけてコンパイルするとこんなアセンブラコードになります。

        .file   "hello.c"
        .option nopic
        .attribute arch, "rv32i2p0"
        .attribute unaligned_access, 0
        .attribute stack_align, 16
        .text
        .section        .rodata
        .align  2
.LC0:
        .string "Hello, RISCV\n"
        .text
        .align  2
        .globl  main
        .type   main, @function
main:
        addi    sp,sp,-16
        sw      ra,12(sp)
        sw      s0,8(sp)
        addi    s0,sp,16
        li      a2,13
        lui     a5,%hi(.LC0)
        addi    a1,a5,%lo(.LC0)
        li      a0,1
        call    write
        li      a5,0
        mv      a0,a5
        lw      ra,12(sp)
        lw      s0,8(sp)
        addi    sp,sp,16
        jr      ra
        .size   main, .-main
        .ident  "GCC: (GNU) 9.2.0"

こちらが実行結果です。

$ cmake-build-debug/RISCV_Emulator hello
Elf file name: hello
This is an Elf file
Program Header 0:Type: LOAD. Copy to 0x10000 from 0x0, size 5270. 
Memory size extended to 100000
Loaded
Program Header 1:Type: LOAD. Copy to 0x12498 from 0x1498, size 2132. Loaded
Section .bss found at 0x01cec.
Secure BSS.
Entry point is 0x10090
Section .symtab(2) found at 0x01d1c.
Number of symbols = 92, (1472 bytes)
Section .strtab found at 0x022dc.
Symbol "__global_pointer$" found at index 34.
Global Pointer Value = 0x12ca8.

Memory size extended to 800000
Execution start
Hello, RISCV
Return value: 0.

いろいろエミュレータが生成したメッセージに紛れていますが、最後に"Hello, RISCV"と表示されています。成功です。

この例ではwrite以外のシステムコールを使わずに済むように、write()を使って出力させましたが、printf()でも動作するようです。

RISCV-TESTSの実行。

前回のエントリでも触れましたが、RISCVにはRISCV-TESTSという名前の標準テストがあります。

github.com

このテストのうち、rv32ui(RV32ユーザーレベル、整数命令のみ)を実行してみたところ、LH命令にサイン拡張のバグが見つかりました。

テスト重要ですね。

他にも何個か見つかったバグを修正し、すべてのテストがパスするようになりました。

まとめ

RISCVのエミュレータC++で書いて、簡単なプログラムのELF形式実行ファイルを処理できるようになりました。

また、RISCV-TESTSのユーザーレベル整数命令テストをパスすることを確認しました。

このエミュレータのコードは私のgitリポジトリで公開しています。

github.com

RISCVエミュレータの途中経過

はじめに

前回のエントリーの続きです。

ちょっと今週来週雑用で作業ができなそうなので、忘れないように自分用の進捗メモです。 またしても読者の方々の事はあまり考えていません。

わざわざページ開いてくださった方には、もうしわけありません。いつかまとめてエントリーかきます。

このプロジェクトについて

RISC-Vのエミュレータをゆっくり作っています。最終目標はFPGA上で動作させることです。

github.com

すでに同様のものが多数あるのは知っていますが、自分の趣味と学習が目的ですので特に気にしていません。

命令の追加

命令を多数追加しました。いま実装されている命令は次のとおりです。

命令 タイプ
add rd, rs1, rs2 R
addi rd, rs1, im12 I
and rd, rs1, rs2 R
andi rd, rs1, imm12 I
beq rs1, rs2, offset B
bge rs1, rs2, offset B
bltu rs1, rs2, offset B
bne B
jal rd, offset21 J
jalr rd, offset(rs1) I
lw rd, offset(rs1) I
lui rd, imm24 U
or rd, rs1, rs2 R
ori rd, rs1, imm12 I
sll rd, rs1, rs2 R
slli rd, rs1, shamt I
slt rd, rs1, rs2 R
slti rd, rs1, imm12 I
sltiu rd, rs1, imm12 I
sltu rd, rs1, imm12 R
sra rd, rs1, imm12 R
srai rd, rs1, imm12 I
srl rd, rs1, imm12 R
srli rd, rs1, imm12 I
sub rd, rs1, rs2 R
xor rd, rs1, rs2 R
xori rd, rs1, imm12 I

結構増えました! RV32Iの50命令中、22個実装しています。

実行できるようになったもの。

命令を増やしたので、実行できるアルゴリズムが増えました。 例えばRISC-V原典にのっているインサートソートが実行できます。

// A1 is n and A3 points to array[0]
// A4 is i, A5 is j, A6 is x
addi A3, A0, 4
addi A4, ZERO, 1
// Outer Loop
bltu A4, A1, 8
jalr ZERO, RA, 0
lw A6, A3, 0
addi A2, A3, 0
addi A5, A4, 0
// Inner Loop
lw A7, A2, -4
bgte A6, A7, 20
sw A2, A7, 0
addi A5, A5, -1
addi A2, A2, -4
bne A5, ZERO, -20
// End of Inner Loop
slli A5, A5, 2
add A5, A0, A5
sw A5, A6, 0
addi A4, A4, 1
addi A3, A3, 4
jal ZERO, -64
// End of Outer Loop

レジスターA3にソートする配列へのポインター、A1にソートする要素の数を代入して読み出すとソートしてくれます。 こちら動作結果です。

Before: 116 211 664 486 472 777 429 528 290 433 136 777 502 357 160 481 731 675 802 671 427 21 949 350 997 985 960 483 836 987 126 305 551 790 791 375 919 220 255 209 6 392 339 508 101 499 989 832 174 791 856 953 164 805 655 161 142 616 644 331 955 122 636 506 265 427 233 184 999 489 746 357 881 85 865 982 584 207 167 110 998 23 415 515 180 71 676 675 39 673 6 994 795 642 853 412 421 86 949 772

After: 6 6 21 23 39 71 85 86 101 110 116 122 126 136 142 160 161 164 167 174 180 184 207 209 211 220 233 255 265 290 305 331 339 350 357 357 375 392 412 415 421 427 427 429 433 472 481 483 486 489 499 502 506 508 515 528 551 584 616 636 642 644 655 664 671 673 675 675 676 731 746 772 777 777 790 791 791 795 802 805 832 836 853 856 865 881 919 949 949 953 955 960 982 985 987 989 994 997 998 999

なんだかCPUの動作っぽくなってきました。

テストの追加

複雑になってきたのでテストを追加しました。

今回のコードではテストは主に、アセンブラ作成部分のテストと、CPUの動作のテストに分かれています。

アセンブラ部分のテスト

アセンブラ作成部分では一度符号拡張で見つけにくいエラーをだしてしまったのでカバレッジを上げることにしました。 具体的には以下の部分をランダム化したテストを各命令毎に作成しました。

  • rd
  • rs1
  • rs2
  • immediate

命令タイプによって存在しないものは除きます。 テストの内容は、

  1. ランダム化したパラメータを作成
  2. ランダム化したパラメータからバイナリ命令を作成
  3. アセンブラ機能を使ってパラーメータからバイナリ命令を作成
  4. 2で作成したものと3で作成したものを比較。

というものです。 3でテストを作成したのも自分自身なので、同じ誤解を2回している可能性はありますが、そうではないテクニカルな失敗はかなり防げます。

このテストは実装した全命令に対してそれぞれ100回ずつ行っています。

CPU部分のテスト

CPU側では上記のような命令ごとのユニットテストは難しいのですが、なるべくそれに近いものを作っています。 たとえば、add rd, rs1, rs2のテストでは、次のようなバイナリー・コードを生成します。

addi rs1, ZERO, value1 & 0xFFF
lui rs1, (value1 >> 12)
addi rs2, ZERO, value2 & 0xFFF
lui rs2, (value2 >> 12)
add rd, rs1, rs2
addi A0, rd, 0
jalr ZERO, RA, 0

ここで、value1, value2, rd, rs1, rs2はランダム化されたテストパラメータです。

これでadd rd, rs1, rs2に与えるパラメータ(レジスタの組み合わせ、レジスタの内容)のカバレージはかなり上がると思います。

ランダム化したテストはそれぞれ100回ずつ行っています。

また、同様のテストは他のR Typeの命令(cmd rd, rs1, rs2形式のもの)や、addiなどI Typeの命令(cmd rd, rs1, imm12形式のもの)でも行いました。

これらのユニットテストの他に、前回の1から10まで足し合わせるプログラムと、前述のソートプログラムをテストとして、コードを変更するたびに実行しています。

次のステップ

  • Bタイプ命令(blt等)へのランダム化テストの追加
  • LD/SW命令等の追加(+テスト)
  • 外部ファイル読み取り
  • 最小限のシステムコールのエミュレーション(テキストの表示くらいしたい)
  • システム系命令の対応
  • テストの自動化
  • MMUとか割り込みとかどうしよう(最終目標はハードウェア実装なので)

先は長いですね。

追記

記事のアップロード後早速こんな有用なコメントをいただきました。ありがとうございます。

当該のテストはこちらです。今後のTODOにいれておきます。

https://twitter.com/LDScell/status/1175905081050329088?s=20 github.com

RISCVエミュレータを作り始めた

はじめに

RISCVエミュレータを作り始めました。このエントリーはそのメモです。 個人的なメモなので、読者の方の事はまだあまり考えていません。

ある程度めどがたったらもうちょっと読みやすいエントリーを書きます。

目標

(完全な車輪の再発明ですが、個人の趣味ですので。)

参考図書

リポジトリ

https://github.com/moizumi99/m99_riscv_emulator

作業

  • 「動かしてわかるCPUの作り方10講」(以下CPU10講)のエミュレータを元に、雛形を作成。

    • ターゲット言語をC++に変更。
    • 拡張に備えてファイルを分割

      • RISCV_Emulator.cc (メイン)
      • RISCV_cpu.cc (エミュレータ本体)
      • load_assembler.cc (アセンブラ・コードの作成・ロード)
      • instruction_encdec.cc (機械語を32bitにパックするためのコード)
      • bit_tools.cc (ビットの切り出し等用のユーティリティ)
      • load_assembler_test.cc (load_assembler.cc用のテスト)
    • 拡張のための変更

      • 32bit命令のために配列をshortからuint32_tに変更
      • メモリ配列を大きくして、romとramを共通化
      • レジスタを32個に拡張
      • その他いろいろ(Makefileの作成など)
  • RISC-V 命令対応への変更

最初に使用する命令の選定

まずは「動かしてわかるCPUの作り方10講」で使われている1から10まで足し合わせるアセンブリコードを再現したい。

  • 「動かしてわかるCPUの作り方10講」のCPUの命令と、RISCVの命令(RV32I)の対応
CPU10講CPU CPU10講動作 対応するRISC-V命令
mov RegA <- RegB add rd, rs1, zeroで代用。(zeroはゼロレジスタ)
add RegA <- RegA + RegB add rd, rs1, rs2 (x[rd] <- x[rs1] + x[rs2])
ldl RegA(Low) <- Data (8bit) addi rd, zero, immediate (x[rd] <- zero + immeidate)で代用
ldh RegA(High) <- Data (8bit) 今回はスキップ。必要ならlui (Load Upper Immediateを実装)
cmp flag <- (RegA == RegB) 今回はスキップ(次のbeqで代用)
je pc <- Addr if flag==1 beq rs1, rs2, offset (pc += offset if rs1 == rs2)で代用
jmp pc <- Addr jal x0, offset (pc += offset)
ld RegA <- M[Addr] 今回はスキップ(レジスタを使うので)
st M[Addr] <- RegA 今回はスキップ(レジスタを使うので)
hlt 停止 ret (実体はjalr x0, x1, 0)で代用。x1は戻りアドレス

とりあえず、このエミュレータはret命令で停止するものとします。

それぞれの命令のラベルとタイプ、ファンクションのリストです。

命令 タイプ ラベル funct7 or 6 funct3
add R 0110011 0000000 000
addi I 0010011 NA 000
sub R 0110011 0100000 000
and R 0110011 0000000 111
or R 0110011 0000000 110
slli I 0010011 000000 001
srli I 0010011 000000 101
srai I 0010011 010000 101
beq B 1100011 NA 000
jal J 1101111 NA NA
jalr I 1100111 NA NA

funct6という名称は実際にはありませんが、immediateの上位6ビットが固定になっている部分を便宜上このように表記しました。

アセンブリコードの変更

まずは「動かしてわかるCPUの作り方10講」のサンプルコードを変換してみます。

# t0: カウンタ(初期値0)
addi t0, zero, 0
# t1; 和(初期値0)
addi t1, zero, 0
# t2: 上限(10)
addi t2, zero, 10
# ループ先頭
# t0 = t0 + 1
addi t0, t0, 1
# t1 = t1 + t0
add t1, t1, t0
# t0 = t2 なら+ 8ジャンプ
beq t0, t2, 8
# -16 (4命令バック)ジャンプ
jal x0, -16
# A0 (返り値を保存するレジスタに結果を移動)
add a0, t1, x0
# リターン(a0は戻り値)
jalr x0, ra, 0

実際に使われているのは、addi, add, beq, jal, jalだけなのでこれらの命令を優先して実装します。

命令のエンコーダー

まずはCPUに渡すバイナリを作る必要があります。

RISC-V原典を眺めながら、各命令のopcode、funct3、funct7、レジスタ番号、即値を32bitに埋め込んでいくコードを書きます。

RISC-V(RV32I)の命令は次の6つのタイプにわかれます。

  • R-type
  • I-type
  • S-type
  • B-type
  • U-type
  • J-type

それぞれフォーマットが異なり、タイプごとに引数の種類も異なります。

そこで必要な命令毎に

  1. タイプを判断しフォーマットを選択
  2. opcodeとfunct3、funct7を32bit内に埋め込む
  3. 必要な引数(rd, rs1, rs2, immediate )を判断
  4. 引数の情報を32bit内の指定された位置に埋め込む

というコードを書きます。

RISC-Vでは、何故か一部の即値のビット位置が入れ替わっていて、これを入れ替えるのが大変でした。C/C++でのビット演算に苦労していると、こんなアドバイスをいただきました。

喜んで使わせていただきます。これでいちいちマスクする苦労から開放されました。

ビットフィールドを使って、それぞれの要素(レジスタ番号、即値、opcode等)をクラスメンバーとして定義します。

例えばこんな感じです。

class bitfield {
  public:
    virtual uint32_t value() { return 0; };
    virtual void set_value(uint32_t value) {};

    uint8_t opcode: 7;
    uint8_t rd: 5;
    uint8_t rs2: 5;
    uint8_t rs1: 5;
    uint8_t funct3: 3;
};

class r_type : public bitfield {
  public:
    uint8_t funct7: 7;
    uint32_t value();
    void set_value(uint32_t value);
};

最初はunionとstructを使って、こんな感じで書いていたのですが、

union r_type {
    uint32_t value;
    struct {
        uint8_t funct7 : 7;
        uint8_t rs2 : 5;
        uint8_t rs1 : 5;
        uint8_t funct3 : 3;
        uint8_t rd : 5;
        uint8_t opcode : 7;
    };
};

パッキングやパディングが不安なのでとりあえずクラスにしています。 アトリビュートを使ってうまくいくようならこの方法にもどそうと思います。

機械語を生成する部分は、CPU10講にならって決め打ちのバイナリを生成するコードを書きました。

void load_assembler(uint32_t *mem) {
  mem[0] = asm_addi(T0, ZERO, 0);
  mem[1] = asm_addi(T1, ZERO, 0);
  mem[2] = asm_addi(T2, ZERO, 10);
  mem[3] = asm_addi(T0, T0, 1);
  mem[4] = asm_add(T1, T1, T0);
  mem[5] = asm_beq(T0, T2, 8);
  mem[6] = asm_jal(ZERO, -16);
  mem[7] = asm_add(A0, T1, ZERO);
  mem[8] = asm_jalr(ZERO, RA, 0);
}

これで実行対象機械語コードが用意できました。次はいよいよCPUエミュレータ側のコードです。

命令のデコード

命令のデコードはCPU10講の内容を参考に、RISC-V用に次の用なものにしました。

  1. 下7bitのオペコード(opcode)を読み込む
  2. 必要があれば、14:12ビットのfunct3を読み込む
  3. 必要があれば、31:25ビットのfunct7を読み込む(今回は未実装)
  4. opcode、funct3とfunct7から命令の種類を判定
  5. 必要に応じてrd(ディスティネーションレジスタ)、rs1(対象レジスタ1)、rs2(対象レジスタ2)をデコード
  6. 必要に応じて即値(12bit, 13bit, 21bit, 24bitの4種類がある)をデコード

さきほどシャッフルした即値のビットを戻す作業が面倒ですが、それ以外は単に対応する位置にあるビットを切り出すだけです。

命令の実行

今回選んだ命令に関しては、命令の実行はCPU10講と似た形式です。大きな違いは

  • 命令種類が減った
  • ゼロ番目のレジスタはゼロレジスタ
  • ジャンプが相対ジャンプになった
  • ret命令で動作終了

程度です

その他

実は今回一番複雑だったのは機械語を作成する部分、次がデコーダーです。複雑なので最初からテストを(割と)ちゃんと書きました。 各アセンブラの命令毎に、2〜3の命令例が正しいバイナリになっているか、またバイナリが正しくデコードされているのかを確認しました。コードはload_assembler_test.ccにあります。実は今回一番時間がかかっている部分かもしれません。

ファイルが増えたのでMakefileを作成しておきました。

実行結果

では実行してみます。

$ ./RISCV_Emulator 
Assembler set.
Execution start
   PC    Binary     T0     T1     T2     T3     A0
    0  00000293      0      0      0      0      0
    4  00000313      0      0      0      0      0
    8  00a00393      0      0      0      0      0
   12  00128293      0      0     10      0      0
   16  00530333      1      0     10      0      0
   20  00728463      1      1     10      0      0
   24  ff1ff06f      1      1     10      0      0
    8  00a00393      1      1     10      0      0
   12  00128293      1      1     10      0      0
   16  00530333      2      1     10      0      0
   20  00728463      2      3     10      0      0
   24  ff1ff06f      2      3     10      0      0
    8  00a00393      2      3     10      0      0
   12  00128293      2      3     10      0      0
   16  00530333      3      3     10      0      0
   20  00728463      3      6     10      0      0
   24  ff1ff06f      3      6     10      0      0
    8  00a00393      3      6     10      0      0
   12  00128293      3      6     10      0      0
   16  00530333      4      6     10      0      0
   20  00728463      4     10     10      0      0
   24  ff1ff06f      4     10     10      0      0
    8  00a00393      4     10     10      0      0
   12  00128293      4     10     10      0      0
   16  00530333      5     10     10      0      0
   20  00728463      5     15     10      0      0
   24  ff1ff06f      5     15     10      0      0
    8  00a00393      5     15     10      0      0
   12  00128293      5     15     10      0      0
   16  00530333      6     15     10      0      0
   20  00728463      6     21     10      0      0
   24  ff1ff06f      6     21     10      0      0
    8  00a00393      6     21     10      0      0
   12  00128293      6     21     10      0      0
   16  00530333      7     21     10      0      0
   20  00728463      7     28     10      0      0
   24  ff1ff06f      7     28     10      0      0
    8  00a00393      7     28     10      0      0
   12  00128293      7     28     10      0      0
   16  00530333      8     28     10      0      0
   20  00728463      8     36     10      0      0
   24  ff1ff06f      8     36     10      0      0
    8  00a00393      8     36     10      0      0
   12  00128293      8     36     10      0      0
   16  00530333      9     36     10      0      0
   20  00728463      9     45     10      0      0
   24  ff1ff06f      9     45     10      0      0
    8  00a00393      9     45     10      0      0
   12  00128293      9     45     10      0      0
   16  00530333     10     45     10      0      0
   20  00728463     10     55     10      0      0
   28  00030533     10     55     10      0      0
   32  00008067     10     55     10      0     55
Return value: 55

実行できました!

今後の課題

次のステップですが、

  • テスト用コードのリファクタリング(今は手作業なので命令が増えると大変)
  • ロードストア等、対応する命令を増やす
  • メモリ領域を増やす
  • 外部プログラムファイルの読み込み(できればelf形式で)

というあたりを考えています。

最終目標はFPGAで動作するCPUをRTLで書くことなのですが、これは年単位の目標です。