オシロスコープをExcelで制御して波形データを取得する

## 1. はじめに
この記事は[オシロスコープをExcelで制御して測定値とスクリーンショットを取得する](https://qiita.com/pbjpkas/items/5cb8034c43ccb6e4146f)の続編です。オシロスコープに :WAVeform:DATA? コマンドを発行して波形データを取得できるよう改修しました。

## 2. 波形データの取得
以下のコマンドを使用します。コマンドの説明は[RIGOL DS1104Z](https://jp.rigol.com/products/oscillosopes/ds1000z.html)シリーズのものを参照しているため機種によって異なる可能性があります。

- :WAVeform:SOURce
- :WAVeform:MODE
- :WAVeform:FORMat
- :WAVeform:STARt
- :WAVeform:STOP
- :WAVeform:DATA?

### 2.1 :WAVeform:SOURce
読み出す波形データのデータソースを指定します。
例）:WAVeform:SOURce CHAN1

?を付けて発行すると設定されているデータソースを返却します。
例）:WAVeform:SOURce? → CHAN1

### 2.2 :WAVeform:MODE
NORMal、MAXimun、RAWのいずれかを指定します。
例）:WAVeform:MODE NORMal

- NORMal：ディスプレイに表示されている波形データを読み出します。
- RAW：オシロのメモリ内のデータを読み出します。
- MAXimum：実行状態ではディスプレイに表示されている波形データを、停止状態ではメモリ内のデータを読み出します。
- データソースがMATHの場合はNORMalのみ有効。
- RAWは停止状態のときのみ有効。
- 後述の:WAVeform:DATAコマンドは取得できるデータ量に制限があるため一回で読み出せない場合は:WAVeform:STARt、:WAVeform:STOPで読み出す区間を指定しながら繰り返しデータを取得します。

本稿ではNORMalを指定します。

### 2.3 :WAVeform:FORMat
WORD、BYTE、ASCiiを指定します。
例）:WAVeform:MODE ASCii

- WORD：1個の波形データをWORD(16bit)で読み取ります。Lowerの8bitが有効で、Upperの8bitは0です。
- BYTE：1個の波形データをBYTE(8bit)で読み取ります。
- ASCii：1個の波形の実際の電圧を読み取ります。波形データ同士はカンマで区切られます。

本稿ではASCiiを指定します。

### 2.4 :WAVeform:STARt
読み出す波形データの起点を指定します。初期値は1です。
例）:WAVeform:STARt 1

- NORMal：1～1200の値
- MAX：1～現在の画面上の有効ポイント数
- RAW：1〜現在の最大記憶容量

本稿では1を指定します。

### 2.5 :WAVeform:STOP
読み出す波形データの終点を指定します。初期値は1200です。
例）:WAVeform:STOP 1200

- NORMal：1～1200の値
- MAX：1～現在の画面上の有効ポイント数
- RAW：1〜現在の最大記憶容量

本稿では1200を指定します。

### 2.6 :WAVeform:DATA?
SOURceで指定されたデータソースの波形データを、指定されたモード、区間、フォーマットで読み出します。

波形データはTMC[^1]データ記述子ヘッダと波形データの2つの部分で構成されます。
- TMCデータ記述子ヘッダ
  - #Nxxxxxxxxxの形式
  - #：識別子
  - N：Length Blockのバイト数(9)
  - xxxxxxxxx：Length Block(波形データのバイト数を格納するブロック)
- 波形データ
  - 波形の各ポイントの電圧を","で区切って羅列（FORMatがASCiiの場合）

例）#9000015895-3.874302e-07,3.999961e-02,3.999961e-02,-3.874302e-07, ... ,2.919997e+00
※TMCデータ記述子ヘッダより、波形データは15895バイトと分かる。

本稿ではCHAN1の波形データをNORMalモード、1～1200の区間、ASCii形式で読み出します。

## 3. 動作
あらかじめオシロスコープのCH1にプローブ補償信号（1kHzの矩形波信号）を入力して動作させたときの例を以下に示します。
![Excelの画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/6c95b96a-acc7-3725-ca22-abf7b8be83ff.png)

- 15行目でデータソースを指定しています。
- 16行目でスクリーンショットを取得しています。
- 17行目で波形データを読み出しています。TMCデータ記述子ヘッダの情報をC列に格納しています。波形データは「ファイル名-シート名-受信データセル座標.csv」というファイルに保存します。
![export-files.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/e0fc5610-c12b-9eec-b957-ae5be8a0742a.png)

### 3.1 スクリーンショットと読み出したデータに基づくグラフ
スクリーンショットと読み出したデータに基づくグラフを以下に示します。

![GPIB-WavData-WAVeformData2-16-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/22ef76ca-0243-a3d7-c2d5-d43f191b1a3f.png)
![Excelグラフ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/35916db5-1ad0-7fbf-8884-a11bf93b6420.png)

- 波形データのファイルの一行目にデータソースとタイムベースのスケールを自動で付加しています。
- グラフは保存されたcsvファイルをExcelで開いて散布図で描画したものです。グラフのタイトルは1行目のデータがそのまま反映されています。

## 4. ソースコード（波形データ読み出し部分）
ソースコード全文は付録をご覧ください。
- モード、フォーマット、区間を設定します。
- データソース、タイムベーススケールを取得します。
- 波形データを読み出し、TMCデータ記述子ヘッダの情報をExcelのセルへ、波形データをファイルへ保存します。

```vb:ReadWaveformData()
Function ReadWaveformData(Line As Long, StrTxd As String, ColRxd As Long, DSO As VisaComLib.FormattedIO488)
    DSO.WriteString (":WAVeform:MODE NORMal")
    DSO.WriteString (":WAVeform:FORMat ASCii")
    DSO.WriteString (":WAVeform:STARt 1")
    DSO.WriteString (":WAVeform:STOP 1200")
    
    Dim Channel As String
    DSO.WriteString (":WAVeform:SOURce?")
    Channel = DSO.ReadString()
    Channel = Left(Channel, Len(Channel) - 1) '末尾の改行コードを捨てる
    
    Dim TimebaseScale As String
    DSO.WriteString (":TIMebase:SCALe?")
    TimebaseScale = DSO.ReadString()
    
    'WaveformDataはTMCデータ記述子ヘッダ(TMC data description header)と波形データの2つのパートで構成される。
    'TMCデータ記述子ヘッダ:
    '  #Nxxxxxxxxxの形式
    '  #:識別子
    '  N:Length Blockのバイト数(9)
    '  xxxxxxxxx:Length Block(波形データのバイト数を格納するブロック)
    '波形データ
    '  波形の各ポイントの電圧を、","で区切って羅列
    Dim WaveformData As String
    DSO.WriteString (":WAVeform:DATA?")
    WaveformData = DSO.ReadString()
        
    'TMCデータ記述子ヘッダ部分
    Dim TMCDataDescriptionHeader As String
    TMCDataDescriptionHeader = Left(WaveformData, 11)
    ActiveSheet.Cells(Line, ColRxd) = TMCDataDescriptionHeader
    
    '波形データ部分
    WaveformData = Replace(Mid(WaveformData, 12), ",", vbCr)
    
    'save Waveform Data to file
    Dim DataFileName As String
    DataFileName = GenerateDataFileName(Line, ColRxd) + ".csv"
    
    Dim DataString As String
    DataString = Channel + "/" + TimebaseScale + WaveformData
        
    Dim FreeFileNum As Long
    FreeFileNum = FreeFile
    
    Open DataFileName For Binary As #FreeFileNum
            Put #FreeFileNum, , DataString
    Close #FreeFileNum
End Function
```

## 5. おわりに
ひとまず波形データを自動で取得できるようになりました。シートを指定してそこに波形データを出力するように改修すると使い勝手が向上するものと思います。

## 付録A.参考資料
- [DS1000Z Programming Guide EN](https://jp.rigol.com/Jp/Index/listView/catid/27/tp/4/cat/7/xl/26)
- [DS1000Z Programming Guide CN](https://rigol.com/Images/DS1000Z_ProgrammingGuide_CN_tcm4-3028.pdf)

## 付録B.動作確認環境
- Windows 10 64bit 21H1
- Microsoft® Excel® 2016 MSO (バージョン 2204 ビルド 16.0.15128.20240) 32 ビット
- VISA-COM 5.9 Type Library
  - [Keysight IO Libraries Suite](https://www.keysight.com/find/iolib) 2018 Update 0.2に同梱されているもの
- [RIGOL DS1104Z](https://jp.rigol.com/products/oscillosopes/ds1000z.html)

## 付録C.ソースコード一式
10行目のLINE_PARSER_LOOPの値を変えると繰り返し実行の開始行を変えることができます。

```vb
Option Explicit

Private Declare Sub Sleep Lib "KERNEL32.dll" (ByVal dwMilliseconds As Long)

Const COL_PARSER_CMD = 1     'A列：パーサ用コマンドカラム
Const COL_PARSER_TXD = 2     'B列：パーサ用送信データカラム
Const COL_PARSER_RXD = 3     'C列：パーサ用受信データカラム

Const LINE_PARSER_START = 10 'パーサ用コマンド開始行
Const LINE_PARSER_LOOP = 20  'パーサ用ループ処理開始行

Const LINE_TABLE_START = 10  '測定テーブル用コマンド開始行

Const VISA_ADDR_DSO = "C6"   'DSO : Digital Storage Oscilloscope
Const VISA_ADDR_TB = "C7"    'TB  : Target Board

'「テスト開始」ボタン押下で呼出すマクロ
Sub Parser()
    Dim i As Long
    Dim LoopNum As Long
    Dim LoopNumMax As Long
    LoopNumMax = 1           'いったん1で設定(LOOPコマンドで上書き可)

    Dim StrCmd As String
    Dim StrTxd As String

    LoopNum = 0
    Do While LoopNum < LoopNumMax

        i = LINE_PARSER_START
        Do While ActiveSheet.Cells(i, COL_PARSER_CMD) <> ""

            If LoopNum >= 1 And i < LINE_PARSER_LOOP Then
                '処理をスキップする

            Else
                ActiveSheet.Cells(i, COL_PARSER_CMD).Select

                StrCmd = ActiveSheet.Cells(i, COL_PARSER_CMD)
                StrTxd = ActiveSheet.Cells(i, COL_PARSER_TXD)

                If InStr(StrCmd, "//") > 0 Then
                    '何もしない
                ElseIf StrCmd = "DSO" Then
                    Call ControlDSO(i, StrTxd, COL_PARSER_RXD)

                ElseIf StrCmd = "TB" Then
                    Call ControlTB(i, StrTxd, COL_PARSER_RXD)

                ElseIf StrCmd = "WAITMS" Then
                    Call SysWaitMs(StrTxd)

                ElseIf StrCmd = "MSGBOX" Then
                    MsgBox (StrTxd)

                ElseIf StrCmd = "LOOP" Then
                    If IsNumeric(StrTxd) = True Then
                        LoopNumMax = CInt(StrTxd)
                    Else
                        MsgBox ("unknown value")
                    End If

                ElseIf StrCmd = "TABLE" Then
                    Call SysTable(LoopNum, StrTxd)

                Else
                    MsgBox ("unknown command")

                End If

            End If

            i = i + 1
        Loop

        LoopNum = LoopNum + 1
    Loop
End Sub

Function SysTable(LoopNum As Long, ColTable As String)
    Dim i As Long
    Dim Column As Long
    Dim StrCmd As String
    Dim StrTxd As String

    i = LoopNum + LINE_TABLE_START

    Column = Range(ColTable + "1").Column
    ActiveSheet.Cells(i, Column).Select

    StrCmd = ActiveSheet.Cells(i, Column)
    StrTxd = ActiveSheet.Cells(i, Column + 1)

    If StrCmd = "DSO" Then
        Call ControlDSO(i, StrTxd, Column + 2)

    ElseIf StrCmd = "TB" Then
        Call ControlTB(i, StrTxd, Column + 2)

    End If

    i = i + 1

End Function

'Line:送信データが記述されているセルの行番号
'StrTxd:送信データ
'ColRxd:受信データの格納先セルの列番号
Function ControlDSO(Line As Long, StrTxd As String, ColRxd As Long)
    Dim VisaAddr As String
    VisaAddr = ActiveSheet.Range(VISA_ADDR_DSO)
    
    Dim RM As New VisaComLib.ResourceManager
    Dim DSO As New VisaComLib.FormattedIO488
    Set DSO.IO = RM.Open(VisaAddr)
    
    If InStr(StrTxd, ":DISPlay:DATA?") > 0 Then
        Call ReadDisplayData(Line, StrTxd, ColRxd, DSO)
    
    ElseIf InStr(StrTxd, ":WAVeform:DATA?") > 0 Then
        Call ReadWaveformData(Line, StrTxd, ColRxd, DSO)
        
    ElseIf InStr(StrTxd, "?") > 0 Then
        DSO.WriteString StrTxd
        ActiveSheet.Cells(Line, ColRxd) = Replace(DSO.ReadString(), vbLf, "")
    
    Else
        'MsgBox (StrTxd)
        DSO.WriteString StrTxd

    End If
    
    DSO.IO.Close
    Set DSO = Nothing
    Set RM = Nothing
End Function

'拡張子は呼び出し元で付けること
Function GenerateDataFileName(Line As Long, ColRxd As Long) As String
    Dim FilePath As String
    FilePath = ThisWorkbook.Path
    
    Dim FileName As String
    FileName = Left(ThisWorkbook.Name, Len(ThisWorkbook.Name) - 5)
    
    Dim SheetName As String
    SheetName = ActiveSheet.Name
    
    Dim DataFileName As String
    DataFileName = FilePath + "\" + FileName + "-" + SheetName + "-" + CStr(Line) + "-" + CStr(ColRxd)
    
    GenerateDataFileName = DataFileName
End Function
    
Function ReadDisplayData(Line As Long, StrTxd As String, ColRxd As Long, DSO As VisaComLib.FormattedIO488)
    DSO.WriteString StrTxd
    
    Dim ReadIEEEBlockData() As Byte
    ReadIEEEBlockData = DSO.ReadIEEEBlock(BinaryType_UI1)
    
    Dim DataFileName As String
    DataFileName = GenerateDataFileName(Line, ColRxd) + ".png"

    Dim FreeFileNum As Long
    FreeFileNum = FreeFile
    
    Open DataFileName For Binary As #FreeFileNum
            Put #FreeFileNum, , ReadIEEEBlockData
    Close #FreeFileNum
    
    With ActiveSheet.Pictures.Insert(DataFileName)
        .Top = Cells(Line, ColRxd).Top
        .Left = Cells(Line, ColRxd).Left
        .Height = Cells(Line, ColRxd).Height
    End With
End Function

Function ReadWaveformData(Line As Long, StrTxd As String, ColRxd As Long, DSO As VisaComLib.FormattedIO488)
    DSO.WriteString (":WAVeform:MODE NORMal")
    DSO.WriteString (":WAVeform:FORMat ASCii")
    DSO.WriteString (":WAVeform:STARt 1")
    DSO.WriteString (":WAVeform:STOP 1200")
    
    Dim Channel As String
    DSO.WriteString (":WAVeform:SOURce?")
    Channel = DSO.ReadString()
    Channel = Left(Channel, Len(Channel) - 1) '末尾の改行コードを捨てる
    
    Dim TimebaseScale As String
    DSO.WriteString (":TIMebase:SCALe?")
    TimebaseScale = DSO.ReadString()
    
    'WaveformDataはTMCデータ記述子ヘッダ(TMC data description header)と波形データの2つのパートで構成される。
    'TMCデータ記述子ヘッダ:
    '  #Nxxxxxxxxxの形式
    '  #:識別子
    '  N:Length Blockのバイト数(9)
    '  xxxxxxxxx:Length Block(波形データのバイト数を格納するブロック)
    '波形データ
    '  波形の各ポイントの電圧を、","で区切って羅列
    Dim WaveformData As String
    DSO.WriteString (":WAVeform:DATA?")
    WaveformData = DSO.ReadString()
        
    'TMCデータ記述子ヘッダ部分
    Dim TMCDataDescriptionHeader As String
    TMCDataDescriptionHeader = Left(WaveformData, 11)
    ActiveSheet.Cells(Line, ColRxd) = TMCDataDescriptionHeader
    
    '波形データ部分
    WaveformData = Replace(Mid(WaveformData, 12), ",", vbCr)
    
    'save Waveform Data to file
    Dim DataFileName As String
    DataFileName = GenerateDataFileName(Line, ColRxd) + ".csv"
    
    Dim DataString As String
    DataString = Channel + "/" + TimebaseScale + WaveformData
        
    Dim FreeFileNum As Long
    FreeFileNum = FreeFile
    
    Open DataFileName For Binary As #FreeFileNum
            Put #FreeFileNum, , DataString
    Close #FreeFileNum
End Function

Function ControlTB(Line As Long, StrTxd As String, ColRxd As Long)
    Dim i As Long
    Dim j As Long
    Dim StrVal As String
    Dim StrArray() As String
'    Dim DbgStr As String

    Dim VisaAddr As String
    VisaAddr = ActiveSheet.Range(VISA_ADDR_TB)

    Dim RM As New VisaComLib.ResourceManager
    Dim TB As New VisaComLib.FormattedIO488  'TB : Target Board

    Set TB.IO = RM.Open(VisaAddr)
    Dim Sfc As VisaComLib.ISerial            'RS232C通信設定
    Set Sfc = TB.IO
    Sfc.BaudRate = 115200
    Sfc.DataBits = 8
    Sfc.Parity = ASRL_PAR_NONE
    Sfc.StopBits = ASRL_STOP_ONE
    Sfc.FlowControl = ASRL_FLOW_NONE

    TB.IO.TerminationCharacter = 10
    TB.IO.TerminationCharacterEnabled = True

    TB.WriteString StrTxd

    StrVal = ""
    For i = 1 To 3                           'Write→ReadのあいだにSleepを入れる
        Sleep (10)
        DoEvents

        StrVal = StrVal + TB.ReadString()
        ReDim StrArray(1 To Len(StrVal))

        For j = 1 To UBound(StrArray)        '制御文字を削除する
            StrArray(j) = Mid(StrVal, j, 1)
            If Asc(StrArray(j)) <= 31 Or 127 <= Asc(StrArray(j)) Then
'                DbgStr = CStr(i) + "," + CStr(j) + "," + CStr(Asc(StrArray(j)))
'                MsgBox (DbgStr)
                StrArray(j) = ""
            End If
        Next j
        StrVal = Join(StrArray, "")

        If i <= 2 Then
            StrVal = StrVal + "/"
        End If

        If StrVal <> "" Then
            ActiveSheet.Cells(Line, ColRxd).Value = StrVal
            'Exit For
        End If
    Next

    TB.IO.Close
    Set TB = Nothing
    Set RM = Nothing
End Function

Function SysWaitMs(WaitMs As String)
    Dim i As Long
    Dim LoopNum As Long

    If IsNumeric(WaitMs) = True Then
        LoopNum = CInt(WaitMs) / 100
        For i = 1 To LoopNum
            Sleep (100)
            DoEvents
        Next
    Else
        MsgBox ("unknown value")
    End If
End Function
```

[^1]: USB TMC(Universal Serial Bus Test & Measurement Class)