LoginSignup
15
5

More than 1 year has passed since last update.

ElixirでMODBUS(MELSEC-FX5U/Q ~PC間通信)

Last updated at Posted at 2021-12-09

1.はじめに

この記事はNervesJP Advent Calendar 2021の6日目の記事です。
5日目は @torifukukaiou さんのrclexから始めるROS 2でした。

FA機器を遠隔制御する通信規格「MODBUS」でイーサネット通信に対応した「MODBUS/TCP」を使って、ElixirからPLCを遠隔制御するテストをしてみました。

(2022年2月:MELSEQ-Qで扱う例を追記しました)

関連記事

使用言語
Elixir こちらの記事
Rust https://qiita.com/myasu/items/935d46644a37420bd75c

2.構成

PC側のOSはWindows10で、UbuntuLinuxはVirtualBoxの上で動かしてます。
GXWorksからPLCへの書き込みは、USBを使わずLAN経由でもOKです。

PLC側をMODBUSスレーブ、PC側をMODBUSマスターとします。
image.png

FX5Uシーケンサです。(何故か手元に・・・)

image.png

なんかエラー出てるけど・・・

3.MELSEC FX5U PLC側の準備

GXWork3を起動して、PLCと通信できる状態にします。

(1)ユニットパラメータの設定

  • ①「ナビゲーション」→「ユニットパラメータ」→「Ethernetポート」を開く
    image.png

  • ②「IPアドレス設定」に、PLCのIPアドレスを指定
    image.png

  • ③「相手機器接続構成設定」をダブルクリックで開く → 「ユニット一覧」の「MODBUS/TCP接続機器」をウィンドウ下部のイラストにドラッグ&ドロップ → 「設定を反映して保存」
    image.png
    ※この図の「コネクションNo.1」が、後述のUNITID=0x00 に対応します。

  • ④「ユニットパラメータ」の「デバイス割り付け」→「詳細設定」をクリック
    image.png

  • ⑤「MODBUSデバイス割り付けパラメータ」はデフォルトのままでOKです。(そのままOKボタンを押す)
    image.png

以上で最小限の設定は完了です。

PLCへの書き込みで「PCパラメータ」をPLCに転送します。電源を入れ直すと、設定が反映されます。

(2)動作確認用のラダー

PLC側の状態を取得するときの確認のため、簡単なラダーを準備しておきます。

  • Y0を2秒おきにON/OFF
  • D0を1秒ごとにインクリメント image.png

PLCへの書き込みで「PCパラメータ」をPLCに転送します。

最後に、PCとPLCが通信できていることを確認するため、パソコン側からPLCにPINGを送ってみます。

コマンドライン
$ ping 192.168.5.41
PING 192.168.5.41 (192.168.5.41) 56(84) バイトのデータ
64 バイト応答 送信元 192.168.5.41: icmp_seq=1 ttl=63 時間=25.3ミリ秒
64 バイト応答 送信元 192.168.5.41: icmp_seq=2 ttl=63 時間=0.873ミリ秒
64 バイト応答 送信元 192.168.5.41: icmp_seq=3 ttl=63 時間=0.970ミリ秒
^C
--- 192.168.5.41 ping 統計 ---
送信パケット数 3, 受信パケット数 3, パケット損失 0%, 時間 2043ミリ秒
rtt 最小/平均/最大/mdev = 0.873/9.051/25.311/11.497ミリ秒

4.PC側の準備

Elixirのインストール参考

コマンドライン
$ elixir --version
Erlang/OTP 23 [erts-11.2.2.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe]
Elixir 1.11.4 (compiled with Erlang/OTP 23)

(1)modbusライブラリ

今回使用するライブラリはこちらです。1
https://hex.pm/packages/modbus

通信用の関数
{:ok, pid} = Master.start_link([ip: {192, 168, 5, 41}, port: 502])

