Vengineerの妄想

人生を妄想しています。

Grok 4.1 Beta さんに、RTLコードに対するテストベンチ・テストプログラム・実行環境を作ってもらった

はじめに

昨年の3月に、Grok 3さんに、LPDDR5 メモリコントローラを SystemVerilogで書いてもらいました。

vengineer.hatenablog.com

今回は、RTLコードがあるものに対して、

  • テストベンチ
  • テストプログラム
  • 実行環境

を作ってもらいました。

お題は、APBインターフェースのUART

下記のAPBインターフェースのUARTのシミュレーションをするための、テストベンチを作成して、 Verilatorを使って、確かめて、 カバレッジが100%になるように、色々なテストを作成して、全体(CI)を実行できるように、Pythonコードを生成して、https://github.com/pulp-platform/apb_uart_sv

何度かやり取りして、使えそうなものが出来上がってきたのは、次のようなもの。

推奨ディレクトリ構造

apb_uart_verilator/
├── rtl/
│   ├── apb_uart.sv               ← リポジトリのメイン(または apb_uart_sv.sv)
│   ├── uart_rx.sv
│   ├── uart_tx.sv
│   ├── uart_interrupt.sv
│   ├── io_generic_fifo.sv
│   └── ...(他全て)
├── tb/
│   ├── tb_top.sv                 ← 共通トップ(クロック・リセット・DUTインスタンス)
│   ├── apb_driver.sv             ← APB write/read task をまとめたモジュール
│   ├── tests/
│   │   ├── test_basic.sv
│   │   ├── test_loopback.sv
│   │   ├── test_all_lcr_configs.sv
│   │   ├── test_fifo_depth.sv
│   │   ├── test_interrupts.sv
│   │   ├── test_error_injection.sv
│   │   └── ...(追加テストはここに増やす)
│   └── uart_loopback.sv          ← オプション:tx→rx接続ヘルパー
└── sim/
    ├── run_test.py               ← Pythonでテスト選択&実行
    └── compile_all.sh            ← 共通コンパイルスクリプト

主要ファイル例

1. tb_top.sv (共通部分)

