1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

UVM の run phase を理解する ― シミュレータ開発側の視点から

1
Last updated at Posted at 2026-03-14

はじめに

趣味でSystemVerilogシミュレータ「sukimasim」を開発しています。sukimasimはIEEE 1800-2023準拠を目指したC++20製のシミュレータで、現在 Ibex がシンプルなDV(Design Verification)環境では動作するレベルになっています。

次の目標として、Ibex UVM DV環境での動作を目指しています。Ibexはlowrisc.orgが開発するオープンソースの32bit RISC-Vプロセッサコア(2ステージ・インオーダー、RV32IMC対応)で、GoogleのOpenTitanプロジェクトのRoot of Trustにも採用されています。そのDV環境は、SystemVerilog/UVMベースの本格的な検証テストベンチです。

Ibex UVM DV環境の構成を簡単に紹介すると、以下のようになっています。

  • RISCV-DV(Google開発)によるランダム命令生成 → コンパイル → バイナリ生成
  • メモリインターフェースエージェント(命令フェッチ用 / LSU用の2つ)がスレーブシーケンスでコアのメモリリクエストに応答
  • 割り込みエージェントがテスト中にランダムな割り込み刺激を投入
  • コアの実行トレースとISSゴールデンモデル(Spike)のトレースを比較して正当性を検証
  • OpenTitanのVerification Stagesに基づいて検証成熟度を管理(V2S達成済み:コード/機能カバレッジ90%超)

このテストベンチはUVMのフェーズ機構をフル活用しており、run phaseでの各種シーケンス実行、objectionによるテスト終了制御、タイムアウト管理などが不可欠です。sukimasimでこの環境を動かすためには、シミュレータ側でrun phaseを正しく実装する必要があり、その過程で得た知見を本記事にまとめました。

本記事では、UVMのrun phaseについて通常のユーザー視点に加え、シミュレータ実装側の視点も交えて解説してみます。

UVMフェーズの全体像

UVMのテストベンチは、フェーズと呼ばれる一連のステップに従って実行されます。大きく3つのグループに分かれます。

グループ フェーズ 時間消費 実行方向
Build-time buildconnectend_of_elaborationstart_of_simulation なし function
Run-time run(+ 12個のsub-phase) あり task
Post-run extractcheckreportfinal なし function

Build-timeフェーズでテストベンチの構築と接続を行い、run-timeフェーズでシミュレーションを実行し、post-runフェーズで結果の確認とレポートを行う、という流れです。

フェーズの実行方向

functionベースのフェーズには実行方向があり、これは意外と見落とされがちなポイントという気がします。

フェーズ 実行方向 基底クラス
build_phase トップダウン uvm_topdown_phase
connect_phase ボトムアップ uvm_bottomup_phase
end_of_elaboration_phase ボトムアップ uvm_bottomup_phase
start_of_simulation_phase ボトムアップ uvm_bottomup_phase
extract_phase ボトムアップ uvm_bottomup_phase
check_phase ボトムアップ uvm_bottomup_phase
report_phase ボトムアップ uvm_bottomup_phase
final_phase トップダウン uvm_topdown_phase

build_phaseがトップダウンなのは、親コンポーネントが子コンポーネントを生成(create)する必要があるため、論理的に必然です。

注目すべきは final_phaseもトップダウン である点です。UVM全体でトップダウンのフェーズはbuild_phasefinal_phaseの2つだけであり、他はすべてボトムアップまたは並行実行(taskフェーズ)です。final_phaseがトップダウンである理由は、親が先にリソース解放やファイルクローズの最終処理を行い、子がそれに続く、という順序が自然だからです。

run_phase(およびサブフェーズ)はtaskベースであり、各コンポーネントのtaskが fork-join_none で同時起動されるため、トップダウン/ボトムアップの概念はありません。

run_phaseの基本

taskベースのフェーズ

run phaseの最大の特徴は、他のフェーズと異なり taskとして定義される 点です。UVMソースコードでは uvm_run_phase extends uvm_task_phase と定義されています。

// build_phaseはfunction(時間消費なし)
function void build_phase(uvm_phase phase);
  // ...
endfunction

