らんらん技術日記

日々の学習メモに

UVMチュートリアル(送信編)

UVMの第二回。
しばらくUVMについて勉強してみたのですが、確かにこれはハードルが高い・・・。学習サイトで一番最初に言われることが「UVMは学習コストが非常に大きい、でもそれを乗り越えたら爆発的に開発効率が上がる!」です。ホントかよw とりあえず学習コストの方はマジです。
まぁ、何事も実際に手を動かさないと始まらないものです。騙されたと思って、簡単なプロジェクトを起こしてみたいと思います。参考にしたサイト・本は以下の通り。どれも少し目を通しただけで、まだ途中ですよ。

Udemy Learn to build OVM & UVM Testbenches from scratch
Udemyの無料で受講できる授業。講師の方は、intelIBMのプロジェクト経験者。
Free SystemVerilog Tutorial - Learn to build OVM & UVM Testbenches from scratch | Udemy

Verification Academy
Mentor社が運営している学習サイトです。
Verification Academy - The most comprehensive resource for verification training. | Verification Academy

A Practical Guide to Adopting the Universal Verification Methodology (UVM) Second Edition
Cadenceの検証エンジニアが著した本。

テスト対象

テストというからには、テスト対象がないことには始まりません。シンプルにネーブル信号付きカウンタを検証することにします。Verilog HDLで書くとこんな感じ。

module counter ( input clk, input rst_n, input en_i,  output reg [7:0] cnt_o );

    always @(posedge clk or negedge rst_n) 
        if (!rst_n) 
            cnt_o <= 8'b0;
        else
            if (en_i) 
                cnt_o <= cnt_o + 1'b1;
endmodule

もう見ただけで完璧に動きそうです! これの何を検証するのって感じですw

検証に必要なソースコード

それでは、上のカウンタの検証環境をUVMで作りたいと思います。理想はいろいろあると思うのですが、今回はカウンタを駆動することを目標にします。以下図のような検証環境を作ることにします。

f:id:yukirunrun:20200120224315p:plain:w500

・・・たかがカウンタの検証とは思えないくらい大規模ですね。これを作るのだと気づいた時、すさまじい衝撃を受けましたよ。
ソースコードとして以下を準備することになります。

  • counter.v
  • cnt_wrapper.sv
  • cnt_if.sv
  • top.sv
  • cnt_Item.svh
  • cnt_driver.svh
  • cnt_sequence.svh
  • cnt_env.svh
  • cnt_test.svh

上の図中のSequencer(シーケンサ)はソースコードとして起こさないで、UVMの標準コンポーネントを使うことにします。これでソースコードを1つ減らせます。よかった、よかった。。。

トランザクションを定義する(cnt_item.svh)

そもそも何でUVMを使うのかというと、理由の一つはトランザクションレベルで検証を行いたいからです。私の解釈では、トランザクションとは通信電文や指示書みたいな概念です。ポジティブエッジがどうとか、タイミングがどうとかいった信号レベルの話はしません。カウンタの場合ですと、「10個カウントしてくれ」とか「カウントを2個あげてくれ」といったレベルで話をしたいのです。UVMでは、トランザクションはSystem VerilogのClassを使って定義するのが通例のようです。

