Verilator + GoogleTest で SystemVerilog のモジュールを単体テストする
この記事は TSG Advent Calendar 2023 初日のエントリです。
Verilog/SystemVerilog で書かれたモジュールをテストする方法として VUnit や cocotb がありますが、 普段から C++ を書いている自分としては、RTL のテストを C++ で完結させたいという欲求があり、C++ で RTL をテストする方法を探していました。
そこで、C++ 用のテストフレームワークである GoogleTest を Verilator と組み合わせて使う方法を試してみました。 これが意外と簡単にできたので紹介します。需要は謎です。
TL;DR
GitHub - sh-mug/verilog-unittest-sample のサンプルコードを見てくれ!
テスト対象のモジュール
テスト対象のモジュールとして、inst
に応じて、a
と b
を演算して rslt
に結果を出力する ALU を用意しました。
この ALU が意図した動作をしているかをテストします。
`timescale 1ns / 1ps module alu ( input [2:0] inst, input [31:0] a, input [31:0] b, output logic [31:0] rslt ); logic [4:0] shamt; logic alu_lt; logic alu_ltu; logic [31:0] alu_add; logic [31:0] alu_sll; logic [31:0] alu_xor; logic [31:0] alu_srl; logic [31:0] alu_or; logic [31:0] alu_and; always_comb begin shamt = b[4:0]; alu_lt = $signed(a) < $signed(b); alu_ltu = a < b; alu_add = a + b; alu_sll = a << shamt; alu_xor = a ^ b; alu_srl = a >> shamt; alu_or = a | b; alu_and = a & b; case (inst) 3'b000: rslt = alu_add; 3'b001: rslt = alu_sll; 3'b010: rslt = {31'b0, alu_lt}; 3'b011: rslt = {31'b0, alu_ltu}; 3'b100: rslt = alu_xor; 3'b101: rslt = alu_srl; 3'b110: rslt = alu_or; 3'b111: rslt = alu_and; default: rslt = 0; endcase end endmodule : alu
テストコード
テストコードは、GoogleTest の TEST_F
マクロを使って書きます。
まず、テストフィクスチャ(テストのセットアップやクリーンアップを行うクラス)を作成し、その中で Verilator とモジュールのシミュレーションを初期化します。以下は、テストフィクスチャと1つのテストケースの例です。
テストフィクスチャ TestAlu
テストクラス TestAlu
では、テストケース間で共通の設定を行います。具体的には、テスト用の ValuForTest
インスタンスをセットアップし、テスト後にクリーンアップを行っています。
class TestAlu : public ::testing::Test { protected: TestAlu() : engine(seed_gen()), dist_int(INT_MIN, INT_MAX), dist_5bit(0, 31) {} ValuForTest *dut; std::random_device seed_gen; std::mt19937_64 engine; std::uniform_int_distribution<int> dist_int; std::uniform_int_distribution<unsigned char> dist_5bit; int inst; int a; int b; void SetUp() override { dut = new ValuForTest(); } void TearDown() override { dut->final(); delete dut; } };
また、演算実行の一連の流れを exec
メソッドとして定義するため、Verilator で生成された Valu
クラスを継承して ValuForTest
クラスを定義しています。
void ValuForTest::exec(const int &_inst, const int &_a, const int &_b) { inst = _inst; a = _a; b = _b; eval(); }
テストケース例 TEST_F(TestAlu, ADD)
テストケースの一つとして、 TEST_F(TestAlu, ADD)
で inst
が ADD 演算の場合のテストを行います。具体的には、N
回ランダムな整数 a
と b
を生成し、これを SystemVerilog モジュールに与えて計算結果が期待通りかどうかを ASSERT_EQ
マクロを使用して検証しています。
const unsigned N = 1000000; TEST_F(TestAlu, ADD) { inst = 0b000; for (unsigned i = 0; i < N; ++i) { a = dist_int(engine); b = dist_int(engine); dut->exec(inst, a, b); ASSERT_EQ(dut->rslt, a + b); } }
この例では ADD 演算のみを示していますが、他の演算子に対するテストケースも同様の構造で追加できます。これにより、テスト対象の SystemVerilog モジュールが期待通りに動作しているかを確認できます。
テスト実行
SystemVerilog モジュールの単体テストを実行するために、CMake を使用してテストベンチをビルドし、実行結果を確認します。
CMake を使用したテストベンチのビルド
まず、テストベンチのビルドには以下の CMakeLists.txt を使用します。このファイルでは、Verilator と GoogleTest をプロジェクトに取り込み、テスト実行用のバイナリをビルドします。必要な部分を抜き出しています。
cmake_minimum_required(VERSION 3.14) project(verilog_unittest_sample) # ... Verilator と GoogleTest の取り込み(省略) # Test フォルダ内のソースファイルを含むテスト実行用のバイナリを定義 enable_testing() add_executable(test_all test/test_alu.cpp test/main.cpp ) target_link_libraries( test_all PRIVATE GTest::gtest_main ) # テストバイナリのプロパティを設定 set_target_properties(test_all PROPERTIES CXX_STANDARD 17 CXX_STANDARD_REQUIRED ON COMPILE_FLAGS "-Wall -g -fsanitize=address" LINK_FLAGS "-fsanitize=address" ) # GoogleTest によるテストの自動検出 include(GoogleTest) gtest_discover_tests(test_all) # Verilate を使用して Verilog モジュールをビルド verilate(test_all INCLUDE_DIRS "src" SOURCES src/alu.sv PREFIX Valu )
このCMakeLists.txtでは、add_executable
でテスト用のバイナリを定義しています。また、Verilate を使用して SystemVerilog モジュールをビルドするために verilate
を設定しています。
テストベンチのビルドと実行
以下のコマンドで CMake を使用してプロジェクトをビルドします。
$ cmake -S . -B build -G Ninja $ ninja -C build
ビルドが完了したら、以下のコマンドでテストを実行します。
$ build/test_all
これにより、GoogleTest がテストを自動的に検出し、SystemVerilog モジュールの単体テストが実行されます。 そうすると、以下のような結果が表示され、ALU の各演算子に対するテストが実行されたことがわかります。
[==========] Running 8 tests from 1 test suite. [----------] Global test environment set-up. [----------] 8 tests from TestAlu [ RUN ] TestAlu.ADD [ OK ] TestAlu.ADD (337 ms) [ RUN ] TestAlu.SLL [ OK ] TestAlu.SLL (278 ms) [ RUN ] TestAlu.SLT [ OK ] TestAlu.SLT (272 ms) [ RUN ] TestAlu.SLTU [ OK ] TestAlu.SLTU (281 ms) [ RUN ] TestAlu.XOR [ OK ] TestAlu.XOR (267 ms) [ RUN ] TestAlu.SRL [ OK ] TestAlu.SRL (279 ms) [ RUN ] TestAlu.OR [ OK ] TestAlu.OR (260 ms) [ RUN ] TestAlu.AND [ OK ] TestAlu.AND (273 ms) [----------] 8 tests from TestAlu (2252 ms total) [----------] Global test environment tear-down [==========] 8 tests from 1 test suite ran. (2252 ms total) [ PASSED ] 8 tests.
試しに、ALU の演算結果が期待通りにならないように alu.sv
を修正してみます。
たとえば、加算命令実行時に alu_add
の代わりに alu_xor
を出力するようにしてみます。
- 3'b000: rslt = alu_add; + 3'b000: rslt = alu_xor;
この状態で再度テストを実行すると、以下のように加算命令のテストが失敗することがわかります。
[==========] Running 8 tests from 1 test suite. [----------] Global test environment set-up. [----------] 8 tests from TestAlu [ RUN ] TestAlu.ADD /home/mug/verilog-unittest-sample/test/test_alu.cpp:49: Failure Expected equality of these values: dut->rslt Which is: 481875563 a + b Which is: 549918315 [ FAILED ] TestAlu.ADD (61 ms) ...(以下略) [----------] 8 tests from TestAlu (1958 ms total) [----------] Global test environment tear-down [==========] 8 tests from 1 test suite ran. (1958 ms total) [ PASSED ] 7 tests. [ FAILED ] 1 test, listed below: [ FAILED ] TestAlu.ADD 1 FAILED TEST
おまけ:GitHub Actions での自動テスト
上で生成したテストバイナリを用いれば、GitHub Actions で自動テストを行うこともできます。記事が長くなってしまったので詳細は省略しますが、hdlc/verilator というありがたいコンテナイメージがあります。このコンテナ上で新しいバージョンの Verilator を利用でき(2023 年 12 月 1 日現在では v5.018)、GitHub Actions で簡単にテストを実行できます。
まとめ
Verilator と GoogleTest を組み合わせて、SystemVerilog のモジュールを単体テストする方法を紹介しました。 GoogleTest のテストフィクスチャを用いることで、RTL の単体テストを簡単に書くことが出来そうです。 ここまでで紹介したサンプルコードは、GitHub - sh-mug/verilog-unittest-sample で公開しています。
現状の方法では単体テストを行うモジュールごとに Verilator でライブラリを生成する必要があるので、これの改善方法を考えていきたいと思います。