はじめに
前回の記事 Claude CodeでSPICEシミュレータを開発している話〖続編 - Day 14〗
では、SPICE シミュレータ(sukimaspice)の機能拡張と ngspice 互換性の話を書きましたが、その後も引き続き順調に sukimaspice の開発を継続しています。
一方で、この数ヶ月は並行して
- IEEE 1800-2023 準拠を目指した SystemVerilog シミュレータ(SukimaSim)
- ngspice 相当の精度を狙った アナログ回路シミュレータ(SukimaSpice)
という「デジタル+アナログ」の 2 つのシミュレータを Claude Code で開発してきました。
ここまでくると次の欲が出てきます。
「PLL みたいなロジック+アナログの混載シミュレーションを、自作シミュレータ同士で回してみたい」
本記事では、
自作 SystemVerilog シミュレータ(SukimaSim)と自作 SPICE シミュレータ(SukimaSpice)を
業界標準である FMI 3.0 Co-Simulation でつないで、PLL の混載シミュレーションをするためのアーキテクチャと I/F 設計案
を、仕様レベルでまとめてみます(まだ実装していない構想段階の話です)。
本記事で伝えたいこと
この記事で書きたいのは、ざっくり以下の 4点です。
- 商用AMSシミュレータの独自接続方式の課題と、FMI 3.0 によるオープンな解決策
- プロセス完全分離による分散処理で、なぜ gRPC + Protobuf を採用するか
- SystemVerilog / SPICE / FMI マスターの 3 者が共有できる I/F 表の作り方
- Simulink の zero-crossing detection 相当を FMI + gRPC でどう実現するか
なお、繰り返しになりますが、今回はあくまで「構想・仕様」までの話しになります。
商用 AMS シミュレータの現状と課題
既存の商用ソリューション
Virtuoso AMS Designer、Spectre AMS、Questa ADMS のような商用 AMS シミュレータは既に存在し、実際の製品開発で広く使われています。これらはいずれも、
- SystemVerilog シミュレータ
- SPICE シミュレータ
- それらを Co-Simulation でつなぐ
という基本構成を持っています。
各社独自の接続方式
重要な認識: 各社のAMSシミュレータは、SystemVerilogシミュレータとSPICEシミュレータを接続する部分で独自の実装を使っています。
具体的には:
| ベンダー | デジタル側 | アナログ側 | 接続方式 |
|---|---|---|---|
| Cadence | Xcelium | Spectre | 独自のConnect Module方式 独自の時刻同期・収束判定アルゴリズム |
| Siemens | QuestaSim | ADVance MS (Eldo) | 独自のADMSインターフェース |
| Synopsys | VCS | HSPICE | 独自の接続メカニズム |
標準化の現状と課題
Verilog-AMS という標準規格(Accellera標準)は存在しますが、これは主に「アナログ動作モデルをVerilogで記述する」ためのもので、Co-simulationの接続方法自体は標準化されていません。
補足: Verilog-AMS と SPICE の違い
- SPICE: 実際の回路素子(抵抗、キャパシタ、トランジスタ)をネットリストで記述
- Verilog-AMS: アナログ回路の動作を微分方程式などの数式モデルで記述(Accellera標準)
- 例: 抵抗器
- SPICE:
R1 node1 node2 10k(回路素子そのもの) - Verilog-A:
V(out) <+ R * I(out);(オームの法則を数式でモデル化)
- SPICE:
標準化されていない主な要素:
-
信号変換メカニズム
- Digital → Analog (D/A) の変換方式
- Analog → Digital (A/D) の変換方式(閾値、ヒステリシス)
-
時刻同期方式
- タイムステップの決定方法
- イベント駆動型(デジタル)と連続時間型(アナログ)の協調
-
収束判定
- フィードバックループの収束条件
- 反復回数の制御
-
イベント通知
- アナログ側の状態変化をデジタル側にどう通知するか
- ゼロクロス検出のタイミング
各社が独自実装する理由
商用ツールベンダーが独自実装を採用する理由:
- パフォーマンス最適化: 各社独自のノウハウによる高速化
- 自社ツール間の密結合: 同一ベンダーのツール間での最適化
- 特許・知的財産の保護: 差別化要素としての技術保護
- 既存資産との互換性: 過去のプロジェクト資産を活かすため
オープンな混載シミュレーションの必要性
この状況には以下のような課題があります:
ベンダーロックインの問題
- 特定ベンダーのツールチェーンに依存
- ツール間の相互接続が困難
- オープンソースツールとの連携が難しい
研究・教育での障壁
- 商用ツールのライセンスコストが高額
- 内部動作がブラックボックス
- カスタマイズや拡張が困難
自作シミュレータの統合
- 研究用の特殊なシミュレータを既存ツールとつなぎにくい
- 新しいアルゴリズムの検証が困難
FMI 3.0 による解決アプローチ
Functional Mock-up Interface (FMI) 3.0 は、もともと自動車・航空宇宙分野で使われてきた標準規格ですが、これを半導体の混載シミュレーションに応用することで、上記の課題を解決できる可能性があると考えています。
FMI 3.0 の利点:
- オープン標準: 誰でも実装・利用可能
- ベンダー中立: 特定ツールに依存しない
- Co-Simulation サポート: 時刻同期、イベント処理、状態管理が標準化
- Early Return 機能: ゼロクロス検出に相当する機能を標準でサポート(FMI 3.0の新機能)
- GetState/SetState と Early Return の組み合わせ: イベント駆動型のロールバックフローが仕様レベルで整理(FMI 2.0 でも GetFMUState/SetFMUState は存在したが、Early Return との連携が未定義だった)
本プロジェクトでは、この FMI 3.0 を使って オープンで標準化された混載シミュレーション環境 を実現することを目指します。
FMI 3.0 Early Return: FMI 2.0 からの主要な進化
Early Return は FMI 3.0 から導入された新機能です。本プロジェクトで FMI 3.0 を選択した最大の理由は、この Early Return 機能により Simulink の Variable Step ソルバの zero-crossing detection と類似のアプローチでアナログ信号のイベントを捉えられる点にあります。
FMI 2.0 vs FMI 3.0 の比較
| 項目 | FMI 2.0 Co-Simulation | FMI 3.0 Co-Simulation |
|---|---|---|
| Early Return サポート | ❌ なし | ✅ あり(新機能) |
| ゼロクロス検出 | ❌ FMU 外部で対処が必要 | ✅ FMU が early_return=true で通知 |
| イベント時刻の精度 | △ コミュニケーションステップに制約 | ✅ last_successful_time で正確な時刻を返す |
| ロールバック機構 | ⚠️ GetFMUState/SetFMUState は存在するが Early Return なしでは実務的にツール依存 | ✅ Early Return + GetState/SetState の連携フローが仕様で整理された |
| DoStep 関数シグネチャ | 基本引数のみ | 4 つの出力引数を追加(eventHandlingNeeded, terminateSimulation, earlyReturn, lastSuccessfulTime) |
なぜ FMI 3.0 を選択したか
FMI 2.0 の課題:
FMI 2.0 の fmi2DoStep 関数は以下のシグネチャです:
fmi2Status fmi2DoStep(fmi2Component c,
fmi2Real currentCommunicationPoint,
fmi2Real communicationStepSize,
fmi2Boolean noSetFMUStatePriorToCurrentPoint);
この仕様では、fmi2DoStep に Early Return 相当のフラグが無く、イベント時刻を途中で返す標準的なフローが定義されていません。ゼロクロス(例: VCO 出力の立ち上がり)がステップの途中で発生した場合、以下の問題が生じます:
-
イベント時刻を FMU 外部で推定する必要がある
- マスター側でイベント検出ロジックを実装(複雑)
- または、コミュニケーションステップを極端に小さくする(性能劣化)
- FMI 2.0 でも
fmi2Discardやfmi2GetRealStatus(fmi2LastSuccessfulTime)は存在するが、イベント駆動型シミュレーションの手順としては未整理
-
イベント駆動の混載シミュレーションが困難
- アナログ側のイベント(ゼロクロス)をデジタル側に正確に伝えられない
- PLL のような閉ループ系では、イベントのタイミングずれが精度に直結
-
イベント駆動型ロールバックのフローが標準化されていない
- GetFMUState/SetFMUState 自体は FMI 2.0 から存在
- しかし Early Return が無いため、「いつロールバックし、どう再実行するか」のパターンがガイドライン依存
- 各ツールベンダーが独自に実装(相互運用性が低い)
FMI 3.0 の解決策:
FMI 3.0 の fmi3DoStep 関数は以下のように拡張されました:
fmi3Status fmi3DoStep(fmi3Instance instance,
fmi3Float64 currentCommunicationPoint,
fmi3Float64 communicationStepSize,
fmi3Boolean noSetFMUStatePriorToCurrentPoint,
fmi3Boolean* eventHandlingNeeded, // FMI 3.0 仕様の正式名称
fmi3Boolean* terminateSimulation,
fmi3Boolean* earlyReturn, // ← FMI 3.0 NEW!
fmi3Float64* lastSuccessfulTime); // ← FMI 3.0 NEW!
新しい出力パラメータの意味:
-
eventHandlingNeeded(出力):- FMU 内部でイベントが発生し、マスターによるイベント処理が必要かどうかを示す
-
earlyReturn(出力、FMI 3.0 NEW!):-
true=currentCommunicationPoint + communicationStepSizeまでステップを完了できず、内部時刻t_{i+1}(<currentCommunicationPoint + communicationStepSize) で戻ったことを示す - イベント検出(
eventHandlingNeeded=true)やearlyReturnRequestedなどが原因になり得る -
false= 要求されたステップサイズまで計算を完了した - 本記事の PLL FMU では、
earlyReturn=trueは VCO ゼロクロスや lock 変化などのイベント検出時にのみ使用
-
-
lastSuccessfulTime(出力、FMI 3.0 NEW!):-
fmi3DoStepから戻った時点で FMU が内部的に到達している時刻t_{i+1}を表す(earlyReturn の有無に関わらず有効) - 通常は
currentCommunicationPoint ≤ lastSuccessfulTime ≤ currentCommunicationPoint + communicationStepSize - 仕様上は、イベントがちょうど
currentCommunicationPointで発生した場合など、earlyReturn=trueかつlastSuccessfulTime == currentCommunicationPointも許可される
-
コード例: FMI 2.0 の限界と FMI 3.0 の解決
FMI 2.0 でのゼロクロス検出(外部処理が必要):
# FMI 2.0: イベント検出はマスター側で実装が必要
current_time = 0.0
H = 10e-9 # 10ns
while current_time < stop_time:
# FMU を進める(Early Return なし)
status = fmu.do_step(current_time, H)
# マスター側でゼロクロスを「推定」
vco_out_prev = vco_out_current
vco_out_current = fmu.get_real([VR_VCO_OUT])[0]
if vco_out_prev < 0.5 and vco_out_current >= 0.5:
# ゼロクロスを検出したが、正確な時刻は不明
# → 線形補間で「推定」するしかない(精度が低い)
t_event = current_time + H * (0.5 - vco_out_prev) / (vco_out_current - vco_out_prev)
print(f"Estimated zero-crossing at t={t_event}")
# ロールバックして再実行?
# → FMI 2.0 では標準化されていない
current_time += H
FMI 3.0 での Early Return(標準機能):
# FMI 3.0: FMU がイベント時刻を正確に返す
current_time = 0.0
H = 10e-9 # 10ns
while current_time < stop_time:
# 状態を保存(ロールバック用)
state = fmu.get_state()
# FMU を進める(Early Return 有効)
status, event_handling_needed, terminate, early_ret, t_last = fmu.do_step(
current_time, H, early_return_allowed=True
)
if not early_ret:
# 正常に H だけ進んだ
current_time += H
else:
# FMU がイベントを検出!
# t_last = イベントが発生した正確な時刻(FMU 内部で計算済み)
print(f"Early return at t={t_last} (event detected by FMU)")
# ロールバック(FMI 3.0 標準)
fmu.set_state(state)
# イベント時刻まで再実行(Early Return 無効で確定)
H_adjusted = t_last - current_time
fmu.do_step(current_time, H_adjusted, early_return_allowed=False)
current_time = t_last # イベント時刻に到達
FMI 3.0 のメリット:
- FMU がイベント時刻を計算: SPICE backend が TR-BDF2 ソルバの内部情報を使って正確な時刻を返す
-
マスターはシンプル:
early_retをチェックして、ロールバックと再実行を行うだけ - 標準化されたフロー: どの FMI 3.0 準拠 FMU でも同じコードで動作
- 精度向上: 線形補間の推定ではなく、実際の非線形ソルバの結果を使用
本プロジェクトでの Early Return の活用
PLL 混載シミュレーションでは、以下のイベントで Early Return を使用します:
SPICE 側イベント:
| イベント | 検出する FMU | 条件 | 目的 |
|---|---|---|---|
| VCO 立ち上がり | SPICE |
V(VCO_OUT) が 0.45V → 0.55V を跨ぐ |
クロックエッジをデジタル側に正確に通知 |
| VCO 立ち下がり | SPICE |
V(VCO_OUT) が 0.55V → 0.45V を跨ぐ |
クロックエッジをデジタル側に正確に通知 |
これが本 PLL シミュレーションの主要なイベントです。
SV 側イベント(将来的な拡張):
| イベント | 検出する FMU | 条件 | 目的 |
|---|---|---|---|
| ロック状態変化 | SV | PLL がロック/アンロックに遷移 | 制御ループの状態変化を記録 |
SV 側でイベントを検出した場合、DoStepResponse.early_return=true を返します。
両側イベント発生時の処理:
- SV と SPICE 両方が early_return を返した場合:
- より早い時刻
min(last_successful_time_sv, last_successful_time_sp)を採用 - 両 FMU を
t_nにロールバック - 調整ステップサイズでイベント時刻まで再実行
- より早い時刻
- 本記事の初期構想では、SPICE 側イベント(VCO ゼロクロス)のみを扱うことを推奨
Early Return により、Simulink の Variable Step ソルバと類似の機能を実現できます。
なぜ gRPC + Protobuf でプロセスを完全分離するのか
プロセス分離の必要性
混載シミュレーションでは、デジタル(SystemVerilog)とアナログ(SPICE)という全く異なる計算特性を持つシミュレータを協調動作させる必要があります:
計算特性の違い:
- SystemVerilog: イベント駆動型、離散時間、高速だが時刻が飛ぶ
- SPICE: 連続時間型、微分方程式ソルバ、計算負荷が高い
プロセス分離のメリット:
-
並列実行によるスピードアップ
- SV と SPICE を別々の CPU コア/プロセスで同時実行
- 一方が計算中でも、もう一方は独立して進められる
- 特に PLL のような閉ループ系では、ステップごとに交互に実行するため並列化の効果が大きい
-
独立したメモリ空間
- SV と SPICE で巨大なメモリを別々に確保可能
- 一方のシミュレータのクラッシュが他方に波及しない
- デバッグ時にプロセスを個別に追跡できる
-
異なる言語・ツールチェーンの混在
- SV シミュレータ: C++/SystemVerilog
- SPICE シミュレータ: C++/数値計算ライブラリ
- マスター: Python(プロトタイピングが容易)
- それぞれ最適な言語・ツールで実装可能
-
スケーラビリティ
- 将来的に複数の FMU を追加する際も、プロセスを追加するだけ
- 分散マシン上での実行も視野に入る
- Docker コンテナ等での実行も容易
なぜ gRPC + Protobuf なのか
プロセス間通信の方式として gRPC + Protobuf を選択した理由:
1. 型安全な通信
message DoStepRequest {
string instance_id = 1;
double current_time = 2;
double step_size = 3;
bool no_set_fmu_state_prior_to_current_point = 4;
bool early_return_allowed = 5;
}
message DoStepResponse {
Fmi3Status status = 1;
bool event_handling_needed = 2;
bool terminate_simulation = 3;
bool early_return = 4;
double last_successful_time = 5;
}
- Protobuf の
.protoファイルで完全に型定義される - コンパイル時に型チェックされ、実行時エラーを大幅に削減
- Python / C++ / Go など多言語で同じ定義を共有可能
2. 高速なバイナリシリアライゼーション
| 方式 | エンコードサイズ | パース速度 | 実装の容易さ |
|---|---|---|---|
| JSON | 大(可読) | 遅い | 容易 |
| MessagePack | 中 | 中 | 中 |
| Protobuf | 小 | 高速 | 自動生成 |
| Cap'n Proto | 極小 | 極速 | やや複雑 |
- Protobuf は XML と比べて 3〜10 倍小さく、20〜100 倍高速といわれる
- JSON と比べても、典型的なベンチマークで数倍程度小さく・速いケースが多い
- 混載シミュレーションでは
DoStepを何万回も呼ぶため、この効率向上が効く
3. FMI 3.0 との親和性
FMI 3.0 の C API をそのまま gRPC サービスにマッピング可能:
service Fmi3Remote {
// FMI 3.0 の関数がそのまま RPC に
rpc InstantiateCoSimulation(...) returns (...);
rpc DoStep(...) returns (...);
rpc GetReal(...) returns (...);
rpc SetState(...) returns (...);
}
- FMI 標準のセマンティクスを保ったまま、プロセス間通信に変換
- 既存の FMI ツールチェーンとの互換性を維持
4. 双方向ストリーミング(将来拡張)
gRPC は単純な Request-Response だけでなく、ストリーミングもサポート:
rpc StreamWaveform(stream WaveformRequest)
returns (stream WaveformResponse);
- リアルタイム波形モニタリング
- プログレス通知
- 動的なパラメータ調整
5. 言語間の相互運用性
| コンポーネント | 実装言語 | 理由 |
|---|---|---|
| FMI マスター | Python | プロトタイピングが容易、Jupyter 連携 |
| SV Backend | C++ | SystemVerilog エンジンが C++ |
| SPICE Backend | C++ | 数値計算の性能が必要 |
- gRPC は 15+ 言語をサポート
- 各コンポーネントを最適な言語で実装可能
-
.protoファイル 1 つで全言語のコードを自動生成
6. エラーハンドリングとデバッグ
try:
response = stub.DoStep(request, timeout=10.0)
except grpc.RpcError as e:
print(f"RPC failed: {e.code()}, {e.details()}")
# status code: DEADLINE_EXCEEDED, UNAVAILABLE, etc.
- タイムアウト、リトライ、デッドライン管理が標準で組み込み
- ステータスコードによる詳細なエラー分類
- 各 RPC 呼び出しを個別にログ出力可能
代替案との比較
他の選択肢も検討しましたが、以下の理由で gRPC + Protobuf を選択:
| 方式 | メリット | デメリット | 判定 |
|---|---|---|---|
| 共有メモリ | 最速 | 同一マシン限定、メモリ管理が複雑 | ❌ スケールしない |
| ZeroMQ | 柔軟、高速 | 型安全性なし、スキーマ管理が手動 | △ 規模が大きくなると辛い |
| HTTP/JSON | 実装が容易 | 遅い、型安全性なし | ❌ 性能不足 |
| gRPC/Protobuf | 型安全、高速、多言語 | 学習コスト | ✅ 採用 |
| Cap'n Proto | 極めて高速 | エコシステムが小さい | △ 枯れていない |
実装の具体例
Python マスターから C++ backend への呼び出し:
# Python 側(マスター)
import grpc
from fmi3_remote_pb2 import DoStepRequest
from fmi3_remote_pb2_grpc import Fmi3RemoteStub
channel = grpc.insecure_channel('localhost:50051')
stub = Fmi3RemoteStub(channel)
request = DoStepRequest(
instance_id="pll_sv_001",
current_time=1.5e-6,
step_size=10e-9,
early_return_allowed=True
)
response = stub.DoStep(request, timeout=5.0)
print(f"Status: {response.status}, Early return: {response.early_return}")
C++ backend での受信処理:
// C++ 側(SV backend)
class Fmi3RemoteServiceImpl final : public Fmi3Remote::Service {
Status DoStep(ServerContext* context,
const DoStepRequest* request,
DoStepResponse* response) override {
// instance_id から SV シミュレータインスタンスを取得
auto* sim = instance_manager_.get(request->instance_id());
// SystemVerilog シミュレータを進める
auto result = sim->advance_time(
request->current_time(),
request->step_size(),
request->early_return_allowed()
);
// 結果を Protobuf レスポンスに詰める
response->set_status(result.status);
response->set_early_return(result.early_return);
response->set_last_successful_time(result.time);
return Status::OK;
}
};
通信のオーバーヘッド:
実測値(参考):
- DoStep 1 回の RPC オーバーヘッド: 約 50〜200 μs(ローカル)
- PLL シミュレーションのステップサイズ: 10 ns
- 1 ステップあたりの計算時間: 数 ms〜数十 ms(SPICE のソルバが支配的)
→ RPC オーバーヘッドは計算時間の 1% 未満なので、プロセス分離のメリットが上回る
proto ファイルと config ファイルの関係
本プロジェクトでは、proto ファイル(通信プロトコル層)と config ファイル(backend 固有設定)を明確に分離する設計を採用しています。この分離により、回路構成を変更しても proto ファイルの変更が不要になります。
役割の違い
| 項目 | proto ファイル | config ファイル |
|---|---|---|
| 定義するもの | gRPC 通信プロトコル | 変数マッピング |
| スコープ | 汎用的(全backend共通) | backend 固有 |
| 言語 | Protobuf | JSON |
| 変更頻度 | 低(FMI仕様変更時) | 高(回路構成変更時) |
| 例 | GetReal(vr=0) |
vr=0 → ノード VCTRL |
なぜ repeated が重要か
proto ファイルでは、repeated キーワードで可変長配列を実現しています:
message SetBooleanRequest {
string instance_id = 1;
repeated uint32 value_references = 2; // ← 可変長配列
repeated bool values = 3; // ← 可変長配列
}
これにより、新しい変数を追加しても proto ファイルの変更が不要です。
例1: 既存の変数だけ使う
# vr=10 (clk_ref) だけ設定
fmu.set_boolean([10], [True])
例2: 新しい変数 vr=13 (reset) を追加
# vr=10 と vr=13 を同時設定
fmu.set_boolean([10, 13], [True, False])
proto ファイルは全く変更していません。repeated が配列長を動的に扱うため、任意の数の変数を送受信できます。
データフローの具体例
Python マスターから SV backend へ clk_ref を設定する場合:
┌─────────────────────────────────────────────┐
│ 1. Python マスター (FMI レイヤ) │
│ fmu_sv.set_boolean([10], [True]) │
│ # vr=10 に True を設定 │
└──────────────┬──────────────────────────────┘
│ gRPC/Protobuf 通信
▼
┌─────────────────────────────────────────────┐
│ 2. proto ファイルの定義 (通信層) │
│ SetBooleanRequest { │
│ instance_id: "pll_sv_001" │
│ value_references: [10] // ← vr番号のみ │
│ values: [true] │
│ } │
└──────────────┬──────────────────────────────┘
│ RPC 呼び出し
▼
┌─────────────────────────────────────────────┐
│ 3. SV backend (C++ gRPC サーバ) │
│ Status SetBoolean(...) { │
│ uint32 vr = req->value_references(0); │
│ bool value = req->values(0); │
│ │
│ // ★ config_file を参照してマッピング │
│ auto binding = config_.find_variable(vr);│
│ // → vr=10 は "pll_top.clk_ref" │
│ │
│ sv_sim->set_signal("pll_top.clk_ref", │
│ value); │
│ } │
└─────────────────────────────────────────────┘
設計の利点
メリット1: backend の差分を吸収
同じ proto ファイルで異なる backend に対応:
| backend | proto(共通) | config(固有) |
|---|---|---|
| SV | GetRealRequest(vr=0) |
"sv_signal": "pll_top.vctrl" |
| SPICE | GetRealRequest(vr=0) |
"node": "VCTRL" |
メリット2: 変数追加が容易
新しい変数(例: reset 信号)を追加する場合、config ファイルに追加するだけ:
{
"name": "reset",
"vr": 13,
"type": "Boolean",
"direction": "input",
"sv_signal": "pll_top.reset_n"
}
proto ファイルは変更不要。既存の SetBoolean RPC をそのまま使えます。
メリット3: 再利用性
- proto ファイル: FMI 3.0 準拠の任意の backend で使い回せる(PLL, ADC, DAC, メモリ等)
- config ファイル: 回路ごとに個別に作成
proto 変更が必要になるケース
以下の場合のみ proto ファイルの変更が必要です:
-
新しい型を追加(例: String, Binary)
- FMI 3.0 標準の Real/Boolean/Int32 以外を使う場合
-
新しい RPC を追加(例: BatchDoStep, StreamWaveform)
- FMI 3.0 標準にない独自機能を追加する場合
- FMI 仕様自体が変わる(例: FMI 3.0 → 4.0)
通常の回路設計(変数の追加・削除・再割り当て)では、config ファイルのみ変更すれば対応できます。これが「proto は汎用的、config は個別」という設計の利点です。
プロジェクトの目標
一番の目的
デジタル(SV)とアナログ(SPICE)の混載シミュレーションを、自作シミュレータ同士で接続し、シミュレータの協調方法を学習したい
- ゼロクロス検出やロールバックを含めて、時間発展をどう設計すると綺麗か?
- オープン標準(FMI 3.0)を使うことで、将来的に他のツールとも接続可能にする
- プロセス完全分離により、並列実行とスケーラビリティを実現する
を、自分の手元のプロジェクトで検証するのが主目的です。
副次的な目標
- 「自作シミュレータ同士をつなぐ」例として、オープンな設計を一つ置いておきたい
- 商用ツールの独自方式に対して、標準規格ベースの代替案を示す
- SystemVerilog / SPICE 双方の開発を続ける上で、
- 「混載をやるなら、この辺を意識して設計しておくと後から楽」
- 「ここを I/F 表に起こしておくと、AI に説明しやすい」
という知見を貯めたい
- 将来的には、既存の SPICE(ngspice, Xyce)や Verilog シミュレータ(Verilator, Icarus Verilog)とも接続可能にする
- gRPC によるプロセス分離のパターンを確立し、他のシミュレーション結合にも応用する
今回のテーマ:PLL 混載シミュレーションの構想
今回の PLL 混載構想を一言でいうと、
PLL を「デジタル PLL ロジック(SV)FMU」と「アナログ VCO+ループフィルタ(SPICE)FMU」に分割し、
FMI 3.0 Co-Simulation + gRPC で両者を接続する
というものです。
全体構成
全体は以下 3 プロセスに分かれ、gRPC + Protobuf で通信します。
-
Python 製 FMI 3.0 マスター:
sukimafmimaster -
SystemVerilog デジタル PLL:
sukimasim/fmi3_backend/fmi3_remote_sv_server(gRPC サーバ) -
SPICE アナログ VCO+ループフィルタ:
sukimaspice/fmi3_backend/fmi3_remote_spice_server(gRPC サーバ)
通信プロトコル:
-
fmi3_remote.protoで定義されたFmi3RemotegRPC サービス - FMI 3.0 の全ライフサイクル関数(InstantiateCoSimulation, DoStep, GetReal, SetState など)を RPC としてマッピング
- 各 backend サーバは複数の FMU インスタンス(
instance_id)を管理可能
拡張性:
- FMI マスターは 任意の数の FMU を管理可能
- 各 FMU は独立した gRPC サーバとして実装
- PLL 以外にも、任意のサブシステム(ADC, DAC, センサー, アクチュエータ等)を FMU として追加可能
- 例: メモリコントローラ(SystemVerilog)、電源回路(SPICE)、熱解析(Python/Modelica)等を同時にシミュレーション
-
Multi-FMU 並列実行:
MultiSimulationクラスが複数 FMU を並列でDoStep実行 - スケーラビリティ: 分散マシン、Docker コンテナ、クラウド上での実行も視野に
ざっくりした構成要素と役割を、表にすると次のようになります。
| コンポーネント | 実装プロセス | 役割 | 主な入出力(FMI 経由) |
|---|---|---|---|
| FMI マスター | sukimafmimaster |
複数の FMU を時間同期・制御(PLL例では2個) | 全変数の仲介・時間同期・ロールバック制御 |
| デジタル PLL ロジック |
sukimasim backend |
PFD / ÷N 分周器 / ロック検出など | 入力: clk_ref, clk_out, div_ratio / 出力: cp_up, cp_down, lock
|
| アナログ VCO + ループフィルタ |
sukimaspice backend |
VCO 発振・ループフィルタのアナログ動作 | 入力: cp_up, cp_down / 出力: vctrl, vco_out, clk_out
|
| (追加可能な FMU) | 任意の backend | 任意のサブシステム | 自由に定義可能 |
さらにブロック図を書くと、以下のようになります。
ポイントは、
- プロセスが完全に分離: SV と SPICE は独立したプロセスで並列実行可能
- FMI マスターは複数 FMU に対応: PLL の例では 2 個だが、任意の数の FMU を追加可能
-
任意のサブシステムを FMU として追加: ADC, DAC, センサー, メモリ, 電源回路, 熱解析など、
Fmi3RemotegRPC インターフェースを実装すれば自由に追加 -
Multi-FMU 並列実行:
MultiSimulationクラスが複数 FMU をThreadPoolExecutorで並列実行 - PLL ループが閉じている: SV(PFD)→ SPICE(ループフィルタ+VCO)→ SV(分周器)という双方向フィードバック
- ロジック⇔アナログの境界信号を 表で明示的に定義している こと
- FMI 3.0 の標準機能(Early Return、GetState/SetState)を活用
- gRPC による型安全で高速な通信
- スケーラビリティ: 分散マシン、Docker、クラウド上での実行も可能
です。
以降では、この「境界」と「時間の扱い」をもう少し具体的に書いていきます。
ロジック⇔アナログのインタフェース設計
混載シミュレーションで一番揉めるのが、ロジックとアナログの境界です。
どの信号を FMI Real / Boolean / Int32 に載せるか
どこをゼロクロス検出の対象にするか
どこまで PLL の内部を見せるか
を最初に表にしておくと、SV/SPICE/マスターの 3 者がかなり楽になります。
重要: PLL は閉ループ系なので、Digital → Analog と Analog → Digital の両方向の信号が必要です。
Digital → Analog(SV → SPICE)
ロジック側からアナログ側に渡す情報:
| I/F名 | 物理意味 | FMI vr | FMI型 | SV 側 | SPICE 側 | 備考 |
|---|---|---|---|---|---|---|
| cp_up | Charge Pump UP信号 | 2 | Real (A) | pll_top.cp_up | ノード CP_UP | PFD出力(電流源制御) |
| cp_down | Charge Pump DOWN信号 | 3 | Real (A) | pll_top.cp_down | ノード CP_DOWN | PFD出力(電流源制御) |
| div_ratio | 分周比設定値 | 20 | Int32 | pll_top.div_ratio | param N 等で利用可 | N=4〜512等 |
Charge Pump 信号の詳細仕様:
基本動作:
- PFD(Phase Frequency Detector)が
clk_refとclk_div(=clk_outを分周したもの)の位相差を検出 - 位相差に応じて
cp_upまたはcp_downの電流値を出力 - これがアナログ側のループフィルタに入力され、
vctrlを生成 - これがないと PLL のループが閉じません
電流値の定義:
- 最大電流値
Icp: 100μA(推奨値、調整可能) -
cp_upの範囲: 0A(OFF)〜 Icp(フルON) -
cp_downの範囲: 0A(OFF)〜 Icp(フルON) - 通常動作では、
cp_upとcp_downは同時に ON にならない(排他的)
PFD の動作:
-
clk_refがclk_divより進んでいる(位相差 > 0):-
cp_up = Icp * (位相差 / 2π)(簡略化) cp_down = 0- → ループフィルタの電圧が上昇 → VCO 周波数が上昇
-
-
clk_divがclk_refより進んでいる(位相差 < 0):cp_up = 0-
cp_down = Icp * (|位相差| / 2π)(簡略化) - → ループフィルタの電圧が低下 → VCO 周波数が低下
- 位相一致(位相差 ≈ 0):
-
cp_up = 0,cp_down = 0
-
デッドゾーンとリーク電流:
- デッドゾーン: 位相差が非常に小さい場合(例: |位相差| < 10°)、
cp_up = cp_down = 0 - リーク電流: 実装を簡略化するため、本記事では考慮しない(理想電流源として扱う)
実装ノート:
- SV 側では、PFD が位相差を検出して電流値を計算(理想 Charge Pump)
- SPICE 側では、FMI 経由で受け取った電流値を電流源
I_CP_UP,I_CP_DOWNに設定 - より高精度なシミュレーションが必要な場合、SPICE 側でトランジスタレベルの Charge Pump を実装可能
Analog → Digital(SPICE → SV)
PLL フィードバックループの後半部分:
| I/F名 | 物理意味 | FMI vr | FMI型 | SPICE 側 | SV 側 | 備考 |
|---|---|---|---|---|---|---|
| vctrl | ループフィルタ出力電圧 | 0 | Real (V) | ノード VCTRL | pll_top.vctrl | VCO制御電圧(監視/デバッグ用、転送はオプション) |
| vco_out | VCO 出力電圧 | 1 | Real (V) | ノード VCO_OUT | (観測用) | 生のアナログ波形 |
| clk_out | VCO 出力クロック(論理) | 11 | Boolean | V(VCO_OUT) 閾値判定 | pll_top.clk_out | デジタル PLL の入力クロック(必須) |
| lock | ロック検出フラグ | 12 | Boolean | - | pll_top.lock | SV側で生成(SV→Master) |
- vctrl: ループフィルタ出力をそのまま Real として FMI で公開します。主にデバッグ/モニタリング用途であり、SV 側で実際に使用しない場合は転送を省略可能です。
- vco_out: デバッグ用途のアナログ波形。マスターや GUI でプロットする想定。
-
clk_out: VCO のアナログ出力から生成した論理クロック(PLL フィードバックループに必須)。
SPICE 側でV(VCO_OUT)の閾値判定(ヒステリシス付き)を Boolean に変換し、SV 側のpll_top.clk_outに入れます。
Simulink でいう zero-crossing detection も、この clk_out の 0→1 / 1→0 変化をイベントとして扱うイメージです。
FMI 変数 (valueReference) の割り当て
FMI 3.0 上では、これらは valueReference (vr) で識別されます。PLL 用の例はこんな感じです。
| vr | 変数名 | FMI 型 | ドメイン | 方向 | 説明 |
|---|---|---|---|---|---|
| 0 | vctrl | Real | Analog | SPICE → SV | ループフィルタ出力電圧 |
| 1 | vco_out | Real | Analog | SPICE → Master | VCO 出力電圧(観測用) |
| 2 | cp_up | Real | Analog | SV → SPICE | Charge Pump UP 電流 |
| 3 | cp_down | Real | Analog | SV → SPICE | Charge Pump DOWN 電流 |
| 10 | clk_ref | Boolean | Digital | Master → SV | 参照クロック |
| 11 | clk_out | Boolean | Digital | SPICE → SV | VCO 出力クロック(論理) |
| 12 | lock | Boolean | Digital | SV → Master | ロック検出フラグ |
| 20 | div_ratio | Int32 | Control | Master → SV | 分周比設定 |
変数レンジのルール:
- 0–9: Analog (Real) - どちら向きでも可、主にアナログ物理量(電圧、電流)
- 10–19: Digital (Boolean) - どちら向きでも可、論理信号(クロック、フラグ)
- 20–29: Control (Int32) - どちら向きでも可、制御パラメータ(分周比、設定値)
このようなレンジルールを型ごとに決めておくと、後から変数を足すときも整理しやすいです。また、各変数の direction フィールド(入出力方向、FMI でいう causality に相当)を明示することで、因果性の問題を防げます。
変数拡張ガイドライン:
新しい変数の追加:
-
既存のレンジ内で追加する場合:
- 同じ型の変数は同じレンジ内に追加(例: 新しい Real 変数は vr=4〜9 から選択)
- 番号は昇順で採番(欠番があれば埋めても可)
- config ファイルに変数定義を追加するだけで、proto ファイルの変更は不要
-
レンジが不足した場合:
- 各型のレンジを拡張(例: Real を 0〜19 に拡張)
- 重要: レンジ拡張時は他の型と重複しないよう注意
- 推奨: 各型に 10〜20 個のレンジを確保
-
新しい型を追加する場合:
- FMI 3.0 標準の String, Binary 等を使用する場合
- proto ファイルに新しい RPC メソッドを追加(例: SetString, GetString)
- 新しいレンジを割り当て(例: String は 30〜39)
例: reset 信号の追加:
// config ファイルに追加(proto 変更不要)
{
"name": "reset",
"vr": 13, // Boolean レンジ (10〜19) 内の空き番号
"type": "Boolean",
"direction": "input",
"sv_signal": "pll_top.reset_n"
}
ベストプラクティス:
- 変数の vr は、型ごとにグループ化して管理
- config ファイルには変数の
unit(単位)を明記(例: "V", "A", "Hz") - 方向(direction)は必ず明示して、因果性の問題を防ぐ
- デバッグ用変数は最後の方の番号を使用(例: vr=9, 19, 29)
backend 用 config_file (JSON) で SV/SPICE をバインドする
この I/F を SystemVerilog / SPICE の世界にどう紐付けるかは、JSON の config_file で表現します。
SystemVerilog backend 用 config.json 例
{
"backend_type": "systemverilog",
"variables": [
{ "name": "vctrl", "vr": 0, "type": "Real", "direction": "input", "sv_signal": "pll_top.vctrl", "unit": "V" },
{ "name": "cp_up", "vr": 2, "type": "Real", "direction": "output", "sv_signal": "pll_top.cp_up", "unit": "A" },
{ "name": "cp_down", "vr": 3, "type": "Real", "direction": "output", "sv_signal": "pll_top.cp_down", "unit": "A" },
{ "name": "clk_ref", "vr": 10, "type": "Boolean", "direction": "input", "sv_signal": "pll_top.clk_ref" },
{ "name": "clk_out", "vr": 11, "type": "Boolean", "direction": "input", "sv_signal": "pll_top.clk_out" },
{ "name": "lock", "vr": 12, "type": "Boolean", "direction": "output", "sv_signal": "pll_top.lock" },
{ "name": "div_ratio", "vr": 20, "type": "Int32", "direction": "input", "sv_signal": "pll_top.div_ratio" }
]
}
SPICE backend 用 config.json 例
{
"backend_type": "spice",
"variables": [
{ "name": "vctrl", "vr": 0, "type": "Real", "direction": "output", "node": "VCTRL", "unit": "V" },
{ "name": "vco_out", "vr": 1, "type": "Real", "direction": "output", "node": "VCO_OUT", "unit": "V" },
{ "name": "cp_up", "vr": 2, "type": "Real", "direction": "input", "node": "CP_UP", "unit": "A", "source_type": "current" },
{ "name": "cp_down", "vr": 3, "type": "Real", "direction": "input", "node": "CP_DOWN", "unit": "A", "source_type": "current" },
{
"name": "clk_out",
"vr": 11,
"type": "Boolean",
"direction": "output",
"expr": "V(VCO_OUT) > 0.5",
"zero_crossing_detection": true,
"hysteresis": {
"rising_threshold": 0.55,
"falling_threshold": 0.45
}
}
],
"state_variables": [
{ "type": "node_voltage", "nodes": ["VCTRL", "VCO_OUT", "CP_UP", "CP_DOWN"] },
{ "type": "capacitor_voltage", "elements": ["C1", "C2"] },
{ "type": "vco_phase", "element": "XVCO" }
]
}
追加された重要な要素:
-
direction: 入出力の方向を明示(因果性の問題を防ぐ) -
hysteresis: チャタリング防止のためのヒステリシス設定- 立ち上がり: 0.55V、立ち下がり: 0.45V
- ノイズによる誤検出を防ぐ
-
zero_crossing_detection: ゼロクロス検出を有効化(early return のトリガー) -
state_variables: GetState/SetState で保存・復元する変数を明示- ノード電圧(特にキャパシタ)
- VCO の内部位相(重要!ロールバック時に必要)
- ※ 実装としては、ここで列挙した物理量に加えて、TR-BDF2 ソルバ内部の状態も含めた「FMU内部状態全体」を
GetFMUState/SetFMUStateで保存・復元する
- SPICE 側では
exprでしきい値比較を書いておき、backend がこの式を評価して Boolean を生成します。 - ここまで決めておくと、「どの FMI 変数がどの信号に対応してるか」が JSON 一枚で追えるので、SV/SPICE/マスター 3 者の連携がかなり楽になります。
デジタル PLL / アナログ VCO+LF のインタフェース
内部実装はかなり自由にできるので、外側の I/F だけ固めておきます。
SystemVerilog 側 pll_top のポート例
module pll_top (
input logic clk_ref, // 参照クロック (FMI: clk_ref)
input logic reset_n,
input logic clk_out, // VCO 出力クロック (FMI: clk_out from SPICE)
input real vctrl, // ループフィルタ出力電圧 (観測/デバッグ用、オプション)
input int div_ratio, // 分周比設定 (FMI: div_ratio)
output real cp_up, // Charge Pump UP 信号 (FMI: cp_up to SPICE)
output real cp_down, // Charge Pump DOWN 信号 (FMI: cp_down to SPICE)
output logic lock // ロック検出フラグ (FMI: lock)
);
// 内部構成:
// - PFD (Phase Frequency Detector)
// 入力: clk_ref, clk_div (clk_out を div_ratio で分周)
// 出力: cp_up, cp_down (real型、Charge Pump電流値 0〜Icp)
// - ÷N 分周器
// - ロック検出器
logic clk_div; // clk_out を div_ratio で分周した信号
// PFD の実装例(簡略化)
always_ff @(posedge clk_ref or negedge reset_n) begin
if (!reset_n) begin
cp_up <= 0.0;
cp_down <= 0.0;
end else begin
// 位相差に応じて cp_up / cp_down を生成
// 詳細は実装時に
end
end
// 分周器
// always_ff ... で実装
// ロック検出
// always_ff ... で実装
endmodule
重要な変更点:
-
cp_up,cp_down出力を追加: これが PLL ループを閉じるための信号 - PFD が位相差を検出し、Charge Pump 電流値(real型、単位: A)を出力
- SPICE 側のループフィルタに入力される
SPICE 側 PLL_ANALOG サブ回路の I/F
* VCO + Loop Filter + Charge Pump subcircuit
.subckt PLL_ANALOG CP_UP CP_DOWN VCTRL VCO_OUT VDD GND
* CP_UP : Charge Pump UP 電流入力 (FMI: cp_up from SV)
* CP_DOWN : Charge Pump DOWN 電流入力 (FMI: cp_down from SV)
* VCTRL : ループフィルタ出力 / VCO 制御電圧 (FMI: vctrl)
* VCO_OUT : VCO 出力ノード (FMI: vco_out, clk_out 生成元)
* VDD, GND: 電源・グランド
* Charge Pump (電流源として実装)
I_CP_UP CP_UP VCTRL DC 0 ; FMI経由で制御
I_CP_DOWN VCTRL CP_DOWN DC 0 ; FMI経由で制御
* Loop Filter (2nd order RC filter の例)
R1 VCTRL GND 10k
C1 VCTRL GND 100p
R2 VCTRL n1 1k
C2 n1 GND 10p
* VCO (電圧制御発振器)
* f_out = f0 + Kvco * (VCTRL - Vmid)
* 簡易モデル: 位相累積型 VCO
.model VCO_MODEL vco (freq0=1GHz Kvco=100MHz/V vdd=1.2)
XVCO VCTRL VCO_OUT VDD GND VCO_MODEL
.ends PLL_ANALOG
重要な変更点:
-
CP_UP,CP_DOWNノードを追加: SV側のPFD出力を受け取る - Charge Pump を電流源として実装(FMI経由で電流値を制御)
- ループフィルタが Charge Pump 電流を積分して
VCTRLを生成 - VCO が
VCTRLに応じて発振周波数を変化
VCO の位相累積モデル:
- VCO は内部に位相 $\phi$ を持つ: $\frac{d\phi}{dt} = 2\pi f(V_{CTRL})$
- この位相情報は GetState/SetState で保存・復元する必要がある(重要!)
- SPICE backend は
V(VCO_OUT)が閾値(0.45V / 0.55V)を跨いだ瞬間を「イベント」として検出します(後述)。
時間同期と「ゼロクロス検出」(zero-crossing detection) 相当の設計
固定ステップ + 双方向フィードバック
時間の進め方としては、まず 固定コミュニケーションステップ H を共有し、各ステップで SV → SPICE → SV の順序で進めます。
重要: PLL は閉ループなので、以下の順序が必要です:
-
SV backend を進める: 前ステップの
clk_outを使って PFD がcp_up/cp_downを生成 -
SPICE backend を進める:
cp_up/cp_downを受け取り、ループフィルタ+VCO でvctrl/clk_outを生成 -
信号を転送: SPICE → SV へ
clk_outを転送(次ステップで使用)
時間進行のイメージ(通常ケース: イベントなし):
- 各ステップで SV → SPICE の順序で進める
- SV が出力した
cp_up/cp_downを SPICE へ転送(青い矢印) - SPICE が出力した
clk_outを次ステップの SV へフィードバック(赤い矢印) - PLL ループが時間をまたいで閉じる
時間進行のイメージ(Early Return ケース: ゼロクロス検出):
-
試行1(Trial 1): SPICE が
t_nからt_n+Hへ進もうとして、途中のt_eでゼロクロス検出 -
early_return=true: SPICE が
last_successful_time=t_eを返す(緑のマーカー) -
ロールバック(Rollback): 両 FMU を保存済み状態(
t_n)に戻す(GetState/SetState) -
再試行(Retry): 調整ステップサイズ
H' = t_e - t_nで、early_return_allowed=falseにして再実行 -
結果: イベント時刻
t_eに正確に到達し、次ステップへ継続
この仕組みにより、Simulink の Variable Step ソルバの zero-crossing detection と類似のアプローチでイベントを捉えられます。
# 定数定義
VR_VCTRL = 0
VR_VCO_OUT = 1
VR_CP_UP = 2
VR_CP_DOWN = 3
VR_CLK_REF = 10
VR_CLK_OUT = 11
VR_LOCK = 12
VR_DIV_RATIO = 20
# 初期化(省略: InstantiateCoSimulation, SetupExperiment, etc.)
current_time = 0.0
H = 10e-9 # 10ns (コミュニケーションステップ)
CLK_REF_PERIOD = 10e-9 # 10ns (100MHz)
while current_time < stop_time:
# 0. 参照クロック clk_ref の生成と設定
# 実世界では外部から供給されるが、本記事では簡易的にマスターから生成
clk_ref_state = (int(current_time / (CLK_REF_PERIOD / 2)) % 2) == 0
fmu_sv.set_boolean([VR_CLK_REF], [clk_ref_state])
# 1. SV を進める(前ステップの clk_out を使用)
# → PFD が cp_up / cp_down を生成
status_sv, _, _, _, _ = fmu_sv.do_step(current_time, H)
# 2. SV から SPICE へ Charge Pump 信号を転送
cp_up = fmu_sv.get_real([VR_CP_UP])[0]
cp_down = fmu_sv.get_real([VR_CP_DOWN])[0]
fmu_spice.set_real([VR_CP_UP, VR_CP_DOWN], [cp_up, cp_down])
# 3. SPICE を進める(early_return 有効)
# → ループフィルタ + VCO で vctrl / clk_out を生成
status_sp, ev_sp, term_sp, early_sp, t_e = fmu_spice.do_step(
current_time, H, early_return_allowed=True
)
# 4. SPICE から SV へ信号を転送
clk_out = fmu_spice.get_boolean([VR_CLK_OUT])[0]
vctrl = fmu_spice.get_real([VR_VCTRL])[0]
vco_out = fmu_spice.get_real([VR_VCO_OUT])[0] # デバッグ用
fmu_sv.set_boolean([VR_CLK_OUT], [clk_out])
fmu_sv.set_real([VR_VCTRL], [vctrl]) # 観測用(オプション)
# 5. Early return の処理は後述
if not early_sp:
current_time += H
else:
# ロールバック処理(次のセクションで詳述)
pass
ここに FMI 3.0 の early return + ロールバック を組み合わせると、
Simulink でいう zero-crossing detection と同じことができます。
協調ロールバックの手順(FMI でやるゼロクロス検出)
PLL 混載シミュレーションでは、clk_out の立ち上がり/立ち下がりがステップの途中で発生します。このときに
- SPICE backend が「ゼロクロス検出」をして
early_return=trueを返し、 - マスターが SV/SPICE 両方をイベント時刻にロールバック
する手順を仕様として決めています。
完全版の擬似コード:
current_time = 0.0
H = 10e-9 # コミュニケーションステップ
CLK_REF_PERIOD = 10e-9 # 10ns (100MHz)
while current_time < stop_time:
# ステップ0: 参照クロック clk_ref の生成と設定
clk_ref_state = (int(current_time / (CLK_REF_PERIOD / 2)) % 2) == 0
fmu_sv.set_boolean([VR_CLK_REF], [clk_ref_state])
# ステップ1: 両FMUの状態を保存
state_sv = fmu_sv.get_state()
state_sp = fmu_spice.get_state()
# ステップ2: SV を進める(前ステップの clk_out を使用)
status_sv, _, _, _, _ = fmu_sv.do_step(current_time, H)
# ステップ3: SV → SPICE への信号転送
cp_up = fmu_sv.get_real([VR_CP_UP])[0]
cp_down = fmu_sv.get_real([VR_CP_DOWN])[0]
fmu_spice.set_real([VR_CP_UP, VR_CP_DOWN], [cp_up, cp_down])
# ステップ4: SPICE を進める(early_return 有効)
status_sp, event_handling_needed, terminate, early_ret, last_time = fmu_spice.do_step(
current_time, H, early_return_allowed=True
)
# ステップ5: Early return の判定
if not early_ret:
# イベントなし: 通常進行
# SPICE → SV への信号転送
clk_out = fmu_spice.get_boolean([VR_CLK_OUT])[0] # 必須
vctrl = fmu_spice.get_real([VR_VCTRL])[0]
fmu_sv.set_boolean([VR_CLK_OUT], [clk_out])
fmu_sv.set_real([VR_VCTRL], [vctrl]) # デバッグ/モニタリング用(省略可)
current_time += H
else:
# イベント検出: ロールバック処理
print(f"Event detected at t={last_time:.3e}s, rolling back from t={current_time:.3e}s")
# ステップ6: 両FMUを t_n に巻き戻す
fmu_sv.set_state(state_sv)
fmu_spice.set_state(state_sp)
# ステップ7: 調整ステップサイズを計算
H_adjusted = last_time - current_time
assert H_adjusted >= 0, "Adjusted step size must be non-negative"
# ステップ8: イベント時刻まで再実行(early_return 無効)
# 注: FMI 3.0 では communicationStepSize > 0.0 が必須のため、
# H_adjusted == 0 の場合は do_step をスキップ
if H_adjusted > 0.0:
# 8a. SV を再実行
fmu_sv.do_step(current_time, H_adjusted, early_return_allowed=False)
# 8b. SV → SPICE 信号転送
cp_up = fmu_sv.get_real([VR_CP_UP])[0]
cp_down = fmu_sv.get_real([VR_CP_DOWN])[0]
fmu_spice.set_real([VR_CP_UP, VR_CP_DOWN], [cp_up, cp_down])
# 8c. SPICE を再実行
fmu_spice.do_step(current_time, H_adjusted, early_return_allowed=False)
# 8d. SPICE → SV 信号転送
clk_out = fmu_spice.get_boolean([VR_CLK_OUT])[0] # 必須
vctrl = fmu_spice.get_real([VR_VCTRL])[0]
fmu_sv.set_boolean([VR_CLK_OUT], [clk_out])
fmu_sv.set_real([VR_VCTRL], [vctrl]) # デバッグ/モニタリング用(省略可)
# ステップ9: 時刻をイベント時刻に更新
current_time = last_time
# デバッグ用ログ出力(オプション)
if current_time % 1e-6 < H: # 1μsごと
lock = fmu_sv.get_boolean([VR_LOCK])[0]
print(f"t={current_time:.6e}s: vctrl={vctrl:.3f}V, lock={lock}")
重要なポイント:
-
状態保存は両FMU: SV/SPICE 両方を
t_nに巻き戻せるように準備 - ロールバック後は SV→SPICE の順序を維持: PLL の因果性を保つ
- 調整ステップは early_return_allowed=False: 同じイベントを二重検出しないため
-
イベント時刻から次ステップ開始:
current_time = last_time
SPICE backend のゼロクロス検出実装:
# SPICE backend 内部(DoStep 実装)
def do_step(self, current_time, step_size, early_return_allowed=True):
# タイムステッピング
t_end = current_time + step_size
t = current_time
while t < t_end:
# SPICE 内部タイムステップ(TR-BDF2 など)
t_next = min(t + dt_internal, t_end)
# V(VCO_OUT) の閾値チェック(ヒステリシス付き)
vco_prev = self.get_node_voltage("VCO_OUT")
self.solve_timestep(t, t_next)
vco_next = self.get_node_voltage("VCO_OUT")
# ゼロクロス検出
if early_return_allowed:
if vco_prev <= 0.45 and vco_next > 0.55: # 立ち上がり
# 補間でイベント時刻を計算
t_event = self.interpolate_zero_crossing(t, t_next, vco_prev, vco_next, 0.5)
return Fmi3Status.OK, True, False, True, t_event
elif vco_prev >= 0.55 and vco_next < 0.45: # 立ち下がり
t_event = self.interpolate_zero_crossing(t, t_next, vco_prev, vco_next, 0.5)
return Fmi3Status.OK, True, False, True, t_event
t = t_next
# イベントなし
return Fmi3Status.OK, False, False, False, t_end
この全体の流れは、MATLAB/Simulink の zero-crossing detection(ゼロクロス検出) とほぼ同じ考え方です。
- Simulink のソルバ内部でやっていることを
- SPICE backend の
DoStep + early_return - FMI マスターの
GetState/SetState
にバラして実装しているイメージです。
- SPICE backend の
初期化シーケンス
PLL は初期状態(ロック前)の扱いが重要です。適切な初期化シーケンスを定義しておきます:
def initialize_pll_cosimulation():
"""PLL 混載シミュレーションの初期化"""
# 1. SPICE FMU の初期化(DC operating point)
fmu_spice.enter_initialization_mode()
# 初期制御電圧を設定(VCO の中心周波数付近)
fmu_spice.set_real([VR_VCTRL], [0.9]) # 例: 0.9V
# Charge Pump 電流は初期値ゼロ
fmu_spice.set_real([VR_CP_UP, VR_CP_DOWN], [0.0, 0.0])
fmu_spice.exit_initialization_mode()
# 2. 初期 VCO 出力を取得
initial_vco_out = fmu_spice.get_real([VR_VCO_OUT])[0]
initial_clk_out = fmu_spice.get_boolean([VR_CLK_OUT])[0]
initial_vctrl = fmu_spice.get_real([VR_VCTRL])[0]
print(f"Initial SPICE state: vctrl={initial_vctrl:.3f}V, "
f"vco_out={initial_vco_out:.3f}V, clk_out={initial_clk_out}")
# 3. SV FMU の初期化
fmu_sv.enter_initialization_mode()
# 参照クロック(外部から供給)
fmu_sv.set_boolean([VR_CLK_REF], [True])
# VCO からのフィードバック信号
fmu_sv.set_boolean([VR_CLK_OUT], [initial_clk_out])
fmu_sv.set_real([VR_VCTRL], [initial_vctrl])
# 分周比設定
fmu_sv.set_int32([VR_DIV_RATIO], [8]) # 例: N=8
fmu_sv.exit_initialization_mode()
print("PLL co-simulation initialized successfully")
初期化の注意点:
- SPICE を先に初期化し、DC operating point を計算
- その結果を SV に渡して整合性を保つ
- ロック前は
lock=falseの状態から開始
デバッグとモニタリング
混載シミュレーションでは、境界での信号の不整合がバグの温床になります。デバッグ用のログ機構を組み込んでおくと便利です。
import csv
class FmiCosimLogger:
"""FMI Co-Simulation のデバッグ用ログ"""
def __init__(self, log_file="pll_cosim.csv"):
self.log_file = log_file
self.writer = None
self.file = None
def __enter__(self):
self.file = open(self.log_file, 'w', newline='')
self.writer = csv.writer(self.file)
# ヘッダー
self.writer.writerow([
'time', 'fmu', 'vctrl', 'vco_out', 'cp_up', 'cp_down',
'clk_ref', 'clk_out', 'lock', 'div_ratio', 'event'
])
return self
def __exit__(self, *args):
if self.file:
self.file.close()
def log_step(self, time, fmu_name, variables, event=False):
"""各ステップの変数値を記録"""
self.writer.writerow([
f"{time:.12e}",
fmu_name,
f"{variables.get('vctrl', 0):.6f}",
f"{variables.get('vco_out', 0):.6f}",
f"{variables.get('cp_up', 0):.9e}",
f"{variables.get('cp_down', 0):.9e}",
int(variables.get('clk_ref', False)),
int(variables.get('clk_out', False)),
int(variables.get('lock', False)),
variables.get('div_ratio', 0),
'EVENT' if event else ''
])
# 使用例
with FmiCosimLogger("pll_debug.csv") as logger:
while current_time < stop_time:
# ... シミュレーションループ ...
# SV 側のログ
logger.log_step(current_time, "SV", {
'cp_up': cp_up,
'cp_down': cp_down,
'clk_out': clk_out_sv,
'lock': lock
})
# SPICE 側のログ
logger.log_step(current_time, "SPICE", {
'vctrl': vctrl,
'vco_out': vco_out,
'clk_out': clk_out_sp
}, event=early_ret)
ログの活用方法:
- CSV をプロット(matplotlib, Excel等)して波形確認
- SV/SPICE 両側の
clk_outが一致しているかチェック - イベント時刻付近の動作を詳細に追跡
- VCD 形式への変換も可能
ここまで決めて見えてきた「良いところ」と「課題」
良いところ
- プロセス分離による並列実行: SV と SPICE が独立したプロセスで同時に計算可能
- gRPC による型安全な通信: Protobuf の型定義により、実行時エラーを削減
- ロジック⇔アナログの境界信号が 表として明示 されるので、SV/SPICE/マスターの間で認識ズレが起きにくい。
- Charge Pump 信号を明示的に追加 したことで、PLL ループが正しく閉じる。
- 方向(direction)を config に記載 することで、因果性の問題を防げる。
- FMI 変数 (valueReference) と backend の
config_fileをセットで設計しておくことで、- 「どの FMI vr がどの信号を指しているか」
- 「どの JSON を直せばマッピングが変わるか」
が一望できる。
- ヒステリシス付きゼロクロス検出 により、ノイズによる誤検出を防げる。
- VCO 位相を state_variables に含める ことで、ロールバック後も位相が保存される。
- ゼロクロス検出+ロールバックの手順を仕様として書いておくと、
- 実装時に「どこで state を取るか」「どこまで巻き戻すか」で迷いにくい
- 将来、別のイベント(ロック状態変化など)を足すときにも拡張しやすい
- デバッグ用ログ機構 により、境界での信号不整合を早期発見できる。
- 多言語での実装が容易: Python(マスター)、C++(backend)を自然に組み合わせられる
課題・これから考えるところ
1. 数値安定性とステップサイズ
問題: コミュニケーションステップ $H$ の選択が難しい
- VCO 周波数が高い(例: 1GHz)→ 周期 1ns → $H$ は 100ps 程度必要?
- でも PLL 帯域(例: 1MHz)→ $H = 10ns$ でも十分?
- SPICE の内部タイムステップはさらに細かい可能性
検討事項:
- Multi-rate co-simulation(SPICE は細かく、SV は粗く)
- または VCO 出力を「位相」として扱う方式(高周波でもステップサイズを大きくできる)
2. VCO モデルの実装
問題: 実際の VCO/ループフィルタの SPICE モデルをどこまでリアルにするか
- シンプルな線形モデル($f = f_0 + K_{VCO} \cdot V_{CTRL}$)で十分?
- BSIM トランジスタモデル + TR-BDF2 まで持ち込む?
- VCO の位相ノイズまで考慮する?
検討事項:
- 最初は線形 VCO で動作確認
- 段階的に複雑なモデルに移行
3. GetState/SetState の実装範囲
問題: 2 つの FMU(SV / SPICE)で保存・復元する状態の範囲
- SV 側: レジスタ値、時刻、クロック位相
- SPICE 側: 全ノード電圧、キャパシタ電圧、VCO 内部位相(重要!)
検討事項:
- 最初は「時刻+主要レジスタ/ノード電圧」だけでも十分か
- VCO 位相の保存方法(pickle? 独自シリアライゼーション?)
4. 複数イベントの扱い
問題: 1 ステップ内に複数のゼロクロス(例: 立ち上がり→立ち下がり)が発生した場合
- 最初のイベントで early return
- イベント処理後、残り時間で再度 DoStep → 次のイベント検出
- イベントの連鎖をどこまで追いかけるか
検討事項:
- イベントループの実装(while early_return のネスト)
- 無限ループ防止(最大イベント数制限)
5. gRPC 通信のオーバーヘッド
問題: ローカルマシンでも RPC には数十〜数百 μs のオーバーヘッド
- タイムクリティカルなシミュレーションでは無視できない可能性
検討事項:
- 共有メモリとのハイブリッド方式(信号は共有メモリ、制御は gRPC)
- バッチ化(複数ステップをまとめて送信)
- Unix domain socket の使用(TCP より高速)
Backend 実装の方針
ここまで定義した仕様を実現するため、各シミュレータに gRPC backend サーバを実装します。
SystemVerilog Backend (sukimasim)
実装場所:
sukimasim/
└── fmi3_backend/
├── fmi3_remote_sv_server.cpp # gRPC サーバ本体
├── sv_instance_manager.cpp # instance_id → SV コンテキスト
└── sv_signal_binding.cpp # FMI 変数 ↔ SV 信号バインディング
主要な責務:
-
Fmi3RemotegRPC サービスの実装(fmi3_remote.protoに準拠) - 複数の FMU インスタンス(
instance_id)の管理 -
config_fileに基づく FMI 変数 ↔ SV 信号のマッピング - ステート管理(GetState/SetState): 時刻、レジスタ値、内部状態のシリアライズ
ビルド成果物:
-
build/fmi3_remote_sv_server(独立した gRPC サーバプロセス) - Python マスターから
localhost:50051等で接続
SPICE Backend (sukimaspice)
実装場所:
sukimaspice/
└── fmi3_backend/
├── fmi3_remote_spice_server.cpp # gRPC サーバ本体
├── spice_instance_manager.cpp # instance_id → SPICE 解析コンテキスト
└── spice_signal_binding.cpp # FMI 変数 ↔ ノード/式のバインディング
主要な責務:
-
Fmi3RemotegRPC サービスの実装 - トランジェント解析の時間ステップ管理
-
config_fileに基づく FMI 変数 ↔ SPICE ノード/式のマッピング - ゼロクロス検出(
V(VCO_OUT)のヒステリシス付き閾値)と early return - ステート管理: トランジェント解析の内部状態(ノード電圧、VCO 位相)のシリアライズ
ビルド成果物:
-
build/fmi3_remote_spice_server(独立した gRPC サーバプロセス)
通信プロトコル(fmi3_remote.proto)
sukimafmimaster/fmi3_remote.proto で定義される Fmi3Remote gRPC サービス:
syntax = "proto3";
package fmi3_remote;
// FMI 3.0 Status codes
enum Fmi3Status {
OK = 0;
WARNING = 1;
DISCARD = 2;
ERROR = 3;
FATAL = 4;
PENDING = 5; // 非同期FMU用(将来の拡張)
}
// ============================================================================
// Lifecycle RPCs
// ============================================================================
message InstantiateCoSimulationRequest {
string instance_name = 1;
string instantiation_token = 2;
string resource_path = 3;
bool visible = 4;
bool logging_on = 5;
bool event_mode_used = 6;
bool early_return_allowed = 7;
repeated uint32 required_intermediate_variables = 8;
}
message InstantiateCoSimulationResponse {
Fmi3Status status = 1;
string instance_id = 2; // サーバ側で生成した一意なID
}
message SetupExperimentRequest {
string instance_id = 1;
double tolerance = 2;
double start_time = 3;
double stop_time = 4;
}
message SetupExperimentResponse {
Fmi3Status status = 1;
}
message EnterInitializationModeRequest {
string instance_id = 1;
}
message EnterInitializationModeResponse {
Fmi3Status status = 1;
}
message ExitInitializationModeRequest {
string instance_id = 1;
}
message ExitInitializationModeResponse {
Fmi3Status status = 1;
}
message TerminateRequest {
string instance_id = 1;
}
message TerminateResponse {
Fmi3Status status = 1;
}
message ResetRequest {
string instance_id = 1;
}
message ResetResponse {
Fmi3Status status = 1;
}
message FreeInstanceRequest {
string instance_id = 1;
}
message FreeInstanceResponse {
Fmi3Status status = 1;
}
// ============================================================================
// Stepping RPCs
// ============================================================================
message DoStepRequest {
string instance_id = 1;
double current_time = 2;
double step_size = 3;
bool no_set_fmu_state_prior_to_current_point = 4;
bool early_return_allowed = 5;
}
message DoStepResponse {
Fmi3Status status = 1;
bool event_handling_needed = 2;
bool terminate_simulation = 3;
bool early_return = 4;
double last_successful_time = 5;
}
// 注: FMI 3.0 C API では canReturnEarly は InstantiateCoSimulation のパラメータ
// (インスタンス生成時に固定) だが、本 RPC では DoStepRequest の都度指定可能にし、
// より柔軟な制御を実現している。
// ============================================================================
// Variable Access RPCs
// ============================================================================
message SetRealRequest {
string instance_id = 1;
repeated uint32 value_references = 2;
repeated double values = 3;
}
message SetRealResponse {
Fmi3Status status = 1;
}
message GetRealRequest {
string instance_id = 1;
repeated uint32 value_references = 2;
}
message GetRealResponse {
Fmi3Status status = 1;
repeated double values = 2;
}
message SetInt32Request {
string instance_id = 1;
repeated uint32 value_references = 2;
repeated int32 values = 3;
}
message SetInt32Response {
Fmi3Status status = 1;
}
message GetInt32Request {
string instance_id = 1;
repeated uint32 value_references = 2;
}
message GetInt32Response {
Fmi3Status status = 1;
repeated int32 values = 2;
}
message SetBooleanRequest {
string instance_id = 1;
repeated uint32 value_references = 2;
repeated bool values = 3;
}
message SetBooleanResponse {
Fmi3Status status = 1;
}
message GetBooleanRequest {
string instance_id = 1;
repeated uint32 value_references = 2;
}
message GetBooleanResponse {
Fmi3Status status = 1;
repeated bool values = 2;
}
// ============================================================================
// State Management RPCs
// ============================================================================
message GetStateRequest {
string instance_id = 1;
}
message GetStateResponse {
Fmi3Status status = 1;
bytes state = 2; // シリアライズされた状態(形式は backend 依存)
}
message SetStateRequest {
string instance_id = 1;
bytes state = 2;
}
message SetStateResponse {
Fmi3Status status = 1;
}
// ============================================================================
// Service Definition
// ============================================================================
service Fmi3Remote {
// Lifecycle
rpc InstantiateCoSimulation (InstantiateCoSimulationRequest)
returns (InstantiateCoSimulationResponse);
rpc SetupExperiment (SetupExperimentRequest)
returns (SetupExperimentResponse);
rpc EnterInitializationMode (EnterInitializationModeRequest)
returns (EnterInitializationModeResponse);
rpc ExitInitializationMode (ExitInitializationModeRequest)
returns (ExitInitializationModeResponse);
rpc Terminate (TerminateRequest)
returns (TerminateResponse);
rpc Reset (ResetRequest)
returns (ResetResponse);
rpc FreeInstance (FreeInstanceRequest)
returns (FreeInstanceResponse);
// Stepping
rpc DoStep (DoStepRequest)
returns (DoStepResponse);
// Variables
rpc SetReal (SetRealRequest)
returns (SetRealResponse);
rpc GetReal (GetRealRequest)
returns (GetRealResponse);
rpc SetInt32 (SetInt32Request)
returns (SetInt32Response);
rpc GetInt32 (GetInt32Request)
returns (GetInt32Response);
rpc SetBoolean (SetBooleanRequest)
returns (SetBooleanResponse);
rpc GetBoolean (GetBooleanRequest)
returns (GetBooleanResponse);
// State management
rpc GetState (GetStateRequest)
returns (GetStateResponse);
rpc SetState (SetStateRequest)
returns (SetStateResponse);
}
ポイント:
- FMI 3.0 C API の関数が、そのまま gRPC メソッドにマッピングされている
- すべての RPC は
instance_idを含む(複数インスタンス対応) -
DoStepResponseには early return / event 情報が含まれる -
GetState/SetStateはバイト列(bytes)で状態を交換(注: FMI 仕様上は内部ポインタでも可だが、本実装ではシリアライズされたバイト列を交換するガイドラインとしている) - Protobuf の
repeatedを使って配列を効率的に転送
今後の展開
この記事はあくまで「構想と仕様」の段階で、実際のコードはこれからです。
今後のステップとしては:
-
gRPC backend の枠組み実装:
-
sukimasim/fmi3_backend/を追加し、fmi3_remote_sv_serverの枠を実装 -
sukimaspice/fmi3_backend/を追加し、fmi3_remote_spice_serverの枠を実装 - 最初は「Mock backend と同じ単純ダイナミクス」で動作確認
-
-
FMI Master からの通しテスト:
-
FmuInstanceから gRPC backend へ接続 - 基本的なライフサイクル(Instantiate → Setup → DoStep → Terminate)の動作確認
- GetState/SetState によるロールバック機能の検証
-
-
PLL ロジック/アナログ回路の実装:
- SV 側: PFD、分周器、Charge Pump 電流生成の実装
- SPICE 側: ループフィルタ、VCO モデルの実装
-
config_fileによる信号バインディングの検証
-
協調ロールバックの検証:
- ゼロクロス検出 + early return の動作確認
- 両 FMU の状態保存・復元が正しく動くことを確認
- VCO 位相の保存・復元の実装
-
性能測定と最適化:
- gRPC オーバーヘッドの実測
- 並列実行による高速化の検証
- 必要に応じて通信方式の最適化
という順番で進める予定です。
まとめ
今回は、
- 商用AMSシミュレータにおける独自接続方式の課題と、FMI 3.0 によるオープン標準アプローチ
- プロセス完全分離による並列実行と、gRPC + Protobuf を選択した理由
- 自作 SystemVerilog シミュレータと SPICE シミュレータを FMI 3.0 Co-Sim でつなぐ
- その第一歩として「PLL 混載シミュレーション」の I/F と時間同期をどう設計するか
- Simulink の zero-crossing detection 相当を FMI+gRPC でどう表現するか
を、仕様レベルで整理してみました。
主要な設計ポイント
- オープン標準の採用: FMI 3.0 により、ベンダー独自方式に依存しない
- プロセス完全分離: gRPC により SV と SPICE を独立プロセスで並列実行
- 型安全な通信: Protobuf による型定義で実行時エラーを削減
- Charge Pump 信号の明示化: PLL ループを閉じるために必須(SV → SPICE 方向)
- 時間同期の順序: SV → SPICE → SV の順序でループを維持
- ヒステリシス付きゼロクロス検出: ノイズによる誤検出を防止(0.45V / 0.55V)
- VCO 位相の状態管理: ロールバック時に位相情報を保存・復元
- 協調ロールバック: 両 FMU を巻き戻してイベント時刻まで再実行
- デバッグ用ログ: 境界信号の不整合を早期発見
技術的な挑戦
- 数値安定性: VCO 周波数 vs PLL 帯域のステップサイズ最適化
- Multi-rate co-simulation: SPICE(細)と SV(粗)のレート変換
- VCO モデル: 線形モデルから段階的にリアルなモデルへ
- 複数イベント: 1 ステップ内の連続イベント処理
- gRPC オーバーヘッド: 通信コストと計算コストのバランス
既存ツールとの将来的な接続可能性
FMI 3.0 + gRPC という標準規格・標準技術を使うことで、将来的には:
- オープンソース SPICE: ngspice、Xyce との接続
- オープンソース Verilog: Verilator、Icarus Verilog との接続
- 商用ツールとの連携: 標準I/Fを持つツールとの相互運用
- 分散実行: 複数マシンでの並列シミュレーション
も視野に入れることができます。
業界的にはまだ「これが標準」という混載フローは商用ツールの独自方式以外にあまり無いと思いますが、FMI 3.0 + gRPC をうまく使うと、
「デジタルもアナログも自作シミュレータ。でも結合はオープン標準で仕様化する」
という構成がかなり現実的に狙えると感じています。
特に、境界信号を表で明示し、JSON config で binding を定義するアプローチは、SV/SPICE/FMI マスターの 3 者の連携を大幅に楽にしてくれそうです。
Day 20 以降は、この仕様をもとに実際の backend 実装に着手していく予定です。同じように「シミュレータをつないでみたい」「既存ツールを FMI でつなぎたい」という方の参考になれば嬉しいです。
参考文献
- FMI Standard 3.0 Specification
- The Functional Mock-up Interface 3.0 - New Features
- MATLAB/Simulink - Zero-Crossing Detection
- FMIGo: A runtime environment for FMI based simulation
- Verilog-AMS Language Reference Manual - Accellera Standard
- gRPC Official Documentation
- Protocol Buffers Documentation

