はじめに
昨年の3月に、Grok 3さんに、LPDDR5 メモリコントローラを SystemVerilogで書いてもらいました。
今回は、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=basic → verilator ... 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_BASIC で ifdef 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 を使って、自動的に実行してもらえるようになると、楽ちんなんですが、それってできますかね。