以前UVMでDPI-Cを使ってC/C++のテストシーケンスを作る記事を書いた。
この例ではレジスタ のアドレス値をC/C++からUVM側に渡してバスにアクセスをしていた。しかし、アドレスの値ではなくて、レジスタ名でのアクセスをしたい。C/C++側でアドレスマッピングが定義されたヘッダファイルが用意されていて、それを利用できれば特に問題ないが、そうでない場合にはUVMのRAL(Register Abstraction Layer)を利用する事でC/C++からもレジスタ名でアクセスできるようになる。
UVMのRALにはレジスタ名やフィールド名をで文字列で検索してレジスタオブジェクトやフィールドオブジェクトを返してくれるAPI functionが用意されている。なので、RALのこの機能を利用してC/C++からレジスタ名、フィールド名でRAL経由でレジスタにアクセスする例を紹介する。
RAL向けUVM sequenceの拡張
UVMにはuvm_reg_sequenceと言うRALを使う上での便利なシーケンスが既に用意されていて、RALのRegister Blockへのハンドルであるmodelやレジスタアクセスの為のAPIタスクのwrite_reg(), read_reg()をメンバーとして持っている。詳細は以下参照。
なので、レジスタにアクセスを行うUVMテストシーケンスは通常はこれを継承して実装すればよい。しかし、文字列でレジスタ名・フィールド名にアクセスするようなタスクは用意されていない。そこで、まずはこのシーケンスを拡張してそのようなタスクをメンバに持つシーケンスを作る。
以下、my_uvm_pkgにmy_ral_sequene_baseと言うuvm_reg_sequenceを継承したレジスタ名でのレジスタアクセスを可能にするシーケンス作ってみた。
`ifndef MY_UVM_PKG
`define MY_UVM_PKG
`include "uvm_macros.svh"
package my_uvm_pkg;
import uvm_pkg::*;
virtual class my_ral_sequence_base#(type BASE=uvm_sequence#(uvm_reg_item)) extends uvm_reg_sequence#(BASE);
function new (string name="");
super.new(name);
auto_predict = 1;
endfunction
//--------- To Start This Sequence By Passing RAL Instead Of Sequencer ------
virtual task start_on_ral(uvm_reg_block ral_model, uvm_sequence_base parent=null);
this.model = ral_model;
this.start(ral_model.default_map.get_sequencer(), parent);
endtask
//---------------------------------------------------------------------------
string ral_name = "ral_model";
bit auto_predict = 1;
virtual function void check_model();
if (this.model == null) begin
if(!uvm_config_db#(uvm_reg_block)::get(this.get_sequencer(), this.get_sequence_path(), ral_name, this.model) )
`uvm_fatal(get_name(), {ral_name, " was not gotten!!!"})
end
endfunction
virtual task pre_body();
super.pre_body();
check_model();
this.model.default_map.set_auto_predict (auto_predict);
//this.model.default_map.set_check_on_read(1);
endtask
pure virtual task body();
uvm_status_e status;
virtual task reg_write(input string reg_name, int unsigned data);
//this.write_reg(this.model.get_reg_by_name(reg_name), status, data);
this.model.write_reg_by_name( .status(status) , .name(reg_name), .data(data), .parent(this));
if (status != UVM_IS_OK)
`uvm_error(get_name(), {reg_name, " is not found!!"})
endtask
virtual task reg_read(input string reg_name, output int unsigned data);
//this.read_reg(this.model.get_reg_by_name(reg_name), status, data);
this.model.read_reg_by_name( .status(status) , .name(reg_name), .data(data), .parent(this));
if (status != UVM_IS_OK)
`uvm_error(get_name(), {reg_name, " is not found!!"})
endtask
virtual function void reg_ralset(input string reg_name, int unsigned data);
uvm_reg l_reg = this.model.get_reg_by_name(reg_name);
if (l_reg == null) begin
`uvm_error(get_name(), {reg_name, " is not found!!"})
return;
end
l_reg.set(data);
endfunction
virtual function int unsigned reg_ralget(input string reg_name);
uvm_reg l_reg = this.model.get_reg_by_name(reg_name);
if (l_reg == null) begin
`uvm_error(get_name(), {reg_name, " is not found!!"})
return 0;
end
return(l_reg.get_mirrored_value(reg_name));
endfunction
virtual function uvm_reg_field find_fld (input string name, delimiter=".");
uvm_reg l_reg;
uvm_reg_field l_fld;
string reg_name;
string fld_name;
string split_q[$];
uvm_pkg::uvm_split_string(name, delimiter.getc(0), split_q);
if (split_q.size() != 2 )
`uvm_error(get_name(), {"Illegal Register Field Name, ", name})
reg_name = split_q[0];
fld_name = split_q[1];
l_reg = this.model.get_reg_by_name(reg_name);
if (l_reg == null) begin
`uvm_error(get_name(), {reg_name, " is not found!!"})
return (null);
end
l_fld = l_reg.get_field_by_name(fld_name);
if (l_fld == null) begin
`uvm_error(get_name(), {fld_name, " is not found!!"})
return (null);
end
return (l_fld);
endfunction
virtual function void fld_ralset(input string name, int unsigned data);
uvm_reg_field l_fld = find_fld(name);
if (l_fld == null) return;
l_fld.set(data);
endfunction
virtual function int unsigned fld_ralget(input string name);
uvm_reg_field l_fld = find_fld(name);
if (l_fld == null) return 0;
return(l_fld.get_mirrored_value());
endfunction
virtual task reg_update(input string reg_name);
this.update_reg(this.model.get_reg_by_name(reg_name), status);
if (status != UVM_IS_OK)
`uvm_error(get_name(), {reg_name, " is not found!!"})
endtask
virtual task reg_mirror(input string reg_name);
this.mirror_reg(this.model.get_reg_by_name(reg_name), status);
if (status != UVM_IS_OK)
`uvm_error(get_name(), {reg_name, " is not found!!"})
endtask
endclass
endpackage
`endif
このシーケンスクラスでは
reg_write (input string reg_name, int unsigned data);
reg_read (input string reg_name, output int unsigned data);
reg_ralset (input string reg_name, int unsigned data);
reg_ralget (input string reg_name);
fld_ralset (input string name, int unsigned data);
fld_ralget (input string name);
reg_update (input string reg_name);
reg_mirror (input string reg_name);
と言うtask/functionを用意した。
reg_write/readはそのままstringのレジスタ名でレジスタにアクセスするtask。reg/fld_ralset/getは同じくstringのレジスタ名およびフィールド名でRALにアクセスするfunction。実際のレジスタではなくRALへのアクセスなのでタスクではなくファンクション。実際のレジスタへのアクセスはreg_update/mirrorで行う。updateはwrite、mirrorはreadに対応。writeはRALにsetしてからupdate。readは逆にmirrorしてからRALからget。
uvm_reg_blockには
write_reg_by_name()
read_reg_by_name()
get_reg_by_name()
uvm_regには
get_field_by_name()
と言うstringのレジスタ名・フィールド名で検索およびアクセスするAPIが用意されている。これらを利用する事により名前でのレジスタおよびフィールドへのアクセスを可能にしている。
これで通常のUVMシーケンスでstringのレジスタ名・フィールド名でレジスタアクセスできるようになった。
以下はmy_ral_sequence_baseを使って、レジスタフィールド名でWriteアクセスをするシーケンスの例。
class example_ral_sequence extends my_uvm_pkg::my_ral_sequence_base;
`uvm_object_utils(example_ral_sequence)
.
.
.
virtual task body()
//this.reg_write("CONFIG_AAA", 100);
this.fld_ralset("CONFIG_AAA.param_0", 9);
this.fld_ralset("CONFIG_AAA.param_1", 9);
this.fld_ralset("CONFIG_AAA.param_2", 1);
this.fld_ralset("CONFIG_AAA.param_3", 2);
this.fld_ralset("CONFIG_AAA.param_4", 2);
this.fld_ralset("CONFIG_AAA.param_5", 2);
this.fld_ralset("CONFIG_AAA.param_6", 2);
this.reg_update("CONFIG_AAA");
endtask
.
.
.
endclass
DPI-Cでレジスタアクセスtask/functionのコール
次にC/C++からこれらを利用できるようにする。その為には当然DPI-Cを用いてこれらのtask/functionにアクセスするstaticなtask/functionを用意する。DPI-Cでimport/exportするtask/functionはstaticである。なのでそのままではdynamic objectであるmy_ral_sequence_baseのautomaticなtask/functionを直接コールできない。
そこでまずシーケンスをインスタンス化して、そのハンドルをstaticなfunction/taskと同じmy_uvm_pkgに置いて、そのハンドル経由でアクセスするようにする。
以下、my_uvm_pkgにそのようなtask/functionを追加してimport/exportでDPI-CでC/C++からアクセスできるようにした。
`ifndef MY_UVM_PKG
`define MY_UVM_PKG
`include "uvm_macros.svh"
package my_uvm_pkg;
import uvm_pkg::*;
virtual class my_ral_sequence_base#(type BASE=uvm_sequence#(uvm_reg_item)) extends uvm_reg_sequence#(BASE);
.
.
.
virtual task start_c_sequence(int id=0);
if (m_ral_seq != null)
`uvm_fatal("MY_UVM_PKG", $sformatf("m_ral_seq is not empty. <%s>",m_ral_seq.get_full_name()))
m_ral_seq = this;
`uvm_info(get_name(), $sformatf("start_c_sequence %s %0d",this.get_name(), id), UVM_MEDIUM)
C_reg_sequence(this.get_name(), id);
//C_reg_sequence(id);
m_ral_seq = null;
endtask
endclass
//---------- For C_Program ---------------
//import "DPI-C" context task C_reg_sequence(input int id); //input string p_name, input int id);
import "DPI-C" context task C_reg_sequence(input string p_name, input int id);
export "DPI-C" ral_reg_write = task reg_write ;
export "DPI-C" ral_reg_set = function reg_ralset;
export "DPI-C" ral_fld_set = function fld_ralset;
export "DPI-C" ral_reg_update = task reg_update;
export "DPI-C" ral_reg_read = task reg_read ;
export "DPI-C" ral_reg_mirror = task reg_mirror;
export "DPI-C" ral_reg_get = function reg_ralget;
export "DPI-C" ral_fld_get = function fld_ralget;
export "DPI-C" sv_print = function print_msg;
function void print_msg(input string msg);
$display(msg);
endfunction
my_ral_sequence_base m_ral_seq;
task reg_write(input string reg_name, input int data);
m_ral_seq.reg_write(reg_name, data);
endtask
task reg_read (input string reg_name, output int data);
m_ral_seq.reg_read(reg_name, data);
endtask
function void reg_ralset (input string reg_name, input int data);
m_ral_seq.reg_ralset (reg_name, data);
endfunction
function int reg_ralget (input string reg_name);
return (m_ral_seq.reg_ralget(reg_name));
endfunction
task reg_update(input string reg_name);
m_ral_seq.reg_update(reg_name);
endtask
task reg_mirror(input string reg_name);
m_ral_seq.reg_mirror(reg_name);
endtask
function void fld_ralset (input string reg_fld_name, input int data);
m_ral_seq.fld_ralset (reg_fld_name, data);
endfunction
function int fld_ralget (input string reg_fld_name);
return (m_ral_seq.fld_ralget(reg_fld_name));
endfunction
//---------------------------------------------------
class my_c_ral_sequence extends my_ral_sequence_base;
`uvm_object_utils(my_c_ral_sequence)
function new (string name="");
super.new(name);
endfunction
int id;
virtual task pre_body();
super.pre_body();
if (m_ral_seq != null)
`uvm_fatal(get_type_name(), $sformatf("m_ral_seq is not empty. <%s>",m_ral_seq.get_full_name()))
m_ral_seq = this;
endtask
virtual task body();
//C_reg_sequence(this.id);
C_reg_sequence(this.get_name(), this.id);
endtask
virtual task post_body();
super.post_body();
m_ral_seq = null;
endtask
endclass
//---------------------------------------------------
endpackage
`endif
インスタンス化したシーケンスのハンドルはmy_umv_pkg内の
my_ral_sequence_base m_ral_seq;
への代入を想定している。このシーケンスハンドルにアクセスするstaticなtask/functionは以下である。
reg_write ;
reg_ralset;
fld_ralset;
reg_update;
reg_read ;
reg_mirror;
reg_ralget;
fld_ralget;
これらがexportされC/C++側からコールされる。
そして、これらをコールするC/C++を走らせる必要がある。また前述のようにインスタンス化したシーケンスのハンドルをmy_uvm_pkg::m_ral_seqに割り当てる必要がある。それをやっているのがmy_ral_sequence_baseを継承したmy_c_ral_sequenceと言うシーケンスクラスである。
my_c_ral_sequenceではまずpre_body()で自身のクラスハンドルであるthisをmy_uvm_pkg::my_ral_seqに代入している。そしてbody()で、DPI-CでimportされたC/C++のfunctionがであるC_reg_sequenceをコールしている。
なのでこのUVMシーケンスであるmy_c_ral_sequenceをインスタンス化し、start()させればC/C++からRAL経由で名前でのレジスタアクセスが可能となる。
また、ついでに、my_ral_sequence_baseにも自身のクラスハンドルのmy_uvm_pkg::my_ral_seqへの代入と、C_reg_sequenceのコールを行うタスクをstart_c_sequence()としても追加した。なのでmy_ral_sequence_baseの継承クラスのbody()からstart_c_sequence()をコールしても同様の事が可能である。どちらの方法を使っても良いが、my_c_ral_sequenceを使う場合には、どのシーケンス或いはtest階層からでもC/C++を走らせる事ができる一方、my_ral_sequence_baseのstart_c_sequence()の場合には、その継承クラスシーケンスクラスからしか利用できないと言う違いがある。そもそもmy_c_ral_sequenceがmy_ral_sequence_baseの継承クラスではあるが。。。
ちなみにDPI-CでimportするC/C++のタスクはstaticnなので、競合を避ける為に、どちらも開始時にmy_uvm_pkg::my_ral_seq==nullである事をチェックし、修了時にmy_uvm_pkg::my_ral_seq=nullを代入して、自身のハンドルの代入を解除している。
以下はC/C++からのレジスターフィールド名でのWriteアクセスの例。
extern "C" void sv_print (const char *);
extern "C" void ral_reg_write (const char *, int );
extern "C" void ral_reg_set (const char *, int );
extern "C" void ral_fld_set (const char *, int );
extern "C" void ral_reg_update(const char *);
extern "C" void ral_reg_read (const char *, int *);
extern "C" void ral_reg_mirror(const char *);
extern "C" int ral_reg_get (const char *);
extern "C" int ral_fld_get (const char *);
extern "C" int C_reg_sequence(const char * p_name, int id);
int C_reg_sequence(const char * p_name, int id)
{
sv_print("-------------------------------------");
sv_print(" CONFIG_AAA Write Access");
sv_print("-------------------------------------");
ral_fld_set ("CONFIG_AAA.param_0" ,255); //8bit
ral_fld_set ("CONFIG_AAA.param_1", 254); //8bit
ral_fld_set ("CONFIG_AAA.param_2", 1); //4bit
ral_fld_set ("CONFIG_AAA.param_3", 15); //4bit
ral_fld_set ("CONFIG_AAA.param_4", 3); //2bit
ral_fld_set ("CONFIG_AAA.param_5", 3); //2bit
ral_fld_set ("CONFIG_AAA.param_6", 2); //2bit
ral_fld_set ("CONFIG_AAA.param_7", 2); //2bit
ral_reg_update("CONFIG_AAA");
sv_print("CONFIG_AAA Was Updated.");
}
以上ここで紹介したmy_uvm_pkgの内容は特にアプリケーションに特化している訳ではなく、一般的にどのアプリケーションにも適用可能である。なので、この様なUVMに不足している機能をいろいろと拡張したカスタムuvm_pkgライブラリを用意して、会社や部署単位で運用をする事をお勧めする。今回紹介したmy_uvm_pkgなんかそのままコピペして使ってもらって構わない。
動作サンプル
以下に今回紹介した方法で実際に実行可能な例を置いておく。Vivado Simulator (xsim)で実行可能なようにしてある。
git clone https://github.com/AlphaLyrae0/UVM_RAL_DPI_C_Example.git
でディレクトリクローン出来ると思う。多分。
> cd UVM_RAL_DPI_C_Example
> make all
とすればコンパイル以下3つのテストの実行をする。
- example_ral_test
- DPI-Cを使わない通常ののUVM RALシーケンスを使ったテスト。レジスタ名およびフィールド名でアクセスする。
- c_ral_test
- C/C++のシーケンスが走るテスト。レジスタ名およびフィールド名でアクセスする。
- unite_test
- 上記2つのシーケンスが続けて走るテスト。最後にもう一回start_c_sequence()を使ってC/C++のレジスタアクセスシーケンスが走る。
個別のテストを実行をするには
> make run TEST_NAME=<Test Name>
或いは
> make run_<Test Name>
とする。
因みに、DPI-CでC/C++を使うには、シミュレーション実行時に-sv_libでShared Objectを指定するようにSVのLRMでも決められていて、通常のシミュレータであればそうなっている。しかしVivado Simulator (xsim)の アホな 独自の仕様では、xelabでSVコンパイル時に指定する必要がある。DPI-Cを使う理由の一つにSVコンパイルとC/C++コンパイルをそれぞれ独立にして、分けたいと言うのがあるが、SVコンパイルにC/C++コンパイルが必要だと、せっかくのその独立性が失われてしまう。幸いなことにSVコンパイル後にC/C++のファイルを編集してC/C++だけを再コンパイルして*.soを作り直しても正常に動作するようある。なのでMakefileではそうなるようにしてある。C/C++ファイルを編集して再度テストをrunさせるとC/C++だけコンパイルされてテストの動作が変わるのを確かめてみて下さい。