みみみ

わーい

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のコアの動作をゴールデンモデルと比較して検証するフレームワークです。 ゴールデンモデルとは、検証において正しく設計されているとするモデルのことです。

github.com

検証対象のコアの仕様(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が生成されます。

spikeをRISCOFで検証した結果

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を次のように編集しました。 ISARV64IMZicsrphysical_addr_sz24に変更して、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は実装していないので、mtimemtimecmpを削除しました。

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にあるプログラムを少し変更して利用しています。

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 = .;
}

.tohostMMIOなのでNOLOADに設定して、位置をbssの後ろにしています。 CPU側の.tohostMMIOのアドレスも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しています。

bluecoreでRISCOFを実行する (失敗)

バグを見つける

レポートの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)
};

RV32Iの即値シフト
RV64Iの即値シフト命令、shamtが広がっている

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