
【第4回】Pythonによるオシロスコープ波形データ解析の秘訣【デジタルデータ編】
2021.12.20
Python でオシロスコープ波形データを解析しよう!
前回に引き続き、オシロスコープから得た csv ファイルを Python で解析していく方法をご紹介します。
今回は「デジタル編」として、クロック波形やシリアル通信の解析を行っていきましょう!
実際の波形データを Python でオシロスコープから取得する方法は、前回の記事をご参照ください。
前回の記事はこちら!
Python でデジタルデータを解析するメリット
デジタルデータとは?
まずは、今回の解析対象である「デジタルデータ」について説明します。
デジタルデータは、「High」と「Low」の2値で決まる論理データのことです。
これを可視化したのが、「クロック波形」と「データ波形」となります。
普通、2値の論理は、クロック波形の「立ち上がり」もしくは「立ち下がり」のエッジタイミングで決まります。
例えば、シリアル通信のデータ送受信では、クロックエッジにおけるデータ波形の電圧値で論理値を決定しているのです。
デジタルデータにおけるクロック
デジタルデータを決定する「クロック波形」は、電源電圧と同じ電圧値と、0V の電圧値を一定周期で繰り返す信号です。
クロック波形の品質は、周期の安定性が最重要ポイントで、常に一定値とはなり得ません。
そして、一定期間の周期のズレを「ジッタ」といいます。
デジタル波形データ解析のメリット
この「クロックジッタ」は、オシロスコープの画面上で波形を重ねたり、内蔵機能で測定できます。
前回のアナログデータ同様、ここでも Python を使うことで、波形データの転送からジッタ測定まで自動化できます。
同時に、シリアル通信など、クロックに同期したデータの解析も行えるのです。
データ取得から解析までを全てこなせるのは、Python ならではのメリットと言えるでしょう。
波形データの準備
今回解析するのは、「クロックのジッタ」と「クロックに対する論理データ値」です。
まずは、Python のコードで波形データを生成していきましょう!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | import pandas as pd import random import math X_SAMPLE = 3000 NOISE_SEED_OFF = 123 #CLOCK CLOCK_HLFINT = 200 CLOCK_INT = CLOCK_HLFINT*2 JITTER = 20 NOISE_AMP_1=0.1 AMP_1= 5.0 RC_CLOCK=10 #DATA DATA_INT = 400 AMP_2 =5 NOISE_AMP_2=0.1 DATA =[0,1,0,0,1,0,1,0,0] RC_DATA = 14 xx = [ i for i in range(3000)] clock_wave=[] noise_1=[] for i in range(len(xx)): random.seed(NOISE_SEED_OFF+i) noise_1.append(NOISE_AMP_1*random.randint(-100,100)/100) if (i % CLOCK_INT) < CLOCK_HLFINT: clock_wave.append(noise_1[i]) if(i % CLOCK_INT) ==0: jitter = random.randint(0,JITTER) elif (i % CLOCK_INT) >= CLOCK_HLFINT: time = (i % CLOCK_INT)-CLOCK_HLFINT if time <jitter : clock_wave.append(noise_1[i]) else: rise_dt = AMP_1*(1 - math.exp(-1/RC_CLOCK*xx[time-jitter])) clock_wave.append(rise_dt + noise_1[i]) data_wave=[] noise_2=[] prev_dt =0 no=0 for i in range(len(xx)): random.seed(NOISE_SEED_OFF+i) noise_2.append(NOISE_AMP_2*random.randint(-100,100)/100) if (i % DATA_INT) == 0: prev_dt=DATA[no] no+=1 if prev_dt == 0: if DATA[no] > 0: time = i % DATA_INT rise_dt = AMP_2*(1 - math.exp(-1/RC_DATA*xx[time])) data_wave.append(rise_dt + noise_2[i]) else: data_wave.append(noise_2[i]) else: if DATA[no] > 0: data_wave.append(AMP_2+noise_2[i]) else: time = i % DATA_INT rise_dt = AMP_2*math.exp(-1/RC_DATA*xx[time]) data_wave.append(rise_dt + noise_2[i]) wave_data= pd.DataFrame({"wave_1":clock_wave,"wave_2":data_wave}) wave_data.to_csv("digital_wave_test.csv") |
上記コードでは、実際の波形データにある「ランダムノイズ成分」や「過渡応答」、「ジッタ成分」についても再現されています。
今回生成する波形データのサンプル数は3,000個で、仕様は以下の通りです。
- 振幅 5V のクロック波形(チャンネル1)
- 振幅 5V で HIGH と LOW を繰り返すデータ波形(チャンネル2)
クロック波形
クロック波形をグラフ化すると、このようになります。