// run_phaseはtask(時間消費あり)
task run_phase(uvm_phase phase);
  // クロック待ち、ドライブ、モニタなど時間消費する処理が書ける
  @(posedge clk);
  // ...
endtask

functionフェーズは呼び出し元に即座に戻りますが、run_phaseはtaskなので #delay@eventwait などの時間消費文を含むことができます。シミュレータ側から見ると、これはrun_phaseの本体をプロセス(スレッド)としてスケジューリングする必要があることを意味します。

重要な点として、UVM 1.2リファレンスには「The completion of the task does not imply, nor is it required for, the end of phase(taskの完了はフェーズの終了を意味しないし、必須でもない)」と記載されています。つまり、run_phaseのtask自体がreturnしても、他のコンポーネントがobjectionを保持していればフェーズは終了しません。

objectionメカニズム

run_phaseの終了は、objection(異議)メカニズムによって制御されます。

task run_phase(uvm_phase phase);
  phase.raise_objection(this, "Start test");  // 「まだ終わらないで」

  // テストシーケンスの実行
  seq.start(sequencer);

  phase.drop_objection(this, "End test");     // 「終わってOK」
endtask

すべてのコンポーネントがobjectionをdropした時点で、run_phaseが終了します。どのコンポーネントもobjectionをraiseしなかった場合、run_phaseは即座に終了します。 これはUVM初心者が最も陥りやすい落とし穴の一つです。

objectionの内部構造

シミュレータ実装の観点から、uvm_objectionクラスの内部構造を見てみましょう。UVMリファレンス実装では以下の連想配列で管理されています。

  • m_source_count[uvm_object]:そのオブジェクト自身が発行したobjection数
  • m_total_count[uvm_object]:そのオブジェクトと全子孫の合計objection数
  • m_drain_time[uvm_object]:オブジェクトごとのdrain time

objectionのraise/dropは階層的に伝播します。子コンポーネントがraise_objectionすると、そのコンポーネントのm_source_countm_total_countが+1され、さらに親→祖親→uvm_topまでm_total_countのみが+1されます。uvm_topm_total_countが0になった時点で、フェーズ終了処理が開始されます。

child.raise_objection()
  → child: m_source_count=1, m_total_count=1
  → parent:                  m_total_count=1  (伝播)
  → uvm_top:                 m_total_count=1  (伝播)

child.drop_objection()
  → child: m_source_count=0, m_total_count=0
  → parent:                  m_total_count=0  (伝播)
  → uvm_top:                 m_total_count=0  (伝播) → フェーズ終了処理開始

drain_time

drain_timeのデフォルト値は0nsです。全objectionがdropされた後に追加の待機時間を挿入したい場合、set_drain_time()で設定します。

task run_phase(uvm_phase phase);
  // drain_timeを設定(UVM 1.2推奨の書き方)
  phase.phase_done.set_drain_time(this, 100ns);

  phase.raise_objection(this);
  seq.start(sequencer);
  phase.drop_objection(this);
  // → drop後、100ns待ってからフェーズ終了
endtask

drain_time中に新たなobjectionがraiseされると、drain処理はキャンセルされ、通常のraise処理が再開されます。この「キャンセル可能なタイマー」の実装は、シミュレータ側でも考慮が必要です。

raise(A) → raise(B) → drop(A) → drop(B) → [drain_time 100ns] → フェーズ終了
 count=1    count=2    count=1    count=0    ↑ この間にraiseされたらキャンセル

run_phaseの12個のsub-phase

sub-phaseとは

UVM 1.1以降、run_phaseには12個のsub-phase(サブフェーズ)が定義されています。

run_phase(全体を包含)
 │
 ├── pre_reset_phase
 ├── reset_phase
 ├── post_reset_phase
 ├── pre_configure_phase
 ├── configure_phase
 ├── post_configure_phase
 ├── pre_main_phase
 ├── main_phase
 ├── post_main_phase
 ├── pre_shutdown_phase
 ├── shutdown_phase
 └── post_shutdown_phase

各サブフェーズの意図する役割は以下のとおりです。

