はじめに
趣味で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 |
build → connect → end_of_elaboration → start_of_simulation
|
なし | function |
| Run-time |
run(+ 12個のsub-phase) |
あり | task |
| Post-run |
extract → check → report → final
|
なし | 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_phaseとfinal_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、@event、wait などの時間消費文を含むことができます。シミュレータ側から見ると、これは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_countとm_total_countが+1され、さらに親→祖親→uvm_topまでm_total_countのみが+1されます。uvm_topのm_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_phase→ Common 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時の内部動作は以下のとおりです。
-
set_jump_phase(phase)でターゲットフェーズを指定 -
end_prematurely()で現在のフェーズを即座に終了 - 実行中の全プロセスとシーケンスが即座にkillされる
- objectionカウントは自動的に0にリセットされる
-
phase_ready_to_endは呼ばれない -
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