ツールの作り方 – 通信編/MCプロトコル

ツール

今回は三菱MCプロトコルの3Eフレームをベースに送受信データを説明します。
※他のフレーム(1C~4C、1E、4E)については特に説明しません。
※MCプロトコルのマニュアルが手元にないと理解できないので前回を参考に入手してください。

伝文フォーマットの確認

送信データ(PC→PLC)

No項目備考
1サブヘッダ5000固定値、フレームによって異なる
2ネットワーク番号0
1~239
自局
他局
3PC番号FF
01~EF,FE
自局
他局
4要求先ユニットI/O番号3FF
3D0~3D3
3E0~3E3
000~1FF
自局CPU
制御/待機CPU
マルチCPU
マルチドロップ接続上のC24の管理CPU
5要求先ユニット局番号00
00~1F
マルチドロップ以外
マルチドロップ接続上の局
6要求データ長**No7~No10までのデータ長
7CPU監視タイマ0
1~65535
処理完了まで無限待ち
処理完了まで値×250msの待ち
8コマンド0401
1401 など
読出・書込等のコマンド
9サブコマンド0000
0001 など
コマンドによって異なる
iQ-Rシリーズの場合はQ/Lシリーズと異なる
10要求データ部****コマンドに付随するデータ
デバイスや書込値など

No2~No5については基本的に自局にアクセスするパターンと思うので上段側の値を使います。他局(PLC間のネットワーク越し)へアクセスする必要がある場合はマニュアルを参考に適切な値を設定してください。おそらく1~2回マニュアルを読むだけではよく分からないかもしれませんが。。

No7の監視タイマは適切な値を入れる必要があります。マニュアルによると自局は0.25~10秒(0x0001~0x0028)、他局は0.5~60秒(0x0002~0x0F0)が推奨されています。
通信処理で何かしらのタイムアウト監視をする場合はその時間よりも長くしないと意味がなくなるので注意しましょう。

受信データ(PLC→PC)

No項目備考
1サブヘッダD000固定値、フレームによって異なる
2ネットワーク番号0(アクセス局)
3PC番号FF(アクセス局)
4要求先ユニットI/O番号3FF(アクセス局)
5要求先ユニット局番号00(アクセス局)
6応答データ長**No7+No8のデータ長
7終了コード****正常時は0000
8応答データ部****【正常時】
・読出データ
・書込の場合は応答データが無い
【異常時】
・(応答局)ネットワーク番号
・(応答局)PC番号
・要求先ユニットI/O番号
・要求先ユニット局番号
・コマンド
・サブコマンド

No2~No5については無視して構いません。
重要なのはNo7の終了コードを必ずチェックすることです。正常時は0が返され、異常時はエラーコードが返されます。ただしエラーコードの詳細はマニュアルのたらい回しになるので注意してください。(CPUかEthernetユニット)

ちなみに伝文フォーマットの中には”ヘッダ”と称した部分が先頭にあります。

TCP/IP,UDP/IP用のヘッダです。要求伝文のヘッダは,外部機器側で付加して送信します。なお,通常は外部機器によって自動的に付加されます。応答伝文のヘッダはE71によって自動的に設定されます。

MCプロトコルのマニュアル(SH080003ad.pdf)より

Ethernetの通信ではユーザが意識しなくてもよい部分にいくつかのプロトコルが階層的に集まって通信が出来る仕組みになっています。ユーザの知らないところで自動的に何かしらのデータが付加されて送信されることになるのですが、この自動的に付加されるデータ=マニュアルでいうヘッダのことです。

初めてマニュアルの上記文面を読んだときに”何を言っているのか良く分からない”と思いましたが、、
要は華麗にスルーしてOKです。

コマンドとデバイスの指定

コマンドとサブコマンド

※デバイスの読み書きを抜粋

種別コマンドサブコマンド
(Q/Lシリーズ)
サブコマンド
(iQ-Rシリーズ)
ワード単位 一括読出040100000002
ビット単位 一括読出040100010003
ワード単位 一括書込140100000002
ビット単位 一括書込140100010003
ワード単位 ランダム読出040300000002