サブフェーズ群 役割
pre_reset / reset / post_reset リセット投入・解除・安定化
pre_configure / configure / post_configure レジスタ設定・SW初期化
pre_main / main / post_main メインのテストトラフィック実行
pre_shutdown / shutdown / post_shutdown 残留トランザクション処理・終了処理

run_phaseとsub-phaseの関係 ― Phase Domain

ここが少しややこしいポイントです。run_phaseと12個のsub-phaseは 並行して実行 されます。UVM 1.2リファレンスにも以下のように明記されています。

This uvm_task_phase calls the uvm_component::run_phase virtual method. This phase runs in parallel to the runtime phases, uvm_pre_reset_phase through uvm_post_shutdown_phase.

この並行実行のメカニズムはPhase Domain(フェーズドメイン) によって実現されています。

  • run_phaseCommon Domain に所属
  • 12個のサブフェーズ → UVM Domain に所属

2つのドメインは以下のように同期します。

start_of_simulation_phase 終了
          │
    ┌─────┴──────────────────┐
    │                        │
    ▼                        ▼
  run_phase              pre_reset_phase
  (Common Domain)            │
    │                    reset_phase
    │                        │
    │                    post_reset_phase
    │                        │
    │                    ... (順次実行) ...
    │                        │
    │                    post_shutdown_phase
    │                    (UVM Domain)
    └─────┬──────────────────┘
          │ ← 両方の完了を待つ
          ▼
    extract_phase

実務上の重要な含意として、run_phaseにobjectionをraiseしているコンポーネントがある限り、たとえ全サブフェーズが終了していてもextract_phaseには進みません(逆も同様)。両方のドメインが完了して初めてpost-runフェーズに遷移します。

sub-phaseの使いどころ

実務では、sub-phaseを細かく使い分けるプロジェクトと、run_phaseだけで済ませるプロジェクトに分かれます。

sub-phaseを使う例:

task reset_phase(uvm_phase phase);
  phase.raise_objection(this);
  // リセットシーケンスを実行
  vif.rst_n <= 1'b0;
  #100ns;
  vif.rst_n <= 1'b1;
  #10ns;
  phase.drop_objection(this);
endtask

task main_phase(uvm_phase phase);
  phase.raise_objection(this);
  // メインのテストシーケンスを実行
  seq.start(sequencer);
  phase.drop_objection(this);
endtask

task shutdown_phase(uvm_phase phase);
  phase.raise_objection(this);
  // 残留トランザクションの完了を待つ
  wait(scoreboard.pending_count == 0);
  phase.drop_objection(this);
endtask

sub-phaseを使うメリットは、検証フローの各段階が明確に分離される点です。特にリセットシーケンスが複雑な設計や、複数のエージェントが協調して動作する環境で有用です。

一方、多くのプロジェクトでは run_phase 内でシーケンスを直列に記述するだけで十分なため、sub-phaseの実装は省略されることも多いです。DVConの論文 "Run-Time Phasing in UVM: Ready for the Big Time or Dead in the Water?" でも、サブフェーズの実用性については賛否が議論されています。

phase_ready_to_end ― フェーズ終了前の最後の介入点

phase_ready_to_end(uvm_phase phase)uvm_component の仮想関数コールバックで、全objectionがdropされた直後、フェーズが実際に終了するに呼ばれます。

function void phase_ready_to_end(uvm_phase phase);
  if (phase.get_name() == "run") begin
    // スコアボードにまだ未比較のトランザクションが残っていたらフェーズを延長
    if (scoreboard.pending_count > 0) begin
      phase.raise_objection(this, "Waiting for scoreboard drain");
      fork begin
        wait(scoreboard.pending_count == 0);
        phase.drop_objection(this, "Scoreboard drained");
      end join_none
    end
  end
endfunction

この仕組みにより、「テストシーケンスは終わったが、パイプラインにまだデータが残っている」という状況でフェーズを安全に延長できます。

注意点として、phase_ready_to_end内でobjectionをraise→dropすると、再びphase_ready_to_endが呼ばれます。このイテレーションは最大20回max_ready_to_end_iterで変更可能)に制限されており、超過すると強制終了されます。

UVM 1.2でのシーケンス実行パターンの変更

starting_phaseのprotected化

UVM 1.1以前では、シーケンスのstarting_phaseメンバに直接アクセスしてobjectionを操作していました。

