05日目~モデルのひな形ステップ3

※2018/02/11 文中の説明文を一部修正しました

今回は、以下のことを実施していきます。ボリューム多めかもしれません。難しいかもしれません。

動作の流れ

  • UVMのスティミュラス(テストパタン)発生までの流れを押さえておくことはとても重要です。
    • sequenceから
      • sequence_itemを
      • sequencerに渡す
    • sequencerが受け取って、
    • driverに渡す

という流れになります。

sequence_itemを記述する

sequence_itemとは、トランザクションの発行に必要な情報を格納するクラスです。

xxx_seq_itemと略したり、xxx_transferと書く場合もあります。UVMのサンプルでは後者です。

とりあえず、適当にアドレスとデータを定義してみます。

class sample_seq_item extends uvm_sequence_item;
  byte addr, data;
  bit  write;
  `uvm_object_utils(sample_seq_item)
  function new (string name="sample_seq_item_inst");
    super.new(name);
  endfunction
endclass

商用シミュレーターを使っている方は、アドレスとデータ定義の先頭に "rand" をつけておくといいです。modelsim-aseは、この rand を扱えないので、ここでは定義しません。

driverとsequencerをつなぐ

パラメータを設定する

ここでちょっとだけ、UVMクラスライブラリを参照しようと思います。

uvm_driverは

class uvm_driver #(type REQ=uvm_sequence_item,
                   type RSP=REQ) extends uvm_component;
  uvm_seq_item_pull_port #(REQ, RSP) seq_item_port;

こんな風に定義されています。パラメータとして、「型」を指定することになっています。REQ, RSPと書かれたところです。

そして、seq_item_port というポートが宣言されています。

一方、uvm_sequencerは

class uvm_sequencer #(type REQ=uvm_sequence_item, RSP=REQ)
                                   extends uvm_sequencer_param_base #(REQ, RSP);
  uvm_seq_item_pull_imp #(REQ, RSP, this_type) seq_item_export;

というように、ちょっと複雑に見える定義なのですが、こちらもパラメータとして、「型」を定義することになっています。そして、seq_item_export というポートが宣言されています。

まあとりあえず、「そういうもんなんだ」くらいに捉えておいてください。上記を元にして、4日目で記述したdriverとsequencerを修正します。

--- sample_driver.sv

class sample_driver extends uvm_driver #(sample_seq_item);

--- sample_sequencer.sv

class sample_sequencer extends uvm_sequencer #(sample_seq_item);

どちらも、class定義行を修正するだけです。渡す「型」は、上の方で定義したsequence_itemのクラス名です。

つなぐ

次に、driverとsequencerをつなぎます。つながないと、初めに説明した「流れ」が作れないためです。修正するのは、uvm_agentです。

  function void connect_phase(uvm_phase phase);
    if(get_is_active() == UVM_ACTIVE)begin
      driver.seq_item_port.connect(sequencer.seq_item_export);
    end
  endfunction

このコードを、build_phaseの次に追加します。太字のところは、先ほどUVMで定義された各クラスのコード引用にあったポートです。名前的には、ポートが出し側でエクスポートが受け側となります。 黄色のところは、UVMクラスライブラリで以下のように定義されているのを使っています。

virtual class uvm_agent extends uvm_component;
  uvm_active_passive_enum is_active = UVM_ACTIVE;
  virtual function uvm_active_passive_enum get_is_active();
    return is_active;
  endfunction

何に使うかは、ここでは置いておきます。とりあえず初期値は「is_active = UVM_ACTIVE」ですので、uvm_agentに追記するコードは条件がtrueとなり、driverとsequencerがつながることになります。

interfaceを記述する

とりあえずこんな感じで。

--- sample_if.sv

interface sample_if(input logic clk, rstz);
  logic write;  // 1:write, 0:read
  logic valid;
  logic [7:0] addr, data;
endinterface

virtual interfaceとリンクする

ここが結構わかりづらいところかもしれません。いや、ぼく自身もOVM手法のときより「わかりづらい」気がしました。でも、考え方としては新しい方(ここで説明する方法)がいいかなぁということ で、以下に説明します。

まずinterfaceですが、これは単純に言えば「moduleとmoduleの接続の橋渡しをする人」なわけです。それともう一つ役割があって、クラスの中で「仮想接続」することができます。クラスの中には interfaceを置けないのですが、「仮想」つまりvirtual的に置くことはできるんです。それを virtual interface と呼んでいます。

それではまず、テストベンチトップにinterfaceを組み込みましょう。

--- tb_top.sv

`timescale 1ps/1ps
module tb_top;
  // UVM class library
  `include "uvm_macros.svh"
  import uvm_pkg::*;
  // uvm user code
  `include "sample_model.svh"
  `include "sample_test.sv"
  /////////////////////////////////////
  logic clk, rstz;
  sample_if vif(clk,rstz);
  // clk
  initial begin
    clk <= 1'b1;
    #100;
    forever #50 clk <= ~clk;
  end
  // rstz
  initial begin
    rstz     <= 1'b0;
    #80 rstz <= 1'b1;
  end
  initial begin

uvm_config_db#(virtual sample_if)::set(uvm_root::get(), "*.env.*", "vif", vif);

    run_test();
  end
endmodule

黄色いところを追加しました。なんとなく動かしてみせるために、それっぽくリセットとクロックも作りました。

initial文中の謎めいた記述ですが、これはUVMに組み込まれている「データベース」の一種です。指定したオブジェクトの、指定した範囲の、指定したフィールド名(変数、メンバ)に、このmoduleに組 み込んだ「vif」をセットする、という意味です。

  • 第一引数 uvm_root::get()は、「まあこういうもの」として使ってください。ただし、テストベンチトップのmoduleに限ります。(後述する、class内で使用するuvm_config_dbでは異なってきま す)
  • 第二引数 "*.env.*" は、第一引数で取得した uvm_test の下に、uvm_envを env という名前で組み込んでいるので、それの下ですよ!という表記になります。
  • 第三引数 第二引数で探し出したインスタンス各階層に、"vif" というフィールドを設けます。から"vif"というフィールド名を探します。正規表現指定可能だそうです。
  • 第四引数 そこに、vif を渡します。(ハンドル渡し、参照というんですかね)

ちょっと難しいかもですが、UVMにあるubusサンプルを少し変えています(第二引数のところ)。これで動作するのを確認し、またenvをenbにするとFatalする(後述)するのを確認したので、第二引数 の意味は正しいと思います。

driverにvirtual interfaceを組み込む

こんな感じになります。

class sample_driver extends uvm_driver #(sample_seq_item);
  virtual sample_if vif;
  `uvm_component_utils(sample_driver)
  function new (string name, uvm_component parent);
    super.new(name, parent);
  endfunction
  function void build_phase(uvm_phase phase);
    bit status;
    super.build_phase(phase);
    status = uvm_config_db#(virtual sample_if)::get(this, "", "vif", vif);
    if(status==1'b0)
      uvm_report_fatal("NOVIF", {"virtual interface must be set for: ",get_full_name(),".vif"});
  endfunction
  task run_phase(uvm_phase phase);
    uvm_report_info("DRIVER", "Hi");
  endtask
endclass
  • まず、virtualでinterfaceがインスタンスされています。
  • build_phaseが追加になりました。この処理内容は、UVMのubusサンプルを少しだけ噛み砕いています。
    • uvm_config_dbで、"virtual sample_if" という型で、"vif" という名前から値を取り出します。
      • 取り出しが成功したら、その値を vif にセットします。
      • 取り出しが失敗したら、Fatal Errorを発行します

なお、monitorにも virtual interface 組み込みが必要になってきますが、その記述は driver と全く同じなので、記述例は省略します。

sequenceを記述する

sequenceは、テストベクタ・テストシナリオとも呼んだりします。sequence記述のお勧めは、

  1. baseとなるsequenceクラスを定義して、そこに「共通記述」を埋め込む
  2. sequence毎に異なる記述を、1番をextendsしたクラスの中に定義することで、記述量の削減を行う

--- sample_seq_lib.sv

最初のクラス、sample_base_seqを定義して、共通となる記述を埋め込みます。

このクラスを virtual class とすることで、このクラス自体を直接使う(インスタンス)できないようにしています。

いくつかポイントを書いておきます。サンプルコードは説明の下に掲載しています。

  1. function new
    • do_not_randomize = 1ですが、これはbuilt-inメソッドであるrandomize()が使えないmodelsim-ase用の設定値です。商用シミュレーターを使用していて、randomize()メソッドの使えるシミュレー タでは、do_not_randomizeの行を削除します。
  2. virtual task pre_body()、post_body()
    • sequenceは、task body() が自動で呼ばれます。pre_bodyは、名前の通り、bodyを呼ぶ前に呼ばれるメソッドです。ここで、raise_objectionを行います。これは、オブジェクションメカニズムと呼 ばれる仕組みで、UVMはobjectionが空だとすぐにSimulationを停止させてしまいます。一見面倒な仕組みに見えますが、UVMでは複数のテストベクタ・テストシナリオ動作を考慮しており、それぞ れの処理完了タイミングは一般に異なります。オブジェクションメカニズムを使うことにより、例えば3つのsequenceを実行すると、3回raise_objectionされます。そして、sequenceが終わる毎 にdrop_objectionされ、3回dropされるとrun_phaseが終了します。
  3. class write_seq
    • sample_base_seqから拡張しているので、do_not_randomizeは「super.new」で実施されるし、pre_body, post_bodyは引き継がれるので記述不要。
    • task bodyを記述して、スティミュラスを定義します。
      • req という名前のメンバは、下記記述の1行目、「uvm_sequence #(sample_seq_item)」で指定している、sample_seq_itemという型になっています。これを生成します。
        • `uvm_create(req)
      • 生成したreqは、sample_seq_itemなので、sample_seq_itemに定義していたメンバに値をセットします。
        • req.write <= 1'b1; // write
        • req.addr <= 8'h10; // address
        • req.data <= 8'h55; // data
      • 値をセットしたreqを、sequencer経由でdriverに送るのが、`uvm_send(req)です。

なお、商用シミュレーターを使用していて、randomize()メソッドが使える場合には、通常以下のようにします。

  • sample_seq_itemの各メンバは、rand宣言する
  • `uvm_create(req)、`uvm_send(req)を使用しないで、
    • `uvm_do(req) または
    • `uvm_do_with(req, { constraints }
  • を使用します。

家の環境ではrandomize()メソッドが使えず、記述の検証が行えないため、詳細は記述しません。

※uvm-1.2を使用した場合、以下サンプルコード内のpre_body, post_bodyの if文ブロックがまるまる使えなくなります。このケースの回避策は00a. uvm-1.2を試すを参考にしてください。

virtual class sample_base_seq extends uvm_sequence #(sample_seq_item);
  function new(string name="sample_base_seq");
    super.new(name);
    do_not_randomize = 1;
  endfunction
  virtual task pre_body();
    if (starting_phase!=null) begin
       `uvm_info(get_type_name(),
                 $sformatf("%s pre_body() raising %s objection",
                           get_sequence_path(),
                           starting_phase.get_name()), UVM_MEDIUM);
       starting_phase.raise_objection(this);
    end
  endtask
  // Drop the objection in the post_body so the objection is removed when
  // the root sequence is complete.
  virtual task post_body();
    if (starting_phase!=null) begin
       `uvm_info(get_type_name(),
                 $sformatf("%s post_body() dropping %s objection",
                           get_sequence_path(),
                           starting_phase.get_name()), UVM_MEDIUM);
      starting_phase.drop_objection(this);
    end
  endtask
endclass
//------------------------------------------------------------------------
class write_seq extends sample_base_seq;
  `uvm_object_utils(write_seq)
  function new (string name="write_seq");
    super.new(name);
  endfunction
  virtual task body();
    $display("Hello SEQ");
    `uvm_create(req)
    req.write <= 1'b1;
    req.addr  <= 8'h10;
    req.data  <= 8'h55;
    `uvm_send(req)
    #1000;
  endtask
endclass

driverを更新する

sequence → sequencerへ渡された seq_item をdriverで受け取って、virtual interfaceを経由して実際に動かす記述を追加します。

task run_phaseを更新します。

  task run_phase(uvm_phase phase);
    uvm_report_info("DRIVER", "Hi");
    vif.valid <= 1'b0;
    @(posedge vif.rstz);  // wait reset negate
    forever begin
      seq_item_port.get_next_item(req);  // wait seq_item from sequence (via sequencer)
      @(posedge vif.clk); // sync clk
      vif.valid <= 1'b1;
      vif.write <= req.write;
      vif.addr  <= req.addr;
      vif.data  <= req.data;
      @(posedge vif.clk);
      vif.valid <= 1'b0;
      seq_item_port.item_done(rsp);
    end
  endtask
  • リセット期間中に、valid信号をネゲートしておきます。
  • リセット解除を待ちます。
  • forever文を記述します。これがないと、driverはseq_itemを1度受け取って処理したら終わってしまいます。
  • seq_item_port.get_next_item(req)で、sequenceからsendしたseq_itemをreqで受け取ります。reqは、uvm_driverに初めから定義されたメンバです。
  • @(posedge vif.clk) でクロック同期します。vif. で、interfaceの中にアクセスしています。
  • vif.valid <= 1'b1; ... で、interfaceの信号をドライブしています。
  • 1サイクル待ってから、valid信号をネゲートします。
  • seq_item_port.item_done(rsp)を実行すると、sequenceの `uvm_send(req) の次の行に進みます。

monitorを更新する

一応monitorの記述も書いておきます。

class sample_monitor extends uvm_monitor;
  virtual sample_if vif;
  `uvm_component_utils(sample_monitor)
  function new (string name, uvm_component parent);
    super.new(name, parent);
  endfunction
  function void build_phase(uvm_phase phase);
    bit status;
    super.build_phase(phase);
    status = uvm_config_db#(virtual sample_if)::get(this, "", "vif", vif);
    if(status==1'b0)
      uvm_report_fatal("NOVIF", {"virtual interface must be set for: ",get_full_name(),".vif"});
  endfunction
  task run_phase(uvm_phase phase);
    string s_trans_kind;
    uvm_report_info("MONITOR", "Hi");
    forever begin
      @(posedge vif.valid);
      if(vif.write===1'b1)
        s_trans_kind = "write";
      else if(vif.write===1'b0)
        s_trans_kind = "read";
      else
        s_trans_kind = "Unknown";
      uvm_report_info("MON", $sformatf("%s addr=%02xh data=%02xh", s_trans_kind, vif.addr, vif.data));
    end
  endtask
endclass

sample_model.svh

このファイルは、以下のようになります。

`include "sample_seq_item.sv"
`include "sample_seq_lib.sv"
`include "sample_driver.sv"
`include "sample_monitor.sv"
`include "sample_sequencer.sv"
`include "sample_agent.sv"
`include "sample_env.sv"

コンパイル

  1. 上記でsample_if.svを更新したので、コンパイルしておきましょう。
    • vlog -sv model/sample_if.sv
    • sample_if.svを include ファイルから除いているのは、class記述と分離したいという気持ちと、分けないとmodelsim-aseでうまくいかなかったからです。
  2. あとはいつも通りに。
    1. vlog -sv tb_top.sv +incdir+./+model/+../uvm-1.1d/src

実行

  • いつも通りに。
    • vsim -c tb_top +UVM_TESTNAME=sample_test -do "run -all;quit"
  • monitorも記述しておくと、実行ログからこの環境が期待通り動いていることが確認できます。
# UVM_INFO @ 0: uvm_test_top.env [ENV] Hello ENV
# UVM_INFO @ 0: uvm_test_top.env.agent [AGENT] Hi
# UVM_INFO @ 0: uvm_test_top.env.agent.sequencer [SEQR] Hi
# UVM_INFO @ 0: uvm_test_top.env.agent.sequencer [SEQR] default_sequence is uvm_random_sequence
# UVM_INFO @ 0: uvm_test_top.env.agent.monitor [MONITOR] Hi
# UVM_INFO @ 0: uvm_test_top.env.agent.driver [DRIVER] Hi
# UVM_INFO model//sample_seq_lib.sv(13) @ 0: uvm_test_top.env.agent.sequencer@@write_seq [write_seq] write_seq pre_body() raising run objection
# Hello SEQ
# UVM_INFO @ 200: uvm_test_top.env.agent.monitor [MON] write addr=10h data=55h
# UVM_INFO model//sample_seq_lib.sv(25) @ 1300: uvm_test_top.env.agent.sequencer@@write_seq [write_seq] write_seq post_body() dropping run objection
# UVM_INFO ../uvm-1.1d/src/base/uvm_objection.svh(1268) @ 1300: reporter [TEST_DONE] 'run' phase is ready to proceed to the 'extract' phase