Verilator + GoogleTest で SystemVerilog のモジュールを単体テストする

この記事は TSG Advent Calendar 2023 初日のエントリです。

Verilog/SystemVerilog で書かれたモジュールをテストする方法として VUnitcocotb がありますが、 普段から C++ を書いている自分としては、RTL のテストを C++ で完結させたいという欲求があり、C++ で RTL をテストする方法を探していました。

そこで、C++ 用のテストフレームワークである GoogleTest を Verilator と組み合わせて使う方法を試してみました。 これが意外と簡単にできたので紹介します。需要は謎です。

TL;DR

GitHub - sh-mug/verilog-unittest-sample のサンプルコードを見てくれ!

テスト対象のモジュール

テスト対象のモジュールとして、inst に応じて、ab を演算して 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 回ランダムな整数 ab を生成し、これを 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 でライブラリを生成する必要があるので、これの改善方法を考えていきたいと思います。