🦔

Vivado Simulator(xsim)でUVMやってみた

2022/12/04に公開
7

はじめに

AMD(Xilinx)のFPGA開発ツールである Vivado は 7 Series 以降のFPGA向けのツールで SystemVerilog を使うことができます。今まであまり積極的には SystemVerilog に手を出してこなかったのですが、そろそろ Spartan-6 などの古いデバイスを扱うことも無くなってきたので、少し前から徐々に SystemVerilog の利用もするようになってきました。
そもそもテスト対象(DUTというらしいです)が Verilog でも、テストベンチだけ SystemVerilog で書くということは可能ですので、ツール等の環境が整ってくれれば SystemVerilog の活用は視野に入ってきます。

調べてみたところ SystemVerilog の世界では、テストや検証を行うのにUVMというものがよく用いられるようです。
テスト用のツールというと、例えば C/C++ ですとGoogle Testとかよく使いますし、Rust のように言語内にテスト機能が組み込まれているものもあります。SystemVerilog でもデファクトスタンダードで便利なものがあるなら是非試してみたいところです。

当初「お高いEDAツールでしか使えないんでしょ?」と思っていたのですが、どうやら無償で使える Vivado Simulator(xsim) が UVM に対応しているようです。xsim 凄いです。
無料で使えるならちょっと試してみようということで、あれこれ試してみましたので、作業記録を兼ねてここに記事として残しておきたいと思います。

本記事では Vivado 2022.2 を用いており、Xilinx FPGA 向けに UVM を試してみる話しとさせていただきます。

また、筆者は「UVMって何?」という状態からつまみ食いしつつ、正味一週間も弄ってないレベルで「同じ段階の方の手助けになればよいな」という趣旨で初心者ながらにこの記事を書いておりますので、有識者の方、どうかマサカリを投げずに生暖かくご指導いただければ幸いです。

参考にした情報など

さっそなのですが、xsim には最初から UVM がインストールされているので、使うだけなら UVM のダウンロードもインストールも不要です。
デフォルトのインストールだと

/tools/Xilinx/Vivado/2022.2/data/system_verilog/uvm_1.2/

にファイルがあるようで、ディレクトリ名のとおり UVM 1.2 が入っているようです。初心者に優しいですね。有難くこのまま使うことにします。

一方で、インストールはしませんがドキュメントやサンプルコードは欲しいので、こちらから UVM1.2 のものを一式ダウンロードいたしました。

また、UVM 1.2 の公式のドキュメント類以外に、参考させて頂いたサイトとしては

などがあります。この場をお借りして感謝申し上げます。

なお、UVM のバージョンが古いことを除くと最後のサイトが一番情報量が多くよくまとまっていたように思いました。英語なのですがブラウザの自動翻訳でもあまり困らなかったので、英語苦手な方にもオススメしておきます。

最初に理解すべきと思った点

初心者的に最初に少し混乱したのですが UVM といったときに、2つの大きな側面をがあるように感じました。

  • 「こういう風に検証しましょう」というデザインパターン的な概念
  • デザインパターンを実現するためのクラスライブラリの提供

UVM と言ったときにどちらの理解も並行して進めていくことが重要に思いました。

UVMのクラスライブラリとしての側面

以下、uvm_user_guide_1.2.pdf からの引用した図です。

UVM Class Diagram

UVM のクラスライブラリは uvm_object の派生として、いろいろなクラスが用意されています。

一方で、オブジェクト指向言語向けのよくあるクラスライブラリと大きく変わらない思想であり、普段 C++ や Python などでプログラミングしている人にはそれほど抵抗がないという事にも気づけます。

私は当初「サイクル単位でゴリゴリとRTLでテストパターンを書く概念」や「moduleでの階層構造のイメージ」を持ったまま、いきなりサンプルソースコードを読もうとしてハマりました。そういった概念は一旦置いておいて「単なるオブジェクト指向のソフトウェアライブラリ」として先にクラス図を眺めておくと理解が早いと思います。

そのうえで上の図のうち今私が理解した範囲で少し書いておきます(いろいろ自信ないですが)。

あと UVM では sequence と sequencer は1文字違いで全く別物でどちらも重要なので読むときにはご注意ください(私はいろいろやらかしました)。

uvm_object

名前管理/シリアライズ/コピー/クローン/print などの基本機能が付与された基本クラスで、すべてのクラスはこのクラスの継承となるようです。
オブジェクト指向でよく見かけるパターンですが、すべての uvm のクラスが共通に持つ機能が定義されているようです。

uvm_component

