【第4回】Pythonによるオシロスコープ波形データ解析の秘訣【デジタルデータ編】
IT技術
Python でオシロスコープ波形データを解析しよう!

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

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

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








