UVMチュートリアル(送信編)
UVMの第二回。
しばらくUVMについて勉強してみたのですが、確かにこれはハードルが高い・・・。学習サイトで一番最初に言われることが「UVMは学習コストが非常に大きい、でもそれを乗り越えたら爆発的に開発効率が上がる!」です。ホントかよw とりあえず学習コストの方はマジです。
まぁ、何事も実際に手を動かさないと始まらないものです。騙されたと思って、簡単なプロジェクトを起こしてみたいと思います。参考にしたサイト・本は以下の通り。どれも少し目を通しただけで、まだ途中ですよ。
Udemy Learn to build OVM & UVM Testbenches from scratch
Udemyの無料で受講できる授業。講師の方は、intelやIBMのプロジェクト経験者。
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
検証に必要なソースコード
それでは、上のカウンタの検証環境をUVMで作りたいと思います。理想はいろいろあると思うのですが、今回はカウンタを駆動することを目標にします。以下図のような検証環境を作ることにします。
・・・たかがカウンタの検証とは思えないくらい大規模ですね。これを作るのだと気づいた時、すさまじい衝撃を受けましたよ。
ソースコードとして以下を準備することになります。
- 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_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
トランザクションを信号に変換する(cnt_if.svcnt、cnt_driver.svh、cnt_wrapper.sv)
ここまでトランザクションの話をしてきました。しかし残念ながら、肝心の検証対象(カウンタ)はトランザクションを解釈できません。今回は カウンタをVerilog HDLで記述しているので、そもそもクラスの概念がありません(仮にSystem Verilogで記述した場合も、論理合成を考えるとクラスを扱うのは難しいでしょうね)。カウンタを検証するには、トランザクションをデジタル信号に変換する仕組みが必要になります。
そんなわけで、カウンタのインタフェース仕様の話に移ります。作りたいのはイネーブル付きのカウンタなので、クロック図は下になるでしょうか。
こんなのは今考えることじゃないですよね。カウンタのソースコード(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_phase と drive_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