// UVM 1.1スタイル(非推奨)
task body();
  starting_phase.raise_objection(this);
  // ...
  starting_phase.drop_objection(this);
endtask

UVM 1.2ではこのメンバがprotected化され、get_starting_phase() / set_starting_phase() でアクセスする方式に変更されています。

set_automatic_phase_objection

UVM 1.2で追加された便利な機能として、set_automatic_phase_objection(1) があります。

class my_sequence extends uvm_sequence #(my_item);
  function new(string name = "my_sequence");
    super.new(name);
    set_automatic_phase_objection(1);  // 自動objection有効化
  endfunction

  task body();
    // raise/dropを手動で書く必要がない
    // start()時に自動raise、body()完了時に自動drop
    `uvm_do(req)
  endtask
endclass

ただし、body()foreverループがある場合、taskが永遠に完了しないためobjectionがdropされません。この場合は手動でのobjection管理が必要です。

シミュレータ実装側から見たrun_phase

sukimasimの開発で実際に対応が必要だった(あるいは今後必要になる)項目を整理します。Ibex UVM DV環境では、run_phase内でRISCV-DVが生成したバイナリのロード、メモリインターフェースエージェントのスレーブシーケンス実行、割り込みエージェントによるランダム刺激の投入、テストタイムアウトの監視などが行われるため、以下の各項目はすべて実践的に重要です。

1. プロセスとしてのフェーズ実行

run_phaseはtaskなので、各コンポーネントのrun_phaseをそれぞれ独立したプロセスとして起動する必要があります。すべてのコンポーネントのrun_phaseが fork-join_none 的に同時起動され、objectionベースで終了が管理されます。

コンポーネントA.run_phase  ──fork──→  プロセスA
コンポーネントB.run_phase  ──fork──→  プロセスB
コンポーネントC.run_phase  ──fork──→  プロセスC
                                        │
                              全objection drop
                                        │
                              phase_ready_to_end (各コンポーネント)
                                        │
                              drain_time 経過
                                        │
                              phase_ended (各コンポーネント)
                                        │
                              全プロセスkill → フェーズ終了

2. phase.raise/drop_objectionのDPI経由の実装

UVMはSystemVerilogのクラスライブラリですが、シミュレータはobjectionカウンタの状態を把握してフェーズ遷移を制御する必要があります。純粋にSystemVerilog側で完結する実装と、シミュレータカーネルと連携する実装の両方が考えられます。

3. timeout機構

UVMにはrun_phaseのタイムアウト機構があります。デフォルト値はUVM_DEFAULT_TIMEOUTマクロで定義されており、UVM 1.2ソースコード(src/macros/uvm_global_defines.svh)では 9200秒(= 9.2 × 10¹² ns)に設定されています。

// ソースコードでの定義
`define UVM_DEFAULT_TIMEOUT 9200s

タイムアウトに到達すると UVM_FATAL(タグ[PH_TIMEOUT])が発行され、シミュレーションが即座に終了します。変更方法は3つあり、優先度順に以下のとおりです。

// 方法1: コマンドラインplusarg(最優先)
// +UVM_TIMEOUT=200000,NO
//   第2引数: YES=以降の上書き可 / NO=以降の上書き不可

// 方法2: 実行時に設定
uvm_top.set_timeout(1ms, 0);  // 第2引数0で以降の上書きを禁止

// 方法3: コンパイル時にマクロを上書き
// +define+UVM_DEFAULT_TIMEOUT=500us

4. phase jumpへの対応

UVMはフェーズの「ジャンプ」をサポートしています。phase jumpはハードジャンプのみであり、ソフトジャンプ(ネゴシエーション型)は実装されていません。

task main_phase(uvm_phase phase);
  phase.raise_objection(this);
  fork
    begin: main_traffic
      main_seq.start(agent.sequencer);
    end
    begin: reset_watch
      // 動作中のリセットを検出
      @(negedge vif.rstn);
      phase.jump(uvm_pre_reset_phase::get());
    end
  join_any
  phase.drop_objection(this);
endtask