デバイス指定について

※よく使いそうなものを抜粋

デバイス表記Ascii
(Q/L)
Binary
(Q/L)
Ascii
(iQ-R)
Binary
(iQ-R)
特殊リレー10進SM0x91SM**0x0091
特殊レジスタ10進SD0xA9SD**0x00A9
入力16進X*0x9CX***0x009C
出力16進Y*0x9DY***0x009D
内部リレー10進M*0x90M***0x0090
ラッチリレー10進L*0x92L***0x0092
アナンシェータ10進F*0x93F***0x0093
リンクリレー16進B*0xA0B***0x00A0
データレジスタ10進D*0xA8D***0x00A8
リンクレジスタ16進W*0xB4W***0x00B4
タイマ 接点10進TS0xC1TS**0x00C1
タイマ コイル10進TC0xC0TC**0x00C0
タイマ 現在値10進TN0xC2TN**0x00C2
カウンタ 接点10進CS0xC4CS**0x00C4
カウンタ コイル10進CC0xC3CC**0x00C3
カウンタ 現在値10進CN0xC5CN**0x00C5
リンク特殊リレー16進SB0xA1SB**0x00A1
リンク特殊レジスタ16進SW0xB5SW**0x00B5
ファイルレジスタ(ブロック)10進R*0xAFR***0x00AF
ファイルレジスタ(連番)16進ZR0xB0ZR**0x00B0

【Q/Lシリーズの場合】
Ascii → 2文字のデバイスコード(種別)と6文字のデバイス番号
Binary → 1Byteのデバイスコード(種別)と3Byteのデバイス番号

【iQ-Rシリーズの場合】
Ascii → 4文字のデバイスコード(種別)と8文字のデバイス番号
Binary → 2Byteのデバイスコード(種別)と4Byteのデバイス番号

【デバイス点数の指定】
Ascii → 16進4文字で指定
Binary → 2Byteで指定

デバイス読出

