0
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?

環境構築編からの続きです


実際の例として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

PyBERTの起動画面:
pybert.png

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

pattern.png

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/ に保存します

fix_gerber2ems_pos_csv.py
#!/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 は基板メーカーの値を使います

stackup.json
{
  "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/ に保存します

touchstone_convert.py
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のResultsEyes タブで結果を確認します

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

0
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
0
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?