riscv-testsによるテスト
第3章では、RV32IのCPUを実装しました。 簡単なテストを作成して動作を確かめましたが、 まだテストできていない命令が複数あります。 そこで、riscv-testsというテストを利用することで、 CPUがある程度正しく動いているらしいことを確かめます。
riscv-testsとは何か?
riscv-testsは、RISC-Vのプロセッサ向けのユニットテストやベンチマークテストの集合です。 命令や機能ごとにテストが用意されており、 これを利用することで簡単に実装を確かめられます。 すべての命令のすべての場合を網羅するようなテストではないため、 riscv-testsをパスしても、確実に実装が正しいとは言えないことに注意してください[1]。
GitHubのriscv-software-src/riscv-tests からソースコードをダウンロードできます。
riscv-testsのビルド
riscv-testsのビルドが面倒、もしくはよく分からなくなってしまった方へ
https://github.com/nananapo/riscv-tests-bin/tree/bin4
完成品を上記のURLにおいておきます。 core/testにコピーしてください。
riscv-testsをビルドする
まず、riscv-testsをcloneします (リスト1)。
▼リスト5.1: riscv-testsのclone
$ 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を変更する必要があります(リスト2)。
▼リスト5.2: riscv-tests/env/p/link.ld
OUTPUT_ARCH( "riscv" )
ENTRY(_start)
SECTIONS
{
. = 0x00000000; ← 先頭を0x00000000に変更する
riscv-testsをビルドします。 必要なソフトウェアがインストールされていない場合、適宜インストールしてください (リスト3)。
▼リスト5.3: riscv-testsのビルド
$ cd riscv-testsをcloneしたディレクトリ
$ autoconf
$ ./configure --prefix=core/testへのパス
$ make
$ make install
core/testにshareディレクトリが作成されます。
成果物を$readmemhで読み込める形式に変換する
riscv-testsをビルドできましたが、 これは$readmemhシステムタスクで読み込める形式(以降HEX形式と呼びます)ではありません。 これをCPUでテストを実行できるように、 ビルドしたテストのバイナリファイルをHEX形式に変換します。
まず、バイナリファイルをHEX形式に変換するPythonプログラムtest/bin2hex.pyを作成します(リスト4)。
▼リスト5.4: test/bin2hex.py 差分をみる
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.5: ELFファイルをバイナリファイルに変換する
$ find share/ -type f -not -name "*.dump" -exec riscv32-unknown-elf-objcopy -O binary {} {}.bin \;
最後に、objcopyで生成されたバイナリファイルを、 PythonプログラムでHEXファイルに変換します(リスト6)。
▼リスト5.6: バイナリファイルをHEXファイルに変換する
$ find share/ -type f -name "*.bin" -exec sh -c "python3 bin2hex.py 4 {} > {}.hex" \;
テスト内容の確認
riscv-testsには複数のテストが用意されていますが、 本章では、名前がrv32ui-p-から始まるRV32I向けのテストを利用します。
例えば、ADD命令のテストであるtest/share/riscv-tests/isa/rv32ui-p-add.dumpを読んでみます(リスト7)。 rv32ui-p-add.dumpは、rv32ui-p-addのダンプファイルです。
▼リスト5.7: rv32ui-p-add.dump
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のアドレスは、リンカの設定ファイルに記述されています(リスト8)。 プログラムのサイズは0x1000よりも小さいため、 .tohostのアドレスは0x1000になります。
▼リスト5.8: riscv-tests/env/p/link.ld
OUTPUT_ARCH( "riscv" )
ENTRY(_start)
SECTIONS
{
. = 0x00000000;
.text.init : { *(.text.init) }
. = ALIGN(0x1000);
.tohost : { *(.tohost) }
テストの終了検知
テストを実行するとき、テストの終了を検知して、成功か失敗かを報告する必要があります。
riscv-testsはテストの終了を示すために、.tohostにLSBが1な値を書き込みます。 書き込まれた値が32'h1のとき、テストが正常に終了したことを表しています。 それ以外のときは、テストが失敗したことを表しています。
riscv-testsが終了したことを検知する処理をtopモジュールに記述します。 topモジュールでメモリへのアクセスを監視し、 .tohostにLSBが1な値が書き込まれたら、 test_successに結果を書き込んでテストを終了します。 (リスト9)。
▼リスト5.9: メモリアクセスを監視して終了を検知する (top.veryl) 差分をみる
// 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はポートとして定義します (リスト10)。
▼リスト5.10: テスト結果を報告するためのポートを宣言する (top.veryl) 差分をみる
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マクロが定義されているときにのみ存在するようになっています。
テストの実行
試しにADD命令のテストを実行してみましょう。 ADD命令のテストのHEXファイルはtest/share/riscv-tests/isa/rv32ui-p-add.bin.hexです。
TEST_MODEマクロを定義してシミュレータをビルドし、正常に動くことを確認します(リスト11)。
▼リスト5.11: ADD命令のriscv-testsを実行する
$ 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]。
複数のテストの自動実行
ADD命令以外の命令もテストしたいですが、わざわざコマンドを手打ちしたくありません。 自動でテストを実行して、その結果を報告するプログラムを作成しましょう。
test/test.pyを作成し、次のように記述します(リスト12)。
▼リスト5.12: test.py 差分をみる
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++プログラムを変更します (リスト13)。
▼リスト5.13: tb_verilator.cpp 差分をみる
#ifdef TEST_MODE
return dut->test_success != 1;
#endif
それでは、RV32Iのテストを実行しましょう。 riscv-testsのRV32I向けのテストの接頭辞であるrv32ui-p-を引数に指定します(リスト14)。
▼リスト5.14: rv32ui-pから始まるテストを実行する
$ 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は、 ロードストアするサイズに整列されていないアドレスへのロードストア命令のテストです。 これは後の章で例外として対処するため、今は無視します。