#1.はじめに
横河電機の「リアルタイムOSコントローラ(e-RT3)」における入出力モジュールの制御を、並行処理や耐障害性、ホットデプロイなどの特徴を持つElixir言語から制御する方法をまとめてみました。
目標としては、前回記事のように、Elixirのシェルからコマンドで出力リレーの操作、入力の検知ができることとしています。
リアルタイムOSコントローラ e-RT3 関連記事
第1回 | セットアップ編 |
第2回 | 入出力ユニット編 (PythonとC言語から制御) |
第3回(今回) | Elixirから制御編 |
第4回 | ROS2から制御編 |
第5回 | Rustから制御編 |
第6回 | Goから制御編 |
#2.なぜElixir?
PLCの多くはラダーなどの言語で書かれています。
FA系では非常に馴染みのある言語ですが、C#やPythonなどの言語とは大分概念が違うので、IT系の方が触るとなると取っ掛かりに戸惑いがあります。
比較的新しいPLC(e-RT3など)では、LinuxやリアルタイムOS等が自由に動かせる環境が整ってきて、多くの高級言語も容易に扱う事ができるようになりました。
それらの言語の中で、長期に渡る安定動作や分散処理、RUN中書き込み1など、PLCならではの機能を要求する場合に対応出来そうなのが、Elixirかなと考えています。
- Elixirの特徴(分かり易い記事です)
- 組込み向けElixirの可能性について
まだまだ作例が少ないのですが、上手くいけばラダーに変わる高級言語になり得るかと期待しているので、個人的に色々模索しています。
現状、筆者自身の組込ElixirのターゲットはRaspberryPiですが、同じDebian系のLinuxが使えるe-RT3でも行けるんじゃないか?と思って、今回の記事にチャレンジしてみました。
##(参考)組込みElixirに関するコミュニティの紹介
-
NervesJP
- 関数型言語ElixirによるIoT開発フレームワークNervesのコミュニティです
-
kochi.ex -- Elixir community in Kochi、高知組み込み会
- 高知エリアで活発に活動されている組込みElixirコミュニティです
- Qiitaに投稿されている記事
#3.考え方
ここでは、NIFs2 という仕組みを使います。
大ざっぱな操作の流れのイメージは下記の通りです。
[Elixir]
↓
[nifm3.c] -> [relay.c] -> [libm3.so] -> e-RT3のIOユニット
名称 | 役割 |
---|---|
libm3.so | e-RT3のIOユニット制御のライブラリ(メーカから提供) |
relay.c |
libm3.so のAPIを操作するC言語のソース |
nifm3.c | Elixirからrelay.c の関数を呼ぶためのラッパー(NIFs) |
libm3.soの配置状況
$ ls -l /usr/local/lib
total 692
lrwxrwxrwx 1 root root 19 Mar 21 2020 libert3dgc.so -> libert3dgc.so.1.1.1
lrwxrwxrwx 1 root root 19 Mar 21 2020 libert3dgc.so.1 -> libert3dgc.so.1.1.1
-rw-r--r-- 1 root root 662644 Mar 17 2020 libert3dgc.so.1.1.1
lrwxrwxrwx 1 root root 14 Mar 5 2019 libm3.so -> libm3.so.1.0.1
lrwxrwxrwx 1 root root 14 Mar 5 2019 libm3.so.1 -> libm3.so.1.0.1
-rw-r--r-- 1 root root 36348 Mar 5 2019 libm3.so.1.0.1
#4.Elixirのインストール
別記事を参照してください。
#5.ソースコード
長くなるので、主要な部分だけ抜粋してます。
全てのソースファイルはGithubに置いてありますので、こちらを参照してください。(ファイル置いただけですので、読みにくくてすみません・・・)
##(1)NIFs
static ErlNifFunc nif_funcs
のところで、Elixir側から呼び出す関数名と、C言語側(relayio.c
)との対応付けをしています。
#include <stdio.h>
#include <erl_nif.h>
#include "relayio.h"
(・・・省略・・・)
/**
* @brief 出力リレー書き込み
* @param env nif指定の引数
* @param argc nif指定の引数
* @param argv nif指定の引数
* @return ERL_NIF_TERM
*/
static ERL_NIF_TERM _writeM3OutRelayP(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[])
{
int unit, slot, pos, data;
int ret = -1;
//引数を取り出し
enif_get_int(env, argv[0], &unit);
enif_get_int(env, argv[1], &slot);
enif_get_int(env, argv[2], &pos);
enif_get_int(env, argv[3], &data);
//処理
ret = WriteOutRelay1(unit, slot, pos, data);
if (ret == 0)
//正常なら、得た値を代入
ret = data;
//読み込み結果を返す
//エラー時は負の値
return enif_make_uint(env, ret);
}
/**
* @brief define the array of ErlNifFunc beforehand
*
*/
static ErlNifFunc nif_funcs[] =
{
// {erl_function_name, erl_function_arity, c_function}
{"readinrelay", 4, _readM3InRelay},
{"readinrelay", 3, _readM3InRelayP},
{"readoutrelay", 4, _readM3OutRelay},
{"writeoutrelay", 5, _writeM3OutRelay},
{"writeoutrelay", 4, _writeM3OutRelayP},
};
ERL_NIF_INIT(Elixir.NifTest, nif_funcs, NULL, NULL, NULL, NULL)
##(2)API呼び出しのためのC言語ソース
ここは、前記事の「リアルタイムOSコントローラ e-RT3 (入出力ユニット編)」を参考に書いています。
#include <stdio.h>
#include <ert3/m3lib.h>
#include <errno.h>
#include "relayio.h"
(・・・省略・・・)
/**
* @brief 出力リレー書き込み
*
* @param unit ユニット番号を指定(0~7)
* @param slot スロット番号を指定(1~16)
* @param pos 出力リレー番号を指定(1~64)
* @param data 書き込みデータ
* @return int
*/
unsigned int WriteOutRelay1(int unit, int slot, int pos, unsigned short data)
{
unsigned int result = -1;
//API呼び出し
result = writeM3OutRelayP(unit, slot, pos, data);
if (result != 0)
//エラーメッセージ
result = errcommon((char *)__func__, __LINE__, result);
else
//返り値にスロット番号・ポジション・データを付加
result = (slot * 1000 + pos * 10 + data);
//書き込み結果を返す
return result;
}
(・・・省略・・・)
#ifndef RELAYIO_H
#define RELAYIO_H
int ReadInRelayCommon(int , int , int , int , int*);
unsigned int ReadOutRelayCommon(int , int , int , int , unsigned int*);
unsigned int WriteOutRelay1(int unit, int slot, int pos, unsigned short data);
unsigned int WriteOutRelay(int unit, int slot, int pos, unsigned int data, unsigned int mask);
int errcommon(char *func, int line, int result);
#endif
##(3)Makefile
関連するソースファイルをまとめてビルドします。
#----------------------------------------------------------
# @file Makefile
# @author myasu
# @brief nif for e-RT3 API Makefile
# @version 0.1
# @date 2020-08-10
#
# @copyright Copyright (c) 2020
#----------------------------------------------------------
CC = gcc
CFLAGS = -O4 -Wall -I/usr/local/include -shared -fPIC -I$(ERL_INCLUDE_PATH)
ERL_INCLUDE_PATH=$(shell elixir -e 'IO.puts [:code.root_dir, "/erts-", :erlang.system_info(:version), "/include"]')
DEST = /usr/local/bin
LDFLAGS = -L/usr/local/lib
LIBS = -lm -lm3
OBJS = nifm3.o relayio.o relayio.h
PROGRAM = nifm3.so
all: $(PROGRAM)
$(PROGRAM): $(OBJS)
$(CC) $(OBJS) $(CFLAGS) $(LDFLAGS) $(LIBS) -o $(PROGRAM)
clean:; rm -f *.o *.c.* *~ $(PROGRAM)
install: $(PROGRAM)
install -s $(PROGRAM) $(DEST)
##(4)Elixir側の実装
defmodule NifTest do
@moduledoc """
Documentation for `NifTest`.
Elixirからのm3ライブラリへのアクセスサンプル
"""
@on_load :init
@doc """
init
初期化
"""
def init do
#nifのロード
:erlang.load_nif("./nifm3", 0)
end
(・・・省略・・・)
@doc """
writeoutrelay
出力リレー書き込み(1ビット単位)
## Parameter
- 1:ユニット番号を指定(0~7)
- 2:出力リレーを設置のスロット番号を指定(1~16)
- 3:出力リレー番号を指定(1~32)
- 4:書き込みデータ(0 / 1)
## Examples
iex> NifTest.writeoutrelay(0,2,1,1) #出力リレー1だけON
iex> NifTest.writeoutrelay(0,2,10,0) #出力リレー10だけOFF
"""
def writeoutrelay(_, _, _, _) do
"writeoutrelay/4 : NIF library not loaded"
end
end
####補足
@on_load :init
は、Elixirのビルドの段階3 で展開されます。(実行時ではない)
##(5)ビルド
$ make
gcc -O4 -Wall -I/usr/local/include -shared -fPIC -I/home/ert3/.asdf/installs/erlang/22.0.7/erts-10.4.4/include -c -o relayio.o relayio.c
gcc nifm3.o relayio.o relayio.h -O4 -Wall -I/usr/local/include -shared -fPIC -I/home/ert3/.asdf/installs/erlang/22.0.7/erts-10.4.4/include -L/usr/local/lib -lm -lm3 -o nifm3.so
##(6)実行例
モジュールの配置状態は、第1回の記事と同じです。
スロット番号 | モジュール |
---|---|
1 | (CPU) |
2 | 出力リレー32ch |
3 | 入力リレー32ch |
4 | (ADコンバータ) |
5 | (DAコンバータ) |
ert3@ert3u:~/gitwork/elixir/nifm3$ iex nif_test.ex
Erlang/OTP 22 [erts-10.4.4] [source] [smp:2:2] [ds:2:2:10] [async-threads:1] [hipe]
Interactive Elixir (1.9.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>
###リレー出力
####①出力リレーをまとめてON/OFF
#出力リレーをまとめてON/OFF
iex(1)> NifTest.writeoutrelay(0,2,1,0xffffffff,0xffffffff)
4294967295
iex(2)> NifTest.writeoutrelay(0,2,1,0x00000000,0xffffffff)
0
上記を実行したときの、出力の状態。
全てOFF状態→ON状態です。
####②出力リレーの一部だけONにして、その状態を読み取り
#特定の出力リレー1つだけ(ch1)をON/OFF
iex(3)> NifTest.writeoutrelay(0,2,1,1)
iex(4)> NifTest.writeoutrelay(0,2,1,0)
#特定の出力リレー1つだけ(ch32)をON/OFF
iex(5)> NifTest.writeoutrelay(0,2,32,1)
iex(6)> NifTest.writeoutrelay(0,2,32,0)
#特定の出力リレー2つ(ch1, ch2)をONにして、そのリレーの状態を読み取り
iex(15)> NifTest.writeoutrelay(0,2,1,0x00000011,0xffffffff)
17
iex(16)> NifTest.readoutrelay(0,2,1,2)
17
#特定の出力リレー2つ(ch1, ch32)をONにして、そのリレーの状態を読み取り
iex(7)> NifTest.writeoutrelay(0,2,1,1)
iex(8)> NifTest.writeoutrelay(0,2,32,1)
iex(9)> NifTest.readoutrelay(0,2,1,2)
2147483649
上記を実行したときの、出力の状態。
ch1と32がON、残りは全てOFF状態です。
###リレー入力
####③入力リレーをまとめて読み取り
#入力リレーをまとめて読み取り(1~32)
iex(2)> NifTest.readinrelay(0,3,1,2)
4294967295
iex(3)> NifTest.readinrelay(0,3,1,2)
0
上記を実行したときの、入力(スイッチ)の状態。
全てON状態です。
####④特定の入力リレー1つだけを読み取り
#特定の入力リレー1つだけ(ch1→ON)を読み取り
iex(4)> NifTest.readinrelay(0,3,1)
1
#特定の入力リレー1つだけ(ch32→ON)を読み取り
iex(5)> NifTest.readinrelay(0,3,32)
1
#特定の入力リレー1つだけ(ch2→OFF)を読み取り
iex(6)> NifTest.readinrelay(0,3,2)
0
上記を実行したときの、入力(スイッチ)の状態。
ch1と32がON、残りは全てOFF状態です。
#6.おわりに
ElixirのNIFsを扱う勉強を兼ねて、試してみました。
今回は、とりあえず”Lチカ”する程度の内容で留めていますが、FA機器の制御をするまで発展させたいと思っています。(実験が纏まるまで、少々お待ちください・・・)
#参考資料
- 本記事のソースコード
- NIF入門
- e-RT3 m3ライブラリの使い方
-
A NIF is a function that is implemented in C instead of Erlang. NIFs - Erlang ↩