phase jump時の内部動作は以下のとおりです。

  1. set_jump_phase(phase) でターゲットフェーズを指定
  2. end_prematurely() で現在のフェーズを即座に終了
  3. 実行中の全プロセスとシーケンスが即座にkillされる
  4. objectionカウントは自動的に0にリセットされる
  5. phase_ready_to_end呼ばれない
  6. phase_ended呼ばれる(クリーンアップに使用可能)

phase_ended内ではget_jump_target()でジャンプ先を取得でき、get_run_count()で再実行回数を確認できます。無限ループ防止のガードに使えます。

function void phase_ended(uvm_phase phase);
  uvm_phase jump_to = phase.get_jump_target();
  if (jump_to != null)
    `uvm_info("JUMP", $sformatf("Jumping to %s", jump_to.get_name()), UVM_LOW)
endfunction

重要な制約として、phase jumpはUVM Domainの12サブフェーズ内でのみ機能します。Common Domain(build、connect等)への後方ジャンプはサポートされていません。また、前方ジャンプ(サブフェーズのスキップ)は技術的に可能ですが、前提条件が満たされない危険があるため注意が必要です。

5. phase_started / phase_endedコールバック

シミュレータが正しくフェーズ遷移を管理するには、以下の3つのコールバックを適切に発火させる必要があります。

コールバック 呼ばれるタイミング phase jump時
phase_started(phase) フェーズ開始直前 呼ばれる
phase_ready_to_end(phase) 全objection drop後、終了前 呼ばれない
phase_ended(phase) フェーズ終了直後 呼ばれる

phase_ready_to_endがphase jump時に呼ばれないのは非対称な動作であり、実装時に注意が必要です。

デバッグ用コマンドラインオプション

UVMはフェーズとobjectionのデバッグ用にplusargを提供しています(Accellera実装固有、IEEE 1800.2標準には含まれません)。

# フェーズ遷移のトレース
+UVM_PHASE_TRACE

# objectionのraise/dropのトレース
+UVM_OBJECTION_TRACE

+UVM_PHASE_TRACEを有効にすると、各フェーズの開始・終了・ジャンプが時刻付きで表示されます。+UVM_OBJECTION_TRACEではobjectionのraise/dropが呼び出し元の情報付きで表示されます。フェーズ終了のハングやobjectionの漏れを調査する際に非常に有用です。

まとめ

項目 内容
run_phaseの性質 taskベース(uvm_task_phase継承)、時間消費あり
終了制御 objectionメカニズム(階層的カウンタ方式)
drain_time デフォルト0ns、set_drain_time()で設定
sub-phase 12個、run_phaseとPhase Domainで並行実行
phase_ready_to_end 全objection drop後のコールバック(最大20回)
phase jump ハードジャンプのみ、UVM Domain内で後方/前方
timeout デフォルト9200秒、plusarg +UVM_TIMEOUT で変更
デバッグ +UVM_PHASE_TRACE+UVM_OBJECTION_TRACE
シミュレータ側の課題 プロセス管理、objection追跡、コールバック発火順序、phase jump

run_phaseはUVM検証環境の実行エンジンであり、ユーザーから見れば「テストシーケンスを走らせる場所」ですが、シミュレータ開発側から見ると「複数プロセスの協調実行と動的な終了制御」という実装課題が詰まったフェーズでした。

特にphase jump時のphase_ready_to_endの非発火、Phase Domainによる並行実行の同期制御、objectionの階層的な伝播など、ドキュメントだけでは分かりにくい挙動が多く、sukimasimの開発を通じてUVMの仕組みの裏側を理解できたのは良い経験でした。

Ibex UVM DV環境は、RISCV-DVによるランダム命令生成、複数エージェントの協調動作、ISSとのco-simulationなど、run phaseの機能をフル活用した実践的なテストベンチです。sukimasimでこの環境を動かせるようになることが当面の目標であり、その過程で得た知見は今後も記事にしていきたいと思います。

参考

  • IEEE Std 1800.2-2020 (UVM Standard)
  • IEEE Std 1800-2023 (SystemVerilog LRM)
  • UVM 1.2 Reference Manual (Accellera)
  • Accellera UVM Reference Implementation (GitHub)
  • lowRISC Ibex RISC-V Core Documentation: Verification
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?