はじめに
xAI の Grok さんに、
Grok さんは、SystemVerilogのテストベンチとDPI-Cを使ったC++のテストプログラムでのテスト環境を作れるますか? 対象は、APB(Arm Peripheral Bus) Interface の SPI コントローラです。
をお願いしてみました。
はい、SystemVerilogのテストベンチとDPI-C(Direct Programming Interface for C)を活用したC++のテストプログラムで、APBインターフェース付きのSPIコントローラーのテスト環境を作成できます。以下で、基本的な構造とサンプルコードを説明します。
全体の概要
- 対象DUT (Design Under Test): APBスレーブとして動作するSPIコントローラー。APB経由でレジスタアクセスを行い、SPIマスター/スレーブとして外部デバイスと通信。
- テスト環境の構成:
- SystemVerilogテストベンチ: DUTをインスタンス化し、APBバス信号を駆動。DPI-Cを使ってC++側からテストシーケンスを呼び出し。
- C++テストプログラム: DPI-C経由でSystemVerilog側を制御。テストケースを実装(例: レジスタ書き込み/読み込み、SPIデータ転送)。
- シミュレーション環境: Synopsys VCS、Cadence Xcelium、Mentor Questaなどのツールでコンパイル・実行。DPI-CでC++をリンク。
- 前提: SPIコントローラーのRTL(Verilog/SystemVerilog)が既に存在するものと仮定。APBは標準的なプロトコル(PSEL, PENABLE, PADDR, PWRITE, PWDATA, PRDATAなど)を使用。
- 注意: このサンプルは簡易版。実際のプロジェクトでは、UVM(Universal Verification Methodology)で拡張したり、エラー処理を追加してください。コンパイル時は、DPIヘッダーを生成し、C++を共有ライブラリとしてリンク。
1. SystemVerilogテストベンチ (testbench.sv)
このファイルでDUTをインスタンス化し、DPI-C関数を宣言。C++から呼び出されるエクスポート関数を実装。
// testbench.sv
`include "spi_controller.sv" // DUTのRTLファイルをインクルード(仮定)
import "DPI-C" function void c_test_init(); // C++から呼び出す初期化関数(オプション)
export "DPI-C" function sv_apb_write; // C++から呼び出されるAPB書き込み関数
export "DPI-C" function sv_apb_read; // C++から呼び出されるAPB読み込み関数
export "DPI-C" function sv_spi_check; // SPI転送結果チェック関数(例)
module testbench;
// APB信号定義
logic clk, rst_n;
logic psel, penable, pwrite;
logic [31:0] paddr, pwdata;
logic [31:0] prdata;
logic pready, pslverr;
// SPI信号定義(例: マスター側)
logic spi_mosi, spi_miso, spi_sck, spi_cs;
// DUTインスタンス
spi_controller dut (
.clk(clk),
.rst_n(rst_n),
.psel(psel),
.penable(penable),
.pwrite(pwrite),
.paddr(paddr),
.pwdata(pwdata),
.prdata(prdata),
.pready(pready),
.pslverr(pslverr),
.spi_mosi(spi_mosi),
.spi_miso(spi_miso), // 外部デバイスからフィードバック(テストベンチでモデル化)
.spi_sck(spi_sck),
.spi_cs(spi_cs)
);
// クロック生成
initial begin
clk = 0;
forever #5 clk = ~clk;
end
// リセット
initial begin
rst_n = 0;
#10 rst_n = 1;
end
// DPI-Cエクスポート関数: APB書き込み
function void sv_apb_write(input int addr, input int data);
@(posedge clk);
psel = 1;
penable = 0;
paddr = addr;
pwrite = 1;
pwdata = data;
@(posedge clk);
penable = 1;
wait(pready);
psel = 0;
penable = 0;
endfunction
// DPI-Cエクスポート関数: APB読み込み
function int sv_apb_read(input int addr);
int data;
@(posedge clk);
psel = 1;
penable = 0;
paddr = addr;
pwrite = 0;
@(posedge clk);
penable = 1;
wait(pready);
data = prdata;
psel = 0;
penable = 0;
return data;
endfunction
// DPI-Cエクスポート関数: SPI転送チェック(例: 簡易)
function void sv_spi_check();
// ここでSPI信号を監視し、結果をC++に返す(実装例省略)
$display("SPI check performed");
endfunction
// テスト開始(C++から制御される)
initial begin
// C++側から制御されるため、ここは待機
#1000 $finish; // タイムアウト終了
end
endmodule
2. DPI-Cヘッダーファイル (dpi_header.h)
シミュレータで生成されるDPIヘッダー(例: svdpi.h)を基に、C++側で使用。実際にはシミュレータが自動生成しますが、参考として。
// dpi_header.h (シミュレータ生成例) extern void sv_apb_write(int addr, int data); extern int sv_apb_read(int addr); extern void sv_spi_check();
3. C++テストプログラム (test_program.cpp)
DPI関数を呼び出してテストを実行。コンパイル時は共有ライブラリ(.so)としてビルド。
// test_program.cpp #include <iostream> #include "dpi_header.h" // DPI関数宣言 int main() { std::cout << "Starting C++ test program for APB-SPI Controller" << std::endl; // テストケース1: レジスタ書き込み(例: SPIコントロールレジスタ addr=0x00, data=0x01で有効化) sv_apb_write(0x00, 0x01); std::cout << "Wrote to control register" << std::endl; // テストケース2: データ書き込み(例: TXデータレジスタ addr=0x04, data=0xAA) sv_apb_write(0x04, 0xAA); std::cout << "Wrote TX data" << std::endl; // テストケース3: レジスタ読み込み(例: ステータスレジスタ addr=0x08) int status = sv_apb_read(0x08); std::cout << "Read status: " << std::hex << status << std::endl; // テストケース4: SPI転送チェック sv_spi_check(); std::cout << "SPI check completed" << std::endl; // 追加のテストケースをここに実装(ループ、条件分岐など) std::cout << "Test completed" << std::endl; return 0; }
4. ビルドと実行方法
- コンパイル:
- SystemVerilog:
vcs -sverilog -dpi -P dpi.tab testbench.sv -o simv(VCS例。dpi.tabでDPI関数を指定)。 - C++:
g++ -shared -o test_lib.so test_program.cpp(共有ライブラリとして)。 - リンク: シミュレータでC++ライブラリをロード(例: VCSの
-loadオプション)。
- SystemVerilog:
- 実行:
./simv -guiでシミュレーション開始。C++側はDPI経由で呼び出されるが、メインはテストベンチ側。C++をスタンドアロンでテストしたい場合、モックDPI関数を追加。 - 拡張: SPIスレーブ側をテストベンチでモデル化(例: BFM - Bus Functional Model)。エラー注入やランダムテストをC++で実装。
この環境で、C++から柔軟にテストシーケンスを記述でき、SystemVerilogでハードウェア信号を扱えます。実際のSPIコントローラーのレジスタマップに合わせてアドレス/データを調整してください。追加の詳細が必要なら、教えてください!
すぐに実行したいので、Makefile も作ってもらいました。
以下は、前回のサンプル(testbench.sv + test_program.cpp)をVCSでDPI-Cを使って実行するためのMakefile例です。
主な前提:
- シミュレータ:Synopsys VCS(64bit版を想定)
- C++コードは共有ライブラリ(.so)としてコンパイル
- C++側からSVのexport関数を呼び出すスタイル(前回の例と同じ)
- ファイル名:
- testbench.sv …… テストベンチ(DUT含む)
- test_program.cpp …… C++テストプログラム
- dpi_header.h …… (シミュレータが生成するsvdpi.hなどをインクルード)
Makefile (VCS向け)
# Makefile for APB-SPI Controller DPI-C testbench with VCS
# ------------------------------
# 基本設定
# ------------------------------
VCS = vcs
VCS_OPTS = -sverilog -full64 -debug_all -timescale=1ns/1ps \
-notice -line +v2k -kdb +acc+rw \
+define+SIMULATION
# C++コンパイラ設定
CXX = g++
CXXFLAGS = -Wall -Wextra -g -fPIC -m64 -std=c++11 -O2
LDFLAGS = -shared
# ファイル名
SV_TOP = testbench
SV_FILE = $(SV_TOP).sv
CPP_FILE = test_program.cpp
LIB_NAME = dpi_test
LIB_FILE = lib$(LIB_NAME).so
# 実行ファイル名
SIMV = simv
# 出力ディレクトリ(任意)
OUT_DIR = ./build
LOG_FILE = $(OUT_DIR)/compile.log
# ------------------------------
# ターゲット
# ------------------------------
all: $(SIMV)
# 1. C++を共有ライブラリにコンパイル
$(LIB_FILE): $(CPP_FILE)
@mkdir -p $(OUT_DIR)
$(CXX) $(CXXFLAGS) $^ $(LDFLAGS) -o $(OUT_DIR)/$@
# 2. VCSでSystemVerilog + DPI共有ライブラリをコンパイル
$(SIMV): $(SV_FILE) $(LIB_FILE)
@mkdir -p $(OUT_DIR)
$(VCS) $(VCS_OPTS) \
$(SV_FILE) \
$(OUT_DIR)/$(LIB_FILE) \
+incdir+. \
-L$(OUT_DIR) -l$(LIB_NAME) \
-o $(OUT_DIR)/$@ 2>&1 | tee $(LOG_FILE)
# シミュレーション実行(GUIあり)
run: $(SIMV)
@cd $(OUT_DIR) && ./$(SIMV) -gui +UVM_NO_RELNOTES &
# コマンドライン実行(GUIなし)
run-cli: $(SIMV)
@cd $(OUT_DIR) && ./$(SIMV) -ucli -do "run -all; quit -f"
# クリーン
clean:
rm -rf $(OUT_DIR) csrc *.da *.log *.key DVEfiles *.h *.tab *.so ucli.key
.PHONY: all run run-cli clean
使い方
ファイルを同じディレクトリに置く
testbench.sv
test_program.cpp実行例
# 全部ビルド make # ビルドしてGUIでシミュレーション開始 make run # コマンドラインで最後まで走らせる make run-cli # クリーンアップ make clean
よく使われる追加オプション(必要に応じて追加)
| 目的 | VCSオプション例 | Makefileへの追加例 |
|---|---|---|
| UVMを使う場合 | -ntb_opts uvm |
VCS_OPTS += -ntb_opts uvm |
| 波形をfsdbで保存 | +fsdbfile=wave.fsdb -debug_access+all |
VCS_OPTS += +fsdbfile=wave.fsdb ... |
| Verdiデバッグ強化 | -debug_access+all -kdb |
既に入っている |
| C++側にSVヘッダが必要な場合 | -P dpi.tab(古いスタイル) |
最近は不要なことが多い |
| 64bit明示 | -full64 |
既に入っている |
| より詳細なログ | -notice -diag |
VCS_OPTS += -notice -diag |
注意点(2025-2026年現在のVCS事情)
-full64は最近でもよく使われますが、最新VCSではデフォルト64bitの場合もあるので、環境次第で削除可。lib*.soを-Ldir -l名でリンクする方法が最近は推奨されています(直接.soを指定する方法もまだ動きますが)。- C++側で
extern "C"が必要な関数は、export/importで正しくマングリング回避してください。 svdpi.hはVCSコンパイル時に自動生成されるので、通常はtest_program.cppで#include "svdpi.h"と書けばOKです。
テストプログラムは、1つだけじゃないので、その対応もしてもらった
テストプログラムは、1つだけじゃなくて、たくさんあるんだよ。その点も対応して。
はい、了解しました!
テストプログラムが複数ある場合(例: test_basic.cpp, test_stress.cpp, test_error.cpp など)、それぞれを独立したC++テストケースとして扱い、1回のシミュレーションで1つのテストプログラムだけを実行するのが最もシンプルで一般的です。
以下のようにMakefileを拡張します。
改善方針
- C++ソースを
tests/ディレクトリにまとめる - 各テストプログラム → 対応する共有ライブラリ(例:
libtest_basic.so,libtest_stress.so)を生成 - シミュレーション実行時に どのテストを使うか引数で指定(
make run TEST=basicのように) - デフォルトは
basicなど
ディレクトリ構造例(推奨)
project/ ├── testbench.sv # SVテストベンチ(変更なし) ├── spi_controller.sv # DUT ├── tests/ # ← ここに複数テストを置く │ ├── test_basic.cpp │ ├── test_stress.cpp │ ├── test_error_injection.cpp │ └── common_dpi.cpp # (オプション)共通コードがあれば ├── Makefile └── build/ # 出力先(自動生成)
更新版 Makefile(複数テスト対応)
# Makefile for APB-SPI Controller DPI-C testbench with multiple C++ tests (VCS)
# ------------------------------
# 設定
# ------------------------------
VCS = vcs
VCS_OPTS = -sverilog -full64 -debug_all -timescale=1ns/1ps \
-notice -line +v2k -kdb +acc+rw \
+define+SIMULATION
CXX = g++
CXXFLAGS = -Wall -Wextra -g -fPIC -m64 -std=c++17 -O2 -I.
LDFLAGS = -shared
SV_TOP = testbench
SV_FILE = $(SV_TOP).sv
OUT_DIR = ./build
LOG_FILE = $(OUT_DIR)/compile.log
# テストプログラム一覧(拡張しやすいよう変数化)
TESTS ?= basic stress error_injection # ← ここにテスト名を追加していく
# デフォルトのテスト(make run だけ叩いたとき)
DEFAULT_TEST = basic
# 自動で .cpp と .so を対応付ける
TEST_CPPS = $(addprefix tests/test_,$(addsuffix .cpp,$(TESTS)))
TEST_LIBS = $(addprefix $(OUT_DIR)/libtest_,$(addsuffix .so,$(TESTS)))
# 現在選択中のテスト(make run TEST=xxx で上書き)
TEST ?= $(DEFAULT_TEST)
CURRENT_LIB = $(OUT_DIR)/libtest_$(TEST).so
# ------------------------------
# メインターゲット
# ------------------------------
all: $(SIMV) $(TEST_LIBS)
simv: $(OUT_DIR)/$(SV_TOP)
# 1. 各テストプログラム → 共有ライブラリ生成
$(OUT_DIR)/libtest_%.so: tests/test_%.cpp
@mkdir -p $(OUT_DIR)
$(CXX) $(CXXFLAGS) $< $(LDFLAGS) -o $@
# 共通コードがある場合はここに追加例
# $(OUT_DIR)/libtest_%.so: tests/test_%.cpp tests/common_dpi.o
# $(CXX) $(CXXFLAGS) $^ $(LDFLAGS) -o $@
# 2. VCSコンパイル(SV + 選択したテストのライブラリをリンク)
$(OUT_DIR)/$(SV_TOP): $(SV_FILE) $(CURRENT_LIB)
@mkdir -p $(OUT_DIR)
$(VCS) $(VCS_OPTS) \
$(SV_FILE) \
$(CURRENT_LIB) \
+incdir+. \
-L$(OUT_DIR) -ltest_$(TEST) \
-o $@ 2>&1 | tee $(LOG_FILE)
# ------------------------------
# 実行コマンド
# ------------------------------
# GUIで実行(例: make run TEST=stress)
run: $(OUT_DIR)/$(SV_TOP)
@echo "Running simulation with test: $(TEST)"
@cd $(OUT_DIR) && ./$(SV_TOP) -gui +UVM_NO_RELNOTES &
# コマンドライン実行(最後まで走らせて終了)
run-cli: $(OUT_DIR)/$(SV_TOP)
@echo "Running CLI simulation with test: $(TEST)"
@cd $(OUT_DIR) && ./$(SV_TOP) -ucli -do "run -all; quit -f"
# 全部のテストを順番に実行(CI/回帰用)
regression: all
@for t in $(TESTS); do \
echo "===== Running test: $$t ====="; \
$(MAKE) run-cli TEST=$$t || exit 1; \
done
# クリーン
clean:
rm -rf $(OUT_DIR) csrc *.da *.log *.key DVEfiles *.h *.tab ucli.key *.so *.dSYM
# ヘルプ
help:
@echo "Usage:"
@echo " make # デフォルトテスト($(DEFAULT_TEST))をビルド"
@echo " make all # 全テストのライブラリ + simv"
@echo " make run TEST=stress # stressテストをGUIで実行"
@echo " make run-cli TEST=error_injection"
@echo " make regression # 全テストを順番にCLI実行"
@echo "Available tests: $(TESTS)"
.PHONY: all run run-cli regression clean help
使い方の例
# デフォルト(basic)でビルド & GUI実行 make run # stressテストだけ実行 make run TEST=stress # エラー注入テストをCLIで make run-cli TEST=error_injection # 全部のテストを一気に回帰実行 make regression # 新しいテストを追加したいとき # 1. tests/test_new_feature.cpp を作成 # 2. Makefileの TESTS に new_feature を追加 # → 自動で対応
C++側の実装Tips(複数テスト共通化)
各 test_xxx.cpp で以下のように書くと便利です:
// tests/test_basic.cpp #include "svdpi.h" extern "C" { extern void sv_apb_write(int addr, int data); extern int sv_apb_read(int addr); // ... } int main() { printf("[BASIC] Starting test...\n"); sv_apb_write(0x00, 0x00000001); // SPI enable sv_apb_write(0x04, 0x0000005A); // TX data // ... 基本シーケンス printf("[BASIC] Test done.\n"); return 0; }
- 共通部分(ログ関数、エラーチェック、ユーティリティ)は
tests/common.hppや別ファイルに分けてインクルード - テスト名を環境変数や引数で受け取って表示させることも可能(
getenv("TEST_NAME")など)
これでテストケースが増えてもメンテナンスしやすく、拡張しやすい形になります。
何か特定のテストケース例(stressやerror injectionなど)も欲しい場合、教えてください!
おわりに
サクッと、作ってくれました。
2009年12月に下記のようなことをやりましたが、このくらいのことなら、生成AIさんたちは、もうできちゃいますね。。。(笑)










