UVMでDUTへのアクセス方法をまとめる

初めての投稿になります！
基本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にあります。論文によると、以下の２つの方法があるようです。

- 仮想インタフェース（Virtual Interface）を使う
- 抽象クラス（Abstract Class）を使う

本稿では、UVMによる記述例を使って、この２つの方法を紹介したいと思います。

## 仮想インタフェース（Virtual Interface）
テスト用クラスのプロパティに仮想インタフェースを用意し、仮想インタフェースを通してDUTにアクセスする方法です。一からテスト環境を構築する場合、普通はこちらを使うのではないでしょうか。UVMのチュートリアルをやっていると、必ず仮想インタフェースの節が存在するほどです。
![VIF.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/628886/a0c6c5e9-35f6-9749-8960-83b13ff88b6e.png)
それでは、ソースコードの紹介に入ります。ダミープロジェクトということで、接頭語にdummybusをつけることにします。
まずはDUTがないと始まりません。4kByteのシングルポートRAMにします。

```verilog:ram.v
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のポートをそのまま持ってきます。

```verilog:dummybus_if.sv
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があれば十分でしょうか。

```verilog:dummybus_item.svh
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からの信号をモニタします。

```verilog:dummybus_vif_driver.svh
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パターンという単語を目にしたことがあると思います。「新しいクラス」と「既存クラス」のインタフェースに不整合があるとき、「新しいクラス」から「既存クラス」を扱うためのアダプタを作ろう、というやつです。「既存クラス」を弄らないことがポイントです。このベストプラクティスに従って、上記問題を解決します。
![Aclass.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/628886/1c16eba7-ec91-bac6-ebad-24a45279cf4e.png)
先ほどのRAM（ram.v）に対し、以下のようなBFMが既にあるものとします。タスクの中身は面倒なので省略しています。

```verilog:dummybus_bfm.v
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修飾子を付けます。

```verilog:dummybus_abstract.svh
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）を抽象クラスで扱える形式に変換するクラスを作成します。トランザクションの中身に応じて、抽象クラスの持つファンクションやタスクをコールします。

```verilog:dummybus_bfm_driver.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にアクセスしています。

```verilog:dummybus_concrete.svh
`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を使っています。無料なので、お金を積まなくても試せると思います。
・パッケージ

```verilog:dummybus_pkg.sv
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シーケンス

```verilog:dummybus_sequence.svh
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テスト
面倒なので、仮想インタフェースと抽象クラスのテストを一緒にしています。

```verilog:dummybus_test.svh
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）を駆動しますが、抽象クラスはメッセージを出すだけです。

```verilog:tb.sv
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についてきたライブラリを指定しています。

```:run.do
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
```
以上です。