初めての投稿になります!
基本Qiitaは読んでばかりなのですが、たまには書く方もしてみようかなと、ふと思い立った次第です。
というかHDL系の話題ってQiitaではあんまり多くないですよね。
やっている人が単純に少ないのか、あまりオープンな文化じゃないのか、真実の程は定かではありませんが・・・逆に言えば、まだまだ貢献の余地が残っているということです。
僕自身、エンジニアとしては未熟ですが、誰かのお役に立てれば光栄です。間違っていたらご指摘くださいね。
「動的な世界」と「静的な世界」を結びつける
SystemVerilogでは、Verilog HDLからの機能拡張として、C++のようなクラスの概念が導入されました。クラスを使うことのメリットは、テスト環境を動的に構築できることにあります。テストの開始から終了の間に、クラスの生成や多態性を駆使することによって、テスト環境を変えることができるのです。
一方で、そもそもHDLはデジタル回路をデザインするための言語なので、テスト対象のデザイン(= DUT、Design Under Test)は静的な要素になります。ASICやFPGAの動作中に、回路が動的に生成されたら怖いですよね。そのトランジスタはどこからやってきたのって感じです笑
ここで疑問が生じます。クラスが持たらした「動的な世界」、DUTが存在する「静的な世界」、これらをどう結びつければいいのでしょうか。以下の論文には、検証のプロによる回答がきれいにまとまっています。
Daviid Rich著:The Missing Link: The Testbench to DUT Connection
論文を読みたい方は、みんな大好きVerification Academyにあります。論文によると、以下の2つの方法があるようです。
- 仮想インタフェース(Virtual Interface)を使う
- 抽象クラス(Abstract Class)を使う
本稿では、UVMによる記述例を使って、この2つの方法を紹介したいと思います。
仮想インタフェース(Virtual Interface)
テスト用クラスのプロパティに仮想インタフェースを用意し、仮想インタフェースを通してDUTにアクセスする方法です。一からテスト環境を構築する場合、普通はこちらを使うのではないでしょうか。UVMのチュートリアルをやっていると、必ず仮想インタフェースの節が存在するほどです。
それでは、ソースコードの紹介に入ります。ダミープロジェクトということで、接頭語にdummybusをつけることにします。
まずはDUTがないと始まりません。4kByteのシングルポートRAMにします。
module ram (
input wire clk,
input wire [31:0] w_data,
input wire [11:0] addr,
input wire write,
output wire [31:0] r_data
);
reg [31:0] ram [1023:0];
reg [31:0] addr_q;
always @ (posedge clk)
begin
if (write)
ram[addr] <= w_data;
addr_q <= addr;
end
assign r_data = ram[addr_q];
endmodule
次にインタフェースを定義します。DUTのポートをそのまま持ってきます。
interface dummybus_if (input clk);
logic [11:0] addr;
logic [31:0] w_data;
logic [31:0] r_data;
logic write;
endinterface: dummybus_if
DUTへのトランザクションを定義します。アドレスとデータ、Read/Writeがあれば十分でしょうか。
typedef enum bit {READ, WRITE} direction_e;
class dummybus_item extends uvm_sequence_item;
`uvm_object_utils(dummybus_item)
bit [9:0] addr;
bit [31:0] data;
direction_e direction;
function new(string name = "dummybus_item");
super.new(name);
endfunction: new
function void set_transaction (
bit [9:0] addr,
bit [31:0] data,
direction_e direction);
this.addr = addr;
this.data = data;
this.direction = direction;
endfunction: set_transaction
endclass: dummybus_item
トランザクションを信号に変換するためのクラスを作ります。先ほど作成したインタフェースを、仮想(virtual)インタフェースとしてプロパティに設定します。仮想インタフェースを経由して、DUTへの信号をドライブ、あるいはDUTからの信号をモニタします。
class dummybus_vif_driver extends uvm_driver # (dummybus_item);
virtual dummybus_if vif;
`uvm_component_utils(dummybus_vif_driver);
function new(string name , uvm_component parent);
super.new(name, parent);
endfunction: new
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
if (!uvm_config_db#(virtual dummybus_if)::get(this, get_full_name(), "vif", vif))
`uvm_error("NO_VIF", "Failed to getting virtual interface")
endfunction: connect_phase
task run_phase(uvm_phase phase);
forever begin
seq_item_port.get_next_item(req);
repeat (2) @(posedge vif.clk);
if (req.direction == READ) begin
vif.addr = req.addr;
vif.write = 1'b0;
repeat (2) @(posedge vif.clk);
$display("[VIF] time:%0t, Read %0d from the address %0d",$time, vif.r_data, vif.addr);
end
else begin
vif.addr = req.addr;
vif.w_data = req.data;
vif.write = 1'b1;
repeat (2) @(posedge vif.clk);
$display("[VIF] time:%0t, Write %0d in the address %0d",$time, vif.w_data, vif.addr);
end
seq_item_port.item_done(req);
end
endtask: run_phase
endclass: dummybus_vif_driver
後はテストベンチにて、DUTとインタフェースを配置、インタフェースと仮想インタフェースを結びます。
これにて仮想インタフェースでDUTにアクセスする仕組みの完成です!
動作するソースコードはまとめて最後に紹介します。
抽象クラス(Abstract Class)
テスト用クラスのプロパティとして抽象クラスを用意し、抽象クラスを通してDUTにアクセスする方法です。既存の検証用資産を流用したい場合、こちらの方法を使って解決することができます。というか、僕が本稿を書いているモチベーションはここにあります。この方法を知るまでに、Verification Academyの質問フォームをどれだけ辿ったことか・・・。
利用シーンの例を紹介します。BFM(Bus Function Model)がVerilog HDLによるモジュールとして既にあります。モジュールは静的な要素であるため、クラスのプロパティとして持つことはできません。かといって、BFMをクラス形式に書き換えるのは面倒です。そもそも、ベンダが提供するBFMだと手の付けようがありません。さて、この資産を活用するにはどうしたらいいでしょうか。
オブジェクト指向言語を勉強した人なら、Adapterパターンという単語を目にしたことがあると思います。「新しいクラス」と「既存クラス」のインタフェースに不整合があるとき、「新しいクラス」から「既存クラス」を扱うためのアダプタを作ろう、というやつです。「既存クラス」を弄らないことがポイントです。このベストプラクティスに従って、上記問題を解決します。
先ほどのRAM(ram.v)に対し、以下のようなBFMが既にあるものとします。タスクの中身は面倒なので省略しています。
module dummybus_bfm(
input clk
);
task send_transaction(
input [11:0] addr, input [31:0] data, input direction
);
begin
@(posedge clk);
if (direction == 1'b0)
$display("[BFM] time:%0t, Read a value from the address %0d",$time, addr);
else
$display("[BFM] time:%0t, Write %0d in the address %0d",$time, data, addr);
end
endtask
endmodule
BFMに対応した抽象クラスを作成します。クラス定義にvirutal修飾子、ファンクション・タスク定義にはpure virtual修飾子を付けます。
virtual class dummybus_abstract;
pure virtual task send_transaction(bit[11:0] addr, bit[31:0] data, bit direction);
endclass: dummybus_abstract
トランザクション(dummybus_item.svh)を抽象クラスで扱える形式に変換するクラスを作成します。トランザクションの中身に応じて、抽象クラスの持つファンクションやタスクをコールします。
class dummybus_bfm_driver extends uvm_driver # (dummybus_item);
dummybus_abstract bfm_adapt;
`uvm_component_utils(dummybus_bfm_driver);
function new(string name , uvm_component parent);
super.new(name, parent);
endfunction: new
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
if (!uvm_config_db#(dummybus_abstract)::get(this, get_full_name(), "bfm_adapt", bfm_adapt))
`uvm_error("NO_BFM", "Failed to getting concrete class")
endfunction: connect_phase
task run_phase(uvm_phase phase);
forever begin
seq_item_port.get_next_item(req);
bfm_adapt.send_transaction(req.addr, req.data, req.direction);
seq_item_port.item_done(req);
end
endtask: run_phase
endclass: dummybus_bfm_driver
抽象クラスを派生し、具象クラスを作成します。ここでBFMにアクセスしています。
`define BFM $root.tb.bfm
class dummybus_concrete extends dummybus_abstract;
task send_transaction(bit[11:0] addr, bit[31:0] data, bit direction);
`BFM.send_transaction(addr, data, direction);
endtask;
endclass: dummybus_concrete
ここで注意したいのは、具象クラスからBFMにアクセスする際、$rootから階層的にインスタンスツリーを辿っていることです。つまり、具象クラスはテストベンチの構造ありきで作っています。当然のことながら、テストベンチが変わると、具象クラスも変える必要があります。
UVMでは、テスト用のクラス一式をパッケージにまとめることが推奨されていますが、具象クラスはパッケージに入れません(というか、パッケージを使う理由が、$root文を弾くため、という記述を見た記憶があります)。テストベンチに直接配置またはインクルードして使います。
後はテストベンチにて、DUTとBFMを配置し、具象クラスを実体、具象クラスと抽象クラスを結びます。
これにて、抽象クラスでDUTにアクセスする仕組みの完成です!
動作するソースコードはまとめて最後に紹介します。
まとめ、その他コード一式
テスト用クラスからDUTにアクセスするための代表的な方法を紹介しました。
個人的な意見としては、特に制約がなければ仮想インタフェースがお勧めです。
既にBFMがある場合は、BFMを書き直すのではなく、仮想クラスを使いましょう。
最後に、僕が動作確認したコードを紹介します。既に紹介したコードは再掲しません。シミュレータはQuartus Prime Lite Edition 18.1についてきたModelsim Intel Starter Edition 10.5bを使っています。無料なので、お金を積まなくても試せると思います。
・パッケージ
package dummybus_pkg;
import uvm_pkg::*;
`include "uvm_macros.svh"
`include "dummybus_abstract.svh"
`include "dummybus_item.svh"
`include "dummybus_vif_driver.svh"
`include "dummybus_bfm_driver.svh"
`include "dummybus_sequence.svh"
`include "dummybus_test.svh"
endpackage: dummybus_pkg
・UVMシーケンス
class dummybus_sequence extends uvm_sequence #(dummybus_item);
`uvm_object_utils(dummybus_sequence);
dummybus_item item;
function new(string name = "");
super.new(name);
endfunction: new
virtual task body;
item = dummybus_item::type_id::create("item");
start_item(item);
item.set_transaction(12'h100, 32'h1, WRITE);
finish_item(item);
start_item(item);
item.set_transaction(12'h100, 32'h1, READ);
finish_item(item);
endtask: body
endclass: dummybus_sequence
・UVMテスト
面倒なので、仮想インタフェースと抽象クラスのテストを一緒にしています。
class dummybus_test extends uvm_test;
`uvm_component_utils(dummybus_test)
dummybus_bfm_driver bfm_drv;
dummybus_vif_driver vif_drv;
uvm_sequencer #(dummybus_item) bfm_seqr;
uvm_sequencer #(dummybus_item) vif_seqr;
dummybus_sequence seq;
function new(string name = "dummybus_test", uvm_component parent = null);
super.new(name, parent);
endfunction: new
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
bfm_drv = dummybus_bfm_driver::type_id::create("bfm_drv",this);
vif_drv = dummybus_vif_driver::type_id::create("vif_drv",this);
bfm_seqr = new("bfm_seqr",this);
vif_seqr = new("vif_seqr",this);
seq = dummybus_sequence::type_id::create("seq",this);
endfunction: build_phase
function void connect_phase(uvm_phase phase);
bfm_drv.seq_item_port.connect(bfm_seqr.seq_item_export);
vif_drv.seq_item_port.connect(vif_seqr.seq_item_export);
endfunction: connect_phase
task run_phase(uvm_phase phase);
phase.raise_objection(this);
seq.start(bfm_seqr);
seq.start(vif_seqr);
phase.drop_objection(this);
endtask: run_phase
endclass: dummybus_test
・テストベンチ
仮想インタフェースはDUT(ram)を駆動しますが、抽象クラスはメッセージを出すだけです。
import uvm_pkg::*;
import dummybus_pkg::*;
module tb ();
`include "uvm_macros.svh"
`include "dummybus_concrete.svh"
localparam CLOCK_PERIOD = 100;
reg clk = 1'b0;
dummybus_concrete bfm_adapt;
dummybus_bfm bfm(
.clk (clk)
);
dummybus_if vif(
.clk (clk)
);
always begin
#(CLOCK_PERIOD / 2);
clk = ~clk;
end
ram inst_ram(
.clk (vif.clk),
.w_data (vif.w_data),
.addr (vif.addr),
.write (vif.write),
.r_data (vif.r_data)
);
initial begin
bfm_adapt = new();
uvm_config_db#(dummybus_abstract)::set(null, "*", "bfm_adapt", bfm_adapt);
uvm_config_db#(virtual dummybus_if)::set(null, "*", "vif", vif);
run_test();
end
endmodule
・Modelsimのコマンドスクリプト
コマンドプロンプトやシェル等で、「vsim -c -do "run.do"」で実行できます。UVM_SRCのパスは、UVMライブラリのトップを指定してください。僕の場合、Quartus Primeについてきたライブラリを指定しています。
set UVM_SRC C:/intelFPGA_lite/18.1/modelsim_ase/verilog_src/uvm-1.2/src
vlib work
vlog -sv $UVM_SRC/uvm.sv +incdir+$UVM_SRC $UVM_SRC/dpi/uvm_dpi.cc -ccflags -DQUESTA
vlog src/ram.v
vlog src/dummybus_bfm.v
vlog -sv src/dummybus_if.sv
vlog -sv src/dummybus_pkg.sv +incdir+$UVM_SRC
vlog -sv src/tb.sv +incdir+$UVM_SRC
vsim -novopt +UVM_TESTNAME=dummybus_test -L work tb -do "run -all"
exit
以上です。