Master.exec(pid, {functionCode, unitId, startRegister, Value})
引数 意味 今回のFX5Uでの例
ip 対象となるPLCのIPアドレス 192.168.5.41
port 接続先ポート 502
functionCode 操作コマンド (補足は後述のコードで)
unitId ユニットID(コネクションNo) 0x00
startRegister 対象のレジスタの開始位置 0x00(補足は後述のコードで)
Value (read系) 読み込みビット数 (補足は後述のコードで)
Value (write系) コイル:1→ON, 0→OFF、レジスタ→値 -
Value (write系複数ビット) 出力のON/OFF(リスト型で渡せます) 例→[0, 1, 0, 1]

functionCodeに記述する内容は以下の通りです。(FX5U PLCで使えるものだけ抜粋)

機能 atom (略)
コイルYの読み込み :rc read coils
コイルYへ書き込み :fc force coils
コイルXの読み込み :ri read inputs
データレジスタDの読み込み :rhr read holding registers
データレジスタDへ書き込み :phr preset holding registers

startRegisterに記述する内容は以下の通りです。
(前述のGXWorks2/3「MODBUSデバイス割り付けパラメータ」にデフォルトで指定されるアドレスです。よく使いそうなモノだけ抜粋)

項目 functionCodeのatom デバイス MODBUS先頭デバイス番号
コイル :rc / :fc Y0 0
M0 8192
SM0 20480
B0 30720
入力 :ri X0 0
保持レジスタ :rhr / :phr D0 0
SD0 20480
W0 30720
基本のコード
#start link
{:ok, pid} = Master.start_link([ip: {192, 168, 5, 41}, port: 502])

#Y1コイル書き込み
#               ユニットID↓    ↓MODBUS先頭デバイス番号
Master.exec(pid, {:fc, 0x00, 0x01, 1})

#Y1~Y4まで複数同時にコイル書き込み
#                                  ↓配列でまとめて指定も可能
Master.exec(pid, {:fc, 0x00, 0x01, [1, 0, 1, 0]})

#D0レジスタから8ワード読み込み
Master.exec(pid, {:rhr, 0x00, 0x00, 8})

#D1レジスタに1ワード書き込み
Master.exec(pid, {:phr, 0x00, 0x01, 0x12})

(2)コードの作成

MODBUSでお話しするだけの、簡単なコードを作ってみます。

コマンドライン
#作業ディレクトリを作成
$ mkdir work
$ cd work

#プロジェクト作成
$ mix new modbus_prac
$ cd modbus_prac

必要なライブラリを追加します。

mix.exs
defmodule ModbusPrac.MixProject do
  use Mix.Project

・・・(省略)・・・

  # Run "mix help deps" to learn about dependencies.
  defp deps do
    [
      # {:dep_from_hexpm, "~> 0.3.0"},
      # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}

      # ↓↓追加
      {:modbus, "~> 0.3.7"}
    ]
  end
end

ライブラリを取得、ビルド。

コマンドライン
$ mix hex.local
$ mix deps.get
$ mix deps.compile

5.実験

iexを立ち上げて下準備

コマンドライン
$ iex -S mix
Erlang/OTP 23 [erts-11.2.2.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe]
Compiling 1 file (.ex)
Interactive Elixir (1.11.4) - press Ctrl+C to exit (type h() ENTER for help)

