この記事ではエンジャーさんよりSiglent製 SVA1015XとPythonプログラムを用いて周波数特性を自動測定する方法について解説します。

 

周波数特性とは

高周波回路の設計や評価において、入力信号が周波数によってどのように変化するかを知ることは極めて重要です。この特性を周波数特性と呼び、回路が意図した通りに動作しているかを判断する指標となります。
通常これらの測定を手動で行う場合、スペクトラムアナライザのパネル操作、データの保存、PCでのグラフ化といった一連の作業を測定対象(DUT)ごとに繰り返す必要があります。しかしプログラムによる自動測定を導入すれば、ボタン一つで複数のDUTを同一条件で正確に測定し、即座に比較グラフまで作成できます。このように測定ミスを減らすだけでなく、解析にかかる時間を大幅に短縮できるのが自動測定の大きな利点です。

 

プログラムの概要

今回使用するプログラムは、ネットワーク経由でSVA1015Xを制御し、トラッキングジェネレータ(TG)機能を利用して周波数特性を取得するものです。

IPアドレス設定

プログラムを実行する前に、まずは計測器とPCをLANケーブルで接続します。そしてプログラム内の「INSTRUMENT_IP」という項目にSVA1015XのIPアドレスを入力します。
IPアドレスは System→ Interface→ LANから確認できます。

IPアドレスの確認

図 IPアドレスの確認

例えば、計測器のIPアドレスが「192.168.1.31」であれば、14行目を以下のように修正します。

INSTRUMENT_IP = '192.168.1.31'

この設定を行うことで、PCがネットワーク上の計測器を特定し、制御コマンドを送ることが可能になります。

測定条件設定

測定条件は18~32行目の config{}で囲った値で設定します。

周波数範囲

測定の範囲を定義するのは start_freq(開始周波数)と stop_freq(終了周波数)です。本プログラムでは 10 MHz から 1 GHz までの範囲をカバーするように設定しています。この範囲内でどれだけ細かくデータを取るかを決めるのが points(ポイント数)です。今回は「751」点を設定しており、これは 1 GHz という広い帯域を約 1.33 MHz 刻みの高い分解能でスキャンすることを意味します。

分解能

信号の解析精度に直結するのが、rbw(分解能帯域幅)と vbw(ビデオ帯域幅)です。RBW は信号を抽出する際の帯域フィルタの幅であり、数値を小さくするほど近接した信号を分離する能力が高まります。今回は掃引時間を短縮し、効率的な測定を行うためにどちらも1 MHzとしています。

レベル

計測器の物理的な損傷を防ぎつつ正確な値を得るために、tg_level(トラッキングジェネレータ出力)を 0 dBm に、内部の attenuator(アッテネータ)を 20 dB に設定しています。例えばダイナミックレンジを広げてノイズの影響を極限まで抑えたい場合には、rbw を10 kHz程度まで絞り込み、sweep_count(掃引回数)を増やすことで、より安定した定量的な評価を実行できます。

 

測定手順

ここからは実際にプログラムを使用して測定を進める手順を紹介します。

保存先の選択

プログラムを起動すると、まず測定データの保存先を選択するウィンドウが表示されます。適切なフォルダを選択すると、計測器への接続を開始します。

ターミナル表示(保存先を決定し、計測器と接続できた状態)

図 ターミナル表示(保存先を決定し、計測器と接続できた状態)

接続が成功すると、計測器が初期化(*RST)され、周波数範囲(10MHz〜1GHz)やスイープポイント数(751点)などの測定条件が自動で設定されます。

ノーマライズの実施

高周波測定ではケーブル自体の損失を除去するためにノーマライズ(正規化)を行います。 画面の指示に従い、計測器の「TG Out」と「RF In」をスルーケーブルで直接接続します。

ターミナル表示(ノーマライズの開始前)

図 ターミナル表示(ノーマライズの開始前)

スルーケーブルで接続した様子

図 スルーケーブルで接続した様子

接続が完了したら Enterキーを押下します。するとプログラムが基準となる波形を記憶し、以後の測定値からこの誤差分を差し引く設定を自動で行います。これにより、DUTそのものの特性を正確に抽出できます。

ノーマライズ実行後の画面

図 ノーマライズ実行後の画面


DUTの測定

ノーマライズが完了したらDUTの測定に移ります。今回はThru(ケーブルのみ)と、5種類の固定アッテネータ(3dB、6dB、10dB、20dB、30dB)を順番に測定します。

  1. TG OutとRF Inの間にDUTを挿入します。

    DUTとしてアッテネータを挿入した様子