データ波形
一方、データ波形のグラフはこんな感じです。

時系列逐次処理でデータを解析!
今回も、波形データに特化した「時系列逐次処理」で処理を行っていきます。
詳しい方法は、前回の記事を参照してください。
前回同様、以降の処理は繰り返し文の中に記述していきます。
前回の記事はこちら
①クロックデータの解析
さて、まずはクロックデータの解析です。
クロック波形のジッタを取得しましょう!
該当するコードは下記になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | #CH1:クロック波形のRise/Fallタイミングを測定 ##CLKのRiseを検出 if index_dt % DET_SMP_INT == 0: #リングバッファへの保存 for i in range(len(clk_buf)-1): clk_buf[len(clk_buf)-1-i] = clk_buf[len(clk_buf)-1-i-1] clk_buf[0]=ch1_dt #5回連続で値が閾値を超えた場合に閾値超過と判定する for i in range(len(clk_buf)): #Rise検出フラグがTrueでないとRise検出は行わない if clk_buf[i] > CLK_LOGIC_TH and clk_logic_det_f[0]==True: if i >4 : clk_rise_point.append([index_dt,ch1_dt]) clk_logic_det_f =[False,True] #Riseを検出したらデータ値の取得を開始する data_det_f = True break #Fall検出フラグがTrueでないとFall検出は行わない elif clk_buf[i] < CLK_LOGIC_TH and clk_logic_det_f[1]==True: if i >4 : clk_fall_point.append([index_dt,ch1_dt]) clk_logic_det_f =[True,False] break else: break |
クロック立ち上がりタイミングの取得
クロック波形を解析するにあたって必要なことは、「立ち上がりと立ち下がりを順々に検出していくこと」です。
リングバッファを使って、5回連続で閾値を超えたタイミングを立ち上がりとします。
今回の閾値は中間の「2.5V」としましたが、実際の閾値は、立ち上がりと立ち下がりで異なるケースもあるようです。
フラグで検出タイミングを管理
検出のタイミングで前回と異なる点は
1 | if clk_buf[i] > CLK_LOGIC_TH and clk_logic_det_f[0]==True: |
の箇所で clk_logic_det_f[0]==True: という条件が増えている点です。
今回は、「立ち上がり→立ち下がり→立ち上がり」のように、交互に検出を行うためです。
そのため、以下のように、要素を2つ持つフラグを作りましょう。
1 | clk_logic_det_f =[True,False] |
1番目を立ち上がり、2番目を立ち下がりの検出フラグとして、コード上で管理していきます。
立ち上がり検出後のコードで
1 | clk_logic_det_f =[False,True] |
としているのは、立ち上がり検出をやめて、立ち下がり検出を次から行うように切り替えているからです。
検出した立ち上がりの位置と値は
1 | clk_rise_point.append([index_dt,ch1_dt]) |
によってリストへ付加されていきます。
立ち下がりタイミングの取得
立ち上がり検出後は、5回連続で閾値以下となったタイミングで、立ち下がりを検出します。
コード上では
1 | elif clk_buf[i] < CLK_LOGIC_TH and clk_logic_det_f[1]==True: |
となっており、検出するとその位置を記録し、フラグを立ち上がり検出に切り替えるようになっています。
記録した立ち上がりタイミングからのジッタ測定
こうして波形データを最後まで読み出したら、検出した立ち上がり位置が、リスト clk_rise_point に記録されます。
このリストを使って
1 2 3 4 5 | #ジッタの測定 clk_rise_int=[] ##Riseの間隔を記録 for i in range(len(clk_rise_point)-1): clk_rise_int.append(clk_rise_point[i+1][0]-clk_rise_point[i][0]) |
という計算を行えば、立ち上がり検出の間隔、つまり「周期」を求めることができます。
この周期の変動量が「ジッタ」です。
普通、最大変動量をジッタ量として評価するので、このクロックのジッタは
1 | print("CLK Jitter:"+str(max(clk_rise_int)-min(clk_rise_int))) |
を計算して CLK Jitter:18.0 ということになります。
②データ波形の解析
続いて、データ波形を解析していきましょう!
コードの全体像は以下になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #CH2:クロックRise時のデータ波形の論理値を取得 if index_dt % DET_SMP_INT == 0 and data_det_f==True: #リングバッファへの保存 for i in range(len(data_buf)-1): data_buf[len(data_buf)-1-i] = data_buf[len(data_buf)-1-i-1] #CH2の電圧値に応じてHIGH/LOWに分ける if ch2_dt > DATA_LOGIC_TH: data_buf[0]=HIGH else: data_buf[0]=LOW #バッファの全ての値が同じ論理ならデータを確定する if all([data_buf[i] == HIGH for i in range(len(data_buf))]): detect_data.append(HIGH) data_det_f=False data_buf =[ X for i in range(10)] elif all(data_buf[i] == LOW for i in range(len(data_buf))): detect_data.append(LOW) data_det_f=False data_buf =[ X for i in range(10)] |
クロックとデータの連携
一般的には、データ波形は単体ではなく、クロック波形の解析とセットで行われます。
そのため、最初にデータとクロックの連動が必要です。
実際の回路動作でも、クロックの立ち上がりタイミングに同期して、データ波形の論理を確認するようになっています。
立ち上がり検出時に
1 | data_det_f = True |
というフラグ管理をして、解析するタイミングを決定します。
クロック立ち上がりタイミングでのデータ確定
クロックの立ち上がりを検出したら、同タイミングでデータ波形の論理を確認します。
データ波形を読み取り、クロックタイミングでのデータ値を確定しなければなりません。
ただし、一度の検出で確定してしまうと、ノイズなど誤検出が発生するリスクがあります。
all 関数でデータを確定させる
こうしたリスクを防ぐには、連続して複数のデータをサンプリングするのが有効です。
1 | if all([data_buf[i] == HIGH for i in range(len(data_buf))]): |
上記コードで、バッファに貯めたデータの論理が全て一致した時に、データを確定するようにしています。
Python の all 関数を使えば、リスト内の値が全て同一かを1行で検出可能です。
データが確定したら、バッファを「不定値 X」で初期化しておきましょう!
取得データの表示
クロックと同様に、データ読み込みが終了した時点で、リスト detect_data に確定したデータ配列が格納されています。
print で確認してみると
1 | [1, 0, 0, 1, 0, 1, 0] |
となっており、HIGH と LOW の論理データが順番に取得できていることがわかりますね。
サンプルコード:クロック波形とデータ波形を同時に解析し、ジッタとデジタルデータを取得
最後に、ここまでで紹介した解析処理をそのまま実行できるコードを載せておきます。
ファイル名やスキップする行を変えれば、どんな波形データでも対応できますよ!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | wave_path = "./digital_wave_test.csv" wave_file = open(wave_path,'r') skip_row_count = 1 #論理値用の値 HIGH = 1 LOW = 0 X = 2 #設定値関連 MAX_REC = 5 #最大・最小を記録する際に何個まで記録するか CLK_LOGIC_TH = 5*0.5 #チャンネル2で検出する電圧の値(既定電圧の20%) DATA_LOGIC_TH = 5*0.5 #チャンネル2で検出する電圧の値(既定電圧の80%) DET_SMP_INT = 2 #電圧閾値を検出する際にデータ比較をするサンプル間隔 #ヘッダ行等のスキップ while skip_row_count !=0: row_data = wave_file.readline() skip_row_count-=1 #変数の定義 clk_rise_point=[] clk_fall_point=[] detect_data=[] clk_buf =[ 0 for i in range(10)] data_buf =[ X for i in range(10)] #データの読み込み開始 ##最初の1行目はループ前に読み込む row_data = wave_file.readline() split_data = row_data.split(",") clk_logic_det_f =[True,False] data_det_f = False dt_cnt=0 while row_data: index_dt = float(split_data[0]) ch1_dt = float(split_data[1]) ch2_dt = float(split_data[2]) ###########この間に解析処理を入れる########## #CH1:クロック波形のRise/Fallタイミングを測定 ##CLKのRiseを検出 if index_dt % DET_SMP_INT == 0: #リングバッファへの保存 for i in range(len(clk_buf)-1): clk_buf[len(clk_buf)-1-i] = clk_buf[len(clk_buf)-1-i-1] clk_buf[0]=ch1_dt #5回連続で値が閾値を超えた場合に閾値超過と判定する for i in range(len(clk_buf)): #Rise検出フラグがTrueでないとRise検出は行わない if clk_buf[i] > CLK_LOGIC_TH and clk_logic_det_f[0]==True: if i >4 : clk_rise_point.append([index_dt,ch1_dt]) clk_logic_det_f =[False,True] #Riseを検出したらデータ値の取得を開始する data_det_f = True break #Fall検出フラグがTrueでないとFall検出は行わない elif clk_buf[i] < CLK_LOGIC_TH and clk_logic_det_f[1]==True: if i >4 : clk_fall_point.append([index_dt,ch1_dt]) clk_logic_det_f =[True,False] break else: break #CH2:クロックRise時のデータ波形の論理値を取得 if index_dt % DET_SMP_INT == 0 and data_det_f==True: #リングバッファへの保存 for i in range(len(data_buf)-1): data_buf[len(data_buf)-1-i] = data_buf[len(data_buf)-1-i-1] #CH2の電圧値に応じてHIGH/LOWに分ける if ch2_dt > DATA_LOGIC_TH: data_buf[0]=HIGH else: data_buf[0]=LOW #バッファの全ての値が同じ論理ならデータを確定する if all([data_buf[i] == HIGH for i in range(len(data_buf))]): detect_data.append(HIGH) data_det_f=False data_buf =[ X for i in range(10)] elif all(data_buf[i] == LOW for i in range(len(data_buf))): detect_data.append(LOW) data_det_f=False data_buf =[ X for i in range(10)] ####################################### ##次の行を読み込む。ここで最終行なら空白が返ってくるためループ終了 dt_cnt+=1 row_data = wave_file.readline() split_data = row_data.split(",") wave_file.close() #ジッタの測定 clk_rise_int=[] ##Riseの間隔を記録 for i in range(len(clk_rise_point)-1): clk_rise_int.append(clk_rise_point[i+1][0]-clk_rise_point[i][0]) print("CLK Jitter:"+str(max(clk_rise_int)-min(clk_rise_int))) ##確定したデータを確認 print(str(detect_data)) |
第5回へつづく!
今回は、デジタル回路や通信波形の基本である「クロック波形と対応するデータ波形の解析手法」を紹介しました。
正直、この程度ならオシロスコープの機能だけでも十分まかなえます。
しかし、Python なら、さらに高度な解析が可能ですし、何より「自動解析」という強みがあります。
ぜひ、本記事を応用して役立ててくださいね!
そして次回は、「Python で Modbus 通信を行う方法」について解説していきます。
第5回はこちら!
第1回はこちら!
こちらの記事もオススメ!
書いた人はこんな人

- 「好きを仕事にするエンジニア集団」の(株)ライトコードです!
ライトコードは、福岡、東京、大阪の3拠点で事業展開するIT企業です。
現在は、国内を代表する大手IT企業を取引先にもち、ITシステムの受託事業が中心。
いずれも直取引で、月間PV数1億を超えるWebサービスのシステム開発・運営、インフラの構築・運用に携わっています。
システム開発依頼・お見積もり大歓迎!
また、現在「WEBエンジニア」「モバイルエンジニア」「営業」「WEBデザイナー」「WEBディレクター」を積極採用中です!
インターンや新卒採用も行っております。
以下よりご応募をお待ちしております!
https://rightcode.co.jp/recruit
ライトコードの日常12月 1, 2023ライトコードクエスト〜東京オフィス歴史編〜
ITエンタメ10月 13, 2023Netflixの成功はレコメンドエンジン?
ライトコードの日常8月 30, 2023退職者の最終出社日に密着してみた!
ITエンタメ8月 3, 2023世界初の量産型ポータブルコンピュータを開発したのに倒産!?アダム・オズボーン