Verilog RTLのデザインをSystemCで検証する環境のサンプルです。
通常、Verilog RTLのデザインは、Verilogのテストベンチでテストデータ生成や出力データの検証を行いますが、VPI(Verilog Procedural Interface)という機能を使うとSystemCでテストデータ生成や出力データの検証が行えます。
VerilogシミュレータとSystemCの連携は、商用シミュレータを使うと簡単に行えますが、無償ツールのIcarus Verilogでも連携することができました。
VPIの連携には、以下の2つのサイトを参考にしました。ありがとうございます。
・http://japanese.sugawara-systems.com/tutorial/tutorial/vpi/vpi.htm
・http://www.asic-world.com/systemc/hdl.html
サンプルの動作環境
このサンプルは次の環境で動作を確認しました。
- OS: CentOS7.7
- Icarus Verilog: 11.0
- SystemC: 2.3.3
サンプルの概要
サンプルは、Verilogでインスタンス化したmod_aという簡単なRTLに対して、SystemCからランダムデータの入力を行い、SystemCでRTLの出力とリファレンスデータの一致確認を行っています。
DUTのmod_a.vは、8ビットの2入力を内部で加算して出力する単純な回路です。
module mod_a (
input clk,
input rst_x,
input i_valid,
input [7:0] i_in_a,
input [7:0] i_in_b,
output reg o_valid,
output reg [7:0] o_out
);
always @(posedge clk or negedge rst_x) begin
if (~rst_x) begin
o_valid <= 1'b0;
o_out <= 8'h0;
end else begin
o_valid <= i_valid;
o_out <= i_in_a + i_in_b;
end
end
endmodule
サンプルの詳細
Verilog側でクロックとリセットの生成を行い、SystemC側でDUTへの入力データの生成と、DUTからの出力とリファレンスデータの一致確認を行っています。リファレンスデータの格納にはsc_fifoを利用しています。
test_1.v
Verilog側のテストベンチは、DUTのインスタンス化とクロックとリセットの生成のみを行っています。SystemCとの連携は、$sc_stubという関数を呼び出すことで行っています。clkの生成は、初期値を0にしないと(初期値が1だと)、SystemC側のclk.pos()が逆になるようです。
`timescale 1ns/1ns
module test_module;
reg clk;
reg rst_x;
reg i_valid;
reg [7:0] i_in_a;
reg [7:0] i_in_b;
wire o_valid;
wire [7:0] o_out;
mod_a _mod_a (
.clk(clk),
.rst_x(rst_x),
.i_valid(i_valid),
.i_in_a(i_in_a),
.i_in_b(i_in_b),
.o_valid(o_valid),
.o_out(o_out)
);
parameter CLK_HALF_CYCLE=5;
initial begin
clk = 0; // clear 0, not 1 otherwise systemc clk.pos() flipped
forever begin
#CLK_HALF_CYCLE clk = ~clk;
end
end
initial begin
$display("Icarus Verilog started");
$dumpvars;
// SystemC Connection
$sc_stub(clk,
rst_x,
i_valid,
i_in_a,
i_in_b,
o_valid,
o_out);
reset;
end
vpi_stub.c
Verilog側で呼び出す$sc_stubは、vpi_register_systf()で登録しています。
// my task
static void my_task()
{
s_vpi_systf_data tf_data;
tf_data.type = vpiSysTask;
tf_data.tfname = "$sc_stub";
tf_data.calltf = sc_mod_a_tf;
tf_data.compiletf = 0;
tf_data.sizetf = 0;
vpi_register_systf(&tf_data);
}
// register my task
void (*vlog_startup_routines[])() = {
my_task,
0
};
$sc_stubが呼ばれると、sc_mod_a_tf()が呼ばれます。sc_mod_a_tf()では、Verilog側の信号との接続、clkに同期した信号が変化した時のコールバック関数 sc_clk_callback()、非同期リセットrst_xが変化した時のコールバック関数sc_async_callback()の登録を行っています。 init_sc()は、SystemC側の初期化を行っています。
int sc_mod_a_tf(char *user_data) {
(中略)
// get arguments from RTL
inst_h = vpi_handle(vpiSysTfCall, 0);
args = vpi_iterate(vpiArgument, inst_h);
// set arguments
ip->clk = vpi_scan(args);
ip->rst_x = vpi_scan(args);
ip->i_valid = vpi_scan(args);
ip->i_in_a = vpi_scan(args);
ip->i_in_b = vpi_scan(args);
ip->o_valid = vpi_scan(args);
ip->o_out = vpi_scan(args);
vpi_free_object(args);
// setup callback (clk sync)
cb_data_s.user_data = (char *)ip;
cb_data_s.reason = cbValueChange;
cb_data_s.cb_rtn = sc_clk_callback; // callback
cb_data_s.time = &time_s;
cb_data_s.value = &value_s;
time_s.type = vpiSimTime;
value_s.format = vpiIntVal;
cb_data_s.obj = ip->clk;
vpi_register_cb(&cb_data_s);
// setup callback (clk async)
cb_data_s.user_data = (char *)ip;
cb_data_s.reason = cbValueChange;
cb_data_s.cb_rtn = sc_async_callback; // callback
cb_data_s.time = &time_s;
cb_data_s.value = &value_s;
time_s.type = vpiSimTime;
value_s.format = vpiIntVal;
cb_data_s.obj = ip->rst_x;
vpi_register_cb(&cb_data_s);
init_sc(); // Initialize SystemC
コールバック関数sc_clk_callback()
clkに同期した信号をSystemCに取り込み、exec_sc()で時間を進めてSystemCで生成したデータをRTLに渡しています。done信号でexit_sc()を呼び出してシミュレーションを終了します。
// callback at Value change
int sc_clk_callback(p_cb_data cb_data)
{
t_if *ip;
s_vpi_value value_s;
//static unsigned long SimNow = 0;
// IO ports systemC testbench
static IN_VECTOR invector;
static OUT_VECTOR outvector;
ip = (t_if *)cb_data->user_data;
// Read current RTL value
value_s.format = vpiIntVal;
vpi_get_value(ip->clk, &value_s);
invector.clk = value_s.value.integer;
vpi_get_value(ip->o_valid, &value_s);
invector.o_valid = value_s.value.integer;
vpi_get_value(ip->o_out, &value_s);
invector.o_out = value_s.value.integer;
// SystemC Execution
exec_sc(&invector, &outvector, (CLK_HALF_CYCLE));
// Write current RTL value
value_s.value.integer = outvector.i_valid;
vpi_put_value(ip->i_valid, &value_s, 0, vpiNoDelay);
value_s.value.integer = outvector.i_in_a;
vpi_put_value(ip->i_in_a, &value_s, 0, vpiNoDelay);
value_s.value.integer = outvector.i_in_b;
vpi_put_value(ip->i_in_b, &value_s, 0, vpiNoDelay);
if (outvector.done) {
exit_sc();
tf_dofinish();
}
return(0);
}
コールバック関数sc_async_callback()
非同期のリセット信号rst_xをSystemCに取り込んでいます。信号を取り込むだけで時間は進めていません。
int sc_async_callback(p_cb_data cb_data)
{
t_if *ip;
s_vpi_value value_s;
// IO ports systemC testbench
static IN_VECTOR invector;
ip = (t_if *)cb_data->user_data;
// Read current RTL value
value_s.format = vpiIntVal;
vpi_get_value(ip->rst_x, &value_s);
invector.rst_x = value_s.value.integer;
sample_sig(&invector);
return(0);
}
mod_a_tb.cpp
SystemC側でmod_aモジュールのインスタンス化と信号接続、VPIから呼ばれる関数の実装を行っています。exec_sc()は、clkの立下りと立ち上がりの両エッジで呼ばれるので、RTL側の信号ドライブは立下り時だけ行うようにしています。立ち上がりでRTLの信号を変化させる場合は、Verilogテストベンチ側でレーシングの対策を行う必要があります。
// test_moudle instance
mod_a u_mod_a("u_mod_a");
// signals
sc_signal<bool> clk;
sc_signal<bool> rst_x;
sc_signal<bool> i_valid;
sc_signal<sc_uint<8> > i_in_a;
sc_signal<sc_uint<8> > i_in_b;
sc_signal<bool> o_valid;
sc_signal<sc_uint<8> > o_out;
sc_signal<int> done;
// Top-Level testbench
void init_sc() {
// Port mapping
u_mod_a.clk(clk);
u_mod_a.rst_x(rst_x);
u_mod_a.i_valid(i_valid);
u_mod_a.i_in_a(i_in_a);
u_mod_a.i_in_b(i_in_b);
u_mod_a.o_valid(o_valid);
u_mod_a.o_out(o_out);
u_mod_a.done(done);
// Initialize SC
sc_start(0,SC_NS);
cout<<"#"<<sc_time_stamp()<<" SystemC started"<<endl;
}
void sample_hdl(void *In_vector) {
IN_VECTOR *p = (IN_VECTOR *)In_vector;
clk.write(p->clk);
o_valid.write(p->o_valid);
o_out.write(p->o_out);
}
void drive_hdl(void *Out_vector) {
OUT_VECTOR *p = (OUT_VECTOR *)Out_vector;
p->i_valid = i_valid.read();
p->i_in_a = i_in_a.read();
p->i_in_b = i_in_b.read();
p->done = done;
}
void advance_sim(unsigned long simtime) {
sc_start((int)simtime,SC_NS);
}
void sample_sig(void *In_vector) {
IN_VECTOR *p = (IN_VECTOR *)In_vector;
rst_x.write(p->rst_x);
}
void exec_sc(void *invector, void *outvector, unsigned long simtime) {
sample_hdl(invector);
advance_sim(simtime); // rise and fall
if (clk.read() == 0)
drive_hdl(outvector);
}
void exit_sc() {
cout<<"#"<<sc_time_stamp()<<" SystemC stopped"<<endl;
sc_stop();
}
mod_a.h
DUTへの入力データの生成、リファレンスデータの生成、信号モニター、テストシナリオのコントロールを行っています。テストシナリオのコントロール関数以外はSC_THREADです。
SC_MODULE (mod_a) {
sc_in<bool> clk;
sc_in<bool> rst_x;
sc_out<bool> i_valid;
sc_out<sc_uint<8> > i_in_a;
sc_out<sc_uint<8> > i_in_b;
sc_in<bool> o_valid;
sc_in<sc_uint<8> > o_out;
sc_out<int> done; // done signal
sc_fifo<sc_uint<8> > ref_fifo; // reference fifo
void reference();
void monitor();
void done_signal();
void stimulus();
sc_uint<8> o_out_ref;
bool error;
bool sim_done;
SC_CTOR(mod_a) {
error = false;
sim_done = false;
SC_THREAD(monitor);
sensitive << clk.pos();
SC_THREAD(reference);
sensitive << clk.pos();
SC_THREAD(done_signal);
sensitive << clk.pos();
SC_CTHREAD(stimulus,clk.pos());
}
};
sc_fifoを読み書きする関数をSC_CTHREADとすると、シミュレーション時に次のようなワーニングが表示されます。
Info: (I804) /IEEE_Std_1666/deprecated: all waits except wait() and
wait(N) are deprecated for SC_CTHREAD, use an SC_THREAD instead
mod_a.cpp
入力データの生成とテストシナリオのコントロール
mod_a::stimulus()で行っています。リセット解除を待ってから、DUTに乱数を入力しています。
void mod_a::stimulus() {
while (true) {
while (rst_x.read() == 0)
wait();
cout<<"#"<<sc_time_stamp()<<" rst_x is deasserted " << endl;
for (int i = 0; i < 10; i++) {
i_valid.write(1);
i_in_a.write(rand());
i_in_b.write(rand());
wait();
}
i_valid.write(0);
wait(10);
sim_done = true;
std::cout << "=======================================" << std::endl;
std::cout << " The simulation is successfully done." << std::endl;
std::cout << "=======================================" << std::endl;
wait();
}
}
リファレンスデータの生成
mod_a::reference()で行っています。DUTが正しく動作した場合の期待値をsc_fifoに格納しています。
void mod_a::reference() {
while (true) {
if (i_valid.read()==1) {
ref_fifo.write(i_in_a.read() + i_in_b.read());
}
wait();
}
}
信号モニター
mod_a::monitor()で信号のモニターと期待値の一致確認を行っています。DUTの出力と期待値が不一致の時はシミュレーションを終了しています。
void mod_a::monitor() {
while (true) {
// Input side
if (o_valid.read() == 1) {
std::cout << "Mon Input: #" <<sc_time_stamp() << " ";
std::cout << "i_valid = " << std::hex << i_valid.read() << " ";
std::cout << "i_in_a = " << std::hex << i_in_a.read() << " ";
std::cout << "i_in_b = " << std::hex << i_in_b.read() << std::endl;
}
// Output side
if (o_valid.read() == 1) {
std::cout << "Mon Output: #" <<sc_time_stamp() << " ";
std::cout << "o_valid = " << std::hex << o_valid.read() << " ";
std::cout << "o_out = " << std::hex << o_out.read() << std::endl;
// data check
o_out_ref = ref_fifo.read();
if (o_out_ref != o_out.read()) {
std::cout << "!!!!!! Data mismatch " <<sc_time_stamp() << " ";
std::cout << "o_out = " << std::hex << o_out.read() << " ";
std::cout << "ref = " << std::hex << o_out_ref << std::endl;
error = true;
wait();
}
}
wait();
}
}
コンパイルと実行のスクリプト
iverilogでRTLをコンパイルします。cとcppはiverilog-vpiでコンパイルします。LD_LIBRARY_PATHにSystemCのライブラリを指定しないと、実行時にエラーになります。コンパイル時に-rpathを指定することで不要にできるかもしれません。
# compile verilog
iverilog -c ../bin/cmd.txt \
-v \
-y ${RTL_DIR} \
-o ${TOP_MODULE} \
${sim_file}
# compile systemc
iverilog-vpi \
-I${SYSC_DIR} \
-I${STUB_DIR} \
-I${SYSTEMC_DIR}/include \
-L${SYSTEMC_DIR}/lib-linux64 \
${STUB_DIR}/vpi_stub.c \
${SYSC_DIR}/mod_a.cpp \
${SYSC_DIR}/mod_a_tb.cpp \
-lstdc++ -lsystemc
# simulation
export LD_LIBRARY_PATH=${SYSTEMC_DIR}/lib-linux64
vvp -M. -mvpi_stub ${TOP_MODULE} -v
実行結果
シミュレーションを実行すると、次のようなログが表示されます。SystemC側で生成された入力データでDUTが動作しているのが分かります。
Icarus Verilog started
VCD info: dumpfile dump.vcd opened for output.
#0 s SystemC started
#100 ns rst_x is deasserted
Mon Input: #120 ns i_valid = 1 i_in_a = 069 i_in_b = 073
Mon Output: #120 ns o_valid = 1 o_out = 02d
Mon Input: #130 ns i_valid = 1 i_in_a = 051 i_in_b = 0ff
Mon Output: #130 ns o_valid = 1 o_out = 0dc
Mon Input: #140 ns i_valid = 1 i_in_a = 04a i_in_b = 0ec
Mon Output: #140 ns o_valid = 1 o_out = 050
Mon Input: #150 ns i_valid = 1 i_in_a = 029 i_in_b = 0cd
Mon Output: #150 ns o_valid = 1 o_out = 036
Mon Input: #160 ns i_valid = 1 i_in_a = 0ba i_in_b = 0ab
Mon Output: #160 ns o_valid = 1 o_out = 0f6
Mon Input: #170 ns i_valid = 1 i_in_a = 0f2 i_in_b = 0fb
Mon Output: #170 ns o_valid = 1 o_out = 065
Mon Input: #180 ns i_valid = 1 i_in_a = 0e3 i_in_b = 046
Mon Output: #180 ns o_valid = 1 o_out = 0ed
Mon Input: #190 ns i_valid = 1 i_in_a = 07c i_in_b = 0c2
Mon Output: #190 ns o_valid = 1 o_out = 029
Mon Input: #200 ns i_valid = 1 i_in_a = 054 i_in_b = 0f8
Mon Output: #200 ns o_valid = 1 o_out = 03e
Mon Input: #210 ns i_valid = 0 i_in_a = 054 i_in_b = 0f8
Mon Output: #210 ns o_valid = 1 o_out = 04c
=======================================
The simulation is successfully done.
=======================================
#310 ns SystemC stopped
Info: /OSCI/SystemC: Simulation stopped by user.