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を記述している方はお試しすることをお勧めします。