通信のフォーマット、コマンド、デバイス指定の方法が分かれば後はコードに落とし込めばOKです。
例としてBinaryでD1000から100点読み出す場合のコードです。

    ' 送受信バッファと変数間のコピーで使うAPI。ByRefにしてポインタ渡し。VBAやVB6.0以前だと"**** As Any"という型が使えたけどVB.net以降はAnyが使えない。
    Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (ByRef Destination As Byte, ByRef Source As Short, ByVal length As Integer)
    Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (ByRef Destination As Short, ByRef Source As Byte, ByVal length As Integer)
    Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (ByRef Destination As Byte, ByRef Source As Integer, ByVal length As Integer)
    Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (ByRef Destination As Integer, ByRef Source As Byte, ByVal length As Integer)

    Private Sub ReadSample()
        Dim tx() As Byte            ' 送信バッファ
        Dim idx As Integer          ' 送信バッファ アクセスIndex
        Dim lenIdx As Integer       ' 要求データ長 格納Index
        Dim reqLen As Short         ' 要求データ長(Byte単位)
        Dim sentLen As Integer      ' 送信Byte数

        Dim cmd As Short = &H401    ' コマンド(読出)
        Dim scmd As Short = &H0     ' サブコマンド(ワード単位)
        Dim devTyp As Byte          ' デバイスコード
        Dim devNum As Integer       ' デバイス番号
        Dim readLen As Short        ' 読出点数

        Dim iNet As Integer = 0     ' PC番号
        Dim iPC As Integer = &HFF   ' ネットワーク番号
        Dim iIO As Integer = &H3FF  ' 要求先ユニットI/O番号
        Dim iSTN As Integer = 0     ' 要求先ユニット局番号
        Dim iCPU As Integer = 20    ' CPU監視タイマ


        ' 送信データ
        ReDim tx(4095)  ' バッファサイズは適当に。
        idx = 0

        ' サブヘッダ
        tx(idx) = CByte(&H50)
        tx(idx + 1) = CByte(&H0)
        idx += 2

        ' ネットワーク番号 *1Byte
        CopyMemory(tx(idx), iNet, 1)
        idx += 1
        ' PC番号 *1Byte
        CopyMemory(tx(idx), iPC, 1)
        idx += 1
        ' 要求先ユニットI/O番号 *2Byte
        CopyMemory(tx(idx), iIO, 2)
        idx += 2
        ' 要求先ユニット局番号 *1Byte
        CopyMemory(tx(idx), iSTN, 1)
        idx += 1
        ' 要求データ長(CPU監視タイマ~要求データ部のバイト数)
        lenIdx = idx
        reqLen = 12     ' CPU監視(2)+コマンド(2)+サブコマンド(2)+デバイス(4)+点数(2)で計12Byte
        CopyMemory(tx(idx), reqLen, 2)
        idx += 2
        ' CPU監視タイマ(250ms単位) *2Byte
        CopyMemory(tx(idx), iCPU, 2)
        idx += 2

        ' コマンド *2Byte
        CopyMemory(tx(idx), cmd, 2)
        idx += 2
        ' サブコマンド *2Byte
        CopyMemory(tx(idx), scmd, 2)
        idx += 2

        ' デバイス *4Byte
        devTyp = &HA8   ' A8 = D
        devNum = 1000
        CopyMemory(tx(idx), devNum, 4)  ' デバイス番号は3Byteだが面倒なので4Byteコピー
        tx(idx + 3) = devTyp            ' 最上位Byteにデバイスコード(1Byte)を上書き
        idx += 4
        ' 読出点数 *2Byte
        readLen = 100
        CopyMemory(tx(idx), readLen, 2)
        idx += 2

        ' 送信
        ' _sockはSocketクラスで既にConnectされているものとする
        sentLen = _sock.Send(tx, idx, SocketFlags.None)



        Dim rx() As Byte            ' 受信バッファ
        Dim recvLen As Integer      ' 受信Byte数
        Dim resLen As Short         ' 応答データ長
        Dim endCode As Short        ' 終了コード
        Dim readData() As Short     ' 読出データ(デバイス値)
        Dim i As Integer

        ' 受信
        ReDim rx(4095)  ' バッファサイズは適当に。
        recvLen = _sock.Receive(rx)

        ' チェック
        If recvLen < 12 Then Return ' 12Byte未満の受信はそもそもNG。
        ' サブヘッダ
        idx = 0
        If rx(idx) <> &HD0 OrElse rx(idx + 1) <> 0 Then Return
        idx += 2
        ' ネットワーク番号1Byte + PC番号1Byte + 要求先ユニットI/O番号2Byte + 要求先ユニット局番号1Byteは無視
        idx += 5
        ' 応答データ長
        CopyMemory(resLen, rx(idx), 2)
        resLen -= 2S    ' 終了コード(2Byte)分を差し引いておく
        idx += 2
        ' 終了コード
        CopyMemory(endCode, rx(idx), 2)
        idx += 2
        If endCode <> 0 Then Return ' 正常時は0

        ' 応答データ部=読出データ
        If resLen <> readLen * 2 Then Return ' 応答データ(Byte数)は読出点数×2と一致しないとNG
        ReDim readData(readLen - 1)
        For i = 0 To readLen - 1
            CopyMemory(readData(i), rx(idx), 2)
            idx += 2
        Next

    End Sub

ポイントは大きく2つあります。

1つはCopyMemoryとして定義しているRtlMoveMemoryというAPIです。
送受信バッファはByte型配列のためShort型やInteger型の値で直接アクセス出来ません。そこでこのAPIを使うことにより変数間の値をサクッとコピー(格納)出来るようになります。このようなAPIを使わないとおそらく簡潔にコードを記述できないと思います。(APIを使わない場合はBitConverterクラスを使うことになるハズですが、送信データ作成部分の記述が煩雑になるかと。。)

もう1つはマルチバイトデータ(2,4,8などの複数Byte)の扱いです。
一般的にBinary通信におけるマルチバイトデータの場合は(明示されないかぎり)上位Byteから順に扱うのが暗黙の了解です。(俗にいうビッグエンディアンというヤツ)
ただしMCプロトコルではよくよくマニュアルを読むとリトルエンディアンであることが明記されているのが分かります。(“バイトオーダー”や”エンディアン”という単語の記載は無いですが。)
Windows内部ではマルチバイトデータをリトルエンディアンで扱うのでShort型(2Byte)やInteger型(4Byte)の値をそのままコピーすれば都合よく送受信データと合致することになります。

