三菱PLC通信 構造体でトライ -コマンド受信

前回に続き、今回は構造体でコマンドを設定しての受信について説明します。
まずは受信の構造体からです。

//受信コマンドヘッダ
struct QNa3E_Frame_Receive{
    std::uint16_t subheader;
    std::uint8_t network_number;
    std::uint8_t pc_number;
    std::uint16_t request_IO_number;
    std::uint8_t request_station_number;
    std::uint16_t response_data_length;
    std::uint16_t terminal_code;
}rsv;

これも、リファレンスマニュアルのQnA互換3Eフレームのバイナリコードで更新する場合を見ながら作成しました。UDP受信については前々回の三菱PLCとイーサネット通信をご参照ください。

// 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);

今回はこの受信バッファを構造体に入れます。

// 受信データを受信ヘッダへコピー
int rsv_size = sizeof(QNa3E_Frame_Receive);
memcpy(&rsv, &recv_buf,rsv_size);

構造体のサイズ分memcpyでコピーしています。

// 受信したデータを書き出し
for(size_t i=rsv_size; i<recv_buf_len; i+=2)
    std::cout  << (recv_buf[i + 1] << 8) + recv_buf[i] << std::endl;

受信したデータを標準出力で出力します。受信バッファrecv_bufには1バイトずつデータが入っているので、1ワードずつ出力するには、forループで2つずつ進めて上位バイトを8bitシフトさせて下位バイトを足して、ワードデータにして表示しました。出力は下記のようになります。

1
10
100

構造体で作成することができました。次回からはMX Componentの関数にあるような感じでクラスを作っていきます。

三菱PLC通信 構造体でトライ -コマンド送信

前回の記事三菱PLCとイーサネット通信では、文字列に書いたコマンドを送信して受信データを文字列で表示しましたが、今回はそれを構造体にして書いてみます。

コードはまたgistsにアップします。
melsec_struct_test.cpp

それではコードを説明します。まずは送信コマンドのヘッダの構造体です。

#pragma pack(push,1)

//送信コマンドヘッダ
struct QNa3E_Frame_Send{
    std::uint16_t subheader=0x0050;
    std::uint8_t network_number=0x00;
    std::uint8_t pc_number=0xFF;
    std::uint16_t request_IO_number=0x03FF;
    std::uint8_t request_station_number=0x00;
    std::uint16_t request_data_length;
    std::uint16_t cpu_monitoring_timer=0x10;
} qna;

pragma pack(push,1)を入れることにより、バイナリで転送したときの間を開けずに詰める事ができます。もし入れない場合は「std::uint8_t request_station_number=0x00; //要求先ユニット局番号」の後に00が入ってしまいます。構造体宣言の最後にはpragma pack(pop)と書きます。

構造体はリファレンスマニュアルのQnA互換3Eフレームのバイナリコードで更新する場合を見ながら作成しました。

次に一括読み出しの構造体です。

//一括読み出しコマンド
struct ReadAllCommand{
    std::uint16_t command=0x0401;
    std::uint16_t sub_command=0x0000;
    std::uint32_t top_device;
    std::uint16_t number_of_device;
} read_all;

デバイスはDが0xA8、Mが0x90と値が決められているので列挙体で作成しました。std c++11からは列挙体の基礎となる型を指定できます。

//デバイス
enum struct Device_type : std::uint32_t{
    D = 0xA8,
    X = 0x9C,
    Y = 0x9D,
    M = 0x90,
};

次にmain関数に移ります。まずはデバイス番号を設定します。前回の例と一緒でD300から3ワードという設定です。

/デバイス番号を設定
std::uint32_t device = static_cast<std::uint32_t>(Device_type::D);

列挙体ではキャストして変数へ格納します。

read_all.top_device = ( device << 24) | 300;  // D300
read_all.number_of_device = 3; // 3ワード

デバイスは4バイトのうち先頭の1バイトがDとかMなどの種類で、残り3バイトがアドレスになります。列挙体で格納したデータを3バイト分(24bit)シフトしました。デバイス数はそのまま数値を入力します。

qna.request_data_length=sizeof(qna.cpu_monitoring_timer) + sizeof(read_all)

で、要求データ長を計算しています。
次にUDP送信です。

// 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());
socket.send_to(asio::buffer(&qna, sizeof(qna)), endpoint);
socket.send_to(asio::buffer(&read_all, sizeof(read_all)), endpoint);

ASIOについては前回の記事を参照してください。send_toで送信するところは、送信ヘッダと受信コマンドを構造体毎に送っています。

レスポンスの受信については次回説明します。

三菱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

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