`timescale 1ns / 1ps

module tb_top;

  // クロック・リセット
  logic clk = 0;
  logic rst_n = 0;

  always #5 clk = ~clk;

  initial begin
    rst_n = 0;
    #40;
    rst_n = 1;
  end

  // APBインタフェース
  logic               psel     = 0;
  logic               penable  = 0;
  logic               pwrite   = 0;
  logic        [31:0] paddr    = 0;
  logic        [31:0] pwdata   = 0;
  logic        [31:0] prdata;
  logic               pready;
  logic               pslverr;

  // UARTピン
  logic uart_rxd_i = 1'b1;
  logic uart_txd_o;
  logic uart_cts_ni = 1'b1;
  logic uart_rts_no;

  // DUT
  apb_uart dut (  // ← リポジトリによっては apb_uart_sv かも
    .PCLK       (clk),
    .PRESETn    (rst_n),
    .PSEL       (psel),
    .PENABLE    (penable),
    .PWRITE     (pwrite),
    .PADDR      (paddr[7:0]),   // 16550系は通常8bit
    .PWDATA     (pwdata),
    .PRDATA     (prdata),
    .PREADY     (pready),
    .PSLVERR    (pslverr),
    .uart_rxd_i (uart_rxd_i),
    .uart_txd_o (uart_txd_o),
    .uart_cts_ni(uart_cts_ni),
    .uart_rts_no(uart_rts_no)
  );

  // 波形ダンプ
  initial begin
    $dumpfile("trace.fst");
    $dumpvars(0, tb_top);
  end

  // テスト選択(+define ではなく、コンパイル時にトップを変更)
  // → 実際は compile 時に -top を変えるか、wrapperを使う

endmodule

2. apb_driver.sv (共通ドライバ)

module apb_driver (
  input  logic       clk,
  input  logic       rst_n,
  output logic       psel,
  output logic       penable,
  output logic       pwrite,
  output logic [31:0] paddr,
  output logic [31:0] pwdata,
  input  logic [31:0] prdata,
  input  logic       pready,
  input  logic       pslverr
);

  task automatic write(
    input logic [31:0] addr,
    input logic [31:0] data
  );
    @(posedge clk);
    paddr   = addr;
    pwdata  = data;
    pwrite  = 1;
    psel    = 1;
    @(posedge clk);
    penable = 1;
    @(posedge clk iff pready);
    @(posedge clk);
    psel    = 0;
    penable = 0;
    pwrite  = 0;
  endtask

  task automatic read(
    input  logic [31:0] addr,
    output logic [31:0] data
  );
    @(posedge clk);
    paddr   = addr;
    pwrite  = 0;
    psel    = 1;
    @(posedge clk);
    penable = 1;
    @(posedge clk iff pready);
    data    = prdata;
    @(posedge clk);
    psel    = 0;
    penable = 0;
  endtask

endmodule

3. テスト例:tests/test_basic.sv

// tests/test_basic.sv
module test_basic;

  import "DPI-C" function void set_test_name(string name); // オプション

  initial begin
    $display("\n=== Starting test_basic ===");

    // リセット待ちは tb_top で済んでいる前提
    repeat(10) @(posedge tb_top.clk);

    tb_top.apb_driver.write(8'h03, 8'h03);  // LCR = 8N1
    tb_top.apb_driver.write(8'h02, 8'hC1);  // FIFO enable + reset

    tb_top.apb_driver.write(8'h00, 8'h41);  // 'A' 送信
    repeat(800) @(posedge tb_top.clk);

    logic [31:0] rdata;
    tb_top.apb_driver.read(8'h00, rdata);
    $display("Received: %c (%h)", rdata[7:0], rdata[7:0]);

    $display("test_basic finished");
    // $finish();   ← ここではfinishせず、トップ側で制御
  end

endmodule

4. トップでテストを選択する方法(現実的な2パターン)

パターンA:wrapperモジュールで選択(おすすめ)

// tb_wrapper.sv
module tb_wrapper;

  tb_top top (.*);

  // テストインスタンス(コンパイル時に1つだけ有効にする)
  `ifdef TEST_BASIC
    test_basic t_basic();
  `elsif TEST_LOOPBACK
    test_loopback t_loop();
  `elsif TEST_ALL_LCR
    test_all_lcr_configs t_lcr();
  `else
    initial $fatal(1, "No test selected. Use +define+TEST_XXX");
  `endif

endmodule

→ Verilatorコンパイル時に -top tb_wrapper + +define+TEST_BASIC などを使う

パターンB:make / python で別々にコンパイル(よりクリーン)

各テストごとに独立したトップをコンパイル

例:make TEST=basicverilator ... tb_top.sv tests/test_basic.sv ... -top tb_top

Python側で制御

# sim/run_test.py
import subprocess
import sys

tests = ["basic", "loopback", "all_lcr", "fifo_depth", "interrupts"]

if len(sys.argv) < 2 or sys.argv[1] not in tests + ["all"]:
    print("Usage: python run_test.py <test_name | all>")
    sys.exit(1)

target = sys.argv[1]

def run_single(test):
    print(f"\n=== Compiling & Running: {test} ===")
    cmd = [
        "verilator", "--binary", "--trace-fst", "--trace-structs",
        "-Wall", "--assert",
        "--coverage-line", "--coverage-toggle", "--coverage-branch",
        "-I./rtl",
        "rtl/*.sv",
        "tb/tb_top.sv", "tb/apb_driver.sv",
        f"tb/tests/test_{test}.sv",
        "--top-module", "tb_top"
    ]
    subprocess.run(cmd, check=True)
    subprocess.run(["./obj_dir/Vtb_top"], check=False)  # failしても波形残す

if target == "all":
    for t in tests:
        run_single(t)
else:
    run_single(target)

まとめ

  • 各テストは tb/tests/test_名前.sv に独立して記述
  • 共通部分(クロック、DUT、ドライバ)は tb_top.sv / apb_driver.sv で共有
  • Pythonスクリプトで「どのテストを実行するか」を簡単に選択・回帰可能
  • カバレッジを集めたい場合は --coverage-* を常時付け、verilator_coverage でマージ