また上記サンプルについての注意点が3つあります。

1つ目は1回の交信(送信&受信)において読出は最大960点(ワード)という制約があります。(MCプロトコル的に。フレームや通信経路によっても最大数が変わる。ランダム読出だと192とか。)
そのため”10000点読み出したい”などの場合は分割して読出処理を実行しなければなりません。通信処理を呼び出すアプリケーション側でその対応をするよりは通信処理側で対応するほうが使い勝手が良くなります。

2つ目はデバイス指定についてです。上記サンプルではいきなりデバイスコードの値を代入していますが、デバイス(文字列が無難)と点数は引数として与えデバイスに応じたコードを返すメソッドを追加しないと使い物になりません。。

3つ目は環境の問題(?)です。手元にあったVisual Studio 2008を使っていますが、コンパイル設定を変えないと正常動作しないので注意してください。
具体的には下図のようにターゲットCPUをAny→x86に変えておく必要があります。(特に64bit OSの場合は変更しないとダメです。。 Visual Studio 2010以降の環境でもおそらく同じことが起きると推測。)

VBのコンパイラ設定
C#のコンパイラ設定

デバイス書込

例としてBinaryでM1000をSET(1)する場合のコードです。

    ' 送受信バッファと変数間のコピーで使うAPI。ByRefにしてポインタ渡し。VBAやVB6.0以前だと"**** As Any"という型が使えたけどVB.net以降はAnyが使えない。
    Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (ByRef Destination As Byte, ByRef Source As Short, ByVal length As Integer)
    Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (ByRef Destination As Short, ByRef Source As Byte, ByVal length As Integer)
    Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (ByRef Destination As Byte, ByRef Source As Integer, ByVal length As Integer)
    Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (ByRef Destination As Integer, ByRef Source As Byte, ByVal length As Integer)

    Private Sub WriteSample()
        Dim tx() As Byte            ' 送信バッファ
        Dim idx As Integer          ' 送信バッファ アクセスIndex
        Dim lenIdx As Integer       ' 要求データ長 格納Index
        Dim reqLen As Short         ' 要求データ長(Byte単位)
        Dim sentLen As Integer      ' 送信Byte数

        Dim cmd As Short = &H1401   ' コマンド(書込)
        Dim scmd As Short = &H1     ' サブコマンド(ビット単位)
        Dim devTyp As Byte          ' デバイスコード
        Dim devNum As Integer       ' デバイス番号
        Dim writeLen As Short       ' 書込点数

        Dim iNet As Integer = 0     ' PC番号
        Dim iPC As Integer = &HFF   ' ネットワーク番号
        Dim iIO As Integer = &H3FF  ' 要求先ユニットI/O番号
        Dim iSTN As Integer = 0     ' 要求先ユニット局番号
        Dim iCPU As Integer = 20    ' CPU監視タイマ


        ' 送信データ
        ReDim tx(4095)  ' バッファサイズは適当に。
        idx = 0

        ' サブヘッダ
        tx(idx) = CByte(&H50)
        tx(idx + 1) = CByte(&H0)
        idx += 2

        ' ネットワーク番号 *1Byte
        CopyMemory(tx(idx), iNet, 1)
        idx += 1
        ' PC番号 *1Byte
        CopyMemory(tx(idx), iPC, 1)
        idx += 1
        ' 要求先ユニットI/O番号 *2Byte
        CopyMemory(tx(idx), iIO, 2)
        idx += 2
        ' 要求先ユニット局番号 *1Byte
        CopyMemory(tx(idx), iSTN, 1)
        idx += 1
        ' 要求データ長(CPU監視タイマ~要求データ部のバイト数)
        lenIdx = idx
        reqLen = 13     ' CPU監視(2)+コマンド(2)+サブコマンド(2)+デバイス(4)+点数(2)+書込データ(1)で計13Byte
        CopyMemory(tx(idx), reqLen, 2)
        idx += 2
        ' CPU監視タイマ(250ms単位) *2Byte
        CopyMemory(tx(idx), iCPU, 2)
        idx += 2

        ' コマンド *2Byte
        CopyMemory(tx(idx), cmd, 2)
        idx += 2
        ' サブコマンド *2Byte
        CopyMemory(tx(idx), scmd, 2)
        idx += 2

        ' デバイス *4Byte
        devTyp = &H90   ' 90 = M
        devNum = 1000
        CopyMemory(tx(idx), devNum, 4)  ' デバイス番号は3Byteだが面倒なので4Byteコピー
        tx(idx + 3) = devTyp            ' 最上位Byteにデバイスコード(1Byte)を上書き
        idx += 4
        ' 書込点数 *2Byte
        writeLen = 1
        CopyMemory(tx(idx), writeLen, 2)
        idx += 2
        ' 書込データ *1Byte
        tx(idx) = 1     ' SET=1, RST=0
        idx += 1

        ' 送信
        ' _sockはSocketクラスで既にConnectされているものとする
        sentLen = _sock.Send(tx, idx, SocketFlags.None)



        Dim rx() As Byte            ' 受信バッファ
        Dim recvLen As Integer      ' 受信Byte数
        Dim resLen As Short         ' 応答データ長
        Dim endCode As Short        ' 終了コード

        ' 受信
        ReDim rx(4095)  ' バッファサイズは適当に。
        recvLen = _sock.Receive(rx)

        ' チェック
        If recvLen < 12 Then Return ' 12Byte未満の受信はそもそもNG。
        ' サブヘッダ
        idx = 0
        If rx(idx) <> &HD0 OrElse rx(idx + 1) <> 0 Then Return
        idx += 2
        ' ネットワーク番号1Byte + PC番号1Byte + 要求先ユニットI/O番号2Byte + 要求先ユニット局番号1Byteは無視
        idx += 5
        ' 応答データ長
        CopyMemory(resLen, rx(idx), 2)
        resLen -= 2S    ' 終了コード(2Byte)分を差し引いておく
        idx += 2
        ' 終了コード
        CopyMemory(endCode, rx(idx), 2)
        idx += 2
        If endCode <> 0 Then Return ' 正常時は0

        ' 応答データ部=書込の場合は無し

    End Sub

