第5章
riscv-testsによるテスト
第3章では、RV32IのCPUを実装しました。簡単なテストを作成して動作を確かめましたが、まだテストできていない命令が複数あります。そこで、riscv-testsというテストを利用することで、CPUがある程度正しく動いているらしいことを確かめます。
5.1 riscv-testsとは何か?
riscv-testsは、RISC-Vのプロセッサ向けのユニットテストやベンチマークテストの集合です。命令や機能ごとにテストが用意されており、これを利用することで簡単に実装を確かめられます。すべての命令のすべての場合を網羅するようなテストではないため、riscv-testsをパスしても、確実に実装が正しいとは言えないことに注意してください*1。
[*1] 実装の正しさを完全に確かめるには形式的検証(formal verification)を行う必要があります
GitHubのriscv-software-src/riscv-testsからソースコードをダウンロードできます。
5.2 riscv-testsのビルド
riscv-testsのビルドが面倒、もしくはよく分からなくなってしまった方へ
https://github.com/nananapo/riscv-tests-bin/tree/bin4
完成品を上記のURLにおいておきます。core/testにコピーしてください。
5.2.1 riscv-testsをビルドする
まず、riscv-testsをcloneします(リスト5.1)。
$ git clone https://github.com/riscv-software-src/riscv-tests $ cd riscv-tests $ git submodule update --init --recursive
riscv-testsは、プログラムの実行が0x80000000
から始まると仮定した設定になっています。しかし、CPUはアドレス0x00000000
から実行を開始するため、リンカにわたす設定ファイルenv/p/link.ld
を変更する必要があります(リスト5.2)。
OUTPUT_ARCH( "riscv" )
ENTRY(_start)
SECTIONS
{
. = 0x00000000; ← 先頭を0x00000000に変更する
riscv-testsをビルドします。必要なソフトウェアがインストールされていない場合、適宜インストールしてください(リスト5.3)。
$ cd riscv-testsをcloneしたディレクトリ $ autoconf $ ./configure --prefix=core/testへのパス $ make $ make install
core/testにshareディレクトリが作成されます。
5.2.2 成果物を$readmemhで読み込める形式に変換する
riscv-testsをビルドできましたが、これは$readmemh
システムタスクで読み込める形式(以降HEX形式と呼びます)ではありません。これをCPUでテストを実行できるように、ビルドしたテストのバイナリファイルをHEX形式に変換します。
まず、バイナリファイルをHEX形式に変換するPythonプログラムtest/bin2hex.py
を作成します(リスト5.4)。
import sys # 使い方を表示する def print_usage(): print(sys.argv[1]) print("Usage:", sys.argv[0], "[bytes per line] [filename]") exit() # コマンドライン引数を受け取る args = sys.argv[1:] if len(args) != 2: print_usage() BYTES_PER_LINE = None try: BYTES_PER_LINE = int(args[0]) except: print_usage() FILE_NAME = args[1] # バイナリファイルを読み込む allbytes = [] with open(FILE_NAME, "rb") as f: allbytes = f.read() # 値を文字列に変換する bytestrs = [] for b in allbytes: bytestrs.append(format(b, '02x')) # 00を足すことでBYTES_PER_LINEの倍数に揃える bytestrs += ["00"] * (BYTES_PER_LINE - len(bytestrs) % BYTES_PER_LINE) # 出力 results = [] for i in range(0, len(bytestrs), BYTES_PER_LINE): s = "" for j in range(BYTES_PER_LINE): s += bytestrs[i + BYTES_PER_LINE - j - 1] results.append(s) print("\n".join(results))
このプログラムは、第二引数に指定されるバイナリファイルを、第一引数に与えられた数のバイト毎に区切り、16進数のテキストで出力します。
HEXファイルに変換する前に、ビルドした成果物を確認する必要があります。例えばtest/share/riscv-tests/isa/rv32ui-p-add
はELFファイル*2です。CPUはELFを直接に実行する機能を持っていないため、riscv64-unknown-elf-objcopy
を利用して、ELFファイルを余計な情報を取り除いたバイナリファイルに変換します(リスト5.5)。
[*2] ELF(Executable and Linkable Format)とは実行可能ファイルの形式です
$ find share/ -type f -not -name "*.dump" -exec riscv32-unknown-elf-objcopy -O binary {} {}.bin \;
最後に、objcopyで生成されたバイナリファイルを、PythonプログラムでHEXファイルに変換します(リスト5.6)。
$ find share/ -type f -name "*.bin" -exec sh -c "python3 bin2hex.py 4 {} > {}.hex" \;
5.3 テスト内容の確認
riscv-testsには複数のテストが用意されていますが、本章では、名前がrv32ui-p-
から始まるRV32I向けのテストを利用します。
例えば、ADD命令のテストであるtest/share/riscv-tests/isa/rv32ui-p-add.dump
を読んでみます(リスト5.7)。rv32ui-p-add.dump
は、rv32ui-p-add
のダンプファイルです。
Disassembly of section .text.init: 00000000 <_start>: 0: 0500006f j 50 <reset_vector> 00000004 <trap_vector>: 4: 34202f73 csrr t5,mcause ← t5 = mcause ... 18: 00b00f93 li t6,11 1c: 03ff0063 beq t5,t6,3c <write_tohost> ... 0000003c <write_tohost>: ← 0x1000にテスト結果を書き込む 3c: 00001f17 auipc t5,0x1 40: fc3f2223 sw gp,-60(t5) # 1000 <tohost> ... 00000050 <reset_vector>: 50: 00000093 li ra,0 ... ← レジスタ値のゼロ初期化 c8: 00000f93 li t6,0 ... 130: 00000297 auipc t0,0x0 134: ed428293 addi t0,t0,-300 # 4 <trap_vector> 138: 30529073 csrw mtvec,t0 ← mtvecにtrap_vectorのアドレスを書き込む ... 178: 00000297 auipc t0,0x0 17c: 01428293 addi t0,t0,20 # 18c <test_2> 180: 34129073 csrw mepc,t0 ← mepcにtest_2のアドレスを書き込む ... 188: 30200073 mret ← mepcのアドレス=test_2にジャンプする 0000018c <test_2>: ← 0 + 0 = 0のテスト 18c: 00200193 li gp,2 ← gp = 2 190: 00000593 li a1,0 194: 00000613 li a2,0 198: 00c58733 add a4,a1,a2 19c: 00000393 li t2,0 1a0: 4c771663 bne a4,t2,66c <fail> ... 0000066c <fail>: ← 失敗したときのジャンプ先 ... 674: 00119193 sll gp,gp,0x1 ← gpを1ビット左シフトする 678: 0011e193 or gp,gp,1 ← gpのLSBを1にする ... 684: 00000073 ecall 00000688 <pass>: ← すべてのテストに成功したときのジャンプ先 ... 68c: 00100193 li gp,1 ← gp = 1 690: 05d00893 li a7,93 694: 00000513 li a0,0 698: 00000073 ecall 69c: c0001073 unimp
命令のテストは次の流れで実行されます。
- _start : reset_vectorにジャンプする。
- reset_vector : 各種状態を初期化する。
- test_* : テストを実行する。命令の結果がおかしかったらfailにジャンプする。最後まで正常に実行できたらpassにジャンプする。
- fail、pass : テストの成否をレジスタに書き込み、trap_vectorにジャンプする。
- trap_vector : write_tohostにジャンプする。
- write_tohost : テスト結果をメモリに書き込む。ここでループする。
_start
から実行を開始し、最終的にwrite_tohost
に移動します。テスト結果はメモリの.tohost
に書き込まれます。.tohost
のアドレスは、リンカの設定ファイルに記述されています(リスト5.8)。プログラムのサイズは0x1000
よりも小さいため、.tohost
のアドレスは0x1000
になります。
OUTPUT_ARCH( "riscv" ) ENTRY(_start) SECTIONS { . = 0x00000000; .text.init : { *(.text.init) } . = ALIGN(0x1000); .tohost : { *(.tohost) }
5.4 テストの終了検知
テストを実行するとき、テストの終了を検知して、成功か失敗かを報告する必要があります。
riscv-testsはテストの終了を示すために、.tohost
にLSBが1
な値を書き込みます。書き込まれた値が32'h1
のとき、テストが正常に終了したことを表しています。それ以外のときは、テストが失敗したことを表しています。
riscv-testsが終了したことを検知する処理をtopモジュールに記述します。topモジュールでメモリへのアクセスを監視し、.tohost
にLSBが1
な値が書き込まれたら、test_success
に結果を書き込んでテストを終了します。(リスト5.9)。
// riscv-testsの終了を検知する #[ifdef(TEST_MODE)] always_ff { let RISCVTESTS_TOHOST_ADDR: Addr = 'h1000 as Addr; if d_membus.valid && d_membus.ready && d_membus.wen == 1 && d_membus.addr == RISCVTESTS_TOHOST_ADDR && d_membus.wdata[lsb] == 1'b1 { test_success = d_membus.wdata == 1; if d_membus.wdata == 1 { $display("riscv-tests success!"); } else { $display("riscv-tests failed!"); $error ("wdata : %h", d_membus.wdata); } $finish(); } }
test_success
はポートとして定義します(リスト5.10)。
module top #( param MEMORY_FILEPATH_IS_ENV: bit = 1 , param MEMORY_FILEPATH : string = "MEMORY_FILE_PATH", ) ( clk: input clock, rst: input reset, #[ifdef(TEST_MODE)] test_success: output bit, ) {
アトリビュートによって、終了検知のコードとtest_success
ポートはTEST_MODE
マクロが定義されているときにのみ存在するようになっています。
5.5 テストの実行
試しにADD命令のテストを実行してみましょう。ADD命令のテストのHEXファイルはtest/share/riscv-tests/isa/rv32ui-p-add.bin.hex
です。
TEST_MODEマクロを定義してシミュレータをビルドし、正常に動くことを確認します(リスト5.11)。
$ make build $ make sim VERILATOR_FLAGS="-DTEST_MODE" ← TEST_MODEマクロを定義してビルドする $ ./obj_dir/sim test/share/riscv-tests/isa/rv32ui-p-add.bin.hex 0 # 4 00000000 : 0500006f # 8 00000050 : 00000093 ... # 593 00000040 : fc3f2223 itype : 000100 imm : ffffffc4 rs1[30] : 0000103c rs2[ 3] : 00000001 op1 : 0000103c op2 : ffffffc4 alu res : 00001000 mem stall : 1 mem rdata : ff1ff06f riscv-tests success! - ~/core/src/top.sv:26: Verilog $finish
riscv-tests success!
と表示され、テストが正常終了しました*3。
[*3] 実行が終了しない場合はどこかしらにバグがあります。rv32ui-p-add.dumpと実行ログを見比べて、頑張って原因を探してください
5.6 複数のテストの自動実行
ADD命令以外の命令もテストしたいですが、わざわざコマンドを手打ちしたくありません。自動でテストを実行して、その結果を報告するプログラムを作成しましょう。
test/test.py
を作成し、次のように記述します(リスト5.12)。
import argparse import os import subprocess parser = argparse.ArgumentParser() parser.add_argument("sim_path", help="path to simlator") parser.add_argument("dir", help="directory includes test") parser.add_argument("files", nargs='*', help="test hex file names") parser.add_argument("-r", "--recursive", action='store_true', help="search file recursively") parser.add_argument("-e", "--extension", default="hex", help="test file extension") parser.add_argument("-o", "--output_dir", default="results", help="result output directory") parser.add_argument("-t", "--time_limit", type=float, default=10, help="limit of execution time. set 0 to nolimit") args = parser.parse_args() # run test def test(file_name): result_file_path = os.path.join(args.output_dir, file_name.replace(os.sep, "_") + ".txt") cmd = args.sim_path + " " + file_name + " 0" success = False with open(result_file_path, "w") as f: no = f.fileno() p = subprocess.Popen("exec " + cmd, shell=True, stdout=no, stderr=no) try: p.wait(None if args.time_limit == 0 else args.time_limit) success = p.returncode == 0 except: pass finally: p.terminate() p.kill() print(("PASS" if success else "FAIL") + " : "+ file_name) return (file_name, success) # search files def dir_walk(dir): for entry in os.scandir(dir): if entry.is_dir(): if args.recursive: for e in dir_walk(entry.path): yield e continue if entry.is_file(): if not entry.name.endswith(args.extension): continue if len(args.files) == 0: yield entry.path for f in args.files: if entry.name.find(f) != -1: yield entry.path break if __name__ == '__main__': os.makedirs(args.output_dir, exist_ok=True) res_strs = [] res_statuses = [] for hexpath in dir_walk(args.dir): f, s = test(os.path.abspath(hexpath)) res_strs.append(("PASS" if s else "FAIL") + " : " + f) res_statuses.append(s) res_strs = sorted(res_strs) statusText = "Test Result : " + str(sum(res_statuses)) + " / " + str(len(res_statuses)) with open(os.path.join(args.output_dir, "result.txt"), "w", encoding='utf-8') as f: f.write(statusText + "\n") f.write("\n".join(res_strs)) print(statusText) if sum(res_statuses) != len(res_statuses): exit(1)
このPythonプログラムは、第2引数で指定したディレクトリに存在する、第3引数で指定した文字列を名前に含むファイルを、第1引数で指定したシミュレータで実行し、その結果を報告します。
次のオプションの引数が存在します。
- -r
- 第2引数で指定されたディレクトリの中にあるディレクトリも走査します。 デフォルトでは走査しません。
- -e 拡張子
-
指定した拡張子のファイルのみを対象にテストします。
HEXファイルをテストしたい場合は、
-e hex
にします。 デフォルトではhex
が指定されています。 - -o ディレクトリ
-
指定したディレクトリにテスト結果を格納します。
デフォルトでは
result
ディレクトリに格納します。 - -t 時間
- テストに時間制限を設けます。 0を指定すると時間制限はなくなります。 デフォルト値は10(秒)です。
テストが成功したか失敗したかの判定には、シミュレータの終了コードを利用しています。テストが失敗したときに終了コードが1
になるように、Verilatorに渡しているC++プログラムを変更します(リスト5.13)。
#ifdef TEST_MODE return dut->test_success != 1; #endif
それでは、RV32Iのテストを実行しましょう。riscv-testsのRV32I向けのテストの接頭辞であるrv32ui-p-
を引数に指定します(リスト5.14)。
$ make build $ make sim VERILATOR_FLAGS="-DTEST_MODE" $ python3 test/test.py -r obj_dir/sim test/share rv32ui-p- PASS : ~/core/test/share/riscv-tests/isa/rv32ui-p-lh.bin.hex PASS : ~/core/test/share/riscv-tests/isa/rv32ui-p-sb.bin.hex PASS : ~/core/test/share/riscv-tests/isa/rv32ui-p-sltiu.bin.hex PASS : ~/core/test/share/riscv-tests/isa/rv32ui-p-sh.bin.hex PASS : ~/core/test/share/riscv-tests/isa/rv32ui-p-bltu.bin.hex PASS : ~/core/test/share/riscv-tests/isa/rv32ui-p-or.bin.hex PASS : ~/core/test/share/riscv-tests/isa/rv32ui-p-sra.bin.hex PASS : ~/core/test/share/riscv-tests/isa/rv32ui-p-xor.bin.hex PASS : ~/core/test/share/riscv-tests/isa/rv32ui-p-addi.bin.hex PASS : ~/core/test/share/riscv-tests/isa/rv32ui-p-srai.bin.hex PASS : ~/core/test/share/riscv-tests/isa/rv32ui-p-srli.bin.hex PASS : ~/core/test/share/riscv-tests/isa/rv32ui-p-auipc.bin.hex PASS : ~/core/test/share/riscv-tests/isa/rv32ui-p-slli.bin.hex PASS : ~/core/test/share/riscv-tests/isa/rv32ui-p-slti.bin.hex PASS : ~/core/test/share/riscv-tests/isa/rv32ui-p-lb.bin.hex PASS : ~/core/test/share/riscv-tests/isa/rv32ui-p-lw.bin.hex PASS : ~/core/test/share/riscv-tests/isa/rv32ui-p-bge.bin.hex PASS : ~/core/test/share/riscv-tests/isa/rv32ui-p-sub.bin.hex PASS : ~/core/test/share/riscv-tests/isa/rv32ui-p-xori.bin.hex PASS : ~/core/test/share/riscv-tests/isa/rv32ui-p-sw.bin.hex PASS : ~/core/test/share/riscv-tests/isa/rv32ui-p-beq.bin.hex PASS : ~/core/test/share/riscv-tests/isa/rv32ui-p-fence_i.bin.hex PASS : ~/core/test/share/riscv-tests/isa/rv32ui-p-jal.bin.hex PASS : ~/core/test/share/riscv-tests/isa/rv32ui-p-and.bin.hex PASS : ~/core/test/share/riscv-tests/isa/rv32ui-p-lui.bin.hex PASS : ~/core/test/share/riscv-tests/isa/rv32ui-p-bgeu.bin.hex PASS : ~/core/test/share/riscv-tests/isa/rv32ui-p-slt.bin.hex PASS : ~/core/test/share/riscv-tests/isa/rv32ui-p-sll.bin.hex PASS : ~/core/test/share/riscv-tests/isa/rv32ui-p-jalr.bin.hex PASS : ~/core/test/share/riscv-tests/isa/rv32ui-p-add.bin.hex PASS : ~/core/test/share/riscv-tests/isa/rv32ui-p-simple.bin.hex PASS : ~/core/test/share/riscv-tests/isa/rv32ui-p-andi.bin.hex FAIL : ~/core/test/share/riscv-tests/isa/rv32ui-p-ma_data.bin.hex PASS : ~/core/test/share/riscv-tests/isa/rv32ui-p-lhu.bin.hex PASS : ~/core/test/share/riscv-tests/isa/rv32ui-p-lbu.bin.hex PASS : ~/core/test/share/riscv-tests/isa/rv32ui-p-sltu.bin.hex PASS : ~/core/test/share/riscv-tests/isa/rv32ui-p-ori.bin.hex PASS : ~/core/test/share/riscv-tests/isa/rv32ui-p-blt.bin.hex PASS : ~/core/test/share/riscv-tests/isa/rv32ui-p-bne.bin.hex PASS : ~/core/test/share/riscv-tests/isa/rv32ui-p-srl.bin.hex Test Result : 39 / 40
rv32ui-p-
から始まる40個のテストの内、39個のテストに成功しました。テストの詳細な結果はresultsディレクトリに格納されています。
rv32ui-p-ma_data
は、ロードストアするサイズに整列されていないアドレスへのロードストア命令のテストです。これは後の章で例外として対処するため、今は無視します。