図 DUTとしてアッテネータを挿入した様子

  1. ターミナルにEnterキーで測定開始と表示されるので、Enterキーを押下します。
  2. 保存ファイル名を入力と表示されるので、任意の名前を入力してEnterキーを押下します。(例:att_3dB)
  3. 自動でスイープが行われ、CSVデータが保存された後にグラフが表示されます。

3dBのアッテネータ測定時に表示されたグラフ

図 3dBのアッテネータ測定時に表示されたグラフ

  1. グラフの表示を閉じると次の測定に進みます。

ターミナル表示(測定実施後)

図 ターミナル表示(測定実施後)

測定結果の比較

すべての測定が終了しターミナルに「q」と入力すると、保存されたすべてのデータを読み込んだ比較図が自動で生成されます。

すべての測定結果の比較グラフ

図 すべての測定結果の比較グラフ

ターミナル表示(測定終了後)

図 ターミナル表示(測定終了後)

一番上の0dB付近の線がThru、その下にそれぞれ3dB、6dBと順にラインが並んでいるのがわかります。この結果を見ると700MHz程度までは平坦な減衰特性を示しているのに対して、それ以上の周波数では減衰量が若干波打っており、インピーダンス不整合が生じていることが一目で確認できます。
このように自動測定を活用することで、手動では手間のかかる複数データの比較も、非常にスムーズかつ定量的に実行できるようになります。

 

付録:自動測定プログラム(Python)

import socket

import time

import os

import csv

import tkinter as tk

from tkinter import filedialog

import matplotlib.pyplot as plt

import sys

 

# ==========================================

# ユーザー設定項目 (Conditions)

# ==========================================

# 【重要】計測器の実際のIPアドレスに書き換えてください

INSTRUMENT_IP = '192.168.1.xx' 

PORT = 5025                    

 

# 条件設定 (Configuration)

CONFIG = {

    'start_freq': '10 MHz',     # 開始周波数

    'stop_freq': '1 GHz',       # 終了周波数

    'points': 751,              # ポイント数

    'unit': 'dBm',              # 単位

    'ref_level': '0 dBm',       # リファレンスレベル

    'ref_position': '10',       # リファレンスポジション

    'scale': '10 dB',           # スケール

    'attenuator': '20 dB',      # アッテネータ

    'preamp': 'OFF',            # プリアンプ

    'rbw': '1 MHz',             # RBW

    'vbw': '1 MHz',             # VBW

    'tg_level': '0 dBm',      # TG出力レベル

    'sweep_count': 10,          # 掃引回数

}

 

# ==========================================

# 計測器制御クラス

# ==========================================

class SVA1015X_Controller:

    def __init__(self, ip, port):

        self.ip = ip

        self.port = port

        self.sock = None

 

    def connect(self):

        print(f"{self.ip}:{self.port} に接続中...")

        try:

            self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

            self.sock.settimeout(5.0)

            self.sock.connect((self.ip, self.port))

            print("接続に成功しました。")

            idn = self.query("*IDN?")

            print(f"機器ID: {idn}")

            return True

        except ConnectionRefusedError:

            print(f"\n[エラー] 接続が拒否されました (WinError 10061)。")

            print("IPアドレスを確認してください。")

            return False

        except socket.timeout:

            print(f"\n[エラー] 接続がタイムアウトしました。")

            return False

        except Exception as e:

            print(f"\n[エラー] 接続失敗: {e}")

            return False

 

    def send(self, cmd):

        if not self.sock: return

        try:

            self.sock.sendall(f"{cmd}\n".encode())

            time.sleep(0.05)

        except Exception as e:

            print(f"送信エラー: {e}")

 

    def query(self, cmd):

        if not self.sock: return ""

        self.send(cmd)

        try:

            response = b""

            while True:

                chunk = self.sock.recv(4096)

                response += chunk

                if response.endswith(b'\n'):

                    break

            return response.decode().strip()

        except Exception as e:

            print(f"受信エラー: {e}")

            return ""

 

    def get_sweep_time(self):

        try:

            st_str = self.query(":SENS:SWE:TIME?")

            return float(st_str)

        except:

            return 0.5

 

    def close(self):

        """TG OFF -> Local -> 切断"""

        if self.sock:

            try:

                print("TG出力をOFFにします...")

                self.sock.sendall(b":OUTP OFF\n")

                time.sleep(0.2)

               

                print("Localモードへ移行します...")

                self.sock.sendall(b":SYST:LOC\n")

                time.sleep(0.5)

               

                self.sock.close()

            except Exception as e:

                print(f"切断処理エラー: {e}")

            self.sock = None

            print("接続を切断しました。")

 