ポイントや注意点は読出とほぼ同様です。
上記はビット1点書込ですがワード単位書込でも交信1回の最大数に制約があります。(詳細はマニュアルを参照ください)

ちなみに今回はBinary通信のサンプルにしていますがAsciiでも可能です。その場合は文字列変数に伝文を格納して1文字ずつByteに変換するのが手っ取り早いと思います。
ただ、、マニュアルにも記載されていますがAsciiよりもBinaryのほうが送受信するデータが約半分になるので速度的に有利です。

また今回の紹介したものはあくまでサンプルなので実践的なものにははほど遠いです。。
通信処理のインターフェースはMX Componentを参考にすると”なるほど!”と思える部分が多いです。興味があればマニュアルをダウンロードしてみることをオススメします。

PLC側の通信設定

三菱の場合はCPU内蔵Ethernetと単体のEtherntユニットで機能・設定が少し異なります。

CPU内蔵EthernetでMCプロトコルを使う場合の設定

  • IPアドレス
    任意値を設定します。192.168.***.***あたりを使うパターンが一般的に多いです。
  • サブネットマスクパターン
    デフォルト(未入力)のままでOKです。入力した場合はデフォルトルータIPアドレスも入力しなければなりません。
  • 交信データコード設定
    バイナリかASCIIのどちらかを選択します。どちらでもOKですがオススメはバイナリです。
  • RUN中書込を許可する(FTPとMCプロトコル)
    MCプロトコルで書込をしたい場合はチェックを入れます。チェックを外すとデバイスへの値書込が出来なくなります。
  • オープン設定
    TCP/UDPを設定します。詳細は下記。
  • その他
    他の設定はMCプロトコルを使う上では関係がありません。
  • TCPの場合
    上図1、2のように設定します。
  • UDPの場合
    上図3、4のように設定します。
  • 自局ポート番号
    PLC側のポート番号です。1025~4999、5010~65534で任意です。
    ※同じポート番号を指定することも可
  • その他
    使用したい通信回線分だけ設定する必要があります。
    上図の設定例の場合PC側からTCPで接続した場合、2個目まではOKですが3個目以降はConnect出来ません。