UVM の driver とか monitor とか検証におけるいろいろな役割のコンポーネントの元となるクラスのようです。

特徴的に思ったのが名前の管理で Verilog のモジュールなどのインスタンス名(ドットで繋がれた階層的なアレ)とよく似た構造でクラスインスタンスを階層的に名前で扱える機構があるようです。
生成時に親を渡すことで階層構造が作られ、文字列で対象を指定できるようになるようです。

テストシナリオ進行の各フェーズで呼び出される function や task が定義されており、それぞれ適宜オーバーライドして、必要なタイミングでやりたいことを記述していくスタイルのようです。

基本的に「uvm_component から派生する下記のクラスを継承したコンポーネントを作成してテストを構築しなさい」という事のようです。

  • uvm_deriver
  • uvm_sequencer
  • uvm_monitor
  • uvm_agent
  • uvm_scoreboard
  • uvm_env
  • uvm_test

各コンポーネントは階層的に構成され、それぞれで通信しながらテスト全体を構築していくようです。

uvm_sequence_item

最終的にテストドライバに渡されるテスト対象への操作情報の定義を行うための元クラスのようです。

このクラスの下にさらに uvm_sequence があり、こちらはテストシナリオそのものを記述するクラスになってくるようです。

例えばメモリアクセスのようなものだと

  • ランダムに読み書きを繰り返してみる
  • 連続書き込みをしてみる
  • 連続読み込みをしてみる
  • etc.

と、テストシーケンスをいろいろ思いつくかと思いますが、そういうテストパターンの数だけ、uvm_sequence から派生したクラスを作っていくスタイルのように理解しました。

uvm_resource

まだよく理解出来ていないのですが、全体的なテスト設定などのリソースを管理するもののようです。

テスト全体の設定データを管理する uvm_config_db を経由して、DUT の virtual interface を渡すような使い方ができるようです。

テストのデザインパターン的な側面

順番は前後しますが、再び uvm_user_guide_1.2.pdf からの引用です。

picture 2

以下、間違いがあるかもしれませんが、現時点での私の理解を私なりの言葉で書いてみます。

UVM Testbench

テストベンチのトップモジュールです。 テスト対象である DUT と UVM Test を含みます。

ここで一点注意点があり、DUT は module ですが、UVM Test は uvm_test を継承した class として実装されます。

したがって、UVM Testbench と DUT はモジュール階層でありながら、UVM Test 以下は、uvm_componentクラスから継承された各種コンポーネントのクラスインスタンスの階層を表しています(module と class が同じように書かれており、初心者の私はしばらく混乱していました)。

そしてこの図を見て気が付く通り、DUT と UVM Test 配下の各種コンポーネントが DUT が接続されています。

階層を超えて接続しないといけないのですが、モジュールのようにポートで接続されるのではなく、SystemVerilog の virtual interface の機能によって接続されます(DUTは module ですが、この矢印はポートの wire 接続のようなものではなく、virtual interface の事を指しているようです)。

したがって、図にはありませんが、インターフェースクラスを準備するというのもDUTの準備とセットでテストベンチのトップ階層で必要なこととなるようです。

UVM はテストを行う為の手段の一つですので、まずDUTはとても重要と思っています。
私が当初戸惑った理由の一つとして、DUT不在のまま UVM Test だけでUVMを説明するサンプルが案外多かったことです。

私の場合、目的(What)に触れずに手段(How)の説明を読んでも何のためにそれをするのかなかなか頭に入ってこなかったので、この記事では最初から DUT を置いて話を進めたいと思います。

UVM Test

この中が UVM の本体になってきます。

テストベンチなので、DUT を検証/テストすることが一番の目的ではありますが、その際テストに使う部品をなるべく再利用できるように体系化するという2番目の目的を最大化するためにUVMが生み出されたのだと理解しています。

テスト項目である Sequences、テスト設定である Config をUVM Environment に渡すことでテストを行うという大枠がここにあるようです

UVM Environment

uvm_env クラスから継承して作るコンポーネントで、テスト構造がこの中に構成されるようです。

ここに4つのコンポーネントが出てきますが、今の私の理解の範囲で

  • UVM Agent : テストを行う主体(1つでも良いし、複数も可能)
  • UVM Environment : さらに階層的に環境を構築する場合入れ子にできる(無くても良い)
  • UVM Sequencer : 複数の UVM Sequence を読み込んでいく中継役
  • UVM Scoreboard : 期待どおりの動作か確認してテスト結果を判定する

というもののようです。