class cnt_item extends uvm_sequence_item;
    rand logic [2:0] cnt;
    `uvm_object_utils(cnt_item)
    
    function new(string name = "cnt_item");
      super.new(name);
    endfunction : new
    
    virtual  function string convert2string;
      string s;
      s = super.convert2string();
      $sformat(s, "cnt=%d\n", cnt);
      return s;
   endfunction : convert2string

  endclass : cnt_item

コード中、大切なのは変数cntのみです。カウントしたい数をこの変数に格納すれば、それがトランザクションになります。それ以外はUVMに従ったおまじないですね。クラスをuvm_sequcence_itemから派生していることに注意してください。

シーケンスを作る(cnt_sequence.svh)

トランザクションの型ができたら、それを実体化することを考えます。トランザクションは一度だけ実体化してもいいのですが、検証という観点からは、複数のトランザクションをシーケンスとして実体化した方が都合がいいです。カウンタを例にすると微妙なので・・・例えば、RAMへのWriteをした後、RAMへのReadをするとかね。今回はカウンタなので、単純にカウント命令を複数回発行するシーケンスにします。

class cnt_sequence extends uvm_sequence #(cnt_item);
    `uvm_object_utils(cnt_sequence);
    
    function new(string name = "");
       super.new(name);
    endfunction: new
    
    cnt_item item;
    
    task body;
        repeat (3) begin
        item = new();
        start_item(item);
        assert(item.randomize());
        `uvm_info("SEQ",{"Send transaction:", item.convert2string()},UVM_MEDIUM);
        finish_item(item);
     end
    endtask: body    
 endclass : cnt_sequence

bodyタスクが一番大事です。カウンタのトランザクションを実体化、そのループを3回繰り返しています。後はUVMのおまじないで、クラスをuvm_sequenceから派生していることにやはり注意です。

トランザクションを信号に変換する(cnt_if.svcnt、cnt_driver.svh、cnt_wrapper.sv)

ここまでトランザクションの話をしてきました。しかし残念ながら、肝心の検証対象(カウンタ)はトランザクションを解釈できません。今回は カウンタをVerilog HDLで記述しているので、そもそもクラスの概念がありません(仮にSystem Verilogで記述した場合も、論理合成を考えるとクラスを扱うのは難しいでしょうね)。カウンタを検証するには、トランザクションをデジタル信号に変換する仕組みが必要になります。
そんなわけで、カウンタのインタフェース仕様の話に移ります。作りたいのはイネーブル付きのカウンタなので、クロック図は下になるでしょうか。


f:id:yukirunrun:20200121222758p:plain:w400

こんなのは今考えることじゃないですよね。カウンタのソースコード(counter.v)を作った時点で、この波形が頭にあるわけです。UVMでデジタル信号を扱うには、まずインタフェースを定義します。System VerilogのInterfaceを使います。

interface cnt_if (input clk, input rst_n);
    logic en = 0;
    logic [7:0] cnt;
endinterface : cnt_if

クロックとリセットはインタフェース内で定義する方法もあるのですが、今回は外部入力にしてみました。
インタフェースができたら、それをドライブするためのクラスを用意します。

class cnt_driver extends uvm_driver # (cnt_item);
    virtual cnt_if vif;
    `uvm_component_utils(cnt_driver);

    function new(string name , uvm_component parent);
        super.new(name, parent);
    endfunction: new
    extern virtual function void connect_phase(uvm_phase phase);
    extern virtual task run_phase(uvm_phase phase);
    extern virtual protected task drive_transfer(cnt_item item);
endclass: cnt_driver