EthernetユニットでMCプロトコルを使う場合の設定

  • 先頭I/O No.
    PCパラメータ(I/O割付設定)に合わせた値
  • ネットワークNo.
    1~239で任意
  • 局番
    1~64で任意
  • 交信データコード設定
    バイナリかASCIIのどちらかを選択します。どちらでもOKですがオススメはバイナリです。
  • イニシャルタイミング設定
    どちらでも良いですが常にOPEN待ちがベターです。
  • IPアドレス設定
    任意値を設定します。192.168.***.***あたりを使うパターンが一般的に多いです。
  • RUN中書込を許可する
    MCプロトコルで書込をしたい場合はチェックを入れます。チェックを外すとデバイスへの値書込が出来なくなります。
  • 送信フレーム設定
    Ethernet(V2.0)を選択します。
  • TCP生存確認設定
    どちらでもOKです。
  • TCPの場合
    上図1のように設定します。(生存確認はどちらでも良い)
  • UDPの場合
    上図3のように設定します。(生存確認はどちらでも良い)
    交信相手(PC側)のIPアドレスとポート番号を決める必要があります。
    ※UDPはココで決めうち設定する必要があるのでイマイチ使いづらい
     →通常PCのソケット通信で使うポート番号はOS任せに空いているものを使うのが一般的なのでポートを指定するのが面倒
    ※未確認だが交信相手を”192.168.1.255″、ポート65535とするのが良いかも?
     交信相手が一部任意になる??誰か知っていたら教えてください(笑)
  • その他
    使用したい通信回線分だけ設定する必要があります。

コメント

  1. ryu より:

    https://qiita.com/pcyuki1990/items/0fb937efcef22d2807f0

    こんなものもあるようですが

    • さきちゃん さきちゃん より:

      情報ありがとうございます!
      ちょっとサイトを見ただけですが結構な力作だと思います。使い勝手も良さそうな感じがしますね。
      以前からフリーのPLC用通信ライブラリはいくつかあったもののCOMやアドインのようなものが多く、イマイチ使いづらいと思ってました。(結局目的に合わず自前の通信処理を作りましたが。。)
      いやー、いつか私もなんちゃって通信ライブラリでも作って公開しようと考えてましたが、出遅れましたね(^^;

  2. kitty より:

    本家本元は確かに世界最高水準の技術力を持ったメーカーなんでしょうが、

    「特定の会社のフレームワークに依存しない」という方針がたぶんおありでしょうから、基本的に長期保守ソフトはC++で、

    あるいはソケットもバークレーソケットじゃなくて、生で書いてるかもしれませんねえ・・

    そういう意味で開発コストというのは単純機能であっても尋常じゃなさそう。。

    そういうのに囚われない、.NET Framework とかふつうに使える立場の人間は気楽でよいです。

    ちなみにライブラリ、ソフトの性質上でしょうが、なんか産業カメラのメーカから質問のメールがあって、需要はあるようです。

    実際に使われるとは思ってませんが、需要の可能性がある限りは、暇を見つけて少しずつ充実しようかなと思ってます。

    • さきちゃん さきちゃん より:

      ライブラリの類はやはりC/C++系というイメージがついて回りますね。。
      最初通信処理をまとめるときにベストな形は何か?ということを考えたらC/C++と思ってました。
      でもよくよく考えるとイマドキC/C++を普通に使えるような人ならばライブラリを探す前に作ってる気もします。。
      それは置いといて、、やはり.Net FrameworkやWindowsに毒された環境は楽です。(いろいろ積み重なると面倒くさいのは否めませんが。)
      PLCは産業用のモノなので関連するものもやはり一般向けとは異なるので思いもよらないところから需要があるかもしれませんね。