環境構築編からの続きです
実際の例としてKiCadで設計した2Gbps 差動クロック配線(特性インピーダンス100Ω)について解析をしてみます
解析の手順の概要は以下です
KiCad設計
↓
Gerber / Drill / Position CSV出力
↓
gerber2ems + openEMSで電磁界解析
↓
Sパラメータ取得
↓
Touchstone .s4p 変換
↓
scikit-rfで確認
↓
PyBERTでアイ解析
2. 仮想環境の全体方針
今回の作業では、PyBERT と gerber2ems/openEMS で必要な NumPy/SciPy/CSXCAD 周辺の依存関係が異なるためopenEMS / gerber2ems 用環境 と PyBERT 用環境 を分けます
使用する仮想環境
今回の環境では以下の2つを使います
openEMS / gerber2ems 用:
/home/user/opt/openEMS/venv
PyBERT 用:
/home/user/.venv/pybert
PYTHONPATHを混ぜないために、仮想環境を切り替える前に、基本的に以下を実行します
unset PYTHONPATH
hash -r
3. openEMS / gerber2ems 用仮想環境
仮想環境を有効化
gerber2ems を実行するときは、openEMS 側の venv を使います
unset PYTHONPATH
hash -r
source /home/user/opt/openEMS/venv/bin/activate
パスが通っていることを確認:
which python
which gerber2ems
期待値:
/home/user/opt/openEMS/venv/bin/python
/home/user/opt/openEMS/venv/bin/gerber2ems
CSXCADのインストールを確認:
python -c "import CSXCAD; print(CSXCAD)"
CSXCAD が import できればOK
<module 'CSXCAD' from '/home/user/opt/openEMS/venv/lib/python3.12/site-packages/CSXCAD/__init__.py'>
openEMS 側の venv に gerber2ems が無い場合
gerber2ems: command not found の場合は、openEMS venv に gerber2ems を入れます
cd ~/work
git clone https://github.com/antmicro/gerber2ems.git
cd gerber2ems
python -m pip install .
確認:
which gerber2ems
gerber2ems --help
依存関係確認
python -m pip check
不足が出る場合は、表示されたパッケージを追加します
例:
python -m pip install numpy pandas matplotlib scikit-rf
4. PyBERT 用仮想環境
PyBERT環境を有効化
PyBERT を使うときは、openEMS venv ではなく PyBERT 用環境を使います
deactivate 2>/dev/null
unset PYTHONPATH
hash -r
source ~/.venv/pybert/bin/activate
確認:
which python
which pybert
python -m pip --version
期待例:
/home/user/.venv/pybert/bin/python
/home/user/.venv/pybert/bin/pybert
pip ... from /home/user/.venv/pybert/...
PyBERT用NumPy/SciPyのバージョン固定
PyBERTでは NumPy 2.x 系で問題が起きたので、バージョンを固定します
python -m pip install --force-reinstall \
"numpy==1.26.4" \
"scipy==1.14.0" \
"kiwisolver==1.4.9"
バージョンを確認:
python -c "import numpy, scipy, kiwisolver; print(numpy.__version__, scipy.__version__, kiwisolver.__version__)"
期待値:
1.26.4 1.14.0 1.4.9
PyBERT起動
pybert
5. 作業ディレクトリ構成
今回の作業ディレクトリと作成するファイルの例は以下になります
~/work/test/
├── fab/
│ ├── myproject-F_Cu.gbr
│ ├── myproject-In1_Cu.gbr
│ ├── myproject-In2_Cu.gbr
│ ├── myproject-B_Cu.gbr
│ ├── myproject-Edge_Cuts.gbr
│ ├── myproject-PTH.drl
│ ├── myproject-top-pos.csv
│ └── stackup.json
├── simulation.json
├── fix_gerber2ems_pos_csv.py
├── touchstone_convert.py
├── clk.s4p
└── ems/
6. KiCad側のポート設定
伝送路解析のためには、ガーバーデータの各伝送ラインのシミュレーション用の Simulation_Port(SP1〜SP4) を配置する必要があります
KiCad標準には Simulation_Port という部品は無いので、任意の1ピン部品(ここではテストポイント)を使います
例:
TestPoint:TestPoint_Pad_D1.0mm
回路図及び基板パターンにテストポイントを配置し、各フットプリントのプロパティを以下にします
Reference: SP1, SP2, SP3, SP4
Value: Simulation_Port
信号ラインとポート対応
各Simulation_Portは以下のように接続します
SP1 = CKP 始端
SP2 = CKP 終端
SP3 = CKN 始端
SP4 = CKN 終端
gerber2ems 内部では以下になります
Port_0 = SP1
Port_1 = SP2
Port_2 = SP3
Port_3 = SP4
したがって後述する simulation.json では 0始まりで扱います
ports[0] = SP1
ports[1] = SP2
ports[2] = SP3
ports[3] = SP4
今回は、以下のパターンの伝送路特性の解析をしてみます
主要パラメータは以下です
パターン幅:0.127mm, 差動間隔:0.2mm, 誘電体厚:0.1mm, 導体厚:0.035mm
7. KiCadから出力するファイル
4層基板の場合、KiCadから以下を出力します
ガーバーファイル
F_Cu.gbr
In1_Cu.gbr
In2_Cu.gbr
B_Cu.gbr
Edge_Cuts.gbr
ドリルファイル
PTH.drl
マップファイル
top-pos.csv
出力した各ファイルを以下の名前に変更し ~/work/test/fab/ に格納します
fab/myproject-B_Cu.gbr
fab/myproject-Edge_Cuts.gbr
fab/myproject-F_Cu.gbr
fab/myproject-In1_Cu.gbr
fab/myproject-In2_Cu.gbr
fab/myproject-PTH.drl
fab/myproject-top-pos.csv
8. pos.csv の整形
KiCad出力の *-pos.csv は以下のように引用符付きになります
"SP1","Simulation_Port","TestPoint_Pad_D1.0mm",19.431000,52.324000,0.000000,top
gerber2ems では引用符付きの形式を正しく認識しないので、gerber2ems向けには以下のように整形する必要があります
SP1,Simulation_Port,Simulation_Port,19.431000,52.324000,0.000000,top
そのために、以下の整形スクリプトを作成し、fix_gerber2ems_pos_csv.pyという名前で ~/work/test/ に保存します
#!/usr/bin/env python3
"""
Typical usage:
./fix_gerber2ems_pos_csv.py fab/myproject-top-pos.csv
If your CSV currently contains SP0..SP3 and you want SP1..SP4:
./fix_gerber2ems_pos_csv.py fab/myproject-top-pos.csv --shift-sp-to-one
"""
import argparse
import csv
from pathlib import Path
REQUIRED_COLS = ["Ref", "Val", "Package", "PosX", "PosY", "Rot", "Side"]
def clean(value: object) -> str:
"""Return a stripped string without surrounding double quotes."""
return str(value or "").strip().strip('"')
def shift_sp_ref_to_one(ref: str) -> str:
"""Convert SP0->SP1, SP1->SP2, ... . Leave non-SP refs unchanged."""
if not ref.startswith("SP"):
return ref
num = ref[2:]
if not num.isdigit():
return ref
return f"SP{int(num) + 1}"
def is_sp_ref(ref: str) -> bool:
"""Return True for SP references with numeric suffix, e.g. SP1, SP2."""
return ref.startswith("SP") and ref[2:].isdigit()
def main() -> None:
parser = argparse.ArgumentParser(
description="Normalize KiCad *-pos.csv for gerber2ems Simulation_Port detection."
)
parser.add_argument(
"csv_file",
nargs="?",
default="fab/myproject-top-pos.csv",
help="Path to KiCad position CSV. Default: fab/myproject-top-pos.csv",
)
parser.add_argument(
"--shift-sp-to-one",
action="store_true",
help=(
"Renumber SP0->SP1, SP1->SP2, SP2->SP3, ... . "
"Use this if the CSV currently uses SP0-based names."
),
)
parser.add_argument(
"--no-backup",
action="store_true",
help="Do not create a .bak backup file.",
)
args = parser.parse_args()
p = Path(args.csv_file)
if not p.exists():
raise SystemExit(f"ERROR: file not found: {p}")
with p.open(newline="", encoding="utf-8-sig") as f:
reader = csv.DictReader(f)
if reader.fieldnames is None:
raise SystemExit("ERROR: CSV header not found")
reader.fieldnames = [clean(name) for name in reader.fieldnames]
missing = [c for c in REQUIRED_COLS if c not in reader.fieldnames]
if missing:
raise SystemExit(f"ERROR: missing columns: {', '.join(missing)}")
rows = list(reader)
if not args.no_backup:
backup = p.with_suffix(p.suffix + ".bak")
backup.write_bytes(p.read_bytes())
print(f"Backup written: {backup}")
sp_count = 0
sp_refs = []
with p.open("w", newline="", encoding="utf-8") as f:
f.write(",".join(REQUIRED_COLS) + "\n")
for r in rows:
ref = clean(r.get("Ref"))
if args.shift_sp_to_one:
ref = shift_sp_ref_to_one(ref)
val = clean(r.get("Val"))
package = clean(r.get("Package"))
if is_sp_ref(ref):
val = "Simulation_Port"
package = "Simulation_Port"
sp_count += 1
sp_refs.append(ref)
out = {
"Ref": ref,
"Val": val,
"Package": package,
"PosX": clean(r.get("PosX")),
"PosY": clean(r.get("PosY")),
"Rot": clean(r.get("Rot")),
"Side": clean(r.get("Side")),
}
f.write(",".join(out[c] for c in REQUIRED_COLS) + "\n")
print(f"Normalized: {p}")
print(f"Simulation ports updated: {sp_count}")
if sp_refs:
print("Simulation port refs: " + ", ".join(sp_refs))
if any(ref == "SP0" for ref in sp_refs):
print(
"WARNING: SP0 remains in the CSV. "
"gerber2ems normally expects SP1-based refs in the position file."
)
print(" If needed, rerun with --shift-sp-to-one.")
if __name__ == "__main__":
main()
マップファイルを指定して実行します
python fix_gerber2ems_pos_csv.py fab/myproject-top-pos.csv
実行結果の確認:
grep -n "SP" fab/myproject-top-pos.csv
期待値:
SP1,Simulation_Port,Simulation_Port,19.431000,52.324000,0.000000,top
SP2,Simulation_Port,Simulation_Port,37.465000,7.112000,0.000000,top
SP3,Simulation_Port,Simulation_Port,16.891000,51.181000,0.000000,top
SP4,Simulation_Port,Simulation_Port,39.497000,7.112000,0.000000,top
9. stackup.json の作成
基板のスタックアップ構成を記述する stackup.json を作成し、~/work/test/fab/ に保存します(以下は4層基板の例)
thickness, epsilon, lossTangent は基板メーカーの値を使います
{
"layers": [
{
"name": "F_Cu",
"type": "copper",
"color": null,
"thickness": 0.035,
"material": null,
"epsilon": null,
"lossTangent": null
},
{
"name": "dielectric 1",
"type": "prepreg",
"color": null,
"thickness": 0.1,
"material": "FR4",
"epsilon": 4.5,
"lossTangent": 0.02
},
{
"name": "In1_Cu",
"type": "copper",
"color": null,
"thickness": 0.035,
"material": null,
"epsilon": null,
"lossTangent": null
},
{
"name": "dielectric 2",
"type": "core",
"color": null,
"thickness": 1.265,
"material": "FR4",
"epsilon": 4.5,
"lossTangent": 0.02
},
{
"name": "In2_Cu",
"type": "copper",
"color": null,
"thickness": 0.035,
"material": null,
"epsilon": null,
"lossTangent": null
},
{
"name": "dielectric 3",
"type": "prepreg",
"color": null,
"thickness": 0.1,
"material": "FR4",
"epsilon": 4.5,
"lossTangent": 0.02
},
{
"name": "B_Cu",
"type": "copper",
"color": null,
"thickness": 0.035,
"material": null,
"epsilon": null,
"lossTangent": null
}
],
"format_version": "1.0"
}
10. simulation.json の作成
gerber2emsのシミュレーションパラメーターの設定ファイル simulation.json を作成し ~/work/test/ に保存します
{
"format_version": "1.1",
"frequency": {
"start": 100000000.0,
"stop": 6000000000.0
},
"max_steps": 80000,
"via": {
"filling_epsilon": 1,
"plating_thickness": 50
},
"mesh": {
"xy": 50,
"inter_layers": 4,
"margin": {
"xy": 200,
"z": 200
}
},
"margin": {
"xy": 1000,
"z": 1000
},
"ports": [
{
"width": 600,
"length": 500,
"impedance": 50,
"layer": 0,
"plane": 1,
"excite": true
},
{
"width": 600,
"length": 500,
"impedance": 50,
"layer": 0,
"plane": 1,
"excite": true
},
{
"width": 600,
"length": 500,
"impedance": 50,
"layer": 0,
"plane": 1,
"excite": true
},
{
"width": 600,
"length": 500,
"impedance": 50,
"layer": 0,
"plane": 1,
"excite": true
}
],
"traces": [
{
"start": 0,
"stop": 1,
"name": "CKP"
},
{
"start": 2,
"stop": 3,
"name": "CKN"
}
],
"differential_pairs": [
{
"start_p": 0,
"stop_p": 1,
"start_n": 2,
"stop_n": 3,
"name": "CLK"
}
]
}
差動パターン解析用にSパラメータを4ポートすべて取得するので、全ポートを励振対象にしています
"excite": true
これにより4ポート解析に必要なSパラメータが4個生成されます
ems/simulation/Sx0.csv
ems/simulation/Sx1.csv
ems/simulation/Sx2.csv
ems/simulation/Sx3.csv
11. gerber2ems / openEMS 実行
仮想環境を有効化
unset PYTHONPATH
hash -r
source /home/user/opt/openEMS/venv/bin/activate
確認:
which python
which gerber2ems
python -c "import CSXCAD; print(CSXCAD)"
実行
以下のコマンドで実行します (完了まで数時間かかります)
cd ~/work/test/
rm -rf ems
gerber2ems -a
実行開始のログの例:
Parsing config
Loading config from /home/tomorrow56/work/test/simulation.json
[16:21:28][INFO] Creating geometry
[16:21:28][INFO] Processing gerber files (may take a while for larger boards)
[16:22:48][INFO] Adding copper from gerber files
正常終了ログの例:
Rendering S-parameter plots
Rendering impedance plots
Rendering smith charts
Rendering differential pair S-parameter plots
Rendering differential pair impedance plots
Rendering trace delay plots
12. 結果確認
find ems/results -maxdepth 1 -type f | sort
find ems/simulation -maxdepth 1 -type f | sort
期待結果:以下のファイルが生成される
ems/results/diff_delay.png
ems/results/CKN_delay.png
ems/results/CKP_delay.png
ems/results/Port_0_data.csv
ems/results/Port_1_data.csv
ems/results/Port_2_data.csv
ems/results/Port_3_data.csv
ems/results/SDD_Diff.png
ems/results/Z_diff.png
ems/simulation/Sx0.csv
ems/simulation/Sx1.csv
ems/simulation/Sx2.csv
ems/simulation/Sx3.csv
13. ポート対応確認
シミュレーション結果のポート対応を以下のコマンドで確認する
python - <<'PY'
import re
from pathlib import Path
xml = Path("ems/geometry/geometry.xml").read_text()
for n in range(4):
m = re.search(
rf'<Metal ID="[^"]+" Name="Port_{n}">(.*?)</Metal>',
xml,
flags=re.S
)
if not m:
print(f"Port_{n}: not found")
continue
block = m.group(1)
pts = re.findall(
r'<P[12] X="([0-9.eE+-]+)" Y="([0-9.eE+-]+)" Z="([0-9.eE+-]+)"',
block
)
xs = [float(p[0]) / 10000 for p in pts]
ys = [float(p[1]) / 10000 for p in pts]
print(f"Port_{n}: center X={(min(xs)+max(xs))/2:.6f} mm, Y={(min(ys)+max(ys))/2:.6f} mm")
PY
期待値:
Port_0 = SP1 : CKP 始端
Port_1 = SP2 : CKP 終端
Port_2 = SP3 : CKN 始端
Port_3 = SP4 : CKN 終端
14. Touchstone .s4p 変換
Touchstone変換は、pandas, numpy, scikit-rf が入っている環境(openEMS venv )で実行します
変換スクリプト
変換スクリプト touchstone_convert.py を作成し ~/work/test/ に保存します
import numpy as np
import pandas as pd
import skrf as rf
from pathlib import Path
def load_sx_csv(path):
"""
gerber2ems の SxN.csv を読み込む。
CSV構造:
Frequency [MHz],
re(S0-N), re(S1-N), re(S2-N), re(S3-N),
im(S0-N), im(S1-N), im(S2-N), im(S3-N)
"""
df = pd.read_csv(path)
freq = df.iloc[:, 0].values * 1e6
real = df.iloc[:, 1:5].values
imag = df.iloc[:, 5:9].values
s_complex = real + 1j * imag
return freq, s_complex
def csv_to_s4p(base_dir, output_path):
"""
gerber2ems の Sx0.csv〜Sx3.csv から Touchstone .s4p を生成する。
"""
paths = [Path(base_dir) / f"Sx{i}.csv" for i in range(4)]
data = []
freq = None
for p in paths:
f, s = load_sx_csv(p)
if freq is None:
freq = f
data.append(s)
s_matrix = np.stack(data, axis=2)
ntwk = rf.Network(
frequency=rf.Frequency.from_f(freq, unit="hz"),
s=s_matrix,
z0=50
)
ntwk.write_touchstone(output_path)
print(f"Touchstoneファイルを書き出しました: {output_path}.s4p")
if __name__ == "__main__":
csv_to_s4p(
base_dir="ems/simulation",
output_path="clk"
)
実行します
cd ~/work/test/
python touchstone_convert.py
確認:
ls -l clk.s4p
head -n 10 clk.s4p
15. PyBERT解析
PyBERT環境へ切り替え
deactivate 2>/dev/null
unset PYTHONPATH
hash -r
source ~/.venv/pybert/bin/activate
確認:
which python
which pybert
python -c "import numpy, scipy; print(numpy.__version__, scipy.__version__)"
期待値:
numpy 1.26.4
scipy 1.14.0
PyBERT起動
pybert
PyBERT設定
PyBERT UIで伝送路に作成したTouchstoneファイル(s4p)を設定します
Config > Channel > Interconnect > From File
設定:
File: clk.s4p
Use file: ON
解析条件
2Gbps想定:
Bit Rate: 2.0 Gbps
fMax: 6〜10 GHz
初期値が10Gbpsなどの場合は、対象仕様に合わせて変更してください
実行
PyBERT UIで以下を実行します
Simulation > Run Simulation
結果確認
PyBERT UIのResults → Eyes タブで結果を確認します
Channel:チャンネルの無補正のEyeパターン
- Tx De-emphasis & Noise:Tx De-emphasis適用時
- CTLE:CTLE適用時
- PyBERT Native DFE:DFE適用時
まとめ
無料ツールの組み合わせで、伝送路のEyeパターン解析(シミュレーション)ができました
今回の内容は簡易的なシミュレーションの結果で、クロストーク等は配慮されていません
あくまでも設計時の目安として、最終的には基板で評価するようにしてください
参考リンク
JLCPCB インピーダンス制御多層高精度プリント基板
https://jlcpcb.com/jp/impedance
JLCPCB Impedance Calculator
https://jlcpcb.com/pcb-impedance-calculator

