RISC-VのCPUを少しずつ作る順序を考える
この記事は「自作CPU Advent Calendar 2024」の21日目の記事です。
コンピュータのレイヤを下ると、そこにはCPUがあります。CPUはどんな機械なのか?何をしているのか?なぜ速いのか?これを理解する遠回りで直線的な方法は「CPUを作る」ことです。作ってしまえばお手のもの、安心してさらにレイヤを下れます。
はじめに
RISC-Vは命令セットアーキテクチャの一種であり、仕様が公開されていて、自由に無料で改造、実装することができます。
RISC-VのCPUは、足し算やビット演算、メモリにアクセスする命令などの単純な命令をまとめた「基本整数命令セット」を必ず実装しています。例えばレジスタとメモリ空間の幅が32ビットな基本整数命令セットはRV32I、64ビットだとRV64Iとして定義されています。
乗算や例外など、基本整数命令セットに含まれない機能や命令は「拡張」によって追加で実装します。例えば乗算除算剰余を求める命令はM拡張として定義されています。M拡張が実装されたRV32IのCPUはRV32IMと呼びます。
それではRISC-VのCPUを作りたいとき、どのように実装していけばいいでしょうか?
RV32IMを作るとしたら、せっかく機能が拡張として分離されているので、最初にRV32Iを実装して、次にM拡張を実装するとテストやデバッグがしやすくて便利です。
しかし、RV32IMなら方針を立てるのは簡単ですが、OSを動かせるくらいの拡張を実装するとなると話は変わります。例えば例外はどのタイミングで実装すればいいでしょうか?例外は多くの拡張にまたがる機能です。
CPUの実装に限らず、複雑な機能は実装するタイミングによって、実装、テスト、デバッグの容易さが変わります。良い順序で実装できた場合、驚くほど簡単に実装できることがあります。
最近、RISC-VのCPUを実装する同人誌の構成を考えたときに、どういった順序で実装すると良いかを考えました。「良い」順序とは、CPUをステップバイステップで実装するうえで次を満たすような順序です。
- コードの差分が少ない
- ある機能を実装するとき、それ以外の機能を中途半端に実装する必要がない
- ある機能を説明するとき、それ以外の機能については後述する必要がない
全体の構成を大きく分けると、簡単な拡張の実装、高速化、その他の拡張の実装というステップを踏みます。
OSを実行できるくらいまで実装する
まずはOSを実行できるくらいの機能を実装します。
目標とするアーキテクチャはRV64IMA_Zicntr_Zicsr_Zifenceiです。
高速化はほとんど行わず、機能を足すことだけを考えます。 唯一、パイプライン化だけ行います。
RV32Iを実装する
RV64IはRV32Iのスーパーセットのようなものなので、先にRV32Iを実装するのが良いです。 このとき、後でXLENを変えることを考慮したコードを書くと差分が減ります。
ロード命令とストア命令は、前に発行されたロード、ストアが完了するのを待たせます。 こうすることで、A拡張、FENCE命令、Zifencei拡張を楽に実装できます。 また、フェンスを実装するまでメモリモデルについて考える必要がなくなります。
Zicsr拡張を実装する
riscv-testsなどのテストスイートを動かすために、CSRに読み書きする命令とECALL命令、MRET命令を実装します。 CSRはECALL例外に必要なmepc、mcause、mtvecだけを実装します。
実装したらテストを動かします。
RV64Iを実装する
RV32I_Zicsrが動くことを確認したらRV64Iを実装します。 命令が数種類追加されるのと、シフト命令の挙動が変わります。
パイプライン化する
同時に1つの命令しか実行できないままに機能を増やしていくと高速化するときの作業が複雑になるため、この状態でパイプライン化します。
Zifencei拡張を実装する
パイプライン化した影響でFENCE.I命令がnopのままでは正しく動かないことになるので、 パイプラインをフラッシュする形で実装します。
FENCE命令の説明と実装(何も実装しない)はここでやってもいいかも。
M拡張を実装する
ここからはeembc/coremarkを動かすことを小目標に実装を進めます。 Coremarkを動かすには、M拡張とMMIOが必要です。
MMIO、Zicntr拡張を実装する
MMIOはprintfを実装するために使います。 UART TXを実装したらUART RXを実装します。 割り込みは作らなくていいです。
これでCoremarkを動かせます。
例外を実装する
A拡張を実装する前に例外を実装します。
- Instruction address misaligned
- Illegal instruction
- Load address misaligned
- Store/AMO address misaligned
reservedな命令のデコードの動作はUNSPECIFIEDですが、Illegal Instruction Exceptionが発生するようにします。
A拡張を実装する
今のところシングルコアかつTSOな実装なので、A拡張の実装は非常に単純になります。 aq/rlは無視します。
C拡張を実装する
C拡張の命令には同じ意味を持つ32ビットの命令が存在するため、16ビットの命令を32ビットに変換するユニットを挟むことによって実装します。 Zcb拡張も実装します。
IALIGNが16になるのでInstruction address misaligned例外が発生しなくなります。 せっかく実装したのにもったいない感はありますが、misa.Cを変更可能にしたら発生するようになるはずなので問題なし。
CLINTを実装する
タイマ割り込みとソフトウェア割り込みを実装します。 まだM-modeしかないので単純です。
S-mode、U-modeを実装する
S-modeのCSRを地道に実装します。 すぐに使わない機能(パフォーマンスカウンタとか)は実装しないでいいです。 例外と割り込みの動作の実装はちゃんとやります。
satpの実装は重いので、最後にやりましょう。 TLBなしのSv32を実現するモジュールを作ったら、パラメータを変えるだけでSv39、Sv48、Sv57を実装できるはずです。 例外も実装します。
PLICを実装する
割り込みを実装できているはずなので、PLICを実装します。 必要なデバイスとのインタフェースはここで実装します。
OSを起動しながら、未実装の機能を作る
ここまでくれば、OSをある程度まで実行できるはずです。 未実装の機能があれば、起動しながら確認します。
高速化する
簡単な機能を実装したら、ようやく高速化します。
ここからはバグらせるととんでもない沼にハマるため、 機能の有効無効を切り替えられるようにしながら実装します。 問題の切り分けができるようにするのが大事です。
インオーダー実行のまま高速化したあと、アウトオブオーダー実行によって高速化します。
分岐予測
実装方式によりますが、最も簡単に実装できるのは分岐予測です。 コア側で予測が正しいかを判定させるだけで分岐予測を実装可能になります。
予測の成功率を観測すると面白いです。
TLBを実装する
ページングが有効なとき、TLBがないので命令フェッチに数サイクルかかっているはずです。 TLBを実装すると、ページングが無効のときとほぼ同じサイクル数でフェッチできるようになります。
最初に命令フェッチ用のTLBを作り、次にロードストア用のTLBを作ります。 2つの違いは基本的にDビットだけなので、Dを考慮するかどうかをパラメータ化するのが良いです。
ページのサイズ別のTLBを作ったうえで、 直近のメモリアクセスと同じページにアクセスする場合を高速化できると非常にTLBが効きます。
キャッシュを実装する
キャッシュは慎重に実装します。 TLB、命令キャッシュ、データキャッシュのコヒーレンシを考える必要があるので大変。
実装方式は、ダイレクトマップ方式、セットアソシアティブ方式の順に実装します。 キャッシュラインの幅はパラメータ化したほうが良いです。
筆者はキャッシュのバグで時間を溶かしたので、ランダムなデータを読み書きするプログラムを書いてテストしました。 オススメです。 キャッシュはSystemVerilogで書いてもChiselで書いても苦行でした。たぶん何で書いても苦行。
ライトバックかライトスルーのどちらにするか、パイプライン化するか、アウトオブオーダーに結果を返すかなど、大変に深堀りすることができます。 やりたいところまでやりましょう。
アウトオブオーダー実行を実装する
分岐予測とキャッシュが効くようになったらOoOを実装します。 逆に言うと、分岐予測とキャッシュが効いていないならOoOを実装する意味がほとんどないです。
筆者的にはキャッシュの実装が面倒すぎたので、OoOの実装のほうが簡単に感じました。
メモリアクセスの並び替えを実装する
メモリにアクセスする命令の内、並び替えて良いものを並び替えてしまいます。 筆者は作ったことがないです。
Ztsoを実装するならaq/rlを上書きしてしまえばいいはず。
マルチコア化
これも作ったことがありません。 キャッシュと割り込みの制御とかが変わるはずです。
その他の機能を実装する
RVA23U64を実装するとして、残りをどう実装していくかを考えてみました。 ここからは順序がないです。
まず、F、Zfhmin、D、B、V、Zicond、Zc*は独立して実装できそうです。 ZihpmとZktは複数の拡張にまたがりそうなので早めに実装しておいたほうが良いかも。
キャッシュの制御系とZawrsは実装が面倒そうな雰囲気があります。 キャッシュを作る時点で考慮した実装をしても良いかもしれないです。
RVA23S64にはH拡張を実装する必要があるようで大変そうでした。 それ以外はちまちました拡張なので簡単そう。
おわり
実装するだけなら時間をかけるだけで出来そうですが、解説を書きつつ実装するとなるとまだ考えることがありそうです。
RVA23S64を実装したボードが出るのはいつになるんですかね。
RISC-Vの検証フレームワークRISCOFでCPUを検証する
この記事はHardware Description Language Advent Calendar 2024の16日目の記事です
はじめに
RISC-VのCPUを記述しはじめて丁度2年くらいになりました。 簡単にRISC-Vのコアを検証するには、よくriscv-testsが用いられます。 最近、RISCOFというフレームワークを使って自作のRISC-Vコアを検証してみたので、その方法をまとめます。
RISCOF
RISCOF(The RISC-V Compatibility Framework)はRISC-Vのコアの動作をゴールデンモデルと比較して検証するフレームワークです。 ゴールデンモデルとは、検証において正しく設計されているとするモデルのことです。
検証対象のコアの仕様(ISA、実装しているCSR)をYAMLで記述すると、 テストの生成(選択)、テストを実行して比較、結果の報告を自動で行ってくれます。 riscv-testsと比べると、実行するテストの選択が不要、テストの量が多い、リッチな形式(HTML)と内容で結果が報告されるのが利点です。 ただし、テストに対応するのに少し手間がかかります。一回環境を構築してしまえば後は簡単です。
今回はテストプログラムにriscv-arch-testというテストセットを利用します。
カバレッジモードというモードで実行するとカバレッジを得られて、 カバレッジとCGFという設定をRISC-V CTGに渡すとカバレッジを考慮してテストを生成してくれるみたいです。 これで生成されたテストを使ってRISCOFを実行すると検証の効果が高くなりそうです。 また今度試します。
RISCOFのドキュメントはここにあります。 riscof.readthedocs.io
RISCOFを実行する
本記事は、環境構築、サンプルの実行、自作RISC-Vコアの検証、結果の確認、再度実行という流れで記述されています。 それでは始めましょう。
環境構築
RISCOFのインストール
まず、RISCOFをインストールします。 RISCOFはPython3で実行されるので、Pythonをインストールしてください。
RISCOFはpipでインストールすることができます。
pip3 install riscof
コンパイル手段の用意
RISCOFによって選択されたテストをコンパイルする手段が必要です。 私はRISC-V GNU Compiler Toolchainを使っています。 github.com
ゴールデンモデルの用意
ゴールデンモデルとしてRISCV Sail Modelを利用します。 sail-riscvは、SailというISAのDSLで書かれたRISC-Vのモデルです。 github.com
まず、Sailをインストールします。 SailはGitHubのリリースページから入手することができます。 RISCOFのドキュメントに従うと古いSailが入って、sail-riscvをmakeできないので気を付けてください。 github.com
sail-riscvをインストールします。
git clone https://github.com/riscv/sail-riscv.git cd sail-riscv make ARCH=RV32 make # RV32のsail-riscvのインストール ARCH=RV64 make # RV64のsail-riscvのインストール
成果物はc_emulatorディレクトリのriscv_sim_XLENです。
パスを通しておいてください。
Spikeを検証する
Spike(riscv-isa-sim)は、C++で記述されたRISC-Vの公式の命令セットシミュレータです。 自作のコアを検証する前にSpikeでお試しします。
Spikeのインストール
まず、Spikeをインストールします。
READMEに従ってビルドしてください。ビルドしたらパスを通し、spikeで利用できることを確かめてください。
github.com
テンプレートの生成
riscof setupコマンドでRISCOFのテンプレートを作成します。
テスト対象としてspikeを指定します。
$ mkdir riscof $ cd riscof $ riscof setup --dutname=spike INFO | ****** RISCOF: RISC-V Architectural Test Framework 1.25.3 ******* INFO | using riscv_isac version : 0.18.0 INFO | using riscv_config version : 3.18.3 INFO | Setting up sample plugin requirements [Old files will be overwritten] INFO | Creating sample Plugin directory for [DUT]: spike INFO | Creating sample Plugin directory for [REF]: sail_cSim INFO | Creating Sample Config File INFO | **NOTE**: Please update the paths of the reference and plugins in the config.ini file
ディレクトリはこんな感じになります。
$ tree
.
├── config.ini
├── sail_cSim
│ ├── __init__.py
│ ├── env
│ │ ├── link.ld
│ │ └── model_test.h
│ └── riscof_sail_cSim.py
└── spike
├── env
│ ├── link.ld
│ └── model_test.h
├── riscof_spike.py
├── spike_isa.yaml
└── spike_platform.yaml
RISCOFのテスト対象、ゴールデンモデル(Reference)はプラグインで設定されます。
どのプラグインを使用してテストするかなどの設定はconfig.iniに記述します。
下記のようになっていることを確認してください。
[RISCOF] ReferencePlugin=sail_cSim ReferencePluginPath=/home/kanataso/Documents/bluecore/core/riscof/sail_cSim DUTPlugin=spike DUTPluginPath=/home/kanataso/Documents/bluecore/core/riscof/spike [spike] pluginpath=/home/kanataso/Documents/bluecore/core/riscof/spike ispec=/home/kanataso/Documents/bluecore/core/riscof/spike/spike_isa.yaml pspec=/home/kanataso/Documents/bluecore/core/riscof/spike/spike_platform.yaml target_run=1 [sail_cSim] pluginpath=/home/kanataso/Documents/bluecore/core/riscof/sail_cSim
ReferencePluginでゴールデンモデル、DUTPluginでテスト対象を指定します。
それぞれのプラグインの設定は[プラグイン名]で指定しています。
テストを実行する
とりあえずそのまま動かしてみましょう。
riscv-arch-testをriscofを使ってクローンします。
riscof arch-test --clone
設定ファイルをvalidateします。 何も変更していないので成功するはずです。
riscof validateyaml --config=config.ini
riscv-arch-testからテストを自動で選択します。
テスト対象(spike)のプラグインの設定を読み込んで自動で選択してくれます。
選択されたテストはriscof_work/test_list.yamlに記述されています。
riscof testlist --config=config.ini --suite=riscv-arch-test/riscv-test-suite/ --env=riscv-arch-test/riscv-test-suite/env
テストを実行します。
riscof run --config=config.ini --suite=riscv-arch-test/riscv-test-suite/ --env=riscv-arch-test/riscv-test-suite/env
テストが終了すると、riscof_work/report.htmlに結果を報告するHTMLが生成されます。