#予め以下の操作をしておいてpidを準備
iex(1)> alias Modbus.Tcp.Master
Modbus.Tcp.Master
iex(2)> {:ok, pid} = Master.start_link(ip: {192, 168, 5, 41}, port: 502)
{:ok, #PID<0.209.0>}

(1)出力YリレーをON・OFF

GXWorksでのリレーYの割当は以下のようになってます。

出力Y startRegister
Y0 0x00
Y1 0x01
Y2 0x02
Y3 0x03
・・・

Y0はラダーの方で2secのフリック制御をしているので、ここではY1~Y4までを同時にON・OFFしてみます。

iex
# Y1~4 ON
iex(3)> Master.exec(pid, {:fc, 0x00, 0x01, [1, 1, 1, 1]})
:ok

# Y1~4 OFF
iex(4)> Master.exec(pid, {:fc, 0x00, 0x01, [0, 0, 0, 0]})
:ok

image.png

(2)出力Yリレーの状態を取得

startRegisterが0x00(=Y0)から8ビット分読み込んでみます。

iex
# Y0リレーが点滅している
iex(5)> Master.exec(pid, {:rc, 0x00, 0x00, 8})
{:ok, [0, 0, 0, 0, 0, 0, 0, 0]}
iex(6)> Master.exec(pid, {:rc, 0x00, 0x00, 8})
{:ok, [1, 0, 0, 0, 0, 0, 0, 0]}

# Y1~4 ON
iex(7)> Master.exec(pid, {:fc, 0x00, 0x01, [1, 1, 1, 1]})
:ok
# 読み込むと、Y1~4がONになっている(フリックしているY0もたまたまONになっている)
iex(8)> Master.exec(pid, {:rc, 0x00, 0x00, 8})           
{:ok, [1, 1, 1, 1, 1, 0, 0, 0]}

# Y1~4 OFF
iex(9)> Master.exec(pid, {:fc,0x00, 0x01, [0, 0, 0, 0]})
:ok
# 読み込むと、Y1~4がONになっている(フリックしているY0もたまたまONになっている)
iex(10)> Master.exec(pid, {:rc, 0x00, 0x00, 8})           
{:ok, [1, 0, 0, 0, 0, 0, 0, 0]}

(3)データレジスタDの読み書き

GXWorksでのデータレジスタDの割当は以下のようになってます。

データレジスタ startRegister
D0 0x00
D1 0x01
D2 0x02
D3 0x03
・・・

D0はラダーの方で1秒ごとにインクリメントしているので、ここではD0~D7までをまとめて読み込んでみます。D0に相当する0ビット目の値が変化しています。

iex
iex(11)> Master.exec(pid, {:rhr, 0x00, 0x00, 8})
{:ok, [14874, 0, 0, 0, 0, 0, 0, 0]}
iex(12)> Master.exec(pid, {:rhr, 0x00, 0x00, 8})
{:ok, [14878, 0, 0, 0, 0, 0, 0, 0]}
iex(13)> Master.exec(pid, {:rhr, 0x00, 0x00, 8})
{:ok, [14880, 0, 0, 0, 0, 0, 0, 0]}

D1=10, D2=16に書き換えます。

iex
iex(14)> Master.exec(pid, {:phr, 0x00, 0x01, [10, 16]})  
:ok
iex(15)> Master.exec(pid, {:rhr, 0x00, 0x00, 8})             
{:ok, [15171, 10, 16, 0, 0, 0, 0, 0]}

D0~D2を0に書き換えます。

iex
iex(16)> Master.exec(pid, {:phr, 0x00, 0x00, [0, 0, 0]})
:ok
iex(17)> Master.exec(pid, {:rhr, 0x00, 0x00, 8})              
{:ok, [1, 0, 0, 0, 0, 0, 0, 0]}
iex(18)> 

D0も0に戻って、そこからラダーのフリック動作でインクリメントしていることが分かります。

6.MELSEC-Q PLCを使う場合

(2022/2追記)

<今回トライした環境>

スロット#1の「QJ71MT91」(MODBUS/TCP)で試してみます。

  • IOの割り付け例 image.png image.png

(1)インテリジェント機能ユニットの設定

  • ①「ナビゲーション」→「インテリジェント機能ユニット」→「QJ71MT91」→「スイッチ設定」を開く
    image.png

  • ②「スイッチ設定」を画像に倣って設定

    • IPアドレスは各自の環境に合わせてください
    • 「○○パラメータ起動方法」
      • 「ユーザ設定パラメータで起動」に指定
    • 「RUN中書き込み許可・禁止設定」
      • 禁止:MODBUS経由でのレジスタ・コイルは読み出しだけ可能です。(書き込み命令は無視されてタイムアウトします:詳細は後述します)
      • 許可:(こっちに設定)MODBUS経由でのレジスタ・コイルの読み書きが可能です。

image.png

  • ③保存して、PLCにパラメータを転送する
    image.png

  • ④全ての電源をOFFして、再度ONすることで、変更したパラメータを反映できます。(保存、転送だけでは、インテリジェント機能ユニットのパラメータ変更は反映されないので注意)

(2)Wレジスタ、Bレジスタの読み書き例

コード

lib/mb.ex
defmodule Mb do
  @moduledoc """
    MODBUS / TCP Master test
    for MELSEC Q
    2022/2/4-5
  """
  alias Modbus.Tcp.Master
  # 通信相手のIPアドレスとポート
  @target_address {192, 168, 5, 51}
  @target_port 502
  # ユニット番号(ベースボード1枚なら0x00で固定でOK)
  @unit_no 0x00

  # 先頭MODBUSデバイス番号
  # コイル系
  @c_Y 0
  @c_M 8192
  @c_SM 20480
  @c_B 30720
  # 入力系
  @i_X 0
  # 入力レジスタ系(MELSEC系は無し)
  # 保持レジスタ系
  @r_D 0
  @r_SD 20480
  @r_W 30720
  @r_SW 40960

  @doc """
  Initialize
  """
  def init do
    Master.start_link(ip: @target_address, port: @target_port, timeout: 2000)
  end

  @doc """
  コイル読み込み
  """
  def coilread(addr \\ 0x0) do
    # 初期化
    {:ok, pid} = init()
    # 開始のメッセージ
    IO.puts("* coil read [start addr: #{addr}]")
    # ループ開始
    coilread_loop(pid, addr)
  end

  @doc """
  コイル読み込み・ループ
  """
  def coilread_loop(pid, addr, count \\ 0) do
    # 先頭アドレスaddrを起点に8ワード読み込み
    Master.exec(pid, {:rc, @unit_no, @c_B + addr, 8})
    |> IO.inspect(label: " [#{count}] > ")

    Process.sleep(500)
    # 決まった回数だけ繰り返し
    if count < 4 do
      coilread_loop(pid, addr, count + 1)
    end
  end

  @doc """
  コイル書き込み
  """
  def coilwrite(data \\ [1, 1, 1, 1], addr \\ 0x0) do
    # 初期化
    {:ok, pid} = init()
    # 開始のメッセージ
    IO.puts("* coil write [start addr: #{addr}] -> data: #{inspect(data)}")
    # 書き込み
    Master.exec(pid, {:fc, @unit_no, @c_B + addr, data})
    |> IO.inspect(label: " > ")
  end

  @doc """
  レジスタ読み込み
  """
  def regread(addr \\ 0x0) do
    # 初期化
    {:ok, pid} = init()
    # 開始のメッセージ
    IO.puts("* register read [start addr: #{addr}]")
    # ループ開始
    regread_loop(pid, addr)
  end

  @doc """
  レジスタ読み込み・ループ
  """
  def regread_loop(pid, addr, count \\ 0) do
    # 先頭アドレスaddrを起点に8ワード読み込み
    Master.exec(pid, {:rhr, @unit_no, @r_W + addr, 8})
    |> IO.inspect(label: " [#{count}] > ")

    Process.sleep(500)
    # 決まった回数だけ繰り返し
    if count < 4 do
      regread_loop(pid, addr, count + 1)
    end
  end

  @doc """
  レジスタ書き込み
  """
  def regwrite(data \\ [0x6800, 0x4A08, 0x68A3, 0x4A4A, 0x6AAA], addr \\ 0x0) do
    # 初期化
    {:ok, pid} = init()
    # 開始のメッセージ
    IO.puts("* register write [start addr: #{addr}] -> data: #{inspect(data)}")
    # 書き込み
    Master.exec(pid, {:phr, @unit_no, @r_W + addr, data})
    |> IO.inspect(label: " > ")
  end
end

実行例

  • コイルBの読み書き
コマンドライン
$ iex -S mix
Erlang/OTP 23 [erts-11.2.2.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe]

Interactive Elixir (1.11.4) - press Ctrl+C to exit (type h() ENTER for help)

#コイルB0~B3まで1を書き込み
iex(1)> Mb.coilwrite            
* coil write [start addr: 0] -> data: [1, 1, 1, 1]
 > : :ok
:ok

#コイルB0~B8まで読み込み
iex(2)> Mb.coilread 
* coil read [start addr: 0]
 [0] > : {:ok, [1, 1, 1, 1, 0, 0, 0, 0]}
 [1] > : {:ok, [1, 1, 1, 1, 0, 0, 0, 0]}
 [2] > : {:ok, [1, 1, 1, 1, 0, 0, 0, 0]}
 [3] > : {:ok, [1, 1, 1, 1, 0, 0, 0, 0]}
 [4] > : {:ok, [1, 1, 1, 1, 0, 0, 0, 0]}
nil
iex(3)> 

image.png

  • レジスタWの読み書き
コマンドライン
iex(3)> Mbm2.regwrite
* register write [start addr: 0] -> data: [26624, 18952, 26787, 19018, 27306]
 > : :ok
:ok
iex(4)> Mbm2.regread 
* register read [start addr: 0]
 [0] > : {:ok, [26624, 18952, 26787, 19018, 27306, 0, 0, 0]}
 [1] > : {:ok, [26624, 18952, 26787, 19018, 27306, 0, 0, 0]}
 [2] > : {:ok, [26624, 18952, 26787, 19018, 27306, 0, 0, 0]}
 [3] > : {:ok, [26624, 18952, 26787, 19018, 27306, 0, 0, 0]}
 [4] > : {:ok, [26624, 18952, 26787, 19018, 27306, 0, 0, 0]}
nil

image.png

(3)ハマリどころのメモ

①レジスタの読み込みはできるけど、書き込みができない

MELSEQ-QのMODBUS/TCPユニット(QJ71MT91)へ書き込みしたところ、タイムアウトしてしまう。(PINGは通るし、ソースコード中のinit関数も通っている)

コマンドライン
#読み込みはOK
iex(1)>  Master.exec(pid, {:rhr, @unit_no, @r_W, 8})    
1 > : {:ok, [0, 0, 0, 0, 0, 0, 0, 0]}
2 > : {:ok, [0, 0, 0, 0, 0, 0, 0, 0]}
・・・

#書き込みがタイムアウト?
iex(2)> Master.exec(pid, {:phr, @unit_no, @r_W, [24576, 18952, 26787, 19018, 27306]})

12:27:19.491 [error] GenServer #PID<0.207.0> terminating
** (MatchError) no match of right hand side value: {:error, :timeout}
    (modbus 0.3.7) lib/master.ex:115: anonymous fn/3 in Modbus.Tcp.Master.exec/3
    (elixir 1.11.4) lib/agent/server.ex:16: Agent.Server.handle_call/3
    (stdlib 3.14.2.1) gen_server.erl:715: :gen_server.try_handle_call/4
    (stdlib 3.14.2.1) gen_server.erl:744: :gen_server.handle_msg/6
    (stdlib 3.14.2.1) proc_lib.erl:226: :proc_lib.init_p_do_apply/3
Last message (from #PID<0.191.0>): {:get_and_update, #Function<0.72871438/1 in Modbus.Tcp.Master.exec/3>}
State: {#Port<0.12>, 0}
Client #PID<0.191.0> is alive
・・・

ユニットのERRランプが点灯しているので、詳細情報を確認。
image.png

image.png

image.png

処置

先ほど書いたとおり、QJ71MT91のスイッチ設定のなかで「RUN中書き込み許可」に設定が必要です。

image.png

※変更したら、全てのパラメータ情報(PLC本体、インテリジェントバイスも)をPLCに転送して、PLCの電源を入れ直して新しいパラメータを反映させます。

7.まとめ

めっちゃ久しぶりにQiitaの記事を書きました・・・

今年もkochi-exさんの勉強会に参加させて頂いて、その中で取り組んだ課題になります。
相変わらず、PLC周りの設定はググっても情報が出てこないので変なところで苦労しました・・・
これから「PLCとElixirを繋いで、ナウでヤングなプロダクトを作るぜ!」とアップをはじめている同志諸君の参考になれば幸いです。

もう一つのライブラリmodbuxを使う方は、きっと @kikuyuta センセが記事を書いてくださると思うので!請うご期待!!!

(そして来年はもっとアウトプットしますので、今後ともごひいきに・・・

参考資料


  1. メンテが途絶えて久しいようなのでこちらでもOK。https://hex.pm/packages/modbux 

15
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
5