三菱PLCとイーサネット通信

またまた久しぶりの更新になってしまいました。今回のテーマは三菱PLCとイーサネット通信。将来的にはRaspberry Piなどの非WindowsのからPLCへアクセスしたいと考えています。

まずは通信の確認。Perlで通信確認をしているブログがあるのでこれを参考にしました。Raspberry Piで動作させるならPerlでも十分なのですが、マイコンでも動作させることを考えて、C++で作成しました。

このサイトを参考ししています。

Perlで三菱シーケンサーとSocket通信

出来上がったプログラムはGistに載せました。

melsec_test.cpp

やっていることは、UDP接続でD300~D302のデータメモリを受信をリクエストして、返答を受信するという内容です。基本的に参考にしたサイトの内容をC++に置き換えているだけですが、通信方式はUDPにしたところが違います。

環境ですが、MacbookにUSB接続優先LANでPLCと接続しています。UDP通信にはAsio C++ Libraryを使っています。Asioはhppファイルのみで構成されているので、ファイルをダウンロードして、適当なフォルダに移して、コンパイル時にインクルードするだけで使用できます。コンパイルはこのような感じに。

clang++ -std=c++11 -stdlib=libc++ -g -O0 -W -I../../source/asio-1.12.2/include melsec_test.cpp -o melsec_test

プログラムの内容について説明していくと

Boostに依存したくない場合はインクルードする前に、ASIO_STANDALONEマクロを実行する必要があるとのこと。

#define ASIO_STANDALONE
#include <iostream>
#include <asio.hpp>

テストなのでMCプロトコルの受信コマンドを文字列で定義します。コマンドの詳細については「Perlで三菱シーケンサーとSocket通信」を確認してください。

// 送信コマンド
std::string cmd = "500000FFFF03000C001000010400002C0100A80300";
std::cout << "send: " << cmd << std::endl

array配列を作って、文字列をバイナリデータに変換しています。perlだとpack関数があるようですが、ここでは自分で作りました。(もしC++に便利なライブラリがあるのなら教えていただきたいです。)

//16進数アスキーを数値に変換
char hex2char(char h)
{
    if (h >= '0' && h <= '9')
        return h - '0';
    else if (h >= 'A' && h <= 'F')
        return h - 'A' + 10;
    else if (h >= 'a' && h <= 'f')
        return h - 'a' + 10;
    return 0;
}

文字を一文字ずつ数値に変換して、char一つについて2つの数値が入るように詰めて配列に入れています。16進数の文字列を0〜15に変換する関数も作りました。

// 送信コマンドをバイナリデータへ変換
std::array<unsigned char, 128> send_buf;

int cnt;
for (size_t i = 0; i < cmd.size(); i += 2)
{
    cnt = i / 2;
    send_buf[cnt] = (hex2char(cmd[i]) << 4) + hex2char(cmd[i + 1]);
    ;
}
int send_buf_len = cnt + 1;

ASIOの送信部分です。非同期I/Oオブジェクトを作って、送信先のアドレスとポートをエンドポイントに設定し、ソケットを開き、データをバッファに入れて送信。という流れです。ASIOについてはあまり理解していないので引き続き勉強が必要です。

// UDP送信
std::string const host = "192.168.0.10";
short const port = 1025;
asio::io_context context;
asio::ip::udp::endpoint endpoint(
    asio::ip::address::from_string(host), port);
asio::ip::udp::socket socket(context);
socket.open(asio::ip::udp::v4());
s

ちなみにTCP送信のときは次のようにすればできます。(PLC側にTCPポートを1026に登録)

// TCP送信
std::string const host = "192.168.0.10";
short const port = 1026;
asio::io_context context;
asio::ip::tcp::endpoint endpoint(
    asio::ip::address::from_string(host), port);
asio::ip::tcp::socket socket(context);
asio::connect(socket,&endpoint);
socket.write_some(asio::buffer(send_buf, send_buf_len));

次にUDPからの受信。送信とは逆に配列を作って、受信バッファから配列に入れます。

// UDP受信
std::array<unsigned char, 128> recv_buf;
asio::ip::udp::endpoint sender_endpoint;

size_t recv_buf_len = socket.receive_from(
    asio::buffer(recv_buf), sender_endpoint);

最後に受信したバイナリデータを文字列に変えていきます。Perlではunpack関数があるのですが、ここでも自作しました。配列を一つずつ見ていきcharの上位と下位をそれぞれ文字列に変換します。

// 受信バイナリデータを文字列に変換
std::string result = "";
for (size_t i = 0; i < recv_buf_len; i++)
{
    result += char2hex((recv_buf[i] >> 4) & 0xf);
    result += char2hex(recv_buf[i] & 0xf);
}

数値を文字にする関数は下記のとおりです。

//0から15までの数値をアスキーに変換
char char2hex(char h)
{
    if (h >= 0 && h <= 9)
        return h + '0';
    else if (h >= 10 && h <= 15)
        return h - 10 + 'A';

    return 0;
}

実行結果は下記の通り。Perlと同じ結果になりました。

send: 500000FFFF03000C001000010400002C0100A80300
rev : D00000FFFF03000800000001000A006400

次回以降、コマンドの構造体を作り、ライブラリで使えるようにしていきたいです。