オシロが欲しい
オシロが欲しい、正確には本格的なオシロが欲しい。
これまで(おそらく)10年以上前に、数千円で売られていた、簡易的な秋月のオシロスコープ (DSO062) を使ってきました。 別でロジアナは持っているので、このオシロは「ちょっと機能が多く反応が早いテスター」「ロジアナに入力しても大丈夫な電圧か、サンプリングレートをどうするか決める用」です。
ただ、さすがに最近は厳しくなってきており、特にノンタッチキーのデータを読むときに痛感。 いま取り組んでいる事柄でも、大した測定じゃないのになんとか手持ちの機器で測定できるようこの測定のためだけに回路を組む始末。
本格的なオシロが欲しくないですか? 具体的にはこんな要件のさ……。
- メモリ長が長い
- 苦しめられたので、数十Mptsは欲しい
- 入力は2ch以上
- 差動信号でも2ch分は見たい
- (差動プローブは高くて買えないので)差動入力あり or 2ch分の入力を演算して差動1ch相当が欲しい
- 現実的には差動入力はあまり無いため、演算することを前提に「入力は4ch」が要件となる
- 周波数帯域はあまりこだわらない
- 数MHzの信号が見れれば良い
- (もちろん広帯域のほうが嬉しい)
- 予算は10万円以内
- (もちろん安いほうが嬉しい)
- ユーザやレビューが多い
- なんとなく品質が予測できること
SIGLENT SDS804Xを購入
以前は「次オシロを買うとしたらAnalog Discoveryだ。信号発生器も付いているし」と思ったものですが、メモリ長があまり無いことと最近の(円安による)価格設定だと割高に感じて見送りました。 PicoScopeはメモリ長が長いやつは高い! というかメモリ長が長いやつはどんなオシロでもだいたい高い! ヤフオクで数十年前の、かつては数十万?数百万円?したブランドオシロを買う? いやでもフロッピーディスクを使わないといけないような機器をこれから使い始めるのはちょっと……。 DSP版としてWindowsとFDDが同梱販売されていた時代じゃないんですよ。 ほか、よく分からないけどカタログスペックは良いオシロはありましたが、よく分からなかったのでなし。
最終的に候補に残ったのは、「RIGOL DHO800シリーズ」「SIGLENT SDS800X HDシリーズ」「RIGOL DHO900シリーズ」です。 この順番で、カタログスペックや価格的にもだいたい「梅→竹→松」となっています。 それぞれのシリーズのうち4chかつ周波数帯域が狭い(安い)型番で、自分が興味ある部分のスペックを示すとこんな感じです。
RIGOL DHO804 | SIGLENT SDS804X HD | RIGOL DHO914 | |
---|---|---|---|
周波数帯域 | 70 MHz (÷5 = 14 MHz) | 70 MHz (÷5 = 14 MHz) | 125 MHz (÷5 = 25 MHz) |
最大サンプリングレート | 1ch: 1.25 GSa/s (÷10 = 125 MHz) 2ch: 625 MSa/s (÷10 = 62.5 MHz) 4ch: 312.5 MSa/s (÷10 = 31.25 MHz) (※ 312.5 MSa/s ÷ 2.5 = 125 MHz) | 1ch: 2 GSa/s (÷10 = 200 MHz) 2ch: 1 GSa/s (÷10 = 100 MHz) 4ch: 500 MSa/s (÷10 = 50 MHz) (※ 500 Msa/s ÷ 2.5 = 200 MHz) | 1ch: 1.25 GSa/s (÷10 = 125 MHz) 2ch: 625 MSa/s (÷10 = 62.5 MHz) 4ch: 312.5 MSa/s (÷10 = 31.25 MHz) (※ 312.5 MSa/s ÷ 2.5 = 125 MHz) |
メモリ長 | 1ch: 25 Mpts 2ch: 10 Mpts 4ch: 1 Mpts | 1ch: 50 Mpts 2ch: 25 Mpts 4ch: 10 Mpts | 1ch: 50 Mpts 2ch: 25 Mpts 4ch: 10 Mpts |
外部インターフェース | USB, LAN, HDMIなど | USB, LANなど | USB, LAN, HDMIなど |
メーカーサイト記載価格 | 54,000円+税(59,400円) | $439(1ドル160円換算で70,240円+税や手数料?) | 90,000円+税(99,000円) |
実売価格(7/2時点) | 59,400円 (Amazon.co.jp) | 68,200円 (Amazon.co.jp) | 99,000円 (Amazon.co.jp) |
なおADCは、RIGOL DHO800/900でもSIGLENT SDS800X HDでもすべて12bitとなっています。 自分の要件だと必ずしも12bitである必要はありません。 一方で、これらのオシロは8bitと比べそれほど価格差があるわけでもなく、わざわざ8bit側を選びたい事情もないです。
さて、この比較を見ると「SIGLENT SDS804X HD」が性能はRIGOL DHO914寄りなのに価格はDHO804寄りで良いですね。 ならまぁ松竹梅の法則で選ぶか、というのは判断が早い👺すぎます。 この章のタイトルが「SIGLENT SDS804Xを購入」なので察しがつくかもしれませんが、すでにそのモデルを購入しており製品の優劣というよりは自分の判断を正当化したくSIGLENT側を持ち上げる比較表になってしまったかもしれません。 ここでは自分が興味あるスペックしか載せておらず、たとえば波形取り込みレートに関してはRIGOL側のほうが有利に見える、またRIGOLにしかない機能(電源がUSB Type-Cだったり)もありますがその辺は省いています。
でもやっぱり自分が購入したのは「SIGLENT SDS804X HD」でした。 決め手はメモリ長、とそれに対する価格が安価なことです。
かっこE pic.twitter.com/e0PzxsMKKA
— lrks (@__lrks__) June 25, 2024
以下からは、セットアップと各種機能の試用について行っていきます。
セットアップ
まず、クイックスタートのガイドにしたがってプローブの校正を行いました。 校正用端子に繋いで “Auto Setup” ボタンを押すか、プローブ設定画面の “Probe Check”(校正した結果が正常範囲内か確認してくれる)を実行するとそれらしい波形が見えます。
プローブ校正の様子
もったいなくて “Remove film before use” が外せません。 いつかは外しますが、プローブ校正程度ならこのままでもなんとかなる、 それにWebコンソールからも(noVNCで!)アクセスできるのでまだいけます。
Webコンソールの画面
波形の保存
波形の保存も試します。 まず、Arduinoを用意し
- だいたい50秒おきに、
- (analogWrite由来の)だいたい490Hz 1/2 dutyのPWMの波形を50msほど出力する
- その後115200 baud のUARTで「count:<カウンタの値><改行コード>」というメッセージを出力する
ようにしました。 このUART出力をch1・PWM出力をch2としてオシロスコープに入力、ch2をトリガーにして(UART出力はzoomして)観測した結果は次のとおりです。
Arduinoから出力した波形
さらに(ch1 + ch2)/5
という謎演算を入れ、メモリ長なども調整したのがこれです。
Arduinoから出力した波形と謎演算結果
この波形を保存します。 まず、Utility
からSave/Recall
に進み、保存するチャネル(特定のチャネル or 全チャネル)や保存形式(容量を節約するためBinary(別途ツールでCSVに変換できる) or 人間が見やすいようにCSV)などを選択したうえで、USBメモリに保存が可能です。 Webコンソールからも直接ダウンロードできたり(あまりに大きいデータは不可)、試していないですがSMBや一時的に?オシロスコープのローカルに保存もできるようでした。
ここではF1の波形をCSVファイルとして保存し、雑にプロットすると……こうなります。
波形データのプロット結果
このプロットだとグラフの形が正しいかは保証しない(タイムスタンプを使っていないので。ちなみにExcelの表示上時間の数値が丸められているが実際のデータはもっと桁数がある)ですが、まぁデータとしてはそれっぽいですね。
SCPIによる操作
次にSCPIによる操作を試します。 SCPIとは?あとVISA?LXI?という規格も出てくるが? あまりに馴染みがなく説明できませんが、これらは計測器を遠隔から操作するための仕組みであり、 これに沿った操作をしてみます。
SIGLENTが公開しているプログラミングガイドを参照し機器の情報を返すコマンドを、TCP/IP経由でPythonとpyvisa(およびそのBackendのpyvisa-py)ライブラリを使って叩いてみると……こうなるです(一部マスク)。
$ pip3 install pyvisa pyvisa-py
$ python3
>>> import pyvisa as visa
>>> sds = visa.ResourceManager().open_resource("TCPIP::192.168.xx.xx::INSTR")
>>> sds.query('*IDN?')
'Siglent Technologies,SDS804X HD,XXXXXXXXXXXXXX,3.8.12.1.1.3.8\n'
これを発展させ、前章「波形の保存」の構成でトリガーが来るたびにF1の波形を取得してみます。 スクリプトの大雑把な構成としてはこんな感じです。
- (手動でサンプリングレートやトリガーを設定する)
- トリガーに引っかかるのを待つ
- 測定中断
- F1の波形を取得する
- 測定を再開して2.に戻る
「トリガーに引っかかるのを待つ」「測定中断」部分について、実際はオシロスコープ側のトリガーをシングルショットにすることで実現しています。 本当はノーマルモードだろうが何だろうが、オシロスコープ側からトリガーに引っかかったことを通知してもらって……だと嬉しかったのですが、機器のためかTCP/IPで繋いでいるためか(USBでNI-VISAを使っていれば別の方法もあったかも)そんな方法は見つけられませんでした。 物理的にトリガー出力端子を見る?という方法もありますがちょっと手間です。 ということで、トリガーをシングルショットとし、スクリプト側から定期的にオシロスコープの状態を確認することで似たようなことを実現しています。
実際のスクリプトはこうです
import pyvisa as visa
import struct
import datetime
import math
import time
#
# Programming Guide, Read Waveform Data Example より
#
def parse_preamble(recv_all):
recv = recv_all[recv_all.find(b'#') + 11:] # `#9<9-Digits>`の後にバイナリデータが続く
WAVE_ARRAY_1 = recv[0x3c:0x3f + 1]
wave_array_count = recv[0x74:0x77 + 1]
first_point = recv[0x84:0x87 + 1]
sp = recv[0x88:0x8b + 1]
v_scale = recv[0x9c:0x9f + 1]
v_offset = recv[0xa0:0xa3 + 1]
interval = recv[0xb0:0xb3 + 1]
code_per_div = recv[0xa4:0Xa7 + 1]
adc_bit = recv[0xac:0xad + 1]
delay = recv[0xb4:0xbb + 1]
tdiv = recv[0x144:0x145 + 1]
probe = recv[0x148:0x14b + 1]
tdiv_enum = [200e-12,500e-12, 1e-9,\
2e-9, 5e-9, 10e-9, 20e-9, 50e-9, 100e-9, 200e-9, 500e-9, \
1e-6, 2e-6, 5e-6, 10e-6, 20e-6, 50e-6, 100e-6, 200e-6, 500e-6, \
1e-3, 2e-3, 5e-3, 10e-3, 20e-3, 50e-3, 100e-3, 200e-3, 500e-3, \
1, 2, 5, 10, 20, 50, 100, 200, 500, 1000]
data_bytes = struct.unpack('i', WAVE_ARRAY_1)[0]
point_num = struct.unpack('i', wave_array_count)[0]
fp = struct.unpack('i', first_point)[0]
sp = struct.unpack('i', sp)[0]
interval = struct.unpack('f', interval)[0]
delay = struct.unpack('d', delay)[0]
tdiv_index = struct.unpack('h', tdiv)[0]
probe = struct.unpack('f', probe)[0]
vdiv = struct.unpack('f', v_scale)[0] * probe
offset = struct.unpack('f', v_offset)[0] * probe
code = struct.unpack('f', code_per_div)[0]
adc_bit = struct.unpack('h', adc_bit)[0]
tdiv = tdiv_enum[tdiv_index]
return vdiv, offset, interval, delay, tdiv, code, adc_bit
def save_waveform():
sds.write(f":WAVeform:SOURce {channel}")
sds.write(":WAVeform:STARt 0")
sds.write(":WAVeform:PREamble?")
vdiv, ofst, interval, trdl, tdiv, vcode_per, adc_bit = parse_preamble(sds.read_raw())
print(f"vdiv={vdiv}, ofst={ofst}, interval={interval}, trdl={trdl}, tdiv={tdiv}, vcode_per={vcode_per}, adc_bit={adc_bit}")
points = float(sds.query(":ACQuire:POINts?").strip())
one_piece_num = float(sds.query(":WAVeform:MAXPoint?").strip())
read_times = math.ceil(points / one_piece_num)
if points > one_piece_num: sds.write(":WAVeform:POINt {}".format(one_piece_num))
sds.write(f":WAVeform:WIDTh {'WORD' if adc_bit > 8 else 'BYTE'}")
recv_byte = b''
for i in range(0, read_times):
sds.write(f":WAVeform:STARt {i*one_piece_num}")
sds.write(":WAVeform:DATA?")
recv_rtn = sds.read_raw().rstrip()
block_start = recv_rtn.find(b'#')
data_digit = int(recv_rtn[block_start + 1:block_start + 2])
data_start = block_start + 2 + data_digit
recv_byte += recv_rtn[data_start:]
if adc_bit > 8:
convert_data = struct.unpack("%dh"%points, recv_byte)
else:
convert_data = struct.unpack("%db"%points, recv_byte)
time_value = []
volt_value = []
for idx in range(len(convert_data)):
volt_value.append(convert_data[idx] / vcode_per * float(vdiv) - float(ofst))
time_value.append(-(float(tdiv) * 5) + idx * interval + float(trdl))
filename = f"data-{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}.csv"
with open(filename, 'w') as file:
for i in range(len(convert_data)): file.write(f"{time_value[i]},{volt_value[i]}\n")
print(f"save {filename}")
if __name__ == '__main__':
ipaddr = "192.168.xx.xx"
channel = "F1"
sds = visa.ResourceManager().open_resource(f"TCPIP::{ipaddr}::INSTR")
sds.timeout = 2000 # 2 sec
sds.chunk_size = 20 * 2**20 # 20 MBytes
while True:
time.sleep(1)
status = sds.query(':TRIGger:STATus?').strip()
if status != 'Stop': continue
print('TRIG!!!')
save_waveform()
sds.write(':TRIGger:RUN')
1秒ごとにオシロスコープの状態を確認し、トリガーに引っかかっていそうなら波形を取得しカレントディレクトリのdata-YYYYMMDDhhmmss.csv
にタイムスタンプと電圧を書き込み、次のトリガーを待ちます。
取得したデータをまた雑にプロットしてみるとこうなって、まぁ大丈夫そうですね。
SCPIにより取得した波形
ちなみに、SCPIのコマンドは :WAVeform:STARt
という感じで慣れません。 GNUコーディング規約のインデントと同じくらい奇妙に見える。 SCPIでは先頭の:
と小文字部分は省略できるようで、WAV:STAR
というコマンドにしても良いそうです。 うーん……慣れない。
プロトコルデコード
あとプロトコルデコードも試してみましょう。 このオシロでは、標準でI2C、SPI、UART、CAN、LINのデコードが可能なようです。
「波形の保存」で述べたUART (ch1)をデコードしてみると……こうなります。 それっぽいですね。
UARTのデコード結果
ちなみにこのデコード結果を保存するには? 「波形の保存」だと本当に波形が保存されるだけです。 (またはデコード結果を含めて保存する方法は見つけられませんでした。) 一方、特定のSCPIコマンドを使えばデコード結果が得られるようです。
>>> sds.query(':DECode:BUS1:RESult?')
"uart,rx,rx_err;\nuart,tx,tx_err;\n'c',;\n'o',;\n'u',;\n'n',;\n't',;\n':',;\n'2',;\n'0',;\n'\r',;\n'\n',;\n\n"
>>> print(sds.query(':DECode:BUS1:RESult?'))
uart,rx,rx_err;
uart,tx,tx_err;
'c',;
'o',;
'u',;
'n',;
't',;
':',;
'2',;
'0',;
',;
'
',;
とはいえ、あまりにも長いデータは別途ロジアナか何かで収集した方が楽でしょう。
以上。 おわり。