基本的にテストですので、テスト対象の DUT に色んなテストパターンの操作を行って、その結果が期待通りか確認していくという流れは他のテストベンチと何ら変わることはなく、その流れを素直に役割別にしてコンポーネント分割したものと捉えております。

UVM Agent

この図ではまだ出てきませんが、内部には

  • uvm_driver を継承する DUTを制御するドライバクラスのコンポーネント
  • uvm_sequncer を継承して テストシーケンスをドライバに送るシーケンサクラスのコンポーネント
  • uvm_monitor を継承して Driver に制御される DUT の動きを監視して Scoreboard に送るモニタクラスのコンポーネント

の3つのコンポーネントが含まれて、実際の DUT へのアクセスを行っていくようです。

実際にやってみた

作ってみたサンプル

恥ずかしながら私が勉強がてら最初にやってみたコードをこちらに置いておきます。

source /tools/Xilinx/Vivado/2022.2/settings64.sh 

git clone https://github.com/ryuz/study_uvm.git
cd study_uvm/mem_test/xsim/
make

のようにすれば動くのではないかと思います。

コードの概要

簡単なメモリモデルをいろいろなサンプルを見ながら作ってみました(まだなんかコンパイル通っただけ感が強いですが)。

  • DUT(memory.sv) とその Interfaceクラス(mem_if.sv) をまず用意して、作成した UVM のテスト環境に繋ぐ仕組みになっている
  • テストシーケンス(テストシナリオ)は mem_sequence.sv にある
  • 期待値比較は mem_scoreboard.sv にある

あたりを最初に頭に置いておくと、「DUT に対して何らかのテストシナリオを流し込んで結果を判定する」というテストの基本を見失わずに UVM を掘っていけるのではないかと思う次第です。

(例えば、UVMのサンプルの hello_world はそもそも DUT が出てきません。なので、私は当初結構な時間 UVM をどうテストに使えばいいのか感触が掴めないままサンプル内を彷徨ってしまいました)。

Makefile

https://github.com/ryuz/study_uvm/blob/master/mem_test/xsim/Makefile

Vivado シミュレーターでは xvlog でコンパイルして、 xelab でエラボレートして、xsim で実行しますが、xvlog と xela に -L uvm と渡すだけで UVM が使えるようになるようです。

xsim に置いては、--testplusarg でパラメータを渡していますが、これによって実行するテストをコントロールできるようです。

また、UVM の利用では、 include をわりと多用する傾向にあるようですので --include でのインクルードパスの設定は重要ですね。

DUT

何はなくともまず DUT です。今回は Xilinx でメモリ推定してくれるコードを簡素化して対象としてみました。

https://github.com/ryuz/study_uvm/blob/master/mem_test/memory.sv

DUTへの Interface

https://github.com/ryuz/study_uvm/blob/master/mem_test/mem_if.sv

本来はここにアサーション記述したり、modport 定義したりすると良いのでしょうが、今回は最小パターンを作るのが目的なのでシンプルにしています。

トップモジュール

https://github.com/ryuz/study_uvm/blob/master/mem_test/mem_testbench.sv

やっとることは

  • 同じないとして uvm の import や作成したモデルの include による結合
  • DUT と Interface(mem_if) を作成し、virtual interface として uvm モデルに渡す
  • クロックやリセットを供給する
  • run_test() を呼び出して制御を uvm に渡す

といったところです。

テスト本体

https://github.com/ryuz/study_uvm/blob/master/mem_test/model/mem_test.sv

ここで初めて uvm_component に属する uvm_test から派生させたクラスが出てきます。

uvm_component由来の自作クラスを作るときは

  • uvm_component_utils (もしくはuvm_component_utils_begin/uvm_component_utils_end) マクロなどを使って基本機能を登録
  • new では名前や親などの階層構造が作れる情報を付与して super クラスを読んでおく

などが必要となるようです。

build_phase や run_phase などは、テストの進行フェーズにしたがって呼ばれるもののようで、例えばこちらの記事などが詳しかったです。

ここでは build_phase で シーケンス と テスト環境を生成しています。

テスト環境

https://github.com/ryuz/study_uvm/blob/master/mem_test/model/mem_env.sv

テスト環境です。
uvm_env クラスを継承した後、エージェントとスコアボードを生成しています。

生成

シーケンスアイテム

https://github.com/ryuz/study_uvm/blob/master/mem_test/model/mem_seq_item.sv

後述のシーケンサからテストドライバに渡されるアイテムですが、SystemVerilog の rand 機能でランダムなパターンが生成できるようにしています。

