Pythonでオシロスコープ波形のタイミング解析(その2)

[前回](https://qiita.com/nachtele/items/7923f12418c067e3e57f)紹介したタイミング解析ツールについて、少し詳しく解説していきます。

# class TimingAnalyzer

## 初期化

TimAn.py の中身である class TimingAnalyzer がタイミング解析ツールの本体です。
最初に、解析設定リストを与えて初期化します。
解析設定リストを適切に作成することで、タイミング解析ツール本体のプログラムコードを変更することなく、Setup Time, Hold Time, Rise Time, etc. といった解析項目を自由に設定することができます。

```python
ta = TimingAnalyzer(tas)
```

TimingAnalyzereインスタンスは、下記のインスタンス変数を持ちます。

```python
        self.v = {}     #現在値
        self.t = {}     #最終イベント時刻
        self.e = {}     #イベント発生フラグ
        self.hv = {}    #履歴(値)
        self.ht = {}    #履歴(時刻)
        self.hg = {}    #計算対象の値
```

これらの辞書には、解析設定リストにより定義された「解析式」の情報が保持されます。。

tas は下記の構成になっています。

```python
tas = [
    [ #全データポイント解析
        [ #解析式
            [(辞書キー), [(初期値), (遷移時刻初期値)], [
                [[[条件], [条件], …], [代入式]] 
                ,[[[条件], [条件], …], [代入式]]
                , …
           ]
        ]
        ,[(解析式)]
        , …
    ]
    ,[ #イベント有データポイントのみ解析
        [(解析式)]
        , …
    ]
]
```
tas は大きく2ブロックに分かれ、前半は全データポイントについて解析する項目、後半は最初のデータポイントと前半ブロックでイベント(遷移)が発生したデータポイントのみ解析する項目です。解析対象となるオシロスコープのデータ数は100万ポイント程度になり、全ポイントについて全項目解析すると解析時間がかなりかかるため、時間短縮のためにこのように分けています。
ここで、tasリストの2階層目を「解析式」と呼ぶことにします。この解析式で、"SCL rise time" や "Start condition hold time" といった解析項目を表現することになります。
また、"SCL" の値を "SCL rise time" の条件や代入式に使用するというように、他の解析式を参照することもできます。
さらに、最終的な出力対象にはならない中間的な状態を保持するためにも解析式を使います。

解析式の最初の項目は辞書キーです。他の解析式で参照するときや、後で結果を参照するときに使います。
2番目は初期値で、「現在値」と「最終イベント時刻」の初期値を設定します。
3番名は条件式リストで、1つ以上の条件式で構成されます。条件式は、条件リストと代入式からなります。1つの条件式内の条件リストはAND判定され、全条件を満たす場合に、代入式が実行され、「現在値」と「最終イベント時刻」が更新され、「履歴(値)」と「履歴(時刻)」が追加されます。条件式が複数ある場合、順次評価され、成立した場合はそれより後の条件式はスキップされます。すなわち、解析式内の複数の条件式は if～elif の関係にあり、OR条件を表現することができます。

下記に簡単な例を示します。

```python
    chSCL = '$0'
    chSDA = '$1'
    thSCL = [3.3 * 0.3, 3.3 * 0.7]
    thSDA = [3.3 * 0.3, 3.3 * 0.7]
    tas = [
        [   #全データポイント解析
        ['SCL', [3, None], [
            [[['>', 'SCL', 1], ['<', chSCL, thSCL[0]]], ['=', 0]]
            ,[[['<', 'SCL', 2], ['>', chSCL, thSCL[1]]], ['=', 3]]
            ,[[['==', 'SCL', 0], ['>', chSCL, thSCL[0]]], ['=', 1]]
            ,[[['==', 'SCL', 3], ['<', chSCL, thSCL[1]]], ['=', 2]]
        ]]
        ,['SDA', [3, None], [
            [[['>', 'SDA', 1], ['<', chSDA, thSDA[0]]], ['=', 0]]
            ,[[['<', 'SDA', 2], ['>', chSDA, thSDA[1]]], ['=', 3]]
            ,[[['==', 'SDA', 0], ['>', chSDA, thSDA[0]]], ['=', 1]]
            ,[[['==', 'SDA', 3], ['<', chSDA, thSDA[1]]], ['=', 2]]
        ]]
        ]
        ,[  #イベント有データポイントのみ解析
        ['tR_SCL', [0, None], [ #SCL rise time
            [[['==', '!SCL', 1], ['==', 'SCL', 3], ['==', ['SCL',-2], 1]], ['-', '@', ['@SCL',-2]]]
            ,[[['==', '!SCL', 1], ['==', 'SCL', 3]], ['-', '@', '@']]
        ]]
        ,['tF_SCL', [0, None], [ #SCL fall time
            [[['==', '!SCL', 1], ['==', 'SCL', 0], ['==', ['SCL',-2], 2]], ['-', '@', ['@SCL',-2]]]
            ,[[['==', '!SCL', 1], ['==', 'SCL', 0]], ['-', '@', '@']]
        ]]
    ]
 ```

この例では、SCLを0番目の波形、SDAを1番目の波形に割り当てています。信号レベルを3.3Vとし、下側閾値を3.3V * 0.3 = 0.99V、上側閾値を3.3V * 0.7 = 2.31V としています。
最初の解析式'SCL'は、0,1,2,3の4種類の状態を持ち、Lowレベルを0、Highレベルを3、Lowレベルから下側閾値を超えて上側閾値を超えていない状態を1、Highレベルから上限閾値を下回り下側閾値を下回っていない状態を2としています。'SDA'も同様です。
全データポイント解析するのはこの2つだけで、後は、最初のデータポイントと、'SCL'または'SDA'のイベントが発生したデータポイントのみ解析します。

'tR_SCL'は SCL rise time の解析式です。'SCL'が1から3になったとき、現在時刻(SCLが上側閾値を上回った)から前回の'SCL'遷移時刻(SCLが下側閾値を上回った)を引き算して'tR_SCL'の値とします。
もしくは、'SCL'がそれ以外の値(0)からいきなり3になったとき、現在時刻から同じ現在時刻を引き算して(すなわち0)'tR_SCL'の値とします。これは、オシロスコープの1サンプリング分以下の時間で下側閾値と上側閾値を一度に超えてしまったということを示しています。ここで、0を代入せずに現在時刻から同じ現在時刻を引き算しているのは、後で解析結果を表示するときに処理を複雑化しないためです。
'tF_SCL'は SCL fall time の解析式で、'tR_SCL'とは逆の遷移を見ています。

実際のI2C用の解析設定では、以降にさらに多数の解析式があります。

## 解析

先ほど初期化した TimingAnalyzer インスタンスの datainメソッドに1データポイント分のデータを与えます。

```python
        ta.datain(tm, dt)
```
ここで、 tm は時刻(実数:秒単位)、dtは全チャネル分のデータ(実数:通常は電圧値)を含むリストです。
オシロスコープのデータを読み込んで、datainメソッドをデータポイント数分ループするのですが、100万ポイントともなると処理に何秒かかかるので、1万ポイントごとに進捗表示を出すようにしています。

```python
with open(srcFile, 'rb') as f:
    srcDat = f.read()
dat0 = vdsDat(srcDat)
timestep = 20 * dat0.tbScale / dat0.chI[0].samplesPerScreen
xmin = timestep * (dat0.chI[0].samplesPerScreen * dat0.tbOffset / 1000  - dat0.chI[0].sizDat / 2)
xmax = timestep * dat0.chI[0].sizDat + xmin
tim = np.arange(xmin, xmax, timestep)
dat = []
for chi, chd in zip(dat0.chI, dat0.chDat):
    dat.append((np.array(chd) - chi.vOffset) * chi.VPP)

tdat = np.array(dat).T
dlen = len(tdat)
for idx in range(0,dlen,10000):
    to = idx + 10000
    if to > dlen: to = dlen
    print('\r', to, '/', dlen, end='')
    for tm, dt in zip(tim[idx:to], tdat[idx:to]):
        ta.datain(tm, dt)
```

タイミング解析の結果はTimingAnalyzerのインスタンス変数hvに、その元となる時刻情報はhgに辞書形式で格納されています。辞書の値はリストになっており、例えば、'tHD_STA' (Start condition hold time) が3回発生していたら下記のようになります。
```python
ta.hv['tHD_STA']
[7.799999999999999e-07, 7.799999999999959e-07, 7.800000000000027e-07]
ta.hg['tHD_STA']
[[1.6e-06, 8.2e-07], [4.04e-05, 3.9620000000000004e-05], [9.84e-05, 9.762e-05]]
```

なお、これらの値には1e-20以下程度の誤差が含まれていますが、解析のために与えた時刻情報の量子化誤差に起因するものなので、上記例の3つの結果値は全て7.80e-7秒、すなわち780nsということになります。
また、発生回数が0回なら辞書の値は空のリストになります。例えば、解析対象波形がI2Cの1フレームだけであれば、'tBUF' (Bus free time) は発生しないので、ta.hv['tBUF'] も ta.hg['tBUF'] も空のリストとなります。

解析結果の良否を判定するには、これらの値が規格値の下限と上限に対してどれくらい余裕があるのかを見るということになります。しかしそれだけではなく、多くの場合、実際の信号波形の該当部分を拡大表示して確認するとともに資料に残します。

[次回](https://qiita.com/nachtele/items/52c08ee9e5366bba3279)は、解析結果の資料(一覧表、波形画像)出力について解説します。