function void cnt_driver::connect_phase(uvm_phase phase);
    super.connect_phase(phase);
    if (!uvm_config_db#(virtual cnt_if)::get(this, get_full_name(), "vif", vif))
        `uvm_error("NOVIF", "Failed to getting virtual interface")
endfunction : connect_phase

task cnt_driver::run_phase(uvm_phase phase);
    forever begin
        seq_item_port.get_next_item(req);
        drive_transfer(req);
        seq_item_port.item_done(req);
    end
endtask : run_phase

task cnt_driver::drive_transfer (input cnt_item item);
    wait(vif.rst_n);
    repeat(item.cnt) begin
        repeat(2) @(posedge vif.clk);
        vif.en <= 1;
    end
    vif.en <= 0;
    repeat(2) @(posedge vif.clk);
endtask : drive_transfer 

taskやfunctionの実装をクラス外に出している理由は、 Cadenceの本で推奨していたからです。推奨というか、Cadenceがそういう文化なのだと思います。 ソースコードで重要なのは、run_phasedrive_transferのタスク ぐらいです。これらのタスクで先ほど定義したインタフェースを駆動しています。
最後におまけ。検証対象をSystem Verilogで書いた場合は、interfaceをそのまま使えます。しかしVerilog HDLやVHDLの場合には、intefaceの概念がないので一工夫必要です。今回はカウンタをVerilog HDLで書いているので、下みたいにラッパークラスを定義してあげます。

module cnt_wrapper (
    cnt_if v_if
);
    counter inst_counter(
        .clk    (v_if.clk),
        .rst_n  (v_if.rst_n),
        .en_i   (v_if.en),
        .cnt_o  (v_if.cnt));
endmodule

テスト環境の作成(cnt_env.svh)

ここまでトランザクションの定義に始まり、トランザクションのシーケンス、検証対象のインタフェース、トランザクションをデジタル信号に変換するドライバという流れで見てきました。これらを組み合わせることで、トランザクションを元にして、特定のインタフェースを駆動するための環境を定義できます。以下のようなソースコードになります。

class cnt_env extends uvm_env;
    `uvm_component_utils(cnt_env)
    
    cnt_driver drv;
    uvm_sequencer #(cnt_item) seqr;
    cnt_sequence seq;
    
    function new (string name, uvm_component parent);
       super.new(name,parent);
    endfunction : new;
 
    function void build_phase(uvm_phase phase);
       drv  = cnt_driver::type_id::create("drv",this);
       seqr = new("seqr",this);
       seq  = cnt_sequence::type_id::create("seq",this);
    endfunction
    
    function void connect_phase(uvm_phase phase);
       drv.seq_item_port.connect(seqr.seq_item_export);
    endfunction : connect_phase
    
    task run_phase(uvm_phase phase);
       phase.raise_objection(this);
       seq.start(seqr);
       phase.drop_objection(this);
    endtask : run_phase
 endclass : cnt_env

これにてカウンタをテストするための環境ができました! 本当はインタフェースを駆動するためのエージェントを定義して、テスト環境でエージェントを実体化するのがよいみたいですが・・・今回は他にインタフェースがないので省略です。なにより面倒ですw
ソースコード中にはuvm_sequencerという文字もあります。本記事の最初の方で軽く触れた、シーケンサ(Sequencer)のことです。シーケンスをドライバに転送する役割を担います。扱うシーケンスの数が増えてくると、独自のシーケンサを定義した方がいいみたいです。今回はシーケンスが1つしかないので、標準的なシーケンサを使いました。

テストケースの作成(cnt_test.svh)

最後のひと踏ん張りです! テストケースを作ります。UVMには下位コンポーネントに対するコンフィギュレーションの仕組みがあるのですが、その仕組みを利用して、先ほど作成したテスト環境の条件を振ることができます。一つの環境を作ってしまえば、テストケースを何個も作ることができるのです。今回は特に条件を振るほどのことはないので、一つのテストケースだけ作ります。

class cnt_test extends uvm_test;
    `uvm_component_utils(cnt_test)
    cnt_env env;
    
    function new(string name, uvm_component parent);
       super.new(name, parent);
    endfunction : new
 
    virtual function void build_phase(uvm_phase phase);
       super.build_phase(phase);
       env = cnt_env::type_id::create("env", this);
    endfunction : build_phase
 endclass : cnt_test

やっていることは、先ほど作ったテスト環境の実体化だけですね。他、派生元クラスがuvm_testなことにも注意です。

テストベンチの作成(top.sv)

ようやく見知った形に来ました。いわゆるテストベンチです。検証対象のカウンタとインタフェースの実体化、インタフェースをUVMのテスト環境とつなげてあげます。

import uvm_pkg::*;
import cnt_pkg::*;

module top;
   logic clk = 1'b0;
   logic rst_n;

   cnt_if v_if(
       .clk (clk),
       .rst_n (rst_n));
   cnt_wrapper DUT(v_if);

   always begin
        #10 clk <= ~clk;
   end

   initial begin
     rst_n <= 1'b0;
     repeat(2) @(posedge clk);
     rst_n <= 1'b1;
   end

   initial begin
          uvm_config_db#(virtual cnt_if)::set(null, "*", "vif", v_if);
          run_test("cnt_test");
   end     
      
endmodule // top

上の方にcnt_pkgとあるのは、これまで作ったクラスをパッケージにしているものです。Mentorのお知恵をお借りしています。

package cnt_pkg;
    import uvm_pkg::*;
    
    `include "uvm_macros.svh"
    `include "cnt_item.svh" 
    `include "cnt_sequence.svh" 
    `include "cnt_driver.svh"
    `include "cnt_env.svh"
    `include "cnt_test.svh"   
 
 endpackage // cnt_pkg

シミュレーションの実行

それではシミュレーションを回してみます。使うシミュレータは、XilinxのVivadoに内蔵しているシミュレータです。なぜなら無償かつrandomizeに対応しているから。同じく無償のシミュレータにはModelsim Intel Starter Editionがあるのですが、randomizeが使えません。そこの部分をカットしないとコンパイルは通らないと思います。シミュレーション波形は下になります。

f:id:yukirunrun:20200123235518p:plain

無事、カウンタを駆動できています! 現状では全然UVMのメリットが感じられませんが、とりあえず動いたことが重要です。次回はこの環境もう少し改善してみます。