92個のテストに成功して、0個のテストに失敗しました。 これでSpikeの検証は終わりです。
自作RISC-Vコアの検証
ここからが本題です。自作のRISC-VのコアをRISCOFで検証します。
検証対象はVerylで記述しているbluecoreというCPUです。 GitHubで公開しています。 github.com
CPUの実装手順についてはVerylで作るCPUでまとめています。
2024/12/13時点でのISAはRV64IMZicsrです。
CSRはmtvec、mcause、mepcしか実装していません。
プラグインを作成する
テンプレートの生成
新しくテンプレートを生成します。
$ mkdir riscof $ cd riscof $ riscof setup --refname=sail_cSim --dutname=bluecore
isa、platformの設定
bluecoreプラグインのディレクトリ(bluecore)が生成されています。構造はこんな感じです。
$ tree . ├── bluecore_isa.yaml ├── bluecore_platform.yaml ├── env │ ├── link.ld │ └── model_test.h └── riscof_bluecore.py
bluecore_isa.yamlでISA、CSRの実装状況、
bluecore_platform.yamlでCPU特有の設定を記述します。
それぞれの仕様はRISCV-CONFIGのドキュメントに書かれています。 riscv-config.readthedocs.io
bluecore_isa.yamlを次のように編集しました。
ISAをRV64IMZicsr、physical_addr_szを24に変更して、CSRの設定を削除しています。
physical_addr_szは、実装の物理アドレス空間の幅です。
hart_ids: [0] hart0: ISA: RV64IMZicsr physical_addr_sz: 24 User_Spec_Version: '2.3' supported_xlen: [64]
CPUの実装のアドレスの幅はある程度広い必要があります。 メモリ空間が狭い場合、ジャンプ命令のテストに失敗します。
bluecore_platform.yamlを次のように編集しました。
PLICは実装していないので、mtimeとmtimecmpを削除しました。
nmi: label: nmi_vector reset: label: reset_vector
yamlを検証します。
riscof validateyaml --config=config.ini
signatureの出力
RISCOFのテストは、
テストによってsignature(アドレスがbegin_signatureからend_signatureの範囲のデータ)を変更して、
ゴールデンモデルのsignatureと比較することによって成否を判定します。
成否の判定のためにはsignatureを出力する必要があるため、
MMIOで任意のデータを出力できるようにします。
MMIOのアドレスは0x00000000_FFFFFF88にしました。
always_ff { let SIGNATURE_ADDR: Addr = 64'h00000000_FFFFFF88 as Addr; if d_membus_mem.valid && d_membus_mem.ready && d_membus_mem.wen == 1 && d_membus_mem.addr == SIGNATURE_ADDR { $display("signature: %h", d_membus.wdata[31:0]); } }
signatureを出力するプログラムをenv/model_test.hに記述します。
RVMODEL_HALTは、テストの最後に実行されるプログラムです。
//RV_COMPLIANCE_HALT
#define RVMODEL_HALT \
la a0, begin_signature; \
la a1, end_signature; \
li a2, 0xFFFFFF88; \
copy_loop: \
beq a0, a1, copy_loop_end; \
lw t0, 0(a0); \
sw t0, 0(a2); \
addi a0, a0, 4; \
j copy_loop; \
copy_loop_end: \
li x1, 1; \
write_tohost: \
sw x1, tohost, t5; \
j write_tohost;
Twitterで教えてもらったissueにあるプログラムを少し変更して利用しています。
signature というのを出力するとそれを Spike などのシミュレータが出力する signature と比較して検証してくれます。
— shimooka (@shmknrk) December 9, 2024
signature はメモリ上の begin_signature と end_signature のセクションで囲まれた領域の値のことで、この issue のような方法で出力させるとよいです。https://t.co/LAfcM496co
link.ldの変更
bluecoreは0x00000000_00000000から実行を開始します。
また、RISCOFは.tohostに値が書き込まれたときにテストを終了します。
これに対応するために、env/link.ldを変更します。
OUTPUT_ARCH( "riscv" )
ENTRY(rvtest_entry_point)
SECTIONS
{
. = 0x00000000;
.text.init : { *(.text.init) }
. = ALIGN(0x1000);
.text : { *(.text) }
. = ALIGN(0x1000);
.data : { *(.data) }
.data.string : { *(.data.string)}
.bss : { *(.bss) }
. = ALIGN(0x10000000);
.tohost (NOLOAD) : { *(.tohost) }
_end = .;
}
.tohostはMMIOなのでNOLOADに設定して、位置をbssの後ろにしています。
CPU側の.tohostのMMIOのアドレスも0x10000000に設定しておきます。
プラグインの記述
コアをどう動かすか、どう検証するかはriscof_bluecore.pyで記述します。
変更する必要があるのはrunTests関数です。
もともと記述されているMakefileを使った処理をコメントアウトして、
下の方でコメントアウトされているコマンドを利用した処理のコメントを外します。
元の内容はテストをコンパイルして、実行してsignatureが書き込まれたファイルを生成するという流れのものです。
if self.target_run: # build the command for running the elf on the DUT. In this case we use spike and indicate # the isa arg that we parsed in the build stage, elf filename and signature filename. # Template is for spike. Please change for your DUT execute = self.dut_exe + ' --isa={0} +signature={1} +signature-granularity=4 {2}'.format(self.isa, sig_file, elf) logger.debug('Executing on Spike ' + execute) # launch the execute command. Change the test_dir if required. utils.shellCommand(execute).run(cwd=test_dir)
大事なのは上記の処理です。
Spikeがテンプレートになっているので、Spikeを実行するコマンドを生成して実行しています。
signatureはsig_fileに4バイトずつテキスト形式で出力します。
これを、次のように編集しました。
logfile = str(elf) + ".log" if self.target_run: # elf -> bin cmd = "riscv64-unknown-elf-objcopy -O binary {0} {1}.bin".format(elf, elf) utils.shellCommand(cmd).run(cwd=test_dir) # bin -> hex bin2hex_path = "/home/kanataso/Documents/bluecore/core/test/bin2hex.py" cmd = "python3 {0} 8 {1}.bin > {2}.hex".format(bin2hex_path, elf, elf) utils.shellCommand(cmd).run(cwd=test_dir) # run test execute = "/home/kanataso/Documents/bluecore/core/obj_dir/sim " + str(elf) + ".hex 1000000 > " + logfile logger.debug('Executing on bluecore ' + execute) utils.shellCommand(execute).run(cwd=test_dir) # シグネチャの保存 sigs = [] with open(test_dir + "/" + logfile, mode="r") as fd: lines = fd.readlines() prefix = "signature: " for l in lines: if not l.startswith(prefix): continue sigs.append(l[len(prefix):len(prefix) + 8]) with open(sig_file, mode="w") as fd: sigs = list(map(lambda s: s + "\n", sigs)) fd.writelines(sigs)
bluecoreで実行できる形式にelfを変換して、
テストを実行するコマンドを実行、
ログのsignature:が含まれる行のデータをsig_fileに出力しています。
テストの実行
テストを実行します。
$ riscof testlist --config=config.ini --suite=riscv-arch-test/riscv-test-suite/ --env=riscv-arch-test/riscv-test-suite/env # テストの選択 $ riscof run --config=config.ini --suite=riscv-arch-test/riscv-test-suite/ --env=riscv-arch-test/riscv-test-suite/env # 実行
テストの終了を待ってレポートを見てみると27個もFAILしています。

バグを見つける
レポートのbgeuのテストの詳細がこれです。
commit_id:b91f98f3a0e908bad4680c2e3901fbc24b63a563
MACROS:
TEST_CASE_1=True
XLEN=64
File1 Path:/home/kanataso/Documents/bluecore/core/test/riscof/riscof_work/rv64i_m/I/src/bgeu-01.S/dut/DUT-bluecore.signature
File2 Path:/home/kanataso/Documents/bluecore/core/test/riscof/riscof_work/rv64i_m/I/src/bgeu-01.S/ref/Reference-sail_c_simulator.signature
Match Line# File1 File2
* 478 00000002 00000001
479 00000000 00000000
* 528 00000002 00000001
529 00000000 00000000
* 626 00000002 00000003
627 00000000 00000000
* 676 00000002 00000001
677 00000000 00000000
* 826 00000002 00000003
827 00000000 00000000
問題が発生している箇所を見つける手順は次の通り
- 478行目 = 478回目にsignatureの値をMMIOに書き込む命令が、どこのアドレス(signature_beginからendの中のどこ)の値を読み込んでいるかを確認する
- 分かったsignatureのアドレスに書き込んでいる命令を見つける
elfをdumpしてログと比較していると、SRL命令の動作がSRA命令になっているのを発見しました。 ALUの実装を見ると、RV64Iのとき即値シフト命令のshamtの幅が広がっているのを考慮できておらず、SRLがSRAになってしまっているのを発見しました。
3'b101: result = if ctrl.funct7 == 0 { // <- funct7[0]はshamt sel_w(ctrl.is_op32, srl32, srl) } else { sel_w(ctrl.is_op32, sra32, sra) };


funct7のlsbは比較しないように修正して、CPUのシミュレータをコンパイルしなおします。
3'b101: result = if ctrl.funct7[6:1] == 0 { sel_w(ctrl.is_op32, srl32, srl) } else { sel_w(ctrl.is_op32, sra32, sra) };
もう一度RISCOFを実行する
テストを実行します。
riscof run --config=config.ini --suite=riscv-arch-test/riscv-test-suite/ --env=riscv-arch-test/riscv-test-suite/env

CSRと例外の実装が不完全なので、ミスアライン系とecall、ebreakのテストに失敗しました。 それ以外はPASSしたのでOK
おわり
RISCOFを使ってriscv-arch-testのテストを実行し、自作のCPUのバグを見つけることができました。 RISC-V CTGも早めに触りたいです。
Verylで作るCPUを印刷する前に検証しておけば良かった感があります、残念。 逆に、(今のところ)1つしかバグが無い(かもしれない)ことが分かってよかったです。
導入は意外と簡単なので、RISC-VのCPUを記述している方はお試しすることをお勧めします。