uvm_object_utils_begin と uvm_object_utils_end で囲まれた範囲でメンバ変数を指定することで uvm_object の機能を付与できるようです。
フラグの意味などはドキュメントを見るべきなのですが、こらちなども参考になりました。

シーケンス

https://github.com/ryuz/study_uvm/blob/master/mem_test/model/mem_sequence.sv

基本的にテストシーケンスの数だけuvm_sequenceを継承したクラスを作るようですが、今回は

  • randomize() にて生成するランダムなパターンを64回実行する

という、シンプルなものを1つだけ作ってみました。

基本的に「要求を送り、レスポンスを待つ」がワンセットのようです。
要求とレスポンスはそれぞれ定義できるようですが今回は同じもの(mem_seq_item)を使っています。

最終的に後述のドライバの部分で virtual interface を介した DUT に対する操作として実行されるようです。

スコアボード(テスト結果の判定)

https://github.com/ryuz/study_uvm/blob/master/mem_test/model/mem_scoreboard.sv

スコアボードにて、テストの結果が期待通りだったかどうかの判定を行うようです(やや自信なし)。

後述のモニタから送られてくる DUTへの操作の監視内容を受け取って、期待通りの結果か判断します。

ここではメモリリードが行われた次のサイクルで期待した値が読みだせているかどうかのチェックのみ行うモデルをあまり難しいことはせずに書いてみました。

エージェント

https://github.com/ryuz/study_uvm/blob/master/mem_test/model/mem_agent.sv

ソースそのままに driver、sequencer、monitor の3点セットを束ねるものだと理解しました。

ドライバ

https://github.com/ryuz/study_uvm/blob/master/mem_test/model/mem_driver.sv

シーケンスアイテムを受け取って、virtual interface 経由で DUT を操作します。

build_phase() で、トップモジュールで定義した DUT への virtual interface を uvm_config_db から取得しています。

run_phase() の中で

  • テストシーケンスアイテムを受け取る
  • DUTへの信号を設定する
  • レスポンスを返す

という処理を行っています。

シーケンサ

https://github.com/ryuz/study_uvm/blob/master/mem_test/model/mem_sequencer.sv

今回はほぼ何もすることはなく、uvm_sequencer から継承しただけになっています。

初見には uvm_sequence と名前が紛らわしいという点が注意点でしょうか。

モニター

https://github.com/ryuz/study_uvm/blob/master/mem_test/model/mem_monitor.sv

uvm_monitor から派生したクラスですが構造はドライバとよく似ており、build_phase() で DUT への virtual interface 経由で DUT を取得しています。

run_phase() では、ドライバがDUTを操作する信号を監視して、スコアボードに送るということをやっています。

inclue 用のファイル

https://github.com/ryuz/study_uvm/blob/master/mem_test/model/mem_model.svh

SystemVerilog では class は、モジュールの中などに書くことになるので、クラスごとにファイルを分けて include する事で、ソースコードの見通しを良くするということが行われるようです。

今回はこちらを真似させていただきました。

おわりに(感想など)

今回見よう見まねであちこちのWebサイトや、UVMに付属するサンプルなど見ながら UVM に挑戦してみましたが、単にちょっと動かしてみるだけでもなかなか覚えることが多そうに感じました(この記事も思ったより長くなってしました)。

合成可能記述に関して言えば Verilog から SystemVerilog への移行は、拡張子を変えるだけのところから初めて、struct や enum や ビット幅キャストや interface など、少しづつ使うものを増やしていく移行も可能かと思います。

一方で、検証に関しては今まで書いていた Verilog のテストベンチから、「少しづつUVMを取り入れよう」というわけにはいかないなというのは感じました。今回、簡単なメモリテストを動かすだけでもかなりのファイル数ですので、やはりある程度のチュートリアルやローカルでの試行錯誤をこなしてから実践に投入するのがよさそうに思いました。

個人的には FPGA しか触らないというのもあるかと思いますが、なかなか最初のとっかかりが掴めずに苦しみました。
まだよくわかっていない部分も多いので、間違いも多いかもしれませんが、同様の立場から触ってみようという方の参考になれば幸いです。

<追記>

上で「ちょっと動かしてみるだけでもなかなか覚えることが多そう」とぼやいておりましたところ、@vega77 様が素晴らしい記事を作成くださいました。

https://qiita.com/vega77/items/0393ba588de26bc805ae

非常に勉強になりました。厚く御礼申し上げます。

あとがき(おまけ)

私のよくやるFPGAのデバッグスタイルとして

  1. ZynqMP 上で bitstream を動的にダウンロードして、ソフトウェアデバッガとILAでデバッグする
  2. Verilator + Pybind11でPythonから機能検証する
  3. Verilator + OpenCVでC++で高速検証する
  4. どうしても必要な時は RTLのテストベンチを書き、タイミング検証を行う

