【第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回はこちら!
こちらの記事もオススメ!
2020.07.17ライトコード的「やってみた!」シリーズ「やってみた!」を集めました!(株)ライトコードが今まで作ってきた「やってみた!」記事を集めてみました!※作成日が新し...
2020.07.30Python 特集実装編※最新記事順Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみた!P...
ライトコードでは、エンジニアを積極採用中!
ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。
採用情報へ
「好きを仕事にするエンジニア集団」の(株)ライトコードです! ライトコードは、福岡、東京、大阪、名古屋の4拠点で事業展開するIT企業です。 現在は、国内を代表する大手IT企業を取引先にもち、ITシステムの受託事業が中心。 いずれも直取引で、月間PV数1億を超えるWebサービスのシステム開発・運営、インフラの構築・運用に携わっています。 システム開発依頼・お見積もり大歓迎! また、現在「WEBエンジニア」「モバイルエンジニア」「営業」「WEBデザイナー」を積極採用中です! インターンや新卒採用も行っております。 以下よりご応募をお待ちしております! https://rightcode.co.jp/recruit