VerilogでRISC-Vを実装してみた

この記事は[NAIST Advent Calendar 2022](https://qiita.com/advent-calendar/2022/naist) 6日目の記事です。

# 事始め
かつて学部時代に作った[Ayumu](https://qiita.com/Prokuma/items/0b2ce47378c745ee263e)という8ビット自作CPUについて投稿したことがありますが、最後に

> 次はRISC-V命令セットの32ビットCPUを設計しようと思っています。上手くいくかどうかわかりませんが…

と書いてあったのがことの始まり。つまりAyumuの続編となります。

これは大学院の授業の課題でもあったので、製作の紆余曲折をこれから述べていきます。

## 宣言はしたが結局実装を始めない怠け者、やっと動き出す
AyumuはC言語のコンパイラが存在せず、しかもアセンブラすら存在しなかったので機械語を直打ちするしかありませんでしたが、これでは実用性がなく、かといってコンパイラを実装するのはCPU自作以上に大変なことであったため、次作はRISC-Vに対応していと思いました。
しかし、怠け者の自分は後伸ばしにしてことを始めず、そのまま学部を卒業し、前の大学を去っていきました。
そしてやってきた新たな環境で、以前とは比べられないほど豊富な機材を使ってRISC-Vの自作CPUを作ってみたいと思いました。
そのことを、研究室の皆さんに宣言したところ、先生からは「じゃそれPBLの課題にすれば？」とのコメントをいただき、RISC-Vの実装物語が始まりました。

## 初心者には多少厳しい要求条件
先生は次のような条件を挙げていました。
- キャッシュメモリと入れ替えアルゴリズムを実装すること
- プログラムはメモリにロードしてVerilogを変えなくてもプログラムを変更できるようにすること
- RISC-V命令セットをしっかり実装すること

まぁ、最後は当たり前なことですが上二つが結構厳しかったんですよね...あれ実装たいへんなんだよな...と思いながら作り始めました。実際最も時間かけたものキャッシュメモリでしたし。

# 仕様決め
## 名前は「Kasumi」
前作が「Ayumu」だったので、「ラブライブ！虹ヶ咲学園スクールアイドル同好会」のアニメの登場順に倣って二番目に登場した「中須かすみ」から「Kasumi」と決定。

## 実装する命令セット
これはRISC-Vで最もシンプルな命令セットであるRV32I、つまり浮動小数点や乗除を実装していない命令セットにしました。
RV32Eが最小でもあると思いますが、これはレジスタ数が少ないのはもちろんコンパイラの都合もあると思うので実装した人が多いRV32Iにすることに...

## パイプライン処理
Kasumiは教科書的な五段階パイプラインを実装しています。分岐予測には大した工夫はしていませんし、分岐が成立したらパイプラインバブルを発生させる初歩的なCPUです。
しかし、前の命令と後ろの命令で依存関係はあるけど前の命令で後ろの命令で使うレジスタの値が分かった場合直接その値を入れる処理をしているなど、それなりにこだわっている部分もあります。

## キャッシュメモリ
アドレスでキャッシュをマッピングする最も単純なやり方で実装しています。最近のCPUはもちろんちゃんと考えてあってこういう単純なやり方でキャッシュは実装していませんが、キャッシュメモリを実装するのは初めてなのでここは簡単なやり方にしました。
キャッシュラインを跨いでいる場合の処理を実装しているなど、RISCではあんまりいらないような機構も実装しています（これが悲劇に繋がりますが...)

## シミュレーション環境
Icarus VerilogとGTKWaveを使用しました。

## テストボード
研究室にあったZedboardで実装することにしました。しかし、Zynqは触ったこともないし色々コケまくった結果動かせてはいませんが...

## テストプログラム
RISC-Vは[riscv-tests](https://github.com/riscv-software-src/riscv-tests)というテストプログラムを実装したものがあります。各種命令が正しく動くのかを確認できます。
実装する命令セットの命令が正しく実装されているかの検証ができるので、実際コンパイルして動かしてみながら検証を行いました。

# 構造
## Kasumiの五段階パイプライン
KasumiのCoreには次のようなファイルがあります。
```shell
- core.v # コアのまとめ
- csr.v # CSRの宣言
- decode.v # 命令のデコード
- execute.v # 演算部 
- fetch.v # 命令フェッチ
- memory_access.v # メモリからの読み出し・メモリへの書き込み
- reg_file.v # レジスタファイル
- write_back.v # レジスタへの書き込み
```

パイプラインはfetch→decode→execute→memory_access→write_backの五段階に構成され、パイプライン制御はcoreの部分で行っています（つまりパイプラインレジスタはcore.vに実装されています）。
CSRは仕組み的にレジスタと同じですが、書き込む、読み出すタイミングで考えるとメモリに近いのでmemory_access.vで処理しています。

## キャッシュメモリ
キャッシュメモリはダイレクトマッピング方式で設計され、アドレスごとに使われるブロックは固定されています。DRAMとのクロックが異なるのは想定済みで、そのためのFIFOも自前で実装しています。
Xilinxボード上で実装するのでAXI仕様に固定してもよかったのですが、Alteraボード上でも動くような汎用的なものがほしかったのでインターフェースは独自仕様にしています。AXIとの接続は専用のIPを実装して接続するようになっています。

## トップモジュール「kasumi」のインターフェース
Verilogファイルの一部を載せてもいいですが、わかりやすいようにVivadoの切り取りを載せます。
![2022-11-22-152607.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/487309/6e4a0e2e-899c-97dd-221f-1b1b317d1f00.png)

- reset/reset_sys: CPUリセット関係の信号
- clk: CPUクロック
- dram_clk: 接続先のDRAMのクロック
- is_write_prog(data)_line: キャッシュに書き込むための信号
- is_write_t_main: DRAMに書き込むための信号
- read_prog(data)_data: 読み込むキャッシュライン
- read_prog(data)_addr: 読み込むキャッシュラインのアドレスの先頭7桁
- is_req_f_prog(data): キャッシュミスした時の信号・データリクエスト
- req_addr_f_prog(data): リクエストするデータのメモリアドレス
- fifo_empty: DRAMに書き込むデータのFIFOが空になったら1
- write_back_data: DRAMに書き込むデータ
- write_back_addr: DRAMに書き込むデータのアドレス

# シミュレーション・検証
## GtkWave
kasumi_test.vおよびkasumi_tesst_2.vを作成し、test_ramを作成してriscv-testsで生成したバイナリファイルを読み込ませIcarus Verilog上でテストを行いました。特権モードでないRV32I命令のテストは全て通りました。
![2022-08-08_194311.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/487309/f533265c-4e21-f822-a167-98d030a7de1e.png)

## Vivado
デバイスの表示
![2022-08-03_113613.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/487309/cc8adc3c-6347-96b3-884c-d76710b9b6f7.png)

合成後の使用量（Zedboard/XC7Z020）

ブロック | 使用量
-------|------
LUT | 69%
LUTRAM | 5%
FF | 17%
BRAM | 92%
BUFG | 3%

# 苦労した点
## そもそもBRAM(SRAM)の仕様を理解していなかった
0クロックですぐSRAMからデータが取り出せると思ったらそうではなかったのでそこら辺の実装の変更が大変でした。これを専門的にやってきた方なら笑うようなことですが、自分は学部時代までHDLとは無縁の学科に在学していたので、BRAMの仕様を実際使おうとしない限りわかるわけがなく、「コンパイラが良さげに合成してくれるだろう」と思い込んでいて調べるという発想もありませんでした。
VerilogのようなHDLでハードウェア設計を行うときは言語仕様以外にも物理的なことを考えながら設計しないとならないことを再度確認する機会でした。

## 論理的には正しいはずなのに...
そもそもソフトウェア界隈の住人だった自分は「論理的に正しければ動く」という謎の信頼（？）があったので、物理的なことは全く考えずコードをダラダラ書いた結果遅れまくって結局Implementationを通りませんでした。
原因はおそらく複雑すぎるキャッシュメモリのキャッシュラインまたはキャッシュブロックをまたがっている時の処理。しかしこれを全部書き直すのも嫌だったのでクロックを下げて何とか（100MHz→45MHz程度）。

## AXIとPS/PLの連携プログラム書けなかった
そもそも資料が少なく、SoC搭載のFPGAを触るのは今回が初めてだったので何の経験もない状態だったので、結局連携用のプログラムローダを作成するのはできませんでした。これが動かせてない原因ですね。
AXIとKasumiを連携する橋渡し的なIPは自前で実装しましたが、結局連携プログラムの作成ができなかったので試しようがない...

# 最後に
## 感想
自作CPU界隈でよくあるようなCPUを「おもちゃ」と主張し、初心者に厳しい条件を要求した指導教員の先生のおかげで実用的なものに近い（実用的とは言ってない）CPUを設計してみることができました。
これまで使ったことないFPGAのBRAMも使ってみたし、物理的な特性を理解していないと大変な目に遭うことを学べました。
もちろんFPGAで実際動かしてみたわけではないので、動くがどうかは正直保障はできませんが、次は浮動小数点命令を実装したそこそこ実用的なCPUを作ってみたいのでKasumiプロジェクトは未検証ですが終わりにすることにしました。
ソースコードの利用はライセンスに従えば自由ですが、メンテナンスはできないのでご了承ください。

## 参考資料
RISC-V命令セット: https://riscv.org/technical/specifications/
コンピュータアーキテクチャ： https://www.ohmsha.co.jp/book/9784274212536/
FPGA関係: https://msyksphinz.hatenablog.com/

## ソースコード
Github: https://github.com/Prokuma/Kasumi