def select_save_directory():

    root = tk.Tk()

    root.withdraw()

    root.attributes('-topmost', True)

    print("保存先フォルダを選択してください...")

    directory = filedialog.askdirectory(title="保存先フォルダの選択")

    root.destroy()

    return directory

 

# ==========================================

# データ処理関数

# ==========================================

def load_and_compare_data(save_dir, file_list):

    """

    保存されたCSVファイルを読み込み、all.csv作成と比較グラフ描画・保存を行う

    """

    if not file_list:

        print("比較対象のファイルがありません。")

        return

 

    print(f"\n--- 保存された {len(file_list)} 個のファイルを読み込んでいます ---")

   

    all_data = []

   

    # 1. 各ファイルを読み込む

    for file_info in file_list:

        filepath = file_info['path']

        filename = file_info['name']

       

        try:

            freqs = []

            amps = []

            with open(filepath, 'r') as f:

                reader = csv.reader(f)

                for row in reader:

                    if len(row) >= 2:

                        try:

                            f_val = float(row[0])

                            a_val = float(row[1])

                            freqs.append(f_val)

                            amps.append(a_val)

                        except ValueError:

                            continue

           

            if freqs:

                all_data.append({'name': filename, 'freqs': freqs, 'amps': amps})

        except Exception as e:

            print(f"ファイル読み込みエラー ({filename}): {e}")

 

    if not all_data:

        print("有効なデータが見つかりませんでした。")

        return

 

    # 2. 統合CSVデータの作成 (all.csv)

    base_freqs = all_data[0]['freqs']

    merged_filepath = os.path.join(save_dir, "all.csv")

 

    try:

        with open(merged_filepath, 'w', newline='') as f:

            writer = csv.writer(f)

           

            # ヘッダー: Frequency, File1, File2...

            header = ["Frequency (Hz)"] + [d['name'] for d in all_data]

            writer.writerow(header)

           

            # データ書き込み

            num_points = len(base_freqs)

            for i in range(num_points):

                row = [base_freqs[i]]

                for d in all_data:

                    if i < len(d['amps']):

                        row.append(d['amps'][i])

                    else:

                        row.append("")

                writer.writerow(row)

       

        print(f"\n全データを統合して保存しました: {merged_filepath}")

       

    except Exception as e:

        print(f"all.csv 保存エラー: {e}")

 

    # 3. 比較グラフの描画と保存

    print("比較グラフを描画・保存します...")

    plt.figure(figsize=(12, 8))

   

    for d in all_data:

        plt.plot(d['freqs'], d['amps'], label=d['name'], alpha=0.8)

   

    plt.title("Measurement Comparison")

    plt.xlabel("Frequency (Hz)")

    plt.ylabel("Amplitude")

    plt.legend()

    plt.grid(True, which='both', linestyle='--', alpha=0.7)

    plt.tight_layout()

 

    # --- 画像保存処理 ---

    image_filename = "comparison_graph.png"

    image_filepath = os.path.join(save_dir, image_filename)

    try:

        plt.savefig(image_filepath)

        print(f"比較グラフ画像を保存しました: {image_filepath}")

    except Exception as e:

        print(f"画像保存エラー: {e}")

    # ------------------

 

    plt.show()

    print("比較グラフを閉じました。")

 

 

# ==========================================

# メイン処理

# ==========================================