といった感じの種類を使い分けることが多いです。ハードウェア検証をしているのではなく、ソフトウェアテストの延長に近い感じでしょうか。

FPGAの場合、ロジック部分も含めてダイナミックローディングできますので、いきなり網羅的にRTLの検証を行うのではなく大雑把にシステムが成立するかの PoC(Proof of Concept) を行ってから、アジャイルに仕様を洗練していき、方向性が固まってから必要な時に必要なだけの堅牢性を確保すべく、効率的な検証をするような流れも可能です。場合によっては先にリリースしてから徐々にアップデートで対応していくことも可能でしょう。

一方で、このようなやり方におけるデバッグにおいては、バグの箇所の素早い切り分けなども重要で、信頼できるソースとそうでないソースの見分けは重要です。
特によく使うRTLモジュールは 上の 4. の部分をしっかりやっておいて、信頼できるモジュールにしておくのが全体の効率化に寄与します。

Verilog-2001 時代のテストではなんだかんだでAXIバス回りなどこのあたりなどにあれこれと書いたモデルが溜まってきていますが、UVM だともう少し体系的に資産化できるのかもしれません。なにより SystemVerilog の検証の為の多数の言語機能も思いのほか xsim がしっかり対応しているようなので取り入れていくチャンスな気がします。

ソフトウェアのあり方が多様化すると同時に、テスト/検証の技法も多様化しているので、選択肢として沢山の技法を知っておくことは、開発の幅を広げることに貢献してくれそうに思います。

また、今回 Twitter で呟いていたところ、有難いコメントを幾つかいただきました。

のようなお話とか、

などのアドバイスも頂いており、初心者の粋を脱したらこちらも触れてみたいと思っておりますが、まだ辿りつけておりません(tvip-axiは Star の数がすごいですね、人気のライブラリがあるというのは uvm をやるモチベーションの一つになりそうですね)。

その他の参考情報

SystemVerilogのLRMは登録すればGet programで 無償でDownloadできるようです。

アドベントカレンダー

本記事はHDL Advent Calendar 2022向けの記事として作成しております。

GitHubで編集を提案

Discussion

石谷太一石谷太一

EDA Playground であれば、商用のシミュレータも試用することができます。
なので、勉強程度であれば、ここを使うのも良いかと思います。

Ryuz88Ryuz88

ありがとうございます。実は最初に一度触ってよくわからずにxsimに戻ってきたという経緯があります。もう少し理解が進んで余裕が出てきたらまた触ってみるかもしれません。

VegaVega

一方で、検証に関しては今まで書いていた Verilog のテストベンチから、「少しづつUVMを取り入れよう」というわけにはいかないなというのは感じました。

UVMもやりようによって既存の環境に少しずつ取り入れていく事が可能です。教科書通りのやり方だとあんまりそういう事が書かれていないので記事を書こうかなぁと思いつつも書けていません…。そのうち書こうと思います。

しかしVivadoでUVMサポートしている事を知りませんでした。フリーでUVMが動くSimulatorはありがたいですね。早速自宅Winddows PCのWSL環境にインストールしてみました!

Ryuz88Ryuz88

拙い記事にコメント頂き、ありがとうございます。

UVMもやりようによって既存の環境に少しずつ取り入れていく事が可能です。教科書通りのやり方だとあんまりそういう事が書かれていないので

教科書的な順でやると、環境、エージェント、シーケンサ、ドライバ... といっぱい準備して、やっとそこから DUT が出てくるという感じで、結構長く感じました。
一方で、そもそも UVM などが動く高尚なものに触れる機会もなく verilog-1995 時代のノリのまま $readmemh したテストベクタを DUTに値を入れて、結果を $fdisplay して、期待値と比較するだけのようなベタ書きのテストベンチもまだまだ多く見かけ(私の周りだけかもですが)、折角無償の Vivado で UVM 使えるなら、あまりハードルを高くせずにそういうものへの第一歩目が踏み出せるといいなと思っている次第です。

記事を書こうかなぁと思いつつも書けていません…。そのうち書こうと思います。

その際には是非リンクさせて頂ければ幸いです。

Ryuz88Ryuz88

ありがとうございます。
いくつか動かしながら覗かせていただいていますが、非UVMなテストベンチにだんだんUVM要素が追加されていくのは、手ごたえを感じつつ勉強していけるので非常にいいなと思いました。
参考にさせていただきます。