テストプログラムの実行の部分の修正

Q :トップでテストを選択する方法のところは、+オプションを使って実行時に選択する方法がSystemVerilogの機能にはあるけど、Verilatorでも使える?

A : はい、Verilatorでも +define+ を使ったマクロ選択は可能ですが、重要な制限があります。

Verilatorは コンパイル時(verilation時) にしか +define+(または -D)でマクロを定義できません。
つまり、シミュレーション実行時(生成された実行ファイルを実行する時)には変更できません

Verilatorでの挙動まとめ(2025年現在)

項目 Verilatorの対応状況 詳細
+define+TEST_BASIC をverilatorコマンドに渡す ◯(完全にサポート) 例: verilator ... +define+TEST_BASICifdef TEST_BASIC が有効になる
シミュレーション実行時(./obj_dir/Vtop ...)に +define+ を渡す ×(効かない) 実行ファイルに渡しても無視される
シミュレーション実行時にマクロを動的に切り替える方法 ×(標準ではできない) マクロはC++コードにベイクされるため
代替手段1: 実行時引数でテスト選択(+test=xxx など) ◯(推奨) $test$plusargs または $value$plusargs をSystemVerilog側で使う
代替手段2: 別々のコンパイル(各テストごとにverilator実行) ◯(よく使われる) Pythonスクリプトで管理しやすい

現実的なおすすめ方法(Verilatorでよく使われるパターン)

パターン1: 実行時引数で選択(一番柔軟・再コンパイル不要)

tb_top.sv に以下のように記述します。

initial begin
    string test_name = "";

    if ($value$plusargs("TEST=%s", test_name)) begin
        $display("Running test: %s", test_name);
    end else begin
        $fatal(1, "No test specified. Use +TEST=basic or +TEST=loopback etc.");
    end

    // ここから分岐
    if (test_name == "basic") begin
        // basicテストのシーケンス
        apb_driver.write('h03, 'h03);  // LCR 8N1
        // ...
    end
    else if (test_name == "loopback") begin
        // loopbackテスト
    end
    else if (test_name == "all_lcr") begin
        // 全LCR組み合わせテスト
    end
    else begin
        $fatal(1, "Unknown test: %s", test_name);
    end

    // テスト終了後にfinish
    repeat(100) @(posedge clk);
    $finish;
end

実行例(Python側も簡単)

./obj_dir/Vtb_top +TEST=basic
./obj_dir/Vtb_top +TEST=loopback

コンパイル不要 でテストを切り替えられるので、CI/CDでも高速。

パターン2: 各テストごとに独立コンパイル(前回の提案に近い)

# 基本テスト
verilator --binary --trace-fst ... tb_top.sv tests/test_basic.sv +define+TEST_BASIC

# LCR全組み合わせテスト
verilator --binary --trace-fst ... tb_top.sv tests/test_all_lcr_configs.sv +define+TEST_ALL_LCR

Python側で

def run(test_name):
    define = f"+define+TEST_{test_name.upper()}"
    cmd = ["verilator", "--binary", ... , define, f"tests/test_{test_name}.sv"]
    # 実行

どちらも +define+verilatorコマンド(コンパイル時) に渡すものなので問題なく使えます。

結論

  • Verilatorでも SystemVerilog標準の +define+ は完全に使えます(コンパイル時のみ)
  • 実行時にテストを切り替えたい場合は $value$plusargs / $test$plusargs を使うのが一番自然で高速
  • ifdef で無理に分けるより、1つのtbで分岐 した方がメンテナンスしやすいケースがほとんど

おわりに

Verilog HDLのRTLコードがあれば、ここまでできちゃいます。ただし、1発で Verilator にて、Build できないです。修正が結構必要です。

生成してもらったコードを何もしないで、Verilatorやmake, Python を使って、自動的に実行してもらえるようになると、楽ちんなんですが、それってできますかね。