def main():

    inst = SVA1015X_Controller(INSTRUMENT_IP, PORT)

    saved_files_info = []

   

    try:

        # --- 1. ディレクトリ選択 ---

        save_dir = select_save_directory()

        if not save_dir:

            return

 

        # --- 2. 接続 ---

        if not inst.connect():

            return

 

        # --- 3. 条件設定 ---

        print("\n--- 機器の設定 ---")

        inst.send("*RST")

        time.sleep(2)

 

        inst.send(f":SENS:FREQ:STAR {CONFIG['start_freq']}")

        inst.send(f":SENS:FREQ:STOP {CONFIG['stop_freq']}")

        inst.send(f":UNIT:POW {CONFIG['unit']}")

        inst.send(f":DISP:WIND:TRAC:Y:RLEV {CONFIG['ref_level']}")

        inst.send(f":DISP:WIND:TRAC:Y:PDIV {CONFIG['scale']}")

        inst.send(f":POW:ATT {CONFIG['attenuator']}")

        inst.send(f":POW:GAIN {CONFIG['preamp']}")

        inst.send(f":SENS:BWID {CONFIG['rbw']}")

        inst.send(f":SENS:BWID:VID {CONFIG['vbw']}")

        inst.send(f":SOUR:POW {CONFIG['tg_level']}")

        inst.send(f":SENS:SWE:COUN {CONFIG['sweep_count']}")

 

        print("設定完了。")

 

        # --- 4. ノーマライズ ---

        print("\n--- ノーマライズ ---")

        print("スルーケーブルを接続してください (TG Out -> RF In)。")

        input("Enterキーを押すとノーマライズを開始します...")

 

        inst.send(":OUTP ON")

        inst.send(":TRAC1:MODE MAXH")

        inst.send(":INIT:REST")

 

        sw_time = inst.get_sweep_time()

        wait_time = sw_time * CONFIG['sweep_count'] * 1.2

        print(f"波形安定待ち ({wait_time:.2f} 秒)...")

        time.sleep(wait_time)

 

        inst.send(":TRAC1:MODE VIEW")

        inst.send(":TRAC:COPY:REF")

        inst.send(":CALC:NTD ON")

        inst.send(f":DISP:WIND:TRAC:Y:RPOS {CONFIG['ref_position']}")

        inst.send(":TRAC1:MODE WRIT")

        print("ノーマライズ完了。")

 

        # --- 5. 本測定ループ ---

        while True:

            print("\n==============================")

            print("      測定準備完了")

            print("==============================")

            user_input = input("Enterキーで測定開始 ('q'で終了して比較): ")

            if user_input.lower() == 'q':

                break

           

            filename = input("保存ファイル名を入力 (拡張子不要): ").strip()

            if not filename:

                filename = f"data_{int(time.time())}"

            filepath = os.path.join(save_dir, f"{filename}.csv")

 

            print("測定中...")

            inst.send(":TRAC1:MODE MAXH")

           

            sw_time = inst.get_sweep_time()

            wait_time = sw_time * CONFIG['sweep_count'] * 1.2

            print(f"待機中 ({wait_time:.2f} 秒)...")

           

            inst.send(":INIT:REST")

            time.sleep(wait_time)

 

            inst.send(":TRAC1:MODE VIEW")

 

            print("データ取得中...")

            data_str = inst.query(":TRAC:DATA? 1")

           

            if data_str:

                try:

                    amplitudes = [float(x) for x in data_str.split(',')]

                    num_points = len(amplitudes)

                    start_f = float(inst.query(":SENS:FREQ:STAR?"))

                    stop_f = float(inst.query(":SENS:FREQ:STOP?"))

                    step_f = (stop_f - start_f) / (num_points - 1) if num_points > 1 else 0

                    frequencies = [start_f + i * step_f for i in range(num_points)]

 

                    with open(filepath, 'w', newline='') as f:

                        writer = csv.writer(f)

                        writer.writerow(["# 条件設定"])

                        for k, v in CONFIG.items():

                            writer.writerow([f"# {k}", v])

                        writer.writerow([])

                        writer.writerow(["Frequency (Hz)", "Amplitude"])

                        for freq, amp in zip(frequencies, amplitudes):

                            writer.writerow([freq, amp])

                    print(f"保存完了: {filepath}")

 

                    saved_files_info.append({'path': filepath, 'name': filename})

 

                    print("今回のグラフを表示します(閉じると次へ)...")

                    plt.figure(figsize=(8, 5))

                    plt.plot(frequencies, amplitudes)

                    plt.title(f"Result: {filename}")

                    plt.grid(True)

                    plt.show()

 

                except Exception as e:

                    print(f"データ処理エラー: {e}")

            else:

                print("トレースデータの取得に失敗しました。")

 

            inst.send(":TRAC1:MODE WRIT")

 

    except KeyboardInterrupt:

        print("\nユーザーにより中断されました。")

    except Exception as e:

        print(f"\n予期しないエラーが発生しました: {e}")

    finally:

        # --- 終了処理 ---

        if 'inst' in locals():

            inst.close()

       

        if len(saved_files_info) > 0:

            print("\n==============================")

            print("      比較データ生成")

            print("==============================")

            load_and_compare_data(save_dir, saved_files_info)

        else:

            print("保存されたデータがないため、比較処理をスキップします。")

 

if __name__ == "__main__":

    main()





デモ機レンタルサービス
https://tm-co.co.jp/contact/


© 2026 T&Mコーポレーション株式会社

この記事の続きを読むには無料会員登録またはログインが必要です。

無料会員に登録する

関連製品